/* ============================================================================
   Boston Polymaths — application surfaces
   Plan: D27 (brand-light), §12.2 (this file), §12.3 (canonical palette),
         Appendix E (WCAG 2.2 AA), §9.2 (byte budget), §9.4 (no infinite motion)

   Linked ONLY by /apply, /login, /members, /me, /review, /admin.
   The marketing pages keep main.css + custom.css; these pages must never link
   them (2,269 lines would exhaust the 60 KB budget on stylesheet alone).
   ========================================================================== */

/* --- font: one local face, no third-party origin (§9.2) ------------------- */
@font-face {
  font-family: 'Centrion';
  src: url('./Centrion Font Style/Centrion-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* --- tokens: canonical values from custom.css:74 (§12.3) ----------------- */
:root {
  --bg:            #000000;
  --bg-section:    #0a0a0a;
  --bg-raised:     #141414;
  --text:          #ffffff;
  --text-muted:    #b0b0b0;
  --rule:          #2a2a2a;

  --blue:          #2196f3;
  --purple:        #9c27b0;
  /* these two were referenced by dead tailwind keyframes and defined nowhere;
     defining them here removes the dangling reference (§12.3) */
  --accent-1:      #2196f3;
  --accent-2:      #9c27b0;

  --danger:        #ff6b6b;   /* 4.5:1 on --bg */
  --ok:            #4ade80;

  --display: 'Centrion', 'Source Sans Pro', Georgia, serif;
  --body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

  --measure: 34rem;         /* ~68 characters; essays stay readable */
  --gap: 1rem;
  --radius: 8px;            /* matches .cta-button's 8px (§12.3) */
  --tap: 44px;              /* Appendix E minimum target */
}

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.65;
  /* side gutter never collapses, at any width */
  padding: 0 max(1rem, env(safe-area-inset-left));
}

/* --- layout -------------------------------------------------------------- */
.wrap { max-width: var(--measure); margin: 0 auto; padding-block: 2.5rem 4rem; }
.wrap--wide { max-width: 60rem; }
.wrap--full { max-width: none; }

/* --- type ---------------------------------------------------------------- */
h1, h2, h3 { font-family: var(--display); font-weight: 400; line-height: 1.2; margin: 0 0 0.5rem; }
h1 { font-size: clamp(1.75rem, 6vw, 2.5rem); }
h2 { font-size: clamp(1.25rem, 4vw, 1.5rem); margin-top: 2.5rem; }
h3 { font-size: 1.125rem; margin-top: 1.75rem; }
p { margin: 0 0 1rem; }
a { color: var(--blue); text-underline-offset: 3px; }
a:hover { color: var(--text); }
.eyebrow {
  font-family: var(--display);
  font-size: 0.8125rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-muted); margin: 0 0 0.75rem;
}
.muted { color: var(--text-muted); }
.small { font-size: 0.875rem; }

/* the one brand flourish: a static gradient rule. No animation (§9.4). */
.rule {
  height: 2px; border: 0; margin: 1.5rem 0 2rem;
  background: linear-gradient(90deg, var(--blue), var(--purple) 60%, transparent);
}

/* --- focus: visible everywhere, never removed (Appendix E) --------------- */
:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: 2px;
}

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--bg-raised); color: var(--text); padding: 0.75rem 1rem; z-index: 10;
}
.skip:focus { left: 0; }

/* --- forms --------------------------------------------------------------- */
.field { margin: 0 0 1.75rem; }
.field > label { display: block; font-weight: 600; margin-bottom: 0.375rem; }
.field .hint {
  display: block; color: var(--text-muted); font-size: 0.9375rem;
  margin-bottom: 0.5rem; white-space: pre-line;
}
.req { color: var(--text-muted); font-weight: 400; }   /* text, not colour alone */

input[type="text"], input[type="email"], input[type="tel"],
input[type="password"], select, textarea {
  width: 100%;
  min-height: var(--tap);
  padding: 0.625rem 0.75rem;
  background: var(--bg-section);
  color: var(--text);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  font: inherit;
  font-size: 1rem;              /* never < 16px: smaller zooms the page on iOS */
}
/* Small to start with, growing with what is typed, scrolling inside past a limit (D107).
   `field-sizing: content` is the growth where a browser has it; public/scripts/autogrow.js sets
   the height where it has not; `max-height` is the limit for both, and `overflow-y: auto` the
   scroll bar that then appears INSIDE the box. */
textarea {
  min-height: 5.5rem; max-height: 24rem; overflow-y: auto;
  field-sizing: content; resize: vertical; line-height: 1.65;
}
input:hover, select:hover, textarea:hover { border-color: #3d3d3d; }
::placeholder { color: #6b6b6b; }

/* the OTP field: one input, numeric, autofillable (§12.8) */
.otp {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.5rem; letter-spacing: 0.4em; text-align: center;
}

/* validity is never signalled by colour alone — the message carries it */
[aria-invalid="true"] { border-color: var(--danger); }
.field-error { display: block; color: var(--danger); font-size: 0.9375rem; margin-top: 0.375rem; }

.counter { display: block; text-align: right; color: var(--text-muted); font-size: 0.8125rem; margin-top: 0.25rem; }
.counter[data-near="true"] { color: var(--danger); }

.error-summary {
  border: 1px solid var(--danger); border-radius: var(--radius);
  padding: 1rem 1.25rem; margin-bottom: 2rem;
}
.error-summary h2 { margin-top: 0; font-size: 1.125rem; }
.error-summary ul { margin: 0; padding-left: 1.25rem; }

fieldset { border: 0; padding: 0; margin: 0 0 1.75rem; }
legend { font-weight: 600; padding: 0; margin-bottom: 0.5rem; }
.choice { display: flex; align-items: center; gap: 0.625rem; min-height: var(--tap); }
.choice input { width: 1.125rem; height: 1.125rem; accent-color: var(--blue); flex: none; }

/* toggle state is carried by TEXT, not position alone (Appendix E) */
.toggle { display: flex; align-items: center; gap: 0.75em; min-height: max(var(--tap), 2.25em); }
.toggle-state { font-weight: 600; }
/* The box and its words, in `em` off the column (founder, 2026-09-21, D126: *"make the check
   marks for notifications text scale and size better. They appear too small at all resolutions
   from mobile to desktop"*). A native checkbox does NOT inherit the page's type — it keeps the
   browser's 13px control font — so `font-size: inherit` is what lets `1.375em` mean the column's:
   a 13px box becomes 23px on a phone, 29px at 1080p and 47px at 4K. The label is a step above
   the running text, because it is the whole of what the setting says. */
.toggle > input[type="checkbox"] {
  flex: none; font-size: inherit; width: 1.375em; height: 1.375em; margin: 0;
  accent-color: var(--blue); cursor: pointer;
}
.toggle > label { font-size: 1.0625em; line-height: 1.35; cursor: pointer; }

/* A tick that is not available yet (D125): the box and its words dimmed — the tip must not be, so
   the dimming is theirs and not the row's — and a tip that floats above on hover, in words. The
   pointer is on the ROW: a disabled control takes no pointer events of its own, and the row's
   `:hover` is what answers a pointer over either the box or its label. */
.toggle--locked { position: relative; cursor: not-allowed; }
.toggle--locked > input, .toggle--locked > label { opacity: 0.45; cursor: not-allowed; }
.toggle__tip {
  position: absolute; left: 0; bottom: calc(100% + 0.5em); z-index: 5;
  width: max-content; max-width: min(24em, 100%);
  padding: 0.75em 1em; border: 1px solid #3d3d3d; border-radius: var(--radius);
  background: var(--bg-raised); color: var(--text);
  font-size: 0.875em; line-height: 1.5; text-align: left;
  box-shadow: 0 0.75em 2em rgba(0, 0, 0, 0.7);
  opacity: 0; visibility: hidden; transform: translateY(0.25em); pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s;
}
.toggle--locked:hover > .toggle__tip { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
/* The same words once, under the two ticks: what `aria-describedby` names, and what a screen with
   no hover (a phone, a tablet) shows instead of a tip it could never open. Where there IS hover it
   is kept for a screen reader and taken off the screen. */
.group-mail-note { margin: 0 0 1.25em; }
@media (hover: hover) {
  .group-mail-note {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
}
@media (hover: none) { .toggle__tip { display: none; } }
@media (prefers-reduced-motion: reduce) { .toggle__tip { transition: none; transform: none; } }

/* --- buttons: the static variant of .cta-button (§12.3) ------------------ */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0.75rem 1.5rem;
  border: 0; border-radius: var(--radius);
  font: inherit; font-weight: 600; cursor: pointer; text-decoration: none;
  background: linear-gradient(90deg, var(--blue), var(--purple));  /* frozen, not animated */
  color: #fff;
}
.btn:hover { filter: brightness(1.12); }
.btn[disabled] { opacity: 0.55; cursor: not-allowed; filter: none; }
.btn--quiet { background: none; border: 1px solid var(--rule); color: var(--text); font-weight: 400; }
.btn--quiet:hover { border-color: var(--text-muted); filter: none; }
.btn--block { width: 100%; }

/* --- app header (§12.5 rung table) --------------------------------------- */
.appbar {
  display: flex; align-items: center; gap: 1.25rem; flex-wrap: wrap;
  max-width: 60rem; margin: 0 auto; padding: 1rem 0;
  border-bottom: 1px solid var(--rule);
  font-family: var(--display);
}
.appbar a { color: var(--text-muted); text-decoration: none; }
.appbar a:hover, .appbar a[aria-current="page"] { color: var(--text); }
.appbar .brand { color: var(--text); letter-spacing: 0.08em; }
.appbar .spacer { margin-left: auto; }

/* --- the wall (D41) -------------------------------------------------------
   Masonry with CSS columns: no dimensions needed, no script, tiles keep their own aspect ratio and
   the columns balance by height. The column width is proportional — 15vw between a 10rem floor
   and a 40rem ceiling — so the wall is the same picture on any desktop: six columns at 1440,
   1920 or 3840 wide, four on a tablet, two on a phone. Items fill down each column, which is the
   one thing this differs from a row-by-row grid in; for a wall of moments that order is fine. */
.wall { columns: clamp(10rem, 15vw, 40rem); column-gap: 0.5rem; }
.wall-item { break-inside: avoid; margin: 0 0 0.5rem; cursor: zoom-in; }
.wall-item a { display: block; }
.wall-item img, .wall-item video {
  display: block; width: 100%; height: auto;
  border-radius: 0.25rem; background: var(--bg-raised);
}
/* With public/scripts/wall.js: masonry. The script places each tile absolutely — left, top,
   width and height through the CSSOM from the server-read dimensions (data-w/data-h) — into the
   shortest column, and sets the wall's height to the tallest column. Every item keeps its own
   shape at the column's width, so a portrait stands tall and a landscape sits short. */
.wall.is-masonry { columns: auto; position: relative; }
.wall.is-masonry .wall-item { position: absolute; margin: 0; overflow: hidden; }
.wall.is-masonry .wall-item a { height: 100%; }
.wall.is-masonry .wall-item img, .wall.is-masonry .wall-item video { width: 100%; height: 100%; object-fit: cover; }

/* --- the viewer (D41, v2.42) -----------------------------------------------
   A native <dialog>: modal, focus-trapped, Escape closes it, and its ::backdrop is the dark
   ground. The stage letterboxes the item; the buttons sit at the edges and read as glyphs. */
dialog.lightbox {
  border: 0; padding: 0; margin: 0; background: transparent; color: var(--text);
  width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh;
  /* One size for the viewer's chrome — the three buttons, their glyphs and offsets — in viewport
     units, so it grows with the screen (founder, 2026-09-18: the arrows and the close mark "do not
     sufficiently scale/increase in size when screen size is increased"). 48px on a phone and a
     laptop, 58px at 1080p, 77px at 1440p, 112px at 4K. */
  --chrome: clamp(3rem, 3vw, 7rem);
}
dialog.lightbox::backdrop { background: rgba(0, 0, 0, 0.94); }
dialog.lightbox .lightbox-stage {
  position: absolute; inset: 0; margin: 0; display: grid; place-items: center;
  /* The strip the counter sits in, above and below the media, growing with the counter's type.
     The script sizes the media to fill what is left, scaling it UP as far as the screen allows
     (2026-09-18): without it a 1280×720 clip stayed 1280×720 on a 4K screen, a third of the
     width. The buttons overlay the media's edges, as every large viewer's do. */
  padding: clamp(2.5rem, 2.2vw, 5rem) 0; box-sizing: border-box;
}
dialog.lightbox .lightbox-stage img, dialog.lightbox .lightbox-stage video {
  max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain;
}
dialog.lightbox button {
  position: absolute; z-index: 1; border: 0; background: rgba(0, 0, 0, 0.45); color: #fff;
  font-weight: 300; font-size: calc(var(--chrome) * 0.67); line-height: 1; font-family: system-ui, sans-serif;
  width: var(--chrome); height: var(--chrome); border-radius: 50%;
  cursor: pointer; display: grid; place-items: center;
}
dialog.lightbox button:hover, dialog.lightbox button:focus-visible { background: rgba(255, 255, 255, 0.18); outline: 2px solid #fff; outline-offset: 2px; }
dialog.lightbox .lightbox-close { top: max(1rem, env(safe-area-inset-top), calc(var(--chrome) * 0.3)); right: max(1rem, env(safe-area-inset-right), calc(var(--chrome) * 0.3)); }
dialog.lightbox .lightbox-prev { left: max(0.5rem, env(safe-area-inset-left), calc(var(--chrome) * 0.15)); top: 50%; transform: translateY(-50%); }
dialog.lightbox .lightbox-next { right: max(0.5rem, env(safe-area-inset-right), calc(var(--chrome) * 0.15)); top: 50%; transform: translateY(-50%); }
dialog.lightbox .lightbox-count {
  position: absolute; left: 0; right: 0; bottom: max(0.75rem, env(safe-area-inset-bottom), calc(var(--chrome) * 0.25));
  margin: 0; text-align: center; font-size: clamp(0.875rem, 0.8vw, 1.75rem); color: rgba(255, 255, 255, 0.7);
  font-variant-numeric: tabular-nums; pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  dialog.lightbox[open] .lightbox-stage img, dialog.lightbox[open] .lightbox-stage video { animation: lightbox-in 160ms ease-out; }
}
@keyframes lightbox-in { from { opacity: 0; transform: scale(0.98); } }

/* --- members roster (D25, parked in D41) --------------------------------- */
.roster { list-style: none; margin: 0; padding: 0; }
.roster li {
  display: grid;
  grid-template-columns: 2.5rem 1fr auto;
  gap: 0 0.875rem; align-items: baseline;
  padding: 0.875rem 0; border-bottom: 1px solid var(--rule);
}
.roster .avatar {
  grid-row: span 2; width: 2.5rem; height: 2.5rem; border-radius: 50%;
  background: var(--bg-raised); color: var(--text-muted);
  display: grid; place-items: center; font-size: 0.875rem; font-family: var(--display);
}
.roster .name { font-family: var(--display); font-size: 1.125rem; }
.roster .year { color: var(--text-muted); font-size: 0.9375rem; font-variant-numeric: tabular-nums; }
.roster .links { grid-column: 2 / -1; font-size: 0.9375rem; color: var(--text-muted); }
.roster .links a { margin-right: 0.75rem; }

/* --- review: an application reads as a document, not a grid of divs
       (Appendix E — the anti-slop rule) ------------------------------------ */
.answer { margin: 0 0 2.5rem; }
.answer h3 { color: var(--text); }
.answer blockquote {
  margin: 0; padding-left: 1rem; border-left: 2px solid var(--rule);
  white-space: pre-wrap; line-height: 1.7;
}
mark { background: rgba(33, 150, 243, 0.28); color: inherit; padding: 0.05em 0; }

/* --- status / empty states (Appendix G) ---------------------------------- */
.notice { border: 1px solid var(--rule); border-radius: var(--radius); padding: 1rem 1.25rem; margin: 0 0 2rem; }
/* The last paragraph's own margin is not padding: without this every notice is 1rem taller
   at the bottom than at the top. */
.notice > :last-child { margin-bottom: 0; }
.notice--ok { border-color: var(--ok); }
.notice--bad { border-color: var(--danger); }
.empty { text-align: center; color: var(--text-muted); padding: 3rem 1rem; }
.empty h2 { color: var(--text); }

.badge {
  display: inline-block; padding: 0.125rem 0.5rem; border: 1px solid var(--rule);
  border-radius: 999px; font-size: 0.8125rem; color: var(--text-muted);
}

/* --- tables stay scrollable rather than making the page scroll ----------- */
.scroll-x { overflow-x: auto; }

/* --- splash: the brand loading screen, shared with the marketing pages -----
   Founder, 2026-09-17: the logo loading screen belongs on /forum, /gallery and
   /apply too. The marketing pages do it with an inline `<script>` that adds a
   `loading` class to `<body>`, hides everything else, and removes it on a
   timer. **That mechanism cannot be copied here and must not be**, for three
   separate reasons:

     1. vercel.json's CSP for these routes is `script-src 'self'`, so an inline
        script does not run at all — the overlay would never be dismissed.
     2. /apply must work with JavaScript disabled (D15). An overlay that only a
        script can remove turns the application form into a black rectangle for
        anyone whose JS is off or whose script 404s. That is a worse failure
        than having no splash.
     3. §9.2 counts requests, and a second script on /apply would be the only
        one besides draft.js.

   So this is CSS-only: the overlay dismisses itself with a finite animation and
   `forwards`. No script, nothing to fail, and nothing to remove. `pointer-events:
   none` is the belt to that braces — even if the animation never ran, the page
   underneath stays fully usable rather than being covered by a dead overlay.

   The image is a LOCAL WebP, not the S3 PNG the marketing pages use. Two
   reasons, both hard: `img-src 'self' data:` blocks the S3 origin outright, and
   the Phase 4 gate is "zero third-party requests" on /apply. The source PNG is
   471 KB; composited onto this same #000 ground and stored greyscale it is 25 KB
   for an identical rendering, because the transparency it carried was never
   visible against a black backdrop. ------------------------------------------ */
.splash {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: grid;
  place-items: center;
  background: var(--bg);
  /* Purely decorative: it never swallows a click, even mid-fade. */
  pointer-events: none;
  /* 2.5s hold, then a 0.8s fade — the marketing pages' timing, expressed as
     keyframe percentages so there is no `animation-delay` for the reduced-motion
     guard below to leave behind. */
  animation: splash-out 3.3s ease forwards;
}
@keyframes splash-out {
  0%, 75% { opacity: 1; }
  100%    { opacity: 0; visibility: hidden; }
}

.splash img {
  /* Matches custom.css's .loading-logo: 60vh tall, never wider than the screen. */
  height: 60vh;
  width: auto;
  max-width: 90vw;
  object-fit: contain;
  opacity: 0;
  animation: splash-in 1.5s ease-in-out forwards;
}
@keyframes splash-in { to { opacity: 1; } }

/* Narrow or short screens: width-led, like the marketing pages' media query. */
@media (max-width: 600px), (max-height: 600px) {
  .splash img { width: 75vw; height: auto; max-height: 90vh; }
}

/* The global guard below zeroes animation *duration* but not delay, which is why
   the hold is baked into the keyframes rather than set as a delay: with duration
   near zero this jumps straight to its end state, so somebody who asked for less
   motion gets no splash at all instead of the same 2.5s wait without the fade. */
@media (prefers-reduced-motion: reduce) {
  .splash { display: none; }
}

/* --- hero: the brand title + site nav shared with the marketing pages -----
   Used by /gallery and /forum, which sit in the same nav row as / and
   /hackerhouse and therefore have to look like siblings of them rather than
   like app screens (founder, 2026-09-16).

   This is a deliberate *reconstruction* of custom.css's `.hero-title-glow`,
   not a copy, and the difference is the point. That class is invisible at rest
   — its gradient starts at `background-size: 0% 0%` and only becomes visible
   because `radial-reveal` runs forwards over 5s after a 3.5s delay, and it
   then runs `glow` on an infinite loop. Copying it here would mean a content
   page whose title is blank for three and a half seconds and whose heading
   never stops animating, which §9.4 forbids on these routes for battery
   reasons. So the values below are that animation's *end state*, applied
   statically: `background-size: 300% 300%` and `letter-spacing: 2px` are
   exactly where `radial-reveal` and `letter-spacing` land. Same face, same
   clamp, same weight — visible on first paint, still afterwards.

   Linking custom.css instead was the other option and is rejected: 2,269 lines
   for one heading, carrying eight infinite animations and the starfield with
   it (§9.2, §9.4). ---------------------------------------------------------- */
.hero {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  padding-top: 5rem;      /* Tailwind's pt-20 on the marketing pages */
  padding-bottom: 2rem;
  text-align: center;

  /* Full bleed. `body` carries the side gutter that keeps every app surface off the screen edge,
     and the hero is a block child of it — so the backdrop stopped 1rem short on each side and read
     as a black border, which the landing page does not have (founder, 2026-09-17).

     Cancelled here rather than by moving the gutter off `body`: the same `max()` expression, negated
     as a margin and restored as padding, so the hero reaches both edges while the title and nav sit
     exactly where they did. It is the same expression in both places, so it cancels exactly at every
     width and on a notched device — and no other page moves, which taking the gutter off `body`
     would have risked for /login, /me, /admin and both review surfaces.

     `100vw` was the other way to do this and is wrong: on a desktop with a classic scrollbar it is
     wider than the viewport, which buys a horizontal scrollbar for the whole document. */
  margin-inline: calc(-1 * max(1rem, env(safe-area-inset-left)));
  padding-inline: max(1rem, env(safe-area-inset-left));

  /* Every hero is a positioning context (2026-09-20); only `.hero--video` was before. The
     session control itself positions against `.hero-titlebar`, not this (D76). */
  position: relative;
}

/* The brand title at the top of a page, with the content straight under it (founder, 2026-09-20:
   /login — "remove the hero image/section. Just keep the page title and its animations. Then
   display the login page stuff right below the page title… never have any scrolling needed";
   /myProfile — "the same page title and nav bar… Except no hero image/video"). Not a viewport:
   only as tall as the title and the row under it, with the top padding scaled to the window's
   height so a short laptop still fits the whole door on one screen. */
.hero--compact {
  min-height: 0;
  padding-top: clamp(1.5rem, 6vh, 5rem);
  padding-bottom: 0.5rem;
}
/* What follows a compact hero: the same measure as any page, closer to the title above it, and
   with the vertical rhythm drawn in — the heading sits on its eyebrow, the rule closer to both.
   Measured, not guessed: with these the door's tallest screen (a refused code, with its notice
   and both buttons) fits a 1366x768 laptop, which it missed by 87px at the page defaults. */
.under-title { padding-block: clamp(0.75rem, 2.5vh, 2.5rem) clamp(1rem, 3vh, 3rem); }
.under-title > .eyebrow + h2 { margin-top: 0; }
.under-title > .rule { margin: 1rem 0 1.5rem; }
.under-title .notice { margin-bottom: 1.5rem; }
/* The column's last element ends the page; its trailing margin would only be scroll. */
.under-title > :last-child, .under-title > :last-child > :last-child { margin-bottom: 0; }

/* A hero that carries a film behind the title (`/gallery`, founder 2026-09-18: the voxel Starry
   Night, replacing the Da Vinci plates of 2026-09-17). Opt-in per page: `/forum` and `/apply` keep
   the plain black ground.

   `isolation: isolate` creates a stacking context so the negative z-index below stays *behind the
   hero's own children* and cannot slide behind the page background, which is what a bare
   `z-index: -1` does. The film is absolutely positioned inside the hero rather than `position:
   fixed` like the marketing pages' hero: fixed would keep it on screen under the wall, and the
   wall is meant to sit on the flat ground where a photograph is easiest to read.

   The stars are the hero's own background, under the film: the founder's "black starry sky
   background from the main group landing page… in case it ever does not fully fill the screen".
   They show before the film's first frame arrives, wherever `contain` leaves the film short of the
   box, when the film cannot play, and alone under reduced motion. A LOCAL copy of the landing
   page's image, not its URL: that object is a 1.47 MB PNG at 6000×4125, and a backdrop that
   arrives after the 638 KB film it backs is no backdrop at all. `public/images/stars.webp` is the
   same picture at 2560px, and `img-src 'self'` already admits it.

   No halo, no scrim, no filter (founder, 2026-09-18: *"white with no letter borders or shadow or
   etc, to appear just like it does on the /hackerhouse page"*). The drop-shadow of v2.14 existed
   for white text over bright parchment; over a dark film on a black star field it has no reason
   left. The title is `.hero-title-glow`'s resting gradient and the nav is solid white — the same
   two things /hackerhouse shows. */
.hero--video {
  position: relative;
  isolation: isolate;
  /* The film may be drawn wider than the hero on a portrait screen (see .hero-bg); clip it. */
  overflow: hidden;
  /* How far the film may be zoomed on a portrait screen, as a multiple of the screen's width. The
     voxel cube is the middle 42% of the frame (measured: 28%–70% of its width across the loop), so
     at 2 the middle half of the frame shows and the cube spans 6%–91% of a phone's width, with
     only star field cropped at the sides. 1 means "never zoom" — the whole frame, letterboxed into
     the stars, on every screen. Retune if the film is ever swapped for one framed differently. */
  --film-zoom: 2;
  background: #000 url("/images/stars.webp") center / cover no-repeat;
}

.hero-bg {
  position: absolute;
  /* Centred in the hero's padding box rather than `inset: 0`, because the film is not always the
     hero's size: it keeps its own 16:9 shape (the `width`/`height` attributes carry it before a
     byte arrives) and only its WIDTH is set, in order of what wins —
       - never narrower than the hero, so on a landscape screen it spans the width: on 16:9 it is
         exactly the hero; on a 16:10 laptop it fills the height instead and loses 5% of star
         field at each side;
       - up to --film-zoom × the screen's width on a portrait screen, so the cube fills the width
         and only star field is cropped, rather than a 170px cube afloat in 600px of stars;
       - never taller than the hero (100svh × 16/9 wide), so the band stays inside it.
     clamp() whose maximum is below its minimum resolves to the minimum, which is what an ultrawide
     wants: the full width, with the top and bottom of the star field cropped and the cube whole.
     Wherever the film is short of the hero — above and below the band on a phone, at the sides on
     a laptop — the stars behind it continue past its edge, which is the founder's "in case it ever
     does not fully fill the screen"; the film's own field is black stars, so there is no seam. */
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: -1;
  width: clamp(100%, calc(var(--film-zoom) * 100vw), calc(100svh * 16 / 9));
  height: auto;
  /* `contain`, not `cover`: the box IS the film's shape, so this only matters if the film is ever
     swapped for one of another shape — and then the whole frame still shows, letterboxed. */
  object-fit: contain;
  object-position: center;
}

/* A reader who asked for less motion gets the still stars, not a looping film. The markup already
   withholds the source under this query, so nothing downloads; this hides the element too, so a
   browser that ignores `media` on a video source shows nothing rather than a paused first frame. */
@media (prefers-reduced-motion: reduce) {
  .hero--video .hero-bg { display: none; }
}

.hero-title {
  /* Not an h1 rule: app.css's h1 is the app-page size and this must not move it. */
  font-family: var(--display);
  /* Floor lowered 3rem -> 2rem, 2026-09-17, matching custom.css. At 3rem the clamp stopped being
     responsive below a 480px viewport and pinned the title at 48px — ~416px of text on a 390px
     phone, wider than the screen before any animation ran. 10vw governs all the way down now, and
     always fits: the string is ~8.67em, so it takes 0.867 of the viewport at every size.
     `min(10vw, 14vh)` since 2026-09-18, matching custom.css: a height guard for wide, short windows
     (a quarter of a laptop screen, 944×470, where 10vw gave the title a fifth of the height). 14vh
     bites only above 1.4:1 and under the 8rem ceiling — silent on 1080p, 4K, phones and tablets. */
  font-size: clamp(2rem, min(10vw, 14vh), 8rem);
  /* The brand is one line, at every width and at every frame of D36's animation. */
  white-space: nowrap;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 2px;
  margin: 0;
  padding: 0;
  display: inline-block;
  background: 50% 100% / 300% 300% no-repeat
              radial-gradient(ellipse at bottom,
                rgba(255, 255, 255, 1),
                rgba(255, 255, 255, 0.7),
                rgba(255, 255, 255, 0.7));
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
}

/* The one browser that matters and does not clip a gradient to text would render
   the title invisible, since the fill is transparent. `@supports not` puts the
   colour back rather than leaving a blank hero. */
@supports not ((background-clip: text) or (-webkit-background-clip: text)) {
  .hero-title { color: var(--text); -webkit-text-fill-color: currentColor; }
}

/* The letters carry their own animation, separately from the gradient that fills them — the same
   split custom.css makes. Animating letter-spacing on the element that owns the background would
   drag the background with it, and the two run at different durations besides (2350ms against
   5000ms). `inline-block` so the spacing applies as a box rather than to a run of inline text. */
.hero-title-letters {
  display: inline-block;
  font: inherit;
  letter-spacing: inherit;
}

/* Matches the marketing pages' subtitle row: same face, same 1.25rem, same 1rem
   gap, same `|` separators. `flex-wrap` is not cosmetic — four items plus three
   separators overflow a 400px viewport at this size (§12.7), which is the
   overflow the plan predicted for five items and that four already reach. */
.hero-nav {
  margin-top: 20px;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  /* `em`, not `rem`: the gap is part of the row's typography, so it has to grow with the
     text below or the words crowd together as the font-size climbs. 0.6em/0.8em is the
     old 0.75rem/1rem at the old 1.25rem font-size — unchanged where it was already right. */
  gap: 0.6em 0.8em;
  font-family: var(--display);
  /* Locked to `.hero-title`'s clamp at a constant 5/12, so the row and the title grow together
     instead of the row standing still while the title climbs to 8rem.
         3rem  x 5/12 = 1.25rem     <- the small end, unchanged: it was already right on a phone
        10vw   x 5/12 = 4.1667vw
         8rem  x 5/12 = 3.3333rem
     Both clamps also hit their ceiling at the same 1280px viewport, so the ratio holds at every
     width rather than only at the two ends. The title's 14vh height guard (2026-09-18) is here
     too, at the same 5/12: 5.8333vh. */
  font-size: clamp(1.25rem, min(4.1667vw, 5.8333vh), 3.3333rem);
  color: var(--text);
}
.hero-nav a { color: var(--text); text-decoration: none; }
/* The current page is marked by an underline, and `aria-current` is what puts it
   there — so the visual state and the state a screen reader announces cannot
   disagree, which they can when a class carries one and nothing carries the other. */
.hero-nav a[aria-current="page"] { text-decoration: underline; text-underline-offset: 3px; }
.hero-nav .sep { color: var(--text); pointer-events: none; user-select: none; }

/* Hover only where hovering is a thing. On a touch screen `:hover` sticks after
   a tap, so the glow would stay on whichever link was last pressed. */
@media (hover: hover) {
  .hero-nav a:hover {
    font-weight: 700;
    text-shadow: 0 0 10px rgba(255, 255, 255, 0.8),
                 0 0 20px rgba(255, 255, 255, 0.6),
                 0 0 30px rgba(255, 255, 255, 0.4);
  }
}

/* --- the landing page's title sequence, for the two pages that asked for it ---
   Founder, 2026-09-17: the expanding title and the nav arriving after it, as on the group page.
   Opt-in via `.hero--animated`, and NOT applied to /apply — a five-second reveal in front of an
   application form is a cost the form pays, and /apply was never named. One flag to change.

   **The one piece of the original that is deliberately not copied is `glow`**, which runs
   `5000ms linear infinite` from 7.5s on the landing page. §9.4 forbids an infinite animation on
   these routes outright, for battery, and a heading that pulses forever behind a roster is the
   kind of thing that reads as charming once and as noise every visit after.

   **Every keyframe below defines only its START, with `backwards`/`both` fill.** The resting state
   in the rules above IS the finished state, so the animation runs *towards* what the element
   already is. That inverts the usual failure: if these animations never run — reduced motion, an
   old engine, a stylesheet that half-loaded — the title and nav are simply *there*, correct and
   visible. Writing it the other way round (resting state invisible, `forwards` to visible) is how
   a page ends up with a permanently blank heading on the one browser that skipped the animation.

   Timing is the landing page's, unchanged, and it dovetails with the splash rather than fighting
   it: the splash finishes its fade at 3.3s and the reveal begins at 3.5s. */
.hero--animated .hero-title {
  animation: hero-title-reveal 5000ms ease-in-out 3500ms backwards;
}
@keyframes hero-title-reveal {
  /* The gradient is clipped to the glyphs, so growing it from nothing is what makes the letters
     appear to be lit from below rather than faded in. 300% is the resting value in `.hero-title`. */
  from { background-size: 0% 0%; }
}

.hero--animated .hero-title-letters {
  animation: hero-title-letters 2350ms ease-in-out 3500ms both;
}
@keyframes hero-title-letters {
  /* Only a start, like the other two — so the resting `letter-spacing: 2px` on `.hero-title` is
     where this lands, and the widest frame of the animation IS the final layout. That is what makes
     it safe at every viewport rather than at the ones that happened to get tested: an animation
     that never exceeds the resting width cannot reflow the line or overflow the screen.

     It used to run `0 -> 8px -> 2px`. The 8px peak was the widest moment, and on a phone it pushed
     the title onto a second line that snapped back when the animation settled. `em`, not `px`, so
     the expansion is the same proportion of the glyph on a phone as on a desktop — the old fixed
     8px was 6% of the title on one and 17% on the other. */
  from { letter-spacing: -0.045em; }
}

.hero--animated .hero-nav {
  animation: hero-nav-in 2s ease-in-out 5s backwards;
}
@keyframes hero-nav-in { from { opacity: 0; } }

/* The global guard zeroes animation *duration* and leaves *delay* alone, which on a 3.5s delay
   would mean three and a half seconds of blank hero followed by a snap. Switching the animations
   off entirely is the right reading of the request: the resting states are the finished ones, so
   this renders the arrived-at page immediately. */
@media (prefers-reduced-motion: reduce) {
  .hero--animated .hero-title,
  .hero--animated .hero-title-letters,
  .hero--animated .session-badge--title,
  .hero--animated .hero-nav { animation: none; }
}

/* What follows the hero starts its own viewport section, which is the founder's
   "after the page title, on the next viewheight". `scroll-margin-top` keeps a
   `#main` jump from hiding the heading under nothing — there is no fixed header
   here, so it is small and only exists to leave a little air. */
.after-hero { min-height: 100svh; scroll-margin-top: 1rem; }

/* --- motion: no infinite animation anywhere; honour the OS setting -------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* --- the session control: "Log in", or the member's avatar (plan D47–D50; placement D76) ---
   Founder, 2026-09-18: *"Add a login button in the top right of the page… Once a user is logged
   in, that login button is replaced by a circle profile photo they selected at sign in."* And,
   2026-09-20, on seeing it: *"improve the size scaling of the profile icon button… It should not
   be that far to the right. Maybe you can consider keeping it to the right of the page title?…
   never put the profile icon button nor the Log in button above the page title, such as on
   mobile. Maybe instead we can just incorporate it into the nav bar for mobile view? But desktop
   keep them floating in the right side."*

   So there are two placements and a media query shows exactly one (D76):

   BESIDE THE TITLE, on a desktop. `.hero-titlebar` shrink-wraps the title, and the control is
   absolute inside it at `left: 100% + gap`, centred on the title's line — so it follows the
   title's real right edge at every size with nobody measuring the font. The `min()` is a guard,
   not the design: it stops the control leaving the viewport if a window is tighter than the
   query below expects.

   IN THE NAV ROW, on a phone and in any window too tight to hold it beside the title. It is the
   row's last item, with no separator of its own, so a narrow row wraps it onto a centred line
   by itself rather than leaving a stray bar. Never above the title.

   "TOO TIGHT" IS ARITHMETIC, NOT A DEVICE. The title is ~8.33em + 32px wide and its size is
   clamp(2rem, min(10vw, 14vh), 8rem), so the room beside it is (100vw − title) / 2. That is
   under the ~100px the control needs whenever the window is narrower than 3:2 and under 1500px
   wide (a portrait phone, a 4:3 tablet either way up, a tall half-screen window); in every wider
   shape the 14vh guard keeps the title short enough, and past 1500px the 8rem cap does. Hence the
   query below, of which D43's phone query is a subset.

   SIZE follows the screen (clamp on vw): 44px — Appendix E's minimum target — up to a 1830px
   window, 80px on 4K, where a fixed 48px read as a speck. The label and the pill scale from that
   one number, so both states are the same height and the swap moves nothing.

   One block, byte-identical in custom.css, because the two static pages read that stylesheet and
   not this one; lib/pages-wiring.test.ts holds the copies equal. Every value is LITERAL for that
   reason: custom.css defines none of this file's tokens, and a var() a stylesheet cannot resolve
   silently drops the declaration — which is how the badge first rendered 19px tall in the system
   font. The ground is a 0.55 scrim: measured over the gilded painting, white text clears 4.5:1
   on it at every size tried, and over pure white by arithmetic (4.8:1). `.badge` elsewhere in
   this file means a small text pill stating state; `.session-badge` is this control. */
/* session-badge:start */
/* The title is the way home, on every page that wears it (founder, 2026-09-20). Nothing about
   how it LOOKS may change: the gradient is the parent's, clipped to this text, so the link takes
   the parent's colour and transparent fill and draws no underline. `pointer-events` because the
   landing page's hero sits under a wrapper that takes none. The outline is for a keyboard only. */
.hero-title-link {
  color: inherit;
  -webkit-text-fill-color: inherit;
  text-decoration: none;
  pointer-events: auto;
}
.hero-title-link:hover { color: inherit; }
.hero-title-link:focus-visible { outline: 2px solid #fff; outline-offset: 0.1em; border-radius: 0.05em; }
.hero-titlebar {
  position: relative;
  display: inline-flex;
  max-width: 100%;
}
.session-badge {
  /* The FACE's diameter. Three quarters of the title's own size — `.hero-title`'s clamp is
     `min(10vw, 14vh)` capped at 8rem, so this is `min(7.5vw, 10.5vh)` capped at 6rem — which
     makes the avatar about as tall as the title's capitals at every window, instead of a fixed
     sliver of the width that left it 46px beside a 128px title (founder, 2026-09-20: "on desktop
     it needs to scale/increase in size much more/much better"). */
  --badge-size: clamp(2.75rem, min(7.5vw, 10.5vh), 6rem);
  /* The WORD's height ("Log in", "Profile"): a pill that tall would be a billboard, so it takes
     seven tenths of the face's size and never less than the 44px target. */
  --badge-pill: max(2.75rem, calc(var(--badge-size) * 0.7));
  --badge-w: calc(var(--badge-pill) * 1.8);
  --badge-gap: clamp(0.75rem, 1.4vw, 3rem);
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: var(--badge-w);
  height: var(--badge-pill);
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.75);
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-family: 'Centrion', 'Source Sans Pro', sans-serif;
  font-size: calc(var(--badge-pill) * 0.36);
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  pointer-events: auto;
}
.session-badge:hover,
.session-badge:focus-visible { border-color: #fff; background: rgba(255, 255, 255, 0.12); }
.session-badge--avatar {
  --badge-w: var(--badge-size);
  height: var(--badge-size);
  border-radius: 50%;
  background: #000;
}
.session-badge--avatar img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}
/* The ring that marked the control as "you are here" on /myProfile (D82) went with the control on
   that page (D128). Everywhere the control is drawn now, it is the way there, not a claim to be
   on it — so there is nothing left to ring. */
.session-badge--title {
  position: absolute;
  z-index: 2;
  top: 50%;
  left: min(calc(100% + var(--badge-gap)), calc(100% + (100vw - 100%) / 2 - var(--badge-w) - 0.75rem));
  transform: translateY(-50%);
}
.session-badge--nav { display: none; }
@media screen and (max-width: 600px), screen and (max-width: 1500px) and (max-aspect-ratio: 3/2) {
  .session-badge--title { display: none; }
  /* In the nav row the title's clamp is the wrong ruler (a phone's title is 39px). Its own: 60px
     on a phone — it was 44 — growing to 76 on a tablet. */
  .session-badge--nav { display: inline-flex; --badge-size: clamp(3.75rem, 8vw, 4.75rem); }
}
/* session-badge:end */
/* D64 (founder, 2026-09-20: "fade with the nav"): beside the title, the control arrives with the
   nav row — same animation, duration and delay as `.hero--animated .hero-nav` — never before the
   brand. `backwards` holds it invisible until then; the reduced-motion guard switches it off with
   the trio, so a reduced-motion visitor sees it at once rather than never. In the nav row it
   needs nothing: it is inside the element that fades. custom.css does the same for the two
   static pages with the nav's own `subtitleFadeIn`, outside the copied block. */
.hero--animated .session-badge--title { animation: hero-nav-in 2s ease-in-out 5s backwards; }

/* --- the avatar picker: /welcome's second screen, and /me (plan D54, D56, §6, 2026-09-20) ---
   Founder: *"Display all in equal sized circles… Add a little description name label under each
   one… Once selected, the other options grey out. But the complete/next button becomes colored
   and selectable."*

   Native radios in labels, no script (D15/D54). The tap target is the whole label — image AND
   name — so a member whose images have not arrived still has a named, tappable target. The
   radio itself is visually hidden but keyboard-reachable: arrow keys move, Space selects, Enter
   submits. Selection carries three signals because Appendix E forbids colour alone: a ring, a
   check mark, and the radio's own checked state for assistive tech. The ring and the mark come
   from `:checked +`, which every browser has; only the grey-out of the OTHERS and the arming of
   the button need `:has()`, and a browser without it shows every circle at full strength and an
   always-solid button — the state the page has before anyone chooses — with `required` still
   blocking an empty submit natively. */
.avatar-grid {
  border: 0;
  margin: 0 0 1.75rem;
  padding: 0;
  min-width: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.5rem 1rem;
}
@media (min-width: 600px) {
  .avatar-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.avatar-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.625rem;
  cursor: pointer;
  text-align: center;
}
.avatar-tile__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}
.avatar-tile__circle {
  position: relative;
  display: block;
  width: min(100%, 9rem);
  aspect-ratio: 1;
  border-radius: 50%;
  background: #111;
  box-shadow: 0 0 0 3px transparent;
  transition: box-shadow 150ms ease, opacity 150ms ease, filter 150ms ease;
}
.avatar-tile__circle img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}
/* The display face: the screen's whole content is six historical names, and the system stack is
   the difference between this and a generic picker (plan §6.2). */
.avatar-tile__name { font-family: var(--display); font-size: 0.9375rem; line-height: 1.3; color: var(--text); }
.avatar-tile__input:checked + .avatar-tile__circle { box-shadow: 0 0 0 3px #fff; }
.avatar-tile__input:checked + .avatar-tile__circle::after {
  content: "\2713";
  position: absolute;
  right: 6%;
  bottom: 6%;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: #fff;
  color: #000;
  font: 700 0.9375rem/1.5rem var(--display);
  text-align: center;
}
.avatar-tile__input:focus-visible + .avatar-tile__circle { outline: 3px solid var(--blue); outline-offset: 4px; }
/* The founder's grey-out: the OTHERS, once anything is chosen. The image only — the name under
   it stays at full contrast, so no label drops below AA. */
.avatar-grid:has(input:checked) .avatar-tile:not(:has(input:checked)) .avatar-tile__circle {
  opacity: 0.35;
  filter: grayscale(1);
}
/* The way back to screen 1, under the screen's own button (D77). */
.welcome-restart { margin-top: 0.75rem; }

/* --- questions that appear only when they apply, and "Other" boxes only for "Other" (D98) -- */
/* No script: `:has()` reads the select's chosen option. EVERYTHING here is inside `@supports`, so
   a browser that cannot do the hiding keeps the old page — every question shown, the sentence
   saying when to answer it shown with it — which is the only way its form stays fillable.
   One selector per condition, because CSS cannot join one attribute's value to another's:
   lib/background.test.ts holds this list against every `visible_if` in the Background questions. */
@supports selector(:has(*)) {
  .field[data-when], .field-when, .other-box { display: none; }
  main:has(select[name="occupation"] option[value="Student"]:checked) .field[data-when="occupation=Student"],
  main:has(select[name="occupation"] option[value="Working"]:checked) .field[data-when="occupation=Working"] { display: block; }
  /* The box sits directly after its select (lib/render-application.ts). */
  select:has(option[value="other"]:checked) + .other-box { display: block; }
}
.other-box { margin-top: 0.75rem; }

/* --- an answered question as text with a pencil (D100) ------------------------------------- */
/* /myProfile's Background answers: label over answer, the pencil beside it, and the control only
   once the pencil is pressed — the name under the avatar, applied to a field. */
.field--collapsed { margin-bottom: 1.25rem; }
.field-edit > summary {
  list-style: none; display: flex; flex-wrap: wrap; align-items: center; column-gap: 0.25rem;
  min-height: var(--tap); border-radius: var(--radius); cursor: pointer;
}
.field-edit > summary::-webkit-details-marker { display: none; }
.field-edit > summary:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.field-edit > summary .field__pencil { flex: none; margin-left: 0.5rem; color: var(--text-muted); }
.field-edit > summary:hover .field__pencil, .field-edit[open] > summary .field__pencil { color: var(--text); }
.field-edit__body { margin-top: 0.5rem; }

/* --- a link's address: the rule appears when it is broken, not before (D101) ------------------ */
/* `:user-invalid` is "does not match its pattern, and the person has left the box". The form is
   `novalidate`, so this only ever colours and explains; it never blocks a save. The second block
   is the same thing for a browser that has `:invalid` but not `:user-invalid`. */
.link-error { display: none; }
.link-row input:user-invalid { border-color: var(--danger); }
.link-row input:user-invalid + .link-error { display: block; }
@supports not selector(:user-invalid) {
  .link-row input[pattern]:invalid:not(:focus):not(:placeholder-shown) { border-color: var(--danger); }
  .link-row input[pattern]:invalid:not(:focus):not(:placeholder-shown) + .link-error { display: block; }
}

/* --- /apply: the three questions' minimum (D134) ---------------------------------------------- */
/* Founder, 2026-09-21: *"only display in red text and make the text box edges red color when there
   was text left in the box, and the mouse clicks away from that box, and there was not enough
   characters."* That sentence is `:user-invalid` — invalid, and the person has left the box — with
   one more condition, "there was text": `:not(:placeholder-shown)`, which is why the box carries a
   placeholder of one space. Empty is the `required` rule's business, not this one's. No script is
   needed for any of it; essays.js adds the word count and puts the same sentence in the browser's
   own bubble. The sentence is in the page from the start, for a screen reader, and hidden. */
.essay { position: relative; }
/* A block, so the frame is exactly the box's height: an inline textarea leaves a baseline gap under
   it, which put the count on the box's bottom border (measured: 1px outside it). */
.essay > textarea { display: block; }
.essay__short { display: none; }
.essay > textarea:user-invalid:not(:placeholder-shown) { border-color: var(--danger); }
.essay:has(> textarea:user-invalid:not(:placeholder-shown)) + .essay__short { display: block; }
@supports not selector(:user-invalid) {
  .essay > textarea:invalid:not(:focus):not(:placeholder-shown) { border-color: var(--danger); }
  .essay:has(> textarea:invalid:not(:focus):not(:placeholder-shown)) + .essay__short { display: block; }
}
/* The live word count, small, in the box's bottom-right corner — from the first time the box is
   left short, then as the person types (essays.js). Clear of the resize grip in the very corner,
   and the box keeps a line clear under the words so the count never sits on top of them. */
.essay__count {
  position: absolute; right: 1.5em; bottom: 0.5em;
  font-size: 0.8125em; line-height: 1.2; color: var(--text-muted);
  pointer-events: none; font-variant-numeric: tabular-nums;
}
/* Two classes, not one: `.wrap--fluid textarea` sets the box's padding later in this file at the
   specificity one class would have, and would win (measured: the count sat on the last line). */
.essay.essay--counting > textarea { padding-bottom: 2em; }

/* --- /apply: type that scales with the window (D112) ---------------------------------------- */
/* Founder, 2026-09-20: *"all the text/body of the /apply page needs to scale/size a bit better."*
   What was wrong, measured: every page's column is 16px type in a 34rem (544px) measure at EVERY
   width. That is right on a phone and wrong on a desktop, where it sits under a title of 108–128px
   as a narrow strip of small print — and at 4K it is the same 544px in a 3840px window.

   So the COLUMN's type is fluid — 17px on a phone, ~19px on a laptop, ~21px at 1080p, 22px from
   about 2100px up — and everything inside it is sized in `em` off that one number: headings,
   hints, boxes, buttons, spacing. The measure is in `em` too, so it widens with the type and a
   line stays about 70 characters at every size instead of getting longer as the text gets
   bigger. The column's and not the root's, on purpose: scaling `html` would also move the brand
   title and the nav row, whose `rem` ceilings are what hold them equal to the landing page's. */
/* TWO straight lines and the larger of them, not one line with a ceiling (founder, 2026-09-21,
   D122: *"for /apply and /myProfile the scaling/font sizing increase for a 4k desktop site is
   still not enough"*). The first line is D112's and is unchanged, so nothing at or below a 1080p
   window moves by a pixel; the second is steeper, crosses it at about 2030px, and is what keeps
   the column growing where the old ceiling flattened it at 22px. Measured: 17px at 390,
   19.3 at 1366, 21.3 at 1920 (all three exactly as before), 25.3 at 2560, 33.9 at 3840. */
.wrap--fluid {
  font-size: clamp(1.0625rem, max(0.9rem + 0.36vw, 0.5rem + 0.675vw), 2.25rem);
  max-width: 36em;
}
/* The same scale for a page that must FIT ONE SCREEN (/login, D113): held back by the window's
   height as well as drawn out by its width, so a wide, short laptop does not get type too tall
   for it. The vh term is what keeps the door's everyday screens — the address box, the code box —
   inside 768px at every width; the floor is the phone's either way.

   Raised across the board on 2026-09-20 (founder, D118: *"the smaller text and I guess a little
   bit more for everything else on the /login page needs to scale in size better"*): 18px on a
   phone (was 17), ~20px on a 1366x768 laptop (was 17.6), 24px from 1920 up (was 21.3). The vh
   term still binds on a short window — it is what the measured fit below depends on. */
.wrap--fit { font-size: clamp(1.125rem, min(max(1rem + 0.42vw, 0.5rem + 0.78vw), 0.5rem + 1.55vh), 2.25rem); }
.wrap--fluid .eyebrow { font-size: 0.8125em; margin-bottom: 0.75em; }
.wrap--fluid .otp { font-size: 1.5em; }
.wrap--fluid h2 { font-size: 1.4em; margin-top: 2em; }
.wrap--fluid h2:first-child { margin-top: 0; }
.wrap--fluid p { margin-bottom: 1em; }
.wrap--fluid .small { font-size: 0.875em; }
.wrap--fluid .field { margin-bottom: 1.75em; }
.wrap--fluid .field > label { margin-bottom: 0.375em; }
.wrap--fluid .field .hint, .wrap--fluid .field-error { font-size: 0.9em; }
.wrap--fluid .field .hint { margin-bottom: 0.5em; }
/* On the door, the small print is the page's only explanation — where the code went, how long it
   lives, the way back from a typo — so it is a notch above the fluid column's usual small text
   (D118: *"the smaller text… needs to scale in size better"*). After the two rules above, which
   it overrides at the same specificity. */
.wrap--fit .small, .wrap--fit .field .hint { font-size: 0.9375em; }
/* Never under 16px (iOS zooms the page for less) — the floor of the scale is 17. */
.wrap--fluid input[type="text"], .wrap--fluid input[type="email"], .wrap--fluid input[type="tel"],
.wrap--fluid select, .wrap--fluid textarea {
  font-size: 1em; padding: 0.625em 0.75em; min-height: max(var(--tap), 2.75em);
}
.wrap--fluid textarea { min-height: 5.5em; max-height: 24em; }
.wrap--fluid .btn { font-size: 1em; padding: 0.75em 1.5em; min-height: max(var(--tap), 3em); }
.wrap--fluid .error-summary h2 { font-size: 1.125em; margin-top: 0; }
.wrap--fluid .error-summary, .wrap--fluid .notice { padding: 1em 1.25em; margin-bottom: 2em; }
.wrap--fluid .rule { margin: 1em 0 1.5em; }
.wrap--fluid .souls { margin-bottom: 2.5em; }
.wrap--fluid .souls-track img { height: clamp(6.5rem, 22vw, 9.5em); }
/* Bigger type in the same window means the rhythm has to be drawn in, or the door's tallest
   screen — a refused code, which carries an alert, the form, a resend and the way back — would
   need a scroll to answer. Measured at 1024x600 and 1366x768. */
.wrap--fit p { margin-bottom: 0.75em; }
.wrap--fit .field { margin-bottom: 1.25em; }
.wrap--fit .error-summary, .wrap--fit .notice { margin-bottom: 1.25em; }
.wrap--fit form + form { margin-top: 0.75em; }

/* --- /apply: the account's details, shown and not asked (D96) ----------------------------- */
/* Label over value, as a field reads, with the pencil right beside the value it changes (it
   wraps under a long address on a narrow phone rather than squeezing it). No box: a read-only
   box looks editable. The pencil is a 44px target around an 18px drawing. */
.field--settled { display: flex; flex-wrap: wrap; align-items: center; column-gap: 0.25rem; }
.field__label { flex: 0 0 100%; font-weight: 600; }
.field__value { min-width: 0; overflow-wrap: anywhere; }
.field__edit {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: var(--tap); height: var(--tap); border-radius: var(--radius); color: var(--text-muted);
}
.field__edit:hover { color: var(--text); }
.field__edit:focus-visible { outline: 3px solid var(--blue); outline-offset: 2px; }
.field--settled .field-error { flex: 0 0 100%; }

/* First Name and Last Name side by side (founder, 2026-09-20, D106) — whether they are the
   account's settled text or, for a caller with no profile, ordinary boxes. By id, because the
   renderer does not know which two questions are a pair; every published set names them so.
   Half the column each, less a gutter; stacked again where half a column is too narrow. */
#first_name.field--settled, #last_name.field--settled,
.field:has(> #first_name), .field:has(> #last_name) {
  /* Two halves, one 0.75rem gutter, and room for the space a line break between two inline boxes
     leaves: 2 x (50% - 0.75rem) + 0.75rem + a space is still under 100%, so they never wrap. */
  display: inline-flex; vertical-align: top; width: calc(50% - 0.75rem);
}
.field:has(> #first_name), .field:has(> #last_name) { flex-direction: column; }
#first_name.field--settled, .field:has(> #first_name) { margin-right: 0.75rem; }
@media (max-width: 360px) {
  #first_name.field--settled, #last_name.field--settled,
  .field:has(> #first_name), .field:has(> #last_name) { display: flex; width: auto; margin-right: 0; }
}

/* --- /apply: "Beautiful souls" and the gallery's photographs (D88) ---------------------- */
/* The title is a section heading that happens to be a link: it keeps the heading's colour and face. */
.souls { margin: 0 0 2.5rem; }
/* No underline — the founder's one-time exception (2026-09-20, D108): the title stays a link, as
   the pictures under it do, and says so on hover and on focus instead. */
.souls-title { color: inherit; text-decoration: none; }
.souls-title:focus-visible { outline: 3px solid var(--blue); outline-offset: 4px; border-radius: 2px; }
.souls-title:hover { color: var(--blue); }
/* The strip: a window onto a track twice its content, fading out at both edges. */
.souls-strip {
  display: block; overflow: hidden; margin-top: 1rem;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 88%, transparent 100%);
}
/* The row is printed twice. `padding-right` is one more gap, so the track is exactly two periods
   wide (2 x [n tiles + n gaps]) and sliding it by 50% lands the second copy precisely where the
   first began: no seam. From -50% to 0 is LEFT TO RIGHT, as asked. */
.souls-track {
  --souls-gap: 0.75rem;
  display: flex; gap: var(--souls-gap); padding-right: var(--souls-gap);
  width: max-content;
  animation: souls-drift 60s linear infinite;
}
/* Every tile 4:3 at one height, so the track's width is known before a byte arrives. */
.souls-track img {
  flex: none; display: block;
  height: clamp(6.5rem, 22vw, 10.5rem); width: auto; aspect-ratio: 4 / 3;
  object-fit: cover; border-radius: var(--radius); background: #111;
}
@keyframes souls-drift {
  from { transform: translateX(-50%); }
  to   { transform: translateX(0); }
}
/* Asked for less motion: nothing drifts, the second copy is not shown, and the row can be
   scrolled by hand instead. */
@media (prefers-reduced-motion: reduce) {
  .souls-track { animation: none; }
  .souls-copy { display: none; }
  .souls-strip { overflow-x: auto; -webkit-mask-image: none; mask-image: none; }
}

/* --- /myProfile (D82–D84) ------------------------------------------------- */
/* The account's own actions, on this page only: Apply, the rung's links, Log out. A wrapping row
   of 44px targets; Log out is a form (a mutation is never an anchor) and is pushed to the far
   end where there is room, and simply wraps where there is not. */
.profile-actions { margin: 1.25rem 0 0; }
/* THREE SLOTS, and the same three on every account page (D117): start, middle, and Log out at the
   end. Equal columns either side, so the middle is the middle of the ROW rather than merely
   between two buttons of different widths.

   The slot is a class on the child, not the order of the children: on /myProfile that is
   Apply | Save profile | Log out, for an admin and on /review and /admin it is
   Review | Admin | Log out, and a session that has one console and not the other leaves the empty
   slot empty instead of sliding the other one along. That is what keeps the founder's three
   buttons in the same place across the three pages. */
.profile-actions__row { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0.75rem; }
.profile-actions__start { grid-column: 1; justify-self: start; }
.profile-actions__mid { grid-column: 2; justify-self: center; }
.profile-actions__row > form { grid-column: 3; justify-self: end; margin: 0; }
/* The line beneath: the rung's own links, or an admin's Save profile, or the phone's way back to
   /myProfile — centred under the row. */
.profile-actions__links { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.75rem; margin-top: 0.75rem; }
/* The page you are on is marked, in the border and the weight as well as `aria-current`: state is
   never colour alone (Appendix E). */
.profile-actions [aria-current="page"] { border-color: var(--text); font-weight: 600; }
/* The line that exists only where the shell hides the avatar — the same query as the
   session-badge block's, so exactly one of the two ways to /myProfile is ever on screen. */
.profile-actions__links--phone { display: none; }
@media screen and (max-width: 600px), screen and (max-width: 1500px) and (max-aspect-ratio: 3/2) {
  .profile-actions__links--phone { display: flex; }
}
/* Three 44px buttons do not fit a phone at the desktop padding: tighter on a phone, and on the
   narrowest (320px) the middle one takes the second line, still centred. */
@media (max-width: 480px) {
  .profile-actions__row { gap: 0.5rem; }
  .profile-actions__row .btn { padding-inline: 0.875rem; }
}
@media (max-width: 350px) {
  .profile-actions__row { grid-template-columns: 1fr 1fr; }
  .profile-actions__row > .profile-actions__mid { grid-column: 1 / -1; grid-row: 2; }
  .profile-actions__row > form { grid-column: 2; grid-row: 1; }
}
/* "More suitable for such a button, perhaps not gradient colored": Apply is the site's gradient
   call to action; Save is a control, so it is a plain solid — and unmistakably OFF when there is
   nothing to save (public/scripts/profile.js sets `disabled`), not merely dimmed. */
.btn--save { background: var(--text); color: #000; }
.btn--save:hover { filter: none; background: #dcdcdc; }
.btn--save[disabled], .btn--save[disabled]:hover {
  background: none; border: 1px solid var(--rule); color: var(--text-muted); opacity: 1;
}
/* "Applied!" (D135, D139): the same button, on grey — an application is in and waiting — and still
   a link to /apply, which says when it was received. Not the gradient: the call to action has been
   answered. Since D139 (*"semi transparent grey, and maybe a more modern grey color"*) the grey is
   the cool system grey (142, 142, 147) at a quarter strength over whatever is behind it, frosted,
   with a hairline edge — on this page's black it reads as #242426, and the white text on it is 15:1. */
.btn--applied {
  background: rgba(142, 142, 147, 0.24); color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.14);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
.btn--applied:hover { background: rgba(142, 142, 147, 0.34); color: #fff; filter: none; }
/* The greyed Apply that stood in an admin's row for one evening (D115) is gone with the button
   itself (D117): the row now holds Review and Admin, and nothing on the page mentions applying.
   `.btn--save[disabled]` above is the one "off" face left, and it is a real disabled button. */
/* --- /admin: "What needs you now" as a dashboard (D140) ----------------------------------------- */
/* Founder, 2026-09-21: *"more like a dashboard or like bento box? The text format in bullet point
   makes it seem like an unchanging static text."* Tiles, each one link: a big number, what it
   counts, and where it goes. `auto-fill`, not `auto-fit`, so one tile stays a tile rather than
   stretching across the console's wide column; sized in `em` off the page's fluid type (D119). A
   zero tile is calm grey instead of the site's blue-to-purple, so a quiet week reads as quiet. */
.dash {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 15em), 1fr));
  gap: 1em; margin: 0.75em 0 0.5em;
}
.dash__tile {
  display: flex; flex-direction: column; gap: 0.35em;
  padding: 1.25em 1.4em; border-radius: 16px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: linear-gradient(145deg, rgba(33, 150, 243, 0.18), rgba(156, 39, 176, 0.14));
  color: var(--text); text-decoration: none;
  transition: border-color 0.2s ease, transform 0.2s ease;
}
.dash__tile:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.35); transform: translateY(-2px); }
.dash__tile:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.dash__tile--calm { background: rgba(142, 142, 147, 0.1); }
.dash__num { font-family: var(--display); font-size: 3em; line-height: 1; }
.dash__label { font-size: 1.0625em; font-weight: 600; }
.dash__go { font-size: 0.875em; color: var(--text-muted); }
.dash__note { margin: 0.75em 0 0; }
.dash__note--bad { color: var(--danger); }
@media (prefers-reduced-motion: reduce) {
  .dash__tile { transition: none; }
  .dash__tile:hover { transform: none; }
}

/* The role control (D141): the code box and "Change rung for …" wait until a new rung is chosen.
   "No change" is the select's empty, `required` option, so the select is `:invalid` until then. */
.role-form:has(select[name="rung"]:invalid) .role-form__commit { display: none; }

/* The cycle's name over a console page's own title, centred with it. */
.profile-eyebrow { text-align: center; margin-bottom: 0.25rem; }
.error-summary h3, .error-summary h4 { margin-top: 0; font-size: 1.125rem; }

/* A <summary> wearing `.btn`: the disclosure triangle goes, in both dialects. */
summary.btn { list-style: none; width: fit-content; }
summary.btn::-webkit-details-marker { display: none; }
summary.btn:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }

/* Links (D83). An opened row's "+ Add another link" has done its job; a CLOSED one is not offered
   until the row before it is filled in — `:placeholder-shown` is "this box is still empty", and
   lib/pages/me.ts gives both boxes a placeholder so it can match. No `:has()`, no harm: the rule
   is dropped whole and the button is merely offered early. */
.link-more { margin: 0 0 1.75rem; }
.link-more[open] { margin-bottom: 0; }
.link-more[open] > summary { display: none; }
.link-row:has(input:placeholder-shown) + .link-more:not([open]) { display: none; }

/* --- the profile itself (D85): the face, the name, the six to switch between ------------- */
/* "My Profile", the size of the name under the avatar (D93) — one declaration, shared, so the
   two cannot drift. */
.profile-title, .profile-name { font-size: clamp(1.5rem, 4vw, 2rem); line-height: 1.2; }
.profile-title { margin-top: 0; text-align: center; }
.profile-form { margin: 0; }
.avatar-stage {
  display: flex; flex-direction: column; align-items: center; gap: 0.75rem;
  margin: 1.5rem 0 0; text-align: center;
}
.avatar-hero {
  position: relative;
  width: clamp(8rem, 30vw, 12rem);
  aspect-ratio: 1;
  border-radius: 50%;
  background: #111;
}
/* Six large images, one shown. The radios are in the Info tab's "Avatar Selection" (D103), so
   the checked one is read from `main`, the nearest ancestor they share with the large face.
   `is-current` is the server's choice and the whole story for a browser without `:has()`; with it, the image follows the row's checked radio at once, before
   any save. The ids are lib/avatars.ts's, and lib/pages/me.test.ts holds the two lists equal. */
.avatar-hero__img {
  display: none;
  position: relative; z-index: 1;
  width: 100%; height: 100%;
  object-fit: cover; border-radius: 50%;
}
.avatar-hero__img.is-current { display: block; }
main:has(input[name="avatar"]:checked) .avatar-hero__img { display: none; }
main:has(input[name="avatar"][value="ada-lovelace"]:checked) .avatar-hero__img[data-avatar="ada-lovelace"] { display: block; }
main:has(input[name="avatar"][value="aristotle"]:checked) .avatar-hero__img[data-avatar="aristotle"] { display: block; }
main:has(input[name="avatar"][value="hypatia"]:checked) .avatar-hero__img[data-avatar="hypatia"] { display: block; }
main:has(input[name="avatar"][value="da-vinci"]:checked) .avatar-hero__img[data-avatar="da-vinci"] { display: block; }
main:has(input[name="avatar"][value="florence-nightingale"]:checked) .avatar-hero__img[data-avatar="florence-nightingale"] { display: block; }
main:has(input[name="avatar"][value="von-neumann"]:checked) .avatar-hero__img[data-avatar="von-neumann"] { display: block; }
/* Nothing chosen yet (the gate switched off, D57): a dashed circle under where the face will be.
   The image, once there is one, simply covers it. */
.avatar-hero__empty {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  border: 2px dashed var(--rule); border-radius: 50%;
  font: 400 3rem/1 var(--display); color: var(--text-muted);
}
/* The name under the face, with the pencil that opens its two boxes. */
.name-edit { width: 100%; text-align: center; }
.name-edit > summary {
  list-style: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: var(--tap); max-width: 100%; padding: 0 0.5rem;
  border-radius: var(--radius); cursor: pointer;
}
.name-edit > summary::-webkit-details-marker { display: none; }
.name-edit > summary:focus-visible { outline: 3px solid var(--blue); outline-offset: 3px; }
.profile-name { font-family: var(--display); overflow-wrap: anywhere; }
.name-edit__pencil { flex: none; color: var(--text-muted); }
.name-edit > summary:hover .name-edit__pencil,
.name-edit[open] .name-edit__pencil { color: var(--text); }
.name-edit__fields { margin-top: 1rem; text-align: left; }
.name-edit__fields h3 { margin-top: 0; }
/* The six to switch between, each with its name under it (D93). Three to a row on a phone —
   "Florence Nightingale" needs the width — and all six in one row from 600px. The circles stay
   well under half the large face above. Here the CHOSEN one is the one greyed out: it is the large
   face, and the row is what you could switch to — the reverse of /welcome's grid. Its name is
   muted with it but stays readable. */
.avatar-strip {
  border: 0; margin: 0.75rem 0 0; padding: 0; min-width: 0; width: 100%;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem 0.5rem;
}
@media (min-width: 600px) {
  .avatar-strip { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
.avatar-strip .avatar-tile { gap: 0.375rem; }
.avatar-strip .avatar-tile__circle { width: clamp(3rem, 14vw, 4.5rem); }
.avatar-strip .avatar-tile__name { font-size: 0.8125rem; line-height: 1.25; }
.avatar-strip .avatar-tile__input:checked + .avatar-tile__circle {
  box-shadow: none; opacity: 0.35; filter: grayscale(1); cursor: default;
}
.avatar-strip .avatar-tile__input:checked + .avatar-tile__circle::after { content: none; }
.avatar-strip .avatar-tile__input:checked ~ .avatar-tile__name { color: var(--text-muted); }

/* --- two tabs, no script (D86) ------------------------------------------------------------ */
/* A radio pair ahead of its panels, read by `:checked ~`. The radios stay focusable — arrow keys
   move between them, as between tabs — and hand their focus ring to the label that stands for
   them. The open tab is marked by a rule under it as well as by its colour (Appendix E). */
.tabs { position: relative; margin-top: 2rem; }
.tabs__radio { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }
.tabs__list { display: flex; margin: 0 0 1.75rem; border-bottom: 1px solid var(--rule); }
.tabs__tab {
  flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0.75rem 0.5rem; margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  font-family: var(--display); text-align: center; color: var(--text-muted); cursor: pointer;
}
.tabs__tab:hover { color: var(--text); }
.tabs__panel { display: none; }
.tabs__panel > h3:first-child, .tabs__panel > fieldset:first-child,
.tabs__panel > section:first-child > h3:first-child { margin-top: 0; }
#tab-info:checked ~ .tabs__panel--info,
#tab-account:checked ~ .tabs__panel--account { display: block; }
#tab-info:checked ~ .tabs__list [for="tab-info"],
#tab-account:checked ~ .tabs__list [for="tab-account"] {
  color: var(--text);
  border-image: linear-gradient(90deg, var(--blue), var(--purple)) 1;
}
#tab-info:focus-visible ~ .tabs__list [for="tab-info"],
#tab-account:focus-visible ~ .tabs__list [for="tab-account"] { outline: 3px solid var(--blue); outline-offset: -3px; }

/* Your email (D84): its forms sit apart from the profile form, under their own rule. The
   disclosure that used to be a "Change email" button is a pencil beside the address since D120,
   so it wears `.field-edit` and needs nothing of its own. */
.email-section form { margin-top: 1rem; }
/* The button is quiet until a choice exists, then it is the solid, coloured `.btn`. */
.welcome-form:not(:has(input[name="avatar"]:checked)) .avatar-continue {
  background: none;
  border: 1px solid var(--rule);
  color: var(--text-muted);
  cursor: not-allowed;
}
@media (prefers-reduced-motion: reduce) {
  .avatar-tile__circle { transition: none; }
}

/* --- /myProfile, /review and /admin: type that scales with the window (D119) --------------- */
/* Founder, 2026-09-20: *"the /myProfile page, /review page, /admin page on desktop needs to have
   better scaling. All the text and body content is a bit small, and should scale better."* So the
   three account pages join /apply on `.wrap--fluid` — one column type size, everything inside it
   in `em` — and this block is what the rest of the file left in `rem` on those pages.

   LAST in the file on purpose: several of these override a rule of the same specificity written
   above (the avatar row's names, the profile title), and source order is what decides that. */

/* The consoles are tables and queues. `.wrap--fluid`'s 36em measure is right for prose and far
   too narrow for the review queue, so the wide measure is restated in `em`: 62em is 60rem at the
   phone's type size and grows with the column from there. */
.wrap--fluid.wrap--wide { max-width: 62em; }
/* …but the ACTION ROW keeps the prose measure on a wide page, so the three buttons land on the
   same pixels on all three account pages and not merely in the same order (D117: *"ensure
   consistency of the three buttons which are common across the three pages having the same
   positions as well"*). Both columns are centred, so capping this one aligns them exactly. */
.wrap--wide .profile-actions { max-width: 36em; margin-inline: auto; }

/* Section headings, the counters under a box, and an error summary's own heading. */
.wrap--fluid h3 { font-size: 1.125em; margin-top: 1.75em; }
.wrap--fluid .counter { font-size: 0.8125em; }
.wrap--fluid .badge { font-size: 0.8125em; }
.wrap--fluid .error-summary h3, .wrap--fluid .error-summary h4 { font-size: 1.125em; margin-top: 0; }

/* The page's own name, and the person's name under their avatar — one declaration for both, as
   they have been since D93, and now in `em` off the column like everything else: ~30px on a
   phone, ~37px on a laptop, 38.5px at the ceiling, against the 24–32px clamp it replaces. */
.wrap--fluid .profile-title, .wrap--fluid .profile-name { font-size: 1.75em; }

/* The six faces to switch between, and their names under them. */
/* A run of ticks is one list, not a stack of separate fields: the gap between two of them is a
   line, not a section (D126). */
.wrap--fluid .field:has(> .toggle) { margin-bottom: 0.5em; }

.wrap--fluid .avatar-tile__name { font-size: 0.9375em; }
.wrap--fluid .avatar-strip .avatar-tile__name { font-size: 0.8125em; }

/* The chosen face and the six under it are body content too, and a portrait that stays 192px
   while the text around it grows by half reads as a page that did not finish scaling (D122). The
   `max()` is what leaves every window up to 1080p exactly where it was: the `rem` term wins until
   the column's type passes about 21px, and the `em` term takes over above it. */
.wrap--fluid .avatar-hero { width: clamp(8rem, 30vw, max(12rem, 9em)); }
.wrap--fluid .avatar-strip .avatar-tile__circle { width: clamp(3rem, 14vw, max(4.5rem, 3em)); }

/* Every pencil — beside a name, an answer, an address, a number — was an 18px drawing whatever
   the text beside it measured, which at 4K put a 7px-looking mark next to 34px words (D122). It
   follows the text now, never below the 18 it was: unchanged up to 1080p, ~29px at 3840. The
   `<svg>`'s own width and height attributes stay as the no-CSS fallback. And the 44px target
   around /apply's grows with it, so the box is never smaller than the drawing in it. */
.wrap--fluid .field__pencil, .wrap--fluid .name-edit__pencil { width: max(18px, 0.85em); height: max(18px, 0.85em); }
.wrap--fluid .field__edit { width: max(var(--tap), 1.6em); height: max(var(--tap), 1.6em); }

/* --- /apply on the landing page's night sky (D129) ------------------------------------------ */
/* The same picture the landing page's `.stars` layer draws, from the local copy (the landing page
   hot-links a 1.47 MB PNG at 6000x4125 from another origin, which this page's CSP does not admit
   and its byte budget could not carry; `stars.webp` is that picture at 2560px, 207 KB).

   A FIXED layer, not a background on `body`: `background-attachment: fixed` is ignored by iOS
   Safari, which would scroll the sky away with the first screen. `z-index: -1` in the root
   stacking context puts it above the canvas's black and under everything else, and nothing on
   the page paints an opaque ground of its own over it — the boxes do, which is what keeps them
   readable. No drifting cloud layer: the landing page's is a third-party image in endless
   motion, and an application form is not the place for either. */
.backdrop--stars::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background: #000 url("/images/stars.webp") center / cover no-repeat;
}

/* Visually hidden, still read: the picker's legend. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- /review: the workbench (review workbench plan D301, D302, D312, D327–D329, §6) --------- */
/* The list, the application and the verdict on one screen, at one path. Three panes where there is
   room, two below that, one pane at a time on a phone — decided by CSS alone, from `data-open` on
   the root, which the server sets because it knows whether `?app=` is in the URL (§11.2).

   THE WIDTH FIRST. `accountPage` passes `full` instead of `wide` for this page, and this rule is
   what lets that mean anything: `.wrap--fluid.wrap--wide { max-width: 62em }` above outranks
   `.wrap--full` on specificity, so without a two-class rule of its own the workbench would still
   be capped near 1215px at 1440 and three panes could never appear (§3.26). */
.wrap--fluid.wrap--full { max-width: none; }

/* "The committee room" (D312): black ground, hairlines not cards, the display face for names and
   every number, tabular figures so columns of numbers align. No card, no shadow, no rounded box. */
.num { font-family: var(--display); font-variant-numeric: tabular-nums; }

/* The two side panes scale with the window, not with a fixed number (founder, 2026-09-21: "the left
   and right side bars need to be larger/wider"): at 2000px the list is ~440px and the rail ~560px,
   where 20rem and 22rem had them at a sliver beside an essay capped at 42em anyway. */
.wb {
  --wb-queue: clamp(18rem, 22vw, 30rem);
  --wb-rail: clamp(24rem, 28vw, 42rem);
  display: grid; gap: 0 1.5em;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "bar" "queue" "app" "rail";
}
.wb-bar { grid-area: bar; }
.wb-queue { grid-area: queue; }
.wb-app { grid-area: app; min-width: 0; }
.wb-rail { grid-area: rail; min-width: 0; }
.wb-view { grid-area: view; min-width: 0; }
.wb[data-view="board"] { grid-template-areas: "bar" "view"; }

/* ONE PANE AT A TIME below 60rem (§12.7's phone rules, kept): an open application takes the
   screen, the rail stacks under it, and the queue waits behind "‹ Queue". With nothing open, the
   queue IS the screen — "Start with…" is for a wide window, where review.js follows it itself. */
.wb[data-open="app"] .wb-queue { display: none; }
@media (max-width: 59.99rem) {
  .wb[data-open="none"] .wb-app--empty > :not(.error-summary) { display: none; }
}

/* TWO PANES from 60rem: the application and the verdict side by side — the pair that matters
   while reading, and what the common 1280–1440px laptops get (D301). With nothing open, the queue
   and the "Start with" pane. */
@media (min-width: 60rem) {
  .wb { --wb-rail: clamp(24rem, 32vw, 36rem); grid-template-columns: minmax(0, 1fr) var(--wb-rail); grid-template-areas: "bar bar" "app rail"; }
  .wb[data-open="none"] { grid-template-columns: var(--wb-queue) minmax(0, 1fr); grid-template-areas: "bar bar" "queue app"; }
  .wb[data-view="board"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: "bar" "view"; }
  /* The rail scrolls on its own (D328): sticky, no taller than the window, so no block is cut off
     below a sticky edge, and a flick at its end does not scroll the essay behind it. */
  .wb-rail, .wb-queue {
    position: sticky; top: 1rem; align-self: start;
    max-height: calc(100vh - 2rem); overflow-y: auto; overscroll-behavior: contain;
  }
  .wb-rail { padding-right: 0.25em; }
  .wb-phonebar { display: none; }
}

/* THREE PANES where the essay still gets its measure. The plan's arithmetic said 90rem; measured
   against this page's own fluid type (~20px at 1440–1600), a 20rem queue and a 22rem rail leave a
   38em essay only from about 1520px, and at 1440 the essay would be squeezed to ~34em — the
   crushing D301 exists to prevent. So 95rem: 1280 and 1440 get two panes (as Phase 5's device list
   expects), 1600 and up get three. */
@media (min-width: 95rem) {
  .wb { --wb-rail: clamp(24rem, 28vw, 42rem); }
  .wb[data-open="app"] { grid-template-columns: var(--wb-queue) minmax(0, 1fr) var(--wb-rail); grid-template-areas: "bar bar bar" "queue app rail"; }
  .wb[data-open="app"] .wb-queue { display: block; }
  .wb[data-open="app"] .wb-back { display: none; }
  /* The list folded away: a slim column holding only the way back, and the application takes the
     width. A checkbox at the root is the switch, so no script is needed and a pane swap keeps it. */
  .wb[data-open="app"] .wb-qtoggle { display: flex; }
  .wb[data-open="app"]:has(#wb-hide-queue:checked) { grid-template-columns: 3rem minmax(0, 1fr) var(--wb-rail); }
  .wb[data-open="app"]:has(#wb-hide-queue:checked) .wb-queue > :not(.wb-qtoggle) { display: none; }
  .wb:has(#wb-hide-queue:checked) .wb-qtoggle__hide { display: none; }
  .wb:has(#wb-hide-queue:checked) .wb-qtoggle__show { display: inline; }
}
.wb-qtoggle {
  display: none; align-items: center; min-height: var(--tap); margin: 0 0 0.5em;
  color: var(--text-muted); cursor: pointer; user-select: none;
}
.wb-qtoggle:hover { color: var(--text); }
.wb-qtoggle__show { display: none; font-size: 1.25em; }
.wb:has(#wb-hide-queue:focus-visible) .wb-qtoggle { outline: 2px solid var(--blue); outline-offset: 2px; }

/* The bar: the cycle, the views, the count in words, the pipeline strip — the strip IS the chart. */
.wb-bar { border-bottom: 1px solid var(--rule); padding-bottom: 0.75em; margin-bottom: 1.25em; }
.wb-bar__top { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5em 1.5em; }
.wb-bar p { margin: 0; }
.wb-tabs ul { display: flex; gap: 0.25em; list-style: none; margin: 0; padding: 0; }
.wb-tabs a, .wb-strip__list a {
  display: inline-flex; align-items: center; min-height: var(--tap); padding: 0 0.75em;
  color: var(--text); text-decoration: none; border-radius: 2px;
}
.wb-tabs a[aria-current], .wb-strip__list a[aria-current] { background: var(--bg-raised); font-weight: 600; text-decoration: underline; text-underline-offset: 4px; }
.wb-count { font-family: var(--display); }
.wb-count a, .wb-export, .wb-contact a { display: inline-flex; align-items: center; min-height: var(--tap); }
.wb-count a { color: var(--text); }
.wb-export { margin-left: auto; }
.wb-strip { margin: 0; }
.wb-strip__list { display: flex; flex-wrap: wrap; list-style: none; margin: 0.5em 0 0; padding: 0; }
.wb-strip__list .num { color: var(--text-muted); margin-left: 0.25em; }
.wb-strip--narrow { display: none; }
.wb-strip--narrow summary { min-height: var(--tap); display: flex; align-items: center; cursor: pointer; font-family: var(--display); }
@media (max-width: 59.99rem) {
  .wb-strip--wide { display: none; }
  .wb-strip--narrow { display: block; }
  .wb-strip--narrow .wb-strip__list { flex-direction: column; }
}
.wb-blind { margin-top: 0.5em !important; }

/* The queue: two lines a row, the selected one on the raised ground with a 2px rule — the only
   filled surface on the screen — and `aria-current` so it is announced, not only drawn. */
.wb-search { display: flex; gap: 0.5em; margin: 0 0 0.75em; }
/* The base input rule names text, email, tel and password; a search box is none of those, and
   left alone it renders in the browser's own white. Same box as every other field here. */
.wb-search input {
  flex: 1; min-width: 0; min-height: var(--tap); padding: 0.625em 0.75em;
  background: var(--bg-section); color: var(--text); border: 1px solid var(--rule); border-radius: var(--radius);
  font: inherit; font-size: max(1rem, 1em);
}
/* A summary that opens a list says so: display:flex takes the browser's marker away. */
.wb-strip--narrow summary::after { content: "\00a0▾"; color: var(--text-muted); }
details[open].wb-strip--narrow > summary::after { content: "\00a0▴"; }
.wb-rows { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule); }
.wb-row a {
  display: block; min-height: var(--tap); padding: 0.625em 0.75em;
  border-bottom: 1px solid var(--rule); color: var(--text); text-decoration: none;
}
.wb-row a:hover { background: var(--bg-section); }
.wb-row.is-current a { background: var(--bg-raised); box-shadow: inset 2px 0 0 var(--text); }
.wb-row__name { display: block; font-family: var(--display); font-size: 1.0625em; line-height: 1.3; }
.wb-row__status { display: block; margin-top: 0.125em; }
.wb-row__status strong { font-weight: 600; }

/* The application: its header, then the essay at a reading measure. */
.wb-apphead { margin: 0 0 1em; }
.wb-apphead__nav { display: flex; align-items: center; gap: 0.75em; margin: 0 0 0.5em; }
.wb-back, .wb-step { display: inline-flex; align-items: center; min-height: var(--tap); min-width: var(--tap); }
.wb-pos { margin-left: auto; font-variant-numeric: tabular-nums; }
.wb-apphead__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5em; margin: 0 0 0.25em; }
.wb-apphead__meta .badge .eyebrow { margin: 0; font-size: 1em; }
.wrap--fluid .wb-name { font-family: var(--display); font-size: 1.6em; margin: 0 0 0.25em; }
.wb-name:focus { outline: none; }
.wb-contact { margin: 0 0 0.25em; }
.wb-apphead__set { margin: 0 0 1.5em; }
.wb-essay { max-width: 42em; }
.wb-essay .answer blockquote { line-height: 1.6; }
.wb-app--empty { padding-top: 1em; }

/* The phone's one full-width button (D329): one 44px target at 320px, saying where you are. Sticky
   to the window's bottom while the application is in view, padded clear of the home indicator
   (which reads 0 until the viewport tag gains viewport-fit=cover — Phase 5 checks a real iPhone),
   and gone while a text field has focus so it never sits on the keyboard. */
.wb-phonebar {
  position: sticky; bottom: 0; z-index: 2;
  padding: 0.5em 0 calc(0.5em + env(safe-area-inset-bottom));
  background: var(--bg); border-top: 1px solid var(--rule);
}
.wb:has(textarea:focus, input[type="text"]:focus, input[type="search"]:focus) .wb-phonebar { display: none; }

/* The rail: five blocks separated by hairlines, each headed in the eyebrow's type. */
.wb-block { border-top: 1px solid var(--rule); padding: 1em 0; }
.wb-rail > .wb-block:first-child, .wb-rail > div + .wb-block { border-top: 0; padding-top: 0; }
.wb-block > .eyebrow, .wb-block summary .eyebrow { margin: 0 0 0.75em; }
.wb-receipt { font-family: var(--display); margin: 0 0 0.5em; }
.wb-receipt:focus { outline: none; }
.wb-tag { font-family: var(--body); font-size: 0.8125em; color: var(--text-muted); font-style: italic; }

/* The verdict: a five-segment score and a three-segment Recommend — native radios in a fieldset,
   the segments are CSS on the labels, and the chosen recommendation's gloss shows with no script. */
.seg { position: relative; display: flex; flex-wrap: wrap; margin: 0 0 1em; }
.seg > legend { margin-bottom: 0.375em; }
.seg__input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.seg__label {
  flex: 1 1 0; display: flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: 0 0.5em; margin-left: -1px;
  border: 1px solid var(--rule); cursor: pointer; font-family: var(--display);
}
.seg__label:first-of-type { margin-left: 0; }
.seg__input:checked + .seg__label { background: var(--text); color: #000; border-color: var(--text); }
.seg__input:focus-visible + .seg__label { outline: 2px solid var(--blue); outline-offset: 2px; position: relative; }
.seg__caption { flex-basis: 100%; display: flex; justify-content: space-between; margin: 0.25em 0 0; }
.seg--score .seg__label { min-width: var(--tap); font-variant-numeric: tabular-nums; }
.seg--rec .seg__label { font-family: var(--body); }
.seg__gloss { display: none; flex-basis: 100%; margin: 0.375em 0 0; }
#rec-advance:checked ~ .seg__gloss--advance,
#rec-hold:checked ~ .seg__gloss--hold,
#rec-decline:checked ~ .seg__gloss--decline { display: block; }

/* The vote strip — the signature element (§6.4): names in the display face, scores in tabular
   figures, recommendations as whole words, the average and the spread said above them. */
.wb-strip-sum { font-family: var(--display); margin: 0 0 0.5em; }
.votes { list-style: none; margin: 0 0 0.5em; padding: 0; }
.vote {
  display: grid; grid-template-columns: minmax(0, 1fr) 2em 5.5em; align-items: baseline;
  gap: 0.125em 0.75em; padding: 0.375em 0; border-bottom: 1px solid var(--rule);
}
.vote__who { font-family: var(--display); overflow-wrap: anywhere; }
.vote__score { text-align: right; }
.vote__rec { color: var(--text-muted); }
.vote__tag, .vote .wb-clear { grid-column: 1 / -1; }
.wb-clear summary { cursor: pointer; color: var(--text-muted); min-height: var(--tap); display: flex; align-items: center; }
.wb-clear form { margin-top: 0.5em; }

/* Discussion: the latest three in reach, the rest one control away (D328). */
.comments { list-style: none; margin: 0 0 1em; padding: 0; }
.comments .comment { margin: 0 0 1em; }
.wb-older summary { cursor: pointer; min-height: var(--tap); display: flex; align-items: center; color: var(--text-muted); }
/* The comment box as a message box: the text first, then what it is about and Send on one line. */
.wb-comment textarea { min-height: 3.5em; }
.wb-comment__row { display: flex; gap: 0.5em; align-items: center; margin: 0.5em 0 0.25em; }
.wb-comment__row select { flex: 1; min-width: 0; }
.wb-comment > .small { margin: 0; }
/* A comment on its way: shown at once, faint until the server has it; a failed one says so. */
.comment.is-pending { opacity: 0.6; }
.comment__retry { color: var(--danger); margin: 0.25em 0 0; }
.comment__retry button { min-height: var(--tap); }

/* The decision: the spine in words, Advance labelled by destination and carrying the accent once
   the verdict is done, and the four consequential moves as equal-weight quiet two-steps (D328). */
.stepper { display: flex; flex-wrap: wrap; gap: 0 0.5em; list-style: none; margin: 0 0 0.75em; padding: 0; color: var(--text-muted); }
.stepper li + li::before { content: "—"; margin-right: 0.5em; }
.stepper [aria-current] { color: var(--text); }
.wb-advance { margin: 0 0 0.75em; }
.wb-decisions { display: flex; flex-wrap: wrap; gap: 0.5em; margin: 0 0 0.75em; }
.wb-decide > summary { list-style: none; cursor: pointer; }
.wb-decide > summary::-webkit-details-marker { display: none; }
.wb-decide[open] { flex-basis: 100%; }
.wb-decide[open] > summary { border-color: var(--text-muted); }
.wb-decide form { margin-top: 0.75em; padding-top: 0.75em; border-top: 1px solid var(--rule); }
.wb-decide .field { margin-bottom: 0.75em; }
.wb-withdraw { margin-top: 0.75em; }
.wb-resend { margin-top: 0.5em; }
.wb-reveal { margin-top: 0.75em; }
/* Withdraw sits last and quiet (D324): the one move nobody should reach for by accident. */
.wb-decide[data-decision="withdraw"] > summary { color: var(--text-muted); }

/* History: the narrative, ISO dates, collapsed once it is long. */
.wb-history__all > summary { cursor: pointer; list-style: none; min-height: var(--tap); display: flex; align-items: center; }
.wb-history__all > summary .eyebrow { margin: 0; }
.wb-history__all > summary::-webkit-details-marker { display: none; }
.history { list-style: none; margin: 0; padding: 0; }
.history li { padding: 0.25em 0; border-bottom: 1px solid var(--rule); }

/* The Board (§8.1): columns on a wide screen, stacked stage sections on a phone. */
.wb-board { display: grid; gap: 1.5em; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 60rem) { .wb-board { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.wb-col > .eyebrow { border-bottom: 1px solid var(--rule); padding-bottom: 0.5em; }
.wb-items { list-style: none; margin: 0; padding: 0; }
.wb-item { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25em 0.75em; padding: 0.5em 0; border-bottom: 1px solid var(--rule); }
.wb-item__name { flex-basis: 100%; font-family: var(--display); color: var(--text); text-decoration: none; min-height: var(--tap); display: flex; align-items: center; }
.wb-item__meta { flex: 1 1 auto; }
.wb-item__votes { color: var(--text-muted); }
.wb-col--decided > p { margin: 0 0 0.5em; }
.wb-item__advance { margin: 0; }
.wb-bulk-pick { display: inline-flex; align-items: center; min-height: var(--tap); min-width: var(--tap); }
.wb-bulk { display: flex; align-items: center; gap: 1em; margin: 0 0 1em; }
.wb-bulk[hidden], .wb-bulk-pick[hidden] { display: none; }

/* The skip links to each pane (§12): out of sight until focused. */
.wb-skips { margin: 0; }
.skip-inline { position: absolute; left: -9999px; }
.skip-inline:focus { position: static; left: auto; margin-right: 1em; }

/* A swap in flight (§6.6): the old pane stays readable; a 2px hairline runs along its top after
   300ms, and nothing moves at all under Reduce Motion. */
.wb [aria-busy="true"] { position: relative; }
.wb [aria-busy="true"]::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 2px;
  background: var(--text-muted); opacity: 0; animation: wbBusy 0.2s linear 0.3s forwards;
}
@keyframes wbBusy { to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .wb [aria-busy="true"]::before { animation: none; opacity: 1; } }
.wb-inline-error { color: var(--danger); margin: 0.5em 0; }

/* The keyboard dialog (§9): a native <dialog>, so focus is trapped and restored by the browser. */
.wb-keys { background: var(--bg-raised); color: var(--text); border: 1px solid var(--rule); border-radius: var(--radius); max-width: 32rem; width: calc(100% - 2rem); }
.wb-keys::backdrop { background: rgba(0, 0, 0, 0.6); }
.wb-keys table { border-collapse: collapse; width: 100%; margin: 0 0 1em; }
.wb-keys th, .wb-keys td { text-align: left; padding: 0.375em 0.5em; border-bottom: 1px solid var(--rule); font-weight: 400; }
kbd { font-family: var(--display); border: 1px solid var(--rule); border-radius: 3px; padding: 0 0.3em; }
