/* Atlas — per the design notes (Illuminated Cartography). Read the design notes before editing. */

/* Palette = Gruvbox (warm retro, first-class light + dark). The Illuminated
   Cartography idiom is preserved; only the inkpot changed to Gruvbox. Every
   workstation/text/accent reads from a variable so the scheme flips in one place.
   :root holds the night palette (the map is a night map in both schemes); the
   light scheme restates the panel tokens on the panel containers only — see
   THE LIGHT SCHEME below. */
:root {
  /* structure — Gruvbox dark */
  --abyss: #1d2021;          /* bg0_h — page/void */
  --surface: #282828;        /* bg0 — panels */
  --surface-2: #32302f;      /* bg0_s — raised panels (design system; #3c3836 left text-dim at 4.2:1) */
  --ink-line: #504945;       /* bg2 — strokes */
  --ink-soft: #665c54;       /* bg3 — quiet strokes */
  --parchment: #fbf1c7;      /* light bg0 — the ivory card (contrast flip) */
  --parchment-dim: #ebdbb2;
  --card-ink: #282828;
  --card-ink-soft: #504945;
  --card-w: 320px;           /* the ivory card's width — the ONE place it lives */
  --card-edge: 24px;         /* its inset from the viewport edge; the card's
                                on-screen SHARE (camera.js) is --card-w + 2× this.
                                #card, .card-left and CARD_SHARE_PX all read from
                                these two now, instead of 320 / 344 / 368 drifting
                                apart. */
  --gilt: #d79921;           /* neutral yellow — THE accent */
  --gilt-bright: #fabd2f;    /* bright yellow — cores, hover */
  --gilt-deep: #e0b95f;      /* a filled gilt control under the pointer (the GO buttons) */
  --text: #ebdbb2;           /* fg1 */
  --text-dim: #a89984;       /* fg4 / gray */
  --discover: #427b58;       /* faded green CTA */
  --discover-hover: #4f9268;
  /* semantic (Gruvbox bright accents) — reused by markers, dots, routes */
  --sem-up: #b8bb26;
  --sem-warn: #fabd2f;
  --sem-down: #fb4934;
  --sem-info: #83a598;
  --sem-slumber: #928374;
  /* the Depths' biolume teal — markers, the abyss, the decoy eyes */
  --depth: #7FE0C8;
  --depth-rgb: 127, 224, 200;
  /* accent as rgb triplet so rgba() glows can be theme-aware */
  --gilt-rgb: 215, 153, 33;
  --down-rgb: 251, 73, 52;
  --glide: cubic-bezier(0.4, 0.0, 0.2, 1);
  /* ── THE CROWN (2026-08-15) ─────────────────────────────────────────────
     `--crown-b` is the scene band's bottom edge: 18 top inset + 1 border +
     3 pad + 30 row (the ☰ circle is the tallest thing in it) + 3 pad + 1
     border ≈ 56px — it MEASURES 57 in Chrome, because the Cinzel line box
     runs a shade over 30; the 8px clearance below absorbs that and the token
     stays a number a human can derive. That number used to be hand-baked into `.atlas-tools`
     (62) and `.zoom-stack` (70) and NOTHING else knew it — which is exactly
     why every scene opened underneath the band. It is one token now, spent
     on both hanging clusters, the six scene panels, THE PATH and the Index.
     `--crown-clear` is the first y anything below the band may occupy.
     MUST TRACK `#scenes-list` — its inset, padding, border AND row height;
     the ≤720px block re-measures it (54) rather than guessing. */
  --crown-b: 56px;
  --crown-clear: calc(var(--crown-b) + 8px);
  /* the Index drawer's horizontal claim: width 320 + right inset 18. The
     scene-panel rules cede exactly this; MUST TRACK #index-drawer. */
  --drawer-clear: calc(320px + 18px);
  /* ── panel tokens that flip with the scheme (v1.52.0) ───────────────────
     Wells, hairlines and the older desaturated status hues used to be
     literals inside the panel rules; they are tokens now so the LIGHT block
     below can restate them. The dark values are the literals they replaced,
     so the dark scheme is unchanged. */
  --shade-rgb: 0, 0, 0;      /* sunken wells and shadows: rgba(var(--shade-rgb), calc(a * var(--shade-k))) */
  --shade-k: 1;
  --hair: rgba(255, 255, 255, 0.05);   /* a row's bottom hairline on a panel */
  --text-soft: #b7a983;      /* hint lines between --text and --text-dim */
  --on-gilt: #1a1a17;        /* a label on a filled gilt control */
  --tone-up: #7A9B6E;        /* the v1 status hues the panels still speak in */
  --tone-warn: #C49A52;
  --tone-down: #C06B62;
  --tone-info: #7FB0C4;
  --frame-ground: #101010;   /* behind an embedded service while it loads */
  color-scheme: dark;
}

/* ── THE LIGHT SCHEME (v1.52.0, the design notes 2026-10-19) ─────────────────────
   Panels follow the scheme; the map is a night map in both. So the light
   values are restated ONLY on the panel containers, and everything outside
   them — the stage and its markers and zone labels, the round chrome, the
   scene band, the veil, the announcements, the parchment card — keeps the
   night palette from :root by construction, not by a list of exceptions.
   `data-scheme` on <html> is set by scheme.js (stored choice, else the OS
   preference). Values since 2026-10-31: slate with a deep blue accent, in place
   of the warm Gruvbox Light and its amber. --gilt keeps its name because every
   rule says `gilt`; here it is a deep blue, picked to clear AA at panel sizes
   even on glass (below). All measured by tests/test_scheme_css.py. */
:root[data-scheme="light"] :is(#index-drawer, .ldg-panel, .trb-panel, .rel-panel, .pth-panel,
                               .pal-panel, .gl-panel, .dsk-panel, .wiz-panel,
                               #scenes-list, .scene-home, .chrome, .rl-crumbs, .rl-locator, .rl-hint,
                               .chron-fly-pop, .chron-arrow, .embed-max-arrow, #embed-max-close) {
  --abyss: #d6dae0;
  --surface: #c5cad2;
  --surface-2: #cdd2d9;
  --ink-line: #8e96a3;
  --ink-soft: #475569;
  --text: #0f172a;
  --text-dim: #172033;
  --text-soft: #172033;
  --gilt: #111d50;
  --gilt-bright: #1d4ed8;
  --gilt-rgb: 17, 29, 80;
  --on-gilt: #f1f5f9;
  --sem-up: #166534;
  --sem-warn: #b45309;
  --sem-down: #b91c1c;
  --sem-info: #0369a1;
  --down-rgb: 185, 28, 28;
  --tone-up: #166534;
  --tone-warn: #b45309;
  --tone-down: #b91c1c;
  --tone-info: #0369a1;
  --gilt-deep: #1e3a8a;
  --shade-rgb: 15, 23, 42;
  --shade-k: 0.3;
  --hair: rgba(15, 23, 42, 0.16);
  --frame-ground: #c5cad2;
  color-scheme: light;
}

/* THE LIGHT CHROME (2026-10-20) — every flyout follows the scheme, not just the
   scene panels: the band, the round buttons, the region crumbs/locator/hint,
   the watchings' arrows and pop-ups. They take the light tokens above; these
   are the fills that were literals. Still night by design: the stage and its
   markers, zone labels and region tabs (they are the map), the veil, and the
   parchment card and toast (already light in both). */
/* ⚠ one selector per specificity: `:is()` takes its MOST specific argument's
   weight, so an id in the list would lift the class rules above .chrome.lit */
:root[data-scheme="light"] .chrome { background: rgba(214, 218, 224, 0.42); }
:root[data-scheme="light"] :is(.chron-arrow, .embed-max-arrow) { background: rgba(214, 218, 224, 0.62); }
:root[data-scheme="light"] #embed-max-close { background: rgba(214, 218, 224, 0.62); }
:root[data-scheme="light"] .chrome.lit { background: var(--gilt); }
:root[data-scheme="light"] #scenes-list { background: rgba(214, 218, 224, 0.42); }
/* pale glass over the Depths' black turns muddy, and the navy on it goes with it */
:root[data-scheme="light"] body.depths #scenes-list { background: rgba(214, 218, 224, 0.94); }
:root[data-scheme="light"] body.depths .chrome:not(.lit) { background: rgba(214, 218, 224, 0.9); }
:root[data-scheme="light"] body.scene-open #scenes-list {
  background: rgba(214, 218, 224, 0.94);
  box-shadow: 0 12px 34px rgba(15, 23, 42, 0.22);
}
:root[data-scheme="light"] .scene { text-shadow: none; }
:root[data-scheme="light"] .scene.active { background: rgba(var(--gilt-rgb), 0.12); }
:root[data-scheme="light"] :is(.rl-crumbs, .rl-hint) {
  background: rgba(226, 232, 240, 0.66);
  box-shadow: 0 4px 18px rgba(15, 23, 42, 0.18);
}
:root[data-scheme="light"] .chron-fly-pop { box-shadow: 0 14px 40px rgba(15, 23, 42, 0.22); }

/* THE GLASS (2026-10-31, the owner: "a more transparent theme with a blue accent
   instead of orange", then "more gray/slate and about 30% more transparent").
   A muted grey-slate at 0.46 over a pale scrim until 2026-11-01, when the owner
   asked for every section to sit on Topology's dark dim. Glass that thin over a
   dark dim reads as muddy grey, which is what made Topology hard to read, so the
   panels went nearly solid. These numbers are not a taste: tests/test_scheme_css.py
   composites glass over scrim over black, the worst thing behind a panel, and every
   text token must still clear AA there. */
:root[data-scheme="light"] :is(.ldg-panel, .trb-panel, .rel-panel, .pth-panel, .pal-panel,
                               .gl-panel, .dsk-panel, .wiz-panel) {
  background: rgba(214, 218, 224, 0.94);
  color: var(--text);   /* the page's cream is inherited as a colour, not a token */
  -webkit-backdrop-filter: blur(22px) saturate(1.5);
  backdrop-filter: blur(22px) saturate(1.5);
  border-color: rgba(214, 218, 224, 0.5);
  box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28);
}
:root[data-scheme="light"] :is(#index-drawer) {
  background: rgba(214, 218, 224, 0.6);
  color: var(--text);
  -webkit-backdrop-filter: blur(22px) saturate(1.5);
  backdrop-filter: blur(22px) saturate(1.5);
}
:root[data-scheme="light"] #settings-panel { color: var(--text); }
:root[data-scheme="light"] .scene-home { color: var(--text); }
:root[data-scheme="light"] .scene-home .scene.active { background: rgba(var(--gilt-rgb), 0.12); }
:root[data-scheme="light"] :is(#ledger, #calendar, #seals, #stones, #tribute, #reliquary,
                               #accounts, #trust, #settings) {
  background: rgba(13, 11, 7, 0.82);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

* { box-sizing: border-box; margin: 0; padding: 0; }
/* the [hidden] attribute must win over explicit display rules (rows/headers set
   display:flex, which otherwise beats the UA [hidden]{display:none}) — the Index
   search + tab switching rely on this */
[hidden] { display: none !important; }
html, body { width: 100%; height: 100%; overflow: hidden; background: var(--abyss); }
body { font-family: 'Alegreya Sans', sans-serif; color: var(--text); }
button { font-family: inherit; }

/* paper grain over the whole experience */
body::after {
  content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 950; opacity: 0.5;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.8 0 0 0 0 0.72 0 0 0 0 0.55 0 0 0 0.035 0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── Intro veil ─────────────────────────────────────── */
/* the splash veil is ALWAYS dark, even under the light theme — a fixed dark
   entrance, not tied to the map's palette */
#veil {
  position: fixed; inset: 0; z-index: 900;
  background: #14120d;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 1.4s ease 0.2s, visibility 0s linear 1.7s;
}
#veil .veil-title { color: #EFE7D3; }
#veil .veil-tri span { background: linear-gradient(180deg, #E4C878, var(--gilt)); }
#veil .veil-rule path { stroke: var(--gilt); }
#veil.lifted { opacity: 0; visibility: hidden; pointer-events: none; }
/* the boot that failed: the veil stays down and says why (atlas.js boot().catch) */
.veil-fail .veil-title { font-size: 1.2rem; letter-spacing: 0.2em; }
.veil-fail p { margin-top: 14px; color: var(--text-dim); font-family: 'Alegreya Sans', sans-serif; }
.veil-inner { text-align: center; }
.veil-tri {
  display: grid; grid-template-columns: repeat(2, 30px); justify-content: center;
  width: 60px; margin: 0 auto 24px; position: relative; height: 52px;
  opacity: 0; animation: fadeup 1.2s ease 0.3s forwards;
}
.veil-tri span {
  width: 30px; height: 26px;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
  background: linear-gradient(180deg, var(--gilt-bright), var(--gilt));
  filter: drop-shadow(0 0 10px rgba(var(--gilt-rgb), 0.55));
}
.veil-tri span:first-child { position: absolute; left: 15px; top: 0; }
.veil-tri span:nth-child(2), .veil-tri span:nth-child(3) { margin-top: 26px; }
.veil-title {
  font-family: 'Cinzel', serif; font-weight: 600;
  font-size: clamp(2.6rem, 7vw, 5rem); letter-spacing: 0.2em; color: var(--parchment);
  text-shadow: 0 0 50px rgba(var(--gilt-rgb), 0.3);
  opacity: 0; animation: fadeup 1.4s ease 0.6s forwards;
}
.veil-rule { width: 320px; height: 22px; margin: 26px auto 0; }
.veil-rule path {
  stroke: var(--gilt); stroke-width: 1.2; fill: none;
  stroke-dasharray: 800; stroke-dashoffset: 800;
  animation: inkdraw 2.2s ease-out 1.2s forwards;
}
@keyframes fadeup { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes inkdraw { to { stroke-dashoffset: 0; } }

/* ── Viewport & stage ───────────────────────────────── */
/* the sky is a light realm, so the backdrop behind everything is light — scrolling
   high above the clouds shows this, not black. (The map fills the middle and the
   #void covers the Depths, so this only ever shows up in the high sky.) */
#viewport {
  position: fixed; inset: 0; overflow: hidden; cursor: grab; background: #f8f3df;
  transition: transform 700ms var(--glide);   /* THE TILT (body.tilted, further down) rides this */
}
#viewport.dragging { cursor: grabbing; }
#stage {
  position: absolute; top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
#stage { transition: opacity 200ms ease; }
#stage.gliding { transition: transform 1100ms var(--glide), opacity 200ms ease; }
#stage.rg-fading { opacity: 0; }
#map-base {
  display: block; user-select: none; -webkit-user-drag: none;
  /* the sand-table render carries its own grade; touch it lightly */
  filter: saturate(0.94) contrast(1.03) brightness(0.96);
  transition: filter 900ms ease;
}
#overlay { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; z-index: 4; }

/* ── The void below the table — scroll south into the Depths ── */
#void-fade {
  /* only the last few percent: the map reads to its foot (owner, 2026-11-01) */
  position: absolute; left: -2%; width: 104%; top: 93%; height: 7.5%;
  pointer-events: none; z-index: 3;
  background: linear-gradient(180deg, transparent, rgba(4,3,2,0.72) 62%, #030202 100%);
}
#void {
  position: absolute; left: -30%; width: 160%; top: 99.8%; height: 102%;
  pointer-events: none; z-index: 3; background: #030202;
}

/* ── The Depths' atmosphere — mirror of #sky-realm: fills the descent (the
   void between the table's south edge and the Abyss) with a lit, breathing
   underworld instead of flat black ── */
#depths-realm {
  /* starts at the foot of the black band under the table (owner, 2026-11-01), so
     The edge and internet pins sit on plain black. The glow's first stops fade in
     from transparent, so the fog has no top edge. */
  position: absolute; left: -30%; width: 160%; top: 110%; height: 92%;
  z-index: 4; pointer-events: none; overflow: hidden;
}
.depths-glow {
  position: absolute; inset: 0;
  background:
    /* transparent over the table overlap, fading into dense teal fog below the
       seam; content sits close (≈33-40%) so the fill still reads full */
    linear-gradient(180deg,
      transparent 0%, rgba(9,22,24,0.18) 9%, rgba(11,26,28,0.55) 20%, rgba(12,30,32,0.8) 40%,
      rgba(8,20,22,0.9) 66%, rgba(4,10,11,1) 100%),
    radial-gradient(ellipse 70% 26% at 50% 26%, rgba(90,180,165,0.2), transparent 72%),
    radial-gradient(ellipse 62% 48% at 50% 54%, rgba(58,124,112,0.24), transparent 74%),
    radial-gradient(ellipse 32% 24% at 22% 40%, rgba(70,150,150,0.13), transparent 70%),
    radial-gradient(ellipse 30% 22% at 80% 50%, rgba(60,120,140,0.11), transparent 70%);
}
/* the seam-dissolving blurred haze — the mirror of .cloud-bank::before */
.depths-glow::before {
  content: ''; position: absolute; left: -8%; right: -8%; top: -2%; height: 30%;
  background: linear-gradient(180deg,
    transparent 0%, rgba(7,18,20,0.7) 44%, rgba(9,22,24,0.82) 64%, transparent 100%);
  filter: blur(18px);
}
/* faint bioluminescent motes drifting up, the depths' answer to the clouds */
.depths-motes {
  position: absolute; inset: -10% 0 0 0;
  background-image:
    radial-gradient(2px 2px at 12% 80%, rgba(var(--depth-rgb), 0.5), transparent),
    radial-gradient(2px 2px at 34% 62%, rgba(var(--depth-rgb), 0.35), transparent),
    radial-gradient(1.5px 1.5px at 52% 88%, rgba(140,220,210,0.45), transparent),
    radial-gradient(2px 2px at 68% 50%, rgba(110,200,200,0.3), transparent),
    radial-gradient(1.5px 1.5px at 82% 74%, rgba(var(--depth-rgb), 0.4), transparent),
    radial-gradient(2px 2px at 46% 34%, rgba(120,210,200,0.25), transparent),
    radial-gradient(1.5px 1.5px at 90% 42%, rgba(var(--depth-rgb), 0.3), transparent);
  animation: depths-drift 22s linear infinite;
  opacity: 0.7;
}
@keyframes depths-drift {
  from { transform: translateY(6%); }
  to   { transform: translateY(-8%); }
}
@media (prefers-reduced-motion: reduce) { .depths-motes { animation: none; } }

/* ── The Upper Deck — a lighter dawn above the north edge (tall enough to light
   the Chronicle high above the clouds) ── */
#sky-realm { position: absolute; left: -30%; width: 160%; top: -58.5%; height: 60%; z-index: 6; pointer-events: none; }
/* the sky itself: a soft luminous wash, much lighter than the abyss */
/* a clean, light backdrop above the clouds — no map art up here, just sky, so the
   Chronicle reads on a bright field. Fades to transparent low down where the clouds
   take over and hide the seam. */
.sky-glow {
  position: absolute; inset: 0;
  background:
    linear-gradient(180deg, rgba(248,243,223,0.94) 0%, rgba(244,237,217,0.82) 40%, rgba(238,228,203,0.44) 68%, rgba(232,220,195,0.12) 87%, transparent 96%),
    radial-gradient(ellipse 72% 60% at 50% 20%, rgba(253,248,232,0.6), transparent 78%);
}
/* the cloud bank hugs the seam; a warm ::before haze dissolves the hard PNG edge
   so the fractal clouds above read as real cloud, never as a cut line */
/* the cloud band blankets the sand→sky seam AND dips a little onto the map: tall
   enough that its dense middle sits over the seam while the bottom overlaps the
   render (the clean light .sky-glow owns the space above it) */
/* the bank's foot rests about 6% down the map, under the title bar at the home
   view, so the map reads to its head (owner, 2026-11-01) and the seam stays soft */
.cloud-bank { position: absolute; left: -12%; right: -12%; bottom: -7%; height: 66%; }
/* an opaque warm haze band straddles the seam and fully hides the hard PNG edge;
   the fractal clouds ride on top for texture. Taller + denser than before so no
   sand→sky cut shows while scrolling up; fades both ways so it never reads as a line. */
.cloud-bank::before {
  content: ''; position: absolute; left: -8%; right: -8%; bottom: 6%; height: 60%;
  background: linear-gradient(180deg,
    transparent 0%, rgba(244,236,220,0.6) 22%, rgba(240,231,213,0.98) 42%,
    rgba(236,225,201,0.99) 66%, rgba(226,211,183,0.7) 86%, transparent 100%);
  filter: blur(20px);
}
/* a second, lower opaque strip reinforces just below y=0 so the literal top edge of
   the sand render is always blanketed no matter the scroll position */
.cloud-bank::after {
  content: ''; position: absolute; left: -8%; right: -8%; bottom: 0%; height: 34%;
  background: linear-gradient(180deg, transparent, rgba(238,228,205,0.96) 55%, rgba(224,208,179,0.55) 90%, transparent);
  filter: blur(16px);
}
/* Real clouds from fractal noise (Helio-style): feTurbulence gives natural puffy
   volume no gradient stack can. Dense band straddles the seam, fading wispy both
   above (into sky) and below (into the sand) so no edge shows. */
.cloud-svg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0.9;
  -webkit-mask: linear-gradient(180deg, transparent 3%, #000 20%, #000 82%, transparent 96%);
          mask: linear-gradient(180deg, transparent 3%, #000 20%, #000 82%, transparent 96%);
  animation: clouddrift 120s ease-in-out infinite alternate;
}
.cloud-svg rect { fill: #fbf9f4; }
@keyframes clouddrift {
  from { transform: translateX(-2.5%); }
  to   { transform: translateX(2.5%); }
}
/* (#sky-title / .sky-head / .sky-sub retired 2026-08-15 — see world.js's
   note by buildSkyIsland for the whole story) */

/* ── The Chronicle — live watchings, high above the clouds on a clean backdrop ── */
#chronicle {
  /* Sky and Depths draw at true size at any window (owner, 2026-11-01). They sit
     inside the camera's stage, so a narrower window used to shrink them with the
     map: charts at half size at 1024px. `--fit` is the camera's fit scale, undone
     here. camera.js measures both sections and moves `top` and the scroll limits. */
  position: absolute; z-index: 6;
  top: -48%; left: 50%; transform: translateX(-50%) scale(calc(1 / var(--fit, 1)));
  transform-origin: top center;
  width: min(1560px, calc(100vw - 168px)); text-align: center;   /* clears the zoom column */
  padding: 24px 24px 16px; border-radius: 14px;
  background: linear-gradient(180deg, rgba(246,239,224,0.16), rgba(236,229,212,0.05));
}
.chron-head {
  font-family: 'Cinzel', serif; font-weight: 400; font-size: 26px;
  letter-spacing: 0.34em; color: #463c22;
}
.chron-sub {
  font-family: 'IM Fell English', 'Alegreya Sans', serif; font-style: italic; font-size: 15px;
  color: #6b5f45; margin: 6px 0 18px;
}
.chron-viewer {
  position: relative; border-radius: 8px; overflow: hidden;
  border: 1px solid var(--ink-line); background: rgba(8,10,14,0.6);
  box-shadow: 0 18px 50px rgba(0,0,0,0.5);
}
.chron-stage { display: flex; transition: transform 600ms var(--glide); }
/* default (full-dashboard) window — large so the charts read */
/* 330px is the band plus the section's heading, dots, buttons and the colophon's
   strip, so all of it fits the window below the band */
.chron-slide { flex: 0 0 100%; display: flex; flex-direction: column; height: clamp(320px, calc(100vh - 330px), 860px); text-decoration: none; }
/* single-panel (one graph per card) — a generous window for the one graph */
.chron-slide.solo { height: clamp(320px, calc(100vh - 330px), 640px); }
.chron-slide-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  padding: 9px 14px; background: rgba(0,0,0,0.32); border-bottom: 1px solid var(--ink-line);
}
.chron-title { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.7rem; letter-spacing: 0.2em; color: var(--gilt); }
/* (`.chron-note` lived here. A watching's `note` is shown in the flyout as
   `.cfp-note`, never as `.chron-note`; the rule matched nothing. 2026-07-31.) */
.chron-head-actions { display: flex; align-items: center; gap: 14px; }
.chron-open { font-family: 'Cinzel', serif; font-size: 0.58rem; letter-spacing: 0.14em; color: var(--text-dim); text-decoration: none; }
.chron-open:hover { color: var(--gilt); }
.chron-fs { background: none; border: none; cursor: pointer; color: var(--text-dim); font-size: 0.95rem; line-height: 1; padding: 0; transition: color 150ms; }
.chron-fs:hover { color: var(--gilt); }
#chronicle .chron-fs { color: #8a7d60; }
#chronicle .chron-fs:hover { color: #8a6a1e; }
/* the pane holds the dashboard and the realm's own waiting state on top of it,
   so a cold Grafana never shows you *its* spinner (2026-07-31) */
.chron-pane { flex: 1; position: relative; min-height: 0; background: #0b0d12; }
.chron-frame {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
  background: #0b0d12; opacity: 0; transition: opacity 260ms ease;
}
.chron-slide.scried .chron-frame { opacity: 1; }
/* ── The wheel travels; a watching wakes on a click (2026-08-21) ────────────
   An iframe is a window into another document: a wheel turned over it goes to
   THAT document, and #viewport's listener — the only thing that moves the
   camera — never hears it. The Abyss slide is up to 90vh tall, so down in the
   Depths the wheel was dead across most of the screen and the map "would not
   scroll up" (2026-08-21); the same held over the Chronicle in the sky.
   Frames now SLEEP — `pointer-events: none`, so the wheel and a drag fall
   through to the pane and on to the viewport — and WAKE on a click on their
   pane (the carousel adds `.awake`), sleeping again when the pointer leaves
   the slide or the carousel pages. The contract of every embedded map on the
   web. The maximised ⛶ frame is not a carousel frame and stays live. */
.chron-pane:not(.awake) .chron-frame { pointer-events: none; }
.chron-pane:not(.awake)::after {
  content: 'CLICK TO TOUCH · WHEEL TO TRAVEL';
  position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%);
  font-family: 'Cinzel', serif; font-size: 0.55rem; letter-spacing: 0.16em; white-space: nowrap;
  color: var(--text-dim); background: rgba(0,0,0,0.55); padding: 4px 10px; border-radius: 999px;
  opacity: 0; transition: opacity 200ms ease; pointer-events: none; z-index: 2;
}
.chron-pane:not(.awake):hover::after { opacity: 1; }
/* several watchings sharing one slide (pane_group): quarter-panes in a grid.
   Each cell is its own positioning context, so the absolute chron-frame is
   re-flowed to fill the CELL rather than the whole pane. */
.chron-grid {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-rows: 1fr;
  gap: 10px; padding: 10px;
}
.chron-cell { position: relative; min-height: 0; display: flex; flex-direction: column; }
.chron-cell-cap {
  font-family: 'Cinzel', serif; font-size: 0.54rem; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-dim);
  margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chron-cell .chron-frame, .chron-cell .chron-native { position: relative; inset: auto; height: auto; flex: 1; min-height: 0; }
.chron-scrying {
  position: absolute; inset: 0; display: flex; gap: 7px;
  align-items: center; justify-content: center;
  color: var(--text-dim); transition: opacity 260ms ease;
}
.chron-slide.scried .chron-scrying { opacity: 0; }
.chron-scrying em {
  font-family: 'Alegreya Sans', sans-serif; font-style: italic;
  font-size: 0.86rem; letter-spacing: 0.02em; margin-left: 5px;
}
.chron-scrying span {
  width: 7px; height: 7px; border-radius: 50%; background: var(--gilt);
  opacity: 0.3;
}
/* The dots only tick while their pane is actually reaching for a dashboard.
   Unconditional, the pulse ran forever on all ~22 carousel slides — 19 of
   them parked at about:blank off-table — on a map built for an always-on
   display. `opacity: 0` does not stop an animation any more than it stops a
   filter (the route-glow lesson, same day). */
.chron-slide.scrying .chron-scrying span,
#embed-max:not(.scried) .chron-scrying span,
#glass[data-state="reaching"] .chron-scrying span {
  animation: scry-pulse 1.25s ease-in-out infinite;
}
.chron-scrying span:nth-child(2) { animation-delay: 0.16s; }
.chron-scrying span:nth-child(3) { animation-delay: 0.32s; }
@keyframes scry-pulse { 0%, 100% { opacity: 0.22; transform: scale(0.75); } 50% { opacity: 1; transform: scale(1); } }
@media (prefers-reduced-motion: reduce) { .chron-scrying span { animation: none; opacity: 0.7; } }
/* Sky Chronicle embeds are theme=light Grafana → a light frame that sits in the
   bright sky; the Depths keeps the default dark frame for its dark dashboards */
#chronicle .chron-viewer { background: rgba(250,246,230,0.72); border-color: rgba(var(--gilt-rgb),0.4); box-shadow: 0 16px 44px rgba(90,70,30,0.22); }
#chronicle .chron-slide-head { background: rgba(250,246,230,0.94); border-bottom-color: rgba(var(--gilt-rgb),0.28); }
#chronicle .chron-title { color: #8a6a1e; }
#chronicle .chron-open { color: #8a7d60; }
#chronicle .chron-open:hover { color: #8a6a1e; }
#chronicle .chron-frame, #chronicle .chron-pane { background: #f4f2ea; }
#chronicle .chron-scrying { color: #8a7d60; }
#chronicle .chron-arrow { background: rgba(250,246,230,0.9); border-color: rgba(var(--gilt-rgb),0.45); color: #8a6a1e; }
#chronicle .chron-arrow:hover { border-color: #8a6a1e; }
.chron-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 34px; height: 34px; border-radius: 50%;
  border: 1px solid rgba(var(--gilt-rgb), 0.35); background: rgba(13,11,7,0.72); color: var(--gilt);
  font-size: 1.2rem; line-height: 1; cursor: pointer; backdrop-filter: blur(4px);
  transition: border-color 200ms;
}
.chron-arrow:hover { border-color: var(--gilt); }
.chron-prev { left: 10px; } .chron-next { right: 10px; }
.chron-dots { display: flex; gap: 8px; justify-content: center; margin-top: 12px; }
.chron-dot { width: 8px; height: 8px; border-radius: 50%; border: 0; background: var(--ink-soft); cursor: pointer; padding: 0; transition: background 150ms; }
.chron-dot.active { background: var(--gilt); }
/* button watchings — the ones with no live chart to show. The blurb is a hover
   tooltip and the button itself is the link: click opens the watching (in the
   glass where it can be framed). Was click-to-reveal + a second click on a
   link inside the popup — two clicks to read a report. 2026-08-04. */
.chron-buttons, .abyss-buttons { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 14px; }
.chron-btn-wrap { position: relative; }
.chron-btn {
  display: inline-block; text-decoration: none;   /* it is an <a> now, not a <button> */
  background: rgba(var(--gilt-rgb), 0.07); border: 1px solid rgba(var(--gilt-rgb), 0.3);
  color: var(--gilt); cursor: pointer;
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.62rem; letter-spacing: 0.16em;
  padding: 9px 16px 8px; border-radius: 3px; white-space: nowrap;
  transition: background 160ms, border-color 160ms, color 160ms;
}
.chron-btn:hover { background: rgba(var(--gilt-rgb), 0.14); border-color: var(--gilt); }
.chron-btn-wrap:hover .chron-btn, .chron-btn-wrap:focus-within .chron-btn { background: rgba(var(--gilt-rgb), 0.16); border-color: var(--gilt); color: var(--gilt-bright); }
.chron-btn.broken { color: var(--sem-down); border-color: color-mix(in srgb, var(--sem-down) 40%, transparent); }
.chron-fly-pop {
  position: absolute; left: 50%; bottom: calc(100% + 8px); transform: translateX(-50%);
  width: max-content; max-width: 260px; z-index: 8;
  display: flex; flex-direction: column; gap: 8px; text-align: left;
  padding: 12px 14px; border-radius: 6px;
  background: color-mix(in srgb, var(--surface-2) 92%, transparent);
  border: 1px solid var(--ink-line); box-shadow: 0 14px 40px rgba(0,0,0,0.5);
  backdrop-filter: blur(6px);
  /* hidden until hover/focus-within, not a JS `hidden` flag — keyboard tabbing
     to the button reveals the same blurb the mouse gets. `visibility` (not
     display:none) so the fade can run, and pointer-events:none so the tooltip
     can never eat the click meant for the button underneath it. */
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 140ms ease, visibility 140ms;
}
.chron-btn-wrap:hover .chron-fly-pop,
.chron-btn-wrap:focus-within .chron-fly-pop { opacity: 1; visibility: visible; }
.cfp-note { font-family: 'Alegreya Sans', sans-serif; font-size: 0.82rem; color: var(--text-dim); }

.chron-foot {
  margin-top: 12px; font-family: 'JetBrains Mono', monospace;
  font-size: 10px; letter-spacing: 0.16em; color: #8a7d60;
}
.chron-viewer[hidden], .chron-dots[hidden] { display: none; }

/* ── Sky Island — a drifting cloud with a bit of island peeking (links to Upper Deck).
   Pure CSS (no image blur) so it stays light and never flickers. ── */
/* `pointer-events: none` since 2026-08-15: it is scenery, not the sky's link
   (☁ and the wheel both still rise). z-index stays 5 — it never meets the
   seam-hiding cloud bank on screen, which lives entirely above the map's top
   edge while this floats over the table's northern quarter. */
/* belongs to the realm's own picture, so another map style goes without */
body.on-style #sky-island, body.on-style #sky-shadow, body.on-style #star-hide { display: none; }
#sky-island {
  position: absolute; pointer-events: none; z-index: 5;
  filter: drop-shadow(0 24px 18px rgba(5, 4, 2, 0.38));
  animation: skybob 8s ease-in-out infinite alternate;
  transition: opacity 1200ms ease;
}
/* The islet rides ON the cloud, not under it (2026-08-15). It used to
   sit low in the box with the cloud drawn over the top, so the white puff was
   the thing you saw and the green rock peeked out beneath it — an island
   sinking into weather rather than one held above it. Now the rock is raised
   and lifted a layer, the cloud dropped beneath: a sky island floats above
   its cloud. The offsets below are the tuned values — tuned by eye, twice
   (the first pass separated them so far they read as two objects). */
#sky-islet {
  position: absolute; left: 50%; top: 42%; width: 30%; height: 20%;
  z-index: 1;
  transform: translate(-50%, -50%);
  background: linear-gradient(180deg, #6b7250 0%, #6f5f40 38%, #5b492e 72%, #443722 100%);
  border-radius: 50% 50% 44% 56% / 54% 52% 50% 46%;
  box-shadow: 0 7px 12px rgba(5,4,2,0.4), inset 0 -4px 8px rgba(48,35,20,0.6), inset 0 3px 6px rgba(120,138,98,0.28);
}
/* the orb's cloud: the same fractal-noise clouds, masked into a soft blob —
   seated BELOW the islet since 2026-08-15, so it reads as the cloud the
   island rests on rather than the weather it is buried in */
.orb-cloud-svg {
  position: absolute; left: -4%; top: 10%; width: 108%; height: 96%;
  opacity: 0.9;
  -webkit-mask: radial-gradient(ellipse 56% 52% at 50% 52%, #000 44%, transparent 78%);
          mask: radial-gradient(ellipse 56% 52% at 50% 52%, #000 44%, transparent 78%);
}
@keyframes skybob {
  from { transform: translate(-50%, -50%) translateY(-28px); }
  to   { transform: translate(-50%, -50%) translateY(-44px); }
}
#sky-shadow {
  position: absolute; pointer-events: none; z-index: 4;
  transform: translate(-50%, -50%);
  background: radial-gradient(ellipse 50% 44% at 50% 50%, rgba(6, 5, 3, 0.52), transparent 72%);
  animation: skyshadow 7s ease-in-out infinite alternate;
  transition: opacity 1400ms ease;
}
@keyframes skyshadow {
  from { opacity: 0.95; transform: translate(-50%, -50%) scale(0.97); }
  to   { opacity: 0.75; transform: translate(-50%, -50%) scale(1.05); }
}

/* scroll-linked shades over the table: darkness rises gradually toward the Depths,
   light gathers gradually toward the Sky — driven by --depth-t / --sky-t (world.js) */
#depth-map-shade {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(ellipse 74% 66% at 50% 46%, rgba(6,16,16,0.72), rgba(2,6,7,0.96));
  opacity: calc(var(--depth-t, 0) * 0.94);
}
#sky-map-light {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(246,238,218,0.5), rgba(240,230,206,0.14) 55%, transparent 80%);
  opacity: calc(var(--sky-t, 0) * 0.9);
}

/* ── The Abyss watches — security & cloud reports far below the Depths ── */
#abyss {
  position: absolute; z-index: 6;
  /* the black band runs from the table's edge (100%) to here: the room the edge
     and internet pins are charted in. camera.js ends the world at this section's
     foot, so moving it moves the floor; CFG.DEPTHS_ENGAGE tracks it.
     Same inner padding as #chronicle so the internal spacing still matches. */
  top: 114%; left: 50%; transform: translateX(-50%) scale(calc(1 / var(--fit, 1)));
  transform-origin: top center;
  width: min(1560px, calc(100vw - 168px)); text-align: center; pointer-events: auto;
  padding: 24px 24px 16px; border-radius: 14px;
}
.abyss-head {
  font-family: 'Cinzel', serif; font-weight: 400; font-size: 26px;
  letter-spacing: 0.34em; color: #8FE6D2;
  text-shadow: 0 0 24px rgba(var(--depth-rgb), 0.35);
}
.abyss-sub {
  font-family: 'IM Fell English', 'Alegreya Sans', serif; font-style: italic; font-size: 15px;
  color: #5E8A80; margin: 6px 0 18px;
}
#abyss .chron-btn {
  background: rgba(var(--depth-rgb), 0.06); border-color: rgba(var(--depth-rgb), 0.32); color: #8FE6D2;
}
#abyss .chron-btn:hover { background: rgba(var(--depth-rgb), 0.14); border-color: var(--depth); }
#abyss .chron-btn-wrap:hover .chron-btn, #abyss .chron-btn-wrap:focus-within .chron-btn { background: rgba(var(--depth-rgb), 0.18); border-color: var(--depth); color: #B8F5E8; }
/* down in the dark the abyss glows; up on the sand it's out of sight anyway */
body.depths #abyss .abyss-head { text-shadow: 0 0 30px rgba(var(--depth-rgb), 0.5); }

/* THE NAMING DEEP (2026-08-15) — whois, riding inside the TOP CLIENTS slide:
   the panel that names the busiest client IPs and the tool that asks who
   holds one, in the same frame (`.aw-duo`, built by ui.js slideHTML for a
   watching carrying `whois: true`). The abyss's own teal, mono for the answer
   (it is protocol text), and the answer pane scrolls inside its column. The
   iframe borrows the chron-cell trick: relative + flex, so the absolute
   default re-flows to its share of the row rather than the whole pane. */
/* THE NAMING DEEP stacks BELOW its dashboard, always.
   It used to sit side by side above 900px, and read as part of the Grafana
   panel rather than a tool of its own — a bordered pane containing a chart and
   a form looks like one dashboard with a search box in it (2026-08-21).
   Column was already the proven layout: the max-width:900px rule below has
   rendered exactly this for narrow windows since it shipped. This makes it
   unconditional rather than inventing a new arrangement. */
.aw-duo { position: absolute; inset: 0; display: flex; flex-direction: column; gap: 12px; padding: 12px; }
.aw-duo .chron-frame, .aw-duo .chron-native { position: relative; inset: auto; height: auto; flex: 1.4; min-width: 0; }
.aw-side { flex: 1; min-width: 0; display: flex; flex-direction: column; text-align: left; min-height: 0; }
.aw-head {
  font-family: 'Cinzel', serif; font-weight: 400; font-size: 13px;
  letter-spacing: 0.3em; color: #8FE6D2;
  text-shadow: 0 0 18px rgba(var(--depth-rgb), 0.3);
}
.aw-sub {
  font-family: 'IM Fell English', 'Alegreya Sans', serif; font-style: italic;
  font-size: 12px; color: #5E8A80; margin: 3px 0 10px;
}
.aw-form {
  display: flex; align-items: center; gap: 8px; padding: 7px 10px;
  border: 1px solid rgba(var(--depth-rgb), 0.32); border-radius: 4px;
  background: rgba(4,12,11,0.55);
}
.aw-glyph { color: var(--depth); font-size: 0.95rem; }
.aw-input {
  flex: 1; min-width: 0; background: transparent; border: none; outline: none;
  font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: #B8F5E8;
}
.aw-input::placeholder { color: #4E7A70; }
.aw-go {
  background: rgba(var(--depth-rgb), 0.06); border: 1px solid rgba(var(--depth-rgb), 0.32);
  color: #8FE6D2; border-radius: 3px; cursor: pointer; padding: 5px 12px 4px;
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.6rem; letter-spacing: 0.22em;
  transition: background 150ms, border-color 150ms;
}
.aw-go:hover { background: rgba(var(--depth-rgb), 0.14); border-color: var(--depth); }
.aw-go[disabled] { opacity: 0.5; cursor: wait; }
.aw-out {
  flex: 1; min-height: 0; margin-top: 10px; padding: 10px 12px;
  overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: rgba(var(--depth-rgb), 0.3) transparent;
  border: 1px solid rgba(var(--depth-rgb), 0.22); border-radius: 4px;
  background: rgba(4,12,11,0.65);
  font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; line-height: 1.55;
  color: #9AD8CA; white-space: pre-wrap; word-break: break-word;
}
.aw-out[hidden] { display: none; }
/* the duo is a column at every width now (see .aw-duo above); this rule is
   kept only so a future side-by-side experiment has its narrow-screen escape
   already written. */
@media (max-width: 900px) { .aw-duo { flex-direction: column; } .aw-side { min-width: 0; } }

/* ── The Depths — underworld descent (D / ☾) ─────────── */
/* committed-depths mood (biolume markers/fog) still toggles via the class, but the
   table's darkening is now the gradual #depth-map-shade above — no abrupt filter jump */
/* pause the big filtered/animated layers down in the dark — they repaint the
   whole composite every frame and read as flicker against pure black */
body.depths #sky-island, body.depths #sky-shadow {
  opacity: 0.14;
  animation-play-state: paused;
}
/* the sky bank + its blur filters repaint against the black and read as flicker
   down here — hide the whole sky region while in the Depths */
body.depths #sky-realm { opacity: 0; visibility: hidden; }
/* Depths flicker kill: stop every ambient animation that composites over the
   hue-rotated map. The drifting fog and the 14 breathing marker rings repainting
   large translucent layers each frame are what still read as flicker/artifacts. */
body.depths .fog-a, body.depths .fog-b { animation: none !important; }
body.depths .mk:not(.depths-kin) .mk-ring { animation: none !important; }
body.depths .mk.s-down::after { animation: none !important; opacity: 0.18; }
/* (#star-hide no longer carries a backdrop-filter at all — see its rule above.
   The Depths still darken the patch so it sits in the underworld's grade.) */
body.depths #star-hide {
  background: radial-gradient(ellipse at center,
    rgba(20, 16, 12, 0.62) 0%, rgba(20, 16, 12, 0.5) 55%, rgba(20, 16, 12, 0) 100%);
}
body.depths .fog-a {
  background:
    radial-gradient(ellipse 40% 28% at 22% 32%, rgba(64, 200, 190, 0.10), transparent 70%),
    radial-gradient(ellipse 34% 24% at 76% 62%, rgba(120, 220, 160, 0.07), transparent 70%);
}
body.depths .fog-b {
  background:
    radial-gradient(ellipse 30% 22% at 60% 20%, rgba(64, 180, 200, 0.06), transparent 70%),
    radial-gradient(ellipse 36% 26% at 32% 76%, rgba(100, 210, 170, 0.08), transparent 70%);
}
body.depths #vignette {
  background: radial-gradient(ellipse 68% 62% at 50% 48%, transparent 42%, rgba(2, 4, 4, 0.82) 100%);
}
body.depths .zone-label, body.depths .mk { opacity: 0.28; transition: opacity 800ms ease; }
/* down in the dark the kin markers wake fully */
body.depths .mk.depths-kin { opacity: 1; }
body.depths .mk.depths-kin .mk-core {
  background: var(--depth);
  box-shadow: 0 0 12px var(--depth), 0 0 34px rgba(var(--depth-rgb), 0.65);
}
body.depths .mk.depths-kin .mk-ring { border-color: var(--depth); }
body.depths #colophon { color: #3E5A52; }
.zone-label, .mk { transition: opacity 800ms ease; }

/* fog + vignette (screen-space, not on stage) */
.fog {
  position: absolute; inset: -25%; pointer-events: none; z-index: 20;
}
.fog-a {
  background:
    radial-gradient(ellipse 42% 30% at 18% 28%, rgba(201,191,168,0.055), transparent 70%),
    radial-gradient(ellipse 36% 26% at 78% 64%, rgba(201,191,168,0.04), transparent 70%);
  animation: fogdrift 26s ease-in-out infinite alternate;
}
.fog-b {
  background:
    radial-gradient(ellipse 30% 22% at 62% 18%, rgba(127,176,196,0.035), transparent 70%),
    radial-gradient(ellipse 34% 26% at 30% 78%, rgba(201,191,168,0.045), transparent 70%);
  animation: fogdrift 34s ease-in-out infinite alternate-reverse;
}
@keyframes fogdrift {
  from { transform: translate(-2%, 0) scale(1); }
  to { transform: translate(2.5%, 1.5%) scale(1.06); }
}
#vignette {
  position: absolute; inset: 0; pointer-events: none; z-index: 21;
  background: radial-gradient(ellipse 75% 70% at 50% 48%, transparent 55%, rgba(7,6,3,0.55) 100%);
}

/* ── Zone labels (on stage, fade with zoom) ─────────── */
#zone-labels { position: absolute; inset: 0; pointer-events: none; z-index: 7; }
/* BotW region-reveal style: thin lapidary caps, wide tracking */
.zone-label {
  position: absolute; transform: translate(-50%, -50%);
  font-family: 'Cinzel', serif; font-weight: 400;
  text-transform: uppercase; letter-spacing: 0.42em;
  white-space: nowrap; color: #E8DEC4;
  text-shadow: 0 2px 12px rgba(0,0,0,0.9), 0 0 26px rgba(0,0,0,0.6);
  transition: opacity 600ms ease;
}
.zone-label small {
  display: block; text-align: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: 0.34em; color: var(--text-dim); letter-spacing: 0.3em; margin-top: 6px;
}
/* THE DEPTHS headline reads as atmosphere, not a label: ~2.6× the size (set
   inline in buildZoneLabels beside every label's size), faint, wider-tracked,
   with its data line pinned back to a whisper instead of scaling along. The
   #zone-labels layer already stacks beneath #markers (z 7 < 8) and takes no
   pointer events — the word is scenery the depths markers glow in front of. */

/* ── Markers ────────────────────────────────────────── */
#markers { position: absolute; inset: 0; z-index: 8; pointer-events: none; }
/* THE CONSTELLATION's satellite dots — a sibling layer, so "#markers" keeps
   meaning "the charted places" to every loop that walks it */
#satellites { position: absolute; inset: 0; z-index: 9; pointer-events: none; }
.mk {
  position: absolute; width: 120px; height: 96px;
  transform: translate(-50%, -50%);
  cursor: pointer; pointer-events: auto;
  display: flex; align-items: center; justify-content: center;
}
.mk-dot { position: relative; width: 12px; height: 12px; }
.mk-core {
  position: absolute; inset: 0; border-radius: 50%;
  background: var(--mk-hue, var(--gilt-bright));
  box-shadow: 0 0 8px var(--mk-hue, var(--gilt)), 0 0 20px var(--mk-glow, rgba(var(--gilt-rgb), 0.5));
  transition: transform 200ms ease;
}
.mk-ring {
  position: absolute; inset: -7px; border-radius: 50%;
  border: 1.5px solid var(--mk-hue, var(--gilt));
  animation: breathe 3.2s ease-in-out infinite;
  animation-delay: var(--breathe-delay, 0s);
}
@keyframes breathe {
  0% { transform: scale(0.7); opacity: 0.7; }
  70% { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}
/* The hypervisor node wears the sigil (the heart of the realm) instead of a dot */
.mk[data-id="hypervisor"] .mk-dot { width: 20px; height: 20px; }
.mk[data-id="hypervisor"] .mk-core {
  border-radius: 0; box-shadow: none;
  background: var(--mk-hue, var(--gilt-bright));
  filter: drop-shadow(0 0 5px var(--mk-hue, var(--gilt))) drop-shadow(0 0 12px var(--mk-glow, rgba(var(--gilt-rgb), 0.5)));
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12 3 L6.5 13 L17.5 13 Z M6.5 13 L1 22.5 L12 22.5 Z M17.5 13 L12 22.5 L23 22.5 Z'/%3E%3C/svg%3E") center/contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12 3 L6.5 13 L17.5 13 Z M6.5 13 L1 22.5 L12 22.5 Z M17.5 13 L12 22.5 L23 22.5 Z'/%3E%3C/svg%3E") center/contain no-repeat;
}
.mk[data-id="hypervisor"] .mk-ring { inset: -6px; border-radius: 4px; }

.mk:hover .mk-core, .mk.active .mk-core { transform: scale(1.45); }
/* the marker wakes with a quick bounce as its label rises */
@keyframes mk-wake { 0% { transform: scale(0.85); } 50% { transform: scale(1.35); } 100% { transform: scale(1); } }
.mk:hover .mk-dot, .mk.active .mk-dot { animation: mk-wake 0.42s cubic-bezier(0.2, 1.4, 0.4, 1); }
.mk-label {
  position: absolute; top: calc(50% + 16px); left: 50%; transform: translateX(-50%);
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 12px;
  letter-spacing: 0.26em; color: #CBC1A9; white-space: nowrap;
  text-shadow: 0 2px 8px rgba(0,0,0,0.9), 0 0 18px rgba(0,0,0,0.7);
  opacity: 0; transition: opacity 300ms ease, transform 300ms ease;
  pointer-events: none;
}
.mk:hover .mk-label, .mk.active .mk-label, .mk.labeled .mk-label { opacity: 1; transform: translateX(-50%) translateY(-2px); }

/* ── Floating chrome ────────────────────────────────── */
.chrome {
  position: fixed; z-index: 960;
  width: 42px; height: 42px; border-radius: 50%;
  border: 1px solid rgba(var(--gilt-rgb), 0.35);
  background: rgba(13,11,7,0.4); color: var(--gilt);   /* 0.55 until 2026-10-20 ("a bit transparent", like the band) */
  font-family: 'Cinzel', serif; font-size: 1rem;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; backdrop-filter: blur(8px);
  transition: border-color 250ms, box-shadow 250ms, opacity 600ms;
}
.chrome:hover { border-color: var(--gilt); }   /* its glow is THE HOVER GLOW, at the end of this file */
/* left column: network + the agent + theme (compass now lives in the zoom stack) */
.ci-net { top: 18px; left: 18px; }
.ci-glyph { width: 20px; height: 20px; display: block; }
.ci-net.lit .ci-glyph { color: var(--abyss); }   /* stays legible when the button lights gilt */
.ci-decoys.lit .ci-glyph { color: var(--abyss); }   /* the eye, same rule as its sibling */

/* selected chrome (2026-07-11): a toggled-on tool is unmistakable — filled gilt,
   grown, and it trades the idle glow for a slow proud pulse */
.chrome.lit {
  --lit-rgb: var(--gilt-rgb);
  border-color: var(--gilt);
  color: var(--abyss);
  background: var(--gilt);
  transform: scale(1.16);
  animation: chrome-pulse 2.6s ease-in-out infinite;
}
@keyframes chrome-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(var(--lit-rgb), 0.45), 0 0 22px rgba(var(--lit-rgb), 0.35); }
  50%      { box-shadow: 0 0 0 9px rgba(var(--lit-rgb), 0), 0 0 22px rgba(var(--lit-rgb), 0.35); }
}
@media (prefers-reduced-motion: reduce) { .chrome.lit { animation: none; transform: none; } }
/* the two journeys light in reverse (owner, 2026-11-01), in the theme's own colours:
   the Depths' biolume with dark ink against the black, dark ink with the accent
   against the bright sky. `--card-ink` is the one dark that stays dark in the light
   scheme, whose `--abyss` is pale. `:root` ties these with the light scheme's
   `.chrome.lit`, and being later in the file wins. */
:root .chrome.ci-depths.lit {
  --lit-rgb: var(--depth-rgb); border-color: var(--depth); background: var(--depth); color: var(--card-ink);
}
:root .chrome.ci-sky.lit { border-color: var(--gilt); background: var(--card-ink); color: var(--gilt-bright); }
:root[data-scheme="light"] .chrome.ci-sky.lit { background: var(--gilt); color: var(--on-gilt); }
.ci-menu { top: 18px; right: 18px; }
/* ☰ wears the scene lamp now that ◆ is gone — with the drawer shut it is the
   only sign that a scene is standing open over the map (2026-08-10) */
.ci-menu.lit { border-color: var(--gilt); color: var(--gilt-bright); box-shadow: 0 0 18px rgba(var(--gilt-rgb), 0.35); }

/* SETTINGS' body. A floating panel until 2026-08-10, a section of the drawer
   until 2026-09-04, and now the scrolling body of the SETTINGS scene (see
   `#settings .ldg-panel` by the shared scene chrome). It keeps its id because
   the Surveyor's Desk appends into it and account.js / table.js draw into its
   sections. 22px sides = `.ldg-head`'s own padding, so the folds line up
   under the title. It scrolls itself: the panel is `overflow: hidden` to clip
   to its corners, and a flex item that is a scroll container is the one that
   may shrink below its content. */
#settings-panel {
  padding: 8px 22px 24px; color: var(--parchment); font-family: 'Alegreya Sans', sans-serif;
  overflow-y: auto; min-height: 0;
  scrollbar-width: thin; scrollbar-color: var(--ink-line) transparent;
}
/* The first VISIBLE section stands directly under the head's own rule, so it
   draws none of its own: `.sp-sub`'s top rule is a divider BETWEEN sections,
   and under the head it doubled (2026-09-04). Which section is first depends
   on posture — ACCOUNT in accounts/hybrid, THE SURVEY in standalone — so it is
   "a section with a visible section before it" that gets the rule, not a
   named one (the old `.sp-account .sp-sub:first-child` exception was that
   named one, and wrong the moment ACCOUNT was hidden). */
.sp-group > .sp-sec > .sp-sub { border-top: none; padding-top: 0; margin-top: 8px; }
.sp-group > .sp-sec:not([hidden]) ~ .sp-sec > .sp-sub {
  border-top: 1px solid var(--ink-line); padding-top: 11px; margin-top: 14px;
}
/* Four plain groups. One with nothing for this viewer disappears whole, name
   and all: which sections exist depends on the auth mode and on being an admin. */
.sp-group:not(:has(> .sp-sec:not([hidden]))) { display: none; }
.sp-group + .sp-group { margin-top: 22px; }
.sp-group-name {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.68rem; font-weight: 700;
  letter-spacing: 0.14em; color: var(--text-dim); padding-bottom: 6px;
  border-bottom: 1px solid rgba(var(--gilt-rgb), 0.35);
}
.sp-row { display: flex; align-items: center; justify-content: space-between; font-size: 0.86rem; cursor: pointer; }
.sp-row input { width: 16px; height: 16px; accent-color: var(--gilt); cursor: pointer; }
.sp-hint { font-size: 0.72rem; color: var(--text-soft); margin-top: 8px; line-height: 1.4; }
.sp-actions { display: flex; gap: 8px; margin-top: 12px; }
.sp-btn {
  flex: 1; padding: 6px 8px; font-size: 0.72rem; font-family: inherit;
  background: rgba(var(--gilt-rgb), 0.12); border: 1px solid rgba(var(--gilt-rgb), 0.4);
  color: var(--gilt); border-radius: 7px; cursor: pointer; transition: background 150ms;
}
.sp-btn:hover { background: rgba(var(--gilt-rgb), 0.24); }
/* hybrid's "sign in to the Atlas" is a real link (a navigation, not a fetch),
   so it must wear the button without inheriting an anchor's underline/baseline */
a.sp-btn { text-decoration: none; text-align: center; display: block; }
.sp-btn-warn { color: #d79b8a; border-color: rgba(192, 107, 98, 0.5); }
.sp-btn-warn:hover { background: rgba(192, 107, 98, 0.18); }
.sp-btn-go { color: var(--abyss); background: var(--gilt); border-color: var(--gilt); font-weight: 700; }
.sp-btn-go:hover { background: var(--gilt-deep); }
/* diagnostics: source health. Cinzel micro-caps subhead, mono for the data —
   the design notes pairing. Semantic hues reused, no new colours. */
.sp-sub {
  font-family: 'Cinzel', serif; font-size: 0.66rem; letter-spacing: 0.22em;
  color: var(--text-dim); margin: 14px 0 7px;
  border-top: 1px solid var(--ink-line); padding-top: 11px;
}
/* Folding sections (2026-08-15): inside a `.sp-sec` the subhead is a button —
   full-width, wearing the same micro-caps, plus a caret that states the fold.
   Every section folds, the Desk's appended one included (2026-08-16), and
   untouched sections start CLOSED except ACCOUNT — see applyFolds in ui.js. */
.sp-sec > .sp-sub {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  width: 100%; padding: 11px 0 0; background: none;
  border-left: none; border-right: none; border-bottom: none;
  cursor: pointer; text-align: left;
  transition: color 150ms;
}
.sp-sec > .sp-sub:hover { color: var(--gilt); }
/* the seal's emblem + the admin ledgers (2026-08-16) */
.acct-worn { font-size: 1.05rem; margin-right: 2px; }
.acct-emblems { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0 2px; }
.acct-pw { margin: 8px 0 2px; }
.acct-pw > summary { cursor: pointer; list-style: none; }
.acct-pw > summary::-webkit-details-marker { display: none; }
.acct-pw > summary::before { content: '▸ '; }
.acct-pw[open] > summary::before { content: '▾ '; }
.acct-pw .dir-input { display: block; width: 100%; box-sizing: border-box; margin: 6px 0 0; }
.acct-emb {
  width: 30px; height: 30px; border-radius: 4px; cursor: pointer;
  background: rgba(var(--shade-rgb), calc(0.18 * var(--shade-k))); border: 1px solid var(--ink-line);
  font-size: 0.95rem; line-height: 1; transition: border-color 150ms;
}
.acct-emb:hover { border-color: var(--gilt); }
.acct-emb.on { border-color: var(--gilt); background: rgba(var(--gilt-rgb), 0.14); }
.acct-emb-clear { color: var(--text-dim); font-family: 'Cinzel', serif; }
.usr-row {
  display: flex; flex-direction: column; gap: 4px;
  padding: 8px 0 9px; border-bottom: 1px solid rgba(var(--gilt-rgb), 0.08);
}
.usr-id { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.usr-name { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text); }
.usr-blocked .usr-name { color: var(--sem-down); text-decoration: line-through; }
.usr-acts { display: flex; gap: 5px; flex-wrap: wrap; }
.usr-acts .sp-btn { font-size: 0.56rem; padding: 4px 8px; }
.usr-self { font-family: 'Alegreya Sans', sans-serif; font-size: 0.78rem; color: var(--text-dim); font-style: italic; }
.usr-new { display: flex; gap: 5px; align-items: center; flex-wrap: wrap; margin-top: 10px; }
.usr-new .dir-input { flex: 1; min-width: 90px; }
.usr-new-admin { display: flex; align-items: center; gap: 4px; font-size: 0.78rem; color: var(--text-dim); }
.aud-list { max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.aud-row {
  display: flex; gap: 8px; font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem; color: var(--text-dim); white-space: nowrap;
}
.aud-row.aud-bad .aud-ev { color: var(--sem-down); }
.aud-ev { color: var(--text); min-width: 88px; }
.aud-ts { min-width: 128px; }
.aud-who { color: var(--gilt); }
.aud-ip { margin-left: auto; }

/* the plain-language half of a flavor-named section ("· your account"): body
   face, lowercase, quiet — a translation, not a second title. margin-right:
   auto keeps it hugging the name while the fold caret holds the far edge. */
.sp-sub-plain {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.72rem;
  letter-spacing: 0.02em; text-transform: none; color: var(--text-dim);
  margin-right: auto;
}
.sp-sec > .sp-sub::after {
  content: '▾'; font-size: 0.7rem; letter-spacing: 0;
  transition: transform 200ms var(--glide);
}
.sp-sec.closed > .sp-sub::after { transform: rotate(-90deg); }
.sp-sec.closed > .sp-sec-body { display: none; }
/* THE SEAL YOU CARRY — who the viewer is. The name is a place-name in this
   world's voice (Cinzel caps, like every marker label); the badges reuse the
   gilt chip idiom already on the map; the note is ordinary body text. (Its
   "first section drops the rule" exception is the general first-visible rule
   by `#settings-panel` now.) */
.acct-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.acct-name {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.82rem;
  letter-spacing: 0.16em; color: var(--text);
}
.acct-badge {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.54rem;
  letter-spacing: 0.18em; color: var(--gilt);
  border: 1px solid rgba(var(--gilt-rgb), 0.4); border-radius: 20px; padding: 2px 8px;
}
.acct-badge-quiet { color: var(--text-dim); border-color: var(--ink-line); }
.acct-note {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.8rem;
  color: var(--text-dim); margin-top: 6px; line-height: 1.45;
}
/* THE DIRECTORY — the AD form. Mono inputs (this is configuration data, the
   the design notes pairing), Cinzel micro-cap labels, and a quiet source chip saying
   which layer each value in force came from. Semantic hues reused for the test
   result; no new colours. */
.dir-row { display: block; margin: 10px 0; }
/* CONNECTIONS (connections.js): settings rows and tokens only, no new colours */
.sp-sub-h {
  margin: 16px 0 6px; font-family: 'Cinzel', serif; font-size: 0.66rem;
  letter-spacing: 0.18em; color: var(--gilt);
}
.con-tokens { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; }
.con-token { display: flex; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--hair); }
.con-token.con-dead .con-name { color: var(--text-dim); text-decoration: line-through; }
.con-token .sp-btn { margin-left: auto; }
.con-name { color: var(--text); font-weight: 600; }
.con-meta { color: var(--text-dim); font-size: 0.84rem; }
/* ids, because .dir-input (width: 100%) comes later: the row must shrink to fit,
   or it widens the whole panel and pushes Create out of sight */
#con-name { flex: 1 1 0; min-width: 0; width: auto; }
#con-life { flex: 0 0 auto; width: auto; }
#con-issue { flex: 0 0 auto; }
.con-fresh { margin: 8px 0; padding: 8px 10px; border: 1px solid rgba(var(--gilt-rgb), 0.55); border-radius: 4px; color: var(--text); }
.con-code {
  margin: 4px 0 0; padding: 8px 10px; overflow-x: auto; border-radius: 4px; white-space: pre;
  background: rgba(var(--shade-rgb), calc(0.35 * var(--shade-k))); color: var(--text);
  font: 400 0.8rem/1.45 'JetBrains Mono', monospace; user-select: all;
}
.con-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 12px; }
.con-tab {
  padding: 5px 10px; border: 1px solid var(--ink-line); border-radius: 4px; cursor: pointer;
  background: transparent; color: var(--text-dim); font: inherit; font-size: 0.86rem;
}
.con-tab.on { border-color: rgba(var(--gilt-rgb), 0.7); color: var(--text); }
.con-lang { margin-top: 8px; color: var(--text-dim); font: 400 0.76rem 'JetBrains Mono', monospace; }
.con-system { border-bottom: 1px solid var(--hair); padding: 8px 0; }
.con-system summary { cursor: pointer; display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; }
.con-check { display: flex; align-items: center; gap: 8px; margin: 8px 0; color: var(--text); font-size: 0.9rem; }
/* the review: tick what to add. Same inkpot: gilt stays on the one button that writes */
.con-review { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--hair); }
.con-filter { margin-top: 8px; }
.con-group { margin-top: 14px; }
.con-group .dir-label { margin-bottom: 2px; }
.con-all { margin-left: auto; font-family: 'Alegreya Sans', sans-serif; font-size: 0.74rem; letter-spacing: 0; cursor: pointer; }
.con-row { display: flex; align-items: flex-start; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--hair); }
.con-row[hidden] { display: none; }
.con-pick { flex: 0 0 auto; margin-top: 4px; accent-color: var(--gilt); }
.con-row-text { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
.con-row .sp-btn { flex: 0 0 auto; }
.con-row-line { display: block; }
.con-ip { margin-left: 6px; color: var(--text-dim); font: 400 0.76rem 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; }
.con-off .con-name { color: var(--text-dim); font-weight: 400; }
.con-worry { color: var(--sem-warn); }
.con-ignored { margin-top: 12px; }
.con-ignored summary { display: list-item; }
.dir-label {
  display: flex; align-items: baseline; gap: 8px;
  font-family: 'Cinzel', serif; font-size: 0.6rem; letter-spacing: 0.18em;
  color: var(--text-dim); margin-bottom: 4px;
}
.dir-src { font-family: 'Alegreya Sans', sans-serif; font-size: 0.66rem;
           letter-spacing: 0; color: var(--ink-soft); margin-left: auto; font-style: italic; }
.dir-input {
  width: 100%; background: rgba(var(--shade-rgb), calc(0.25 * var(--shade-k))); color: var(--text);
  border: 1px solid var(--ink-line); border-radius: 3px; padding: 6px 8px;
  font-family: 'JetBrains Mono', monospace; font-size: 0.7rem;
  transition: border-color 150ms;
}
.dir-input:focus { outline: none; border-color: rgba(var(--gilt-rgb), 0.55); }
.dir-input::placeholder { color: var(--ink-soft); }
.dir-test { display: flex; gap: 6px; margin-top: 12px; }
.dir-result {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.78rem;
  color: var(--text-dim); margin-top: 8px; line-height: 1.45; min-height: 1em;
}
.dir-result.dir-ok { color: var(--sem-up); }
.dir-result.dir-bad { color: var(--sem-down); }
.sp-btn[disabled] { opacity: 0.45; cursor: not-allowed; }

/* THE CARTOGRAPHER'S TABLE (2026-08-15) — the render rows. A row is the whole
   button; the ON mark is a gilt edge, not a control of its own. */
.sp-maps { display: flex; flex-direction: column; gap: 6px; }
/* the two provisioning rows under the gallery (2026-08-16): upload + survey.
   The survey input IS a `.dir-input` — an address is data, and the directory's
   mono idiom already exists; `.spm-grow` only adds the flex. */
.spm-new { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
.spm-survey-row { display: flex; gap: 6px; }
.spm-grow { flex: 1; min-width: 0; }
.spm-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: rgba(0,0,0,0.18); border: 1px solid var(--ink-line); border-radius: 4px;
  padding: 6px 8px; cursor: pointer; text-align: left;
  color: var(--text); transition: border-color 150ms, background 150ms;
}
.spm-row:hover { border-color: rgba(var(--gilt-rgb), 0.5); }
.spm-row.on { border-color: var(--gilt); background: rgba(var(--gilt-rgb), 0.08); }
.spm-thumb {
  width: 52px; height: 34px; object-fit: cover; border-radius: 3px; flex-shrink: 0;
  border: 1px solid var(--ink-line); background: var(--abyss);
}
.spm-label { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.62rem; letter-spacing: 0.14em; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.spm-tag {
  margin-left: auto; font-family: 'Alegreya Sans', sans-serif; font-style: italic;
  font-size: 0.66rem; color: var(--text-dim); white-space: nowrap;
}
.spm-tag-theme { margin-left: 6px; color: var(--gilt); }

/* THE TILTED TABLE (Arc T v1, 2026-08-15) — pure CSS perspective on the
   viewport; the camera's math is untouched, which is why the settings hint
   calls the cursor's aim approximate. Ambient animations pause under tilt
   with the same discipline the Depths learned (2026-07-12): large composited
   layers repainting under a 3D transform read as flicker. */
/* (#viewport's transition sits on its base rule, by the stage) */
body.tilted #viewport {
  transform: perspective(1300px) rotateX(26deg);
  transform-origin: 50% 58%;
}
body.tilted .fog-a, body.tilted .fog-b { animation: none !important; }
body.tilted .mk .mk-ring { animation: none; }
@media (prefers-reduced-motion: reduce) { #viewport { transition: none; } }

.sp-sources { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.sp-src {
  font-family: 'JetBrains Mono', monospace; font-size: 0.68rem;
  padding: 2px 7px; border-radius: 20px; border: 1px solid currentColor;
}
.sp-src-ok { color: var(--tone-up); }
.sp-src-silent { color: var(--tone-down); }
.sp-src-age {
  font-family: 'JetBrains Mono', monospace; font-size: 0.66rem;
  color: var(--text-dim); margin-left: auto;
}

/* editing: dots show a move cursor + a gilt ring, breathing paused */
body.editing-map .mk { cursor: grab; }
body.editing-map .mk .mk-dot { box-shadow: 0 0 0 2px rgba(var(--gilt-rgb), 0.7); }
body.editing-map .mk.dragging { cursor: grabbing; z-index: 50; }
body.editing-map .mk .mk-ring { animation: none; }
/* THE CONTROL COLUMN (2026-08-15) — under ☰ at top-RIGHT, moved from top-left
   so one corner carries every control: menu on top, then ☁ ◈ ☾ 👁 + − reading
   down. It keeps the 2026-08-12 wins (a fixed home, no per-card measuring) and
   frees the whole left edge for the card. ⚠ The top was hand-baked (70px)
   from a band that has been rebuilt three times since; it now hangs off
   `--crown-clear` like everything else under the crown (2026-08-15), which
   also puts it just under the cluster THE ATLAS reveals at the same left edge.
   The column runs 2×42 + 10 = 94px, so from its top at `--crown-clear + 92`
   its foot sits at 250px — the LEFT-hand card (the default) carries that
   clearance; a right-hand card clears the crown alone.
   Grep MUST TRACK `#card`. */
/* ZOOM HANGS UNDER THE ATLAS (2026-08-18), and now BELOW the map-tools
   cluster there too (user directive, 2026-08-23, reversing zoom-on-top). It was
   top-RIGHT, sharing that corner with ⚙; zoom is the one pair reached for
   constantly, so it sits under the home button where the map tools gather.
   `left: 24px` is the same alignment `.atlas-tools` uses (18 inset + 1 border
   + 6 pad, +1 to centre the 42px circle). It takes the crown's clearance; the
   map tools drop beside it, not above it (owner, 2026-11-01).
   ⚠ THREE buttons since 2026-09-02 (the owner: the network glyph lives here
   permanently, under −, instead of in the hover cluster — it is reached for
   as often as zoom is), 42px, 10px gaps ⇒ 3×42 + 2×10 = 146px: this column
   runs from --crown-clear → its foot at 210px. `#card.card-left`'s clearance
   MUST TRACK that. */
.zoom-stack { position: fixed; left: 24px; right: auto; top: var(--crown-clear); bottom: auto; z-index: 963; display: flex; flex-direction: column; gap: 10px; transition: opacity 250ms, transform 220ms var(--glide); }
.zoom-stack .chrome { position: static; }
/* The open Index docks right and would lie over the column anyway — hide it
   outright so half-covered buttons can't take half-meant clicks. ☰ itself
   stays: it is the drawer's own toggle. */
body.drawer-open .zoom-stack { opacity: 0; pointer-events: none; }
/* Same corner, same rule (2026-08-15): ⚙ hangs off ☰ into the exact 42px the
   + button occupies — right 18/21, top `--crown-clear` for both — so the pair
   yields while the cluster is out and comes back when it goes. Found by
   hovering it, not by reading it: the draft had ⚙ at 962 over the stack's 960
   and would have left a half-covered + underneath, which is the very thing the
   rule above exists to prevent. It rides the stack's own 250ms opacity fade,
   so the corner reads as changing hands rather than as chrome twitching. */
/* (The old `body:has(.menu-tools.peek)` rule died with the move: ⚙ and zoom no
   longer share a corner, and a rule that can never fire reads as policy while
   doing nothing.) Zoom now yields to NOTHING — the tools cluster was moved
   below it instead, because hiding the control you use constantly to show the
   ones you use occasionally is the wrong way round. */

/* ── THE decoy feed WATCH — the decoys as a map layer (2026-08-15) ──────
   👁 (decoys.js) opens an eye on every place a stone guards, coloured by the
   loudest voice there: the abyss's teal for a silent watcher (deception is
   depths-kin), the semantic amber/red for stirred/SPEAKING, and grey when
   `sources.deception` says nobody is listening. Painted always, shown only
   while the layer is up; the unwatched map recedes so coverage — and its
   gaps — is the thing being read. */
.mk-eye {
  position: absolute; left: 8px; bottom: 8px; width: 17px; height: 17px;
  padding: 0; background: none; border: none; cursor: pointer;
  display: none; z-index: 7; color: var(--depth);
  filter: drop-shadow(0 1px 3px rgba(0,0,0,0.85));
  transition: transform 150ms var(--glide);
}
.mk-eye:hover { transform: scale(1.3); }
.mk-eye svg { width: 100%; height: 100%; display: block; }
/* one mark per map: the eye for a map that asks for it, else the bird */
body.tripwire-eye .tw-bird, body:not(.tripwire-eye) .tw-eye { display: none; }
body.decoys-on .mk-eye { display: block; }
/* With the watch up, the eye IS the marker (2026-09-02): it sits
   centred where the dot was, at roughly three times the dot's size, and the
   dot's core and breathing ring step aside rather than peeking out from
   behind it. The eye still carries the voice colour, so a speaking stone is
   the loudest thing on the map. Its click centres the map on the place
   (decoys.js) — the label under it names whose stone it is. */
body.decoys-on .mk.has-stone .mk-core,
body.decoys-on .mk.has-stone .mk-ring { visibility: hidden; }
body.decoys-on .mk.has-stone .mk-eye {
  left: 50%; top: 50%; bottom: auto; width: 28px; height: 28px;
  transform: translate(-50%, -50%);
  transition: width 200ms var(--glide), height 200ms var(--glide), transform 150ms var(--glide);
}
body.decoys-on .mk.has-stone .mk-eye:hover { transform: translate(-50%, -50%) scale(1.2); }
/* only the SELECTED place's eye grows (2026-09-03 — narrowing the
   first cut, where every watched eye was large): the one you are looking at
   doubles, the rest stay near the size of the dot they replaced. Both sizes
   went up 40% the same day ("increase the size of the decoy icons"). */
body.decoys-on .mk.has-stone.active .mk-eye { width: 56px; height: 56px; }
body.decoys-on .mk.has-stone.active .mk-eye:hover { transform: translate(-50%, -50%) scale(1.1); }
.mk-eye[data-voice="silent"]   { opacity: 0.85; }
.mk-eye[data-voice="unheard"]  { color: var(--text-dim); opacity: 0.8; }
.mk-eye[data-voice="stirred"]  { color: var(--sem-warn); }
.mk-eye[data-voice="speaking"] { color: var(--sem-down); animation: eye-cry 1.6s ease-in-out infinite; }
@keyframes eye-cry {
  0%, 100% { filter: drop-shadow(0 0 2px currentColor); }
  50%      { filter: drop-shadow(0 0 9px currentColor) drop-shadow(0 1px 3px rgba(0,0,0,0.85)); }
}
@media (prefers-reduced-motion: reduce) { .mk-eye[data-voice="speaking"] { animation: none; } }
body.decoys-on .mk:not(.has-stone) { opacity: 0.4; }
/* a watched place says its name — the eye alone doesn't tell you WHOSE stone */
body.decoys-on .mk.has-stone .mk-label { opacity: 1; transform: translateX(-50%) translateY(-2px); }

/* ── Scenes — a band at the crown of the screen, anchored LEFT ─────────────
   (2026-08-15 — out of the drawer, where reaching one cost ☰ → SCENES →
   pick. Left-anchored and given a fill the same day, second ask.)

   Three decisions, all Link's:

   1. **It starts at the left edge, not the middle.** Centring made the row's
      start move every time its contents changed width, and the top-left corner
      is genuinely free — the map tools became a column at top-RIGHT in v1.14.0
      (`.zoom-stack`), so the only thing sharing this row is ☰ at the far right.
      It takes the same 18px inset as every other piece of chrome.

   2. **One row, always.** It used to `flex-wrap`, so a narrow window turned the
      switcher into two or three stacked rows that pushed down over the map and
      moved every label. It is `nowrap` now and SCROLLS instead — a switcher
      that changes height is a switcher whose buttons are never twice in the
      same place. The scrollbar is hidden; the labels tighten first (see the
      ≤720px block), so scrolling is the last resort rather than the first.

   3. **It has a fill now.** The band was deliberately fill-less, each label
      carrying only a text-shadow like a marker label. Over a dark map that is
      elegant and over a bright stretch of parchment it is nearly unreadable —
      and this is the app's primary navigation, which has to be legible on all
      of it, always. The fill was the SAME `rgba(13,11,7,0.55)` + gilt hairline
      the round `.chrome` buttons use, so the band reads as one more piece of
      the same chrome rather than a new workstation.

   Because it now has a fill, it takes pointer events as a whole: a visible bar
   that lets clicks through its gaps to the map beneath is a bar that lies. */
#scenes-list {
  position: fixed; top: 18px; left: 18px; right: 18px;
  z-index: 960; display: flex; flex-wrap: nowrap; align-items: center;
  justify-content: flex-start; gap: 4px;
  padding: 3px 6px; border-radius: 4px;
  /* lighter than the round chrome's 0.55 since 2026-10-20 (owner: "a bit
     transparent"); the stronger blur is what keeps the labels legible over a
     bright stretch of map, the trap point 3 above describes */
  background: rgba(13,11,7,0.45);
  border: 1px solid rgba(var(--gilt-rgb), 0.18);
  backdrop-filter: blur(12px) saturate(1.1);
  transition: opacity 600ms, background 200ms, border-color 200ms;
}
/* The labels scroll in their own strip (owner, 2026-11-01): when the band itself
   scrolled, they ran under the buttons at its end, and nothing said there were more.
   ‹ › show only while something is out of reach (`.over`, ui.js). */
.band-labels {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 4px;
  overflow-x: auto; scrollbar-width: none;
}
.band-labels::-webkit-scrollbar { display: none; }
.band-arrow {
  display: none; flex: none; width: 22px; height: 30px;
  background: transparent; border: 1px solid transparent; border-radius: 2px;
  color: var(--gilt); font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: background 200ms, border-color 250ms;
}
#scenes-list.over .band-arrow { display: block; }
.band-arrow:hover:not(:disabled) { background: rgba(var(--gilt-rgb), 0.12); border-color: var(--gilt); }
.band-arrow:disabled { opacity: 0.25; cursor: default; }

/* ☰ ends the band. The auto margin that pushes it there sits on the ⚙ cluster
   just before it (below) — and it drops the 42px circle for the band's own
   height so the strip stays one line. */
#scenes-list .ci-menu {
  position: static; flex: none;
  width: 30px; height: 30px; font-size: 0.9rem;
}

/* ── ⚙ sits in the band, LEFT of ☰ (2026-09-04) ────────────────────
   It was a fixed cluster hanging BELOW ☰, outside the nav — the same escape
   from the band's backdrop-filter/overflow trap that `.atlas-tools` still
   takes. In flow the trap does not apply (it bites positioned descendants
   only), so the cluster is a flex item that rests at zero width and GROWS
   LEFTWARD out of ☰: ☰ is pinned to the band's end, so a widening item before
   it can only extend toward the scenes. `margin-left: auto` moved here from ☰
   — two auto margins would split the slack and park ⚙ mid-band.
   `overflow: hidden` does the reveal, which is also why the in-band ⚙ casts
   no glow (the lamp rule below has the same reason): a shadow outside the box
   would be clipped to a square edge.
   PINNED since 2026-10-31 (owner: keep it beside the hamburger): a control
   you have to hover to find is one you forget exists, and on a touch screen it
   was never there at all. */
#scenes-list .band-tail {
  /* beside the labels' strip, which is what keeps it on screen; never fixed or
     absolute (the trap above). These sit bare on the band (owner, 2026-11-01). */
  margin-left: auto; flex: none;
  display: inline-flex; align-items: center;
  padding-left: 6px; border-radius: 3px;
}
#scenes-list .menu-tools {
  flex: none;
  display: inline-flex; align-items: center;
  width: 68px; overflow: hidden;   /* two × (30 button + 4 gap): the scheme and the gear */
}
#scenes-list .chrome .ci-glyph { width: 16px; height: 16px; }   /* the band's 30px circles */
/* the band is their fill; `.chrome`'s own blur would still draw a disc on it */
#scenes-list .chrome { background: transparent; border-color: transparent; backdrop-filter: none; }
#scenes-list .chrome:hover { color: var(--gilt); border-color: transparent; box-shadow: 0 0 14px rgba(var(--gilt-rgb), var(--hover-glow-a)); }   /* ☰: the pattern every hover follows */
#scenes-list .menu-tools .chrome {
  position: static; flex: none; margin-right: 4px;
  width: 30px; height: 30px; font-size: 0.95rem;
}
#scenes-list .ci-gear:hover,
#scenes-list .ci-gear.active { box-shadow: none; }
#scenes-list .ci-scheme:hover { box-shadow: none; }   /* the same clip, the same reason */
/* ☀ wears the lit glyph while the light scheme is in force: pressed, not lamp */
#scenes-list .ci-scheme[aria-pressed="true"] { color: var(--gilt-bright); }
#scenes-list .ci-gear.active { border-color: rgba(var(--gilt-rgb), 0.7); color: var(--gilt-bright); }

/* ── ☰'s scene lamp is a lamp, not a beacon (2026-08-15) ─────────────
   `.chrome.lit` is built for a button floating over the map: filled gilt,
   `scale(1.16)`, and a 22px pulsing glow. Two things go wrong when that lands
   on the ☰ INSIDE the band, and they are the same thing seen twice:

   * **It shouts.** A scene being open is a state the band already states —
     the scene's own name is lit in Cinzel two inches to the left. ☰ only has
     to say "and the way back is here", which is a border and a colour.
   * **Its shadow is cut off.** The band sets `overflow-x: auto` for the
     one-row scroll, and CSS computes the *other* axis to `auto` with it — so
     the band clips vertically too. A 30px button grown to 34.8px inside a
     30px content box is clipped on both edges, and the glow, which lives
     entirely outside the button's box, is clipped away completely: what
     reaches the screen is a bright square edge, not a halo. That is the
     "messes up the shadow", and no shadow tuning fixes it — only not
     overflowing does.

   So the in-band lamp keeps the gilt border and bright glyph and drops the
   fill, the growth and the pulse. It cannot overflow, so it cannot be cut. */
#scenes-list .ci-menu.lit {
  background: rgba(13,11,7,0.28);           /* the lamp keeps a faint fill the bare buttons lack */
  border-color: rgba(var(--gilt-rgb), 0.7);
  color: var(--gilt-bright);
  transform: none;
  animation: none;
  box-shadow: none;
}

/* The band used to SHORTEN itself while the Index was open (`right: calc(320px
   + 12px)`), because the drawer was a full-height slab welded to the screen's
   right edge and would have covered the band's tail — where ☰ lives, which is
   the control you need to close it again. Since 2026-08-15 the drawer hangs
   BELOW the band instead of beside it, so the band keeps its full width and ☰
   stays put: a tab above its own sheet. The ≤720px override that undid this
   rule went with it. */

/* ── THE ATLAS is the home button: three jobs, one control ─────────────────
   (2026-08-15.)

   * **Hover** reveals the map tools — ☁ ◈ 👁 ☾ — as circles hanging below it.
     They are layers and journeys OF the atlas, so they belong to the atlas
     rather than to a permanent column: six buttons of chrome standing there
     forever, for controls reached occasionally, is a lot of screen spent on
     availability. Zoom stayed behind in `.zoom-stack` because it is the one
     pair reached constantly.
   * **Click** returns to the atlas when a scene is up, and toggles the Index
     when you are already on it. One button cannot mean two things at once, so
     it means the thing that is available: with a scene open the useful act is
     leaving it, and with no scene open that gesture is spare.
   * The cluster is **absent entirely while a scene is up** — none of those
     tools acts on a scene, and a hover that reveals four dead buttons is worse
     than no hover at all.

   ⚠ **The cluster is NOT inside the band**, and the reason is worth writing
   down because the obvious markup silently does not work. `#scenes-list`
   carries `backdrop-filter`, which makes it the containing block for
   `position: fixed` descendants — so a nested cluster is positioned against
   the BAND, not the viewport — and it carries `overflow-x: auto` for the
   one-row scroll, which then clips that cluster away entirely. Neither
   `fixed` nor `absolute` escapes it. So the cluster is a sibling of the band. */
.scene-home { position: relative; display: inline-flex; flex: none; }

.atlas-tools {
  /* One column just right of the zoom column (owner, 2026-11-01): `.zoom-stack`
     sits at left 24 and is 42 wide, so 24 + 42 + 10, its own gap. top is the
     crown's own clearance; see `--crown-b` in `:root`. */
  position: fixed; top: var(--crown-clear); left: 76px;
  z-index: 962;                             /* just over the band it hangs from */
  display: flex; flex-direction: column; gap: 8px;
  pointer-events: none;                     /* the map goes on under the gaps */
}
/* Each button hides for itself, not the column, because the tripwires are a
   layer you switched on, and a lit one stays in its slot to say so. Sky and Depths
   are places, and you can see you are there (owner, 2026-11-01), so they go with
   the rest. A touch screen cannot hover and this column is the door to the sky, so
   all stay. */
@media (hover: hover) {
  .atlas-tools:not(.peek) .chrome:not(.ci-decoys.lit) {
    opacity: 0; pointer-events: none; transform: translateY(-6px);
  }
}
/* the circles are the same chrome as everywhere else, just smaller and static */
.atlas-tools .chrome {
  position: static; width: 38px; height: 38px; flex: none; pointer-events: auto;
  transition: border-color 250ms, box-shadow 250ms, opacity 180ms, transform 180ms;
}

/* A scene is up: the tools have nothing to act on, so they do not appear at
   all — not even on hover. */
body.scene-open .atlas-tools { display: none; }

/* ── A scene is open: the band comes to the FRONT and firms up ─────────────
   The load-bearing half of this. Every scene overlay is `inset: 0` at z-985
   and the band sat at z-960, so opening a scene COVERED the switcher — the one
   control you want while you are in a scene is the one that was buried, and
   changing scenes meant leaving the one you were in first. It lifts to 988:
   above all three overlays, still below the command palette at 990, which is a
   modal and should cover everything. The fill firms up at the same time so it
   reads as chrome sitting deliberately over the scene rather than a strip that
   failed to be dimmed with the rest of the map. */
body.scene-open #scenes-list {
  z-index: 988;
  background: rgba(13,11,7,0.86);
  border-color: rgba(var(--gilt-rgb), 0.42);
  box-shadow: 0 12px 34px rgba(0,0,0,0.55);
}
.scene {
  background: transparent; border: 1px solid transparent; cursor: pointer;
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.66rem;
  letter-spacing: 0.22em; color: var(--text-dim);
  padding: 8px 11px 7px; border-radius: 2px; text-align: center; white-space: nowrap;
  text-shadow: 0 2px 8px rgba(0,0,0,0.9), 0 0 14px rgba(0,0,0,0.6);
  /* hover glows the accent, only the selected scene wears the outline (owner, 2026-11-01) */
  transition: color 200ms, background 200ms, border-color 250ms, text-shadow 200ms;
}
/* tighter before anything has to scroll */
@media (max-width: 1100px) {
  .scene { padding: 8px 5px 7px; letter-spacing: 0.1em; }
}
.scene:hover, :root[data-scheme="light"] .scene:hover {
  color: var(--gilt); text-shadow: 0 0 10px rgba(var(--gilt-rgb), 0.75);
}
.scene.active { color: var(--gilt); background: rgba(13,11,7,0.45); border-color: var(--gilt); }
/* (`.chron-fly` + its .t/.n lived here — the old scene-band flyout rows. The
   watchings' buttons are `.chron-btn` with a `.chron-fly-pop` tooltip; nothing
   has emitted `chron-fly` since. Removed 2026-08-21.) */

/* the artist's star mark, smoothed back into the sand.

   This used to be `backdrop-filter: blur(18px)`, which is an 18px GPU backdrop
   readback **inside `#stage`** — the element that translates and scales on
   every camera frame. It re-blurred a region of an 8 MB render every time the
   map moved, to hide a mark that never moves in an image that never changes.
   The file already knew: `body.depths #star-hide { backdrop-filter: none }`
   sits under a comment titled "Depths flicker kill".

   A soft radial wash in the sand tone does the same job — smudge a fixed spot
   in a fixed picture — and costs nothing to paint or to scale. (2026-07-31) */
#star-hide {
  position: absolute; left: 89.4%; top: 83.1%; width: 8%; aspect-ratio: 1.4;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: radial-gradient(ellipse at center,
    rgba(150, 124, 90, 0.94) 0%,
    rgba(150, 124, 90, 0.88) 42%,
    rgba(150, 124, 90, 0.55) 70%,
    rgba(150, 124, 90, 0) 100%);
  z-index: 2; pointer-events: none;
}

/* ── Network layer (routes) ─────────────────────────── */
/* (#overlay's z-index: 4 sits on its base rule, by the stage) */
/* VLAN territories — revealed with the network layer */
#territories { pointer-events: none; }
.territory {
  fill: var(--zt-hue); fill-opacity: 0; stroke: var(--zt-hue); stroke-opacity: 0;
  stroke-width: 2.5; stroke-linejoin: round;
  transition: fill-opacity 600ms ease, stroke-opacity 600ms ease;
}
.net-on .territory { fill-opacity: 0.09; stroke-opacity: 0.45; }
/* while a place is split open the VLAN hulls and their tags step far back —
   the ring, its spokes and its reach web are the subject, and with ◈ up the
   layers together read as noise (2026-08-01). A zone the user then
   selects still lifts out: .territory-selected carries !important. */
body.constellation #overlay .territory { fill-opacity: 0.03; stroke-opacity: 0.12; }
body.constellation #overlay .territory-tag { opacity: 0.12; }
#overlay path.route {
  fill: none; stroke-width: 2.5; stroke-linecap: round;
  stroke: var(--route-hue, var(--gilt));   /* the VLAN's colour (set per-line in world.js) */
  opacity: 0; transition: opacity 500ms ease, stroke-dashoffset 1400ms cubic-bezier(0.4, 0, 0.2, 1);
  /* The glow is scoped to `.net-on`. `opacity: 0` does not stop a filter being
     computed — an invisible road was still costing a drop-shadow on every
     raster of a layer that rasters on every zoom, twenty-two times over, for
     something nobody could see. (2026-07-31) */
  filter: none;
}
#overlay.net-on path.route {
  opacity: 0.85;
  filter: drop-shadow(0 0 5px var(--route-hue, rgba(var(--gilt-rgb), 0.5)));
}
/* internet / Depths reaches are dashed so the roads to the outside read differently
   from the LAN's solid VLAN lines */
.route-wan, .route-edge { stroke-width: 2; opacity: 0; }
.route-edge { stroke-width: 1.8; }
#overlay.net-on path.route-wan, #overlay.net-on path.route-edge { opacity: 0.7; }
/* the private mesh — a hidden way: green, finely dotted, softer than the roads */
.route-mesh { stroke-width: 1.8; opacity: 0; stroke: var(--tone-up); stroke-dasharray: 1.5 5; }
#overlay.net-on path.route-mesh { opacity: 0.6; }
/* the telemetry plane — logs + metrics flowing home to the monitoring host: faint teal threads */
.route-telemetry { stroke-width: 1.4; opacity: 0; stroke: var(--sem-info); stroke-dasharray: 1 6; }
#overlay.net-on path.route-telemetry { opacity: 0.42; }
/* a planned place — charted but not yet built: dashed ring, dimmed, no breath */
.mk.mk-planned .mk-dot { opacity: 0.6; }
.mk.mk-planned .mk-ring { border-style: dashed; animation: none; }
.mk.mk-planned .mk-label { opacity: 0.7; font-style: italic; }
/* a road to a dark host pulses so you can trace trouble through the web */
#overlay.net-on path.route-alarm { stroke-width: 3.4; filter: drop-shadow(0 0 8px var(--route-hue)); animation: route-throb 1.4s ease-in-out infinite; }
@keyframes route-throb { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }

/* reachability: when a place is selected, the base roads dim and its own links
   (drawn into #reach) burn bright so you can read what it talks to */
#overlay.net-on.reach-mode path.route { opacity: 0.12; }
#reach { pointer-events: none; }
#reach path.reach-line {
  fill: none; stroke: var(--route-hue, var(--gilt)); stroke-width: 3; stroke-linecap: round;
  filter: drop-shadow(0 0 7px var(--route-hue));
  transition: stroke-dashoffset 900ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* An aside, not a hop: something the service touches that its traffic does not
   travel through (CLOUDFLARE answering for a public name). Half-lit and thin,
   so a glance separates "the road" from "also involved" without needing a
   legend. Deliberately NOT dashed, tempting as that is: the ink-in reveal
   animates stroke-dashoffset against a dasharray the script sets to the line's
   own length, so a decorative dash pattern here would be overwritten on draw
   and the line would arrive solid anyway. Weight and opacity say it instead. */
#reach path.reach-line.reach-aside {
  stroke-width: 1.6; opacity: 0.5;
  filter: drop-shadow(0 0 4px var(--route-hue));
}
/* a reach-line to a dark host pulses amber — the same alarm as the roads (2026-07-20) */
#reach path.reach-line.reach-alarm {
  stroke-width: 3.8; filter: drop-shadow(0 0 9px var(--route-hue));
  animation: route-throb 1.4s ease-in-out infinite;
}
.mk.reach-lit .mk-core { transform: scale(1.5); }
.mk.reach-lit .mk-label { opacity: 1; transform: translateX(-50%) translateY(-2px); }
#markers:has(.reach-lit) .mk:not(.reach-lit) { opacity: 0.3; transition: opacity 300ms; }

/* ── Index drawer ───────────────────────────────────── */
#index-drawer {
  /* A panel that hangs from the band, not a slab that arrives from offscreen
     (2026-08-15). It was a floating panel married to the right screen
     edge (2026-08-12) that slid in from the RIGHT — chrome arriving from
     somewhere other than the control you just pressed. It now hangs directly
     under ☰ at the band's right end, on the band's OWN 18px inset rather than
     the screen edge, and UNFURLS downward out of it.

     Four borders now, because it floats on every side: square-ish shoulders
     under the bar (4px) and the existing rounded feet (14px). overflow:hidden
     still clips the list to the corners.

     The reveal is a clip, not a shove: `inset(0 0 100% 0)` holds it shut at
     the top edge and opens downward, so the panel is never drawn anywhere it
     could not have come from. Width (320) and z (970) are unchanged, so every
     `right:` clearance computed against this drawer still holds. */
  position: fixed; top: calc(var(--crown-b) + 4px); right: 18px; bottom: 12px;
  width: 320px; z-index: 970;
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--ink-line);
  border-radius: 4px 4px 14px 14px;
  box-shadow: 0 24px 60px rgba(var(--shade-rgb), calc(0.6 * var(--shade-k)));
  clip-path: inset(0 0 100% 0);
  transform: translateY(-10px);
  transition: clip-path 400ms var(--glide), transform 400ms var(--glide);
  display: flex; flex-direction: column; overflow: hidden;
}
#index-drawer.open { clip-path: inset(0); transform: none; }
/* A scene's dim runs FULL-BLEED under the floating drawer now (the shove that
   stopped overlays at the drawer's edge left a bright undimmed strip glowing
   around the Index — 2026-08-16). So with a scene up the drawer must
   come to the FRONT of that dim: 986 sits above the overlays at 985, below
   the band at 988 and ⚙ at 987. Only under `scene-open` — at its resting 970
   the ivory card (975) still rightly covers it. */
body.scene-open #index-drawer { z-index: 986; }
/* The head is the title and nothing else since 2026-08-15 — ⚙ and » both left
   it (Link). The panel hangs from ☰, so ☰ closes it; Esc and a click on the
   map still do too, and settings is a scene of its own — the ⚙ in the band
   beside ☰ (2026-09-04). It keeps `space-between` so a control could return
   without the title moving. */
.drawer-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 20px 14px; border-bottom: 1px solid var(--ink-line);
}
.drawer-title { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.72rem; letter-spacing: 0.3em; color: var(--gilt); }
.card-close {
  position: absolute; top: 6px; right: 8px; color: var(--card-ink-soft);
  background: none; border: none; font-size: 1.3rem;
  cursor: pointer; line-height: 1; padding: 4px 8px; transition: color 150ms;
}
.card-close:hover { color: var(--gilt); }

/* three tabs now that ⚙ has moved to the head — they share the width rather
   than sitting at their natural sizes and overflowing (2026-08-10) */
.drawer-tabs { display: flex; gap: 4px; padding: 10px 12px 0; }
.drawer-tabs .dtab { flex: 1 1 auto; min-width: 0; }
.dtab {
  flex: 1; background: transparent; border: 1px solid transparent; border-radius: 3px;
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.64rem; letter-spacing: 0.24em;
  color: var(--text-dim); padding: 8px 0 7px; cursor: pointer;
  transition: color 150ms, background 150ms, border-color 150ms;
}
.dtab:hover { color: var(--text); }
.dtab.active { color: var(--gilt); background: rgba(var(--gilt-rgb), 0.08); border-color: rgba(var(--gilt-rgb), 0.2); }
/* EVERY drawer section scrolls, not just the two lists (2026-08-12).
   SETTINGS grew the account block and THE DIRECTORY's eight-field form and now
   runs past the bottom on a short window, and the drawer itself is
   `overflow: hidden` — it has to be, to clip its contents to the rounded
   corners — so the overflow was not merely unreachable, it was invisible.
   `overflow-y: auto` is also what makes this work inside the column: a flex
   item's automatic minimum size is its content, EXCEPT for scroll containers,
   where it is zero — which is why these can finally shrink and scroll rather
   than pushing the drawer's own height out. */
/* (`#settings-panel` scrolled by this rule too until 2026-09-04; it carries
   its own now, in the SETTINGS scene's seat above.) */
#index-list, #services-list {
  padding: 10px 12px 24px;
  overflow-y: auto; min-height: 0;
  scrollbar-width: thin; scrollbar-color: var(--ink-line) transparent;
}
.svc-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  color: var(--text); text-decoration: none;
  border: 1px solid transparent; border-radius: 4px; padding: 7px 10px;
  transition: background 150ms, border-color 150ms;
}
.svc-row:hover { background: rgba(var(--gilt-rgb), 0.06); border-color: rgba(var(--gilt-rgb), 0.18); }
.svc-name { font-size: 0.9rem; }
.svc-host { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; color: var(--text-dim); }
/* K4 — per-service health dot (Kuma) */
.svc-health {
  width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
  display: inline-block; margin-left: 7px; vertical-align: middle;
  background: var(--ink-soft); transition: background 200ms, box-shadow 200ms;
}
.svc-health.h-up { background: var(--sem-up); box-shadow: 0 0 6px var(--sem-up); }
.svc-health.h-degraded { background: var(--sem-warn); box-shadow: 0 0 6px var(--sem-warn); }
.svc-health.h-down { background: var(--sem-down); box-shadow: 0 0 8px var(--sem-down); }
.svc-health.h-maint { background: var(--sem-warn); opacity: 0.7; }
.svc-health.h-slumber { background: var(--sem-slumber); }
/* pin (H2) */
.svc-row-wrap { display: flex; align-items: center; }
.svc-row-wrap .svc-row { flex: 1; }
.svc-pin {
  background: none; border: none; cursor: pointer; padding: 4px 8px;
  font-size: 0.9rem; line-height: 1; color: var(--ink-soft);
  opacity: 0; transition: opacity 150ms, color 150ms;
}
.svc-row-wrap:hover .svc-pin, .svc-pin.on { opacity: 1; }
.svc-pin.on { color: var(--gilt); }
.svc-pin:hover { color: var(--gilt-bright); }
.idx-zone {
  font-family: 'Cinzel', serif; font-weight: 400; font-size: 0.78rem;
  text-transform: uppercase; letter-spacing: 0.28em;
  color: var(--zone-hue, var(--text-dim));
  margin: 16px 8px 6px; display: flex; align-items: baseline; gap: 8px;
}
.idx-zone .sub { font-family: 'JetBrains Mono', monospace; font-style: normal; font-size: 0.62rem; color: var(--text-dim); }
.idx-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: none; border: 1px solid transparent; border-radius: 4px;
  padding: 8px 10px; cursor: pointer; text-align: left;
  color: var(--text); transition: background 150ms, border-color 150ms;
}
.idx-item:hover { background: rgba(var(--gilt-rgb), 0.06); border-color: rgba(var(--gilt-rgb), 0.18); }
.idx-dot, .pal-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.idx-name { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.7rem; letter-spacing: 0.18em; }
.idx-role { font-size: 0.78rem; color: var(--text-dim); margin-left: auto; text-align: right; max-width: 45%; }

/* ── Ivory detail card ──────────────────────────────── */
/* the detail card is a frosted parchment pane that slides in from the right edge
   as it takes focus (was a blur-up); raised ABOVE the Index so it's never hidden
   behind an open drawer. Translucent glass, but ≥0.9 alpha keeps the data legible. */
#card {
  /* width/right come from --card-w / --card-edge, the ONE definition camera.js's
     CARD_SHARE_PX and the .card-left calc below also read — so they can no
     longer drift and misaim the focus glide (ALIVE #19) */
  position: fixed; right: var(--card-edge); bottom: 60px; width: var(--card-w); z-index: 975;
  /* The clearance rides a custom property because the two sides need different
     ones since the control column moved to top-RIGHT (2026-08-15): a RIGHT-hand
     card shares its edge with ☰ + the column, so it stops below the column's
     foot — 60 bottom + (18 + 42 + 10 + 302 = 372 column, six buttons since the
     decoy eye) + 12 gap = 444. A LEFT-hand card (.card-left, the default)
     has a clear edge and only leaves air for the scene band: 60 + 90 = 150.
     MUST TRACK `.zoom-stack` — its inset, gap, button size AND button count.
     2026-08-15: the column dropped from SIX buttons to two (zoom only — every
     other map tool moved into the cluster THE ATLAS reveals) and ☰ left this
     corner for the scene band, so a right-hand card gets 214px of its height
     back: 60 bottom + (64 `--crown-clear` + 2×42 + 10 gap = 158 column) +
     12 gap = 230.
     🔴 2026-08-18 — THE BURDEN SWAPPED SIDES. Zoom moved out of this corner to
     hang under THE ATLAS, so a RIGHT-hand card no longer clears a column at
     all: all that remains on this edge is ⚙ under ☰ (38px, hover-only), giving
     60 + (64 + 38 = 102) + 12 = 174. The LEFT-hand card — which is the common
     one — inherits the old arithmetic, because the zoom column is now over
     THERE: see `.card-left`. Swapping the two numbers without reading this is
     how the card silently grows under the buttons it is meant to clear. */
  --card-clear: 174px;
  max-height: calc(100vh - var(--card-clear));
  background: color-mix(in srgb, var(--parchment) 91%, transparent);
  color: var(--card-ink);
  border-radius: 4px; overflow: visible;
  backdrop-filter: blur(12px) saturate(1.15);
  box-shadow: 0 24px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(var(--gilt-rgb),0.28);
  /* THE CARD ENTERS FROM THE EDGE IT LANDS ON (2026-08-25 — reverses
     the 2026-08-15 rule, the design notes decisions log).

     From 2026-08-15 both sides flew in from the left on the argument that the
     card is the map's annotation and should arrive from the same edge every
     time. Asked for directly: a panel that slides in from the far side of the
     screen and then parks on the near one reads as having been thrown across
     the map. Coming from behind its own edge, it reads as being pulled out of
     it — the shorter, quieter motion, and the one that matches where the eye
     is already going (the dot the focus glide just anchored).

     `--card-in` is the seam this was always meant to ride: it was minted for
     exactly this flip, so the direction is one property per side and no rule
     below has to out-specify `.visible`. The base value is the RIGHT-hand
     entrance, because the base `#card` is the right-pinned card (`right:
     var(--card-edge)`); `.card-left` flips it, and the ≤720px sheet — pinned
     to the BOTTOM, not to either side — rises from below instead. Every case
     is "from the edge it lands on"; none of them is a special case of it.

     ⚑ `right` animates only while the card is up (`#card.visible` below), and
     that is what makes the start point correct. With it here a hidden card that
     changed sides slid across the whole screen as it arrived, which is how the
     first click of every visit came in from the right (2026-11-01). */
  opacity: 0; transform: var(--card-in, translateX(34px) scale(0.985));
  pointer-events: none;
  transition: opacity 300ms var(--glide), transform 380ms var(--glide);
}
#card.card-left {
  --card-in: translateX(-34px) scale(0.985);
  right: calc(100vw - (var(--card-w) + var(--card-edge)));   /* right edge (card-w + edge = 344) from left ⇒ left edge at --card-edge (24) */
  --card-clear: 282px;
}
/* the body scrolls when a place has a lot; the pinned portrait/close don't.
   they need a higher z than the scroll body (a later sibling) or the body
   paints over them and the ✕ stops taking clicks. */
/* Tracks `#card`'s own max-height via the same custom property. The card is
   `overflow: visible` so the pinned portrait/⊖/–/× can sit outside it — which
   means a scroll body taller than its card does not get clipped, it SPILLS,
   so the two limits must be the one value, per side. */
.card-inner {
  max-height: calc(100vh - var(--card-clear, 174px)); overflow-y: auto; overflow-x: hidden;
  padding: 20px 20px 20px; position: relative;
}
#card .card-portrait, #card .card-close { z-index: 3; }
#card.visible {
  opacity: 1; transform: none; pointer-events: auto;
  transition: opacity 300ms var(--glide), transform 380ms var(--glide), right 380ms var(--glide);
}
/* When the Index opens, only a card the Index would actually cover moves (Link).
   The Index always docks right, so a RIGHT-hand card (right: 24px — its right
   edge sits 24px from the screen edge, inside the 320px drawer) is under it and
   slides clear; a LEFT-hand card (.card-left, the default since 2026-08-11)
   already clears the drawer on any desktop width and stays put instead of
   sliding across to hug the Index. `:not(.card-left)` is an exact proxy for
   "would be covered", not an approximation of it. */
body.drawer-open #card:not(.card-left) { right: 360px; }
/* A scene keeps the Index open (2026-08-11), and the PANELS cede the drawer's
   strip while the DIM does not (2026-08-16). The old rule pulled the whole
   overlay's `right` to the drawer's edge, which un-dimmed the strip along
   with un-covering it — a bright column of map glowing around the floating
   Index inside an otherwise darkened screen. The wrappers stay `inset: 0` so
   the dim runs everywhere (the drawer rises above it, see #index-drawer);
   only the centred panel's ROOM shrinks, via padding on the flex wrapper.
   The strip's one number is `--drawer-clear` (:root). */
body.drawer-open #reliquary, body.drawer-open #tribute, body.drawer-open #ledger,
body.drawer-open #calendar, body.drawer-open #seals,
body.drawer-open #stones, body.drawer-open #annals, body.drawer-open #accounts,
body.drawer-open #trust,
body.drawer-open #settings { padding-right: calc(var(--drawer-clear) + 3vw); }
body.drawer-open .pth-panel { right: calc(5vw + var(--drawer-clear)); }
/* The edge case: when the pan clamp can't grant a focused dot the space
   left of the card (east-edge places at focus zoom), the card crosses to the
   left edge and the dot keeps the right share — the drawer shove, mirrored,
   riding the same `right` transition. Stated as `right` (not `left`) so it
   animates, and so `body.drawer-open` above outranks it (1-1-1 vs 1-1-0):
   an open Index still shoves the card to its own left. */
/* 🔴 150 → 230 on 2026-08-18: the zoom column moved under THE ATLAS, i.e. over
   THIS edge. A left-hand card must now clear it exactly as a right-hand card
   used to — 60 bottom + zoom foot 250 (zoom now sits BELOW the cluster:
   64 `--crown-clear` + 92 offset + 2×42 + 10 gap = 250) + 12 = 322.
   This is the DEFAULT side for the detail card, so getting it wrong is the
   visible bug, not the edge case. MUST TRACK `.zoom-stack`. */
/* 2026-09-02: the zoom column grew a third button (the network glyph, the owner),
   so its foot moved 52px down: 64 + 92 + 3×42 + 2×10 = 302 ⇒ 60 + 302 + 12 = 374. */

.card-bar { height: 3px; border-radius: 4px 4px 0 0; background: linear-gradient(90deg, var(--card-hue, var(--gilt)) 0%, rgba(var(--gilt-rgb), 0.12) 90%); }
.card-portrait {
  width: 54px; height: 54px; border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--card-hue, #4E6E82), #1B2418 78%);
  border: 2px solid var(--parchment);
  box-shadow: 0 4px 14px rgba(0,0,0,0.35);
  position: absolute; top: -24px; left: 20px;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.5rem; line-height: 1;   /* emoji emblem, not a letter */
}
.portrait-logo { width: 27px; height: 27px; display: block; }
/* minimize button — sits just left of the × (2026-07-20) */
.card-min {
  position: absolute; top: 6px; right: 34px; color: var(--card-ink-soft);
  background: none; border: none; cursor: pointer; font-size: 1.3rem;
  line-height: 1; padding: 4px 8px; transition: color 150ms;
}
.card-min:hover { color: var(--gilt); }
#card .card-min { z-index: 3; }
/* minimized: collapse to just the headline; a click on the bar restores it */
#card.minimized { cursor: pointer; }
#card.minimized .card-epi,
#card.minimized .card-lore,
#card.minimized .card-data,
#card.minimized .card-vitals,
#card.minimized .card-links,
#card.minimized .card-scroll,
#card.minimized .card-svc { display: none; }
#card.minimized #card-name { margin-bottom: 0; }
#card.minimized .card-inner { padding-bottom: 14px; }
.card-epi {
  text-align: right; font-family: 'Alegreya Sans', sans-serif; font-style: italic;
  font-size: 0.78rem; letter-spacing: 0.04em;
  color: var(--card-ink-soft); margin-bottom: 14px; min-height: 1em;
}
#card h3 {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 1rem;
  letter-spacing: 0.2em; margin-bottom: 8px;
}
.card-lore { font-size: 0.9rem; line-height: 1.55; color: var(--card-ink-soft); margin-bottom: 12px; }
.card-data {
  font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: #6D6350;
  border-top: 1px solid rgba(33,28,20,0.14); padding-top: 9px; margin-bottom: 6px;
  display: flex; flex-direction: column; gap: 4px; font-variant-numeric: tabular-nums;
}
/* two aligned columns so hosts with many services (the app host) read as a tidy grid */
/* live vitals — CPU + RAM gauges on the node-exporter host cards */
.card-vitals { margin-top: 12px; }
.card-vitals[hidden] { display: none; }
.cv-head, .rel-ports-head {
  font-family: 'Cinzel', serif; font-size: 0.54rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--card-ink-soft); margin-bottom: 6px;
}
.cv-row { display: flex; gap: 8px; }
/* v1.34.0 — the pulse is native ink now (was two Grafana iframes, .cv-panel).
   Parchment tile: label, the number, and a week-long sparkline along the foot. */
.cv-spark {
  position: relative; flex: 1 1 0; min-width: 0; height: 68px;
  border: 1px solid rgba(var(--gilt-rgb),0.3); border-radius: 4px;
  background: var(--parchment-dim); overflow: hidden; padding: 7px 9px 0;
}
.cv-spark-label {
  display: block; font-family: 'Cinzel', serif; font-size: 0.5rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--card-ink-soft);
}
.cv-spark-now {
  display: block; font-family: 'JetBrains Mono', monospace;
  font-variant-numeric: tabular-nums; font-size: 1.05rem; font-weight: 600;
  color: var(--card-ink); line-height: 1.2;
}
.cv-spark-now em { font-style: normal; font-size: 0.7em; color: var(--card-ink-soft); }
.cv-spark-svg { position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 26px; display: block; }
.cv-spark-line { fill: none; stroke: var(--gilt); stroke-width: 1.6; }
.cv-spark-fill { fill: rgba(var(--gilt-rgb), 0.16); }

.card-links { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 10px; }
.card-link {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--discover); color: #F9F5D7; text-decoration: none;
  border: none; cursor: pointer; min-width: 0;
  font-family: 'Cinzel', serif; font-size: 0.6rem; font-weight: 700; letter-spacing: 0.12em;
  padding: 8px 10px 7px; border-radius: 2px; white-space: nowrap; overflow: hidden;
  transition: background 200ms, box-shadow 200ms;
}
.card-link:hover { background: var(--discover-hover); box-shadow: 0 4px 16px rgba(31,59,46,0.5); }
.card-link .svc-health { margin: 0; flex-shrink: 0; }

/* the scroll row — copies the doc path */
.card-scroll { margin-top: 12px; border-top: 1px solid rgba(33,28,20,0.14); padding-top: 9px; }
.scroll-row {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: none; cursor: pointer; padding: 4px 2px;
  text-align: left; border-radius: 3px;
  transition: background 150ms;
}
.scroll-row:hover { background: rgba(var(--gilt-rgb), 0.12); }
.scroll-glyph { font-size: 0.9rem; flex-shrink: 0; }
.scroll-path {
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem;
  color: #6D6350; word-break: break-all; line-height: 1.4;
}

/* ── Service detail sub-view (replaces the node view while it is up) ──────
   It was `position: absolute; inset: 0` — a sheet PAINTED OVER the node view,
   which meant it inherited the node card's height: a service with two lines
   of description floated in a parchment sheet sized for a host with fourteen
   services and three paragraphs of lore (2026-08-16 — "so much blank
   space on some of the services"). It is a FLOW child of `.card-inner` now:
   `#card.svc-open` hides the node view's own children, so the card is exactly
   as tall as the service's content, short for a short service and scrolling
   through `.card-inner`'s existing cap for a long one.
   The entrance always comes FROM THE LEFT — the same edge the card itself
   enters from (v1.19.1), regardless of which side the card sits on; the
   side-matched variant went with the absolute sheet. */
.card-svc {
  position: relative;
  color: var(--card-ink);
  display: flex; flex-direction: column;
  animation: svc-slide 260ms var(--glide);
}
@keyframes svc-slide { from { opacity: 0; transform: translateX(-18px); } to { opacity: 1; transform: none; } }
.card-svc[hidden] { display: none; }
/* the flip: while the pane is up the node view's elements give up their room
   (display, not opacity — a hidden node view must not hold the card open at
   its own height, which was the whole complaint) */
#card.svc-open .card-inner > :not(.card-svc) { display: none; }
.csv-back {
  align-self: flex-start; background: none; border: none; cursor: pointer;
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.6rem; letter-spacing: 0.14em;
  color: var(--card-ink-soft); padding: 2px 0 10px; transition: color 150ms;
}
.csv-back:hover { color: var(--gilt); }
.csv-epi {
  font-family: 'Alegreya Sans', sans-serif; font-style: italic; font-size: 0.76rem;
  color: var(--card-ink-soft); margin-bottom: 6px;
}
.csv-name {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 1rem; letter-spacing: 0.14em;
  display: flex; align-items: center; gap: 9px; margin-bottom: 12px; color: var(--card-ink);
}
.csv-name .ldot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.csv-state { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.csv-state-t { font-family: 'Alegreya Sans', sans-serif; font-size: 0.9rem; color: var(--card-ink); }
.csv-state-t.dim { color: var(--card-ink-soft); font-style: italic; }
.csv-desc {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.86rem; line-height: 1.5;
  color: var(--card-ink-soft); margin: 6px 0 auto; padding-bottom: 12px;
}
.csv-desc strong { color: var(--card-ink); font-weight: 700; }
/* ONE row (2026-08-15): OPEN IT HERE takes the width the two word-buttons
   leave; nothing wraps, so the pane stays as short as its prose */
.csv-actions { display: flex; flex-wrap: nowrap; align-items: stretch; gap: 6px; margin-top: 12px; }
.csv-open {
  flex: 1 1 auto; min-width: 0; text-align: center; white-space: nowrap;
  background: var(--discover); color: #F9F5D7; text-decoration: none;
  font-family: 'Cinzel', serif; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.16em;
  padding: 10px 10px 9px; border-radius: 2px; transition: background 200ms, box-shadow 200ms;
}
.csv-open:hover { background: var(--discover-hover); box-shadow: 0 4px 16px rgba(0,0,0,0.3); }
.csv-metrics {
  flex: 0 0 auto; white-space: nowrap;
  background: none; border: 1px solid var(--discover); color: var(--discover); cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.16em;
  padding: 10px 9px 9px; border-radius: 2px; transition: background 200ms, color 200ms;
}
.csv-metrics:hover { background: var(--discover); color: #F9F5D7; }
/* (`.csv-close` lived here — the service sub-view's own ✕. The sub-view uses
   `.csv-back` and the card's shared ✕ instead, and had stopped rendering one at
   all; the rules matched nothing. It also referenced `--ink-dim`, a token that
   does not exist in the palette. Removed 2026-07-31.) */
.csv-url { font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; color: var(--card-ink-soft); margin-top: 8px; word-break: break-all; }

/* ── Index search box ───────────────────────────────── */
.idx-search-wrap {
  display: flex; align-items: center; gap: 8px;
  margin: 10px 12px 2px; padding: 8px 10px;
  border: 1px solid var(--ink-line); border-radius: 4px; background: rgba(var(--shade-rgb), calc(0.14 * var(--shade-k)));
}
.idx-search-glyph { color: var(--gilt); font-size: 0.95rem; }
#idx-search {
  flex: 1; background: transparent; border: none; outline: none;
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.92rem; color: var(--text);
}
#idx-search::placeholder { color: var(--text-dim); }
.idx-search-clear {
  background: none; border: none; cursor: pointer; color: var(--text-dim);
  font-size: 1.1rem; line-height: 1; padding: 0 4px; transition: color 150ms;
}
.idx-search-clear:hover { color: var(--gilt); }
/* THE SEEKING's handoff row (2026-08-15): under the search box, only while a
   query is typed and the settings toggle is on — see ui.js updateSeekRow */
.idx-seek {
  display: flex; align-items: center; gap: 8px;
  margin: 6px 12px 0; padding: 7px 10px; width: calc(100% - 24px);
  background: none; border: 1px dashed rgba(var(--gilt-rgb), 0.35); border-radius: 4px;
  color: var(--text-dim); cursor: pointer; text-align: left;
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.84rem;
  transition: color 150ms, border-color 150ms, background 150ms;
}
.idx-seek:hover { color: var(--text); border-color: var(--gilt); background: rgba(var(--gilt-rgb), 0.06); }
.idx-seek[hidden] { display: none; }
.idx-seek-glyph { color: var(--gilt); }
.idx-seek-t strong { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.66rem; letter-spacing: 0.18em; color: var(--gilt); }

/* ── Toast + colophon ───────────────────────────────── */
#toast {
  /* CENTRED ON THE SCREEN (2026-09-09: "the top bar covers them").
     It rode at the crown from 2026-07-11 — top: 24px — which was clear until
     the scene band became a full-width strip at top: 18px (v1.16.0). The two
     have been landing on each other ever since: the toast wins on z-index and
     so is legible, but it prints across the band, which is the busiest chrome
     on the page. The drift note met the same wall and was moved to the foot
     on 08-18 for the same reason.
     The middle is the one part of the map guaranteed to be free of chrome —
     the band crowns it, the colophon and the drift note foot it, the zoom
     column holds the right. An announcement is brief and dismissible, so it
     may borrow the middle in a way a standing warning may not; that is
     exactly why the nag went to the foot and this did not follow it there. */
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.98);
  /* ABOVE EVERYTHING (2026-09-27). At 980 it sat under the settings panel
     (985), the Desk and the palette (990) and the glass (1000+) — so every
     toast fired FROM those surfaces ("The account is written.", "Current
     password is wrong.", the rack's "Saved") printed behind the panel that
     fired it and nobody ever saw it. It is brief and pointer-events: none,
     so the top of the stack costs nothing. */
  z-index: 1100; padding: 10px 22px;
  background: var(--parchment); color: var(--card-ink);
  font-family: 'Alegreya Sans', sans-serif; font-weight: 500; font-size: 0.92rem;
  letter-spacing: 0.04em;
  border-radius: 3px; box-shadow: 0 12px 40px rgba(0,0,0,0.6);
  opacity: 0; pointer-events: none;
  transition: opacity 250ms ease, transform 250ms ease;
}
/* opacity + a hair of scale, no slide — the design notes's card entrance. Centring
   means the transform now carries the centring itself, so both states must
   spell it out or the toast leaps to the corner mid-transition. */
#toast.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
#colophon {
  position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 940;
  font-family: 'Cinzel', serif; font-size: 0.6rem; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--ink-soft); pointer-events: none;
}

/* ⛶ enlarge — a dashboard filling the window (dim backdrop + a framed panel) */
#embed-max {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(6,5,3,0.82); backdrop-filter: blur(2px);
  display: flex; padding: 2.5vh 2.5vw;
}
#embed-max[hidden] { display: none; }
#embed-max-frame {
  flex: 1; width: 100%; height: 100%; border: 0; border-radius: 8px;
  background: #f4f2ea; box-shadow: 0 30px 90px rgba(0,0,0,0.6);
  opacity: 0; transition: opacity 260ms ease;
}
#embed-max.scried #embed-max-frame { opacity: 1; }
#embed-max .chron-scrying { z-index: 1; }
#embed-max.scried .chron-scrying { opacity: 0; }
#embed-max-close {
  position: absolute; top: 14px; right: 18px; z-index: 2;
  width: 36px; height: 36px; border-radius: 50%;
  background: rgba(13,11,7,0.72); border: 1px solid rgba(var(--gilt-rgb),0.45); color: var(--gilt);
  font-size: 1.35rem; line-height: 1; cursor: pointer; backdrop-filter: blur(4px);
  transition: border-color 150ms;
}
#embed-max-close:hover { border-color: var(--gilt); }

/* survey drift (Z3) — a quiet parchment chip, top-center, clicks to list the strays */
#drift-note {
  /* MOVED TO THE FOOT (2026-08-18). It sat at top-centre, directly under
     the scene band, where a standing warning competed with the crown every
     time the map opened. It is a nag, not an announcement: the foot is where a
     nag belongs. 46px clears the colophon (bottom 22 + ~10px of Cinzel) with
     air to spare, and the bottom is quieter than that #toast comment claims —
     the zoom stack left this corner entirely on 2026-08-18. */
  position: fixed; bottom: 46px; top: auto; left: 50%; transform: translateX(-50%); z-index: 945;
  display: flex; flex-direction: column-reverse; align-items: center; gap: 6px;
  background: color-mix(in srgb, var(--parchment) 92%, transparent); color: var(--card-ink);
  border: 1px solid rgba(var(--gilt-rgb),0.45); border-radius: 20px; padding: 7px 16px;
  cursor: pointer; backdrop-filter: blur(6px); box-shadow: 0 8px 24px rgba(0,0,0,0.22);
  font-family: 'Alegreya Sans', sans-serif; max-width: min(560px, 82vw);
  transition: border-radius 200ms, opacity 1s ease;
}
#drift-note[hidden] { display: none; }
/* faded out by signals.js; pointer-events, so a clear note cannot eat a map click */
#drift-note.resting { opacity: 0; pointer-events: none; }
#drift-note.open { border-radius: 12px; }
.drift-head { font-size: 0.82rem; letter-spacing: 0.01em; color: #8a5a1e; white-space: nowrap; }
.drift-list { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; padding-top: 2px; }
.drift-list[hidden] { display: none; }
.drift-item {
  font-family: 'JetBrains Mono', monospace; font-size: 0.66rem; color: var(--card-ink-soft);
  background: rgba(var(--gilt-rgb),0.1); border: 1px solid rgba(var(--gilt-rgb),0.2);
  border-radius: 10px; padding: 2px 8px;
}

/* ── Live signal states ──────────────
   Healthy (and no-signal) keeps the untouched zone-hue breathing — only trouble
   repaints, so the eye is drawn to the dark, not to the whole board. Semantic
   colours reuse the design palette; no new colours introduced. */
.mk.s-degraded { --mk-hue: var(--sem-warn); --mk-glow: rgba(250,189,47,0.45); }
.mk.s-down     { --mk-hue: var(--sem-down); --mk-glow: rgba(var(--down-rgb),0.5); }
/* urgent alarm pulse (distinct from the calm breathe) + a throbbing core */
.mk.s-down .mk-ring { animation: alarm-throb 1.05s ease-out infinite; border-width: 2px; }
.mk.s-down .mk-core { animation: core-throb 1.05s ease-in-out infinite; box-shadow: 0 0 11px var(--sem-down), 0 0 28px rgba(var(--down-rgb),0.8); }
@keyframes alarm-throb { 0% { transform: scale(0.5); opacity: 1; } 55% { transform: scale(2); opacity: 0; } 100% { transform: scale(2); opacity: 0; } }
@keyframes core-throb { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.35); } }
/* K3 — a beacon flare rises over any place gone dark */
.mk.s-down::after {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 4px; height: 46px; transform: translate(-50%, -100%);
  background: linear-gradient(to top, rgba(var(--down-rgb),0.85), rgba(var(--down-rgb),0));
  filter: blur(1px); pointer-events: none;
  animation: beacon 1.6s ease-in-out infinite;
}
@keyframes beacon { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.9; } }
.mk.s-slumber { --mk-hue: var(--sem-slumber); --mk-glow: rgba(146,131,116,0.22); opacity: 0.5; }
.mk.s-slumber .mk-ring { animation: none; opacity: 0.2; }
/* Z2 maintenance — "under repair": calm amber, a wrench, alarms suppressed */
.mk.s-maint { --mk-hue: var(--sem-warn); --mk-glow: rgba(250,189,47,0.3); opacity: 0.82; }
.mk.s-maint .mk-ring { animation-duration: 5s; opacity: 0.5; }
.mk.s-maint::after {
  content: '⚒'; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -168%); font-size: 11px; color: #E4C878;
  text-shadow: 0 1px 4px rgba(0,0,0,0.9); pointer-events: none;
}

/* the colophon carries the alarm (K3) or the quiet 'watchers are silent' note */
#colophon.alarm { color: var(--sem-down); letter-spacing: 0.16em; }
#colophon.silent { color: var(--ink-soft); }

/* K5 — 'faith of the realm' uptime on the ivory card */
.card-uptime { color: var(--discover); font-weight: 600; }

/* ── Command palette — dark finding-glass, drawer idiom ── */
#palette {
  position: fixed; inset: 0; z-index: 990;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 16vh; background: rgba(6,5,3,0.45);
  opacity: 0; pointer-events: none; transition: opacity 160ms ease, backdrop-filter 220ms ease;
}
/* Dark: everything behind the finding-glass drains to grey, so the one thing in
   colour is what you are typing into. Light: a soft cream wash and a little
   blur instead — grey under a cream panel reads as broken, not as focus
   (2026-10-20). Browsers without backdrop-filter keep the plain wash. */
#palette.open { opacity: 1; pointer-events: auto; backdrop-filter: grayscale(1); -webkit-backdrop-filter: grayscale(1); }
:root[data-scheme="light"] #palette { background: rgba(203, 213, 225, 0.22); }
:root[data-scheme="light"] #palette.open { backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
.pal-panel {
  width: min(560px, 92vw);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--ink-line); border-radius: 8px;
  box-shadow: 0 24px 70px rgba(var(--shade-rgb), calc(0.7 * var(--shade-k)));
  transform: translateY(-8px); transition: transform 160ms var(--glide);
  overflow: hidden;
}
#palette.open .pal-panel { transform: translateY(0); }
.pal-input-wrap {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px; border-bottom: 1px solid var(--ink-line);
}
.pal-glyph { color: var(--gilt); font-size: 1.1rem; }
#pal-input {
  flex: 1; background: transparent; border: none; outline: none;
  font-family: 'Alegreya Sans', sans-serif; font-size: 1.05rem; color: var(--text);
  letter-spacing: 0.01em;
}
#pal-input::placeholder { color: var(--text-dim); }
.pal-hint {
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem;
  letter-spacing: 0.12em; color: var(--ink-soft);
}
#pal-list { max-height: 46vh; overflow-y: auto; padding: 6px; }
.pal-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: transparent; border: 1px solid transparent; border-radius: 4px;
  padding: 9px 11px; cursor: pointer; text-align: left;
  transition: background 120ms, border-color 120ms;
}
.pal-row.active { background: rgba(var(--gilt-rgb), 0.09); border-color: rgba(var(--gilt-rgb), 0.22); }
.pal-kind { color: var(--text-dim); font-size: 0.8rem; width: 12px; text-align: center; flex-shrink: 0; }
.pal-label {
  font-family: 'Cinzel', serif; font-weight: 600; font-size: 0.8rem;
  letter-spacing: 0.08em; color: var(--text); white-space: nowrap;
}
.pal-sub {
  flex: 1; text-align: right; font-family: 'JetBrains Mono', monospace;
  font-size: 0.66rem; color: var(--text-dim); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.pal-empty {
  padding: 20px 14px; text-align: center; color: var(--text-dim);
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.92rem;
}

/* ── THE LEDGER — IPAM /24 grids ── */
/* The wrapper stays `inset: 0` — the dim should run to the top of the screen,
   BEHIND the band, or a scene would read as a window rather than as the map
   being covered. Only the panel's room changes: it is seated below the crown
   (2026-08-15) instead of centred in the whole viewport, where a tall panel
   slid up under the band and the band survived only by lifting to z-988 and
   firming its own fill. The entrance (translateY(10px) → 0) is untouched and
   now reads as the panel settling into its seat. */
/* ONE scene chrome (2026-08-21). THE LEDGER family, THE TRIBUTE and THE
   RELIQUARY each carried a byte-identical copy of the wrapper, the `.open`
   state, and the head/title/sub/close quartet under their own prefix — five
   rules, three times. The prefixes stay (the JS and templates name them); the
   declarations live here, once, where the first scene declares them. Each
   scene's PANEL keeps its own rule below because the widths genuinely differ:
   the ledgers' 880, THE TRIBUTE's 860, THE ARMORY's 1000, SETTINGS' 600 (forms,
   not ledgers) and THE CALENDAR's 1180 (a day is wider than a list). */
#ledger, #calendar, #seals, #stones, #tribute, #reliquary, #accounts, #trust, #settings {
  position: fixed; inset: 0; z-index: 985;
  display: flex; align-items: center; justify-content: center;
  padding: calc(var(--crown-clear) + 1vh) 3vw 4vh; background: rgba(6,5,3,0.7);
  opacity: 0; pointer-events: none; transition: opacity 200ms ease;
}
#ledger.open, #calendar.open, #seals.open, #stones.open, #tribute.open, #reliquary.open, #accounts.open, #trust.open, #settings.open { opacity: 1; pointer-events: auto; }
.ldg-panel {
  display: flex; flex-direction: column;
  /* 92vh was the whole viewport less a margin; the seat below the crown is
     what is left after `--crown-clear` (grep MUST TRACK the wrapper above) */
  width: min(880px, 96vw); max-height: calc(100vh - var(--crown-clear) - 6vh);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--ink-line); border-radius: 8px;
  box-shadow: 0 28px 80px rgba(var(--shade-rgb), calc(0.75 * var(--shade-k)));
  transform: translateY(10px); transition: transform 200ms var(--glide);
  overflow: hidden;
}
#ledger.open .ldg-panel, #calendar.open .ldg-panel, #seals.open .ldg-panel,
#stones.open .ldg-panel, #accounts.open .ldg-panel, #trust.open .ldg-panel,
#settings.open .ldg-panel { transform: translateY(0); }
/* SETTINGS' seat (2026-09-04): the shared panel, narrower. Its sections were
   drawn for the 320px drawer — label-left/switch-right rows, the eight-field
   directory form, a ledger of knobs — and at 880 a row's two ends stop
   reading as one row. 600 is a document's width, not a ledger's. */
#settings .ldg-panel { width: min(600px, 96vw); }
.ldg-head, .trb-head, .rel-head {
  display: flex; align-items: baseline; gap: 14px;
  padding: 18px 22px 16px; border-bottom: 1px solid var(--ink-line);
}
.ldg-title, .trb-title, .rel-title { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.9rem; letter-spacing: 0.3em; color: var(--gilt); }
.ldg-sub, .trb-sub, .rel-sub { font-family: 'IM Fell English', 'Alegreya Sans', serif; font-style: italic; font-size: 0.9rem; color: var(--text-dim); }
.ldg-close, .trb-close, .rel-close { margin-left: auto; background: none; border: none; color: var(--text-dim); font-size: 1.4rem; cursor: pointer; line-height: 1; padding: 0 4px; transition: color 150ms; }
.ldg-close:hover, .trb-close:hover, .rel-close:hover { color: var(--gilt); }
.ldg-list { display: flex; flex-wrap: wrap; gap: 26px 34px; padding: 24px 22px; overflow-y: auto; }
.ldg-block { display: flex; flex-direction: column; gap: 10px; }
.ldg-zhead { display: flex; flex-direction: column; gap: 2px; }

/* a folded ground keeps its head — the count is the point of leaving it visible
   — and drops everything below it. the monitoring zone and the lookout zone open this way
   (ledger.js FOLDED_BY_DEFAULT); any head can be folded by clicking it. */
.ldg-zhead[role="button"] { cursor: pointer; position: relative; padding-left: 15px; }
.ldg-zhead[role="button"]:hover .ldg-zname { color: var(--gilt); }
.ldg-zhead[role="button"]:focus-visible { outline: 1px solid var(--gilt); outline-offset: 3px; }
.ldg-fold {
  position: absolute; left: 0; top: 2px;
  font-size: 0.7rem; line-height: 1; color: var(--text-dim);
  transition: transform 160ms ease, color 160ms;
}
.ldg-zhead[role="button"]:hover .ldg-fold { color: var(--gilt); }
.ldg-folded > .ldg-zhead .ldg-fold { transform: rotate(-90deg); }
.ldg-folded > *:not(.ldg-zhead) { display: none; }
/* the block's own flex gap would still reserve space with the children gone.
   align-self matters just as much: .ldg-list is a wrapping flex ROW at the
   default align-items:stretch, so a folded block was still drawn as tall as the
   tallest zone beside it — hiding its body but reclaiming none of the height,
   which is the whole point of folding it. */
.ldg-folded { gap: 0; align-self: flex-start; }
.ldg-zname { font-family: 'Cinzel', serif; font-weight: 600; font-size: 0.74rem; letter-spacing: 0.12em; color: var(--text); }
.ldg-zsub { font-family: 'JetBrains Mono', monospace; font-size: 0.64rem; color: var(--text-dim); }
.ldg-zcount { font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; color: var(--ink-soft); letter-spacing: 0.06em; }
.ldg-grid {
  display: grid; grid-template-columns: repeat(16, 11px); grid-auto-rows: 11px; gap: 2px;
  padding: 8px; border: 1px solid var(--ink-line); border-radius: 4px; background: rgba(var(--shade-rgb), calc(0.25 * var(--shade-k)));
}
.ldg-cell { width: 11px; height: 11px; border-radius: 2px; background: rgba(201,191,168,0.05); }
.ldg-cell.edge { background: rgba(var(--shade-rgb), calc(0.35 * var(--shade-k))); }   /* .0 network · .255 broadcast */
.ldg-cell.lit {
  background: var(--c-hue); box-shadow: 0 0 5px var(--c-hue); cursor: pointer;
  transition: transform 120ms;
}
.ldg-cell.lit:hover { transform: scale(1.35); box-shadow: 0 0 10px var(--c-hue); }

/* ── THE TRIBUTE — RAM allocation ── */
/* wrapper + .open: the shared scene chrome beside #ledger */
.trb-panel {
  display: flex; flex-direction: column;
  width: min(860px, 96vw); max-height: calc(100vh - var(--crown-clear) - 6vh);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--ink-line); border-radius: 8px;
  box-shadow: 0 28px 80px rgba(var(--shade-rgb), calc(0.75 * var(--shade-k)));
  transform: translateY(10px); transition: transform 200ms var(--glide);
  overflow: hidden;
}
#tribute.open .trb-panel { transform: translateY(0); }
/* .trb-head / .trb-title / .trb-sub / .trb-close: the shared scene chrome beside .ldg-head */

/* the vessel — 64 GB divided into illuminated segments */
.trb-vessel-wrap { padding: 22px 22px 8px; }
.trb-vessel {
  display: flex; width: 100%; height: 30px;
  border: 1px solid var(--ink-line); border-radius: 4px; overflow: hidden;
  background: rgba(var(--shade-rgb), calc(0.28 * var(--shade-k)));
}
.trb-seg {
  height: 100%; background: var(--seg-hue);
  box-shadow: inset 0 0 0 1px rgba(var(--shade-rgb), calc(0.25 * var(--shade-k))), inset 0 8px 10px rgba(255,255,255,0.12);
  opacity: 0.82; transition: opacity 150ms;
}
.trb-vessel:hover .trb-seg { opacity: 0.55; }
.trb-vessel .trb-seg:hover { opacity: 1; }
.trb-seg.reserve { background: repeating-linear-gradient(45deg, #3A342A, #3A342A 4px, #2b271f 4px, #2b271f 8px); box-shadow: none; }
.trb-seg.free { background: none; box-shadow: inset 0 0 0 1px rgba(var(--gilt-rgb), 0.12); opacity: 1; }
.trb-summary {
  display: flex; gap: 20px; align-items: baseline; flex-wrap: wrap;
  margin-top: 12px; font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
  color: var(--text-dim); font-variant-numeric: tabular-nums;
}
.trb-sum-big { font-size: 1.1rem; color: var(--gilt); }
.trb-sum-big small { font-size: 0.72rem; color: var(--text-dim); }
.trb-sum-free { color: var(--tone-up); }
/* Two vessels now (memory, then processors), so each says which it is. Cinzel
   micro-caps, the same voice the scene buttons use. */
.trb-cap {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.58rem;
  letter-spacing: 0.26em; color: var(--text-dim); margin-bottom: 7px;
}
.trb-cap-2 { margin-top: 20px; }
/* Assigning more vCPU than there are threads is ordinary practice, not a
   fault — so it is stated in the warn tone, never the down tone. */
.trb-sum-over { color: var(--sem-warn, var(--tone-warn)); }

/* per-VM allocation rows */
.trb-list { padding: 10px 22px 22px; overflow-y: auto; }
.trb-row {
  display: grid; grid-template-columns: 150px 1fr auto; align-items: center; gap: 16px;
  width: 100%; background: none; border: none; border-radius: 4px;
  padding: 9px 10px; cursor: pointer; text-align: left;
  transition: background 150ms;
}
.trb-row:hover { background: rgba(var(--gilt-rgb), 0.06); }
.trb-row.off { opacity: 0.5; }
.trb-name { font-family: 'Cinzel', serif; font-weight: 600; font-size: 0.74rem; letter-spacing: 0.1em; color: var(--text); white-space: nowrap; }
.trb-bar { position: relative; height: 14px; background: rgba(var(--shade-rgb), calc(0.3 * var(--shade-k))); border-radius: 3px; overflow: hidden; }
.trb-track {
  position: absolute; inset: 0 auto 0 0; height: 100%;
  background: color-mix(in srgb, var(--row-hue) 30%, transparent);
  border-right: 2px solid var(--row-hue); border-radius: 3px;
}
.trb-fill { position: absolute; inset: 0 auto 0 0; height: 100%; background: var(--row-hue); opacity: 0.85; }
.trb-stat { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--text-dim); white-space: nowrap; font-variant-numeric: tabular-nums; }

@media (max-width: 720px) {
  .trb-row { grid-template-columns: 110px 1fr; }
  .trb-stat { grid-column: 2; }
}

/* ── THE RELIQUARY — rack elevation (NetBox-style) ── */
/* wrapper + .open: the shared scene chrome beside #ledger */
.rel-panel {
  display: flex; flex-direction: column;
  width: min(1000px, 96vw); max-height: calc(100vh - var(--crown-clear) - 6vh);
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--ink-line); border-radius: 8px;
  box-shadow: 0 28px 80px rgba(var(--shade-rgb), calc(0.75 * var(--shade-k)));
  transform: translateY(10px); transition: transform 200ms var(--glide);
  overflow: hidden;
}
#reliquary.open .rel-panel { transform: translateY(0); }
/* .rel-head / .rel-title / .rel-sub / .rel-close: the shared scene chrome beside .ldg-head */
.rel-body { display: flex; gap: 22px; padding: 22px; overflow-y: auto; }

/* the rack elevation.
   `width` is the niche width (460, `#rel-elevation` / `.rel-niche` below), and
   it is stated here for the FLEX ITEM, not just the elevation inside it: the
   column used to be `flex-shrink: 0` with no width, so it sized itself to its
   widest child — and that child was the rear-of-rack strip, one row of three
   buttons as long as the longest Description (v1.39.1 put the surveyor's own
   words on those buttons). A 916px strip made a 916px column, the detail pane
   was pushed clean off the right edge of a 1000px panel, and the body grew a
   horizontal scrollbar the pane had to be found with. A fixed column lets the
   strip wrap inside it (v1.45.1). The sheet is border-box, so the 26px of
   power-rail padding is inside the width and is added here, not assumed. */
.rel-rack { position: relative; padding-left: 26px; flex: none; width: calc(26px + 460px); }
.rel-power {
  position: absolute; left: 8px; top: 0; bottom: 0; width: 2px;
  background: linear-gradient(180deg, rgba(var(--gilt-rgb), 0), var(--gilt) 15%, var(--gilt) 85%, rgba(var(--gilt-rgb), 0));
  opacity: 0.5;
}
.rel-power span {
  position: absolute; left: -4px; top: 50%; transform: rotate(180deg); writing-mode: vertical-rl;
  font-family: 'JetBrains Mono', monospace; font-size: 0.52rem; letter-spacing: 0.14em;
  color: var(--ink-soft); white-space: nowrap;
}
#rel-elevation {
  /* 460 is the niche width (`.rel-niche`, THE ARMORY) — the niches are absolute
     children, so the frame must be exactly as wide as they are. This rule used
     to say 320 and be overridden from the ARMORY block further down; now it
     says the number that renders, once. The flex layout is moot for absolute
     niches but the box styling is live (2026-08-12 survey). */
  display: flex; flex-direction: column; gap: 3px; position: relative;
  width: 460px; padding: 8px; border: 1px solid var(--ink-line);
  border-radius: 4px; background: rgba(0,0,0,0.25);
}
/* detail panel */
.rel-detail {
  flex: 1; min-width: 220px; align-self: flex-start;
  border-left: 3px solid var(--rel-hue, var(--gilt)); padding-left: 18px;
}
.rel-detail-epi { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; letter-spacing: 0.1em; color: var(--text-dim); text-transform: uppercase; }
.rel-detail-name { font-family: 'Cinzel', serif; font-weight: 700; font-size: 1.2rem; letter-spacing: 0.06em; color: var(--text); margin: 4px 0 8px; }
.rel-detail-lore { font-family: 'Alegreya Sans', sans-serif; font-size: 0.92rem; line-height: 1.5; color: var(--text-dim); margin-bottom: 12px; }
.rel-detail-data { display: flex; flex-direction: column; gap: 4px; font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
/* the slot's own Description (v1.39.1) — the surveyor's words about what the
   slot holds, a shade brighter than the place's measured facts around it */
.rel-detail-desc { color: var(--text); }
.rel-detail-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.rel-map-btn {
  margin-top: 14px; background: var(--discover); color: #D9CDAF; border: none;
  font-family: 'Cinzel', serif; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.16em;
  padding: 8px 14px; border-radius: 2px; cursor: pointer; transition: background 200ms;
}
.rel-map-btn:hover { background: var(--discover-hover); }

/* rear-of-rack devices (NAS, PDU) — not in the front elevation */
.rel-rear { margin-top: 14px; display: flex; flex-wrap: wrap; align-items: stretch; gap: 8px; }
.rel-rear-label { align-self: center; font-family: 'JetBrains Mono', monospace; font-size: 0.56rem; letter-spacing: 0.16em; color: var(--ink-soft); text-transform: uppercase; margin-right: 2px; }
/* `min-width: 0` + `max-width: 100%`: a rear vessel's Description is a
   sentence, and a button that refused to be narrower than its sentence is
   what widened the whole column (see `.rel-rack`). It wraps now. */
.rel-rear-item { background: rgba(var(--shade-rgb), calc(0.22 * var(--shade-k))); border: 1px dashed var(--ink-line); border-radius: 3px; padding: 5px 11px; cursor: pointer; display: flex; flex-direction: column; gap: 1px; text-align: left; transition: border-color 150ms; min-width: 0; max-width: 100%; }
.rel-rear-item:hover { border-color: rgba(var(--gilt-rgb), 0.5); }
.rel-rear-name { font-family: 'Cinzel', serif; font-weight: 600; font-size: 0.64rem; letter-spacing: 0.08em; color: var(--text-dim); }
.rel-rear-desc { font-family: 'JetBrains Mono', monospace; font-size: 0.54rem; color: var(--ink-soft); }

@media (max-width: 720px) {
  .rel-body { flex-direction: column; }
  /* (`#rel-elevation { width: 100% }` sat here and never applied — a later
     equal-specificity rule held the width, and a media query adds no
     specificity. The niches are a fixed 460 anyway. Removed 2026-08-21.) */
}

/* chrome fades in after the veil lifts — the scene band is chrome too */
body.pre .chrome, body.pre #colophon,
body.pre #scenes-list { opacity: 0; pointer-events: none; }
/* (a second `#scenes-list { transition: opacity 600ms; }` sat here, and at equal
   specificity it REPLACED the base rule's shorthand — so the band's
   background/border-color fades under `body.scene-open` snapped instead of
   easing. The base rule already carries the opacity fade. Removed 2026-08-21.) */

@media (max-width: 720px) {
  /* The band is shorter here (2px pad instead of 3), so the crown is
     re-MEASURED rather than guessed: 18 inset + 1 + 2 + 30 row + 2 + 1 = 54.
     Everything seated under it — both hanging clusters, the scene panels, THE
     PATH, the Index — follows from this one line. */
  :root { --crown-b: 54px; }
  /* #card.card-left listed too: the bottom sheet must win over a flip class
     left behind by a resize (focusAnchor never sets it at this width). The
     drawer-open selector carries :not(.card-left) to match the desktop rule's
     specificity, so — later in source — it still wins and collapses the card. */
  #card, body.drawer-open #card:not(.card-left), #card.card-left { right: 12px; left: 12px; width: auto; bottom: 70px; }
  /* The sheet's clearance, re-measured for THIS width (2026-08-21): the desktop
     numbers (174 / 230) are built on `bottom: 60` and the 38px ⚙, and neither
     holds here — 70 bottom + (62 `--crown-clear` + 34, the ⚙ that hung under
     the crown until 2026-09-04, kept as headroom) + 12 = 178. Without this
     the sheet's max-height was 10px
     (or, with a stale .card-left, 70px) taller than the room it has. */
  #card, #card.card-left { --card-clear: 178px; }
  /* Pinned to the BOTTOM here, not to either side — so it rises, by the same
     rule that sends the desktop card in from its own edge (2026-08-25). Both
     selectors, because `.card-left` may be left over from a resize:
     focusAnchor never sets it at this width, but it does not clear it either. */
  #card, #card.card-left { --card-in: translateY(34px) scale(0.985); }
  #index-drawer { width: 86vw; }
  /* The band tightens BEFORE it scrolls — it can no longer wrap at all, so
     this is what keeps the whole row reachable without dragging it. */
  .scene { padding: 6px 7px 5px; letter-spacing: 0.14em; font-size: 0.6rem; }
  #scenes-list { padding: 2px 4px; }
  /* (the band no longer shortens for the drawer at any width — the drawer
     hangs BELOW it now, so the override that undid that shortening here is
     gone with the rule it was undoing) */
  .atlas-tools { gap: 6px; }
  .atlas-tools .chrome { width: 34px; height: 34px; }
}

@media (prefers-reduced-motion: reduce) {
  .mk-ring, .fog, #sky-island, #sky-shadow, .mk.s-down::after { animation: none !important; }
  .mk.s-down::after { opacity: 0.7; }   /* beacon stays as a static marker */
  #palette, .pal-panel { transition: none; }
  /* the Index unfurls out of the band (2026-08-15) — reduced motion gets the
     same geometry, arrived at instantly */
  #index-drawer { transition: none; }
  #sky-island { transform: translate(-50%, -50%) translateY(-40px); }
  .veil-title { animation-duration: 0.01s; animation-delay: 0s; }
  .veil-rule path { animation: none; stroke-dashoffset: 0; }
  #stage.gliding { transition: none; }
  #veil { transition-duration: 0.3s, 0s; transition-delay: 0s, 0.3s; }
  /* The card had NO reduced-motion rule at all — it slid and its service pane
     animated regardless, against this file's own "always: full
     prefers-reduced-motion fallback, instant transitions" (the design notes L63).
     Found 2026-08-25 while making the entrance directional, which is exactly
     the change that makes the omission matter. The card still appears and
     still lands on the correct side; it simply stops travelling to get there.
     `transform: none` is needed as well as `transition: none` — without it a
     hidden card sits translated, and with no transition it would POP into
     place from 34px away rather than never having moved. */
  #card { transition: none; transform: none; }
  .card-svc { animation: none; }
}

/* ════════════════════════════════════════════════════════════════════════════
   2026-07-11 — THE LEDGER & THE PATH merge · net-mode selection · embed paging
   ════════════════════════════════════════════════════════════════════════════ */

/* (`.ldg-roads` / `.ldg-road*` lived here — the roads list THE PATH had when it
   was a block inside the Ledger. THE PATH is its own scene drawing `.pth-*`
   (path.js) and the Ledger's route table emits `.ldg-r-*` (ledger.js); no
   `ldg-road` has been rendered since. Removed 2026-08-21.) */

.ldg-ports { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.ldg-port {
  font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
  padding: 3px 8px; border-radius: 3px;
  border: 1px solid color-mix(in srgb, var(--p-hue, var(--gilt)) 55%, transparent);
  color: var(--p-hue, var(--gilt));
}
.ldg-note {
  margin-top: 12px; font-family: 'IM Fell English', serif; font-style: italic;
  font-size: 0.88rem; color: var(--text-dim);
}
.ldg-note code { font-family: 'JetBrains Mono', monospace; font-style: normal; font-size: 0.76rem; color: var(--gilt); }

/* per-VLAN host table */
.ldg-hosts { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 2px 14px; margin-top: 10px; }
.ldg-host {
  display: grid; grid-template-columns: 92px 1fr auto; align-items: center; gap: 8px;
  background: none; border: none; padding: 3px 6px; cursor: pointer;
  border-radius: 4px; text-align: left; color: var(--text);
}
.ldg-host:hover { background: rgba(var(--gilt-rgb), 0.08); }
.ldg-host-ip { font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--z-hue, var(--gilt)); }
.ldg-host-name { display: inline-flex; align-items: center; gap: 6px; font-family: 'Alegreya Sans', sans-serif; font-size: 0.86rem; font-weight: 500; }
.ldg-host-role { font-size: 0.72rem; color: var(--text-dim); white-space: nowrap; }

/* net-mode selection: the chosen place is unmistakable — grown, ring traded
   for a steady beacon sweep; click it again to release the web */
.mk.net-selected .mk-dot { transform: scale(1.5); }
.mk.net-selected .mk-ring {
  animation: mk-beacon 1.8s linear infinite;
  border-color: var(--gilt-bright);
}
.mk.net-selected .mk-core { background: var(--gilt-bright); box-shadow: 0 0 14px var(--gilt-bright); }
.mk.net-selected .mk-label { opacity: 1; color: var(--gilt-bright); }
@keyframes mk-beacon {
  0%   { transform: scale(0.6); opacity: 0.9; }
  100% { transform: scale(2.1); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .mk.net-selected .mk-dot { transform: scale(1.25); }
  .mk.net-selected .mk-ring { animation: none; }
}

/* maximized embed: title + carousel paging */
#embed-max-title {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
  font-family: 'Cinzel', serif; font-size: 0.82rem; letter-spacing: 0.22em;
  color: var(--gilt); z-index: 2; pointer-events: none;
}
.embed-max-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid rgba(var(--gilt-rgb), 0.4); background: rgba(13,11,7,0.72);
  color: var(--gilt); font-size: 1.5rem; line-height: 1; cursor: pointer; z-index: 2;
}
.embed-max-arrow:hover { border-color: var(--gilt); color: var(--gilt-bright); }
#embed-max-prev { left: 16px; }
#embed-max-next { right: 16px; }

/* ════════════════════════════════════════════════════════════════════════════
   2026-07-11b — THE PATH scene (topology) · marker kinds · ledger utilization
   ════════════════════════════════════════════════════════════════════════════ */

/* THE PATH — tiered topology, seated under the crown like every other scene */
/* ⚠ This sat at z-1200 until 2026-08-15, which was correct when it was the
   full-screen scene of a map whose chrome lived in the corners: it covered
   everything. With the scene band across the crown it BURIED the switcher —
   the band lifts to 988 on `body.scene-open`, so THE PATH was the one scene
   you could not leave from the band, and its backdrop swallowed those clicks.
   It joins the family at 985: under the band, over the drawer at 970, and the
   command palette at 990 still covers the lot. */
#path {
  position: fixed; inset: 0; z-index: 985;
  background: rgba(13,11,7,0.82); backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none; transition: opacity 220ms ease;
}
#path.open { opacity: 1; pointer-events: auto; }
.pth-panel {
  position: absolute; inset: var(--crown-clear) 5vw 4vh;
  background: var(--surface); border: 1px solid var(--ink-line); border-radius: 8px;
  display: flex; flex-direction: column; overflow: hidden;
}
/* Sized to the realm, not to the window. --pth-cols is set by path.js to the
   busiest tier's node count; 168px is one comfortable column (a .pth-dot is
   30px, its name and IP the rest). Without the cap the tiers spread across the
   whole monitor and the diagram visibly restyled itself on every resize.
   `auto` margins centre it; the min() keeps narrow windows behaving exactly as
   before. */
.pth-body {
  position: relative; flex: 1;
  width: min(100% - 36px, calc(var(--pth-cols, 8) * 168px));
  margin: 8px auto;
}
#pth-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.pth-edge {
  fill: none; stroke: var(--ink-soft); stroke-width: 0.35;
  vector-effect: non-scaling-stroke; stroke-width: 1.4px; opacity: 0.85;
}
.pth-k-wan    { stroke: var(--tone-info); stroke-dasharray: 6 5; }
.pth-k-edge   { stroke: var(--tone-warn); }
.pth-k-vlan   { stroke: var(--tone-up); }
.pth-k-mesh   { stroke: var(--tone-up); stroke-dasharray: 1.5 5; opacity: 0.7; }
.pth-k-telemetry { stroke: var(--sem-info); stroke-dasharray: 1 6; opacity: 0.5; }
.pth-k-vassal { stroke: var(--ink-soft); stroke-dasharray: 2 4; opacity: 0.6; }
.pth-warn   { stroke: var(--tone-warn) !important; }
.pth-dark   { stroke: var(--tone-down) !important; stroke-width: 2px; animation: pth-alarm 1.6s ease-in-out infinite; }
@keyframes pth-alarm { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

.pth-node {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  background: none; border: none; cursor: pointer; padding: 6px 10px;
  border-radius: 6px;
}
.pth-node:hover { background: rgba(var(--gilt-rgb), 0.08); }
.pth-dot {
  width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--n-hue, var(--gilt)); background: var(--surface-2);
  box-shadow: 0 0 12px color-mix(in srgb, var(--n-hue, var(--gilt)) 30%, transparent);
}
.pth-node.k-vm .pth-dot { border-radius: 50%; }                       /* vm — circle */
.pth-node.k-physical .pth-dot { border-radius: 4px; transform: rotate(45deg); } /* iron — diamond */
.pth-node.k-physical .pth-dot .svc-health { transform: rotate(-45deg); }
.pth-node.k-network .pth-dot { border-radius: 4px; }                  /* ways — square */
.pth-node.st-down .pth-dot { border-color: var(--tone-down); animation: pth-alarm 1.6s ease-in-out infinite; }
/* arrived from a service card — halo its host */
.pth-node.pth-focus .pth-dot {
  box-shadow: 0 0 0 3px rgba(var(--gilt-rgb), 0.55), 0 0 18px rgba(var(--gilt-rgb), 0.5);
  animation: pth-focus-pulse 2.2s ease-in-out infinite;
}
.pth-node.pth-focus .pth-name { color: var(--gilt); }
/* focus mode: the service's reach web stays bright, the rest recedes */
#path.pth-focus-mode .pth-edge:not(.pth-reach) { opacity: 0.18; }
#path.pth-focus-mode .pth-node:not(.pth-focus):not(.pth-lit) { opacity: 0.35; }
.pth-edge.pth-reach { stroke: var(--gilt); stroke-width: 1.6; opacity: 0.95; }
/* Traceable hops (2026-08-09). The click target is a fat transparent twin of
   The edge — `non-scaling-stroke` makes that 14 SCREEN px on a diagram whose
   viewBox is stretched by preserveAspectRatio:none, where a plain stroke-width
   would be squashed to a thread on one axis and a slab on the other. Only hops
   a charted road actually crosses get a twin, so the pointer is a promise. */
.pth-hit {
  fill: none; stroke: transparent; stroke-width: 14px;
  vector-effect: non-scaling-stroke; pointer-events: stroke; cursor: pointer;
}
/* the hop you clicked, inside the roads it belongs to: brighter and thicker
   than the rest of the chain, so the question keeps its place in the answer */
.pth-edge.pth-hop {
  stroke: var(--gilt-bright) !important; stroke-width: 2.6; opacity: 1;
  filter: drop-shadow(0 0 6px var(--gilt));
}
#path .pth-lg-trace { color: var(--gilt); }
#path .pth-lg-from { color: var(--text-dim); }
.pth-node.pth-lit .pth-name { color: var(--text); }
.pth-node.pth-lit .pth-dot { box-shadow: 0 0 10px rgba(var(--gilt-rgb), 0.45); }
.pth-lg-svc strong { color: var(--gilt); }
@keyframes pth-focus-pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(var(--gilt-rgb), 0.55), 0 0 18px rgba(var(--gilt-rgb), 0.5); }
  50%      { box-shadow: 0 0 0 5px rgba(var(--gilt-rgb), 0.3), 0 0 26px rgba(var(--gilt-rgb), 0.65); }
}
.pth-name {
  font-family: 'Cinzel', serif; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.14em; color: var(--text); white-space: nowrap;
}
.pth-ip { font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; color: var(--text-dim); }
.pth-legend {
  display: flex; gap: 18px; align-items: center; flex-wrap: wrap;
  padding: 10px 24px 14px; border-top: 1px solid var(--ink-line);
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.78rem; color: var(--text-dim);
}
.pth-lg { display: inline-flex; align-items: center; gap: 7px; }
.pth-lg-line { display: inline-block; width: 26px; height: 0; border-top: 2px solid var(--ink-soft); }
.pth-lg-line.pth-k-wan { border-top-style: dashed; border-color: var(--tone-info); }
.pth-lg-line.pth-k-edge { border-color: var(--tone-warn); }
.pth-lg-line.pth-k-vlan { border-color: var(--tone-up); }
.pth-lg-line.pth-k-mesh { border-top-style: dotted; border-color: var(--tone-up); }
.pth-lg-line.pth-k-telemetry { border-top-style: dotted; border-color: var(--sem-info); }
.pth-lg-line.pth-k-vassal { border-top-style: dotted; border-color: var(--ink-soft); }
.pth-lg-note { margin-left: auto; font-style: italic; font-family: 'IM Fell English', serif; }

/* Ledger prefix utilization bar (NetBox-flavored) */
.ldg-util {
  position: relative; height: 8px; margin-top: 8px;
  background: rgba(var(--shade-rgb), calc(0.35 * var(--shade-k))); border: 1px solid var(--ink-line); border-radius: 3px;
  overflow: hidden;
}
.ldg-util-fill {
  display: block; height: 100%;
  background: linear-gradient(90deg, color-mix(in srgb, var(--z-hue) 55%, transparent), var(--z-hue));
}
.ldg-util-pct {
  position: absolute; right: 6px; top: -1px;
  font-family: 'JetBrains Mono', monospace; font-size: 0.58rem; color: var(--text-dim);
}

/* Marker kinds on the map: iron reads as a diamond, the ways as a square,
   vms stay the breathing circle (2026-07-11: "hosts should look different") */
.mk[data-shape="square"] .mk-core { border-radius: 3px; }
.mk[data-shape="square"] .mk-ring { border-radius: 5px; }
.mk[data-shape="diamond"] .mk-core { border-radius: 3px; transform: rotate(45deg) scale(1.12); }
.mk[data-shape="diamond"] .mk-ring { border-radius: 6px; transform: rotate(45deg); }
.mk[data-shape="triangle"] .mk-core { border-radius: 0; clip-path: polygon(50% 0, 100% 100%, 0 100%); transform: scale(1.25); }
.mk[data-shape="triangle"] .mk-ring { border-radius: 4px; clip-path: polygon(50% -12%, 112% 112%, -12% 112%); }

/* ════════════════════════════════════════════════════════════════════════════
   2026-07-11c — legible network layer · about overlays · card vassals
   ════════════════════════════════════════════════════════════════════════════ */

/* territory tags: each hull names itself (only while the layer is up) */
.territory-tag {
  font-family: 'Cinzel', serif; font-size: 30px; font-weight: 600;
  letter-spacing: 0.2em; text-anchor: middle;
  opacity: 0; transition: opacity 400ms ease;
  paint-order: stroke; stroke: rgba(13,11,7,0.75); stroke-width: 5px;
  pointer-events: none;
}
#overlay.net-on .territory-tag { opacity: 0.95; }

/* (`#net-legend` lived here. The legend paragraph was removed on 2026-07-12 —
   the territory edge tags and the worded ◈ button carry the explanation — but
   its nine rules stayed behind, matching nothing. Removed 2026-07-31.) */

/* ⓘ what-am-I-looking-at overlays on the watchings */
.chron-info {
  background: none; border: none; color: var(--text-dim);
  font-size: 0.9rem; cursor: pointer; line-height: 1; padding: 0 2px;
}
.chron-info:hover { color: var(--gilt-bright); }
.chron-about {
  position: absolute; top: 40px; left: 12px; right: 12px; z-index: 3;
  background: var(--parchment); color: var(--card-ink);
  border-radius: 4px; padding: 10px 14px;
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.85rem; line-height: 1.55;
  box-shadow: 0 6px 24px rgba(0,0,0,0.5);
}

/* the card's vassal rows */
.card-vassals-head {
  font-family: 'IM Fell English', serif; font-style: italic;
  font-size: 0.82rem; color: var(--card-ink-soft);
  margin: 2px 0 4px; width: 100%;
  grid-column: 1 / -1;   /* a heading rules both columns of .card-links, not one cell */
}
.card-vassal { opacity: 0.92; }

/* pass-throughs: the services that cross a transit place (PANGOLIN, TAILSCALE,
   CLOUDFLARE). Quieter than an owned service — the road is borrowed, not kept,
   the same distinction .con-pass draws on the constellation's spokes. */
.card-cross {
  opacity: 0.92;
  grid-column: 1 / -1;   /* a borrowed road carries its owner's name too, and
                            "NEXTCLOUD (PUBLIC) · THE WILD" does not fit a 136px
                            half-cell — these rows take the full width instead,
                            which also tells them apart from the stalls above */
}
.card-cross .cross-label { overflow: hidden; text-overflow: ellipsis; }
.card-cross .cross-from {
  font-family: 'Alegreya Sans', sans-serif; font-weight: 400;
  font-size: 0.62rem; letter-spacing: 0.02em; font-style: italic;
  opacity: 0.62; margin-left: auto; padding-left: 6px; flex-shrink: 0;
}

/* ════════════════════════════════════════════════════════════════════════════
   2026-07-11d — clickable VLAN territories · void-bound roads · legend hues
   ════════════════════════════════════════════════════════════════════════════ */

/* hulls are interactive while the layer is up */
#overlay.net-on .territory { pointer-events: all; cursor: pointer; }
#overlay.net-on .territory:hover { fill-opacity: 0.16; stroke-opacity: 0.7; }
.territory-selected {
  fill-opacity: 0.2 !important;
  stroke-opacity: 0.95 !important;
  stroke-width: 3px;
  filter: drop-shadow(0 0 14px var(--zt-hue));
}

/* zone-filter mode: lit roads stand proud, the rest recede */
#overlay.zone-mode path.route { opacity: 0.1; }
#overlay.zone-mode path.route.zone-lit { opacity: 1; stroke-width: 2.6; }

/* roads that dive beyond the table — visible only with the network layer up */
#overlay.net-on path.route-void { opacity: 0.6; }


/* ── THE CALENDAR (v1.45.0) ──────────────────────────────────────────────────
   THE RITES (2026-07-11, told the truth 2026-07-28) and THE FORGE'S DAY
   (v1.40.0, a scene v1.42.0) were one page's worth of rows drawn twice. This
   is the page: one row per rite, one clock down the middle, grouped by the
   forge they contend for and then by their turning.

   ONE GRID. `--cal-*` are the four columns — gutter, name, day, meta — and the
   ruler, every row and every group head are laid on the same
   `grid-template-columns`, so an hour on the axis is that hour on every strip
   below it. MUST TRACK each other: change a width and change it once, here.

   COLOUR, per the design notes's "if everything glows, nothing does". The ladder was
   re-cut when the page grew from the forge's five rows to the realm's
   twenty-eight: gold could not be the ordinary bar any more without becoming
   wallpaper. So an ordinary rite is INK — the day's substrate — and the gilt
   is spent on the rites that ask a forge for a model, which is the scarce and
   contended thing this page was built to show. The heavy model (30B, which
   never fit the GPU box's 16 GB) takes the warning tone; a MEETING — two lanes
   wanting one model at once — is the only alarm on the page; "now" is a single
   info-hued hairline. */
#calendar .ldg-panel { width: min(1180px, 96vw); }
/* the day is one column of rows, not the wrapping ledger of blocks .ldg-list
   draws elsewhere — a row that wrapped would leave the clock behind */
#calendar .ldg-list, #cal-list { flex-direction: column; flex-wrap: nowrap; gap: 26px; }
#calendar .ldg-list { padding: 18px 22px 24px; --cal-gut: 18px; --cal-name: 236px; --cal-meta: 176px; --cal-gap: 14px; }
#cal-list { display: flex; }

.cal-ruler, .cal-row {
  display: grid; align-items: center;
  grid-template-columns: var(--cal-gut) var(--cal-name) 1fr var(--cal-meta);
  gap: 0 var(--cal-gap);
}

#cal-list:empty { display: none; }

/* the axis rides the top of the scroll — twenty-eight rows is more than one
   screen, and a ruler you have to scroll back to is a ruler you stop reading */
.cal-ruler {
  position: sticky; top: -18px; z-index: 3; padding: 12px 0 8px;
  background: var(--surface-2);
  box-shadow: 0 6px 10px -8px rgba(var(--shade-rgb), calc(0.8 * var(--shade-k)));
  border-bottom: 1px solid var(--ink-line);
}
.cal-hours {
  position: relative; height: 12px;
  font-family: 'JetBrains Mono', monospace; font-size: 0.56rem;
  letter-spacing: 0.08em; color: var(--text-dim);
}
.cal-hours span { position: absolute; top: 0; transform: translateX(-50%); }
.cal-ruler .cal-name b { font-family: 'Cinzel', serif; letter-spacing: 0.16em; font-size: 0.6rem; color: var(--text-dim); }

/* ── a group: forge, turning, or listed ── */
.cal-grp { display: flex; flex-direction: column; }
.cal-ghead {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px;
  padding: 0 0 8px; margin-bottom: 4px; border-bottom: 1px solid var(--ink-line);
}
.cal-gname { font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.72rem; letter-spacing: 0.2em; color: var(--text); }
.cal-grp.k-forge .cal-gname { color: var(--gilt); }
.cal-grp.k-listed .cal-gname { color: var(--text-dim); }
.cal-gsub { font-family: 'IM Fell English', serif; font-style: italic; font-size: 0.82rem; color: var(--text-dim); }
.cal-gcount { margin-left: auto; font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; letter-spacing: 0.06em; color: var(--text-dim); }
.cal-gfoot {
  display: flex; flex-wrap: wrap; gap: 3px 18px; padding-top: 7px;
  margin-left: calc(var(--cal-gut) + var(--cal-name) + var(--cal-gap));
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; color: var(--text-dim);
}
.cal-gfoot b { color: var(--text); font-weight: 600; }

/* ── a row ── */
.cal-row { min-height: 34px; border-bottom: 1px solid color-mix(in srgb, var(--ink-line) 55%, transparent); }
.cal-row:last-of-type { border-bottom: none; }
.cal-mark { text-align: center; font-size: 0.86rem; line-height: 1; color: color-mix(in srgb, var(--text-dim) 65%, transparent); }
.cal-name { min-width: 0; overflow: hidden; display: flex; flex-direction: column; line-height: 1.2; padding-right: 4px; }
.cal-name b {
  font-family: 'Alegreya Sans', sans-serif; font-weight: 700; font-size: 0.86rem; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-name i {
  font-style: normal; font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; color: var(--text-dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-meta { min-width: 0; overflow: hidden; display: flex; flex-direction: column; line-height: 1.25; text-align: right; }
.cal-meta em, .cal-meta i {
  font-style: normal; font-family: 'JetBrains Mono', monospace;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cal-meta em { font-size: 0.64rem; color: var(--text); }
.cal-truth { font-size: 0.6rem; color: var(--text-dim); }

/* the day itself — an hour hairline per hour is the track's own ground, so the
   ruler above it carries numbers and nothing else */
.cal-track {
  position: relative; height: 16px; border-radius: 2px;
  background:
    repeating-linear-gradient(90deg, transparent 0 calc(100%/8 - 1px),
      color-mix(in srgb, var(--ink-line) 80%, transparent) calc(100%/8 - 1px) calc(100%/8)),
    rgba(var(--shade-rgb), calc(0.28 * var(--shade-k)));
  border: 1px solid color-mix(in srgb, var(--ink-line) 70%, transparent); overflow: hidden;
}
.cal-blk {
  position: absolute; top: 2px; bottom: 2px; min-width: 2px; border-radius: 1px;
  background: var(--text-dim); opacity: 0.72;
}
/* the gilt is the forge's — a rite that asks a model for its work */
.cal-row.tone-warm .cal-blk { background: var(--gilt); opacity: 0.9; }
.cal-row.tone-heavy .cal-blk { background: var(--sem-warn); }
/* the hour is charted; the DAY is the rite's own (a monthly rite's date is not
   a thing this page models) — so it is drawn hollow, never as an instance */
.cal-blk.ghost { background: none; border: 1px solid var(--text-dim); opacity: 1; top: 3px; bottom: 3px; }
.cal-row.tone-warm .cal-blk.ghost { border-color: var(--gilt); }
/* the only alarm on the page */
.cal-blk.hit { outline: 1px dashed var(--sem-down); outline-offset: 1px; opacity: 1; }
/* An ever-turning rite has a rhythm, not an hour: the whole track, so the row
   reads "always, this often" and points at no minute it cannot vouch for. Ink,
   like every other rite that asks no model — what separates it from a block is
   that it never ends, not a colour of its own. `.combed` adds the teeth, and
   calendar.js only asks for them when the interval is half an hour or more:
   288 of them for "every 5 min" was a solid hatch that shouted over every real
   block on the page. */
.cal-wash { position: absolute; inset: 2px 0; background: color-mix(in srgb, var(--text-dim) 26%, transparent); }
.cal-wash.combed {
  background: repeating-linear-gradient(90deg,
    color-mix(in srgb, var(--text-dim) 55%, transparent) 0 1px,
    color-mix(in srgb, var(--text-dim) 10%, transparent) 1px var(--cal-every, 2%));
}
.cal-idle {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: 'JetBrains Mono', monospace; font-size: 0.56rem; letter-spacing: 0.08em;
  color: var(--ink-soft); font-style: normal;
  /* the ground must not read through the words that explain its emptiness */
  background: linear-gradient(90deg, transparent, rgba(var(--shade-rgb), calc(0.55 * var(--shade-k))) 22%, rgba(var(--shade-rgb), calc(0.55 * var(--shade-k))) 78%, transparent);
}
.cal-now {
  position: absolute; top: -1px; bottom: -1px; width: 1px; z-index: 1;
  background: var(--sem-info); box-shadow: 0 0 4px color-mix(in srgb, var(--sem-info) 70%, transparent);
}

/* ── the verdicts ── the wall of kept rites stays quiet; a silent one is the
   only thing with colour in its row, and earns a rule in the gutter so it is
   findable by scrolling rather than by reading */
.cal-row.v-kept .cal-mark { color: var(--sem-up); }
.cal-row.v-late .cal-mark, .cal-row.v-late .cal-truth { color: var(--sem-warn); }
.cal-row.v-missed .cal-mark, .cal-row.v-failed .cal-mark { color: var(--sem-down); }
.cal-row.v-missed .cal-truth, .cal-row.v-failed .cal-truth { color: var(--sem-down); font-weight: 600; }
.cal-row.v-missed, .cal-row.v-failed { box-shadow: inset 2px 0 0 var(--sem-down); }
.cal-row.v-awaited .cal-truth { font-style: italic; opacity: 0.75; }
/* an hour this page READ out of the rite's own note, rather than one the Desk
   wrote — the prose beside it is the authority, and says so on hover */
.cal-row.read .cal-meta em { color: var(--text-dim); border-bottom: 1px dotted var(--ink-soft); align-self: flex-end; }

/* the last few turnings, oldest left */
.cal-ticks { display: inline-flex; gap: 2px; margin-left: 6px; vertical-align: middle; }
.cal-tick { width: 3px; height: 8px; border-radius: 1px; display: inline-block; }
.cal-tick.good { background: color-mix(in srgb, var(--sem-up) 70%, transparent); }
.cal-tick.bad { background: var(--sem-down); }

/* ── the head: the verdict, the count, and the day being drawn ── */
.cal-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 16px;
  padding: 7px 22px 9px; border-bottom: 1px solid var(--ink-line);
  font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; letter-spacing: 0.04em;
}
.cal-alarm { color: var(--sem-down); font-weight: 700; }
.cal-calm { color: var(--sem-up); }
.cal-quiet, .cal-count { color: var(--text-dim); }
.cal-broken { color: var(--sem-down); }
.cal-warnish { color: var(--sem-warn); }
.cal-hits { color: var(--sem-down); }
.cal-next { color: var(--text-dim); }
.cal-step { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; }
.cal-step b { font-size: 0.68rem; letter-spacing: 0.1em; color: var(--gilt); min-width: 128px; text-align: center; }
.cal-arrow, .cal-today {
  background: none; border: 1px solid var(--ink-line); border-radius: 3px; cursor: pointer;
  color: var(--text-dim); font-family: 'Cinzel', serif; font-size: 0.62rem; letter-spacing: 0.12em;
  padding: 2px 7px; line-height: 1.4; transition: color 150ms, border-color 150ms;
}
.cal-arrow:hover:not(:disabled), .cal-today:hover:not(:disabled) { color: var(--gilt); border-color: var(--gilt); }
.cal-today:disabled { opacity: 0.35; cursor: default; }

/* THE FLEET'S DAY rides the shared component, which brings its own dress
   (static/outset-calendar.css, loaded BEFORE this file so what follows wins).
   The component asks for its colours and faces as `--oc-*` and falls back to
   Hub-flavoured names the Atlas does not use — `--gold`, `--border`,
   `--muted` — so without this mapping it would draw the fleet's day in
   hardcoded gruvbox next to a realm's day drawn in the Atlas's own inkpot.
   Same ladder, same hands, one page. */
.cal-grp.k-fleet .calendar-host { margin-top: 6px; }
.calendar-host {
  --oc-bg: var(--surface); --oc-line: var(--ink-line); --oc-text: var(--text);
  --oc-ink: var(--ink-soft); --oc-dim: var(--text-dim);
  --oc-gilt: var(--gilt); --oc-warn: var(--sem-warn); --oc-alarm: var(--sem-down);
  --oc-ok: var(--sem-up); --oc-info: var(--sem-info);
  --oc-font-display: 'Cinzel', serif;
  --oc-font-data: 'JetBrains Mono', monospace;
  --oc-font-body: 'Alegreya Sans', sans-serif;
  /* the fleet's day sits inside the scene panel, not on a full page: its name
     and meta columns match THE CALENDAR's above it so the two read as one */
  --oc-name-w: 250px; --oc-meta-w: 208px; --oc-gut: 18px; --oc-gap: 12px;
}

/* ── the card's pointer ── the day moved to its own scene in v1.42.0 and to
   THE CALENDAR in v1.45.0, so the card keeps one line — what turns next, in
   the vitals' dress — and the door. The button is a `.card-link` like the
   services below it, in the Discover pine, full width because it stands
   alone in its block. */
.card-forge { margin-top: 10px; border-top: 1px solid rgba(33,28,20,0.14); padding-top: 8px; }
.card-forge[hidden] { display: none; }
.card-forge .cal-next { display: block; font-family: 'JetBrains Mono', monospace; font-size: 0.64rem; color: var(--card-ink-soft); margin: 0 0 7px; }
.card-forge .cal-next b { color: var(--card-ink); font-weight: 600; }
.card-forge .card-forge-link { width: 100%; justify-content: center; }

@media (max-width: 980px) {
  #calendar .ldg-list { --cal-name: 170px; --cal-meta: 130px; }
  .cal-gsub { display: none; }
}
.card-patches { color: var(--card-ink-soft); }
.card-patches.warn { color: #a05e1f; font-weight: 600; }
.card-patches.stale { font-style: italic; }

/* ── THE ARMORY — hardware faceplates + drag-to-rearrange (2026-07-20) ── */
/* (#rel-elevation's position/width now live on its base rule above) */
.rel-niche {
  position: absolute; left: 0; width: 460px; height: 34px; box-sizing: border-box;
  border: 1px solid rgba(var(--gilt-rgb), 0.10);
  background: repeating-linear-gradient(90deg, rgba(0,0,0,0.30) 0 2px, rgba(0,0,0,0.16) 2px 7px);
}
.rel-niche.filled { background: none; border-color: transparent; }
.rel-niche-u {
  position: absolute; left: -20px; top: 50%; transform: translateY(-50%);
  font-family: 'JetBrains Mono', monospace; font-size: 0.52rem; color: var(--ink-soft);
}
.rel-dev {
  position: absolute; left: 0; width: 460px; box-sizing: border-box;
  cursor: pointer; overflow: hidden; border-radius: 3px;
  filter: drop-shadow(0 2px 5px rgba(0,0,0,0.5));
  transition: top 160ms cubic-bezier(0.4,0,0.2,1), filter 160ms, box-shadow 160ms;
}
.rel-dev .fp-svg { display: block; width: 100%; height: 100%; }
.rel-dev-tag {
  position: absolute; left: 24px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; gap: 9px; pointer-events: none;
  padding: 4px 12px; border-radius: 5px;
  background: rgba(8,8,6,0.86);
  box-shadow: 0 2px 8px rgba(0,0,0,0.65), inset 0 0 0 1px rgba(var(--gilt-rgb), 0.28);
  text-shadow: 0 1px 2px #000;
}
/* .rel-dev-name is gone (2026-08-10) — the unit's name no longer prints across
   its faceplate. The tag now carries only the U position; hover or click for
   the name. Rule kept out rather than left dangling, so nothing re-adds a
   label by accident and finds it already styled. */
.rel-dev-u { font-family: 'JetBrains Mono', monospace; font-size: 0.66rem; font-weight: 700; color: #f6c453; }
.rel-dev:hover { filter: drop-shadow(0 3px 9px rgba(0,0,0,0.6)); }
.rel-dev.active { box-shadow: 0 0 0 1.5px var(--u-hue, var(--gilt)), 0 0 14px -2px var(--u-hue, var(--gilt)); }
.rel-grip {
  position: absolute; right: 9px; top: 50%; transform: translateY(-50%);
  color: var(--gilt); opacity: 0; font-size: 1rem; pointer-events: none; transition: opacity 150ms;
}
#reliquary.editing .rel-dev { cursor: grab; }
#reliquary.editing .rel-dev .rel-grip { opacity: 0.85; }
#reliquary.editing .rel-niche:not(.filled) {
  border-color: rgba(var(--gilt-rgb), 0.32);
  background: repeating-linear-gradient(90deg, rgba(var(--gilt-rgb), 0.07) 0 2px, rgba(0,0,0,0.14) 2px 7px);
}
.rel-dev.grabbing { cursor: grabbing; z-index: 5; transition: none; filter: drop-shadow(0 8px 18px rgba(0,0,0,0.7)); }
.rel-dev.blocked { box-shadow: 0 0 0 2px var(--sem-down); }

/* faceplate ink — engraved gilt-on-slate */
.fp-panel { fill: #23241e; stroke: rgba(var(--gilt-rgb), 0.35); stroke-width: 1; }
.rel-gateway .fp-panel { fill: #1e2630; }
.rel-forge .fp-panel { fill: #2a221c; }
.rel-patch .fp-panel { fill: #201f1b; }
.rel-switch .fp-panel { fill: #1e2630; }
.rel-storage .fp-panel { fill: #1e2522; }
.rel-power .fp-panel { fill: #28231a; }
.rel-enclosure .fp-panel { fill: #24231f; }
.fp-bay { fill: rgba(0,0,0,0.34); stroke: rgba(var(--gilt-rgb), 0.38); stroke-width: 0.7; }
.fp-ear { fill: rgba(0,0,0,0.28); stroke: rgba(var(--gilt-rgb), 0.28); stroke-width: 0.75; }
.fp-hole { fill: #0c0c0a; stroke: rgba(var(--gilt-rgb), 0.32); stroke-width: 0.6; }
.fp-port, .fp-sfp, .fp-key { fill: rgba(0,0,0,0.5); stroke: rgba(var(--gilt-rgb), 0.5); stroke-width: 0.8; }
.fp-sfp { stroke: rgba(95,168,211,0.75); }
.fp-key { stroke: rgba(var(--gilt-rgb), 0.4); }
.fp-screen { fill: #0c0f0b; stroke: rgba(var(--gilt-rgb), 0.5); stroke-width: 0.8; }
.fp-glyph { fill: rgba(var(--gilt-rgb), 0.55); }
.fp-vent { fill: rgba(0,0,0,0.42); stroke: var(--fp-accent, rgba(var(--gilt-rgb), 0.4)); stroke-width: 0.5; }
.fp-btn { fill: rgba(0,0,0,0.5); stroke: rgba(var(--gilt-rgb), 0.5); stroke-width: 0.8; }
.fp-badge { fill: rgba(0,0,0,0.30); stroke: rgba(var(--gilt-rgb), 0.25); stroke-width: 0.6; }
.fp-led { fill: #57503f; }

/* header rearrange controls */
.rel-head .rel-close { margin-left: 12px; }
.rel-racks { margin-left: 12px; flex-wrap: wrap; }
.rel-racks[hidden] { display: none; }
.rel-tools { margin-left: auto; display: flex; gap: 8px; align-items: center; }
.rel-tool {
  background: rgba(var(--gilt-rgb), 0.08); border: 1px solid rgba(var(--gilt-rgb), 0.35); color: var(--gilt);
  font-family: 'Cinzel', serif; font-size: 0.58rem; letter-spacing: 0.14em; text-transform: uppercase;
  border-radius: 3px; padding: 5px 10px; cursor: pointer; transition: background 150ms, border-color 150ms;
}
.rel-tool:hover { background: rgba(var(--gilt-rgb), 0.16); }
.rel-tool.on { background: var(--gilt); color: var(--on-gilt); border-color: var(--gilt); }
.rel-tool[hidden] { display: none; }

/* ── THE ARMORY: 2D ⇄ 3D toggle + the 3D deep-dive canvas ────────────────
   The segmented control is the button idiom in miniature (Cinzel micro-caps,
   gilt); 3D is armory3d.js. `#rel-elevation` sets its display on an id selector,
   so it needs an explicit [hidden] rule to lose to — plain [hidden] can't. */
.rel-viewtoggle { display: inline-flex; border: 1px solid rgba(var(--gilt-rgb), 0.35); border-radius: 3px; overflow: hidden; }
.rel-seg {
  background: rgba(var(--gilt-rgb), 0.08); border: 0; color: var(--gilt);
  font-family: 'Cinzel', serif; font-size: 0.58rem; letter-spacing: 0.12em; text-transform: uppercase;
  padding: 5px 11px; cursor: pointer; transition: background 150ms, color 150ms;
}
.rel-seg + .rel-seg { border-left: 1px solid rgba(var(--gilt-rgb), 0.28); }
.rel-seg:hover { background: rgba(var(--gilt-rgb), 0.16); }
.rel-seg.is-on { background: var(--gilt); color: var(--on-gilt); }
.rel-seg:disabled { opacity: 0.4; cursor: not-allowed; }

#rel-elevation[hidden] { display: none; }
#rel-3d {
  width: 460px; height: 60vh; min-height: 360px; max-height: 560px;
  position: relative; border: 1px solid var(--ink-line); border-radius: 4px; overflow: hidden;
  /* a soft socket so the dark-metal rack never looks pasted onto a light panel (spec 10) */
  background: radial-gradient(120% 90% at 50% 42%, #32302f 0%, #1d2021 70%, #141414 100%);
}
#rel-3d[hidden] { display: none; }
#rel-3d canvas { display: block; width: 100%; height: 100%; }
.rel-cams { position: absolute; top: 8px; right: 8px; z-index: 1; }
.rel-cams .rel-seg { background: rgba(29, 32, 33, 0.82); }
.rel-cams .rel-seg:hover { background: rgba(60, 56, 54, 0.92); }
.rel-cams .rel-seg.is-on { background: var(--gilt); color: var(--on-gilt); }

/* The vessels' names, off the faceplates and on the keyboard's path into 3D. */
.rel-legend { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.rel-legend[hidden] { display: none; }
.rel-legend-item {
  display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; max-width: 100%;
  background: rgba(var(--gilt-rgb), 0.06); border: 1px solid rgba(var(--gilt-rgb), 0.22);
  color: var(--gilt); border-radius: 3px; padding: 3px 7px; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.56rem; letter-spacing: 0.1em; text-transform: uppercase;
  transition: background 150ms, border-color 150ms;
}
.rel-legend-item .rel-dev-u { opacity: 0.7; }
.rel-legend-item:hover, .rel-legend-item.is-hover { background: rgba(var(--gilt-rgb), 0.16); }
.rel-legend-item.is-on { border-color: var(--gilt); background: rgba(var(--gilt-rgb), 0.2); }

@media (max-width: 720px) {
  #rel-3d { width: 100%; height: 52vh; }
  /* the 2D rack keeps its drawn 460px; a canvas can fit the phone it is on */
  .rel-rack:has(> #rel-3d:not([hidden])) { width: 100%; }
}

/* ── THE LEDGER: nested prefixes, pools, reservations (real CIDR) ────────
   The grid used to be a fixed 256 cells because every segment was assumed to
   be a /24. It now sizes itself, so cells wrap rather than being counted out,
   and pool/reservation bands need their own ink. */
.ldg-child { margin-left: 26px; border-left: 1px solid var(--ink-line); padding-left: 18px; }
.ldg-cell.ldg-r-dhcp { background: rgba(131,165,152,0.28); }
.ldg-cell.ldg-r-reserved { background: rgba(214,93,14,0.26); }
.ldg-legend { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.ldg-legend-item {
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem;
  color: var(--text-dim); border: 1px solid var(--ink-line);
  border-radius: 20px; padding: 2px 9px;
}
.ldg-legend-item.ldg-r-dhcp { border-color: rgba(131,165,152,0.6); color: var(--sem-info); }
.ldg-legend-item.ldg-r-reserved { border-color: rgba(214,93,14,0.55); color: #d65d0e; }
.ldg-legend-item.ldg-next { border-color: rgba(var(--gilt-rgb),0.55); color: var(--gilt); }
.ldg-legend-item code { color: var(--text); }
.ldg-shared {
  font-style: italic; opacity: 0.66; font-size: 0.92em;
}
.ldg-inband { font-style: italic; color: var(--sem-info); opacity: 0.8; }
.ldg-portgroup { display: flex; flex-direction: column; gap: 5px; margin-top: 8px; }
.ldg-portgroup-name {
  font-family: 'Cinzel', serif; font-size: 0.58rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--text-dim);
}
.ldg-port.poe { box-shadow: inset 0 -2px 0 var(--sem-warn); }

/* §20a (v1.34.0) — THE FACEPLATE in the armory detail pane. The port chips
   reuse .ldg-port's language; these rules add the button affordance and the
   answer box a clicked port opens. The pane is parchment, so ink tokens. */
.rel-ports { margin-top: 12px; }
/* (.rel-ports-head is styled with .cv-head, by the card's vitals) */
.rel-ports .ldg-ports { display: flex; flex-wrap: wrap; gap: 4px; }
button.rel-port { background: transparent; cursor: pointer; }
button.rel-port:hover { background: color-mix(in srgb, var(--p-hue, var(--gilt)) 14%, transparent); }
button.rel-port.selected {
  background: color-mix(in srgb, var(--p-hue, var(--gilt)) 22%, transparent);
  border-color: var(--p-hue, var(--gilt));
}
.rel-port-detail {
  margin-top: 8px; padding: 8px 10px; border-radius: 4px;
  border: 1px solid rgba(var(--gilt-rgb),0.3);
  background: var(--parchment-dim);
  font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
  color: var(--card-ink); display: flex; flex-direction: column; gap: 4px;
}
.rel-port-detail[hidden] { display: none; }
.rel-port-facts { color: var(--card-ink-soft); }
.rel-port-peer { font-weight: 600; }
.rel-port-note { color: var(--card-ink-soft); font-style: italic; }
.rel-peer-btn { margin-top: 4px; }

/* ════════════════════════════════════════════════════════════════════════════
   2026-07-31 — THE VIEWING GLASS · THE CONSTELLATION · the card's ⊖
   ════════════════════════════════════════════════════════════════════════════ */

/* ── The card's third chrome button: pull back, stay on this place ─────────
   Sits left of the – and the ×, in the same quiet ink, so the cluster reads as
   one row of three rather than a new control bolted on. */
.card-out {
  position: absolute; top: 6px; right: 60px; color: var(--card-ink-soft);
  background: none; border: none; cursor: pointer; font-size: 1.05rem;
  line-height: 1; padding: 5px 8px; transition: color 150ms;
}
.card-out:hover { color: var(--gilt); }
#card .card-out { z-index: 3; }
#card.minimized .card-out { display: none; }   /* nothing to frame when it's a headline */

/* ── ⁘ Split into its services ─────────────────────────────────────────────
   Full width across the two-column service grid, in the outline voice of THE
   PATH's buttons — it acts on the map, it doesn't open anything. */
.card-split {
  grid-column: 1 / -1;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: none; border: 1px solid var(--discover); color: var(--discover);
  cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.58rem; font-weight: 700; letter-spacing: 0.16em;
  padding: 8px 10px 7px; border-radius: 2px;
  transition: background 200ms, color 200ms;
}
.card-split:hover { background: var(--discover); color: #F9F5D7; }
.card-split .cs-glyph { font-size: 0.9rem; line-height: 1; letter-spacing: 0; }
body.constellation .card-split { background: var(--discover); color: #F9F5D7; }
/* the split/gather label flips with the map's own state — no JS redraw, so
   toggling never rebuilds the card (or its two live vitals gauges) */
.card-split .cs-on { display: none; }
body.constellation .card-split .cs-off { display: none; }
body.constellation .card-split .cs-on { display: inline; }

/* ── THE CONSTELLATION — a place scattered into its services ───────────────
   Satellites are ordinary markers at two-thirds scale with a mono label, so the
   ring reads as "smaller things belonging to the big thing" without inventing a
   second visual language. The host keeps its own dot at the centre. */
.mk.mk-sat { width: 96px; height: 76px; z-index: 9; animation: con-arrive 320ms cubic-bezier(0.2, 1.2, 0.4, 1) backwards; }
/* 10px, not the original 8: with the split open the satellites ARE the
   subject, so they carry a step more presence (dot + label contrast below)
   while the host's dot steps back — see the .mk.active rule after this block */
.mk.mk-sat .mk-dot { width: 10px; height: 10px; }
.mk.mk-sat .mk-ring { inset: -5px; border-width: 1px; }
.mk.mk-sat .mk-core { box-shadow: 0 0 6px var(--mk-hue), 0 0 14px var(--mk-glow); }
.mk-sat .sat-label {
  opacity: 1; top: calc(50% + 12px);
  display: flex; align-items: center; gap: 5px;
  font-family: 'JetBrains Mono', monospace; font-weight: 400;
  font-size: 9px; letter-spacing: 0.1em; text-transform: lowercase;
  /* parchment on a firmer plate (was --text on 0.62/0.22) — louder by contrast,
     not by size: 9px is what the two-ring overlap measurements were taken at */
  color: var(--parchment); padding: 2px 6px; border-radius: 2px;
  background: rgba(13,11,7,0.72); border: 1px solid rgba(var(--gilt-rgb),0.35);
  text-shadow: none;
}
.mk-sat .sat-label .svc-health { margin: 0; flex-shrink: 0; }
/* the « · host-name» suffix on a passing service — quieter than the name */
.mk-sat .sat-label .sat-host { color: var(--text-dim); }
.mk.mk-sat:hover .mk-core { transform: scale(1.6); }
.mk.mk-sat.sat-active .mk-core { transform: scale(1.7); background: var(--gilt-bright); }
.mk.mk-sat.sat-active .sat-label {
  color: var(--gilt-bright); border-color: rgba(var(--gilt-rgb),0.75);
  background: rgba(13,11,7,0.88);
}
/* the whole ring arrives, rather than seventeen dots popping in at once */
@keyframes con-arrive { from { opacity: 0; transform: translate(-50%,-50%) scale(0.6); } }


/* the spokes to each service, and each service's own charted reach */
#constellation { pointer-events: none; }
#constellation path {
  fill: none; stroke: var(--con-hue, var(--gilt)); stroke-linecap: round;
  transition: opacity 320ms ease, stroke-width 320ms ease;
}
#constellation path.con-spoke { stroke-width: 1.4; stroke-dasharray: 2 5; opacity: 0.55; }
/* a pass-through's spoke whispers: the service only borrows this place's
   roads (its label names where it lives). Asked-about (.con-lit) still wins. */
#constellation path.con-spoke.con-pass { stroke-dasharray: 1 6; opacity: 0.3; }
#constellation path.con-line { stroke-width: 2; opacity: 0.3; }
/* The click target for a road (2026-08-09). A 2px line cannot be hit by a real
   pointer, so each road carries an invisible twin with a fat stroke and takes
   the click there. `pointer-events: stroke` — not `all` — so only the band
   around the line answers and the Q-curve's vast empty bounding box does not
   swallow clicks meant for the map beneath it. Opacity is untouched by the
   .con-focus rules below because it has no stroke to dim: transparent is
   transparent at every opacity, so the twin never becomes visible ink. */
#constellation path.con-hit {
  stroke: transparent; stroke-width: 12; fill: none;
  pointer-events: stroke; cursor: pointer; transition: none;
}
/* one service asked about: its lines burn, its siblings recede but stay legible */
#constellation.con-focus path { opacity: 0.09; }
#constellation.con-focus path.con-lit { opacity: 0.95; }
#constellation.con-focus path.con-lit.con-spoke { stroke-width: 2.2; stroke-dasharray: none; }
#constellation.con-focus path.con-lit.con-line {
  stroke-width: 3; filter: drop-shadow(0 0 7px var(--con-hue));
}
/* the other places dim a shade while one host's services hold the stage */
body.constellation .mk:not(.mk-sat):not(.active) { opacity: 0.42; }
/* …and the split host itself steps back furthest: the ring is the subject,
   the place is context. Only the DOT fades — the name stays legible, and
   hover/active still wake it (scale survives; opacity is the statement).
   (No :not(.mk-sat) needed: `active` only ever lands on #markers children;
   satellites carry `sat-active`.) */
body.constellation .mk.active .mk-dot { opacity: 0.35; }

@media (prefers-reduced-motion: reduce) {
  .mk.mk-sat { animation: none; }
  .mk.mk-sat .mk-ring { animation: none; }
}

/* ── THE VIEWING GLASS — a service drawn over the map ──────────────────────
   Not a modal dialog with a service inside it: a pane of the surveyor's glass
   laid on the table. Gilt frame, no parchment (the service brings its own
   colours and they must not fight ours), and the escape hatch always present. */
#glass {
  position: fixed; inset: 0; z-index: 1010;
  background: rgba(13,11,7,0.82); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center;
  animation: gl-in 200ms ease;
}
#glass[hidden] { display: none; }
@keyframes gl-in { from { opacity: 0; } }
.gl-panel {
  width: min(1360px, 94vw); height: min(880px, 90vh);
  display: flex; flex-direction: column;
  background: var(--surface); border: 1px solid rgba(var(--gilt-rgb),0.4);
  border-radius: 8px; overflow: hidden;
  box-shadow: 0 30px 80px rgba(var(--shade-rgb), calc(0.7 * var(--shade-k)));
}
.gl-head {
  display: flex; align-items: baseline; gap: 12px; flex-shrink: 0;
  padding: 12px 16px 11px; border-bottom: 1px solid var(--ink-line);
  background: var(--surface-2); position: relative;
}
.gl-title {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.78rem;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--gilt);
}
.gl-sub {
  font-family: 'Alegreya Sans', sans-serif; font-style: italic;
  font-size: 0.8rem; color: var(--text-dim);
}
.gl-tools { margin-left: auto; display: flex; gap: 8px; padding-right: 30px; }
.gl-tool {
  background: rgba(var(--gilt-rgb), 0.08); border: 1px solid rgba(var(--gilt-rgb), 0.35);
  color: var(--gilt); text-decoration: none; cursor: pointer;
  font-size: 0.8rem; line-height: 1; border-radius: 3px; padding: 6px 10px;
  transition: background 150ms, border-color 150ms;
}
.gl-tool:hover { background: rgba(var(--gilt-rgb), 0.18); border-color: var(--gilt); }
.gl-body { position: relative; flex: 1; min-height: 0; background: var(--frame-ground); }
.gl-frame { width: 100%; height: 100%; border: none; display: block; background: var(--frame-ground); }
#glass[data-state="reaching"] .gl-frame,
#glass[data-state="refused"] .gl-frame { opacity: 0; }
/* the glass reuses the realm's one waiting state (.chron-scrying) — it had
   grown a private near-copy with its own keyframes, sizes and reduced-motion
   override, i.e. two spellings of a signature idiom (removed 2026-08-01) */
#glass .chron-scrying { display: none; }
#glass[data-state="reaching"] .chron-scrying { display: flex; }
.gl-note { position: absolute; inset: 0; display: none; }
#glass[data-state="refused"] .gl-note {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; padding: 40px; text-align: center;
}
.gl-refuse-head {
  font-family: 'Cinzel', serif; font-weight: 700; font-size: 0.8rem;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--gilt);
}
.gl-refuse-why {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.95rem; line-height: 1.55;
  color: var(--text-dim); max-width: 46ch;
}
.gl-refuse-go {
  background: var(--discover); color: #F9F5D7; border: none; cursor: pointer;
  font-family: 'Cinzel', serif; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.16em;
  padding: 11px 18px 10px; border-radius: 2px; margin-top: 4px;
  transition: background 200ms, box-shadow 200ms;
}
.gl-refuse-go:hover { background: var(--discover-hover); box-shadow: 0 4px 16px rgba(var(--shade-rgb), calc(0.4 * var(--shade-k))); }
.gl-foot {
  flex-shrink: 0; padding: 7px 16px 8px; border-top: 1px solid var(--ink-line);
  background: var(--surface-2);
  font-family: 'JetBrains Mono', monospace; font-size: 0.6rem;
  color: var(--text-dim); word-break: break-all;
}
#glass .ldg-close { position: absolute; top: 8px; right: 10px; }

@media (prefers-reduced-motion: reduce) {
  #glass { animation: none; }
  /* the shared .chron-scrying reduced-motion rule covers the dots */
}


/* ── Scene summary heads (.rt-*) — restored v1.47.1 (2026-10-15) ────────────
   Born with THE RITES (2026-07-28) and deleted with it in v1.45.0, but SEALS,
   THE DECOYS, ACCOUNTS and THE TRUST still write their one-line verdict
   into an .rt-head with .rt-alarm / .rt-calm / .rt-waiting spans — so since
   09-07 those heads were unstyled and their alarms never turned red. */
/* THE TRUST says what it is before it says anything else: the word alone
   tells a reader nothing, and the empty state is where they meet it first. */
.tr-about {
  padding: 12px 22px; border-bottom: 1px solid var(--hair);
  font-size: 0.92rem; line-height: 1.5; color: var(--text-dim);
}
.tr-about p + p { margin-top: 8px; }
.tr-about b { color: var(--text); font-weight: 600; }
.rt-head {
  padding: 6px 14px 8px; font-family: 'JetBrains Mono', monospace;
  font-size: 0.72rem; letter-spacing: 0.04em; border-bottom: 1px solid var(--ink-line);
}
.rt-alarm { color: var(--sem-down); font-weight: 700; }
.rt-warn { color: var(--sem-warn); }   /* THE TRUST's "worth fixing" — never styled until v1.47.1 */
.rt-calm { color: var(--sem-up); }
.rt-waiting { color: var(--text-dim); }

/* ── THE SEALS — certificates, soonest to lapse first ──────────────────────
   Sibling of THE RITES: same three-column rhythm, same panel furniture. The
   only colour is time remaining, so the eye sorts fine/soon/act without
   reading a number. */
.seal {
  display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.6fr) auto;
  gap: 10px; align-items: baseline;
  padding: 7px 0 7px 11px; border-left: 2px solid transparent;
  border-bottom: 1px solid var(--hair);
}
.seal:last-child { border-bottom: none; }
.seal-name {
  font-family: 'Cinzel', serif; font-weight: 600; font-size: 0.72rem;
  letter-spacing: 0.06em; color: var(--text); min-width: 0; overflow-wrap: anywhere;
}
.seal-host, .acc-src {
  display: block; font-family: 'Alegreya Sans', sans-serif; font-style: normal;
  font-size: 0.68rem; letter-spacing: 0.04em; color: var(--text-dim); opacity: 0.75;
}
.seal-mid { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.seal-issuer, .seal-renews, .seal-note {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.78rem; color: var(--text-dim);
}
.seal-issuer { color: var(--text); opacity: 0.85; }
.seal-note { opacity: 0.62; font-style: italic; }
/* the finding, not a blank cell: nothing renews this one */
.seal-manual { color: var(--sem-warn); opacity: 0.95; }
.seal-when { text-align: right; white-space: nowrap; }
.seal-days {
  display: block; font-family: 'Cinzel', serif; font-weight: 700;
  font-size: 0.7rem; letter-spacing: 0.08em;
}
.seal-date {
  display: block; font-family: 'JetBrains Mono', monospace;
  font-size: 0.62rem; color: var(--text-dim); opacity: 0.7;
}
.seal.s-calm   { border-left-color: rgba(var(--gilt-rgb), 0.18); }
.seal.s-calm    .seal-days { color: var(--sem-up); }
.seal.s-watch  { border-left-color: color-mix(in srgb, var(--sem-warn) 45%, transparent); }
.seal.s-watch   .seal-days { color: var(--sem-warn); }
.seal.s-soon   { border-left-color: var(--sem-warn); }
.seal.s-soon    .seal-days { color: var(--sem-warn); }
.seal.s-lapsed { border-left-color: var(--sem-down); }
.seal.s-lapsed  .seal-days { color: var(--sem-down); }
.seal.s-unknown .seal-days { color: var(--text-dim); }

@media (max-width: 720px) {
  .seal { grid-template-columns: 1fr auto; }
  .seal-mid { grid-column: 1 / -1; }
}


/* ── ACCOUNTS ─────────────────────────────────────────────────────────────────
   A roster is long and almost all of it is the design working as intended:
   `link` can sudo everywhere, on purpose. The single fact worth crossing the
   room for is an account that elevates WITHOUT BEING ASKED, so `nopasswd` is
   the only loud row on the page — the map's existing `--sem-down`, spent once,
   per this design's own "if everything glows, nothing does". `password` sudo
   takes the warn tone at low weight because it is worth SEEING and not worth
   alarming about; `none` takes no colour at all, which is most of the list.

   Deliberately the same grid, rhythm and left-rule idiom as `.seal` rather
   than a second look for a second inventory — two inventories that read
   differently are two things to learn. No new tokens. */
.acc-row {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  gap: 10px; align-items: baseline;
  padding: 7px 0 7px 11px; border-left: 2px solid transparent;
  border-bottom: 1px solid var(--hair);
}
.acc-row:last-child { border-bottom: none; }
.acc-name {
  font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
  color: var(--text); min-width: 0; overflow-wrap: anywhere;
}
/* a login is a literal string on a host — mono, like every other piece of hard
   data in this app; the Cinzel treatment `.seal-name` gets is for proper nouns */
/* (.acc-src is styled with .seal-host, in THE SEALS) */
.acc-mid { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.acc-sudo {
  font-family: 'Cinzel', serif; font-weight: 700;
  font-size: 0.66rem; letter-spacing: 0.1em; color: var(--text-dim);
}
.acc-groups { display: flex; flex-wrap: wrap; gap: 4px; }
.acc-group {
  font-family: 'JetBrains Mono', monospace; font-size: 0.6rem;
  padding: 1px 6px; border-radius: 20px;   /* chips 20px, per the radius scale */
  color: var(--text-dim); background: var(--hair);
  border: 1px solid rgba(var(--gilt-rgb), 0.14);
}
.acc-note {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.78rem;
  color: var(--text-dim); opacity: 0.62; font-style: italic;
}
.acc-row.acc-plain   .acc-sudo { color: var(--text-dim); opacity: 0.6; }
.acc-row.acc-sudo    { border-left-color: color-mix(in srgb, var(--sem-warn) 35%, transparent); }
.acc-row.acc-sudo    .acc-sudo { color: var(--sem-warn); opacity: 0.85; }
.acc-row.acc-unknown .acc-sudo { color: var(--text-dim); font-style: italic; }
/* the finding */
.acc-row.acc-nopass  { border-left-color: var(--sem-down); }
.acc-row.acc-nopass  .acc-sudo { color: var(--sem-down); }
.acc-flag { color: var(--sem-down); font-weight: 700; }

@media (max-width: 720px) {
  .acc-row { grid-template-columns: 1fr; }
}


/* ── Chart tokens + native watchings (v1.35.0) ──────────────────────────────
   The Chronicle and the Abyss draw their own figures now (static/js/charts.js
   over api/v2/charts) where they used to frame Grafana. Chrome follows the
   region — the sky's viewer is parchment, the Abyss's is dark — so each
   viewer sets the `--ch-*` tokens the chart layer reads. The categorical
   slots are the zone hues (the design notes "same inkpot"): sky, gilt, rust, moss,
   depth, then three quiet neighbours; the parchment set is the same order,
   darkened to clear 3:1 on the light viewer. Semantic inks stay the app's
   own (up/warn/down). */
.chron-viewer {
  --ch-1: #7FB0C4; --ch-2: #C49A52; --ch-3: #C06B62; --ch-4: #7A9B6E;
  --ch-5: #7FE0C8; --ch-6: #9A7FB0; --ch-7: #D9C08A; --ch-8: #8A9BA8;
  --ch-up: #7A9B6E; --ch-warn: #C49A52; --ch-down: #C06B62;
  --ch-grid: rgba(201,165,84,0.12); --ch-ink: #8A8069; --ch-text: #C9BFA8; --ch-surface: #0b0d12;
}
#chronicle .chron-viewer {
  --ch-1: #3F7D95; --ch-2: #A8792E; --ch-3: #B0524A; --ch-4: #5D8452;
  --ch-5: #3F9B86; --ch-6: #7D5F9C; --ch-7: #8C7A3A; --ch-8: #5D6D7A;
  --ch-up: #5D8452; --ch-warn: #A8792E; --ch-down: #B0524A;
  --ch-grid: rgba(60,50,30,0.14); --ch-ink: #6B6046; --ch-text: #2A241A; --ch-surface: #f4f2ea;
}
/* the figure fills its pane exactly as a frame did, and fades in the same way */
.chron-native {
  position: absolute; inset: 0; box-sizing: border-box; padding: 10px 12px 8px;
  opacity: 0; transition: opacity 260ms ease; overflow: hidden;
  font-family: 'Alegreya Sans', system-ui, sans-serif; color: var(--ch-text);
}
.chron-slide.scried .chron-native, #embed-max.scried .chron-native { opacity: 1; }
.chron-native canvas { display: block; }
/* a table is the one figure allowed to be taller than its pane — it scrolls;
   a canvas never does (Chart.js sizes to the box, and a scrollbar would
   feed back into that measurement) */
.chron-native[data-kind="table"] { overflow: auto; }
/* the regions centre their text (the slide heads, the captions); a figure's
   own text sets its alignment back */
.chron-native, .chart-twin { text-align: left; }
/* a native pane never sleeps — nothing here swallows the wheel, so the frame
   hint and the click-to-wake step do not apply */
.chron-pane.native::after { content: none; }
.chron-native .chart-note {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-family: 'IM Fell English', serif; font-style: italic; color: var(--ch-ink); font-size: 0.95rem;
}
/* ☷ the numbers twin — over the figure, never instead of it */
.chart-twin {
  position: absolute; inset: 0; overflow: auto; padding: 10px 12px; z-index: 2;
  background: var(--ch-surface);
}
.chron-native.showdata canvas, .chron-native.showdata .chart-stats { visibility: hidden; }
.chart-table { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.chart-table th {
  text-align: left; font-family: 'Cinzel', serif; font-size: 0.52rem; letter-spacing: 0.16em;
  text-transform: uppercase; color: var(--ch-ink); font-weight: 600;
  padding: 4px 8px 6px; border-bottom: 1px solid var(--ch-grid);
}
.chart-table td { padding: 5px 8px; border-bottom: 1px solid var(--ch-grid); vertical-align: middle; }
.chart-table .num, .chart-table th.num { text-align: right; font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
.chart-table .chart-label { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; word-break: break-all; }
.chart-table td.bar { width: 28%; }
.chart-meter { height: 6px; background: var(--ch-grid); border-radius: 3px; overflow: hidden; }
.chart-meter i { display: block; height: 100%; background: var(--ch-1); }
.chart-ip {
  background: none; border: 0; padding: 0; cursor: pointer; color: var(--ch-text);
  font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; text-decoration: underline dotted; text-underline-offset: 3px;
}
.chart-ip:hover { color: var(--ch-1); }
/* stat tiles — the same parchment-tile idea as the card's pulse (.cv-spark),
   in the region's ink */
.chart-stats {
  height: 100%; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px; align-content: center;
}
.chart-stat {
  position: relative; min-width: 0; height: 84px; overflow: hidden; padding: 9px 11px 0;
  border: 1px solid var(--ch-grid); border-radius: 4px; background: color-mix(in srgb, var(--ch-surface) 92%, var(--ch-1));
}
.chart-stat-label {
  display: block; font-family: 'Cinzel', serif; font-size: 0.5rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--ch-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.chart-stat-now {
  display: block; font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums;
  font-size: 1.25rem; font-weight: 600; color: var(--ch-text); line-height: 1.25;
}
.chart-spark { position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 28px; display: block; }
.chart-spark-line { fill: none; stroke: var(--ch-1); stroke-width: 1.6; }
.chart-spark-fill { fill: var(--ch-1); opacity: 0.16; }
/* the slide head's ☷ sits with ⓘ and ⛶ */
.chron-twin { font-size: 0.95rem; }
/* ⧗ three weeks ⇄ all time — a small label in the head's own ink, lit while
   the wide window is up. It reads the window actually drawn ("3W", "30D",
   "1Y"), never a promise the source cannot keep. */
.chron-range {
  font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.08em; padding: 1px 6px; border-radius: 3px;
  border: 1px solid rgba(var(--gilt-rgb), 0.35); background: none; color: inherit; cursor: pointer;
}
.chron-range[aria-pressed="true"] { background: rgba(var(--gilt-rgb), 0.18); border-color: var(--gilt); }
/* THE SOURCES (v1.37.0): four TEST buttons and a Save share one row and wrap */
.src-tests { flex-wrap: wrap; gap: 6px; }
.acct-note em { font-style: normal; color: var(--text-dim); }
/* THE DEPLOYMENT (v1.38.0): a read-only ledger of knobs — name, value, layer.
   Defaults sit dimmer so the ones somebody actually set are what the eye finds. */
.dep-list { display: flex; flex-direction: column; gap: 2px; max-height: 42vh; overflow: auto; }
.dep-row { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) auto; gap: 8px; align-items: baseline;
  padding: 3px 4px; border-bottom: 1px solid rgba(var(--gilt-rgb), 0.1); font-size: 0.78rem; }
.dep-name { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text); word-break: break-all; }
.dep-val { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-dim); word-break: break-all; }
.dep-secret { font-style: italic; }
.dep-default .dep-name, .dep-default .dep-val { opacity: 0.55; }
/* ⛶ a native watching filling the window: the same framed panel the iframe gets */
#embed-max-native {
  position: relative; flex: 1; width: 100%; height: 100%; border-radius: 8px;
  background: #0b0d12; box-shadow: 0 30px 90px rgba(0,0,0,0.6);
  --ch-1: #7FB0C4; --ch-2: #C49A52; --ch-3: #C06B62; --ch-4: #7A9B6E;
  --ch-5: #7FE0C8; --ch-6: #9A7FB0; --ch-7: #D9C08A; --ch-8: #8A9BA8;
  --ch-up: #7A9B6E; --ch-warn: #C49A52; --ch-down: #C06B62;
  --ch-grid: rgba(201,165,84,0.12); --ch-ink: #8A8069; --ch-text: #C9BFA8; --ch-surface: #0b0d12;
}
#embed-max-native[hidden], #embed-max-frame[hidden] { display: none; }
#embed-max-native .chron-native { padding: 44px 28px 20px; }

/* ── THE DECOYS ────────────────────────────────────────────────────────
   Bait has no legitimate caller, so a stone that speaks is the only thing on
   this page that has happened. It takes the map's existing alarm language and
   nothing else: --sem-down for speaking, --sem-warn for stirred, --text-dim for
   the ordinary silent case. No new colours (the design notes), and the loud one is
   spent exactly once. */
/* One column, NOT the shared wrapping row. Every sibling scene groups its
   contents into `.ldg-block`s that size themselves, so `.ldg-list` wraps them
   side by side. This list is ORDERED — loudest first — and wrapping put the
   second-most-urgent stone beside the third, which throws away the one thing
   the ordering was for. */
#gs-list { flex-direction: column; flex-wrap: nowrap; gap: 0; }

.gs-head { display: flex; flex-direction: column; gap: 4px; }
.gs-verdict {
  font-family: 'Cinzel', serif; font-size: 0.95rem;
  letter-spacing: 0.32em; text-transform: uppercase; color: var(--text);
}
.gs-verdict-loud { color: var(--sem-down); }
.gs-verdict-calm { color: var(--text-dim); }
.gs-verdict-sub { font-size: 0.86rem; color: var(--text-dim); letter-spacing: 0.01em; }
.gs-head-lost .gs-verdict { color: var(--sem-warn); }
.gs-head-dark .gs-verdict { color: var(--text-dim); }
.gs-warn {
  font-family: 'JetBrains Mono', monospace; font-size: 0.78rem;
  color: var(--sem-warn); margin-top: 2px;
}

.gs-row {
  display: flex; gap: 14px; align-items: flex-start;
  padding: 12px 4px; border-bottom: 1px solid var(--ink-line);
}
.gs-row:last-child { border-bottom: 0; }
.gs-voice { flex: 0 0 14px; padding-top: 5px; }
.gs-dot {
  display: block; width: 10px; height: 10px; border-radius: 50%;
  background: var(--ink-soft);
}
.gs-body { flex: 1 1 auto; min-width: 0; }
.gs-line1 { display: flex; align-items: baseline; gap: 10px; }
.gs-name {
  font-family: 'Cinzel', serif; font-size: 0.92rem;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--text);
}
.gs-state {
  font-family: 'JetBrains Mono', monospace; font-size: 0.72rem;
  letter-spacing: 0.18em; color: var(--text-dim);
}
.gs-line2 { font-size: 0.9rem; color: var(--text-dim); margin-top: 2px; }
.gs-line3 {
  display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 5px;
  font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--text-dim);
}
.gs-host { color: var(--text); }
.gs-zone { letter-spacing: 0.1em; }
.gs-trips, .gs-spoke { color: var(--text-dim); }
.gs-flag { color: var(--sem-warn); }

/* Silent is the ordinary state and the one we hope for — it reads as
   background, not as a result. */
.gs-silent .gs-dot { background: var(--ink-soft); }

.gs-stirred .gs-dot { background: var(--sem-warn); }
.gs-stirred .gs-state { color: var(--sem-warn); }

/* A charted stone the engine said nothing about. Not silent — unheard. */
.gs-unheard .gs-dot { background: transparent; border: 1px solid var(--ink-soft); }

/* The one time the quiet map raises its voice (CONCEPT §8). Markers breathe at
   3.2s map-wide, so this does too — the same heartbeat, in the alarm tone. */
.gs-speaking .gs-dot {
  background: var(--sem-down);
  box-shadow: 0 0 0 0 rgba(251,73,52,0.55);
  animation: gs-breathe 3.2s ease-in-out infinite;
}
.gs-speaking .gs-name { color: var(--sem-down); }
.gs-speaking .gs-state { color: var(--sem-down); letter-spacing: 0.24em; }
@keyframes gs-breathe {
  0%, 100% { box-shadow: 0 0 0 0 rgba(251,73,52,0.55); }
  50%      { box-shadow: 0 0 0 7px rgba(251,73,52,0); }
}

/* the design notes: full reduced-motion fallback, always. The stone still reads as
   speaking — it keeps the alarm tone and a static ring — it simply stops
   pulsing, so the state survives without the motion. */
@media (prefers-reduced-motion: reduce) {
  .gs-speaking .gs-dot {
    animation: none;
    box-shadow: 0 0 0 4px rgba(251,73,52,0.28);
  }
}

/* ── THE TRIPWIRES (v1.52.0) ─────────────────────────────
   The decoys scene grown into two families, per-source contact, trips with
   an acknowledge, and the setup a sender needs. Same idiom as the rest of the
   scene: Cinzel micro-caps for heads, mono for data, the one loud colour
   (--sem-down) spent only on a touched tripwire. Tokens only, so the panel
   follows the colour scheme. */
.gs-sources { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; }
.gs-src { font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--text-dim); }
.gs-src b { font-weight: 600; color: var(--text); }
.gs-src-lost, .gs-src-waiting { color: var(--sem-warn); }
.gs-family { padding-top: 6px; }
.gs-family + .gs-family { border-top: 1px solid var(--ink-line); margin-top: 6px; }
.gs-family-head {
  font-family: 'Cinzel', serif; font-size: 0.66rem; font-weight: 700;
  letter-spacing: 0.24em; color: var(--gilt); margin: 10px 0 4px;
}
.gs-family-sub {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.82rem; font-weight: 400;
  letter-spacing: 0.01em; color: var(--text-dim); margin-left: 8px;
}
.gs-trip {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin-top: 4px;
  font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--text-dim);
}
.gs-speaking .gs-trip-line { color: var(--text); }
.gs-trip-loose { padding: 6px 0 6px 24px; border-bottom: 1px solid var(--hair); }
.gs-trip-loose .gs-trip-line { color: var(--sem-down); }
.gs-ack, .gs-copy, .gs-setup-toggle, .gs-add {
  font-family: 'Cinzel', serif; font-size: 0.56rem; letter-spacing: 0.14em;
  text-transform: uppercase; background: none; color: var(--text-dim);
  border: 1px solid var(--ink-line); border-radius: 3px;
  padding: 3px 9px; cursor: pointer; transition: color 150ms, border-color 150ms;
}
.gs-ack:hover, .gs-copy:hover, .gs-setup-toggle:hover, .gs-add:hover,
.gs-ack:focus-visible, .gs-copy:focus-visible, .gs-setup-toggle:focus-visible, .gs-add:focus-visible {
  color: var(--gilt); border-color: var(--gilt);
}
.gs-ack:disabled { opacity: 0.4; cursor: default; }
.gs-setup { border-top: 1px solid var(--ink-line); margin-top: 12px; padding-top: 10px; }
.gs-setup-bar { display: flex; flex-wrap: wrap; gap: 8px; }
.gs-setup-toggle { color: var(--gilt); border-color: rgba(var(--gilt-rgb), 0.4); font-weight: 700; }
.gs-setup-body { margin-top: 8px; }
.gs-setup-p { font-size: 0.9rem; color: var(--text-dim); margin: 8px 0; line-height: 1.5; }
.gs-setup-p code, .gs-notes code {
  font-family: 'JetBrains Mono', monospace; font-size: 0.8em; color: var(--text);
  background: rgba(var(--shade-rgb), calc(0.25 * var(--shade-k))); padding: 1px 5px; border-radius: 3px;
}
.gs-code { margin: 6px 0 10px; border: 1px solid var(--ink-line); border-radius: 4px; overflow: hidden; }
.gs-code-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 4px 8px; background: var(--surface-2);
  font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--text-dim);
}
.gs-code pre {
  margin: 0; padding: 8px 10px; max-height: 260px; overflow: auto;
  font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; line-height: 1.45;
  color: var(--text); background: rgba(var(--shade-rgb), calc(0.22 * var(--shade-k)));
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.gs-notes { margin: 4px 0 8px 18px; font-size: 0.86rem; color: var(--text-dim); line-height: 1.5; }

/* ── THE TRUST ──────────────────────────────────────────────────────────────
   Who may speak for a client. Reuses the ledger panel chrome (.ldg-*) and the
   account row's grammar; only the parts with no precedent are new.

   COLOUR IS SPENT ONLY ON FINDINGS. Seven apps that are all configured
   correctly is the expected state and must read as calm — the design notes's "if
   everything glows, nothing does", the same rule .acc-nopass follows. So the
   form word is the map's dim text until it is wrong, and no row carries a
   left edge unless it carries a finding.
   Addresses are DATA: JetBrains Mono with tabular numerals, like every IP,
   port and U-slot in this app. */
.tr-proxies {
  padding: 0 0 12px; margin-bottom: 12px;
  border-bottom: 1px solid rgba(var(--gilt-rgb), 0.18);
}
.tr-proxies-head {
  font-family: 'Cinzel', serif; font-weight: 700;
  font-size: 0.66rem; letter-spacing: 0.22em; color: var(--text-dim);
  margin-bottom: 8px;
}
.tr-proxy {
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.6fr);
  gap: 10px; align-items: baseline; padding: 4px 0;
}
.tr-proxy-name {
  font-family: 'Cinzel', serif; font-weight: 600;
  font-size: 0.74rem; letter-spacing: 0.08em; color: var(--text);
}
.tr-proxy-note {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.76rem;
  color: var(--text-dim); opacity: 0.62;
}
.tr-addr {
  font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums;
  font-size: 0.72rem; color: var(--text); white-space: nowrap;
}
.tr-row {
  display: grid; grid-template-columns: 5.5rem minmax(0, 1fr);
  gap: 10px; align-items: baseline; padding: 5px 0 5px 11px;
}
.tr-key {
  font-family: 'Cinzel', serif; font-weight: 700;
  font-size: 0.62rem; letter-spacing: 0.14em; color: var(--text-dim); opacity: 0.7;
}
.tr-vals { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: baseline; min-width: 0; }
.tr-believe { display: inline-flex; gap: 6px; align-items: baseline; }
.tr-believe i {
  font-family: 'Alegreya Sans', sans-serif; font-style: italic;
  font-size: 0.74rem; color: var(--text-dim); opacity: 0.7;
}
/* an address we cannot put a name to is the finding, not a detail */
.tr-believe.tr-unknown .tr-addr { color: var(--sem-warn); }
.tr-believe.tr-none i { opacity: 0.55; }
.tr-hint {
  font-family: 'Alegreya Sans', sans-serif; font-style: italic;
  font-size: 0.74rem; color: var(--text-dim); opacity: 0.55;
}
/* the one-word diagnosis, in the head row beside the app's name */
.tr-form-ok   { color: var(--text-dim); opacity: 0.75; }
.tr-form-none { color: var(--sem-warn); }
.tr-form-bad  { color: var(--sem-down); font-weight: 700; }
.tr-finding {
  display: grid; grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: 10px; align-items: baseline;
  margin: 5px 0 0 11px; padding: 6px 0 6px 10px;
  border-left: 2px solid transparent;
}
.tr-level {
  font-family: 'Cinzel', serif; font-weight: 700;
  font-size: 0.62rem; letter-spacing: 0.12em;
}
.tr-text {
  font-family: 'Alegreya Sans', sans-serif; font-size: 0.82rem;
  color: var(--text-dim); line-height: 1.45;
}
.tr-finding.tr-critical { border-left-color: var(--sem-down); }
.tr-finding.tr-critical .tr-level { color: var(--sem-down); }
.tr-finding.tr-warn     { border-left-color: color-mix(in srgb, var(--sem-warn) 45%, transparent); }
.tr-finding.tr-warn     .tr-level { color: var(--sem-warn); }
/* a note is context, not a problem — it must not read as one */
.tr-finding.tr-note     .tr-level { color: var(--text-dim); opacity: 0.55; }
.tr-finding.tr-note     .tr-text { opacity: 0.62; }
.tr-app.tr-flagged .ldg-zname { color: var(--sem-down); }

/* ── REGIONS (v1.49.0) ─────────────────────────────────────
   A region is a rectangle on a map that opens into a detailed map of its own.
   On the main map it is an inked frame with a name tab; open, it is the lens:
   a full-window view over the main map with its own camera, a breadcrumb, a
   locator and a hint line. Frames and lens markers sit inside a scaled stage
   and divide by `--rk` (that stage's zoom) so tabs, borders and dots keep one
   size on screen at every zoom. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

#region-frames { position: absolute; inset: 0; z-index: 7; pointer-events: none; }
.rg-frame {
  position: absolute; box-sizing: border-box; pointer-events: none;
  border: calc(1.5px / var(--rk, 1)) dashed rgba(var(--gilt-rgb), 0.55);
  border-radius: calc(6px / var(--rk, 1));
  box-shadow: inset 0 0 0 calc(1px / var(--rk, 1)) rgba(0, 0, 0, 0.18);
  transition: border-color 200ms ease, background-color 200ms ease;
}
.rg-frame:has(.rg-tab:hover), .rg-frame:has(.rg-tab:focus-visible) {
  border-color: var(--gilt-bright); border-style: solid;
  background-color: rgba(var(--gilt-rgb), 0.07);
}
.rg-tab {
  position: absolute; left: 0; top: 0; pointer-events: auto; cursor: zoom-in;
  /* inside the frame's corner, so a region at the map's top edge never tucks
     its tab under the scene bar */
  transform: translate(var(--tab-dx, 0px), var(--tab-dy, 0px)) scale(calc(1 / var(--rk, 1)));
  transform-origin: 0 0;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px 4px 8px; border: 1px solid rgba(var(--gilt-rgb), 0.6); border-top: 0; border-left: 0;
  border-radius: 0 0 7px 0; background: rgba(29, 32, 33, 0.82);

  color: var(--text); font: 600 11px/1.3 'Cinzel', serif; letter-spacing: 0.18em;
  text-transform: uppercase; white-space: nowrap;
  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.35);
  transition: background-color 160ms ease, color 160ms ease;
}
.rg-tab:hover, .rg-tab:focus-visible { background: rgba(29, 32, 33, 0.95); color: var(--gilt-bright); }
.rg-tab:focus-visible { outline: 2px solid var(--gilt-bright); outline-offset: 2px; }
.rg-glyph { color: var(--gilt-bright); font-size: 13px; letter-spacing: 0; }
/* nudged off the frame's corner (out from under the chrome): it floats, so a full outline */
.rg-frame.rg-nudged .rg-tab { border: 1px solid rgba(var(--gilt-rgb), 0.6); border-radius: 7px; }
.rg-frame.rg-small .rg-name { display: none; }          /* too small on screen to read: the glyph alone */
body.region-open #colophon { opacity: 0; }               /* the footer would sit on the detailed map */
/* A region has edges, not a sky or Depths. Its map sits on the abyss colour. */
body.region-open #viewport, body.rg-crossing #viewport { background: var(--abyss); }
body.region-open #sky-realm, body.region-open #chronicle, body.region-open #void-fade,
body.region-open #void, body.region-open #depths-realm, body.region-open #abyss,
body.region-open #sky-island, body.region-open #sky-shadow, body.region-open #star-hide,
body.region-open #btn-sky, body.region-open #btn-depths { display: none; }

/* only the way back lives here; the map underneath takes the pointer */
.region-lens { position: fixed; inset: 0; z-index: 930; pointer-events: none; }
.region-lens[hidden] { display: none; }
.rl-crumbs, .rl-locator { pointer-events: auto; }

.rl-crumbs {
  position: fixed; left: 76px; top: var(--crown-clear); z-index: 964;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  max-width: calc(100vw - 120px); padding: 6px 12px; border-radius: 8px;
  background: rgba(29, 32, 33, 0.86); border: 1px solid rgba(var(--gilt-rgb), 0.35);
  font: 600 12px/1.3 'Cinzel', serif; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--text-dim); box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
}
.rl-crumb { color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit; }
button.rl-crumb {
  background: none; border: 0; padding: 2px 4px; cursor: pointer; border-radius: 4px;
  color: var(--text-dim);
}
button.rl-crumb:hover, button.rl-crumb:focus-visible { color: var(--gilt-bright); }
button.rl-crumb:focus-visible { outline: 2px solid var(--gilt-bright); outline-offset: 1px; }
.rl-here { color: var(--gilt-bright); padding: 2px 4px; }
.rl-sep { color: rgba(var(--gilt-rgb), 0.6); }

.rl-locator {
  position: fixed; left: 24px; bottom: 24px; z-index: 964; width: 168px; cursor: zoom-out;
  border: 1px solid rgba(var(--gilt-rgb), 0.45); border-radius: 8px; overflow: hidden;
  background: var(--surface); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
  transition: border-color 160ms ease, transform 160ms ease;
}
.rl-locator:hover, .rl-locator:focus-visible { border-color: var(--gilt-bright); transform: translateY(-2px); }
.rl-locator:focus-visible { outline: 2px solid var(--gilt-bright); outline-offset: 2px; }
.rl-locator img { display: block; width: 100%; height: auto; opacity: 0.85; }
.rl-loc-box {
  position: absolute; box-sizing: border-box; border: 2px solid var(--gilt-bright);
  background: rgba(var(--gilt-rgb), 0.18); box-shadow: 0 0 0 999px rgba(0, 0, 0, 0.35);
}

.rl-hint {
  position: fixed; left: 50%; bottom: 28px; z-index: 964; transform: translate(-50%, 8px);
  padding: 7px 16px; border-radius: 999px; pointer-events: none;
  background: rgba(29, 32, 33, 0.9); border: 1px solid rgba(var(--gilt-rgb), 0.4);
  color: var(--text); font: 500 13px/1.3 'Alegreya Sans', sans-serif; letter-spacing: 0.02em;
  opacity: 0; transition: opacity 220ms ease, transform 220ms ease;
}
.rl-hint.on { opacity: 1; transform: translate(-50%, 0); }

@media (max-width: 720px) {
  .rl-crumbs { left: 16px; right: 16px; top: auto; bottom: 16px; max-width: none; }
  .rl-locator { width: 112px; bottom: 76px; left: 16px; }
  .rl-hint { bottom: 140px; }
}
@media (prefers-reduced-motion: reduce) {
  #stage, .rl-hint, .rl-locator { transition: none; }
}

/* ── THE LIGHT SCHEME's one-offs (v1.52.0) ─────────────────────────────────
   The panels' own literals that no shared token describes: a lighter red
   for a warning label, a hover tint of the gilt fill, the IPAM grid's empty
   cell, THE TRIBUTE's reserved stripes. Everything else flips through the
   tokens on the panel containers (top of this file). Rack devices stay dark
   metal in both schemes (the design notes, THE ARMORY rule 10). */
:root[data-scheme="light"] .sp-btn-warn { color: var(--sem-down); border-color: rgba(var(--down-rgb), 0.45); }
:root[data-scheme="light"] .sp-btn-warn:hover { background: rgba(var(--down-rgb), 0.1); }
:root[data-scheme="light"] .sp-btn-go:hover { background: var(--gilt-deep); }
/* Free cells are a visible grid, and a charted one is solid: the realm's hues are
   pale for the dark scheme, so they take the same navy the host addresses do. */
:root[data-scheme="light"] .ldg-cell { background: rgba(15, 23, 42, 0.13); }
:root[data-scheme="light"] .ldg-cell.edge { background: rgba(15, 23, 42, 0.34); }
:root[data-scheme="light"] .ldg-cell.lit {
  background: color-mix(in srgb, var(--c-hue) 45%, var(--text));
  box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.3);
}
/* a line passes behind a label instead of through it */
:root[data-scheme="light"] :is(.pth-name, .pth-ip) {
  background: rgba(202, 206, 212, 0.92); padding: 0 4px; border-radius: 2px;
}
:root[data-scheme="light"] .trb-seg.reserve {
  background: repeating-linear-gradient(45deg, #d5c4a1, #d5c4a1 4px, #ebdbb2 4px, #ebdbb2 8px);
}
:root[data-scheme="light"] .ldg-legend-item.ldg-r-reserved { color: #af3a03; }
/* Dataset hues (a zone's, a port's) were chosen against the night palette; as
   TEXT on a light panel the pale ones vanish. Mixed 40/60 toward ink they keep
   their family and clear AA on cream; strokes and dots keep the pure hue. */
:root[data-scheme="light"] .idx-zone { color: color-mix(in srgb, var(--zone-hue, var(--text-dim)) 40%, var(--text)); }
:root[data-scheme="light"] .ldg-host-ip { color: color-mix(in srgb, var(--z-hue, var(--gilt)) 40%, var(--text)); }
:root[data-scheme="light"] .ldg-port { color: color-mix(in srgb, var(--p-hue, var(--gilt)) 40%, var(--text)); }

/* ── KEYBOARD FOCUS on the panels (v1.52.0, accessibility pass) ─────────────
   One visible ring for every control a panel holds, in the panel's own gilt,
   so it follows the colour scheme (the browser's default ring was the only
   cue, and on some controls nothing showed). Mouse clicks draw nothing
   (:focus-visible). The two search fields keep their borderless inputs and
   light their frame instead. */
:is(#index-drawer, .ldg-panel, .trb-panel, .rel-panel, .pth-panel, .pal-panel, .gl-panel,
    .dsk-panel, .wiz-panel) :is(button, a, summary, [role="button"], [tabindex]):focus-visible {
  outline: 2px solid var(--gilt); outline-offset: 2px;
}
.idx-search-wrap:focus-within { border-color: var(--gilt); }
.pal-input-wrap:focus-within { border-bottom-color: var(--gilt); }

/* ── A bright picture (owner, 2026-11-01) ─────────────────────────────────────
   The Atlas's ink was drawn for a dark sand table. On a light parchment a style
   marks itself `bright` (its sidecar, mapart.py) and the ink comes to it: names sit
   on a dark plate, and the art drops back while the network is up, so the picture
   stays whole the rest of the time. */
body.style-bright .mk-label {
  background: rgba(13, 11, 7, 0.78); color: #F4ECD8; padding: 2px 8px; border-radius: 3px;
  letter-spacing: 0.16em; text-shadow: none;
}
body.style-bright .zone-label {
  background: rgba(13, 11, 7, 0.62); padding: 6px 14px; border-radius: 4px; text-shadow: none;
}
/* a name under the pointer comes up off the parchment (owner, 2026-11-01) */
body.style-bright .mk:hover .mk-label {
  background: rgba(13, 11, 7, 0.96); font-size: 1.2em;
}
body.style-bright .zone-label { pointer-events: auto; transition: opacity 600ms ease, background 150ms, transform 150ms; }
body.style-bright .zone-label:hover {
  background: rgba(13, 11, 7, 0.96); opacity: 1 !important; transform: translate(-50%, -50%) scale(1.15);
}
body.style-bright .mk-dot { filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.9)); }
body.style-bright.net-on #map-base,
body.style-bright.constellation #map-base { filter: brightness(0.42) saturate(0.7); }
body.style-bright.net-on .mk .mk-label { opacity: 1; }
body.style-bright #overlay.net-on path.route { stroke-width: 4.5; }
body.style-bright #overlay.net-on path.route-wan,
body.style-bright #overlay.net-on path.route-edge { opacity: 0.9; }
body.style-bright #overlay.net-on .territory { fill-opacity: 0.16; stroke-opacity: 0.85; }

/* ── THE HOVER GLOW (owner, 2026-11-02) ──────────────────────────────────
   Every control with a hover state answers it the way ☰ does in the band: a
   soft, round gilt glow around its own shape. One token, one rule, so a new
   button joins by adding its selector here rather than inventing a shadow.
   Keyboard focus gets the same answer, since it is the same question.
   Each control keeps its own ink, border and fill changes; only the glow is
   shared. Things ON the map (markers, realms, rack faceplates, the cells of
   the ledger) are not controls and keep their own light. Two controls stay
   dark on purpose: ⚙ and ☀ in the band's `.menu-tools`, whose `overflow:
   hidden` would cut a glow to a square edge (`#scenes-list .ci-gear:hover`
   above says so, and outranks this list: keep the list free of ids).
   A row or chip with square corners would glow square, so each one here gets
   at least the radius scale's smallest step. "If everything glows, nothing
   does" (the design notes) still holds: this light is only ever under the pointer. */
/* Only the strength is a token. The colour is `--gilt-rgb` read where the glow
   is drawn, because the light scheme sets its deep-blue gilt on the panels, not
   on :root: a glow worked out on :root would be the night's gold everywhere. */
:root { --hover-glow-a: 0.45; }
:root[data-scheme="light"] { --hover-glow-a: 0.32; }   /* a shade softer on pale glass */

/* list rows and chips that were square */
:is(.scroll-row, .trb-row, .ldg-host, .pth-node, .dsk-nav, .ann-filter, .rel-seg, .rel-tool,
    .rel-legend-item, .dtab, .sp-sec > .sp-sub) { border-radius: 4px; }

:is(
  /* buttons */
  .chrome, .sp-btn, .chron-btn, .chron-arrow, .chron-open, .chron-fs, .chron-info, .aw-go,
  .band-arrow, .card-close, .card-min, .card-out, .card-link, .card-split, .csv-open,
  .csv-metrics, .csv-back, .idx-seek, .idx-search-clear, .embed-max-arrow,
  .ldg-close, .trb-close, .rel-close, .rel-map-btn, .rel-tool, .gl-tool, .gl-refuse-go,
  .cal-arrow, .cal-today, .gs-ack, .gs-copy, .gs-setup-toggle, .gs-add, .svc-pin, .rl-locator,
  button.rl-crumb, .acct-emb, .dsk-btn, .dsk-icon, .scene-edit, .ann-ack-btn,
  .oc-btn, .oc-actions button, .oc-caret,
  /* tabs */
  .scene, .dtab, .con-tab, .rg-tab, .rel-seg, .ann-filter, .dsk-nav, .sp-sec > .sp-sub,
  /* chips */
  .rel-legend-item, button.rel-port, .wiz-choice, .dsk-mapart-tile, .chart-ip,
  /* list rows */
  .svc-row, .idx-item, .scroll-row, .trb-row, .ldg-host, .pth-node, .spm-row, .rel-rear-item
):is(:hover, :focus-visible):not(:disabled) {
  box-shadow: 0 0 14px rgba(var(--gilt-rgb), var(--hover-glow-a));
}
/* apart, so two ids cannot lift the whole list over the band's own no-glow rules */
#embed-max-close:is(:hover, :focus-visible), #first-survey:is(:hover, :focus-visible) {
  box-shadow: 0 0 14px rgba(var(--gilt-rgb), var(--hover-glow-a));
}
