/*! Amber Console 2.1.0 | BSD-3-Clause | https://github.com/DutchDiederik/AmberConsole
 *  Generated by scripts/build.mjs — edit files in src/, not this one.
 *  Fonts in ../fonts/ are SIL OFL 1.1; see fonts/OFL-*.txt.
 */
/* ===========================================================================
   Amber Console — a monochrome amber-terminal CSS framework
   BSD-3-Clause licensed; see LICENSE. Fonts in fonts/ are SIL OFL 1.1; see
   fonts/OFL-*.txt.
   https://github.com/DutchDiederik/AmberConsole

   NO VERSION NUMBER LIVES IN src/. package.json is the single source of truth;
   scripts/build.mjs stamps it into the dist/ banners. A version written here
   would be inlined into the bundle beside the generated one and drift on the
   first release that forgets it.

   This file is an @import list and nothing else. Every rule lives in one of the
   files below, so you can drop any single component you do not need.

   To embed inside an existing app rather than owning the page, swap
   base/reset.css for base/reset-scoped.css and wrap your markup in .ac-root.
   To skip the vendored webfonts, swap tokens/fonts.css for tokens/fonts-cdn.css.
   Either way, rebuild: `npm run build`.
   =========================================================================== */

/* --- Tokens: the entire API. Override any of these at runtime. --- */
/* >>> src/amber-console.css -> tokens/fonts.css */
/* ===========================================================================
   Amber Console — self-hosted webfonts

   Two bitmap faces, both SIL Open Font License 1.1 (see fonts/OFL-*.txt):

     VT323       everything at 18px and up  -> --ac-font-terminal
     Silkscreen  8-10px micro labels ONLY   -> --ac-font-micro

   These are era-correct SUBSTITUTES, not the original face. The source hardware
   used a mask-ROM bitmap font with no digital release; VT323 is a digitization of
   the DEC VT320 terminal ROM. To swap in a licensed face closer to the hardware,
   replace --ac-font-terminal in tokens/typography.css and nothing else.

   GENERATED by scripts/fetch-fonts.mjs — do not edit by hand.

   Paths are relative and resolve without a server (file:// works):
     from src/tokens/fonts.css  ->  ../fonts/
     from dist/amber-console.css ->  ../fonts/     (rewritten by scripts/build.mjs)

   Prefer not to vendor the binaries? Swap this import for tokens/fonts-cdn.css
   in amber-console.css.
   =========================================================================== */

/* VT323 400 — vietnamese */
@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/VT323-Regular-vietnamese.woff2") format("woff2");
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

/* VT323 400 — latin-ext */
@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/VT323-Regular-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* VT323 400 — latin */
@font-face {
  font-family: "VT323";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/VT323-Regular.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Silkscreen 400 — latin-ext */
@font-face {
  font-family: "Silkscreen";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/Silkscreen-Regular-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Silkscreen 400 — latin */
@font-face {
  font-family: "Silkscreen";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/Silkscreen-Regular.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* >>> src/amber-console.css -> tokens/colors.css */
/* TECHNOLOGY, THEN EMITTER — the palettes.
 *
 * It takes TWO attributes on the root element to name a palette, because the
 * same color word means different physics on different glass:
 *
 *   data-ac-tech      "plasma" (a gas gap emitting directly) or
 *                     "crt" (a phosphor, which emits and then PERSISTS).
 *   data-ac-emitter   which gas or phosphor within that technology.
 *
 * Every block below is selected by BOTH, so a mismatched pair selects nothing
 * rather than silently rendering the wrong hardware.
 *
 *   plasma / neon      24deg. The default. What .ac-bloom simulates.
 *   plasma / helium    pale pink.          plasma / argon    pale lavender.
 *   plasma / krypton   pale violet-white.
 *
 * THE SEVEN PHOSPHORS SPLIT IN TWO, AND THE SPLIT IS WHAT THE TUBE WAS FOR
 * rather than what colour it is. It decides whether a palette is a reasonable
 * default for an interface or a deliberate costume, so the demo board groups
 * them the same way.
 *
 *   GENERAL PURPOSE — built to be read, for hours, by a person.
 *     crt / p3         38deg amber. The classic terminal, and .ac-crt's pair.
 *     crt / p1         green, willemite. The early scope and terminal tube.
 *     crt / p4         television white. A BLEND of two emitters.
 *     crt / p31        green. The monitor and scope phosphor most people picture.
 *
 *   SPECIALIZED — built for an instrument, where reading was not the job. Each
 *   is legible here, but each is a costume with a reason attached.
 *     crt / p7         radar. Blue flash over a yellow-green afterglow that
 *                      holds for seconds so a sweep leaves a picture behind.
 *     crt / p39        long-persistence yellow-green. Same trade as P7 without
 *                      the two coatings: no flicker at any refresh, paid in smear.
 *     crt / p11        photographic recording. Blue is where film emulsion is
 *                      most sensitive; nobody read text off one on purpose.
 *
 * THE FOUR GENERAL TUBES ALL DECAY IN UNDER ~25ms, which is under the ~80ms the
 * eye needs to read a decay as a decay rather than as a switch — so on those the
 * optional effects module has nothing visible to add, and says so by disabling
 * its own switch. Persistence you can SEE is the specialized group's whole
 * point. See ENGINE_FLOOR_MS in ../amber-console.js.
 *
 * P1/P39 and P7/P11 share a ramp on purpose — same compound, different decay.
 * Note that puts one of each pair on either side of the split: the difference
 * between a terminal and an instrument here is time, not colour.
 *
 * WHAT EVERY BLOCK MUST DEFINE: the five --ac-emit-* stops, the three surfaces,
 * --ac-on-fill, the four --ac-halo-* glow triples, and --ac-halo-scatter / --ac-halo-spread.
 * Every other color token is an alias or is built from those, so adding a
 * palette means adding one block and nothing else.
 *
 * A BLOCK MAY ALSO CARRY TIME, where the emitter has a documented decay:
 * --ac-persist, --ac-flicker and --ac-decay-ease. These are consumed WITH A CAP
 * by tokens/effects.css, not raw. The four gases declare none of them and fall
 * back to its :root defaults.
 *
 * --ac-persist-fast IS THE ODD ONE OUT: it is DOCUMENTATION, not an input. A
 * two-layer phosphor declares it to record how fast its FIRST coating decays,
 * and nothing reads it — no rule, no keyframe, no script. It exists because
 * keyframe offsets cannot read a custom property, so ac-ghost-cascade in
 * sim/afterglow.css has to hardcode the crossover as a percentage, and without
 * this token there would be no machine-readable record of the figure that
 * percentage was computed from. Do not wire it up expecting it to do something.
 *
 * EVERY PALETTE IS GATED. scripts/contrast.mjs runs the full pair table against
 * each block independently — a ratio that passes under one emitter and fails
 * under another is a failure. Do not add a stop to one block without the others.
 *
 * GENERATED. Nine of the eleven blocks are computed by scripts/derive-gas.mjs
 * and must not be hand-edited; `npm test` re-derives them and fails on drift.
 * Neon and P3 predate the script and stay hand-built.
 *
 * DEPRECATED, removed in 3.0: data-ac-gas="neon" | "amber". Migrate to
 *   data-ac-tech="plasma" data-ac-emitter="neon"  /  ="crt" ="p3".
 *
 * Why 24deg, why the phosphor bands are back-solved, why saturation is an
 * output rather than a setting, and why P7's halo is not its ink:
 * see RATIONALE.md § tokens/colors.css.
 */

:root,
[data-ac-tech="plasma"][data-ac-emitter="neon"],
[data-ac-gas="neon"] {
  /* Surfaces — warm blacks (the unlit gas panel) */
  --ac-screen: #100600;        /* base panel */
  --ac-screen-raised: #1b0c02; /* subtly lifted region */
  --ac-screen-well: #060200;   /* recessed input wells */

  /* Discharge scale — ONE hue, five intensities. Never introduce a second hue
     WITHIN a palette; these blocks are hardware, not decoration.

     HAND-BUILT, and stays that way: this ramp was solved hue-first at maximum
     saturation, which is not what derive-gas.mjs does, and neither of its gamut
     strategies reproduces it. Each stop is solved to a contrast ratio against
     --ac-screen (10.5 / 7.0 / 5.2 / 3.4) — rotating hue at constant lightness drops
     --ac-emit-50 to 2.79:1 and silently fails the 3:1 non-text gate. Re-solve,
     never re-tint; scripts/contrast.mjs is the check.

     Why the hue is 24deg and why regenerating neon would be a mistake:
     RATIONALE.md § Hue — why neon is 24deg. */
  --ac-emit-100: #ffa86d; /* hot highlight / focus — 10.57:1 */
  --ac-emit-90:  #ff6b08; /* primary discharge    —  7.02:1 */
  --ac-emit-70:  #dd5800; /* secondary            —  5.22:1 */
  --ac-emit-50:  #ab4500; /* dim / disabled       —  3.42:1 */
  --ac-emit-30:  #5b2500; /* trace / ghost        —  1.63:1, decorative */
  --ac-on-fill:   #1e0c00;

  /* THE GLOW SOURCE — four points on the discharge hue, as bare "r, g, b"
     triples so tokens/effects.css can build every halo with its own alphas:
     rgba(var(--ac-halo-1), 0.4). That indirection is why the palettes switch
     cleanly; hardcoding a halo puts an amber corona around neon text.

     --ac-halo-scatter is how much light reaches the halo, with NEON AT 1.00 BY
     DEFINITION — every other palette's figure is stated against it — and
     --ac-halo-spread is its square root. See RATIONALE.md § Scattering. */
  --ac-halo-scatter: 1;
  --ac-halo-spread: 1;

  --ac-halo-1: 255, 120, 30; /* core halo */
  --ac-halo-2: 255, 109, 12; /* mid */
  --ac-halo-3: 255, 102, 0;  /* deep / outermost */
  --ac-halo-4: 255, 140, 64; /* pale — hot cores and the retrace band */
}

/* --- CRT / P3 — the amber phosphor, and the source design system's ramp. Same
       structure as the block above, same contrast gate. NOT a gas: this is a
       phosphor on a tube, which is why it is namespaced under a different
       technology and not sitting beside neon. --- */
[data-ac-tech="crt"][data-ac-emitter="p3"],
[data-ac-gas="amber"] {
  --ac-screen: #0d0700;
  --ac-screen-raised: #170e02;
  --ac-screen-well: #060200;

  --ac-emit-100: #ffd052; /* hot highlight / focus — 13.74:1 */
  --ac-emit-90:  #ffae1e; /* primary discharge    — 10.81:1 */
  --ac-emit-70:  #cd8817; /* secondary            —  6.81:1 */
  --ac-emit-50:  #8d5b10; /* dim / disabled       —  3.47:1 */
  --ac-emit-30:  #4a2f08; /* trace / ghost        —  1.62:1, decorative */
  --ac-on-fill:   #1a0e00;

  --ac-halo-1: 255, 174, 30;
  --ac-halo-2: 255, 150, 12;
  --ac-halo-3: 255, 120, 0;
  --ac-halo-4: 255, 186, 64;

  /* Derived from the band model, replacing a placeholder held at neon's 1.00
     that was wrong by 23%. The RAMP above stays hand-built (emit: false in
     scripts/data/emitters.json); only the halo width was ever a placeholder.
     See RATIONALE.md § Scattering. */
  --ac-halo-scatter: 1.23;
  --ac-halo-spread: 1.11;

  /* Persistence: 25ms to 10% (medium). 22% still lit at the next frame.
     --ac-flicker is the same quantity read the other way; the WCAG amplitude
     budget that consumes it lives in tokens/effects.css, in one place. */
  --ac-persist: 25ms;
  --ac-flicker: 0.785;

  /* The decay curve, sampled from the exponential — a shape no single
     cubic-bezier expresses, which is why it is a linear(). */
  --ac-decay-ease: linear(0, 0.013 0.5%, 0.023 0.9%, 0.041 1.6%, 0.072 2.9%, 0.127 5.3%, 0.218 9.5%, 0.362 17%, 0.564 31%, 0.802 56%, 1.000 100%);
}

/* --- PLASMA / HELIUM — pale pink / salmon-white.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The line data, its source
     and the reason this gas uses the line selection it does are in
     scripts/data/emitters.json.

     CIE 1931 2-deg integral over all 15 listed lines: x=0.3944 y=0.2986.
     Inside sRGB as measured; no gamut mapping needed.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.40:1 on --ac-emit-90. --- */
[data-ac-tech="plasma"][data-ac-emitter="helium"] {
  --ac-screen: #0e0809;
  --ac-screen-raised: #1b1214;
  --ac-screen-well: #060404;

  --ac-emit-100: #ffa2b9;
  --ac-emit-90:  #dc7d96;
  --ac-emit-70:  #bc6a80;
  --ac-emit-50:  #925163;
  --ac-emit-30:  #502a34;
  --ac-on-fill:  #201216;

  --ac-halo-1: 255, 158, 183;
  --ac-halo-2: 255, 153, 180;
  --ac-halo-3: 255, 150, 178;
  --ac-halo-4: 255, 170, 191;

  /* Rayleigh: this gas throws 26% more light into the halo than neon. */
  --ac-halo-scatter: 1.26;
  --ac-halo-spread: 1.12;
}

/* --- PLASMA / ARGON — pale violet-lavender.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The line data, its source
     and the reason this gas uses the line selection it does are in
     scripts/data/emitters.json.

     CIE 1931 2-deg integral over 43 of 102 listed lines: x=0.2163 y=0.1047.
     Outside sRGB at the luminance --ac-emit-90 needs, so it is
     desaturated 35% toward D65 — the gas is paler here than it is in the tube.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.37:1 on --ac-emit-90. --- */
[data-ac-tech="plasma"][data-ac-emitter="argon"] {
  --ac-screen: #0b0812;
  --ac-screen-raised: #171222;
  --ac-screen-well: #050309;

  --ac-emit-100: #c9b0ff;
  --ac-emit-90:  #af81ff;
  --ac-emit-70:  #9f5dff;
  --ac-emit-50:  #8925f1;
  --ac-emit-30:  #4b1088;
  --ac-on-fill:  #1b112c;

  --ac-halo-1: 186, 149, 255;
  --ac-halo-2: 182, 143, 255;
  --ac-halo-3: 180, 138, 255;
  --ac-halo-4: 194, 164, 255;

  /* Rayleigh: this gas throws 49% more light into the halo than neon. */
  --ac-halo-scatter: 1.49;
  --ac-halo-spread: 1.22;
}

/* --- PLASMA / KRYPTON — pale violet-white.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The line data, its source
     and the reason this gas uses the line selection it does are in
     scripts/data/emitters.json.

     CIE 1931 2-deg integral over all 95 listed lines: x=0.3153 y=0.2551.
     Inside sRGB as measured; no gamut mapping needed.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.35:1 on --ac-emit-90. --- */
[data-ac-tech="plasma"][data-ac-emitter="krypton"] {
  --ac-screen: #0b080b;
  --ac-screen-raised: #181318;
  --ac-screen-well: #050405;

  --ac-emit-100: #eaa5eb;
  --ac-emit-90:  #bf86c0;
  --ac-emit-70:  #a372a3;
  --ac-emit-50:  #7f587f;
  --ac-emit-30:  #442e45;
  --ac-on-fill:  #1b141b;

  --ac-halo-1: 251, 185, 252;
  --ac-halo-2: 241, 173, 242;
  --ac-halo-3: 229, 162, 230;
  --ac-halo-4: 254, 201, 255;

  /* Rayleigh: this gas throws 31% more light into the halo than neon. */
  --ac-halo-scatter: 1.31;
  --ac-halo-spread: 1.14;
}

/* --- CRT / P1 — willemite green.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The band data, its source
     and the reason this phosphor uses the band shape it does are in
     scripts/data/emitters.json.

     Band 529nm FWHM 48nm: x=0.2154 y=0.7110.
     Outside sRGB at the luminance --ac-emit-90 needs, so it is
     desaturated 57% toward D65 — the phosphor is paler here than it is in the tube.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.39:1 on --ac-emit-90. --- */
[data-ac-tech="crt"][data-ac-emitter="p1"] {
  --ac-screen: #000c03;
  --ac-screen-raised: #001908;
  --ac-screen-well: #000601;

  --ac-emit-100: #00d871;
  --ac-emit-90:  #00b05b;
  --ac-emit-70:  #00964d;
  --ac-emit-50:  #00753a;
  --ac-emit-30:  #003f1c;
  --ac-on-fill:  #001a08;

  --ac-halo-1: 0, 237, 125;
  --ac-halo-2: 0, 225, 118;
  --ac-halo-3: 0, 212, 111;
  --ac-halo-4: 46, 255, 140;

  /* Rayleigh: this phosphor throws 63% more light into the halo than neon. */
  --ac-halo-scatter: 1.63;
  --ac-halo-spread: 1.28;

  /* Persistence: 24ms to 10% (medium).
     20% still lit when the next frame arrives.
     --ac-flicker is the inter-frame modulation depth these two share; the
     amplitude budget that keeps it under the WCAG flash threshold is in
     tokens/effects.css, in one place, on purpose. */
  --ac-persist: 24ms;
  --ac-flicker: 0.798;

  /* The decay curve itself, sampled from the exponential.
     A shape no single cubic-bezier expresses, which is why it is a linear(). */
  --ac-decay-ease: linear(0, 0.013 0.5%, 0.023 0.9%, 0.041 1.6%, 0.072 2.9%, 0.127 5.3%, 0.218 9.5%, 0.362 17%, 0.564 31%, 0.802 56%, 1.000 100%);
}

/* --- CRT / P4 — white.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The band data, its source
     and the reason this phosphor uses the band shape it does are in
     scripts/data/emitters.json.

     2 bands 466nm FWHM 76nm + 572nm FWHM 130nm x0.5: x=0.2701 y=0.3000.
     Inside sRGB as measured; no gamut mapping needed.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.37:1 on --ac-emit-90. --- */
[data-ac-tech="crt"][data-ac-emitter="p4"] {
  --ac-screen: #08090b;
  --ac-screen-raised: #121516;
  --ac-screen-well: #040405;

  --ac-emit-100: #9dc1d8;
  --ac-emit-90:  #7f9db0;
  --ac-emit-70:  #6c8696;
  --ac-emit-50:  #536874;
  --ac-emit-30:  #2b373e;
  --ac-on-fill:  #121618;

  --ac-halo-1: 176, 211, 234;
  --ac-halo-2: 165, 201, 223;
  --ac-halo-3: 153, 189, 211;
  --ac-halo-4: 197, 228, 248;

  /* Rayleigh: this phosphor throws 55% more light into the halo than neon. */
  --ac-halo-scatter: 1.55;
  --ac-halo-spread: 1.24;

  /* Persistence: 0.06ms to 10% (medium-short).
     Fully dark between frames — flicker at maximum, no persistence to see.
     --ac-flicker is the inter-frame modulation depth these two share; the
     amplitude budget that keeps it under the WCAG flash threshold is in
     tokens/effects.css, in one place, on purpose. */
  --ac-persist: 0.06ms;
  --ac-flicker: 1.000;
}

/* --- CRT / P7 — blue flash over a yellow-green afterglow.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The band data, its source
     and the reason this phosphor uses the band shape it does are in
     scripts/data/emitters.json.

     Band 466nm FWHM 76nm: x=0.1384 y=0.1502.
     Outside sRGB at the luminance --ac-emit-90 needs, so it is
     desaturated 29% toward D65 — the phosphor is paler here than it is in the tube.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.35:1 on --ac-emit-90.

     TWO EMITTERS, IN SEQUENCE. --ac-emit-* is the ZnS:Ag FLASH (x=0.1384 y=0.1502),
     which is what the beam writes. --ac-halo-* is the (Zn,Cd)S:Cu AFTERGLOW
     (x=0.3553 y=0.5373), which is what the screen holds after it. The halo
     therefore does NOT track the ink here, which inverts the rule stated at the
     top of tokens/effects.css and is the reason P7 exists as a part number. --- */
[data-ac-tech="crt"][data-ac-emitter="p7"] {
  --ac-screen: #040a11;
  --ac-screen-raised: #0b1520;
  --ac-screen-well: #020409;

  --ac-emit-100: #8bc0ff;
  --ac-emit-90:  #00a0fb;
  --ac-emit-70:  #0088d7;
  --ac-emit-50:  #0069a8;
  --ac-emit-30:  #00385d;
  --ac-on-fill:  #03182a;

  --ac-halo-1: 156, 226, 72;
  --ac-halo-2: 145, 215, 54;
  --ac-halo-3: 135, 203, 40;
  --ac-halo-4: 176, 242, 107;

  /* Rayleigh: this phosphor throws 47% more light into the halo than neon. */
  --ac-halo-scatter: 1.47;
  --ac-halo-spread: 1.21;

  /* THE FLASH, as a halo triple. --ac-halo-* above is the AFTERGLOW, which is what
     this screen holds; this is the layer the beam actually writes, and the ghost
     keyframe starts here before crossing to the afterglow within one frame.
     --ac-ghost-anim is how this palette tells tokens/effects.css it decays
     through two hues rather than one, without effects.css naming it. */
  --ac-halo-flash: 175, 209, 255;
  --ac-ghost-anim: ac-ghost-cascade;

  /* Persistence: 400ms to 10%, visible tail to 3000ms (long).
     91% still lit when the next frame arrives.
     --ac-flicker is the inter-frame modulation depth these two share; the
     amplitude budget that keeps it under the WCAG flash threshold is in
     tokens/effects.css, in one place, on purpose. */
  --ac-persist: 3000ms;
  --ac-flicker: 0.091;
  --ac-persist-fast: 0.04ms;

  /* Long enough that the next ON edge lands before this one has
     drained: the alarm throbs rather than chops. */
  --ac-blink-anim: ac-blink-decay-long;

  /* The decay curve itself, sampled from the Becquerel power law I0/(1+t/t0)^1.4.
     A shape no single cubic-bezier expresses, which is why it is a linear(). */
  --ac-decay-ease: linear(0, 0.186 0.5%, 0.296 0.9%, 0.441 1.6%, 0.602 2.9%, 0.750 5.3%, 0.862 9.5%, 0.932 17%, 0.971 31%, 0.991 56%, 1.000 100%);
}

/* --- CRT / P11 — blue.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The band data, its source
     and the reason this phosphor uses the band shape it does are in
     scripts/data/emitters.json.

     Band 466nm FWHM 76nm: x=0.1384 y=0.1502.
     Outside sRGB at the luminance --ac-emit-90 needs, so it is
     desaturated 29% toward D65 — the phosphor is paler here than it is in the tube.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.35:1 on --ac-emit-90. --- */
[data-ac-tech="crt"][data-ac-emitter="p11"] {
  --ac-screen: #040a11;
  --ac-screen-raised: #0b1520;
  --ac-screen-well: #020409;

  --ac-emit-100: #8bc0ff;
  --ac-emit-90:  #00a0fb;
  --ac-emit-70:  #0088d7;
  --ac-emit-50:  #0069a8;
  --ac-emit-30:  #00385d;
  --ac-on-fill:  #03182a;

  --ac-halo-1: 87, 174, 255;
  --ac-halo-2: 70, 170, 255;
  --ac-halo-3: 56, 167, 255;
  --ac-halo-4: 119, 184, 255;

  /* Rayleigh: this phosphor throws 104% more light into the halo than neon. */
  --ac-halo-scatter: 2.04;
  --ac-halo-spread: 1.43;

  /* Persistence: 0.035ms to 10% (medium-short).
     Fully dark between frames — flicker at maximum, no persistence to see.
     --ac-flicker is the inter-frame modulation depth these two share; the
     amplitude budget that keeps it under the WCAG flash threshold is in
     tokens/effects.css, in one place, on purpose. */
  --ac-persist: 0.035ms;
  --ac-flicker: 1.000;
}

/* --- CRT / P31 — green.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The band data, its source
     and the reason this phosphor uses the band shape it does are in
     scripts/data/emitters.json.

     Band 513nm FWHM 78nm: x=0.2079 y=0.5295.
     Outside sRGB at the luminance --ac-emit-90 needs, so it is
     desaturated 45% toward D65 — the phosphor is paler here than it is in the tube.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.36:1 on --ac-emit-90. --- */
[data-ac-tech="crt"][data-ac-emitter="p31"] {
  --ac-screen: #010c06;
  --ac-screen-raised: #03180e;
  --ac-screen-well: #010503;

  --ac-emit-100: #00d791;
  --ac-emit-90:  #00af76;
  --ac-emit-70:  #009564;
  --ac-emit-50:  #00744c;
  --ac-emit-30:  #003e27;
  --ac-on-fill:  #001b0e;

  --ac-halo-1: 0, 235, 160;
  --ac-halo-2: 0, 223, 151;
  --ac-halo-3: 0, 210, 142;
  --ac-halo-4: 0, 254, 173;

  /* Rayleigh: this phosphor throws 67% more light into the halo than neon. */
  --ac-halo-scatter: 1.67;
  --ac-halo-spread: 1.29;

  /* Persistence: 0.038ms to 10% (medium-short).
     Fully dark between frames — flicker at maximum, no persistence to see.
     --ac-flicker is the inter-frame modulation depth these two share; the
     amplitude budget that keeps it under the WCAG flash threshold is in
     tokens/effects.css, in one place, on purpose. */
  --ac-persist: 0.038ms;
  --ac-flicker: 1.000;
}

/* --- CRT / P39 — yellow-green, long persistence.

     GENERATED by scripts/derive-gas.mjs. Do not edit these values by hand;
     `npm test` re-derives and fails on drift. The band data, its source
     and the reason this phosphor uses the band shape it does are in
     scripts/data/emitters.json.

     Band 529nm FWHM 52nm: x=0.2248 y=0.6962.
     Outside sRGB at the luminance --ac-emit-90 needs, so it is
     desaturated 54% toward D65 — the phosphor is paler here than it is in the tube.
     Stops solved to 10.5 / 7.0 / 5.2 / 3.4 / 1.63 : 1 against this palette's
     own --ac-screen; --ac-on-fill lands at 6.39:1 on --ac-emit-90. --- */
[data-ac-tech="crt"][data-ac-emitter="p39"] {
  --ac-screen: #000c03;
  --ac-screen-raised: #001908;
  --ac-screen-well: #000601;

  --ac-emit-100: #00d86e;
  --ac-emit-90:  #00b059;
  --ac-emit-70:  #00964b;
  --ac-emit-50:  #007538;
  --ac-emit-30:  #003f1b;
  --ac-on-fill:  #001a07;

  --ac-halo-1: 0, 237, 121;
  --ac-halo-2: 0, 225, 115;
  --ac-halo-3: 0, 212, 108;
  --ac-halo-4: 49, 255, 137;

  /* Rayleigh: this phosphor throws 62% more light into the halo than neon. */
  --ac-halo-scatter: 1.62;
  --ac-halo-spread: 1.27;

  /* Persistence: 400ms to 10%, visible tail to 2000ms (long).
     91% still lit when the next frame arrives.
     --ac-flicker is the inter-frame modulation depth these two share; the
     amplitude budget that keeps it under the WCAG flash threshold is in
     tokens/effects.css, in one place, on purpose. */
  --ac-persist: 2000ms;
  --ac-flicker: 0.091;

  /* Long enough that the next ON edge lands before this one has
     drained: the alarm throbs rather than chops. */
  --ac-blink-anim: ac-blink-decay-long;

  /* The decay curve itself, sampled from the Becquerel power law I0/(1+t/t0)^1.6.
     A shape no single cubic-bezier expresses, which is why it is a linear(). */
  --ac-decay-ease: linear(0, 0.118 0.5%, 0.197 0.9%, 0.313 1.6%, 0.465 2.9%, 0.632 5.3%, 0.781 9.5%, 0.889 17%, 0.953 31%, 0.985 56%, 1.000 100%);
}

/* Aliases and structural tokens — defined once per PALETTE SCOPE, for every
   palette. Anything that is the same shape regardless of which gas is in the
   panel belongs here; anything that is a color belongs in a palette block above.

   THE SELECTOR LIST IS NOT DECORATION, AND `:root` ALONE WAS A FOOTGUN. These
   are indirections — `--ac-ink: var(--ac-emit-90)` — and a var() is substituted
   AT THE ELEMENT THAT DECLARES IT, then inherits as a resolved color. Declared
   only on :root, they resolve once against whatever palette the ROOT carries and
   every descendant inherits that answer.
   So a consumer following the embed guidance in base/reset-scoped.css, who puts
   data-ac-tech / data-ac-emitter on .ac-root rather than on <html>, changed the
   five --ac-emit-* stops on that subtree and nothing else: --ac-ink, --ac-fill
   and --ac-stroke stayed resolved from :root's neon. Half a palette, silently —
   the surfaces moved and the text did not.
   .ac-root IS THE WHOLE FIX, AND THE ATTRIBUTE SELECTORS ARE DELIBERATELY NOT
   HERE. `[data-ac-tech][data-ac-emitter]` would cover every element that can
   carry a palette — and would weigh (0,2,0), which beats a consumer's own
   `:root { --ac-ink: … }` at (0,1,0) and silently breaks the one override the
   REFERENCE chapter tells people they may write. .ac-root weighs the same
   (0,1,0) as :root and wins for its subtree by PROXIMITY rather than by weight,
   which is exactly the behaviour wanted: closer scope, same authority.
   base/print.css carries .ac-root in its own re-point list for the same reason —
   drop it there and a scoped embed prints in amber. */
:root,
.ac-root {
  --ac-ink: var(--ac-emit-90);
  --ac-ink-bright: var(--ac-emit-100);
  --ac-ink-dim: var(--ac-emit-70);
  --ac-ink-faint: var(--ac-emit-50);
  --ac-ink-trace: var(--ac-emit-30);

  /* Inverse video (solid discharge block, dark text) */
  --ac-fill: var(--ac-emit-90);
  --ac-fill-bright: var(--ac-emit-100);

  --ac-stroke: var(--ac-emit-90);
  --ac-stroke-dim: var(--ac-emit-50);

  /* No red, no green. Alarm/danger = inverse video + blink, never a new hue. */
}

/* >>> src/amber-console.css -> tokens/typography.css */
:root {
  --ac-font-terminal: "VT323", "Courier New", ui-monospace, monospace;
  --ac-font-micro: "Silkscreen", "VT323", ui-monospace, monospace;

  /* VT323 is a bitmap face: it needs LARGE sizes. Never below 18px — below that
     the grid collapses and it turns to mud. --ac-type-micro is Silkscreen's,
     never VT323's; components/badge.css states the same rule from the other end.

     TWO DELIBERATE EXCEPTIONS, AND BOTH ARE GLYPHS RATHER THAN TEXT.
     components/select.css sets the dropdown chevron at 14px and
     components/nav.css sets the mark block at 16px, both in the terminal face.
     The floor exists because a bitmap face loses its letterforms when it is
     resampled small, and neither of these HAS a letterform to lose: they are
     solid geometry, one triangle and one filled square, and they read the same
     at any size. Nothing anybody has to READ goes below 18px. Add to that list
     only for another single non-alphanumeric glyph, and say so at the site. */
  --ac-type-display: 44px; /* page titles, banners */
  --ac-type-title: 30px;   /* panel/soft-key headers */
  --ac-type-body: 22px;    /* default UI text */
  --ac-type-small: 18px;   /* field labels, legends */
  --ac-type-micro: 8px;    /* Silkscreen only — bit labels, badges */

  --ac-tracking-display: 0.1em;
  --ac-tracking-body: 0.04em;
  --ac-tracking-micro: 0.08em;

  --ac-leading: 1.15; /* terminal rows sit tight */
}

/* >>> src/amber-console.css -> tokens/spacing.css */
:root {
  /* Character-cell grid: everything snaps to 4px half-cells */
  --ac-space-1: 4px;
  --ac-space-2: 8px;
  --ac-space-3: 12px;
  --ac-space-4: 16px;
  --ac-space-6: 24px;
  --ac-space-8: 32px;
  --ac-space-12: 48px;

  --ac-border-w: 2px;      /* every stroke is 2px — no hairlines */

  /* THE CORNER, QUANTIZED TO THE GRID. 2px is what a gas-discharge panel
     addressing a coarse cell matrix could actually draw, and it is the figure
     .ac-badge and .ac-banner have always used literally.

     This is the BASE value, not the whole story: components/classic.css spends
     it on a real chamfer where `corner-shape` exists, adds the extruded key
     edge, and provides the .ac-rounded opt-out. Those live there so this file
     stays what it says it is — a spacing scale.

     It is stated here rather than only there so that dropping classic.css
     degrades to a plain 2px corner instead of leaving --ac-radius undefined,
     which would make `border-radius: var(--ac-radius)` invalid and square off
     every control in the framework. */
  --ac-radius: 2px;
  --ac-radius-sm: 2px;
}

/* >>> src/amber-console.css -> tokens/effects.css */
:root {
  /* NOTHING IN THIS FILE NAMES A COLOR. Every rgba() is built from the --ac-halo-N
     triples in tokens/colors.css, which is what makes the display switch the
     panel and its glow together. Hardcode one and it keeps an amber corona
     around neon text. (On P7 the halo is a DIFFERENT spectrum from the ink, and
     that is correct rather than a bug — RATIONALE.md § P7.)

     The wide layers scale by --ac-halo-scatter / --ac-halo-spread, per palette, both
     falling back to 1. THE 2px LAYER IS NOT SCALED: it is the glyph's own lit
     edge, not scattered light. RADIUS NEVER SCALES WITH DRIVE — only alpha.

     THE DRIVE TIERS ARE ARITHMETIC, NOT TASTE. Each stop's luminance is already
     fixed by the contrast ratio it was solved to, so the ratios fall out:

       --ac-ink, --ac-stroke        emit-90   7.00:1   drive 1.00
       --ac-ink-dim              emit-70   5.20:1   drive 0.70
       --ac-ink-faint, -dim      emit-50   3.40:1   drive 0.41
       --ac-ink-trace            emit-30   1.63:1   drive 0.11 -> none

     THE TOKEN FAMILIES DO NOT LINE UP and the names actively mislead:
     --ac-ink-dim is emit-70 but --ac-stroke-dim is emit-50. The tier is chosen by
     DRIVE LEVEL, never by whether the token has "dim" in it.

     EACH TIER IS ITS OWN FULLY-RESOLVED TOKEN rather than one --ac-drive
     multiplier, and that is not a stylistic choice — the multiplier version
     fails SILENTLY. Before rebuilding it that way, read RATIONALE.md § Why the
     tiers are three tokens and not one multiplier. The derivation behind the
     table, and why the box tier is separate, are in the same section. */
  --ac-drive-dim: 0.7;
  --ac-drive-faint: 0.41;

  --ac-glow-text:
    0 0 2px rgba(var(--ac-halo-1), 0.6),
    0 0 calc(9px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.26 * var(--ac-halo-scatter, 1))),
    0 0 calc(24px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.1 * var(--ac-halo-scatter, 1)));
  --ac-glow-text-dim:
    0 0 2px rgba(var(--ac-halo-1), calc(0.6 * var(--ac-drive-dim))),
    0 0 calc(9px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.26 * var(--ac-halo-scatter, 1) * var(--ac-drive-dim))),
    0 0 calc(24px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.1 * var(--ac-halo-scatter, 1) * var(--ac-drive-dim)));
  --ac-glow-text-faint:
    0 0 2px rgba(var(--ac-halo-1), calc(0.6 * var(--ac-drive-faint))),
    0 0 calc(9px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.26 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint))),
    0 0 calc(24px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.1 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint)));

  --ac-glow-box:
    0 0 calc(8px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-1), calc(0.32 * var(--ac-halo-scatter, 1))),
    inset 0 0 10px rgba(var(--ac-halo-1), 0.07);
  --ac-glow-box-dim:
    0 0 calc(8px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-1), calc(0.32 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint))),
    inset 0 0 10px rgba(var(--ac-halo-1), calc(0.07 * var(--ac-drive-faint)));

  /* THE EXTRUDED KEY EDGE — the one shadow here that is NOT a halo, so it takes
     neither --ac-halo-spread nor --ac-halo-scatter: those describe light scattering
     through glass, and this is geometry. It stays the same size when the palette
     changes, while its color still follows --ac-halo-1.

     TWO LAYERS BECAUSE IT IS AN EDGE, NOT A GHOST: a bright 2px lip against the
     stroke, then 3px more at lower drive falling away from it. The alphas are
     set against the bloom rather than the screen — see RATIONALE.md § The
     extruded key edge.

     Consumed by components/classic.css and nothing else. Override to blunt or
     kill the extrusion wholesale; print does exactly that. */
  --ac-edge-3d:
    2px 2px 0 rgba(var(--ac-halo-1), 0.92),
    5px 5px 0 rgba(var(--ac-halo-1), 0.6);

  /* A KEY PRESSED IN, AND IT IS A RECESS RATHER THAN A SECOND BAND.

     This used to mirror --ac-edge-3d: the same bright discharge-colored band,
     moved to the top-left. Geometrically that was the sunken key's near wall and
     it was self-consistent — and it read wrong in both directions:

       Same-hue palettes  a pressed key is INVERSE VIDEO, so its fill is already
                          the discharge color. A band in that same color beside
                          that same fill is invisible: on neon a latched .ac-btn
                          --filled showed no 3D at all, which is the "some
                          buttons have no shadow" report.
       P7                 halo and ink are different spectra there, so the band
                          did show — as a green outline around a blue block.
                          That reads as a highlight, not a recess: "inverted
                          instead of depressed".

     A bright band cannot mean two opposite things. Raised is the only state that
     gets one, so the two can never be confused: PROUD = a lit face outside the
     box, PRESSED = a shadow inside it.

     INSET, so it paints over the fill rather than beside it, and the fill is
     always bright when this token is used — :active, --filled and aria-pressed
     all set inverse video. Positive offsets on an inset shadow paint the TOP and
     LEFT inner edges, which is where a recess shades.

     BLACK ALPHA RATHER THAN A PALETTE COLOR, on the same reasoning the cell mesh
     uses: over a lit fill it darkens, and over a dark surface it has nothing to
     take away. That makes it correct on all eleven palettes without naming one.

     A SHADOW RATHER THAN A TRANSFORM, and that is still load-bearing:
     `translate` DOES contribute to scrollable overflow, so every latched key
     flush against its container pushed 4px past it.
     See RATIONALE.md § The extruded key edge. */
  --ac-edge-3d-in:
    inset 2px 2px 0 rgba(0, 0, 0, 0.34),
    inset 5px 5px 0 rgba(0, 0, 0, 0.18);

  /* PERSISTENCE IS A PROPERTY OF THE EMITTER, not of this file. A palette that
     knows its own decay declares --ac-persist, --ac-flicker and a sampled
     --ac-decay-ease in tokens/colors.css; the tokens below are derived from it.
     This block is the fallback for every palette that declares nothing — all
     four gases — so they render unchanged at 105ms / 60ms.

     THE CAP IS THE WHOLE POINT OF min(), and it is a usability decision made
     against the physics on purpose: P7's tail is 3000ms, and a dialog painted
     for three seconds after it closes is a bug that can cite a source.

       --ac-decay        things that must actually LEAVE — [hidden], a closing
                         dialog. Capped at 250ms.
       --ac-decay-fast   de-energizing controls and hover release. Capped at
                         60ms — this fires on every control the pointer crosses.
       --ac-persist-tail ghosts and residual patches. NOT capped; pure
                         decoration, and where the long phosphors earn their keep.

     See RATIONALE.md § The persistence caps. */
  --ac-persist: 105ms;
  --ac-decay: min(var(--ac-persist), 250ms);
  --ac-decay-fast: min(var(--ac-persist), 60ms);
  --ac-persist-tail: var(--ac-persist);
  --ac-decay-ease: cubic-bezier(0.1, 0.72, 0.22, 1);

  /* HOW MUCH OF THE IMAGE IS GONE BY THE NEXT REFRESH, 0..1 — the other half of
     --ac-persist and not an independent knob. A palette that declares one
     declares both, from one decay constant, in scripts/derive-gas.mjs.
     1 is the default and means "no palette said", which resolves the flicker
     amplitude below to exactly the value it had before persistence existed. */
  --ac-flicker: 1;

  /* The cell matrix. A plasma panel is not a lit surface, it is a grid of gas
     gaps at wire intersections, and the barrier ribs between them do not emit.
     --ac-mesh-pitch is the cell spacing; --ac-mesh-wire is the per-axis alpha of
     one rib.

     THE ALPHA IS A BUDGET, NOT A TASTE SETTING. 0.075 lands the mean loss at
     0.050 — exactly what the CRT scanlines beside it cost, so turning plasma on
     costs the same legibility the tube already does. scripts/contrast.mjs cannot
     see an overlay, so nothing will fail if this is raised: redo the arithmetic
     first, in RATIONALE.md § The cell matrix alpha is a budget. */
  --ac-mesh-pitch: 3px;
  --ac-mesh-wire: 0.075;

  /* Blink, as an inherited pair rather than a selector list. base/blink.css
     holds the keyframe, the sites that read these, and the full contract. */
  --ac-blink-name: ac-blink;
  --ac-blink-ease: steps(1);
}

/* ---------------------------------------------------------------------------
   THE HALO IS INHERITED, NOT REQUESTED.

   The glow is light scattered in the glass, and the glass has no idea which
   component a lit cell belongs to. So it is applied ONCE here and inherits to
   every descendant, rather than being opted into per component.

   That direction matters more than it looks: when glow was opt-in, thirty-four
   elements were still rendering flat because nobody remembered to ask.
   Inheriting inverts the failure — a new component glows because it is lit.
   See RATIONALE.md § The halo is inherited, not requested.

   WHAT MUST THEN SUPPRESS IT, and why it is a short list rather than a long one:

     --ac-ink-dim / --ac-ink-faint   glow is the signal of energization, so a disabled
                               control that glows is a lie about the hardware.
     inverse video             dark text on a lit block is the UNLIT part of the
                               block. Unlit cells do not scatter.

   Both are enumerable, and scripts/check-prohibitions.mjs enforces the first —
   a rule that sets dim or faint ink without switching the halo off fails the
   build, because that is the half of this that drifts.

   SUPPRESSION IS ALWAYS `text-shadow: none`, NEVER A TOKEN OVERRIDE.
   var(--ac-glow-text) is resolved HERE, at the frame, and the resolved shadow list
   is what inherits — a descendant setting its own --ac-glow-text changes nothing.

   Print and forced colors need no rule of their own; both already blank it
   upstream. --------------------------------------------------------------- */
.ac-screen,
.ac-root {
  text-shadow: var(--ac-glow-text);
}


/* Blink is law-2 emphasis rather than a simulation — every display technology
   here blinks — and components/ depends on it, so it is base and not sim. */
/* >>> src/amber-console.css -> base/blink.css */
/* ===========================================================================
   Blink — law-2 emphasis, and the one animation that is NOT a simulation.

   Every display technology in this framework blinks, so switching it off says
   nothing about which hardware is being simulated. That is why it is base/ and
   not sim/: components/input.css and components/meter.css are blink sites and
   would lose their alarm if a consumer dropped the simulations.

   The decay variants of these keyframes live with the persistence engine, in
   sim/afterglow.css, because that is what softens the OFF edge.
   =========================================================================== */


/* BLINK IS DRIVEN BY TWO INHERITED TOKENS, NOT BY A SELECTOR LIST.
   ---------------------------------------------------------------------------
   Every blink site reads the same pair, and every environment that wants to
   change blink sets the pair ONCE instead of restating where the blink sites
   are. Custom properties inherit, so a declaration on the root or on the frame
   reaches all of them without a specificity fight — which is what the four
   mirrored copies of this list used to be paying for.

     --ac-blink-name   which keyframe, or `none` to stop blinking
     --ac-blink-ease   its timing function

     --ac-blink-force  the same two, but reserved for the ROOT. Read FIRST at
     --ac-blink-force-ease   every site, and never declared on .ac-afterglow, so
                       an environment override cannot lose to the simulation
                       simply for being further from the element. That inversion
                       is exactly what the doubled `[data-ac-style-blink="off"]
                       .ac-afterglow ...` selectors existed to defeat.

   ADDING A BLINK SITE is now one declaration pair on the site itself. Nothing
   else in this file, and nothing in print.css or a11y.css, has to learn it.

   The two defaults are declared in tokens/effects.css, NOT here. This file is the
   contract and the keyframes; the token block that seeds them sits with the other
   tokens, because a consumer overriding blink overrides it there. */

/* Hard on/off blink — no fades on a plasma panel */
@keyframes ac-blink { 0%, 54% { opacity: 1; } 55%, 100% { opacity: 0.1; } }
.ac-blink {
  animation-name: var(--ac-blink-force, var(--ac-blink-name));
  animation-duration: 1.1s;
  animation-timing-function: var(--ac-blink-force-ease, var(--ac-blink-ease));
  animation-iteration-count: infinite;
}

/* Block cursor — same pair, its own 1s cycle. */
.ac-cursor::after {
  content: "█";
  margin-left: 2px;
  animation-name: var(--ac-blink-force, var(--ac-blink-name));
  animation-duration: 1s;
  animation-timing-function: var(--ac-blink-force-ease, var(--ac-blink-ease));
  animation-iteration-count: infinite;
}

/* A STYLE FLAG, NOT A SIMULATION. Every display technology here blinks, so
   turning it off says nothing about which one is being simulated.

   Off settles LIT, not dark: the keyframe's dark half is 0.1 opacity, and a
   blink stopped there is an element that has silently vanished. Once blink is
   off, the inverse fill and the ✳✳ glyphs are what carry an alarm.

   ONE DECLARATION, on the root, where the flag already is. It uses the -force
   pair rather than --ac-blink-name so that switching the CRT simulation on —
   which declares --ac-blink-name on the nearer .ac-afterglow frame — cannot
   quietly restart a blink the user has turned off. */
[data-ac-style-blink="off"] {
  --ac-blink-force: none;
}


/* --- The screen simulations ---

   ONE ORDERING RULE, AND ONLY ONE: sim/frame.css must come LAST. It holds the
   shared stacking normalisation, the viewport anchoring and the
   prefers-reduced-motion block, all of which override the three hardware files.
   Move it earlier and the simulations paint through each other.

   Everything above it can be reordered freely. sim/screen.css is first because
   the z-index table it documents reads best before the files that spend it —
   that is a reading order, not a cascade requirement.

   The three in between are independent and any of them can simply be deleted.
   That is the point of the split: afterglow alone is ~490 lines and a backdrop-
   filter over the viewport, and a consumer who wants the look without the
   persistence engine could not previously drop it. Selectors left behind in
   screen.css and frame.css for a file you removed match nothing and cost
   nothing. --- */
/* >>> src/amber-console.css -> sim/screen.css */
/* ===========================================================================
   Screen — the z-index contract the three simulations share, plus the one
   static texture that is not a simulation at all.

   ITS POSITION IS NOT LOAD-BEARING, and an earlier version of this header
   claimed otherwise. The z-index budget below is DOCUMENTATION — a comment, not
   a rule — and the only two rules in this file style `.ac-scanlines`, a static
   texture nothing else in sim/ touches. Reorder this file freely.

   sim/frame.css is the one that genuinely must stay last.
   =========================================================================== */


/* ---------------------------------------------------------------------------
   Three independent screen simulations, all applied to the outermost frame:

   .ac-bloom      PLASMA — the panel itself. Amplifies the glow tokens for every
                  descendant, lays a soft bleed layer over it, and rules it into
                  the cell matrix the gas actually sits in.
   .ac-crt        CRT — the tube itself: scanline texture, vignette, slow line
                  drift, retrace sweep, faint mains flicker.
   .ac-afterglow  PERSISTENCE — the panel does not go dark instantly. Anything
                  that disappears decays, live text leaves a ghost of the value
                  it just replaced, the glass holds faint uneven patches, and the
                  image smears while you scroll.

   Three classes, but the demos drive them from two switches: .ac-afterglow rides
   with .ac-crt, because persistence is a property of the same glass the
   scanlines are on. Each still works alone if you wire it yourself. All stop
   under prefers-reduced-motion.

   THE STACK — stated here in one place, because a sticky bar carrying its own
   z-index will otherwise punch straight through the simulations:

     1   frame content
     20  sticky chrome — .ac-nav--sticky, and anything else that outranks content
     40  .ac-bloom::before   plasma bleed
     45  .ac-retrace         CRT retrace band
     45  .ac-persist         afterglow ghosts and residual patches
     48  .ac-mesh            plasma cell mesh
     50  .ac-crt::after      scanlines and vignette

   Everything from 40 up is above the sticky bar on purpose: a screen simulation
   that stops at the edge of the menu bar is not simulating a screen.
   --------------------------------------------------------------------------- */

/* Static scanline texture only (no motion) — for print, thumbnails, cards */
.ac-scanlines { position: relative; }
.ac-scanlines::after {
  content: "";
  position: absolute; inset: 0; z-index: 6;
  pointer-events: none;
  background: repeating-linear-gradient(0deg, rgba(0,0,0,0.17) 0 1px, transparent 1px 3px);
}

/* >>> src/amber-console.css -> sim/plasma.css */
/* ===========================================================================
   PLASMA — the panel itself: the bloom, the cell mesh, and the shimmer of an
   AC sustain. Independent of sim/crt.css and sim/afterglow.css; delete this
   file and .ac-bloom simply stops doing anything.
   =========================================================================== */


/* --- PLASMA --- */
.ac-bloom {
  position: relative;
  /* Same split as the base tokens: the 2px core is the lit cell, everything
     wider is scattered light and answers to --ac-halo-scatter / --ac-halo-spread. This
     is where the difference between the gases actually shows, because the bloom
     is where the halo is big enough to see. */
  /* EVERY TIER IS RESTATED HERE, not just the full one. The bloom redefines the
     glow tokens on the frame, which is a closer ancestor than :root and so wins
     for every descendant — so a tier left undeclared here would keep its :root
     value and a dim label would end up glowing HARDER on a plasma panel than the
     bright text beside it. The bloom changes how much light the glass throws,
     not how hard the cell behind it is driven. */
  --ac-glow-text:
    0 0 2px rgba(var(--ac-halo-4), 0.75),
    0 0 calc(9px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.4 * var(--ac-halo-scatter, 1))),
    0 0 calc(26px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.2 * var(--ac-halo-scatter, 1))),
    0 0 calc(56px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.1 * var(--ac-halo-scatter, 1)));
  --ac-glow-text-dim:
    0 0 2px rgba(var(--ac-halo-4), calc(0.75 * var(--ac-drive-dim))),
    0 0 calc(9px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.4 * var(--ac-halo-scatter, 1) * var(--ac-drive-dim))),
    0 0 calc(26px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.2 * var(--ac-halo-scatter, 1) * var(--ac-drive-dim))),
    0 0 calc(56px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.1 * var(--ac-halo-scatter, 1) * var(--ac-drive-dim)));
  --ac-glow-text-faint:
    0 0 2px rgba(var(--ac-halo-4), calc(0.75 * var(--ac-drive-faint))),
    0 0 calc(9px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.4 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint))),
    0 0 calc(26px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.2 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint))),
    0 0 calc(56px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-3), calc(0.1 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint)));

  --ac-glow-box:
    0 0 calc(11px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-1), calc(0.4 * var(--ac-halo-scatter, 1))),
    0 0 calc(34px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.16 * var(--ac-halo-scatter, 1))),
    inset 0 0 14px rgba(var(--ac-halo-1), 0.1);
  --ac-glow-box-dim:
    0 0 calc(11px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-1), calc(0.4 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint))),
    0 0 calc(34px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-2), calc(0.16 * var(--ac-halo-scatter, 1) * var(--ac-drive-faint))),
    inset 0 0 14px rgba(var(--ac-halo-1), calc(0.1 * var(--ac-drive-faint)));
}
.ac-bloom::before {
  content: "";
  position: absolute; inset: 0; z-index: 40;
  pointer-events: none;
  backdrop-filter: blur(3.5px) brightness(1.22) saturate(1.08);
  -webkit-backdrop-filter: blur(3.5px) brightness(1.22) saturate(1.08);
  mix-blend-mode: screen;
  opacity: 0.34;
  mask-image: radial-gradient(125% 125% at 50% 48%, #000 50%, rgba(0,0,0,0.4) 100%);
  -webkit-mask-image: radial-gradient(125% 125% at 50% 48%, #000 50%, rgba(0,0,0,0.4) 100%);
  animation: ac-bloom-breathe 9s ease-in-out infinite;
}
/* Mains-hum breathing — barely perceptible, just enough to feel alive */
@keyframes ac-bloom-breathe {
  0%, 100% { opacity: 0.31; }
  50%      { opacity: 0.37; }
}

/* THE SCREEN DOOR — a crossed mesh, not a stack of horizontal lines. A cell
   lights at the intersection of two wires, boxed in by barrier ribs that emit
   nothing; the horizontal blanking gaps belong to .ac-crt and the tube it
   simulates. There is no scan here, so there is nothing to blank.

   THE RIBS ARE PURE BLACK RATHER THAN TINTED, and that is the whole trick: over
   the near-black unlit panel black alpha has nothing to take away, so the grid
   appears only where the panel is lit — for free.

   z-index 48 puts it ABOVE the bleed, which is upside down physically and is a
   deliberate performance trade. It is A CHILD NODE, not .ac-bloom::after,
   because .ac-crt::after already owns that pseudo-element for the scanlines and
   would silently replace a mesh declared there. Both explained in
   RATIONALE.md § The screen door. */
.ac-bloom > .ac-mesh {
  position: absolute; inset: 0; z-index: 48;
  pointer-events: none;
  background:
    repeating-linear-gradient(0deg,  rgba(0,0,0,var(--ac-mesh-wire)) 0 1px, transparent 1px var(--ac-mesh-pitch)),
    repeating-linear-gradient(90deg, rgba(0,0,0,var(--ac-mesh-wire)) 0 1px, transparent 1px var(--ac-mesh-pitch));
  animation:
    ac-mesh-buzz 0.29s steps(1, end) infinite,
    ac-mesh-hum 0.37s ease-in-out infinite;
}

/* THE PLASMA SHIMMER — the residue of an AC sustain, which reads as the panel
   being energized rather than merely bright.

   TWO ANIMATIONS, DETUNED, and the periods are load-bearing: 0.29 and 0.37 are
   coprime, so the pair does not repeat for 10.7s, and neither divides into
   ac-crt-hum (5.5s), ac-bloom-breathe (9s), ac-crt-drift (11s) or ac-retrace
   (13s). RETUNE THEM AND ALL FOUR RELATIONSHIPS MUST SURVIVE, or the panel finds
   a beat and starts throbbing.

   Both rest at their zero frame — translate 0, opacity 1. The screenshot harness
   freezes animation, so whatever sits at 0% is what gets baselined. */

/* Sub-pixel vibration of the mesh. steps(1, end) so each position is held and
   then jumped, never interpolated — a wire that eases between two places is a
   wobble. The 0.35px ceiling keeps the wires sharp; past about half a pixel the
   resampling turns the mesh gray and the screen door is gone. */
@keyframes ac-mesh-buzz {
  0%   { transform: translate3d(0, 0, 0); }
  14%  { transform: translate3d(0.28px, -0.16px, 0); }
  28%  { transform: translate3d(-0.19px, 0.31px, 0); }
  43%  { transform: translate3d(0.33px, 0.22px, 0); }
  57%  { transform: translate3d(-0.3px, -0.27px, 0); }
  71%  { transform: translate3d(0.15px, 0.34px, 0); }
  86%  { transform: translate3d(-0.34px, 0.11px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}
/* …and the discharge itself is not perfectly even. A 1.8% swing on a layer whose
   own mean darkening is 5% moves panel luminance by about 0.09%, three orders
   below the WCAG 2.3.1 general flash threshold (>3/sec at >=10% relative
   luminance over >25% of the field). That is a ceiling, not a starting point. */
@keyframes ac-mesh-hum {
  0%, 100% { opacity: 1; }
  23%      { opacity: 0.988; }
  47%      { opacity: 1; }
  61%      { opacity: 0.982; }
  79%      { opacity: 0.994; }
}

/* >>> src/amber-console.css -> sim/crt.css */
/* ===========================================================================
   CRT — the tube itself: scanline texture, vignette, line drift, the retrace
   sweep and the mains flicker. Independent of the other two simulations.
   =========================================================================== */


/* --- CRT --- */
.ac-crt { position: relative; }
/* Scanline texture + vignette, drifting one cell every 11s.

   THE HUM IS A BACKGROUND-COLOR ON THIS LAYER RATHER THAN ITS OPACITY, and the
   difference is the whole point of the flicker being visible at all. This
   element is a DARKENING layer — a black vignette and black scanlines — so
   fading it out made the panel BRIGHTER, which is backwards for a mains sag, and
   what moved was 3.5% of an overlay whose own mean darkening is about 15%: half
   a percent of panel luminance, in the wrong direction. Filling the same layer
   with black instead removes light directly, so the same 3.5% constant is 3.5%
   of the picture and the scanlines stop breathing along with it.

   It costs nothing extra: ac-crt-drift already animates background-position on
   this element, so the layer is being repainted every frame regardless.

   It is NOT a `::before` on .ac-crt, which is what a separate wash layer would
   want. .ac-bloom::before is the plasma bleed, and a frame is allowed to carry
   both classes — screen.css calls the three simulations independent and means
   it — so the two would be one pseudo-element fighting over a backdrop-filter. */
.ac-crt::after {
  content: "";
  position: absolute; inset: 0; z-index: 50;
  pointer-events: none;
  background-color: rgba(0,0,0,0);
  background-image:
    radial-gradient(135% 125% at 50% 50%, transparent 58%, rgba(0,0,0,0.34) 100%),
    repeating-linear-gradient(0deg, rgba(0,0,0,0.15) 0 1px, transparent 1px 3px);
  animation: ac-crt-drift 11s linear infinite, ac-crt-hum 5.5s ease-in-out infinite;
}
@keyframes ac-crt-drift {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 0 0, 0 -3px; }
}
/* MAINS FLICKER, SCALED BY THE PHOSPHOR — and the first thing to say is what
   this is NOT, because the obvious reading of it is wrong.

   THE ACTUAL FLICKER OF A CRT CANNOT BE RENDERED HERE, EVER — a 60Hz modulation
   watched on a 60Hz display is sampling a signal at its own frequency. That is
   Nyquist, not a browser limitation. What IS renderable is the perceptual
   signature, so the FREQUENCY is fixed and slow and only the AMPLITUDE answers
   to the hardware, via --ac-flicker:

     P11, P31, P4   1.000  fully dark between frames. Maximum unsteadiness.
     P1, P3         ~0.79  medium persistence, visibly less settled.
     P7, P39        0.091  the long phosphors — rock steady at any refresh rate,
                           paid for in smear. Both halves, from one number.

   AND THE OTHER HALF IS SPATIAL, which is the part this keyframe cannot carry.
   See the retrace band below: temporal amplitude says how hard the picture
   breathes, the band says WHERE the tube has already gone dark, and only the
   second one is really what a short phosphor looks like.

   THE 0.035 IS A WCAG 2.3.1 BUDGET, NOT A TASTE SETTING, and the margins are
   large on purpose. Raising it spends them; redo the arithmetic first, exactly
   as the --ac-mesh-wire note above demands — RATIONALE.md § The three flash
   budgets. The constant is unchanged from when this animated opacity; what
   changed is that it now lands on the picture instead of on the vignette, which
   is a sevenfold gain in what you can see for no gain in what it spends. */
@keyframes ac-crt-hum {
  0%, 100% { background-color: rgba(0,0,0,0); }
  31%      { background-color: rgba(0,0,0,calc(0.035 * var(--ac-flicker, 1))); }
  32%      { background-color: rgba(0,0,0,0); }
  68%      { background-color: rgba(0,0,0,calc(0.015 * var(--ac-flicker, 1))); }
}

/* THE RETRACE BAND — the unsteadiness of a short phosphor, rendered SPATIALLY
   because temporally it cannot be rendered at all.

   The refresh itself is unwatchable here for the Nyquist reason above. But the
   thing a short phosphor actually did was never the whole screen blinking: at
   any instant only the band the beam had just written was at full brightness,
   and everything behind it had already decayed. That is the dark bar you see
   when a phone camera looks at a CRT, and being SPATIAL it can move as slowly as
   you like and still read — there is no frequency to alias.

   SO THE BAND IS THE RADAR WAKE IN ONE DIMENSION: it moves down the frame, its
   leading (lower) edge is freshly written and bright, and what trails above it
   is the trough where the phosphor has given up. --ac-flicker sets how deep the
   trough goes and how hot the edge is, so P7 and P39 keep the almost-invisible
   lightening sweep this was before, and P11, P31 and P4 get a band you can
   watch. That is the emitter axis finally rendering the difference it claims.

   NORMAL BLENDING, NOT `screen`, and it is the trough that forces it: screen
   only ever adds light, so the dark half of the gradient would be a no-op — and
   a black child inside a screen-blended element is a no-op too, so the trough
   cannot be a pseudo-element hiding inside this one either. The cost is that the
   bright edge alpha-blends rather than adding: at its strongest it moves the
   brightest glyph in the system by about one part in 255, toward the halo color
   it is already glowing in — half of what it was before the amplitude came down.
   The gain is one fewer mix-blend-mode on the page, which a11y.css and print.css
   both have notes about.

   THE HEIGHT DOES NOT SCALE WITH --ac-flicker even though the decayed region
   physically would. ac-retrace translates by percentages of this element's OWN
   box, so a taller band travels further in the same 13s and starts spending most
   of the cycle off-screen — the timing would become a property of the emitter,
   and RATIONALE.md § the detune has four other periods depending on this one
   staying put. Amplitude carries it instead.

   AND THE AMPLITUDE IS HALF WHAT IT WAS, which is a correction rather than a
   taste change. The previous constants — a 8.5% trough under a 10.6% hot edge at
   full flicker — made the band the first thing anybody saw and the last thing
   they stopped seeing: a bar sweeping the page, read as an overlay playing over
   the panel rather than as the panel's own unevenness. The thing being simulated
   is a REGION THAT HAS DECAYED, which is a gradient in brightness across a
   surface, not an object crossing it. At these levels it is something you notice
   on a pale readout and stop noticing on text, which is where it belongs; the
   shape, the timing and the --ac-flicker scaling are all unchanged, so P11 still
   gets several times what P39 gets and the emitter axis still renders. Somebody
   who wants the old reading has one switch — see the flag at the foot of this
   file — and it is a better default to arrive under. */
.ac-crt > .ac-retrace {
  position: absolute; left: 0; right: 0; top: 0; height: 26%;
  z-index: 45; pointer-events: none;
  background: linear-gradient(180deg,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,calc(0.042 * var(--ac-flicker, 1))) 30%,
    rgba(0,0,0,calc(0.025 * var(--ac-flicker, 1))) 58%,
    rgba(var(--ac-halo-1), calc(0.005 + 0.027 * var(--ac-flicker, 1))) 78%,
    rgba(var(--ac-halo-4), calc(0.008 + 0.045 * var(--ac-flicker, 1))) 90%,
    rgba(0,0,0,0) 100%);
  animation: ac-retrace 13s linear infinite;
}
@keyframes ac-retrace {
  0%   { transform: translateY(-110%); }
  100% { transform: translateY(500%); }
}

/* THE BAND IS OPTIONAL, AND IT IS THE ONLY EFFECT HERE THAT IS.
   Everything else on this file is texture: the scanlines, the vignette and the
   hum are the shape and the state of the tube, and they sit still. The band
   TRAVELS — 26% of the frame, across all of it, every 13s, over the one surface
   somebody is trying to read. It is by a wide margin the largest moving object
   in the system, and "I would rather that did not sweep past while I read" is a
   comfort preference in exactly the way the blink flag is.

   TWO WAYS TO SAY IT, and they are for two different people:

     <html data-ac-style-retrace="off">   the whole page. What the demo switch
                                          writes; a viewer preference, and the
                                          one amber-console.js persists.
     …just do not ship the child          the band is a <span class="ac-retrace">
                                          you put in the frame yourself. Leave it
                                          out and there is nothing to switch off.
                                          The author's answer, and it costs a
                                          node rather than a rule.

   The attribute exists because the SCRIPT mounts that child when the CRT
   simulation comes on (SIMS.crt.children in amber-console.js), so a page driving
   the simulation from the toggle cannot express "CRT, but not the band" by
   omission alone. It hides rather than unmounts, so flipping the flag is a style
   recalculation and not a remount of the simulation.

   0,3,0 against the 0,2,0 of the rule above, which is what it takes to win —
   `:root[…]` is an element plus an attribute. It agrees with the reduced-motion
   `display: none` in frame.css rather than fighting it: both say the same word,
   so whichever matches, the band is gone. */
:root[data-ac-style-retrace="off"] .ac-retrace { display: none; }

/* >>> src/amber-console.css -> sim/afterglow.css */
/* ===========================================================================
   AFTERGLOW — the persistence engine, and by a wide margin the most expensive
   thing in the framework: ~490 lines, a viewport-sized backdrop-filter, and a
   registered @property. It is also the most droppable — nothing else needs it.

   THE ONE FILE ALLOWED TO HOLD A `transition`, together with sim/frame.css.
   scripts/check-prohibitions.mjs names them both explicitly, and it names only
   them: a screen redrawn at the refresh has no in-between frames, but the decay
   of a phosphor genuinely is a timed change. See RATIONALE.md § Persistence is
   three phenomena.
   =========================================================================== */

/* --- PLASMA AFTERGLOW ---

   Three things, because "persistence" is three different phenomena and only one
   of them is expressible without JavaScript:

     1. DECAY-OUT. Anything that disappears relaxes instead of switching off.
        Pure CSS, below. This is the one that carries the effect on its own.
     2. GHOSTING. Text that is rewritten leaves the previous value behind for a
        beat. Needs the old value, which only JS has — amber-console.js mounts
        those into .ac-persist. The effect degrades to (1) and (3) without it.
     3. RESIDUAL PATCHES. The glass never sits perfectly uniform; charge relaxes
        unevenly and slowly. Pure CSS, on .ac-persist::before.

   Note the direction: this decays OUT and never in. Nothing here fades a thing
   into existence. A cell lights on the next refresh — instantly — and it is only
   the switching OFF that hardware cannot do sharply. That asymmetry is the whole
   reason this file may hold a `transition` at all while src/components/ may not;
   see the header of stylelint.config.mjs. */
.ac-afterglow {
  position: relative;
  /* Persistence softens the blink's OFF edge — see (4) below. Declared here as
     an inherited pair so it reaches every blink site without naming one. */
  --ac-blink-name: var(--ac-blink-anim, ac-blink-decay);
  --ac-blink-ease: linear;
}

/* (1) DECAY-OUT.

   The transition lives on the DESTINATION state, which is the whole trick:
   transition properties are read from the after-change style, so declaring them
   only on the hidden half means hiding decays and showing snaps back instantly.

   `display` is transitioned with allow-discrete so `display: none` is deferred
   to the end of the decay rather than cutting it off on the first frame. The
   blur is the gap losing definition as it drains — a discharge dies soft, not
   sharp. */
.ac-afterglow [hidden],
.ac-afterglow dialog:not([open]) {
  opacity: 0;
  filter: blur(0.4px) brightness(1.16);
  transition:
    opacity var(--ac-decay) var(--ac-decay-ease),
    filter var(--ac-decay) var(--ac-decay-ease),
    display var(--ac-decay) allow-discrete,
    overlay var(--ac-decay) allow-discrete;
}

/* (2) + (3) The persistence layer. One child element hosts all of it, so a frame
   needs exactly one extra node — same contract as .ac-crt and .ac-retrace.

   `screen` is the physically right blend: emitted light adds, it never
   occludes, so a ghost can only ever brighten what is under it. Note it is set
   on each child rather than on this element, and that is not a style choice —
   mix-blend-mode makes an element a BACKDROP ROOT, and a backdrop root resets
   what `backdrop-filter` inside it can see. Put the blend here and the scroll
   smear below samples an empty backdrop and silently does nothing. */
.ac-afterglow > .ac-persist {
  position: absolute; inset: 0;
  z-index: 45;
  pointer-events: none;
  overflow: hidden;
}

/* (3) RESIDUAL PATCHES — four slow, overlapping pools of charge that never
   quite line up, so the panel is never evenly lit. At 3% over the amber this is
   below the threshold of noticing directly; you only see it when it stops.

   The drift is background-position over an oversized background rather than a
   transform on an oversized box, deliberately: a box larger than its parent adds
   scrollable overflow, and the frame is exactly where a stray 288px of it would
   go unnoticed. A background cannot overflow anything. */
.ac-afterglow > .ac-persist::before {
  content: "";
  position: absolute; inset: 0;
  background:
    radial-gradient(38% 30% at 22% 28%, rgba(var(--ac-halo-1), 0.032), transparent 70%),
    radial-gradient(46% 34% at 78% 62%, rgba(var(--ac-halo-2), 0.026), transparent 72%),
    radial-gradient(30% 44% at 58% 12%, rgba(var(--ac-halo-4), 0.022), transparent 68%),
    radial-gradient(52% 40% at 34% 84%, rgba(var(--ac-halo-3), 0.028), transparent 74%);
  background-size: 150% 150%;
  background-repeat: no-repeat;
  mix-blend-mode: screen;
  animation: ac-persist-drift 47s ease-in-out infinite;
}
@keyframes ac-persist-drift {
  0%, 100% { background-position: 22% 34%; opacity: 0.85; }
  33%      { background-position: 36% 20%; opacity: 1; }
  66%      { background-position: 14% 46%; opacity: 0.9; }
}

/* (4) THE BLINK OFF-EDGE.

   A blinking alarm is the most visible thing on the panel that stops being
   driven, over and over, once a second — so it is the last place the decay
   should be missing. .ac-blink is steps(1) by default and snaps both ways; with
   persistence on, only the ON edge stays hard. A cell lights on the next
   refresh and then relaxes, so: instant up, curve down.

   Retuning the TOKEN PAIR on the frame rather than naming the blink sites is
   what lets one rule cover every one of them — the cursor runs a 1s cycle and
   the rest run 1.1s, and each keeps its own duration. Keyframe stops cannot read
   a custom property, so the window below is a percentage of whatever cycle it
   lands in rather than a duration: 54%->62% is ~88ms at 1.1s and ~80ms on the
   cursor's 1s. Retuning --ac-decay does not move it; the stops are the handle.

   --ac-blink-anim comes from the PALETTE — a long phosphor points it at the
   throb curve below — so this file still never names one. The pair is declared
   on .ac-afterglow itself, where the frame's `position: relative` is. */
/* The stops are spread wider than a literal decay curve would be. A true
   exponential loses most of its light in the first ~80ms, which is close enough
   to the flicker-fusion threshold that the eye files it as a hard switch — the
   decay measures correctly and still looks instant. This holds, then drains over
   the 54%->62% window — ~88ms of the 1.1s cycle — which sits right at the
   threshold described above: the alarm still reads as a hard blink, with the edge
   taken off it rather than a visible fade. The opacity ramp carries it;
   the blur only stops the edge staying razor-sharp on the way down. The ON edge
   is still a single-frame snap. */
@keyframes ac-blink-decay {
  0%, 54%   { opacity: 1; filter: blur(0); }
  56%       { opacity: 0.72; filter: blur(0.08px); }
  58%       { opacity: 0.48; filter: blur(0.16px); }
  60%       { opacity: 0.30; filter: blur(0.22px); }
  61%       { opacity: 0.17; filter: blur(0.27px); }
  62%, 100% { opacity: 0.1; filter: blur(0.3px); }
}

/* THE SAME EDGE ON A LONG PHOSPHOR, which is a different edge entirely.
   The window above is ~88ms of a 1.1s cycle — right for a gas gap and for the
   medium phosphors, and wrong by more than an order of magnitude for P7 and P39,
   whose light is still visible seconds after the drive stops. An alarm on a P39
   screen does not blink so much as pulse: it never reaches the dark half at all,
   because the next ON edge arrives while the previous one is still draining.

   Selected by the PALETTE, not by this file, on the same indirection
   --ac-ghost-anim already uses: keyframe offsets cannot read a custom property,
   so the shape has to be a second keyframe rather than a retuned one, and
   scripts/derive-gas.mjs points the long-class emitters at it. Everything else
   falls through the var() fallback above and is untouched.

   Note where it bottoms out: 0.34, not 0.1. That IS the effect — the floor of a
   long phosphor's blink is lifted because the cell never fully relaxes between
   strikes, and the alarm reads as a throb rather than a chop. */
@keyframes ac-blink-decay-long {
  0%, 54%   { opacity: 1; filter: blur(0); }
  60%       { opacity: 0.88; filter: blur(0.10px); }
  68%       { opacity: 0.72; filter: blur(0.18px); }
  78%       { opacity: 0.57; filter: blur(0.26px); }
  88%       { opacity: 0.44; filter: blur(0.34px); }
  100%      { opacity: 0.34; filter: blur(0.4px); }
}

/* (5) DE-ENERGIZING — a lit cell that goes unlit.

   The single most common light-off event on a control panel is not a node
   disappearing, it is a lamp going out: a pressed button releasing, a tab
   deselecting, an interlock unchecking. Those elements never hide, so the
   decay-out rule above never saw them, and they were snapping off in one frame
   while everything around them relaxed.

   Same mechanism as decay-out, same direction rule: the transition is declared
   on the UNLIT state, so de-energizing decays and energizing is instant.

   On --ac-decay-fast rather than --ac-decay, and that is the difference between
   subtle and distracting. Hover lands here too — moving off a control is a
   de-energization — which means this fires on every control the pointer crosses,
   not just on the ones you press. At the full decay a sweep across a dense panel
   leaves a wake behind the cursor. At the fast one it reads as the glow simply
   taking a moment to let go.

   These live here rather than in the component files on purpose: components stay
   free of `transition`, which is what scripts/check-prohibitions.mjs enforces. */
.ac-afterglow .ac-btn,
.ac-afterglow .ac-tab,
.ac-afterglow .ac-nav__link,
.ac-afterglow .ac-badge,
.ac-afterglow .ac-banner,
.ac-afterglow .ac-check input,
.ac-afterglow .ac-radio input,
.ac-afterglow .ac-toggle__track,
.ac-afterglow .ac-toggle__thumb,
/* The bar was missing from this list entirely, which is why an alarm clearing
   used to snap. Its WIDTH is deliberately not transitioned — that is the live
   edge and it must stay instant; only its color and halo relax. The vacated
   region is (5c)'s job. */
.ac-afterglow .ac-meter__bar,
.ac-afterglow .ac-table__row--active td {
  transition:
    background-color var(--ac-decay-fast) var(--ac-decay-ease),
    border-color var(--ac-decay-fast) var(--ac-decay-ease),
    color var(--ac-decay-fast) var(--ac-decay-ease),
    box-shadow var(--ac-decay-fast) var(--ac-decay-ease),
    text-shadow var(--ac-decay-fast) var(--ac-decay-ease),
    --ac-lit var(--ac-persist-tail) var(--ac-decay-ease);
}

/* (5b) THE AFTERIMAGE — the part a long phosphor actually shows you.
   ------------------------------------------------------------------
   The two halves are split by what they describe, which is also the physical
   split: a control's GEOMETRY snaps on --ac-decay-fast, because a button must
   read as released the instant you let go, and the LIGHT it was emitting decays
   on --ac-persist-tail, uncapped.

   A REGISTERED NUMBER RATHER THAN A box-shadow TRANSITION, so the components
   keep their own shadows and one variable drives the afterimage's alpha and its
   halo radius together. HOVER IS DELIBERATELY NOT IN THE LIT LIST — at a 2s tail
   it leaves a wake of glowing boxes behind the cursor. Both in
   RATIONALE.md § The afterimage. */
/* INHERITS, AND IT HAS TO: the afterimage is a ::after, and a pseudo-element
   does not read its originator's non-inherited properties. With `inherits:
   false` the halo resolved to alpha 0 at every drive level, which looks exactly
   like the effect being off. */
@property --ac-lit {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

/* The lit states. Instant on: no transition is declared here, so --ac-lit jumps
   to 1 and only the release is timed by the rule above. */
.ac-afterglow .ac-btn--filled,
.ac-afterglow .ac-btn[aria-pressed="true"],
.ac-afterglow .ac-tab--active,
.ac-afterglow .ac-tab[aria-selected="true"],
.ac-afterglow .ac-nav__link[aria-current="page"],
.ac-afterglow .ac-badge--filled,
.ac-afterglow .ac-banner--filled,
.ac-afterglow .ac-check input:checked,
.ac-afterglow .ac-radio input:checked,
.ac-afterglow .ac-toggle--on .ac-toggle__thumb,
.ac-afterglow .ac-toggle[aria-pressed="true"] .ac-toggle__thumb,
.ac-afterglow .ac-table__row--active td {
  --ac-lit: 1;
}

/* THE AFTERIMAGE LAYER ITSELF, and it is a short list for a mechanical reason.
   An element has exactly one ::after, and four components have already spent
   theirs: .ac-toggle (thumb), .ac-banner (the ✳✳ ornaments, both pseudos),
   .ac-select (the ▼) and .ac-spinner. Declaring one here would silently replace
   those — the same trap .ac-crt::after and .ac-bloom::after already document.
   .ac-check and .ac-radio light the <input> itself, and a replaced element
   renders no pseudo-element at all.

   Those all still de-energize; they just do it through --ac-lit driving nothing
   but their own halo above, rather than through a shape-accurate afterimage.
   The five below are the ones where the lit area is a solid block big enough for
   the difference to matter, and where ::after was free. */
.ac-afterglow .ac-btn::after,
.ac-afterglow .ac-tab::after,
.ac-afterglow .ac-nav__link::after,
.ac-afterglow .ac-badge::after,
.ac-afterglow .ac-table__row--active td::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* The whole layer's on/off switch, as an inherited token rather than a
     selector list. `block` is what an absolutely-positioned ::after already
     computes to, so stating it changes nothing — it just gives print.css,
     a11y.css and the reduced-motion block below one declaration to write
     instead of five selectors each. */
  display: var(--ac-afterimage-display, block);
  /* NO mix-blend-mode HERE, though the ghosts and patches both use it: a blended
     element forces its own compositing group, which regressed TEXT ANTIALIASING
     on every button and tab on the page. It is also unnecessary over a
     near-black panel. NO BACKGROUND EITHER — --ac-lit sits at 1 for as long as
     the control is lit, so a fill would paint over its inverse-video text
     permanently and spend the --ac-on-fill contrast ratio contrast.mjs gates.
     Both in RATIONALE.md § The afterimage. */
  box-shadow: 0 0 calc(18px * var(--ac-halo-spread, 1)) rgba(var(--ac-halo-1), calc(0.26 * var(--ac-lit)));
}
.ac-afterglow .ac-btn,
.ac-afterglow .ac-tab,
.ac-afterglow .ac-nav__link,
.ac-afterglow .ac-badge,
.ac-afterglow .ac-table__row--active td {
  position: relative;
}

/* Lighting up is instant — a cell energizes on the next refresh. Declared on the
   lit half so it wins only in that direction. */
.ac-afterglow .ac-btn--filled,
.ac-afterglow .ac-btn[aria-pressed="true"],
.ac-afterglow .ac-btn:hover,
.ac-afterglow .ac-tab--active,
.ac-afterglow .ac-tab[aria-selected="true"],
.ac-afterglow .ac-tab:hover,
.ac-afterglow .ac-nav__link[aria-current="page"],
.ac-afterglow .ac-nav__link:hover,
.ac-afterglow .ac-badge--filled,
.ac-afterglow .ac-banner--filled,
.ac-afterglow .ac-check input:checked,
.ac-afterglow .ac-radio input:checked,
.ac-afterglow .ac-toggle--on .ac-toggle__thumb,
.ac-afterglow .ac-toggle[aria-pressed="true"] .ac-toggle__thumb {
  transition: none;
}

/* (5c) A LIT AREA THAT SHRINKS — the bargraph, and a phenomenon none of the
   decays above covers.
   ---------------------------------------------------------------------------
   A meter's element stays, its text is elsewhere and its color never moves; what
   changes is its WIDTH, so the region it vacates was lit a moment ago and is now
   simply not drawn, with nothing timing the difference.

   THE GHOST IS A SECOND BAR THAT LAGS, and the asymmetry falls out of paint
   order rather than needing a rule of its own — which is what makes this cheap.
   Both bars read the same --ac-meter-value; the live one snaps to it and this
   one takes --ac-persist-tail to get there. On a DECREASE the ghost is briefly
   wider, and the strip between them is the vacated region draining. On an
   INCREASE it sits under the live bar, invisible. Instant up, curve down, free.

   This is the half that works with no JavaScript at all, and it models a bar
   falling GRADUALLY — a taper behind a receding edge. A bar that drops SUDDENLY
   should fade uniformly instead; CSS cannot hold the old width to tell those
   apart, so amber-console.effects.js covers that case. See RATIONALE.md § The
   lagging bar.

   The mask keeps it reading as light rather than as a wipe: the trailing end is
   soft, so the strip fades along its length instead of ending on an edge.

   HERE RATHER THAN IN components/meter.css because this is a `transition` and
   scripts/check-prohibitions.mjs forbids those under src/components/. Only the
   `position: relative` that puts the live bar on top belongs to the component. */
.ac-afterglow .ac-meter__track::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--ac-meter-value, 0) * 1%);
  pointer-events: none;
  background: repeating-linear-gradient(
    90deg,
    var(--ac-fill) 0 8px,
    transparent 8px 12px
  );
  opacity: 0.5;
  mask-image: linear-gradient(90deg, #000 0 55%, transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, #000 0 55%, transparent 100%);
  transition: width var(--ac-persist-tail) var(--ac-decay-ease);
}

/* THE SAME GHOST ON AN OVER-RANGE METER, INVERTED WITH THE REST OF IT.
   components/meter.css lights the alarm TRACK and cuts the bar out of it in
   --ac-on-fill, so the rule above — which paints in --ac-fill — draws the ghost
   in exactly the colour the track behind it now is, and the draining strip
   becomes invisible. An alarmed bar was the one bargraph in the system that
   snapped instead of trailing, which is the bug (5c) exists to fix, reintroduced
   by the alarm fill for the one state where a falling reading matters most.
   Same geometry, same timing, the other ink. */
.ac-afterglow .ac-meter--alarm .ac-meter__track::before {
  background: repeating-linear-gradient(
    90deg,
    var(--ac-on-fill) 0 8px,
    transparent 8px 12px
  );
}

/* (6) SCROLL SMEAR.

   Scrolling is the largest light-off event there is: every cell on the panel is
   handed a new value at once. Real gas cannot keep up, and the image
   trails.

   This is a blurred, additive copy of the backdrop, faded in with scroll speed
   and drained when the scroll stops — amber-console.effects.js drives --ac-smear
   from the actual per-frame delta. Two details are load-bearing: it is `fixed`, so
   the cost is one viewport rather than one document (the guide page is 26,000px
   tall), and it filters the BACKDROP rather than blurring the content itself,
   so the text you are reading stays sharp while its afterimage does not. */
/* The image itself loses focus, and this HAS to be a filter on the CONTENT — an
   additive copy of the backdrop cannot do it, however hard it is blurred.

   THE :not() LIST IS THE GLASS, NOT THE IMAGE. Only the frame's real children
   are filtered; the scanline mask, vignette and cell mesh do not go out of focus
   when the picture moves. .ac-nav--sticky is excluded from the other direction:
   it is pinned to the viewport, so its pixels are NOT being handed a new value.

   (THERE ARE NO COPIES OF THIS LIST ANY MORE, and this note used to say to keep
   three of them in step. print.css and a11y.css each carried their own duplicate
   of the selector until --ac-smear-filter replaced both with one declaration —
   so if you change the list, change it HERE and nowhere else. Recreating a copy
   in either of those files is the thing this paragraph now exists to prevent.)

   The blur is isotropic and a real smear is not; the directional form costs
   every page an inline SVG node, which is not worth the axis. See
   RATIONALE.md § Scroll smear. */
.ac-afterglow[data-ac-scrolling] > *:not(.ac-persist, .ac-retrace, .ac-mesh, .ac-nav--sticky) {
  /* The whole filter is the token, not a scale inside it, so an environment that
     wants no smear gets a literal `none` rather than a blur(0px) — which would
     still be a filter, and a filter creates a containing block for fixed
     descendants. One declaration replaces this selector in print.css, a11y.css
     and the reduced-motion block. */
  filter: var(--ac-smear-filter, blur(calc(var(--ac-smear, 0) * 1.8px)));
}

/* …and the panel blooms while it cannot keep up: a blurred, additive copy of
   the backdrop, fixed to the viewport so the cost is one screen rather than one
   document — the guide page is 26,000px tall. */
.ac-afterglow > .ac-persist::after {
  content: "";
  position: fixed; inset: 0;
  pointer-events: none;
  opacity: calc(var(--ac-smear, 0) * 0.45);
  mix-blend-mode: screen;
  backdrop-filter: blur(calc(var(--ac-smear, 0) * 4px)) brightness(1.12);
  -webkit-backdrop-filter: blur(calc(var(--ac-smear, 0) * 4px)) brightness(1.12);
}
/* Nothing to composite when still — and a permanently live backdrop-filter over
   the viewport is exactly the kind of thing that costs a frame budget for free.
   The attribute is set by amber-console.effects.js only while the smear is
   above zero. */
.ac-afterglow:not([data-ac-scrolling]) > .ac-persist::after { display: none; }

/* (2) GHOSTS — a copy of what was just there, parked at the rect it occupied
   and left to drain. Mounted and removed by amber-console.js; nothing in the
   DOM when the effect is off or the module is absent. */
.ac-afterglow .ac-ghost {
  position: absolute;
  margin: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  /* Invisible until the animation drives it. A ghost is only ever legible mid-
     decay, so if animations are off — a screenshot harness, a print, a browser
     that never ran the keyframes — the right thing to show is nothing at all,
     not an opaque duplicate of live text sitting on top of it. */
  opacity: 0;
  /* ON --ac-persist-tail, THE UNCAPPED ONE — a ghost is pure decoration, so this
     is where the long phosphors run at their real length.

     animation-NAME comes from the palette, defaulting to the single-hue curve.
     A two-layer phosphor declares --ac-ghost-anim in its own block and gets the
     cascade below instead; this file never names a palette to do it. */
  animation: var(--ac-ghost-anim, ac-ghost-decay) var(--ac-persist-tail) var(--ac-decay-ease) forwards;
}
/* Continuously-rewritten text — a clock, a frame counter. Capped at 400ms even
   under a long phosphor, and the cap is arithmetic rather than taste: a field
   rewriting once a second under P7's 3000ms tail would stack three generations
   of ghost on top of each other, and the third one is not persistence, it is a
   pile. 400ms keeps at most one generation legible at a time. */
.ac-afterglow .ac-ghost--fast {
  animation-duration: min(var(--ac-persist-tail), 400ms);
}
@keyframes ac-ghost-decay {
  from { opacity: 0.38; filter: blur(0); }
  to   { opacity: 0; filter: blur(0.8px); }
}

/* THE CASCADE — P7, and anything else with two coatings. A ghost here shows not
   one color fading but TWO in sequence: the flash, then the afterglow.

   4% is the crossover, hardcoded because keyframe offsets cannot read a custom
   property. --ac-persist-fast in tokens/colors.css carries the true figure
   (0.04ms on P7) and is read by nothing — it exists so this 4% has a stated
   provenance rather than looking like a number somebody liked. The color is ANIMATED
   rather than set, so it beats the inline `color` amber-console.js copies onto
   the clone. Palettes with one coating never reach this keyframe.
   See RATIONALE.md § Ghosts. */
@keyframes ac-ghost-cascade {
  0%   { opacity: 0.46; color: rgb(var(--ac-halo-flash)); filter: blur(0); }
  4%   { opacity: 0.38; color: rgb(var(--ac-halo-1)); filter: blur(0.1px); }
  100% { opacity: 0; color: rgb(var(--ac-halo-1)); filter: blur(0.8px); }
}

/* (7) THE PAGE ITSELF DECAYS — cross-document persistence, and the only place in
   this system where a REAL framebuffer snapshot is available without JavaScript.

   Everything else here decays an ELEMENT. A phosphor decays whatever was DRAWN,
   including the middle of a glyph; the View Transition API is the one primitive
   that hands us that. The direction rule survives intact — the OLD snapshot
   decays on the emitter's curve, the NEW page gets `animation: none` and is
   simply THERE on the first frame.

   THE AT-RULE IS NOT SHIPPED, DELIBERATELY — THE ONE OPT-IN IN THE FRAMEWORK.
   `@view-transition { navigation: auto; }` is a document-level switch that
   cannot be scoped, and linking a stylesheet must not rewrite how a site
   navigates. Add that one line yourself and the rules below take it from there;
   docs/docs.css does exactly that.

   CAPPED AT 900ms SEPARATELY from --ac-persist-tail: the overlay sits above the
   document while it runs, so P7's 3000ms tail would mean three seconds of a page
   that cannot be clicked. See RATIONALE.md § The page itself decays. */
html:has(.ac-afterglow)::view-transition-old(root) {
  animation: ac-page-decay min(var(--ac-persist-tail), 900ms) var(--ac-decay-ease) forwards;
  mix-blend-mode: screen;
}
html:has(.ac-afterglow)::view-transition-new(root) {
  animation: none;
  opacity: 1;
}
html:has(.ac-afterglow)::view-transition-group(root) {
  animation-duration: min(var(--ac-persist-tail), 900ms);
}
::view-transition {
  pointer-events: none;
}
@keyframes ac-page-decay {
  from { opacity: 0.85; filter: blur(0); }
  to   { opacity: 0; filter: blur(1.1px); }
}

/* >>> src/amber-console.css -> sim/frame.css */
/* ===========================================================================
   Frame — the rules that must run AFTER all three simulations, which is the
   whole reason this is a file rather than a section of one of them:

     - the shared stacking normalisation (content above the overlays)
     - the viewport anchoring: the glass does not scroll, so on a full-screen
       frame the overlays are fixed rather than document-absolute
     - prefers-reduced-motion, which switches all three off at once

   Selectors here naming a simulation you have deleted match nothing and cost
   nothing, so this file is safe to keep whichever of the three you ship.
   =========================================================================== */


.ac-bloom > *, .ac-crt > *, .ac-afterglow > * { position: relative; z-index: 1; }
.ac-crt > .ac-retrace, .ac-bloom > .ac-retrace { z-index: 45; }
.ac-bloom > .ac-mesh, .ac-crt > .ac-mesh { z-index: 48; }

/* --- THE GLASS IS THE VIEWPORT, NOT THE DOCUMENT ---

   Anchoring these overlays to a document-height frame is wrong physically (glass
   does not scroll, so a vignette would darken the top of the PAGE rather than the
   edges of the SCREEN) and practically (a document-height backdrop-filter
   rasterizes in tiles, and the seams band). Fixing them to the viewport fixes the
   look, the physics and the cost at once.

   SCOPED TO .ac-screen ON PURPOSE: .ac-bloom and .ac-crt also go on small tiles,
   which must stay local to the tile. .ac-persist stays document-absolute —
   ghosts are pinned to a document coordinate.
   See RATIONALE.md § The glass is the viewport, not the document. */
.ac-screen.ac-bloom::before,
.ac-screen.ac-crt::after,
.ac-screen.ac-afterglow > .ac-persist::before {
  position: fixed;
  inset: 0;
}
.ac-screen.ac-crt > .ac-retrace {
  position: fixed;
}
.ac-screen.ac-bloom > .ac-mesh {
  position: fixed;
  inset: 0;
}

@media (prefers-reduced-motion: reduce) {
  /* Everything this block switches off that has a token, switched off in one
     place. No selector lists and no specificity restatement — see the blink
     contract at the top of base/blink.css, which is where that mechanism is
     documented.

       blink        every blink site at once.
       smear        smearing the whole viewport in response to scrolling is the
                    single most motion-sick-making thing in here.
       afterimage   no animation of its own — it reads --ac-lit, and the
                    transition blanket below already stops that from moving — but
                    what is left is a STATIC extra halo around every lit control,
                    which is not motion and is also not something anybody opted
                    into. */
  :root {
    --ac-blink-force: none;
    --ac-smear-filter: none;
    --ac-afterimage-display: none;
  }

  .ac-bloom::before, .ac-crt::after,
  .ac-persist::before { animation: none; }
  .ac-retrace { display: none; }

  /* .ac-mesh is the one overlay here that keeps rendering. Note it is in an
     animation rule and NOT in the display rule beside .ac-retrace: the screen
     door is not an effect playing over the panel, it is the shape of the panel,
     and hiding it would change what the hardware IS in response to a motion
     preference. Only the buzz is motion.

     Restated with the child combinator rather than folded into the bare list
     above, and that is the third time this file has had to say it: a media query
     adds no specificity, so `.ac-mesh` (0,1,0) loses to the `.ac-bloom > .ac-mesh`
     (0,2,0) that declares the animation, and the buzz goes on running for exactly
     the users who asked it not to. It has to be matched at its own weight. */
  .ac-bloom > .ac-mesh,
  .ac-crt > .ac-mesh { animation: none; }

  /* Every decay in the simulation is a descendant rule under .ac-afterglow, so
     each one outranks a plain reset here and has to be caught. Restating the
     selector list one by one is how the blink override slipped through twice —
     a blanket is the only version that also covers the next rule added. */
  .ac-afterglow *,
  .ac-afterglow *::before,
  .ac-afterglow *::after { transition: none !important; }

  /* The smear's additive bloom is a node of its own rather than one of the
     filtered children, so --ac-smear-filter above does not reach it. */
  .ac-afterglow > .ac-persist::after { display: none; }

  /* The lagging bar would already be harmless — the blanket above stops its width
     from transitioning, so it sits exactly under the live bar and cannot be seen.
     It goes anyway rather than painting a gradient nobody will ever look at. */
  .ac-afterglow .ac-meter__track::before { display: none; }

  /* A whole page dissolving into the next one is the largest moving image in the
     system. Both halves are cut, not just the old one — leaving the NEW page's
     animation suppressed while the OLD one still ran would be worse than either. */
  html:has(.ac-afterglow)::view-transition-old(root),
  html:has(.ac-afterglow)::view-transition-new(root) { animation: none; }

  /* Persistence is motion by definition: a decaying ghost is a moving image.
     There is nothing to soften here, so the whole thing goes. */
  .ac-ghost { display: none; }
  .ac-afterglow [hidden],
  .ac-afterglow dialog:not([open]) { transition: none; filter: none; }
}


/* --- Base --- */
/* >>> src/amber-console.css -> base/reset.css */
/* ===========================================================================
   Reset — minimal and deliberately opinionated.

   This framework owns the look of a whole screen, so it is assertive: it styles
   `body` directly. Embedding Amber Console inside an existing app instead?
   Import base/reset-scoped.css, which applies the same rules under .ac-root.
   =========================================================================== */

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

body {
  margin: 0;
  background: var(--ac-screen);
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: var(--ac-leading);
  letter-spacing: var(--ac-tracking-body);
  /* Antialiasing softens the bitmap grid and kills the effect. Keep this. */
  -webkit-font-smoothing: none;
}

/* There is no bold in the terminal face — weight carries no meaning here.
   Hierarchy comes from size, intensity and inverse video. */
h1,
h2,
h3,
h4 {
  margin: 0;
  font-weight: 400;
  text-transform: uppercase;
}

/* Undefined links render browser-default blue and instantly break the illusion. */
a {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
  text-decoration: underline;
  text-underline-offset: 3px;
}
a:hover {
  color: var(--ac-fill-bright);
  text-shadow: var(--ac-glow-text);
}

::selection {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
}

/* Form controls do not inherit type by default. On a character grid they must.
   text-shadow is on that list and is the one nobody expects: the halo is applied
   at the frame and inherits everywhere, but a <button> or an <option> stops it,
   so a toggle's own label sat flat on a panel where every word around it glowed.
   A control is made of lit cells like anything else. */
button,
input,
select,
textarea {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  letter-spacing: inherit;
  color: inherit;
  text-shadow: inherit;
}

/* The shared focus ring. Every interactive ac-* class repeats this so it still
   works when a consumer imports components without the reset. */
:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* >>> src/amber-console.css -> base/layout.css */
/* ===========================================================================
   Layout utilities

   Flex and grid with `gap` everywhere — never margins between siblings, which
   would fight the 4px half-cell grid. Every container that can hold a
   long string carries min-width: 0, because .ac-btn--pad has a 130px min-width
   floor that will otherwise push a grid track wide and overflow the page.
   =========================================================================== */

/* --- Screen: the full-height frame that hosts .ac-bloom / .ac-crt --- */
.ac-screen {
  display: flex;
  /* SMALL viewport height, not large and not dynamic.
     `100vh` is the URL-bar-retracted height, so on a phone with the bar showing
     the frame is taller than the glass and a short screen picks up 60-100px of
     scroll containing nothing. `100dvh` would track the bar exactly — and
     re-resolve every time it slides, reflowing a frame that is dragging a
     viewport-sized backdrop-filter (.ac-bloom::before) behind it. `100svh` is
     static and can never overflow. It leaves a gap when the bar retracts, and
     that gap is invisible: this element paints no background of its own, `body`
     owns --ac-screen, and the overlays are fixed to the real viewport regardless.
     Declared after the 100vh fallback, which older engines keep. */
  min-height: 100vh;
  min-height: 100svh;
  flex-direction: column;
  /* The simulation overlays are absolutely positioned to this box, so the frame
     has to clip. `clip` rather than `hidden`, and the difference is load-bearing:
     `hidden` makes this box a scroll container, and a scroll container is what
     `position: sticky` resolves against. The frame never scrolls — the page does
     — so .ac-nav--sticky inside an overflow:hidden frame silently did not stick
     at all. `clip` clips without becoming a scrollport, so the bar sticks to the
     viewport and the overlays still stop at the frame edge. */
  overflow: clip;
}

.ac-screen__body {
  display: flex;
  flex: 1;
  flex-direction: column;
  /* 20px 24px — five and six half-cells. Override with --ac-screen-pad. */
  padding: var(--ac-screen-pad, 20px var(--ac-space-6));
  gap: var(--ac-space-4);
  min-width: 0;
}

/* --- Stack / Row: one axis, gap driven by a --ac-gap custom property --- */
.ac-stack {
  display: flex;
  flex-direction: column;
  gap: var(--ac-gap, var(--ac-space-4));
  min-width: 0;
}

.ac-row {
  display: flex;
  flex-direction: row;
  gap: var(--ac-gap, var(--ac-space-4));
  min-width: 0;
}
.ac-row--wrap { flex-wrap: wrap; }
.ac-row--center { align-items: center; }
.ac-row--baseline { align-items: baseline; }
.ac-row--between { justify-content: space-between; }
.ac-row--end { justify-content: flex-end; }

/* Push everything after it to the far edge. */
.ac-spacer { flex: 1; }

/* These set `display`, which would otherwise beat the [hidden] attribute. */
.ac-stack[hidden],
.ac-row[hidden],
.ac-grid[hidden],
.ac-screen[hidden] {
  display: none;
}

/* Children of a stack/row that should absorb the leftover space. */
.ac-grow { flex: 1; min-width: 0; }

/* Drop to the bottom of a flex column — the "pinned bottom-right" idiom. */
.ac-push { margin-top: auto; }

/* Available to assistive tech, invisible on the panel. For the <legend> of a
   bit group whose visible label is already the panel's own title. */
.ac-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* --- Grid --- */
.ac-grid {
  display: grid;
  grid-template-columns: repeat(var(--ac-cols, 1), minmax(0, 1fr));
  gap: var(--ac-gap, var(--ac-space-6));
  min-width: 0;
}

/* The canonical console split: operate left, instrument right. */
.ac-grid--console {
  grid-template-columns: minmax(0, 1fr) minmax(280px, 380px);
}

.ac-col-2 { grid-column: span 2; }
.ac-col-3 { grid-column: span 3; }
.ac-col-4 { grid-column: span 4; }
.ac-col-6 { grid-column: span 6; }

/* Below this the instrument column can no longer hold its 280px floor beside a
   usable operating column, so both stack. Matches the source console screen. */
@media (max-width: 900px) {
  .ac-grid--console {
    grid-template-columns: minmax(0, 1fr);
  }
  .ac-col-2,
  .ac-col-3,
  .ac-col-4,
  .ac-col-6 {
    grid-column: auto;
  }
}


/* --- Components --- */
/* >>> src/amber-console.css -> components/button.css */
/* ===========================================================================
   Button — the soft key.

   Works on <button>, <a> and <input type="submit">. State hooks are both the
   --filled class and [aria-pressed="true"], so a framework binding an ARIA
   attribute needs no class juggling.

   Ration inverse video: two or three filled surfaces per screen, no more.
   =========================================================================== */

.ac-btn {
  display: inline-block;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  text-transform: uppercase;
  text-decoration: none;
  letter-spacing: var(--ac-tracking-body);
  color: var(--ac-ink);
  background: transparent;
  border: var(--ac-border-w) solid var(--ac-stroke);
  /* HALO PLUS EXTRUSION, IN THAT ORDER, ON EVERY STATE THAT HAS A HALO.
     --ac-edge is the classic key edge and is set only by a corner scope
     (components/classic.css); outside one it is undefined and the fallback
     paints nothing, so a rounded key is unchanged.

     IT IS DECLARED HERE RATHER THAN IN classic.css, and that is the fix for a
     real bug. classic.css used to add the edge from a scope-prefixed rule at
     (0,2,0), which outranked .ac-btn--dim at (0,1,0) and handed a dim key the
     FULL halo — so under the default corner style every dim key glowed as hard
     as a lit one, contradicting the drive-tier law the framework gates
     everywhere else. Each state now carries its own tier and its own edge, so
     the two cannot disagree. */
  box-shadow: var(--ac-glow-box), var(--ac-edge, 0 0 0 transparent);
  border-radius: var(--ac-radius);
  padding: 10px 22px;
  cursor: pointer;
  text-shadow: var(--ac-glow-text);
}

.ac-btn:hover {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
  border-color: var(--ac-ink-bright);
  box-shadow: var(--ac-glow-box), var(--ac-edge, 0 0 0 transparent);
}

/* Press snaps to inverse video. No transition — a redrawn screen has no
   in-between frames. The edge flips to --ac-edge-press: a pressed key is IN, and
   the light is what moves. */
.ac-btn:active {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  box-shadow: var(--ac-glow-box), var(--ac-edge-press, 0 0 0 transparent);
}

/* A latched key reads pressed for as long as it is latched, so --filled and
   aria-pressed take the pressed edge too. */
.ac-btn--filled,
.ac-btn[aria-pressed="true"] {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  box-shadow: var(--ac-glow-box), var(--ac-edge-press, 0 0 0 transparent);
}
/* Restated because .ac-btn:hover weighs the same and lands earlier: without it a
   hovered latched key would pop back out to the resting edge. */
.ac-btn--filled:hover,
.ac-btn[aria-pressed="true"]:hover {
  background: var(--ac-fill-bright);
  color: var(--ac-on-fill);
  text-shadow: none;
  box-shadow: var(--ac-glow-box), var(--ac-edge-press, 0 0 0 transparent);
}

/* Dim is a DRIVE LEVEL, not an off state — emit-50 is still a lit cell, so it
   still scatters, just less. The key is still EXTRUDED, though: the edge is
   geometry and does not answer to how hard the cell behind it is driven. Only
   genuinely inert things go flat; see the :disabled rule below and the
   drive-tier note in tokens/effects.css. */
.ac-btn--dim {
  color: var(--ac-ink-faint);
  border-color: var(--ac-stroke-dim);
  text-shadow: var(--ac-glow-text-faint);
  box-shadow: var(--ac-glow-box-dim), var(--ac-edge, 0 0 0 transparent);
}

.ac-btn--lg {
  font-size: var(--ac-type-title);
  padding: 14px 28px;
}

/* ---------------------------------------------------------------------------
   ACCESSIBILITY WARNING — READ BEFORE USING THIS CLASS.

   .ac-btn--sm computes to 34px tall. WCAG 2.2 Target Size (Minimum,
   2.5.8, AA) asks for 24x24 CSS px and the widely-used practical floor is 44px;
   this clears the spec minimum and misses the comfortable one. On a touch
   screen it is genuinely hard to hit.

   It is not silently resized because the value comes from the source design
   system and every other size in the scale is solved against it.

   SAFE:   dense mouse-driven tooling — a log filter, a table's row actions,
           a settings row in an admin panel.
   UNSAFE: anything on a touch panel, and any PRIMARY action anywhere. Use the
           default .ac-btn for those — it measures 46px tall, clearing the 44px
           practical floor, and it is the default for this reason. (46 rather
           than a round 44 because the height is the sum of the parts: 22px body
           text at line-height 1, plus 10px padding twice, plus a 2px border
           twice. Nothing here is set to a target size directly.)

   If you need small AND touch-safe, keep .ac-btn--sm for the look and give the
   control padding or a pseudo-element that extends the hit area to 44px without
   changing the drawn box. --------------------------------------------------- */
.ac-btn--sm {
  font-size: var(--ac-type-small);
  padding: 6px 14px;
  border-radius: var(--ac-radius-sm);
}

/* The gloved-finger touch pad: label parked top-left, body deliberately empty. */
.ac-btn--pad {
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
  min-height: 96px;
  min-width: 130px;
  padding: 10px 16px;
}

.ac-btn--block {
  display: block;
  width: 100%;
}
.ac-btn--pad.ac-btn--block {
  display: flex;
}

.ac-btn:disabled,
.ac-btn[aria-disabled="true"] {
  color: var(--ac-ink-faint);
  border-color: var(--ac-stroke-dim);
  background: transparent;
  text-shadow: none;
  /* Inert, so no halo at all — not a dimmer one. This is the half of law 1 that
     the drive tiers must never quietly absorb.

     THE RULE HOLDS FOR EVERY CONTROL THAT IS ITSELF THE LAMP: this key,
     .ac-tab, .ac-input, .ac-check and .ac-radio all go to `none`. There is ONE
     stated exception and it is the toggle HOUSING, which keeps
     --ac-glow-box-dim because it is a box that is still drawn rather than a cell
     that is no longer lit — components/toggle.css argues that case where it
     makes it, and the thumb inside it, which IS the lamp, goes dark like
     everything here. Anything else keeping a halo while disabled is drift. */
  box-shadow: none;
  cursor: default;
}

.ac-btn:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* >>> src/amber-console.css -> components/nav.css */
/* ===========================================================================
   Nav — the screen/site menu bar.

   Link labels are Title Case: they are operator soft keys, not system text.
   The active link is [aria-current="page"] — inverse video, no underline.

   MOBILE: the links wrap to a second row. There is deliberately no hamburger —
   law 6 forbids an icon set, and a three-line glyph is an icon. A control
   panel shows you every key it has.
   =========================================================================== */

.ac-nav {
  box-sizing: border-box;
  display: flex;
  align-items: stretch;
  gap: var(--ac-space-4);
  flex-wrap: wrap;
  border-bottom: var(--ac-border-w) solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  background: var(--ac-screen);
  padding: 0 var(--ac-space-6);
}

.ac-nav--sticky {
  position: sticky;
  top: 0;
  z-index: 20;
}

.ac-nav__mark {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  margin-right: auto;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  letter-spacing: 0.22em;
  color: var(--ac-ink-bright);
  text-transform: uppercase;
  text-shadow: var(--ac-glow-text);
}

/* A typeset block, not a logo. There is no logo — see law 6 in README.md. */
.ac-nav__mark::before {
  content: "█";
  /* Below the 18px VT323 floor on purpose — a filled block has no letterform to
     lose. The other stated exception in tokens/typography.css. */
  font-size: 16px;
  color: var(--ac-fill);
}

.ac-nav__links {
  display: flex;
  align-items: stretch;
  gap: var(--ac-space-2);
  flex-wrap: wrap;
}

.ac-nav__link {
  display: flex;
  align-items: center;
  padding: 0 var(--ac-space-4);
  margin: 6px 0;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  color: var(--ac-ink);
  text-decoration: none;
  letter-spacing: var(--ac-tracking-body);
  border: var(--ac-border-w) solid transparent;
  border-radius: var(--ac-radius-sm);
  text-shadow: var(--ac-glow-text);
}

.ac-nav__link:hover {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
  border-color: var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
}

.ac-nav__link[aria-current="page"],
.ac-nav__link--active {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  border-color: var(--ac-fill);
  text-shadow: none;
}

.ac-nav__link:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

.ac-nav__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--ac-space-4);
  font-family: var(--ac-font-micro);
  font-size: var(--ac-type-micro);
  letter-spacing: var(--ac-tracking-micro);
  color: var(--ac-ink-faint);
  text-shadow: var(--ac-glow-text-faint);
  /* THE READOUTS STAY AT THE RIGHT EDGE WHEN THE BAR BREAKS UP.
     .ac-nav__mark's margin-right:auto holds them there only while all three fit
     on one line; the moment the meta strip wraps to a line of its own, that auto
     margin is on a different line and cannot reach it, so the readouts snapped
     back to the left margin and sat under the mark. An auto margin resolves
     against the free space of ITS OWN flex line, so this one is right either
     way: sharing the line, or alone on it. */
  margin-left: auto;
}

/* ===========================================================================
   THE TWO STEPS BETWEEN ONE LINE AND THE PHONE STACK.

   MEASURED, BECAUSE THE WHOLE PROBLEM WAS THAT NOBODY HAD. The docs bar wants
   1326px to hold mark, keys, the display readout and the byline on one line —
   so every laptop was already wrapping, and every width between that and the
   phone stack was an accident rather than a layout. The worst of it was 760-480,
   where the keys broke 4+2 against the left margin with the last two trailing
   off into space: not a strip, not a stack, just a strip out of room.

   STEP ONE, 1400: the keys give back 4px of padding a side. Small, invisible,
   and worth 48px across six keys, which buys the one-line strip about a hundred
   pixels of extra life.

   STEP TWO, 1320: the keys stop sharing a line with anything and become a deck
   of their own. `flex-basis: 100%` takes the whole width, and `flex: 1 1 auto`
   on each key means every ROW of that deck is justified — four keys wide on one
   row, two half-width keys on the next, all of it flush to both margins. A wrap
   you can see the logic of reads as a keypad; the same wrap left ragged reads as
   a bug. The mark and the readouts pair off on the line above, one at each end.

   THE STEP IS AT 1320 AND NOT 1280 BECAUSE 1278 IS WHERE ONE LINE STOPS FITTING,
   and a breakpoint sitting two pixels off the thing it is measuring will be
   wrong the first time a font loads slightly wider. Round numbers are for
   people; this one has to hold. =========================================== */
@media (max-width: 1400px) {
  .ac-nav__link {
    padding: 0 var(--ac-space-3);
  }
}

@media (max-width: 1320px) {
  /* order:1 sends the keys below the readouts even though they come first in
     the markup, which is what pairs the mark and the readouts off on one line —
     masthead at one end, machine state at the other — instead of leaving the
     readouts stranded on a third line of their own. */
  .ac-nav__links {
    order: 1;
    flex-basis: 100%;
    gap: var(--ac-space-2);
  }

  /* Grow to fill the row, and stand tall enough to be pressed: this band is
     where the tablets are, and a 30px key is a mouse target. */
  .ac-nav__link {
    flex: 1 1 auto;
    justify-content: center;
    min-height: 44px;
  }
}

/* ===========================================================================
   PHONE AND SMALL TABLET. One column, centered, and every key a touch target.

   720 RATHER THAN 480, WHICH IS WHERE THIS STARTED. The justified deck above
   holds together while a row can carry three or four keys; under about 720 it
   is dealing two and then one, and a two-key row on a phone is a strip that has
   run out of room rather than a menu. The whole band below the tablet gets the
   same answer instead: a column.

   Each key is full-bleed and 52px tall — past the 44px floor both Apple and
   WCAG 2.5.5 put on a touch target — with a gap between them so a thumb that
   lands slightly off hits nothing instead of the wrong page. Still no
   hamburger: the panel shows every key it has, it just shows them in a column.

   THE BAR STOPS BEING STICKY HERE, and that is the price of the stack rather
   than an oversight. Six 44px keys plus the mark and the readouts is around
   400px of bar; pinned to the top of a 660px phone viewport it would leave a
   third of the screen for the page it is navigating. A menu that large scrolls
   away like everything else.

   THE READING ORDER IS MASTHEAD, STATE, KEYS — the panel names itself, says
   what it is running, and then offers the controls, which is the order a real
   one is labelled in. That needs the meta strip to come apart: the readouts
   belong under the mark rather than after the keys. `display: contents` drops
   the strip's own box so its children become items of the bar itself and can be
   ordered independently; they keep its font and colour, which inherit straight
   through. A page that puts something else in the strip can order it from
   there — the component only claims the two positions it knows about.
   =========================================================================== */
@media (max-width: 720px) {
  .ac-nav {
    position: static;
    justify-content: center;
    /* The strip's own 16px gap goes with its box, so the bar restates it —
       row 8px between stacked keys, column 16px between DISPLAY and REV on
       their shared line. */
    gap: var(--ac-space-2) var(--ac-space-4);
    padding: var(--ac-space-2) var(--ac-space-3);
  }

  .ac-nav__mark {
    order: 1;
    justify-content: center;
    width: 100%;
    padding-bottom: var(--ac-space-1);
  }

  .ac-nav__meta {
    display: contents;
  }

  .ac-nav__meta > * {
    order: 2;
  }

  .ac-nav__links {
    order: 3;
    flex-direction: column;
    width: 100%;
    gap: var(--ac-space-2);
  }

  /* A thumb, not a cursor: title size on a 52px slab, which is the 44px floor
     with room left over rather than exactly at it. `flex: 0 0 auto` undoes the
     justified-row rule from the band above — in a column that grow factor is
     pointing down the page, and a key has no business stretching vertically. */
  .ac-nav__link {
    flex: 0 0 auto;
    justify-content: center;
    min-height: 52px;
    margin: 0;
    font-size: var(--ac-type-title);
  }
}

/* >>> src/amber-console.css -> components/setup.css */
/* ===========================================================================
   Setup — the permanently-open control board under the menu bar.

   It does not open, close, scroll or stick. That is the whole design: a machine
   with a settings page does not hide it behind a key, and a board you have to
   reveal is a board whose state you cannot read at a glance. Every switch the
   panel has is on the glass, all the time, which is the same argument .ac-nav
   makes for wrapping its links to a second row rather than folding them behind a
   menu glyph — a control panel shows you every key it has.

   Nothing here scrolls either, and that is a constraint on the CONTENTS rather
   than a feature of this file: the regions have to be short enough to fit. It is
   why the emitter catalog is split by technology across two regions instead of
   stacked into one tall column — see the demo markup.

     <nav class="ac-nav ac-nav--sticky">…</nav>
     <div class="ac-setup">
       <div class="ac-setup__grid">
         <div class="ac-panel">…</div>   <!-- one region per quarter -->
         …
       </div>
       <div class="ac-setup__foot">…</div>
     </div>

   NOT STICKY, deliberately. The bar above it can be — it is one row and it
   carries the page links. This is four regions tall, and a sticky element that
   size is not chrome any more, it is the page. It scrolls away like content
   because it is content.

   WHEN NOT TO USE: a control the operator reaches for mid-task. This is setup —
   things you set once and then work under. Live controls belong on the console.
   =========================================================================== */

.ac-setup {
  box-sizing: border-box;
  border-bottom: var(--ac-border-w) solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  background: var(--ac-screen);
  padding: var(--ac-space-8) var(--ac-space-6) var(--ac-space-6);
}

/* Quarters — and THREE COLUMNS IS NOT ONE OF THE STOPS, which is why this is
   three stated breakpoints rather than the one auto-fit line it wants to be.
   Four regions across three tracks wraps the fourth under the first and leaves a
   region-sized hole beside it; the board measured 516px tall at four columns and
   847px at three, for less content per row. Two-by-two is the honest middle: the
   two catalogs share a row, the two control stacks share the next.

   minmax(0, 1fr) rather than 1fr because a grid item's automatic minimum size is
   its content, and an emitter row that cannot shrink pushes the track wider than
   its share — one long "NOT FITTED" note and the quarters stop being quarters. */
.ac-setup__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--ac-space-6);
  align-items: start;
}

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

@media (min-width: 1100px) {
  .ac-setup__grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* The board's own background is --ac-screen, so the legend chips that mask the
   panel border behind themselves already match. Stated rather than assumed:
   drop the board on a raised surface and this is the one line to override. */
.ac-setup .ac-panel {
  --ac-panel-title-bg: var(--ac-screen);
}

/* Every region on the board is drawn in --ac-stroke, INCLUDING the read-only ones.
   .ac-panel--dim normally dims its frame as well as its contents, which is right
   for a panel standing on its own — but here four regions sit edge to edge in
   one grid, and one frame drawn a step darker than the three beside it does not
   read as "this one recedes", it reads as a panel that failed to light. The
   board is a single instrument; its divisions are all the same wall.

   The recede is not lost, it moves to where it belongs: the dim title and the
   dim contents still say read-only, and those are the parts an operator is
   actually reading. Only the frame is normalized. */
.ac-setup .ac-panel--dim {
  border-color: var(--ac-stroke);
}

.ac-setup__foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--ac-space-4);
  margin-top: var(--ac-space-6);
  padding-top: var(--ac-space-4);
  border-top: var(--ac-border-w) solid var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
}

@media (max-width: 720px) {
  .ac-setup {
    padding: var(--ac-space-8) var(--ac-space-4) var(--ac-space-4);
  }
  /* One column here, and the regions are tall. Open the gap up so two stacked
     panels do not read as one region with a rule through it. */
  .ac-setup__grid {
    gap: var(--ac-space-8);
  }
}

/* >>> src/amber-console.css -> components/tabs.css */
/* ===========================================================================
   Tabs — soft keys, Title Case.

   Casing is semantic: these are operator-facing, so they are NOT uppercased
   like system text. text-transform: none is load-bearing, not an oversight.

   Markup: role="tablist" > role="tab" + aria-selected, and aria-controls
   pointing at a role="tabpanel". src/amber-console.js adds the arrow-key model;
   the component looks and reads correctly without it.
   =========================================================================== */

.ac-tabs {
  display: flex;
  gap: var(--ac-space-2);
  /* Three title-size soft keys need ~455px. Wrap to a second row rather than
     be clipped — same rule as .ac-nav, and for the same reason: a control panel
     shows you every key it has. */
  flex-wrap: wrap;
}

.ac-tab {
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-title);
  line-height: 1;
  color: var(--ac-ink);
  background: transparent;
  text-transform: none;
  border: var(--ac-border-w) solid var(--ac-stroke);
  /* A TAB IS A KEY, so it is extruded like one. It was not, and that was the
     visible inconsistency: a tab is a bordered, cut-cornered, pressable box that
     is the same species as .ac-btn, sitting beside real buttons with the same
     corner and no edge. "A key is extruded; a panel is a hole in the bezel" —
     a tab is on the key side of that line. Same token, same states, so the two
     cannot drift apart again. See components/button.css. */
  box-shadow: var(--ac-glow-box), var(--ac-edge, 0 0 0 transparent);
  border-radius: var(--ac-radius);
  /* The 20px bottom padding is dead space that makes a soft key read as a
     physical key. It is intentional. Do not snap it to the 4/8px grid. */
  padding: 8px 24px 20px;
  cursor: pointer;
  text-shadow: var(--ac-glow-text);
}

.ac-tab:hover {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
  border-color: var(--ac-ink-bright);
  box-shadow: var(--ac-glow-box), var(--ac-edge, 0 0 0 transparent);
}

/* A selected tab is latched, so it presses IN — same as .ac-btn[aria-pressed]. */
.ac-tab:active {
  box-shadow: var(--ac-glow-box), var(--ac-edge-press, 0 0 0 transparent);
}

.ac-tab--active,
.ac-tab[aria-selected="true"] {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  box-shadow: var(--ac-glow-box), var(--ac-edge-press, 0 0 0 transparent);
}
/* Restated because .ac-tab:hover weighs the same and lands earlier: without it
   a hovered selected tab would pop back out to the raised edge. */
.ac-tab--active:hover,
.ac-tab[aria-selected="true"]:hover {
  box-shadow: var(--ac-glow-box), var(--ac-edge-press, 0 0 0 transparent);
}

.ac-tab:disabled {
  color: var(--ac-ink-faint);
  border-color: var(--ac-stroke-dim);
  /* Inert keeps NO HALO AND NO EDGE, the same as a disabled key — and this line
     used to say exactly that while carrying --ac-glow-box-dim, which is a
     dimmer halo rather than none. A tab is the same species as .ac-btn and
     .ac-btn:disabled goes to `none`, so the two were rendering different
     answers to the same question. See the drive-tier note in
     components/button.css: dim is a LIT cell driven low, off is not lit. */
  box-shadow: none;
  text-shadow: none;
  cursor: default;
}

.ac-tab:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* >>> src/amber-console.css -> components/panel.css */
/* ===========================================================================
   Panel — the framed region. Borders do the layout work; there is no elevation.

   Two title treatments:
     .ac-panel__title     legend chip breaking the top border
     .ac-panel--bar +
     .ac-panel__barTitle  full-width inverse strip (the machine's own heading)

   The legend chip paints --ac-screen to mask the border behind it. On a panel
   sitting over a different background, override --ac-panel-title-bg.
   =========================================================================== */

.ac-panel {
  position: relative;
  box-sizing: border-box;
  border: var(--ac-border-w) solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  border-radius: var(--ac-radius);
  padding: var(--ac-space-6) var(--ac-space-4) var(--ac-space-4);
  min-width: 0;
}

/* Read-only / secondary regions recede. */
.ac-panel--dim {
  border-color: var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
}

.ac-panel__title {
  position: absolute;
  top: -10px;
  left: 12px;
  padding: 0 8px;
  background: var(--ac-panel-title-bg, var(--ac-screen));
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-small);
  line-height: 1;
  color: var(--ac-ink);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  text-shadow: var(--ac-glow-text);
}

.ac-panel--dim .ac-panel__title {
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
}

.ac-panel--bar {
  padding-top: 0;
  overflow: hidden;
}

.ac-panel__barTitle {
  margin: 0 calc(-1 * var(--ac-space-4)) var(--ac-space-3);
  padding: 4px 16px;
  text-align: center;
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

/* >>> src/amber-console.css -> components/input.css */
/* ===========================================================================
   Field / Input / Textarea — recessed data wells.

   Input text is --ac-ink-bright on --ac-screen-well: the operator's own entry is the
   brightest thing in the region, because it is the one part of the panel a
   human wrote.

   Markup: <label class="ac-field">
             <span class="ac-field__label">Title:</span>
             <input class="ac-input" type="text">
           </label>

   Invalid state is blink + inverse video. Never a red border — there is no red.
   =========================================================================== */

.ac-field {
  display: inline-flex;
  flex-direction: column;
  gap: var(--ac-space-1);
  font-family: var(--ac-font-terminal);
  min-width: 0;
}

.ac-field--block {
  display: flex;
  width: 100%;
}

.ac-field__label {
  font-size: var(--ac-type-small);
  line-height: 1;
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
  text-transform: uppercase;
  letter-spacing: var(--ac-tracking-body);
}

.ac-input {
  box-sizing: border-box;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
  background: var(--ac-screen-well);
  border: var(--ac-border-w) solid var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
  border-radius: var(--ac-radius-sm);
  padding: 6px 12px;
  letter-spacing: var(--ac-tracking-body);
  caret-color: var(--ac-ink-bright);
  text-transform: uppercase;
}

.ac-input::placeholder {
  color: var(--ac-ink-faint);
  text-shadow: var(--ac-glow-text-faint);
}

.ac-input:focus {
  outline: none;
  border-color: var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
}

.ac-input:disabled {
  color: var(--ac-ink-faint);
  text-shadow: none;
  border-color: var(--ac-ink-trace);
  /* Inert, so no halo at all — the base rule above carries --ac-glow-box-dim and
     without this line a dead well kept it, glowing harder than its own
     trace-level border implied. Matches .ac-btn:disabled; see the drive-tier
     note in components/button.css. */
  box-shadow: none;
}

.ac-input:read-only {
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
}

.ac-input--block {
  display: block;
  width: 100%;
}

/* textarea keeps the terminal grid: no resize handle chrome, tight leading. */
textarea.ac-input {
  display: block;
  width: 100%;
  line-height: var(--ac-leading);
  resize: vertical;
}

/* Invalid = the machine rejecting the entry. Inverse video carries it; the
   blink is the alarm on top. Pair with aria-invalid for assistive tech. */
.ac-field--invalid .ac-field__label {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
}
.ac-field--invalid .ac-input,
.ac-input[aria-invalid="true"] {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  border-color: var(--ac-stroke);
  caret-color: var(--ac-on-fill);
  /* A blink site: reads the pair declared in tokens/effects.css and explained in
     base/blink.css, which is also what switches it off for the blink flag,
     reduced motion, print and forced colors. Nothing outside this rule needs to
     know this element blinks. */
  animation-name: var(--ac-blink-force, var(--ac-blink-name));
  animation-duration: 1.1s;
  animation-timing-function: var(--ac-blink-force-ease, var(--ac-blink-ease));
  animation-iteration-count: infinite;
}
.ac-field--invalid .ac-input::placeholder,
.ac-input[aria-invalid="true"]::placeholder {
  color: var(--ac-on-fill);
  text-shadow: none;
}

/* >>> src/amber-console.css -> components/select.css */
/* ===========================================================================
   Select — an .ac-input plus a typeset ▼.

   The wrapper exists only to position the glyph. That glyph is a Unicode
   character in the terminal font, not an SVG: law 6, ornament is typographic.

   Markup: <span class="ac-select"><select class="ac-input">…</select></span>
   =========================================================================== */

.ac-select {
  position: relative;
  display: inline-flex;
  min-width: 0;
}

.ac-select--block {
  display: flex;
  width: 100%;
}

.ac-select .ac-input {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 38px;
  cursor: pointer;
  width: 100%;
}

.ac-select::after {
  content: "▼";
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  /* Below the 18px VT323 floor on purpose — a triangle has no letterform to lose
     to resampling. One of the two stated exceptions in tokens/typography.css. */
  font-size: 14px;
  font-family: var(--ac-font-terminal);
}

/* The dropdown list is painted by the OS; this is as far as we can reach. */
.ac-select .ac-input option {
  background: var(--ac-screen);
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
}

.ac-select:has(.ac-input:disabled)::after {
  color: var(--ac-ink-faint);
  text-shadow: none;
}

/* >>> src/amber-console.css -> components/checkbox.css */
/* ===========================================================================
   Checkbox — a status bit, not a to-do tick.

   :checked fills the square solid, exactly like a hardware indicator lamp.
   There is deliberately NO check glyph: the panel lights a cell or it does not.

   Markup: <label class="ac-check"><input type="checkbox"><span>Label</span></label>
   Group them in a <fieldset> with a <legend>.

   The drawn square stays 20px. The clickable LABEL carries min-height: 44px to
   meet the hit-target floor — see README, "Known constraints".
   =========================================================================== */

.ac-check {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  text-transform: uppercase;
  letter-spacing: var(--ac-tracking-body);
  cursor: pointer;
}

.ac-check input {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin: 0;
  flex: none;
  border: var(--ac-border-w) solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  border-radius: 0;
  background: transparent;
  cursor: pointer;
}

.ac-check input:checked {
  background: var(--ac-fill);
  box-shadow: var(--ac-glow-box);
}

.ac-check--disabled,
.ac-check:has(input:disabled) {
  color: var(--ac-ink-faint);
  text-shadow: none;
  cursor: default;
}
.ac-check--disabled input,
.ac-check input:disabled {
  border-color: var(--ac-stroke-dim);
  box-shadow: none;
  cursor: default;
}
.ac-check--disabled input:checked,
.ac-check input:disabled:checked {
  background: var(--ac-ink-faint);
  box-shadow: none;
}

.ac-check input:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* >>> src/amber-console.css -> components/radio.css */
/* ===========================================================================
   Radio — the same indicator lamp, round.

   :checked punches the ring gap with an inset --ac-screen shadow rather than
   drawing a dot, so the lit area reads as one solid gas cell.

   Markup: <label class="ac-radio"><input type="radio" name="mode"><span>Label</span></label>
   =========================================================================== */

.ac-radio {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  text-transform: uppercase;
  letter-spacing: var(--ac-tracking-body);
  cursor: pointer;
}

.ac-radio input {
  appearance: none;
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin: 0;
  flex: none;
  border: var(--ac-border-w) solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  border-radius: 50%;
  background: transparent;
  cursor: pointer;
}

.ac-radio input:checked {
  background: var(--ac-fill);
  box-shadow: inset 0 0 0 3px var(--ac-screen), var(--ac-glow-box);
}

.ac-radio--disabled,
.ac-radio:has(input:disabled) {
  color: var(--ac-ink-faint);
  text-shadow: none;
  cursor: default;
}
.ac-radio--disabled input,
.ac-radio input:disabled {
  border-color: var(--ac-stroke-dim);
  box-shadow: none;
  cursor: default;
}
.ac-radio--disabled input:checked,
.ac-radio input:disabled:checked {
  background: var(--ac-ink-faint);
  box-shadow: inset 0 0 0 3px var(--ac-screen);
}

.ac-radio input:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* >>> src/amber-console.css -> components/toggle.css */
/* ===========================================================================
   Toggle — a two-position switch.

   The literal ON / OFF text is MANDATORY. On a monochrome panel a thumb
   position alone is ambiguous: there is no color to say "this one is live".

   The thumb jumps. No transition.

   Two equivalent forms, both shipped:

     button    <button class="ac-toggle" aria-pressed="false">
                 <span>Plasma</span>
                 <span class="ac-toggle__track"><span class="ac-toggle__thumb"></span></span>
                 <span class="ac-toggle__state">OFF</span>
               </button>
               State hook: aria-pressed (or .ac-toggle--on). Needs JS to flip.

     checkbox  <label class="ac-toggle ac-toggle--input">
                 <span>Plasma</span>
                 <input type="checkbox">
                 <span class="ac-toggle__track"><span class="ac-toggle__thumb"></span></span>
                 <span class="ac-toggle__state"></span>
               </label>
               Pure CSS, no JS. The state text is generated from :checked.
   =========================================================================== */

.ac-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--ac-space-3);
  min-height: 44px;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  text-transform: uppercase;
  letter-spacing: var(--ac-tracking-body);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

.ac-toggle__track {
  position: relative;
  width: 64px;
  height: 28px;
  flex: none;
  border: var(--ac-border-w) solid var(--ac-stroke);
  /* The switch housing is extruded like a key — see the note in button.css for
     why the edge is declared on the component rather than in classic.css. The
     disabled rules below deliberately drop it: a housing that is not driven is
     still drawn, but it no longer stands off the glass. */
  box-shadow: var(--ac-glow-box), var(--ac-edge, 0 0 0 transparent);
  border-radius: var(--ac-radius-sm);
}

.ac-toggle__thumb {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 26px;
  height: 18px;
  background: var(--ac-ink-faint);
}

/* The three ON hooks, stated once: the modifier class, the ARIA state, and the
   CSS-only variant's :checked. They were three identical blocks in two places
   and had already drifted apart on the disabled track below. */
.ac-toggle--on .ac-toggle__thumb,
.ac-toggle[aria-pressed="true"] .ac-toggle__thumb,
.ac-toggle--input input:checked ~ .ac-toggle__track .ac-toggle__thumb {
  left: auto;
  right: 3px;
  background: var(--ac-fill);
  box-shadow: var(--ac-glow-box);
}

.ac-toggle__state {
  min-width: 2.2em;
  text-align: left;
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
}

.ac-toggle--on .ac-toggle__state,
.ac-toggle[aria-pressed="true"] .ac-toggle__state,
.ac-toggle--input input:checked ~ .ac-toggle__state {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
}

.ac-toggle:disabled {
  color: var(--ac-ink-faint);
  text-shadow: none;
  cursor: default;
}
.ac-toggle:disabled .ac-toggle__track {
  border-color: var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
}
.ac-toggle:disabled .ac-toggle__thumb {
  background: var(--ac-ink-trace);
  box-shadow: none;
}

.ac-toggle:focus-visible {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* --- CSS-only variant: <label> wrapping a real checkbox --- */
.ac-toggle--input input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.ac-toggle--input .ac-toggle__state::after {
  content: "OFF";
}

/* The lit thumb and state colour for this variant are declared with the other
   two ON hooks above; only the generated text is specific to it. */
.ac-toggle--input input:checked ~ .ac-toggle__state::after {
  content: "ON";
}

/* Matches .ac-toggle:disabled above, and that is the point: the two forms are
   documented as equivalent, so they have to LOOK equivalent. This one used to
   blank the shadow while the button form dimmed it, which meant a disabled
   switch built the CSS-only way sat flat on the panel while the same switch
   built the button way still read as a drawn housing.

   The housing dims rather than going dark because it is a box that is still
   there, not a lamp that is out. The THUMB is the lamp, and it does go dark —
   see the rule below. */
.ac-toggle--input input:disabled ~ .ac-toggle__track {
  border-color: var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
}
.ac-toggle--input input:disabled ~ .ac-toggle__track .ac-toggle__thumb {
  background: var(--ac-ink-trace);
  box-shadow: none;
}

.ac-toggle--input input:focus-visible ~ .ac-toggle__track {
  outline: 2px dashed var(--ac-ink-dim);
  outline-offset: 3px;
}

/* >>> src/amber-console.css -> components/statusbar.css */
/* ===========================================================================
   Status bar — the machine's voice.

   Inverse video, terse KEY:VALUE strings, no pleasantries: "STATUS:AUTO MODE",
   not "Your show is currently running".

   Put role="status" on it so a screen reader announces changes.

   --line is the quiet sibling: transparent, ruled top and bottom, no glow, for
   data rows that report rather than assert.
   =========================================================================== */

.ac-statusbar {
  box-sizing: border-box;
  display: flex;
  gap: var(--ac-space-8);
  align-items: baseline;
  flex-wrap: wrap;
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: var(--ac-tracking-body);
  padding: 6px var(--ac-space-4);
  box-shadow: var(--ac-glow-box);
  min-width: 0;
}

.ac-statusbar--line {
  background: transparent;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  border-top: var(--ac-border-w) solid var(--ac-stroke-dim);
  border-bottom: var(--ac-border-w) solid var(--ac-stroke-dim);
  /* The `none` that used to sit here canceled the filled bar's halo, which was
     right when a border could not carry one. Now it can: the rules are lit
     cells at emit-50, so they get that tier rather than nothing. Note the text
     above keeps full drive — only the STROKES stepped down. */
  box-shadow: var(--ac-glow-box-dim);
}

/* >>> src/amber-console.css -> components/readout.css */
/* ===========================================================================
   Readout — an instrument value.

   The number is the largest thing on the panel and the only --ac-ink-bright text
   in its region. Units ride at body size so the digits keep the eye.

   Markup: <div class="ac-readout">
             <div class="ac-readout__label">Lamp</div>
             <div class="ac-readout__value">41.0<span class="ac-readout__unit">V</span></div>
           </div>
   =========================================================================== */

.ac-readout {
  display: flex;
  flex-direction: column;
  gap: var(--ac-space-2);
  font-family: var(--ac-font-terminal);
  text-align: center;
  min-width: 0;
}

.ac-readout__label {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  padding: 3px 12px;
  font-size: var(--ac-type-small);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.ac-readout__label--plain {
  background: transparent;
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
  padding: 0;
}

.ac-readout__value {
  font-size: var(--ac-type-display);
  line-height: 1;
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
}

.ac-readout__unit {
  font-size: var(--ac-type-body);
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  margin-left: 6px;
}

/* Hero value — one per screen at most. */
.ac-readout--lg .ac-readout__value {
  font-size: 64px;
}

/* Dense form: label left, value right, on one row. For instrument tables. */
.ac-readout--inline {
  flex-direction: row;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--ac-space-4);
  text-align: left;
}
.ac-readout--inline .ac-readout__value {
  font-size: var(--ac-type-title);
}

/* >>> src/amber-console.css -> components/badge.css */
/* ===========================================================================
   Badge — a micro tag.

   The one place in this system with a 1px border: a badge is a chip, not
   structure, and 2px on something 8px tall reads as a solid block.

   Silkscreen at 8px. This is the ONLY size range where VT323 must not be used —
   it is a bitmap face and turns to mud below 18px.
   =========================================================================== */

.ac-badge {
  display: inline-block;
  font-family: var(--ac-font-micro);
  font-size: var(--ac-type-micro);
  line-height: 1.4;
  letter-spacing: var(--ac-tracking-micro);
  text-transform: uppercase;
  padding: 4px 8px;
  border: 1px solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  border-radius: 2px;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
}

.ac-badge--filled {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  border-color: var(--ac-fill);
}

.ac-badge--dim {
  color: var(--ac-ink-faint);
  text-shadow: var(--ac-glow-text-faint);
  border-color: var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
}

/* >>> src/amber-console.css -> components/banner.css */
/* ===========================================================================
   Banner — the loudest element the system allows. One per screen.

   The ✳✳ glyphs are injected by ::before / ::after, so the markup stays clean
   text. Combine with .ac-blink for a live, unacknowledged alarm — and note that
   the glyphs and the inverse-video pairing are what make it still read as an
   alarm when the blink is switched off by prefers-reduced-motion.

   Put role="alert" on it when it announces a state change.
   =========================================================================== */

.ac-banner {
  box-sizing: border-box;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--ac-space-6);
  border: var(--ac-border-w) solid var(--ac-stroke);
  box-shadow: var(--ac-glow-box);
  border-radius: 2px;
  padding: 10px 24px;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-display);
  line-height: 1;
  letter-spacing: 0.14em;
  color: var(--ac-ink-bright);
  text-transform: uppercase;
  text-shadow: var(--ac-glow-text);
}

.ac-banner::before,
.ac-banner::after {
  content: "✳✳";
  letter-spacing: 0;
}

/* At display size a two-word banner has a min-content width around 400px, which
   does not fit a 390px phone. Step down to title size and tighten the gap
   rather than let the frame clip it. The glyphs and the inverse-video pairing —
   the parts that carry the alarm — are untouched. */
@media (max-width: 480px) {
  .ac-banner {
    gap: var(--ac-space-3);
    padding: 10px var(--ac-space-3);
    font-size: var(--ac-type-title);
    flex-wrap: wrap;
  }
}

/* Inverse video for an alarm that must survive a static screenshot. */
.ac-banner--filled {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  box-shadow: var(--ac-glow-box);
}

.ac-banner--dim {
  color: var(--ac-ink-faint);
  border-color: var(--ac-stroke-dim);
  text-shadow: var(--ac-glow-text-faint);
  box-shadow: var(--ac-glow-box-dim);
}

/* >>> src/amber-console.css -> components/rule.css */
/* ===========================================================================
   Rule — a 2px divider. Use <hr class="ac-hr">.
   =========================================================================== */

.ac-hr {
  border: none;
  border-top: var(--ac-border-w) solid var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
  margin: var(--ac-space-4) 0;
}

/* Full-strength divider, for separating two regions rather than two rows. */
.ac-hr--bright {
  border-top-color: var(--ac-stroke);
}

/* >>> src/amber-console.css -> components/table.css */
/* ===========================================================================
   Table — the data grid this UI genre lives on.

   Inverse-video <thead> in Silkscreen micro caps, --ac-ink-trace row separators,
   zero rounding. Numeric columns get .ac-table__num for tabular alignment.

   __row--active is the live row: inverse video. Add .ac-blink for an
   unacknowledged alarm — the inverse fill is what carries it when the blink is
   suppressed by prefers-reduced-motion.

   Wrap wide tables in <div class="ac-table-scroll"> so the page body never
   scrolls sideways.
   =========================================================================== */

.ac-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-small);
}

.ac-table th {
  text-align: left;
  font-weight: 400;
  text-transform: uppercase;
  font-family: var(--ac-font-micro);
  font-size: var(--ac-type-micro);
  letter-spacing: var(--ac-tracking-micro);
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  padding: 6px 12px;
  white-space: nowrap;
}

.ac-table td {
  padding: 6px 12px;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  border-bottom: var(--ac-border-w) solid var(--ac-ink-trace);
  text-transform: uppercase;
}

.ac-table tbody tr:last-child td {
  border-bottom: none;
}

.ac-table__row--active td {
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
}

.ac-table--zebra tbody tr:nth-child(even) td {
  background: var(--ac-screen-raised);
}

.ac-table--dense td,
.ac-table--dense th {
  padding: 2px 10px;
}

.ac-table__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.ac-table-scroll {
  overflow-x: auto;
  max-width: 100%;
}

/* >>> src/amber-console.css -> components/list.css */
/* ===========================================================================
   List — a keyed log. KEY:VALUE rows joined by leader dots.

   Derived from law 4 (the character grid) and law 6 (ornament is typographic):
   the filler is a 2px dotted rule, which at 2px renders as square cells rather
   than round dots — the same grid the rest of the panel sits on.

   Markup: <dl class="ac-list">
             <div class="ac-list__item">
               <dt class="ac-list__key">Lamp Hours</dt>
               <span class="ac-list__fill"></span>
               <dd class="ac-list__value">1284</dd>
             </div>
           </dl>

   WHEN NOT TO USE: more than one value per key, or anything sortable — that is
   a table. This is for a fixed set of named readings.
   =========================================================================== */

.ac-list {
  display: flex;
  flex-direction: column;
  gap: var(--ac-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: var(--ac-tracking-body);
  min-width: 0;
}

.ac-list__item {
  display: flex;
  align-items: baseline;
  gap: var(--ac-space-2);
  margin: 0;
  min-width: 0;
}

.ac-list__key {
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
  white-space: nowrap;
}

/* An empty flex item's baseline is its bottom margin edge, so the dotted rule
   lands exactly on the row's text baseline. */
.ac-list__fill {
  flex: 1;
  min-width: var(--ac-space-6);
  align-self: baseline;
  border-bottom: var(--ac-border-w) dotted var(--ac-ink-trace);
}

.ac-list__value {
  margin: 0;
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Values that are the point of the panel rather than context. */
.ac-list--bright .ac-list__value {
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
}

.ac-list--dense {
  gap: 0;
  font-size: var(--ac-type-small);
}

.ac-list--dim .ac-list__key {
  color: var(--ac-ink-faint);
  text-shadow: var(--ac-glow-text-faint);
}
.ac-list--dim .ac-list__value {
  color: var(--ac-ink-dim);
  text-shadow: var(--ac-glow-text-dim);
}

/* >>> src/amber-console.css -> components/dialog.css */
/* ===========================================================================
   Dialog — built on the native <dialog> element.

   It appears. There is no scale, no fade, no backdrop blur: law 3 says no
   elevation, and the animation policy says state changes have no in-between
   frames. The backdrop is flat black so the panel behind it reads as powered
   down, not as a blurred photograph.

   Markup: <dialog class="ac-dialog">
             <form method="dialog">
               <h2 class="ac-dialog__title">Confirm</h2>
               <div class="ac-dialog__body">…</div>
               <div class="ac-dialog__actions">
                 <button class="ac-btn" value="cancel">Cancel</button>
                 <button class="ac-btn ac-btn--filled" value="ok">Confirm</button>
               </div>
             </form>
           </dialog>

   Open it with dialog.showModal() — or let src/amber-console.js wire
   [data-ac-dialog-open] / [data-ac-dialog-close] for you.

   WHEN NOT TO USE: anything the operator must be able to see the panel behind.
   A modal on a control surface hides machine state; prefer an .ac-banner.
   =========================================================================== */

.ac-dialog {
  box-sizing: border-box;
  max-width: min(90vw, 560px);
  padding: 0;
  border: var(--ac-border-w) solid var(--ac-stroke);
  border-radius: var(--ac-radius);
  background: var(--ac-screen);
  color: var(--ac-ink);
  text-shadow: var(--ac-glow-text);
  font-family: var(--ac-font-terminal);
  box-shadow: var(--ac-glow-box);
  overflow: hidden;
}

.ac-dialog::backdrop {
  /* Flat, no blur. Overridable, but keep it opaque enough to kill the panel. */
  background: var(--ac-backdrop, rgba(0, 0, 0, 0.72));
}

/* The machine's own heading — same inverse strip as .ac-panel--bar. */
.ac-dialog__title {
  margin: 0;
  padding: 4px 16px;
  text-align: center;
  background: var(--ac-fill);
  color: var(--ac-on-fill);
  text-shadow: none;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

.ac-dialog__body {
  padding: var(--ac-space-4);
  font-size: var(--ac-type-body);
  line-height: var(--ac-leading);
}

.ac-dialog__actions {
  display: flex;
  gap: var(--ac-space-4);
  justify-content: flex-end;
  flex-wrap: wrap;
  padding: 0 var(--ac-space-4) var(--ac-space-4);
}

.ac-dialog--wide {
  max-width: min(94vw, 860px);
}

/* >>> src/amber-console.css -> components/meter.css */
/* ===========================================================================
   Meter — a horizontal bargraph in discrete cells.

   The fill is a repeating-linear-gradient of 8px blocks with 4px gaps: a
   stepped bar, never a smooth one. A continuous gradient would imply a
   precision the panel does not have, and law 3 forbids decorative gradients.

   Set the value as a 0-100 number on --ac-meter-value:

     <div class="ac-meter">
       <div class="ac-meter__track" style="--ac-meter-value: 72"
            role="progressbar" aria-valuenow="72" aria-valuemin="0" aria-valuemax="100">
         <div class="ac-meter__bar"></div>
       </div>
       <div class="ac-meter__scale"><span>0</span><span>50</span><span>100</span></div>
     </div>

   WHEN NOT TO USE: a single precise reading. That is an .ac-readout — a number
   the operator can write down beats a bar they have to estimate.
   =========================================================================== */

.ac-meter {
  display: flex;
  flex-direction: column;
  gap: var(--ac-space-1);
  font-family: var(--ac-font-terminal);
  min-width: 0;
}

.ac-meter__track {
  position: relative;
  height: 24px;
  border: var(--ac-border-w) solid var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
  border-radius: var(--ac-radius-sm);
  background: var(--ac-screen-well);
  overflow: hidden;
}

.ac-meter__bar {
  /* POSITIONED SO IT PAINTS ABOVE ITS OWN GHOST. sim/afterglow.css hangs a
     lagging copy of this bar on .ac-meter__track::before, and an unpositioned
     block child paints BELOW any positioned pseudo-element — so without this the
     draining ghost would cover the live bar instead of trailing behind it. With
     both positioned, tree order settles it and ::before goes underneath. */
  position: relative;
  height: 100%;
  width: calc(var(--ac-meter-value, 0) * 1%);
  background: repeating-linear-gradient(
    90deg,
    var(--ac-fill) 0 8px,
    transparent 8px 12px
  );
  box-shadow: var(--ac-glow-box);
}

.ac-meter__scale {
  display: flex;
  justify-content: space-between;
  font-family: var(--ac-font-micro);
  font-size: var(--ac-type-micro);
  letter-spacing: var(--ac-tracking-micro);
  color: var(--ac-ink-faint);
  text-shadow: var(--ac-glow-text-faint);
}

.ac-meter--lg .ac-meter__track {
  height: 32px;
}

.ac-meter--dim .ac-meter__bar {
  background: repeating-linear-gradient(
    90deg,
    var(--ac-ink-faint) 0 8px,
    transparent 8px 12px
  );
  box-shadow: none;
}

/* Over-range. Inverse video on the whole track, and blink on top — there is no
   red, so the FILL is what carries the alarm and the blink is the emphasis.

   THE TRACK LIGHTS AND THE BAR CUTS INTO IT, which is the only way round that
   works. The bar is already --ac-fill, so filling the track with the same token
   would make the bar vanish into it and the reading would be gone. Inverting the
   pair keeps the value legible and states the alarm at the same time.

   THE FILL IS NOT DECORATION ON TOP OF THE BLINK, IT IS THE HALF THAT SURVIVES.
   Blink is switched off by prefers-reduced-motion, by the blink style flag, by
   print and by forced colors — four environments in which this rule used to
   leave an over-range meter looking exactly like a normal one, because a
   strengthened border was the only thing it changed. That is the same argument
   .ac-banner--filled and .ac-table__row--active already make, and this component
   was the one place the system did not follow it. */
.ac-meter--alarm .ac-meter__track {
  border-color: var(--ac-stroke);
  background: var(--ac-fill);
  box-shadow: var(--ac-glow-box);
}
.ac-meter--alarm .ac-meter__bar {
  background: repeating-linear-gradient(
    90deg,
    var(--ac-on-fill) 0 8px,
    transparent 8px 12px
  );
  /* Unlit cells on a lit block do not scatter — the same rule that takes the
     halo off every other piece of inverse video in the framework. */
  box-shadow: none;
  /* A blink site: reads the pair declared in tokens/effects.css and explained in
     base/blink.css, which is also what switches it off for the blink flag,
     reduced motion, print and forced colors. Nothing outside this rule needs to
     know this element blinks. */
  animation-name: var(--ac-blink-force, var(--ac-blink-name));
  animation-duration: 1.1s;
  animation-timing-function: var(--ac-blink-force-ease, var(--ac-blink-ease));
  animation-iteration-count: infinite;
}

/* >>> src/amber-console.css -> components/keypad.css */
/* ===========================================================================
   Keypad — a 3-column numeric entry grid.

   These panels are operated with gloves, so the keys are large and square-ish
   and the digit is centered rather than parked top-left: on a key with one
   character there is no label/value split for the top-left rule to serve.

   Keys are ordinary .ac-btn elements, so every button state applies unchanged.

   Markup: <div class="ac-keypad">
             <button class="ac-btn ac-keypad__key">7</button>  … 1-9 …
             <button class="ac-btn ac-keypad__key ac-keypad__key--fn">Clr</button>
             <button class="ac-btn ac-keypad__key">0</button>
             <button class="ac-btn ac-keypad__key ac-keypad__key--fn">Ent</button>
           </div>

   Clr / Ent are Title Case: they are operator soft keys, like tabs.

   WHEN NOT TO USE: free text, or anything a hardware keyboard is present for.
   An on-screen keypad exists because there is no keyboard in a projection booth.
   =========================================================================== */

.ac-keypad {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--ac-space-2);
  max-width: 420px;
  min-width: 0;
}

.ac-keypad__key {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Comfortably past the 44px floor without four rows running off a phone. */
  min-height: 72px;
  min-width: 0;
  padding: 10px 16px;
  font-size: var(--ac-type-title);
}

/* CLR / ENT read as commands rather than digits. */
.ac-keypad__key--fn {
  font-size: var(--ac-type-body);
}

.ac-keypad__key--wide {
  grid-column: span 2;
}

.ac-keypad--dense .ac-keypad__key {
  min-height: 56px;
}

/* >>> src/amber-console.css -> components/spinner.css */
/* ===========================================================================
   Spinner — a wait indicator made of characters.

   Not a rotating circle: a circle is an icon, and this system has no icon set.
   This is the terminal's own idiom — one cell cycling through | / — \ on a hard
   steps() clock, so it lands on whole characters the way a real one did.

   Implemented as a 4-line block scrolled inside a 1-cell window by a transform,
   which every browser animates. (Animating the `content` property directly is
   not portable.)

   Markup: <span class="ac-spinner" role="status" aria-label="Working"></span>

   WHEN NOT TO USE: anything whose duration you know. A machine that can measure
   progress should show an .ac-meter; a spinner admits it cannot.
   =========================================================================== */

.ac-spinner {
  display: inline-block;
  overflow: hidden;
  width: 1ch;
  height: 1em;
  line-height: 1;
  vertical-align: -0.1em;
  font-family: var(--ac-font-terminal);
  font-size: var(--ac-type-body);
  color: var(--ac-ink-bright);
  text-shadow: var(--ac-glow-text);
}

.ac-spinner::before {
  content: "|\A/\A—\A\\";
  display: block;
  white-space: pre;
  line-height: 1;
  animation: ac-spinner-turn 0.8s steps(4) infinite;
}

@keyframes ac-spinner-turn {
  to {
    transform: translateY(-4em);
  }
}

.ac-spinner--dim {
  color: var(--ac-ink-faint);
  text-shadow: var(--ac-glow-text-faint);
}

.ac-spinner--lg {
  font-size: var(--ac-type-title);
}

/* Static, but still unmistakably "working". */
@media (prefers-reduced-motion: reduce) {
  .ac-spinner::before {
    animation: none;
  }
}

/* >>> src/amber-console.css -> components/sweep.css */
/* PPI SWEEP — the radar face, and the one place persistence is the whole widget.
   ============================================================================

   Markup:
     <div class="ac-sweep" role="img" aria-label="Search radar, no contacts">
       <span class="ac-sweep__beam"></span>
     </div>

   THE TRAIL IS A GRADIENT, NOT AN ANIMATION, and that is the entire trick. The
   obvious way to draw a decaying radar trail is to stamp a mark every frame and
   fade each one — which is a per-pixel framebuffer problem, needs a canvas, and
   is exactly what CSS cannot do. But a PPI trail is not really a history of
   marks: it is one continuous falloff behind a rotating line, and a
   conic-gradient IS that falloff. Rotate it and the decay comes for free,
   composited on the GPU, in one element, with no script.

   So the angular stops below are the phosphor's decay curve read in degrees
   instead of milliseconds. That is legitimate rather than a pun: the beam sweeps
   at a constant rate, so angle behind the beam IS time since that bearing was
   painted. Same curve, same shape, different axis.

   THE LEADING EDGE IS --ac-halo-flash WHERE THERE IS ONE. On P7 the beam writes in
   blue and the screen holds in yellow-green, so the first few degrees are the
   flash and everything behind is the afterglow — the cascade rendered exactly as
   the hardware does it, and the reason P7 was fitted to radar in the first
   place. Every other emitter falls back to --ac-halo-1 and the leading edge is
   simply a brighter version of the trail, which is also correct for them.

   No `transition` anywhere in this file: scripts/check-prohibitions.mjs forbids
   it in src/components/, and nothing here needs one. */

.ac-sweep {
  position: relative;
  aspect-ratio: 1;
  width: 100%;
  max-width: var(--ac-sweep-size, 260px);
  border: 2px solid var(--ac-stroke-dim);
  box-shadow: var(--ac-glow-box-dim);
  border-radius: 50%;
  overflow: hidden;
  background:
    /* Range rings. Drawn as hard-stopped radial gradients rather than as nested
       elements, so the whole graticule costs no extra nodes. */
    radial-gradient(circle at 50% 50%,
      transparent 0 24%, rgba(var(--ac-halo-3), 0.16) 24% 24.6%, transparent 24.6%),
    radial-gradient(circle at 50% 50%,
      transparent 0 49%, rgba(var(--ac-halo-3), 0.16) 49% 49.6%, transparent 49.6%),
    radial-gradient(circle at 50% 50%,
      transparent 0 74%, rgba(var(--ac-halo-3), 0.13) 74% 74.6%, transparent 74.6%),
    /* The unlit screen behind them. */
    radial-gradient(circle at 50% 50%, var(--ac-screen-raised), var(--ac-screen-well));
}

/* The crosshair, on the face rather than on the beam so it does not rotate. */
.ac-sweep::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to right, transparent calc(50% - 1px), rgba(var(--ac-halo-3), 0.15) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)),
    linear-gradient(to bottom, transparent calc(50% - 1px), rgba(var(--ac-halo-3), 0.15) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px));
}

/* THE BEAM AND ITS WAKE.

   THE STOPS RUN BACKWARDS, AND THE ROTATION DOES NOT. Both a conic-gradient and
   rotate() go clockwise, so a point at screen bearing θ shows gradient angle
   θ − A, where A is the current rotation. Writing the flash at 0deg therefore
   puts the whole falloff at θ ∈ [A, A+arc] — clockwise AHEAD of the beam, in the
   direction it is travelling toward, which is the bug this file shipped with for
   its whole life. Light lands behind a beam, not in front of it.

   So the flash goes at 360deg and the tail counts DOWN from there, and the fix
   is not to reverse the rotation: bearings on a PPI increase clockwise, and the
   bearing scale, the EBL, the guard sector and every contact arm on the radar
   page are built on that. The contacts are also the proof — each one delays its
   strike so that it fires when A equals its own bearing, then drains where the
   beam has already been. The wedge now drains with them instead of against them.

   ALPHAS ARE THE PALETTE'S OWN DECAY LAW READ IN DEGREES. --ac-decay-ease in
   tokens/colors.css is sampled from the Becquerel law I = 1/(1 + t/t0)^1.4, and
   back-solving its linear() stops gives t0 = 0.032 × the tail. Angle behind the
   beam is time since that bearing was painted, so the same curve lands on the
   same shape here — through a ^0.75 perceptual compression, which is what the
   hand-picked stops this replaces were already doing by eye.

   --AC-SWEEP-ARC IS ARITHMETIC RATHER THAN TASTE, and it is a fallback rather
   than a declaration for the same reason --ac-sweep-period and --ac-sweep-size
   are: a property set on the element cannot be reached from a page, and the
   radar page sets two of those three. The period below is the tail × 1.5, so on
   every phosphor above that floor the tail occupies tail ÷ period × 360 =
   240deg of the face. The remaining 10deg is a taper to nothing — the curve is
   a power law, it is still near 2/255 where it runs out, and cutting it off
   there leaves a banded edge where the wake should have gone dark. An emitter
   BELOW the floor wants this smaller: neon drains in 0.16s against a 2.5s
   revolution, which is 23deg — a thin spinning line, not a wake. Deriving it
   here would want calc(var(--ac-persist-tail) / var(--ac-sweep-period) * 360deg)
   and division that type-checks a time against a time, which is younger than
   this file's floor. So it stays a knob, and the 250deg in the stops below is
   what that expression evaluates to.

   THE COLOR MARCH IS THE P7 CASCADE. --ac-halo-flash holds for the first degree and
   a half — about 19ms at 4.5s a revolution, a frame or two, which is what the
   blue ZnS:Ag layer is worth — then hands to --ac-halo-4 at the hottest point of the
   yellow-green (Zn,Cd)S:Cu response and drains down through --ac-halo-1 and --ac-halo-2
   to --ac-halo-3. Every stop keeps the var(--ac-halo-flash, var(--ac-halo-1)) fallback, so a
   single-coating emitter gets a brighter leading edge and no cascade, which is
   correct for it. */
.ac-sweep__beam {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  border-radius: 50%;

  /* Each stop is one reading off that curve, at the fraction of the arc named
     in the multiplier. THE STOPS ARE CLOSE TOGETHER AT THE KNEE because the
     curve is convex and a gradient interpolates straight: sampled sparsely, the
     straight line between two stops runs ABOVE the curve it is meant to be, and
     the first forty degrees fill in as a flat bright slab with an edge on it
     instead of draining. Sampling the knee finely and the tail coarsely is the
     same trade the linear() easings in tokens/colors.css make, for the same
     reason. */
  background: conic-gradient(
    from 0deg,
    rgba(var(--ac-halo-3), 0) calc(360deg - var(--ac-sweep-arc, 250deg)),
    rgba(var(--ac-halo-3), 0.008) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.9),
    rgba(var(--ac-halo-3), 0.024) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.8),
    rgba(var(--ac-halo-3), 0.038) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.6),
    rgba(var(--ac-halo-3), 0.059) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.38),
    rgba(var(--ac-halo-3), 0.085) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.26),
    rgba(var(--ac-halo-2), 0.12) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.18),
    rgba(var(--ac-halo-2), 0.18) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.11),
    rgba(var(--ac-halo-2), 0.25) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.072),
    rgba(var(--ac-halo-1), 0.33) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.05),
    rgba(var(--ac-halo-1), 0.4) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.036),
    rgba(var(--ac-halo-1), 0.49) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.024),
    rgba(var(--ac-halo-4), 0.58) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.016),
    rgba(var(--ac-halo-flash, var(--ac-halo-1)), 0.75) calc(360deg - var(--ac-sweep-arc, 250deg) * 0.006),
    rgba(var(--ac-halo-flash, var(--ac-halo-1)), 0.9) 360deg
  );
  /* THE PERIOD IS THE PHOSPHOR'S, WITH A FLOOR, and the floor is the point.
     A sweep wants to come round about as the trail finishes draining — that is
     the trade P7 and P39 were bought to make. Scaling straight off the tail does
     that for the long phosphors (P39 -> 3s, P7 -> 4.5s), and gives neon 0.16s,
     which is not a radar, it is a strobe. So the floor holds the slow ones
     honest and keeps everything else watchable. */
  animation: ac-sweep-rotate
    var(--ac-sweep-period, max(2.5s, calc(var(--ac-persist-tail, 2s) * 1.5)))
    linear infinite;
}

/* THE ORIGIN, WHICH IS THE ONE PLACE A RADIAL FALLOFF IS EARNED. Everywhere else
   on the face the wake is as bright at the rim as at half range, because every
   bearing is painted for the same length of time however far out you stand. The
   center is not: all 360 bearings cross it, so it takes the whole revolution's
   dose and saturates toward the flash color. That is the bright spot on the
   middle of every scope photograph ever taken.

   It is radially symmetric, so rotating with the beam costs nothing and buys the
   right thing — it lives inside the beam's screen group where the light belongs,
   and it goes out with the beam on standby, which is correct: a set that is not
   transmitting is not writing a center either.

   closest-side pins the gradient's 100% to the radius rather than to the corner,
   so these percentages are RANGES. */
.ac-sweep__beam::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(
    circle closest-side at 50% 50%,
    rgba(var(--ac-halo-flash, var(--ac-halo-1)), 0.3) 0 1.5%,
    rgba(var(--ac-halo-4), 0.2) 3.5%,
    rgba(var(--ac-halo-1), 0.08) 7%,
    rgba(var(--ac-halo-1), 0) 13%
  );
}

/* THE WRITING LINE, AND IT IS AN ARM RATHER THAN AN ANGLE. A few degrees of the
   gradient is a WEDGE: 1.8px wide at the rim of a 260px face and under half a
   pixel at quarter range, so the beam thins out and aliases away exactly where
   the picture is densest. A CRT spot is a constant width at every radius, so the
   line is drawn the way every mark on the radar page is drawn — a span pinned at
   the center, given a height in percent, rotated to a bearing.

   It gets that bearing for free: it is a child of the rotating element, and
   `from 0deg` starts the conic at twelve o'clock, so the bar and the gradient's
   leading edge are the same bearing by construction rather than by two numbers
   being kept in step.

   The horizontal ramp is the spot's finite width. Not a filter: blur(), which
   would put a second raster layer inside one that turns continuously. The
   brightness does not fall off outward because the radial deflection is linear
   and the writing speed is therefore constant. */
.ac-sweep__beam::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 50%;
  width: var(--ac-sweep-line, 3px);
  height: 50%;
  margin-left: calc(var(--ac-sweep-line, 3px) / -2);
  background: linear-gradient(
    to right,
    rgba(var(--ac-halo-flash, var(--ac-halo-1)), 0),
    rgba(var(--ac-halo-flash, var(--ac-halo-1)), 0.95) 50%,
    rgba(var(--ac-halo-flash, var(--ac-halo-1)), 0)
  );
}

@keyframes ac-sweep-rotate {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* A rotating light is unambiguously motion, and unlike the mesh it is not the
   shape of the hardware — it is the hardware doing something. It stops, and what
   is left is a static lit sector, which still reads as a radar face. */
@media (prefers-reduced-motion: reduce) {
  .ac-sweep__beam { animation: none; }
}


/* Classic is a second corner style, not a component, so it comes after every
   component whose radius it re-cuts and whose halo it composes with — and still
   before the environment overrides, so forced colors and print keep the last
   word on the shadows it adds. */
/* >>> src/amber-console.css -> components/classic.css */
/* ===========================================================================
   Classic — the corner a real panel could actually draw, the extruded key edge
   that came with it, and the label parked in the corner of the face.

   THIS IS THE DEFAULT LOOK. An 8px quarter-circle is a thing a modern rasterizer
   does; a gas-discharge panel addressing a coarse cell grid could not, and the
   keys on one stood on a hard offset edge with their label in the top-left corner
   and the body left empty for a value. That is what these panels were, so it is
   what the framework ships. The smooth corner is still here and still supported —
   it is now the thing you opt into rather than the thing you opt out of.

   IT IS THE DEFAULT IN CSS, WITH NO JAVASCRIPT. `:root` carries the classic
   tokens, so a page that links the stylesheet and authors nothing at all gets
   the chamfer and the extruded edge. This used to be true only when
   ../amber-console.js was loaded — that file writes data-ac-style-classic="on",
   and without it a CSS-only consumer silently got the rounded corner this
   paragraph calls an opt-out. The stylesheet says what it means now.

     (nothing)                    classic. The default.
     .ac-rounded                  the way back to the arc, at any scope.
     data-ac-style-classic="off"  the same opt-out for a whole page; what the
                                  optional JS writes when the switch is thrown.
     .ac-classic                  back to classic inside a rounded region.
     data-ac-style-classic="on"   the page-level form of that.

   All are the same set of rules reading the same tokens; none is more real than
   the others, and they nest in either direction.

   HOW THE SCOPING WORKS — the thing to understand before editing. The three
   scopes set nothing but CUSTOM PROPERTIES, and custom properties inherit. Every
   rule below is written ONCE and reads its values out of the cascade, so
   `.ac-rounded` needs no mirror of any rule and nesting resolves by ordinary
   inheritance rather than a specificity coin toss. Adding a fourth scope, or a
   per-region override, is a token block and nothing else.

   IT IS `border-radius` / `corner-shape` AND NOT A CLIP because `clip-path` and
   `mask-image` clip the element's ENTIRE painting, outer box-shadow included —
   so a clipped corner would buy a stepped edge by deleting the discharge halo
   off all four sides. The two corner paths carry different radii rather than one
   degraded chamfer, so Firefox loses the chamfer and nothing else. Both in
   RATIONALE.md § components/classic.css.

   =========================================================================== */

/* --- (1) The scopes. Tokens only; no rule below is repeated per scope. --- */

/* CLASSIC IS THE DEFAULT, IN CSS, WITH NO JAVASCRIPT.
   `:root` leads this list, which is the whole point: a page that links the
   stylesheet and authors no attribute and no class gets the classic corner. The
   two opt-in scopes ride along so a classic REGION works inside a rounded page,
   and so the attribute the optional JS writes keeps meaning what it says. */
:root,
.ac-classic,
[data-ac-style-classic="on"] {
  /* Grid-quantized corner, from tokens/spacing.css. Restated here only so a
     .ac-classic region nested inside a .ac-rounded one gets it back. */
  --ac-radius: 2px;
  --ac-radius-sm: 2px;
  --ac-corner: bevel;
  --ac-edge: var(--ac-edge-3d);
  --ac-edge-press: var(--ac-edge-3d-in);
}

@supports (corner-shape: bevel) {
  /* The default's chamfer. It sits HERE, above the opt-out, and that position is
     load-bearing: `:root` and `[data-ac-style-classic="off"]` both match <html>
     at the same weight, so whichever is written last wins. Below the opt-out,
     this block handed the bevel radii back to a page that had explicitly asked
     for round corners — visible as a chamfered key with no extrusion, which is
     neither of the two looks this file ships. */
  :root {
    --ac-radius: 2px 4px 8px 4px;
    --ac-radius-sm: 1px 2px 4px 2px;
  }
}

/* The opt-out, at any scope: a page (the attribute, which is what the optional
   JS writes) or a region (the class). Declared AFTER the :root default above so
   that when both land on <html> — `:root` and `[data-ac-style-classic="off"]`
   weigh the same — the explicit off wins on order. */
.ac-rounded,
[data-ac-style-classic="off"] {
  --ac-radius: 8px;
  --ac-radius-sm: 4px;
  --ac-corner: round;
  /* A shadow that paints nothing, rather than `none`: `none` is not a valid item
     inside a comma-separated shadow list, so `var(--ac-glow-box), none` would
     invalidate the whole declaration and take the halo down with it. */
  --ac-edge: 0 0 0 transparent;
  --ac-edge-press: 0 0 0 transparent;
}

@supports (corner-shape: bevel) {
  /* Where the shape can be cut, the radius is spent on the CUT instead of an arc,
     and NOT EVENLY: these keys are lit from the top left, so the far corner is
     barely cut and the near one is cut hardest. Cut all four the same and the key
     reads as an octagon. 2 / 4 / 8 / 4 in TL TR BR BL order, from the system's own
     radius steps; --ac-radius-sm runs the same diagonal at half depth.
     See RATIONALE.md § The four corners are not the same size.

     The :root half of this is declared above the opt-out; these two scopes are
     declared below it, so a classic REGION inside a rounded page is chamfered
     rather than inheriting the opt-out's arc. */
  .ac-classic,
  [data-ac-style-classic="on"] {
    --ac-radius: 2px 4px 8px 4px;
    --ac-radius-sm: 1px 2px 4px 2px;
  }
}

/* --- (2) The corner. One rule, three scopes, value from the cascade. --- */

/* THE LIST IS THE EIGHT PLACES A RADIUS IS DECLARED, and it was derived rather
   than chosen: grep `border-radius: var(--ac-radius` across components/ and this is
   what comes back. .ac-badge and .ac-banner are literal 2px, .ac-check is square,
   and .ac-radio and .ac-sweep are circles — a circle has no corner to cut, so none
   of the five belong here. Add to this list when a new component takes a --ac-radius,
   or its corner will stay round while everything around it is cut. */
@supports (corner-shape: bevel) {
  /* The scope half is folded into one :is() so the component list is written
     ONCE. Specificity is unchanged — :is() takes the highest of its arguments,
     so both halves weigh (0,1,0) exactly as the two-selector form did, and the
     pair still lands at (0,2,0). A complex selector inside :is() is safe to lean
     on HERE and nowhere else in this file: everything in this block is already
     gated behind corner-shape, which no engine lacking Selectors-4 supports. */
  :is(
    .ac-classic,
    .ac-rounded,
    :is(.ac-classic, .ac-rounded, [data-ac-style-classic="on"]) *
  ):is(
    .ac-btn, .ac-tab, .ac-panel, .ac-dialog, .ac-input, .ac-nav__link,
    .ac-toggle__track, .ac-meter__track
  ) {
    /* stylelint-disable-next-line property-no-unknown -- corner-shape is newer
       than the property list stylelint ships. The @supports is the real gate; an
       engine that does not know the property never reads this block. */
    corner-shape: var(--ac-corner, round);
  }
}

/* --- (3) The extrusion — on the controls, and only on the controls. --- */

/* A KEY IS EXTRUDED; A PANEL IS A HOLE IN THE BEZEL. The corner belongs to the
   raster and reaches everything, but a drop shadow claims the thing stands off the
   glass, and that is true of a soft key and a switch housing and of nothing else
   on the board. Giving .ac-panel an edge too was tried and reads as a floating
   card, which is a different design language from a control surface.

   THE RULES THAT USED TO BE HERE ARE GONE, AND THIS FILE IS BETTER FOR IT.
   Four scope-prefixed rules once lived at this point — the resting edge, the
   pressed edge, a blank for disabled, and one for the toggle housing — each
   repeating the same doubled `:is(scopes):is(X), :is(scopes) :is(X)` prefix.
   They existed only to APPEND --ac-edge to a box-shadow the component had
   already declared, and they did it by out-specifying the component, which is
   what made a dim key glow at full drive.

   components/button.css and components/toggle.css now write
   `var(--ac-glow-box), var(--ac-edge, 0 0 0 transparent)` on each state
   themselves, reading a token this file sets. Outside a corner scope --ac-edge
   is undefined and paints nothing, so nothing needs to match a scope at all —
   which is exactly what the scopes-set-only-tokens design at the top of this
   file promised, and what these four rules were quietly breaking.

   Disabled needs no rule for the same reason: button.css already blanks the
   shadow, and nothing outranks it any more. */

/* --- (4) The face: label top-left. --- */

/* A soft key is a label plus room for a value, so the label parks in the corner
   and the body is left empty. This is .ac-btn--pad's alignment WITHOUT its 96px
   height, so it applies at any key size and composes with --pad unchanged.

   NO `display` HERE, DELIBERATELY: inline-flex would outweigh .ac-btn--block's
   `display: block` and .ac-btn--pad's `flex`, and a corner style that silently
   unblocks a full-width key is worse than one that does nothing. The three
   properties it does set are inert on a non-flex box.

   THE KEYPAD IS EXEMPT — a digit has no label/value split to serve, and those
   keys are centered on purpose. keypad.css states the reason. */
.ac-classic.ac-btn:not(.ac-keypad__key),
.ac-classic .ac-btn:not(.ac-keypad__key),
[data-ac-style-classic="on"] .ac-btn:not(.ac-keypad__key) {
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
}

/* Back to undeclared, which is what a bare .ac-btn computes to — `normal` on both
   flex properties and an inherited text-align. NOT `center`: that would center a
   --pad, and a pad parks its label top-left in either corner style. --pad is
   excluded outright for the same reason, since it out-specifies nothing here.

   `text-align` is restated rather than token-driven because a CSS-wide keyword
   arriving through var() substitution is the one part of this mechanism not worth
   betting the alignment on. */
.ac-rounded.ac-btn:not(.ac-btn--pad),
.ac-rounded .ac-btn:not(.ac-btn--pad) {
  align-items: normal;
  justify-content: normal;
  text-align: inherit;
}

/* --- (5) Inert keeps nothing, and it no longer has to be said twice. --- */

/* NOTHING HERE ANY MORE, DELIBERATELY.

   Two rules used to sit at this point: one blanking the shadow on a disabled key,
   one restoring the dimmed housing on a disabled toggle. Both existed purely to
   win back ground the extrusion rules above had taken by out-specifying the
   components — `[data-ac-style-classic="on"] .ac-btn` weighs the same as
   `.ac-btn:disabled` and landed later, so a dead key was being handed its halo
   back and needed it removed again.

   With the edge declared on the components, `.ac-btn:disabled { box-shadow:
   none }` in button.css is simply the last word, and `.ac-toggle:disabled
   .ac-toggle__track` in toggle.css already carried --ac-glow-box-dim at a weight
   nothing here contested. Both rules were restating what those files say.

   scripts/check-prohibitions.mjs could never have caught the underlying bug —
   it gates an ink level set without its halo, and the extrusion rules set no
   color — which is the argument for not having written them that way. */

/* --- (6) Print pays for shadow in ink. --- */

/* The extrusion goes and the cut corner stays: the corner is the shape of the
   control and survives being printed, the shadow is a lighting effect and does
   not. Blanked through the token rather than by restating four selectors — which
   is the same reason `--ac-edge` exists at all. */
@media print {
  :root {
    --ac-edge-3d: 0 0 0 transparent;
    --ac-edge-3d-in: 0 0 0 transparent;
  }
}


/* --- Environment overrides. Last, so they win. --- */
/* >>> src/amber-console.css -> base/a11y.css */
/* ===========================================================================
   Forced colors

   This whole design is a color trick, so high-contrast mode needs a real
   fallback rather than a broken one. Per the Forced Colors Mode spec the UA
   forces text-shadow and box-shadow to none, which kills the discharge glow —
   but ONLY while an element keeps forced-color-adjust: auto. What is left to do:

     1. Remove the simulation overlays entirely — they are decoration.
     2. Restate every border in CanvasText, since our borders carry the layout.
     3. Restate inverse video in Highlight / HighlightText, because "this block
        is the machine speaking" is information, not styling, and it would
        otherwise flatten into the page.
     4. Kill the glow again on everything (3) opts out.

   POINT 4 IS NOT BOOKKEEPING. `forced-color-adjust: none` opts an element out
   of forced colors WHOLESALE, and that hands it back its box-shadow — so the
   very rules that preserve inverse video also reinstate an amber discharge halo
   around it, which is an off-palette color bleeding into the one mode that
   exists to have no off-palette colors. Every block below that sets
   forced-color-adjust: none must also restate its shadows.

   The inverse-video list is the whole set of components that fill with
   var(--ac-fill). Grep for it before adding a component: a state that reads as
   "the machine is speaking" and flattens to an outline here has lost its
   meaning, not just its styling.
   =========================================================================== */

@media (forced-colors: active) {
  /* THE HALO AND THE KEY EDGE ARE SWITCHED OFF AT THE TOKEN, NOT PER RULE.
     ---------------------------------------------------------------------
     Every box-shadow this framework paints is built from these four tokens, so
     zeroing them here removes the discharge colour from all of them at once —
     and, crucially, does it in a way NO component rule can out-specify. A rule
     can beat `box-shadow: none` by weighing more; it cannot beat a token it is
     itself reading.

     That distinction is not theoretical. The per-selector suppression below
     lists `.ac-btn--filled` and `.ac-tab--active` at (0,1,0), and a component
     only has to say `.ac-btn--filled:hover` at (0,2,0) to take the halo back —
     which is exactly what happened twice: once when components/classic.css
     appended the extruded edge from a scope-prefixed rule, and again when
     button.css and tabs.css restated the pressed edge on :hover. Both put a
     full-strength rgba(255,120,30) glow around a latched control in the one
     mode that exists to contain no off-palette colour at all.

     `0 0 0 transparent` rather than `none`, because these are consumed inside a
     comma-separated list — `var(--ac-glow-box), var(--ac-edge)` resolving to
     `none, …` is not a valid shadow list and invalidates the whole declaration.
     A transparent shadow paints nothing and stays valid.

     The selector list is every element that REDECLARES one of these: .ac-bloom
     amplifies the glow, and the corner scopes set the edge. Adding a new scope
     means adding it here. */
  :root,
  .ac-bloom,
  .ac-classic,
  .ac-rounded,
  [data-ac-style-classic] {
    --ac-glow-box: 0 0 0 transparent;
    --ac-glow-box-dim: 0 0 0 transparent;
    --ac-edge: 0 0 0 transparent;
    --ac-edge-press: 0 0 0 transparent;
  }

  .ac-bloom::before,
  .ac-crt::after,
  .ac-scanlines::after,
  .ac-retrace,
  .ac-persist,
  .ac-mesh,
  /* The radar wake is a gradient in --ac-halo-N, which forced colors has no way to
     map and no reason to keep. It is also the last blended element left in this
     mode, and one `mix-blend-mode` anywhere promotes the stacking context and
     costs subpixel text antialiasing page-wide — see the matching note in
     print.css. The face and its rings stay; they are drawn with borders. */
  .ac-sweep__beam {
    display: none;
  }

  /* A decaying ghost is a low-contrast duplicate of live text, which is the one
     thing high-contrast mode exists to prevent. Hiding goes back to instant. */
  .ac-afterglow [hidden],
  .ac-afterglow dialog:not([open]) {
    transition: none;
    filter: none;
  }

  /* Same argument for the de-energizing afterimage. The UA forces shadows off
     for anything still at forced-color-adjust: auto, but this halo is built from
     --ac-halo-N rather than from a forced system color, and the elements it sits on
     are exactly the ones this file opts OUT of forced colors further down —
     which hands them their box-shadow back.

     Through the token, so the five selectors are not restated here — and being
     an inherited value rather than a competing rule, there is no longer a weight
     to match. The scroll smear rides along for the same reason, in case the page
     is captured mid-scroll.

     BLINK IS THE ONE THAT COMES BACK rather than going away. It carries the
     alarm, so it survives forced colors — but it returns to a hard steps(1)
     edge, because every frame the decay adds is a partly-transparent one, and a
     user who asked for maximum contrast did not ask for an alarm that spends a
     third of its cycle sliding through it. */
  :root {
    --ac-afterimage-display: none;
    --ac-smear-filter: none;
    --ac-blink-force: ac-blink;
    --ac-blink-force-ease: steps(1);
  }

  /* A trailing bar is a low-contrast duplicate of the live one, which is what high-contrast mode exists to prevent. */
  .ac-afterglow .ac-meter__track::before {
    display: none;
  }

  /* A page dissolving into the next one is a low-contrast duplicate of the whole
     screen, for the same reason the ghosts are. */
  html:has(.ac-afterglow)::view-transition-old(root),
  html:has(.ac-afterglow)::view-transition-new(root) {
    animation: none;
  }

  /* Blanket, for the same reason as the reduced-motion block in
     sim/frame.css: every decay is a descendant rule under .ac-afterglow and
     outranks a plain reset, so catching them by name means missing the next one. */
  .ac-afterglow *,
  .ac-afterglow *::before,
  .ac-afterglow *::after {
    transition: none !important;
  }

  /* NOT redundant with the --ac-blink-force above. That one is what outranks the
     CRT simulation's own blink curve; without this rule restating it for the
     flag, the block above would restart a blink the user has switched off. */
  [data-ac-style-blink="off"] {
    --ac-blink-force: none;
  }

  .ac-btn,
  .ac-tab,
  .ac-panel,
  .ac-input,
  .ac-banner,
  .ac-dialog,
  .ac-toggle__track,
  .ac-meter__track,
  .ac-check input,
  .ac-radio input {
    border-color: CanvasText;
  }

  .ac-nav {
    border-bottom-color: CanvasText;
  }

  .ac-hr {
    border-top-color: CanvasText;
  }

  .ac-table td {
    border-bottom-color: CanvasText;
  }

  .ac-list__fill {
    border-bottom-color: CanvasText;
  }

  /* Inverse video — the machine's voice. Must survive. */
  .ac-btn--filled,
  .ac-btn[aria-pressed="true"],
  .ac-tab--active,
  .ac-tab[aria-selected="true"],
  .ac-nav__link[aria-current="page"],
  .ac-statusbar,
  .ac-panel__barTitle,
  .ac-dialog__title,
  .ac-readout__label,
  .ac-badge--filled,
  /* The loudest element the system has. README says the inverse fill and the
     ✳✳ glyphs are what carry the alarm once blink is switched off — so this is
     the last thing that may flatten into an outlined box here. */
  .ac-banner--filled,
  .ac-table th,
  .ac-table__row--active td {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
    box-shadow: none;
    text-shadow: none;
  }

  /* Rejected entry. Invalid is inverse video PLUS blink, and the blink is
     restated above — without the fill, an invalid field is a normal field that
     flickers, which reads as a rendering fault rather than a rejection. */
  .ac-field--invalid .ac-input,
  .ac-input[aria-invalid="true"] {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
    caret-color: HighlightText;
    box-shadow: none;
    text-shadow: none;
  }
  .ac-field--invalid .ac-input::placeholder,
  .ac-input[aria-invalid="true"]::placeholder {
    color: HighlightText;
  }

  .ac-statusbar--line,
  .ac-readout__label--plain {
    forced-color-adjust: auto;
    background: Canvas;
    color: CanvasText;
  }

  /* Lit indicator lamps and meter fill. The --input toggle is the CSS-only
     variant and carries no aria-pressed at all, so it has to be matched through
     :checked or an ON switch renders with the GrayText thumb below — i.e. it
     reads as OFF. (The mandatory ON/OFF text is why that was survivable rather
     than fatal, which is the rule earning its keep.) */
  .ac-check input:checked,
  .ac-toggle--on .ac-toggle__thumb,
  .ac-toggle[aria-pressed="true"] .ac-toggle__thumb,
  .ac-toggle--input input:checked ~ .ac-toggle__track .ac-toggle__thumb,
  .ac-meter__bar {
    forced-color-adjust: none;
    background: Highlight;
    box-shadow: none;
  }

  /* OVER-RANGE, WITH THE SAME INVERSION THE PALETTE DOES. components/meter.css
     lights the track and cuts the bar out of it, because the blink that used to
     be the only alarm signal is switched off in this mode. Restated here or the
     rule above would paint the bar Highlight on a Canvas track and the alarm
     would read as an ordinary meter — which is the exact failure the fill was
     added to prevent. */
  .ac-meter--alarm .ac-meter__track {
    forced-color-adjust: none;
    background: Highlight;
    box-shadow: none;
  }
  .ac-meter--alarm .ac-meter__bar {
    background: HighlightText;
  }

  /* A meter driven below full is not the same reading as one at full drive, and
     Highlight has no way to say "less". The rule above matched every bar
     unconditionally, so a .ac-meter--dim bar — which is deliberately drawn at
     the faint tier with its halo off — came back at full strength here and
     claimed a reading it does not have. GrayText is this mode's own word for
     present-but-not-active, and it is what the disabled thumb already uses.
     Print needs no equivalent: base/print.css re-points --ac-ink-faint to a mid
     grey, so a dim bar already prints grey against a black one. */
  .ac-meter--dim .ac-meter__bar {
    background: GrayText;
  }

  /* The radio keeps its ring: the gap is punched with an inset shadow, so this
     one cannot take `box-shadow: none` with the rest — the lamp would fill in
     solid and stop reading as a radio. Restate the gap in Canvas instead. */
  .ac-radio input:checked {
    forced-color-adjust: none;
    background: Highlight;
    box-shadow: inset 0 0 0 3px Canvas;
  }

  .ac-toggle__thumb {
    forced-color-adjust: none;
    background: GrayText;
    box-shadow: none;
  }

  :focus-visible {
    outline-color: CanvasText;
  }
}

/* >>> src/amber-console.css -> base/print.css */
/* ===========================================================================
   Print

   A console screenshot has to print legibly on a booth wall, so the whole
   palette inverts to black on white by re-pointing the semantic tokens. Every
   component keys off those tokens, so nothing else needs restating.

   The 2px rules stay — they are the layout. Glow, bloom, scanlines and every
   animation go, because they describe energized hardware and paper is not.

   Literal hex is allowed here, in tokens/colors.css, and in the two sim/ files
   that use #000 inside a mask-image — where the value is alpha rather than a
   color. scripts/check-prohibitions.mjs enforces that list, and splitting
   effects.css let it get shorter: the glow tokens no longer contain a hex.
   =========================================================================== */

@media print {
  /* THE SELECTOR LIST IS THE FIX, AND IT HAS TO STAY THIS SHAPE.
     `:root` alone is specificity (0,1,0). The palettes in tokens/colors.css are
     `[data-ac-tech="crt"][data-ac-emitter="p3"]` — (0,2,0) — and a media query
     adds NO specificity, so a plain `:root` here loses to every palette for the
     four tokens they declare: --ac-screen, --ac-screen-raised, --ac-screen-well
     and --ac-on-fill.
     It lost silently and it lost HALF the palette, which is the dangerous shape.
     --ac-ink, --ac-fill and --ac-stroke are declared at :root in colors.css, so
     those inverted correctly and the inverse-video fills went solid black — over
     text that stayed --ac-on-fill #1e0c00. Black on near-black, 1.05:1, on every
     status strip, panel title, filled key and input on paper. The setup board
     kept --ac-screen too and printed as a full page of solid ink.
     Matching (0,2,0) and relying on this file being imported LAST is the same
     contract the rest of the file already runs on — see the header. Anything
     that beats the palettes works; nothing weaker does.

     .ac-root IS THE FOURTH ENTRY AND IT IS NOT OPTIONAL. tokens/colors.css
     declares the semantic aliases on `:root, .ac-root` so that a scoped embed
     re-resolves them against its own palette. That rule sits on a CLOSER element
     than this one, and proximity beats a same-weight declaration on an ancestor
     — so without .ac-root here, `--ac-ink: var(--ac-emit-90)` on the wrapper
     would win over `--ac-ink: #000` on :root and a scoped embed would print the
     whole panel in amber on white. The two lists have to move together. */
  :root,
  .ac-root,
  [data-ac-tech][data-ac-emitter],
  [data-ac-gas] {
    --ac-screen: #ffffff;
    --ac-screen-raised: #f2f2f2;
    --ac-screen-well: #ffffff;

    --ac-ink: #000000;
    --ac-ink-bright: #000000;
    --ac-ink-dim: #333333;
    --ac-ink-faint: #666666;
    --ac-ink-trace: #999999;

    --ac-fill: #000000;
    --ac-fill-bright: #000000;
    --ac-on-fill: #ffffff;

    --ac-stroke: #000000;
    --ac-stroke-dim: #666666;

    --ac-glow-text: none;
    /* Transparent rather than `none`, and the difference is load-bearing now
       that components compose the halo with the key edge:
       `var(--ac-glow-box), var(--ac-edge)` resolving to `none, …` is not a valid
       shadow list, so the whole declaration is dropped. That happened to leave
       `none` behind and print correctly — by way of an invalid declaration
       rather than on purpose. Same value the forced-colors block in a11y.css
       uses, for the same reason. */
    --ac-glow-box: 0 0 0 transparent;
    --ac-glow-box-dim: 0 0 0 transparent;
    --ac-edge: 0 0 0 transparent;
    --ac-edge-press: 0 0 0 transparent;

    /* Every blink site at once — see the blink header in base/blink.css.
       -force rather than -name so the CRT simulation, which declares -name on a
       nearer ancestor, cannot reinstate a blink on paper. */
    --ac-blink-force: none;

    /* The de-energizing afterimage is a halo around a control that is no longer
       lit — an argument about elapsed time, which paper does not have — and it
       prints as an unexplained smudge. The scroll smear is the same claim about
       a page that is not moving, in case one is captured mid-scroll. Both go
       through their own tokens, so neither of the two selector lists that used
       to be mirrored into this file is restated here at all. */
    --ac-afterimage-display: none;
    --ac-smear-filter: none;
  }

  /* .ac-bloom redefines the glow tokens on the frame itself, which is a closer
     ancestor than :root and so wins for every descendant. Reset it here or the
     whole page prints with an amber halo around the text. */
  .ac-bloom {
    --ac-glow-text: none;
    --ac-glow-box: 0 0 0 transparent;
    --ac-glow-box-dim: 0 0 0 transparent;
  }

  body {
    background: #ffffff;
    color: #000000;
    -webkit-font-smoothing: auto;
  }

  .ac-bloom::before,
  .ac-crt::after,
  .ac-scanlines::after,
  .ac-retrace,
  .ac-persist,
  .ac-mesh,
  /* The radar wake is a rotating light, and paper does not rotate — frozen, it
     prints as an unexplained bright wedge across the face. Hiding it also keeps
     the LAST blended element off the printed page, which matters more than it
     looks: a single `mix-blend-mode` anywhere in the document promotes the
     containing stacking context and costs SUBPIXEL TEXT ANTIALIASING for the
     whole page. Every other overlay here was already hidden in print, so this
     one arriving quietly turned every printed glyph grayscale. */
  .ac-sweep__beam {
    display: none;
  }


  /* A trailing bar is an argument about elapsed time, which paper does not have. */
  .ac-afterglow .ac-meter__track::before {
    display: none;
  }

  /* Paper holds an image indefinitely, which is the opposite problem. A hidden
     element must print as absent, not as a 40%-opacity smudge over the live one. */
  .ac-afterglow [hidden],
  .ac-afterglow dialog:not([open]) {
    transition: none;
    filter: none;
  }

  /* Blanket, for the same reason as the reduced-motion block in
     sim/frame.css: every decay is a descendant rule under .ac-afterglow and
     outranks a plain reset, so catching them by name means missing the next one. */
  .ac-afterglow *,
  .ac-afterglow *::before,
  .ac-afterglow *::after {
    transition: none !important;
  }


  /* The spinner is not a blink site — it runs its own keyframe, so it is still
     named here. Blink itself is switched off via --ac-blink-force above. */
  .ac-spinner::before {
    animation: none;
    filter: none;
  }

  /* EVERY FILLED SURFACE NEEDS THIS, and the reason is that browsers print with
     "background graphics" OFF by default: a background-color without
     print-color-adjust: exact is simply dropped. So this list is not a
     nice-to-have, it is the difference between a page that carries its state to
     paper and one that does not — a set interlock bit prints as an EMPTY
     square, a thrown switch as an empty box, a meter as an empty track.
     Anything whose meaning is "this cell is lit" belongs here.

     The visual suite cannot catch a gap in this list. Playwright's
     emulateMedia({ media: "print" }) renders to a screen surface, which honors
     backgrounds regardless — so console-1280-print.png shows filled interlocks
     that a real printer would not. Check it by hand against a print preview. */

  /* Inverse video — a blinking alarm is invisible on paper, so the ✳✳ glyphs
     and the fill are the only things left carrying it. */
  .ac-statusbar,
  .ac-panel__barTitle,
  .ac-dialog__title,
  .ac-readout__label,
  .ac-table th,
  .ac-table__row--active td,
  .ac-btn--filled,
  .ac-btn[aria-pressed="true"],
  .ac-tab--active,
  .ac-tab[aria-selected="true"],
  .ac-nav__link[aria-current="page"],
  .ac-badge--filled,
  .ac-banner--filled,
  .ac-field--invalid .ac-input,
  .ac-input[aria-invalid="true"],
  /* Lit lamps and fills — the bit field is the densest state on the panel.
     The thumb is listed unconditionally, not just when thrown: an OFF thumb
     that prints as nothing leaves an empty track with no readable position. */
  .ac-check input:checked,
  .ac-radio input:checked,
  .ac-toggle__thumb,
  .ac-meter__bar,
  /* The over-range track, which is inverse video like everything above it and
     is the ONLY thing left saying "alarm" on paper — the blink that used to
     carry it cannot print. Without this the track drops its background and an
     alarmed meter prints as an ordinary one. See components/meter.css. */
  .ac-meter--alarm .ac-meter__track {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }

  .ac-screen {
    min-height: 0;
    overflow: visible;
  }

  .ac-nav--sticky {
    position: static;
  }
}


