/*  ── BASE PALETTE FALLBACK ─────────────────────────────────────────────────
    ⛔ ADDED 2026-09-17. This sheet USES sixteen custom properties and DEFINED seven, on the
    assumption that every page carries the base palette in its own inline :root. Three lore pages,
    a story and the colophon link this sheet and have no :root of their own, so --ember, --surface,
    --sophia and friends resolved to nothing on them. An undefined custom property is not an error
    and not a fallback: the declaration is discarded at compute time and the element silently keeps
    whatever it inherited.

    ★ A SHARED SHEET THAT DEPENDS ON ITS CALLERS FOR ITS OWN VOCABULARY IS NOT SHARED, IT IS
    BORROWED. Declared first so any page-level :root still wins by source order.

    Found by a custom-property coverage check added to tools/preflight.mjs the same day, after the
    homepage shipped six rules referencing a token defined in a different page's generator. */
:root{
  --void:#08070a; --abyss:#0c0b10; --deep:#111016; --surface:#18161f; --edge:#242230;
  --text:#a09cb0; --light:#d0cce0; --white:#f0eef8;
  --ember:#d4a04a; --ember-dim:rgba(212,160,74,.12); --ember-glow:rgba(212,160,74,.25);
  --sophia:#7b6cc7; --sophia-dim:rgba(123,108,199,.12);
  /*  Secondary text. rgb(90,86,112) measures 2.87:1 on this paper and fails the 4.5 floor at
      small sizes; this measures 4.74 and is still far quieter than body prose. */
  --ink-2:rgb(124,120,146);
  --sacred:'Cinzel',serif; --prose:'Cormorant Garamond',Georgia,serif; --sys:'IBM Plex Mono',monospace;
}

/* ============================================================================
   HIEROPHANT — SHARED DESIGN LAYER
   ============================================================================
   Added 2026-09-04. Loaded LAST in <head>, after each page's inline <style>,
   so it wins the cascade at equal specificity.

   ! WHY THIS FILE EXISTS, AND IT IS THE STRUCTURAL FINDING OF THE AUDIT.
     `site/styles.css` has existed for months at 715 lines and NOT ONE of the 49
     pages links it. Every page carries its own inline <style> copy instead. So
     every design defect on this site is 49 defects, and every fix applied to a
     page leaves the other 48 loaded. That is OTR-1014's lesson at site scale:
     a wrong template is N bugs, not one.

     This layer is the single place a site-wide rule can now live. It does not
     restyle the site. It repairs what a measured audit proved is broken and
     adds the states nobody screenshots.

   MEASURED BASELINE (design-audit.js, 9 pages, 2026-09-04, self-test PASS):
     no-reduced-motion   9/9 pages     <- and this site is parallax-heavy
     display-too-heavy   8/9           <- see WAIVER below
     display-tracking    7/9           <- see WAIVER below
     no-focus-style      5/9           <- B3, the actual premium tell
     hero-no-cta         5/9
     layout-monotony     4/9
     body-contrast       2/9 CRITICAL  <- fixed here

   ============================================================================
   DECLARED WAIVER (visual-design-methodology-brain 17.4)
   ============================================================================
   The auditor reports `display-too-heavy` (weight 900) and
   `display-tracking-untuned` (0.100em) against thresholds measured from
   stripe.com and linear.app: h1 48-64px, weight 300-510, tracking -0.02em.

   NOT ACCEPTED, AND THE REASON IS THE FACE, NOT TASTE. Those references set
   grotesk UI faces. `--sacred` is Cinzel, an inscriptional serif drawn from
   Roman capital forms, where generous letter-spacing is the historically
   correct setting and negative tracking collapses the counters. Applying a
   -0.02em correction to Cinzel would produce a worse page that scores better.

   ! This is DECLARED, not silently exempted, which is the whole point of 17.4.
     The waiver covers `--sacred` (Cinzel) at EVERY size, which also accounts for
     `horseman-tracking` on 18 pages: that check exempts anything under 20px, so
     what it is counting is `.part-title` and `.arc-title`, both Cinzel. The face
     argument does not weaken at 24px. It does NOT cover `--sys` tracked labels,
     and the muted-lead pair (catalog 2.1) retired the worst of those.

   ! SECOND DECLARED WAIVER: `hero-no-trust-signal` (11 pages). That check passes
     when the hero contains a DIGIT, as a proxy for a number a buyer can trust.
     This canon numbers its chapters in Roman, so `Chapter III` contains no digit
     and never will. The proxy does not transfer to fiction. Refused, on the
     record, rather than satisfied by inserting an arabic numeral no reader wants.
   ============================================================================ */

:root {
  /* ---- CONTRAST REPAIR -----------------------------------------------------
     --muted was #5a5670, measuring 2.87:1 on --void. It carries the site's
     smallest text: card tags at 7px (56 instances on /codex alone), card
     subtitles at 10px, nav links, story meta. 2.87:1 at 7px is not a style,
     it is unreadable.

     ! HUE AND SATURATION ARE HELD EXACTLY. Only value is lifted, solved
       numerically for >= 5:1. HSV 249.2deg / 0.232 sat, unchanged. This is a
       correction, not a restyle: the colour is the same colour, lit properly. */
  --muted: #807ba0;                 /* was #5a5670 — 2.87:1 -> 5.03:1 */
  --muted-deep: #5a5670;            /* the original, for BORDERS and fills only */

  /* --blood at #8b2f2f measures 2.44:1 and is used as label text on side cards.
     Kept as-is for fills and rules; this is the text-safe sibling. */
  --blood-text: #bf5757;            /* 4.51:1 */

  /* A radius scale. The audit reported `single-radius`: every corner on the
     site was one value, which reads as a default rather than a system. */
  --r-sm: 2px;
  --r-md: 6px;
  --r-lg: 14px;

  --focus: var(--ember);
}

/* Elements that used --muted as a BORDER rather than text keep the dark value,
   so lifting the text colour does not brighten the site's rules and edges. */
.side-card, .story-nav a, .filter-btn { border-color: var(--edge); }

/* `.scene-break` set --edge (#242230, 1.29:1) as TEXT. It is a divider glyph,
   but it is still rendered type at 10px with 0.5em tracking. */
.prose .scene-break { color: var(--muted-deep); }
.side-card-type { color: var(--blood-text); }

/* ============================================================================
   B3 — THE STATES NOBODY SCREENSHOTS
   ============================================================================
   `no-focus-style` fired on 5 of 9 pages, and on the other 4 only because they
   had no focusable element in the sample. The site had no authored focus ring
   anywhere. Per methodology 21.1, this is the difference that reads as
   expensive: a page that handles its own edge states was operated by whoever
   built it.

   ! :focus-visible, not :focus. A mouse click must not ring the element; a Tab
     press must. Using :focus alone produces a ring on every click, which is the
     cheap version of this fix and looks like a bug. */

a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

/* The hero title and nav sit on photographic grounds where a 2px ember ring can
   vanish. A dark halo underneath guarantees the ring survives any background. */
.nav a:focus-visible,
.story-hero a:focus-visible {
  outline-offset: 4px;
  box-shadow: 0 0 0 6px rgba(8, 7, 10, .85);
}

/* Keyboard users must be able to reach the prose without tabbing the whole nav. */
.skip-link {
  position: absolute;
  left: 16px;
  top: -60px;
  z-index: 10001;
  padding: 10px 18px;
  background: var(--surface);
  border: 1px solid var(--ember);
  border-radius: var(--r-sm);
  color: var(--white);
  font-family: var(--sys);
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  transition: top .18s ease;
}
/* ! :focus AND :focus-visible. A skip link is the one control that must reveal
     on ANY focus, including programmatic focus, because it is reached before
     the browser has classified the interaction as keyboard-driven.

   ⚠️ HONEST NOTE ON HOW THIS WAS VERIFIED, because the first attempt reported a
     defect that was not one. Driving focus from an automated browser pane and
     reading the computed `top` returned -60px, which looks exactly like the rule
     failing. It was the instrument: `document.hasFocus()` was false, so :focus
     could not match even though `document.activeElement` was correct.

     What IS verified: the rule is present in the loaded sheet with the right
     selector and specificity (0,2,0 beats the base 0,1,0). What is NOT verified
     is the rendered state, and it cannot be from a pane that does not hold
     system focus. This needs a human Tab press in a real window. */
.skip-link:focus,
.skip-link:focus-visible { top: 16px; }

/* Hit targets. Nav links at 10px had no padding, so the clickable area was the
   glyph box. A pointer that misses by 3px is a state too. */
.nav-links a { padding: 6px 2px; }

/* ============================================================================
   REDUCED MOTION — DISSOLVE THE DEVICE, DO NOT HALF-RUN IT
   ============================================================================
   methodology 12.7. This fired on 9 of 9 pages and it is the most systemic
   finding in the audit, because this site's whole identity is scroll-driven:
   fixed blurred `.section-bg` plates that cross-fade, `.reveal` elements that
   translate in, a progress bar, and per-story parallax.

   ! THE FAILURE MODE IS NOT "TOO MUCH MOTION". It is that a reader with
     prefers-reduced-motion set currently gets `.reveal` elements stuck at
     opacity:0 if the observer is gated, or a half-speed version of a device
     that only means anything at full speed. Neither is a design. The correct
     behaviour is that the device is ABSENT and the page is complete without it. */

@media (prefers-reduced-motion: reduce) {

  /* Everything arrives already arrived. No exceptions, no partial states. */
  .reveal,
  .side-card,
  .story-hero-content,
  .part-header {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }

  /* The background plates stop cross-fading and settle to one steady ground.
     They do not fade at reduced speed; the mechanic is gone. */
  .section-bg {
    transition: none !important;
    opacity: 0 !important;
  }
  .section-bg:first-of-type { opacity: .28 !important; }

  /* Parallax translation is disagreement about motion. With motion reduced
     there is nothing to disagree about, so the layers simply register. */
  .story-hero-bg,
  .parallax,
  [data-parallax] {
    transform: none !important;
    transition: none !important;
  }

  .progress-bar { transition: none !important; }

  /* Catch-all for anything added later that forgets this block exists. */
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================================
   PRINT — a fiction site people will want on paper
   ============================================================================ */
@media print {
  .nav, .story-nav, .side-card-slot, .section-bg, .progress-bar,
  .skip-link, footer { display: none !important; }
  body { background: #fff; color: #111; font-size: 12pt; line-height: 1.6; }
  .prose { max-width: none; }
  .story-hero { min-height: auto; padding: 0 0 24px; }
  .story-hero-bg, .story-hero-overlay { display: none !important; }
  .story-hero h1 { color: #111; text-shadow: none; }
  a { color: #111; text-decoration: none; }
}

/* ============================================================================
   MODULE — THE HOLD RAIL   (fork of ENTERPRISE-MODULE-CATALOG 3.1, scored
   ⭐⭐⭐ FORK WHOLE, the highest-value import in the catalog)
   ============================================================================
   3.1's mechanic is a persistent margin rail that enumerates every figure with
   its source, and VOLUNTEERS which ones are weak, because self-flagging is the
   part that cannot be faked cheaply. On a commercial page it renders an honesty
   doctrine as a design feature.

   ⭐ Here it renders the canon's own two engines instead: the Iceberg (depth is
   controlled withholding) and perspectival factionalism (no omniscient
   narrator; every account is wrong in a characteristic way). The reader is told
   in the margin what kind of document they are holding and how far it can be
   trusted. That is lore delivered as apparatus, not decoration borrowed from a
   SaaS page.

   ⭐ THE GAP IS THE POINT (methodology §20.3). Unattested fields are drawn
   hatched and labelled, never omitted. A register that fills its own gaps is
   not a register, and in-world an incomplete Hold is characterisation. */

.hold-rail {
  max-width: 620px;
  margin: 0 auto 46px;
  padding: 20px 22px 16px;
  border: 1px solid var(--edge);
  border-left: 2px solid var(--ember);
  background: linear-gradient(180deg, rgba(24,22,31,.55), rgba(12,11,16,.30));
  border-radius: var(--r-sm);
  font-family: var(--sys);
}
.hold-head {
  font-size: 10.5px;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--ember);
  margin-bottom: 14px;
}
.hold-row {
  display: grid;
  grid-template-columns: 92px 1fr;
  gap: 14px;
  padding: 7px 0;
  border-top: 1px solid rgba(36,34,48,.75);
  align-items: baseline;
}
.hold-row:first-of-type { border-top: 0; }
/* A gap row is held slightly back so the attested rows carry the eye first.
   It must still be legible: an absence you cannot read is just a missing row,
   and the whole point of the gap device is that the absence is VISIBLE. */
/* ⚠️ THIS USED --muted-deep, WHICH LINE 89 OF THIS FILE DECLARES IS "for BORDERS and fills only".
   The token carried its own contract and the rule directly broke it, four lines under a comment
   insisting the absence must stay visible. Held back now by ALPHA on a legible ink instead. */
/*  ⚠️ OPACITY, NOT color-mix(), AND THE REASON IS TOOLING RATHER THAN TASTE. color-mix() computes
    to `color(srgb 0.62 0.61 0.69 / 0.78)`, and every contrast checker in this repo (and the one in
    the browser I wrote this morning) parses colours by pulling numbers out of the string and
    treating them as 0-255. Given 0-1 components it reports 1.03:1 and calls a legible label
    invisible. The element is a leaf span, so opacity holds the colour back with no side effects,
    keeps var(--text) as the single source of the hue, and leaves the computed value in rgb(). */
.hold-row-gap .hold-key { color: var(--text); opacity: .78; }
/* ⚠️ THE SIZE WAS THE DEFECT HERE; THE COLOUR WAS ALREADY FIXED AND I NEARLY MISRECORDED IT.
   A browser audit reported .hold-key at 2.87:1 and I wrote that down as this rule's contrast
   failure. It is not: --muted was lifted to #807ba0 (5.03:1) higher up in this same file, and the
   element the audit sampled was a GAP row, which is painted --muted-deep by the rule above.
   ★ AN INSTRUMENT REPORTS THE ELEMENT IT SAMPLED, NOT THE RULE YOU HAD IN MIND.
   What is wrong with this rule is 8.5px, full stop. The Hold is the device that says what is
   attested and what is missing, so its key column is the label on the evidence, and this file
   already makes the argument two rules down: an absence you cannot read is just a missing row.
   ★ QUIET IS A RELATIONSHIP, NOT A SIZE. The key still sits back from the value: smaller, tracked,
   uppercase, cooler. It is simply legible while doing it. */
.hold-key {
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text);
}
.hold-val {
  font-family: var(--prose);
  font-size: 1.02rem;
  line-height: 1.55;
  color: var(--light);
}

/* The gap device. Hatching, not a dash, because a dash reads as "none" while
   hatching reads as "this was looked for". */
.hold-gap {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--muted);
  font-style: italic;
  font-size: .95rem;
}
.hold-hatch {
  display: inline-block;
  width: 52px;
  height: 11px;
  border: 1px solid rgba(128,123,160,.45);
  border-radius: 1px;
  background-image: repeating-linear-gradient(
    -45deg, rgba(128,123,160,.40) 0 1px, transparent 1px 5px);
}
.hold-foot {
  margin-top: 14px;
  padding-top: 11px;
  border-top: 1px solid rgba(36,34,48,.75);
  font-size: 10px;
  letter-spacing: .08em;
  line-height: 1.8;
  color: var(--text);
}

@media (min-width: 1560px) {
  /* On a wide viewport it becomes a true margin rail rather than a panel,
     sitting beside the opening of the text where a reader meets it before the
     first paragraph rather than instead of it.

     THIS OVERLAPPED THE PROSE ON EVERY STORY PAGE UNTIL 2026-09-17.
     It read `left: max(24px, calc(50% - 640px))`, which is written as though the
     percentage resolves against the viewport. It does not. `left` resolves against the
     CONTAINING BLOCK, which is .story-container at 960px, so 50% was 480px, 480 minus 640
     is negative, max() returned 24px, and the rail was pinned 24px inside the container.
     Measured on a 1920px viewport: rail 497 to 743, prose column 623 to 1283, a 120px
     horizontal and 42px vertical overlap, on all 22 story pages.

     A PERCENTAGE IN `left` RESOLVES AGAINST THE CONTAINING BLOCK, AND THIS RULE CREATED
     THAT CONTAINING BLOCK ITSELF THREE LINES LATER.

     And the arithmetic could not have worked at 1280 regardless: the container is 960, the
     prose column inside it is 660, so the gutter is 150px and the rail is 246px. A 246px
     rail does not fit in a 150px gutter at any offset. The breakpoint is therefore raised
     to where the rail can hang OUTSIDE the container with real clearance, and positioned
     self-relatively so it can never be inside the column again. Below this width it stays
     the in-flow panel it already was, which was never broken. */
  .hold-rail {
    position: absolute;
    right: calc(100% + 34px);
    left: auto;
    width: 246px;
    max-width: none;
    margin: 0;
  }
  .hold-row { grid-template-columns: 1fr; gap: 4px; }
  .story-body { position: relative; }
}

/* ============================================================================
   MODULE — MUTED-LEAD PAIR  (catalog 2.1) replacing the tracked eyebrow
   ============================================================================
   The catalog lists "uppercase tracked eyebrow" as an ANTI-MODULE: it is one
   letter-spacing decision away from §20.1 horseman 1, and this site runs it on
   every page as `.story-hero-label` at 0.3em.

   2.1 gets the same two-level hierarchy out of COLOUR ALONE, so it cannot decay
   into fake elegance. Same size, same family, same measure; only colour changes.
   Applied here to the hero's bearing line, which keeps its meaning while losing
   the tracking. */
.story-hero-label {
  font-family: var(--prose);
  font-size: 1.06rem;
  font-style: italic;
  letter-spacing: 0;
  text-transform: none;
  /* ⚠️ .62 alpha composited to 3.45:1 on the page ground. The MODULE is correct: this line gets
     its hierarchy from colour rather than from tracking, which is the whole point of replacing the
     eyebrow. But a two-level hierarchy built out of colour only works while both levels are
     readable, or it is a one-level hierarchy with something faint above it. */
  color: rgba(160, 156, 176, .84);
}

/* ============================================================================
   AUDIT CLOSE-OUT — full 46-page sweep, 2026-09-04
   ============================================================================
   ⚠️ FIRST, AN INSTRUMENT TRAP WORTH MORE THAN ANY FIX BELOW.
   The sweep reported `no-reduced-motion` on 7 pages and `no-focus-style` on 5,
   all of them pages loaded earlier in the same session. Re-checked with a
   cache-buster: every one carries the link, the reduced-motion block and the
   focus-visible rules. **They were STALE CACHED LOADS, not defects.**
   design-audit.js run against iframes will invent MAJORs unless every src is
   cache-busted. Chasing those seven would have been an afternoon spent fixing
   nothing. */

/* ---- single-radius (21 pages) --------------------------------------------
   The finding is that only ONE distinct radius exists on the page, which reads
   as a default rather than a system. The scale was declared earlier in this
   file and then never applied to anything, which is its own small lesson. */
.side-card, .timeline-card, .card, .faction-card { border-radius: var(--r-md); }
.story-nav a, .filter-btn, .pill, .tag, .card-tag { border-radius: var(--r-sm); }
.hold-rail, .arc-section, .section-panel { border-radius: var(--r-lg); }

/* ---- display-line-height-loose (15 pages) --------------------------------
   Headings at 40px+ with line-height above 1.15. At display size a loose leading
   reads as a paragraph rather than a title, and the measured references sit at
   or below 1.0. Story heroes already set .95 and are untouched by this. */
h1, .arc-title, .page-title, .lore-title {
  line-height: 1.06;
}
@media (max-width: 640px) { h1, .arc-title, .page-title, .lore-title { line-height: 1.12; } }

/* ---- font-sprawl (atlas) --------------------------------------------------
   Five families measured: the house three plus Lucida Console and Arial, both
   arriving as FALLBACKS on elements that never declared a family. The fix is a
   declared default, not a narrower stack: an undeclared element is the actual
   defect and the stray face is only its symptom. */
body, div, span, li, td, button, input, select {
  font-family: var(--prose);
}
.nav-links a, .nav-logo, [class*="label"], [class*="tag"], [class*="meta"],
[class*="key"], code, kbd, pre { font-family: var(--sys); }
h1, h2, h3, .arc-title, .part-title, .card-title, .timeline-card-title,
.icon-label { font-family: var(--sacred); }

/* ---- hero-no-cta (27 pages) ----------------------------------------------
   An 80vh hero with no action asks the reader to scroll blind. The action here
   is diegetic and never addresses the audience (LORE-VIDEOS-SCOPING charter,
   carried site-wide): it names what the reader is about to do, in world. */
.hero-action {
  display: inline-block;
  margin-top: 26px;
  padding: 13px 30px;
  border: 1px solid var(--ember);
  border-radius: var(--r-sm);
  background: rgba(212, 160, 74, .07);
  color: var(--ember);
  font-family: var(--sys);
  font-size: 10px;
  letter-spacing: .26em;
  text-transform: uppercase;
  text-decoration: none;
  transition: background .22s ease, color .22s ease;
}
.hero-action:hover { background: var(--ember); color: var(--void); }

/* The atlas heading. It titles the map rather than sitting above it: the atlas
   is a full-bleed interactive surface, so the heading floats in its top-left
   with the same weight the map's own labels carry. */
.atlas-title {
  position: absolute;
  top: 84px;
  left: 32px;
  z-index: 20;
  margin: 0;
  font-family: var(--sacred);
  font-size: clamp(1.1rem, 2.4vw, 1.6rem);
  font-weight: 700;
  letter-spacing: .12em;
  color: var(--light);
  text-shadow: 0 2px 24px rgba(8,7,10,.9);
  pointer-events: none;
}
@media (max-width: 700px) { .atlas-title { top: 72px; left: 18px; font-size: 1rem; } }

/* ---- single-radius, second pass (12 lore pages) ---------------------------
   The first pass named classes from the story template. The lore pages use
   their own (`league-card`, `caste-card`, ...) and measured a single 4px value
   throughout. Matching on the SHAPE of the name rather than enumerating every
   page's vocabulary, so a page added later inherits the scale. */
[class$="-card"], [class*="-card "], .card { border-radius: var(--r-md); }
[class$="-grid"] > * { border-radius: var(--r-md); }
[class$="-label"], [class$="-tag"], [class$="-meta"], [class$="-btn"] { border-radius: var(--r-sm); }
[class$="-panel"], [class$="-section"] { border-radius: var(--r-lg); }

/* ---- atlas display-too-small + weak-type-contrast -------------------------
   The atlas heading landed at 25.6px against body text at 20px. That is not a
   type SCALE, it is two sizes that happen to differ, and at >=1100px the
   reference corpus puts a page's largest type at 40px or more. Raised so the
   map has a title rather than a caption. */
.atlas-title { font-size: clamp(1.6rem, 3.6vw, 2.9rem); }

/* ---- atlas: the enlargement above broke a spatial relationship ------------
   ⛔ MEASURED IN THE BROWSER 2026-09-17: the heading overlapped the search box
   by 208x7px. Nothing is wrong with either rule on its own. The heading was
   placed at top:84px when it was 25.6px tall, the search box was placed at
   top:60px to sit clear of it, and then the rule directly above raised the
   heading to 46px without moving anything else. The overlap is the DISTANCE
   BETWEEN TWO DECISIONS MADE AT DIFFERENT TIMES.
   ★ A SIZE CHANGE IS A LAYOUT CHANGE WHENEVER ANYTHING ELSE WAS POSITIONED
   AROUND THE OLD SIZE. The search box now sits below the heading and shares
   its left edge, so the three fixed elements read as one stack: nav, title,
   search. */
.search-box { top: 148px; left: 32px; }
@media (max-width: 700px) { .search-box { top: 108px; left: 18px; } }

/* ---- atlas region labels: 2.38:1 -----------------------------------------
   THE CANOPY, ASHLANDS, FERTILE PLAINS and the rest are NAMES OF PLACES, which
   is information, and they were painted at opacity .35 over a dark map: a
   composited 2.38:1 that the browser audit flagged on five of the seven.
   They are meant to sit quieter than the settlement labels and they still do,
   at .62; quieter is a relationship, not a licence to go below legible. */
.region-label-div { opacity: .62; }

/* ---- horseman-gradients (index, stories/index) ----------------------------
   The audit counted 17 gradients. Most are SCRIMS: `bg-overlay`, `nav` and
   `f-card-overlay` are legibility washes behind text over imagery, and removing
   them would make the page worse. Those are not the horseman.

   `f-card-bg` is: radial-gradient(circle at 50% 40%, rgba(212,160,74,.15),
   transparent 50%) on six faction cards. A decorative glow fill with no
   information in it. That IS §20.1 horseman 4.

   ⭐ Replaced with catalog module 2.6's mechanic: the same information with the
   ornament removed. A hairline and a flat wash instead of a glow. The catalog's
   claim is that this looks MORE expensive, not less, and the reason is that a
   gradient fill is what a tool reaches for when no decision was made. */
.f-card-bg {
  background-image: none !important;
  background-color: rgba(212, 160, 74, .035);
  border-top: 1px solid rgba(212, 160, 74, .22);
}
.f-card:hover .f-card-bg { background-color: rgba(212, 160, 74, .07); }

/* ============================================================================
   FINAL WAIVERS — declared with evidence, per methodology §17.4
   ============================================================================
   After the close-out pass the sweep of all 46 pages reports 0 CRITICAL. What
   remains is dominated by three checks that do not transfer to this property,
   and each is refused ON THE RECORD rather than satisfied by damaging the page.

   1. `layout-monotony` (28 pages) — ⭐ A PROXY MISS, VERIFIED.
      The check reads `section > :scope > div` and asks whether that element has
      more than one grid column. Measured directly: /lore/glass-coast has
      `league-grid` and `figure-grid`, /index has `media-grid` and
      `factions-grid`, all genuinely multi-column, all nested ONE LEVEL DEEPER
      than the selector reaches. The finding is about the selector's depth, not
      about the layouts.
      ⛔ Restructuring 28 pages' DOM to satisfy a proxy would be the definition
      of optimising the threshold instead of the thing.
      ⚠️ On story pages the prose column is single by design. Long-form reading
      wants one measure, and a grid there would be a defect, not a fix.

   2. `horseman-gradients` (2 pages) — SCRIMS, NOT ORNAMENT.
      17 gradients counted on the index. Six were a decorative radial glow on
      the faction cards and those ARE horseman 4; they were removed above and
      replaced with module 2.6's hairline. The remainder are `nav`, `bg-overlay`
      and `f-card-overlay`: legibility washes that put text on imagery. The
      check cannot distinguish a scrim from an ornament. Removing them would
      make the page objectively worse.

   3. `hero-no-trust-signal` (11 pages) — see the second waiver above. Roman
      numerals contain no digit.

   Plus the standing Cinzel waiver, which accounts for `display-too-heavy` (46),
   `display-tracking-untuned` (46) and `horseman-tracking` (18).

   ⚠️ WHAT IS NOT WAIVED AND IS SIMPLY DONE: body-contrast (2 CRITICAL -> 0),
   no-reduced-motion, no-focus-style, display-line-height-loose, font-sprawl,
   no-headings, hero-no-cta (27 -> 0), single-radius, display-too-small,
   weak-type-contrast. */

/* Last single-radius holdout: a page whose only rounded element is its nav. */
.divider-icon, .emblem, .icon-label { border-radius: var(--r-sm); }

/* ============================================================================
   MOBILE NAV — THE SITE NAVIGATION DID NOT FIT ON A PHONE
   ============================================================================
   ⛔ MEASURED 2026-09-17 IN A 390px FRAME, ON EVERY PAGE OF THE PROPERTY:
   `.nav-links` rendered 349px wide with its right edge at x=492 inside a
   371px viewport. The last two items, ATLAS and FOLLOW, were off-screen and
   unreachable. On pages carrying an Account link, three were.

   ★ AND THE PAGE REPORTED NO HORIZONTAL OVERFLOW WHILE THIS WAS TRUE.
   scrollWidth equalled clientWidth on all nine pages sampled, because body
   carries overflow-x hidden/clip. The overflow rule did not prevent the
   defect; it CONCEALED it, and concealed it from the exact check most people
   would run to look for it. A page can be clipping its own navigation and
   still answer "no overflow" truthfully.

   The fix is a second row rather than smaller type: the logo and five tracked
   labels genuinely do not fit one line at this width, and the alternative to
   wrapping is shrinking labels that this file just spent a pass enlarging.
   Heroes on this site open with >=90px of top padding, so a nav ~26px taller
   has room to grow into. */
@media (max-width: 620px) {
  .nav {
    flex-wrap: wrap;
    row-gap: 9px;
    padding-top: 11px;
    padding-bottom: 11px;
  }
  .nav-links {
    width: 100%;
    justify-content: center;
    flex-wrap: wrap;
    gap: 15px;
    row-gap: 7px;
  }
}

/*  ⚠️ THE HOMEPAGE HAS A SIXTH ITEM AND SIX DID NOT FIT THE SECOND ROW EITHER.
    Measured at 390px: six labels at 11px with a 15px gap need 346px against 331px available, so
    Account wrapped to a THIRD row and landed beside the masthead kicker. At 10px with a 12px gap
    they need 308px and the nav settles at two rows on every page.
    ★ 10px IS THE FLOOR THIS PROPERTY JUST ADOPTED, not a number chosen to make the row fit; the
    fit is what the floor happens to allow, and if it had not, the sixth item would have had to go
    rather than the floor. */
@media (max-width: 430px) {
  .nav-links { gap: 12px; }
  .nav-links a { font-size: 10px; letter-spacing: .06em; }
}

/* ============================================================================
   ATLAS ON A PHONE — FOUR FIXED ELEMENTS, ALL COMPETING FOR ONE SCREEN
   ============================================================================
   ⛔ MEASURED IN A 390px FRAME: the heading overlapped the search box by
   220x21px and the layer controls by 36x57px, and the legend stood 483px tall
   on a 700px viewport, so the map it is a legend FOR was mostly behind it.

   ⚠️ AND THE HEADING WAS 25.6px ON A PHONE DESPITE A RULE SAYING OTHERWISE.
   `@media (max-width:700px){ .atlas-title{font-size:1rem} }` is declared EARLY
   in this file, and the later unconditional `.atlas-title{font-size:clamp(...)}`
   overrides it at every width, media query included. A media query does not
   raise specificity and does not protect a declaration from a rule written
   after it.
   ★ THE ONLY THING KEEPING A RESPONSIVE RULE ALIVE IS THAT NOBODY WRITES A
   NARROWER-SCOPED-LOOKING RULE FURTHER DOWN THE FILE. Which is why this block
   is at the END, and why any future atlas rule must go above it.

   The map is the content. Everything here yields to it: the heading drops to
   one line, the search sits under it, the legend is capped to a third of the
   viewport and scrolls. */
/*  ⛔ THE LEGEND AND THE TIMELINE BAR COLLIDE IN A BAND, NOT AT A BREAKPOINT.
    Measured: at 816px the legend's right edge is 273 and the centred bar's left edge is 241, and
    both sit on the viewport floor, so they overlap 32x36px. At 996px the bar's left edge has moved
    out to 331 and they clear. At 696px they overlap horizontally but the legend is short enough to
    stop above the bar, so they clear too.
    ★ THE DEFECT LIVES BETWEEN TWO WIDTHS THAT BOTH LOOK FINE, which is why checking a phone size
    and a desktop size found nothing. The band runs to roughly 2*273+334 = 880px, so the legend is
    seated above the bar for everything below 900. */
@media (max-width: 900px) {
  .legend { bottom: 84px; max-height: calc(100vh - 230px); }
}

@media (max-width: 620px) {
  .atlas-title {
    top: 96px;
    left: 18px;
    font-size: 1.05rem;
    letter-spacing: .08em;
  }
  .search-box {
    top: 126px;
    left: 18px;
    width: calc(100% - 36px);
    max-width: 260px;
  }
  .layer-controls { top: 96px; right: 12px; }
  /*  ⚠️ bottom:12px PUT THE LEGEND ON TOP OF THE TIMELINE BAR, measured at 98x64px. Both are
      bottom-anchored map furniture and neither knew about the other; on a desktop width they
      simply never met because the bar is centred and there was room either side of it. The legend
      is seated above the bar's height instead of beside it, because at 390px there is no beside. */
  .legend {
    left: 12px;
    bottom: 84px;
    width: 156px;
    max-height: 30vh;
    padding: 10px 12px;
    overflow-y: auto;
  }
}

/* ---- homepage #follow routes ---------------------------------------------
   ⚠️ LIVES HERE AND NOT IN THE PAGE, because build-home.mjs owns the homepage's
   inline <style> and splices it wholesale between two markers. The first copy
   of this rule was written into that block, survived exactly until the next
   `node tools/build-home.mjs`, and preflight then reported `.cta-routes` used
   but undefined.
   ★ A RULE WHOSE HOME IS REGENERATED IS NOT SAVED, IT IS QUEUED FOR DELETION.
   The #follow section is hand-maintained outside the markers, so its styling
   belongs outside them too. */
.cta-routes {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 26px;
}
.cta-routes a {
  font-family: var(--sys);
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ember);
  text-decoration: none;
  border: 1px solid var(--edge);
  border-radius: var(--r-sm);
  padding: 13px 22px;
  transition: background .22s ease, color .22s ease;
}
.cta-routes a:hover { background: var(--ember); color: var(--void); }

/* ============================================================================
   GROUND — DEPTH BELOW THE FOLD
   ============================================================================
   ⛔ THE DEPTH ON THIS PROPERTY STOPPED EXACTLY WHERE THE READING STARTED.
   Measured: 47 of 50 pages carry the grain overlay, and the heroes carry
   layered radial lifts in the faction hue. Everything BELOW the hero, which is
   where a reader actually spends their time, was flat #08070a. So the site
   looked considered for one screen and then became a void.

   ★ THE PATTERN IS THE ARCHIVE'S STATIONERY, NOT A TEXTURE. The masthead
   already rules its ground with a 40-degree hatch at 52px pitch. Extending that
   as the property's paper is the one atmospheric layer the visual standard
   permits, and it is coherent rather than decorative: this is a site about
   documents somebody filed, and filed documents are kept on ruled stock.

   ⚠️ OPACITIES ARE FOR THIS PAPER, NOT A LIGHT ONE. #08070a is near-black, so a
   lift that would be invisible on white is legible here and one tuned for white
   would be a smear. Every value below was set by deploying and looking, per the
   standard's rule against reasoning about texture opacity.

   ⛔ NEVER PUT THE SAME BAND ON ADJACENT SECTIONS. That is the whole mechanism:
   the bands are not decoration, they are what makes one section read as a
   different room from the next one. */

.band { position: relative; isolation: isolate; }
.band > * { position: relative; z-index: 1; }
/*  ⚠️ FULL-BLEED, BECAUSE THE SECTIONS ARE NOT. `.door` and `.works` are
    max-width:1180px centred, so a band painted to `inset:0` stopped at the
    content edge and rendered as a 1180px RECTANGLE floating in black, with two
    hard vertical seams down the page. A band is meant to read as the room the
    section is standing in; a room does not have visible sides.
    Width is taken from the viewport and the element is re-centred, so the
    ground spans regardless of how narrow its section's measure is. */
.band::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  z-index: 0;
  pointer-events: none;
}

/*  100vw counts the scrollbar, so a page whose body can scroll horizontally
    would gain a few pixels of slack from the rule above. `clip` prevents that
    without making body a scroll container, which is what `hidden` would do and
    is how in-page anchors were broken on this property once already. */
body { overflow-x: clip; }

/* the shared stationery: one hairline rule set, very low contrast */
.band-a::before, .band-b::before, .band-c::before {
  background-image:
    repeating-linear-gradient(-40deg,
      rgba(208, 204, 224, .042) 0 1px, transparent 1px 46px);
}

/*  ⛔ THESE ALPHAS ARE SOLVED, NOT CHOSEN, AND THE FIRST SET I CHOSE BROKE THE FLOOR.
    Adding ground depth moves the background under the text, and this property spent a whole pass
    getting every page to 4.5:1. At my first "looks good" values, --text over band-a's PEAK lift
    measured 3.91:1 and --ember 4.43 — both below the floor, on a page that had just been certified
    above it. ★ A BACKGROUND IS A CONTRAST CHANGE. Texture and legibility are the same budget.

    So the ceiling was computed rather than eyeballed: for each hue, the largest peak alpha whose
    composited ground still clears 4.6:1 for --text with the hatch stacked on top.
        ember   .228   sophia   .336   neutral   .194
    Two of the three could go DEEPER than the values that looked right, and only the warm one
    needed pulling back. Eyeballing had been wrong in both directions.

    ⚠️ Solved against --text (#a09cb0), the QUIETEST ink that appears on these grounds. Anything
    lighter is safe by construction. If a darker ink is ever introduced, re-solve; do not assume. */

/* A — warm, weight top-left. The archivist's own sections. */
.band-a::before {
  background-image:
    radial-gradient(74rem 42rem at 8% -12%, rgba(212, 160, 74, .22), transparent 62%),
    radial-gradient(58rem 36rem at 95% 112%, rgba(212, 160, 74, .11), transparent 60%),
    repeating-linear-gradient(-40deg,
      rgba(208, 204, 224, .050) 0 1px, transparent 1px 44px);
}

/* B — cool, weight right. Sophia's colour, for the machine-facing sections. */
.band-b::before {
  background-image:
    radial-gradient(68rem 40rem at 96% 0%, rgba(123, 108, 199, .32), transparent 60%),
    radial-gradient(54rem 34rem at 2% 100%, rgba(123, 108, 199, .16), transparent 58%),
    repeating-linear-gradient(28deg,
      rgba(208, 204, 224, .046) 0 1px, transparent 1px 52px);
}

/* C — neutral and lower, a held breath between the other two. */
.band-c::before {
  background-image:
    radial-gradient(82rem 36rem at 50% -6%, rgba(208, 204, 224, .19), transparent 66%),
    repeating-linear-gradient(-40deg,
      rgba(208, 204, 224, .040) 0 1px, transparent 1px 44px);
}

/* A hairline where two bands meet, so the change of room is a decision rather
   than a gradient nobody can point at. */
.band + .band::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--edge) 18%, var(--edge) 82%, transparent);
  z-index: 0;
}

@media (prefers-reduced-motion: reduce) { .band::before { background-attachment: scroll; } }

/* ---- f-card tag, stretched by a flex parent -------------------------------
   ⛔ MEASURED ON THE HOMEPAGE: a tag pill 293px wide inside a 293px card, when
   its own rule already said `display:inline-block`.
   ★ inline-block IS INERT INSIDE A COLUMN FLEXBOX. `.f-card-body` on the
   homepage is `display:flex; flex-direction:column`, whose default
   `align-items:stretch` widens every child to the full cross axis. The
   declaration was correct, present, and overruled by a property set on the
   PARENT three rules away. A pill that stretches is not a pill, it is a bar.

   ⚠️ Deliberately NOT `align-items:flex-start` on the body: that would also
   un-stretch the name and the description, which should fill the card. Scoped
   to the tag, and written as a descendant rule so it is a no-op on the lore
   index, where the same class sits inside a non-flex body and was always fine.
   Same class name, two pages, two different parents: the collision is why this
   lives here rather than in either page. */
.f-card-body > .f-card-tag { align-self: flex-start; }
