@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:300;font-display:block;src:url(/fonts/chakra-petch-latin-300-normal.a02dd380.woff2) format('woff2');}
@font-face{font-family:'Chakra Petch';font-style:normal;font-weight:400;font-display:block;src:url(/fonts/chakra-petch-latin-400-normal.7d75be85.woff2) format('woff2');}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:300;font-display:block;src:url(/fonts/jetbrains-mono-latin-300-normal.722972af.woff2) format('woff2');}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:block;src:url(/fonts/jetbrains-mono-latin-400-normal.14425ba9.woff2) format('woff2');}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:block;src:url(/fonts/jetbrains-mono-latin-500-normal.cb182fee.woff2) format('woff2');}
/* ============================================================
   REMOTE NODE / v2 — authored prototype
   Zero extracted bitmaps. Every visible element is DOM, CSS, SVG or
   canvas. Coordinates live in a 1536 x 1024 design canvas scaled to
   the viewport as a whole (CSS zoom, so text re-lays out crisp).
   Layout grid: 32px page margin, panels on consistent gutters.
   ============================================================ */

:root {
  --mono: 'JetBrains Mono', ui-monospace, monospace;
  --disp: 'Chakra Petch', system-ui, sans-serif;
  --ink: #cfd7e6;
  --ink-b: #eef2fa;
  --dim: #8590a6;
  --faint: #5b6478;
  --pink: rgb(var(--aef4d8f));
  --pink-s: rgb(var(--aff8cba));
  /* SIGNALS: fixed hues that carry a distinction the accent is not carrying --
     the code block's syntax. Two profiles land on top of one of these and
     override it; see SIGNALS in theme-gen.mjs. */
  --sig-fn: 130,216,255;
  --sig-key: 199,146,234;
  --sig-num: 247,178,103;
  /* THE AGENT FAN: one hue per agent, five spokes 72deg apart anchored on the
     profile's mid hue, at ONE lightness and ONE chroma so no agent shouts.
     Spoke 0 is the accent hue, so the set still reads as this theme's. These
     are Neural Pink's; theme-build.mjs emits the set for every profile, and
     `.ag0`..`.ag4` below point `--ag` at the one the element belongs to. */
  --ag-1: 246,147,179;
  --ag-2: 231,168,85;
  --ag-3: 131,202,130;
  --ag-4: 58,201,232;
  --ag-5: 178,169,255;
  --ag: var(--ag-1);
  --line: rgba(168,186,224,.20);
  /* one number, quoted verbatim in the theme module's footer */
  --th-ms: 480ms;
}

* { box-sizing: border-box; }
html, body {
  margin: 0; height: 100%; overflow: hidden;
  font-family: var(--mono);
  -webkit-font-smoothing: antialiased;
}
/* letterbox margins carry the same sky palette, so the scene continues */
body {
  background:
    radial-gradient(60% 55% at 78% 22%, rgba(var(--a5a2656),.34) 0%, transparent 70%),
    radial-gradient(55% 60% at 20% 30%, rgba(28,44,96,.30) 0%, transparent 72%),
    linear-gradient(180deg, #04050c 0%, #070a16 55%, #04050c 100%);
}
body::after {
  content: ''; position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(110% 90% at 50% 50%, transparent 46%, rgba(3,4,10,.72) 100%);
}

/* ---- heading TAGS, and nothing else (PRODUCTION-SPEC.md 2.7) --------------
   The app had two orphaned `<h2>`s and no `<h1>`. Every element listed here was a
   `div` or a `span` and keeps its own position, size, tracking and colour: the
   only thing that changed is the tag under it, and this ruleset exists solely to
   cancel the UA stylesheet's `h1..h4` bold and margin.

   The margin half is the one that would move pixels rather than weight. These
   labels are `position: absolute` with a `top`, and an absolutely positioned box
   with `height: auto` is still displaced by its own `margin-top` -- `.ab-sec` at
   `top: 246px` with the UA's `.83em` would render 9px low, in a module whose
   every offset is measured off a render.

   It sits HERE, above every one of those rules, and it is written as bare class
   selectors rather than `h2.ab-sec` for the same reason: `.pj-ch` sets a
   `margin-top: 22px` it must keep, and only at EQUAL specificity does the later
   declaration win. Qualifying these with their tags would silently flatten that
   22px to zero. Verified with `dbg-moddiff.mjs`, all seven modules. */
.p-title, .ab-sec, .rs-h, .pj-h, .ct-h, .gh-h,
.st-kick, .st-h, .th-kick, .pj-ht, .pj-ch {
  margin: 0; font-weight: inherit;
}

/* the design canvas fills the whole window: scale fits the height, extra
   width flows into the gutters between panels (laid out in JS) */
#fit { position: fixed; inset: 0; z-index: 1; overflow: hidden; }
#canvas { position: absolute; left: 0; top: 0; transform-origin: 0 0; }

#bg { position: absolute; left: -44px; top: -44px; display: block; }
/* the frozen copy of the previous palette's frame, faded out over the same
   480ms the panels cross-fade in. Stacked between #bg and #fx by DOM order. */
#bgx { position: absolute; left: -44px; top: -44px; display: block; pointer-events: none;
       opacity: 1; transition: opacity var(--th-ms) ease; }
#bgx.out { opacity: 0; }
#fx { position: absolute; left: 0; top: 0; mix-blend-mode: screen; pointer-events: none; }

/* A profile switch changes custom properties, and a property that READS one
   transitions when it changes -- so one temporary rule cross-fades every accent
   in the interface at once. It is scoped to the switch (the class is dropped
   after 520ms) so it never competes with the hover cross-fades, which are all
   much shorter and would otherwise run at half a second while it was on. */
html.theming #canvas *, html.theming #canvas *::before, html.theming #canvas *::after {
  transition: color var(--th-ms) ease, background-color var(--th-ms) ease,
              border-color var(--th-ms) ease, fill var(--th-ms) ease,
              stroke var(--th-ms) ease, box-shadow var(--th-ms) ease,
              text-shadow var(--th-ms) ease, filter var(--th-ms) ease,
              opacity var(--th-ms) ease;
}

.mk { position: absolute; fill: rgb(var(--aff4e93)); filter: drop-shadow(0 0 6px rgba(var(--aff4e93),.75)); }

/* ---------------- header / footer: text over shaped SVG rules ----------- */
#hdr, #ftr { position: absolute; left: 0; pointer-events: none; }
#hdr { top: 0; height: 92px; }
#ftr { height: 68px; }
#hdrShape, #ftrShape { position: absolute; left: 0; top: 0; overflow: visible; }

.hdr-w {
  position: absolute; left: 88px; top: 26px; font-family: var(--disp); font-weight: 400;
  font-size: 16.5px; letter-spacing: .428em; color: #f4f7fd; white-space: nowrap; line-height: 1;
  text-shadow: 0 0 18px rgba(200,218,255,.35);
}
.hdr-s { position: absolute; left: 88px; top: 47px; font-size: 9.2px; letter-spacing: .175em; color: #93a0b8; white-space: nowrap; }
.hdr-right {
  position: absolute; right: 32px; top: 27px; display: flex; align-items: center; gap: 12px;
  font-size: 10px; letter-spacing: .105em; color: #aab4c8; white-space: nowrap;
}
.hdr-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--pink); box-shadow: 0 0 7px rgba(var(--aef4d8f),.9); }
.hdr-sep { width: 1px; height: 14px; background: var(--line); }
#clock { font-size: 12.5px; letter-spacing: .10em; color: #dfe6f4; font-variant-numeric: tabular-nums; }

.ftr-slash {
  position: absolute; left: 40px; top: 16px; width: 3px; height: 30px;
  background: var(--pink); transform: skewX(-24deg);
  box-shadow: 0 0 9px rgba(var(--aef4d8f),.8);
}
.ftr-m { position: absolute; left: 64px; top: 13px; font-size: 15px; line-height: 18.4px; letter-spacing: .012em; color: #c2cbdc; white-space: nowrap; }
.ftr-r {
  position: absolute; right: 32px; top: 20px; height: 22px; padding: 0 12px;
  display: flex; align-items: center;
  font-size: 9.6px; letter-spacing: .105em; color: #b3bccd; white-space: nowrap;
  border: 1px solid rgba(168,186,224,.28); background: rgba(90,115,165,.06);
  clip-path: polygon(6px 0,100% 0,100% calc(100% - 6px),calc(100% - 6px) 100%,0 100%,0 6px);
}
.ftr-r em, .p-title em { font-style: normal; color: #6a7488; }
/* .ftr-r is a flex row, so the spaces around the separator collapse: put them back */
.ftr-r em { margin: 0 6px; }

/* ---------------- panel component ---------------------------------------
   Chrome order inside each .panel: shadow silhouette, rim ring (backdrop
   glass), interior core, SVG frame strokes, then title + content.
   All geometry is generated in main.js from one parametric outline.
   ------------------------------------------------------------------------ */
.panel { position: absolute; }
/* chrome (shadow, rim, core, frame) lives in its own layer so it can take
   the render's perspective tilt while the type above stays flat */
.p-chrome { position: absolute; inset: 0; pointer-events: none; }
/* on tilted panels the content rides the same plane as the chrome, so type
   sits in the surface instead of floating flat in front of it */
.p-warp { position: absolute; inset: 0; transform-style: flat; }
.p-shad { position: absolute; inset: 0; pointer-events: none; filter: blur(24px); transform: translate(6px, 18px); }
.p-shad i { position: absolute; inset: 0; display: block; background: rgba(2,4,10,.55); }
.p-glass { position: absolute; inset: 0; pointer-events: none; }
.p-edge { position: absolute; inset: 0; pointer-events: none; }
.p-frame { position: absolute; inset: 0; pointer-events: none; overflow: visible; }

/* ---- THICK GLASS (terminal, profile) — the canonical recipe:
   translucent blurred surface + inset-shadow stack. The hairline top
   highlight, side rim light, wide internal glow and bottom-right falloff
   are what read as thickness; no painted bands. */
.thick .p-glass {
  background:
    radial-gradient(130% 90% at 28% -18%, rgba(150,182,245,.10) 0%, transparent 55%),
    linear-gradient(168deg, rgba(19,28,48,.58) 0%, rgba(9,15,28,.68) 50%, rgba(14,22,40,.62) 100%);
  backdrop-filter: blur(18px) saturate(150%) brightness(.68);
  -webkit-backdrop-filter: blur(18px) saturate(150%) brightness(.68);
}
.thick .p-edge {
  box-shadow:
    inset 0 1.5px 0 rgba(255,255,255,.32),
    inset 1.5px 0 0 rgba(215,228,255,.15),
    inset -1px 0 0 rgba(185,205,245,.09),
    inset 0 -1.5px 0 rgba(205,220,252,.18),
    inset 0 0 26px rgba(150,182,245,.10),
    inset 5px 7px 18px rgba(255,255,255,.05),
    inset -10px -12px 30px rgba(2,6,18,.45);
}
/* a soft diagonal reflection, sliding with the pointer */
.thick .p-glare {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg,
    transparent 32%, rgba(190,212,255,.04) 43%, rgba(210,226,255,.065) 50%,
    rgba(190,212,255,.04) 57%, transparent 68%);
  background-size: 160% 160%;
  transition: background-position .25s ease-out;
}

/* ---- THIN GLASS (system, feed): a clear pane, same recipe, lighter hand */
.thin .p-glass {
  background: linear-gradient(165deg, rgba(140,170,230,.07) 0%, rgba(90,120,180,.03) 50%, rgba(140,170,230,.06) 100%);
  backdrop-filter: blur(3px) saturate(135%) brightness(1.02);
  -webkit-backdrop-filter: blur(3px) saturate(135%) brightness(1.02);
}
.thin .p-edge {
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.18),
    inset 0 -1px 0 rgba(190,210,250,.09),
    inset 0 0 14px rgba(150,182,245,.06);
}
.thin .p-shad { filter: blur(14px); transform: translate(3px, 9px); }
.thin .p-shad i { background: rgba(2,4,10,.26); }

/* the feed sits in front of the brain: a touch more veil keeps its text
   readable without losing the see-through character */
#p-feed .p-glass {
  background: linear-gradient(165deg, rgba(14,22,38,.42) 0%, rgba(9,15,28,.36) 50%, rgba(14,22,38,.40) 100%);
  backdrop-filter: blur(4px) saturate(112%) brightness(.94);
  -webkit-backdrop-filter: blur(4px) saturate(112%) brightness(.94);
}
/* the feed dissolves toward its bottom: atmosphere, not competition */
#p-feed .p-chrome, #p-feed .p-frame {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 40%, rgba(0,0,0,.45) 78%, rgba(0,0,0,.16) 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 40%, rgba(0,0,0,.45) 78%, rgba(0,0,0,.16) 100%);
}
#p-feed #feedCode { opacity: .82; }

.p-title {
  position: absolute; left: 24px; top: 12px; font-size: 10.4px; letter-spacing: .06em;
  color: #d7dfee; white-space: nowrap;
}
.p-title.ind { left: 48px; }
.p-title.sm { font-size: 9.5px; letter-spacing: .17em; color: #dbe3f0; }
.p-body { position: absolute; }
.p-foot {
  position: absolute; left: 26px; right: 26px; bottom: 12px;
  display: flex; justify-content: space-between;
  font-size: 8.8px; letter-spacing: .06em; color: #7e879b;
}
.p-foot .pr { letter-spacing: .075em; }

/* boot: frame draws itself, interior fades in after */
.panel .p-frame .draw {
  stroke-dasharray: 1; stroke-dashoffset: 1;
  transition: stroke-dashoffset .7s cubic-bezier(.25,.8,.3,1);
}
.panel.on .p-frame .draw { stroke-dashoffset: 0; }
.panel .p-chrome, .panel .p-body, .panel .p-title,
.panel .p-foot, .panel .mk, .panel .live-tag, .panel #aboutClose {
  opacity: 0; transition: opacity .5s ease .30s;
}
.panel.on .p-chrome, .panel.on .p-body, .panel.on .p-title,
.panel.on .p-foot, .panel.on .mk, .panel.on .live-tag, .panel.on #aboutClose {
  opacity: 1;
}

/* ---------------- system panel ------------------------------------------ */
#p-sys { left: 32px; top: 120px; width: 150px; height: 400px; }
#p-sys .p-title { left: 18px; top: 14px; }
.sys-dot { position: absolute; right: 16px; top: 16px; width: 5px; height: 5px; border-radius: 50%;
  background: var(--pink); box-shadow: 0 0 7px rgba(var(--aef4d8f),.9); }
.sy-h { position: absolute; left: 18px; font-size: 9.5px; letter-spacing: .12em; color: #aab4c8; }
.sy-h b { display: block; font-weight: 400; color: #e6ecf7; margin-top: 3px; letter-spacing: .10em; }
.sy-div { position: absolute; left: 14px; right: 14px; height: 1px; background: rgba(168,186,224,.14); }
.sy-lab { position: absolute; left: 18px; font-size: 9.5px; letter-spacing: .10em; color: #aab4c8; }
.sy-v { position: absolute; right: 16px; font-weight: 300; font-size: 12px; color: #e9eff8; white-space: nowrap; text-align: right; }
.sy-bars { position: absolute; left: 18px; width: 18px; height: 22px; }
.sy-bars i { position: absolute; bottom: 0; width: 2.5px; background: rgba(190,205,240,.34); }
.sy-spark { position: absolute; left: 44px; width: 88px; height: 24px; overflow: visible; }
.sy-spark .ln { fill: none; stroke: rgb(var(--aef4f8e)); stroke-width: 1.0; stroke-linejoin: round;
  filter: drop-shadow(0 0 2.5px rgba(var(--aef4f8e),.85)); }
.sy-spark .fl { fill: url(#sparkFill); stroke: none; }

/* ---------------- agent feed -------------------------------------------- */
#p-feed { left: 520px; top: 100px; width: 560px; height: 236px; }
.live-tag {
  position: absolute; right: 18px; top: 12px; display: flex; align-items: center; gap: 6px;
  font-size: 8.6px; letter-spacing: .14em; color: rgb(var(--af0a7c8));
}
.live-tag i { width: 5px; height: 5px; border-radius: 50%; background: var(--pink);
  box-shadow: 0 0 7px rgba(var(--aef4d8f),.95); animation: livepulse 1.6s ease infinite; }
@keyframes livepulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
#p-feed .p-body { left: 24px; top: 42px; right: 24px; bottom: 14px; overflow: hidden; }
#feedLog { position: absolute; left: 0; top: 0; width: 100%; height: 84px; overflow: hidden; }
#feedLog div { font-size: 10.9px; line-height: 14px; white-space: nowrap; color: #b9c3d6; }
#feedLog .ts { color: #6f7a91; }
/* one hue per agent, in the feed and on the `status` bus alike. The element
   says WHICH agent it is (`.ag0`..`.ag4`, pipeline order); every rule that
   paints one reads `--ag`, so a new surface costs one declaration, not five. */
.ag0 { --ag: var(--ag-1); } .ag1 { --ag: var(--ag-2); } .ag2 { --ag: var(--ag-3); }
.ag3 { --ag: var(--ag-4); } .ag4 { --ag: var(--ag-5); }
#feedLog .who { color: rgb(var(--ag)); }
#feedCode { position: absolute; left: 20px; top: 96px; right: 0; height: 84px; overflow: hidden; }
#feedCode div { font-size: 10.9px; line-height: 14px; white-space: pre; color: #aab4c9; }

.t-key { color: rgb(var(--sig-key)); } .t-str { color: rgb(var(--aff9ec8)); } .t-com { color: #5d6780; font-style: italic; }
.t-num { color: rgb(var(--sig-num)); } .t-fn { color: rgb(var(--sig-fn)); } .t-tag { color: rgb(var(--aef4d8f)); } .t-pun { color: #7a8399; }

/* ---------------- terminal ---------------------------------------------- */
#p-term { left: 204px; top: 298px; width: 470px; height: 580px; }
#p-term .p-body { left: 38px; top: 50px; width: 400px; bottom: 38px; overflow: hidden; }
#p-term .p-foot { left: 38px; right: 30px; bottom: 16px; }
#termLog div { font-size: 13px; line-height: 20.5px; color: #b9c3d6; letter-spacing: .005em; white-space: nowrap; }
#termLog div.b { color: #e8eef9; }
#termPrompt { font-size: 13px; line-height: 20.5px; color: #e8eef9; display: flex; align-items: center; gap: 7px;
              overflow: hidden; white-space: nowrap; }
#termPrompt .chev { color: var(--pink); }
/* A command is no longer echoed into the log, so the boot text never moves --
   which means a rejected command has nowhere to be reported except here, on the
   prompt row itself. It costs no height, it sits where the reader is already
   looking, and it clears on the next keystroke. */
#termPrompt .err { color: #9aa4b8; }
.caret {
  display: inline-block; width: 7px; height: 13px; vertical-align: -2px;
  background: var(--pink); box-shadow: 0 0 9px rgba(var(--aef4d8f),.95);
  animation: blink 1.05s steps(1) infinite;
}
@keyframes blink { 0%,50% { opacity: 1 } 50.01%,100% { opacity: 0 } }

#termCmds { margin-top: 15px; display: none; }
#termCmds.on { display: block; }
.cmd-rule { height: 1px; background: rgba(190,206,240,.22); margin-bottom: 13px; }
.cmd-head { font-size: 11.5px; letter-spacing: .17em; color: #dbe3f0; margin-bottom: 11px; }
/* The grid must stay 242px tall or the terminal ends in dead air. Dropping
   `experience` and `skills` left 128px of it: the boot log only recovers lines when
   it OVERFLOWS, and it is already showing all eight BOOT lines, so nothing comes back
   from the top and the gap has to absorb the whole loss.
   gap = (242 - rows * rowHeight) / (rows - 1); recompute if the list changes again.
   242 is MEASURED off the accepted nine-row build (grid top y221.5, bottom y463.5),
   not the 263 an earlier note inferred -- 263 was the budget including a recovered log
   line, and at 263 the last row sits 20px below where he signed it off.
   The type went up with it: at seven items this is the site's primary navigation, so
   bigger targets and a looser rhythm beat a stretched 13px list. */
.cmd-grid { display: grid; grid-template-columns: 118px 1fr; row-gap: 22.5px; }
.cmd {
  all: unset; font-family: var(--mono); font-size: 14.5px; line-height: 15.3px;
  color: var(--pink); cursor: pointer; letter-spacing: .01em;
  transition: color .14s ease, text-shadow .14s ease, transform .14s ease;
}
.cmd:hover, .cmd:focus-visible { color: rgb(var(--aff8cba)); text-shadow: 0 0 13px rgba(var(--aef4d8f),.8); transform: translateX(2px); }
/* The focus ring, and the one place on the site where it has to be argued rather
   than declared. `.cmd` is the only focusable element inside the terminal's
   `.p-warp`, so PRODUCTION-SPEC.md 2.7 says to photograph it before believing it.
   `dbg-focus.mjs` is that photograph, and it settles two things:

     - the outline IS drawn in the transformed plane -- measured, its top edge
       drops 1.5 CSS px across an 86 px button, which is exactly the panel's own
       .034 top-edge slope. It shears WITH the panel, not against it, and that is
       what makes it read as part of the terminal.
     - the inset box-shadow ring the plan proposes instead is worse HERE and the
       photograph shows why: `all: unset` plus a grid cell makes this button 82 x
       12 with no padding, so an inset ring is drawn straight through the glyphs. An
       offset outline keeps its 3 px of clearance. To see both again:
         node dbg-focus.mjs .cmd shots/a.png
         node dbg-focus.mjs .cmd shots/b.png '.cmd:focus-visible{outline:none !important;box-shadow:inset 0 0 0 1px rgb(var(--aff8cba)) !important}'

   What did have to change is the ring itself. `rgba(var(--aef4d8f),.55)` measures
   2.38 against the glass behind the grid -- under WCAG 2.2's 3:1 floor for a
   focus indicator, on the site's primary navigation. Solid rgb(var(--aff8cba)), the colour
   the button already takes on hover and focus, measures 8.49, and it is a value
   already in this file so it adds no accent token. 2 px, because 1 px at the
   0.69 canvas zoom is a hairline. */
.cmd:focus-visible { outline: 2px solid rgb(var(--aff8cba)); outline-offset: 3px; }
.cmd-d { font-size: 14.5px; line-height: 15.3px; color: #9aa4b8; letter-spacing: .01em; }

/* ---------------- about -------------------------------------------------- */
#p-about {
  left: 700px; top: 380px; width: 804px; height: 556px;
  opacity: 0; transform: translateY(14px) scale(.99); pointer-events: none;
  transition: opacity .42s cubic-bezier(.22,.9,.28,1), transform .42s cubic-bezier(.22,.9,.28,1);
}
#p-about.on-open { opacity: 1; transform: none; pointer-events: auto; }
/* open() moves focus here rather than to the close cross (see main-ann.js). The
   panel is a focus DESTINATION, not a control -- `tabindex="-1"`, never in the tab
   order -- and the visible affordance is the panel itself arriving. Chrome's UA
   `:focus-visible { outline: auto }` would otherwise paint a browser-blue band
   across the glass on a keyboard-initiated open, which is the one thing hard rule 1
   is most protective of. Every element that IS a control keeps its indicator. */
#p-about:focus { outline: none; }
#aboutClose {
  position: absolute; right: 12px; top: 2.9px; z-index: 6;
  width: 38px; height: 30px; line-height: 30px; text-align: center;
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--mono); font-size: 23px; color: #a8b2c8;
  text-shadow: 0 0 6px rgba(160,180,220,.25);
  transition: color .15s ease, text-shadow .15s ease, transform .15s ease;
}
#aboutClose:hover { transform: scale(1.08); }
#aboutClose:hover, #aboutClose:focus-visible { color: var(--pink-s); text-shadow: 0 0 10px rgba(var(--aef4d8f),.8); }
#p-about .p-body { position: absolute; inset: 0; }

.ab-name {
  position: absolute; left: 53px; top: 52px; margin: 0;
  font-family: var(--disp); font-weight: 300; font-size: 29px; letter-spacing: .13em;
  color: #f3f7fd; line-height: 1; text-shadow: 0 0 26px rgba(190,212,255,.30);
}
.ab-roles { position: absolute; left: 53px; top: 97px; margin: 0; font-size: 13px; line-height: 18.6px; letter-spacing: .10em; color: #b6c0d3; }
.ab-quote {
  position: absolute; left: 51px; top: 149px; width: 350px; padding: 12px 15px;
  font-size: 12.5px; line-height: 16.2px; color: #b3bdd1;
  border: 1px solid rgba(176,196,236,.16); background: rgba(255,255,255,.018);
  clip-path: polygon(9px 0,100% 0,100% calc(100% - 9px),calc(100% - 9px) 100%,0 100%,0 9px);
}
.ab-sec { position: absolute; font-size: 11px; letter-spacing: .17em; color: #9fa9bf; }

/* six rows in a 556 panel: the hero block gives up 4-10px each and the log runs
   y262 to y548. Type is one point up on all three lines versus the five-row
   version, and the row gap still measures ~4.8px. */
.ab-log { position: absolute; left: 53px; top: 262px; width: 360px; }
.ab-row { position: absolute; left: 0; width: 360px; }
.ab-row::before {
  content: ''; position: absolute; left: 0; top: 3px; width: 8px; height: 8px; border-radius: 50%;
  background: #0a0c18; border: 1.6px solid var(--pink); box-shadow: 0 0 8px rgba(var(--aef4d8f),.85);
}
.ab-row .yr { position: absolute; left: 22px; top: 0; font-size: 11.5px; letter-spacing: .11em; color: var(--pink-s); }
.ab-row .tt { position: absolute; left: 22px; top: 14.4px; font-size: 12.5px; letter-spacing: .11em; color: #e2e8f5; white-space: nowrap; }
.ab-row .sb { position: absolute; left: 22px; top: 30px; font-size: 10.5px; letter-spacing: .09em; color: #79839a; }
.ab-row .pill {
  position: absolute; right: 0; top: 0; font-size: 8.5px; letter-spacing: .14em;
  padding: 3px 8px; color: var(--pink-s); border: 1px solid rgba(var(--aef4d8f),.40);
  clip-path: polygon(5px 0,100% 0,100% calc(100% - 5px),calc(100% - 5px) 100%,0 100%,0 5px);
}
.ab-row .pill.done { color: #838da3; border-color: rgba(150,168,208,.24); }

.ab-stats { position: absolute; left: 437px; top: 82px; width: 336px; }
.ab-stat {
  position: absolute; width: 160px; height: 78px; padding: 10px 12px;
  border: 1px solid rgba(176,196,236,.17); background: rgba(255,255,255,.02);
  clip-path: polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px);
}
.ab-stat .k { font-size: 8.5px; letter-spacing: .14em; color: #7d8699; }
.ab-stat .v { font-family: var(--disp); font-weight: 300; font-size: 25px; color: #f2f6fd; line-height: 1; margin-top: 4px; }
/* the glow lives on the svg, not the path: the path's own `filter` is spent on the
   shimmer, and one element cannot animate brightness independently of a drop-shadow
   in the same filter list */
.ab-stat svg { display: block; width: 100%; height: 18px; margin-top: 6px; overflow: visible;
               filter: drop-shadow(0 0 4px rgba(var(--aef4d8f),.85)); }
.ab-stat .ln { fill: none; stroke: var(--pink); stroke-width: 1.25; stroke-linejoin: round; }
.ab-stat .fl { fill: url(#sparkFill); stroke: none; opacity: 0; }

/* replays on every open, because open() drops .on and forces a reflow first */
.mod[data-mod="about"].on .ab-stat .ln {
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: ab-draw .95s cubic-bezier(.35,.75,.35,1) both,
             ab-shine 4.6s linear infinite;
}
.mod[data-mod="about"].on .ab-stat .fl { animation: ab-fl .55s ease both; }
@keyframes ab-draw { to { stroke-dashoffset: 0; } }
@keyframes ab-fl   { from { opacity: 0; } to { opacity: .72; } }
@keyframes ab-shine {
  0%, 100% { filter: brightness(1); }
  6%       { filter: brightness(1.55); }
  17%      { filter: brightness(1); }
}

.ab-matrix { position: absolute; left: 437px; top: 300px; width: 336px; }
.mx { position: absolute; left: 0; width: 336px; height: 14px; }
.mx .k { position: absolute; left: 0; top: 0; font-size: 11.5px; color: #b6c0d3; }
.mx .t { position: absolute; left: 148px; top: 5px; width: 140px; height: 4px; background: rgba(255,255,255,.08); }
.mx .f { display: block; height: 100%; width: 0; background: linear-gradient(90deg,rgb(var(--aa63668)),rgb(var(--aff5fa2))); box-shadow: 0 0 8px rgba(var(--aff5fa2),.7); transition: width 1s cubic-bezier(.2,.9,.3,1); }
.mx .p { position: absolute; right: 0; top: 0; font-size: 10.5px; color: #b6c0d3; font-variant-numeric: tabular-nums; }

/* ---------------- module switching --------------------------------------- */
/* one content shell, many modules. Only the active module is in the flow, so
   nothing from a hidden module can be tabbed to or hit-tested. */
.mod { position: absolute; inset: 0; display: none; }
.mod.on { display: block; }

/* ---------------- resume module ------------------------------------------ */
/* coordinates are panel-local, 804 x 556 design px, matching the about module */
.rs-art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.rs-art path, .rs-art rect, .rs-art circle { fill: none; stroke-linecap: round; stroke-linejoin: round; }

.a-card  { stroke: rgba(178,198,238,.62); stroke-width: 1.35; fill: rgba(255,255,255,.016); }
.a-doc   { stroke: #ced7e6; stroke-width: 1.7; }
.a-badge { stroke: var(--pink); stroke-width: 1.7; fill: rgba(var(--aef4d8f),.05);
           filter: drop-shadow(0 0 5px rgba(var(--aef4d8f),.45)); }

.a-arrow            { stroke: rgb(var(--af9639f)); stroke-width: 3.4; }
.rs-art .a-arrowhead { fill: rgb(var(--af9639f)); stroke: none; }
.a-arrow, .a-arrowhead { filter: drop-shadow(0 0 6px rgba(var(--aef4d8f),.55)); }
.a-tray  { stroke: #c3ccdd; stroke-width: 2; }

.a-rule  { stroke: rgba(168,186,224,.13); stroke-width: 1; }
.a-vrule { stroke: rgba(168,186,224,.13); stroke-width: 1; }
.a-chev  { stroke: rgba(168,186,224,.34); stroke-width: 1.3; }

.a-tick-box { stroke: rgb(var(--af56698)); stroke-width: 1.7; }
.a-tick     { stroke: rgb(var(--af9689f)); stroke-width: 2.4; }
.a-lock     { stroke: rgb(var(--aea6296)); stroke-width: 1.9; }
.a-lock-key { stroke: rgb(var(--aea6296)); stroke-width: 1.9; }
.a-tick-box, .a-tick, .a-lock, .a-lock-key { filter: drop-shadow(0 0 5px rgba(var(--aef4d8f),.45)); }

.a-ring     { stroke: var(--pink); stroke-width: 1.9; filter: drop-shadow(0 0 5px rgba(var(--aef4d8f),.6)); }
.a-ring.dim { stroke: rgba(var(--aef4d8f),.75); filter: none; }

.rs-h {
  position: absolute; left: 48px; top: 64px;
  font-size: 15px; line-height: 1; letter-spacing: .17em; color: #dbe3f0;
}
.rs-badge {
  position: absolute; left: 75px; top: 185px; width: 56px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; letter-spacing: .12em; color: var(--pink-s);
  text-shadow: 0 0 9px rgba(var(--aef4d8f),.7);
}
.rs-file {
  position: absolute; left: 186px; top: 145px;
  font-size: 16px; line-height: 1; letter-spacing: .27em; color: #f2f6fd;
  text-shadow: 0 0 18px rgba(190,212,255,.28);
}
.rs-meta {
  position: absolute; left: 186px; top: 180px;
  font-size: 13px; line-height: 1; letter-spacing: .275em; color: #aab3c6;
}
.rs-meta em { font-style: normal; color: #5f6a80; }
.rs-state {
  position: absolute; left: 209px; top: 217px;
  font-size: 13.5px; line-height: 1; letter-spacing: .22em; color: rgb(var(--af082a5));
  text-shadow: 0 0 9px rgba(var(--aef4d8f),.40);
}

.rs-k   { position: absolute; font-size: 11.5px; line-height: 1; letter-spacing: .10em; color: #7d8699; }
.rs-v   { position: absolute; font-size: 12.2px; line-height: 1; letter-spacing: .06em; color: #9aa4b8; }
.rs-v.sm { font-size: 12.5px; letter-spacing: .04em; }
.rs-sub { position: absolute; font-size: 11.5px; line-height: 1; letter-spacing: .03em; color: #9aa4b8; }
.rs-hex {
  position: absolute; left: 48px; top: 503px;
  font-size: 10px; line-height: 1; letter-spacing: .04em; color: #656e82;
}
.rs-bars {
  position: absolute; left: 48px; top: 473px; width: 232px; height: 16px;
  display: flex; align-items: stretch; gap: 0;
}
/* Two animations on different properties so they never fight: `rs-arm` runs once
   on open and owns opacity + glow, `rs-shine` loops forever and owns brightness.
   Both are driven by a per-bar delay, which is what turns 47 independent bars into
   one travelling wave. Restarts by itself every time the module leaves display:none. */
.rs-bars i {
  display: block; height: 100%; background: rgb(var(--afb7bad));
  box-shadow: 0 0 6px rgba(var(--aef4d8f),.55);
  animation: rs-arm .42s ease-out both, rs-shine 4.6s linear infinite;
}
@keyframes rs-arm {
  0%   { opacity: 0; transform: scaleY(.35); box-shadow: none; }
  55%  { opacity: 1; transform: scaleY(1);   box-shadow: 0 0 13px rgba(var(--aff8cbe),.95); }
  100% { opacity: 1; transform: scaleY(1);   box-shadow: 0 0 6px rgba(var(--aef4d8f),.55); }
}
@keyframes rs-shine {
  0%, 100% { filter: brightness(1); }
  6%       { filter: brightness(1.62); }
  17%      { filter: brightness(1); }
}
/* the tick draws itself once the sweep has passed, so it reads as the result of
   the scan rather than a checkbox that was always ticked */
.mod[data-mod="resume"].on .a-tick {
  stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: rs-draw .42s cubic-bezier(.3,.8,.35,1) 1.0s forwards;
}
.mod[data-mod="resume"].on .a-tick-box { animation: rs-fade .5s ease .82s both; }
@keyframes rs-draw { to { stroke-dashoffset: 0; } }
@keyframes rs-fade { from { opacity: .18; } to { opacity: 1; } }

/* ---- the action button: three states, all drawn ------------------------- */
.rs-btn {
  position: absolute; left: 188px; top: 302px; width: 420px; height: 55px;
  padding: 0; margin: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--mono); display: block;
  -webkit-tap-highlight-color: transparent;
  /* it is an <a download> since the resume left the bundle (PLAN C16), and the UA
     underline would land on the two .rs-btn-t runs */
  text-decoration: none;
}
.rs-btn-art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.rs-btn-art path { fill: none; stroke: none; vector-effect: non-scaling-stroke; }

/* resting layer */
.b-base .b-face { stroke: rgba(var(--af382ac),.80); stroke-width: 1.6; }
.b-base .b-brk  { stroke: rgba(var(--af27ca8),.62); stroke-width: 1.2; }

/* lit layer: identical geometry, cross-faded on top of the resting one */
.b-hot { opacity: 0; transition: opacity .17s ease; }
.b-hot .b-fill { fill: rgba(var(--aef4d8f),.11); }
.b-hot .b-cas {
  stroke: rgba(var(--aff4e92),.85); stroke-width: 4.2;
  filter: blur(1.5px)
          drop-shadow(0 0 6px rgba(var(--aff5498),.95))
          drop-shadow(0 0 18px rgba(var(--aff408c),.75))
          drop-shadow(0 0 36px rgba(var(--aff3682),.42));
}
.b-hot .b-face { stroke: rgba(255,240,246,.92); stroke-width: 1.6; }
.b-hot .b-brk { stroke: #fff; stroke-width: 1.4; }

.rs-btn-t {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  /* the trailing letter-space would push the run left of centre */
  padding: 6px 0 0 .345em;
  font-size: 19px; letter-spacing: .345em; color: rgb(var(--aef6ba0));
  text-shadow: 0 0 10px rgba(var(--aef4d8f),.45);
}
.rs-btn-t.t-hot { color: #fff; text-shadow: 0 0 12px rgba(var(--aff78af),.9); opacity: 0; transition: opacity .17s ease; }

.rs-btn:hover .b-hot, .rs-btn:focus-visible .b-hot,
.rs-btn:hover .t-hot, .rs-btn:focus-visible .t-hot { opacity: 1; }

/* pressed: the lit layer stays up and flips from white-hot to saturated pink */
.rs-btn:active .b-hot, .rs-btn:active .t-hot { opacity: 1; }
.rs-btn:active .b-hot .b-fill { fill: rgba(var(--aef4d8f),.09); }
.rs-btn:active .b-hot .b-cas {
  stroke: rgba(var(--aff3e8a),.95); stroke-width: 5.4;
  filter: blur(1.8px)
          drop-shadow(0 0 7px rgba(var(--aff3c8a),1))
          drop-shadow(0 0 22px rgba(var(--aff3280),.8))
          drop-shadow(0 0 42px rgba(var(--aff2c78),.45));
}
.rs-btn:active .b-hot .b-face { stroke: rgb(var(--aff5c9e)); stroke-width: 2.1; }
.rs-btn:active .b-hot .b-brk { stroke: rgb(var(--aff9dc4)); stroke-width: 1.6; }
.rs-btn:active .t-hot { color: rgb(var(--aff6ba0)); text-shadow: 0 0 13px rgba(var(--aff5096),.85); }

/* ---------------- projects module ----------------------------------------
   Panel-local 804 x 556, same shell as about / resume / contacts.
   Content box x48..756, which is the house grid — NOT the render's own margins.
   `Projects_ui.png` draws the panel at 863x527 (aspect 1.64 against the design
   panel's 1.446), so normalising its x positions straight onto 804 would put the
   cards 20px from the panel edge and disagree with every other module the reader
   flips between. Proportions come from the render, the outer grid does not.

   Columns: cards 48..324, dashed gutter at 340, detail 364..756. */
.pj-art  { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.pj-art path { fill: none; }
.pj-div  { stroke: rgba(168,186,224,.17); stroke-width: 1; stroke-dasharray: 2 6; }
.pj-rule { stroke: rgba(168,186,224,.13); stroke-width: 1; }

.pj-h     { position: absolute; left: 48px; top: 64px;
            font-size: 14px; line-height: 1; letter-spacing: .17em; color: #dbe3f0; }
.pj-range { position: absolute; right: 48px; top: 66px;
            font-size: 11px; line-height: 1; letter-spacing: .17em; color: #7d8699; }
.pj-range em { font-style: normal; color: rgb(var(--aef4d8f)); }

/* ---- the card list ------------------------------------------------------
   Five cards, y100..528 at a 88px pitch. Card height 76 and gap 12 keep the
   render's ~6:1 card-to-gap ratio inside the height the panel actually has. */
/* A flow column, not a fixed pitch: one of his titles wraps to two lines and that
   card has to be a line taller than the others. Gap 10.95 fills y96..532 exactly at
   the natural content heights; if a second title starts wrapping, this is the number
   that gives, not the card padding. */
.pj-cards { position: absolute; left: 48px; top: 96px; width: 276px;
            display: flex; flex-direction: column; gap: 10.95px; }

.pj-card {
  position: relative; width: 276px;
  background: none; border: 0; margin: 0; font: inherit;
  padding: 12px 13px 12px 60px;
  display: block; text-align: left; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* Rounded top corners, chamfered bottom corners — the same asymmetry as the
   resume button (4px top / 14px bottom). It is what stops the card reading as a
   generic rounded rectangle, and it is already a house convention here. */
.pj-frame { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
            pointer-events: none; }
.pj-frame path { fill: none; stroke: none; }
.pj-frame .f-fill { fill: rgba(150,175,235,.012); }
.pj-frame .f-face { stroke: rgba(178,198,238,.30); stroke-width: 1.15;
                    transition: stroke .18s ease; }
/* casing carries the bloom, core carries the hard edge. One stroke reads as a
   thin wire or a smear, never as neon. Same split as the resume button. */
.pj-frame .f-cas  { stroke: none; }

.pj-card:hover .f-face,
.pj-card:focus-visible .f-face { stroke: rgba(206,222,255,.62); }
.pj-card:focus-visible { outline: none; }

.pj-card.sel .f-fill { fill: rgba(var(--aef4d8f),.035); }
.pj-card.sel .f-cas  { stroke: rgba(var(--aff4e8f),.30); stroke-width: 5;
                       filter: blur(3.5px); }
.pj-card.sel .f-face { stroke: rgb(var(--aff4e8f)); stroke-width: 1.5;
                       filter: drop-shadow(0 0 7px rgba(var(--aff4e8f),.75)); }

/* centred by inset+flex rather than a transform: the card's intro animation owns
   `transform`, and a translateY(-50%) here would be overwritten mid-animation */
.pj-num  { position: absolute; left: 17px; top: 0; bottom: 0; display: flex;
           align-items: center;
           font-family: var(--disp); font-size: 27px; line-height: 1;
           letter-spacing: .01em; color: #97a2b8; transition: color .18s ease; }
.pj-card.sel .pj-num { color: rgb(var(--aff6ba3)); text-shadow: 0 0 14px rgba(var(--aff4e8f),.6); }

.pj-t    { display: block; font-size: 12px; line-height: 15px;
           letter-spacing: .055em; color: #dfe6f2; }
.pj-row  { display: flex; align-items: center; justify-content: space-between;
           gap: 8px; margin-top: 5px; }
/* ---- the tier-1 / tier-3 contrast lifts (PRODUCTION-SPEC.md 2.7) ------------
   Four selectors carry information a reader needs and measured below WCAG AA:
   `.pj-d` 4.27, `.pj-focus` (`#pjFocus`) 4.09 and `.th-st` on inactive rows 3.81 --
   all one grey, `#6f788c` -- plus `#feedLog .ts` 4.34, `#6c768d`.

   THE SPEC'S x1.05 IS NOT ENOUGH, measured: the three sit on three different
   backdrops, and on the worst of them -- the theme row under the amber profile,
   rgb(22,29,45) -- x1.05 lands at 4.29 and still fails.

   Each one is therefore lifted to the SMALLEST value that clears 4.5 on ITS OWN
   backdrop, rather than to one grey sized to the worst of the three. That is
   Svyat's call and the reason is the look: one grey sized to the theme row is
   #7d889e, which moves the projects card's domain label by 18 of 255 when 6 is
   all it needs. The cost is three greys where the prototype had one, and it is
   the smaller cost.

     .pj-d          #6f788c -> #737d92   4.27 -> 4.56   max |delta| 6
     .pj-focus      #6f788c -> #798298   4.09 -> 4.62   max |delta| 12
     .th-st         #6f788c -> #7c869c   3.81 -> 4.59   max |delta| 16
     #feedLog .ts   #6c768d -> #6f7a91   4.34 -> 4.58   max |delta| 4   (the spec's x1.03)

   PHASE 5 RE-MEASURED ALL FOUR and `.pj-focus` moved again, from #778096 to #798298.
   Nothing about the site changed: the review round after Phase 4 fixed `test-contrast`
   itself, which had been photographing the backdrop with no swiftshader args and with
   the ResizeObserver double-rebuild race live, so it was reading the HOST GPU rather
   than the build. Under the corrected harness the projects backdrop reads rgb(17,24,39)
   where Phase 4 read rgb(16,22,37), and #778096 on it is **4.49** -- 0.01 under, and
   `test-contrast` and `test-a11y` both went red on it.

   #788197 would clear at 4.55 and is the strict minimum, one step of 1/255 up. #798298 is
   two steps and clears at 4.62. The second step was taken because 4.55 leaves 0.05 of
   headroom against a floor that had just moved by 0.09, and the difference between one
   step and two on a 9.35px label is two units of grey -- under `dbg-pagediff.mjs`'s own
   noise floor either way. That is a Phase 5 departure
   from the strict-minimum rule above, it is worth 0.06, and it is one character to undo.

   16 on the theme row is unavoidable: 3.81 cannot reach 4.5 invisibly. The rest of
   the site is pixel-identical to `docs/reference/`, whole viewport, all eight views
   -- `dbg-pagediff.mjs` is the instrument.

   The FOURTH user of `#6f788c` is the AMBIENT SYNC `DISABLED` state, and it was
   MEASURED rather than assumed either way: the harness cannot see it (the toggle
   defaults to ENABLED) but driven into that state by hand it reads 4.07 on the same
   backdrop as `#pjFocus`, so it takes the same minimum. See the note beside it.

   Every value here is hue 218, well outside `isAccent()`'s 330..45 band (PLAN C7's
   second trap), so none is swept into the accent map: 46 accent tokens before and
   after. HEADROOM IS 0.06 TO 0.12 and the thin end of that has already failed once,
   so re-measure with `test-contrast.mjs` after ANY colour or background change -- and
   after any change to the HARNESS, which is what actually moved it. The backdrop is a
   mean over an animated scene, not a constant. */
.pj-y    { font-size: 10.5px; line-height: 1; letter-spacing: .075em; color: #8d97ab; }
.pj-d    { display: block; margin-top: 5px;
           font-size: 10px; line-height: 1; letter-spacing: .13em; color: #737d92; }

/* The render puts a bare pink dot here. A dot cannot say WHICH of the three
   things it might mean (live / unread / selected), and selection is already
   carried by the frame — so the slot becomes a status chip instead, borrowed
   from `Experience_UI.png`. It sits on the year line, not the title line: the
   title runs to 172px at its longest and would collide. */
/* rides the year row, so it follows a wrapped title down instead of needing its own
   per-card offset */
.pj-chip { flex: none; padding: 2.8px 7px 3px;
           font-size: 8.5px; line-height: 1; letter-spacing: .12em; color: #79839a;
           border: 1px solid rgba(168,186,224,.22); border-radius: 2px; }
.pj-chip.live { color: rgb(var(--aff7cae)); border-color: rgba(var(--aff4e8f),.5);
                background: rgba(var(--aef4d8f),.09);
                box-shadow: 0 0 9px rgba(var(--aef4d8f),.25); }

/* ---- the detail pane ----------------------------------------------------
   Flowed, not absolutely positioned like the other modules: the five entries
   have different bullet counts and different wrap points, and pinning each line
   would mean either padding the short ones or writing every entry to one length. */
.pj-detail { position: absolute; left: 364px; top: 96px; width: 392px; }

/* baseline, so the meta lines up with the title's FIRST line when the title wraps */
.pj-head { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; }
.pj-ht   { flex: 1; font-size: 16px; line-height: 19px; letter-spacing: .03em; color: #f2f6fd;
           text-shadow: 0 0 16px rgba(190,212,255,.22); }
.pj-ht em { font-style: normal; color: #5f6a80; }
.pj-hm   { flex: none; font-size: 9.5px; line-height: 1; letter-spacing: .1em; color: #7d8699;
           white-space: nowrap; }

.pj-p    { margin: 22px 0 0; font-size: 12.4px; line-height: 20px; letter-spacing: 0;
           color: #c0c9da; }
.pj-ch   { margin-top: 22px; font-size: 11px; line-height: 1; letter-spacing: .17em;
           color: #8f9ab0; }
.pj-items { list-style: none; margin: 14px 0 0; padding: 0; }
.pj-items li { position: relative; padding-left: 22px; margin-bottom: 7.5px;
               font-size: 12.4px; line-height: 19.4px; color: #b3bccd; }
.pj-items li::before { content: '>'; position: absolute; left: 0; top: 0;
                       color: rgb(var(--aef4d8f)); }

/* 9.35 and a 3.5px separator margin, not 9.6/5: the longest FOCUS line
   ("PROGRAM DELIVERY / DISTRIBUTED TEAMS / STAKEHOLDERS") is 56 characters and wrapped
   to two lines at the old size. Check `focusH` in dbg-proj.mjs after any edit here --
   8.8px is one line, 17.6 is two. */
.pj-focus { position: absolute; left: 364px; top: 530px; width: 392px;
            font-size: 9.35px; line-height: 1; letter-spacing: .105em; color: #798298; }
.pj-focus em { font-style: normal; color: #4d566a; }
.pj-focus i  { font-style: normal; color: rgb(var(--aef4d8f)); margin: 0 3.5px; }

/* ---- motion -------------------------------------------------------------
   Same grammar as every other module: stagger in once, then hold. The detail
   pane is keyed to `.in`, which buildProjects strips and re-adds around a reflow
   so a card switch replays the intro the same way re-running a command does. */
.mod[data-mod="projects"].on .pj-h     { animation: mod-in .44s ease both; }
.mod[data-mod="projects"].on .pj-range { animation: mod-in .44s ease .06s both; }
.mod[data-mod="projects"].on .pj-art   { animation: gh-fade .7s ease .1s both; }
.mod[data-mod="projects"].on .pj-card  { animation: mod-in .46s ease var(--d) both; }

.pj-detail.in .pj-head  { animation: mod-in .40s ease .04s both; }
.pj-detail.in .pj-p     { animation: mod-in .40s ease .10s both; }
.pj-detail.in .pj-ch    { animation: mod-in .40s ease .16s both; }
.pj-detail.in .pj-items li { animation: mod-in .40s ease var(--d) both; }
.pj-focus.in            { animation: mod-in .40s ease .30s both; }

/* ---------------- contacts module ----------------------------------------- */
/* panel-local coordinates, 804 x 556, same as about and resume */
.ct-art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.c-rule { fill: none; stroke: rgba(168,186,224,.13); stroke-width: 1; }

.ct-h     { position: absolute; left: 48px; top: 83.4px;
            font-size: 14px; line-height: 1; letter-spacing: .14em; color: #dbe3f0; }
.ct-intro { position: absolute; left: 48px; top: 115.8px; margin: 0;
            font-size: 15px; line-height: 19.1px; letter-spacing: 0; color: #c3ccdc; }

/* ---- a channel row -------------------------------------------------------
   The frame, the glyph, the label and the arrow all react to one hover, so
   each row owns its art rather than sharing a panel-wide layer. */
.ct-row { position: absolute; left: 48px; width: 708px; height: 92.4px;
          display: block; text-decoration: none; cursor: pointer;
          -webkit-tap-highlight-color: transparent; }
.ct-frame, .ct-ico { position: absolute; inset: 0; width: 100%; height: 100%;
                     overflow: visible; pointer-events: none; }
.ct-frame path { fill: none; stroke: none; }

.ct-frame .f-fill { fill: rgba(150,175,235,.008); }
.ct-frame .f-face { stroke: rgba(178,198,238,.40); stroke-width: 1.3;
                    transition: stroke .18s ease; }
/* casing carries the bloom, core carries the hard edge — one stroke reads as a
   wire or a smear, never as neon. Same split as the resume button. */
.ct-frame .f-cas  { stroke: rgba(var(--aff4e92),.80); stroke-width: 3.2; opacity: 0;
                    transition: opacity .18s ease;
                    filter: blur(1.3px)
                            drop-shadow(0 0 5px rgba(var(--aff5498),.9))
                            drop-shadow(0 0 16px rgba(var(--aff408c),.55)); }

.ct-ico .i-box  { fill: rgba(var(--aef4d8f),.04); stroke: rgba(var(--aef4d8f),.86); stroke-width: 1.8;
                  filter: drop-shadow(0 0 4px rgba(var(--aef4d8f),.38));
                  transition: stroke .18s ease; }
.ct-ico .i-mark { fill: none; stroke: rgba(var(--afb7bad),.88); stroke-width: 2.2;
                  stroke-linecap: round; stroke-linejoin: round;
                  transition: stroke .18s ease; }
.ct-ico .i-dot  { fill: rgba(var(--afb7bad),.88); stroke: none; transition: fill .18s ease; }

.ct-k { position: absolute; left: 86.6px; top: 24.1px;
        font-size: 12px; line-height: 1; letter-spacing: .12em; color: #7d8699;
        transition: color .18s ease; }
.ct-v { position: absolute; left: 86.6px; top: 46.9px;
        font-size: 20px; line-height: 1; letter-spacing: .02em; color: #eef3fb;
        text-shadow: 0 0 18px rgba(190,212,255,.22); }
/* right-aligned, so the trailing letter-space has to come out of the offset or
   the run sits 1.4px left of where the render puts it */
.ct-a { position: absolute; right: 55.4px; top: 42.9px; text-align: right;
        font-size: 12px; line-height: 1; letter-spacing: .12em; color: rgb(var(--af082a5));
        text-shadow: 0 0 9px rgba(var(--aef4d8f),.35); transition: color .18s ease; }
.ct-a em { font-style: normal; }
.ct-a .a-copy { display: none; }
.ct-row.copied .a-rest { display: none; }
.ct-row.copied .a-copy { display: inline; }
.ct-row.copied .ct-a { color: rgb(var(--affd6e6)); }

.ct-arw { position: absolute; left: 670.4px; top: 42.2px; width: 13.5px; height: 10px;
          overflow: visible; pointer-events: none;
          transition: transform .18s ease; }
.ct-arw path { fill: none; stroke: rgb(var(--af082a5)); stroke-width: 1.5;
               stroke-linecap: round; stroke-linejoin: round;
               transition: stroke .18s ease; }

.ct-row:hover .f-cas,   .ct-row:focus-visible .f-cas   { opacity: 1; }
.ct-row:hover .f-face,  .ct-row:focus-visible .f-face  { stroke: rgba(255,240,246,.85); }
.ct-row:hover .i-box,   .ct-row:focus-visible .i-box   { stroke: rgb(var(--aff6ba4)); }
.ct-row:hover .i-mark,  .ct-row:focus-visible .i-mark  { stroke: rgb(var(--affc3d9)); }
.ct-row:hover .i-dot,   .ct-row:focus-visible .i-dot   { fill: rgb(var(--affc3d9)); }
.ct-row:hover .ct-k,    .ct-row:focus-visible .ct-k    { color: #b6c0d3; }
.ct-row:hover .ct-a,    .ct-row:focus-visible .ct-a    { color: #fff; text-shadow: 0 0 12px rgba(var(--aff78af),.9); }
.ct-row:hover .ct-arw,  .ct-row:focus-visible .ct-arw  { transform: translateX(5px); }
.ct-row:hover .ct-arw path, .ct-row:focus-visible .ct-arw path { stroke: #fff; }

.ct-row:active .f-cas { stroke: rgba(var(--aff3e8a),.95); stroke-width: 4.4;
                        filter: blur(1.6px)
                                drop-shadow(0 0 7px rgba(var(--aff3c8a),1))
                                drop-shadow(0 0 22px rgba(var(--aff3280),.7)); }
.ct-row:active .f-face { stroke: rgb(var(--aff5c9e)); stroke-width: 1.6; }
.ct-row:active .ct-a { color: rgb(var(--aff6ba0)); }
.ct-row:active .ct-arw path { stroke: rgb(var(--aff6ba0)); }

.ct-live { position: absolute; left: 49.6px; top: 477.2px; width: 10px; height: 10px;
           border-radius: 50%; background: var(--pink);
           box-shadow: 0 0 8px rgba(var(--aef4d8f),.85);
           animation: ct-pulse 2.8s ease-in-out infinite; }
.ct-stat { position: absolute; left: 75px; top: 477.6px;
           font-size: 12px; line-height: 1; letter-spacing: .12em; color: #8d97ab; }
.ct-stat em { font-style: normal; color: #5f6a80; }
@keyframes ct-pulse {
  0%, 100% { opacity: 1;   box-shadow: 0 0 8px rgba(var(--aef4d8f),.85); }
  50%      { opacity: .55; box-shadow: 0 0 4px rgba(var(--aef4d8f),.45); }
}

/* intro: replays on every open, because open() drops .on and reflows first */
@keyframes mod-in { from { opacity: 0; transform: translateY(7px); } }
.mod[data-mod="contacts"].on .ct-h     { animation: mod-in .44s ease both; }
.mod[data-mod="contacts"].on .ct-intro { animation: mod-in .44s ease .06s both; }
.mod[data-mod="contacts"].on #ctMail   { animation: mod-in .46s ease .14s both; }
.mod[data-mod="contacts"].on #ctLink   { animation: mod-in .46s ease .22s both; }
.mod[data-mod="contacts"].on .ct-art,
.mod[data-mod="contacts"].on .ct-live,
.mod[data-mod="contacts"].on .ct-stat  { animation: mod-in .44s ease .32s both; }

/* ---------------- github module ------------------------------------------ */
/* NOTE: no overflow:visible here, unlike .rs-art. The beam and the orbit nodes
   travel past the content box on purpose and have to clip to the panel. */
.gh-art { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }

.gh-arcs path { fill: none; }
.o-arc     { stroke: rgba(150,170,215,.28); stroke-width: 1.1; }
.o-arc.dot { stroke: rgba(150,170,215,.34); stroke-dasharray: 2 7; }
.o-arc.mid { stroke: rgba(160,180,225,.40); }
.o-arc.hi  { stroke: rgba(196,178,220,.60); stroke-width: 1.2; }

/* Each node sweeps the drawn span of the outer arc once per cycle and fades at
   both ends. A plain 360deg spin would send them straight through the name and
   the button, and a hard clip would make them pop in mid-air. */
.o-node { transform-box: view-box; transform-origin: 258px 260px;
          animation: gh-orbit 40s linear infinite; }
.o-node circle { fill: rgb(var(--aff5c9e)); filter: drop-shadow(0 0 5px rgba(var(--aff4e8f),.8)); }
@keyframes gh-orbit {
  0%   { transform: rotate(-157.2deg); opacity: 0; }
  9%   { opacity: 1; }
  91%  { opacity: 1; }
  100% { transform: rotate(-22.8deg); opacity: 0; }
}

.gh-trace path { fill: none; stroke: rgba(150,172,215,.30); stroke-width: 1; }
.t-node circle { fill: rgb(var(--aff4e8f)); filter: drop-shadow(0 0 4px rgba(var(--aff4e8f),.7)); }
.t-open { fill: none; stroke: rgba(178,198,238,.5); stroke-width: 1.2; }

.gh-brk path { fill: none; }
.k-out { stroke: rgba(178,198,238,.42); stroke-width: 1.5; }
.k-pk  { stroke: var(--pink); stroke-width: 2.1;
         filter: drop-shadow(0 0 4px rgba(var(--aef4d8f),.7));
         animation: gh-brk 3.4s ease-in-out infinite; }
/* staggered by corner, so the four read as one travelling pulse */
.gh-brk path:nth-child(2) { animation-delay: 0s; }
.gh-brk path:nth-child(4) { animation-delay: .85s; }
.gh-brk path:nth-child(8) { animation-delay: 1.7s; }
.gh-brk path:nth-child(6) { animation-delay: 2.55s; }
@keyframes gh-brk {
  0%, 100% { opacity: .5;  filter: drop-shadow(0 0 3px rgba(var(--aef4d8f),.35)); }
  50%      { opacity: 1;   filter: drop-shadow(0 0 7px rgba(var(--aff5aa0),.95)); }
}

.gh-rail path { fill: none; stroke-linejoin: round; }
.r-brk { stroke: rgba(178,198,238,.5); stroke-width: 1.5; }
.r-seg { stroke: rgba(150,180,225,.44); stroke-width: 1; }

.gh-sig { animation: gh-sig 6.4s linear infinite; }
.s-dot  { fill: rgb(var(--aff4e8f));
          filter: drop-shadow(0 0 6px rgba(var(--aff4e8f),.95))
                  drop-shadow(0 0 16px rgba(var(--aff408c),.55)); }
@keyframes gh-sig {
  0%   { transform: translateX(200px); opacity: 0; }
  7%   { opacity: 1; }
  93%  { opacity: 1; }
  100% { transform: translateX(612px); opacity: 0; }
}

/* the beam sweeps, rests, then sweeps again: a continuous wiper reads as a
   loading bar rather than a scan */
.gh-beam { animation: gh-beam 9.5s ease-in-out infinite; }
@keyframes gh-beam {
  0%       { transform: translateX(150px); opacity: 0; }
  6%       { opacity: .85; }
  58%      { transform: translateX(700px); opacity: .85; }
  66%      { transform: translateX(724px); opacity: 0; }
  100%     { transform: translateX(150px); opacity: 0; }
}

.gh-h      { position: absolute; left: 189px; top: 149.7px;
             font-size: 11px; line-height: 1; letter-spacing: .125em; color: #8f9ab0; }
.gh-name   { position: absolute; left: 190px; top: 185.8px;
             font-size: 29px; line-height: 1; letter-spacing: .022em; color: #f3f7fd;
             text-shadow: 0 0 22px rgba(190,212,255,.25); }
.gh-handle { position: absolute; left: 190px; top: 229.5px;
             font-size: 17px; line-height: 1; letter-spacing: .015em; color: #9aa6bd; }
.gh-cur    { display: inline-block; width: 10px; height: 19.6px; margin-left: 8.4px;
             vertical-align: -4.5px; background: rgb(var(--aff4e8f));
             box-shadow: 0 0 8px rgba(var(--aff4e8f),.75);
             animation: gh-cur 1.06s steps(1) infinite; }
@keyframes gh-cur { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
.gh-blurb  { position: absolute; left: 190px; top: 315.5px; margin: 0;
             font-size: 13.35px; line-height: 1; letter-spacing: 0; color: #b8c2d4; }

/* ---- the action button: outer neon rail, inner chamfered plate ---------- */
.gh-btn { position: absolute; left: 189.3px; top: 375px; width: 370.8px; height: 84px;
          display: block; text-decoration: none; cursor: pointer;
          -webkit-tap-highlight-color: transparent; }
.gh-btn-art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.gh-btn-art rect, .gh-btn-art path { fill: none; stroke: none; vector-effect: non-scaling-stroke; }

.b-base .g-face { stroke: rgba(var(--af45a91),.85); stroke-width: 1.5; }
.b-base .g-in   { stroke: rgba(178,198,238,.28); stroke-width: 1.2; }
.b-base .g-tk   { stroke: rgba(178,198,238,.38); stroke-width: 1.2; }
.gh-btn-art .g-tri { fill: rgb(var(--af9639f)); stroke: none; }

.b-hot { opacity: 0; transition: opacity .17s ease; }
.b-hot .g-fill { fill: rgba(var(--aef4d8f),.10); }
.b-hot .g-cas  { stroke: rgba(var(--aff4e92),.85); stroke-width: 4.2;
                 filter: blur(1.5px)
                         drop-shadow(0 0 6px rgba(var(--aff5498),.95))
                         drop-shadow(0 0 18px rgba(var(--aff408c),.75))
                         drop-shadow(0 0 36px rgba(var(--aff3682),.42)); }
.b-hot .g-face { stroke: rgba(255,240,246,.92); stroke-width: 1.6; }
.b-hot .g-in   { stroke: rgba(var(--affbed7),.45); stroke-width: 1.2; }
.b-hot .g-tk   { stroke: #fff; stroke-width: 1.4; }
.b-hot .g-tri  { fill: rgb(var(--aff9dc4)); }

.gh-btn-r { position: absolute; inset: 0; display: flex; align-items: center;
            justify-content: center; gap: 17.5px; padding-top: 3.7px; }
/* the trailing letter-space would push the run left of centre */
.gh-btn-t { padding-left: .128em; font-size: 16.5px; letter-spacing: .128em;
            color: #eef3fb; text-shadow: 0 0 10px rgba(200,220,255,.30);
            transition: color .17s ease, text-shadow .17s ease; }
/* the cap centre of the label sits above its line-box centre, so the glyph has
   to come up to meet it or the pair reads as tilted */
.gh-btn-w { width: 25px; height: 20px; overflow: visible; margin-bottom: 3.2px;
            transition: transform .17s ease; }
.gh-btn-w path { fill: none; stroke: rgb(var(--aff5c9e)); stroke-width: 2.1;
                 stroke-linecap: round; stroke-linejoin: round;
                 filter: drop-shadow(0 0 6px rgba(var(--aff4e8f),.55));
                 transition: stroke .17s ease; }

.gh-btn:hover .b-hot, .gh-btn:focus-visible .b-hot { opacity: 1; }
.gh-btn:hover .gh-btn-t, .gh-btn:focus-visible .gh-btn-t {
  color: #fff; text-shadow: 0 0 12px rgba(var(--aff78af),.9); }
.gh-btn:hover .gh-btn-w, .gh-btn:focus-visible .gh-btn-w { transform: translateX(6px); }
.gh-btn:hover .gh-btn-w path, .gh-btn:focus-visible .gh-btn-w path { stroke: #fff; }

.gh-btn:active .b-hot { opacity: 1; }
.gh-btn:active .b-hot .g-fill { fill: rgba(var(--aef4d8f),.08); }
.gh-btn:active .b-hot .g-cas {
  stroke: rgba(var(--aff3e8a),.95); stroke-width: 5.4;
  filter: blur(1.8px)
          drop-shadow(0 0 7px rgba(var(--aff3c8a),1))
          drop-shadow(0 0 22px rgba(var(--aff3280),.8))
          drop-shadow(0 0 42px rgba(var(--aff2c78),.45)); }
.gh-btn:active .b-hot .g-face { stroke: rgb(var(--aff5c9e)); stroke-width: 2.1; }
.gh-btn:active .b-hot .g-tk { stroke: rgb(var(--aff9dc4)); stroke-width: 1.6; }
.gh-btn:active .gh-btn-t { color: rgb(var(--aff6ba0)); text-shadow: 0 0 13px rgba(var(--aff5096),.85); }
.gh-btn:active .gh-btn-w path { stroke: rgb(var(--aff6ba0)); }

.mod[data-mod="github"].on .gh-h      { animation: mod-in .44s ease .04s both; }
.mod[data-mod="github"].on .gh-name   { animation: mod-in .48s ease .10s both; }
.mod[data-mod="github"].on .gh-handle { animation: mod-in .48s ease .18s both; }
.mod[data-mod="github"].on .gh-blurb  { animation: mod-in .44s ease .32s both; }
.mod[data-mod="github"].on .gh-btn    { animation: mod-in .48s ease .40s both; }
.mod[data-mod="github"].on .gh-rail   { animation: gh-fade .5s ease .24s both; }
.mod[data-mod="github"].on .gh-arcs,
.mod[data-mod="github"].on .gh-trace,
.mod[data-mod="github"].on .gh-brk    { animation: gh-fade .8s ease both; }
@keyframes gh-fade { from { opacity: 0; } }

/* ---------------- status module ------------------------------------------
   Panel-local 804 x 556, content box x48..756, same as every other module.
   Two columns: the timeline runs x48..356, the comms channel x412..756, split
   by the dashed gutter at x384 -- the same gutter grammar as `projects`, with
   the wider half on the right because the comms lines are 41 characters and the
   step labels are 24.

   The render (`status_ui.png`) draws its content box at x42..762 in a panel of
   the same aspect. Proportions are taken from it; the outer grid is not. Same
   rule as `projects`: match the render's shape, keep the house margins. */
.st-art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
          pointer-events: none; }
.st-art path { fill: none; }
.st-rule { stroke: rgba(168,186,224,.13); stroke-width: 1; }
.st-div  { stroke: rgba(168,186,224,.17); stroke-width: 1; stroke-dasharray: 2 6; }

/* ---- the header block ---------------------------------------------------- */
.st-kick    { position: absolute; left: 48px; top: 48px;
              font-size: 11px; line-height: 1; letter-spacing: .17em; color: #9fa9bf; }
.st-mission { position: absolute; left: 48px; top: 67.5px; margin: 0; font-weight: 400;
              font-size: 23px; line-height: 1; letter-spacing: .095em; color: #f2f6fd;
              text-shadow: 0 0 18px rgba(190,212,255,.24); white-space: nowrap; }
.st-state   { position: absolute; right: 71px; top: 72px;
              font-size: 15px; line-height: 1; letter-spacing: .09em; color: #dbe3f0;
              white-space: nowrap; }
/* the dot's centre sits on the mission title's CAP centre, not on its ink-box
   centre -- the same rule the resume status rings are aligned by */
.st-dot     { position: absolute; left: 746px; top: 75.5px; width: 10px; height: 10px;
              border-radius: 50%; background: var(--pink);
              box-shadow: 0 0 11px rgba(var(--aef4d8f),.85); }

.st-plab    { position: absolute; left: 48px; top: 110.5px;
              font-size: 12.7px; line-height: 1; letter-spacing: .105em; color: #aab4c8; }
.st-plab em { font-style: normal; color: #5f6a80; }
.st-plab b  { font-weight: 400; color: #e9eff8; font-variant-numeric: tabular-nums; }

/* chamfered on the bottom corners only -- the resume button's 4/14 asymmetry,
   scaled to a 19px chip. A symmetric pill would read as a browser control. */
.st-flag {
  position: absolute; right: 48px; top: 108px; height: 19px; padding: 0 9px;
  display: flex; align-items: center; gap: 6px;
  font-size: 9.2px; line-height: 1; letter-spacing: .12em; color: rgb(var(--aff8cba));
  border: 1px solid rgba(var(--aff4e8f),.45); background: rgba(var(--aef4d8f),.07);
  box-shadow: 0 0 12px rgba(var(--aef4d8f),.16);
  clip-path: polygon(0 0,100% 0,100% calc(100% - 7px),calc(100% - 7px) 100%,7px 100%,0 calc(100% - 7px));
  white-space: nowrap;
}
.st-warn { width: 10px; height: 9.2px; overflow: visible; flex: none; }
.st-warn path { fill: none; stroke: rgb(var(--aff6ba3)); stroke-width: 1.2; stroke-linecap: round;
                stroke-linejoin: round; }

/* ---- the mission bar ----------------------------------------------------
   57 flexed segments rather than 57 pitched ones: the lit count is a percentage
   of the whole, so the bar has to land exactly on x756 whatever the division
   rounds to. Gap 4.7 against a ~7.8 segment is the render's ratio. */
.st-bar { position: absolute; left: 48px; top: 138px; width: 708px; height: 15px;
          display: flex; gap: 4.7px; }
.st-bar i { flex: 1; background: rgba(150,170,215,.13); border-radius: .5px;
            transition: background .45s ease, box-shadow .45s ease; }
.st-bar i.lit { background: rgb(var(--aff4e8f)); box-shadow: 0 0 7px rgba(var(--aff4e8f),.55); }

.st-h { position: absolute; top: 189px;
        font-size: 11px; line-height: 1; letter-spacing: .17em; color: #9fa9bf; }

/* ---- the operation timeline ---------------------------------------------
   Six steps at a 38.6 pitch, boxes 270 x 29.8. The rail rings follow the resume
   rule: the ring's OUTER edge sits on the column's left grid line (x48), so the
   centre is at 57, and the ring's centre is on the label's CAP centre, not on
   its ink box. */
.st-rail { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
           pointer-events: none; }
.st-rail path, .st-rail circle { fill: none; }
.st-rail .r-line { stroke: rgba(var(--aef4d8f),.30); stroke-width: 1.4; }
.st-rail .r-line.pend { stroke: rgba(168,186,224,.22); stroke-dasharray: 2 4; }
.st-rail .r-arm  { stroke: rgba(168,186,224,.24); stroke-width: 1; }
.st-rail .r-ring { stroke: var(--pink); stroke-width: 1.9;
                   filter: drop-shadow(0 0 5px rgba(var(--aef4d8f),.6)); }
.st-rail .r-ring.pend { stroke: rgba(168,186,224,.42); filter: none; }
.st-rail .r-dot  { fill: rgb(var(--aff6ba3)); stroke: none; }
/* the live node is a filled disc with its own bloom, so it reads as the one
   thing moving in a column of six identical rings */
.st-rail .r-live { fill: rgb(var(--aff5c9c)); stroke: none;
                   filter: drop-shadow(0 0 9px rgba(var(--aff4e8f),.9)); }

.st-steps { position: absolute; left: 86px; top: 215.6px; width: 270px; height: 223px; }
.st-step  { position: absolute; left: 0; width: 270px; height: 29.8px;
            display: flex; align-items: center; justify-content: space-between;
            gap: 10px; padding: 0 12px; }
.st-frame { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;
            overflow: visible; }
.st-frame path { fill: none; stroke: none; }
.st-frame .f-fill { fill: rgba(150,175,235,.012); }
.st-frame .f-face { stroke: rgba(178,198,238,.26); stroke-width: 1.1; }
.st-step.live .f-fill { fill: rgba(var(--aef4d8f),.045); }
.st-step.live .f-cas  { stroke: rgba(var(--aff4e8f),.28); stroke-width: 5; filter: blur(3.5px); }
.st-step.live .f-face { stroke: rgb(var(--aff4e8f)); stroke-width: 1.35;
                        filter: drop-shadow(0 0 7px rgba(var(--aff4e8f),.7)); }
.st-sl { flex: 1; font-size: 12px; line-height: 1; letter-spacing: .022em; color: #cfd8e7;
         white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.st-sv { flex: none; font-size: 9.6px; line-height: 1; letter-spacing: .12em; color: #79839a; }
.st-step.live .st-sl { color: #f2f6fd; }
.st-step.live .st-sv { color: rgb(var(--aff7cae)); text-shadow: 0 0 9px rgba(var(--aef4d8f),.5); }

/* ---- the agent comms channel --------------------------------------------
   The render shows five static lines. What a transcript cannot show is WHO is
   talking to WHOM, which is the whole joke, so the box gains a bus strip: five
   pips on a rail, and a packet that physically travels from sender to recipient
   before the line lands in the log. The strip costs 40px of a 156px box. */
.st-comms { position: absolute; left: 412px; top: 209px; width: 344px; height: 156px; }
.st-cart  { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.st-cart path, .st-cart circle { fill: none; }
.c-fill { fill: rgba(150,175,235,.014); stroke: none; }
.c-face { stroke: rgba(178,198,238,.28); stroke-width: 1.1; }
.c-hair { stroke: rgba(168,186,224,.13); stroke-width: 1; }
.c-bus  { stroke: rgba(168,186,224,.20); stroke-width: 1; }
.c-pip       { fill: rgba(12,16,26,.9); stroke: rgba(178,198,238,.40); stroke-width: 1.3;
               transition: stroke .2s ease, fill .2s ease; }
/* the pip, the tag and the packet all take the SENDER's hue, which is what
   makes the bus readable: the colour in flight says where the line came from
   before the line arrives. */
.c-pip.hot   { stroke: rgb(var(--ag)); fill: rgba(var(--ag),.22);
               filter: drop-shadow(0 0 8px rgba(var(--ag),.9)); }
.c-pkt       { fill: rgb(var(--ag)); stroke: none; opacity: 0;
               filter: drop-shadow(0 0 7px rgba(var(--ag),.9)); }

.st-tags { position: absolute; inset: 0; pointer-events: none; }
/* the tags carry their agent's hue at rest, dimmed: they are the legend that
   makes a coloured pip and a coloured packet mean something. Hot is the same
   hue at full strength, so lighting up reads as the same agent, louder. */
.st-tag  { position: absolute; top: 28px; transform: translateX(-50%);
           font-size: 7.6px; line-height: 1; letter-spacing: .1em;
           color: rgba(var(--ag),.72);
           white-space: nowrap; transition: color .2s ease, text-shadow .2s ease; }
.st-tag.hot { color: rgb(var(--ag)); text-shadow: 0 0 9px rgba(var(--ag),.7); }

/* five lines, oldest fading out at the top: the same convention as the AGENT
   FEED panel, so a reader who has watched the feed already knows this scrolls */
/* 44..147 in a 156px box: 9px of clearance under the last line, which is what
   keeps it off the bottom-left chamfer (x0..13 from y143). */
.st-log { position: absolute; left: 12px; top: 44px; width: 320px; height: 103px;
          overflow: hidden;
          -webkit-mask-image: linear-gradient(180deg,transparent 0,#000 10px);
          mask-image: linear-gradient(180deg,transparent 0,#000 10px); }
/* 11.2/.015em is a budget, not a taste: the sender column is padded to
   RESEARCHER (10) plus " > ", so the longest line is 44 characters and 320px of
   log holds 46 at this advance. A message longer than 33 characters clips. */
.st-line { font-size: 11.2px; line-height: 20.6px; white-space: nowrap; color: #b9c3d6;
           letter-spacing: .015em; }
/* the sender column is space-padded to the longest name so the `>` lines up;
   `nowrap` alone still collapses those runs, `pre` is what keeps them */
.st-line .who, .st-line .arw { white-space: pre; }
.st-line .who { color: rgb(var(--ag)); }
.st-line .arw { color: #5f6a80; }
.st-line.new { animation: st-slide .34s cubic-bezier(.22,.9,.28,1) both; }
@keyframes st-slide { from { opacity: 0; transform: translateY(7px); } }

/* ---- consensus ----------------------------------------------------------- */
.st-clab    { position: absolute; left: 412px; top: 371px;
              font-size: 17px; line-height: 1; letter-spacing: .05em; color: #e9eff8; }
.st-clab em { font-style: normal; color: #5f6a80; }
.st-clab b  { font-weight: 400; color: rgb(var(--aff7cae)); font-variant-numeric: tabular-nums;
              text-shadow: 0 0 12px rgba(var(--aef4d8f),.45); }

/* This one IS live-scrolling, unlike the `about` sparklines. The rule there was
   that a moving line under "YEARS IN QUALITY 15+" would claim the number is
   changing; here the number IS changing, several times a minute, and a frozen
   trace under a consensus figure that moves would be the lie. */
.st-spark { position: absolute; left: 412px; top: 394px; width: 344px; height: 22px;
            overflow: visible; }
.st-spark .s-ln { fill: none; stroke: rgb(var(--aef4f8e)); stroke-width: 1.1; stroke-linejoin: round;
                  filter: drop-shadow(0 0 3px rgba(var(--aef4f8e),.8)); }
.st-spark .s-bk { fill: rgb(var(--aff5c9c)); stroke: none; }

/* ---- the button ---------------------------------------------------------
   Two stacked copies of one geometry cross-faded on opacity, the same
   construction as the resume and github buttons: fill never filtered, casing
   carries the bloom, face carries the hard edge. */
.st-btn { position: absolute; left: 470px; top: 431px; width: 228px; height: 37px;
          background: none; border: 0; padding: 0; margin: 0; font: inherit;
          cursor: pointer; -webkit-tap-highlight-color: transparent; }
.st-btn-art { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.st-btn-art path { fill: none; stroke: none; }
.st-btn-art .b-fill { fill: rgba(var(--aef4d8f),.05); }
.st-btn .b-base .b-face { stroke: rgba(var(--aff4e8f),.55); stroke-width: 1.15; }
.st-btn .b-hot { opacity: 0; transition: opacity .2s ease; }
.st-btn .b-hot .b-fill { fill: rgba(var(--aef4d8f),.12); }
.st-btn .b-hot .b-cas  { stroke: rgba(var(--aff4e8f),.5); stroke-width: 5; filter: blur(3.5px); }
.st-btn .b-hot .b-face { stroke: rgb(var(--aff5c9c)); stroke-width: 1.5;
                         filter: drop-shadow(0 0 8px rgba(var(--aff4e8f),.8)); }
.st-btn:hover .b-hot, .st-btn:focus-visible .b-hot, .st-btn:active .b-hot { opacity: 1; }
.st-btn:focus-visible { outline: none; }

.st-btn-r { position: absolute; inset: 0; display: flex; align-items: center;
            justify-content: center; gap: 13px; }
.st-btn-t { font-size: 12.5px; line-height: 1; letter-spacing: .128em; color: rgb(var(--affa6c9));
            transition: color .2s ease, text-shadow .2s ease; white-space: nowrap; }
.st-btn:hover .st-btn-t, .st-btn:focus-visible .st-btn-t {
  color: #fff; text-shadow: 0 0 12px rgba(var(--aff78af),.75); }
.st-btn-w { width: 19px; height: 15.2px; overflow: visible; flex: none;
            transition: transform .2s ease; }
.st-btn-w path { fill: none; stroke: rgb(var(--aff7cae)); stroke-width: 1.5; stroke-linecap: round;
                 stroke-linejoin: round; transition: stroke .2s ease; }
.st-btn:hover .st-btn-w { transform: translateX(3px); }
.st-btn:hover .st-btn-w path { stroke: #fff; }
/* while a round is running the button is inert but still legible: dimming the
   label alone would leave a bright neon frame around dead type */
.st-btn.busy { cursor: default; }
.st-btn.busy .st-btn-t { color: rgb(var(--aa9718b)); }
.st-btn.busy .b-base .b-face { stroke: rgba(var(--aff4e8f),.26); }
.st-btn.busy .b-hot { opacity: 0 !important; }
.st-btn.busy .st-btn-w path { stroke: rgb(var(--aa9718b)); }

.st-foot    { position: absolute; left: 48px; top: 496px; display: flex; align-items: center;
              font-size: 12.3px; line-height: 1; letter-spacing: .146em; color: #7d8699;
              white-space: nowrap; }
.st-foot em { font-style: normal; color: rgb(var(--aef4d8f)); margin: 0 2px; }
.st-foot .caret { margin-left: 9px; }

/* ---- motion -------------------------------------------------------------
   Same grammar as the other five modules: stagger in once on open, then hold.
   The only things that keep moving afterwards are the consensus trace and the
   bus, and both are driven from JS rather than from a CSS loop. */
.mod[data-mod="status"].on .st-art     { animation: gh-fade .7s ease .1s both; }
.mod[data-mod="status"].on .st-kick    { animation: mod-in .44s ease both; }
.mod[data-mod="status"].on .st-mission { animation: mod-in .5s  ease .05s both; }
.mod[data-mod="status"].on .st-state,
.mod[data-mod="status"].on .st-dot     { animation: mod-in .44s ease .14s both; }
.mod[data-mod="status"].on .st-plab    { animation: mod-in .44s ease .18s both; }
.mod[data-mod="status"].on .st-flag    { animation: mod-in .44s ease .26s both; }
.mod[data-mod="status"].on .st-bar     { animation: mod-in .44s ease .22s both; }
.mod[data-mod="status"].on .st-h       { animation: mod-in .44s ease .28s both; }
.mod[data-mod="status"].on .st-rail    { animation: gh-fade .8s ease .32s both; }
.mod[data-mod="status"].on .st-step    { animation: mod-in .42s ease var(--d) both; }
.mod[data-mod="status"].on .st-comms   { animation: mod-in .48s ease .36s both; }
.mod[data-mod="status"].on .st-clab    { animation: mod-in .44s ease .46s both; }
.mod[data-mod="status"].on .st-spark   { animation: gh-fade .6s ease .52s both; }
.mod[data-mod="status"].on .st-btn     { animation: mod-in .48s ease .56s both; }
.mod[data-mod="status"].on .st-foot    { animation: mod-in .44s ease .62s both; }

/* ---------------- theme module -------------------------------------------
   Panel-local 804 x 556, content box x48..756.

   `theme_ui.png` draws its rows 17px from the panel edge and its heading column
   15px further in again, so the render's outer text sits on the card's NUMERAL
   column rather than on the card's edge. The house grid wins, as in `projects`:
   rows span the content box x48..756 and the heading, intro and footer sit on x48
   with it. Losing that one internal alignment is cheaper than moving this module's
   kicker off the line every other module's kicker sits on -- they share one panel
   and the reader switches between them.

   Row internals keep the render's proportions: numeral at 68.5, name column at
   118.7, three 50px swatches on a 59.4 pitch from 325.7, sparkline 89 wide at
   521.5, status label right-aligned to 688, radio centred on 725.7.

   The rows are a FIXED 708 x 74.6 on an 86.5 pitch, so the chamfered frame is a
   build-time constant and nothing in this module measures the DOM -- the same
   choice `status` made, and for the same reason: it keeps the module clear of the
   zero-viewport / draw-at-load bug class that cost `projects` a round. */
.th-art  { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
           pointer-events: none; }
.th-rule { fill: none; stroke: rgba(168,186,224,.13); stroke-width: 1; }

.th-kick  { position: absolute; left: 48px; top: 62.9px;
            font-size: 11.5px; line-height: 1; letter-spacing: .17em; color: #9fa9bf; }
.th-intro { position: absolute; left: 48px; top: 88.3px; margin: 0; width: 708px;
            font-size: 12.2px; line-height: 1; letter-spacing: 0; color: #aab4c8; }

.th-list { position: absolute; left: 48px; top: 121.4px; width: 708px; height: 334.2px; }

/* ---- a profile row ------------------------------------------------------
   All four corners are chamfered, which is NOT the house r-top / chamfer-bottom
   asymmetry used on buttons and step boxes. It is what the render draws, and the
   symmetry earns its place here: this row is a two-ended object -- an index at
   one end, a radio at the other -- and a top-heavy silhouette would point at one
   of them. Frame states are the resume/contacts cross-fade: `.f-fill` is never
   filtered, `.f-cas` carries the bloom, `.f-face` the hard edge. */
.th-row { position: absolute; left: 0; width: 708px; height: 74.6px; padding: 0;
          border: 0; background: none; font: inherit; text-align: left;
          cursor: pointer; -webkit-tap-highlight-color: transparent; }
.th-fr  { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
          pointer-events: none; }
.th-fr path { fill: none; stroke: none; }
.th-fr .f-fill { fill: rgba(150,175,235,.012); transition: fill .2s ease; }
.th-fr .f-face { stroke: rgba(178,198,238,.30); stroke-width: 1.2;
                 transition: stroke .2s ease; }
/* much lighter than the contacts row's casing: that one is 708 x 92 and lights up
   for a moment under the cursor, this one is 708 x 74.6 and stays lit for as long
   as the profile is selected. At contacts' weight the selected row read as a lamp
   rather than as a selection. */
.th-fr .f-cas  { stroke: rgba(var(--aff4e92),.5); stroke-width: 1.6; opacity: 0;
                 transition: opacity .2s ease;
                 filter: blur(.9px) drop-shadow(0 0 6px rgba(var(--aff4a8e),.5)); }

.th-n  { position: absolute; left: 20.5px; top: 0; bottom: 0; display: flex;
         align-items: center; font-family: var(--disp); font-weight: 300;
         font-size: 29.5px; line-height: 1; letter-spacing: .02em; color: #7d8699;
         font-variant-numeric: tabular-nums; transition: color .2s ease; }
.th-nm { position: absolute; left: 70.7px; top: 20.7px;
         font-size: 15px; line-height: 1; letter-spacing: .085em; color: #dbe3f0;
         transition: color .2s ease, text-shadow .2s ease; }
.th-sb { position: absolute; left: 70.7px; top: 42.6px;
         font-size: 11.6px; line-height: 1; letter-spacing: 0; color: #79839a;
         transition: color .2s ease; }

/* the swatches are the profile's own three colours, stated literally: they are
   the specification, not a sample of whatever accent is currently active */
.th-sw { position: absolute; top: 17.3px; width: 50px; height: 40px; border-radius: 3px;
         background: var(--c); }

/* each row's trace runs in its OWN profile colour, so the four read as four
   signals rather than four labels. Draw once on open, then hold -- the site's
   motion grammar; a live-scrolling trace here would claim something is measured. */
.th-sp { position: absolute; left: 473.5px; top: 26.3px; width: 89px; height: 22px;
         overflow: visible; }
.th-sp path { fill: none; stroke: var(--c); stroke-width: 1.5;
              stroke-linecap: round; stroke-linejoin: round;
              filter: drop-shadow(0 0 5px var(--g)); }

.th-st { position: absolute; right: 68px; top: 0; bottom: 0; display: flex;
         align-items: center; font-size: 11.5px; line-height: 1; letter-spacing: .05em;
         color: #7c869c; transition: color .2s ease, text-shadow .2s ease; }
.th-rd { position: absolute; right: 20.3px; top: 0; bottom: 0; margin: auto 0;
         width: 20.4px; height: 20.4px; overflow: visible; }
.th-rd circle { fill: none; }
.th-rd .r-out { stroke: rgba(178,198,238,.42); stroke-width: 1.3;
                transition: stroke .2s ease; }
.th-rd .r-in  { stroke: none; fill: var(--pink); opacity: 0;
                transition: opacity .2s ease; }

.th-row:hover .f-face, .th-row:focus-visible .f-face { stroke: rgba(210,226,255,.6); }
.th-row:hover .f-fill, .th-row:focus-visible .f-fill { fill: rgba(150,175,235,.035); }
.th-row:hover .th-n,   .th-row:focus-visible .th-n   { color: #aab4c8; }
.th-row:hover .th-st,  .th-row:focus-visible .th-st  { color: #dbe3f0; }
.th-row:hover .r-out,  .th-row:focus-visible .r-out  { stroke: rgba(230,240,255,.8); }

/* selected: the row the reader is on is the one accent object on a panel whose
   whole subject is the accent */
.th-row.on .f-cas  { opacity: 1; }
.th-row.on .f-face { stroke: rgb(var(--aff5c9e)); stroke-width: 1.3; }
.th-row.on .f-fill { fill: rgba(var(--aef4d8f),.05); }
.th-row.on .th-n   { color: #f2f6fd; }
.th-row.on .th-nm  { color: #fff; text-shadow: 0 0 14px rgba(var(--aff78b4),.4); }
.th-row.on .th-sb  { color: #9aa4b8; }
.th-row.on .th-st  { color: rgb(var(--aff6ba0)); text-shadow: 0 0 9px rgba(var(--aef4d8f),.4); }
.th-row.on .r-out  { stroke: rgb(var(--aff5c9e)); }
.th-row.on .r-in   { opacity: 1; }
.th-row.on .th-rd  { filter: drop-shadow(0 0 6px rgba(var(--aef4d8f),.65)); }

/* ---- footer strip -------------------------------------------------------
   Every item on this line is load-bearing. SMOOTH 480MS is the real cross-fade
   duration (THEME_MS in the module script, and --th-ms below); AMBIENT SYNC
   really does decouple the background canvas from the panels; RESET DEFAULT
   really does return to Neural Pink with sync on. */
.th-foot { position: absolute; left: 48px; top: 501.4px; display: flex;
           align-items: center; gap: 44px; }
.th-lab, .th-tog { font-size: 12.3px; line-height: 1; letter-spacing: .012em;
                   color: #8d97ab; white-space: nowrap; }
.th-lab em, .th-tog em { font-style: normal; color: #5f6a80; }
.th-tog { padding: 0; border: 0; background: none; font-family: var(--mono);
          cursor: pointer; transition: color .18s ease; }
.th-tog b { font-weight: 400; color: #dbe3f0; transition: color .18s ease; }
.th-tog:hover, .th-tog:focus-visible { color: #dbe3f0; }
.th-tog:hover b, .th-tog:focus-visible b { color: #fff; }
.th-sep { width: 6px; height: 6px; border-radius: 50%; background: var(--pink);
          box-shadow: 0 0 8px rgba(var(--aef4d8f),.8); flex: none; }
/* MEASURED, not assumed, and the measurement changed the answer. `test-contrast.mjs`
   never photographs this: AMBIENT SYNC defaults to ENABLED, so the harness only ever
   sees `.th-tog b`'s own colour and this value never renders. Driven into its DISABLED
   state, under all four profiles, `#6f788c` read **4.07** and failed like `#pjFocus`.

   `dbg-sync.mjs` is that measurement, written down rather than done by hand -- Phase 4
   took it by hand and Phase 5 then had no way to re-take it when the harness's own
   calibration was corrected and every tier-1 number moved. Re-measured there:
   **4.57 on rgb(16.1,21.8,36.4), identical across all four profiles.**

   That backdrop is DARKER than `#pjFocus`'s rgb(17,24,39), which is why this stayed at
   #778096 when `.pj-focus` had to move to #798298. Phase 4 lifted them together on the
   assumption that they share a backdrop; they sit 12 units apart in blue and only one
   of them needed the second step. Each takes the minimum for ITS OWN backdrop, which
   is the rule the projects block states.

   It is invisible at rest, which is exactly why it was going to be missed. */
#thSync[aria-pressed="false"] b { color: #778096; }

/* ---- motion ------------------------------------------------------------- */
.mod[data-mod="theme"].on .th-art   { animation: gh-fade .7s ease .1s both; }
.mod[data-mod="theme"].on .th-kick  { animation: mod-in .44s ease both; }
.mod[data-mod="theme"].on .th-intro { animation: mod-in .44s ease .06s both; }
.mod[data-mod="theme"].on .th-row   { animation: mod-in .46s ease var(--d) both; }
.mod[data-mod="theme"].on .th-sp path { animation: th-draw .85s ease var(--d2) both; }
.mod[data-mod="theme"].on .th-foot  { animation: mod-in .44s ease .5s both; }
@keyframes th-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }

/* the command whose module is open is marked in the terminal list */
.cmd.live { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px; }

@media (prefers-reduced-motion: reduce) {
  .caret, .live-tag i { animation: none; }
  #p-about { transition: opacity .01s linear; transform: none; }
  .b-hot, .t-hot { transition: none; }
  .rs-bars i { animation: none; }
  .mod[data-mod="resume"].on .a-tick { animation: none; stroke-dashoffset: 0; }
  .mod[data-mod="resume"].on .a-tick-box { animation: none; }
  .ct-live, .o-node, .k-pk, .gh-sig, .gh-beam, .gh-cur { animation: none; }
  .st-bar i, .c-pip, .st-tag, .st-btn .b-hot, .st-btn-t, .st-btn-w,
  .st-btn-w path, .st-frame .f-face { transition: none; }
  .gh-beam { opacity: 0; }
  /* park each node where the reference render puts it, not all three at once */
  .o-node:nth-child(1) { transform: rotate(-20deg);  opacity: 1; }
  .o-node:nth-child(2) { transform: rotate(-56.2deg); opacity: 1; }
  .o-node:nth-child(3) { transform: rotate(-139.1deg); opacity: 1; }
  .gh-sig { transform: translateX(481px); opacity: 1; }
  .k-pk { opacity: 1; }
  .ct-frame .f-cas, .ct-frame .f-face, .ct-arw, .ct-arw path, .ct-a, .ct-k,
  .ct-ico .i-box, .ct-ico .i-mark, .ct-ico .i-dot,
  .gh-btn-t, .gh-btn-w, .gh-btn-w path { transition: none; }
  /* the intro rules carry four classes each, so a `.mod.on *` reset loses to
     them on specificity and the module would sit at its 0% frame (invisible).
     This is the accessibility escape hatch: state it as such. */
  .mod.on .ct-h, .mod.on .ct-intro, .mod.on .ct-row, .mod.on .ct-art,
  .mod.on .ct-live, .mod.on .ct-stat,
  .mod.on .gh-h, .mod.on .gh-name, .mod.on .gh-handle, .mod.on .gh-blurb,
  .mod.on .gh-btn, .mod.on .gh-rail, .mod.on .gh-arcs, .mod.on .gh-trace,
  .mod.on .gh-brk,
  .mod.on .pj-h, .mod.on .pj-range, .mod.on .pj-art, .mod.on .pj-card,
  .pj-detail.in .pj-head, .pj-detail.in .pj-p, .pj-detail.in .pj-ch,
  .pj-detail.in .pj-items li, .pj-focus.in,
  .mod.on .st-art, .mod.on .st-kick, .mod.on .st-mission, .mod.on .st-state,
  .mod.on .st-plab, .mod.on .st-flag, .mod.on .st-bar, .mod.on .st-h,
  .mod.on .st-rail, .mod.on .st-step, .mod.on .st-comms, .mod.on .st-clab,
  .mod.on .st-spark, .mod.on .st-btn, .mod.on .st-foot, .st-line.new,
  .mod.on .th-art, .mod.on .th-kick, .mod.on .th-intro, .mod.on .th-row,
  .mod.on .th-foot {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  /* the trace is drawn by stroke-dashoffset, so killing the animation alone
     would leave it at its 0% frame -- fully dashed out, i.e. invisible */
  .mod.on .th-sp path { animation: none !important; stroke-dashoffset: 0 !important; }
  /* the dot's `transform: none` above would be harmless, but the tag is centred
     WITH a transform, so it has to keep it */
  .mod.on .st-dot { animation: none !important; opacity: 1 !important; }
  .st-tag { transform: translateX(-50%) !important; }
  .pj-frame .f-face, .pj-num { transition: none; }
  .mx .f { transition: none; }
  .th-fr .f-fill, .th-fr .f-face, .th-fr .f-cas, .th-n, .th-nm, .th-sb,
  .th-st, .th-rd .r-out, .th-rd .r-in, .th-tog, .th-tog b { transition: none; }
  #bgx { transition: none; display: none; }
  .mod[data-mod="about"].on .ab-stat .ln {
    animation: none !important; stroke-dashoffset: 0 !important; }
  .mod[data-mod="about"].on .ab-stat .fl { animation: none !important; opacity: .72 !important; }
  .panel .p-frame .draw { transition: none; stroke-dashoffset: 0; }
  .panel .p-chrome, .panel .p-body, .panel .p-title,
  .panel .p-foot, .panel .mk, .panel .live-tag, .panel #aboutClose { transition: none; }
}

@media print {
  .mod.on .ct-h, .mod.on .ct-intro, .mod.on .ct-row, .mod.on .ct-art,
  .mod.on .ct-live, .mod.on .ct-stat,
  .mod.on .gh-h, .mod.on .gh-name, .mod.on .gh-handle, .mod.on .gh-blurb,
  .mod.on .gh-btn, .mod.on .gh-rail, .mod.on .gh-arcs, .mod.on .gh-trace,
  .mod.on .gh-brk,
  .mod.on .pj-h, .mod.on .pj-range, .mod.on .pj-art, .mod.on .pj-card,
  .pj-detail.in .pj-head, .pj-detail.in .pj-p, .pj-detail.in .pj-ch,
  .pj-detail.in .pj-items li, .pj-focus.in,
  .mod.on .st-art, .mod.on .st-kick, .mod.on .st-mission, .mod.on .st-state,
  .mod.on .st-plab, .mod.on .st-flag, .mod.on .st-bar, .mod.on .st-h,
  .mod.on .st-rail, .mod.on .st-step, .mod.on .st-comms, .mod.on .st-clab,
  .mod.on .st-spark, .mod.on .st-btn, .mod.on .st-foot, .st-line.new,
  .mod.on .th-art, .mod.on .th-kick, .mod.on .th-intro, .mod.on .th-row,
  .mod.on .th-foot {
    animation: none !important; opacity: 1 !important; transform: none !important;
  }
  /* the trace is drawn by stroke-dashoffset, so killing the animation alone
     would leave it at its 0% frame -- fully dashed out, i.e. invisible */
  .mod.on .th-sp path { animation: none !important; stroke-dashoffset: 0 !important; }
  /* the dot's `transform: none` above would be harmless, but the tag is centred
     WITH a transform, so it has to keep it */
  .mod.on .st-dot { animation: none !important; opacity: 1 !important; }
  .st-tag { transform: translateX(-50%) !important; }
  /* PRODUCTION-SPEC.md 2.7: "prefers-reduced-motion and @media print selector
     lists must be extended for any new element with an intro animation, or the
     module renders empty." Two modules were already missing from THIS list while
     being present in the reduced-motion one, and both fail the same way: a
     stroke-dashoffset intro that is killed without being parked sits at its 0%
     frame, which is fully dashed out. `.a-tick` is the resume's integrity tick,
     `.ab-stat .ln` the six sparklines under CORE STATS. Printed, both were blank.
     `test-print.mjs` now asserts all seven modules rather than printing two. */
  .mod[data-mod="resume"].on .a-tick {
    animation: none !important; stroke-dashoffset: 0 !important; }
  .mod[data-mod="resume"].on .a-tick-box { animation: none !important; opacity: 1 !important; }
  .mod[data-mod="about"].on .ab-stat .ln {
    animation: none !important; stroke-dashoffset: 0 !important; }
  .mod[data-mod="about"].on .ab-stat .fl { animation: none !important; opacity: .72 !important; }
  /* the 47 integrity bars open at `opacity: 0` and are carried to 1 by `rs-arm`, so a
     printer that snapshots before the .42s wave has passed gets an empty strip. The
     reduced-motion block already parks them; print now does the same. */
  .rs-bars i { animation: none !important; }
}


/* ---- the static route document (PRODUCTION-SPEC.md 5) ----------------------
   Hidden by the stylesheet, which is the fail-open half of the pair: no CSS at all
   and #doc is visible, unstyled and readable, while #fit is a canvas nobody can see.
   The typography that makes it pleasant is inlined per shell instead, for the same
   reason. The <noscript> override and the 1.5 s __RN_BOOTED guard are emitted by
   prerender.mjs and live nowhere near this file.
   Inert in the single artifact, which has no #doc at all. */
#doc { display: none }

/* ---- accent profiles (generated by scripts/theme-build.mjs) ---------------- */
:root{
  --a5a2656: 90,38,86;
  --aa63668: 166,54,104;
  --aa9718b: 169,113,139;
  --aea6296: 234,98,150;
  --aef4d8f: 239,77,143;
  --aef4f8e: 239,79,142;
  --aef6ba0: 239,107,160;
  --af082a5: 240,130,165;
  --af0a7c8: 240,167,200;
  --af27ca8: 242,124,168;
  --af382ac: 243,130,172;
  --af45a91: 244,90,145;
  --af56698: 245,102,152;
  --af9639f: 249,99,159;
  --af9689f: 249,104,159;
  --afb7bad: 251,123,173;
  --aff2c78: 255,44,120;
  --aff3280: 255,50,128;
  --aff3682: 255,54,130;
  --aff3c8a: 255,60,138;
  --aff3e8a: 255,62,138;
  --aff408c: 255,64,140;
  --aff4a8e: 255,74,142;
  --aff4e8f: 255,78,143;
  --aff4e92: 255,78,146;
  --aff4e93: 255,78,147;
  --aff5096: 255,80,150;
  --aff5498: 255,84,152;
  --aff5aa0: 255,90,160;
  --aff5c9c: 255,92,156;
  --aff5c9e: 255,92,158;
  --aff5fa2: 255,95,162;
  --aff6ba0: 255,107,160;
  --aff6ba3: 255,107,163;
  --aff6ba4: 255,107,164;
  --aff78af: 255,120,175;
  --aff78b4: 255,120,180;
  --aff7cae: 255,124,174;
  --aff8cba: 255,140,186;
  --aff8cbe: 255,140,190;
  --aff9dc4: 255,157,196;
  --aff9ec8: 255,158,200;
  --affa6c9: 255,166,201;
  --affbed7: 255,190,215;
  --affc3d9: 255,195,217;
  --affd6e6: 255,214,230;
}
html[data-theme="pink"]{
  --a5a2656: 90,38,86;
  --aa63668: 166,54,104;
  --aa9718b: 169,113,139;
  --aea6296: 234,98,150;
  --aef4d8f: 239,77,143;
  --aef4f8e: 239,79,142;
  --aef6ba0: 239,107,160;
  --af082a5: 240,130,165;
  --af0a7c8: 240,167,200;
  --af27ca8: 242,124,168;
  --af382ac: 243,130,172;
  --af45a91: 244,90,145;
  --af56698: 245,102,152;
  --af9639f: 249,99,159;
  --af9689f: 249,104,159;
  --afb7bad: 251,123,173;
  --aff2c78: 255,44,120;
  --aff3280: 255,50,128;
  --aff3682: 255,54,130;
  --aff3c8a: 255,60,138;
  --aff3e8a: 255,62,138;
  --aff408c: 255,64,140;
  --aff4a8e: 255,74,142;
  --aff4e8f: 255,78,143;
  --aff4e92: 255,78,146;
  --aff4e93: 255,78,147;
  --aff5096: 255,80,150;
  --aff5498: 255,84,152;
  --aff5aa0: 255,90,160;
  --aff5c9c: 255,92,156;
  --aff5c9e: 255,92,158;
  --aff5fa2: 255,95,162;
  --aff6ba0: 255,107,160;
  --aff6ba3: 255,107,163;
  --aff6ba4: 255,107,164;
  --aff78af: 255,120,175;
  --aff78b4: 255,120,180;
  --aff7cae: 255,124,174;
  --aff8cba: 255,140,186;
  --aff8cbe: 255,140,190;
  --aff9dc4: 255,157,196;
  --aff9ec8: 255,158,200;
  --affa6c9: 255,166,201;
  --affbed7: 255,190,215;
  --affc3d9: 255,195,217;
  --affd6e6: 255,214,230;
  --sig-fn: 130,216,255;
  --sig-num: 247,178,103;
  --ag-1: 246,147,179;
  --ag-2: 231,168,85;
  --ag-3: 131,202,130;
  --ag-4: 58,201,232;
  --ag-5: 178,169,255;
}
html[data-theme="mint"]{
  --a5a2656: 36,75,55;
  --aa63668: 35,127,115;
  --aa9718b: 124,159,145;
  --aea6296: 103,194,168;
  --aef4d8f: 76,191,163;
  --aef4f8e: 78,191,164;
  --aef6ba0: 113,199,172;
  --af082a5: 136,205,186;
  --af0a7c8: 177,219,199;
  --af27ca8: 130,205,181;
  --af382ac: 137,207,184;
  --af45a91: 90,196,172;
  --af56698: 104,200,176;
  --af9639f: 102,202,173;
  --af9689f: 106,203,176;
  --afb7bad: 128,210,183;
  --aff2c78: 0,192,172;
  --aff3280: 17,194,170;
  --aff3682: 30,195,170;
  --aff3c8a: 48,197,169;
  --aff3e8a: 50,197,169;
  --aff408c: 54,198,169;
  --aff4a8e: 67,199,172;
  --aff4e8f: 71,200,174;
  --aff4e92: 73,200,172;
  --aff4e93: 73,200,172;
  --aff5096: 77,201,171;
  --aff5498: 82,202,172;
  --aff5aa0: 91,204,172;
  --aff5c9c: 91,204,175;
  --aff5c9e: 92,204,174;
  --aff5fa2: 97,205,173;
  --aff6ba0: 108,207,181;
  --aff6ba3: 109,207,180;
  --aff6ba4: 109,207,179;
  --aff78af: 125,211,183;
  --aff78b4: 126,212,180;
  --aff7cae: 128,212,186;
  --aff8cba: 146,217,192;
  --aff8cbe: 147,217,190;
  --aff9dc4: 164,222,200;
  --aff9ec8: 166,223,199;
  --affa6c9: 173,225,204;
  --affbed7: 196,232,217;
  --affc3d9: 200,234,220;
  --affd6e6: 218,240,230;
  --sig-fn: 130,216,255;
  --sig-num: 247,178,103;
  --ag-1: 75,207,176;
  --ag-2: 112,189,255;
  --ag-3: 215,156,233;
  --ag-4: 251,150,134;
  --ag-5: 198,184,82;
}
html[data-theme="amber"]{
  --a5a2656: 83,56,38;
  --aa63668: 131,101,52;
  --aa9718b: 161,139,116;
  --aea6296: 203,157,92;
  --aef4d8f: 202,149,71;
  --aef4f8e: 202,150,73;
  --aef6ba0: 210,162,99;
  --af082a5: 211,175,120;
  --af0a7c8: 227,195,159;
  --af27ca8: 214,172,114;
  --af382ac: 216,176,119;
  --af45a91: 206,158,81;
  --af56698: 209,163,92;
  --af9639f: 215,162,88;
  --af9689f: 214,165,92;
  --afb7bad: 221,174,110;
  --aff2c78: 199,149,34;
  --aff3280: 203,150,41;
  --aff3682: 205,151,45;
  --aff3c8a: 209,152,51;
  --aff3e8a: 209,152,52;
  --aff408c: 210,153,54;
  --aff4a8e: 210,156,62;
  --aff4e8f: 211,158,66;
  --aff4e92: 212,157,66;
  --aff4e93: 213,157,66;
  --aff5096: 214,157,68;
  --aff5498: 215,159,71;
  --aff5aa0: 218,160,77;
  --aff5c9c: 216,162,79;
  --aff5c9e: 217,162,79;
  --aff5fa2: 219,162,82;
  --aff6ba0: 217,169,92;
  --aff6ba3: 219,168,93;
  --aff6ba4: 219,168,93;
  --aff78af: 224,174,105;
  --aff78b4: 227,173,105;
  --aff7cae: 223,176,109;
  --aff8cba: 229,184,125;
  --aff8cbe: 231,184,125;
  --aff9dc4: 232,194,143;
  --aff9ec8: 235,194,145;
  --affa6c9: 234,199,154;
  --affbed7: 239,213,181;
  --affc3d9: 240,217,187;
  --affd6e6: 245,228,209;
  --sig-fn: 130,216,255;
  --sig-num: 134,216,152;
  --ag-1: 228,170,83;
  --ag-2: 126,203,135;
  --ag-3: 63,200,235;
  --ag-4: 182,167,255;
  --ag-5: 247,147,174;
}
html[data-theme="blue"]{
  --a5a2656: 0,43,56;
  --aa63668: 41,102,153;
  --aa9718b: 63,131,174;
  --aea6296: 79,148,205;
  --aef4d8f: 60,141,210;
  --aef4f8e: 63,141,210;
  --aef6ba0: 84,160,211;
  --af082a5: 108,175,221;
  --af0a7c8: 122,211,241;
  --af27ca8: 103,173,217;
  --af382ac: 106,179,221;
  --af45a91: 75,149,214;
  --af56698: 85,158,216;
  --af9639f: 76,162,220;
  --af9689f: 83,164,220;
  --afb7bad: 101,180,224;
  --aff2c78: 61,135,225;
  --aff3280: 56,138,225;
  --aff3682: 57,139,224;
  --aff3c8a: 53,142,224;
  --aff3e8a: 55,143,224;
  --aff408c: 54,145,224;
  --aff4a8e: 63,149,225;
  --aff4e8f: 66,151,225;
  --aff4e92: 63,152,225;
  --aff4e93: 61,152,225;
  --aff5096: 60,154,225;
  --aff5498: 63,156,225;
  --aff5aa0: 63,162,225;
  --aff5c9c: 70,161,225;
  --aff5c9e: 68,162,225;
  --aff5fa2: 68,165,225;
  --aff6ba0: 88,170,226;
  --aff6ba3: 85,171,226;
  --aff6ba4: 84,171,226;
  --aff78af: 96,181,227;
  --aff78b4: 92,183,227;
  --aff7cae: 103,183,228;
  --aff8cba: 115,196,233;
  --aff8cbe: 114,197,232;
  --aff9dc4: 124,210,243;
  --aff9ec8: 123,211,243;
  --affa6c9: 129,215,248;
  --affbed7: 134,221,254;
  --affc3d9: 148,224,255;
  --affd6e6: 202,235,248;
  --sig-fn: 156,222,169;
  --sig-num: 247,178,103;
  --ag-1: 115,188,255;
  --ag-2: 217,155,231;
  --ag-3: 251,151,132;
  --ag-4: 195,185,83;
  --ag-5: 72,207,178;
}
