/* --- style.css --- */
/* ---- Inter, self-hosted (2026-08-28) ---------------------------------
   Latin-subset, weight-axis (100-900) VARIABLE Inter, vendored in
   fonts/ -- see fonts/README.md and fonts/inter-OFL.txt (SIL OFL 1.1).
   The font stack has always named "Inter" first, but nothing ever
   loaded it, so the font-feature-settings on `body` below (cv05 / cv08 /
   ss03) was a no-op for every visitor who did not happen to have Inter
   installed. This makes it real. Two files, ~98KB total; font-display:
   swap, so first paint is never blocked -- a brief fallback flash is the
   right trade for that. Italic is a real cut, not a synthesised slant,
   which matters on a site this dense with cited <em> titles. */
@font-face {
  font-family: "InterVariable";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/inter-latin-wght-normal.woff2") format("woff2");
}
@font-face {
  font-family: "InterVariable";
  font-style: italic;
  font-weight: 100 900;
  font-display: swap;
  src: url("../fonts/inter-latin-wght-italic.woff2") format("woff2");
}

:root {
  /* Tells the browser which palette its OWN surfaces should use: form-control chrome, date
     pickers, <select> menus, checkboxes, autofill, scrollbar tracks, the overscroll canvas.
     Without it a dark page showed light native controls (and a light page dark ones in some
     engines). Follows the theme in the three places the tokens do: here, the light media
     block, and theme.css's explicit Light / Dark / Dim. */
  color-scheme: dark;
  --bg: #121a2e;
  --panel: #1a2440;
  --text: #e8ecf6;
  --muted: #9aa5bd;
  --violet: #a78bfa;
  --teal: #2dd4bf;
  /* Links get their OWN token rather than borrowing --teal. In dark mode the
     two are the same value; in light mode --teal has to stay bright enough to
     work as an accent on bars, gradients and borders, while a link has to clear
     4.5:1 as body text. One variable cannot do both jobs, and making --teal
     dark enough for text would drag the brand accent down with it site-wide. */
  --link: #2dd4bf;
  --border: #2d3b5e;
  --green: #4ade80;
  --yellow: #facc15;
  --red: #f87171;
  --nav-bg: rgba(18, 26, 46, 0.82);
  --dot: rgba(167, 139, 250, 0.07);
  /* THE GRAIN (researched top-10 item 7, 2026-09-23). One texture, shared
     by style.css's own body::before AND vivid.css's richer override of it
     -- a custom property rather than the same long data: URI pasted twice,
     since it is genuinely the same image both times, not two things that
     happen to look alike. Theme-independent on purpose: unlike --glow1/
     --glow2/--dot above, it is never re-declared under the light-mode or
     explicit-theme blocks, so every appearance state inherits this one
     value from here. See style.css's body::before for what it does and
     why a plain, low, fixed alpha rather than mix-blend-mode: overlay. */
  --grain-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch' result='n'/%3E%3CfeColorMatrix in='n' type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.035 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  --glow1: rgba(139, 92, 246, 0.22);
  --glow2: rgba(45, 212, 191, 0.15);
  --shadow: rgba(0, 0, 0, 0.3);
  --h1-a: #e8ecf6;
  --h1-b: #a78bfa;
  --h1-c: #2dd4bf;
  --rowhover: rgba(148, 163, 184, 0.06);

  /* Surface ladder. --bg is the canvas, --panel is surface-1 (every card,
     hero, input). --surface-2 / -3 are the deeper steps for a surface that
     sits ON a panel -- a code chip, a formula block, a nested well -- so
     nesting reads by tone rather than by stacking a shadow. Additive, in the
     same spirit as the --sp / --r scale above: nothing is forced onto it,
     it exists so a nested surface has a named step to reach for. `code` and
     `.formula` are the first two things that use it. */
  --surface-2: #202c4a;
  --surface-3: #283759;

  /* ---- Spacing / radius / elevation scale (added 2026-08-23, item 07 of
     "The Ladder"). Additive only: the ~700 existing ad-hoc px values in this
     file are untouched -- refactoring every one to match would be a
     22-page-blast-radius change for zero reader-visible benefit. New shared
     components (starting with the rung rail below) use these; the scale
     exists so the NEXT one doesn't have to invent its own numbers.
     Theme-independent by design (spacing and radius don't change between
     light and dark); elevation composes var(--shadow), which already does. */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 28px; --sp-7: 40px;
  --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-pill: 999px;
  --elev-1: 0 1px 2px var(--shadow);
  --elev-2: 0 10px 28px var(--shadow);
}
@media (prefers-color-scheme: light) {
  :root {
    color-scheme: light;
    --bg: #f8fafc;
    --panel: #ffffff;
    --text: #0f172a;
    --muted: #526180;
    --violet: #7c3aed;
    /* teal-600 #0d9488 measured 3.58:1 on --bg and 3.74:1 on --panel. Both are
       under the 4.5:1 minimum for body text, and a light-mode sweep of every
       text node on all 17 pages found 88 elements failing on exactly this one
       value -- h4 headings, bold runs, active .preset buttons, em, eyebrows,
       measurement tags, the archive question numbers. One variable, 88
       failures, thirteen pages.
       teal-700 is one step darker, still unmistakably the same colour, and
       measures 5.23:1 on --bg and 5.47:1 on --panel. It is also the exact value
       pqc.html had already been patching in page-locally three separate times,
       so this promotes a proven local fix to source rather than inventing a new
       one. Non-text uses (accent bars, borders, focus rings, accent-color) only
       get darker, which on a light background is neutral-to-better.
       --h1-c and --glow2 are separate tokens and are deliberately untouched.
       DARK MODE IS UNCHANGED -- this lives only inside the light media query. */
    --teal: #0f766e;
    /* Links keep their OWN token even though it currently equals --teal in both
       modes. --teal is the brand accent and may legitimately be brightened
       later for bars and glows; link text may not, because it carries a hard
       4.5:1 floor. Splitting them now means that future change cannot silently
       reintroduce this bug. */
    --link: #0f766e;
    --border: #dbe2ef;
    --nav-bg: rgba(248, 250, 252, 0.85);
    --dot: rgba(124, 58, 237, 0.06);
    --glow1: rgba(124, 58, 237, 0.08);
    --glow2: rgba(13, 148, 136, 0.07);
    --shadow: rgba(15, 23, 42, 0.08);
    --h1-a: #0f172a;
    --h1-b: #7c3aed;
    --h1-c: #0d9488;
    --rowhover: rgba(15, 23, 42, 0.04);
    /* The three semantic colours were never given light-mode values, so on a
       light background they measured green 1.67:1, yellow 1.46:1 and red
       2.64:1 against #f8fafc — all far under the 4.5:1 minimum, and the first
       two effectively invisible. That is the 🟢🟡🔴 tier system, which is the
       brand's central teaching device, unreadable for every visitor whose
       device is set to light. Measured, then replaced with the darkest
       versions that still read as the same colour: 4.79 / 4.80 / 6.18:1.
       Dark mode is untouched — these live only inside this media query. */
    --green: #15803d;
    --yellow: #b45309;
    --red: #b91c1c;
    /* Surface ladder, light. --panel is white; a surface sitting on it steps
       DOWN in lightness (toward the canvas and past it) so it reads as inset. */
    --surface-2: #f1f5fb;
    --surface-3: #e9eff8;
  }
}
* { box-sizing: border-box; margin: 0; padding: 0; }
/* Form controls do not inherit font-family from the page -- the UA stylesheet
   pins <button>/<input> to a platform default (Arial on Windows). Almost every
   button component on the site already works around this with `font: inherit`,
   but a handful never did (#navtrig, bare .preset, the Bloch gate buttons, the
   daily-question options, the qubit-pill ket). Harmless while the body font was
   ~Segoe UI anyway; visible now that InterVariable actually loads. One reset,
   family only -- components keep whatever size/weight they set. */
button, input, select, textarea { font-family: inherit; }
/* Smooth scrolling plus a sticky header means every in-page anchor has to be
   told about the header, or it lands underneath it. This site runs almost
   entirely on deep links -- the nav chips point at widget ids, the Ledger at
   #c-<slug>, the Frontier at #q-<slug>, the two curriculum pages at #t01..#t24
   -- so "the thing you clicked is behind the bar" is not an edge case here, it
   is the main path. --nav-h carries a static default that is correct for the
   one-row header at every width it is sticky at; alive.js replaces it with the
   measured height (and keeps it measured through resize), so a header that
   wraps to two rows still parks its targets in the open. Below 720px the header
   is not sticky, so the padding is deliberately zero. */
html { scroll-behavior: smooth; }
/* THE TYPE SIZE (2026-09-24). Chinmoy: "people should not have to zoom in to
   check what's written there or strain their eyes." Every font-size on the
   site is in rem, so the root is the one dial that moves all of it: body text
   goes 16px -> 17px on a phone and 18px from 720px up, and every label,
   caption and control scales in proportion. A PERCENTAGE, not px, so a reader
   who has already raised their browser's default size keeps that on top. */
/* 2026-09-25: phones went 17px -> 18px as well (Chinmoy: "increase fonts for mobile").
   Measured first: on a 375px viewport 72% of all text was the 17px body and a quarter
   was the 14.5px captions, notes and 12px labels, which is where a phone reader squints.
   One root, one dial: the whole hierarchy moves together. */
html { font-size: 112.5%; }
:root { --nav-h: 82px; }
@media (min-width: 720px) {
  html { scroll-padding-top: calc(var(--nav-h) + 14px); }
}
body {
  background: var(--bg);
  color: var(--text);
  font-family: InterVariable, Inter, "Segoe UI", -apple-system, system-ui, sans-serif;
  /* Inter character variants: cv05 gives the lowercase l a tail and cv08
     puts serifs on the uppercase I, so l / I / 1 stop colliding -- worth
     having on a site this dense with notation and ket vectors. ss03 rounds
     the quotes and commas. All three are no-ops on Segoe UI / San Francisco
     (the fonts most visitors actually get -- Inter is named but not loaded),
     and font-feature-settings does not switch off the default kern/liga/calt.
     If Inter is ever self-hosted, this is already in place. */
  font-feature-settings: "cv05" 1, "cv08" 1, "ss03" 1;
  font-optical-sizing: auto;
  line-height: 1.65;
  /* Breaks a word only when it cannot fit on a line by itself. With the
     larger type (2026-09-24) two did not on a 375px phone and pushed the
     whole page sideways: analog.html's "disagree/disagree/..." run and a
     file path in <code> on ledger.html. */
  overflow-wrap: break-word;
}
/* ambient layer: two soft glows + the lattice dot grid (the Lattice IS the
   brand) + a film-grain texture (2026-09-23, researched top-10 item 7).

   A wide radial gradient over a near-black ground is exactly where 8-bit
   colour BANDS -- visible steps instead of a smooth fade, worst on the
   cheap displays a reader is likeliest to be on. A few percent of noise
   breaks the banding up by dithering the boundary, which is the same
   reason film stock and printed halftones never band the way flat digital
   colour does; it reads as texture, not decoration.

   Self-contained: an SVG data: URI (img-src already allows data:, so this
   needs no CSP change) with feTurbulence generating the noise and
   feColorMatrix converting it straight to a LOW, FIXED alpha -- white
   speckle at ~3.5%, baked into the image itself rather than tuned via a
   separate CSS opacity, so it is the same texture in every browser that
   can rasterise an SVG filter, not a second moving part to keep in sync.
   Composited NORMALLY, not blended: mix-blend-mode: overlay reads more
   "correct" in photography but doubles the surface this rule has to be
   proven safe on, and normal compositing at this alpha is close enough to
   invisible in light mode that it costs nothing to leave unconditional. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    radial-gradient(640px circle at 12% -4%, var(--glow1), transparent 62%),
    radial-gradient(760px circle at 88% 104%, var(--glow2), transparent 62%),
    radial-gradient(circle, var(--dot) 1px, transparent 1.5px),
    var(--grain-url);
  background-size: auto, auto, 26px 26px, 140px 140px;
}
/* hairline accent bar, drifting slowly */
body::after {
  content: "";
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: 60;
  background: linear-gradient(90deg, #8b5cf6, #2dd4bf, #8b5cf6);
  background-size: 200% 100%;
  animation: drift 14s linear infinite;
}
.wrap { max-width: 780px; margin: 0 auto; padding: 0 20px 80px; }
/* Fixed at 780px regardless of viewport, the whole page (nav included, since
   it lives inside .wrap) sat in a narrow centre column with dead margins on
   any screen wider than a tablet -- reading text that size is fine, but every
   grid, table and card on the site was still capped to that width too,
   so widescreen desktops never got to use the extra room. Widened in two
   steps rather than one jump so very large monitors don't overshoot into
   uncomfortably long text lines.

   2026-09-24: the 1024-1439 step went 880px -> 1180px with the type-size
   change above. At 18px the header needs ~942px to stay one row, and an
   880px column wrapped it to two on every page. Those widths have no rails
   in the gutters (rails.css starts at 1440), so the column simply takes the
   screen, the way index.html's own home.css override already did; running
   prose is still held to THE MEASURE below. 1440 and up stays 1040px
   because the rails are positioned against exactly that width. */
@media (min-width: 1024px) { .wrap { max-width: 1180px; } }
@media (min-width: 1440px) { .wrap { max-width: 1040px; } }

/* THE MEASURE. The two steps above did what that comment wanted for grids,
   tables, cards and games -- and carried the running prose along with them,
   which is the one thing that does not want the extra room. Measured
   2026-09-22 at 1440px: 99 characters per line (counted in '0' advances, so
   the true character count is higher still) against the 65-75 that long-form
   text is set at. A reader loses the start of the next line at that length.
   The fix keeps the container wide and caps only the running prose, so a
   table still spans 1040px while the paragraph above it does not. Headings
   are deliberately NOT capped: they are short, and h2 carries a drawn accent
   whose width is part of the section rule. */
.wrap { --measure: 70ch; }
@media (min-width: 1024px) {
  .wrap > p, .wrap > ul, .wrap > ol, .wrap > blockquote, .wrap > dl {
    max-width: var(--measure);
  }
}
/* :not(.rung-rail) on every bare `nav` rule below, header through mobile
   overrides: rung.js (2026-08-23) creates its own <nav class="rung-rail">
   landmark for "The Ladder", and a bare element selector matches BOTH navs,
   not just the header. Most of what leaked through was harmless -- .rung-
   rail's own class rule (higher specificity) already overrides display,
   gap, padding and margin -- but position and z-index are never redeclared
   there, so this header rule's position + z-index:40 passed straight
   through, giving the rail its OWN stacking context at the SAME z-index as
   the header nav. Tied z-index resolves by DOM order, and the rail comes
   after the header in the document, so it painted ON TOP of an open nav
   dropdown (.navpanel's z-index:45 only outranks siblings inside the
   header's own stacking context -- it says nothing about a second, later,
   equal-z-index context outside it). Found 2026-08-28: the dropdown was
   rendering behind the rail on any page carrying data-rung. */
nav:not(.rung-rail) {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 18px 0; border-bottom: 1px solid var(--border); margin-bottom: 40px;
  /* position:relative is the containing block the .navpanel dropdowns are
     anchored to. It USED to live in its own one-line rule further down this
     file (in the STRUCTURED NAV block, added 2026-08-07) -- which, being a
     later rule of identical specificity, silently overrode the
     `position: sticky` in the >=720px media query immediately below. The
     sticky, blurred header the design system documents therefore did not
     exist on any page for three weeks: it scrolled away like a static bar.
     Found 2026-08-28 by reading the computed value rather than the source.
     Declaring it HERE, before the media query, restores the cascade the
     media query was always written to win. */
  position: relative;
}
@media (min-width: 720px) {
  nav:not(.rung-rail) {
    position: sticky; top: 0; z-index: 40;
    background: var(--nav-bg);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
}
/* Historical note: the brand and the old Lattice trigger (now the category
   dropdowns/#navtrig) were once under the 44px touch minimum; the Path link
   still can't just grow, because its active-state underline has to stay
   tight to the text — so it keeps its box and grows an invisible 44px band
   inside itself instead. */
nav .brand { display: flex; align-items: center; gap: 10px; min-height: 44px; font-weight: 700; font-size: 1.1rem; text-decoration: none; color: var(--text); margin-right: auto; }
nav .brand svg { width: 30px; height: 30px; transition: transform 0.5s ease; }
nav .brand:hover svg { transform: rotate(90deg); }
nav .brand svg circle:nth-of-type(2) { animation: qpulse 3.6s ease-in-out infinite; }
nav .brand svg circle:nth-of-type(3) { animation: qpulse 3.6s ease-in-out 1.8s infinite; }
nav a.link { position: relative; color: var(--muted); text-decoration: none; font-size: 0.92rem; padding-bottom: 2px; border-bottom: 2px solid transparent; transition: color 0.15s ease, border-color 0.15s ease; }
nav a.link::after { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 44px; transform: translateY(-50%); }
nav a.link:hover { color: var(--text); }
nav a.link.active { color: var(--text); border-bottom-color: var(--teal); }
/* researched top-10 item 8, 2026-09-23: balanced headings, no orphaned
   last word; pretty-wrapped prose, no orphaned last word on a paragraph
   either. Both degrade to today's plain wrapping wherever unsupported --
   text-wrap is a value a non-supporting browser simply ignores, so this
   needed no @supports gate and touches no other rule on the page. */
h1, h2, h3 { text-wrap: balance; }
h1 {
  /* --h1-w: the headline's own FINAL weight, as a token rather than a bare
     number -- vivid.css overrides it to 800 on vivid pages via the same
     name, and the settle animation below reads it back rather than each
     file needing its own copy of the keyframes. */
  --h1-w: 700;
  font-size: 1.9rem; line-height: 1.25; margin-bottom: 10px;
  font-weight: var(--h1-w);
  background: linear-gradient(95deg, var(--h1-a) 30%, var(--h1-b) 72%, var(--h1-c) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: fadeUp 0.5s ease-out both;
}
/* researched top-10 item 9, 2026-09-23: the headline settles from a lighter
   weight to its real one, once, on arrival. Costs nothing extra to
   download -- fonts/inter-latin-wght-normal.woff2 is a true variable font
   (@font-face declares font-weight: 100 900), so this is the SAME file
   already loaded for every other weight on the page, just a different
   frame of it. A second, independent animation on the SAME element as
   fadeUp above -- the two are listed together, not two competing
   `animation` declarations, since a second `animation` shorthand would
   replace the first rather than add to it. font-weight is its own
   property, untouched by fadeUp's opacity/transform, so both run at once
   without fighting. Killed entirely by the site's existing blanket
   `*, *::before, *::after { animation: none !important }` reduced-motion
   rule below -- no separate guard needed here. */
@media (prefers-reduced-motion: no-preference) {
  /* ease-out, matching fadeUp on the same element -- not motion.css's
     --ease-out-quint, so style.css keeps needing nothing from a file it
     does not itself require. */
  h1 { animation: fadeUp 0.5s ease-out both, h1-settle 700ms ease-out both; }
}
@keyframes h1-settle { from { font-weight: 380; } to { font-weight: var(--h1-w); } }
h2 { font-size: 1.25rem; margin: 36px 0 12px; padding-left: 13px; position: relative; }
h2::before {
  content: "";
  position: absolute; left: 0; top: 0.22em; bottom: 0.22em;
  width: 3px; border-radius: 2px;
  background: linear-gradient(180deg, var(--violet), var(--teal));
}
p { margin-bottom: 14px; text-wrap: pretty; }
/* .lede is the same under-h1 intro role as .tagline -- about/apply/changelog/
   insights/privacy/standing/terms just wrote the other name for it. It had no
   rule of its own, so those seven pages were silently falling back to plain
   `p`: full-brightness text and no entrance animation, next to 23 pages that
   get the muted, spaced, fade-up treatment. One selector, not a rename across
   seven files, since the two names mean the same thing on the page. */
.tagline, .lede { color: var(--muted); font-size: 1.05rem; margin-bottom: 28px; animation: fadeUp 0.5s ease-out 0.08s both; }
/* The spine: one sentence, above the three-door router, answering the
   question the router itself cannot -- why quantum/OR/security are one site
   rather than three. Reuses h2::before's own violet->teal rule rather than
   inventing a second accent language: that gradient already means "the fork"
   everywhere else on the page. */
.spine-tag { font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); font-weight: 700; margin: 22px 0 7px; }
.spine {
  position: relative; padding-left: 15px; max-width: 66ch;
  font-size: .98rem; color: var(--muted); margin: 0 0 26px;
}
.spine::before {
  content: ""; position: absolute; left: 0; top: .2em; bottom: .2em;
  width: 3px; border-radius: 2px;
  background: linear-gradient(180deg, var(--violet), var(--teal));
}
.spine a { color: var(--text); text-decoration: underline; text-decoration-color: var(--border); text-underline-offset: 3px; }
.spine a:hover { text-decoration-color: var(--teal); }

/* The rung rail (rung.js). First real use of the item-07 token scale above:
   spacing and radius come from the sp and r-pill tokens defined near the
   top of this file, not new one-off numbers. Violet for the five linear
   rungs, teal for the fork -- the same split the homepage spine and
   analog.html already use, so the colour means the same thing everywhere
   it appears rather than being decided fresh per page.

   FOUND 2026-08-28, fixed in the same commit as the nav-scoping z-index fix
   below. The previous wording of this comment named the two token
   families by writing them asterisk-then-slash-then-dashes back to back,
   with no space between the star and the slash. Those two symbols
   adjacent, in that order, are how a CSS comment ends -- there is no way
   to escape that inside a comment -- so it closed this comment right
   there, several lines early. Everything after that point, up to the
   comment's REAL intended ending, was then handed to the CSS parser as
   actual code. It is plain English prose, so it cannot form a valid
   selector, and a rule whose selector fails to parse is thrown away
   completely, declarations and all -- and the next opening brace the
   parser reached after that point belonged to this file's own .rung-rail
   rule. So THAT rule -- background, border, radius, padding, margin, font
   size, all of it -- was the one silently discarded, every time this file
   loaded, since the day it was added.

   It went unnoticed because the dots and the fork chip inside the rail set
   their own border and radius independently and still looked right; only
   the outer pill -- the rail's own background and border -- was ever
   missing. Confirmed by testing the rule in isolation (it parsed and
   applied perfectly on its own) and by counting this file's comment
   delimiters, which came up one closing mark ahead of its opening marks,
   exactly the signature of one comment closing twice.

   The lesson for next time: in a comment near shorthand token names, never
   let a literal star and a literal slash end up next to each other, even
   by coincidence of two unrelated tokens sitting side by side -- write the
   token names with a word or a space between them instead. */
.rung-rail {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin: var(--sp-2) 0 var(--sp-5); padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--border); border-radius: var(--r-pill);
  background: var(--panel); font-size: .82rem;
}
.rung-rail-lab { color: var(--muted); font-weight: 700; font-size: .7rem; text-transform: uppercase; letter-spacing: .08em; white-space: nowrap; }
.rung-rail-track { display: flex; align-items: center; gap: 2px; }
.rung-dot {
  display: grid; place-items: center; width: 26px; height: 26px; border-radius: var(--r-pill);
  border: 1.5px solid var(--border); color: var(--muted); text-decoration: none;
  font-family: "Cambria Math", Georgia, serif; font-size: .72rem;
  transition: border-color .15s ease, color .15s ease, background-color .15s ease;
}
.rung-dot:hover { border-color: var(--violet); color: var(--text); }
.rung-dot.is-cur { border-color: var(--violet); background: var(--violet); color: #04021f; font-weight: 700; }
.rung-seg { width: 14px; height: 1.5px; background: var(--border); flex: none; }
.rung-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; }
.rung-fork {
  display: inline-flex; align-items: center; gap: 4px; color: var(--muted); text-decoration: none;
  font-size: .78rem; padding: 3px var(--sp-2); border-radius: var(--r-pill); border: 1px dashed var(--border);
  white-space: nowrap;
}
.rung-fork:hover { border-color: var(--teal); color: var(--text); }
.rung-fork.is-cur { border-color: var(--teal); border-style: solid; color: var(--teal); font-weight: 700; }
@media (prefers-color-scheme: light) {
  .rung-dot.is-cur { color: #f8fafc; }
}
@media (max-width: 480px) {
  .rung-rail { font-size: .76rem; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); }
  .rung-dot { width: 22px; height: 22px; }
  .rung-seg { width: 8px; }
}
/* Depth badge (tools/depth_badges.py): one, two or three ascending bars, so the level is the COUNT of bars and never
   only a colour. Plain = 1, Working = 2, Formal = 3. Inherits the colour of the chip or text it sits in. */
.dpt { display: inline-block; width: 1.15em; height: .95em; vertical-align: -.1em; font-style: normal; background: currentColor;
  clip-path: polygon(0 100%, 0 66%, 26% 66%, 26% 100%, 37% 100%, 37% 33%, 63% 33%, 63% 100%, 74% 100%, 74% 0, 100% 0, 100% 100%); }
.dpt-1 { background: linear-gradient(90deg, currentColor 0 26%, color-mix(in srgb, currentColor 28%, transparent) 26% 100%); }
.dpt-2 { background: linear-gradient(90deg, currentColor 0 63%, color-mix(in srgb, currentColor 28%, transparent) 63% 100%); }
.dpt-3 { background: currentColor; }

/* Evidence-tier badge (SymbiQ.ev in core.js): a shape plus its label, so colour is never the only signal. */
.ev { display: inline-flex; align-items: center; gap: .4em; font-size: .8rem; font-weight: 600; line-height: 1.2;
  padding: 2px 10px 2px 7px; border-radius: 99px; border: 1px solid currentColor; color: var(--muted); white-space: nowrap; }
.ev::before { content: ""; flex: none; width: .8em; height: .8em; background: currentColor; }
.ev-proven { color: var(--teal); } .ev-proven::before { clip-path: circle(50%); }
.ev-heuristic { color: var(--yellow); } .ev-heuristic::before { clip-path: polygon(50% 4%, 100% 96%, 0 96%); }
.ev-inspired { color: var(--violet); } .ev-inspired::before { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.ev-frontier { color: var(--red); } .ev-frontier::before { clip-path: polygon(50% 0, 62% 36%, 100% 38%, 70% 60%, 80% 96%, 50% 74%, 20% 96%, 30% 60%, 0 38%, 38% 36%); }
.tier { display: inline-block; font-size: 0.8rem; font-weight: 600; padding: 2px 10px; border-radius: 99px; border: 1px solid var(--border); margin-bottom: 10px; }
.tier.g { color: var(--green); background: rgba(74, 222, 128, 0.08); border-color: rgba(74, 222, 128, 0.3); }
.tier.y { color: var(--yellow); background: rgba(250, 204, 21, 0.08); border-color: rgba(250, 204, 21, 0.3); }
.tier.r { color: var(--red); background: rgba(248, 113, 113, 0.08); border-color: rgba(248, 113, 113, 0.3); }
.card {
  background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
  padding: 22px 24px; margin: 18px 0;
  transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.card:hover { transform: translateY(-3px); border-color: rgba(45, 212, 191, 0.45); box-shadow: 0 10px 28px var(--shadow); }
.card h3 { margin-bottom: 8px; font-size: 1.05rem; }
a { color: var(--link); text-decoration-color: transparent; text-underline-offset: 3px; transition: text-decoration-color 0.15s ease; }
a:hover { text-decoration-color: currentColor; }

/* WCAG 1.4.1: when colour is the ONLY thing separating a link from the text
   around it, it needs 3:1 against that text. Measured, this site failed badly
   in dark mode -- #2dd4bf against body #e8ecf6 is 1.57:1, so a link and a
   sentence are near-identical in luminance and only hue tells them apart. Any
   reader with a colour-vision deficiency, or any greyscale display, loses them
   entirely. Underlining prose links removes the dependency on colour outright,
   which is the fix WCAG actually asks for; the light-mode --link darkening
   above handles 1.4.3 separately.

   Scoped to bare <a> inside prose blocks, because `a` is global here and
   underlining nav items, cards and CTA buttons would wreck them. The site's
   convention makes this exact: every inline prose link is a bare <a>, and
   everything button-like carries a class (.bigplay, .cta, .herocta-alt,
   .orjump). Verified across all 17 pages -- 125 bare prose links, and the only
   classed ones are those four CTA kinds. If a future prose link needs a class,
   underline it explicitly rather than widening this selector. */
:is(p, li, dd, td, figcaption, blockquote) a:not([class]) {
  text-decoration-line: underline;
  text-decoration-color: color-mix(in srgb, currentColor 45%, transparent);
  text-decoration-thickness: 1px;
}
:is(p, li, dd, td, figcaption, blockquote) a:not([class]):hover {
  text-decoration-color: currentColor;
  text-decoration-thickness: 2px;
}
/* The topic indexes on The Feasible Region and The Machinery are menus, not prose: each row
   is a numbered, padded, hover-highlighted block that already reads as a link. The prose
   rule above underlined both the title and its right-hand blurb. Rows keep their hover fill. */
.fr-toc li a:not([class]) { text-decoration-line: none; }
/* The grid owns its spacing. .card carries margin: 18px 0 for cards that sit alone in the
   flow, so inside a grid the rows were 50px apart (14 gap + 2 x 18 margin) while the
   columns were 14px apart. Zeroing the card margin and moving it into the grid's own
   margin keeps the space above and below (24 + 18 = 42px) and evens the gaps to 16px. */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); gap: 16px; margin: 42px 0; }
.grid > .card { margin: 0; }
/* Two-up variant for cards that carry a paragraph, not a label. At the 1180px column
   the auto-fit grid above makes four ~260px columns of 8-10-line paragraphs, which is
   the wrong shape for reading; two columns keep a line to ~60 characters. */
.grid.grid-2 { grid-template-columns: 1fr; }
@media (min-width: 720px) { .grid.grid-2 { grid-template-columns: repeat(2, 1fr); } }
.grid .card { animation: fadeUp 0.5s ease-out both; }
.grid .card:nth-child(2) { animation-delay: 0.08s; }
.grid .card:nth-child(3) { animation-delay: 0.16s; }
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 0.95rem; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border); }
th { color: var(--muted); font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; }
tr:hover td { background: var(--rowhover); }
.overflow { overflow-x: auto; }
footer { margin-top: 60px; padding-top: 20px; border-top: 1px solid var(--border); color: var(--muted); font-size: 0.88rem; }
.todo { color: var(--yellow); font-size: 0.85rem; }
code { background: var(--surface-2); border: 1px solid var(--border); border-radius: 5px; padding: 1px 6px; font-size: 0.9em; }
.formula {
  background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--violet);
  border-radius: 8px; padding: 12px 16px; margin: 12px 0;
  font-family: "Cambria Math", Georgia, serif; overflow-x: auto;
}
.preset {
  background: var(--panel); color: var(--text); border: 1px solid var(--border);
  border-radius: 8px; padding: 6px 12px; margin: 0 6px 6px 0; cursor: pointer; font-size: 0.85rem;
  /* The 44px minimum. Most .preset buttons clear it on padding alone, which is
     why it was never noticed that the single-character gate buttons on the
     Bloch sphere are 40px WIDE and the PQC tool rows 30px TALL. A control that
     clears the minimum by accident of its label is not a control that clears
     the minimum, so it is stated as a rule. */
  min-height: 44px; min-width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
}
.preset:hover { border-color: var(--teal); transform: translateY(-1px); }
/* Every range slider on the site rendered at the browser default of 16px tall.
   The estate model set its own height and nothing else did. */
input[type=range] { height: 44px; cursor: pointer; accent-color: var(--teal); }
/* The standalone call to action at the foot of a card -- "Start here →",
   "The mathematics →". 26 of them across 8 pages, all 22px tall. Prose links
   inside a sentence are deliberately NOT touched: they are text, not targets,
   and giving them a 44px box would wreck the typography. */
.card > a { display: inline-flex; align-items: center; min-height: 44px; }
/* Same job, different shape: a call to action that is the entire paragraph.
   `p > a:only-child` would have been wrong -- :only-child ignores text
   siblings, so it also matches a link inside a sentence. Marked in the markup
   instead, which cannot misfire. */
.cta { display: inline-flex; align-items: center; min-height: 44px; }
.preset.on { border-color: var(--teal); color: var(--teal); font-weight: 600; }

/* ── curriculum topic tracking (curriculum-track.js) ─────────────────────
   Shared by formalism.html and feasible.html. .mod-mark reuses .preset/.on
   directly rather than inventing a new button language -- the "already
   approved gradient/interaction language" rule. Only .mod-check and the
   progress bar are genuinely new here. */
.mod-check {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%; margin-left: 8px;
  background: rgba(45,212,191,.16); color: var(--teal);
  font-size: .72rem; font-weight: 700; vertical-align: middle;
}
.mod-progress-wrap { display: flex; align-items: center; gap: 10px; margin: 4px 0 20px; flex-wrap: wrap; }
.mod-progress-track { flex: 1 1 140px; max-width: 220px; height: 6px; border-radius: 999px; background: var(--border); overflow: hidden; }
.mod-progress-bar { display: block; height: 100%; width: 0%; background: var(--teal); border-radius: 999px; transition: width .3s ease; }
#mod-progress-label { font-size: .82rem; color: var(--muted); }
.mod-complete-note {
  margin: -8px 0 20px; padding: 10px 14px; border: 1px solid var(--teal); border-radius: 10px;
  background: rgba(45,212,191,.08); font-size: .88rem;
}
.mod-complete-note a { color: var(--teal); font-weight: 600; }

/* ── homepage intent router (index.html only, but shared so it inherits the
   site's existing card/token language rather than a page-local reinvention) */
.introute { margin: 8px 0 30px; }
.introute-lede { font-size: .85rem; color: var(--muted); margin: 0 0 10px; }
.introute-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 10px; }
.introute-card {
  display: flex; flex-direction: column; gap: 3px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--panel);
  text-decoration: none; color: var(--text); min-height: 44px;
  transition: border-color .18s ease, transform .18s ease;
}
.introute-card:hover { border-color: var(--teal); transform: translateY(-2px); }
.introute-card .ic { font-size: 1.2rem; }
.introute-card .tt { font-weight: 700; font-size: .93rem; }
.introute-card .sb { font-size: .78rem; color: var(--muted); }
/* Returning-visitor progress line, written in by the hub script on index.html.
   Muted by default ("not started"), teal once there is real progress to show,
   so the eye lands on the track you were actually in. */
.introute-card .pg { font-size: .74rem; color: var(--muted); margin-top: 4px;
  font-variant-numeric: tabular-nums; }
.introute-card.resume { border-color: rgba(45, 212, 191, 0.45); }
.introute-card.resume .pg { color: var(--teal); font-weight: 600; }
@media (prefers-color-scheme: light) { .introute-card.resume .pg { color: #0f766e; } }
@media (max-width: 560px) { .introute-grid { grid-template-columns: 1fr; } }

/* ── BROWSER SURFACES ──────────────────────────────────────────────────────
   The parts of the page nobody draws: the selection highlight, the text
   caret, the scrollbar, the list marker. Left alone they ship the browser's
   own defaults, which belong to no theme -- a pale grey scrollbar running
   down the side of a #0b0f1a page is the single loudest unthemed surface
   here, and it is on every long page on the site.
   Each of these gets the same two-value treatment as every other colour
   token (The Two-Value Rule): one unconditional value that IS the dark
   theme, one measured light value. Selection was already half-done -- it had
   a dark value and no light one, so light mode was landing lavender-at-40%
   by luck rather than by measurement.  Added 2026-08-28. */
::selection { background: rgba(139, 92, 246, 0.40); }

/* Scrollbars. `scrollbar-color` is the standard property (Firefox, Chrome
   121+) and wins over the -webkit- pseudo-elements where both are
   understood; the pseudo-elements below are here for Safari, which has no
   `scrollbar-color`. Native width is kept -- `thin` is a usability tax on a
   page this long -- and the thumb is the same hairline the cards use, so the
   scrollbar reads as part of the same drawing. */
html { scrollbar-color: var(--border) transparent; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--border); border-radius: 999px;
  border: 3px solid var(--bg);      /* inset the thumb without a second element */
}
::-webkit-scrollbar-thumb:hover { background: var(--muted); }

/* The caret is the one piece of UI the visitor is looking straight at while
   typing, and it is teal on every other instrument on this site. */
input, textarea, select, [contenteditable] { caret-color: var(--teal); }

/* Placeholder text was the one browser surface this site never themed, so it
   shipped Chrome's default #757575 on every input on all 27 pages. Measured
   2026-09-22: 4.09:1 on the dark input ground and 4.40:1 on the light one --
   38 of 42 inputs site-wide sat under the 4.5:1 floor that the rest of this
   palette is measured against. --muted is the token that already means
   "secondary text, contrast-locked in both themes", which is exactly what a
   placeholder is; it clears the floor on both grounds. */
::placeholder { color: var(--muted); opacity: 1; }

/* Markers carry structure, not content -- muted, like every other piece of
   scaffolding under The Colour Law. */
li::marker { color: var(--muted); }

a:focus-visible, button:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 4px; }
/* A floor for everything focusable that is NOT an <a> or a <button>: summaries
   (this site is built out of <details>), form fields, and anything with an
   explicit tabindex. Wrapped in :where() so it carries ZERO specificity --
   every one of the ~20 component-level focus rules further down this file
   still wins outright. This only fills the gaps. */
:where(summary, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 4px;
}
@media (prefers-color-scheme: light) {
  /* Lavender-at-40% over #f8fafc measures 10.2:1 against ink, so the dark
     value was never unreadable in light mode -- it was simply never chosen.
     This is the light-mode violet (#7c3aed) at the alpha that reads as a
     deliberate highlight rather than a bruise: 11.4:1 against ink. */
  ::selection { background: rgba(124, 58, 237, 0.22); }
  ::-webkit-scrollbar-thumb { background: var(--border); border-color: var(--bg); }
  ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
}
@media (max-width: 719px) {
  nav:not(.rung-rail) { gap: 10px; padding: 14px 0; margin-bottom: 30px; }
  nav a.link { font-size: 0.86rem; }
  nav .brand { font-size: 1.02rem; }
  nav .brand svg { width: 26px; height: 26px; }
}
/* keep the bar on ONE row on a phone: shed the optional words, not the meaning */
@media (max-width: 460px) {
  .longword { display: none; }
  nav:not(.rung-rail) { gap: 8px; }
}

/* ================================================================
   STRUCTURED NAV (2026-08-07) — replaces the Lattice launcher.
   Five visible categories, native <details class="navcat">: a real dropdown
   at desktop width, an accordion section at mobile width, same markup
   either way. No search box — the categories ARE the navigation. Every
   link is still a real <a>, so crawlability is unchanged.
   ================================================================ */
/* `position: relative` for this rule now lives in the base nav block near the
   top of the file -- see the note there. It cannot be declared here: this rule
   comes AFTER the >=720px sticky media query, and at equal specificity the
   later rule wins, which is exactly how the sticky header was lost. */
nav .link.pathlink { color: var(--violet); font-weight: 600; }
nav .link.pathlink:hover { color: var(--violet); }

/* ── L1 accounts, redesigned 2026-08-28. History: this was first a right-
   pushed inline form (broke, because there was never room -- see the "stray
   floating box" writeup this replaces), then a forced full-width row (fixed
   the wrap, but Chinmoy's actual ask was smaller than that: a compact
   button, tucked to the side, that opens a profile once signed in).

   So it now reuses the SAME <details class="navcat"> mechanism the five
   category menus already use, rather than inventing a second popover
   pattern: nav.js's querySelectorAll('.navcat') is unscoped, so this gets
   one-open-at-a-time, outside-click-closes and Escape for free, no JS
   change needed there. The trigger is a plain circular icon button; the
   panel is a .navpanel like every other menu, just right-edge-anchored
   since this is the rightmost control and a left-aligned popover would run
   off the viewport.

   Visibility is handled in markup, not CSS: the <details> ships `hidden` in
   every page's nav, and auth.js clears that attribute only once Supabase has
   actually loaded and a client exists. That is a stricter version of the
   old promise ("shows nothing if it never loads") -- previously an empty
   CSS selector had to infer that state; now JS states it directly, so a
   half-configured or slow-loading auth can't flash a dead button that opens
   an empty popover. */
.sq-account { margin-left: auto; }
.sq-account > summary {
  padding: 0; width: 40px; height: 40px;
  border-radius: 50%; justify-content: center;
  background: var(--panel); border: 1px solid var(--border);
}
.sq-account > summary::after { content: none; }   /* icon-only trigger -- no menu-label chevron */
.sq-account > summary:hover { border-color: var(--teal); background: var(--rowhover); }
.sq-account[open] > summary { border-color: var(--teal); background: var(--rowhover); }
.sq-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; font-size: 1.05rem; line-height: 1;
}
/* Signed in, the icon becomes an initial -- the actual "profile" cue Chinmoy
   asked for, at a glance, without opening the panel. Brand gradient rather
   than a flat fill so it reads as "you" and not a sixth nav-category icon. */
.sq-account.signed-in .sq-avatar {
  background: linear-gradient(135deg, var(--violet), var(--teal));
  color: #04211d; font-weight: 700; border-radius: 50%;
}
.sq-account .navpanel { left: auto; right: 0; min-width: 220px; }

.sq-auth-form { display: flex; gap: 6px; margin: 0; }
.sq-auth-form input[type=email] { flex: 1 1 auto; min-width: 0; padding: 8px 10px; font-size: .85rem; }
.sq-auth-form button { padding: 8px 14px; min-height: 40px; font-size: .85rem; white-space: nowrap; }
.sq-auth-status { margin: 8px 0 0; font-size: .78rem; color: var(--muted); }
.sq-auth-me { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
.sq-auth-name { font-size: .92rem; font-weight: 600; }
.sq-auth-no { color: var(--muted); font-weight: 400; }
.sq-auth-me button {
  align-self: stretch;
  background: none; border: 1px solid var(--border); color: var(--muted);
  border-radius: 8px; padding: 6px 12px; min-height: 36px; font: inherit; font-size: .82rem;
  cursor: pointer; transition: border-color .15s ease, color .15s ease;
}
.sq-auth-me button:hover { border-color: var(--teal); color: var(--text); }

/* The 719px rule two blocks below sets EVERY .navcat to position:static so
   an open category pushes the mobile accordion open inline. That is right
   for the five link menus but wrong here: an account popover pushing the
   rest of the nav down reads as broken, not helpful. Higher specificity
   (two classes) wins over that rule regardless of source order, so this
   control stays a floating popover -- the same "below or to the side"
   behaviour -- at every viewport width, phone included. */
@media (max-width: 719px) {
  .sq-account.navcat { position: relative; }
  .sq-account .navpanel {
    position: absolute; top: 100%; margin-top: 8px;
    border: 1px solid var(--border); box-shadow: 0 16px 40px -20px var(--shadow);
    padding: 8px; background: var(--panel);
  }
}

#navtrig { display: none; }
#navcats { display: flex; align-items: center; gap: 2px; flex-wrap: wrap; }

.navcat { position: relative; }
.navcat > summary {
  list-style: none; cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
  min-height: 44px; padding: 6px 11px; border-radius: 8px; color: var(--muted);
  font-size: .9rem; font-weight: 600;
  transition: color .15s ease, background .15s ease;
}
.navcat > summary::-webkit-details-marker { display: none; }
.navcat > summary::after { content: "\25BE"; font-size: .62rem; opacity: .65; }
.navcat > summary:hover { color: var(--text); background: var(--rowhover); }
.navcat[open] > summary, .navcat.navcat-cur > summary { color: var(--teal); background: var(--rowhover); }

.navpanel {
  position: absolute; top: 100%; left: 0; margin-top: 6px; min-width: 250px;
  background: var(--panel); border: 1px solid var(--border); border-radius: 12px;
  padding: 8px; box-shadow: 0 16px 40px -20px var(--shadow); z-index: 45;
}
.navrow { margin: 1px 0; }
.navmain {
  display: flex; flex-direction: column; justify-content: center; min-height: 44px;
  padding: 7px 10px; border-radius: 8px; text-decoration: none; color: var(--text);
  font-size: .89rem; line-height: 1.3;
  transition: background .14s ease;
}
.navmain:hover { background: var(--rowhover); }
.navmain.active { color: var(--teal); font-weight: 700; }
.navmain .sub { display: block; font-size: .74rem; color: var(--muted); font-weight: 400; margin-top: 1px; }
.navchip {
  display: inline-flex; align-items: center; min-height: 44px; margin-left: 6px;
  font-size: .76rem; color: var(--teal); text-decoration: none; padding: 4px 8px;
  border-radius: 99px; border: 1px solid var(--border);
}
.navchip:hover { border-color: var(--teal); }

/* Sub-headings inside a dropdown (2026-09-26, the Learn / Play / Community regroup).
   Learn holds two tracks, so at 1000px and up its panel lays them out as two columns
   instead of one 900px-tall list. Below that the panel is one column and scrolls inside
   itself, so it can never run off the bottom of a short laptop screen. */
.navhead {
  margin: 10px 10px 2px; padding: 0; font-size: .68rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); font-weight: 700;
}
.navpanel > .navhead:first-child, .navcol > .navhead:first-child { margin-top: 4px; }
@media (min-width: 720px) {
  .navpanel { max-height: calc(100vh - 110px); overflow-y: auto; }
}
@media (min-width: 1000px) {
  .navpanel-wide { display: grid; grid-template-columns: repeat(2, minmax(min(100%, 270px), 1fr)); gap: 0 14px; min-width: 590px; }
}
/* A small static dot on a tab marks the two places a first-time visitor should not miss:
   the live security tools, and the new swipe deck. No pulse, no label: the width budget
   of the nav's first row is 40-49px of control and no more. */
.navcat[data-hot] > summary::before, .navcat[data-new] > summary::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--teal); flex: none;
}
.navcat[data-new] > summary::before { background: var(--violet); }
/* Light mode's teal (#0d9488) is dark enough that plain var(--teal) text
   fails 4.5:1 here -- measured 3.30/3.74/3.74:1 on the active summary, the
   current-page link and the widget chips. Same shape as every other
   teal-on-light-background miss this project keeps finding: darkened to the
   same #0f766e already used elsewhere on the site, re-measures 5.1-5.5:1. */
@media (prefers-color-scheme: light) {
  .navcat[open] > summary, .navcat.navcat-cur > summary,
  .navmain.active, .navchip { color: #0f766e; }
}

@media (max-width: 719px) {
  #navtrig {
    display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
    padding: 7px 14px 7px 10px; background: var(--panel); color: var(--text);
    border: 1px solid var(--border); border-radius: 99px; font: inherit; font-size: .86rem; cursor: pointer;
  }
  #navcats {
    display: none; flex-direction: column; gap: 1px; width: 100%;
    position: absolute; left: 0; right: 0; top: 100%; margin-top: 8px;
    background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 10px;
    box-shadow: 0 16px 40px -20px var(--shadow); z-index: 45; max-height: 72vh; overflow-y: auto;
  }
  #navcats.open { display: flex; }
  .navcat { position: static; }
  .navpanel { position: static; margin: 2px 0 6px 6px; border: none; box-shadow: none; padding: 0; background: none; }
}

/* ================================================================
   THE ARCADE — games live behind a door, not in a nav menu
   ================================================================ */
.gate {
  position: relative; border: 1px solid var(--border); border-radius: 18px; overflow: hidden;
  padding: 46px 26px; text-align: center; cursor: pointer; margin: 26px 0 10px;
  background: linear-gradient(150deg, rgba(167,139,250,.10), rgba(45,212,191,.08));
  transition: border-color .3s ease, box-shadow .35s ease, transform .3s ease;
}
.gate:hover { border-color: rgba(45,212,191,.5); box-shadow: 0 22px 60px -30px rgba(45,212,191,.6); transform: translateY(-2px); }
.gate svg { display: block; width: 100%; max-width: 260px; height: auto; margin: 0 auto 16px; }
.gate .seam { stroke: var(--teal); stroke-width: 1.5; opacity: .55; }
.gate .brick { fill: none; stroke: var(--violet); stroke-width: 1.2; opacity: .40; }
.gate:hover .brick { opacity: .75; }
.gate .gnode { fill: var(--teal); }
.gate h3 { font-size: 1.25rem; margin-bottom: 6px; }
.gate p { color: var(--muted); max-width: 460px; margin: 0 auto 4px; }
.gate .push { display: inline-block; margin-top: 14px; color: var(--teal); font-weight: 600; font-size: .92rem; }
.gate.opened { display: none; }

.cabs { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 14px; margin: 20px 0 10px; }
/* The three banners over the Arcade floor: a heading that spans the whole grid row, then its
   cabinets, then a plain line of links to the same kind of game on other pages. */
.cabband { grid-column: 1 / -1; margin-top: 12px; }
.cabband:first-child { margin-top: 0; }
.cabband h3 { margin: 0 0 2px; font-size: 1.05rem; }
.cabband p { margin: 0; color: var(--muted); font-size: .9rem; }
.cabalso { grid-column: 1 / -1; margin: -4px 0 0; font-size: .86rem; color: var(--muted); }
.cab {
  text-align: left; background: var(--panel); border: 1px solid var(--border); border-radius: 14px;
  padding: 18px 20px; cursor: pointer; font: inherit; color: var(--text); position: relative; overflow: hidden;
  transition: transform .2s ease, border-color .2s ease, box-shadow .25s ease;
}
.cab::after {
  content: ""; position: absolute; inset: auto -30% -60% -30%; height: 100px; pointer-events: none;
  background: radial-gradient(closest-side, rgba(45,212,191,.20), transparent);
  opacity: 0; transition: opacity .3s ease;
}
.cab:hover { transform: translateY(-4px); border-color: rgba(45,212,191,.5); box-shadow: 0 16px 40px -22px var(--shadow); }
.cab:hover::after { opacity: 1; }
.cab.on { border-color: var(--teal); }
.cab .cabname { font-weight: 700; font-size: 1.02rem; display: block; margin-bottom: 4px; }
.cab .cabhook { color: var(--muted); font-size: .86rem; line-height: 1.45; display: block; }
.cab .cabtag { display: inline-block; margin-top: 10px; font-size: .7rem; letter-spacing: .06em;
               text-transform: uppercase; color: var(--teal); }
.cab .cabdone { position: absolute; top: 12px; right: 14px; color: var(--teal); font-size: .8rem; }
.stage { margin-top: 6px; }
.stage-top { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin-bottom: 4px; }
.stage-back { background: none; border: none; color: var(--muted); font: inherit; font-size: .86rem;
              cursor: pointer; padding: 0; text-decoration: underline; }
.stage-back:hover { color: var(--teal); }

/* ================================================================
   MISSION FRAME — the same engines, worn as story (journey.html)
   ================================================================ */
.mission { border: 1px solid var(--border); border-radius: 16px; overflow: hidden; margin: 16px 0;
           background: var(--panel); }
.mission-head { padding: 20px 22px 14px; background: linear-gradient(140deg, rgba(167,139,250,.11), transparent 70%); }
.mission-head .eyebrow { font-size: .7rem; letter-spacing: .13em; text-transform: uppercase; color: var(--muted); }
.mission-head h3 { font-size: 1.22rem; margin: 4px 0 2px; }
.mission-tag { display: inline-block; font-size: .7rem; padding: 2px 9px; border-radius: 99px;
               border: 1px solid var(--border); color: var(--muted); margin-left: 8px; vertical-align: middle; }
.mission-tag.done { color: var(--teal); border-color: rgba(45,212,191,.5); }
.speech { display: flex; gap: 13px; align-items: flex-start; margin: 14px 0 4px; }
.speech .face {
  flex: 0 0 44px; height: 44px; border-radius: 14px; display: grid; place-items: center;
  font-weight: 800; font-size: 1.05rem; border: 1px solid var(--border);
  background: linear-gradient(135deg, rgba(45,212,191,.18), rgba(167,139,250,.18));
}
.speech .said { font-style: italic; color: var(--text); }
.speech .said b { font-style: normal; color: var(--muted); display: block; font-size: .74rem;
                  letter-spacing: .08em; text-transform: uppercase; margin-bottom: 2px; font-weight: 700; }
.mission-body { padding: 4px 22px 20px; }
.whatsreal {
  border-left: 3px solid var(--violet); border-radius: 0 10px 10px 0;
  background: var(--bg); padding: 13px 16px; margin: 16px 0 4px; font-size: .9rem;
}
.whatsreal .wr-h { font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; color: var(--violet);
                   font-weight: 700; margin-bottom: 5px; }
.whatsreal p { margin-bottom: 8px; }
.whatsreal p:last-child { margin-bottom: 0; }
.orbadge { display: inline-block; font-size: .68rem; border: 1px solid var(--border); border-radius: 99px;
           padding: 1px 8px; color: var(--muted); margin-left: 6px; white-space: nowrap; }

/* ================================================================
   FIGURES — inline SVG diagrams.
   Deliberately NOT JavaScript: a diagram is content, so it ships in
   the HTML, is crawlable, and survives with scripts off. Colour law
   across every figure on the site:
     teal   = AI / real / shipping today
     violet = quantum / theory / still ahead
     yellow = the thing to look at
     muted  = context and scaffolding
   ================================================================ */
.fig { margin: 22px 0; }
/* A diagram that shrinks to fit a phone is a diagram nobody can read: at 375px
   these viewBoxes scale to ~50%, which would render 10px labels at 5px. So the
   SVG keeps a legible floor and its own scroller takes over below that — the
   page itself never scrolls sideways. */
.figscroll { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; margin: 0 -2px; padding: 2px; }
.figscroll svg { display: block; width: 100%; height: auto; min-width: 470px; overflow: visible; }
.figscroll::-webkit-scrollbar { height: 6px; }
.figscroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 99px; }
.fig figcaption { color: var(--muted); font-size: .86rem; line-height: 1.5; margin-top: 10px; }
.fig figcaption strong { color: var(--text); }
.fig .swipe { display: none; font-size: .74rem; color: var(--muted); margin-top: 6px; }
@media (max-width: 520px) { .fig .swipe { display: block; } }
.figcard {
  border: 1px solid var(--border); border-radius: 16px; padding: 20px 22px;
  background: linear-gradient(150deg, rgba(167,139,250,.07), rgba(45,212,191,.05));
  transition: border-color .3s ease, box-shadow .3s ease;
}
.figcard:hover { border-color: rgba(45,212,191,.35); box-shadow: 0 14px 40px -24px rgba(45,212,191,.45); }
.fig .lbl   { fill: var(--muted); font-weight: 600; font-size: 11.5px;  line-height: 1; }
.fig .lbl-s { fill: var(--muted); font-weight: 500; font-size: 10px;    line-height: 1; }
.fig .lbl-b { fill: var(--text);  font-weight: 700; font-size: 13.5px;  line-height: 1; }
.fig .num   { fill: var(--text);  font-weight: 700; font-size: 14.5px;  line-height: 1; font-variant-numeric: tabular-nums; }
.fig .teal  { fill: var(--teal); } .fig .violet { fill: var(--violet); }
.fig .yellow{ fill: var(--yellow); }
.fig .sMuted{ stroke: var(--border); fill: none; }

/* the symbiosis loop */
.figloop .node { fill: var(--panel); stroke-width: 2; }
.figloop .node.ai { stroke: var(--teal); } .figloop .node.q { stroke: var(--violet); }
.figloop .arc { fill: none; stroke-width: 2.5; stroke-linecap: round; }
.figloop .arc.now { stroke: var(--teal); }
.figloop .arc.ahead { stroke: var(--violet); stroke-dasharray: 7 6; }
.figloop .flow { fill: var(--teal); }
.figloop .pulse { animation: figflow 3.4s linear infinite; }
.figloop .pulse2 { animation: figflow 3.4s linear 1.7s infinite; }
.figloop .f1 { offset-path: path("M 206 100 Q 320 26 434 100"); }
.figloop .f2 { offset-path: path("M 434 160 Q 320 234 206 160"); }
@keyframes figflow { from { offset-distance: 0%; } to { offset-distance: 100%; } }

/* log-scale bars (the collapsing estimate) */
.figbar .track { fill: var(--bg); stroke: var(--border); }
.figbar .bar { transition: width .6s cubic-bezier(.2,.7,.3,1); }
.figbar .bar.old { fill: var(--violet); } .figbar .bar.new { fill: var(--teal); }
.figbar .bar.today { fill: var(--yellow); }

/* the choir (basics) */
.figchoir .singer { fill: var(--panel); stroke: var(--border); stroke-width: 1.5; }
.figchoir .singer.off { stroke: var(--yellow); stroke-width: 2.5; }
.figchoir .listen { stroke: var(--teal); fill: none; stroke-width: 1.5; stroke-dasharray: 3 3; }
.figchoir .conductor { fill: rgba(45,212,191,.14); stroke: var(--teal); stroke-width: 2; }

/* the cycle (concepts) */
.figcycle .box { fill: var(--panel); stroke: var(--border); stroke-width: 1.5; }
.figcycle .box.ai { stroke: var(--teal); stroke-width: 2.5; fill: rgba(45,212,191,.10); }
.figcycle .flowline { stroke: var(--border); fill: none; stroke-width: 2; marker-end: url(#figarrow); }

/* the surface code (deep) */
.figcode .plq { stroke-width: 1.2; }
.figcode .plq.z { fill: rgba(167,139,250,.16); stroke: var(--violet); }
.figcode .plq.x { fill: rgba(45,212,191,.13); stroke: var(--teal); }
.figcode .dq { fill: var(--panel); stroke: var(--text); stroke-width: 1.6; }
.figcode .sq { stroke-width: 1.4; }
.figcode .sq.z { fill: var(--violet); stroke: var(--bg); }
.figcode .sq.x { fill: var(--teal); stroke: var(--bg); }

/* the overhead patch (logical qubit) */
.figpatch .phys { fill: rgba(167,139,250,.22); stroke: var(--violet); stroke-width: .8; }
.figpatch .logi { fill: rgba(45,212,191,.16); stroke: var(--teal); stroke-width: 2.5; }

@media (prefers-reduced-motion: reduce) {
  .figloop .pulse, .figloop .pulse2 { animation: none !important; }
}

/* ---------- shared game UI (reused by every playable widget) ---------- */
.hud {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 11px;
  padding: 12px 16px; margin-bottom: 14px;
}
.hud-side { display: flex; flex-direction: column; min-width: 76px; }
.hud-side.right { align-items: flex-end; }
.hud-label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); }
.hud-score { font-size: 1.7rem; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; transition: color 0.2s ease; }
.hud-score.lead { color: var(--teal); }
.hud-mid { text-align: center; font-size: 0.82rem; color: var(--muted); }
.dotrow { display: flex; align-items: center; gap: 9px; margin: 5px 0; }
.dotrow .who { font-size: 0.72rem; color: var(--muted); width: 66px; flex: none; text-transform: uppercase; letter-spacing: 0.05em; }
.dots { display: flex; gap: 5px; flex-wrap: wrap; }
.dot {
  width: 15px; height: 15px; border-radius: 50%; background: var(--border);
  display: block; transition: background 0.25s ease, transform 0.25s ease, box-shadow 0.2s ease;
}
.dot.win { background: var(--teal); }
.dot.lose { background: var(--red); }
.dot.now { box-shadow: 0 0 0 2px var(--violet); transform: scale(1.15); }
.dot.pop { animation: dotpop 0.4s ease-out; }
.verdict {
  border-radius: 10px; padding: 11px 15px; font-weight: 600; margin: 0 0 10px;
  border: 1px solid var(--border); animation: fadeUp 0.32s ease-out both;
}
.verdict.good { background: rgba(45, 212, 191, 0.12); border-color: rgba(45, 212, 191, 0.45); color: var(--teal); }
.verdict.bad { background: rgba(248, 113, 113, 0.11); border-color: rgba(248, 113, 113, 0.42); color: var(--red); }
.verdict.split { background: rgba(167, 139, 250, 0.12); border-color: rgba(167, 139, 250, 0.45); color: var(--violet); }

/* THE FRAME (games.js) — a mentor's line on a loss, and a small stat panel on
   a clear. Both live INSIDE a .verdict, so they inherit its colour; these rules
   only set weight, rhythm and a divider. Restrained on purpose. */
.g-mentor {
  margin-top: 9px; padding-top: 9px; border-top: 1px solid currentColor;
  font-weight: 400; font-style: italic; font-size: 0.9rem; opacity: 0.92;
}
.g-ceremony { margin-top: 10px; padding-top: 10px; border-top: 1px solid currentColor; }
.g-cr-head {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.09em;
  margin-bottom: 6px;
}
.g-cr-first {
  font-weight: 600; letter-spacing: 0.04em; opacity: 0.75;
  border: 1px solid currentColor; border-radius: 99px; padding: 0 6px; margin-left: 4px;
}
.g-cr-line { font-weight: 400; font-size: 0.9rem; line-height: 1.55; margin: 3px 0; }
.g-cr-codex {
  font-weight: 400; font-style: italic; font-size: 0.86rem; line-height: 1.55;
  margin-top: 7px; opacity: 0.85;
}

.rows { display: grid; grid-template-columns: auto 1fr; gap: 5px 12px; font-size: 0.9rem; align-items: baseline; }
.rows dt { color: var(--muted); white-space: nowrap; }
.rows dd { margin: 0; }
.chip {
  display: inline-block; font-family: "Cambria Math", Georgia, serif; font-size: 0.86rem;
  background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 1px 7px; margin-right: 4px;
}
.chip.err { border-color: rgba(248, 113, 113, 0.5); color: var(--red); }
.chip.you { border-color: rgba(45, 212, 191, 0.5); color: var(--teal); }
.chip.dec { border-color: rgba(167, 139, 250, 0.5); color: var(--violet); }

/* ================================================================
   GAME ENGINE STYLES — shared, because games.js mounts the same
   engines in two places (the Arcade and the Path's missions).
   ================================================================ */
/* level / hole / district chips */
.holes { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; margin: 4px 0 12px; }
.hole { min-width: 30px; height: 30px; padding: 0 6px; border-radius: 8px; border: 1px solid var(--border);
        background: var(--bg); display: grid; place-items: center; font-size: .8rem; font-weight: 600;
        color: var(--muted); cursor: pointer; transition: all .2s ease; }
.hole:hover { border-color: var(--teal); transform: translateY(-2px); }
.hole.now { border-color: var(--teal); color: var(--teal); box-shadow: 0 0 0 1px var(--teal); }
.hole.done { background: rgba(45,212,191,.14); border-color: rgba(45,212,191,.5); color: var(--teal); }
.hole.over { background: rgba(250,204,21,.12); border-color: rgba(250,204,21,.45); color: var(--yellow); }

/* --- FRAME.ladder, the shared level-progression chrome (22_ Part C1) --- */
.ld-strip { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; margin: 4px 0 10px; }
.ld-chip { min-width: 34px; min-height: 34px; padding: 0 6px; border-radius: 8px; border: 1px solid var(--border);
           background: var(--bg); display: grid; place-items: center; font-size: .85rem; font-weight: 700;
           color: var(--muted); cursor: pointer; transition: all .2s ease; }
.ld-chip:hover:not(:disabled) { border-color: var(--teal); transform: translateY(-2px); }
.ld-chip.now { border-color: var(--teal); color: var(--teal); box-shadow: 0 0 0 1px var(--teal); }
.ld-chip.done { background: rgba(45,212,191,.14); border-color: rgba(45,212,191,.5); color: var(--teal); font-size: 1rem; }
.ld-chip.locked, .ld-chip:disabled { opacity: .5; cursor: not-allowed; }
.ld-teach { text-align: center; font-size: .88rem; color: var(--muted); margin: 0 0 12px; line-height: 1.5; }
.ld-teach strong { color: var(--text); }

/* --- the annealing volcano --- */
.vwrap { display: flex; align-items: stretch; gap: 12px; }
.vterrain { display: block; flex: 1 1 auto; min-width: 0; height: auto; }
.vfill { stroke: none; }
.vline { fill: none; stroke: var(--border); stroke-width: 2; stroke-linejoin: round; }
.vgoal { fill: var(--teal); }
.vseen { fill: var(--violet); }
.vwalker { transition: transform .5s cubic-bezier(.3,.7,.3,1); }
.vhalo { fill: var(--yellow); opacity: .17; animation: vpulse 2.2s ease-in-out infinite; }
.vcore { fill: var(--yellow); stroke: var(--bg); stroke-width: 1.5; }
@keyframes vpulse { 0%,100% { opacity: .17; } 50% { opacity: .05; } }
.vtherm { flex: 0 0 26px; border: 1px solid var(--border); border-radius: 99px; background: var(--bg);
          position: relative; display: flex; align-items: flex-end; padding: 3px; overflow: hidden; }
.vtherm-fill { width: 100%; border-radius: 99px; background: var(--teal); height: 10%;
               transition: height .45s cubic-bezier(.3,.7,.3,1), background-color .45s ease; }
.vtherm-fill.hot { background: linear-gradient(180deg, var(--yellow), var(--red)); }
.vtherm-fill.warm { background: linear-gradient(180deg, var(--violet), var(--teal)); }
.vtherm-fill.cold { background: var(--teal); }
.vtherm-lbl { position: absolute; bottom: -1px; left: 50%; transform: translateX(-50%) rotate(-90deg);
              transform-origin: center; font-size: .58rem; color: var(--muted); white-space: nowrap;
              pointer-events: none; }
.vsched { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; margin: 12px 0 2px; }
.vslot { width: 26px; height: 26px; border-radius: 7px; border: 1px dashed var(--border); display: grid;
         place-items: center; font-size: .72rem; color: var(--muted); transition: all .2s ease; }
.vslot.now { border-style: solid; border-color: var(--violet); color: var(--violet); }
.vslot.c { border-style: solid; background: rgba(45,212,191,.14); border-color: rgba(45,212,191,.5); color: var(--teal); }
.vslot.h { border-style: solid; background: rgba(167,139,250,.12); border-color: rgba(167,139,250,.45); color: var(--violet); }
.vslot.s { border-style: solid; background: rgba(250,204,21,.14); border-color: rgba(250,204,21,.5); color: var(--yellow); }

/* --- circuit golf --- */
.golfsvg { display: block; margin: 0 auto; max-width: 300px; width: 100%; height: auto; }
.gsphere { fill: none; stroke: var(--border); }
.gequator { fill: none; stroke: var(--border); stroke-dasharray: 3 4; }
.gaxis { stroke: var(--border); stroke-width: 1; opacity: .5; }
.gaxlab { fill: var(--muted); font-size: 10px; font-family: inherit; text-anchor: middle; }
.gvec { stroke: var(--teal); stroke-width: 2.5; stroke-linecap: round; transition: all .38s cubic-bezier(.2,.7,.3,1); }
.gdot { fill: var(--teal); transition: all .38s cubic-bezier(.2,.7,.3,1); }
.gtarget { fill: none; stroke: var(--violet); stroke-width: 2; stroke-dasharray: 4 3; }
.gtargetdot { fill: var(--violet); opacity: .8; }

/* --- grover's escape --- */
.gr-bars { display: flex; align-items: flex-end; gap: 3px; height: 152px; padding: 8px 8px 0; margin: 6px 0 4px;
           border: 1px solid var(--border); border-radius: 12px; background: var(--bg); overflow: hidden; }
.gr-bar { flex: 1 1 0; min-width: 0; background: var(--border); border-radius: 3px 3px 0 0; min-height: 2px;
          align-self: flex-end; transition: height .5s cubic-bezier(.2,.7,.3,1), background-color .25s ease; }
.gr-bar.exit  { background: var(--teal); }
.gr-bar.flash { background: var(--yellow); }
.gr-bar.hit   { background: var(--teal); box-shadow: 0 0 0 2px rgba(45,212,191,.55); }
.gr-bar.miss  { background: var(--violet); box-shadow: 0 0 0 2px rgba(167,139,250,.5); }

/* --- max-cut / graph city --- */
.mcsvg { display: block; margin: 0 auto; max-width: 320px; width: 100%; height: auto; }
.mc-edge { stroke: var(--border); stroke-width: 2; transition: stroke .3s ease, stroke-width .3s ease, opacity .3s ease; }
.mc-edge.cut { stroke: var(--yellow); stroke-width: 3.5; }
.mc-edge.uncut { stroke: var(--border); stroke-dasharray: 4 5; opacity: .5; }
.mc-node { stroke: var(--bg); stroke-width: 3; cursor: pointer; transition: fill .25s ease; }
.mc-node.A { fill: var(--teal); }
.mc-node.B { fill: var(--violet); }
.mc-node:hover { stroke: var(--text); }

/* --- quantum tic-tac-toe: "The Twins" ---
   The threads ARE the entanglement, drawn. Everything below exists so that
   "your move closed a loop" is something you can see rather than be told. */
.qboard { position: relative; max-width: 340px; margin: 0 auto; }
.qgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; position: relative; }
.qthreads { position: absolute; inset: 0; pointer-events: none; z-index: 3; overflow: visible; }
.qthread { fill: none; stroke-width: 2.5; stroke-linecap: round; opacity: .55;
           transition: opacity .25s ease, stroke-width .25s ease; }
.qthread.X { stroke: var(--teal); }
.qthread.O { stroke: var(--violet); }
.qthread.ring { stroke: var(--yellow); opacity: .95; stroke-width: 3.5;
                animation: ringpulse 1.5s ease-in-out infinite; }
.qthread.resolving { stroke: var(--yellow); opacity: 1; stroke-width: 4.5; animation: none; }
.qthreadpip { stroke: var(--bg); stroke-width: 2; opacity: .9; transition: all .2s ease; }
.qthreadpip.X { fill: var(--teal); }
.qthreadpip.O { fill: var(--violet); }
.qthreadpip.ring, .qthreadpip.resolving { fill: var(--yellow); }
.qthreadnum { fill: var(--bg); font-weight: 700; font-size: 9px; text-anchor: middle; dominant-baseline: central;
              pointer-events: none; }
@keyframes ringpulse { 50% { opacity: .45; } }

/* the outcome preview: hover a glowing mark, see the whole reality it drags in */
.qcell.previewing { border-color: rgba(250,204,21,.65); background: rgba(250,204,21,.07); }
.spooky.willland { opacity: 1; transform: scale(1.28); border-color: var(--yellow);
                   background: rgba(250,204,21,.26); font-weight: 700; }
.spooky.willvanish { opacity: .16; }
.spooky { transition: opacity .18s ease, transform .18s ease, background .18s ease, border-color .18s ease; }
.qcell { aspect-ratio: 1; border: 1px solid var(--border); border-radius: 11px; background: var(--bg);
         position: relative; cursor: pointer; padding: 4px; overflow: hidden;
         display: flex; flex-wrap: wrap; align-content: center; justify-content: center; gap: 2px;
         transition: border-color .18s ease, background .18s ease, transform .18s ease; }
.qcell:hover:not(.locked) { border-color: var(--teal); transform: translateY(-2px); }
.qcell.sel { border-color: var(--teal); box-shadow: 0 0 0 1px var(--teal); }
.qcell.locked { cursor: default; }
.qcell.cyc { animation: cycpulse 1.1s ease-in-out infinite; }
@keyframes cycpulse { 50% { border-color: var(--yellow); box-shadow: 0 0 0 2px rgba(250,204,21,.35); } }
.spooky { font-family: "Cambria Math", Georgia, serif; font-size: .82rem; opacity: .72; padding: 1px 5px;
          border-radius: 5px; border: 1px dashed var(--border); animation: fadeUp .3s ease-out both;
          position: relative; z-index: 4; }
.spooky.X { color: var(--teal); } .spooky.O { color: var(--violet); }
.spooky.choosable { cursor: pointer; border-style: solid; border-color: var(--yellow); background: rgba(250,204,21,.12);
                    opacity: 1; animation: ghostbeat 1.6s ease-in-out infinite; }
.spooky.choosable:hover, .spooky.choosable:focus-visible { transform: scale(1.2); background: rgba(250,204,21,.26);
                    animation: none; outline: none; }
@keyframes ghostbeat { 50% { box-shadow: 0 0 0 3px rgba(250,204,21,.22); } }
.classical { font-family: "Cambria Math", Georgia, serif; font-size: 2.1rem; font-weight: 700; line-height: 1;
             animation: flippop .42s ease-out both; }
.classical.X { color: var(--teal); } .classical.O { color: var(--violet); }
.qcell.win { background: rgba(45,212,191,.1); border-color: rgba(45,212,191,.55); }
.turnbar { display: flex; align-items: center; justify-content: center; gap: 14px; margin-bottom: 10px; }
.turnbar .who { padding: 5px 14px; border-radius: 99px; border: 1px solid var(--border); font-weight: 600;
                font-size: .9rem; width: auto; text-transform: none; letter-spacing: 0; color: var(--muted);
                transition: all .22s ease; }
.turnbar .who.on.X { color: var(--teal); border-color: rgba(45,212,191,.5); background: rgba(45,212,191,.1); }
.turnbar .who.on.O { color: var(--violet); border-color: rgba(167,139,250,.5); background: rgba(167,139,250,.1); }

/* --- "how to play": the rules, behind one obvious button ---------------
   Chinmoy, 2026-07-30: "rules and how to play are not properly shown, it
   should be in an expandable button". The panel used to be an always-open
   grid, and on the Solver's Path it was not shown at all. It is now a
   <details> that sits ABOVE the game on every surface, opens itself the
   first time you meet a machine, and stays shut on return. */
.gamerules { border: 1px solid var(--border); border-radius: 14px; margin: 2px 0 18px; overflow: hidden;
  background: linear-gradient(135deg, rgba(45,212,191,.05), rgba(167,139,250,.05));
  transition: border-color .3s ease, box-shadow .3s ease; }
.gamerules[open] { border-color: rgba(45,212,191,.32); }
.gamerules:hover { border-color: rgba(45,212,191,.42); box-shadow: 0 10px 34px -18px rgba(45,212,191,.4); }
.gamerules > summary { display: flex; align-items: center; gap: 12px; cursor: pointer; list-style: none;
  padding: 13px 16px; min-height: 52px; font-family: inherit; -webkit-tap-highlight-color: transparent; }
.gamerules > summary::-webkit-details-marker { display: none; }
.gamerules > summary:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.gamerules .gr-ico { font-size: 1.15rem; line-height: 1; flex: none; }
.gamerules .gr-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.gamerules .gr-txt b { font-size: 1rem; font-weight: 700; color: var(--teal); letter-spacing: .01em; }
.gamerules .gr-txt i { font-style: normal; font-size: .82rem; color: var(--muted); line-height: 1.35; }
.gamerules .gr-x { flex: none; width: 26px; height: 26px; border-radius: 8px; border: 1px solid var(--border);
  position: relative; transition: transform .3s cubic-bezier(.22,.7,.3,1), border-color .3s ease; }
.gamerules .gr-x::before, .gamerules .gr-x::after { content: ""; position: absolute; inset: 50% 6px auto 6px;
  height: 2px; background: var(--teal); border-radius: 2px; transform: translateY(-1px); }
.gamerules .gr-x::after { transform: translateY(-1px) rotate(90deg); transition: opacity .3s ease; }
.gamerules[open] .gr-x { transform: rotate(180deg); border-color: rgba(45,212,191,.45); }
.gamerules[open] .gr-x::after { opacity: 0; }
.gamerules .gameabout { margin: 0; border: 0; border-top: 1px solid var(--border);
  border-radius: 0; background: none; box-shadow: none; }
.gamerules .gameabout:hover { border-color: var(--border); box-shadow: none; }
.gamerules .orjump { margin: 0 16px 16px; }
@media (max-width: 560px) {
  .gamerules > summary { padding: 11px 13px; gap: 10px; }
  .gamerules .gr-txt i { font-size: .78rem; }
}
@media (prefers-reduced-motion: reduce) { .gamerules .gr-x { transition: none; } }

/* --- .foldsec: a long prose section, folded ------------------------------
   Same visual language as .gamerules above (deliberately — one collapsible
   idiom on the site, not two), applied to reference material rather than a
   game's rules. It exists because the homepage had grown to twelve stacked
   full-length sections, two of which are reference lists a returning reader
   scrolls past every single visit. Folded, not deleted: the content is still
   in the HTML, still crawlable, still linkable by anchor — it just no longer
   costs three screens of scroll to get past.
   Rule of use: only fold a section a reader can be expected NOT to need this
   visit. Never fold the thing the page is for. */
.foldsec { border: 1px solid var(--border); border-radius: 14px; margin: 26px 0; overflow: hidden;
  background: linear-gradient(135deg, rgba(45,212,191,.04), rgba(167,139,250,.04)); }
.foldsec[open] { border-color: rgba(45,212,191,.28); }
.foldsec:hover { border-color: rgba(45,212,191,.38); }
.foldsec > summary { display: flex; align-items: center; gap: 12px; cursor: pointer; list-style: none;
  padding: 15px 18px; min-height: 52px; -webkit-tap-highlight-color: transparent; }
.foldsec > summary::-webkit-details-marker { display: none; }
.foldsec > summary:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.foldsec .fs-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.foldsec .fs-txt b { font-size: 1.12rem; font-weight: 700; color: var(--text); letter-spacing: -.01em; }
.foldsec .fs-txt i { font-style: normal; font-size: .84rem; color: var(--muted); line-height: 1.4; }
.foldsec .fs-x { flex: none; width: 26px; height: 26px; border-radius: 8px; border: 1px solid var(--border);
  position: relative; transition: transform .3s cubic-bezier(.22,.7,.3,1), border-color .3s ease; }
.foldsec .fs-x::before, .foldsec .fs-x::after { content: ""; position: absolute; inset: 50% 6px auto 6px;
  height: 2px; background: var(--teal); border-radius: 2px; transform: translateY(-1px); }
.foldsec .fs-x::after { transform: translateY(-1px) rotate(90deg); transition: opacity .3s ease; }
.foldsec[open] .fs-x { transform: rotate(180deg); border-color: rgba(45,212,191,.45); }
.foldsec[open] .fs-x::after { opacity: 0; }
.foldsec .fs-body { padding: 0 18px 6px; border-top: 1px solid var(--border); }
.foldsec .fs-body > h2:first-child, .foldsec .fs-body > h3:first-child { margin-top: 18px; }
@media (max-width: 560px) {
  .foldsec > summary { padding: 13px 14px; gap: 10px; }
  .foldsec .fs-txt b { font-size: 1.02rem; }
  .foldsec .fs-body { padding: 0 14px 4px; }
}
@media (prefers-reduced-motion: reduce) { .foldsec .fs-x { transition: none; } }

/* --- "about this game" panels --- */
.gameabout { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 22px; margin: 2px 0 20px; padding: 16px 20px;
  border: 1px solid var(--border); border-radius: 14px;
  background: linear-gradient(135deg, rgba(45,212,191,.06), rgba(167,139,250,.06));
  transition: border-color .3s ease, box-shadow .3s ease; }
.gameabout:hover { border-color: rgba(45,212,191,.35); box-shadow: 0 10px 34px -18px rgba(45,212,191,.4); }
@media (max-width: 560px) { .gameabout { grid-template-columns: 1fr; gap: 12px; padding: 14px 16px; } }
.gameabout > div { font-size: .92rem; line-height: 1.5; }
.gameabout .lbl { display: block; font-size: .7rem; letter-spacing: .09em; text-transform: uppercase;
                  color: var(--teal); font-weight: 700; margin-bottom: 3px; }
.gameabout .lbl.v { color: var(--violet); }
.gameabout .tier { display: inline-block; font-size: .68rem; color: var(--muted); border: 1px solid var(--border);
                   border-radius: 99px; padding: 1px 8px; margin-left: 4px; white-space: nowrap; }
.gameabout a { font-weight: 600; white-space: nowrap; }

/* ---------- difficulty selector (games.js LEVELS) ----------
   One chrome control, no engine fork. 🟢 shows an orientation line and forces
   the rules open; 🔴 hides the on-screen guidance the engine renders (the
   legend strip) and keeps the rules shut. Information, never physics. */
.gamechrome { margin: 2px 0 14px; }
.glevel { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.glevel-b { font: inherit; font-size: .82rem; font-weight: 600; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px;
  border-radius: 999px; border: 1px solid var(--border); background: var(--panel);
  color: var(--muted); transition: border-color .18s ease, color .18s ease, background .18s ease; }
.glevel-b:hover { border-color: rgba(45,212,191,.42); color: var(--text); }
.glevel-b[aria-pressed="true"] { color: var(--text); border-color: var(--teal);
  background: linear-gradient(180deg, rgba(45,212,191,.14), rgba(45,212,191,.05)); }
.glevel-b:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.glevel-i { font-size: .9em; line-height: 1; }
.glevel-blurb { flex-basis: 100%; margin: 4px 0 0; font-size: .8rem; color: var(--muted); line-height: 1.5; }
.glevel-orient-slot:empty { display: none; }
.glevel-orient { margin: 9px 0 0; font-size: .9rem; line-height: 1.55; color: var(--text);
  border-left: 2px solid var(--teal); padding: 2px 0 2px 12px; }
[data-glevel="ruthless"] .legend { display: none; }

/* ---------- motion layer: restrained, premium, never bouncy ----------
   Rules of thumb used here: one property pair (opacity + small translate),
   short durations (<= 520ms), a single easing curve, no loops except where a
   pulse carries meaning. Everything is disabled under prefers-reduced-motion. */
.reveal { opacity: 0; transform: translateY(14px); }
.reveal.in { opacity: 1; transform: none; transition: opacity .52s cubic-bezier(.22,.7,.3,1), transform .52s cubic-bezier(.22,.7,.3,1); }
/* stagger children of a revealed grid without needing per-element JS */
.reveal.in > * { animation: fadeUp .5s cubic-bezier(.22,.7,.3,1) both; }
.reveal.in > *:nth-child(2) { animation-delay: .06s; }
.reveal.in > *:nth-child(3) { animation-delay: .12s; }
.reveal.in > *:nth-child(4) { animation-delay: .18s; }

/* tactile press feedback — the cheapest way to feel responsive */
.preset:active { transform: translateY(1px) scale(.985); }
.preset { will-change: transform; }

/* headings draw their accent bar in on reveal */
.reveal h2::before, h2::before { transform-origin: top; }
.reveal.in h2::before { animation: growbar .5s cubic-bezier(.22,.7,.3,1) both; }

/* Links get a wipe underline rather than a blunt on/off.
 *
 * ⚠️ WRAPPED IN :where() DELIBERATELY — DO NOT UNWRAP IT.
 * This is a DEFAULT for ordinary prose links, so it must carry zero
 * specificity. Written plainly as `.wrap a:not(.link):not(.brand):not(.preset)`
 * it scored (0,4,1) and beat every button component that is an <a>, replacing
 * their background gradient with a 0%-wide underline. That left `.bigplay`,
 * `.orjump` and `.nextup` as near-black text on a transparent background —
 * invisible on the dark theme. Reported by Chinmoy 2026-07-30 on the homepage
 * "Enter The Feasible Region" button; the identical <button> on journey.html
 * was unaffected, which is what pinned the cause.
 *
 * :where() makes this (0,0,0), so ANY component class wins automatically and
 * no future button has to be added to a :not() chain to be visible. The
 * :not() list is kept for the pre-existing non-class cases.
 */
:where(.wrap a:not(.link):not(.brand):not(.preset)) {
  background-image: linear-gradient(currentColor, currentColor);
  background-size: 0% 1px; background-position: 0 100%; background-repeat: no-repeat;
  transition: background-size .24s cubic-bezier(.22,.7,.3,1);
}
:where(.wrap a:not(.link):not(.brand):not(.preset):hover) { background-size: 100% 1px; text-decoration-color: transparent; }

@keyframes growbar { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes dotpop { 0% { transform: scale(0.5); } 60% { transform: scale(1.3); } 100% { transform: scale(1); } }
@keyframes flippop { 0% { transform: scale(0.4); opacity: 0.2; } 65% { transform: scale(1.25); } 100% { transform: scale(1); opacity: 1; } }
@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes drift { to { background-position: 200% 0; } }
@keyframes qpulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* ================================================================
   ARCHIVE — the question back-catalogue (archive.html)
   ================================================================ */
.archq { padding: 0; }
.archq > summary {
  list-style: none; cursor: pointer; padding: 16px 22px;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
.archq > summary::-webkit-details-marker { display: none; }
.archq > summary::after {
  content: "\25B8"; margin-left: auto; color: var(--muted);
  transition: transform .18s cubic-bezier(.22,.7,.3,1);
}
.archq[open] > summary::after { transform: rotate(90deg); }
.archq-num { color: var(--teal); font-weight: 700; font-variant-numeric: tabular-nums; }
.archq-txt { flex: 1 1 260px; min-width: 0; }
.archq-body { padding: 0 22px 20px; border-top: 1px solid var(--border); margin-top: 2px; }
.archq-full { font-weight: 600; margin: 16px 0 12px; }
.archq-opts { margin: 0 0 14px; padding-left: 22px; }
.archq-opts li { margin: 5px 0; color: var(--muted); }
.archq-opts li.right { color: var(--text); }
.archq-opts li.right b { color: var(--teal); }
.archq-hint, .archq-signal { font-size: .92rem; color: var(--muted); }
.archq-ex { margin: 14px 0; padding-left: 14px; border-left: 2px solid var(--border); }
.archq-ex h4 { margin: 0 0 4px; font-size: .9rem; }
.archq-ex p { margin: 0; }
.archq-loading { color: var(--muted); margin: 14px 0 0; }

/* ================================================================
   OR JUMP — the button that appears wherever quantum meets
   operations research, and sends you to The Feasible Region.
   ================================================================ */
.orjump {
  display: inline-flex; align-items: center; gap: 8px;
  background: linear-gradient(135deg, rgba(45,212,191,.10), rgba(167,139,250,.10));
  border: 1px solid var(--border); border-left: 3px solid var(--teal);
  border-radius: 10px; padding: 10px 14px; margin: 14px 0;
  color: var(--text); text-decoration: none; font-size: .92rem;
  transition: border-color .18s cubic-bezier(.22,.7,.3,1), transform .18s cubic-bezier(.22,.7,.3,1);
}
.orjump:hover { border-color: var(--teal); transform: translateY(-2px); }
.orjump b { color: var(--teal); }
.orjump .arr { margin-left: 2px; color: var(--muted); }

/* ================================================================
   FORMS — newsletter signup and the correction/error-bounty report.
   ================================================================ */
.sqform { display: flex; flex-wrap: wrap; gap: 10px; margin: 14px 0 6px; }
/* ⚠ This enumerated [type=email] and [type=text] until 2026-09-22, so the
   Ledger's "Source URL" field -- an <input type="url"> -- matched nothing and
   rendered as a raw browser box: solid WHITE inside a dark form, 19px tall
   against its siblings' 47px, and under half the 44px touch target every
   other control here meets. An allowlist of types silently excludes the next
   type somebody uses, so this excludes the handful that must NOT be styled as
   text fields instead, and covers every text-like type by default. */
.sqform input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=hidden]),
.sqform textarea {
  flex: 1 1 220px; min-width: 0;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
  padding: 10px 12px; font: inherit; font-size: .95rem;
  transition: border-color .18s cubic-bezier(.22,.7,.3,1);
}
.sqform textarea { flex-basis: 100%; min-height: 130px; resize: vertical; }
.sqform input:focus, .sqform textarea:focus {
  outline: none; border-color: var(--teal);
  /* a 1px border-colour change alone is a weak focus indicator (WCAG 2.4.7 / 1.4.11); the
     ring is the visible part, the border keeps the shape */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--teal) 30%, transparent);
}
/* Was a flat solid-teal fill -- the one button on the site with no gradient,
   next to "Load a code" which already got the nicer ghost treatment. Brought
   in line with .bigplay's gradient language (Chinmoy, 2026-08-06: "some of
   the buttons are ugly, no gradient, single shade bright color"). */
.sqform button {
  background: linear-gradient(135deg, var(--teal), #22b8a6); color: #04211d; border: none;
  border-radius: 8px; padding: 10px 20px; min-height: 44px; font: inherit; font-weight: 700;
  cursor: pointer; white-space: nowrap; box-shadow: 0 6px 16px rgba(45,212,191,.22);
  transition: transform .18s cubic-bezier(.22,.7,.3,1), box-shadow .18s ease, filter .18s ease;
}
.sqform button:hover { transform: translateY(-2px); box-shadow: 0 10px 22px rgba(45,212,191,.3); filter: brightness(1.06); }
.sqform button:active { transform: translateY(1px) scale(.985); }
.sqform button[disabled] { opacity: .55; cursor: default; transform: none; }
@media (prefers-color-scheme: light) {
  .sqform button { background: linear-gradient(135deg, #0f766e, #115e59); color: #ffffff;
    box-shadow: 0 6px 16px rgba(17,94,89,.22); }
  .sqform button:hover { box-shadow: 0 10px 22px rgba(17,94,89,.3); }
}
.sqnote { font-size: .86rem; color: var(--muted); margin: 4px 0 0; }
.sqmsg { margin: 10px 0 0; font-size: .93rem; }
.sqmsg.ok  { color: var(--teal); }
.sqmsg.err { color: var(--red); }
.sqfield { flex: 1 1 100%; max-width: 44rem; }
/* The inputs' own `flex: 1 1 220px` only works when the parent is a flex container, and a
   .sqfield is a plain block: so every field shrank to its browser-default width (250px, 228px,
   181px on one form). Text-like fields and textareas now fill their field; dates and numbers
   keep their natural width. */
.sqfield input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=hidden]):not([type=submit]):not([type=button]):not([type=date]):not([type=number]),
.sqfield textarea { width: 100%; box-sizing: border-box; }
.sqfield label { display: block; font-size: .86rem; color: var(--muted); margin-bottom: 5px; }

/* ================================================================
   GAME NAVIGATION — the two things a player must never have to hunt
   for: how to start, and where to go once a stage is over.
   ================================================================ */
/* 2026-08-07: dialled back on Chinmoy's "too flashy and too big". Both
   classes were deliberately enlarged/animated on 2026-07-25 and 2026-07-29
   after real reports of visitors missing them -- so this keeps them a clear
   primary control (filled, bordered, still >=44px) while dropping the size,
   weight and motion that made them shout. If discoverability regresses,
   that history is why -- flag it rather than re-inflating silently.
   2026-08-20: dialled down again on the same complaint, one notch further.
   Padding/font shrink, but the 44px floor is now an explicit min-height
   rather than something padding happened to clear -- shrinking padding
   without that would have silently broken the tap-target rule. What makes
   it read as noticeable at the smaller size instead of just "a small
   button": a full pill radius, which nothing else on the site uses (every
   .preset/.cta is 8px), so shape carries the distinction size used to. */
.bigplay {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: linear-gradient(135deg, var(--teal), #22b8a6);
  color: #04211d; border: none; border-radius: 999px; min-height: 44px;
  padding: 8px 18px; font: inherit; font-size: .88rem; font-weight: 700;
  letter-spacing: .01em; cursor: pointer; margin: 6px 0 4px;
  box-shadow: 0 3px 10px rgba(45,212,191,.14);
  transition: box-shadow .18s ease, filter .18s ease;
}
.bigplay:hover { box-shadow: 0 5px 14px rgba(45,212,191,.2); filter: brightness(1.04); }
.bigplay:active { filter: brightness(.97); }
.bigplay .kbd { font-size: .72rem; font-weight: 600; opacity: .82; }
/* Light mode needs its own pairing, measured rather than guessed. The dark
   theme's bright teal takes near-black text at 9.09:1, but light mode's teal
   is dark (#0d9488), where the same text scores a marginal 4.52 and white
   scores a failing 3.74. So light mode gets a deeper gradient and white text:
   5.47:1 at the light end, 7.58:1 at the dark end. */
@media (prefers-color-scheme: light) {
  .bigplay { background: linear-gradient(135deg, #0f766e, #115e59); color: #ffffff;
             box-shadow: 0 3px 10px rgba(17,94,89,.18); }
  .bigplay:hover { box-shadow: 0 5px 14px rgba(17,94,89,.24); }
  .bigplay.ghost { background: transparent; color: #0f766e; border-color: #0f766e; }
}
.bigplay.ghost {
  background: transparent; color: var(--teal);
  border: 1.5px solid var(--teal); box-shadow: none;
}
.bigplay.ghost:hover { box-shadow: 0 4px 12px rgba(45,212,191,.14); }
/* No page used a disabled .bigplay before circuits.html's widget: with no
   :disabled rule the button kept its full bright gradient and a pointer
   cursor, indistinguishable from an enabled one even though clicks did
   nothing. Same treatment .ms-tell:disabled already uses elsewhere. */
.bigplay:disabled, .bigplay:disabled:hover { opacity: .45; cursor: not-allowed; filter: none; box-shadow: none; }

/* The completion CTA. Still visually distinct (border, tint, an arrow) so it
   is not missed, but no longer animated or oversized. */
.nextup {
  display: block; margin: 16px 0 6px; padding: 14px 18px;
  background: linear-gradient(135deg, rgba(45,212,191,.1), rgba(167,139,250,.08));
  border: 1px solid var(--teal); border-radius: 12px;
  text-decoration: none; color: var(--text);
  box-shadow: 0 4px 14px rgba(45,212,191,.1);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.nextup:hover { border-color: rgba(45,212,191,.75); box-shadow: 0 6px 18px rgba(45,212,191,.16); }
/* It ships as an <a> on some surfaces and a <button> on others (the OR games
   change level in place rather than navigating). Strip the button chrome so
   the two are indistinguishable. */
button.nextup { width: 100%; text-align: left; font: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; }
button.nextup:focus-visible { outline: 3px solid var(--violet); outline-offset: 3px; }
.nextup .eyebrow {
  display: block; font-size: .72rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--teal); font-weight: 700; margin-bottom: 5px;
}
.nextup .headline { display: block; font-size: 1.05rem; font-weight: 700; line-height: 1.3; }
.nextup .sub { display: block; color: var(--muted); font-size: .89rem; margin-top: 4px; }
.nextup .go {
  display: inline-block; margin-top: 10px; font-weight: 700; color: var(--teal); font-size: .95rem;
}
.nextup .go::after { content: " \25B8"; }

/* ================================================================
   OR HERO — the homepage's operations-research invitation.
   ================================================================ */
.orhero {
  margin: 34px 0; padding: 30px 32px; border-radius: 16px;
  background:
    radial-gradient(120% 140% at 8% 0%, rgba(45,212,191,.12), transparent 62%),
    radial-gradient(120% 140% at 96% 100%, rgba(167,139,250,.13), transparent 60%),
    var(--panel);
  border: 1px solid var(--border); border-top: 3px solid var(--teal);
}
.orhero .eyebrow {
  font-size: .76rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--teal); font-weight: 700; margin: 0 0 10px;
}
.orhero h2 { margin: 0 0 12px; font-size: 1.75rem; }
.orhero h2::before { display: none; }
.orhero p { margin: 0 0 14px; }
.orhero .orhero-lede { font-size: 1.06rem; }
.orhero-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px; margin: 20px 0;
}
.orhero-grid div {
  padding: 14px 16px; border-radius: 10px;
  background: var(--bg); border: 1px solid var(--border);
}
.orhero-grid b { display: block; color: var(--teal); font-size: .92rem; margin-bottom: 4px; }
.orhero-grid span { font-size: .89rem; color: var(--muted); }
/* The games hero. Added 2026-07-30 after an audit found that the homepage
   body contained ZERO links to journey.html or play.html — the one thing on
   this site nobody else has was reachable only from the nav bar. Violet-led,
   because on this site violet is the quantum half; the OR hero keeps teal. */
.gamehero { border-top-color: var(--violet); }
.gamehero .eyebrow { color: var(--violet); }
.gamehero .orhero-grid b { color: var(--violet); }
.gamehero .gh-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.gamehero .bigplay.ghost { border-color: var(--violet); color: var(--violet); }
.gamehero .gh-note { font-size: .87rem; color: var(--muted); margin: 14px 0 0; }

/* The PQC hero. Added 2026-08-21 -- pqc.html had zero links from the homepage
   body (nav-overlay only), unlike every other major section, which is its own
   "too much navigation to reach it" complaint. Same ghost-button mechanism as
   .gamehero's violet Arcade link, just amber -- reusing the already-approved
   pattern rather than inventing a fourth button language. Amber reads as
   "check this" rather than "explore this", which fits a page whose whole
   point is finding out whether YOUR system is exposed. --yellow is already
   the site's verified 4.5:1+ tier colour in both schemes, so no new contrast
   work is needed here. */
.pqhero { border-top-color: var(--yellow); }
.pqhero .eyebrow { color: var(--yellow); }
.pqhero .orhero-grid b { color: var(--yellow); }
.pqhero .bigplay.ghost { border-color: var(--yellow); color: var(--yellow); }

@media (max-width: 719px) {
  .orhero { padding: 24px 20px; }
  .orhero h2 { font-size: 1.4rem; }
  .bigplay { width: 100%; padding: 9px 16px; font-size: .86rem; }
  .gamehero .gh-row { flex-direction: column; align-items: stretch; }
}

/* ================================================================
   OR GAMES — The Feasible Region's two playables.
   Shares the house game-UI language (.hud/.verdict/.preset) and adds
   only what the two need: a live feasible region, and a search tree.
   ================================================================ */
.orhud {
  display: flex; align-items: baseline; gap: 8px 16px; flex-wrap: wrap;
  padding: 12px 16px; margin-bottom: 14px;
  border: 1px solid var(--border); border-radius: 12px;
  background: linear-gradient(135deg, rgba(45,212,191,.09), transparent 70%);
}
.orhud-k { font-size: .68rem; letter-spacing: .13em; text-transform: uppercase; color: var(--muted); font-weight: 700; }
.orhud-v { font-weight: 700; font-variant-numeric: tabular-nums; margin-right: 6px; }
.orhud-v.big { font-size: 1.45rem; color: var(--teal); }

.orsplit { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: 20px; align-items: start; }
@media (max-width: 720px) { .orsplit { grid-template-columns: 1fr; } }

.orviz { min-width: 0; }
.orsvg { display: block; width: 100%; height: auto; overflow: visible; }
.orregion {
  fill: rgba(45,212,191,.15); stroke: var(--teal); stroke-width: 2.5;
  transition: all .5s cubic-bezier(.22,.7,.3,1);
}
.orline {
  fill: none; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: .45;
  transition: opacity .4s ease, stroke-width .4s ease;
}
.orline.tight { stroke-dasharray: none; stroke-width: 2.6; opacity: 1; }
.oropt {
  fill: var(--yellow); stroke: var(--bg); stroke-width: 2.5;
  transition: cx .55s cubic-bezier(.22,.7,.3,1), cy .55s cubic-bezier(.22,.7,.3,1);
}
.orax { fill: var(--muted); font-weight: 600; font-size: 11px; }
.orcap { font-size: .84rem; color: var(--muted); margin: 10px 0 0; }
.orcap b { color: var(--text); }

.oreyebrow {
  font-size: .7rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--teal); font-weight: 700; margin: 0 0 10px;
}

/* the three purchase buttons — each one is a whole argument, so they are
   cards rather than chips */
.orbuy {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: var(--bg); color: var(--text);
  border: 1px solid var(--border); border-left: 3px solid var(--border);
  border-radius: 11px; padding: 12px 14px; margin: 0 0 10px; font: inherit;
  transition: border-color .18s cubic-bezier(.22,.7,.3,1), transform .18s cubic-bezier(.22,.7,.3,1);
}
.orbuy:hover:not([disabled]) { border-left-color: var(--teal); transform: translateX(3px); }
.orbuy:active:not([disabled]) { transform: translateX(1px) scale(.995); }
.orbuy[disabled] { opacity: .5; cursor: default; }
.orbuy-top { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: .95rem; }
.orbuy-top i { width: 10px; height: 10px; border-radius: 99px; flex: 0 0 auto; }
.orbuy-top em {
  margin-left: auto; font-style: normal; font-size: .66rem; letter-spacing: .1em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 99px; border: 1px solid var(--border);
}
.orbuy-top em.tight { color: var(--teal); border-color: rgba(45,212,191,.5); }
.orbuy-top em.slack { color: var(--muted); }
.orbuy-price { display: block; margin-top: 6px; font-size: 1.05rem; font-weight: 700; color: var(--teal); }
.orbuy[disabled] .orbuy-price, .orbuy-price:empty { color: var(--muted); }
.orbuy-range { display: block; font-size: .82rem; color: var(--muted); margin-top: 2px; }
.orbuy-go { display: block; font-size: .84rem; color: var(--muted); margin-top: 7px; }
.orbuy-go .warn { color: var(--yellow); font-weight: 700; }

.orlog {
  font-size: .8rem; color: var(--muted); margin: 14px 0 0;
  padding-top: 10px; border-top: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}

.orfinal { border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; background: var(--bg); }
.orfinal.win { border-color: var(--teal); background: linear-gradient(135deg, rgba(45,212,191,.12), transparent 70%); }
.orfinal-v { font-size: 2rem; font-weight: 800; color: var(--teal); margin: 0 0 8px; font-variant-numeric: tabular-nums; }
.orfinal-v span { font-size: .85rem; font-weight: 600; color: var(--muted); }
.orfinal p { font-size: .92rem; }
.orfinal-note { font-size: .85rem; color: var(--muted); border-left: 2px solid var(--border); padding-left: 12px; }
.orfinal-note.quantum { border-left-color: var(--violet); }
.orfinal-note b { color: var(--text); }

/* ---------------- The Prune: the search tree ---------------- */
.orbar {
  position: relative; height: 26px; border-radius: 99px; overflow: hidden;
  background: var(--bg); border: 1px solid var(--border); margin-bottom: 14px;
}
.orbar i {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--teal), var(--violet));
  transition: width .5s cubic-bezier(.22,.7,.3,1);
}
.orbar span {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: .76rem; font-weight: 700; color: var(--text); text-shadow: 0 1px 3px var(--bg);
}
.ortreewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.ortree { display: block; width: 100%; min-width: 320px; height: auto; }
.oredge { fill: none; stroke: var(--border); stroke-width: 1.5; }
.oredge.cut, .oredge.heavy { opacity: .3; stroke-dasharray: 3 3; }
.ornode circle { fill: var(--panel); stroke: var(--border); stroke-width: 2; transition: all .25s cubic-bezier(.22,.7,.3,1); }
.ornode text { fill: var(--muted); font-weight: 700; font-size: 10px; }
.ornode.open { cursor: pointer; }
.ornode.open circle { fill: var(--bg); stroke: var(--teal); stroke-width: 2.5; }
.ornode.open text { fill: var(--teal); }
.ornode.open:hover circle, .ornode.open:focus circle { fill: var(--teal); r: 11; }
.ornode.open:focus { outline: none; }
.ornode.done circle { fill: var(--border); stroke: var(--border); }
.ornode.cut circle, .ornode.heavy circle { fill: none; stroke: var(--border); opacity: .45; }
.ornode.cut text.killed { fill: var(--muted); font-size: 8.5px; font-weight: 600; opacity: .8; }
.ornode.leaf circle { fill: var(--muted); stroke: none; }
.ornode.best circle { fill: var(--yellow); stroke: var(--bg); stroke-width: 2; }
@media (prefers-reduced-motion: reduce) {
  .orregion, .oropt, .orbar i, .ornode circle { transition: none; }
}

/* ================================================================
   OR GAMES v2 — levels, always-available controls, and the two
   things the audit found missing: the recipe, and the crates.
   ================================================================ */
.lvbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 14px; }
.lv {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  background: var(--bg); color: var(--muted); font: inherit; font-size: .86rem;
  border: 1px solid var(--border); border-radius: 10px; padding: 8px 13px;
  transition: border-color .18s cubic-bezier(.22,.7,.3,1), color .18s ease, transform .18s cubic-bezier(.22,.7,.3,1);
}
.lv:hover { transform: translateY(-2px); border-color: var(--teal); }
.lv.on { border-color: var(--teal); color: var(--text); background: linear-gradient(135deg, rgba(45,212,191,.14), transparent 75%); }
.lv-n {
  display: grid; place-items: center; width: 20px; height: 20px; border-radius: 99px;
  background: var(--border); color: var(--text); font-size: .72rem; font-weight: 800; flex: 0 0 auto;
}
.lv.on .lv-n { background: var(--teal); color: #04211d; }
.lv.done .lv-n { background: var(--teal); color: #04211d; }
.lv-tick { color: var(--teal); font-weight: 800; }
.orstory { color: var(--muted); font-size: .92rem; font-style: italic; margin: 0 0 14px; }

.orctl { margin-left: auto; display: flex; gap: 7px; }
.ctrlbtn {
  background: none; border: 1px solid var(--border); border-radius: 8px; color: var(--muted);
  font: inherit; font-size: .78rem; padding: 5px 11px; cursor: pointer;
  transition: border-color .16s ease, color .16s ease;
}
.ctrlbtn:hover:not([disabled]) { border-color: var(--teal); color: var(--teal); }
.ctrlbtn[disabled] { opacity: .4; cursor: default; }

.orprog { height: 4px; border-radius: 99px; background: var(--border); overflow: hidden; margin: -6px 0 16px; }
.orprog i {
  display: block; height: 100%; border-radius: 99px;
  background: linear-gradient(90deg, var(--teal), var(--violet));
  transition: width .45s cubic-bezier(.22,.7,.3,1);
}

/* the recipe — what A and B actually earn and consume. The audit found
   players were asked to optimise two products never described anywhere. */
.orrec { width: 100%; border-collapse: collapse; font-size: .86rem; margin: 0 0 4px; }
.orrec th, .orrec td { padding: 6px 8px; text-align: right; border-bottom: 1px solid var(--border); }
.orrec thead th { color: var(--muted); font-size: .72rem; letter-spacing: .09em; text-transform: uppercase; font-weight: 700; }
.orrec tbody th[scope=row] { text-align: left; font-weight: 600; display: flex; align-items: center; gap: 7px; }
.orrec tbody th i { width: 9px; height: 9px; border-radius: 99px; flex: 0 0 auto; }
.orrec .have { font-variant-numeric: tabular-nums; font-weight: 700; }
.orrec .up { color: var(--teal); font-size: .78rem; font-weight: 700; }
.orrec .profitrow th, .orrec .profitrow td { border-bottom: none; color: var(--teal); font-weight: 700; }

/* the crates — the blocker fix. You could not see the knapsack you were solving. */
.orcrates { margin: 16px 0 4px; }
.capnote { color: var(--muted); font-weight: 600; letter-spacing: 0; text-transform: none; font-size: .78rem; margin-left: 8px; }
.cratelist { display: flex; flex-wrap: wrap; gap: 6px; }
.crate {
  display: inline-flex; flex-direction: column; align-items: center; line-height: 1.15;
  border: 1px solid var(--border); border-radius: 8px; padding: 5px 9px; min-width: 46px;
  background: var(--bg); transition: border-color .2s ease, background .2s ease, transform .2s cubic-bezier(.22,.7,.3,1);
}
.crate b { font-size: .92rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.crate i { font-style: normal; font-size: .68rem; color: var(--muted); }
.crate.in {
  border-color: var(--teal); background: linear-gradient(135deg, rgba(45,212,191,.18), transparent 80%);
  transform: translateY(-2px);
}
.crate.in b { color: var(--teal); }

/* Mobile, rebuilt 2026-07-30 ("even in mobile version some inconsistency is
   there"). The old rule hid `.lv-t` outright, so on a phone the level picker
   became a row of bare numbers — and because a cleared level still rendered
   its tick, the chips came out at two different widths (60px vs 42px), which
   is what read as broken. Names now stay and the row scrolls sideways
   instead. Tap targets are also brought up to 44px, which none of these
   controls met (level tabs were 38px, undo/restart 32px). */
@media (max-width: 719px) {
  .orctl { margin-left: 0; width: 100%; }
  .lvbar {
    flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity; padding-bottom: 4px; margin-bottom: 12px;
    scrollbar-width: thin;
  }
  .lv {
    padding: 7px 12px; min-height: 44px; flex: 0 0 auto; scroll-snap-align: start;
    max-width: 46vw;
  }
  .lv:hover { transform: none; }               /* no hover state on touch */
  .lv-t {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
  }
  .ctrlbtn { min-height: 44px; padding: 8px 14px; font-size: .82rem; }
  .orrec { font-size: .82rem; }
}
@media (prefers-reduced-motion: reduce) {
  .lv, .crate, .orprog i { transition: none; }
}

/* Previous questions, inside The Question card. The archive existed but
   was only reachable from the Lattice overlay, so from the homepage the
   questions still appeared to vanish (reported 2026-07-30). */
.dq-prev { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.dq-prev > summary {
  list-style: none; cursor: pointer; display: flex; align-items: center; gap: 9px;
  font-size: .93rem; color: var(--muted); padding: 4px 0;
}
.dq-prev > summary::-webkit-details-marker { display: none; }
.dq-prev > summary b { color: var(--teal); font-weight: 700; }
.dq-prev > summary:hover b { text-decoration: underline; }
.dq-prev-x {
  display: grid; place-items: center; width: 20px; height: 20px; flex: 0 0 auto;
  border: 1px solid var(--border); border-radius: 6px; color: var(--teal); font-weight: 800;
  transition: transform .2s cubic-bezier(.22,.7,.3,1), border-color .2s ease;
}
.dq-prev[open] .dq-prev-x { transform: rotate(45deg); border-color: var(--teal); }
.dq-prev #dq-archive { margin-top: 12px; }
.dq-prev .archq { margin: 8px 0; }
.archq-count { font-size: .88rem; color: var(--muted); margin: 0 0 10px; }
.archq-more { margin: 12px 0 0; font-size: .9rem; }
@media (prefers-reduced-motion: reduce) { .dq-prev-x { transition: none; } }

/* ================================================================
   THE SCENE KIT  (scene.js — Phase 0 of the desk design notes)
   ----------------------------------------------------------------
   A mission stops being a widget in a bordered card and becomes a
   place. Everything here is the frame AROUND a game mount; nothing
   in this block is read by any engine, threshold or scoring path.

   Deliberate: a scene keeps its dark palette in light mode too.
   The rest of the site inverts because it is a document; a world
   is a world, and a corridor lit like a spreadsheet is not one.
   So every colour inside .scene-host is stated outright rather
   than inherited, and contrast is checked against the dark ground.
   ================================================================ */

:root { --scene-sat: 1; }               /* driven by scene.bindCoherence() */

.scene-host {
  position: relative; isolation: isolate; overflow: hidden;
  border-radius: 18px; color: #e5e9f1;
}
.scene-bg {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-size: cover; background-position: center;
  /* Certainty looks like grey: saturation follows the coherence meter. */
  filter: saturate(var(--scene-sat));
}
.scene-canvas { display: block; width: 100%; height: 100%; }
.scene-host > *:not(.scene-bg) { position: relative; z-index: 1; }

/* The stage sits on the world with just enough veil to stay readable. */
.scene-stage {
  position: relative; min-height: 320px; padding: 26px 22px;
  background: linear-gradient(180deg, rgba(11,15,26,.30), rgba(11,15,26,.72));
}
.scene-beat { opacity: 0; transform: translateY(10px); }
.scene-beat.in { opacity: 1; transform: none; transition: opacity .42s ease, transform .42s cubic-bezier(.22,.7,.3,1); }
.scene-beat.out { opacity: 0; transform: translateY(-8px); transition: opacity .3s ease, transform .3s ease; position: absolute; inset: 26px 22px auto; }

/* ---- the act card: one enormous word, one thin rule, a long fade ---- */
.scene-card {
  position: absolute; inset: 0; z-index: 3; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px; text-align: center;
  padding: 30px; background: rgba(11,15,26,.90); opacity: 0;
  transition: opacity .7s ease;
}
.scene-card.in { opacity: 1; }
.scene-card.out { opacity: 0; transition: opacity .5s ease; }
.scene-card .sc-act {
  font-size: .78rem; letter-spacing: .22em; text-transform: uppercase;
  color: #a78bfa; font-weight: 700;
}
.scene-card .sc-word {
  margin: 0; font-size: clamp(2.1rem, 8vw, 4.4rem); font-weight: 800;
  letter-spacing: -.02em; line-height: 1.02; color: #f1f5fb; max-width: 14ch;
}
.scene-card .sc-rule { display: block; width: 74px; height: 1px; background: rgba(229,233,241,.42); }
.scene-card .sc-line {
  margin: 0; max-width: 34ch; font-size: 1.02rem; line-height: 1.55;
  color: rgba(229,233,241,.82); font-style: italic;
}
.scene-card .sc-skip {
  position: absolute; right: 14px; bottom: 12px; background: none; border: 0;
  color: rgba(229,233,241,.55); font: inherit; font-size: .84rem; cursor: pointer;
  /* it shipped at 38px in Phase 1 — every mission opens on it, so it is one of
     the most-tapped controls in the game */
  min-height: 44px; padding: 8px 12px;
}
.scene-card .sc-skip:hover { color: #2dd4bf; }
.scene-card .sc-skip:focus-visible { outline: 2px solid #2dd4bf; outline-offset: 2px; border-radius: 6px; }

/* ---- portraits: drawn by code, and they react ---- */
.scene-face { width: 72px; height: 84px; display: block; flex: none; }
.scene-face--lg { width: 108px; height: 126px; }

/* ---- the sound toggle: obvious, and off until asked ---- */
.scene-audio {
  display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px;
  min-height: 44px; border-radius: 999px; cursor: pointer; font: inherit; font-size: .84rem;
  background: rgba(11,15,26,.55); border: 1px solid rgba(148,163,184,.28); color: rgba(229,233,241,.8);
  transition: border-color .2s ease, color .2s ease;
}
.scene-audio:hover { border-color: rgba(45,212,191,.5); color: #2dd4bf; }
.scene-audio[aria-pressed="true"] { border-color: rgba(45,212,191,.55); color: #2dd4bf; }
.scene-audio[aria-pressed="true"] .sa-ico { color: #2dd4bf; }
.scene-audio:focus-visible { outline: 2px solid #2dd4bf; outline-offset: 2px; }

/* ---- the share card: the frame worth keeping ---- */
.sharecard { margin-top: 16px; }
.sharecard-img {
  display: block; width: 100%; height: auto; max-width: 560px;
  border-radius: 12px; border: 1px solid var(--border);
}
.sharecard-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 12px 0 0; }
.sharecard-note { font-size: .86rem; color: var(--muted); }

@media (max-width: 719px) {
  .scene-stage { padding: 20px 14px; min-height: 260px; }
  .scene-beat.out { inset: 20px 14px auto; }
  .scene-face { width: 58px; height: 68px; }
}
@media (prefers-reduced-motion: reduce) {
  .scene-beat, .scene-beat.in, .scene-beat.out, .scene-card, .scene-audio { transition: none; }
  .scene-beat { opacity: 1; transform: none; }
  .scene-card { opacity: 1; }
}

/* ================================================================
   MISSIONS  (missions.js — Phase 1: Act II, Rue, Grover's Escape)
   ----------------------------------------------------------------
   A mission is a scene with four beats, not a widget with a
   paragraph above it. Everything here is chrome around a mounted
   engine; none of it is read by any engine or scoring path.

   The token block below is deliberate. A mission re-declares the
   dark palette locally, so the world stays a world in light mode
   too — and, more practically, so the engine mounted inside it
   inherits one coherent set of colours instead of a light card
   sitting in a dark corridor.
   ================================================================ */

.mission-scene {
  color-scheme: dark;
  --bg: #121a2e; --panel: #1a2440; --text: #e8ecf6; --muted: #9aa5bd;
  --violet: #a78bfa; --teal: #2dd4bf; --border: #2d3b5e;
  --rowhover: rgba(148,163,184,.06); --shadow: rgba(0,0,0,.45);
  color: var(--text); min-height: 460px;
}
.mission-scene .scene-stage { min-height: 460px; }

.ms-arrive, .ms-ask, .ms-end { max-width: 46ch; margin: 0 auto; text-align: center; }
.ms-arrive { padding-top: 18px; }
.ms-face { display: flex; justify-content: center; margin-bottom: 10px; }
.ms-line {
  margin: 6px 0 10px; padding: 0; border: 0; font-size: clamp(1.05rem, 3.4vw, 1.32rem);
  line-height: 1.55; font-style: italic; color: #f1f5fb;
}
.ms-who { font-size: .8rem; letter-spacing: .14em; text-transform: uppercase; color: var(--violet); font-weight: 700; }
.ms-who span { color: var(--muted); font-weight: 400; letter-spacing: .04em; text-transform: none; }
.ms-go { margin-top: 20px; }

.ms-say { font-size: .98rem; line-height: 1.6; }
.ms-say p { margin: 0 0 10px; }
.ms-say-sm { font-size: .94rem; text-align: center; margin: 4px 0 12px; min-height: 3.2em; }
.ms-quiet { color: var(--muted); font-style: italic; }
.ms-cost {
  margin: 16px 0 0; padding: 11px 14px; border-radius: 12px; font-size: .87rem; line-height: 1.55;
  color: var(--muted); background: rgba(167,139,250,.07); border: 1px solid rgba(167,139,250,.22);
}

.ms-hud { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; }
.ms-face-sm { margin: 0; }
.ms-meter { flex: 1 1 auto; min-width: 0; }
.ms-engine { margin-top: 4px; }

/* the moment she is wrong — three ways out, and the game punishes none of them */
.ms-choice {
  margin: 4px 0 16px; padding: 14px; border-radius: 14px;
  background: rgba(11,15,26,.62); border: 1px solid rgba(167,139,250,.34);
}
.ms-choice-h {
  font-size: .76rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--violet); font-weight: 700; margin-bottom: 10px; text-align: center;
}
.ms-opt {
  display: block; width: 100%; text-align: left; margin: 0 0 8px; padding: 11px 14px;
  min-height: 46px; border-radius: 11px; cursor: pointer; font: inherit; font-size: .95rem;
  color: var(--text); background: rgba(19,26,43,.9); border: 1px solid var(--border);
  transition: border-color .2s ease, transform .2s cubic-bezier(.22,.7,.3,1);
}
.ms-opt span { display: block; font-size: .8rem; color: var(--muted); margin-top: 2px; }
.ms-opt:hover { border-color: var(--teal); transform: translateX(3px); }
.ms-opt:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.ms-opt-key { border-color: rgba(45,212,191,.42); }
.ms-opt-key:hover { border-color: var(--teal); }

.ms-endh { margin: 4px 0 12px; font-size: 1.42rem; font-weight: 800; color: #f1f5fb; }
.ms-duo { margin: 0 0 8px; font-style: italic; font-size: 1.06rem; color: #f1f5fb; }
.ms-note { color: var(--muted); font-size: .9rem; }
.ms-codex {
  margin: 16px 0 0; padding: 13px 15px; border-radius: 13px; text-align: left; font-size: .88rem; line-height: 1.55;
  background: rgba(45,212,191,.08); border: 1px solid rgba(45,212,191,.30); color: var(--text);
}
.ms-codex span { color: var(--muted); }
/* The ceremony through-line: missions.js appends this on any clear that the
   richer .ms-codex box does not already cover, so every clear names the day's
   lesson once and marks a first — the parity with the Arcade's FRAME.ceremony.
   Quiet on purpose: it sits under the ending, not beside the .ms-codex box. */
.ms-through { margin: 14px 0 0; font-size: .84rem; line-height: 1.55; color: var(--muted); }
.ms-through-b {
  font-size: .68rem; letter-spacing: .14em; text-transform: uppercase;
  font-weight: 700; color: var(--teal); margin-right: 6px;
}
.ms-again { margin: 18px 0 0; display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.ms-bar { position: absolute; right: 12px; top: 12px; z-index: 4; display: flex; align-items: center; gap: 8px; }

/* Every beat needs a way out, not just the one after you win — a mission
   with no exit until the end is a trap, and it was one until this button
   existed. Matches .scene-audio's pill so the two read as one control group. */
.ms-leave {
  display: inline-flex; align-items: center; gap: 6px; padding: 7px 13px;
  min-height: 44px; border-radius: 999px; cursor: pointer; font: inherit; font-size: .84rem;
  background: rgba(11,15,26,.55); border: 1px solid rgba(148,163,184,.28); color: rgba(229,233,241,.8);
  transition: border-color .2s ease, color .2s ease;
}
.ms-leave:hover { border-color: rgba(248,113,113,.5); color: #f87171; }
.ms-leave:focus-visible { outline: 2px solid #2dd4bf; outline-offset: 2px; }

@media (max-width: 719px) {
  .mission-scene, .mission-scene .scene-stage { min-height: 420px; }
  .ms-hud { gap: 10px; }
  .ms-bar { right: 8px; top: 8px; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
  .ms-line { font-size: 1.02rem; }
}
@media (prefers-reduced-motion: reduce) { .ms-opt { transition: none; } }

/* The Arcade's showcase card — one rebuilt mission, offered before the floor. */
.showcase {
  position: relative; margin: 0 0 26px; padding: 26px 24px; border-radius: 18px; overflow: hidden;
  border: 1px solid rgba(167,139,250,.34);
  background:
    radial-gradient(90% 130% at 18% 0%, rgba(167,139,250,.16), transparent 62%),
    radial-gradient(70% 120% at 88% 100%, rgba(45,212,191,.13), transparent 60%),
    var(--panel);
}
.showcase-lead { max-width: 60ch; }
.showcase-tag {
  display: inline-block; margin-bottom: 10px; padding: 4px 11px; border-radius: 999px;
  font-size: .72rem; font-weight: 800; letter-spacing: .13em; text-transform: uppercase;
  color: var(--violet); background: rgba(167,139,250,.14); border: 1px solid rgba(167,139,250,.34);
}
.showcase h2 { margin: 0 0 10px; }
.showcase h2::after { display: none; }
.showcase p { margin: 0 0 10px; }
.showcase-note { font-size: .86rem; color: var(--muted); }
.showcase .bigplay { margin-top: 8px; }
@media (max-width: 719px) { .showcase { padding: 20px 16px; } }

/* Touch targets. The 2026-07-30 mobile pass raised `.ctrlbtn` and `.lv` to the
   44px minimum but missed `.preset`, which is the most-used control on the
   site — it drives every widget on every page and sat at 30px. Found while
   measuring the rebuilt mission at 375px, 2026-08-02. */
@media (max-width: 719px) {
  .preset { min-height: 44px; padding-top: 9px; padding-bottom: 9px; }
}

/* ---- Act I's dial: the six gates as turns, not letters -----------------
   14_ §5 asked for a different verb — turn, not type. So the lettered
   buttons become detents on a ring, each saying what it actually is
   (a half turn about X, an eighth about Z), which is more tactile AND
   more true than "X" was. Drag it, tap a detent, or focus one and press
   Enter — a control you can only drag is a control some people cannot
   use at all. */
.ms-dialwrap { text-align: center; margin: 2px 0 12px; }
.ms-dial { width: min(300px, 74vw); height: auto; touch-action: none; cursor: grab; display: inline-block; }
.ms-dial:active { cursor: grabbing; }
.dial-plate { fill: rgba(11,15,26,.55); stroke: rgba(148,163,184,.22); stroke-width: 1.5; }
.dial-hub   { fill: rgba(19,26,43,.92); stroke: rgba(167,139,250,.30); stroke-width: 1.5; }
.dial-needle { stroke: var(--teal); stroke-width: 3.5; stroke-linecap: round;
  transition: all .22s cubic-bezier(.22,.7,.3,1); }
.dial-label { text-anchor: middle; fill: #f1f5fb; font-size: 15px; font-weight: 800; }
.dial-sub   { text-anchor: middle; fill: #9aa5bd; font-size: 9.5px; }
.dial-detent circle {
  fill: rgba(19,26,43,.95); stroke: rgba(148,163,184,.30); stroke-width: 1.5;
  transition: fill .18s ease, stroke .18s ease;
}
.dial-detent { cursor: pointer; }
.dial-detent:hover circle, .dial-detent.on circle { stroke: var(--teal); fill: rgba(45,212,191,.14); }
.dial-detent:focus-visible { outline: none; }
.dial-detent:focus-visible circle { stroke: var(--violet); stroke-width: 3; }
.dial-g  { text-anchor: middle; fill: #e8ecf6; font-size: 10.5px; font-weight: 800; pointer-events: none; }
.dial-ax { text-anchor: middle; fill: #9aa5bd; font-size: 8px; pointer-events: none; }
.ms-dialhint { font-size: .82rem; color: var(--muted); margin: 8px auto 0; max-width: 40ch; }

/* Ada will tell you the route, and being told is not the same as knowing */
.ms-tell {
  display: inline-flex; align-items: center; gap: 9px; margin: 0 0 12px; padding: 9px 15px;
  min-height: 44px; border-radius: 999px; cursor: pointer; font: inherit; font-size: .9rem;
  color: var(--text); background: rgba(167,139,250,.10); border: 1px solid rgba(167,139,250,.34);
  transition: border-color .2s ease;
}
.ms-tell:hover:not(:disabled) { border-color: var(--violet); }
.ms-tell:disabled { opacity: .5; cursor: not-allowed; }
.ms-tell:focus-visible { outline: 2px solid var(--violet); outline-offset: 2px; }
.ms-tell .gl { color: var(--violet); }
.ms-tell-cost { font-size: .8rem; color: var(--muted); }
.ms-told {
  margin: 0 0 12px; padding: 10px 14px; border-radius: 12px; font-size: .9rem; line-height: 1.55;
  background: rgba(167,139,250,.09); border: 1px solid rgba(167,139,250,.26); color: var(--text);
}
.ms-told .gl { color: var(--violet); }
.ms-told em { color: var(--muted); font-size: .86rem; }
.ms-hint { color: var(--muted); font-weight: 400; }

@media (max-width: 719px) { .ms-dial { width: min(270px, 82vw); } }
@media (prefers-reduced-motion: reduce) {
  .dial-needle, .dial-detent circle, .ms-tell { transition: none; }
}

/* two rebuilt acts, offered side by side above the cabinet floor */
.showcase-picks { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); margin-top: 18px; }
.pick {
  display: flex; flex-direction: column; gap: 5px; text-align: left; padding: 16px 18px;
  border-radius: 15px; cursor: pointer; font: inherit; color: var(--text);
  background: color-mix(in srgb, var(--bg) 34%, transparent); border: 1px solid var(--border);
  transition: border-color .2s ease, transform .2s cubic-bezier(.22,.7,.3,1);
}
.pick:hover { border-color: var(--teal); transform: translateY(-3px); }
.pick:focus-visible { outline: 2px solid var(--teal); outline-offset: 3px; }
.pick-act { font-size: .72rem; letter-spacing: .13em; text-transform: uppercase; color: var(--violet); font-weight: 800; }
.pick-title { font-size: 1.12rem; font-weight: 800; line-height: 1.25; }
.pick-blurb { font-size: .88rem; color: var(--muted); line-height: 1.5; }
.pick-go { margin-top: 6px; font-size: .84rem; font-weight: 700; color: var(--teal); }
@media (prefers-reduced-motion: reduce) { .pick { transition: none; } }

/* Three measured schedules side by side — the Salt Flat's point, shown
   rather than asserted: the crude schedule beats the clever one. */
.ms-scores { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin: 14px 0 6px; }
.ms-scores > div {
  flex: 1 1 110px; max-width: 150px; padding: 12px 10px; border-radius: 12px; text-align: center;
  background: rgba(11,15,26,.45); border: 1px solid var(--border);
}
.ms-scores > div.hi { border-color: rgba(45,212,191,.5); background: rgba(45,212,191,.09); }
.ms-scores b { display: block; font-size: 1.42rem; font-weight: 800; color: var(--text); line-height: 1.1; }
.ms-scores > div.hi b { color: var(--teal); }
.ms-scores span { display: block; margin-top: 5px; font-size: .76rem; color: var(--muted); line-height: 1.4; }

/* Kai and Lyra are two portraits, always shown together — they are one pair */
.ms-face-pair { display: flex; justify-content: center; gap: 6px; }
.ms-nosig {
  margin: 4px 0 14px; padding: 14px; border-radius: 14px;
  background: rgba(11,15,26,.55); border: 1px solid rgba(148,163,184,.26);
}
.ms-nosig .ms-scores { margin: 8px 0 4px; }
.ms-nosig .ms-note { margin: 6px 0 0; text-align: center; }

/* ================================================================
   THE DECODER DUEL, mounted anywhere (games.js `duel`)
   ----------------------------------------------------------------
   qec.html has drawn this board since 2026-07-20 from its own page
   <style>. The engine now also runs inside the Path and the Arcade,
   so the board's own rules have to travel with it — scoped under
   .duelwrap so they cannot reach any other .dq/.pq on the site
   (the figure code blocks use those class names too).
   The shared kit (.hud, .dot, .verdict, .rows, .chip) is already
   global above and is reused unchanged.
   ================================================================ */
.duelwrap .duelboard { display: block; margin: 0 auto; max-width: 360px; width: 100%; height: auto; }
@keyframes alarm { 50% { fill: rgba(250,204,21,.36); } }
.duelwrap .pq { fill: var(--panel); stroke: var(--teal); stroke-opacity: .35; stroke-width: 1.5; transition: fill .15s; }
.duelwrap .pq.lit { fill: rgba(250,204,21,.22); stroke: var(--yellow); stroke-opacity: 1; animation: alarm 1.6s ease-in-out infinite; }
.duelwrap .px { fill: none; stroke: var(--border); stroke-dasharray: 4 4; }
.duelwrap .plabel { fill: var(--muted); font-size: 12px; font-family: inherit; pointer-events: none; }
.duelwrap .dq { fill: var(--bg); stroke: var(--muted); stroke-width: 2; cursor: pointer; transition: fill .15s; }
.duelwrap .dq:focus, .duelwrap .dqwrap:hover .dq { stroke: var(--teal); outline: none; }
/* the 44px touch target, invisible, sitting over a 21px drawn qubit */
.duelwrap .dqhit { fill: transparent; stroke: none; cursor: pointer; }
.duelwrap .dq.armed { stroke: var(--teal); stroke-width: 2.5; }
.duelwrap .dq.flip { fill: var(--red); stroke: var(--red); }
.duelwrap .dq.flip.popin { animation: flippop .42s ease-out; transform-box: fill-box; transform-origin: center; }
.duelwrap .pickring { fill: none; stroke: var(--teal); stroke-width: 2.5; }
.duelwrap .decring { fill: none; stroke: var(--violet); stroke-width: 2; stroke-dasharray: 4 3; }
.duelwrap .duelctl { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 4px; }
.duelwrap .duelnote { margin: 8px 0 0; color: var(--muted); font-size: .9rem; }
.duelwrap .duellog table { font-size: .85rem; margin-top: 8px; }
.duelwrap .duellog td, .duelwrap .duellog th { padding: 3px 8px; }
@media (prefers-reduced-motion: reduce) {
  .duelwrap .pq.lit, .duelwrap .dq.flip.popin { animation: none; }
}

/* ================================================================
   ACT VI — THE KNOT  (missions.js `knot`)
   ----------------------------------------------------------------
   The Hollow Oracle has no face on purpose. It is not a person; it
   is a sentence people say. So it gets a voice with no portrait,
   set in the one register the rest of the scene never uses — flat,
   grey, certain. Certainty looks like grey (14_ §6.4), and this is
   that rule applied to a character.
   ================================================================ */
.ms-oracle {
  margin: 4px 0 12px; padding: 11px 15px; border-radius: 4px; text-align: left;
  background: rgba(148,163,184,.07); border-left: 3px solid rgba(148,163,184,.5);
  color: #c3cad9; font-size: .93rem; line-height: 1.55; letter-spacing: .01em;
}
.ms-oracle b {
  display: block; font-size: .7rem; letter-spacing: .18em; text-transform: uppercase;
  color: #8b95a9; font-weight: 700; margin-bottom: 4px;
}
.ms-oracle.hollow { opacity: .5; border-left-color: rgba(148,163,184,.22); font-style: italic; }
.ms-obey {
  display: block; width: 100%; margin: 0 0 10px; padding: 10px 14px; min-height: 46px;
  border-radius: 4px; cursor: pointer; font: inherit; font-size: .92rem; text-align: left;
  color: #c3cad9; background: rgba(148,163,184,.08); border: 1px solid rgba(148,163,184,.3);
  transition: border-color .2s ease, color .2s ease;
}
.ms-obey span { display: block; font-size: .78rem; color: var(--muted); margin-top: 2px; }
.ms-obey:hover { border-color: rgba(148,163,184,.6); color: var(--text); }
.ms-obey:focus-visible { outline: 2px solid var(--muted); outline-offset: 2px; }
.ms-obey[disabled] { opacity: .4; cursor: default; }
/* naming the defect. Its own little board, because the engine's qubits already
   mean "the repair I am committing this round" and one control must not mean
   two things at once. */
.ms-namegrid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
  max-width: 190px; margin: 0 auto 10px;
}
.ms-q {
  min-height: 46px; border-radius: 10px; cursor: pointer; font: inherit; font-size: .9rem;
  color: var(--text); background: rgba(19,26,43,.9); border: 1px solid var(--border);
  transition: border-color .18s ease, background .18s ease;
}
.ms-q:hover { border-color: rgba(45,212,191,.5); }
.ms-q:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.ms-q.on { border-color: var(--teal); background: rgba(45,212,191,.14); color: var(--teal); font-weight: 700; }
.ms-name { margin: 0 0 10px; font-size: .9rem; color: var(--muted); text-align: center; }
.ms-name b { color: var(--teal); font-weight: 700; }
.ms-tier {
  margin: 12px 0 0; padding: 12px 14px; border-radius: 12px; font-size: .86rem; line-height: 1.6;
  color: var(--muted); background: rgba(11,15,26,.55); border: 1px solid rgba(148,163,184,.24); text-align: left;
}
.ms-tier b { color: #dbe2ef; }
.ms-tier .tier { white-space: nowrap; }

/* ==========================================================================
   DEPTH TOGGLE, CHECK-YOUR-UNDERSTANDING, PATHWAYS   (tiers.js, 2026-08-05)
   The three tiers stopped being one article in three costumes, so the reader
   now needs a way to choose between them that is not the nav.
   ========================================================================== */

.tier-lede { color: var(--muted); font-size: .95rem; border-left: 2px solid var(--border);
  padding-left: 14px; margin: 0 0 22px; }

/* --- the depth bar ------------------------------------------------------- */
.tbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 26px 0 24px; padding: 12px 14px; border: 1px solid var(--border);
  border-radius: 14px; background: var(--panel); position: sticky; top: calc(var(--nav-h, 82px) + 4px); z-index: 20;
  backdrop-filter: blur(9px); }
.tbar-lab { font-size: .78rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); font-weight: 600; }
.tbar-btns { display: flex; gap: 6px; flex-wrap: wrap; }
.tbar-blurb { font-size: .85rem; color: var(--muted); margin-left: auto; }

.tbtn { display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font: inherit; font-size: .85rem; font-weight: 600; color: var(--muted);
  background: transparent; border: 1px solid var(--border); border-radius: 99px;
  padding: 7px 14px; min-height: 44px; transition: color .18s, border-color .18s, background .18s; }
.tbtn:hover { color: var(--text); border-color: var(--muted); }
.tbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.tbtn-chip { font-size: .95em; line-height: 1; }
.tbtn.is-on { color: var(--text); border-color: currentColor; background: rgba(255,255,255,.04); }
.tbtn.t-g.is-on { color: var(--green);  border-color: rgba(74,222,128,.55);  background: rgba(74,222,128,.10); }
.tbtn.t-y.is-on { color: var(--yellow); border-color: rgba(250,204,21,.55);  background: rgba(250,204,21,.10); }
.tbtn.t-r.is-on { color: var(--red);    border-color: rgba(248,113,113,.55); background: rgba(248,113,113,.10); }
.tbtn.t-all.is-on { color: var(--teal); border-color: rgba(45,212,191,.55);  background: rgba(45,212,191,.10); }


/* --- "so what" panel, 🟢 only -------------------------------------------- */
.sowhat { border: 1px solid rgba(74,222,128,.32); background: rgba(74,222,128,.06);
  border-radius: 14px; padding: 18px 20px; margin: 26px 0; }
.sowhat h3 { margin: 0 0 8px; color: var(--green); font-size: 1.05rem; }
.sowhat p:last-child { margin-bottom: 0; }

/* --- open problems list, 🔴 only ----------------------------------------- */
.openq { padding-left: 20px; }
.openq li { margin-bottom: 12px; }
.refs p { margin: 0; line-height: 1.85; }

/* --- check your understanding -------------------------------------------- */
.cyu { border: 1px solid var(--border); border-radius: 14px; padding: 18px 20px;
  margin: 28px 0; background: var(--panel); }
.cyu-q { font-weight: 600; margin: 0 0 4px; }
.cyu-eyebrow { font-size: .72rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--teal); font-weight: 700; margin: 0 0 8px; }
.cyu-opts { display: flex; flex-direction: column; gap: 8px; margin: 14px 0 0; }
/* The options sit ON the card, so they take the next step of the surface
   ladder rather than `transparent`. Until 2026-09-22 these were unfilled
   outlines on a panel, which read as a bulleted list rather than as four
   things you can press -- a small cost when eleven of twelve questions were
   hidden behind a depth, a real one now that every reader meets them. */
.cyu-opts button { text-align: left; font: inherit; font-size: .93rem; cursor: pointer;
  color: var(--text); background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 11px 14px; min-height: 44px;
  transition: border-color .18s, background .18s, opacity .18s; }
.cyu-opts button:hover:not(.locked) { border-color: var(--muted); background: var(--surface-3); }
.cyu-opts button:active:not(.locked) { background: var(--surface-2); }
.cyu-opts button:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.cyu-opts button.locked { cursor: default; }
/* Once answered, the options nobody picked and that were not the answer step
   back, so the eye lands on the verdict instead of re-reading four choices. */
.cyu-opts button.locked:not(.is-right):not(.is-wrong) { opacity: .45; }
.cyu-opts button.is-right { border-color: rgba(74,222,128,.6); background: rgba(74,222,128,.10); color: var(--green); }
.cyu-opts button.is-wrong { border-color: rgba(248,113,113,.5); background: rgba(248,113,113,.08); color: var(--red); }
.cyu-out { margin: 14px 0 6px; font-weight: 600; font-size: .9rem; }
.cyu-out.ok { color: var(--green); } .cyu-out.no { color: var(--yellow); }
.cyu-why { margin: 0; color: var(--muted); font-size: .93rem; line-height: 1.7; }
.cyu-why strong { color: var(--text); }

/* --- onward pathway ------------------------------------------------------ */
.pathway { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
  gap: 12px; margin: 30px 0 8px; }
.pathway a { display: block; border: 1px solid var(--border); border-radius: 14px;
  padding: 16px 18px; text-decoration: none; background: var(--panel);
  transition: border-color .18s, transform .18s; }
.pathway a:hover { border-color: var(--teal); transform: translateY(-2px); }
.pathway .pw-k { display: block; font-size: .72rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
.pathway .pw-t { display: block; font-weight: 700; color: var(--text); margin-bottom: 4px; }
.pathway .pw-s { display: block; font-size: .87rem; color: var(--muted); line-height: 1.55; }

.pathway-nudge { border: 1px solid var(--border); border-left: 3px solid var(--teal);
  border-radius: 12px; padding: 14px 18px; margin: 22px 0; background: var(--panel);
  font-size: .93rem; color: var(--muted); animation: tfade .3s ease both; }
.pathway-nudge.warm { border-left-color: var(--green); }
.pathway-nudge strong { color: var(--text); }
.pathway-nudge a { font-weight: 600; }

/* --- reading progress ---------------------------------------------------- */
.readbar { position: fixed; top: 0; left: 0; right: 0; height: 2px; z-index: 90;
  background: transparent; pointer-events: none; }
.readbar i { display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--teal), var(--violet)); }

/* --- the corrections log ------------------------------------------------- */
/* The per-entry rules (.corr, .corr-head, .corr-where, .corr-kind + the
   .k-num/.k-lab/.k-code kind tints, .corr dl/dt/dd, .corr-caught, .corr-lesson,
   .corr-filters, .tbar-links) were removed 2026-08-29. They dressed the
   21-entry filterable log that was retired on 2026-08-15 when the page reset
   to "0 corrections -- the count starts at launch"; `<ol class="corr-log">` is
   now empty and no page or script references any of them. What stays is what
   the live page still uses: the summary cards, the empty log container, the
   .corr-f filter buttons tiers.js queries, and the format note. */
.corr-summary h3 { margin-top: 0; }
.corr-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px; margin: 18px 0; }
.corr-stats div { border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px;
  background: rgba(255,255,255,.02); }
.corr-stats b { display: block; font-size: 1.6rem; line-height: 1.1; color: var(--teal); }
.corr-stats span { display: block; font-size: .8rem; color: var(--muted); margin-top: 4px; line-height: 1.45; }

.corr-f { font: inherit; font-size: .82rem; font-weight: 600; cursor: pointer;
  color: var(--muted); background: transparent; border: 1px solid var(--border);
  border-radius: 99px; padding: 8px 14px; min-height: 44px; transition: color .18s, border-color .18s; }
.corr-f:hover { color: var(--text); border-color: var(--muted); }
.corr-f:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.corr-f.is-on { color: var(--text); border-color: var(--teal); background: rgba(45,212,191,.10); }

.corr-log { list-style: none; padding: 0; margin: 0; counter-reset: corr; }
.corr-format { color: var(--muted); font-size: .93rem; margin: 22px 0 8px; }

@media (max-width: 620px) {
  .tbar { position: static; gap: 8px; padding: 12px; }
  .tbar-blurb { margin-left: 0; width: 100%; }
}
a.tbtn { text-decoration: none; }

/* ================================================================
   INTERACTION POLISH (2026-08-19) — one press-feedback language for
   every custom control that didn't already have one.

   The site had two button languages by accident, not by design:
   .preset/.bigplay/.sqform button/.orbuy all press down on :active
   (translateY(1px) scale(.985), or the equivalent), while roughly a
   dozen other clickable classes — pill toggles, OR-tool chips, the
   volcano's dial detents, mission choice buttons — had a :hover but
   nothing at all on :active, so clicking them read as inert next to
   the ones that already got this treatment. This closes that gap
   using the SAME motion already approved (translateY/scale on press,
   the site's one easing curve cubic-bezier(.22,.7,.3,1)) rather than
   inventing a louder one — see memory symbiq-button-taste.md:
   Chinmoy dialled .bigplay BACK from flashier/bigger on 2026-08-07,
   so "more attractive" here means *consistent*, not *bigger*.

   Appended at file end deliberately: several of these classes carry
   a :hover transform declared earlier in the cascade, and every rule
   below has equal specificity to its own :hover sibling (one class +
   one pseudo-class), so source order is what decides which wins when
   a real click holds both states true at once — being last means the
   press always wins over the hover-lift for that instant.
   ================================================================ */

/* pill-shaped toggles and leave-controls: hover only ever changed
   colour, so give them the same press-down the filled buttons have */
.tbtn:active, .corr-f:active, .scene-audio:active, .ms-leave:active,
.ms-tell:active:not(:disabled) {
  transform: scale(.97);
}

/* bordered controls that already lift/slide on hover — press-down
   replaces rather than fights that motion for the moment of the click */
.hole:active, .orjump:active, .ms-opt:active, .lv:active, .ctrlbtn:active:not([disabled]),
.pick:active, button.nextup:active {
  transform: scale(.97) !important;
}

/* SVG-drawn controls: a raw shape's transform-origin sits at the SVG
   viewport corner, not its own centre, so scaling it on press lands
   off-centre and looks like a glitch. Brightness reads as "pressed"
   without moving anything. */
.mc-node:active, .duelwrap .dq:active, .dial-detent:active circle, .ornode.open:active circle {
  filter: brightness(1.4);
}

/* The estate tool's "Load a code" button (journey.html) was the one
   button on the site hand-styled with an inline background/border
   override instead of a class — which meant it kept .sqform button's
   base teal glow-shadow under a transparent fill (a shadow with
   nothing to cast it) and had no hover/press feedback of its own.
   Given a real ghost variant, matching .bigplay.ghost's pattern. */
.sqform button.ghost {
  background: transparent; color: var(--teal); border: 1.5px solid var(--teal); box-shadow: none;
}
.sqform button.ghost:hover { box-shadow: 0 4px 12px rgba(45,212,191,.14); filter: none; }
.sqform button.ghost:active { filter: none; }
@media (prefers-color-scheme: light) {
  .sqform button.ghost { color: #0f766e; border-color: #0f766e; }
}

@media (prefers-reduced-motion: reduce) {
  .tbtn, .corr-f, .scene-audio, .ms-leave, .ms-tell, .hole, .orjump, .ms-opt, .lv, .ctrlbtn, .pick,
  .mc-node, .duelwrap .dq, .dial-detent circle, .ornode.open circle { transition: none; }
}

/* A real photograph inside an article (2026-09-26). Credit and licence go in the caption, next to
   the picture, because a CC BY licence asks for that. Width/height attributes are set in the
   markup so the page does not jump when the file arrives. */
.photo { margin: 18px 0; }
.photo img { display: block; width: 100%; height: auto; border-radius: 12px; border: 1px solid var(--border); }
.photo figcaption { margin-top: 6px; font-size: .8rem; line-height: 1.5; color: var(--muted); }
.photo figcaption a { color: inherit; text-decoration: underline; }
/* Two photographs side by side under one caption; one column on a phone. */
.photo-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.photo-pair figcaption { grid-column: 1 / -1; }
.photo-pair img { height: 100%; object-fit: cover; }
@media (max-width: 560px) { .photo-pair { grid-template-columns: 1fr; } }
/* A line-art diagram (dark strokes on transparent) needs a light ground in every theme. */
.photo-portrait { max-width: 300px; margin-left: auto; margin-right: auto; }
.photo-portrait figcaption { text-align: left; }
.photo-light img { background: #fff; padding: 12px; box-sizing: border-box; object-fit: contain; max-height: 260px; }

/* =====================================================================================
   THE RESPONSIVE LAYER (2026-09-26): every screen, from a 280px folded cover to a tri-fold
   opened flat. Checked by tools/verify_devices.mjs across 21 real device sizes.

   What was wrong, measured at 390px: text used 90% of the screen (20px gutters plus card
   padding on top), the root type was a fixed 18px on every phone, and an open menu could push
   its neighbours into a second flex column off the right edge.

   1. TYPE. Fixed 18px became fluid on phones: 18px at 320 wide, ~19.8px at 390, capped at
      20.5px from about 430 up to the tablet breakpoint. Desktop is unchanged.
   2. GUTTERS. 12-20px, scaled to the screen, and never less than the device's safe-area inset
      (a notch, a rounded corner, a curved fold edge). Needs viewport-fit=cover in each page's
      viewport meta, which every page now carries.
   3. CARDS run edge to edge on phones, so their text lines up with the prose above and below
      instead of sitting in a narrower box.
   4. Foldables: when the browser reports two screens with a hinge between them, the page stays on
      the first screen and never puts a line of text across the hinge.
   5. Short landscape phones lose the sticky header (it was a fifth of the screen).
   ===================================================================================== */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }   /* no inflation on rotate */
:root { --gutter: clamp(12px, 3.4vw, 20px); }
.wrap {
  --gl: max(var(--gutter), env(safe-area-inset-left, 0px));
  --gr: max(var(--gutter), env(safe-area-inset-right, 0px));
  padding-left: var(--gl); padding-right: var(--gr);
  padding-bottom: max(80px, calc(env(safe-area-inset-bottom, 0px) + 56px));
}
@media (max-width: 719.98px) {
  html { font-size: clamp(18px, 12px + 2vw, 20.5px); }
  /* the header: brand, Menu, Path link and account on one row wherever they can, tighter where not */
  nav:not(.rung-rail) { gap: 6px 10px; padding: 10px 0; margin-bottom: 22px; }
  #navcats { flex-wrap: nowrap; }        /* was inherited `wrap`: a tall open menu pushed the next menus into a column off-screen */
  .navpanel { max-width: 100%; }
  /* iOS zooms the page when a field under 16px takes focus */
  input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]),
  textarea, select { font-size: max(16px, 1rem); }
  .wrap p, .wrap li { hyphens: auto; -webkit-hyphens: auto; }
}
@media (max-width: 359.98px) {
  nav .brand { font-size: 1rem; gap: 7px; }
  nav .brand svg { width: 26px; height: 26px; }
  #navtrig { padding: 7px 11px 7px 9px; }
  nav a.pathlink { font-size: .82rem; }
}
/* the original Galaxy Fold's cover screen is 280px wide: logo only, wordmark kept for screen readers */
@media (max-width: 319.98px) {
  nav .brand { font-size: 0; gap: 0; }
  nav .brand svg { width: 30px; height: 30px; }
}
@media (max-width: 560px) {
  /* Cards bleed to the screen edge; their padding equals the page gutter, so text keeps one left edge. */
  .wrap > .card, .wrap > .grid > .card {
    margin-left: calc(-1 * var(--gl)); margin-right: calc(-1 * var(--gr));
    border-radius: 0; border-left-width: 0; border-right-width: 0;
    padding-left: var(--gl); padding-right: var(--gr);
  }
  .card { padding-top: 16px; padding-bottom: 16px; }
  .wrap > .card:hover, .wrap > .grid > .card:hover { transform: none; }
}
/* Menus at tablet and desktop width: the last three tabs open their panel toward the left so it cannot
   run past the right edge, and no panel is wider than the screen. */
@media (min-width: 720px) {
  .navpanel { max-width: min(380px, calc(100vw - 24px)); }
  .navpanel-wide { max-width: none; }
  .navcat[data-cat="reality"] > .navpanel, .navcat[data-cat="community"] > .navpanel,
  .navcat[data-cat="play"] > .navpanel { left: auto; right: 0; }
  .navpanel { max-height: calc(100vh - 110px); max-height: calc(100dvh - 110px); }
}
/* Foldables and dual-screen devices that report a hinge: keep the page on the first screen. */
@media (horizontal-viewport-segments: 2) {
  .wrap { max-width: min(env(viewport-segment-width 0 0, 780px), 780px); margin-left: 0; margin-right: auto; }
}
/* A phone on its side has ~375px of height: a sticky 82px header would be a fifth of it. */
@media (orientation: landscape) and (max-height: 520px) {
  nav:not(.rung-rail) { position: relative; }
  html { scroll-padding-top: 14px; }
}
/* A long button label wraps inside the button rather than pushing the page wider than the screen. */
.sqform button, .preset, .bigplay, .herocta-alt { max-width: 100%; }
.sqform button { white-space: normal; text-align: center; }
.gamerules .gr-txt b, .gamerules .gr-txt i { overflow-wrap: anywhere; }
@media (max-width: 560px) { .gameabout a, .mod-out a { white-space: normal; } }
.lm-steps { padding-left: 1.35em; margin: 8px 0 18px; } .lm-steps li { margin: 0 0 10px; padding-left: 4px; }

/* ===== 2026-10-04 bold pass, site-wide: bigger headline, tinted organic cards, a scroll-progress thread, CSS-only scroll reveals.
   Everything here is transform / paint only (no layout change, so no CLS), uses the existing tokens (dark, dim and light all follow),
   and the blanket reduced-motion rule above switches every animation off. ===== */
@media (min-width: 700px) { h1 { font-size: clamp(2rem, 1.4rem + 1.6vw, 2.75rem); letter-spacing: -.012em; } }
h1 { background-size: 220% 100%; }
@media (prefers-reduced-motion: no-preference) { h1 { animation: fadeUp 0.5s ease-out both, h1-settle 700ms ease-out both, h1-sheen 14s ease-in-out 1s infinite alternate; } }
@keyframes h1-sheen { from { background-position: 0% 0; } to { background-position: 100% 0; } }
h2::before { width: 4px; box-shadow: 0 0 14px -1px color-mix(in srgb, var(--teal) 70%, transparent); }
.card { position: relative; border-radius: 14px 24px 16px 26px;
  background: linear-gradient(155deg, color-mix(in srgb, var(--teal) 7%, var(--panel)), var(--panel) 46%, color-mix(in srgb, var(--violet) 6%, var(--panel))); }
.card:nth-of-type(even) { border-radius: 24px 14px 26px 16px; }
.card::before { content: ""; position: absolute; left: 22px; right: 22px; top: -1px; height: 2px; border-radius: 2px; pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--violet), var(--teal), transparent); opacity: .55; transition: opacity .25s ease; }
.card:hover { box-shadow: 0 16px 36px -14px color-mix(in srgb, var(--teal) 45%, transparent), 0 10px 28px var(--shadow); }
.card:hover::before { opacity: 1; }
::selection { background: color-mix(in srgb, var(--teal) 40%, transparent); }
.wrap p > a:not([class]), .wrap li > a:not([class]) { background: linear-gradient(var(--teal), var(--teal)) 0 100% / 0 1.5px no-repeat; transition: background-size .25s ease, text-decoration-color .15s ease; }
.wrap p > a:not([class]):hover, .wrap li > a:not([class]):hover { background-size: 100% 1.5px; }
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    html::before { content: ""; position: fixed; left: 0; top: 0; height: 3px; width: 100%; z-index: 9999; pointer-events: none; transform-origin: 0 50%;
      background: linear-gradient(90deg, var(--violet), var(--teal), #fbbf24); animation: sq-thread linear both; animation-timeline: scroll(root block); }
    @keyframes sq-thread { from { transform: scaleX(0); } to { transform: scaleX(1); } }
    .wrap > .card, .wrap > .tiles, .wrap > figure { animation: sq-rise linear both; animation-timeline: view(); animation-range: entry 0% entry 30%; }
    @keyframes sq-rise { from { transform: translateY(26px) scale(.985); } to { transform: none; } }
  }
}

/* Skip link (written by build_nav.py on every page): off-screen until it takes keyboard focus, then the first
   thing on the page. The target is an empty span after the header, so focus lands where the content starts. */
.skiplink { position: absolute; left: 12px; top: -60px; z-index: 10000; padding: 10px 16px; border-radius: 10px;
  background: var(--accent, #2dd4bf); color: #04121a; font-weight: 700; text-decoration: none; }
.skiplink:focus { top: 12px; outline: 2px solid currentColor; outline-offset: 2px; }
.skiptarget { display: block; height: 0; outline: none; }
/* --- motion.css --- */
/* ================================================================
   MOTION LAYER (2026-08-27)

   A SEPARATE stylesheet on purpose. style.css is 117KB across 24 pages and
   index.html already records the 2026-07-24 scar: touching it forces a
   cache-buster bump on every page that loads it. Nothing in here is needed
   by style.css and nothing in style.css is needed by this, so it ships as
   its own file with its own version query. Adding motion should never be a
   24-page deploy.

   TWO TIERS, DELIBERATELY SPLIT:

     - Everything under §1 and §2 is safe on every page and ships everywhere.
       Tokens are additive (no existing name is redefined) and view
       transitions degrade to "the navigation you already had".

     - Everything under §3 onward is gated behind [data-motion] on <body>.
       Scroll reveals and hover changes touch layout-adjacent properties, and
       23 of these pages have widgets this file has never been tested
       against. So pages opt IN, one attribute at a time, rather than
       inheriting motion by surprise. index.html is the first opt-in.

   THE TRANSFORM RULE (read before adding anything here):
   reveals animate `translate`, NEVER `transform`. style.css already uses
   transform for hover lifts (.card:hover, .introute-card:hover). A running
   animation with fill:both beats a normal declaration in the cascade, so
   animating transform here would silently kill every hover lift on the page.
   `translate` and `transform` are independent properties that compose, so
   the reveal and the lift coexist. This is the one rule that is not
   negotiable in this file.
   ================================================================ */

/* ---------------------------------------------------------------- §1
   MOTION TOKENS

   The 12 @keyframes already in style.css were each invented locally --
   every one picked its own duration and its own easing, because there was
   nothing to inherit. Same problem --sp-* / --r-* solved for spacing on
   2026-08-23; this is that fix applied to time. Additive only: no existing
   animation is retimed, these exist so the NEXT one doesn't guess.
   ---------------------------------------------------------------- */
:root {
  --dur-fast: 140ms;    /* state echo: press, toggle, focus */
  --dur-base: 260ms;    /* the default; matches the .18s-.3s already in use */
  --dur-slow: 620ms;    /* arrivals and departures */
  --dur-amble: 8s;      /* ambient loops that must never draw the eye */

  /* Decelerating, no overshoot. Things arrive and settle; nothing bounces.
     A site whose thesis is "the mathematics does the judging" should not
     have springy furniture. */
  --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}

/* ---------------------------------------------------------------- §2
   CROSS-DOCUMENT VIEW TRANSITIONS

   The single highest ratio of perceived quality to lines of code available
   to this site. 24 static pages that currently flash white between every
   click become one continuous surface. Chromium and Safari 18.2+ honour it;
   Firefox is still behind a flag at time of writing and simply gets today's
   navigation, which is the correct fallback -- nothing to detect, nothing
   to polyfill.

   REQUIRES the rule on BOTH the outgoing and incoming page, which is why
   this file ships site-wide even though §3 does not.
   ---------------------------------------------------------------- */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation-duration: var(--dur-base); }
  ::view-transition-new(root) {
    animation: mo-vt-in var(--dur-slow) var(--ease-out-quint) both;
  }
  @keyframes mo-vt-in {
    from { opacity: 0; translate: 0 10px; }
    to   { opacity: 1; translate: 0 0; }
  }
}

/* The brand and the page title are the two things genuinely shared by all 24
   pages, so they morph across the navigation instead of cross-fading with
   everything else. Verified 2026-08-27: exactly one <h1> and one .brand per
   page on all 24 -- a duplicate name aborts the whole transition, so if a
   page ever grows a second h1 this is the rule that breaks. */
nav .brand { view-transition-name: mo-brand; }
.wrap > h1 { view-transition-name: mo-title; }

/* THE DOOR MORPH (2026-09-23, doormorph.js). Three names, never declared in
   CSS on any element -- doormorph.js writes them as an inline style, once,
   only for the one navigation a router-door click starts, then hands the
   destination's h1 back to mo-title above. A static rule here would be a
   note about a name nothing on the page carries; this only retimes the
   browser's own default cross-fade-and-resize group animation to the site's
   law (decelerate, settle, never bounce) for the moment it IS in use. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(sq-door-quantum),
  ::view-transition-group(sq-door-or),
  ::view-transition-group(sq-door-pqc) {
    animation-duration: var(--dur-slow);
    animation-timing-function: var(--ease-out-quint);
  }
}

/* prefers-reduced-motion still gets the transition -- it is a cross-fade,
   not movement -- but loses the travel. Killing it entirely would be worse:
   the white flash it replaces is itself a motion event. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation-duration: 1ms !important; }
}

/* ================================================================
   §3 ONWARD: opt-in. Requires <body data-motion>.
   ================================================================ */

/* ---------------------------------------------------------------- §3
   SCROLL REVEALS -- DELIBERATELY NOT HERE.

   This file briefly shipped a scroll-driven reveal (`animation: mo-rise`
   on a `view()` timeline). It was removed on 2026-08-27, the same day, and
   it must not come back. Read this before adding one.

   THE SITE ALREADY HAS SCROLL REVEALS. nav.js builds them: it tags
   `h2, .card, .grid, table, .formula, .cabs, .gate` with `.reveal`, adds
   `.in` from an IntersectionObserver, and style.css:899-913 does the rest --
   including a per-child stagger via fadeUp and the growbar sweep on
   `h2::before`. It also carries three safety properties this file cannot
   reproduce: nav.js is the ONLY thing that ever adds `.reveal`, so a script
   failure leaves the page fully visible; reduced motion bails out before
   anything is hidden; and a 2.5s timer force-reveals everything no matter
   what.

   WHY A SECOND ONE IS ACTIVELY BROKEN, not merely redundant: `.reveal` sets
   `opacity: 0` as a normal declaration. A CSS animation with `fill: both`
   outranks a normal declaration, so mo-rise forced those elements visible
   BEFORE the observer fired -- content appeared pre-shown, sitting 14px low,
   and then snapped into place when `.in` finally landed. The site's own
   reveal never got to play.

   If more elements should reveal -- `section`, `.introute`, `.spine` and
   `footer` are the current gaps -- the fix is ONE LINE in nav.js's target
   selector, not a second mechanism here. One reveal system.

   Removing this is also what keeps the rest of this file safe to enable
   page by page: with no `translate` animation in play, nothing here creates
   a containing block, so no `position: sticky` or `fixed` descendant can be
   torn off the viewport by turning motion on. Verified against the three
   sticky elements on this site (nav, .tbar, and pqc.html's .pq-jump).
   ---------------------------------------------------------------- */

/* ---------------------------------------------------------------- §4
   HOVER PHYSICS

   style.css already lifts .card and .introute-card. This does not replace
   that -- it retimes it onto the shared easing and adds the one thing the
   flat version is missing: a light source. --mo-x/--mo-y are written by
   atmosphere.js from the pointer, so the sheen tracks the cursor across the
   card instead of sitting in a fixed corner. With JS off the fallback is a
   centred highlight, which is a still-fine card.
   ---------------------------------------------------------------- */
@property --mo-sheen { syntax: "<number>"; initial-value: 0; inherits: false; }

[data-motion] .card,
[data-motion] .introute-card {
  position: relative;
  transition: transform var(--dur-base) var(--ease-out-quint),
              border-color var(--dur-base) var(--ease-out-quint),
              box-shadow var(--dur-base) var(--ease-out-quint),
              --mo-sheen var(--dur-base) linear;
}
[data-motion] .card::after,
[data-motion] .introute-card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: var(--mo-sheen, 0);
  background: radial-gradient(
    230px circle at var(--mo-x, 50%) var(--mo-y, 50%),
    rgba(45, 212, 191, 0.10),
    rgba(139, 92, 246, 0.06) 38%,
    transparent 66%
  );
}
[data-motion] .card:hover,
[data-motion] .introute-card:hover { --mo-sheen: 1; }

/* The lift itself, deepened slightly. .card already moved -3px; the extra
   is in the shadow, not the travel, so nothing reflows further than it
   used to. */
[data-motion] .card:hover { box-shadow: 0 14px 34px var(--shadow); }
[data-motion] .introute-card:hover { box-shadow: 0 10px 24px var(--shadow); }

/* .bigplay gets the shared EASING and nothing else. No lift, no scale, no
   pulse -- deliberately.

   That restraint is not a style preference, it is the standing outcome of
   three rounds of feedback on this exact control (2026-08-06, 08-07, 08-20),
   each one asking for less than the last. The translateY(-2px) hover lift
   was removed outright in the third round. Re-adding one here because "the
   motion layer touches buttons now" would silently undo a decision that was
   made three times. Brightness and shadow already carry the hover state and
   they are enough.

   If a future pass wants the primary CTA more noticeable, reach for shape,
   not size or motion -- that is what the pill radius is already doing. */
[data-motion] .bigplay {
  transition: box-shadow var(--dur-base) var(--ease-out-quint),
              filter var(--dur-base) var(--ease-out-quint);
}

/* ---------------------------------------------------------------- §5
   THE LIVE BORDER

   Exactly one element gets this, and it is the playable teaser. #tryit is
   the only thing on the homepage that asks to be touched, so it is the only
   thing allowed to look like it is awake. Eight seconds per revolution and
   low alpha: it reads as "powered", not "urgent". If a second element ever
   wants this rule, the answer is almost certainly no.

   @property is what makes the angle interpolate at all -- a plain custom
   property is a string to the animation engine and would step, not sweep.
   ---------------------------------------------------------------- */
@property --mo-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }

@supports (background: conic-gradient(from var(--mo-angle), red, blue)) {
  @media (prefers-reduced-motion: no-preference) {
    [data-motion] #tryit {
      position: relative;
      border-color: transparent;
      background:
        linear-gradient(var(--panel), var(--panel)) padding-box,
        conic-gradient(from var(--mo-angle),
          rgba(139, 92, 246, 0.55),
          rgba(45, 212, 191, 0.55) 25%,
          rgba(139, 92, 246, 0.10) 50%,
          rgba(45, 212, 191, 0.55) 75%,
          rgba(139, 92, 246, 0.55)) border-box;
      animation: mo-spin var(--dur-amble) linear infinite;
    }
    @keyframes mo-spin { to { --mo-angle: 360deg; } }
  }
}

/* ---------------------------------------------------------------- §6
   THE LATTICE

   The generative background, drawn by atmosphere.js. Fixed, behind
   everything, and deliberately NOT a video: the brand mark is a lattice and
   this site has never shipped a single photographic byte, so the honest
   version of "dynamic background" here is the lattice actually behaving like
   one. Costs ~4KB and no network request.

   z-index -1 puts it in the same plane as body::before (the glows and the
   26px dot grid). Later in the DOM, so it paints on top of them -- both are
   transparent, so what you get is the static lattice with a live one moving
   through it, which is the intended read.
   ---------------------------------------------------------------- */
#mo-lattice {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;   /* clicks pass through; atmosphere.js listens on window */
  opacity: 0;
  transition: opacity 1.2s var(--ease-out-quint);
}
#mo-lattice.on { opacity: 1; }

/* ---------------------------------------------------------------- §7
   STATE

   Two fixed overlays and one readout. See qubit.js for what drives them.

   ONE overlay, not two, and backdrop-filter rather than filter. Three
   reasons, all of them load-bearing:

     - `filter` on an ancestor becomes the containing block for fixed
       descendants, which would tear the sticky nav and the ambient layer off
       the viewport. A backdrop-filter sibling changes no containing block.

     - invert() and hue-rotate() must apply IN THAT ORDER. Splitting them
       across a mix-blend-mode layer and a backdrop-filter layer makes the
       order depend on paint order and on how a given engine builds its
       backdrop root. One filter chain on one element is unambiguous.

     - inversion is the whole point. invert() is what the X gate does to a
       basis state, so the CSS primitive and the physics are the same
       operation. That is why this maps to a filter and not to a theme class.

   THE TWO CHANNELS. Lightness carries the bit, hue carries the phase, and
   they stay independent:
     --qz-inv  = |beta|^2      -> how far the page is toward |1>
     --qz-hue  = 180deg * |beta|^2  + the relative phase in degrees
   The 180deg term is compensation, not decoration: inverting RGB also
   complements the hue, so rotating back by 180 restores the brand colours at
   inverted lightness. Without it a measured |1> renders teal as red, which
   would mean a bit flip had silently changed the phase channel too.

   Filter is dropped entirely at |0> (see the .idle class) so an untouched
   page pays nothing for a full-viewport backdrop filter it is not using.
   ---------------------------------------------------------------- */
#qz-fx {
  position: fixed;
  inset: 0;
  z-index: 9997;
  pointer-events: none;
  backdrop-filter: invert(var(--qz-inv, 0)) hue-rotate(var(--qz-hue, 0deg));
  -webkit-backdrop-filter: invert(var(--qz-inv, 0)) hue-rotate(var(--qz-hue, 0deg));
  transition: backdrop-filter var(--dur-slow) var(--ease-in-out),
              -webkit-backdrop-filter var(--dur-slow) var(--ease-in-out);
}
#qz-fx.idle {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* The readout. Ships dim and says only "|0⟩" -- enough that someone who
   reads it twice notices it is describing the page itself, not enough to
   explain anything. It brightens the moment a gate lands. */
#qz-pill {
  position: fixed;
  right: max(14px, env(safe-area-inset-right, 0px));
  bottom: max(14px, env(safe-area-inset-bottom, 0px));
  z-index: 9998;   /* above #qz-fx: the readout reports the state, so it must
                      not be inverted along with it */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill, 999px);
  background: var(--nav-bg);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: var(--muted);
  font-size: 0.74rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  cursor: pointer;
  opacity: 0.34;
  transition: opacity var(--dur-base) var(--ease-out-quint),
              border-color var(--dur-base) var(--ease-out-quint),
              color var(--dur-base) var(--ease-out-quint);
}
/* 44px touch minimum without a 44px-tall pill. Same trick nav a.link already
   uses for the Path link, and for the same reason given there: the box has to
   stay tight to its text, so it grows an invisible band inside itself instead.
   Measured at 41x29 before this rule, which would have been the one control on
   the site under the floor. */
#qz-pill::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  min-width: 44px;
  min-height: 44px;
  transform: translate(-50%, -50%);
}
#qz-pill:hover { opacity: 0.72; }
#qz-pill.live {
  opacity: 1;
  color: var(--text);
  border-color: rgba(45, 212, 191, 0.45);
}
#qz-pill .qz-ket { font-weight: 700; }
#qz-pill .qz-gate {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 4px;
  background: rgba(139, 92, 246, 0.18);
  color: var(--violet);
  font-weight: 700;
  font-size: 0.72rem;
}
#qz-pill .qz-hint { color: var(--muted); font-size: 0.68rem; }
#qz-pill.flash { animation: mo-flash var(--dur-slow) var(--ease-out-quint); }
@keyframes mo-flash {
  0%   { border-color: var(--teal); box-shadow: 0 0 0 0 rgba(45, 212, 191, 0.4); }
  100% { border-color: rgba(45, 212, 191, 0.45); box-shadow: 0 0 0 14px rgba(45, 212, 191, 0); }
}

/* Screen-reader announcement target for state changes. Visually gone,
   still read: the page changing colour under someone is worth a word. */
.qz-sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 560px) {
  #qz-pill { right: max(10px, env(safe-area-inset-right, 0px)); bottom: max(10px, env(safe-area-inset-bottom, 0px)); font-size: 0.7rem; padding: 5px 10px; }
  #qz-pill .qz-hint { display: none; }
}

/* ---------------------------------------------------------------- §8
   REDUCED MOTION

   style.css already carries 14 of these blocks; this file honours the same
   contract. State still CHANGES -- the qubit is the point, and someone who
   turned motion down did not ask to be locked out of it -- it just stops
   travelling to get there.
   ---------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  #qz-fx { transition-duration: 1ms; }
  #mo-lattice { transition-duration: 1ms; }
  #qz-pill.flash { animation: none; }
}
/* --- rails.css --- */
/* SymbiQ — rails.css: the peripheral layer (added 2026-08-28).
 *
 * WHAT THIS IS FOR
 * The reading column is capped at 780 / 880 / 1040px by design: prose past
 * about 75 characters a line stops being readable, so widening .wrap is not
 * the fix for a wide monitor. Measured on the homepage at 1920x1000, that
 * cap leaves 432px of empty background on the left and 447px on the right,
 * and past 2000px it is over 700px a side. This file puts orientation into
 * that space — where you are in the page, where you are on the Ladder, what
 * you have already cleared, where to go next — without touching the measure.
 *
 * THE ARCHITECTURE RULE, SAME AS motion.css
 * None of this lives in style.css. That file is 123KB shared by 24 pages and
 * carries the 2026-07-24 scar: touching it forces a cache-buster bump on
 * every page. A peripheral layer should never be a 24-page deploy, so it
 * ships as its own stylesheet with its own version query, exactly as the
 * motion layer does.
 *
 * PROGRESSIVE ENHANCEMENT, SAME AS rung.js AND nav.js
 * Nothing here is required for any page to work. rails.js inserts the rails
 * only when the viewport is at least 1440px wide, and only on a page whose
 * body carries data-rails. If the script fails, is served stale, or throws,
 * what remains is the page exactly as it was before this file existed.
 *
 * GEOMETRY — why the offsets are written as calc(50% + 520px)
 * Each rail claims the whole gutter between one viewport edge and the
 * reading column, then centres a narrow column inside it. The inner edge is
 * expressed against 50% of the viewport rather than against 100vw because
 * 100vw includes the classic scrollbar in several engines and the rails
 * would sit a few pixels off. 520px is half of the widest .wrap tier
 * (1040px), which is the only tier that exists above the 1440px gate.
 *
 * NOTE FOR ANY FUTURE EDIT OF THE COMMENTS IN THIS FILE
 * A literal star followed by a literal slash closes a CSS comment, and no
 * escape exists inside one. That mistake silently killed the rung rail's own
 * styling for five days in August 2026, because a rule whose selector fails
 * to parse is dropped whole, declarations included. Do not let a star and a
 * slash land next to each other in this file, not even when describing the
 * bug. Write token-family names as prose instead of as a wildcard.
 */

/* ---------------------------------------------------------------- §0
   THE FLOOR

   This one declaration lives OUTSIDE the gate, and it is the difference
   between a graceful narrow viewport and a broken one. Every other rule in
   this file sits inside a min-width query, so below the gate a .sqrail
   element has no styling of its own at all: it would fall back to a plain
   block box in the normal document flow and dump the section index, the
   ladder and the next-page card as loose content at the foot of the page.

   rails.js does remove the rails when the query stops matching, but the
   stylesheet must be correct without the script — a resize, a zoom step or
   a device rotation can outrun a listener, and a matchMedia change event
   that never arrives (as happens under some emulated viewports) must be a
   non-event, not a layout collapse. Found by resizing a built page from
   1920 to 1280 and watching the rails reappear in the flow.
   ---------------------------------------------------------------- */
.sqrail { display: none; }

/* ---------------------------------------------------------------- §1
   THE GATE

   Everything below is inside one min-width query. rails.js runs the same
   1440px test in matchMedia and only builds the DOM when it passes, so the
   markup and the styling can never disagree — resize below the gate and the
   rails are removed rather than merely hidden.
   ---------------------------------------------------------------- */
@media (min-width: 1440px) {

  /* ---------------------------------------------------------------- §2
     THE GUTTER ZONES

     z-index 30 sits deliberately below the sticky header nav (40) and below
     the nav dropdown panels (45). The rails never overlap either one
     horizontally, but a page that grows a wider element later should have
     the header win, not the margin furniture.

     Both zones are pointer-events: none so that the empty parts of the
     gutter stay click-through; only the inner column takes the pointer
     back. Without this a 440px-wide invisible box on each side would eat
     text-selection drags that start in the margin.
     ---------------------------------------------------------------- */
  .sqrail {
    position: fixed;
    top: 0;
    bottom: 0;
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
  }
  .sqrail-l { left: 0; right: calc(50% + 520px); }
  .sqrail-r { right: 0; left: calc(50% + 520px); }

  .sqrail-col {
    pointer-events: auto;
    width: min(248px, calc(100% - 36px));
    max-height: calc(100vh - 150px);   /* clears the state readout, which is
                                          fixed 14px from the bottom right */
    overflow-y: auto;
    overscroll-behavior: contain;
    display: flex;
    flex-direction: column;
    gap: var(--sp-5, 20px);
    scrollbar-width: thin;
  }
  .sqrail-col::-webkit-scrollbar { width: 4px; }
  .sqrail-col::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }

  /* The eyebrow that labels each module. Same treatment as the rung rail's
     own label, so the two read as one family. */
  .sqrail-lab {
    color: var(--muted);
    font-weight: 700;
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.09em;
    margin-bottom: var(--sp-3, 12px);
  }

  /* ---------------------------------------------------------------- §2b
     THE BARE-nav LEAK — read this before adding another landmark here

     rails.js builds two real landmarks, nav.sqrail-idx and nav.sqrail-ladder.
     style.css styles the page header with FIVE bare element rules spelled
     nav:not(.rung-rail) — base layout, the 720px sticky block, two mobile
     overrides, and position: relative. A bare element selector matches every
     nav on the page, so all five landed on both rails. What that looked like,
     found on the live site 2026-08-28 from a screenshot:

       align-items: center   every index entry and every rung centred on its
                             own text width, so the list staggered in and out
                             instead of aligning to one edge. The obvious one.
       border-bottom         a rule under the last index entry, belonging to
                             nothing.
       margin-bottom 40px    a hole between the ladder and the card under it.
       padding 18px 0        the index sitting low under its own label.
       gap 18px              rows pushed apart, unrelated to the design.
       position + z-index    sticky at z-index 40 INSIDE a scrolling column,
                             tied with the page header.

     This is the third time a bare nav rule has leaked into a component
     landmark. The 2026-08-23 rung rail lost its own styling the same way and
     was patched by adding one negation, which is what left the next nav to
     find the same rake. The real repair is for style.css to target its header
     positively rather than by element, and it belongs in that file the next
     time there is a reason to touch it — style.css is 123KB shared by 24
     pages and a bump costs every reader a refetch.

     Until then this block wins on its own: nav.sqrail-idx is one element plus
     one class, exactly the weight of nav:not(.rung-rail), and rails.css loads
     after style.css, so a tie resolves here. Fixing it on this side also
     repairs the page for anyone still holding a cached style.css, which a
     change in that file could not do.

     Every property the five rules set is restated, including the ones that
     look harmless. backdrop-filter is the one to keep: a filtered ancestor
     becomes the containing block for fixed descendants, which is how a stray
     blur tears a fixed element off the viewport.
     ---------------------------------------------------------------- */
  nav.sqrail-idx,
  nav.sqrail-ladder {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 0;
    margin: 0;
    padding: 0;
    border: 0;
    position: static;
    top: auto;
    z-index: auto;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* ---------------------------------------------------------------- §3
     THE SCROLL SPINE

     A hairline at the inner edge of the left gutter, running the full
     height of the viewport, with the read portion filled violet-to-teal.
     It is the one element here that is allowed to be full height: it is a
     measurement of the page, so its length should be the page.

     scaleY on a pseudo-element, not height, so the fill costs no layout on
     every scroll frame. It is a pseudo-element of a fixed, non-transformed
     parent, so it creates no containing block anything else depends on.
     ---------------------------------------------------------------- */
  .sqrail-spine {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    width: 2px;
    background: var(--border);
    opacity: 0.55;
    pointer-events: none;
  }
  .sqrail-spine::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, var(--violet), var(--teal));
    transform-origin: top;
    transform: scaleY(var(--sq-read, 0));
  }

  /* ---------------------------------------------------------------- §4
     ON THIS PAGE — the left index

     Built from the page's own h2 elements by rails.js. Each entry is a dot
     on a track plus a label clamped to two lines, because several section
     titles on this site are full sentences and a 248px column cannot hold
     them. The full text stays in the title attribute and in the link text
     itself for assistive technology, so nothing is lost, only wrapped.
     ---------------------------------------------------------------- */
  .sqrail-idx { display: flex; flex-direction: column; }
  .sqrail-item {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3, 12px);
    padding: 7px 0;
    color: var(--muted);
    font-size: 0.78rem;
    line-height: 1.35;
    text-decoration: none;
    transition: color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  .sqrail-item .sqrail-dot {
    flex: none;
    width: 7px;
    height: 7px;
    /* 5px, not the 4px that looks right in the source. Measured: the dot
       centred at 14.5px from the row top while the first text line centres
       at 15.42px, so the dot sat a pixel high against the word beside it.
       Optical alignment is what the eye reads, not the box model. */
    margin-top: 5px;
    border-radius: 999px;
    border: 1.5px solid var(--border);
    background: transparent;
    transition: border-color var(--dur-base, 260ms) var(--ease-out-quint, ease),
                background-color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  .sqrail-item .sqrail-txt {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .sqrail-item:hover { color: var(--text); }
  .sqrail-item:hover .sqrail-dot { border-color: var(--violet); }
  .sqrail-item.is-cur { color: var(--text); font-weight: 600; }
  .sqrail-item.is-cur .sqrail-dot { border-color: var(--violet); background: var(--violet); }
  /* WHERE YOU HAVE BEEN, carried by the dot rather than by dimming the row.
     The first version faded passed entries to opacity 0.62, which measured
     3.64:1 against the page — under the 4.5:1 floor, so every section you
     had already read became the least legible thing in the rail. Exactly
     backwards, and the kind of defect that only shows up when you measure
     the composited colour instead of the declared one.
     Now the text stays at full strength in all three states and the dot
     carries the distinction: hollow ahead of you, filled behind you, filled
     violet where you are. Three states, no contrast spent. */
  .sqrail-item.is-done .sqrail-dot { background: var(--muted); border-color: var(--muted); }

  /* ---------------------------------------------------------------- §5
     THE LADDER, VERTICAL

     On a page carrying data-rung, rails.js draws the same five rungs the
     horizontal pill draws — and hides that pill, because two copies of one
     fact is clutter, not reinforcement. Below 1440px the vertical rail is
     never built, so the horizontal pill is the only version and nothing is
     lost. The class is set on the html element by rails.js so the rule can
     be scoped without a body-attribute guess.
     ---------------------------------------------------------------- */
  html.sq-vrung .rung-rail { display: none; }

  .sqrail-ladder { display: flex; flex-direction: column; }
  .sqrail-rung {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--sp-3, 12px);
    padding: 5px 0;
    color: var(--muted);
    font-size: 0.76rem;
    text-decoration: none;
    transition: color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  .sqrail-rung .sqrail-id {
    flex: none;
    display: grid;
    place-items: center;
    width: 24px;
    height: 24px;
    border-radius: 999px;
    border: 1px solid var(--border);
    font-size: 0.64rem;
    font-weight: 700;
    background: var(--panel);
    background: color-mix(in srgb, var(--panel) 72%, transparent);
    transition: border-color var(--dur-base, 260ms) var(--ease-out-quint, ease),
                background-color var(--dur-base, 260ms) var(--ease-out-quint, ease),
                color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  /* The connector between two rungs. Drawn on the entry rather than as its
     own element so the markup stays one node per rung. */
  .sqrail-rung + .sqrail-rung::before {
    content: "";
    position: absolute;
    left: 11.25px;   /* chip centre measured at 12.0px; a 1.5px line starting
                        at 11.5 centres at 12.25 and leaned right of its own
                        rungs. */
    top: -5px;
    height: 10px;
    width: 1.5px;
    background: var(--border);
  }
  .sqrail-rung:hover { color: var(--text); }
  .sqrail-rung:hover .sqrail-id { border-color: var(--violet); }
  .sqrail-rung.is-cur { color: var(--text); font-weight: 700; }
  .sqrail-rung.is-cur .sqrail-id { border-color: var(--violet); background: var(--violet); color: #04021f; }
  /* Same correction style.css already carries for the horizontal rail's own
     current dot, and for the same reason. --violet darkens to 7c3aed in
     light mode, and the near-black label that reads cleanly on the light
     violet measures 3.57:1 against it -- under the 4.5:1 floor for a badge
     set at roughly ten pixels. Flipping the label to the page's own near-
     white takes it to 5.4:1. */
  @media (prefers-color-scheme: light) {
    .sqrail-rung.is-cur .sqrail-id { color: #f8fafc; }
  }

  .sqrail-fork {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-top: var(--sp-3, 12px);
    padding: 5px 10px;
    border: 1px dashed var(--border);
    border-radius: 999px;
    color: var(--muted);
    font-size: 0.7rem;
    text-decoration: none;
    align-self: flex-start;
    transition: border-color var(--dur-base, 260ms) var(--ease-out-quint, ease),
                color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  .sqrail-fork svg { flex: none; opacity: 0.85; }
  .sqrail-fork:hover { border-color: var(--teal); color: var(--text); }
  .sqrail-fork:hover svg, .sqrail-fork.is-cur svg { opacity: 1; }
  .sqrail-fork.is-cur { border-style: solid; border-color: var(--teal); color: var(--teal); font-weight: 700; }

  /* ---------------------------------------------------------------- §6
     YOUR PROGRESS

     Read out of the same two localStorage keys the homepage router already
     reads. Rendered only when there is something to report — a first-time
     visitor gets no empty meters, because an empty meter is a reproach.
     ---------------------------------------------------------------- */
  .sqrail-prog { display: flex; flex-direction: column; gap: var(--sp-3, 12px); }
  .sqrail-meter { display: block; text-decoration: none; color: var(--muted); }
  .sqrail-meter .sqrail-mtop {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-2, 8px);
    font-size: 0.72rem;
    margin-bottom: 5px;
  }
  .sqrail-meter .sqrail-mnum {
    font-variant-numeric: tabular-nums;
    color: var(--text);
    font-weight: 700;
  }
  .sqrail-bar {
    height: 3px;
    border-radius: 999px;
    background: var(--border);
    overflow: hidden;
  }
  .sqrail-bar i {
    display: block;
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--violet), var(--teal));
    transform-origin: left;
    transform: scaleX(var(--sq-p, 0));
  }
  .sqrail-meter:hover { color: var(--text); }

  /* ---------------------------------------------------------------- §7
     WHERE NEXT

     One link, not a menu. The site already has a full navigation; the point
     of this module is to answer "and then?" without asking the reader to
     open anything. Restrained on purpose: the button trend on this site is
     monotonically less flash, so this is a bordered chip with a gradient
     hairline on top, not a filled call to action competing with the real one
     in the page.
     ---------------------------------------------------------------- */
  /* The fill is translucent, and that is not a decorative choice. The
     generative lattice draws at z-index -1 across the whole viewport, and
     the place it was always most visible is exactly the empty margin this
     file has just filled. A solid panel out here does not merely sit in
     front of the lattice, it reads as having replaced it. At around 72% the
     nodes and their cursor bonds still travel behind the card, the rail
     still reads as a card, and the text keeps its contrast, because both
     the panel and the page behind it are near-black in dark mode and
     near-white in light. Solid first as the fallback for anything without
     color-mix. Applies to the state module and the rung chips below too. */
  .sqrail-next {
    display: block;
    padding: var(--sp-3, 12px) var(--sp-4, 16px);
    border: 1px solid var(--border);
    border-top: 2px solid transparent;
    border-radius: var(--r-md, 10px);
    background:
      linear-gradient(var(--panel), var(--panel)) padding-box,
      linear-gradient(90deg, var(--violet), var(--teal)) border-box;
    background:
      linear-gradient(color-mix(in srgb, var(--panel) 72%, transparent),
                      color-mix(in srgb, var(--panel) 72%, transparent)) padding-box,
      linear-gradient(90deg, var(--violet), var(--teal)) border-box;
    color: var(--muted);
    font-size: 0.76rem;
    line-height: 1.4;
    text-decoration: none;
    transition: border-color var(--dur-base, 260ms) var(--ease-out-quint, ease),
                color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  .sqrail-next b { display: block; color: var(--text); font-size: 0.84rem; margin-bottom: 2px; }
  .sqrail-next:hover { color: var(--text); }

  /* ---------------------------------------------------------------- §8
     THE STATE MODULE

     Hidden until the reader has already found the keyboard layer for
     themselves. It reports what their own gates composed to; it never names
     a key. Someone who has not pressed anything sees nothing here, which is
     the point — this is a reward for discovery, not a hint toward it.
     ---------------------------------------------------------------- */
  .sqrail-state {
    border: 1px solid var(--border);
    border-radius: var(--r-md, 10px);
    padding: var(--sp-3, 12px) var(--sp-4, 16px);
    background: var(--panel);
    background: color-mix(in srgb, var(--panel) 72%, transparent);
    font-size: 0.72rem;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
  }
  .sqrail-state dl { display: grid; grid-template-columns: auto 1fr; gap: 3px var(--sp-3, 12px); margin: 0; }
  .sqrail-state dt { color: var(--muted); }
  .sqrail-state dd { margin: 0; color: var(--text); text-align: right; }
  .sqrail-state .sqrail-ket { color: var(--teal); font-weight: 700; }

  /* ---------------------------------------------------------------- §9
     BACK TO TOP

     Appears once the reader is a screen and a half down. Sits at the foot of
     the left column so it never competes with the index above it.
     ---------------------------------------------------------------- */
  .sqrail-top {
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2, 8px);
    /* 32px, up from the 24px the padding alone produced. The rails only
       exist at 1440px and up, which is a pointer surface, so the site's 44px
       touch floor is not the governing number here -- but 24px is small for
       a real button under any input, and this is the only true button in
       either rail. The dense index entries below stay at their own rhythm on
       purpose: a section list is scanned, and 44px a row would push most
       pages' index into a scroller for no gain to a mouse. */
    min-height: 32px;
    padding: 6px 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--panel);
    background: color-mix(in srgb, var(--panel) 72%, transparent);
    color: var(--muted);
    font-size: 0.7rem;
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-base, 260ms) var(--ease-out-quint, ease),
                visibility var(--dur-base, 260ms) var(--ease-out-quint, ease),
                border-color var(--dur-base, 260ms) var(--ease-out-quint, ease),
                color var(--dur-base, 260ms) var(--ease-out-quint, ease);
  }
  .sqrail-top.is-on { opacity: 1; visibility: visible; }
  .sqrail-top:hover { border-color: var(--violet); color: var(--text); }

  /* ---------------------------------------------------------------- §10
     ARRIVAL

     One fade, once, on build. Deliberately not a scroll reveal: nav.js has
     owned scroll reveals site-wide since long before this file, and a second
     reveal system does not merely duplicate the first, it overrides it —
     an animation with fill both outranks the normal opacity declaration
     that .reveal relies on. See motion.css section 3 for the full account.
     The rails are outside .wrap and carry no .reveal, so they are invisible
     to nav.js and this local fade cannot collide with it.

     translate, never transform: a running animation with fill both would
     otherwise outrank every hover lift declared in style.css.

     SCOPED TO .sqrail-mod, NOT TO EVERY CHILD OF THE COLUMN, and that is
     load-bearing rather than tidy. An animation with fill both outranks a
     normal declaration of the same property. The back-to-top button is also
     a direct child of the column and holds itself hidden with a normal
     opacity: 0; had this animation matched it, its end state of opacity 1
     would have pinned it visible from first paint and no amount of class
     toggling could have hidden it again. Every module carries .sqrail-mod;
     the button deliberately does not.
     ---------------------------------------------------------------- */
  @media (prefers-reduced-motion: no-preference) {
    .sqrail-mod { animation: sqrail-in var(--dur-slow, 620ms) var(--ease-out-quint, ease) both; }
    .sqrail-mod:nth-child(2) { animation-delay: 90ms; }
    .sqrail-mod:nth-child(3) { animation-delay: 180ms; }
    .sqrail-mod:nth-child(4) { animation-delay: 270ms; }
    .sqrail-spine::after { transition: transform 120ms linear; }
    .sqrail-bar i { transition: transform var(--dur-slow, 620ms) var(--ease-out-quint, ease); }
  }
  @keyframes sqrail-in {
    from { opacity: 0; translate: 0 8px; }
    to   { opacity: 1; translate: 0 0; }
  }

  /* ---------------------------------------------------------------- §11
     PRESS

     Every control in both rails had a default, a hover and a focus ring, and
     nothing for the moment of the click itself. On a fast connection the
     page moves before you notice; on a slow one the rail looked inert while
     the navigation was already underway.

     One idea, applied everywhere rather than six unrelated effects: pressing
     a control fills its accent violet, which is the same mark the rail uses
     for "this is where you are". Press previews arrival. The fork keeps teal
     because teal is what the fork means throughout the site, and a press is
     not the place to change an element's colour language.
     ---------------------------------------------------------------- */
  .sqrail-item:active .sqrail-dot { background: var(--violet); border-color: var(--violet); }
  .sqrail-rung:active .sqrail-id { background: var(--violet); border-color: var(--violet); color: #04021f; }
  .sqrail-fork:active { border-style: solid; border-color: var(--teal); }
  /* An inset ring rather than a border colour: the next card paints its
     border from a gradient through border-box, which no border-color can
     reach. */
  .sqrail-next:active { box-shadow: inset 0 0 0 1px var(--violet); }
  .sqrail-meter:active .sqrail-bar { box-shadow: inset 0 0 0 1px var(--violet); }
  .sqrail-top:active { border-color: var(--violet); color: var(--text); }
  @media (prefers-color-scheme: light) {
    .sqrail-rung:active .sqrail-id { color: #f8fafc; }
  }

  /* Focus stays visible: these are real links a keyboard reader can tab
     into, and they sit on a background the default outline can vanish
     against. */
  .sqrail :focus-visible {
    outline: 2px solid var(--violet);
    outline-offset: 3px;
    border-radius: var(--r-sm, 6px);
  }
}

/* ---------------------------------------------------------------- §12
   WIDE BANDS — letting the non-prose blocks use the room

   The 1040px cap is a rule about PROSE. A row of cards is not prose: its
   legibility comes from the card, not from the line length of the column
   holding it, so on a screen with room to spare it can be wider than the
   text above it. This is the second half of filling a wide monitor — the
   rails take the margins, and the blocks that can honestly use more width
   take some of what is left.

   WHY THE OFFSET IS WRITTEN THIS WAY. --sq-out is how far a band may hang
   past the reading column on EACH side, and it is derived, not chosen. At
   these widths the rail column is 248px centred in a gutter of
   (100vw - 1040px) / 2, so its inner edge sits at a quarter of that gutter
   plus 124px from the viewport edge. Solving "band edge stays clear of the
   rail" gives (100vw - 1040px) / 4 minus a constant, which is the middle
   term below. The constant was raised from 128 to 140 after measuring: 128
   left a 20px channel between the rail text and the card border, which is
   close enough to read as a collision rather than a gutter. 140 measures
   32px and costs 12px of band width a side. It yields 25px a side at 1700,
   80px at 1920, and hits the 140px cap at 2160 — always clear of the rails
   by construction rather than by a number someone guessed and will not
   re-check when the rail width changes.
   clamp's lower bound of zero means that even if this query is ever widened
   downward by mistake, the worst case is no breakout, never a band narrower
   than the column it sits in.

   The width is expressed against the containing block (100% of .wrap's
   content box) rather than against the viewport, so the band stays centred
   on the column whether or not a classic scrollbar is present. Only the
   clearance term uses 100vw, where being a few pixels out costs nothing.

   SCOPED TO DIRECT CHILDREN OF .wrap, and that is what makes it safe.
   Measured across all 24 pages: every top-level grid holds 2, 3, 4 or 5
   cards, and auto-fit lays each of those out in one full row at these
   widths with no orphan. The only 6-card grids on the site are both nested
   — one inside a foldsec panel on the homepage, one inside a wrapper on
   play.html — and content inside a fold is bounded by the fold, so it
   should not break out of it anyway. If a six-card grid is ever added at
   top level, check it here before assuming this still holds: at 1184px
   auto-fit resolves to five tracks and leaves one card alone on a second
   row.
   ---------------------------------------------------------------- */
@media (min-width: 1700px) {
  .wrap > .grid,
  .wrap > .orhero {
    --sq-out: clamp(0px, calc((100vw - 1040px) / 4 - 140px), 140px);
    width: calc(100% + 2 * var(--sq-out));
    margin-left: calc(0px - var(--sq-out));
  }
}

/* Margin furniture is screen furniture. On paper the reading column is the
   whole document, and a band that hangs past it has nothing to hang into. */
@media print {
  .sqrail { display: none; }
  .wrap > .grid,
  .wrap > .orhero { width: auto; margin-left: 0; }
}
/* --- alive.css --- */
/* SymbiQ — alive.css
 * ============================================================================
 * THE RESPONSE LAYER.  Opt-in via <body data-alive>.
 *
 * Its own file with its own cache-buster, for the reason motion.css, rails.css
 * and vivid.css each have one: style.css is shared by 24 pages and touching it
 * forces a bump on every one of them (the 2026-07-24 scar).
 *
 * WHAT THIS LAYER IS FOR.  The site already has atmosphere -- the lattice
 * drifts, the heroes glow, cards lift under the pointer.  What it did not have
 * was a reply.  Follow one of the several hundred deep links this site runs on
 * and you arrive in silence, with no confirmation of which of the forty things
 * on the page you were sent to.  Open one of the several hundred <details> and
 * the page jump-cuts: 3,000px of new content appears between one frame and the
 * next, and the thing you were reading is somewhere else now.  Neither is a
 * missing decoration; both are a missing answer.
 *
 * So the rule for everything in this file: it fires because the visitor did
 * something, it says what happened, and then it gets out of the way.  Nothing
 * here starts on its own, nothing here loops, and nothing here moves in a way
 * that would violate the house rule against springy or overshooting easing --
 * arrival does not move AT ALL, it resolves in place.
 *
 * Every rule below is scoped to body[data-alive] plus a JS-added class or
 * attribute, so a page that does not opt in is byte-for-byte unchanged, and a
 * page whose alive.js fails to load keeps working exactly as it does today.
 * ==========================================================================*/

body[data-alive] {
  /* Local aliases so this file never depends on a token style.css might not
     have defined at parse time.  Values match the house scale. */
  --al-ease: cubic-bezier(0.22, 1, 0.36, 1);   /* the house --ease-out-quint */
  --al-quick: 0.16s;
  --al-base: 0.26s;
}

/* ── 1. ARRIVAL ────────────────────────────────────────────────────────────
   You followed a link to #something.  This is the page confirming which
   something.

   Deliberately motionless.  The house rule is that nothing bounces and nothing
   overshoots; the honest reading of that rule for an arrival cue is that it
   must not travel either -- the target is already exactly where it belongs,
   and sliding it in would be a lie about its position.  So what resolves is
   light, not geometry: a 1px accent ring and a very low-alpha wash come up
   over ~260ms, hold, and leave over ~700ms.  The element never moves a pixel,
   which is also why this needs no separate reduced-motion path for movement.

   Uses `outline` rather than `border` or `box-shadow`: an outline draws
   outside the box without participating in layout, so a card cannot reflow, a
   table cannot shift a column, and a heading's own ::before accent bar is not
   pushed off its 3px. */
body[data-alive] [data-sq-arrived] {
  outline: 1px solid transparent;
  outline-offset: 4px;
  border-radius: var(--r-sm);   /* on the house 6/10/14 scale, not an ad-hoc 4px */
  animation: sq-arrive 1.35s var(--al-ease) both;
}
/* A heading's arrival is carried by the accent bar it already has, brightened,
   rather than by a ring drawn around a line of text -- that bar IS the
   heading's mark everywhere else on this site, so flaring it is the more
   literal answer to "this one". */
body[data-alive] h2[data-sq-arrived],
body[data-alive] h3[data-sq-arrived] { animation-name: sq-arrive-text; }
body[data-alive] h2[data-sq-arrived]::before {
  transform-origin: left center;   /* thicken into the gutter, not over the text */
  animation: sq-arrive-bar 1.35s var(--al-ease) both;
}

@keyframes sq-arrive {
  0%   { outline-color: transparent;
         background-color: transparent; }
  19%  { outline-color: color-mix(in srgb, var(--teal) 62%, transparent);
         background-color: color-mix(in srgb, var(--teal) 9%, transparent); }
  46%  { outline-color: color-mix(in srgb, var(--teal) 62%, transparent);
         background-color: color-mix(in srgb, var(--teal) 9%, transparent); }
  100% { outline-color: transparent;
         background-color: transparent; }
}
@keyframes sq-arrive-text {
  0%   { color: inherit; }
  19%  { color: var(--teal); }
  46%  { color: var(--teal); }
  100% { color: inherit; }
}
@keyframes sq-arrive-bar {
  0%   { filter: none;                           transform: scaleX(1); }
  19%  { filter: brightness(1.5) saturate(1.25); transform: scaleX(1.9); }
  46%  { filter: brightness(1.5) saturate(1.25); transform: scaleX(1.9); }
  100% { filter: none;                           transform: scaleX(1); }
}

/* No color-mix (Safari before 16.2, older Chromium): fall back to the flat
   accent at a fixed alpha rather than dropping the cue entirely. */
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  @keyframes sq-arrive {
    0%   { outline-color: transparent; }
    19%  { outline-color: rgba(45, 212, 191, 0.62); }
    46%  { outline-color: rgba(45, 212, 191, 0.62); }
    100% { outline-color: transparent; }
  }
}

/* ── 2. THE SECTION PERMALINK ──────────────────────────────────────────────
   A reference site whose entire navigation is deep links had no way to GET a
   deep link.  The nav chips know that qec.html#qec-widget exists; a reader who
   wanted to send that to someone had to read it out of the source.

   The control is drawn, not typographic: a two-link chain at a 1.7px stroke,
   the same weight as every other piece of inline SVG here.

   It ships INLINE, after the heading text, because that is the placement that
   is correct at every width.  In one band -- wide enough for an empty gutter,
   narrow enough that the peripheral rails have not claimed it -- it is
   promoted out into that gutter, where it costs the heading no width at all.
   The band is bounded on both sides deliberately: below 1100px there is no
   gutter, and at 1440px rails.js fills both gutters with the section index and
   the progress readout (measured: the left rail column ends at x=204 and a
   hanging button would start at x=206).

   Never `display:none` and never `visibility:hidden`: it stays in the tab
   order and reachable by a screen reader at every width.  Only opacity is
   conditional, and only where there is a pointer to reveal it with. */
body[data-alive] .sq-permalink {
  --sq-pl-size: 30px;
  position: relative;      /* the 44px hit band below anchors to this */
  vertical-align: middle;
  margin-left: 4px;
  opacity: 0.42;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--sq-pl-size);
  height: var(--sq-pl-size);
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: opacity var(--al-base) var(--al-ease),
              color var(--al-quick) var(--al-ease),
              background-color var(--al-quick) var(--al-ease);
}
/* The 44px touch minimum without a 44px-tall button.  Exactly the trick
   `nav a.link` and `#qz-pill` already use, for the same reason each of them
   gives: the visible box has to stay tight to a 15px glyph sitting beside a
   heading, so it grows an invisible band inside itself instead.  Measured at
   30x30 before this rule -- under the floor on every touch device, which is
   the one place this control is permanently visible. */
body[data-alive] .sq-permalink::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

body[data-alive] .sq-permalink svg {
  width: 15px; height: 15px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
body[data-alive] .sq-permalink .sq-pl-done { display: none; }
body[data-alive] .sq-permalink[data-copied] .sq-pl-link { display: none; }
body[data-alive] .sq-permalink[data-copied] .sq-pl-done { display: block; }
body[data-alive] .sq-permalink[data-copied] { color: var(--teal); opacity: 1; }

/* With a pointer, it stays out of the way until the heading is under it --
   twelve faintly visible icons down a reading page is furniture. Without one
   (touch), there is no hover to reveal it with, so the 0.42 above stands: a
   control that cannot be discovered is not a control. */
@media (hover: hover) {
  body[data-alive] .sq-permalink { opacity: 0; }
}
body[data-alive] .sq-anchored:hover > .sq-permalink,
body[data-alive] .sq-permalink:hover,
body[data-alive] .sq-permalink:focus-visible { opacity: 1; }
body[data-alive] .sq-permalink:hover { color: var(--teal); background: var(--rowhover); }
body[data-alive] .sq-permalink:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

/* h2 already sets position:relative for its accent bar; h3 and the rest do
   not, and both the promoted button and the confirmation note are positioned
   against their heading. */
body[data-alive] .sq-anchored { position: relative; }

/* The band with an empty gutter.  `hover: hover` as well, because the reveal
   this placement depends on is a hover -- on a touch device the button would
   simply be invisible out there. */
@media (min-width: 1100px) and (max-width: 1439px) and (hover: hover) {
  body[data-alive] .sq-permalink {
    position: absolute;
    left: calc(var(--sq-pl-size) * -1 - 6px);
    top: 50%;
    transform: translateY(-50%);
    margin-left: 0;
  }
}

/* The confirmation is a word, in the house label style, not a floating toast.
   aria-live sits on this element so the same word reaches a screen reader at
   the moment it reaches everyone else. */
body[data-alive] .sq-pl-note {
  position: absolute;
  left: 0;
  top: 100%;
  margin-top: 2px;
  font-size: 0.7rem;             /* the documented label step */
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--teal);
  white-space: nowrap;
  pointer-events: none;
  animation: sq-note 1.9s var(--al-ease) both;
}
@keyframes sq-note {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  70%  { opacity: 1; }
  100% { opacity: 0; }
}

/* ── 3. THE UNFOLD ─────────────────────────────────────────────────────────
   This site is built out of <details>: 24 curriculum topics, every .foldsec,
   every .gamerules, every Ledger claim, every Frontier question.  Native
   <details> has exactly one transition between its two states, which is none:
   the content is not there, and then it is, and everything below it is
   somewhere else now.  On a topic that opens 3,000px of content that is not a
   small infelicity -- the reader loses their place, on the single most common
   interaction the site has.

   alive.js animates the height and hands the duration to CSS through a
   per-element variable.  The attribute below exists only while a fold is in
   motion; the moment it settles the element is untouched markup again, so an
   interrupted transition cannot leave a page in a broken state.

   ON `transition: height`.  This is a deliberate exception to "never animate a
   layout property", taken with the cost understood and bounded, not by
   oversight.  The usual substitute -- a `grid-template-rows: 0fr -> 1fr`
   wrapper -- needs a single wrapping child, and only ONE of the four fold
   families here has one (`.mod > .mod-body`); `.foldsec`, `.gamerules` and the
   Ledger claims each hold several loose children, so adopting it would mean a
   markup change on 24 pages to animate a box.  The cost is instead capped
   three ways: nothing over FOLD_CAP of new content animates at all, the
   duration tops out at 420ms, and reduced motion removes it entirely. */
body[data-alive] details[data-sq-folding] {
  overflow: hidden;
  transition: height var(--sq-fold-dur, 0.3s) var(--al-ease);
}
/* The tall tier.  Past alive.js's FOLD_CAP the height is not animated at all
   -- the content resolves in place instead -- so this state must NOT take the
   height transition or the overflow clip with it. */
body[data-alive] details[data-sq-folding="fade"] {
  overflow: visible;
  transition: none;
}
/* The content resolves slightly out of phase with the height, so the fold
   reads as one gesture rather than as a box growing with the text already
   sitting inside it. */
body[data-alive] details[data-sq-folding] > :not(summary) {
  animation: sq-unfold-in var(--sq-fold-dur, 0.3s) var(--al-ease) both;
}
body[data-alive] details[data-sq-folding="out"] > :not(summary) {
  animation-name: sq-unfold-out;
  animation-duration: calc(var(--sq-fold-dur, 0.3s) * 0.7);   /* exit faster than entrance */
}
@keyframes sq-unfold-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes sq-unfold-out { from { opacity: 1; } to { opacity: 0; } }

/* ── 5. THE VERDICT ───────────────────────────────────────────────────────
   Every playable and every tool on this site ends the same way: you did
   something, and a .verdict banner tells you what the mathematics made of
   it.  On the first answer the banner fades up (style.css). On the SECOND
   and every one after -- a new gate, another slider drag, the next round --
   the text is simply replaced between one frame and the next, with no sign
   that the machine just re-decided.  That is the same missing answer §1
   fixes for arrival, one layer in: the reply changed and nothing said so.

   alive.js watches each .verdict (and #dq-out, the one that is not classed)
   for a content change and stamps it for ~0.9s.  What resolves is light in
   the banner's own already-chosen colour -- teal for a pass, red for a
   miss, lavender for a split -- so the cue cannot contradict the verdict it
   is drawing attention to.  Motionless, on the same reading of the house
   rule as arrival: the banner is where it belongs, only its border and fill
   brighten and settle.  Fires only on a real change, never on load (the
   first fade already covers that), never on a loop. */
body[data-alive] .verdict[data-sq-verdict],
body[data-alive] #dq-out[data-sq-verdict] > *:first-child {
  animation: sq-verdict 0.9s var(--al-ease) both;
}
@keyframes sq-verdict {
  0%   { box-shadow: 0 0 0 0 transparent; }
  22%  { box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 55%, transparent),
                     0 6px 22px -8px color-mix(in srgb, currentColor 40%, transparent); }
  55%  { box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 55%, transparent),
                     0 6px 22px -8px color-mix(in srgb, currentColor 40%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  @keyframes sq-verdict {
    0%   { box-shadow: 0 0 0 0 rgba(45,212,191,0); }
    22%  { box-shadow: 0 0 0 2px rgba(45,212,191,.5); }
    55%  { box-shadow: 0 0 0 2px rgba(45,212,191,.5); }
    100% { box-shadow: 0 0 0 0 rgba(45,212,191,0); }
  }
}

/* ── 6. THE PRESS ─────────────────────────────────────────────────────────
   DESIGN.md: "Motion is almost entirely feedback -- a press, a toggle."
   The toggle is covered (.preset.on). The press was not: tap a gate, a
   difficulty, a preset row, and the control changes state with no moment of
   contact.  A real instrument's key travels when you push it.  One frame
   down, released on ease-out -- 1px, never a scale, so nothing around it
   reflows and the no-springy rule is kept.  Pointer devices only; a tap
   highlight on touch already carries this. */
@media (hover: hover) {
  body[data-alive] .preset:active,
  body[data-alive] .gatebtn:active,
  body[data-alive] .cab:active,
  body[data-alive] .dq-opt:active,
  body[data-alive] button.bigplay:active {
    transform: translateY(1px);
    transition: transform 0.04s var(--al-ease);
  }
}

/* ── 7. THE MARKER ────────────────────────────────────────────────────────
   rails.js builds a section index in the left gutter, but only at >=1440px
   -- below that the gutter is gone and there is nothing.  On a 17,000px
   reference page (feasible has 12 sections and 24 topics; formalism, 20)
   that leaves a phone or a laptop with no way to see where they are or jump
   anywhere.  This is the small-screen half of that same wayfinder: a
   frosted pill in the corner that says which section you are in and, on a
   tap, opens the list.

   It fires nothing on its own, loops nothing, and moves only when the
   visitor opens or closes it -- same charter as the rest of this file.
   Gated the exact opposite way to rails.css (max-width 1439) so the two
   can never both be on screen.  Bottom-LEFT, because #qz-pill owns
   bottom-right.  Frosted from the glass layer's own tokens when the page
   opts into it, a settled opaque panel otherwise. */
body[data-alive] .sq-marker {
  position: fixed;
  left: max(14px, env(safe-area-inset-left, 0px));
  bottom: max(14px, env(safe-area-inset-bottom, 0px));
  z-index: 95;                  /* over .readbar (90), under #qz-pill (9998) */
  max-width: min(76vw, 320px);
  font-size: 0.78rem;
  color: var(--muted);
  animation: sq-marker-in 0.5s var(--al-ease) both;
}
@keyframes sq-marker-in { from { opacity: 0; } to { opacity: 1; } }
@media print { body[data-alive] .sq-marker { display: none; } }

/* the collapsed control */
body[data-alive] .sq-marker-tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 100%;
  padding: 7px 13px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill, 999px);
  background: var(--nav-bg, var(--panel));
  color: var(--muted);
  font: inherit;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  cursor: pointer;
  /* The quiet resting state dims the CONTENTS, not the chip. It used to dim the
     whole element (opacity .62), which let the page's own text show through the
     frosted glass: on a phone "1/5" printed straight over a heading. */
  transition: border-color var(--al-quick) var(--al-ease),
              color var(--al-quick) var(--al-ease),
              transform 0.05s var(--al-ease);
}
body[data-alive] .sq-marker-tab > * {
  opacity: 0.68;
  transition: opacity var(--al-base) var(--al-ease);
}
body[data-alive] .sq-marker-tab:hover > *,
body[data-alive] .sq-marker-tab:focus-visible > *,
body[data-alive] .sq-marker[data-open] .sq-marker-tab > * { opacity: 1; }
body[data-alive] .sq-marker-tab:hover,
body[data-alive] .sq-marker-tab:focus-visible { border-color: var(--teal); color: var(--text); }
body[data-alive] .sq-marker-tab:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
body[data-alive] .sq-marker-tab:active { transform: translateY(1px); }
body[data-alive] .sq-marker[data-open] .sq-marker-tab { border-color: var(--teal); color: var(--text); }

body[data-alive] .sq-marker-glyph { color: var(--violet); flex: none; }
body[data-alive] .sq-marker-count { flex: none; font-weight: 700; color: var(--text); }
body[data-alive] .sq-marker-here {
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis; min-width: 0;
}
@media (max-width: 400px) { body[data-alive] .sq-marker-here { display: none; } }

/* the opened list */
body[data-alive] .sq-marker-list {
  display: block;
  position: absolute;
  left: 0;
  bottom: calc(100% + 8px);
  width: max-content;
  max-width: min(78vw, 320px);
  max-height: min(56vh, 460px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--r-md, 10px);
  background: var(--panel);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.5);
  opacity: 0;
  transform: translateY(6px);
  pointer-events: none;
  transition: opacity var(--al-base) var(--al-ease),
              transform var(--al-base) var(--al-ease);
}
body[data-alive] .sq-marker[data-open] .sq-marker-list {
  opacity: 1;
  transform: none;
  pointer-events: auto;
}
body[data-alive] .sq-marker-item {
  display: flex;
  align-items: baseline;
  gap: 9px;
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: var(--r-sm, 6px);
  background: transparent;
  color: var(--muted);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--al-quick) var(--al-ease),
              color var(--al-quick) var(--al-ease);
}
body[data-alive] .sq-marker-item:hover,
body[data-alive] .sq-marker-item:focus-visible { background: var(--rowhover, rgba(148,163,184,.08)); color: var(--text); outline: none; }
body[data-alive] .sq-marker-item:focus-visible { box-shadow: inset 0 0 0 1px var(--teal); }
body[data-alive] .sq-marker-item[aria-current="true"] { color: var(--teal); }
body[data-alive] .sq-marker-idx {
  flex: none; width: 1.4em; text-align: right;
  font-variant-numeric: tabular-nums; font-size: 0.72rem; opacity: 0.7;
}
body[data-alive] .sq-marker-item[aria-current="true"] .sq-marker-idx { opacity: 1; font-weight: 700; }

/* frost it from the glass layer's own material tokens, only where the page
   opted into glass and the browser can actually blur */
@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  body[data-alive][data-glass] .sq-marker-tab,
  body[data-alive][data-glass] .sq-marker-list {
    background: var(--gl-tint-2, var(--panel));
    border-color: var(--gl-edge, var(--border));
    -webkit-backdrop-filter: blur(var(--gl-blur, 14px)) saturate(var(--gl-sat, 1.4));
    backdrop-filter: blur(var(--gl-blur, 14px)) saturate(var(--gl-sat, 1.4));
    box-shadow: inset 0 1px 0 var(--gl-rim-hi, rgba(255,255,255,.14)),
                var(--gl-depth, 0 18px 40px -18px rgba(0,0,0,.5));
  }
}

@media (min-width: 1440px) { body[data-alive] .sq-marker { display: none; } }

/* "back to top", the first row of the open list. rails.js gives the gutter
   rail one of these; the marker gets the same, and on a phone it is the
   fastest way up a 17,000px page. */
body[data-alive] .sq-marker-top {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 9px;
  margin-bottom: 2px;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: var(--r-sm, 6px) var(--r-sm, 6px) 0 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  letter-spacing: 0.02em;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--al-quick) var(--al-ease), color var(--al-quick) var(--al-ease);
}
body[data-alive] .sq-marker-top:hover,
body[data-alive] .sq-marker-top:focus-visible {
  background: var(--rowhover, rgba(148,163,184,.08)); color: var(--text); outline: none;
}
body[data-alive] .sq-marker-top:focus-visible { box-shadow: inset 0 0 0 1px var(--teal); }

/* ── 8. THE KEYS ──────────────────────────────────────────────────────────
   rails.css §"the keyboard layer" already says it out loud: the shortcuts
   (qubit.js's X-T / M / R) are "something you find rather than" are told,
   and the only hint is a dim readout in a corner nobody looks at.  `?`
   fixes that -- it is the near-universal "show me the keys" key, and this
   overlay is where every shortcut on the page finally has a printed home.

   j / k step between section headings (n / p alias), m toggles the marker.
   All of it is inert while a field has focus or a modifier is held, so it
   never eats a real shortcut or a letter someone meant to type. */
body[data-alive] .sq-help {
  position: fixed;
  inset: 0;
  z-index: 9996;                /* under #qz-pill/#qz-fx, over everything else */
  display: grid;
  place-items: center;
  padding: 24px;
  background: color-mix(in srgb, var(--bg) 62%, transparent);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--al-base) var(--al-ease);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  body[data-alive] .sq-help { background: rgba(0, 0, 0, 0.55); }
}
body[data-alive] .sq-help[data-open] { opacity: 1; pointer-events: auto; }

body[data-alive] .sq-help-card {
  width: min(92vw, 440px);
  max-height: 80vh;
  overflow-y: auto;
  padding: 20px 22px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg, 14px);
  background: var(--panel);
  box-shadow: 0 30px 70px -24px rgba(0, 0, 0, 0.6);
  transform: translateY(8px);
  transition: transform var(--al-base) var(--al-ease);
}
body[data-alive] .sq-help[data-open] .sq-help-card { transform: none; }
@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  body[data-alive][data-glass] .sq-help-card {
    background: var(--gl-tint-2, var(--panel));
    border-color: var(--gl-edge, var(--border));
    -webkit-backdrop-filter: blur(var(--gl-blur, 16px)) saturate(var(--gl-sat, 1.5));
    backdrop-filter: blur(var(--gl-blur, 16px)) saturate(var(--gl-sat, 1.5));
    box-shadow: inset 0 1px 0 var(--gl-rim-hi, rgba(255,255,255,.14)),
                var(--gl-depth-hi, 0 30px 64px -26px rgba(0,0,0,.55));
  }
}
body[data-alive] .sq-help-card h2 {
  margin: 0 0 14px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
body[data-alive] .sq-help-card h2::before { content: none; }   /* no accent bar here */
body[data-alive] .sq-help-row {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 7px 0;
  border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
  font-size: 0.92rem;
  color: var(--text);
}
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
  body[data-alive] .sq-help-row { border-top-color: var(--border); }
}
body[data-alive] .sq-help-row:first-of-type { border-top: 0; }
body[data-alive] .sq-help-keys { flex: none; display: flex; gap: 5px; min-width: 92px; }
body[data-alive] .sq-help kbd {
  font: 700 0.78rem/1 var(--mono, ui-monospace, "SF Mono", Menlo, Consolas, monospace);
  padding: 3px 7px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--bg);
  color: var(--text);
  white-space: nowrap;
}
body[data-alive] .sq-help-what { color: var(--muted); }
body[data-alive] .sq-help-hint {
  margin: 14px 0 0;
  font-size: 0.8rem;
  color: var(--muted);
}

/* ── REDUCED MOTION ────────────────────────────────────────────────────────
   Reduced motion means less movement, not less information.

   style.css line ~1178 takes the nuclear option under this query:

       *, *::before, *::after { animation: none !important; transition: none !important; }

   That is the right call for decoration, and the wrong one for a cue whose
   whole job is to answer "which of the forty things on this page did I just
   ask for?".  Measured in a real browser with the preference set, arrival
   rendered as literally nothing: `animation-name: none`, outline transparent.
   The rule cannot be argued with -- it is `!important` on a universal selector
   -- so the cue is re-expressed instead of re-fought.  The attribute sits on
   the element for ~1.3s either way, so a plainly PAINTED ring carries exactly
   the same information with exactly zero movement, which is what the
   preference actually asks for.

   No `color-mix` here on purpose: a solid 1px accent ring needs no alpha maths
   and is theme-correct in both modes from the token alone.  The wash is
   dropped -- it exists to soften an animated flare, and there is no flare now.

   `.sq-pl-note` needs nothing: its `sq-note` keyframes only fade it in and out,
   so with animations off it simply renders at its natural full opacity and the
   JS removes it on schedule.  The copy confirmation stays visible.

   The unfold is the one thing here that genuinely changes geometry, so it is
   the one thing that goes entirely: alive.js tests the same query and never
   starts a fold, and this is the belt to that pair of braces. */
@media (prefers-reduced-motion: reduce) {
  body[data-alive] details[data-sq-folding] {
    transition: none;
    height: auto !important;
  }
  body[data-alive] [data-sq-arrived] { outline-color: var(--teal); }
  body[data-alive] h2[data-sq-arrived],
  body[data-alive] h3[data-sq-arrived] { color: var(--teal); }
  body[data-alive] h2[data-sq-arrived]::before { filter: brightness(1.5) saturate(1.25); }

  /* The verdict cue re-expressed with no movement: a plainly painted 2px
     ring in the banner's own colour, on the element for the same ~0.9s the
     JS keeps the attribute.  The press travel simply does not happen. */
  body[data-alive] .verdict[data-sq-verdict] {
    animation: none;
    box-shadow: 0 0 0 2px currentColor;
  }
  body[data-alive] #dq-out[data-sq-verdict] > *:first-child { animation: none; }
  body[data-alive] .preset:active,
  body[data-alive] .gatebtn:active,
  body[data-alive] .cab:active,
  body[data-alive] .dq-opt:active,
  body[data-alive] button.bigplay:active { transform: none; }

  /* the marker still opens, it just does not slide: the list appears in
     place with no transform and no timed transition */
  body[data-alive] .sq-marker { animation: none; }
  body[data-alive] .sq-marker-list { transition: none; transform: none; }
  body[data-alive] .sq-marker-tab:active { transform: none; }

  /* the help overlay appears with no slide, still fades so it does not
     hard-cut over the page */
  body[data-alive] .sq-help-card { transition: none; transform: none; }
}
/* --- hud.css --- */
/* ===================================================================
   hud.css — the progress chip in the nav, and the homepage return card.
   Injected by hud.js; see that file's header for why both exist.

   Kept OUT of style.css on purpose, the same rule motion.css, rails.css,
   vivid.css and alive.css follow: style.css is the shared 2,000-line base
   every page depends on, so an additive layer that only some readers ever
   see gets its own file and can be reverted by deleting one <link>.

   The gradient is the site's existing brand pair (violet -> teal), the same
   one .sq-account.signed-in already uses. Restrained on purpose: this is a
   readout, not a trophy. It must never out-shout the nav it sits in.
   =================================================================== */

/* ---- the nav chip ---------------------------------------------------- */
/* margin-left:auto so the chip is pushed to the right end of the nav, the
   same way .sq-account is. With both present the chip claims the slack and
   the avatar sits immediately after it -- two 40px discs, "where you are"
   then "who you are". */
.hud-slot { display: inline-flex; align-items: center; margin-left: auto; }

/* Forty pixels round, the same footprint as .sq-account's avatar, because
   that is the entire budget: the nav's first row fits a 40-49px control and
   wraps at 50 (measured). A wrap would add 58px of header to every page. */
.hud-chip {
  position: relative; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  text-decoration: none; color: var(--text);
  /* The ring IS the coherence value: a conic sweep to --hud-p, the rest track.
     Set on the element by hud.js so there is one number in one place. */
  background: conic-gradient(var(--hud-ring, var(--violet)) var(--hud-p, 0%),
                             var(--border) 0);
  transition: transform .2s ease;
}
/* The inner disc that turns the sweep into a ring and gives the numeral a
   surface. Inset 3px = a 3px ring, thick enough to read at a glance. */
.hud-chip::before {
  content: ''; position: absolute; inset: 3px;
  border-radius: 50%; background: var(--panel);
}
.hud-chip:hover { transform: scale(1.06); }
.hud-chip:hover::before { background: var(--rowhover); }
.hud-chip:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

.hud-num {
  position: relative;                       /* above ::before */
  font-size: .62rem; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
  color: var(--text);
}

/* coherence.js's four levels, the one place colour carries meaning.
   Below 15% the Static is winning and the ring should say so without a word. */
.hud-chip[data-level="high"]   { --hud-ring: var(--teal); }
.hud-chip[data-level="mid"]    { --hud-ring: var(--violet); }
.hud-chip[data-level="low"]    { --hud-ring: #b45309; }
.hud-chip[data-level="static"] { --hud-ring: #b3324a; }

/* The chip's full sentence, for screen readers; the dial is a summary. */
.hud-sr {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;
}

/* ---- the homepage return card ---------------------------------------- */
.hud-return {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px;
  margin: 14px 0 0; padding: 11px 15px;
  border: 1px solid var(--border); border-left: 3px solid var(--teal);
  border-radius: 10px;
  background: linear-gradient(100deg, rgba(45,212,191,.10), transparent 70%);
  font-size: .92rem; color: var(--muted);
}
.hud-return b { color: var(--text); font-weight: 700; }
.hud-return-lab {
  font-size: .7rem; letter-spacing: .14em; text-transform: uppercase;
  font-weight: 700; color: var(--teal);
}
/* --border is a hairline colour; as a text glyph it renders as a gap rather
   than a separator. --muted at half strength reads as punctuation without
   competing with the facts on either side. */
.hud-dot { color: color-mix(in srgb, var(--muted) 55%, transparent); }
.hud-return-go {
  font-weight: 600; color: var(--teal);
  text-decoration: none; white-space: nowrap;
}
.hud-return-go:hover { text-decoration: underline; }
/* No margin-left:auto here on purpose. Pushing the link to the right edge
   fits at some progress states and misses by a few pixels at others (the act
   names are different lengths), and when it misses, a wrapped flex item that
   still carries an auto margin lands alone on the right of its own line --
   which looks like a bug rather than a layout. Letting it flow as the last
   clause of the sentence wraps like prose, i.e. correctly, at every width. */

/* ---- the homepage Contract-of-the-Day line -------------------------- */
/* A sibling of .hud-return. The return card says "where was I on the Path";
   this says "today's cross-arcade Contract and the streak riding on it". It
   reads the same symbiq_contract_v1 the Arcade card writes, so no games.js is
   needed on the homepage. Violet left-accent (the return card's is teal) so
   two stacked lines never read as one card shown twice. Homepage only, and
   nothing at all for someone who has never taken a Contract. */
/* Plain block, not flex: the streak reads "🔥 4-day streak" and the "4" must
   sit tight against "-day", which a flex gap between children would break. The
   link flows as the last clause and wraps like prose -- the same reasoning as
   .hud-return-go's note below. */
.hud-contract {
  margin: 10px 0 0; padding: 10px 15px;
  border: 1px solid var(--border); border-left: 3px solid var(--violet);
  border-radius: 10px;
  font-size: .9rem; line-height: 1.55; color: var(--muted);
}
.hud-contract b { color: var(--text); font-weight: 700; }
.hud-c-lab {
  font-size: .7rem; letter-spacing: .14em; text-transform: uppercase;
  font-weight: 700; color: var(--violet); margin-right: 4px;
}
.hud-contract .hud-return-go { margin-left: 4px; }
.hud-c-ok { color: var(--teal); font-weight: 700; }

@media (prefers-reduced-motion: reduce) {
  .hud-coh > i, .hud-chip { transition: none; }
}

/* the share card dialog (hud.js shareCard) */
dialog.sq-share { margin: auto; width: min(720px, calc(100vw - 24px)); padding: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); color: var(--text); }
dialog.sq-share::backdrop { background: rgba(4, 7, 15, .66); }
.sq-share-in { margin: 0; padding: 16px 18px 18px; }
.sq-share-in h2 { margin: 0 0 10px; font-size: 1.1rem; }
.sq-share canvas { display: block; width: 100%; height: auto; border-radius: 10px; border: 1px solid var(--border); }
.sq-share-msg { min-height: 1.4em; margin: 8px 0 0; font-size: .88rem; color: var(--muted); }
.sq-share-act { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.sq-share-act button { min-height: 44px; }
/* --- theme.css --- */
/* ================================================================
   THEME LAYER (2026-08-30)   —   site-wide, all 24 pages

   Turns the site's implicit "follow the OS" scheme into an explicit,
   remembered CHOICE: System / Light / Dim / Dark, plus two comfort
   switches (reduce motion, reduce transparency). Pairs with
   `theme.js`, which writes `data-theme` / `data-motion-pref` /
   `data-transparency-pref` on <html> and builds the footer control.

   WHY A MIRROR FILE, NOT AN EDIT TO style.css.
   The whole codebase themes through `@media (prefers-color-scheme)`.
   An attribute cannot re-trigger a media query, so forcing a scheme
   against the OS means the token sets have to be re-declared under an
   attribute selector. Editing style.css to do that is a 24-page
   cache bump on the shared 2,300-line base; this project's standing
   rule (see DESIGN.md) is that a scheme experiment must not be that.
   So the token sets are MIRRORED here, exactly as vivid.css already
   mirrors style.css's token structure. Specificity + load order do
   the overriding; nothing existing is touched.

   KEEP IN SYNC. The blocks below mirror, value for value:
     - base    : style.css  :root (dark) + @media light :root
     - vivid   : vivid.css  body[data-vivid] (dark) + its light block
     - glass   : glass.css  body[data-glass] light block (--gl-*)
     - 7 component rules that style.css only gave an @media-light form
       (nav active colour, .bigplay, .sqform button, a few more)
   If any of those move in the source, move them here too. A drift
   check is cheap: force Light on a light-OS machine and diff against
   the media-query rendering.

   HOW "SYSTEM" WORKS. theme.js removes the attribute entirely for
   System, so `@media (prefers-color-scheme)` in style.css is back in
   charge and this file contributes nothing. Every `[data-theme=...]`
   block here is inert until a real choice is made.
   ================================================================ */

/* ---------------------------------------------------------------- §1
   BASE TOKENS  (mirror of style.css :root + @media light :root)

   `:root[data-theme="light"]` is (0,2,0); style.css's `@media light
   :root` is (0,1,0); the bare `:root` is (0,1,0). So each block here
   wins regardless of the OS setting, which is the point. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f8fafc;
  --panel: #ffffff;
  --text: #0f172a;
  --muted: #526180;
  --violet: #7c3aed;
  --teal: #0f766e;
  --link: #0f766e;
  --border: #dbe2ef;
  --nav-bg: rgba(248, 250, 252, 0.85);
  --dot: rgba(124, 58, 237, 0.06);
  --glow1: rgba(124, 58, 237, 0.08);
  --glow2: rgba(13, 148, 136, 0.07);
  --shadow: rgba(15, 23, 42, 0.08);
  --h1-a: #0f172a;
  --h1-b: #7c3aed;
  --h1-c: #0d9488;
  --rowhover: rgba(15, 23, 42, 0.04);
  --green: #15803d;
  --yellow: #b45309;
  --red: #b91c1c;
  --surface-2: #f1f5fb;
  --surface-3: #e9eff8;
  color-scheme: light;
}

/* Dark and Dim share the full dark set; Dim then nudges the surfaces
   and text softer in §1b. (mirror of style.css :root) */
:root[data-theme="dark"],
:root[data-theme="dim"] {
  color-scheme: dark;
  --bg: #121a2e;
  --panel: #1a2440;
  --text: #e8ecf6;
  --muted: #9aa5bd;
  --violet: #a78bfa;
  --teal: #2dd4bf;
  --link: #2dd4bf;
  --border: #2d3b5e;
  --green: #4ade80;
  --yellow: #facc15;
  --red: #f87171;
  --nav-bg: rgba(18, 26, 46, 0.82);
  --dot: rgba(167, 139, 250, 0.07);
  --glow1: rgba(139, 92, 246, 0.22);
  --glow2: rgba(45, 212, 191, 0.15);
  --shadow: rgba(0, 0, 0, 0.3);
  --h1-a: #e8ecf6;
  --h1-b: #a78bfa;
  --h1-c: #2dd4bf;
  --rowhover: rgba(148, 163, 184, 0.06);
  --surface-2: #202c4a;
  --surface-3: #283759;
  color-scheme: dark;
}

/* §1b  DIM — a softer dark. Same hues, same accents, same contrast
   law; the canvas lifts a little off true black, the text steps back
   from pure white, the hairlines ease. Nothing here carries meaning
   that the tier colours or the violet/teal split depend on. */
:root[data-theme="dim"] {
  --bg: #151d31;
  --panel: #1f2944;
  --text: #d9deea;
  --muted: #949fb6;
  --border: #34446a;
  --shadow: rgba(0, 0, 0, 0.22);
  --surface-2: #232f4d;
  --surface-3: #2b3961;
  --rowhover: rgba(148, 163, 184, 0.05);
}

/* ---------------------------------------------------------------- §2
   VIVID TOKENS  (mirror of vivid.css body[data-vivid] + its light
   block). `data-theme` lives on <html>, so the selector is
   `:root[data-theme] body[data-vivid]` -- (0,3,1), which outranks
   vivid.css's own `body[data-vivid]` (0,2,0) whatever the media. */
:root[data-theme="light"] body[data-vivid] {
  --bg: #f8fafc;
  --panel: #ffffff;
  --panel-2: #eef3fb;
  --text: #0f172a;
  --muted: #526180;
  --violet: #7c3aed;
  --indigo: #4a5ad0;
  --teal: #0f766e;
  --link: #0f766e;
  --border: #dbe2ef;
  --green: #15803d;
  --yellow: #b45309;
  --red: #b91c1c;
  --nav-bg: rgba(248, 250, 252, 0.85);
  --dot: rgba(124, 58, 237, 0.06);
  --glow1: rgba(124, 58, 237, 0.09);
  --glow2: rgba(13, 148, 136, 0.08);
  --glow3: rgba(74, 90, 208, 0.06);
  --shadow: rgba(15, 23, 42, 0.09);
  --rowhover: rgba(15, 23, 42, 0.04);
  --h1-a: #0f172a;
  --h1-b: #7c3aed;
  --h1-c: #0d9488;
  --spectrum: linear-gradient(100deg, #7c3aed, #4a5ad0 46%, #0f766e);
}

/* Warm retint, 2026-09-02 -- mirrors vivid.css's own dark block value
   for value (that file's §1 header carries the full reasoning). Kept
   in sync here because an explicit Dark/Dim choice re-triggers this
   block instead of the plain @media query vivid.css uses. */
:root[data-theme="dark"] body[data-vivid],
:root[data-theme="dim"] body[data-vivid] {
  --bg: #16100f;
  --panel: #221816;
  --panel-2: #2b1f1b;
  --text: #e8ecf6;
  --muted: #9aa5bd;
  --violet: #c586e6;
  --indigo: #e0a15c;
  --teal: #33d696;
  --link: #33d696;
  --border: #362721;
  --green: #4ade80;
  --yellow: #facc15;
  --red: #f87171;
  --nav-bg: rgba(22, 16, 15, 0.82);
  --dot: rgba(197, 134, 230, 0.08);
  --glow1: rgba(197, 134, 230, 0.16);
  --glow2: rgba(51, 214, 150, 0.11);
  --glow3: rgba(224, 161, 92, 0.08);
  --shadow: rgba(20, 10, 8, 0.36);
  --h1-a: #e8ecf6;
  --h1-b: #c586e6;
  --h1-c: #33d696;
  --rowhover: rgba(148, 163, 184, 0.06);
  --spectrum: linear-gradient(100deg, var(--violet), var(--indigo) 46%, var(--teal));
}

/* Warm retint, 2026-09-02: a softer version of the same warm void
   above, same relative lift the pre-existing blue-toned values had.
   --text/--muted are the one thing left exactly as they already were
   -- the dim comfort-dimming feature is unrelated to this retint, and
   both are already near-neutral. */
:root[data-theme="dim"] body[data-vivid] {
  --bg: #1c1513;
  --panel: #261c19;
  --panel-2: #2f2320;
  --text: #d9deea;
  --muted: #949fb6;
  --border: #3a2b25;
  --shadow: rgba(20, 10, 8, 0.28);
}

/* ---------------------------------------------------------------- §3
   GLASS TINTS  (mirror of glass.css body[data-glass] light block).
   Dark and Dim both use glass.css's own dark tints -- they already
   read correctly over the dimmer canvas -- so only Light is restated. */
:root[data-theme="light"] body[data-glass] {
  --gl-tint:   rgba(255, 255, 255, 0.78);
  --gl-tint-2: rgba(244, 247, 252, 0.96);
  --gl-nav:    rgba(248, 250, 252, 0.80);
  --gl-blur:      16px;
  --gl-blur-card: 10px;
  --gl-blur-hero: 6px;
  --gl-sat:       1.4;
  --gl-rim:    rgba(255, 255, 255, 0.75);
  --gl-rim-hi: rgba(255, 255, 255, 0.92);
  --gl-edge: color-mix(in oklab, var(--indigo, #4a5ad0) 20%, var(--border));
  --gl-depth:    0 20px 44px -26px rgba(15, 23, 42, 0.18);
  --gl-depth-hi: 0 28px 58px -28px rgba(15, 23, 42, 0.22);
  --gl-cast:     color-mix(in oklab, var(--violet) 12%, transparent);
}

/* ---------------------------------------------------------------- §4
   COMPONENT RULES THAT WERE @media-LIGHT ONLY

   style.css darkens teal-as-text and re-pairs the CTA gradient for
   light backgrounds inside `@media (prefers-color-scheme: light)`.
   Those never fire when Light is FORCED on a dark-OS machine, so the
   contrast fixes they carry would be lost. Re-key them to
   `[data-theme="light"]`; and because forcing Dark/Dim on a LIGHT-OS
   machine makes the media versions fire wrongly, restate the dark
   forms too. (0,2,x) — beats the (0,1,x) media rules. */

/* -- forced LIGHT -- */
:root[data-theme="light"] .navcat[open] > summary,
:root[data-theme="light"] .navcat.navcat-cur > summary,
:root[data-theme="light"] .navmain.active,
:root[data-theme="light"] .navchip { color: #0f766e; }

:root[data-theme="light"] .bigplay {
  background: linear-gradient(135deg, #0f766e, #115e59);
  color: #ffffff;
  box-shadow: 0 3px 10px rgba(17, 94, 89, 0.18);
}
:root[data-theme="light"] .bigplay:hover { box-shadow: 0 5px 14px rgba(17, 94, 89, 0.24); }
:root[data-theme="light"] .bigplay.ghost {
  background: transparent; color: #0f766e; border-color: #0f766e;
}

:root[data-theme="light"] .sqform button {
  background: linear-gradient(135deg, #0f766e, #115e59);
  color: #ffffff;
  box-shadow: 0 6px 16px rgba(17, 94, 89, 0.22);
}
:root[data-theme="light"] .sqform button:hover { box-shadow: 0 10px 22px rgba(17, 94, 89, 0.3); }
:root[data-theme="light"] .sqform button.ghost { color: #0f766e; border-color: #0f766e; }

:root[data-theme="light"] .introute-card.resume .pg { color: #0f766e; }
:root[data-theme="light"] .rung-dot.is-cur { color: #f8fafc; }
:root[data-theme="light"] ::selection { background: rgba(124, 58, 237, 0.22); }

/* -- forced DARK / DIM on a light-OS machine: hold the dark forms -- */
:root[data-theme="dark"] .bigplay,
:root[data-theme="dim"] .bigplay {
  background: linear-gradient(135deg, var(--violet), var(--teal));
  color: #04211d;
}
:root[data-theme="dark"] .bigplay.ghost,
:root[data-theme="dim"] .bigplay.ghost {
  background: transparent; color: var(--teal); border-color: var(--teal);
}
:root[data-theme="dark"] .sqform button,
:root[data-theme="dim"] .sqform button {
  background: linear-gradient(135deg, var(--teal), #22b8a6);
  color: #04211d;
  box-shadow: 0 6px 16px rgba(45, 212, 191, 0.22);
}
:root[data-theme="dark"] .navcat[open] > summary,
:root[data-theme="dim"] .navcat[open] > summary,
:root[data-theme="dark"] .navmain.active,
:root[data-theme="dim"] .navmain.active,
:root[data-theme="dark"] .navchip,
:root[data-theme="dim"] .navchip { color: var(--teal); }

/* ---------------------------------------------------------------- §5
   THE TWO COMFORT SWITCHES

   `prefers-reduced-motion` / `prefers-reduced-transparency` are the
   right primary signal and every layer already honours them. These
   let a visitor dial the same thing back explicitly, per-site, when
   their OS setting says otherwise. Broad `!important` sweeps, the
   same shape a UA reset uses. */
:root[data-motion-pref="reduce"] *,
:root[data-motion-pref="reduce"] *::before,
:root[data-motion-pref="reduce"] *::after {
  animation-duration: 0.001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 0.001ms !important;
}
:root[data-motion-pref="reduce"] { scroll-behavior: auto !important; }

:root[data-transparency-pref="reduce"] *,
:root[data-transparency-pref="reduce"] *::before,
:root[data-transparency-pref="reduce"] *::after {
  -webkit-backdrop-filter: none !important;
  backdrop-filter: none !important;
}
/* with the blur gone the frosted surfaces must not be left see-through */
:root[data-transparency-pref="reduce"] body[data-glass] nav:not(.rung-rail) { background: var(--nav-bg) !important; }
:root[data-transparency-pref="reduce"] body[data-glass] .navpanel,
:root[data-transparency-pref="reduce"] body[data-glass] #navcats,
:root[data-transparency-pref="reduce"] body[data-glass] .sq-account .navpanel,
:root[data-transparency-pref="reduce"] body[data-glass] .card,
:root[data-transparency-pref="reduce"] body[data-glass] .introute-card,
:root[data-transparency-pref="reduce"] body[data-glass] .hud-return,
:root[data-transparency-pref="reduce"] body[data-glass] .hud-contract { background: var(--panel) !important; }

/* ---------------------------------------------------------------- §6
   THE APPEARANCE CONTROL   (built by theme.js, appended to <footer>)

   A quiet block: a label, a four-way segmented switch, and two
   toggles. Uses the site's own tokens and the --sp / --r scale, so
   it re-themes with everything else and needs no dark/light fork. */
.sq-appear {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  font-size: 0.8rem;
}
.sq-appear-lab {
  font-size: 0.7rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--muted);
}
.sq-seg {
  display: inline-flex;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill, 999px);
  background: var(--surface-2);
}
.sq-seg button {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 5px 12px;
  min-height: 32px;
  border-radius: var(--r-pill, 999px);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.sq-seg button:hover { color: var(--text); }
.sq-seg button[aria-pressed="true"] {
  color: var(--text);
  background: var(--panel);
  box-shadow: 0 1px 3px var(--shadow);
}
.sq-seg button:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

.sq-appear-tog {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
}
.sq-appear-tog input {
  appearance: none;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  position: relative;
  cursor: pointer;
  flex: none;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.sq-appear-tog input::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--muted);
  transform: translateY(-50%);
  transition: transform 0.16s var(--ease-out-quint, ease-out), background 0.15s ease;
}
.sq-appear-tog input:checked {
  background: color-mix(in oklab, var(--teal) 30%, var(--surface-2));
  border-color: color-mix(in oklab, var(--teal) 50%, var(--border));
}
.sq-appear-tog input:checked::after {
  transform: translateY(-50%) translateX(14px);
  background: var(--teal);
}
.sq-appear-tog input:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .sq-seg button, .sq-appear-tog input, .sq-appear-tog input::after { transition: none; }
}
/* --- glass.css --- */
/* ================================================================
   GLASS LAYER (2026-08-30)

   A spatial-glass / frosted-material pass. Shipped 2026-08-30 on the
   six vivid pages (index + bitcoin, qec, ai, compare, logical-qubit);
   extended to all 24 the same day at Chinmoy's ask. Everything is
   scoped under `body[data-glass]`, so:

     - a page without the attribute is byte-for-byte untouched, and
     - the whole layer reverts by deleting one attribute + one <link>.

   It ships as its own file with its own cache-buster for the same
   reason motion.css / rails.css / vivid.css / alive.css / hud.css do:
   style.css is shared by 24 pages and the 2026-07-24 scar says
   touching it is a 24-page deploy. A material experiment must never
   be that.

   WHAT THIS IS. Not a re-theme -- a re-surface. The palette, the
   semantic colour law (teal = AI/OR/real, violet = quantum/theory,
   yellow = security/check-this), every contrast-locked token and
   every tap-target rule survive intact. What changes is the
   MATERIAL: a handful of surfaces stop being flat opaque panels and
   start reading as panes of frosted glass floating above the vivid
   ambient field --

     1. a translucent fill that samples the aurora behind it
        (backdrop-filter: blur + saturate),
     2. a 1px rim light on the top edge (inset highlight),
     3. a hue-tinted hairline border instead of flat grey,
     4. a soft, wide, downward depth shadow so the pane reads as
        lifted off the page rather than printed on it.

   Applied to CHROME, not body copy: the sticky nav, the dropdown
   menus, the cards / route cards, the section heroes, the arcade
   door, and the two HUD cards. Nothing that carries a paragraph of
   reading gets frosted directly -- text always sits on a panel with
   its own settled tone.

   THE DELIBERATE NON-CHANGES.
   * No text colour moves. Only background, border, box-shadow and
     backdrop-filter are touched here.
   * .bigplay is not in this file. Three rounds of feedback asked
     that control to be quieter; a glass rim would make it louder.
   * No keyframe animation. Glass here is a static material, not a
     motion effect -- which also means there is nothing for a
     reduced-motion UA to switch off.

   GUARD RAILS.
   * Translucency + blur live inside `@supports (backdrop-filter ...)`
     with an opaque value declared FIRST, so a browser without the
     property gets a solid panel and loses nothing structural.
   * `@media (prefers-reduced-transparency: reduce)` (§7) forces the
     whole set back to opaque for anyone who has asked for it.
   * backdrop-filter establishes a containing block for fixed
     descendants. Every selector here is a leaf surface -- a card, a
     menu, a hero -- none of them wrap the fixed .readbar or the
     fixed peripheral rails (those are body-level), so the fixed
     layers are unaffected. Verified against rails.css / tiers.js.

   color-mix() and `--indigo` are borrowed from vivid.css. On the six
   vivid pages the token is always defined; on the other 18 it is not,
   so every var(--indigo ...) here passes a literal fallback and the
   layer degrades to the plain --border edge.
   ================================================================ */

/* ---------------------------------------------------------------- §1
   THE MATERIAL TOKENS

   body[data-glass] is specificity (0,2,0) -- it outranks :root and
   the light-mode :root block regardless of the media query, so the
   light block below must re-declare the FULL set or a dark value
   leaks into light mode (the same rule vivid.css §1 documents). */
body[data-glass] {
  /* translucent fills. The blur + rim light are what read as "glass"
     here -- NOT see-through-ness -- so these sit high enough that text
     on the surface stays solid against anything behind it. The menus
     in particular overlay the gradient-clipped hero headline, so
     --gl-tint-2 is near-opaque (0.66 shipped first and the h1 bled
     straight through it -- fixed 2026-08-30). */
    --gl-tint:   rgba(28, 38, 68, 0.74);   /* cards / route cards */
  --gl-tint-2: rgba(30, 41, 70, 0.95);   /* floating menus -- must stay readable over the h1 */
  --gl-nav:    rgba(22, 31, 56, 0.72);    /* the sticky header */

  /* blur strengths -- heavy for surfaces that overlap scrolling
     content, moderate for in-flow cards so a long page stays cheap */
  --gl-blur:      18px;
  --gl-blur-card: 10px;
  --gl-blur-hero: 6px;
  --gl-sat:       1.6;

  /* rim light on the top edge */
  --gl-rim:    rgba(255, 255, 255, 0.10);
  --gl-rim-hi: rgba(255, 255, 255, 0.17);

  /* hue-tinted hairline: indigo-leaning, same idea as vivid's card edge */
  --gl-edge: color-mix(in oklab, var(--indigo, #7f8cf3) 30%, var(--border));

  /* depth: wide, soft, downward, near-black */
  --gl-depth:    0 20px 48px -24px rgba(0, 0, 0, 0.50);
  --gl-depth-hi: 0 30px 64px -26px rgba(0, 0, 0, 0.55);
  --gl-cast:     color-mix(in oklab, var(--violet) 22%, transparent);
}

/* Warm retint, 2026-09-02: --gl-edge and --gl-cast above already pick
   up vivid.css's new --violet/--indigo automatically via var(), but
   --gl-tint/--gl-tint-2/--gl-nav are literal rgba() values with no
   variable in them at all -- so without this, every frosted card,
   menu and header on the six warm-retinted pages would stay the OLD
   cool blue-black tint while the ground around them turned warm. This
   compound selector (0,2,1) outranks the plain body[data-glass] rule
   (0,1,1) above regardless of source order, so it only ever applies
   where both attributes are present -- the six vivid pages, exactly.
   --gl-rim/--gl-rim-hi are a neutral white highlight and read fine on
   either ground, so they are left alone. */
body[data-vivid][data-glass] {
  --gl-tint:   rgba(43, 31, 27, 0.74);   /* was rgba(18,26,44,.74) -- warm panel-2 tone */
  --gl-tint-2: rgba(34, 25, 23, 0.95);   /* was rgba(19,27,45,.95) -- warm panel tone */
  --gl-nav:    rgba(22, 16, 15, 0.72);   /* was rgba(9,13,23,.72) -- the new --bg itself */
}

@media (prefers-color-scheme: light) {
  body[data-glass] {
    --gl-tint:   rgba(255, 255, 255, 0.78);
    --gl-tint-2: rgba(244, 247, 252, 0.96);
    --gl-nav:    rgba(248, 250, 252, 0.80);

    --gl-blur:      16px;
    --gl-blur-card: 10px;
    --gl-blur-hero: 6px;
    --gl-sat:       1.4;

    --gl-rim:    rgba(255, 255, 255, 0.75);
    --gl-rim-hi: rgba(255, 255, 255, 0.92);

    --gl-edge: color-mix(in oklab, var(--indigo, #4a5ad0) 20%, var(--border));

    --gl-depth:    0 20px 44px -26px rgba(15, 23, 42, 0.18);
    --gl-depth-hi: 0 28px 58px -28px rgba(15, 23, 42, 0.22);
    --gl-cast:     color-mix(in oklab, var(--violet) 12%, transparent);
  }

  /* The warm retint above is (0,2,1), which outranks the (0,1,1) block just
     written, so on the vivid pages (ai, bitcoin, compare, logical-qubit, qec
     -- the whole Quantum track) it leaked warm-DARK glass into light mode:
     a near-black header slab and dark answer boxes carrying dark text.
     Live from 2026-09-02 to 2026-09-25, invisible because the pages were only
     ever looked at in dark. Re-declared here at a specificity that wins, and
     only when the reader has not explicitly chosen Dark or Dim (those two keep
     the warm tokens; :root[data-theme="light"] is handled in theme.css). */
  :root:not([data-theme="dark"]):not([data-theme="dim"]) body[data-vivid][data-glass] {
    --gl-tint:   rgba(255, 255, 255, 0.78);
    --gl-tint-2: rgba(244, 247, 252, 0.96);
    --gl-nav:    rgba(248, 250, 252, 0.80);
  }
}

/* The mirror image: a reader on a LIGHT operating system who picks Dark or Dim
   in the theme control. theme.css re-declares glass only for Light, so on
   those readers the @media (prefers-color-scheme: light) block above still
   matched and painted a pale header slab over a dark page. These win on
   specificity (0,3,1) / (0,4,1), whatever the OS says. */
:root[data-theme="dark"] body[data-glass],
:root[data-theme="dim"] body[data-glass] {
  --gl-tint:   rgba(28, 38, 68, 0.74);
  --gl-tint-2: rgba(30, 41, 70, 0.95);
  --gl-nav:    rgba(22, 31, 56, 0.72);
  --gl-blur:      18px;
  --gl-blur-card: 10px;
  --gl-blur-hero: 6px;
  --gl-sat:       1.6;
  --gl-rim:    rgba(255, 255, 255, 0.10);
  --gl-rim-hi: rgba(255, 255, 255, 0.17);
  --gl-edge: color-mix(in oklab, var(--indigo, #7f8cf3) 30%, var(--border));
  --gl-depth:    0 20px 48px -24px rgba(0, 0, 0, 0.50);
  --gl-depth-hi: 0 30px 64px -26px rgba(0, 0, 0, 0.55);
  --gl-cast:     color-mix(in oklab, var(--violet) 22%, transparent);
}
:root[data-theme="dark"] body[data-vivid][data-glass],
:root[data-theme="dim"] body[data-vivid][data-glass] {
  --gl-tint:   rgba(43, 31, 27, 0.74);
  --gl-tint-2: rgba(34, 25, 23, 0.95);
  --gl-nav:    rgba(22, 16, 15, 0.72);
}

/* ---------------------------------------------------------------- §2
   THE STICKY HEADER

   style.css already frosts the header at >=720px (blur(10px) over a
   0.82-alpha fill). This deepens it: more blur, a saturate bump so
   the colour behind the glass stays alive, a lower-alpha fill, a
   1px top rim light and a soft cast underneath so the bar reads as
   a floating shelf rather than a painted stripe. Same media query
   as style.css -- below 720px the header is a static bar and gets
   nothing. */
@media (min-width: 720px) {
  body[data-glass] nav:not(.rung-rail) {
    background: var(--nav-bg);   /* opaque-ish fallback from style.css */
    background: var(--gl-nav);
    box-shadow:
      inset 0 -1px 0 color-mix(in oklab, var(--border) 60%, transparent),
      0 10px 30px -18px rgba(0, 0, 0, 0.5);
  }

  @supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
    body[data-glass] nav:not(.rung-rail) {
      -webkit-backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
      backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
    }
  }

  /* the rim: a 1px gradient hairline along the very top edge. nav has
     position:relative (its own rule, the .navpanel anchor), so an
     absolute pseudo is anchored correctly. pointer-events:none and
     1px tall -- it sits over nothing interactive. nav has no other
     ::after (the link underline pseudo is `nav a.link::after`). */
  body[data-glass] nav:not(.rung-rail)::after {
    content: "";
    position: absolute; left: 0; right: 0; top: 0; height: 1px;
    background: linear-gradient(90deg, transparent, var(--gl-rim-hi), transparent);
    pointer-events: none;
  }
}

/* ---------------------------------------------------------------- §3
   THE FLOATING MENUS

   The category dropdown, the account popover, and the mobile nav
   sheet. These genuinely overlap scrolling content, so glass sells
   hardest here: heavy blur, the more-opaque --gl-tint-2 (a menu must
   stay readable over anything), rim + depth.

   Scope care: at <=719px style.css collapses the nested `.navpanel`
   to a bare list (background:none, no border) inside the `#navcats`
   accordion. So the desktop `.navpanel` glass is gated to >=720px,
   and the mobile sheet glass rides on `#navcats` itself, which is
   the element that is actually a floating panel at that width. The
   account popover stays a floating popover at every width (its own
   two-class rule in style.css), so it can take glass unconditionally. */
@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {

  @media (min-width: 720px) {
    body[data-glass] .navpanel {
      background: var(--panel);   /* fallback */
      background: var(--gl-tint-2);
      border-color: var(--gl-edge);
      -webkit-backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
      backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
      box-shadow:
        inset 0 1px 0 var(--gl-rim),
        0 22px 52px -22px rgba(0, 0, 0, 0.55);
    }
  }

  @media (max-width: 719px) {
    body[data-glass] #navcats {
      background: var(--panel);   /* fallback */
      background: var(--gl-tint-2);
      border-color: var(--gl-edge);
      -webkit-backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
      backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
      box-shadow:
        inset 0 1px 0 var(--gl-rim),
        0 22px 52px -22px rgba(0, 0, 0, 0.55);
    }
  }

  body[data-glass] .sq-account .navpanel {
    background: var(--panel);   /* fallback */
    background: var(--gl-tint-2);
    border-color: var(--gl-edge);
    -webkit-backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
    backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
    box-shadow:
      inset 0 1px 0 var(--gl-rim),
      0 22px 52px -22px rgba(0, 0, 0, 0.55);
  }
}

/* ---------------------------------------------------------------- §4
   CARDS & ROUTE CARDS

   vivid.css §4 already gave these a violet top-wash, an indigo edge
   and a tinted shadow. Glass keeps that language and swaps the solid
   --panel base for a translucent one that frosts the field behind
   it. The opaque baseline (rim + depth + edge, no blur) is declared
   first so a browser without backdrop-filter still gets the lift;
   the translucent fill is the enhancement.

   The hover state keeps style.css's -3px lift untouched -- glass only
   brightens the rim and deepens the cast, it adds no travel. */
body[data-glass] .card,
body[data-glass] .introute-card {
  border-color: var(--gl-edge);
  box-shadow:
    inset 0 1px 0 var(--gl-rim),
    var(--gl-depth);
}
body[data-glass] .card:hover,
body[data-glass] .introute-card:hover {
  box-shadow:
    inset 0 1px 0 var(--gl-rim-hi),
    var(--gl-depth-hi),
    0 0 26px -6px color-mix(in oklab, var(--violet) 22%, transparent);
}

@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  body[data-glass] .card,
  body[data-glass] .introute-card {
    background: var(--panel);   /* fallback */
    background: linear-gradient(180deg,
      color-mix(in oklab, var(--violet) 7%, var(--gl-tint)),
      var(--gl-tint) 46%);
    -webkit-backdrop-filter: blur(var(--gl-blur-card)) saturate(var(--gl-sat));
    backdrop-filter: blur(var(--gl-blur-card)) saturate(var(--gl-sat));
  }
}

/* ---------------------------------------------------------------- §5
   THE SECTION HEROES & THE ARCADE DOOR

   .orhero already carries a two-corner aurora from vivid.css; its
   background is load-bearing and is left completely alone. Glass
   adds only the frame: a brighter top rim, a violet-cast bloom
   under it, the standard depth, and a light 6px backdrop blur that
   softens the ambient field showing through the rounded corners.

   .gate (the arcade door) gets rim + depth only -- its gradient
   fill and hover bloom are already doing the work. */
body[data-glass] .orhero {
  border-color: var(--gl-edge);
  box-shadow:
    inset 0 1px 0 var(--gl-rim-hi),
    0 30px 70px -40px var(--gl-cast),
    var(--gl-depth);
}
body[data-glass] .gate {
  box-shadow:
    inset 0 1px 0 var(--gl-rim),
    var(--gl-depth);
}

@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  body[data-glass] .orhero {
    -webkit-backdrop-filter: blur(var(--gl-blur-hero)) saturate(1.3);
    backdrop-filter: blur(var(--gl-blur-hero)) saturate(1.3);
  }
}

/* ---------------------------------------------------------------- §6
   THE HUD CARDS

   hud.css's return card (teal left rule) and Contract-of-the-Day
   card (violet left rule) each have a meaningful left-accent and a
   faint directional wash that both stay. Glass adds a frost behind
   the wash, the rim, and the depth so the two lines read as small
   glass tickets clipped to the top of the page. The left-border
   identity is untouched. */
body[data-glass] .hud-return,
body[data-glass] .hud-contract {
  box-shadow:
    inset 0 1px 0 var(--gl-rim),
    var(--gl-depth);
}

@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  body[data-glass] .hud-return,
  body[data-glass] .hud-contract {
    -webkit-backdrop-filter: blur(8px) saturate(var(--gl-sat));
    backdrop-filter: blur(8px) saturate(var(--gl-sat));
  }
}

/* ---------------------------------------------------------------- §7
   REDUCED TRANSPARENCY -- explicit opt-out

   Anyone who has asked their OS to reduce transparency gets every
   frosted surface back as a solid panel. Unknown-media-feature
   UAs evaluate this block as false and simply keep the glass, which
   is the safe direction. Mirrors the belt-and-braces reduced-motion
   block vivid.css §7 keeps. */
@media (prefers-reduced-transparency: reduce) {
  body[data-glass] nav:not(.rung-rail),
  body[data-glass] .navpanel,
  body[data-glass] #navcats,
  body[data-glass] .sq-account .navpanel,
  body[data-glass] .card,
  body[data-glass] .introute-card,
  body[data-glass] .orhero,
  body[data-glass] .hud-return,
  body[data-glass] .hud-contract {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  body[data-glass] nav:not(.rung-rail) { background: var(--nav-bg); }
  body[data-glass] .navpanel,
  body[data-glass] #navcats,
  body[data-glass] .sq-account .navpanel { background: var(--panel); }
  body[data-glass] .card,
  body[data-glass] .introute-card { background: var(--panel); }
}
/* --- dynamics.css --- */
/* ================================================================
   DYNAMICS LAYER (2026-08-30)

   Rides the same gate as glass.css -- `body[data-glass]`, all 24
   pages as of 2026-08-30 -- and pairs with `dynamics.js`. Where
   glass.css is the MATERIAL (frosted panes, rim light, depth), this
   is the RESPONSE: the premium-feel motion the pages were missing.

   It does NOT add ambient motion. The site's motion law stands
   (DESIGN.md: "motion is feedback plus one ambient layer; nothing
   bounces"), and the existing system already carries the ambient
   half -- atmosphere.js's lattice, motion.css's pointer sheen,
   nav.js's scroll reveals, alive.js's animated unfold. What was
   still missing:

     1. the category dropdowns blinked open instead of arriving,
     2. the glass never caught the light -- it sat inert,
     3. interactive surfaces transitioned some properties and
        snapped others, so state changes read as half-finished.

   All three are FEEDBACK -- they fire only from a pointer, a click,
   or a scroll the visitor performed. Every block is inside
   `@media (prefers-reduced-motion: no-preference)` or degrades to a
   transition a reduced-motion UA runs at 1ms.

   THE PROPERTY RULE (inherited from motion.css, non-negotiable):
   never animate `transform`. style.css uses it for the hover lifts
   (.card:hover). `translate` and `scale` are independent properties
   that compose with it, so this file uses those and leaves
   `transform` alone. A `fill: both` animation on `transform` would
   silently kill every hover lift on the page.

   Durations and easings come from motion.css §1 (`--dur-fast` /
   `--dur-base`, `--ease-out-quint`). Nothing here invents its own.
   ================================================================ */

/* ---------------------------------------------------------------- §1
   THE MENUS ARRIVE

   `<details class="navcat">` toggles `[open]` in one frame and the
   UA flips `.navpanel` from `display:none` to shown -- a hard cut.
   `@starting-style` gives the first rendered frame a from-state, so
   the panel fades and drops the last 6px into place instead. It is
   the same trick motion.css §2 calls "the highest ratio of
   perceived quality to lines of code": a browser without
   `@starting-style` (pre-Chrome 117 / Safari 17.5 / Firefox 129)
   simply shows the menu instantly, exactly as it does today. No JS,
   nothing to detect.

   Enter only. On close the UA pulls `display:none` immediately;
   animating an exit would need `transition-behavior: allow-discrete`
   and buys nothing on a menu -- a dismissed menu should just be
   gone. */
@media (prefers-reduced-motion: no-preference) {
  body[data-glass] .navcat > .navpanel,
  body[data-glass] .sq-account .navpanel,
  body[data-glass] #navcats {
    transition:
      opacity var(--dur-fast, 140ms) var(--ease-out-quint, cubic-bezier(0.22, 1, 0.36, 1)),
      translate var(--dur-fast, 140ms) var(--ease-out-quint, cubic-bezier(0.22, 1, 0.36, 1));
  }
  body[data-glass] .navcat[open] > .navpanel,
  body[data-glass] .sq-account.navcat[open] .navpanel {
    opacity: 1;
    translate: 0 0;
  }
  @starting-style {
    body[data-glass] .navcat[open] > .navpanel,
    body[data-glass] .sq-account.navcat[open] .navpanel {
      opacity: 0;
      translate: 0 -6px;
    }
  }
  /* The mobile sheet (`#navcats.open` at <=719px) gets the same
     arrival. `.open` is a class, not `[open]`, so it needs its own
     from-state. */
  body[data-glass] #navcats.open { opacity: 1; translate: 0 0; }
  @starting-style {
    body[data-glass] #navcats.open { opacity: 0; translate: 0 -6px; }
  }
}

/* ---------------------------------------------------------------- §2
   THE GLASS CATCHES THE LIGHT

   dynamics.js writes `--dyn-x` / `--dyn-y` (viewport %) while the
   pointer is over the header, and clears them on leave. A single
   soft white radial -- 6% peak alpha, gone by 60% radius -- rides
   under the nav's content and above its frosted fill, so moving the
   cursor across the bar lights the glass the way a real pane picks
   up a light source. It is a pseudo-element on the existing
   `nav::after` sibling slot... except glass.css already used
   `::after` for the top rim, so this is `::before`.

   At >=720px style.css already gives nav `position: sticky` AND
   `z-index: 40`, so the header is its own stacking context: the
   pseudo at `z-index: 0`, the real nav children lifted to
   `z-index: 1`, and the `.navpanel` dropdown at its existing
   `z-index: 45` all resolve inside nav, in that order -- glint at
   the bottom, dropdown on top. No `isolation` needed and none
   added (nav's stacking history is delicate). `pointer-events:
   none`; opacity eases in/out so first paint and pointer-leave
   don't pop. */
@media (prefers-reduced-motion: no-preference) and (min-width: 720px) {
  body[data-glass] nav:not(.rung-rail)::before {
    content: "";
    position: absolute; inset: 0;
    z-index: 0;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--dur-base, 260ms) var(--ease-out-quint, ease-out);
    background: radial-gradient(
      340px circle at var(--dyn-x, 50%) var(--dyn-y, -20%),
      rgba(255, 255, 255, 0.06),
      transparent 60%
    );
  }
  body[data-glass] nav:not(.rung-rail):hover::before { opacity: 1; }
  /* keep the real nav content above the glint */
  body[data-glass] nav:not(.rung-rail) > * { position: relative; z-index: 1; }
}

@media (prefers-color-scheme: light) {
  body[data-glass] nav:not(.rung-rail)::before {
    background: radial-gradient(
      340px circle at var(--dyn-x, 50%) var(--dyn-y, -20%),
      rgba(255, 255, 255, 0.5),
      transparent 60%
    );
  }
}

/* ---------------------------------------------------------------- §3
   STATE CHANGES FINISH

   The site's controls transition the properties each rule happened
   to think of -- `.preset` does border+color+transform, `.navchip`
   does border only, `.bigplay` none. The result is a state change
   where some of the box moves and the rest jumps. This gives the
   interactive set on the glass pages one consistent, short echo:
   everything eases, and a press dips the control 1.5% via `scale`
   (its own property -- composes with the hover `transform`, never
   replaces it).

   `--dur-fast` is motion.css's "state echo: press, toggle, focus"
   token. Reduced motion drops the dip and keeps instant. */
@media (prefers-reduced-motion: no-preference) {
  body[data-glass] :is(
    .bigplay, .cta, .herocta, .herocta-alt, .orjump,
    .preset, .navchip, #navtrig, .navmain, .tier, button
  ) {
    transition:
      background-color var(--dur-fast, 140ms) var(--ease-out-quint, ease-out),
      border-color var(--dur-fast, 140ms) var(--ease-out-quint, ease-out),
      color var(--dur-fast, 140ms) var(--ease-out-quint, ease-out),
      box-shadow var(--dur-base, 260ms) var(--ease-out-quint, ease-out),
      scale var(--dur-fast, 140ms) var(--ease-out-quint, ease-out);
  }
  body[data-glass] :is(
    .bigplay, .cta, .herocta, .herocta-alt, .orjump,
    .preset, .navchip, #navtrig, button
  ):active {
    scale: 0.985;
  }
}

/* ---------------------------------------------------------------- §4
   THE HEADER SETTLES ONTO THE PAGE

   dynamics.js sets `data-scrolled` on <html> once the page has
   moved past ~40px. The frosted header then deepens its cast and
   tightens its blur a touch -- it reads as lifting off the content
   it now covers. box-shadow + backdrop-filter only: no padding, no
   size, nothing that touches the nav's width budget or its role as
   the .navpanel containing block. Eased both ways. */
@media (min-width: 720px) {
  body[data-glass] nav:not(.rung-rail) {
    transition:
      box-shadow var(--dur-base, 260ms) var(--ease-out-quint, ease-out),
      background-color var(--dur-base, 260ms) var(--ease-out-quint, ease-out);
  }
  html[data-scrolled] body[data-glass] nav:not(.rung-rail) {
    box-shadow:
      inset 0 -1px 0 color-mix(in oklab, var(--border) 70%, transparent),
      0 18px 44px -22px rgba(0, 0, 0, 0.6);
  }
  @supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
    html[data-scrolled] body[data-glass] nav:not(.rung-rail) {
      -webkit-backdrop-filter: blur(24px) saturate(1.7);
      backdrop-filter: blur(24px) saturate(1.7);
    }
  }
}

/* ---------------------------------------------------------------- §6
   THE SMALL STUFF

   A handful of one-line responses that cost nothing and are the
   difference between "a website" and "a made thing". Each is
   feedback (hover / open / select), each eases on a motion.css
   token, none of them move layout.

   6a. THE CHEVRON TURNS. `.navcat > summary::after` is a "▾"; it
       flips to point up when the menu is open. A `transition` on a
       pseudo-element's `transform` -- not a `fill:both` animation --
       so the motion.css "never animate transform" rule (which is
       about animations stealing the hover-lift cascade) does not
       apply.
   6b. THE NAV UNDERLINE WIPES IN. Today a nav link's underline is a
       2px border that snaps between transparent and teal. This lays
       a scaleX(0->1) bar under it that grows from the left on hover
       and for the active link. `nav a.link` is already
       position:relative; `::after` is taken by style.css (the 44px
       touch band) so this is `::before`. The existing border is
       left alone -- on the active link the two 2px lines simply
       coincide.
   6c. MENU ITEMS LEAN IN. `.navmain` shifts 3px right on hover
       (`translate`, its own property). The row highlight it already
       has now has somewhere to go.
   6d. SELECTION IS ON-BRAND. Dragging to select text picks up a
       teal wash instead of the OS blue.
   6e. THE SCROLLBAR BELONGS. Thin, violet, transparent track --
       on the page scroller (via :has, so the rule still only fires
       on a glass page) and every inner scroll area. */
@media (prefers-reduced-motion: no-preference) {
  /* 6a */
  body[data-glass] .navcat > summary::after {
    display: inline-block;
    transition: transform var(--dur-fast, 140ms) var(--ease-out-quint, ease-out),
                opacity var(--dur-fast, 140ms) var(--ease-out-quint, ease-out);
  }
  body[data-glass] .navcat[open] > summary::after {
    transform: rotate(180deg);
    opacity: 0.9;
  }

  /* 6b */
  body[data-glass] nav a.link::before {
    content: "";
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 2px;
    background: var(--teal);
    border-radius: 2px;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--dur-base, 260ms) var(--ease-out-quint, ease-out);
  }
  body[data-glass] nav a.link:hover::before,
  body[data-glass] nav a.link:focus-visible::before,
  body[data-glass] nav a.link.active::before {
    transform: scaleX(1);
  }
  /* the wipe bar IS the underline now -- drop style.css's static
     border on the active link so the two 2px lines don't stack */
  body[data-glass] nav a.link.active { border-bottom-color: transparent; }

  /* 6c */
  body[data-glass] .navmain {
    transition: background var(--dur-fast, 140ms) var(--ease-out-quint, ease-out),
                translate var(--dur-fast, 140ms) var(--ease-out-quint, ease-out);
  }
  body[data-glass] .navmain:hover { translate: 3px 0; }
}

/* 6d -- not motion-gated, it is a colour */
body[data-glass] ::selection {
  background: color-mix(in oklab, var(--teal) 30%, transparent);
}

/* 6e -- not motion-gated, it is chrome */
html:has(body[data-glass]) {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklab, var(--violet) 42%, transparent) transparent;
}
body[data-glass] ::-webkit-scrollbar { width: 10px; height: 10px; }
body[data-glass] ::-webkit-scrollbar-track { background: transparent; }
body[data-glass] ::-webkit-scrollbar-thumb {
  background: color-mix(in oklab, var(--violet) 34%, transparent);
  border-radius: 99px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
body[data-glass] ::-webkit-scrollbar-thumb:hover {
  background: color-mix(in oklab, var(--teal) 42%, transparent);
  background-clip: padding-box;
}

/* ---------------------------------------------------------------- §7
   REDUCED MOTION -- belt and braces

   §1-§3, §6 are already inside no-preference queries; this kills the
   things that would still move on a UA that ignores the query. */
@media (prefers-reduced-motion: reduce) {
  body[data-glass] .navcat > .navpanel,
  body[data-glass] .sq-account .navpanel,
  body[data-glass] #navcats,
  body[data-glass] nav:not(.rung-rail),
  body[data-glass] nav:not(.rung-rail)::before,
  body[data-glass] nav a.link::before,
  body[data-glass] .navcat > summary::after,
  body[data-glass] .navmain {
    transition: none;
    translate: none;
  }
}

/* ==========================================================================
   DEPTH (2026-10-05): a quiet 3D layer for every page. A perspective grid floor and three slow rings sit behind the content
   and drift with the pointer and the scroll; cards tilt toward the pointer. Nothing animates by itself (only pointer and
   scroll events move it), it is off for reduced motion and for the footer switch "Reduce 3D depth", and it never touches
   layout (fixed layer behind everything, transforms only). dynamics.js builds it; this is its look.
   ========================================================================== */
.sq-depth-bg { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; perspective: 900px; perspective-origin: 50% 30%; opacity: .55; }
html:not([data-depth]) .sq-depth-bg { display: none; }
.sq-depth-bg i { position: absolute; display: block; }
.sq-depth-bg .floor { left: -60%; right: -60%; bottom: -6%; height: 62%; transform-origin: 50% 100%; transform: rotateX(70deg) translate3d(var(--dx, 0px), 0, 0);
  background-image: linear-gradient(color-mix(in srgb, var(--teal, #2dd4bf) 34%, transparent) 1px, transparent 1px), linear-gradient(90deg, color-mix(in srgb, var(--violet, #8b5cf6) 30%, transparent) 1px, transparent 1px);
  background-size: 64px 64px; background-position: 0 var(--sy, 0px); -webkit-mask-image: linear-gradient(to top, #000 4%, transparent 78%); mask-image: linear-gradient(to top, #000 4%, transparent 78%); }
.sq-depth-bg .ring { border-radius: 50%; border: 1.5px solid color-mix(in srgb, var(--teal, #2dd4bf) 42%, transparent); box-shadow: 0 0 40px color-mix(in srgb, var(--violet, #8b5cf6) 22%, transparent), inset 0 0 30px color-mix(in srgb, var(--teal, #2dd4bf) 12%, transparent);
  transform: translate3d(var(--rx, 0px), var(--ry, 0px), 0) rotateX(var(--ra, 62deg)) rotateY(var(--rb, 0deg)); }
.sq-depth-bg .r1 { width: 460px; height: 460px; right: -140px; top: 6%; }
.sq-depth-bg .r2 { width: 280px; height: 280px; left: -90px; top: 48%; border-color: color-mix(in srgb, var(--violet, #8b5cf6) 44%, transparent); }
.sq-depth-bg .r3 { width: 190px; height: 190px; right: 14%; bottom: 10%; border-color: color-mix(in srgb, #fbbf24 38%, transparent); }
@media (prefers-color-scheme: light) { html:not([data-theme]) .sq-depth-bg { opacity: .5; } }
html[data-theme="light"] .sq-depth-bg { opacity: .5; }
@media (max-width: 700px) { .sq-depth-bg .r1 { width: 300px; height: 300px; right: -120px; } .sq-depth-bg .r2 { display: none; } }
.sq-tilt { transition: transform .45s cubic-bezier(.2, .8, .2, 1), box-shadow .35s ease; }
html[data-depth] .sq-tilt.is-tilting { transition: transform .08s linear, box-shadow .2s ease; transform: perspective(820px) rotateX(var(--tx, 0deg)) rotateY(var(--ty, 0deg)) translateY(-3px) translateZ(8px); box-shadow: 0 22px 44px -18px rgba(0, 0, 0, .5), 0 0 0 1px color-mix(in srgb, var(--teal, #2dd4bf) 30%, transparent); position: relative; z-index: 2; }
@media (prefers-reduced-motion: reduce) { .sq-depth-bg { display: none; } .sq-tilt { transition: none; } }
/* --- formalism.css --- */
/* ===================================================================
   THE MACHINERY (formalism.html) — quantum's own visual identity.

   Structurally a sibling of feasible.css (same masthead / numbered-module
   / four-layer shape — see the desk design notes for
   why that shape was reused rather than invented twice), but NOT the same
   skin. feasible.css's accent is deliberately amber — "classical, proven,
   shipping for 80 years" — and its background is graph paper, both signatures
   of Operations Research. This page is quantum content, so it gets SymbiQ's
   own accent (violet, the colour used everywhere else quantum on this site)
   and the site's own dot-lattice background, not OR's — loading feasible.css
   directly on this page was the bug that made it look like a reskinned OR
   page instead of a SymbiQ one.

   Loaded only by formalism.html, so nothing here disturbs the other pages.
   =================================================================== */

/* ---- masthead -------------------------------------------------------- */
.fr-mast { margin: 6px 0 30px; padding-bottom: 26px; border-bottom: 1px solid var(--border); }
.fr-kicker {
  display: inline-flex; align-items: center; gap: 9px;
  font-size: .7rem; letter-spacing: .2em; text-transform: uppercase; font-weight: 700;
  color: var(--violet); border: 1px solid rgba(167,139,250,.34); border-radius: 99px;
  padding: 5px 13px; margin-bottom: 18px;
}
.fr-kicker::before { content: '◆'; font-size: .8rem; }
body.fr h1 {
  font-size: clamp(2.1rem, 6vw, 3.5rem); line-height: 1.02; letter-spacing: -.028em;
  background: linear-gradient(100deg, var(--text) 22%, var(--violet) 62%, var(--teal) 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  margin: 0 0 16px;
}
.fr-lede { font-size: 1.12rem; max-width: 62ch; color: var(--text); margin: 0 0 6px; }
.fr-sub  { color: var(--muted); max-width: 66ch; margin: 10px 0 0; }
.fr-stats { display: flex; flex-wrap: wrap; gap: 10px 30px; margin-top: 24px; }
.fr-stats div { min-width: 92px; }
.fr-stats b { display: block; font-size: 1.5rem; font-weight: 800; color: var(--violet); font-variant-numeric: tabular-nums; }
.fr-stats span { font-size: .78rem; color: var(--muted); }

/* ---- section headings, numbered like a manual ------------------------ */
body.fr h2 { letter-spacing: -.015em; }
body.fr h2::before { background: var(--violet) !important; }
.fr-secnum {
  display: block; font-size: .7rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--violet); font-weight: 700; margin-bottom: 6px;
}

/* ---- the contents rail ----------------------------------------------- */
.fr-toc {
  border: 1px solid var(--border); border-left: 3px solid var(--violet);
  border-radius: 12px; padding: 18px 20px; margin: 24px 0 34px;
  background: linear-gradient(135deg, rgba(167,139,250,.13), transparent 72%);
}
.fr-toc h3 { margin: 0 0 4px; font-size: .95rem; }
.fr-toc p { margin: 0 0 14px; font-size: .86rem; color: var(--muted); }
.fr-toc ol { list-style: none; counter-reset: t; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 2px 18px; }
.fr-toc li { counter-increment: t; }
/* Number left, title right, subtitle UNDER the title -- the same three-part
   shape `.mod > summary` already uses below (mod-n / mod-t / mod-q), so the
   contents rail and the modules it points at read as one component.

   This was a flex row until 2026-08-28, with the subtitle pushed right by
   `margin-left:auto; flex: 0 0 auto`. In a 232px-minimum grid column there is
   no room to push anything: the subtitle sat at its full max-content width and
   OVERLAPPED the next column's number and title on desktop, while on mobile it
   pushed the whole document to 388px -- a horizontal scrollbar at 375px, found
   by tools/smoke.mjs. Stacking removes the competition for the same row. */
.fr-toc li a {
  display: grid; grid-template-columns: auto 1fr; gap: 1px 10px;
  align-items: baseline; padding: 7px 8px; border-radius: 8px;
  text-decoration: none; color: var(--text); font-size: .92rem;
  min-height: 44px;
}
.fr-toc li a::before {
  content: counter(t, decimal-leading-zero); color: var(--violet); font-weight: 700;
  font-size: .75rem; font-variant-numeric: tabular-nums; grid-row: span 2;
}
.fr-toc li a:hover { background: var(--rowhover); }
.fr-toc li a em { grid-column: 2; font-style: normal; color: var(--muted); font-size: .8rem; min-width: 0; }

/* ---- a curriculum module --------------------------------------------- */
.mod {
  border: 1px solid var(--border); border-radius: 14px; margin: 0 0 12px;
  background: var(--panel); overflow: hidden;
  transition: border-color .2s cubic-bezier(.22,.7,.3,1), box-shadow .2s ease;
}
.mod[open] { border-color: rgba(167,139,250,.34); box-shadow: 0 12px 34px -22px rgba(0,0,0,.5); }
.mod > summary {
  list-style: none; cursor: pointer; padding: 17px 20px;
  display: grid; grid-template-columns: auto 1fr auto; gap: 4px 14px; align-items: center;
}
.mod > summary::-webkit-details-marker { display: none; }
.mod > summary:hover { background: var(--rowhover); }
.mod-n {
  grid-row: span 2; font-size: 1.05rem; font-weight: 800; color: var(--violet);
  font-variant-numeric: tabular-nums; opacity: .85;
}
.mod-t { font-weight: 700; font-size: 1.06rem; }
.mod-q { grid-column: 2; font-size: .87rem; color: var(--muted); }
.mod-x {
  grid-row: span 2; color: var(--muted); font-size: 1.2rem; line-height: 1;
  transition: transform .22s cubic-bezier(.22,.7,.3,1);
}
.mod[open] .mod-x { transform: rotate(45deg); color: var(--violet); }
.mod-body {
  padding: 4px 20px 22px; border-top: 1px solid var(--border);
  /* A closed module hides this box (native <details>), so this is inert until
     "open all seventeen" -- and there it earns its keep: the browser paints
     the module bodies (and their SVG figures) actually on screen instead of
     all 17 at once. Deliberately NOT on .mod itself: a closed module's height
     must stay exact, or the deep-link scroll into #f16 lands hundreds of px
     short. contain-intrinsic-size is a rough open-body height; `auto` pins the
     real size once seen. Every byte stays in the DOM -- crawlable, Ctrl-F
     finds it, the a11y tree keeps it. */
  content-visibility: auto;
  contain-intrinsic-size: auto 3000px;
}

/* the four layers, always in this order */
.layer { margin: 20px 0 0; }
.layer:first-child { margin-top: 18px; }
.layer > h4 {
  display: flex; align-items: center; gap: 9px; margin: 0 0 9px;
  font-size: .71rem; letter-spacing: .17em; text-transform: uppercase; font-weight: 700;
  color: var(--muted);
}
.layer > h4::before {
  content: ''; width: 16px; height: 2px; border-radius: 2px; background: var(--violet); flex: 0 0 auto;
}
.layer.see > h4    { color: var(--violet); }
.layer.maths > h4  { color: var(--teal); }
.layer.maths > h4::before { background: var(--teal); }
.layer.real > h4   { color: var(--text); }
.layer.real > h4::before  { background: var(--text); }
.layer p:last-child { margin-bottom: 0; }
.layer.maths {
  background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px;
}
.layer.maths .eq {
  display: block; margin: 12px 0; padding: 12px 14px; overflow-x: auto;
  background: var(--panel); border-left: 3px solid var(--teal); border-radius: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88rem; line-height: 1.7;
}
.layer.real {
  border-left: 3px solid var(--violet); padding-left: 15px;
}
.layer.real .where {
  display: inline-block; font-size: .72rem; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 700; color: var(--violet); margin-bottom: 6px;
}

/* module figures — inline SVG, per the house rule, never JS */
.modfig { margin: 0; }
.modfig svg { display: block; width: 100%; height: auto; overflow: visible; }
.modfig figcaption { font-size: .85rem; color: var(--muted); margin-top: 10px; }
.modfig figcaption b { color: var(--text); }
.modscroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.modscroll > svg { min-width: 430px; }
.fr .lbl   { fill: var(--muted); font-weight: 600; font-size: 11.5px; }
.fr .lbl-s { fill: var(--muted); font-weight: 500; font-size: 10px; }
.fr .lbl-b { fill: var(--text);  font-weight: 700; font-size: 13px; }
.fr .or    { fill: var(--violet); }
.fr .tl    { fill: var(--teal); }
.fr .vi    { fill: var(--violet); }
.fr .sOr   { stroke: var(--violet); fill: none; }
.fr .sTl   { stroke: var(--teal);  fill: none; }
.fr .sMut  { stroke: var(--border); fill: none; }

/* footer strip of a module: the bridges out */
.mod-out { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--border); }
.mod-out a {
  display: inline-flex; align-items: center; gap: 7px; text-decoration: none;
  font-size: .84rem; font-weight: 600; padding: 8px 13px; border-radius: 9px;
  border: 1px solid var(--border); color: var(--text);
  transition: border-color .18s cubic-bezier(.22,.7,.3,1), transform .18s cubic-bezier(.22,.7,.3,1);
}
.mod-out a:hover { transform: translateY(-2px); }
.mod-out a.play  { border-color: rgba(167,139,250,.34); color: var(--violet); }
.mod-out a.play:hover  { border-color: var(--violet); }
.mod-out a.q     { border-color: rgba(45,212,191,.45); color: var(--teal); }
.mod-out a.q:hover     { border-color: var(--teal); }

/* expand/collapse everything */
.fr-allctl { display: flex; gap: 10px; align-items: center; margin: 0 0 16px; flex-wrap: wrap; }
.fr-allctl button {
  background: none; border: 1px solid var(--border); border-radius: 8px; color: var(--muted);
  font: inherit; font-size: .82rem; padding: 6px 12px; cursor: pointer;
  transition: border-color .18s ease, color .18s ease;
  min-height: 44px;
}
.fr-allctl button:hover { border-color: var(--violet); color: var(--violet); }
.fr-allctl .count { font-size: .82rem; color: var(--muted); margin-left: auto; }

@media (max-width: 719px) {
  .fr-mast { margin-bottom: 22px; }
  .fr-stats { gap: 10px 20px; }
  .fr-stats b { font-size: 1.25rem; }
  .mod > summary { padding: 14px 15px; gap: 3px 11px; }
  .mod-body { padding: 4px 15px 18px; }
  .layer.maths { padding: 12px 13px; }
}
@media (prefers-reduced-motion: reduce) {
  .mod, .mod-x, .mod-out a { transition: none; }
}


/* ---- topic 13: the peaks explorer (shor-peaks.js) ----------------------------------------------------------- */
.sp { margin: 10px 0 6px; }
.sp-ctl, .sp-ctl2, .sp-run { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin: 0 0 10px; }
.sp-ctl label, .sp-ctl2 label { display: inline-flex; align-items: center; gap: 8px; font-size: .9rem; }
.sp select { min-height: 44px; padding: 4px 10px; border-radius: 10px; border: 1px solid var(--border); background: var(--panel); color: var(--text); font: inherit; }
.sp-info { font-size: .84rem; color: var(--muted); }
.sp-grid { display: grid; gap: 12px; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); margin: 0 0 8px; }
.sp-fig { margin: 0; }
.sp-fig canvas { display: block; width: 100%; height: 190px; border: 1px solid var(--border); border-radius: 12px; background: var(--panel); }
.sp-fig figcaption { font-size: .8rem; color: var(--muted); margin-top: 4px; }
.sp-kl input[type=range] { width: clamp(140px, 30vw, 300px); min-height: 44px; accent-color: var(--teal); }
.sp-zl input { width: 20px; height: 20px; accent-color: var(--teal); }
.sp-zl { min-height: 44px; }
.sp-read { margin: 6px 0; }
.sp-tally { margin: 4px 0 8px; font-size: .92rem; }
.sp-note { font-size: .85rem; color: var(--muted); margin: 8px 0 0; }
.sp .preset[aria-pressed="true"] { border-color: var(--teal); background: rgba(45, 212, 191, .16); }
@media (max-width: 640px) { .sp-grid { grid-template-columns: minmax(0, 1fr); } }
/* --- crypto-tools.css --- */
/* crypto-tools.css -- the four "Try it" toys of the Security course (site/crypto-tools.js). */
.ct { display: grid; gap: 10px; max-width: 760px; margin: 8px 0 4px; }
.ct-row { display: grid; gap: 4px; }
.ct-row label { font-size: .84rem; color: var(--muted); }
.ct input[type=text], .ct input[type=number], .ct select { min-height: 44px; padding: 0 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--panel); color: inherit; font: inherit; width: 100%; box-sizing: border-box; }
.ct-out, .ct-big, .ct-note { margin: 0; font-size: .9rem; line-height: 1.55; overflow-wrap: anywhere; }
.ct-big { font-size: 1rem; }
.ct-note { color: var(--muted); font-size: .84rem; }
.ct-out code { font-size: .74rem; word-break: break-all; }
.ct-act { display: flex; flex-wrap: wrap; gap: 8px; }
.ct-act .preset { min-height: 44px; }
.ct-strip { display: grid; grid-template-columns: repeat(64, 1fr); gap: 1px; max-width: 640px; }
.ct-strip i { display: block; aspect-ratio: 1; background: color-mix(in srgb, var(--muted) 22%, transparent); border-radius: 1px; }
.ct-strip i.on { background: var(--violet); }
.ct-tab { width: 100%; border-collapse: collapse; font-size: .82rem; }
.ct-tab th, .ct-tab td { padding: 5px 8px; border-bottom: 1px solid var(--border); text-align: left; }
.ct-tab thead th { color: var(--muted); font-weight: 600; }
.ct-tab td.rev { color: var(--red); }
.ct-keys { overflow-x: auto; }
/* --- lexicon.css --- */
/* ================================================================
   THE LEXICON (2026-09-01)  —  site-wide, all 24 pages.

   Part of "the signature": one shared visual primitive, a hairline
   dotted underline meaning "there is substance under this," with two
   consumers. This file styles both: `.lx-term` (a concept, drawn from
   the 126-entry site/data/concepts.json index by lexicon.js) and
   `.rcpt` (a verified number, drawn from site/data/receipts.json by
   receipts.js). The underline must never mean a third thing.

   Colour Law: the underline is muted at rest -- neither .lx-term nor
   .rcpt is itself a claim about which half of the argument you're in,
   so it carries no chromatic accent until you act on it. Feedback on
   hover/focus/open shifts toward mint, per the "verified/pass" rule
   in DESIGN.md's Colour section.

   Tonal-Ladder rule: the popover is a surface sitting ON a panel (or
   on canvas), so it steps to --surface-2/--surface-3 rather than
   reaching for a shadow at rest; --elev-2 only appears as a genuine
   state response (the card itself entering).

   Reversible: remove one <link> and one <script> per page and both
   consumers vanish -- the terms/numbers they mark up are still plain
   text, because lexicon.js only ever wraps prose, never rewrites it.
   ================================================================ */

.lx-term, .rcpt {
  cursor: help;
  border-bottom: 1px dotted var(--muted);
  text-underline-offset: 2px;
  -webkit-text-decoration-color: var(--muted);
  text-decoration-color: var(--muted);
}
.rcpt { font-variant-numeric: tabular-nums; }

.lx-term:hover, .lx-term:focus-visible,
.rcpt:hover, .rcpt:focus-visible {
  border-bottom-color: var(--teal);
}
.lx-term[aria-expanded="true"], .rcpt[aria-expanded="true"] {
  border-bottom-style: solid;
  border-bottom-color: var(--teal);
}
.lx-term:focus-visible, .rcpt:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
  border-radius: 2px;
}

/* "Show the receipts" toggle target state (receipts.js adds this class to
   <body>; see receipts.css/receipts.js for the toggle control itself). Every
   .rcpt on the page lights at once -- the single frame that is the actual
   signature. Mint fill, because a lit receipt IS a "verified/pass" state. */
body.rcpt-on .rcpt {
  border-bottom-style: solid;
  border-bottom-color: var(--teal);
  background: rgba(45, 212, 191, 0.10);
  border-radius: 3px;
  padding: 0 1px;
}

/* ---- The shared popover card -------------------------------------------
   One element, reused by every trigger on the page (lexicon.js owns it;
   receipts.js calls into SymbiQ.sig, the same engine, rather than building
   a second one). Positioned in JS; CSS only ever states its resting look
   and its two visibility states. */
.sig-card {
  position: absolute;
  z-index: 95;
  /* A static fallback; place() in lexicon.js sets an exact inline max-width
     per open (min(320, viewport - 24)) because CSS min()+calc() nesting was
     found, live, to resolve to 0px on at least one real rendering engine --
     a silent collapse that no static audit would have caught, only opening
     the card and measuring it did. Belt-and-braces: this value still holds
     if the JS path is ever skipped. */
  max-width: 320px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  box-shadow: var(--elev-2);
  padding: var(--sp-4);
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--text);
  opacity: 0;
  transform: translateY(-4px);
  pointer-events: none;
  visibility: hidden;
}
.sig-card.open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  visibility: visible;
}
@media (prefers-reduced-motion: no-preference) {
  .sig-card { transition: opacity 160ms var(--ease-out-quint, ease), transform 160ms var(--ease-out-quint, ease); }
}

.sig-card-title { font-weight: 700; margin: 0 0 6px; color: var(--text); }
.sig-card-body { margin: 0 0 10px; color: var(--text); }
.sig-card-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; flex-wrap: wrap;
}
.sig-card-foot .tier { margin-bottom: 0; }
.sig-card-kind {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--muted);
}
.sig-card-link { font-size: 0.84rem; white-space: nowrap; }

/* receipt-only rows: derivation line in the numeric/monospace register the
   rest of the site already uses for formulas, plus the falsifier line. */
.sig-card-formula {
  font-family: "Cambria Math", Georgia, serif;
  background: var(--surface-3);
  border-radius: 6px;
  padding: 6px 9px;
  margin: 0 0 8px;
  font-size: 0.82rem;
  overflow-x: auto;
}
.sig-card-meta { color: var(--muted); font-size: 0.78rem; margin: 0 0 4px; }
.sig-card-wrong { color: var(--muted); font-size: 0.78rem; margin: 0 0 10px; font-style: italic; }

/* Coarse pointer (touch): a bottom sheet instead of a floating card, same
   content, so nothing requires precise hover to reach. */
@media (pointer: coarse) {
  .sig-card {
    position: fixed;
    left: 0; right: 0; bottom: 0; top: auto;
    max-width: none;
    width: auto;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    transform: translateY(100%);
    box-shadow: 0 -20px 44px -20px var(--shadow);
    max-height: min(60vh, 420px);
    overflow-y: auto;
    padding-bottom: calc(var(--sp-6) + env(safe-area-inset-bottom, 0px));
  }
  .sig-card.open { transform: translateY(0); }
  .sig-backdrop {
    position: fixed; inset: 0; z-index: 94; background: rgba(0, 0, 0, 0.35);
    opacity: 0; pointer-events: none; visibility: hidden;
  }
  .sig-backdrop.open { opacity: 1; pointer-events: auto; visibility: visible; }
  @media (prefers-reduced-motion: no-preference) {
    .sig-backdrop { transition: opacity 160ms ease; }
  }
}
/* --- receipts.css --- */
/* ================================================================
   THE RECEIPTS TOGGLE (2026-09-01)  —  loaded site-wide, renders
   only on a page with >=1 entry in site/data/receipts.json.

   The underline and popover chrome for `.rcpt` itself live in
   lexicon.css (the shared primitive, see its own header). This file
   is only the page-level control: "Show the receipts," the single
   frame that is the actual signature described in PART A2 of
   the desk design notes.
   ================================================================ */

.rcpt-toggle-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 14px 0 22px;
}
.rcpt-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 16px;
  border-radius: var(--r-pill, 999px);
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  font: inherit; font-size: 0.86rem; font-weight: 700;
  cursor: pointer;
}
.rcpt-toggle-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--muted);
  flex: none;
}
.rcpt-toggle:hover { border-color: var(--teal); }
.rcpt-toggle[aria-pressed="true"] {
  border-color: var(--teal);
  background: rgba(45, 212, 191, 0.10);
  color: var(--teal);
}
.rcpt-toggle[aria-pressed="true"] .rcpt-toggle-dot { background: var(--teal); }
@media (prefers-reduced-motion: no-preference) {
  .rcpt-toggle { transition: border-color var(--dur-fast, 140ms) ease, background var(--dur-fast, 140ms) ease, color var(--dur-fast, 140ms) ease; }
}
.rcpt-toggle-note { font-size: 0.82rem; color: var(--muted); }
/* --- search.css --- */
/* SymbiQ, the search overlay. Own file, own cache-buster, same rule as
   motion.css / lexicon.css: additive, and reverts by deleting this <link>,
   search.js, and the one nav entry that points at #search.

   Everything is painted from the site's own tokens (--bg / --panel / --text /
   --muted / --border / --teal / --violet), so light, dim and dark all come
   out right without a single theme-specific rule here. */

.sr-wrap {
  position: fixed; inset: 0; z-index: 9000;
  display: flex; align-items: flex-start; justify-content: center;
  padding: clamp(16px, 8vh, 96px) 16px 16px;
}
.sr-wrap[hidden] { display: none !important; }

/* lock the page behind the overlay without the layout shift a plain
   overflow:hidden causes on a scrollbar-visible desktop */
html.sr-open { overflow: hidden; }

.sr-scrim {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(2px);
}

.sr-panel {
  position: relative;
  width: min(680px, 100%);
  max-height: min(72vh, 720px);
  display: flex; flex-direction: column;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .45);
  overflow: hidden;
}

.sr-top { display: flex; align-items: center; gap: 8px; padding: 12px 12px 8px; }

#sr-input {
  flex: 1 1 auto; min-width: 0;
  font: inherit; font-size: 1.02rem;
  padding: 10px 12px;
  color: var(--text); background: var(--bg);
  border: 1px solid var(--border); border-radius: 9px;
}
#sr-input:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
#sr-input::-webkit-search-cancel-button { filter: grayscale(1) opacity(.6); }

.sr-x {
  flex: 0 0 auto; font: inherit; font-size: .72rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); background: transparent;
  border: 1px solid var(--border); border-radius: 7px;
  padding: 8px 10px; cursor: pointer;
}
.sr-x:hover, .sr-x:focus-visible { color: var(--teal); border-color: var(--teal); }

.sr-status { margin: 0; padding: 0 16px 8px; font-size: .8rem; color: var(--muted); }

.sr-list {
  margin: 0; padding: 0 8px 8px; list-style: none;
  overflow-y: auto; flex: 1 1 auto;
}
.sr-hit {
  display: block; padding: 9px 10px; border-radius: 9px;
  text-decoration: none; color: inherit;
}
.sr-hit:hover { background: var(--rowhover, rgba(127, 127, 127, .10)); }
.sr-hit[aria-current="true"] {
  background: var(--rowhover, rgba(127, 127, 127, .10));
  box-shadow: inset 2px 0 0 var(--teal);
}
.sr-hit:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

.sr-head {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  font-weight: 600;
}
.sr-snip {
  display: block; margin-top: 2px;
  font-size: .84rem; color: var(--muted); line-height: 1.45;
}
.sr-hit mark {
  background: rgba(45, 212, 191, .22); color: inherit;
  border-radius: 3px; padding: 0 1px;
}

.sr-kind {
  font-size: .66rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--border); border-radius: 999px;
  padding: 1px 7px; white-space: nowrap;
}
.sr-kind.sr-concept { color: var(--violet); border-color: rgba(167, 139, 250, .45); }
/* the page a section hit lives on: plain, not a pill -- these are full
   sentences, and a sentence in an uppercase pill is unreadable */
.sr-page {
  font-size: .76rem; font-weight: 400; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 46%;
}
.sr-page::before { content: "— "; }
.sr-tier {
  font-size: .64rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  border: 1px solid var(--border); border-radius: 999px; padding: 1px 6px;
}
.sr-tier.t-g { color: var(--green); border-color: rgba(74, 222, 128, .4); }
.sr-tier.t-y { color: var(--yellow); border-color: rgba(250, 204, 21, .4); }
.sr-tier.t-r { color: var(--red); border-color: rgba(248, 113, 113, .4); }

.sr-foot {
  margin: 0; padding: 8px 16px 12px;
  border-top: 1px solid var(--border);
  font-size: .76rem; color: var(--muted);
}

@media (max-width: 560px) {
  .sr-wrap { padding: 8px; }
  .sr-panel { max-height: 88vh; }
  .sr-foot { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .sr-scrim { backdrop-filter: none; }
}
/* --- surf.css --- */
/* SymbiQ, surf.css: the peek card, the wormhole and the footer button (surf.js). Tokens only, so every appearance follows. */
.sf-peek { position: fixed; z-index: 70; display: grid; gap: 4px; padding: 12px 14px; border-radius: 14px; background: var(--surface-2, var(--panel)); color: var(--text);
  border: 1px solid color-mix(in srgb, var(--teal) 40%, var(--border)); box-shadow: 0 18px 40px -18px var(--shadow, rgba(0,0,0,.5)); font-size: .88rem; line-height: 1.45;
  opacity: 0; visibility: hidden; transform: translateY(4px); transition: opacity .15s ease, transform .15s ease, visibility 0s .15s; pointer-events: none; }
.sf-peek.on { opacity: 1; visibility: visible; transform: none; transition: opacity .15s ease, transform .15s ease; pointer-events: auto; }
.sf-peek b { font-size: .95rem; }
.sf-peek span { color: var(--muted); }
.sf-peek i { font-style: normal; font-size: .74rem; letter-spacing: .04em; color: var(--teal); }

.sf-go { min-height: 44px; margin-left: 10px; padding: 0 8px; border: 0; background: none; font: inherit; color: var(--muted); cursor: pointer; text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 4px; }
.sf-go:hover, .sf-go:focus-visible { color: var(--teal); }
.sf-go:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 6px; }

.sf-worm { position: fixed; inset: 0; z-index: 2147482000; display: grid; place-items: center; background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--bg, #0b1020) 55%, transparent), color-mix(in srgb, var(--bg, #0b1020) 92%, transparent) 70%); animation: sfin .35s ease both; cursor: pointer; }
.sf-worm p { position: relative; margin: 0; max-width: min(28rem, 86vw); text-align: center; display: grid; gap: 6px; color: var(--muted); }
.sf-worm p b { font-size: clamp(1.2rem, 4vw, 1.7rem); color: var(--text); }
.sf-worm p small { font-size: .8rem; }
.sf-rings { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none; }
.sf-rings i { position: absolute; width: 20vmin; height: 20vmin; border-radius: 50%; border: 2px solid var(--teal); opacity: 0; animation: sfring 1.5s cubic-bezier(.2,.7,.3,1) infinite; }
.sf-rings i:nth-child(2) { border-color: var(--violet); animation-delay: .3s; }
.sf-rings i:nth-child(3) { animation-delay: .6s; }
.sf-worm.still .sf-rings i { animation: none; opacity: .5; }
@keyframes sfin { from { opacity: 0; } to { opacity: 1; } }
@keyframes sfring { 0% { transform: scale(.2); opacity: .9; } 100% { transform: scale(3.4); opacity: 0; } }
@media (prefers-reduced-motion: reduce) { .sf-worm { animation: none; } .sf-peek, .sf-peek.on { transition: none; transform: none; } }
/* --- nav2.css --- */
/* nav2.css -- the four-tab header, its panels, the You menu and the phone tab bar.
   Markup is written by tools/build_nav.py from tools/site_map.py; behaviour is nav2.js.
   Without JavaScript every tab label is still a link to its hub page, so nothing is lost.

   One quiet accent per tab (Learn teal, Play violet, Check blue, Secure rose; none of them is the
   Optimisation amber). The accent shows on the active underline, the panel's top edge, the hub band
   and the homepage door, nowhere else. */

:root {
  --tab-learn: #2dd4bf; --tab-play: #a78bfa; --tab-check: #60a5fa; --tab-secure: #fb7185;
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) { --tab-learn: #0f766e; --tab-play: #6d28d9; --tab-check: #1d4ed8; --tab-secure: #be123c; }
}
:root[data-theme="light"] { --tab-learn: #0f766e; --tab-play: #6d28d9; --tab-check: #1d4ed8; --tab-secure: #be123c; }

.navtab[data-tab="learn"], .tabbar a[data-tab="learn"], .hub[data-tab="learn"], .door[data-tab="learn"] { --tab-accent: var(--tab-learn); }
.navtab[data-tab="play"], .tabbar a[data-tab="play"], .hub[data-tab="play"], .door[data-tab="play"] { --tab-accent: var(--tab-play); }
.navtab[data-tab="check"], .tabbar a[data-tab="check"], .hub[data-tab="check"], .door[data-tab="check"] { --tab-accent: var(--tab-check); }
.navtab[data-tab="secure"], .tabbar a[data-tab="secure"], .hub[data-tab="secure"], .door[data-tab="secure"] { --tab-accent: var(--tab-secure); }
.tabbar a[data-tab="home"] { --tab-accent: var(--teal); }

/* ---- the top bar ------------------------------------------------------------------------------ */
nav .brand { margin-right: 0; }
body[data-glass] nav:not(.rung-rail) > .navtabs, body[data-glass] nav:not(.rung-rail) > .navright, nav .navtabs, nav .navright { position: static; }
nav .navtabs { list-style: none; display: flex; align-items: center; gap: 2px; margin: 0 0 0 10px; padding: 0; }
.navtab { position: static; display: flex; align-items: center; }
.navtab-link {
  display: inline-flex; align-items: center; min-height: 44px; padding: 6px 2px 4px 12px;
  color: var(--muted); font-weight: 600; font-size: .92rem; text-decoration: none;
  border-bottom: 2px solid transparent; transition: color .15s ease, border-color .15s ease;
}
.navtab-link:hover { color: var(--text); }
.navtab-link.active, .navtab-link.cur { color: var(--text); border-bottom-color: var(--tab-accent); }
.navtab-arrow {
  display: inline-flex; align-items: center; justify-content: center; min-width: 32px; min-height: 44px;
  padding: 0; background: none; border: 0; border-radius: 8px; color: var(--muted); cursor: pointer;
  border-bottom: 2px solid transparent;
}
.navtab-arrow:hover { color: var(--text); }
.navtab-arrow svg { transition: transform .15s ease; }
.navtab[data-open] .navtab-arrow svg, .nav-you[data-open] .you-btn svg:last-child { transform: rotate(180deg); }
.navtab[data-open] .navtab-link { color: var(--text); }
.navtab-link:focus-visible, .navtab-arrow:focus-visible, .nav-search:focus-visible, .nav-continue:focus-visible,
.you-btn:focus-visible, .tabbar a:focus-visible, .np-feature:focus-visible { outline: 2px solid var(--tab-accent, var(--teal)); outline-offset: 2px; }

.navright { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.nav-search, .you-btn, .nav-continue {
  display: inline-flex; align-items: center; gap: 7px; min-height: 44px; min-width: 44px; padding: 0 11px;
  border: 1px solid var(--border); border-radius: 10px; background: none; color: var(--muted);
  font: inherit; font-size: .88rem; font-weight: 600; text-decoration: none; cursor: pointer; justify-content: center;
}
.nav-search:hover, .you-btn:hover, .nav-continue:hover { color: var(--text); border-color: var(--muted); }
.nav-search kbd {
  font: inherit; font-size: .72rem; padding: 1px 6px; border: 1px solid var(--border); border-radius: 5px; color: var(--muted);
}
.nav-continue { color: var(--text); border-color: var(--teal); }
.nav-continue[hidden], .navpanel2[hidden] { display: none !important; }

/* ---- the panels (tab menus and the You menu) --------------------------------------------------- */
nav .navpanel2 {
  position: absolute; top: 100%; left: 0; z-index: 46; width: min(100%, 940px); margin-top: 0;
  display: grid; grid-template-columns: minmax(0, 1fr) 210px; gap: 22px; align-items: start;
  padding: 18px 20px 20px; background: var(--panel); border: 1px solid var(--border);
  border-top: 2px solid var(--tab-accent, var(--teal)); border-radius: 0 0 14px 14px;
  box-shadow: 0 22px 50px -22px var(--shadow); max-height: calc(100vh - 110px); overflow-y: auto;
}
.navpanel2 .np-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(175px, 1fr)); gap: 16px; }
.navpanel2 .navhead { margin: 0 10px 4px; }
.navpanel2 .navmain .q { display: block; font-weight: 600; }
.np-feature {
  display: flex; flex-direction: column; gap: 6px; padding: 16px; border-radius: 12px; text-decoration: none; color: var(--text);
  border: 1px solid var(--tab-accent); background: color-mix(in srgb, var(--tab-accent) 9%, var(--panel));
}
.np-feature:hover { background: color-mix(in srgb, var(--tab-accent) 15%, var(--panel)); }
.np-feature .eyebrow { font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 700; color: var(--tab-accent); }
.np-feature b { font-size: 1.02rem; line-height: 1.3; }
.np-feature .sub { font-size: .82rem; color: var(--muted); }

nav .np-you { left: auto; right: 0; width: min(380px, 100%); display: block; border-top-color: var(--teal); }
.np-you .you-progress { padding: 4px 10px 12px; margin-bottom: 6px; border-bottom: 1px solid var(--border); }
.np-you .you-progress .sub { margin: 0; color: var(--muted); font-size: .84rem; }
.you-rows { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; margin: 0; font-size: .86rem; }
.you-rows dt { color: var(--muted); }
.you-rows dd { margin: 0; font-weight: 600; }

/* ---- the phone layout: logo, search and You on top; four tabs and Home in a bar at the bottom ---- */
nav.tabbar { display: none; }
@media (max-width: 719px) {
  nav .navtabs, .nav-continue { display: none; }
  .you-label { display: none; }
  .nav-search kbd { display: none; }
  nav .navpanel2 { position: fixed; top: auto; left: 8px; right: 8px; bottom: calc(70px + env(safe-area-inset-bottom)); width: auto;
    grid-template-columns: 1fr; border-radius: 14px; }
  nav.tabbar {
    display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 50; margin: 0; gap: 0;
    padding: 6px 6px calc(6px + env(safe-area-inset-bottom)); border-top: 1px solid var(--border); border-bottom: 0;
    background: var(--nav-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  }
  .tabbar a {
    flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
    min-height: 52px; color: var(--muted); text-decoration: none; font-size: .7rem; font-weight: 600; border-radius: 10px;
  }
  .tabbar a.cur, .tabbar a[aria-current="page"] { color: var(--tab-accent); }
  body { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }
  /* floating widgets that sat in the bottom corners move up out from under the bar */
  #qz-pill { bottom: calc(78px + env(safe-area-inset-bottom)) !important; }
  .sq-marker { bottom: calc(80px + env(safe-area-inset-bottom)) !important; }
}
@media (min-width: 720px) and (max-width: 919px) {
  .navtab-link { padding-left: 8px; font-size: .86rem; }
  .nav-search kbd { display: none; }
  .nav-continue { display: none; }
}

/* ---- tab hubs: header band, one start card, then the list -------------------------------------- */
.hub { --band: color-mix(in srgb, var(--tab-accent) 10%, transparent); }
.hubband {
  margin: 4px 0 22px; padding: 22px 22px 20px; border-radius: 16px; border: 1px solid var(--border);
  border-top: 3px solid var(--tab-accent); background: linear-gradient(180deg, var(--band), transparent);
}
.hubband .eyebrow { color: var(--tab-accent); font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 700; margin: 0 0 4px; }
.hubband h1 { margin: 0 0 6px; }
.hubband .tagline { margin: 0; color: var(--muted); max-width: 62ch; }
@media (max-width: 480px) {
  .hubband { padding: 12px 0 4px; border: 0; border-top: 3px solid var(--tab-accent); border-radius: 0; background: none; }
  .hubstart { padding: 14px 0 14px 14px; border-radius: 0 12px 12px 0; }
}
.hubstart {
  display: block; margin: 0 0 26px; padding: 18px 20px; border-radius: 14px; text-decoration: none; color: var(--text);
  border: 1px solid var(--tab-accent); background: color-mix(in srgb, var(--tab-accent) 8%, var(--panel));
}
.hubstart:hover { background: color-mix(in srgb, var(--tab-accent) 14%, var(--panel)); }
.hubstart .eyebrow { color: var(--tab-accent); font-size: .72rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 700; }
.hubstart b { display: block; font-size: 1.15rem; margin: 2px 0 4px; }
.hubstart span:last-child { color: var(--muted); }
.hublist { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 26px 22px; }
.hublist h2 { font-size: 1.05rem; margin: 0 0 8px; padding-bottom: 6px; border-bottom: 2px solid color-mix(in srgb, var(--tab-accent) 55%, transparent); }
.hublist a { display: block; min-height: 44px; padding: 8px 4px; text-decoration: none; color: var(--text); border-radius: 8px; }
.hublist a:hover { background: var(--rowhover); }
.hublist a b { display: block; font-size: .95rem; }
.hublist a span { display: block; color: var(--muted); font-size: .85rem; line-height: 1.4; }

/* ---- the footer link row: About, How we check, AI use, Corrections, Changelog, RSS, Privacy ---- */
.footlinks { margin: 18px 0 6px; padding-top: 14px; border-top: 1px solid var(--border); font-size: .86rem; color: var(--muted); line-height: 2.2; }
.footlinks a { color: var(--muted); display: inline-block; min-height: 44px; line-height: 44px; padding: 0 4px; text-decoration: none; }
.footlinks a:hover { color: var(--text); text-decoration: underline; }

/* ---- glass for the Phase 2 surfaces (the menus, hub pages and doors were written after glass.css and its
   .navpanel / .card selectors never matched them, so they went flat). Same tokens, same gate, same blur. ---- */
@supports ((backdrop-filter: blur(2px)) or (-webkit-backdrop-filter: blur(2px))) {
  body[data-glass] nav .navpanel2 {
    background: var(--panel);
    background: var(--gl-tint-2);
    border-color: var(--gl-edge);
    border-top-color: var(--tab-accent, var(--teal));
    -webkit-backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
    backdrop-filter: blur(var(--gl-blur)) saturate(var(--gl-sat));
    box-shadow: inset 0 1px 0 var(--gl-rim), 0 22px 52px -22px rgba(0, 0, 0, 0.55);
  }
  body[data-glass] .hubband,
  body[data-glass] .hubstart,
  body[data-glass] .hublist section,
  body[data-glass] .door,
  body[data-glass] .hjump .jump-grid a,
  body[data-glass] .hh-about {
    background: var(--panel);
    background: linear-gradient(160deg, color-mix(in srgb, var(--tab-accent, var(--violet)) 12%, var(--gl-tint)), var(--gl-tint) 70%);
    border-color: var(--gl-edge);
    -webkit-backdrop-filter: blur(var(--gl-blur-card)) saturate(var(--gl-sat));
    backdrop-filter: blur(var(--gl-blur-card)) saturate(var(--gl-sat));
    box-shadow: inset 0 1px 0 var(--gl-rim), var(--gl-depth);
  }
  body[data-glass] .door, body[data-glass] .hubband, body[data-glass] .hubstart { border-top-color: var(--tab-accent, var(--teal)); }
  body[data-glass] .door:hover, body[data-glass] .hubstart:hover { box-shadow: inset 0 1px 0 var(--gl-rim-hi), var(--gl-depth-hi), 0 0 28px -8px var(--tab-accent, var(--violet)); }
}

/* ---- pictures and a livelier ground ------------------------------------------------------------ */
.hublist section { padding: 14px 16px 10px; border: 1px solid var(--border); border-radius: 14px; }
.hubband { display: flex; align-items: center; gap: 22px; }
.hubband .hb-text { flex: 1 1 320px; min-width: 0; }
.hubart { flex: 0 1 340px; width: 340px; max-width: 42%; height: auto; border-radius: 14px; box-shadow: 0 12px 34px -16px color-mix(in srgb, var(--tab-accent) 60%, transparent); }
@media (max-width: 720px) { .hubband { flex-direction: column; align-items: stretch; } .hubart { max-width: 100%; width: 100%; order: -1; } }
.tr-art { display: block; width: 100%; max-width: 560px; height: auto; margin: 6px 0 12px; border-radius: 14px; }
.door .d-ico { width: 44px; height: 44px; margin-bottom: 2px; transition: transform .25s ease, filter .25s ease; }
.door:hover .d-ico { transform: translateY(-2px) scale(1.08); filter: drop-shadow(0 0 8px var(--tab-accent)); }
.hh-about { margin: 6px 0 18px; padding: 16px 20px; border: 1px solid var(--border); border-left: 3px solid var(--violet); border-radius: 14px; background: var(--panel); }
.hh-about-k { margin: 0 0 4px; font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--violet); }
.hh-about p { margin: 0; max-width: 72ch; font-size: 1.04rem; line-height: 1.6; }
.hh-about, .door, .hubstart, .hublist section, .jump-grid a { transition: transform .2s ease, box-shadow .25s ease, border-color .2s ease; }
.hublist section:hover { border-color: var(--tab-accent); }
/* a still warm-and-cool aurora (static on purpose: an animated full-page layer made the play verifier 80x slower under software rendering) behind everything: amber and rose against the existing violet and teal */
html::after {
  content: ""; position: fixed; inset: -20%; z-index: -1; pointer-events: none; opacity: .9;
  background:
    radial-gradient(40% 34% at 82% 12%, rgba(251, 191, 36, .15), transparent 70%),
    radial-gradient(38% 32% at 10% 78%, rgba(251, 113, 133, .13), transparent 70%),
    radial-gradient(34% 30% at 55% 50%, rgba(96, 165, 250, .12), transparent 70%);
}
@media (prefers-color-scheme: light) { html:not([data-theme])::after { opacity: .5; } }
html[data-theme="light"]::after { opacity: .5; }

/* phones: the responsive layer keeps text near the screen edge, so the new boxes lose their frame there */
@media (max-width: 480px) {
  body[data-glass] .hh-about, .hh-about { padding: 10px 0 4px; border: 0; border-top: 2px solid var(--violet); border-radius: 0; background: none; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
  body[data-glass] .hublist section, .hublist section { padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none; }
  .orhero { padding: 18px 0 8px; border: 0; border-top: 3px solid var(--teal); border-radius: 0; background: none; }
}
/* --- lesson.css --- */
/* lesson.css -- the shared parts of the page templates: breadcrumb, "you'll be able to" line, three end doors,
   the sources-and-AI card, and the reading measure. Markup is written by tools/apply_templates.py. */

.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 4px; margin: -14px 0 14px; font-size: .84rem; color: var(--muted); }
.crumbs a { display: inline-flex; align-items: center; min-height: 44px; padding: 0 2px; color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--text); text-decoration: underline; }
.crumbs [aria-current] { color: var(--text); }

.ability { max-width: 70ch; margin: 10px 0 18px; padding: 10px 14px; border-left: 3px solid var(--teal); background: rgba(45, 212, 191, .06); border-radius: 0 10px 10px 0; font-size: 1rem; }
.ability b { color: var(--text); }

/* a reading measure of 60-75 characters for running prose (cards, tables, figures and widgets keep their own width) */
.wrap > p, .wrap > ul, .wrap > ol, .wrap > .layer > p { max-width: 72ch; }

.enddoors { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); gap: 12px; margin: 34px 0 18px; }
.enddoors a { display: flex; flex-direction: column; gap: 4px; min-height: 44px; padding: 14px 16px; border: 1px solid var(--border); border-radius: 12px; text-decoration: none; color: var(--text); background: var(--panel); }
.enddoors a:hover { border-color: var(--teal); }
.enddoors .eyebrow { font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 700; color: var(--muted); }
.enddoors b { font-size: .98rem; line-height: 1.35; }
.enddoors a:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

.aicard { max-width: 72ch; margin: 22px 0 10px; padding: 12px 14px; border: 1px dashed var(--border); border-radius: 10px; color: var(--muted); font-size: .88rem; }
.aicard p { margin: 0; }
.aicard .aicard-h { margin: 0 0 4px; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 700; }
.aicard b { color: var(--text); font-weight: 600; }
.aicard a { color: var(--link); }

/* game-stage template: rules and "what is real" share one drawer under the game */
.gamerules .gr-real { padding: 0 16px 14px; color: var(--muted); font-size: .88rem; line-height: 1.5; }
.gamerules .gr-real .lbl { color: var(--text); font-weight: 600; margin-right: 4px; }

/* tool template: the tool is the hero, its explanation collapses underneath */
.toolnote { margin: 10px 0 26px; padding: 0 16px; border: 1px solid var(--border); border-radius: 12px; }
.toolnote > summary { display: flex; align-items: center; min-height: 44px; cursor: pointer; font-weight: 600; color: var(--muted); }
.toolnote[open] > summary { color: var(--text); }
.toolnote > summary:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

/* the "today" cards (today.js): a rated Reality Check card and a glossary term */
.discover { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; margin: 18px 0; }
@media (min-width: 720px) { .discover { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.dc-card {
  display: flex; flex-direction: column; gap: 6px; align-items: flex-start;
  padding: 15px 16px; border-radius: 12px;
  border: 1px solid var(--border); background: var(--panel);
}
.dc-eyebrow { font-size: .66rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.dc-title { margin: 0; font-size: 1.02rem; font-weight: 700; line-height: 1.3; max-width: none; }
.dc-body { margin: 0; font-size: .88rem; line-height: 1.45; color: var(--muted); max-width: none; }
.dc-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: .78rem; color: var(--muted); }
.dc-meter { display: inline-flex; gap: 3px; }
.dc-meter i { width: 14px; height: 6px; border-radius: 3px; background: var(--border); }
.dc-meter i.on { background: var(--teal); }
.dc-row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-top: auto; padding-top: 4px; }
.dc-link, .dc-again { font-size: .86rem; font-weight: 600; color: var(--teal); text-decoration: none; background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit; }
.dc-link:hover, .dc-again:hover { text-decoration: underline; }
.dc-link:focus-visible, .dc-again:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

/* a faction named on the pages where its failure mode is the subject (tools/lessons.py FACTIONS, apply_templates.py) */
.faction { margin: 12px 0 14px; padding: 10px 14px; border-left: 3px solid var(--yellow); border-radius: 0 10px 10px 0; background: var(--panel); font-size: .92rem; line-height: 1.5; }
.faction b { color: var(--text); }
.faction span { color: var(--muted); }
.faction a { display: inline-flex; align-items: center; min-height: 44px; margin-left: 4px; font-weight: 600; }

/* a quest card (site/quest.js): a chain of steps on different pages, ending in a Codex entry */
.quest { margin: 14px 0 18px; padding: 14px 18px; border: 1px solid var(--border); border-left: 3px solid var(--violet); border-radius: 12px; background: var(--panel); }
.quest-k { margin: 0; font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 700; color: var(--violet); }
.quest-h { margin: 2px 0 4px; font-size: 1.15rem; padding-left: 0; }
.quest-h::before { display: none; }
.quest-b { margin: 0 0 8px; color: var(--muted); font-size: .92rem; }
.quest-n { font-weight: 700; color: var(--text); margin-left: 6px; }
.quest-steps { list-style: none; margin: 0; padding: 0; }
.quest-steps li { display: flex; align-items: center; gap: 10px; min-height: 44px; }
.quest-steps a { display: inline-flex; align-items: center; min-height: 44px; }
.quest-ck { width: 22px; text-align: center; color: var(--muted); }
.quest-steps .is-done .quest-ck { color: var(--teal); font-weight: 700; }
.quest-done { margin: 8px 0 0; font-weight: 600; color: var(--teal); }
/* --- course.css --- */
/* course.css -- the course player: topic pages (stepper, progress rail) and the course index's topic list.
   Markup: tools/build_course.py. Behaviour: course.js. */

/* ---- the course index: one row per topic, in place of the old open-in-place strips ---- */
.courselist { display: grid; gap: 8px; margin: 14px 0 28px; }
.coursetopic {
  display: grid; grid-template-columns: 2.6rem minmax(0, 1fr) 1.6rem; align-items: center; gap: 4px 12px;
  min-height: 44px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--panel); color: var(--text); text-decoration: none;
}
.coursetopic:hover { border-color: var(--teal); }
.coursetopic:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.coursetopic .ct-n { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--muted); font-size: 1.05rem; }
.coursetopic .ct-t { font-weight: 600; }
.coursetopic .ct-q { grid-column: 2; color: var(--muted); font-size: .88rem; line-height: 1.4; }
.coursetopic .cp-ck { grid-row: 1; grid-column: 3; color: var(--teal); font-weight: 700; text-align: right; }
.coursetopic.is-done { border-color: color-mix(in srgb, var(--teal) 45%, var(--border)); }
.cp-resume { display: block; margin: 0 0 18px; padding: 12px 16px; border: 1px solid var(--teal); border-radius: 12px; text-decoration: none; color: var(--text); background: rgba(45, 212, 191, .07); }
.cp-resume[hidden] { display: none; }
.cp-resume span { display: block; font-size: .7rem; letter-spacing: .1em; text-transform: uppercase; font-weight: 700; color: var(--teal); }

/* ---- a topic page ---- */
.cp-head .eyebrow { color: var(--muted); font-size: .8rem; letter-spacing: .06em; text-transform: uppercase; font-weight: 600; }
.cp-head .eyebrow a { color: inherit; }
.cp-head h1 { margin-top: 4px; }

.cp-rail { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; padding: 0; margin: 10px 0 18px; }
.cp-rail a {
  display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; padding: 0 6px;
  border: 1px solid var(--border); border-radius: 10px; color: var(--muted); text-decoration: none;
  font-size: .82rem; font-variant-numeric: tabular-nums; position: relative;
}
.cp-rail a:hover { color: var(--text); border-color: var(--muted); }
.cp-rail a[aria-current="page"] { color: var(--text); border-color: var(--teal); box-shadow: inset 0 -3px 0 var(--teal); font-weight: 700; }
.cp-rail a.is-done::after { content: "\2713"; position: absolute; top: 1px; right: 4px; font-size: .62rem; color: var(--teal); }

.cp-section { margin: 0 0 16px; max-width: 72ch; color: var(--muted); }
.cp-section > summary { cursor: pointer; min-height: 44px; display: flex; align-items: center; color: var(--muted); font-weight: 600; }
.cp-section p { margin: 0 0 .7em; }

.cp-stepper { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 18px; padding: 0; }
.cp-stepper button {
  display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px; border: 1px solid var(--border);
  border-radius: 99px; background: transparent; color: var(--muted); font: inherit; font-size: .88rem; font-weight: 600; cursor: pointer;
}
.cp-stepper button b { display: inline-flex; align-items: center; justify-content: center; width: 1.5em; height: 1.5em; border-radius: 50%; border: 1px solid currentColor; font-size: .78em; }
.cp-stepper button:hover { color: var(--text); }
.cp-stepper button.is-on { color: var(--text); border-color: var(--teal); background: rgba(45, 212, 191, .08); }
.cp-stepper button.is-past b { background: var(--teal); border-color: var(--teal); color: var(--bg); }
.cp-stepper button:focus-visible, .cp-foot button:focus-visible, .cp-foot a:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.course:not(.is-stepped) .cp-stepper, .course:not(.is-stepped) .cp-foot .cp-prev, .course:not(.is-stepped) .cp-foot .cp-next { display: none; }

.cp-step[hidden] { display: none; }
.cp-step > .layer { margin-top: 0; }

.cp-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 22px 0 10px; }
.cp-foot button { min-height: 44px; padding: 0 18px; border-radius: 10px; font: inherit; font-weight: 600; cursor: pointer; border: 1px solid var(--border); background: transparent; color: var(--text); }
.cp-foot button:disabled { opacity: .4; cursor: default; }
.cp-foot .cp-next { border-color: var(--teal); }
.cp-check { border: 1px solid var(--border); border-radius: 14px; padding: 18px 20px; margin: 22px 0 8px; background: var(--panel); }
.cp-check .cyu-opts button { min-height: 44px; }
.cp-check .cyu-opts button:disabled:not(.is-right) { opacity: .55; cursor: default; }
.cp-check .cyu-why[hidden] { display: none; }
.cp-out { display: flex; flex-wrap: wrap; gap: 8px 18px; margin: 4px 0 14px; }
.cp-out a { display: inline-flex; align-items: center; min-height: 44px; }
.cp-neighbours { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; margin: 6px 0 18px; }
.cp-neighbours a { display: inline-flex; flex-direction: column; min-height: 44px; padding: 10px 14px; border: 1px solid var(--border); border-radius: 10px; text-decoration: none; color: var(--text); max-width: 48%; }
.cp-neighbours a span { font-size: .72rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); font-weight: 700; }
.cp-neighbours a:hover { border-color: var(--teal); }
.cp-neighbours .cp-nx { margin-left: auto; text-align: right; }
@media (max-width: 480px) { .cp-neighbours a { max-width: 100%; flex: 1 1 100%; } }
