/**
 * DONAT — the public pages.
 *
 * ## The frame
 *
 * The top bar and the footer are both plum, always, on every page. That is a deliberate frame:
 * the site reads as a paper document held inside the club's own dark chrome, and it removes a
 * whole class of bug the previous stylesheet had — a bar that was transparent over a dark hero
 * and therefore invisible on every page that did not have one. The bar sits on `--ink-800`, the
 * hero sits on `--ink-800`, so on the landing page the seam between them does not exist.
 *
 * ## The rhythm
 *
 * Vertical space between sections comes from `.quiet-zone` in base.css and from nowhere else. No
 * rule in this file sets `padding-block` on a section. The old set had `.section` and `.cta`
 * both claiming it, so two classes on one element silently doubled or cancelled the gap
 * depending on which file happened to load last; the layer order plus a single source of rhythm
 * is what makes that impossible now.
 *
 * ## The hero
 *
 * The band is the club's own footage at full width with the copy over it, and the whole of the
 * design's boldness is spent there. Everything below is deliberately quiet: hairlines, one accent,
 * and the QR language reduced to a field you half-see and a finder mark at each section head.
 *
 * It began as a donut — the footage masked into an annulus with a real hole at the centre, which
 * is both the painted logo and the geometry the officer QR maker punches for its mark. The idea
 * was better than the result. On screen it was a circle with a hole in it, the club was a 30rem
 * disc in one corner, and two thirds of the frame was plum. The note that killed it was the right
 * one: the video should be wider. It is four times the area now.
 */

@layer site {
  /* ==========================================================================================
     Top bar
     ========================================================================================== */

  .topbar {
    position: sticky;
    top: 0;
    z-index: var(--z-topbar);
    /* `--band`, not the `--ink-800` literal this was: the bar is dark on both grounds, but in
       the dark scheme it lifts to `--ink-700` so that it stays a frame around the document
       rather than dissolving into a page that is now darker than it. Everything inside it is
       still written as plum-context literals, and correctly so — they are light-on-dark
       treatments that do not care how dark the ground is. */
    background: var(--band);
    transition: box-shadow var(--dur-2) var(--ease);
  }

  .topbar.is-scrolled {
    box-shadow: 0 1px 0 rgb(255 255 255 / 0.1), 0 12px 32px rgb(16 11 22 / 0.36);
  }

  .topbar__inner {
    display: flex;
    align-items: center;
    gap: var(--s5);
    min-height: var(--topbar-h);
  }

  .brand {
    display: flex;
    align-items: center;
    gap: var(--s3);
    margin-right: auto;
    color: #f7f2f8;
  }

  .brand__mark {
    flex: 0 0 auto;
    width: 2.25rem;
    height: 2.25rem;
  }

  .brand__word {
    font-family: var(--font-display);
    font-size: var(--t-lg);
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1;
  }

  .brand__tag {
    padding-left: var(--s3);
    border-left: 1px solid rgb(255 255 255 / 0.22);
    color: #c3b6cd;
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  .topnav {
    display: none;
  }

  .topnav__list {
    display: flex;
    align-items: center;
    gap: var(--s1);
  }

  .topnav__link {
    display: flex;
    align-items: center;
    min-height: 2.5rem;
    padding-inline: var(--s3);
    border-radius: var(--r2);
    color: #c3b6cd;
    font-size: var(--t-base);
    font-weight: 500;
    transition:
      color var(--dur-1) var(--ease),
      background-color var(--dur-1) var(--ease);
  }

  .topnav__link:hover {
    background: rgb(255 255 255 / 0.07);
    color: #f7f2f8;
  }

  /* The current page is marked with `aria-current`, not a class. One attribute carries both the
     announcement and the styling, so the two cannot disagree. */
  .topnav__link[aria-current='page'] {
    color: #f7f2f8;
  }

  .topnav__link[aria-current='page']::after {
    width: var(--module);
    height: var(--module);
    margin-left: var(--s2);
    border-radius: 1px;
    background: var(--glaze);
    content: '';
  }

  .topbar__end {
    display: flex;
    align-items: center;
    gap: var(--s2);
  }

  /* The bar inherits the paper context from :root, so buttons inside it need the plum one. */
  .topbar .btn--quiet {
    border-color: rgb(255 255 255 / 0.24);
    color: #f7f2f8;
  }

  .topbar .btn--quiet:hover {
    border-color: rgb(255 255 255 / 0.5);
    background: rgb(255 255 255 / 0.08);
  }

  .topbar .btn--primary {
    background: var(--glaze);
    color: var(--ink-900);
  }

  .topbar .btn--primary:hover {
    background: #f7cbf9;
  }

  .topbar :focus-visible {
    outline-color: var(--glaze);
  }

  /* The theme control, in the plum context the bar paints by hand. ui.css writes it against the
     semantic tokens, which are the paper ones here because this bar deliberately does not carry
     `.on-ink` — the same two lines `.menu-btn` and `.topnav__link` need, for the same reason. */
  .topbar .theme-toggle {
    color: #c3b6cd;
  }

  .topbar .theme-toggle:hover {
    background: rgb(255 255 255 / 0.08);
    color: #f7f2f8;
  }

  .topbar .pwa-install-btn {
    color: #c3b6cd;
  }

  .topbar .pwa-install-btn:hover {
    background: rgb(255 255 255 / 0.08);
    color: #f7f2f8;
  }

  .topbar__signin {
    display: none;
  }

  /* ---- The menu button: three bars that become a cross ------------------------------------ */

  .menu-btn {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    border-radius: var(--r2);
    color: #f7f2f8;
  }

  .menu-btn:hover {
    background: rgb(255 255 255 / 0.08);
  }

  .menu-btn__lines,
  .menu-btn__lines::before,
  .menu-btn__lines::after {
    display: block;
    width: 1.25rem;
    height: 2px;
    border-radius: 2px;
    background: currentcolor;
    transition: transform var(--dur-2) var(--ease), opacity var(--dur-1) var(--ease);
  }

  .menu-btn__lines {
    position: relative;
  }

  .menu-btn__lines::before {
    position: absolute;
    top: -6px;
    content: '';
  }

  .menu-btn__lines::after {
    position: absolute;
    top: 6px;
    content: '';
  }

  .menu-btn[aria-expanded='true'] .menu-btn__lines {
    opacity: 0;
  }

  .menu-btn[aria-expanded='true'] .menu-btn__lines::before {
    opacity: 1;
    transform: translateY(6px) rotate(45deg);
  }

  .menu-btn[aria-expanded='true'] .menu-btn__lines::after {
    opacity: 1;
    transform: translateY(-6px) rotate(-45deg);
  }

  @media (min-width: 900px) {
    .topnav,
    .topbar__signin {
      display: flex;
    }

    .menu-btn {
      display: none;
    }
  }

  /* ==========================================================================================
     The slide-over menu
     ========================================================================================== */

  /**
   * The sheet sits *under* the top bar rather than over it.
   *
   * There is no close control inside the panel: the menu button becomes a cross and that is the
   * only way out. At `--z-sheet` the panel would cover the button that opened it, leaving Escape
   * as the only route on a device with no Escape key. So the panel and its scrim sit one and two
   * below `--z-topbar` and the bar stays lit and tappable above them.
   *
   * Closed, it is translated off-screen *and* `visibility: hidden`, because a panel that is only
   * moved is still in the tab order — a keyboard user tabs off the bar into four links they
   * cannot see. Hiding is delayed by the slide duration so it does not vanish mid-transition.
   */
  .sheet {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: calc(var(--z-topbar) - 1);
    width: min(88vw, 22rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    visibility: hidden;
    background: var(--bg);
    box-shadow:
      -1px 0 0 var(--line),
      -24px 0 56px rgb(16 11 22 / 0.5);
    transform: translateX(100%);
    transition:
      transform var(--dur-3) var(--ease-out),
      visibility 0s linear var(--dur-3);
  }

  .sheet.is-open {
    visibility: visible;
    transform: none;
    transition:
      transform var(--dur-3) var(--ease-out),
      visibility 0s;
  }

  /* main.js drops the `hidden` attribute before it adds `.is-open`, so the wash has a frame to
     fade in from rather than appearing at full strength. */
  .sheet-scrim {
    position: fixed;
    inset: 0;
    z-index: calc(var(--z-topbar) - 2);
    background: var(--scrim);
    opacity: 0;
    transition: opacity var(--dur-2) var(--ease);
  }

  .sheet-scrim.is-open {
    opacity: 1;
  }

  /* The top padding is the bar the panel slides under; the bottom one is the home indicator. */
  .sheet__inner {
    display: flex;
    flex-direction: column;
    gap: var(--s6);
    min-height: 100%;
    padding: calc(var(--topbar-h) + var(--s5)) var(--s5)
      calc(var(--s6) + env(safe-area-inset-bottom, 0px));
  }

  .sheet__list {
    display: flex;
    flex-direction: column;
  }

  .sheet__link {
    display: flex;
    align-items: center;
    min-height: 3.25rem;
    border-bottom: 1px solid var(--line-quiet);
    color: var(--fg-muted);
    font-family: var(--font-display);
    font-size: var(--t-xl);
    font-weight: 600;
    letter-spacing: var(--track-tight);
  }

  .sheet__link:hover {
    color: var(--fg);
  }

  /* Marked the same way as the top bar's current page: one attribute carries the announcement
     and the module, so the two cannot disagree. */
  .sheet__link[aria-current='page'] {
    color: var(--fg);
  }

  .sheet__link[aria-current='page']::after {
    width: var(--module);
    height: var(--module);
    margin-left: var(--s3);
    border-radius: 1px;
    background: var(--accent);
    content: '';
  }

  .sheet__actions {
    display: grid;
    gap: var(--s3);
  }

  /* Pinned to the foot of the panel however short the link list is. */
  .sheet__foot {
    margin-top: auto;
    color: var(--fg-faint);
    font-family: var(--font-mono);
    font-size: var(--t-xs);
    line-height: 1.5;
  }

  @media (min-width: 900px) {
    .sheet,
    .sheet-scrim {
      display: none;
    }
  }

  /* ==========================================================================================
     The hero
     ========================================================================================== */

  /**
   * The footage is the hero.
   *
   * It was masked into a donut — the club's own logo geometry, and the same shape the officer QR
   * maker punches out for its mark. On screen it read as a circle with a hole in it and nothing
   * more: the club was a 30rem disc in one corner and the other two thirds of the frame were
   * plum. This is the same file at roughly four times the area, with the copy over it, which is
   * the version where you can actually see who the club is.
   *
   * The QR language stays, in the two places it earns: `.hero__field` over the whole band, and
   * the `.finder` marks that head every section below.
   */
  .hero {
    position: relative;
    display: grid;
    align-content: center;
    min-height: min(calc(100svh - var(--topbar-h)), 44rem);
    overflow: hidden;
    background: var(--ink-900);
  }

  .hero__stage {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }

  .hero__media {
    width: 100%;
    height: 100%;
    /* The poster is repeated as a background because the `poster` attribute stops painting the
       moment a `src` decodes a frame. See the `play()` rejection handler in js/main.js — every
       other way this can fail lands on the still instead of on flat ink. */
    background-color: var(--ink-900);
    background-image: url('../assets/images/hero-poster.jpg');
    background-position: center 30%;
    background-size: cover;
    object-fit: cover;
    object-position: center 30%;
  }

  /**
   * The scrim is what makes text on footage legible, and it is a gradient rather than a flat wash
   * so the far side of the frame stays close to the footage's own contrast.
   *
   * Measured against the worst case, which is a white frame behind the copy: at the 0.82 stop the
   * blend is #3b3740, and `--fg` on it is 10.6:1 while `--fg-muted` is 6.0:1. Both hold whatever
   * the video is doing, which is the only way to use type over moving pictures honestly.
   *
   * Vertical on a phone, because the copy sits over the whole width there; horizontal from 900px,
   * where it sits in the left half and the right half can stay open.
   */
  .hero__scrim {
    position: absolute;
    inset: 0;
    background-image:
      linear-gradient(
        to top,
        var(--ink-900) 0%,
        rgb(16 11 22 / 0.88) 42%,
        rgb(16 11 22 / 0.6) 78%,
        rgb(16 11 22 / 0.45) 100%
      );
  }

  @media (min-width: 900px) {
    .hero__scrim {
      background-image:
        linear-gradient(
          to top,
          var(--ink-900) 0%,
          rgb(16 11 22 / 0.35) 34%,
          transparent 70%
        ),
        linear-gradient(
          to right,
          var(--ink-900) 0%,
          rgb(16 11 22 / 0.82) 34%,
          rgb(16 11 22 / 0.4) 62%,
          rgb(16 11 22 / 0.16) 100%
        );
    }
  }

  /**
   * The QR field.
   *
   * This is the symbol the rest of the design is derived from, so it is present and it is almost
   * invisible: two hairlines on a four-module pitch, masked away toward the bottom right. If it
   * reads as a grid at a glance it is too strong — the point is that you notice the page is built
   * on something before you notice what.
   */
  .hero__field,
  .page-head__field {
    --field-line: rgb(255 255 255 / 0.05);
    position: absolute;
    inset: 0;
    background-image:
      linear-gradient(to right, var(--field-line) 0 1px, transparent 1px),
      linear-gradient(to bottom, var(--field-line) 0 1px, transparent 1px);
    background-size: calc(var(--module) * 4) calc(var(--module) * 4);
    /* Three stops rather than two. With a single hard falloff the grid stopped on a visible
       straight line partway across the hero and read as a rendering artifact instead of a
       texture; the middle stop is what turns the edge into a fade. */
    -webkit-mask-image: radial-gradient(
      130% 120% at 0% 0%,
      #000 0%,
      rgb(0 0 0 / 0.45) 38%,
      transparent 82%
    );
    mask-image: radial-gradient(
      130% 120% at 0% 0%,
      #000 0%,
      rgb(0 0 0 / 0.45) 38%,
      transparent 82%
    );
    pointer-events: none;
  }

  /**
   * The hero is the one band with no `.quiet-zone`: its height is a viewport measure, not a step
   * in the rhythm. The padding that keeps a tall hero off the top bar therefore lives on the inner
   * wrapper, where a second class cannot add to it.
   *
   * `min-width: 0` is what stops a 152px headline forcing the grid wider than the viewport — a
   * grid item's automatic minimum is its longest word.
   */
  .hero__inner {
    position: relative;
    display: grid;
    align-items: center;
    padding-block: clamp(var(--s7), 7vw, var(--s9));
  }

  .hero__inner > * {
    min-width: 0;
  }

  /* A measure, not a column. The copy stops well short of the frame's right edge so the footage
     has somewhere to be, and so the headline is never the widest thing on the page. */
  .hero__copy {
    max-width: 34rem;
  }

  @media (min-width: 900px) {
    .hero__copy {
      max-width: 40rem;
    }
  }

  /* No gradient fill, no stroke, no shadow. The word is set as tight as the face will take and
     the footage behind it carries the rest — two loud things would cancel. */
  .hero__title {
    font-size: var(--t-hero);
    font-weight: 800;
    line-height: 0.84;
    letter-spacing: -0.05em;
  }

  .hero__expand {
    margin-top: var(--s4);
    color: var(--fg-muted);
    font-size: var(--t-lg);
    line-height: var(--lh-normal);
  }

  .hero__expand span {
    color: var(--fg);
  }

  .hero__tagline {
    margin-top: var(--s5);
    font-family: var(--font-display);
    font-size: var(--t-xl);
    font-weight: 600;
    line-height: var(--lh-snug);
    letter-spacing: var(--track-tight);
  }

  /**
   * The rotating word.
   *
   * `min-width` in `ch` because the candidate words differ in length: the line must not reflow as
   * the word swaps. The keyline is `text-decoration` and not a `border-bottom` for the same
   * reason in reverse — a border spans the reserved 8ch and leaves a rule hanging past the end of
   * a shorter word, while an underline hugs the glyphs it belongs to. The movement is in
   * motion.css. `.dynamic-word` is the same element after main.js rewrites its class list, so
   * both names carry the same treatment and the word cannot lose its colour mid-rotation.
   */
  .rotor,
  .dynamic-word {
    display: inline-block;
    min-width: 8ch;
    color: var(--accent);
    text-decoration: underline;
    text-decoration-color: var(--accent-line);
    text-decoration-skip-ink: none;
    text-decoration-thickness: 2px;
    text-underline-offset: 0.22em;
  }

  .hero__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s3);
    margin-top: clamp(var(--s5), 4vw, var(--s6));
  }

  /* Stacked and full width first: two 46px buttons do not fit across 320px without one of the
     labels wrapping, and a wrapped label makes the pair look like an error. */
  .hero__actions > * {
    flex: 1 1 100%;
  }

  @media (min-width: 480px) {
    .hero__actions > * {
      flex: 0 1 auto;
    }
  }

  /**
   * Three equal columns on a phone, one row from 480px.
   *
   * Wrapping was leaving "Partner orgs" alone on a second row, offset from the two above it and
   * reading as an orphan rather than as the third of three. A grid keeps the three pairs in one
   * band at every width; it is a stat row, and a stat row that wraps stops being one.
   */
  .hero__facts {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--s4) var(--s3);
    margin-top: clamp(var(--s6), 5vw, var(--s7));
  }

  /* The verticals appear only once the row is wide enough to hold all three pairs comfortably.
     Below that the columns are narrow and a rule between them crowds labels that are already
     wrapping. The gap carries the separation instead. */
  @media (min-width: 480px) {
    .hero__facts {
      grid-template-columns: repeat(3, max-content);
      gap: var(--s4) 0;
    }

    .hero__facts > * {
      padding-inline: var(--s5);
    }

    .hero__facts > * + * {
      border-left: 1px solid var(--line);
    }

    .hero__facts > :first-child {
      padding-left: 0;
    }
  }

  .hero__facts dt {
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: 500;
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  /* Tabular figures: these are numbers a reader compares, and proportional digits make 2016 and
     2025 different widths. */
  .hero__facts dd {
    margin-top: var(--s2);
    font-family: var(--font-display);
    font-size: var(--t-xl);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1;
    letter-spacing: var(--track-tight);
  }

  /**
   * The play control.
   *
   * Pinned to the bottom-right corner of the band rather than floated over the copy: on a phone the
   * copy fills the width, and a control that lands on top of a sentence is a control somebody taps
   * by accident. The safe-area inset is there because on an installed PWA in landscape this corner
   * is where the home indicator sits.
   *
   * The button ships `hidden` and `main.js` reveals it only once the footage reports itself
   * playable, so it never appears on a device where the video was skipped.
   */
  .hero__toggle {
    position: absolute;
    right: calc(var(--gutter) + env(safe-area-inset-right, 0px));
    bottom: calc(var(--s5) + env(safe-area-inset-bottom, 0px));
    z-index: var(--z-raise);
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    border: 1px solid rgb(255 255 255 / 0.22);
    border-radius: var(--r-pill);
    background: rgb(16 11 22 / 0.55);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    color: #f7f2f8;
  }

  .hero__toggle:hover {
    border-color: rgb(255 255 255 / 0.5);
    background: rgb(16 11 22 / 0.75);
  }

  .hero__icon {
    width: 1.125rem;
    height: 1.125rem;
  }

  .hero__icon--play {
    display: none;
  }

  /* main.js sets `.is-paused` from the video's own `paused` property rather than from the click,
     so the icon cannot end up claiming the opposite of what the media is doing. */
  .hero__toggle.is-paused .hero__icon--pause {
    display: none;
  }

  .hero__toggle.is-paused .hero__icon--play {
    display: block;
  }


  /* ==========================================================================================
     Section heads
     ========================================================================================== */

  .section-head {
    margin-bottom: clamp(var(--s6), 4vw, var(--s8));
  }

  /* The finder mark and the eyebrow are one unit: the mark says "start reading here" and the
     eyebrow says what kind of thing follows. Neither works alone. */
  .section-mark {
    display: flex;
    align-items: center;
    gap: var(--s3);
    margin-bottom: var(--s4);
  }

  .section-head .lede {
    margin-top: var(--s4);
  }

  .section-head--center {
    margin-inline: auto;
    max-width: 44rem;
    text-align: center;
  }

  .section-head--center .section-mark {
    justify-content: center;
  }

  .section-head--center .lede {
    margin-inline: auto;
  }

  /* ==========================================================================================
     Editorial band
     ========================================================================================== */

  .editorial__grid {
    display: grid;
    gap: clamp(var(--s6), 5vw, var(--s8));
  }

  @media (min-width: 900px) {
    .editorial__grid {
      grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
      align-items: center;
    }
  }

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

  .editorial__copy .btn-link {
    margin-top: var(--s5);
  }

  /* The offset behind the frame is one module, in the hairline colour the module rules use: the
     photograph is pinned to the same grid as everything else. A second element would be easier to
     nudge and one more thing to keep in step, so it is a shadow. */
  .editorial__media img {
    width: 100%;
    border: 1px solid var(--line);
    border-radius: var(--r5);
    box-shadow: var(--module) var(--module) 0 var(--accent-line);
  }

  .editorial__caption {
    margin-top: var(--s5);
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--t-xs);
    line-height: 1.5;
  }

  /* ==========================================================================================
     The three branches
     ========================================================================================== */

  /* `.branches-grid` is the name `dash/overview.js` builds for its shortcut tiles. Same look, one
     rule, so the dashboard reads as the same product as the public page. */
  .branches__grid,
  .branches-grid {
    display: grid;
    gap: var(--s5);
  }

  @media (min-width: 700px) {
    .branches__grid,
    .branches-grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  @media (min-width: 1000px) {
    .branches__grid,
    .branches-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }

  /**
   * `--branch` is the card's whole identity: the top edge and the finder mark both read it, and
   * the finder is the only place in the design a sprinkle appears at full strength. Nothing else
   * is tinted, because three tinted cards read as three warnings.
   *
   * `text-align` and `isolation` are for `.branch-card--action`, the dashboard's copy of this
   * card: it is a <button>, so it would centre its own text, and it needs its art painted behind
   * the words rather than over them. The variant needs no rule of its own.
   */
  .branch-card {
    --branch: var(--accent);
    position: relative;
    isolation: isolate;
    overflow: hidden;
    padding: clamp(var(--s5), 3vw, var(--s6));
    border: 1px solid var(--line);
    border-top: 3px solid var(--branch);
    border-radius: var(--r4);
    background: var(--bg-raise);
    text-align: left;
  }

  .branch-card--munchkin {
    --branch: var(--munchkin);
  }

  .branch-card--prober {
    --branch: var(--prober);
  }

  .branch-card--cipher {
    --branch: var(--cipher);
  }

  .branch-card .finder {
    color: var(--branch);
  }

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

  .branch-title {
    margin-top: var(--s4);
    font-size: var(--t-xl);
    font-weight: 700;
  }

  .branch-card__text {
    margin-top: var(--s3);
    color: var(--fg-muted);
    font-size: var(--t-base);
    line-height: 1.55;
  }

  /* Decorative, so it is allowed to be cropped by the card's own corner. Behind the text, so a
     description that runs to four lines never sits on top of it. */
  /**
   * `.branch-card__art` used to be here.
   *
   * The three branch PNGs are photographs, not marks, and at the 14% opacity that kept them from
   * fighting the body copy they read as a smudge in the corner of the card rather than as
   * anything deliberate — an accident, not a decision. The cards carry the branch through the
   * finder mark's colour and the top edge, which is enough. Removing them also drops three image
   * requests from the landing page, which matters more than the decoration did for the audience
   * this design keeps optimising for: a mid-range Android on mobile data.
   */

  /* ==========================================================================================
     What the portal does
     ========================================================================================== */

  .pitch__grid {
    display: grid;
    gap: var(--s7);
  }

  @media (min-width: 900px) {
    .pitch__grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: var(--s6);
    }
  }

  /* A grid item's automatic minimum is its longest word, and `minmax(0, 1fr)` only frees the
     track: without this a URL in the copy would push the item out of its column. */
  .pitch__item {
    min-width: 0;
  }

  /* A filled module, not an 01. These are three things the portal does, not three steps, and a
     number would tell a member to do them in an order that does not exist. */
  .pitch__index {
    display: block;
    width: calc(var(--module) * 2);
    height: calc(var(--module) * 2);
    border-radius: 1px;
    background: var(--accent);
  }

  .pitch__title {
    margin-top: var(--s4);
    font-size: var(--t-lg);
  }

  .pitch__text {
    margin-top: var(--s3);
    color: var(--fg-muted);
    font-size: var(--t-base);
    line-height: 1.55;
  }

  .pitch__cta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s4);
    margin-top: clamp(var(--s6), 5vw, var(--s8));
  }

  /* ==========================================================================================
     Partners
     ========================================================================================== */

  /* Quiet on purpose: these are organisations the club works with, not sponsors who paid for
     placement, so the row sits under the content rather than competing with it. */
  .partners {
    text-align: center;
  }

  .partners__label {
    justify-content: center;
  }

  .partners__list {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--s5) var(--s6);
    margin-top: var(--s6);
  }

  .partners__item {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s3);
    width: 8.5rem;
    color: var(--fg-muted);
    font-size: var(--t-sm);
    line-height: 1.4;
  }

  .partners__item img {
    width: 3.5rem;
    height: 3.5rem;
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    object-fit: cover;
  }

  /* ==========================================================================================
     Page head — about, join
     ========================================================================================== */

  .page-head {
    position: relative;
    overflow: hidden;
  }

  /* Half the hero's strength. This band is a third of the height, and the same alpha over a
     shorter box reads twice as busy. */
  .page-head__field {
    --field-line: rgb(255 255 255 / 0.022);
  }

  /* Rhythm on the inner wrapper for the same reason as the hero: this header is not a section in
     the stack, and putting the padding here means `.quiet-zone` can never add a second helping. */
  .page-head__inner {
    position: relative;
    padding-block: clamp(var(--s7), 8vw, var(--s9));
  }

  .page-head__inner .lede {
    margin-top: var(--s4);
  }

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

  /* ==========================================================================================
     Vision and mission
     ========================================================================================== */

  .values__grid {
    display: grid;
    gap: var(--s5);
  }

  @media (min-width: 900px) {
    .values__grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  /* A keyline on the leading edge in a branch colour, and no tint anywhere: the two blocks say
     different things, they are not two states. */
  .value-block {
    padding: clamp(var(--s5), 3vw, var(--s6));
    border: 1px solid var(--line);
    border-left: 3px solid var(--value-tone, var(--accent));
    border-radius: var(--r4);
    background: var(--bg-raise);
  }

  .value-block--vision {
    --value-tone: var(--munchkin);
  }

  .value-block--mission {
    --value-tone: var(--cipher);
  }

  .value-block h3 {
    margin-top: var(--s3);
  }

  .value-block__text {
    margin-top: var(--s3);
    color: var(--fg-muted);
    font-size: var(--t-base);
    line-height: 1.55;
  }
  /* ==========================================================================================
     History
     ========================================================================================== */

  /**
   * The year picker scrolls.
   *
   * The previous stylesheet set `display: none` on this row below 768px, so a phone showed the
   * 2016 entry and offered no way to reach another year — nine years of the club's history were
   * desktop-only. Nine 44px buttons do not fit across 320px and never will, so the row scrolls
   * and snaps instead of disappearing. The scrollbar is hidden because the half-visible button at
   * the edge already says there is more to the right.
   */
  .timeline__years {
    display: flex;
    gap: var(--s2);
    margin-bottom: var(--s6);
    padding-bottom: var(--s2);
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
  }

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

  .timeline-node {
    display: inline-flex;
    flex: 0 0 auto;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
    padding-inline: var(--s4);
    border: 1px solid var(--line);
    border-radius: var(--r-pill);
    background: var(--bg-raise);
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--t-sm);
    font-variant-numeric: tabular-nums;
    scroll-snap-align: center;
  }

  .timeline-node:hover {
    border-color: var(--line-strong);
    color: var(--fg);
  }

  /* main.js keeps `.active` and `aria-selected` in step on the same click, so the filled year is
     also the announced one. */
  .timeline-node.active {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--accent-fg);
  }

  .timeline-label {
    line-height: 1;
  }

  .timeline-display {
    padding: clamp(var(--s5), 4vw, var(--s7));
    border: 1px solid var(--line);
    border-radius: var(--r4);
    background: var(--bg-raise);
  }

  .timeline-title {
    margin-top: var(--s3);
    font-size: var(--t-2xl);
  }

  .timeline-desc {
    margin-top: var(--s4);
    max-width: var(--maxw-prose);
    color: var(--fg-muted);
    font-size: var(--t-md);
    line-height: var(--lh-normal);
  }

  /* ==========================================================================================
     Two ways in
     ========================================================================================== */

  .choice-grid {
    display: grid;
    gap: var(--s5);
  }

  @media (min-width: 800px) {
    .choice-grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  .choice-card {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    padding: clamp(var(--s5), 4vw, var(--s7));
    border: 1px solid var(--line);
    border-top: 3px solid var(--choice-tone, var(--accent));
    border-radius: var(--r4);
    background: var(--bg-raise);
  }

  .choice-card--member {
    --choice-tone: var(--accent);
  }

  /* The crust, not a second glaze: officer registration is the same act on a different track, and
     two identical cards would make the choice look arbitrary. */
  .choice-card--officer {
    --choice-tone: var(--crust);
  }

  .choice-card__text {
    color: var(--fg-muted);
    font-size: var(--t-base);
    line-height: 1.55;
  }

  /* Pinned to the foot so the two buttons line up even when one description runs a line longer. */
  .choice-card > .btn,
  .choice-card > .btn-primary,
  .choice-card > .btn-secondary {
    margin-top: auto;
  }

  /* ==========================================================================================
     How to apply
     ========================================================================================== */

  /**
   * The one place in this design where numbers are correct.
   *
   * Every section head gets a finder mark instead of an index, because those sections are not
   * read in order. Applying is: demographics, then the officer questions, then consent — you
   * cannot do the third first. So these are numbered, and the connector says it a second time for
   * anyone skimming.
   */
  .steps {
    --step-num: 2.25rem;
    display: grid;
    gap: var(--s6);
    max-width: 46rem;
  }

  .steps__item {
    position: relative;
    display: grid;
    grid-template-columns: var(--step-num) minmax(0, 1fr);
    gap: var(--s2) var(--s4);
  }

  /* Stops short of both circles rather than running behind them, and reaches into the row gap to
     meet the next one. The last item has no line because there is nothing after it. */
  .steps__item:not(:last-child)::before {
    position: absolute;
    top: calc(var(--step-num) + var(--s2));
    bottom: calc(var(--s2) - var(--s6));
    left: calc(var(--step-num) / 2 - 0.5px);
    width: 1px;
    background: var(--line);
    content: '';
  }

  .steps__num {
    display: grid;
    grid-row: 1 / span 2;
    place-items: center;
    align-self: start;
    width: var(--step-num);
    height: var(--step-num);
    border-radius: var(--r-pill);
    background: var(--accent-wash);
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: var(--t-sm);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }

  .steps__title {
    font-size: var(--t-lg);
  }

  .steps__text {
    color: var(--fg-muted);
    font-size: var(--t-base);
    line-height: 1.55;
  }

  /* ==========================================================================================
     Status pages — roster, 404, offline
     ========================================================================================== */

  /**
   * `<main class="status-page">` owns its own vertical space.
   *
   * It is the only main that does, and it can: there is one panel on the page, centred in the
   * viewport rather than sitting in a stack of sections, so there is no `.quiet-zone` here to
   * double with. `worker/shell.js` serves the offline copy of this page from the edge, which is
   * why the panel text is styled under both its own name and the legacy `.section-desc` — the
   * strings in the worker and in the older pages have not been renamed.
   */
  .status-page {
    display: grid;
    place-items: center;
    min-height: 60svh;
    padding: var(--quiet) var(--gutter);
    text-align: center;
  }

  .status-panel {
    max-width: 40rem;
  }

  .status-code {
    display: block;
    color: var(--accent);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: 500;
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  .status-panel h1 {
    margin-top: var(--s4);
  }

  .status-panel__text,
  .section-desc {
    margin-top: var(--s4);
    max-width: var(--maxw-prose);
    color: var(--fg-muted);
    font-size: var(--t-lg);
    line-height: var(--lh-normal);
  }

  /* Centred only inside the panel: `.section-desc` also appears in left-aligned copy on the older
     pages, and an auto margin there would pull those paragraphs off the text's left edge. */
  .status-panel .status-panel__text,
  .status-panel .section-desc {
    margin-inline: auto;
  }

  .status-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--s3);
    margin-top: var(--s6);
  }

  /* ==========================================================================================
     The registration form
     ========================================================================================== */

  /* Same story as `.status-page`: a <main> with one box in it, carrying the rhythm itself because
     there is no section stack here to inherit one from. It must not also carry `.quiet-zone`. */
  .form-page {
    padding-block: var(--quiet);
  }

  /**
   * The step tabs are a progress indicator.
   *
   * They are real buttons — recruit.js lets you step back through them — but they are drawn as
   * labels on a rail rather than as controls, because a member who sees three tab-shaped boxes
   * fills in the last one first and only then finds out the first was required. Equal columns
   * rather than a scrolling row, so all three are visible at 320px even when a label wraps, and
   * `align-items: end` keeps the wrapped one sitting on the rail with the others.
   */
  .form-step-tabs {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    align-items: end;
    gap: var(--s3);
    margin-bottom: var(--s6);
    border-bottom: 2px solid var(--line);
  }

  /* `--fg-muted` and not `--fg-faint`: an 11px label on the form's white ground has to clear
     4.5:1, and the faint token is a 2.9:1 colour meant for dark surfaces. */
  .form-step-tab {
    position: relative;
    padding-block: var(--s3);
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: 500;
    line-height: 1.4;
    letter-spacing: var(--track-label);
    text-align: left;
    text-transform: uppercase;
  }

  .form-step-tab.active {
    color: var(--fg);
  }

  /* The lit segment sits on the rail rather than above it, so the two read as one line with the
     current step burning in it. */
  .form-step-tab.active::after {
    position: absolute;
    right: 0;
    bottom: -2px;
    left: 0;
    height: 2px;
    background: var(--accent);
    content: '';
  }

  /* recruit.js switches panels with the `hidden` attribute and ui.css spaces every field inside,
     so the panel only has to make sure it does not add a margin of its own under the rail. */
  .form-step-panel > :first-child {
    margin-top: 0;
  }

  /**
   * The consent step.
   *
   * Three things stacked, in the order somebody actually needs them: what they are about to
   * send, what happens to it, and the box that says yes. The recap is not decoration — this is
   * the last screen before an application an administrator has to review by hand, and a
   * mistyped student number found here costs nothing to fix.
   *
   * The panel's own heading, small and quiet: the page already has an `h1`, and the rail above
   * already says which step this is, so a loud second title would be a third thing saying it.
   */
  .step-heading {
    margin-bottom: var(--s4);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: 500;
    letter-spacing: var(--track-label);
    line-height: 1.4;
    text-transform: uppercase;
    color: var(--fg-muted);
  }

  .constitution-panel .consent-row {
    margin-top: var(--s4);
  }

  /* A recessed block rather than another bordered card, so it sits *under* the consent callout
     in the reading order visually as well as in the markup. `auto 1fr` keeps the labels in one
     column without a fixed width that a longer word would break out of. */
  .review-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--s2) var(--s4);
    margin: 0 0 var(--s5);
    padding: var(--s4);
    border: 1px solid var(--line);
    border-radius: var(--r3);
    background: var(--bg-sunken);
  }

  .review-list dt {
    color: var(--fg-muted);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    letter-spacing: var(--track-label);
    line-height: 1.7;
    text-transform: uppercase;
  }

  /* `anywhere`, because the whole point of the recap is the address, and the address is forty
     characters that must not push the grid wider than the phone it is being read on. */
  .review-list dd {
    margin: 0;
    color: var(--fg);
    font-size: var(--t-base);
    line-height: var(--lh-normal);
    overflow-wrap: anywhere;
  }

  @media (max-width: 26.5em) {
    .review-list {
      grid-template-columns: 1fr;
      gap: var(--s1);
    }

    .review-list dd + dt {
      margin-top: var(--s3);
    }
  }

  /* The notice was `.field-hint` — muted, 13px, the least emphasised text on the page — which is
     an odd way to present the one paragraph the member is being asked to agree to. Same words,
     given a surface and an accent rule so they read as a statement rather than as fine print. */
  .consent-note {
    margin-bottom: var(--s5);
    padding: var(--s4) var(--s5);
    border: 1px solid var(--accent-line);
    border-left: 3px solid var(--accent);
    border-radius: var(--r2);
    background: var(--accent-wash);
  }

  .consent-note__title {
    margin-bottom: var(--s2);
    color: var(--fg);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: 500;
    letter-spacing: var(--track-label);
    line-height: 1.4;
    text-transform: uppercase;
  }

  .consent-note p {
    margin: 0;
    color: var(--fg);
    font-size: var(--t-sm);
    line-height: 1.55;
  }

  /* `.consent-row` and `.consent-row__text` used to live here. They moved to ui.css when the
     constitution reader started using them: that page loads portal.css rather than site.css, and a
     class defined only here renders completely unstyled in the portal half of the application while
     looking perfectly fine in this file. See the layer order at the top of base.css. */

  /**
   * Shown in place of the form when officer applications are closed.
   *
   * Styled as a card rather than as an error, because it is neither the visitor's mistake nor a
   * fault: the club is not recruiting officers this month. It carries the member path as its
   * one action, since that is the thing somebody who arrived here can still do.
   */
  .form-notice {
    padding: clamp(var(--s5), 4vw, var(--s7));
    border: 1px solid var(--line);
    border-radius: var(--r4);
    background: var(--bg-raise);
    box-shadow: var(--sh2);
    text-align: center;
  }

  .form-notice-title {
    margin-bottom: var(--s3);
    font-size: var(--t-lg);
    line-height: var(--lh-snug);
  }

  .form-notice p {
    margin: 0 auto var(--s5);
    max-width: 44ch;
    color: var(--fg-muted);
    line-height: var(--lh-prose);
  }

  .constitution-dialog {
    inset: 50% auto auto 50%;
    margin: 0;
    width: calc(100vw - 2rem);
    max-width: 58rem;
    height: min(90svh, 52rem);
    transform: translate(-50%, -50%);
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r4);
    background: var(--bg-raise);
    box-shadow: var(--sh3);
    color: var(--fg);
  }

  .constitution-dialog::backdrop {
    background: var(--scrim);
  }

  .constitution-dialog__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s4);
    padding: var(--s4) var(--s5);
    border-bottom: 1px solid var(--line);
  }

  .constitution-dialog__head h2 {
    margin-top: var(--s1);
    font-size: var(--t-xl);
  }

  .constitution-dialog__body {
    height: calc(100% - 5.5rem);
    overflow-y: auto;
    scrollbar-width: none;
  }

  .constitution-dialog__body::-webkit-scrollbar {
    display: none;
  }

  .constitution-dialog .consti-reader {
    max-height: none;
    margin: 0;
    overflow: visible;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .turnstile-dialog {
    inset: 50% auto auto 50%;
    margin: 0;
    width: min(calc(100vw - 2rem), 26rem);
    transform: translate(-50%, -50%);
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--r4);
    background: var(--bg-raise);
    box-shadow: var(--sh3);
    color: var(--fg);
  }

  .turnstile-dialog::backdrop {
    background: var(--scrim);
  }

  .turnstile-dialog__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s4) var(--s5);
    border-bottom: 1px solid var(--line);
  }

  .turnstile-dialog__head h2 {
    margin: 0;
    font-size: var(--t-lg);
  }

  .turnstile-dialog__body {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: var(--s5) var(--s4);
    min-height: 9rem;
    gap: var(--s3);
  }

  .turnstile-dialog__hint {
    margin: 0;
    font-size: var(--t-sm);
    color: var(--fg-muted);
    text-align: center;
  }

  /* ==========================================================================================
     Footer
     ========================================================================================== */

  /* The footer is plum on every page, which is the other half of the frame the top bar starts.
     Its padding lives here rather than on `.footer` itself, so the element that carries the
     ground and the element that carries the rhythm stay separate. */
  .footer__inner {
    display: grid;
    gap: var(--s7) var(--s6);
    padding-block: clamp(var(--s7), 6vw, var(--s8));
  }

  @media (min-width: 700px) {
    .footer__inner {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  /* `minmax(0, …)` on every track: an email address in the last column would otherwise refuse to
     shrink, push the brand column out and take the whole grid past the viewport. */
  @media (min-width: 900px) {
    .footer__inner {
      grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr));
    }
  }

  .footer__brand {
    display: grid;
    justify-items: start;
    gap: var(--s4);
  }

  .footer__blurb {
    max-width: 34rem;
    color: var(--fg-muted);
    font-size: var(--t-base);
    line-height: 1.55;
  }

  .footer__sprinkles {
    width: 7.5rem;
    height: auto;
  }

  .footer__col {
    display: grid;
    align-content: start;
    gap: var(--s3);
  }

  .footer__title {
    color: var(--fg-faint);
    font-family: var(--font-mono);
    font-size: var(--t-micro);
    font-weight: 500;
    letter-spacing: var(--track-label);
    text-transform: uppercase;
  }

  /* 44px rows rather than a tight list: this is the bottom of the page, where a thumb already is,
     and it is the last chance to reach the roster or the sign-in. */
  .footer__links a {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    color: var(--fg-muted);
    font-size: var(--t-base);
  }

  .footer__links a:hover {
    color: var(--fg);
    text-decoration: underline;
    text-decoration-color: var(--accent);
    text-decoration-thickness: 2px;
    text-underline-offset: 0.25em;
  }

  .footer__address {
    color: var(--fg-muted);
    font-size: var(--t-base);
    font-style: normal;
    line-height: var(--lh-normal);
  }

  /* The safe-area inset is the home indicator on an iPhone: the small print is the last thing on
     the page, and without it the copyright line sits under the bar the system draws. */
  .footer__base {
    display: grid;
    gap: var(--s2);
    padding-block: var(--s5) calc(var(--s5) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--line);
    color: var(--fg-faint);
    font-size: var(--t-sm);
    line-height: 1.5;
  }

  @media (min-width: 700px) {
    .footer__base {
      grid-template-columns: auto minmax(0, auto);
      justify-content: space-between;
      gap: var(--s5);
    }
  }
}
