/* 00-reset.css */
/* Reset — normalise the handful of defaults that actually get in the way. */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  overflow-x: hidden;
}

body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
  margin: 0;
}

/*
 * Load-bearing, not housekeeping.
 *
 * The user-agent rule for [hidden] has the lowest possible specificity, so any
 * later rule that sets `display` on a matching element silently reveals it.
 * There is a deliberately hidden element on /ressurser whose whole point is to
 * be in the source but not on the page — see partials/link-card.html — and one
 * innocent `.link-card p { display: block }` would expose it. Keep this.
 */
[hidden] {
  display: none !important;
}

img,
picture,
svg,
video {
  display: block;
  max-width: 100%;
}

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: none;
}

ul[class],
ol[class] {
  list-style: none;
  margin: 0;
  padding: 0;
}

a {
  color: var(--g1);
  text-shadow: var(--glow);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

a:hover {
  color: var(--g-hi);
}

hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: var(--s6) 0;
}

:target {
  scroll-margin-top: calc(var(--nav-h) + var(--s5));
}

/* Terminals invert their selection; one line, instantly recognisable. */
::selection {
  background: var(--g1);
  color: var(--bg);
  text-shadow: none;
}

/* Focus: visible on every surface, including on top of a green fill. */
:focus-visible {
  outline: var(--focus);
  outline-offset: var(--focus-offset);
}

::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.12);
  border-radius: var(--r-pill);
  border: 3px solid var(--bg);
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(60, 179, 79, 0.45);
}

/* 01-tokens.css */
/*
 * Design tokens.
 *
 * The palette comes from the terminal theme; the status colours were retuned
 * for the darker, cooler background. The originals were chosen against the old
 * #353033 and fall below 4.5:1 on #181b23 — the values here clear it.
 */

:root {
  /* ── brand ── */
  --g1: #3cb34f;
  --g2: #2d8c40;
  --g3: #1a5c2a;
  --g-hi: #6ed47e;

  /*
   * Text on a green fill. White over --g1 is about 2.6:1 and fails AA at any
   * size; near-black clears 7:1 and reads sharper besides.
   */
  --on-green: #10160f;

  /* ── surfaces ── */
  --bg: #181b23;
  --surface: #1f2330;
  --card: #252a36;
  --card-hover: #2a3040;
  --border: rgba(255, 255, 255, 0.1);
  --border-strong: rgba(255, 255, 255, 0.16);
  --border-green: rgba(60, 179, 79, 0.35);
  --overlay: rgba(13, 15, 20, 0.6);

  /* ── text ── */
  --text: #f0f2f8;
  --dim: rgba(240, 242, 248, 0.75);
  --muted: rgba(240, 242, 248, 0.55); /* on --bg / --surface only */
  --muted-strong: rgba(240, 242, 248, 0.72); /* on --card, keeps 4.5:1 */

  /* ── status ── */
  --error: #f87171;
  --error-bg: rgba(239, 68, 68, 0.1);
  --error-bd: rgba(239, 68, 68, 0.3);
  --warning: #fbbf24;
  --warning-bg: rgba(202, 138, 4, 0.12);
  --warning-bd: rgba(202, 138, 4, 0.32);
  --info: #60a5fa;
  --info-bg: rgba(59, 130, 246, 0.1);
  --info-bd: rgba(59, 130, 246, 0.3);
  --success: #7ecc8d;
  --success-bg: rgba(60, 179, 79, 0.1);
  --success-bd: rgba(60, 179, 79, 0.3);

  /* ── type ── */
  --font-display: "VT323", "JetBrains Mono", ui-monospace, monospace;
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
    monospace;
  --font-body: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
    Roboto, "Noto Sans", sans-serif;

  --fs-xs: 0.75rem;
  --fs-sm: 0.875rem;
  --fs-base: 1rem;
  --fs-lg: 1.125rem;
  --fs-h3: 1.25rem;
  --fs-h2: 1.5rem;
  --fs-h1: clamp(1.75rem, 1.2rem + 2vw, 2.5rem);
  --fs-display: clamp(2.75rem, 1.5rem + 6vw, 6rem);

  --lh-tight: 1.2;
  --lh-body: 1.7;
  --track-cap: 0.1em;

  /* ── space (4px base) ── */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2rem;
  --s7: 3rem;
  --s8: 4.5rem;

  /* ── geometry ── */
  --r-sm: 4px;
  --r: 6px;
  --r-lg: 10px;
  --r-pill: 99px;
  /*
   * The window is the viewport now; this caps the content inside it so an
   * ultrawide monitor does not put the status line's two ends a metre apart.
   *
   * rem, not em. An em resolves against the element's own font size, so the
   * same token would cap the 12px status line at 1200px and the 14px console
   * at 1400px — the bar's contents would each centre to a different width and
   * visibly fail to line up with the pane above them.
   */
  --maxw-pane: 100rem;
  --measure: 68ch;
  --nav-h: 48px;
  /* Height of the pinned console + status bar, so the pane can clear it. */
  --bar-h: 66px;

  /* ── effect ── */
  --focus: 2px solid var(--g1);
  --focus-offset: 2px;
  --shadow-card: 0 4px 20px rgba(0, 0, 0, 0.25);
  --shadow-glow: 0 0 0 1px var(--border-green), 0 4px 20px rgba(60, 179, 79, 0.08);
  --dots: radial-gradient(circle, rgba(255, 255, 255, 0.045) 1px, transparent 1px);
  --dots-size: 28px 28px;

  /*
   * Phosphor glow — the one thing taken from the reference site, whose
   * effects.css layers three shadows of its accent colour on everything.
   *
   * Applied to green and chrome only. A halo over seven hundred words of
   * Norwegian is the difference between atmosphere and eye strain, and those
   * are the pages somebody deciding whether to join actually reads.
   */
  --glow: 0 0 6px rgba(60, 179, 79, 0.55), 0 0 18px rgba(60, 179, 79, 0.25);
  --glow-lg: 0 0 7px rgba(60, 179, 79, 0.6), 0 0 25px rgba(60, 179, 79, 0.3),
    0 0 40px rgba(60, 179, 79, 0.15);
  --glow-soft: 0 0 8px rgba(240, 242, 248, 0.18);
  --bullet: "» ";

  /* Chevron for <select>, inlined so it needs no extra request. */
  --svg-arrow: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23f0f2f8%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
}

/* 02-typography.css */
/*
 * Typography — three faces, three jobs.
 *
 * VT323 is the signature voice but it has one weight, no italic and a small
 * x-height, so it is confined to display sizes. JetBrains Mono carries the
 * interface chrome. Prose is set in a real text face, because /historie is
 * seven hundred words of Norwegian and legibility wins there.
 *
 * All three are self-hosted: routing visitors to a font CDN would leak their
 * IP to a third party, which is not something this particular organisation
 * should be doing. Run scripts/fetch-fonts.sh to populate assets/static/fonts;
 * until then every stack falls back cleanly to a system face.
 */

@font-face {
  font-family: "VT323";
  src: url("/assets/fonts/vt323-latin-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/assets/fonts/jetbrains-mono-latin-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/assets/fonts/jetbrains-mono-latin-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("/assets/fonts/ibm-plex-sans-latin-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Sans";
  src: url("/assets/fonts/ibm-plex-sans-latin-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

/*
 * Mono is the default voice now that the page is a terminal; the readable face
 * is reserved for the places where it earns its keep.
 *
 * /historie is around seven hundred words of Norwegian and /om-itemize is not
 * far behind. Monospace across that much text measurably slows reading and
 * reads as a wall, and those are exactly the pages a prospective member
 * actually reads — so paragraphs and list items inside .prose keep IBM Plex
 * Sans while everything else goes mono.
 */
body {
  font-family: var(--font-mono);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  color: var(--text);
  /* Mono ligatures fuse -> and != into glyphs that read oddly mid-sentence. */
  font-variant-ligatures: none;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-mono);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
  text-wrap: balance;
}

h1 {
  font-size: var(--fs-h1);
  margin-bottom: var(--s5);
}

h2 {
  font-size: var(--fs-h2);
  margin: var(--s7) 0 var(--s4);
}

h3 {
  font-size: var(--fs-h3);
  margin: var(--s5) 0 var(--s3);
}

/* ── Prose ─────────────────────────────────────────────────────────────── */

.prose {
  max-width: var(--measure);
}

.prose p,
.prose li,
.prose address,
.prose blockquote {
  font-family: var(--font-body);
  /* Explicit, not incidental. Links inside a paragraph do glow — that is the
     accent doing its job — but the running text never does. */
  text-shadow: none;
}

.prose p,
.prose ul,
.prose ol {
  margin: 0 0 var(--s4);
}

.prose > :last-child {
  margin-bottom: 0;
}

.prose strong,
.prose b {
  font-weight: 600;
}

.prose ul,
.prose ol {
  padding-left: 1.4em;
}

.prose li {
  /*
   * The custom bullet from the previous site. ::marker can be coloured where
   * list-style-type alone cannot; the shorthand stays as a fallback.
   */
  list-style: var(--bullet);
  padding-left: 0.1em;
}

.prose li::marker {
  content: var(--bullet);
  color: var(--g1);
}

.prose li + li {
  margin-top: var(--s2);
}

/* ── Display ───────────────────────────────────────────────────────────── */

.display {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  font-weight: 400;
  line-height: 0.9;
  letter-spacing: 0.02em;
}

/* Small uppercase mono label — roles, table headers, field names. */
.eyebrow {
  text-shadow: var(--glow);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-cap);
  color: var(--g1);
}

/* ── Code ──────────────────────────────────────────────────────────────── */

code,
kbd,
samp,
pre {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

code {
  text-shadow: var(--glow);
  background: rgba(0, 0, 0, 0.3);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--r-sm);
  padding: 0.1em 0.4em;
  color: var(--g-hi);
  overflow-wrap: break-word;
}

code.block,
pre {
  display: block;
  padding: var(--s4);
  white-space: pre-wrap;
  overflow-x: auto;
  line-height: 1.5;
  color: var(--text);
}

/* 03-layout.css */
/*
 * Layout — the terminal window that is the page.
 *
 * The window is a frame around a normally-scrolling document, not a fixed pane
 * with its own scrollbar. That distinction is what keeps the sticky chrome, the
 * #anchor jumps on /ressurser and every responsive breakpoint working: those
 * queries are viewport-based, so a narrow inner pane inside a wide window would
 * have picked the wide layout for a container that could not hold it.
 */

/*
 * Cross-document view transitions. The browser cross-fades between pages with
 * no script, no history handling and nothing to get wrong; where it is not
 * supported this line does nothing and navigation is ordinary. That is the
 * whole reason to prefer it to intercepting clicks and swapping content by
 * hand.
 */
@view-transition {
  navigation: auto;
}

body {
  margin: 0;
  min-height: 100vh;
  background-color: var(--bg);
}

.skiplink {
  position: absolute;
  left: -9999px;
}

.skiplink:focus {
  left: var(--s4);
  top: var(--s2);
  z-index: 9999;
  background: var(--surface);
  border: 1px solid var(--border-green);
  border-radius: var(--r);
  padding: var(--s2) var(--s4);
  color: var(--text);
}

/* ── The window ────────────────────────────────────────────────────────── */

/*
 * The window is the viewport. No margin, no radius, no border — a terminal
 * fills the screen, and the moment it floats on a background it reads as a
 * widget on a page instead.
 */
.shell {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* ── Chrome ────────────────────────────────────────────────────────────── */

.chrome {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--nav-h);
  padding: 0 var(--s5);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.chrome__inner {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  max-width: var(--maxw-pane);
  margin-inline: auto;
}

.chrome__dots {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.chrome__dots i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--border-strong);
}

/* Only the live one glows, so it reads as a status light rather than
   borrowed window decoration. */
.chrome__dots i:first-child {
  background: var(--g1);
  box-shadow: var(--glow);
}

.chrome__actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
  flex-shrink: 0;
}

.chrome__actions a {
  display: inline-flex;
  padding: var(--s2);
  color: var(--dim);
  border-radius: var(--r);
  transition: color 0.15s, background-color 0.15s;
}

.chrome__actions a:hover {
  color: var(--g1);
  background: rgba(60, 179, 79, 0.1);
}

/* ── Tabs ──────────────────────────────────────────────────────────────── */

/*
 * Wide: a strip across the chrome, scrolling if it has to.
 */
.tabs {
  min-width: 0;
  flex: 1;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  /* Fades the right edge so the strip reads as scrollable when it overflows. */
  mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
}

/* The button belongs to the narrow layout only. */
.menu-btn {
  display: none;
}

.tabs::-webkit-scrollbar {
  display: none;
}

.tabs__list {
  display: flex;
  align-items: stretch;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  width: max-content;
}

.tab {
  display: flex;
  align-items: center;
  height: var(--nav-h);
  padding: 0 var(--s4);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  transition: color 0.15s, background-color 0.15s, border-color 0.15s;
}

.tab:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.03);
}

.tab[aria-current="page"] {
  color: var(--text);
  background: rgba(60, 179, 79, 0.07);
  border-bottom-color: var(--g1);
  box-shadow: inset 0 -1px 12px rgba(60, 179, 79, 0.12);
}

/* ── Menu, below the breakpoint ────────────────────────────────────────── */

/*
 * A scrolling strip on a phone shows two destinations and hides the rest behind
 * a drag. The menu shows all six at once, which is the whole job of navigation.
 *
 * Gated on html[data-js]: without scripting there is no toggle to press, so the
 * list simply renders and every destination stays reachable.
 */
@media (max-width: 51.99rem) {
  .chrome {
    padding: 0 var(--s4);
  }

  .chrome__dots {
    display: none;
  }

  html[data-js] .menu-btn {
    display: flex;
    align-items: center;
    gap: var(--s3);
    height: var(--nav-h);
    padding: 0;
    font-family: var(--font-mono);
    font-size: var(--fs-sm);
    color: var(--dim);
    cursor: pointer;
  }

  .menu-btn__path {
    color: var(--g1);
    text-shadow: var(--glow);
  }

  /* Three bars, drawn rather than shipped as an icon. */
  .menu-btn__icon,
  .menu-btn__icon::before,
  .menu-btn__icon::after {
    display: block;
    width: 16px;
    height: 2px;
    background: currentColor;
  }

  .menu-btn__icon {
    position: relative;
  }

  .menu-btn__icon::before,
  .menu-btn__icon::after {
    content: "";
    position: absolute;
    left: 0;
  }

  .menu-btn__icon::before {
    top: -5px;
  }

  .menu-btn__icon::after {
    top: 5px;
  }

  /* Open: the bars become a cross. */
  .menu-btn[aria-expanded="true"] .menu-btn__icon {
    background: transparent;
  }

  .menu-btn[aria-expanded="true"] .menu-btn__icon::before {
    top: 0;
    transform: rotate(45deg);
  }

  .menu-btn[aria-expanded="true"] .menu-btn__icon::after {
    top: 0;
    transform: rotate(-45deg);
  }

  /*
   * The listing drops below the chrome as a panel rather than growing it. The
   * chrome is position:fixed, so it is already the containing block — nothing
   * else has to change to anchor this. The insets are zero and the panel still
   * spans the full width: offsets resolve against the containing block's
   * padding box, and the chrome's padding box reaches its edges.
   */
  html[data-js] .tabs {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex: none;
    overflow: visible;
    mask-image: none;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    box-shadow: var(--shadow-card);
    /*
     * display:none, never a transform. A menu moved offscreen is still
     * focusable — the previous site let keyboard users tab into an invisible
     * panel, and that is not coming back with the menu.
     */
    display: none;
  }

  html[data-js] .tabs[data-open] {
    display: block;
  }

  /*
   * Without scripting there is no toggle, so the list renders in full and the
   * chrome becomes as tall as six destinations. Pinned, that would sit on top
   * of the first screenful — so it goes back into the flow, and the pane stops
   * reserving height for a bar that is no longer floating above it.
   */
  html:not([data-js]) .chrome {
    position: static;
  }

  html:not([data-js]) .shell__pane {
    padding-top: var(--s5);
  }

  .tabs__list {
    flex-direction: column;
    align-items: stretch;
    width: auto;
    padding: var(--s2) 0;
  }

  .tab {
    height: auto;
    padding: var(--s3) var(--s4) var(--s3) var(--s5);
    border-bottom: none;
    border-left: 2px solid transparent;
  }

  /* The listing marks the current entry the way a terminal would. */
  .tab[aria-current="page"] {
    border-bottom-color: transparent;
    border-left-color: var(--g1);
    box-shadow: none;
  }

  .tab[aria-current="page"]::before {
    content: "\25AE";
    margin-right: var(--s2);
    color: var(--g1);
    text-shadow: var(--glow);
  }
}

/* ── Pane ──────────────────────────────────────────────────────────────── */

.shell__pane {
  position: relative;
  flex: 1;
  /* Clears the fixed chrome above and the pinned bar below. */
  padding: calc(var(--nav-h) + var(--s5)) var(--s5) calc(var(--bar-h) + var(--s6));
  /* The dot grid moved in here from the body: outside the window it framed
     the page, inside it sits behind the session where it belongs. */
  background-image: var(--dots);
  background-size: var(--dots-size);
}

/*
 * Content is centred within a cap rather than running the full width. The
 * window is edge to edge; the reading is not.
 */
.shell__pane > * {
  max-width: var(--maxw-pane);
  margin-inline: auto;
}

main {
  display: block;
}

main > section + section {
  margin-top: var(--s7);
}

/* ── The pinned bar ────────────────────────────────────────────────────── */

/*
 * Console and status line together at the foot of the screen — tmux's command
 * prompt and status bar, which is what they already were in spirit.
 */
.shell__bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  background: var(--surface);
  border-top: 1px solid var(--border);
}

.shell__bar > * {
  max-width: var(--maxw-pane);
  margin-inline: auto;
}

/*
 * On a phone the bar unpins. A fixed input at the bottom of a small viewport
 * fights the on-screen keyboard, and the vertical space is worth more than a
 * feature nobody reaches for on a touch device.
 */
@media (max-width: 51.99rem) {
  .shell__bar {
    position: static;
    border-top: none;
  }

  .shell__pane {
    padding-bottom: var(--s6);
  }
}

/* ── Status line ───────────────────────────────────────────────────────── */

/*
 * The shape a tmux or vim status bar takes: where you are, how to move, who you
 * are. Every field is real state.
 */
.status {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s2) var(--s5);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* Standing alone at the end of the document, it needs its own rule above. */
@media (max-width: 51.99rem) {
  .status {
    border-top: 1px solid var(--border);
    margin-top: var(--s6);
  }
}

.status__path {
  color: var(--g1);
  text-shadow: var(--glow);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.status__hint {
  display: flex;
  align-items: center;
  gap: var(--s1);
  white-space: nowrap;
}

.status__hint kbd {
  font-family: inherit;
  font-size: inherit;
  padding: 0 0.35em;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  color: var(--dim);
}

.status__session {
  white-space: nowrap;
  color: var(--dim);
}

.status__session::before {
  content: "[ ";
  color: var(--g1);
  text-shadow: var(--glow);
}

.status__session::after {
  content: " ]";
  color: var(--g1);
  text-shadow: var(--glow);
}

/* Below this the hint competes with the path for room it does not have. */
@media (max-width: 39.99rem) {
  .status__hint {
    display: none;
  }

  .status__session {
    margin-left: auto;
  }
}

/* ── Footer, as pane output ────────────────────────────────────────────── */

.footer {
  padding: 0;
  background: none;
  border: none;
  margin-top: var(--s4);
}

.footer__inner {
  max-width: none;
  margin: 0;
  display: grid;
  gap: var(--s6);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 52rem) {
  .footer__inner {
    grid-template-columns: 1fr auto 1fr;
  }

  .footer__logo {
    justify-self: center;
  }

  .footer__socials {
    justify-self: end;
    text-align: right;
  }
}

.footer h2 {
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-cap);
  color: var(--g1);
  margin: 0 0 var(--s4);
}

.footer address {
  font-style: normal;
  color: var(--muted);
}

.footer__logo img {
  width: 160px;
  height: auto;
  opacity: 0.9;
}

.footer__block + .footer__block {
  margin-top: var(--s5);
}

.footer__label {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: var(--track-cap);
  color: var(--muted);
  display: block;
}

.footer__socials ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

.footer__socials li {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

@media (min-width: 52rem) {
  .footer__socials li {
    justify-content: flex-end;
  }
}

.footer__socials a {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
  color: var(--muted);
  text-decoration: none;
  transition: color 0.2s;
}

.footer__socials a:hover {
  color: var(--g1);
}

/* ── Cursor keyframe ───────────────────────────────────────────────────── */

@keyframes cursor-blink {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
}

/* 04-components.css */
/* Components — cards, buttons, panels, tables, disclosures, dialogs. */

/* ── Buttons ───────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  padding: var(--s2) var(--s5);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  line-height: 1.6;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--card);
  color: var(--text);
  transition: background-color 0.15s, border-color 0.15s, box-shadow 0.15s,
    color 0.15s;
}

.btn:hover {
  background: var(--card-hover);
  border-color: var(--border-strong);
  color: var(--text);
}

.btn--primary {
  background: var(--g1);
  border-color: var(--g1);
  color: var(--on-green);
}

.btn--primary:hover {
  background: var(--g-hi);
  border-color: var(--g-hi);
  color: var(--on-green);
  box-shadow: 0 0 16px rgba(60, 179, 79, 0.3);
}

/* A green ring on a green fill is invisible. */
.btn--primary:focus-visible {
  outline-color: var(--text);
}

.btn--ghost {
  background: transparent;
  border-color: var(--border-green);
  color: var(--g1);
}

.btn--ghost:hover {
  background: rgba(60, 179, 79, 0.1);
  color: var(--g-hi);
}

.btn--danger {
  background: transparent;
  border-color: var(--error-bd);
  color: var(--error);
}

.btn--danger:hover {
  background: var(--error-bg);
  border-color: var(--error);
  color: var(--error);
}

.btn--lg {
  font-size: var(--fs-lg);
  padding: var(--s3) var(--s6);
}

.btn--block {
  width: 100%;
}

.btn--icon {
  padding: var(--s2);
}

.btn:disabled,
.btn[aria-busy="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* ── Icons ─────────────────────────────────────────────────────────────── */

.icon {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  vertical-align: -0.125em;
  fill: currentColor;
}

.icon--lg {
  width: 1.35em;
  height: 1.35em;
}

/* ── Cards ─────────────────────────────────────────────────────────────── */

.card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s5);
  transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s;
}

.card--hover:hover {
  border-color: var(--border-green);
  box-shadow: var(--shadow-glow);
  transform: translateY(-1px);
}

.grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: 1fr;
}

@media (min-width: 40rem) {
  .grid--people {
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  }
}

@media (min-width: 64rem) {
  .grid--links {
    grid-template-columns: 1fr 1fr;
  }
}

/* The window is edge to edge now, so a third column finally fits. */
@media (min-width: 96rem) {
  .grid--links {
    grid-template-columns: repeat(3, 1fr);
  }
}

/* ── Section header ────────────────────────────────────────────────────── */

/*
 * The // marker is reserved for headings that are jump targets, so it means
 * "this section has an address" rather than being decoration.
 */
.section-head {
  display: flex;
  align-items: baseline;
  gap: 0.5em;
  padding-bottom: var(--s3);
  margin: var(--s8) 0 var(--s5);
  border-bottom: 1px solid var(--border);
  scroll-margin-top: calc(var(--nav-h) + var(--s4));
}

.section-head::before {
  content: "//";
  flex-shrink: 0;
  color: var(--g1);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 0.85em;
  letter-spacing: -0.05em;
}

.section-head h2 {
  margin: 0;
  font-size: var(--fs-base);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-cap);
}

/* ── Jump navigation ───────────────────────────────────────────────────── */

.jumpnav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  list-style: none;
  margin: 0 0 var(--s6);
  padding: 0;
}

.jumpnav a {
  display: inline-block;
  padding: var(--s1) var(--s3);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--dim);
  text-decoration: none;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  transition: border-color 0.15s, color 0.15s;
}

.jumpnav a:hover {
  border-color: var(--border-green);
  color: var(--g1);
}

/* ── Person card ───────────────────────────────────────────────────────── */

.person__role {
  display: block;
  margin-bottom: var(--s1);
}

.person__name {
  font-weight: 600;
}

.person__mail {
  display: block;
  margin-top: var(--s1);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--muted-strong);
  overflow-wrap: anywhere;
}

/* ── Link card ─────────────────────────────────────────────────────────── */

.link-card__title {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--g1);
  text-decoration: none;
}

.link-card__title:hover {
  color: var(--g-hi);
  text-decoration: underline;
}

.link-card__title::after {
  content: " ↗";
  font-size: 0.8em;
  opacity: 0.6;
}

.link-card p {
  margin: var(--s2) 0 0;
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--muted-strong);
}

/* ── Panels (alerts, callouts) ─────────────────────────────────────────── */

.panel {
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--r);
  padding: var(--s4) var(--s5);
  margin: var(--s5) 0;
  /* A panel is a sentence to read, so it keeps to the reading column rather
     than following the window out to the edge of a wide screen. */
  max-width: 62rem;
}

.panel--success {
  background: var(--success-bg);
  border-color: var(--success-bd);
  border-left-color: var(--success);
}

.panel--error {
  background: var(--error-bg);
  border-color: var(--error-bd);
  border-left-color: var(--error);
}

.panel--info {
  background: var(--info-bg);
  border-color: var(--info-bd);
  border-left-color: var(--info);
}

.panel--warning {
  background: var(--warning-bg);
  border-color: var(--warning-bd);
  border-left-color: var(--warning);
}

/* Terminal-style block, for the resignation email template. */
.terminal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: var(--s5);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  white-space: pre-wrap;
  overflow-x: auto;
}

/* ── Tables ────────────────────────────────────────────────────────────── */

.table-wrap {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}

thead th {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  text-align: left;
}

th,
td {
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--border);
}

tbody tr:nth-child(odd) {
  background: rgba(255, 255, 255, 0.02);
}

tbody tr:hover {
  background: rgba(255, 255, 255, 0.05);
}

/* ── Disclosure ────────────────────────────────────────────────────────── */

details {
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--card);
}

details > summary {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  list-style: none;
  border-radius: var(--r);
}

details > summary::-webkit-details-marker {
  display: none;
}

details > summary::after {
  content: "▾";
  margin-left: auto;
  font-size: 1.75em;
  line-height: 1;
  color: var(--g1);
  text-shadow: var(--glow);
  transition: transform 0.2s;
}

details[open] > summary::after {
  transform: rotate(180deg);
}

details > summary:hover {
  color: var(--g1);
}

details > .details__body {
  padding: 0 var(--s5) var(--s5);
  border-top: 1px solid var(--border);
  padding-top: var(--s5);
}

/* ── Dialog ────────────────────────────────────────────────────────────── */

dialog {
  max-width: min(90vw, 32rem);
  padding: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  color: var(--text);
}

dialog::backdrop {
  background: var(--overlay);
}

.dialog__header {
  padding: var(--s5);
  border-bottom: 1px solid var(--border);
  font-family: var(--font-mono);
  font-weight: 700;
}

.dialog__body {
  padding: var(--s5);
  white-space: pre-wrap;
}

.dialog__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--s3);
  padding: var(--s4) var(--s5);
  border-top: 1px solid var(--border);
}

/* ── Toasts ────────────────────────────────────────────────────────────── */

.toasts {
  position: fixed;
  right: var(--s4);
  bottom: var(--s4);
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
  max-width: min(90vw, 24rem);
}

.toast {
  padding: var(--s3) var(--s4);
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  font-size: var(--fs-sm);
}

.toast--success {
  border-left-color: var(--success);
}

.toast--error {
  border-left-color: var(--error);
}

.toast--info {
  border-left-color: var(--info);
}

.toast--warning {
  border-left-color: var(--warning);
}

/* ── Chips ─────────────────────────────────────────────────────────────── */

.chip {
  display: inline-block;
  padding: 0 var(--s2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  vertical-align: middle;
}

/* 05-forms.css */
/* Forms. */

/*
 * The window runs edge to edge; a form does not follow it out there. A text
 * field a metre wide is harder to fill in than one at reading width, and a
 * label pinned to the far left of a control on the far right stops reading as
 * a pair.
 *
 * Scoped to main so it never reaches the console, which lives in the bar and
 * is sized by it.
 */
main form {
  max-width: 62rem;
}

input,
textarea,
select {
  width: 100%;
  padding: var(--s2) var(--s3);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border);
  border-radius: var(--r);
  color: var(--text);
  font-size: var(--fs-base);
  transition: border-color 0.15s, box-shadow 0.15s, background-color 0.15s;
}

input:hover,
textarea:hover,
select:hover {
  border-color: var(--border-strong);
}

input:focus,
textarea:focus,
select:focus {
  outline: none;
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--border-green);
  box-shadow: 0 0 0 3px rgba(60, 179, 79, 0.12);
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: var(--focus);
  outline-offset: var(--focus-offset);
}

input::placeholder,
textarea::placeholder {
  color: var(--muted);
}

input:disabled,
textarea:disabled,
select:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

textarea {
  min-height: 8rem;
  resize: vertical;
  line-height: 1.6;
}

select {
  appearance: none;
  padding-right: var(--s7);
  background-image: var(--svg-arrow);
  background-repeat: no-repeat;
  background-position: right var(--s4) top 50%;
  background-size: 0.65em auto;
  cursor: pointer;
}

label {
  cursor: pointer;
}

fieldset {
  border: none;
  margin: 0;
  padding: 0;
}

legend {
  padding: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--dim);
}

/* ── Field rows ────────────────────────────────────────────────────────── */

.field {
  display: grid;
  gap: var(--s1);
  margin-bottom: var(--s5);
}

.field > label {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--dim);
}

.field__hint {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}

.field__error {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--error);
}

.field:has(.field__error) input,
.field:has(.field__error) select,
.field:has(.field__error) textarea,
.field input[aria-invalid="true"],
.field select[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  border-color: var(--error-bd);
}

@media (min-width: 48rem) {
  .field {
    grid-template-columns: 14rem 1fr;
    align-items: baseline;
    column-gap: var(--s5);
  }

  .field__hint,
  .field__error {
    grid-column: 2;
  }
}

/* ── Checkbox and radio ────────────────────────────────────────────────── */

.check {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s4);
}

input[type="checkbox"],
input[type="radio"] {
  appearance: none;
  width: 1.1em;
  height: 1.1em;
  flex-shrink: 0;
  margin: 0;
  padding: 0;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border-strong);
  cursor: pointer;
  display: grid;
  place-content: center;
}

input[type="checkbox"] {
  border-radius: var(--r-sm);
}

input[type="radio"] {
  border-radius: 50%;
}

input[type="checkbox"]::before {
  content: "";
  width: 0.6em;
  height: 0.6em;
  transform: scale(0);
  transition: transform 0.1s ease-out;
  background: var(--on-green);
  clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}

input[type="radio"]::before {
  content: "";
  width: 0.5em;
  height: 0.5em;
  border-radius: 50%;
  transform: scale(0);
  transition: transform 0.1s ease-out;
  background: var(--on-green);
}

input[type="checkbox"]:checked,
input[type="radio"]:checked {
  background: var(--g1);
  border-color: var(--g1);
}

input[type="checkbox"]:checked::before,
input[type="radio"]:checked::before {
  transform: scale(1);
}

/* ── Member type picker ────────────────────────────────────────────────── */

/*
 * The registration form's conditional fields, without JavaScript.
 *
 * A radio group plus the sibling combinator does what the old fetch-driven
 * form did: display:none takes the inactive fields out of the tab order and
 * the accessibility tree, so no extra scripting is needed to keep them out of
 * a keyboard user's way.
 *
 * Hidden inputs still submit, so the server must select the fields for the
 * chosen type rather than trusting whatever arrives.
 */
/*
 * The radios must be siblings of .typepick__fields for the combinator below
 * to reach them, so they are direct children laid out with flex rather than
 * nested inside a wrapper.
 */
.typepick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-bottom: var(--s5);
}

.typepick__label {
  flex: 0 0 100%;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--dim);
}

.typepick > input[type="radio"] + label {
  margin-right: var(--s5);
}

.typepick__fields {
  flex: 0 0 100%;
  margin-top: var(--s5);
}

.typepick__fields > [data-when] {
  display: none;
}

#type-student:checked ~ .typepick__fields > [data-when~="student"],
#type-employee:checked ~ .typepick__fields > [data-when~="employee"],
#type-alumni:checked ~ .typepick__fields > [data-when~="alumni"] {
  display: grid;
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s6);
}

/* 06-effects.css */
/*
 * Effects.
 *
 * Three moving things on the whole site: the brainfuck stream and the glitching
 * logo on the front page, and one text-reveal per page. Plus the blinking
 * cursor after the wordmark. Everything else is still. Restraint is what keeps
 * this reading as a design rather than a costume.
 *
 * Deliberately not ported from the theme prototype: the full-viewport opacity
 * flicker, which animates at roughly 6.7 Hz — inside the band that triggers
 * photosensitive seizures — and the permanent chromatic text-shadow wobble,
 * which destroys legibility and pins a compositor repaint forever.
 */


/* ── Hero ──────────────────────────────────────────────────────────────── */

.hero {
  text-align: center;
}

/*
 * Scanlines belong to the window. Now that the window is the page they cover
 * the whole pane, which is the point — but they stay very faint, because this
 * is laid over every paragraph on the site rather than over a hero.
 */
.shell__pane::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: repeating-linear-gradient(
    0deg,
    transparent 0 2px,
    rgba(0, 0, 0, 0.035) 2px 3px
  );
}

/*
 * The program stream now lives inside the frame — see 06b-terminal.css. The
 * rules that used to position it as a layer behind the hero are gone rather
 * than left to fight the new ones: an id selector outranks a class, so the
 * leftovers silently pulled the program out of the frame and dragged it across
 * the commands above it.
 */


/* ── Glitching logo ────────────────────────────────────────────────────── */

/*
 * Chromatic aberration from three stacked copies of the wordmark. Each colour
 * layer stays fully clipped away for ninety per cent of its cycle, so the
 * effect is a one-second tear roughly every ten seconds rather than constant
 * motion.
 */
.logo-glitch {
  position: relative;
  display: inline-block;
  width: min(90%, 30rem);
  margin: 0 auto var(--s6);
}

.logo-glitch img {
  width: 100%;
  height: auto;
}

.logo-glitch img.l-red,
.logo-glitch img.l-blue {
  position: absolute;
  top: 0;
  pointer-events: none;
}

.logo-glitch img.l-red {
  left: 3px;
  filter: drop-shadow(-2px 0 red);
  animation: glitch-1 10s infinite linear alternate-reverse;
}

.logo-glitch img.l-blue {
  left: -3px;
  filter: drop-shadow(-2px 0 blue);
  animation: glitch-2 10s infinite linear alternate-reverse;
}

@keyframes glitch-1 {
  0%,
  90% {
    clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
  }
  91% {
    clip-path: polygon(0 12%, 100% 12%, 100% 20%, 0 20%);
  }
  92% {
    clip-path: polygon(0 63%, 100% 63%, 100% 68%, 0 68%);
  }
  93% {
    clip-path: polygon(0 31%, 100% 31%, 100% 39%, 0 39%);
  }
  94% {
    clip-path: polygon(0 78%, 100% 78%, 100% 83%, 0 83%);
  }
  95% {
    clip-path: polygon(0 5%, 100% 5%, 100% 11%, 0 11%);
  }
  96% {
    clip-path: polygon(0 45%, 100% 45%, 100% 52%, 0 52%);
  }
  97% {
    clip-path: polygon(0 88%, 100% 88%, 100% 94%, 0 94%);
  }
  98% {
    clip-path: polygon(0 22%, 100% 22%, 100% 27%, 0 27%);
  }
  99%,
  100% {
    clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
  }
}

@keyframes glitch-2 {
  0%,
  90% {
    clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
  }
  91% {
    clip-path: polygon(0 55%, 100% 55%, 100% 61%, 0 61%);
  }
  92% {
    clip-path: polygon(0 9%, 100% 9%, 100% 16%, 0 16%);
  }
  93% {
    clip-path: polygon(0 71%, 100% 71%, 100% 77%, 0 77%);
  }
  94% {
    clip-path: polygon(0 36%, 100% 36%, 100% 41%, 0 41%);
  }
  95% {
    clip-path: polygon(0 84%, 100% 84%, 100% 90%, 0 90%);
  }
  96% {
    clip-path: polygon(0 18%, 100% 18%, 100% 24%, 0 24%);
  }
  97% {
    clip-path: polygon(0 48%, 100% 48%, 100% 56%, 0 56%);
  }
  98% {
    clip-path: polygon(0 66%, 100% 66%, 100% 72%, 0 72%);
  }
  99%,
  100% {
    clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);
  }
}

/* ── QR plate ──────────────────────────────────────────────────────────── */

/*
 * A QR code on a dark background is a scanner-reliability problem at a busy
 * event, so the code stays black on white with a quiet zone and the design
 * expresses itself in the plate around it.
 */
.qr-plate {
  width: max-content;
  max-width: min(80vw, 340px);
  margin: var(--s6) auto;
  padding: var(--s5);
  background: #fff;
  border-radius: var(--r);
}

.qr-plate svg {
  display: block;
  width: 100%;
  height: auto;
  shape-rendering: crispEdges;
}

/* ── Reduced motion ────────────────────────────────────────────────────── */

/*
 * CSS cannot stop a JavaScript-driven text animation, so the brainfuck stream
 * and the text reveal check prefers-reduced-motion themselves in app.js. This
 * block covers everything declarative.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .logo-glitch img.l-red,
  .logo-glitch img.l-blue {
    display: none;
  }

  .nav__brand::after {
    animation: none;
    opacity: 1;
  }

  /*
   * The terminal sequence is not hidden here, only left un-animated. The
   * markup already contains the finished session — program, output and all —
   * and the script simply does not clear it. Hiding it would remove content
   * rather than motion, which is not what the preference asks for.
   */
  .shell__pane::after {
    display: none;
  }
}

/* 06b-terminal.css */
/*
 * Terminal chrome.
 *
 * The site already spoke in terminal idiom — a prompt, a mono face, a dot
 * grid — but decoratively: the shell was a costume over an ordinary page. The
 * pieces here are the ones that carry meaning.
 *
 * The hero is the argument. That brainfuck program is not ambient noise; it
 * prints this organisation's name, so the front page shows it being typed,
 * run, and producing the wordmark as its output. Same effect as before, now
 * the point of the page rather than texture behind it.
 */

/* ── The shell line ───────────────────────────────────────────────────── */

/*
 * The line each page opens with. It names what the page is — cat for a
 * document, ls for a listing — and typing it on arrival is what makes changing
 * page read as a session continuing rather than a document being replaced.
 */
.cmd {
  margin: 0 0 var(--s4);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--dim);
  word-break: break-word;
}

.cmd::before {
  content: "$ ";
  color: var(--g1);
  text-shadow: var(--glow-lg);
}

/* The board is root; see the session note in 03-layout.css. */
.p-styret .cmd::before {
  content: "# ";
  color: var(--g-hi);
  text-shadow: var(--glow-lg);
}

/* Lines that are part of the page's own output, never retyped. */
.cmd--static {
  margin-top: var(--s5);
}

.cmd--static:first-child {
  margin-top: 0;
}

/*
 * While the line is being typed it carries a cursor. The attribute is added and
 * removed by script, so nothing blinks on a page that never animates.
 */
.cmd[data-typing]::after {
  content: "▊";
  color: var(--g1);
  text-shadow: var(--glow-lg);
  animation: cursor-blink 1.1s step-end infinite;
}

/* ── Front-page sequence ───────────────────────────────────────────────── */

.hero__src {
  text-shadow: var(--glow);
  margin: 0 0 var(--s4);
  font-family: var(--font-mono);
  font-size: clamp(0.7rem, 1.5vw, 0.8rem);
  line-height: 1.6;
  color: var(--g2);
  opacity: 0.75;
  word-break: break-all;
  white-space: pre-wrap;
  min-height: 4.5em;
  user-select: none;
}

.hero__out {
  position: relative;
  margin: var(--s3) 0 var(--s5);
  opacity: 0;
  transition: opacity 0.4s ease-out;
}

.hero__out[data-shown] {
  opacity: 1;
}

.hero__out img {
  width: min(100%, 26rem);
  height: auto;
}

/* ── Corner brackets ───────────────────────────────────────────────────── */

/*
 * Cards get drawn corners rather than a plain rounded rectangle. Two
 * pseudo-elements, opposite corners, so the shape reads as bracketed without
 * needing four extra nodes.
 */
.card {
  position: relative;
}

.card::before,
.card::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  border-color: var(--border-green);
  border-style: solid;
  opacity: 0;
  transition: opacity 0.15s;
  pointer-events: none;
}

.card::before {
  top: -1px;
  left: -1px;
  border-width: 1px 0 0 1px;
}

.card::after {
  bottom: -1px;
  right: -1px;
  border-width: 0 1px 1px 0;
}

.card--hover:hover::before,
.card--hover:hover::after {
  opacity: 1;
}

/* ── Status markers ────────────────────────────────────────────────────── */

/*
 * Square brackets rather than pills. A bracketed token is how a terminal
 * marks state, and it costs nothing to say it in the reader's own idiom.
 */
.chip {
  border: none;
  border-radius: 0;
  padding: 0;
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.chip::before {
  content: "[ ";
  color: var(--g1);
  text-shadow: var(--glow);
}

.chip::after {
  content: " ]";
  color: var(--g1);
  text-shadow: var(--glow);
}

/* ── Terminal-style inputs ─────────────────────────────────────────────── */

/*
 * A caret before each field. Purely typographic — the input keeps its own
 * box, so nothing about focus or hit area changes.
 */
.field > label::before {
  content: "❯ ";
  color: var(--g1);
}

/* ── Section rules ─────────────────────────────────────────────────────── */

/*
 * The // marker already means "this section is addressable". The rule after
 * it now fades out rather than running the full width, which stops a page of
 * sections reading as a stack of boxes.
 */
.section-head {
  border-bottom: none;
  position: relative;
  padding-bottom: var(--s3);
}

/*
 * A drawn rule rather than a hairline: box-drawing characters are how a
 * terminal separates things, and the run fades out so a page of sections does
 * not read as a stack of boxes.
 */
.section-head::after {
  /*
   * Long enough to outrun the widest header the pane cap allows (100rem), so
   * the run is always clipped by overflow rather than simply running out —
   * which would leave a hard stop partway along, where the mask is still most
   * of the way opaque.
   */
  content: "────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.35em;
  overflow: hidden;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1;
  color: var(--border-green);
  mask-image: linear-gradient(to right, #000, rgba(0, 0, 0, 0.25) 35%, transparent);
}

/* ── Reduced motion ────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  /*
   * Nothing is typed, so nothing carries a cursor and the front page's output
   * is simply present. The markup already holds the finished session — this
   * only stops it being animated, it never hides content.
   */
  .cmd[data-typing]::after {
    animation: none;
  }

  .hero__out {
    opacity: 1;
    transition: none;
  }
}

/* 07-pages.css */
/* Page-specific rules — only what is genuinely local to one page. */

/* ── / ─────────────────────────────────────────────────────────────────── */

.p-index .lead {
  max-width: 42ch;
  margin: 0 auto var(--s6);
  font-size: var(--fs-lg);
  color: var(--dim);
}

.p-index .questions {
  list-style: none;
  margin: 0 auto var(--s7);
  padding: 0;
  max-width: 44ch;
  font-family: var(--font-mono);
  color: var(--dim);
}

.p-index .questions li {
  padding: var(--s1) 0;
}

.p-index .questions li::before {
  content: "? ";
  color: var(--g1);
}

.p-index .outro {
  margin-top: var(--s7);
  text-align: center;
  color: var(--muted);
}

/* ── /historie ─────────────────────────────────────────────────────────── */

/*
 * The 2014 naming vote. The candidates are the most characteristic thing on
 * the page, so they get their own panel instead of being an inline list.
 */
.p-historie .ballot {
  margin: var(--s6) 0;
  padding: var(--s5) var(--s6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
}

.p-historie .ballot ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.p-historie .ballot li {
  padding: var(--s1) 0;
  list-style: none;
}

.p-historie .ballot li::marker {
  content: "";
}

.p-historie .colophon {
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* ── /arrangementer ────────────────────────────────────────────────────── */

/* Room between the board's form and the list it edits, so the two read as
   separate things rather than one stack. */
.p-arrangementer .admin {
  margin: var(--s5) 0 var(--s7);
}

.events {
  display: grid;
  gap: var(--s5);
  list-style: none;
  padding: 0;
  margin: 0;
}

.event dl {
  display: grid;
  /* A shared minimum keeps the value column aligned across cards even when
     one of them has a longer label like "Registrering". */
  grid-template-columns: minmax(7.5rem, max-content) 1fr;
  gap: var(--s2) var(--s5);
  margin: var(--s4) 0 0;
  align-items: baseline;
}

.event dt {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: var(--track-cap);
  color: var(--muted-strong);
}

.event dd {
  margin: 0;
}

/* Board-authored text: newlines are meaningful, HTML is not.
   Named event__info rather than .info so it cannot collide with the status
   utility of that name, which would tint the description blue. */
.event dd.event__info {
  white-space: pre-wrap;
}

@media (max-width: 39.99rem) {
  .event dl {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .event dt {
    margin-top: var(--s3);
  }
}

/* Past events get a diagonal hatch, carried over from the previous site. */
.event--past {
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(255, 255, 255, 0.02) 0 3em,
    rgba(255, 255, 255, 0.05) 3em 6em
  );
}

.event__header {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s3);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--border);
}

.event__header h3 {
  margin: 0;
}

.event__header h3::before {
  content: "> ";
  color: var(--g1);
  text-shadow: var(--glow);
}

.event__admin {
  position: absolute;
  top: var(--s4);
  right: var(--s4);
  display: flex;
  gap: var(--s2);
}

.events__empty {
  color: var(--muted);
}

/* ── /profil ───────────────────────────────────────────────────────────── */

.profile {
  display: grid;
  gap: var(--s6);
  grid-template-columns: 1fr;
  align-items: start;
}

@media (min-width: 40rem) {
  .profile {
    grid-template-columns: minmax(8rem, 3fr) 7fr;
  }
}

.profile__avatar {
  width: 100%;
  max-width: 12rem;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  border: 4px solid var(--border-strong);
}

.profile dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--s3) var(--s5);
  margin: 0;
}

.profile dt {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: var(--track-cap);
  color: var(--muted-strong);
}

.profile dd {
  margin: 0;
  overflow-wrap: anywhere;
}

/* ── /innsjekk ─────────────────────────────────────────────────────────── */

.checkin-code {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 5vw, 2.5rem);
  letter-spacing: 0.05em;
  text-align: center;
  word-break: break-all;
}

/* The profile id is diagnostic detail, not something to squeeze onto a phone. */
@media (max-width: 43.74rem) {
  .attendance th:nth-child(2),
  .attendance td:nth-child(2) {
    display: none;
  }
}

/* ── /innsjekk-qr ──────────────────────────────────────────────────────── */

.receipt {
  display: grid;
  place-content: center;
  gap: var(--s5);
  min-height: 50vh;
  text-align: center;
}

.receipt__face {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  line-height: 1;
  color: var(--g1);
}

/* ── Error page ────────────────────────────────────────────────────────── */

.error-page {
  display: grid;
  place-content: center;
  gap: var(--s4);
  min-height: 50vh;
  text-align: center;
}

.error-page__status {
  font-family: var(--font-display);
  font-size: var(--fs-display);
  line-height: 1;
  color: var(--g1);
}

.error-page__message {
  color: var(--dim);
}

/* 07b-console.css */
/*
 * The command line, and the arrival animation's hidden state.
 *
 * Everything here is gated on html[data-js]. Without scripting the console
 * would be a dead input and the hidden state would be a blank page, so neither
 * exists at all — the server never hides anything, and there is a test
 * asserting the served markup is complete.
 */

[data-js-only] {
  display: none;
}

html[data-js] [data-js-only] {
  display: initial;
}

html[data-js] .status__hint {
  display: flex;
}

/* ── Arrival ───────────────────────────────────────────────────────────── */

/*
 * The pane is faded, not hidden, while the command types. Content that is
 * display:none cannot be found by in-page search or read ahead by a screen
 * reader, and there is no reason to take it away — only to let the command
 * land first.
 */
html[data-js] .shell__pane[data-arriving] main,
html[data-js] .shell__pane[data-arriving] .footer,
html[data-js] .shell__pane[data-arriving] .cmd--static {
  opacity: 0;
}

html[data-js] .shell__pane main,
html[data-js] .shell__pane .footer,
html[data-js] .shell__pane .cmd--static {
  opacity: 1;
  transition: opacity 0.25s ease-out;
}

/* ── Console ───────────────────────────────────────────────────────────── */

/*
 * Sits in the pinned bar, not at the end of the content — so it carries no rule
 * above it and no top margin. The bar provides both.
 */
html[data-js] .console {
  display: block;
  padding: var(--s2) var(--s5) 0;
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
}

/*
 * Command output appears above the input, and the bar is anchored to the bottom
 * of the screen — so it grows upward and must not be allowed to swallow the
 * page. Past this it scrolls.
 */
.console__out {
  max-height: 30vh;
  overflow-y: auto;
}

/* No console at all below the breakpoint: a fixed input fights the on-screen
   keyboard, and the space is worth more than a feature nobody reaches for on a
   phone. */
@media (max-width: 51.99rem) {
  html[data-js] .console {
    display: none;
  }
}

.console__out:empty {
  display: none;
}

.console__out {
  margin-bottom: var(--s2);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}

/* Echo of what was typed, so the session reads back. */
.console__echo {
  color: var(--dim);
}

.console__echo::before {
  content: "$ ";
  color: var(--g1);
}

.console__line {
  color: var(--muted-strong);
  white-space: pre-wrap;
  word-break: break-word;
}

.console__line a {
  color: var(--g1);
}

.console__line--error {
  color: var(--error);
}

html[data-js] .console__form {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
}

.console__prompt {
  color: var(--g1);
  text-shadow: var(--glow-lg);
  flex-shrink: 0;
}

.console__in {
  flex: 1;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  border-radius: 0;
  font-family: inherit;
  font-size: inherit;
  color: var(--text);
  caret-color: var(--g1);
}

.console__in:hover,
.console__in:focus {
  border: none;
  background: none;
  box-shadow: none;
  outline: none;
}

/* The caret is the focus indicator inside a terminal, but that is invisible to
   anyone tabbing through — so the row gets a ring of its own. */
html[data-js] .console__form:focus-within {
  outline: var(--focus);
  outline-offset: var(--focus-offset);
  border-radius: var(--r-sm);
}

.console__in::placeholder {
  color: var(--muted);
  opacity: 0.7;
}

@media (prefers-reduced-motion: reduce) {
  /*
   * No arrival animation at all: the pane is complete the moment it renders.
   * The script does not run either, so this is belt and braces.
   */
  html[data-js] .shell__pane[data-arriving] main,
  html[data-js] .shell__pane[data-arriving] .footer,
  html[data-js] .shell__pane[data-arriving] .cmd--static {
    opacity: 1;
  }
}

/* 08-utilities.css */
/* Utilities. Last in load order so they win. */

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.muted {
  color: var(--muted);
}

.dim {
  color: var(--dim);
}

.text-center {
  text-align: center;
}

.mono {
  font-family: var(--font-mono);
}

.nowrap {
  white-space: nowrap;
}

.stack > * + * {
  margin-top: var(--s4);
}

.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}

.push-right {
  margin-left: auto;
}

.success {
  color: var(--success);
}

.warning {
  color: var(--warning);
}

.info {
  color: var(--info);
}

.error {
  color: var(--error);
}

/*
 * Email addresses are assembled by app.js from an encoded attribute so the
 * plain string never appears in the HTML source. The <noscript> fallback is
 * styled to match so the page reads the same either way.
 */
.eml {
  font-family: var(--font-mono);
  overflow-wrap: anywhere;
}

/*
 * Spacing utilities, replacing inline style attributes.
 *
 * Those attributes never took effect in production: the Content-Security-
 * Policy allows style-src 'self' and no 'unsafe-inline', which blocks a style
 * attribute as surely as it blocks a <style> block. The layout only looked
 * correct in review, where the policy is easy to overlook.
 */
.flush-top {
  margin-top: 0;
}

.flush-bottom {
  margin-bottom: 0;
}

.gap-top {
  margin-top: var(--s6);
}

.gap-top-sm {
  margin-top: var(--s5);
}

/* 08b-crt.css */
/*
 * CRT overlay, and the guards on it.
 *
 * Loaded last on purpose: this is an overlay that has to sit above everything,
 * and the accessibility guards below have to be able to switch off glow that
 * earlier files set. It defines no utility of its own, so nothing here competes
 * with 08-utilities.css.
 */

/*
 * Film grain. The reference site plays a noise video for this; ours is an SVG
 * feTurbulence as a data URI — no request, no decoder, and data: is already
 * allowed by the CSP's img-src, so the policy needs no exception.
 *
 * Very faint. This lies over every paragraph on the site, not over a hero.
 */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.028;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/*
 * A faint vignette, which is what a curved tube does to its own corners. Kept
 * well below the point where it darkens anything being read.
 */
.shell__pane {
  background-image: var(--dots),
    radial-gradient(
      ellipse at center,
      transparent 55%,
      rgba(0, 0, 0, 0.22) 100%
    );
  background-size: var(--dots-size), cover;
  background-attachment: scroll, fixed;
}

/* ── Guards ────────────────────────────────────────────────────────────── */

/*
 * Someone who asked their system for sharper text has asked for the exact
 * opposite of a phosphor halo. Drop all of it, and the grain with it.
 */
@media (prefers-contrast: more) {
  *,
  *::before,
  *::after {
    text-shadow: none !important;
  }

  body::after {
    display: none;
  }

  .shell__pane {
    background-image: none;
  }
}

/*
 * Forced-colours mode replaces the palette outright; an overlay and a glow are
 * noise on top of a scheme the user chose deliberately.
 */
@media (forced-colors: active) {
  body::after {
    display: none;
  }

  *,
  *::before,
  *::after {
    text-shadow: none !important;
  }
}

