/* ═══════════════════════════════════════════════════════════════════════════
   geds-theme.css — the two themes for geds.neuralciberguard.org, plus the
   page components the hub introduces that no shared stylesheet owns.
   ---------------------------------------------------------------------------
   THE THEME MECHANISM IS UNCHANGED AND MUST STAY UNCHANGED.
   One attribute on <html> — data-theme="dark" | "light" — written before first
   paint by js/geds-boot.js, persisted under the localStorage key `ncg:theme`
   that every property on this domain shares, and announced with the
   `ncg:themechange` event that js/geds-atmos.js listens for. Nothing below
   introduces a second switch, a second key or a second event. Adding a theme
   system beside an existing one is how a site ends up with two of them, and
   the reader gets whichever loads last.

   WHAT DID CHANGE (2026-08-09): the DARK PALETTE.
   The previous dark set was a verbatim copy of the games hub's warm charcoal
   (#1C1917 paper, #F5F1EA ink), and the file's own comment argued for keeping
   the two identical so the properties felt like one company. That argument is
   still right about MECHANISM and wrong about HUE for this property. The games
   hub is an editorial surface; this is a security control plane, and it is read
   at night by people watching something. The brief for it is a secure digital
   observatory: deep space, midnight blue, indigo, deep violet, restrained
   cyan — depth rather than warmth.

   REJECTED ALTERNATIVES, so this is not relitigated:
   · Keep the warm dark and add cool ACCENTS only. Tried first. A cyan edge on
     a brown page reads as a colour error, not a decision; the ground has to
     move or nothing does.
   · Fork a whole second stylesheet for "geds dark". That is the second theme
     system this file's header forbids.
   · Push the change into sites/shared/tokens.css so both properties move
     together. tokens.css is a synced copy governed by its own header; a
     property-specific hue decision does not belong in the shared contract, and
     changing it here is exactly the local override the layering was built for.

   WHAT IS SHARED AND STAYS SHARED: the storage key, the attribute, the event,
   the token NAMES, and the crimson accent family. A reader who picked dark on
   the games hub still lands on dark here; the room is just a different room.

   TOKEN LAYERING. Two independent namespaces are remapped below and both are
   required:
     --cream* / --ink* / --crimson* …  owned by css/neural.css, consumed by the
                                       page and by css/geds-dashboard.css.
     --ncg-dark-*                      owned by css/tokens.css, consumed by the
                                       shared components in css/components.css.
   Remapping only the first leaves every shared component rendering a warm-dark
   island on a midnight page. tokens.css keeps its structure — its §18 dark
   role block still aliases these literals — this file only re-points what the
   literals are.

   CONTRAST. Every foreground below was measured against all three dark grounds
   (#0A0F1E paper, #111829 card, #18213A raised) and the LOWEST of the three is
   the number quoted. Nothing here is under 4.5:1 at text sizes; decorative
   values say so explicitly.
   ═══════════════════════════════════════════════════════════════════════════ */


/* ═══════════════════════════════════════════════════════════════════════════
   §1 · LIGHT — the precision laboratory.
   Warm white, ivory, mineral neutrals, graphite. The neural.css light values
   are already that palette and are left exactly as they are; this block only
   ADDS the names the new components below need, plus the mineral accents that
   the atmosphere canvas reads.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --gem-quartz:   #E9C4CC;
  --gem-salt:     #F4A896;
  --gem-opal:     #FF8A3D;
  --gem-amethyst: #7B4B94;
  --gem-geode:    #C9A227;

  --atmos-web:  0.05;
  --atmos-star: 0;
  --atmos-gem:  0.09;
  /* --atmos-touch is how much alpha the lattice GAINS at the top of its five
     proximity tiers, added to --atmos-web rather than multiplying it, so the
     token reads directly as "how much brighter can the cursor make this" and
     zero is a true off switch rather than a dimmer. js/geds-atmos.js refuses to
     bind a pointer listener at all below 0.001.
     Light is the precision laboratory: 0.05 → 0.095 at the apex, roughly a
     doubling of a stroke that starts almost invisible, which is a response you
     notice only once you have moved the cursor. A larger value put a visible
     violet halo on a white page and read as a cursor toy.        DECORATIVE. */
  --atmos-touch: 0.045;

  /* Instrument colours. Used for classification, never for state on their own —
     every place they appear, a word appears with them.
       indigo  9.60:1 on cream · 8.33:1 on cream-3
       cyan    5.72:1 on cream · 4.96:1 on cream-3
       violet  8.05:1 on cream · 6.98:1 on cream-3                            */
  --gx-indigo: #2C3E7A;
  --gx-cyan:   #1F6C7A;
  --gx-violet: #5B3E86;

  /* Surfaces for the panels this page introduces. Light keeps a true white
     card on the cream page — the same separation the delivery tables use. */
  --gx-panel:      #FFFFFF;
  --gx-panel-2:    var(--cream-2);
  --gx-rule:       var(--cream-3);
  --gx-rule-firm:  rgba(28,25,23,.16);
  --gx-shadow:     0 1px 2px rgba(28,25,23,.05), 0 10px 30px rgba(28,25,23,.06);

  /* Status. Measured on cream and cream-2; see the §4 arithmetic in
     sites/shared/tokens.css for the warn value, which is the one that moved. */
  --gx-ok:     #1F7A3A;  --gx-ok-bg:     #E7F3EB;  --gx-ok-line:     rgba(31,122,58,.32);
  --gx-warn:   #8A4B00;  --gx-warn-bg:   #FBEFD9;  --gx-warn-line:   rgba(138,75,0,.32);
  --gx-danger: #B81D30;  --gx-danger-bg: #FCE8EB;  --gx-danger-line: rgba(184,29,48,.30);
  --gx-info:   #1E5285;  --gx-info-bg:   #E3ECF5;  --gx-info-line:   rgba(30,82,133,.28);

  color-scheme: light;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §2 · DARK — the observatory.
   ═══════════════════════════════════════════════════════════════════════════ */

:root[data-theme='dark'] {
  /* -- neural.css namespace ------------------------------------------------ */
  --cream:#0A0F1E;   --cream-2:#111829;  --cream-3:#222C44;
  --ink:#EAEFF9;     --ink-2:#B9C4DA;    --ink-3:#8B98B4;
  /*    16.56:1           10.88:1              6.59:1   — on paper            */
  --crimson:#E098A6; --crimson-d:#F0B2BE;
  /*    8.37:1                10.74:1                                         */
  --crimson-soft:rgba(224,152,166,.12);
  --crimson-line:rgba(224,152,166,.30);
  --charcoal:#06090F; --charcoal-2:#0E1424;
  --shadow: 0 1px 2px rgba(0,0,0,.55), 0 14px 40px rgba(0,0,0,.5);

  /* -- tokens.css dark literals ------------------------------------------- */
  /* Re-pointed, not renamed: components.css reads the ROLE aliases in
     tokens.css §18, and those aliases resolve through these. */
  --ncg-dark-paper:#0A0F1E;   --ncg-dark-paper-2:#06090F;
  --ncg-dark-card:#111829;    --ncg-dark-card-2:#18213A;
  --ncg-dark-cream:#1E2740;
  --ncg-dark-ink:#EAEFF9;     /* 15.35:1 on card AAA                          */
  --ncg-dark-ink-2:#B9C4DA;   /* 10.09:1 AAA                                  */
  --ncg-dark-ink-3:#8B98B4;   /*  6.11:1 AA — safe as a UI border too         */
  --ncg-dark-ink-4:#5E6A84;   /*  3.26:1 — DECORATIVE ONLY, never text        */
  --ncg-dark-line:#202A42;    --ncg-dark-line-2:#2C3855;
  --ncg-dark-crimson:#E098A6;      /* 7.76:1 on card AAA                      */
  --ncg-dark-crimson-2:#E9A9B5;
  --ncg-dark-crimson-ink:#F0B2BE;  /* 9.96:1 AAA                              */
  --ncg-dark-gold:#F0B860;    --ncg-dark-gold-t:#F0B860;   /* 9.89:1 on card  */
  --ncg-dark-green:#5FBF88;   --ncg-dark-green-2:#6FCF97;
  --ncg-dark-green-t:#6FCF97; /* 9.31:1 on card AAA                           */
  --ncg-dark-copper:#E8A05C;  --ncg-dark-copper-2:#F0B27A;
  --ncg-dark-copper-t:#E8A05C;

  /* -- mineral + instrument ------------------------------------------------ */
  --gem-quartz:   #C9D4F0;
  --gem-salt:     #8FA6E8;
  --gem-opal:     #7FD3E8;
  --gem-amethyst: #B39BE0;
  --gem-geode:    #F0B860;

  --gx-indigo: #8FA6E8;  /* 9.26:1 on paper  */
  --gx-cyan:   #7FD3E8;  /* 11.26:1          */
  --gx-violet: #B39BE0;  /*  7.89:1          */

  --gx-panel:     #111829;
  --gx-panel-2:   #18213A;
  --gx-rule:      #202A42;
  --gx-rule-firm: #2C3855;
  --gx-shadow:    0 1px 2px rgba(0,0,0,.5), 0 18px 46px rgba(0,0,0,.45);

  --gx-ok:     #6FCF97;  --gx-ok-bg:     #12241C;  --gx-ok-line:     rgba(111,207,151,.34);
  --gx-warn:   #F0B860;  --gx-warn-bg:   #241C10;  --gx-warn-line:   rgba(240,184,96,.34);
  --gx-danger: #F58C9A;  --gx-danger-bg: #2A121A;  --gx-danger-line: rgba(245,140,154,.34);
  --gx-info:   #8FB8E8;  --gx-info-bg:   #101E2E;  --gx-info-line:   rgba(143,184,232,.30);
  /* tint pairings measured: ok 8.53:1 · warn 9.39:1 · danger 7.58:1 · info 8.17:1 */

  --atmos-web:  0.08;
  --atmos-star: 0.85;
  --atmos-gem:  0.16;
  /* 0.08 → 0.18 at the apex, under the 0.22 ceiling geds-atmos.js clamps to.
     The lit tiers walk --gem-quartz toward --gem-amethyst, so this is violet
     silk at 0.18 over the §3 sky, not over the flat #0A0F1E the contrast note
     above was measured against. That composite was NOT measured and does not
     need to be: it is a 1px decorative stroke that carries no information, is
     never the sole signal for anything, and disappears entirely under
     prefers-reduced-motion (§13). Labelled DECORATIVE for the same reason
     --ncg-dark-ink-4 is. */
  --atmos-touch: 0.10;

  color-scheme: dark;
}

/* Follow the operating system until the reader states a preference.
   This block is not redundant with the one above: js/geds-boot.js sets
   data-theme before paint, but with scripting disabled entirely the attribute
   never appears, and without this the neural tokens and the tokens.css
   literals would disagree — a light card on a dark page. Mirrored, not
   abbreviated, because a partial mirror is the same bug written smaller. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --cream:#0A0F1E;   --cream-2:#111829;  --cream-3:#222C44;
    --ink:#EAEFF9;     --ink-2:#B9C4DA;    --ink-3:#8B98B4;
    --crimson:#E098A6; --crimson-d:#F0B2BE;
    --crimson-soft:rgba(224,152,166,.12);
    --crimson-line:rgba(224,152,166,.30);
    --charcoal:#06090F; --charcoal-2:#0E1424;
    --shadow: 0 1px 2px rgba(0,0,0,.55), 0 14px 40px rgba(0,0,0,.5);

    --ncg-dark-paper:#0A0F1E;   --ncg-dark-paper-2:#06090F;
    --ncg-dark-card:#111829;    --ncg-dark-card-2:#18213A;
    --ncg-dark-cream:#1E2740;
    --ncg-dark-ink:#EAEFF9;     --ncg-dark-ink-2:#B9C4DA;
    --ncg-dark-ink-3:#8B98B4;   --ncg-dark-ink-4:#5E6A84;
    --ncg-dark-line:#202A42;    --ncg-dark-line-2:#2C3855;
    --ncg-dark-crimson:#E098A6; --ncg-dark-crimson-2:#E9A9B5;
    --ncg-dark-crimson-ink:#F0B2BE;
    --ncg-dark-gold:#F0B860;    --ncg-dark-gold-t:#F0B860;
    --ncg-dark-green:#5FBF88;   --ncg-dark-green-2:#6FCF97;
    --ncg-dark-green-t:#6FCF97;
    --ncg-dark-copper:#E8A05C;  --ncg-dark-copper-2:#F0B27A;
    --ncg-dark-copper-t:#E8A05C;

    --gem-quartz:#C9D4F0; --gem-salt:#8FA6E8;
    --gem-opal:#7FD3E8;   --gem-amethyst:#B39BE0; --gem-geode:#F0B860;
    --gx-indigo:#8FA6E8;  --gx-cyan:#7FD3E8;      --gx-violet:#B39BE0;
    --gx-panel:#111829;   --gx-panel-2:#18213A;
    --gx-rule:#202A42;    --gx-rule-firm:#2C3855;
    --gx-shadow:0 1px 2px rgba(0,0,0,.5), 0 18px 46px rgba(0,0,0,.45);
    --gx-ok:#6FCF97;     --gx-ok-bg:#12241C;     --gx-ok-line:rgba(111,207,151,.34);
    --gx-warn:#F0B860;   --gx-warn-bg:#241C10;   --gx-warn-line:rgba(240,184,96,.34);
    --gx-danger:#F58C9A; --gx-danger-bg:#2A121A; --gx-danger-line:rgba(245,140,154,.34);
    --gx-info:#8FB8E8;   --gx-info-bg:#101E2E;   --gx-info-line:rgba(143,184,232,.30);
    --atmos-web:.08; --atmos-star:.85; --atmos-gem:.16; --atmos-touch:.10;
    color-scheme: dark;
  }
}


/* ═══════════════════════════════════════════════════════════════════════════
   §3 · THE SKY
   On <html>, not <body>: the atmosphere canvas sits at z-index -1, which
   paints behind body's background but in front of the root element's. Put the
   sky on body and the canvas is never seen.
   Three very wide, very low-opacity radials — violet high right, indigo low
   left, cyan along the bottom. They are the depth cue; the stars are drawn by
   the canvas, not by this.
   ═══════════════════════════════════════════════════════════════════════════ */

:root[data-theme='dark'],
:root:not([data-theme='light']) { background-attachment: fixed; }

:root[data-theme='dark'] {
  background:
    radial-gradient(1200px 700px at 86% -8%,  rgba(179,155,224,.16), transparent 62%),
    radial-gradient(1000px 620px at 6%  10%,  rgba(143,166,232,.10), transparent 60%),
    radial-gradient(1400px 820px at 50% 108%, rgba(127,211,232,.08), transparent 64%),
    var(--cream);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    background:
      radial-gradient(1200px 700px at 86% -8%,  rgba(179,155,224,.16), transparent 62%),
      radial-gradient(1000px 620px at 6%  10%,  rgba(143,166,232,.10), transparent 60%),
      radial-gradient(1400px 820px at 50% 108%, rgba(127,211,232,.08), transparent 64%),
      var(--cream);
  }
}

/* neural.css paints a 22px dot grid on <body> at rgba(28,25,23,.05) — an ink
   dot, invisible on a midnight ground. Dark gets the same texture inverted, at
   a lower alpha because a light dot on a dark field reads roughly twice as
   loud as its arithmetic suggests. */
:root[data-theme='dark'] body {
  background-color: transparent;
  background-image: radial-gradient(rgba(201,212,240,.045) 1px, transparent 1px);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) body {
    background-color: transparent;
    background-image: radial-gradient(rgba(201,212,240,.045) 1px, transparent 1px);
  }
}

#atmos {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §4 · THEME TOGGLE
   ═══════════════════════════════════════════════════════════════════════════ */

.theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: .55ch;
  padding: .5em .95em;
  min-height: 44px;
  border-radius: 999px;
  border: 1px solid var(--crimson-line);
  background: var(--crimson-soft);
  color: var(--crimson);
  font: 500 11px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: .13em;
  text-transform: uppercase;
  cursor: pointer;
}
.theme-toggle:hover { background: color-mix(in srgb, var(--crimson) 14%, transparent); }
.theme-toggle:focus-visible { outline: 2px solid var(--crimson); outline-offset: 3px; }
.theme-toggle .t-dark { display: none; }
:root[data-theme='dark'] .theme-toggle .t-dark { display: inline; }
:root[data-theme='dark'] .theme-toggle .t-light { display: none; }


/* ═══════════════════════════════════════════════════════════════════════════
   §5 · SECTION RULE
   One gradient, used sparingly, is the whole mineral gesture in the page
   chrome. This is a product site and the restraint is the point.
   ═══════════════════════════════════════════════════════════════════════════ */

.sec + .sec::before,
hr {
  content: '';
  display: block;
  height: 1px;
  border: 0;
  background: linear-gradient(90deg, transparent,
    color-mix(in srgb, var(--gx-cyan) 26%, transparent),
    color-mix(in srgb, var(--gx-violet) 26%, transparent), transparent);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §6 · CODE BLOCKS IN THE DARK THEME
   neural.css draws .cmd on --charcoal, which in light is near-black on cream —
   a deliberate dark island. In the observatory palette --charcoal (#06090F) is
   four points off the page itself, so the island disappears. Dark therefore
   gets an explicit edge and a lifted ink instead of a lifted ground: a code
   block should read as recessed, and inverting that on one theme only would
   be two different components wearing one class.
   ═══════════════════════════════════════════════════════════════════════════ */

:root[data-theme='dark'] .cmd,
:root[data-theme='dark'] pre.cmd {
  border-color: var(--gx-rule-firm);
  color: #D3DCEE;               /* 14.46:1 on #06090F AAA */
  box-shadow: inset 0 1px 0 rgba(201,212,240,.05);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §7 · SPECIMEN PANEL — .spec
   The page's one repeating information object: a labelled row of facts.
   Used for the four Guardian capability summaries and for the delivery notes.
   A definition list, because that is what it is; the mono key column is the
   only decoration, and it is a real <dt> so it survives with styles off.
   Deliberately NOT a card grid: the brief asks for fewer cards, not more.
   ═══════════════════════════════════════════════════════════════════════════ */

.spec {
  margin: 18px 0 0;
  border-top: 1px solid var(--gx-rule);
}
.spec__row {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: 4px 24px;
  padding: 14px 0;
  border-bottom: 1px solid var(--gx-rule);
}
@media (max-width: 640px) {
  .spec__row { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}
.spec__k {
  margin: 0;
  font: 500 11px/1.5 var(--mono, ui-monospace, monospace);
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.spec__v {
  margin: 0;
  font-size: 15px;
  line-height: 1.65;
  color: var(--ink-2);
}
.spec__v strong { color: var(--ink); font-weight: 600; }
.spec__v + .spec__v { margin-top: 6px; }
.spec__v code { word-break: break-word; }

/* A severity or status word travelling with a row. Never the only signal — the
   word inside it is the signal; the tint is the second channel. */
.sev {
  display: inline-block;
  font: 600 10px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid currentColor;
  white-space: nowrap;
  vertical-align: 1px;
}
.sev--critical { color: var(--gx-danger); background: var(--gx-danger-bg); }
.sev--high     { color: var(--gx-danger); background: transparent; }
.sev--medium   { color: var(--gx-warn);   background: transparent; }
.sev--low      { color: var(--gx-info);   background: transparent; }
.sev--ok       { color: var(--gx-ok);     background: var(--gx-ok-bg); }


/* ═══════════════════════════════════════════════════════════════════════════
   §8 · GUARDIAN SURFACE ENTRY — .surface
   Four of these, identical in structure, in sequence. The consistency IS the
   design: a reader who has parsed the first one can find the same fact in the
   same place in the other three.
   ═══════════════════════════════════════════════════════════════════════════ */

.surface {
  padding: 44px 0;
  border-top: 1px solid var(--gx-rule);
}
.surface:first-of-type { border-top: 0; }

.surface__index {
  margin: 0 0 10px;
  font: 500 11px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: .2em;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.surface__name {
  font-family: var(--serif);
  font-size: clamp(23px, 3vw, 31px);
  font-weight: 600;
  letter-spacing: -.01em;
  line-height: 1.1;
  color: var(--ink);
  margin: 0;
}
.surface__where {
  margin: 8px 0 0;
  font: 500 12px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .06em;
  color: var(--gx-cyan);
}
.surface__purpose {
  margin: 14px 0 0;
  max-width: 60ch;
  font-size: clamp(16px, 2vw, 18.5px);
  line-height: 1.6;
  color: var(--ink-2);
}
.surface__purpose strong { color: var(--ink); font-weight: 600; }

.surface__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 34px;
  align-items: start;
  margin-top: 26px;
}
@media (max-width: 900px) {
  .surface__grid { grid-template-columns: minmax(0, 1fr); gap: 24px; }
}

/* The visual. A specimen, not an illustration: real values from the shipped
   implementation, set as an instrument reading. */
.surface__visual {
  margin: 0;
  border: 1px solid var(--gx-rule);
  border-radius: 14px;
  background: var(--gx-panel);
  box-shadow: var(--gx-shadow);
  overflow: hidden;
}
.surface__visual-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--gx-rule);
  background: var(--gx-panel-2);
  font: 500 10.5px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.surface__visual-body { padding: 6px 16px 14px; }
.surface__visual figcaption,
.surface__visual .surface__visual-note {
  display: block;
  padding: 12px 16px;
  border-top: 1px solid var(--gx-rule);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ink-2);
}

/* Rows inside a specimen. One line = one fact. */
.readout { list-style: none; margin: 0; padding: 0; }
.readout li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 6px 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--gx-rule);
}
.readout li:last-child { border-bottom: 0; }
.readout b {
  font: 500 12px/1.4 var(--mono, ui-monospace, monospace);
  letter-spacing: .04em;
  color: var(--ink);
}
/* `:not(.sev)` is load-bearing. Written as a bare `.readout span` this rule
   also caught the severity pill, which then took `grid-column: 1 / -1` and
   rendered as a full-width bordered band on its own row instead of a chip
   beside the rule name. The description is the only unclassed span in a row. */
.readout li > span:not(.sev) {
  grid-column: 1 / -1;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-2);
}
.readout .sev { justify-self: end; }

.surface__foot {
  margin-top: 26px;
  padding: 18px 20px;
  border-left: 2px solid var(--gx-cyan);
  background: var(--gx-panel-2);
  border-radius: 0 12px 12px 0;
}
.surface__foot p { margin: 0; font-size: 15px; line-height: 1.65; color: var(--ink-2); }
.surface__foot p + p { margin-top: 10px; }
.surface__foot strong { color: var(--ink); font-weight: 600; }
.surface__foot .btn { margin-top: 14px; }


/* ═══════════════════════════════════════════════════════════════════════════
   §9 · THE DEMONSTRATION FRAME — .demo-frame
   The frame exists to do one job the demo itself cannot: name the address the
   thing lives at, and state that what is inside is a demonstration. Both
   labels are markup, so they survive in reader mode, in print and in a
   screenshot of the frame alone. Never soften or remove either.
   ═══════════════════════════════════════════════════════════════════════════ */

.demo-frame {
  margin: 26px 0 0;
  border: 1px solid var(--gx-rule);
  border-radius: 16px;
  background: var(--gx-panel);
  box-shadow: var(--gx-shadow);
  overflow: hidden;
}
.demo-frame__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 16px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--gx-rule);
  background: var(--gx-panel-2);
}
.demo-frame__host {
  font: 500 13px/1.4 var(--mono, ui-monospace, monospace);
  letter-spacing: .02em;
  color: var(--ink);
  word-break: break-word;
}
.demo-frame__tag {
  margin-left: auto;
  font: 500 10px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gx-warn);
  border: 1px solid var(--gx-warn-line);
  background: var(--gx-warn-bg);
  border-radius: 999px;
  padding: 3px 10px;
  white-space: nowrap;
}
.demo-frame__body { padding: 20px; }
@media (max-width: 600px) { .demo-frame__body { padding: 14px; } }
.demo-frame__body > * + * { margin-top: 22px; }
.demo-frame__foot {
  padding: 14px 20px;
  border-top: 1px solid var(--gx-rule);
  background: var(--gx-panel-2);
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-2);
}
.demo-frame__foot p { margin: 0; color: inherit; }
.demo-frame__foot p + p { margin-top: 8px; }

/* Caption above each mounted demo, so a reader landing mid-page knows which
   surface they are looking at without waiting for the animation. */
.demo-cap {
  margin: 0 0 10px;
  font: 500 10.5px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-2);
}


/* ═══════════════════════════════════════════════════════════════════════════
   §10 · ARCHITECTURE CONSTELLATION — .arch
   Five nodes and the wires between them.

   WHY THE NODES ARE HTML BUTTONS AND THE WIRES ARE AN SVG.
   The obvious build is one SVG containing shapes and <text>. It was rejected:
   a fixed viewBox cannot reflow a label, so on a 360px screen every caption
   renders at about 8px, and a diagram nobody can read is decoration. HTML
   nodes reflow, zoom, translate, inherit the type ramp and are focusable for
   free. The wires stay SVG because that is the one part with no text in it.

   The wire coordinates are written by js/geds-arch.js from the measured
   positions of the nodes — guessing percentages only works while the grid is
   exactly three equal columns, and it stops being true the moment a label
   wraps. With scripting off the SVG stays empty and the five nodes still read
   as a labelled group; nothing is lost but the lines.
   ═══════════════════════════════════════════════════════════════════════════ */

.arch { margin: 26px 0 0; }

.arch__stage { position: relative; }

.arch__wires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.arch__wire {
  stroke: var(--gx-rule-firm);
  stroke-width: 1;
  fill: none;
}
/* An active wire doubles in width as well as changing colour, so the state is
   not carried by hue alone. The wires are decorative in any case — they are
   aria-hidden, and the authoritative statement of which surfaces are involved
   in a step is the step's own prose plus the node edge below. */
.arch__wire.is-live {
  stroke: var(--gx-cyan);
  stroke-width: 2;
}

/* THE NODES MUST NOT FILL THEIR COLUMNS, AND THE ROWS MUST NOT TOUCH.
   Measured, twice, on a 1265px viewport. Full-width nodes in a 1fr column with
   an 18px gap produced 30px wires; capping the node at 19rem produced 56px.
   Both are stubs, because almost the whole diagonal runs INSIDE the two
   rectangles and only the clear space between them is drawn.

   The clear horizontal run works out to 1.5·column + gap − 1.5·node, so the
   node cap is the lever that matters and it has to move a long way: at a
   ~327px column, 15rem of node opens about 154px. The row gap is the other
   half — with rows sized to their content the vertical run is exactly the row
   gap, so 60px is what stops the wires reading as horizontal rules.           */
.arch__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 60px 24px;
  align-items: center;
}
@media (max-width: 760px) {
  .arch__grid { grid-template-columns: minmax(0, 1fr); }
  .arch__wires { display: none; }
}

.arch__node {
  display: block;
  width: 100%;
  max-width: 15rem;
  text-align: left;
  padding: 16px 18px;
  border: 1px solid var(--gx-rule);
  border-radius: 14px;
  background: var(--gx-panel);
  color: var(--ink);
  font-family: var(--sans);
  cursor: pointer;
  box-shadow: var(--gx-shadow);
  transition: border-color .2s ease, transform .2s ease;
}
.arch__node:hover { border-color: var(--gx-cyan); transform: translateY(-2px); }
.arch__node:focus-visible { outline: 3px solid var(--crimson); outline-offset: 3px; }
/* Selection is carried by the left rule and the word in .arch__node-state,
   never by the tint alone. */
.arch__node[aria-selected='true'] {
  border-color: var(--gx-cyan);
  box-shadow: inset 3px 0 0 var(--gx-cyan), var(--gx-shadow);
}
/* Involved-in-this-step. The inset ring thickens the visible edge without a
   border-width change, which would reflow the grid and move every wire endpoint
   the moment a step advanced — measurably, since the wires are drawn from
   layout. Thickness is the second channel beside the hue. */
.arch__node[data-lit='true'] {
  border-color: var(--gx-cyan);
  box-shadow: inset 0 0 0 1px var(--gx-cyan), var(--gx-shadow);
}

.arch__node-kind {
  display: block;
  font: 500 10px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.arch__node-name {
  display: block;
  margin-top: 4px;
  font-family: var(--serif);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.2;
  color: var(--ink);
}
.arch__node-role {
  display: block;
  margin-top: 4px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-2);
}

/* The hub is the only node with a lifted ground — it is the one the other four
   report to. */
.arch__node--hub {
  background: var(--gx-panel-2);
  border-color: var(--gx-rule-firm);
}
.arch__node--hub .arch__node-name { font-size: 19px; }

/* Explicit placement belongs INSIDE the breakpoint. Left unconditional, the
   hub's `grid-column: 2` survives into the single-column stack and the browser
   creates an implicit second column to satisfy it — the four spokes sit in
   column one and the hub alone sits in a phantom column two, at half width. */
@media (min-width: 761px) {
  .arch__grid > [data-node='desktop'] { grid-column: 1; grid-row: 1; justify-self: start; }
  .arch__grid > [data-node='mobile']  { grid-column: 3; grid-row: 1; justify-self: end; }
  .arch__grid > .arch__node--hub      { grid-column: 2; grid-row: 2; justify-self: center; }
  .arch__grid > [data-node='sentry']  { grid-column: 1; grid-row: 3; justify-self: start; }
  .arch__grid > [data-node='web']     { grid-column: 3; grid-row: 3; justify-self: end; }
}
@media (max-width: 760px) {
  /* Stacked, the cap would leave a 19rem column against a full-width page. */
  .arch__node { max-width: none; }
}

/* The reading panels. All five are in the document; the script hides four of
   them and only then are they a tab set. With scripting off every panel is
   visible and the page simply reads as five short paragraphs. */
.arch__panels { margin-top: 20px; }
.arch__panel {
  padding: 20px 22px;
  border: 1px solid var(--gx-rule);
  border-radius: 14px;
  background: var(--gx-panel);
}
.arch__panel + .arch__panel { margin-top: 12px; }
.arch__panel h4 {
  margin: 0 0 8px;
  font-family: var(--serif);
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.arch__panel p { margin: 0; font-size: 15px; line-height: 1.65; color: var(--ink-2); }
.arch__panel p + p { margin-top: 10px; }
.arch__panel:focus-visible { outline: 3px solid var(--crimson); outline-offset: 3px; }


/* ═══════════════════════════════════════════════════════════════════════════
   §11 · THE INCIDENT WALKTHROUGH — .story
   A real <ol>: the numbering is list semantics, not typed digits, so a screen
   reader announces "list, 4 items" and each position. The script adds previous
   and next controls and marks the current step; with scripting off it is a
   numbered list that reads top to bottom, which is the same information in the
   same order.
   ═══════════════════════════════════════════════════════════════════════════ */

.story {
  counter-reset: gx-step;
  list-style: none;
  margin: 24px 0 0;
  padding: 0;
  border-top: 1px solid var(--gx-rule);
}
.story__step {
  counter-increment: gx-step;
  position: relative;
  margin: 0;
  padding: 20px 0 20px 54px;
  border-bottom: 1px solid var(--gx-rule);
  color: var(--ink-2);
}
.story__step::before {
  content: counter(gx-step);
  position: absolute;
  left: 0;
  top: 20px;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1px solid var(--gx-rule-firm);
  border-radius: 50%;
  font: 600 12px/1 var(--mono, ui-monospace, monospace);
  font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
.story__step[aria-current='step']::before {
  border-color: var(--gx-cyan);
  color: var(--gx-cyan);
}
.story__title {
  margin: 0 0 6px;
  font-family: var(--serif);
  font-size: 18px;
  font-weight: 600;
  color: var(--ink);
}
.story__step p { margin: 0; font-size: 15px; line-height: 1.7; color: var(--ink-2); }
.story__step p + p { margin-top: 8px; }
.story__step strong { color: var(--ink); font-weight: 600; }

/* Injected by the script. Rendered only when it can actually do something —
   a control that needs JavaScript must not exist without it. */
.story__nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
}
.story__nav button {
  min-height: 44px;
  padding: 10px 18px;
  border-radius: 10px;
  border: 1.5px solid var(--ink);
  background: none;
  color: var(--ink);
  font: 600 14px/1 var(--sans);
  cursor: pointer;
}
.story__nav button:hover:not(:disabled) { border-color: var(--gx-cyan); color: var(--gx-cyan); }
.story__nav button:disabled { opacity: .45; cursor: default; }
.story__nav button:focus-visible { outline: 3px solid var(--crimson); outline-offset: 2px; }
.story__pos {
  font: 500 12px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: .1em;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}


/* ═══════════════════════════════════════════════════════════════════════════
   §12 · HONEST-LIMITS BLOCK — .limits
   Used wherever the page states what is NOT built. It is deliberately the same
   visual weight as the capability copy around it rather than a footnote: a
   limitation set in small grey type is a limitation you are hiding.
   ═══════════════════════════════════════════════════════════════════════════ */

.limits {
  margin: 24px 0 0;
  padding: 20px 22px;
  border: 1px solid var(--gx-warn-line);
  border-left-width: 3px;
  border-radius: 0 12px 12px 0;
  background: var(--gx-warn-bg);
}
.limits h3, .limits h4 {
  margin: 0 0 10px;
  font: 600 11px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gx-warn);
}
.limits ul { list-style: none; margin: 0; padding: 0; }
.limits li {
  padding: 8px 0 8px 18px;
  position: relative;
  font-size: 14.5px;
  line-height: 1.65;
  color: var(--ink-2);
}
.limits li::before {
  content: '—';
  position: absolute;
  left: 0;
  color: var(--gx-warn);
}
.limits li strong { color: var(--ink); font-weight: 600; }
.limits > p { margin: 0 0 10px; font-size: 14.5px; line-height: 1.65; color: var(--ink-2); }


/* ═══════════════════════════════════════════════════════════════════════════
   §13 · REDUCED MOTION
   The atmosphere reads --atmos-star and --atmos-touch and renders one still
   frame, so these declarations switch the starfield and the pointer response
   off without the script needing to know why.

   SPECIFICITY — THIS BLOCK WAS BROKEN IN DARK AND IS FIXED HERE.
   It used to read `:root { --atmos-star: 0 }`. That selector is (0,1,0). The
   two places that turn the stars ON are `:root[data-theme='dark']` (§2, line
   ~168) and `:root:not([data-theme='light'])` (§2's OS mirror), and BOTH are
   (0,2,0) — an attribute selector outranks a plain pseudo-class no matter how
   much later the losing rule appears in the file. So a reader in dark theme
   with "reduce motion" set kept the full 0.85 starfield, and this block's own
   comment claimed otherwise. Restating all three selectors at matching
   specificity, later in source order, is what actually wins.

   REJECTED: !important. It would work, and it would be the first !important in
   this stylesheet — a tool that hides specificity mistakes instead of fixing
   them, in the one file whose entire job is a token cascade.

   --atmos-touch: 0 is DELIBERATELY REDUNDANT with the check in
   js/geds-atmos.js, because the two fail independently: the script's check is
   what reacts when the reader changes the OS setting mid-session, and this one
   is what still holds if the module throws before it ever reads the
   preference. The value here is also what the still frame is drawn with, so
   the reduced-motion composition is the plain quartz lattice at tier 0 with no
   violet anywhere — a complete picture, not a degraded one.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  :root,
  :root[data-theme='dark'],
  :root:not([data-theme='light']) { --atmos-star: 0; --atmos-touch: 0; }
  .arch__node:hover { transform: none; }
  .arch__node, .story__nav button { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   §14 · PRINT
   The constellation and the demonstrations are screen objects. In print the
   surrounding prose already carries every fact they show, so they are dropped
   rather than rendered as empty boxes.
   ═══════════════════════════════════════════════════════════════════════════ */

@media print {
  #atmos, .theme-toggle, .arch__stage, .story__nav { display: none !important; }
  .arch__panel[hidden] { display: block !important; }
  .surface__visual, .demo-frame, .arch__panel { box-shadow: none; border: 1px solid #999; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   Guardian Sentry panel of the phone demonstration (.gsen__*)
   ---------------------------------------------------------------------------
   MOVED HERE 2026-08-09, and the move is a BUG FIX rather than tidying.

   These twelve classes were written into a separate architecture stylesheet because the
   agent adding them did not own css/geds-dashboard.css that pass. But
   that sheet was never in the <link> list of index.html — so
   every one of these rules was dead, and the Sentry panel of the phone demo
   shipped unstyled. Measured before the move: 12 .gsen__ classes referenced by
   js/geds-mobile.js, 12 defined in the unloaded sheet, 0 defined in any sheet
   the page actually loads.

   geds-theme.css is loaded, and already owns the .arch__* and .story__* rules
   this section's sibling component uses, so it is where these belong.
   ═══════════════════════════════════════════════════════════════════════════ */
/* ═══════════════════════════════════════════════════════════════════════════
   PHONE DEMO ADDITIONS — the .gsen__* classes used by js/geds-mobile.js.
   ---------------------------------------------------------------------------
   WHY THEY LIVE IN THIS FILE. The phone demo's existing .gphone__* rules are
   in css/geds-dashboard.css, which another agent is holding this pass. These
   are additive-only, under a prefix that appears nowhere else in the site, in
   the one stylesheet this pass is allowed to create — which the page loads
   anyway for the architecture figure. If the two files are ever reconciled,
   move this block to sit with the .gphone__* rules and delete this note.

   The demo now shows two detectors side by side, so the wording carries the
   distinction and the styling has to as well: the SMS strip is visually a
   different instrument from the Senior verdict below it, and reads as an
   aside rather than an answer.
   ═══════════════════════════════════════════════════════════════════════════ */

.gphone-wrap {
  /* Light defaults for the tokens the dark blocks above override. The phone
     demo needs the same measured severity scale as the map: the palette it
     inherits from css/geds-dashboard.css puts #B4653A at 4.02:1 on cream,
     which is under AA and this label is 9px. */
  --ga-on-accent:  #fff;
  --ga-sev-medium: #9A5327;
  --ga-sev-high:   #A8442F;
  --gs-line: var(--crimson-line, rgba(145,73,85,.22));
  --gs-panel: var(--cream, #FAF9F6);
  --gs-bg: var(--cream-2, #F3F1EA);
  --gs-ink: var(--ink, #1C1917);
  --gs-ink-2: var(--ink-2, #44403C);
  --gs-ink-3: var(--ink-3, #78716C);
  --gs-accent: var(--crimson, #914955);
}

/* ---- the deceptive link, shown as the two facts it is --------------------- */

/* Sits directly under .gphone__sms-body, which already carries a 10px bottom
   margin — this one takes the gap rather than adding a second one. */
.gsen__linkrow {
  margin: -4px 0 10px;
  display: grid;
  gap: 2px;
}
.gsen__linkshown {
  font: 500 12px/1.4 var(--mono, ui-monospace, monospace);
  color: var(--gs-accent);
  text-decoration: underline;
  overflow-wrap: anywhere;
}
.gsen__linkreal {
  font: 400 11px/1.4 var(--mono, ui-monospace, monospace);
  color: var(--v-scam, #A8442F);
  overflow-wrap: anywhere;
}

/* ---- the SMS watch strip ------------------------------------------------- */

.gsen__sms {
  margin: 0 0 12px;
  padding: 9px 11px;
  border: 1px dashed var(--gs-line);
  border-radius: 10px;
  background: var(--gs-bg);
}
.gsen__sms[data-applies='false'] { opacity: .8; }
.gsen__smshead {
  margin: 0 0 5px;
  font: 600 9.5px/1.4 var(--mono, ui-monospace, monospace);
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--gs-ink-3);
}
.gsen__smslist {
  list-style: none;
  margin: 0 0 5px;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 6px;
}
.gsen__smslist li {
  padding: .18em .55em;
  border-radius: 999px;
  border: 1px solid var(--gs-line);
  background: var(--gs-panel);
  font: 400 10.5px/1.5 var(--sans, Inter, system-ui, sans-serif);
  color: var(--gs-ink-2);
}
.gsen__smssum {
  margin: 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--gs-ink-2);
}
.gsen__sms[data-notify='true'] { border-style: solid; border-color: var(--v-scam, #A8442F); }
.gsen__sms[data-notify='true'] .gsen__smssum { color: var(--v-scam, #A8442F); font-weight: 600; }

/* ---- signal level, so "strong" is never carried by tone alone ------------- */

/* .gphone__signals li is a single-column grid, so this only needs to stop
   stretching; a grid-column of 2 would silently open an implicit second
   column and pull the label off the text it belongs to. */
.gsen__lvl {
  justify-self: start;
  margin-top: 3px;
  padding: .12em .5em;
  border-radius: 999px;
  font: 600 9px/1.6 var(--mono, ui-monospace, monospace);
  letter-spacing: .1em;
  text-transform: uppercase;
  font-style: normal;
  border: 1px solid var(--gs-line);
  color: var(--gs-ink-3);
  background: var(--gs-panel);
}
.gsen__lvl[data-level='possible'] { color: var(--ga-sev-medium); border-color: currentColor; }
.gsen__lvl[data-level='strong']   { color: var(--ga-sev-high); border-color: currentColor; }

/* ---- why this scenario is here ------------------------------------------- */

.gsen__why {
  margin: 10px 0 12px;
  padding: 9px 11px;
  border-left: 3px solid var(--gs-accent);
  border-radius: 0 8px 8px 0;
  background: var(--gs-bg);
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--gs-ink-2);
}

/* ---- the explainer ------------------------------------------------------- */

.gsen__explain {
  display: grid;
  gap: 12px;
}
@media (min-width: 900px) {
  .gsen__explain { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}
.gsen__block {
  min-width: 0;
  padding: 13px 15px;
  border: 1px solid var(--gs-line);
  border-radius: 12px;
  background: var(--gs-panel);
}
.gsen__block h4 {
  margin: 0 0 7px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--gs-ink);
}
.gsen__block p {
  margin: 0 0 8px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--gs-ink-2);
}
.gsen__block p:last-child { margin-bottom: 0; }
.gsen__list {
  margin: 0;
  padding: 0 0 0 1.1em;
  display: grid;
  gap: 6px;
}
.gsen__list li {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--gs-ink-2);
}

@media (forced-colors: active) {
  .gsen__sms, .gsen__block, .gsen__smslist li, .gsen__lvl { border: 1px solid CanvasText; }
}

/* Badge ink follows the badge fill. .gphone__tab-badge is declared #fff in
   css/geds-dashboard.css against light severity fills; once the dark theme
   lightens those fills (above) white stops working, so this flips with them.
   Two selectors rather than an edit to that file, for the ownership reason
   given at the head of the phone-demo block. */
:root[data-theme='dark'] .gphone__tab-badge { color: #1C1917; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .gphone__tab-badge { color: #1C1917; }
}
