/* ==========================================================================
   Kwaze portal — kwaze.net
   Identity: "delivery bay". Committed dark palette (explicit, never inherited),
   monospace-uppercase voice, Kwaze-blurple accent, hairline structural grid.
   Hand-authored. No frameworks, no external resources.
   ========================================================================== */

:root {
  color-scheme: dark;

  /* ============ THE RAMP, AND THE BLACK GROUND UNDER IT ====================
     TWO owner rulings, both live, and the second one NARROWS the first.

     2026-08-29, dictated with a numbered screenshot of five swatches:
       "all loading bars should be 2 the regular windows should b 4 and the in
        betweens can be 3 and 5"
     2026-08-29, LATER, after seeing that shipped:
       "also i liked the origional black background for the website what we
        have now is to purple"

     The second ruling is the owner's newer word, so it wins where the two
     touch - and they touch in exactly one place: the LARGE FLAT SURFACES. The
     page ground, the card tiles and the hover lift go back to the near-black
     blue-grey family this site shipped before the ramp. Those values are NOT a
     new near-black picked by eye; they are the ORIGINAL TOKENS READ OUT OF
     COMMIT baad307, byte for byte, and re-measured before being trusted.
     Everything that ACTS stays on the ramp: the loading bar is still tier 2
     exactly, buttons still fill at tier 2, links and focus rings are still the
     bright ramp hue, the spinner still sweeps tier 1 -> tier 2. The identity is
     still Vodou purple. It is simply no longer the wallpaper.

     WHICH TIERS SURVIVE, AND WHY IT IS EXACTLY THESE THREE - measured against
     the restored ground #0e141b, not argued:
       TIER 1 #c900ff  KEPT. The spinner's near stop and the bright highlight.
       TIER 2 #8900ae  KEPT. "all loading bars should be 2", and the acting
                       fill. The ruling's one literal number; it did not move.
       TIER 5 #000000  KEPT, but ONLY as the loading bar's track. As a card tile
                       it measures 1.13:1 against the restored ground - that is
                       not "recessed", it is invisible. As a 6px groove under a
                       tier-2 bar it is the best track that exists, and that is
                       provable rather than preferred (see --loadbar-track).
       TIER 3 #5d0076  RETIRED on this surface. It was the hover lift, and at
                       1.52:1 on the restored ground it is not a lift at all.
       TIER 4 #330040  RETIRED on this surface. It was the page ground.
     The two tiers that stop being used here are the two that were WALLPAPER;
     the three that survive are the three that were INSTRUMENTS. That is the
     owner's second sentence expressed in tokens rather than in taste. Tiers 3
     and 4 are named above in prose ON PURPOSE and are deliberately not declared
     below: a live token nobody reads is how a retired decision comes back.
     They remain the owner's anchors and are one line away if they are wanted.

     SIBLING SURFACES ARE DELIBERATELY NOT FOLLOWING THIS FILE. surreallabs.net
     keeps its amber identity (standing ruling). The launcher's own chrome and
     the in-launcher game pane (kwaze-launcher-pages) STAY on the full ramp,
     grounds included - the ruling above says "the website", and a pane inside
     an application is not a website to the person looking at it. That decision
     is recorded at the top of kwaze-launcher-pages/template/kwaze-page.css. */
  --ramp-1: #c900ff;   /* TIER 1 - the bright highlight; spinner near stop */
  --ramp-2: #8900ae;   /* TIER 2 - "all loading bars", and every acting fill */
  --ramp-5: #000000;   /* TIER 5 - the loading bar's track, and nothing else */

  /* TIER 1 IS STILL UNASSIGNED BY THE RULING. The owner numbered it and gave it
     no job, so it is used here ONLY as the bright highlight - the spinner's
     near stop - and deliberately NOT as a text colour and NOT as a large fill,
     PENDING THE OWNER. The reason is measured, not aesthetic: as a button fill
     it drops the label to 3.72:1, under the 4.5:1 that .btn's 15px/700 text
     needs. If the owner assigns tier 1, that is a re-point of this one line. */
  --kwaze-bright: var(--ramp-1);

  /* --- SURFACES: the original ASCENDING ladder, restored -------------------
     Read out of baad307. The ramp had to BRACKET its ground (tile below it,
     lift above it) because #000000 is darker than a #330040 ground and a
     monotonic ladder was arithmetically impossible from those anchors. On a
     near-black ground that constraint is gone, so the natural ordering is back:
       --bg    the page ground, and the .field__input fill
       --bg-2  the tile, ONE step up  (.bay .door .post .threadrow .featured
               .signedin .specs .empty)
       --bg-3  the lift, TWO steps up (hover, .callout, the arrival flash)
     Relative luminance 0.00673 < 0.01113 < 0.01770, so every hover in this file
     lifts again in the direction its own rule already assumed. Note that not
     one hover rule changed when the ramp landed and not one changes back now:
     --bg-3 has been the LIFT token throughout, and only its value travelled.
     .bay--soon keeps `background: var(--bg)`, which now means a not-yet bay
     recedes to the page ground while live bays sit one step proud - the same
     intent the bracketed ladder was expressing by melting. */
  --bg:        #0e141b;   /* page ground   - original, restored from baad307 */
  --bg-2:      #141c25;   /* recessed tile - original, restored from baad307 */
  --bg-3:      #1b2530;   /* raised lift   - original, restored from baad307 */

  /* Hairlines. --line is the original value untouched: it is pure decoration -
     the card seams and the hero grid - and carries no contrast floor.
     --line-2 is the ONE neutral that could not simply be restored. It draws the
     .field__input border, the .btn--ghost and .btn--locked borders, the
     .whoami__link underline and the .post__action border - all UI-component
     boundaries, so WCAG 1.4.11 asks 3:1 against the surface each one sits on.
     The original #33434f measures 1.81 / 1.68 / 1.52 and fails on all three (it
     was already failing before the ramp, so this is an old defect being fixed
     while the file is open, not a regression being inherited). The ramp's
     #993cb3 clears the ground at 3.22 but misses a card at 2.99. So this value
     is LIFTED at the original hue 210 and the original saturation, to the
     lowest lightness that clears 3.0 on all three surfaces - exactly the
     treatment the ramp pass gave --draft. Ratios vs --bg / --bg-2 / --bg-3. */
  --line:      #26333f;   /* decorative seams; 1.43 / 1.33 / 1.20, no floor */
  --line-2:    #587088;   /* control borders; 3.60 / 3.34 / 3.02, floor 3.0 */

  /* --- TEXT: the original neutrals, restored and RE-MEASURED ---------------
     The ramp moved these onto hue 287.3 because a blue-grey text family over a
     purple ground was a hue disagreement. The ground is blue-grey again, so
     that argument now runs the other way: lavender text over a near-black
     blue-grey ground would reintroduce the same disagreement from the opposite
     side. All three original tiers were re-measured against the restored
     surfaces before being trusted rather than assumed to still hold.

     THE WORST GROUND IS --bg-3, NOT THE PAGE GROUND, and it is worst by
     measurement: it is the lightest surface in the file, and a hovered row and
     the .callout both put body text on it. Floor 4.5:1 throughout; nothing here
     leans on the 3:1 large-text allowance, and .btn at 15px/700 would not
     qualify for it anyway. Ratios are vs --bg / --bg-2 / --bg-3. */
  --ink:       #eaf0f6;   /* 16.12 / 14.96 / 13.51 */
  --ink-2:     #9fb0c0;   /*  8.33 /  7.73 /  6.98 */
  --ink-3:     #7e90a0;   /*  5.63 /  5.22 /  4.72 - the muted tier, at its floor */

  /* --- ACCENT ---------------------------------------------------------------
     The accent is split in two because one deep purple cannot be both a fill
     and readable text. Both halves stay on the ramp hue 287.3 - this is the
     half of the palette the owner did NOT ask to change.
       --accent       the FILL. TIER 2, the ruling's own loading-bar anchor.
                      Buttons take it because the thing that ACTS and the thing
                      that shows it acting should be one colour.
       --accent-text  accent TEXT and thin UI, on any of the three grounds.
     Ratios vs --bg / --bg-2 / --bg-3, re-measured on the restored grounds. */
  --accent:            var(--ramp-2);  /* tier 2 fill; label 6.85:1 */
  --accent-text:       #e277ff;   /* 7.33 / 6.80 / 6.14 */
  --accent-text-hover: #eca6ff;   /* 10.11 / 9.38 / 8.47 */
  /* The fill hover lifts TOWARD tier 1 without reaching it: tier 1 itself
     measures 3.72:1 under --on-accent and fails. This is the brightest lift
     that keeps the label over 4.5 (measured 5.01). Against the restored page
     ground it reads 3.22 where it read 3.01 against the retired #330040, so the
     hovered button's edge got MORE visible, not less. Note that a 3:1 boundary
     is not reachable for either fill state while the accent is pinned to tier 2
     - the rest fill is 2.19 against a card - so what identifies the button is
     its label at 5.01:1, and the hover reads as a state change against the rest
     fill rather than against the page. */
  --accent-fill-hover: #a900d6;
  --on-accent:         #f7ecfa;   /* 6.85 on --accent, 5.01 on --accent-fill-hover */

  /* "ALL LOADING BARS SHOULD BE 2" - the ruling's one literal instruction, so
     this is TIER 2 EXACTLY and the black-ground ruling does not touch it. It is
     its own token rather than a reuse of --accent so that --accent stays free
     to be re-pointed later without dragging the loading bars off the tier the
     owner named by number. */
  --loadbar:           var(--ramp-2);

  /* ...AND THE TRACK IT SWEEPS AGAINST IS NOW PINNED TOO, for the same reason
     one level down. The track used to be spelled `var(--bg-2)`, which meant the
     bar's legibility floated with wherever the surface ladder happened to be:
     under the ramp --bg-2 was #000000 and the bar read 2.67:1, and restoring
     the near-black ladder would silently have dropped it to 2.19 without one
     line of this file appearing to change. So the track is pinned to TIER 5,
     and measured.
     WHY 2.67 IS THE ANSWER AND NOT A COMPROMISE: black is the darkest colour
     that exists, so 2.67:1 is the CEILING for a #8900ae bar against any track
     whatsoever. The alternatives on the restored ladder measure --bg 2.36,
     --bg-2 2.19, --bg-3 1.98. WCAG 1.4.11 would ask 3:1 and that is simply
     unreachable while the bar is pinned to tier 2, which is the owner's
     explicit instruction and outranks the guideline here; the bar is also
     framed by a --line hairline and carries role="progressbar", so nothing is
     being conveyed by contrast alone. */
  --loadbar-track:     var(--ramp-5);

  /* The spinner ring stays a SWEEP, and that is functional rather than
     decorative: it animates by ROTATING, and a ring of one flat colour is
     rotation-invariant - it would show no motion at all. Both stops are the
     owner's own anchors (tier 1 -> tier 2). Stop-to-stop delta 1.84:1, enough
     to read as turning. On the restored ground the ring is slightly better off
     than it was: 4.34:1 at the near stop against 4.06 on the retired purple
     ground, and 2.36 against 2.20 at the far stop. Small, and stated as small -
     a first draft of this comment claimed 2.61 for the old ground, which was
     never measured and was wrong by 1.45. */
  --spin-near:         var(--kwaze-bright);
  --spin-far:          var(--ramp-2);

  --live:      #4fd08a;   /* 9.45 / 8.77 / 7.92 - unchanged across both palettes */
  --draft:     #e583a7;   /* 7.17 / 6.65 / 6.01 - KEEPS the 2026-08-29 lift. The
                             purple ground that forced it is gone and the older
                             #e0709a would pass here too, but a value lifted to
                             fix a measured failure is not un-lifted for tidiness. */

  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text",
          "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "Segoe UI Mono", SFMono-Regular, "SF Mono",
          Menlo, Consolas, "Liberation Mono", monospace;

  --wrap: 68rem;
  --pad:  clamp(1.25rem, 4vw, 2.5rem);
}

* { box-sizing: border-box; }

/* THE `hidden` ATTRIBUTE IS THIS SITE'S ONLY STATE SWITCH, SO IT HAS TO WIN.
   Browsers already hide such elements from their own USER-AGENT stylesheet, but
   an author `display:` beats a user-agent one no matter how weak its selector
   is. (That sentence deliberately does not spell the user-agent rule out as a
   literal. The ship check greps this file for the declaration below, and a
   comment quoting it would be counted as a second copy -- documentation
   becoming input to its own measurement.)
   This file sets `display:` on exactly the three things auth.js hides --
   .doors (grid), .lock (flex), .signedin (flex) -- so `el.hidden = true` was
   changing an attribute and nothing else. Measured in Chrome on 2026-08-27,
   before this rule existed: the sign-in form painted 544x377 on /account/
   WHILE SIGNED IN, the signed-in panel painted 1008x227 on /account/ while
   signed OUT, and both "sign in to download" notes painted on an unlocked
   /download/. Three visible bugs, one missing line.

   `!important` rather than putting the rule last: source order only wins
   against equal specificity, so the next `#panel { display: … }` anyone writes
   would silently re-open the whole class. The site never wants a hidden
   element laid out, so there is no case to lose. */
[hidden] { display: none !important; }

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

body {
  margin: 0;
  /* Explicit ground: the page never borrows the host's theme. */
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.6;
  font-synthesis-weight: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}

img, svg { max-width: 100%; height: auto; }

a { color: var(--accent-text); text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
a:hover { color: var(--accent-text-hover); }

:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 3px;
  border-radius: 1px;
}

/* --- skip link ---------------------------------------------------------- */

/* Hidden by clipping rather than by an off-canvas offset: a negative `left`
   is the classic recipe but it parks a box outside the layout, which is the
   one thing that can quietly create a horizontal scrollbar. Clipping cannot. */
.skip {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 99;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  background: var(--accent);
  color: var(--on-accent);
  padding: 0.7rem 1.1rem;
  font: 600 0.8125rem/1 var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
}
.skip:focus {
  width: auto;
  height: auto;
  overflow: visible;
  clip-path: none;
}

/* --- shell -------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--pad);
}

/* --- the mark: KWAZE / LAUNCHER over a completed progress rule -----------
   A two-line lockup. LAUNCHER is set smaller and tracked out so it justifies
   to roughly KWAZE's measure — the classic stacked-wordmark trick, done in
   arithmetic rather than by eye so it holds at every clamp size.
     KWAZE:    5 chars x (0.60em advance + 0.16em tracking) = 3.64em wide
     LAUNCHER: 8 chars x (0.60em + 0.55em) at 0.42 scale    = 3.63em wide
   Both sizes come from --mark-size so the ratio survives the fluid type. */

.mark {
  display: inline-block;
  text-decoration: none;
  color: var(--ink);
  /* defaults; each placement overrides these two */
  --mark-size: 1.0625rem;
  --sub-size: 0.625rem;
  --sub-track: 0.22em;
}
.mark__word {
  display: block;
  font-family: var(--mono);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.16em;
  /* trailing tracking pushes the last glyph off-centre; pull it back */
  margin-right: -0.16em;
  line-height: 1;
  font-size: var(--mark-size);
}
.mark__word--sub {
  font-size: var(--sub-size);
  font-weight: 500;
  letter-spacing: var(--sub-track);
  margin-right: calc(var(--sub-track) * -1);
  margin-top: 0.42em;
  color: var(--ink-2);
}
.mark__rule {
  display: block;
  height: 3px;
  margin-top: 0.5em;
  background: var(--line-2);
  position: relative;
  overflow: hidden;
}
.mark__rule::after {
  content: "";
  position: absolute;
  inset: 0 38% 0 0;
  background: var(--accent);
}

/* --- masthead ----------------------------------------------------------- */

.masthead {
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}
.masthead__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 1.15rem;
}
/* masthead keeps the .mark defaults (1.0625rem / 0.625rem sub) */
.masthead .mark__rule { height: 2px; margin-top: 0.35em; }

/* --- hero --------------------------------------------------------------- */

.hero {
  position: relative;
  border-bottom: 1px solid var(--line);
  padding-block: clamp(3rem, 9vw, 6rem);
  overflow: hidden;
}
/* Flat engineered field — hairline grid, not a gradient wash. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    repeating-linear-gradient(to right,  var(--line) 0 1px, transparent 1px 5rem),
    repeating-linear-gradient(to bottom, var(--line) 0 1px, transparent 1px 5rem);
  opacity: 0.4;
  mask-image: linear-gradient(to bottom, #000 0%, transparent 85%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 85%);
  pointer-events: none;
}
.hero > .wrap { position: relative; }

/* The hero eyebrow was removed on 2026-08-20 (owner red-pen): it carried the
   tagline, which is now the lead, and a hero should not say the same thing
   twice. The mark now opens the page unaccompanied. */

.hero__mark {
  margin-bottom: 2rem;
  --mark-size: clamp(2.75rem, 12vw, 6rem);
  --sub-size: calc(var(--mark-size) * 0.42);
  --sub-track: 0.55em;
}
.hero__mark .mark__rule { height: clamp(4px, 1vw, 6px); margin-top: 0.62em; }

.hero__lead {
  /* the only prose in the hero now, so it carries more weight than before */
  font-size: clamp(1.375rem, 1.1rem + 1.2vw, 2rem);
  line-height: 1.3;
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0;
  max-width: 26ch;
}
/* .hero__body went with the explanatory paragraph it styled (same red-pen).
   Nothing else on this page used it — verified by grep, not assumed. */

.featured {
  margin-top: 2.5rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent-text);
  background: var(--bg-2);
  padding: 1.1rem 1.35rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1.25rem;
  max-width: 46rem;
}
.featured__label {
  font: 600 0.6875rem/1.4 var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.featured__name { font-weight: 650; }
.featured__meta {
  font: 500 0.8125rem/1.4 var(--mono);
  color: var(--ink-3);
}

/* --- sections ----------------------------------------------------------- */

.section { padding-block: clamp(3rem, 7vw, 5rem); border-bottom: 1px solid var(--line); }

.section__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1.25rem;
  margin-bottom: 2rem;
}
/* .section__num retired 2026-08-25 with the four-page split, for the reason
   the sibling site retired its own the same day (owner: "tabs insted of 01,
   02, etc so its just a clean website"): the masthead tabs carry the sequence
   now. A page showing "02" with no 01 or 03 anywhere on it is a positional
   landmark that stopped being true the moment the page split. */
.section__title {
  font-family: var(--mono);
  font-size: clamp(1.125rem, 1rem + 0.6vw, 1.5rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin: 0;
}
.section__note {
  flex-basis: 100%;
  margin: 0.25rem 0 0;
  color: var(--ink-2);
  max-width: 62ch;
}

/* --- download bays ------------------------------------------------------ */

/* 1px gaps over a line-coloured backdrop = crisp hairline dividers. */
.bays {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
@media (min-width: 42rem) {
  .bays { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.bay {
  background: var(--bg-2);
  padding: clamp(1.25rem, 3vw, 1.85rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}
.bay--soon { background: var(--bg); }

.bay__head { display: flex; flex-direction: column; gap: 0.6rem; }
.bay__title {
  font-family: var(--mono);
  font-size: 1.375rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0;
}
.bay--soon .bay__title { color: var(--ink-2); }
.bay__sub {
  margin: 0;
  font: 500 0.8125rem/1.5 var(--mono);
  color: var(--ink-3);
}

.chip {
  align-self: flex-start;
  font: 600 0.6875rem/1 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 0.4rem 0.6rem;
  border: 1px solid currentColor;
}
.chip--live { color: var(--live); }
.chip--soon { color: var(--ink-3); }
/* injected by app.js on a confident user-agent match; additive only */
.chip--you { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }

.bay__note {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--ink-2);
}
.bay__spacer { margin-top: auto; }

/* --- buttons ------------------------------------------------------------ */

.btn {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  width: 100%;
  padding: 0.9rem 1.15rem;
  border: 1px solid transparent;
  font-family: var(--mono);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
/* The third opacity-on-text retirement of this pass. .btn__meta inherits its
   colour from FOUR different buttons, so one opacity was composited against
   four different fills and only ever measured against some of them: at 0.8 on
   the restored palette it read 4.74 on a primary, 9.95 on a ghost, 5.39 on a
   locked and 3.83 on an inert -- one failure hidden inside three passes. Size
   and weight already say "secondary" (12px/500 against the label's 15px/700),
   so the opacity was buying nothing it was not already getting. Full strength:
   6.85 / 14.96 / 7.73 / 5.22, all four clear. */
.btn__meta {
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.btn--primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn--primary:hover { background: var(--accent-fill-hover); border-color: var(--accent-fill-hover); color: var(--on-accent); }

.btn--ghost { background: transparent; border-color: var(--line-2); color: var(--ink); }
.btn--ghost:hover { background: var(--bg-3); border-color: var(--ink-3); color: var(--ink); }

.btn--inert {
  background: transparent;
  border-color: var(--line);
  border-style: dashed;
  color: var(--ink-3);
  cursor: not-allowed;
}

/* IN FLIGHT. auth.js's busy() has always set `button.disabled = true` while a
   form is submitting -- and until 2026-08-27 nothing in this file drew that,
   so a submit button that had stopped working looked exactly like one that
   still did, and the only cue that anything was happening was a line of small
   text. The gate now asserts the submit is really disabled mid-flight; this
   rule is what makes that state legible rather than merely true. */
.btn:disabled {
  cursor: progress;
  opacity: 0.55;
  filter: saturate(0.4);
}
/* ...and hover must not brighten a button that cannot be pressed. Only the
   three form submits are ever disabled (busy() touches `button[type=submit]`
   and nothing else) and all three are .btn--primary, so this is the one
   variant that needs its hover held still. */
.btn--primary:disabled:hover { background: var(--accent); border-color: var(--accent); }

.bay__links { display: flex; flex-direction: column; gap: 0.6rem; }

/* --- verify disclosure -------------------------------------------------- */

.verify {
  border-top: 1px solid var(--line);
  padding-top: 0.9rem;
  font-size: 0.8125rem;
}
.verify > summary {
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
  list-style: none;
}
.verify > summary::-webkit-details-marker { display: none; }
.verify > summary::before { content: "+ "; color: var(--accent-text); }
.verify[open] > summary::before { content: "\2212 "; }
.verify > summary:hover { color: var(--ink-2); }

.verify__body { padding-top: 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; }
.verify__label {
  margin: 0;
  font: 600 0.6875rem/1 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.hash {
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.7;
  color: var(--ink-2);
  /* long hex must wrap, never widen the page */
  word-break: break-all;
  overflow-wrap: anywhere;
  display: block;
  background: var(--bg);
  border: 1px solid var(--line);
  padding: 0.55rem 0.7rem;
}
.copy {
  align-self: flex-start;
  background: transparent;
  border: 1px solid var(--line-2);
  color: var(--ink-2);
  font: 600 0.6875rem/1 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.45rem 0.7rem;
  cursor: pointer;
}
.copy:hover { border-color: var(--ink-3); color: var(--ink); }
.verify__how { margin: 0; color: var(--ink-3); font-size: 0.75rem; }
.verify__how code {
  font-family: var(--mono);
  color: var(--ink-2);
  overflow-wrap: anywhere;
}

/* --- callout (SmartScreen etc.) ----------------------------------------- */

.callout {
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-2);
  background: var(--bg-3);
  padding: 0.9rem 1.1rem;
  font-size: 0.875rem;
  color: var(--ink-2);
  margin: 0;
}
.callout strong { color: var(--ink); font-weight: 650; }
.callout code {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--ink);
  white-space: nowrap;
}

/* --- spec list ---------------------------------------------------------- */

.specs {
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
@media (min-width: 34rem) {
  .specs { grid-template-columns: minmax(7rem, 14rem) minmax(0, 1fr); }
}
.specs > dt {
  background: var(--bg-2);
  padding: 0.85rem 1.1rem;
  font: 600 0.6875rem/1.5 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.specs > dd {
  background: var(--bg-2);
  margin: 0;
  padding: 0.85rem 1.1rem;
  font-size: 0.9375rem;
  color: var(--ink);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* --- draft marker ------------------------------------------------------- */

.draft {
  border-left: 3px dashed var(--draft);
  padding-left: 0.85rem;
}
.draft__tag {
  display: inline-block;
  font: 700 0.625rem/1 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--draft);
  border: 1px dashed var(--draft);
  padding: 0.3rem 0.45rem;
  margin-bottom: 0.5rem;
}

/* --- footer ------------------------------------------------------------- */
/* Hairline rule above, mono meta line. The outbound credit line that used to
   sit here was removed on 2026-08-20 (owner red-pen: Kwaze stands as its own
   brand on this site), so the footer now carries the meta line alone. */

/* FOOTER — PARKED, NOT DELETED. 2026-08-26: the owner removed the "Alpha software /
   Downloads served from get.kwaze.net" line ("remove this from kawaze"), which left an
   empty 6rem bar, so the <footer> element came out of every page too (four that day;
   five since the front page and the download tab split later the same day). But it IS
   coming back: "eventually that footer will have contact info but leave it blank for now
   because i havent set up a support email". These rules stay so that day is a markup
   paste, not a rebuild — each page carries a comment marking where the element goes.
   Unused today by design; do not "clean up". */
.footer { padding-block: 2.75rem 3.5rem; }
.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
}
.footer__meta {
  margin: 0;
  font: 500 0.75rem/1.7 var(--mono);
  letter-spacing: 0.04em;
  color: var(--ink-3);
  /* left-aligned at every width: with the credit line gone this is the only
     child, and right-aligning a lone flex item at the left edge reads as a
     mistake rather than a decision. */
  text-align: left;
}

/* ^ 2026-08-27: `text-align: left` and the closing brace above were ORPHANED --
   left at the top level of this file with no selector and no opening brace when
   f1f6501 ("drop the footer") deleted the `.footer__meta {` line under them and
   69c6577 re-added the rule without them. That is not a harmless stray: the CSS
   parser reads an orphaned declaration as the BEGINNING OF A SELECTOR and keeps
   consuming until the next `{`, so the orphan SWALLOWED THE NEXT RULE WHOLE.
   The casualty was the FIRST RULE AFTER IT -- `.whoami`, the masthead account
   strip, which then had no styling at all. (Named, not cited by line: whichever
   rule follows this comment is the one at risk, and a line number here would go
   stale the first time anything above it moved.)
   Measured in Chrome: 131 rules parsed from a file with 132 blocks, `.whoami`
   absent from the CSSOM, the strip computing `display:block` instead of `flex`,
   so its `gap` never applied and the account name and "Sign out" rendered with
   0.00px between them ("surrealSign out") at every width. Nothing warned: the
   sheet loaded, every other rule worked. `scripts/cssaudit.py` now fails the
   gate on this shape. */

/* ==========================================================================
   ACCOUNT — added 2026-08-20 with the kwaze-auth service.

   Same construction as the download bays: 1px gaps over a line-coloured
   backdrop for true hairline dividers, mono uppercase labels, the blurple accent.
   Nothing new is invented here; the account panels are bays with fields in
   them, because a sign-in form that looked like a different website would
   read as a phishing page on the site's own domain.
   ========================================================================== */

/* .skip--second retired 2026-08-25 with the four-page split. The second skip
   link was "Skip to account", which was a jump to a section on this page and
   is now a nav tab -- so it is a link, not a skip. One skip link per page. */

/* --- masthead account strip --------------------------------------------- */

/* The gap is a DELIBERATE VALUE, not whatever flex happens to leave, because
   flex leaves nothing: two adjacent inline-level children with no whitespace
   between them touch at 0.00px, which is exactly what shipped while the rule
   above this one was being swallowed by a parse error (owner: "surreal" and
   "Sign out" printed on top of each other; then "the account name needs to be
   a little farther to the left from the sign out button").
     column gap 1.5rem = 24px against a 12px mono glyph run -- about two
   characters of clear air, so the name and the link read as two things.
     row gap 0.5rem is the STACKED case, and what actually triggers it is NAME
   LENGTH, not the viewport: measured in Chrome, a short name keeps both
   children on one row all the way down to 240px, while a 32-character name
   (the longest kwaze-auth accepts -- see maxlength on #signin-username) pushes
   the link onto its own line at 400px and below. 8px is what stops those two
   rows reading as one paragraph.
     Both numbers are measured rather than guessed: layout_gate.py asserts a
   24px gap on all four pages at 1280 / 500 / 360 / 320, and fails on the
   0.00px the page shipped with. */
.whoami {
  display: flex;
  align-items: baseline;
  gap: 0.5rem 1.5rem;
  flex-wrap: wrap;
  font: 600 0.75rem/1.4 var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  min-width: 0;
}
.whoami__name {
  color: var(--ink);
  /* an account name is at most 32 characters and must never widen the bar */
  overflow-wrap: anywhere;
  min-width: 0;
}
.whoami__name::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 0.55em;
  background: var(--live);
  vertical-align: 0.1em;
}
.whoami__link {
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid var(--line-2);
  padding-bottom: 2px;
}
.whoami__link:hover { color: var(--accent-text); border-bottom-color: var(--accent-text); }
.whoami__link--button {
  background: none;
  border: 0;
  border-bottom: 1px solid var(--line-2);
  padding: 0 0 2px;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}

/* --- locked downloads ---------------------------------------------------- */

/* A locked button is still a real link: the gate is Caddy's forward_auth, and
   clicking through sends a signed-out visitor back to the account section by
   way of a 303. So this styles a state, it does not enforce one. */
.btn--locked {
  background: transparent;
  border-color: var(--line-2);
  border-style: dashed;
  color: var(--ink-2);
}
.btn--locked:hover {
  background: var(--bg-3);
  border-color: var(--ink-3);
  color: var(--ink);
}

.lock {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  font: 600 0.6875rem/1.4 var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.lock__dot {
  flex: none;
  width: 6px;
  height: 6px;
  background: var(--accent-text);
}
.lock a { color: var(--accent-text); text-decoration: none; border-bottom: 1px solid transparent; }
.lock a:hover { border-bottom-color: currentColor; }

/* --- the three doors ------------------------------------------------------ */

.doors {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
@media (min-width: 46rem) {
  .doors { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 68rem) {
  .doors { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.door {
  background: var(--bg-2);
  padding: clamp(1.25rem, 3vw, 1.85rem);
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  min-width: 0;
}
.door__title {
  font-family: var(--mono);
  font-size: 1.125rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0;
}
.door__note {
  margin: 0;
  font-size: 0.875rem;
  color: var(--ink-2);
}
.door__submit { margin-top: auto; }

/* --- fields --------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; }
.field__label {
  font: 600 0.6875rem/1.4 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
/* Same retirement as the placeholder below, and this one was failing SILENTLY
   on both palettes: at opacity 0.7 it measured 3.86:1 on the ramp ground and
   3.23:1 on the restored one, and no lightness of --ink-3 rescues it (even a
   two-step lift only reaches 4.19). The de-emphasis is carried by WEIGHT
   instead, which costs no contrast at all: the parent .field__label is 600, so
   400 here reads as clearly secondary while the colour stays at a measured
   5.22:1 on the card it sits on. */
.field__optional {
  color: var(--ink-3);
  font-weight: 400;
  letter-spacing: 0.1em;
}
.field__input {
  width: 100%;
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--line-2);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 0.9375rem;
  padding: 0.7rem 0.8rem;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
}
.field__input:focus {
  outline: 2px solid var(--accent-text);
  outline-offset: 1px;
  border-color: var(--accent-text);
}
/* NO OPACITY, AND THAT IS THE FIX RATHER THAN A TUNING OF IT. This one
   declaration has now failed its contrast floor on THREE successive palettes --
   2.79:1 at opacity 0.6 on the pre-ramp ground, 3.15:1 at 0.6 on the ramp
   ground, and 4.02:1 at 0.8 on the restored ground -- because `opacity`
   composites against whatever is behind it, so the number nobody can see in
   this line changes every time a ground moves. Three palettes is enough
   evidence: the mechanism is the defect, not the value.
   --ink-3 at FULL strength measures 5.63:1 on the field's own fill, and the
   distinction the opacity was drawing is drawn better by what it is next to --
   a typed value is --ink at 16.12:1, which is a 2.9x luminance gap. */
.field__input::placeholder { color: var(--ink-3); }
.field__input--code { letter-spacing: 0.14em; text-transform: uppercase; }
.field__input--area { resize: vertical; line-height: 1.5; font-family: var(--sans); }
.field__hint { font-size: 0.75rem; color: var(--ink-3); }

/* --- .loadbar — the site's loading indicator ------------------------------
   A NAMED, REUSABLE COMPONENT, not a one-off for the sign-in form (owner
   2026-08-27: "when signing in their needs to be a loading bar", and "we will
   need a loading page stub anyway later"). Nothing about it knows what it is
   waiting for.

   TO USE IT ON ANY PAGE, paste this and nothing else:

       <div class="loadbar" role="progressbar" aria-label="Loading" hidden>
         <span class="loadbar__fill"></span>
       </div>

   and toggle the `hidden` attribute. Inside a <form> you do not even do that:
   auth.js's busy() finds `.loadbar` within the form it is given and drives it
   for you, so a future form gets its loading bar by adding markup only.
   Authored `hidden`, so a page with scripts off never shows a bar that can
   never finish.

   It is INDETERMINATE on purpose -- /api/login reports no progress, and a bar
   that fakes a percentage is lying about something the visitor is anxious
   about. It sweeps to say "still working" and nothing more. */
.loadbar {
  /* 6px, not 3: `* { box-sizing: border-box }` at the top of this file means
     the 1px hairline is taken OUT of the height, so a 3px track left a 1px
     accent line -- measured, and far too faint for the one control the owner
     asked to be able to see. 6px leaves a 4px sweep against a 1px frame. */
  height: 6px;
  /* The track is PINNED, not inherited from the surface ladder. It used to be
     spelled `var(--bg-2)`, which was tier 5 (#000000) under the ramp and read
     2.67:1 against a tier-2 bar. When the 2026-08-29 black-ground ruling put
     the near-black ladder back, --bg-2 became #141c25 and that same unchanged
     line would silently have dropped the bar to 2.19:1 -- a legibility
     regression with no diff to show for it. So the one control the owner asked
     to be able to see reads its own token, and 2.67:1 is preserved exactly.
     See --loadbar-track in :root for why 2.67 is a ceiling, not a compromise. */
  background: var(--loadbar-track);
  border: 1px solid var(--line);
  /* the sweep is a transform that runs the child past both ends of the track */
  overflow: hidden;
}
.loadbar__fill {
  display: block;
  width: 34%;
  height: 100%;
  /* TIER 2 EXACTLY - "all loading bars should be 2" (owner, 2026-08-29). It
     used to be --accent-text, which followed the accent wherever that went; it
     no longer does, because the owner pinned this one by number. */
  background: var(--loadbar);
  animation: loadbar-sweep 1.05s ease-in-out infinite;
}
@keyframes loadbar-sweep {
  from { transform: translateX(-105%); }
  to   { transform: translateX(400%); }
}
/* Reduced motion gets a STATIC FULL BAR, not a fast one. The global reduce
   block near the top of this file clamps every animation to 0.01ms, which
   would leave this one flickering at the start of its sweep forever -- worse
   than the motion it was avoiding. `animation-name: none` stops it outright
   whatever the duration says, and a filled bar still reads as "working". */
@media (prefers-reduced-motion: reduce) {
  .loadbar__fill { width: 100%; animation: none; }
}

/* --- .spinner — the INDETERMINATE loading ring ----------------------------
   A gradient ring that rotates: a disc filled with the magenta→blurple sweep,
   with a concentric cutout of the SURFACE colour punched out of the middle so
   only the ring shows. Because both are concentric circles the cutout is
   rotation-invariant, so spinning the disc animates the gradient and nothing
   else. Owner intent: "loading" — ported from the launcher spinner ticket
   (2026-08-28) as the WEBSITE's indeterminate indicator.

   .loadbar (above) stays for DETERMINATE-feeling, in-form progress (the two-step
   sign-in). This is for the honest INDETERMINATE waits: the initial /api/me
   account check (auth.js wires it below), and any future one.

   THREE knobs, all tokens so a placement re-colours by variable, never by fork:
     --spin-size     the diameter (60px base; .spinner--sm is 20px inline).
     --spin-from/-to the gradient stops. Default to the brand ring
                     (--spin-near → --spin-far), which under the 2026-08-29
                     ramp is TIER 1 → TIER 2 — both of them the owner's own
                     anchors, where this used to be a fixed magenta → blurple
                     pair from the retired palette. It stays a SWEEP rather
                     than collapsing to the single tier-2 fill, and that is
                     functional, not decorative: the ring animates by ROTATING,
                     and a ring of one flat colour is rotation-invariant, so a
                     flat ring would show no motion whatsoever.
     --spin-surface  the colour of the cut-out centre. It MUST match whatever
                     the ring sits on or the ring reads as a dark dot — so it is
                     a variable, set per placement (defaults to --bg). */
.spinner {
  --spin-size: 60px;
  --spin-from: var(--spin-near);
  --spin-to: var(--spin-far);
  --spin-surface: var(--bg);
  display: inline-block;
  vertical-align: middle;
  flex: none;
  width: var(--spin-size);
  height: var(--spin-size);
  border-radius: 50%;
  background: linear-gradient(var(--spin-from), var(--spin-to));
  position: relative;
  animation: spinner-rotate 0.5s linear infinite;
}
.spinner::before {
  content: "";
  position: absolute;
  /* 7.5% each side -> an 85% cut-out, the thin ring the ticket specifies */
  inset: 7.5%;
  border-radius: 50%;
  background: var(--spin-surface);
}
/* Small inline variant — beside "Checking your account…" etc. At 20px a 7.5%
   inset is a 1.5px ring that all but vanishes, so the small ring is thicker. */
.spinner--sm { --spin-size: 20px; }
.spinner--sm::before { inset: 14%; }
@keyframes spinner-rotate { to { transform: rotate(360deg); } }

/* Reduced motion gets a STATIC ring, not a fast one — same reasoning as
   .loadbar above: the global reduce block clamps every animation to 0.01ms,
   which would leave this spinning invisibly-fast forever (worse than the
   motion). `animation: none` stops it outright whatever the duration says, and
   a gradient ring still reads as "loading" while it is paired with a text
   label. An infinite fast spin is exactly what this media query exists for. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; }
}

/* The initial account-check row (account page): a small spinner + a mono label,
   authored hidden and revealed by auth.js only while the first /api/me is in
   flight. Collapsed with [hidden] the rest of the time. */
.checking {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin: 0 0 1.75rem;
  font: 600 0.75rem/1.4 var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.checking .spinner--sm { --spin-surface: var(--bg); }

/* --- form messages -------------------------------------------------------- */

.formmsg {
  margin: 0;
  min-height: 1.2em;
  font: 500 0.8125rem/1.5 var(--mono);
  color: var(--ink-2);
  overflow-wrap: anywhere;
}
.formmsg--bad  { color: var(--draft); }
.formmsg--good { color: var(--live); }

/* The signed-in panel's message line lives OUTSIDE the panel (2026-08-25): it
   has to outlive it, because "sign out everywhere" hides the panel and then
   reports whether the game devices were really revoked. These two rules are
   what stop that costing anything:
     - the panel's own padding and gap, re-applied, so a line that is no longer
       inside the box still lines up with the box's text instead of its border;
     - :empty collapses it to zero, so the signed-out visitor -- who is every
       first-time visitor -- does not get a blank gap above the sign-in door. */
#signedin-msg {
  margin-top: 0.6rem;                             /* = .signedin's own gap */
  padding-inline: clamp(1.25rem, 3vw, 1.85rem);   /* = .signedin's padding */
}
#signedin-msg:empty { min-height: 0; margin-top: 0; }

/* --- signed-in panel ------------------------------------------------------ */

.signedin {
  border: 1px solid var(--line);
  border-left: 3px solid var(--live);
  background: var(--bg-2);
  padding: clamp(1.25rem, 3vw, 1.85rem);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
}
/* JUST SIGNED IN, as opposed to arriving already signed in. auth.js adds this
   only on the page view where the visitor actually submitted the form (owner
   2026-08-27: "it doesnt change enough for me to be comfortable"). The panel
   appearing is now a real change on its own -- the sign-in form genuinely
   disappears since [hidden] started working -- and this is the arrival marker
   on top of it: the accent takes over the live-green edge for the rest of the
   page view, with one slow settle so the eye is taken to it.
   The global reduce block clamps the settle to nothing; the accent edge is
   static and carries the meaning by itself, so nothing is lost without it. */
.signedin--fresh {
  border-left-color: var(--accent-text);
  animation: signedin-arrive 1.4s ease-out 1;
}
@keyframes signedin-arrive {
  from { background: var(--bg-3); border-color: var(--accent-text); }
  to   { background: var(--bg-2); }
}

.signedin__label {
  margin: 0;
  font: 600 0.6875rem/1.4 var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.signedin__name {
  margin: 0;
  font-family: var(--mono);
  font-size: clamp(1.25rem, 1.1rem + 0.7vw, 1.75rem);
  font-weight: 700;
  letter-spacing: 0.04em;
  overflow-wrap: anywhere;
}
.signedin__note { margin: 0; color: var(--ink-2); font-size: 0.9375rem; }
.signedin__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 0.4rem;
}
.signedin__actions .btn { width: auto; flex: 1 1 14rem; min-width: 0; }

/* A third button weight: quieter than ghost, for the destructive-ish action
   that should be findable without being inviting. */
.btn--quiet {
  background: transparent;
  border-color: var(--line);
  color: var(--ink-3);
}
.btn--quiet:hover { border-color: var(--draft); color: var(--draft); }

/* Fields and buttons inherit no font from the browser's form defaults, so the
   reset is explicit rather than assumed. */
button, input, textarea, select { font-family: inherit; font-size: 100%; }

/* ==========================================================================
   SEPARATE PAGES - added 2026-08-25, widened to five 2026-08-26, to six
   2026-08-28 (the forum) and to seven the same day (/device/). The dated
   paragraphs below are the running history.

   The portal became four pages (download, account, create account, what you
   need) on the owner's ruling that kwaze needed "the same treatment" the
   studio site got: real pages, not places on one scroll. Everything below
   serves that split and nothing else -- the nav, the current-page marker, the
   page-level heading that takes over from the in-page section head, and the
   two door counts the split created. No new palette and no new geometry: the
   nav is the section-head's own mono-uppercase voice at label size, so the
   split costs the design nothing.

   2026-08-26: five pages, and four again from 2026-08-27 when the owner
   retired /requirements/. The front page and the download tab came apart
   (owner: "the kwaze front page should be separate from the download tab so
   split"), so / is now the hero alone and the bays live at /download/. The
   only rule that needed adding is .hero__cta, at the bottom of this block --
   the nav below simply carries one more entry, and .page__title took over
   the moved section's head the same way it took over /account/'s.

   2026-08-28: SIX pages. The forum added /forum/ and /forum/thread/, and the
   nav below carries a fifth entry. The pattern held a third time: no rule in
   this block needed changing, because .nav is a flex row that simply takes
   another child and .page__title was already the heading both new pages use.
   The forum's own classes are a separate block at the very bottom of this
   file. NOTE for whoever counts next: the thread page's aria-current sits on
   Forum, not on a tab of its own -- a thread is a place INSIDE the forum --
   so there are six pages and five tabs, and those two numbers are meant to
   differ.

   2026-08-28, later: SEVEN pages. /device/ is where a signed-in person
   approves the code their launcher is showing. It is the first page that is
   not a TAB -- there is no nav entry for it, because you arrive from a
   launcher rather than from the masthead -- so the running count is now seven
   pages, five tabs and SIX aria-current markers, and all three numbers are
   meant to differ. The pattern held a fourth time: this block needed no rule
   and neither did any other. /device/ is built entirely from classes that were
   already here (.doors/.door/.field/.btn/.callout/.formmsg/.loadbar/.checking,
   and .signedin reused as the confirmation panel), which is also why this
   file's cache-bust was UNCHANGED by that page -- it stayed at ?v=kwaze2 --
   because nothing in here moved except these comments, and bumping the query
   on seven pages would have thrown away every warm cache to ship a paragraph.
   (2026-08-29: the palette ruling DID move this file, top to bottom, and did
   bump all seven pages to ?v=kwaze3. A real repaint is precisely the case the
   bump exists for, so the reasoning above is unchanged -- only its answer is.
   The SAME DAY, the owner's black-ground ruling repainted it again and took
   the query to ?v=kwaze4 on all seven; the forum-editing ruling later the same
   day added the edit-stamp block to the forum section at the bottom of this
   file and took it to
   ?v=kwaze5, which is where it stands. auth.js was deliberately NOT bumped with
   any of them: that file changed in none of these passes, and bumping an
   unchanged asset is the exact waste the paragraph above is about. forum.js DID
   change in the editing pass and moved kwaze1 -> kwaze5 with this file, so the
   two assets that changed together carry the same word. Later the same day the
   cross-domain sign-in pass changed auth.js and nothing else -- it learned to
   honour /account/?next= and hand the visitor back to surreallabs.net -- so
   auth.js alone moved kwaze3 -> kwaze6 on all seven pages while THIS file
   stayed at kwaze5, which is the same rule read the other way round.)
   ========================================================================== */

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.35rem;
  font: 600 0.75rem/1.4 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.nav a {
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
}
.nav a:hover { color: var(--accent-text); border-bottom-color: var(--accent-text); }

/* The page you are on is marked by INK WEIGHT AND A PERSISTENT UNDERLINE — the
   surreallabs masthead's exact pattern, on this palette's ink (owner 2026-08-25:
   "make the kwaze site banner tabs more like surreallabs"). Not colour alone:
   weight + underline carry it for a visitor who cannot separate the accent from
   slate, and the accent stays the hover voice, never the resting one. */
.nav a[aria-current="page"] {
  color: var(--ink);
  font-weight: 700;
  border-bottom-color: var(--ink);
}
.nav a[aria-current="page"]:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* A page's own title, where the one page had a numbered section head. Same
   mono-uppercase voice as .section__title, one step up in size because it is
   now the page's title rather than one of three stops on it. */
.page__title {
  font-family: var(--mono);
  font-size: clamp(1.375rem, 1.15rem + 1vw, 1.875rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin: 0;
}

/* Door counts after the split. .doors was written for the one page that held
   all three, so its 68rem rule is three columns -- which would park the lone
   sign-in door in column one of an empty three-column grid and read as a
   layout bug rather than a decision.

   These two sit AFTER .doors' own breakpoints deliberately: same specificity,
   so later source order is what makes them win, and a modifier outside any
   media query therefore beats .doors at every width. */
.doors--one { grid-template-columns: minmax(0, 1fr); max-width: 34rem; }
@media (min-width: 68rem) {
  .doors--two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The front page's one route onward, added 2026-08-26 with the / vs /download/
   split. No new component: the control inside is the site's own
   .btn.btn--primary, exactly the one the bays use, so the front page promises
   the same thing the download buttons deliver.

   This wrapper exists for one reason and it is not decoration: .btn is
   `width: 100%` (it was written to fill a bay column), so an unwrapped button
   would stretch the full width of the hero's .wrap and read as a banner rather
   than a button. The cap matches .hero__lead's own measure, and the margin is
   .featured's, so the hero keeps one rhythm down its left edge. */
.hero__cta { margin-top: 2.5rem; max-width: 26rem; }

/* The front page's games grid, added 2026-08-27 with the games section (owner:
   "ourworld needs to be on the launcher landing page along with other games i
   might make").

   .bays' own 42rem rule is a FIXED two columns, which parks a lone game card
   beside an empty cell -- and .bays paints its own background with the line
   colour to draw the hairline gutters, so that empty cell is not blank space,
   it is a solid slab. Exactly the defect .doors--one was written for.

   auto-fit rather than a count modifier, and that is the whole point: adding
   the second game has to be a copy of the <article> and NOTHING ELSE, so the
   grid has to work out its own column count. One card fills the row, two sit
   side by side, three wrap. min(22rem, 100%) is what stops the track forcing
   a horizontal scrollbar below 22rem -- a bare 22rem minimum would.

   It sits AFTER .bays' breakpoint at equal specificity, so source order is
   what makes it win, and being outside any media query it wins at every
   width -- the same construction the .doors--one and .doors--two modifiers
   use, and for the same reason. (Named rather than pointed at: a "see above"
   rots the next time anything between them moves.) */
.bays--games { grid-template-columns: repeat(auto-fit, minmax(min(22rem, 100%), 1fr)); }

/* ==========================================================================
   FORUM - added 2026-08-28 (owner: "then add a forum to the website" +
   "the forum should require an account obviously no annonimous posting").

   NO NEW PALETTE AND NO NEW GEOMETRY. Every colour below is one of this
   file's own tokens, and the two lists reuse .doors' construction exactly:
   1px grid gaps over a --line ground, so the dividers are real hairlines
   rather than borders that double up at the seams. The forum therefore costs
   the design nothing and inherits every future token change for free.

   THE ACCENT SPLIT IS OBSERVED. Thread titles and pager links take
   --accent-text (5.73:1 on --bg); --accent is the FILL only and is never used
   as text here - as text it measures 2.02:1, which is a fail, and the .btn
   rules above already carry the one legitimate use of it (a light
   --on-accent label on the deep fill).

   DEPENDENCY WORTH STATING: every state switch here is the `hidden`
   attribute, which works only because of the attribute rule near the top of
   this file. Two of the classes below set `display:`, so they inherit that
   dependency. Do not add a forum rule that beats it.
   ========================================================================== */

/* --- the board ----------------------------------------------------------- */

.threadlist { margin-top: 0.5rem; }

.threadlist__items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}

.threadrow {
  background: var(--bg-2);
  padding: clamp(0.9rem, 2.2vw, 1.25rem);
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  min-width: 0;
}
.threadrow:hover { background: var(--bg-3); }

.threadrow__title {
  color: var(--accent-text);
  font-family: var(--mono);
  font-size: 0.9375rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-decoration: none;
  /* A 120-character title is one line at this size on a wide screen and wraps
     on a narrow one. `anywhere` is what stops an unbroken 120-character run
     pushing the row wider than its column. */
  overflow-wrap: anywhere;
}
.threadrow__title:hover { color: var(--accent-text-hover); text-decoration: underline; }

.threadrow__meta {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.5rem;
  font: 500 0.75rem/1.5 var(--mono);
  color: var(--ink-3);
}
.threadrow__author { color: var(--ink-2); }
.threadrow__sep { color: var(--line-2); }
/* Two one-word STATES under a board row's title, drawn identically because
   they are the same kind of thing. EDITED is the ruling's marker on the board;
   the when and who stay off the list and live on the thread. */
.threadrow__locked,
.threadrow__edited { color: var(--draft); text-transform: uppercase; letter-spacing: 0.1em; }

/* --- one thread ---------------------------------------------------------- */

.threadback {
  margin: 0 0 1.25rem;
  font: 600 0.75rem/1.4 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.threadback a { color: var(--ink-2); text-decoration: none; }
.threadback a:hover { color: var(--accent-text); }

.postlist {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  margin-top: 0.5rem;
}

.post {
  background: var(--bg-2);
  padding: clamp(1rem, 2.5vw, 1.5rem);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
}

.post__head {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.5rem;
  font: 500 0.75rem/1.5 var(--mono);
  color: var(--ink-3);
}
.post__author { color: var(--ink); font-weight: 700; letter-spacing: 0.04em; }
.post__sep { color: var(--line-2); }
.post__when { color: var(--ink-3); }

/* THE ONE DECLARATION THAT MAKES PLAIN TEXT READABLE, and it is load-bearing
   rather than cosmetic. `pre-wrap` preserves the line breaks the author typed
   - which is the ONLY formatting this forum has, because bodies are plain
   text with no markdown, no HTML and no BBCode. `anywhere` is what stops a
   2,000-character unbroken string (a pasted URL, a wall of one repeated
   character) blowing the column out and giving the whole page a horizontal
   scrollbar. */
.post__body {
  margin: 0;
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* A tombstone. The author and the timestamp stay - that is what keeps the
   replies around it making sense - and only the body reads as absent. */
.post--deleted .post__body { color: var(--ink-3); font-style: italic; }

.post__actions { margin: 0; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.post__action {
  background: transparent;
  border: 1px solid var(--line-2);
  color: var(--ink-3);
  font: 600 0.6875rem/1 var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 0.4rem 0.6rem;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}
.post__action:hover { border-color: var(--draft); color: var(--draft); }
.post__action:disabled { opacity: 0.5; cursor: default; border-color: var(--line); color: var(--ink-3); }

/* --- the edit stamps (owner ruling 2026-08-29) ----------------------------
   "then let me edit threads and make shure it shows when it was changed and
   who changed it t the bottom right of the post as well as it clearly saying
   edited under the titlle". Two thirds of that ruling is POSITIONAL and
   therefore lives here rather than in forum.js: the WORD goes under the title,
   the WHEN and WHO go bottom-right of the post.

   NO NEW COLOUR IS DECLARED. --draft is already this file's state-word colour
   (it draws the board's LOCKED marker) and --ink-3 is already its muted
   metadata tier; both were re-measured against the restored grounds in the
   token block at the top, so nothing here needs a new measurement either. */

.edited {
  /* ITS OWN LINE UNDER THE <h1>, not a word beside it. .section__head is a
     wrapping flex row, so a full basis is what puts this under the title
     rather than after it — and when edited_at is null the element is `hidden`,
     which carries display:none !important, so it takes no line at all and the
     heading and meta go back to sharing one. */
  flex: 0 0 100%;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.5rem;
  font: 500 0.75rem/1.5 var(--mono);
}

/* THE WORD, and it is the loud half on purpose — the ruling asks for it
   "clearly". Uppercase, tracked, in the same --draft the LOCKED marker uses:
   the one colour in this file that already means "a state, not a link".
   7.17 / 6.65 / 6.01 against the three grounds, so it clears the 4.5 floor on
   all of them at this size. */
.edited__word {
  color: var(--draft);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

/* THE STAMP, and it is the QUIET half: this is metadata, and it sits in the
   same muted tier the post timestamps already use. */
.edited__stamp {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem;
  color: var(--ink-3);
}
/* "by" is a WORD, not a separator, so it is pinned to the muted TEXT tier and
   deliberately NOT to --line-2 like .post__sep: --line-2 measures 3.34 on a
   card, which is a control-boundary floor and not a text one. Spelled out
   rather than left to inheritance so the next person moving colours around can
   see which floor this one answers to. */
.edited__joiner { color: var(--ink-3); }
.edited__who { color: var(--ink-2); }

/* THE POST FOOTER: what you may do on the left, what was done to it on the
   right. Absent entirely when a post has neither. */
.post__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
}

/* BOTTOM RIGHT — the ruling's own words — and `margin-left:auto` is what makes
   it right rather than a text-align. An auto margin still pushes the item to
   the end of the line it WRAPS ONTO, which is what happens on a narrow screen
   when the actions row and the stamp can no longer share one; a text-align
   would only align text inside a box that had already been placed left. */
.post__edited {
  margin: 0 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: baseline;
  gap: 0.2rem 0.4rem;
  font: 500 0.6875rem/1.5 var(--mono);
  text-align: right;
}

/* --- the in-place editor -------------------------------------------------
   NO MODAL AND NO NEW COMPONENT: the field is .field__input, the buttons are
   .post__action, and this is only the box that stacks them. */
.editor {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
}
/* The title editor stands in for the <h1> INSIDE .section__head's flex row,
   so it takes the whole line the heading had. */
.editor--title { flex: 0 0 100%; }

/* --- pager and empty state ----------------------------------------------- */

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1.25rem;
  margin-top: 1.25rem;
  font: 600 0.75rem/1.4 var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.pager__link { color: var(--accent-text); text-decoration: none; border-bottom: 1px solid transparent; }
.pager__link:hover { color: var(--accent-text-hover); border-bottom-color: currentColor; }
.pager__where { color: var(--ink-3); }
/* An EMPTY pager collapses rather than leaving a gap: forum.js writes nothing
   into it on a one-page board, and :empty is what keeps that honest. */
.pager:empty { margin-top: 0; }

.empty {
  margin: 0;
  padding: clamp(1.5rem, 4vw, 2.5rem);
  border: 1px solid var(--line);
  background: var(--bg-2);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 0.875rem;
  text-align: center;
}

/* The posting form and the signed-out prompt sit UNDER the list, with room
   above them. Both forum pages carry a .doors--one wrapper for the form, which
   is the existing one-column construction from /account/ - so there is no new
   rule for the form itself, only this separation. */
.threadlist + .pager,
.postlist + .pager { margin-bottom: 1.5rem; }
.callout[data-signed-out-only],
.doors[data-needs-session] { margin-top: 1.5rem; }
