/* cache-bust v2: fresh content hash, paired with the _worker.js fix that 404s a
   missing asset instead of serving the HTML shell — so this new CSS URL cannot
   be poisoned in Cloudflare's edge cache the way the prior hashes were. */
@font-face {
    font-family: "osl";
    src: url("/fonts/osl.woff") format("woff");
    font-weight: normal;
    font-style: normal;
    font-display: block;
}
@font-face {
    font-family: "osr";
    src: url("/fonts/osr.woff") format("woff");
    font-weight: normal;
    font-style: normal;
    font-display: block;
}

/* Reader design tokens (Slice 0 — minimal; Slice 2 expands). */
:root {
    --canvas:         #211c16;  /* warm charcoal — lifted off near-black (was #15110d) */
    --bar-tint:       rgba(33,28,22,.72);  /* translucent --canvas for the frosted top/bottom bars (backdrop-blur) */
    --atmo-tint:      #211c16;  /* default = Origins era; overwritten by atmosphere::apply_tint on scroll */
    --canvas-deep:    #181410;
    --page-bg:        #15110d;  /* the reader text-column background (was hard-coded) */
    --ink:            #d8d0bf;
    --ink-soft:       #b8b09e;  /* secondary text — a step below --ink, above --ink-dim.
                                   Used by the map (loading/count/legend/popover-aside);
                                   was referenced but never defined, so those elements
                                   silently inherited full --ink instead of softening. */
    --ink-dim:        #998f7d;
    --ink-faint:      #7a7160;
    --ink-veryfaint:  #3e382d;
    --rule:           #342d22;
    --rule-soft:      #2b2519;
    --head:           #b8a890;
    --oxblood:        #a82828;
    --oxblood-deep:   #7a1e1e;
    --gilt:           #b39250;  /* softened from #c9a24a — less saturated/bright so it stops stinging */
    --gilt-deep:      #8a6e2e;
    --moss:           #7a9a6a;
    --moss-deep:      #4e6840;
    --steel:          #6c8aa0;
    --steel-deep:     #3e5668;
    --violet:         #8a7298;
    --violet-deep:    #5a4a68;
    --iron:           #4a4a4a;
    --iron-deep:      #2a2a2a;
    --sepia:          #8c6a3a;
    --sepia-deep:     #5e4622;
    --serif-body:    "osr", Georgia, "Iowan Old Style", "Hoefler Text", serif;
    --serif-display: "osl", "osr", Georgia, serif;
    /* Juxtaposition card text. Was a system sans (typeset A/B test), but the app
       now uses osl/osr everywhere — repointed to the osr serif stack so the lens
       matches the reader. Token name kept so its three call sites need no edit. */
    --jx-sans:       var(--serif-body);
    /* Single source of truth for the fixed topbar's height. The topbar, the
       content spacer (.topbar-pad), and the lens overlay all use this so they
       line up identically on every page — no per-page magic numbers. */
    --topbar-h: 48px;
    --overlay-bg:     rgba(24,20,16,.9);  /* translucent map legend/caption panels */
    /* Contributor cork-board + post-it palette (corkboard.rs). The theme
       sticky reuses canvas/ink vars so it follows the active theme for free. */
    --cork:       #6b4f2e;   /* base cork brown */
    --cork-dark:  #5a4227;
    --cork-frame: #4a3620;   /* wooden frame (reads as dark wood on both themes) */
    --sticky-theme-bg:  var(--canvas-deep);
    --sticky-theme-ink: var(--ink);
    --sticky-yellow: #e8d074;  --sticky-yellow-ink: #3a3110;
    --sticky-pink:   #e0a0ad;  --sticky-pink-ink:   #3f1c25;
    --sticky-blue:   #94b7cc;  --sticky-blue-ink:   #152a36;
    --sticky-green:  #a7c58f;  --sticky-green-ink:  #20330f;
}

/* ============================================================
   THEME: paper (light). The dark tokens above are the base.
   Paper applies when the OS prefers light AND the user hasn't
   forced a theme, OR when the user forces light. Forced dark
   re-asserts the base. Accents are remapped here: gilt/moss/steel
   darken to their -deep values so they read on cream (the cards set
   `--accent: var(--gilt)` inline, so this remap reaches them for free).
   The forced-light and media-light selectors tie at specificity (0,2,0);
   forced-light wins by source order, and the :not([data-theme="dark"])
   guard deactivates the media block under forced dark.
   ============================================================ */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --canvas:        #f3ecdd;
    --bar-tint:      rgba(243,236,221,.72);
    --atmo-tint:     #f3ecdd;
    --canvas-deep:   #ece3d0;
    --ink:           #2b2722;
    --ink-soft:      #4d473c;
    --ink-dim:       #6b6354;
    --ink-faint:     #8a8170;
    --ink-veryfaint: #cdc4b0;
    --rule:          #ddd4c0;
    --rule-soft:     #e7dfcd;
    --head:          #4a4032;
    /* hybrid accent remap (light only) */
    --gilt:          #8a6e2e;
    --moss:          #4e6840;
    --steel:         #3e5668;
    --page-bg:       #f7f2e8;
    --overlay-bg:    rgba(247,242,232,.92);
    --cork:      #c9a878;
    --cork-dark: #bd9a66;
  }
}

/* Forced light — same paper tokens, regardless of OS. */
:root[data-theme="light"] {
  --canvas:        #f3ecdd;
  --bar-tint:      rgba(243,236,221,.72);
  --atmo-tint:     #f3ecdd;
  --canvas-deep:   #ece3d0;
  --ink:           #2b2722;
  --ink-soft:      #4d473c;
  --ink-dim:       #6b6354;
  --ink-faint:     #8a8170;
  --ink-veryfaint: #cdc4b0;
  --rule:          #ddd4c0;
  --rule-soft:     #e7dfcd;
  --head:          #4a4032;
  --gilt:          #8a6e2e;
  --moss:          #4e6840;
  --steel:         #3e5668;
  --page-bg:       #f7f2e8;
  --overlay-bg:    rgba(247,242,232,.92);
  --cork:      #c9a878;
  --cork-dark: #bd9a66;
}

/* Light-theme only: a teeny warm accent on the top bar and the side scale, so
   they read as gently distinct from the flat canvas. Kept VERY subtle (gilt at
   ~5–9% alpha) — a whisper, not a band. Scoped to [data-theme="light"] so the
   dark theme is untouched. */
:root[data-theme="light"] .topbar {
    /* Faint gilt wash over the flat frosted tint (blur inherited from the base
       .topbar rule). */
    background:
        linear-gradient(to bottom, rgba(138,110,46,.05), transparent 70%),
        var(--bar-tint);
}
/* Light-theme only: darken the wide shaded band the scale sits on so it reads as
   a distinct panel instead of fading into the page canvas. The fill runs the full
   width of the scale — solid out to the divider line (~44px), then a sharp 3px
   cutover to transparent right at the divider — so the whole scale column reads as
   one panel rather than a narrow strip hugging the ticks. */
:root[data-theme="light"] .scrubber {
    background: linear-gradient(to left, #efe7d6 44px, transparent 47px);
}
:root[data-theme="light"] .scrub-rail {
    /* Nudge the rail off neutral toward a faint gilt so the scale picks up the
       same warm hint as the bar. */
    background: var(--ink-faint);
}
/* Light-theme only: darken the scale marks so the ticks and year labels read
   crisply against the bright canvas instead of fading into it. Dark theme keeps
   its own (lighter) inks. */
:root[data-theme="light"] .scrub-tick       { background: var(--ink-faint); }
:root[data-theme="light"] .scrub-tick-major { background: var(--ink-dim); }
:root[data-theme="light"] .scrub-label      { color: var(--ink-soft); }

/* Forced dark — re-assert the base so it wins over the media query. */
:root[data-theme="dark"] {
  --canvas:        #211c16;
  --atmo-tint:     #211c16;
  --canvas-deep:   #181410;
  --ink:           #d8d0bf;
  --ink-soft:      #b8b09e;
  --ink-dim:       #998f7d;
  --ink-faint:     #7a7160;
  --ink-veryfaint: #3e382d;
  --rule:          #342d22;
  --rule-soft:     #2b2519;
  --head:          #b8a890;
  --gilt:          #b39250;
  --moss:          #7a9a6a;
  --steel:         #6c8aa0;
  --page-bg:       #15110d;
  --overlay-bg:    rgba(24,20,16,.9);
  --cork:      #6b4f2e;
  --cork-dark: #5a4227;
}

/* No italics anywhere in the reader. */
* { font-style: normal !important; box-sizing: border-box; margin: 0; padding: 0; }

/* Themed scrollbars. The OS default is a bright white bar that glares against
   the sepia/charcoal page — replace it with the engraved-rule tone, which
   flips light/dark with the rest of the theme via --rule. Track stays
   transparent; only the thumb is drawn. */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--rule) transparent;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
    background: var(--rule);
    border-radius: 6px;
    /* inset the thumb off the track edges so it reads as a slim pill */
    border: 2px solid transparent;
    background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-dim); background-clip: content-box; }

html, body {
    background: var(--atmo-tint);
    color: var(--ink);
    font-family: var(--serif-body);
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    min-height: 100vh;
    min-height: 100dvh;
    touch-action: pan-y;
    /* Stop top rubber-band overscroll so the content can't bounce away from
       the fixed header and reveal a gap underneath it. */
    overscroll-behavior-y: none;
}
/* The era cross-fade (whole-viewport background animating on every era change
   while scrolling) is the most motion-prominent effect in the stream; gate it
   so a "Reduce Motion" user gets an instant swap, not a 300ms fade. */
@media (prefers-reduced-motion: no-preference) {
    html, body { transition: background 300ms ease; }
}

/* Auth pages (login / signup / signup-verify) */
.auth-shell {
    max-width: 420px;
    margin: 0 auto;
    padding: 60px 24px 40px;
    color: var(--ink);
}
.auth-title {
    font-size: 30px;
    color: var(--head);
    font-weight: 600;
    letter-spacing: -0.01em;
    margin-bottom: 8px;
}
.auth-sub {
    color: var(--ink-dim);
    margin-bottom: 28px;
    font-size: 16px;
}
.auth-form {
    display: flex;
    flex-direction: column;
    gap: 14px;
}
.auth-input {
    padding: 14px 16px;
    background: var(--rule-soft);
    border: 1px solid var(--rule);
    border-radius: 10px;
    color: var(--ink);
    font-size: 18px;
    font-family: inherit;
    letter-spacing: 0.04em;
}
/* The placeholder inherited full --ink, so the sample key (1234-5678-9012) read
   like a prepopulated value. --ink-faint alone was still too readable on the
   input bg (it's the color for visible secondary UI), so drop the opacity to
   land it clearly in "hint" territory — readable but obviously not entered text.
   Opacity is theme-safe: it fades --ink-faint proportionally in both light and
   dark. (opacity replaces Firefox's default placeholder dimming rather than
   pinning it to 1.) */
.auth-input::placeholder {
    color: var(--ink-faint);
    opacity: 0.55;
}
.auth-input:focus {
    outline: none;
    border-color: var(--head);
}
.auth-btn {
    padding: 13px 18px;
    background: var(--head);
    color: var(--canvas);
    border: none;
    border-radius: 10px;
    font-size: 16px;
    font-family: inherit;
    font-weight: 600;
    cursor: pointer;
    letter-spacing: 0.02em;
}
.auth-btn[disabled] { opacity: 0.5; cursor: default; }
.auth-err {
    margin-top: 14px;
    color: var(--oxblood);
    background: var(--rule-soft);
    padding: 10px 14px;
    border-radius: 8px;
    border-left: 3px solid var(--oxblood);
    font-size: 14px;
}

/* /add — the new-event form. A roomier auth-shell with labeled fields. */
.add-shell { max-width: 560px; }
.add-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.add-label {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ink-dim);
    text-transform: uppercase;
}
.add-textarea {
    resize: vertical;
    min-height: 90px;
    line-height: 1.5;
    letter-spacing: normal;
}
/* Month / Day / Order side by side. Each input flexes equally; the number
   spinners stay usable at this width. */
.add-row { display: flex; gap: 10px; }
.add-row .auth-input { flex: 1; min-width: 0; letter-spacing: normal; }
.auth-altlink {
    margin-top: 26px;
    color: var(--ink-faint);
    font-size: 14px;
}
.auth-altlink a {
    color: var(--head);
    text-decoration: underline;
}
.auth-label {
    display: flex;
    flex-direction: column;
    gap: 6px;
    color: var(--ink-dim);
    font-size: 13px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.auth-label .auth-input {
    margin-top: 4px;
    text-transform: none;
    letter-spacing: 0.01em;
    color: var(--ink);
    font-size: 16px;
}
.auth-textarea {
    resize: vertical;
    min-height: 72px;
    font-family: inherit;
}
.auth-fieldset {
    border: 1px solid var(--rule);
    border-radius: 10px;
    padding: 12px 16px 14px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    background: var(--rule-soft);
}
.auth-fieldset legend {
    padding: 0 6px;
}
.auth-radio {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--ink);
    font-size: 16px;
    letter-spacing: 0;
    text-transform: none;
    cursor: pointer;
}
.auth-summary {
    margin: 12px 0 22px;
    padding: 16px 18px;
    background: var(--rule-soft);
    border: 1px solid var(--rule);
    border-radius: 10px;
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 10px 18px;
    color: var(--ink);
    font-size: 16px;
}
.auth-summary dt {
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 12px;
    align-self: center;
}
.auth-summary dd { margin: 0; }
.auth-actions {
    display: flex;
    gap: 10px;
    margin-top: 12px;
}
.admin-shell {
    max-width: 720px;
    margin: 0 auto;
    padding: 40px 24px;
}
.admin-title {
    font-size: 24px;
    color: var(--head);
    margin-bottom: 22px;
}
.admin-empty {
    color: var(--ink-dim);
    padding: 24px 0;
}
.signup-req-list {
    list-style: none;
}
.signup-req {
    padding: 16px 18px;
    border: 1px solid var(--rule);
    border-radius: 10px;
    background: var(--rule-soft);
    margin-bottom: 10px;
}
.signup-req-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 14px;
    color: var(--ink);
}
.signup-req-email {
    color: var(--ink-dim);
    font-size: 14px;
}
.signup-req-fields {
    margin: 10px 0 8px;
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 4px 14px;
    font-size: 14px;
    color: var(--ink);
}
.signup-req-fields dt {
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 11px;
    align-self: center;
}
.signup-req-fields dd { margin: 0; }
.signup-req-note {
    color: var(--ink-dim);
    font-style: normal;
}
.signup-req-meta {
    margin-top: 4px;
    font-size: 12px;
    color: var(--ink-faint);
    letter-spacing: 0.04em;
}
.signup-req-actions {
    margin-top: 12px;
    display: flex;
    gap: 8px;
}
.auth-btn-secondary {
    background: transparent !important;
    color: var(--ink-dim) !important;
    border: 1px solid var(--rule) !important;
}

/* Reader top bar (fixed) */
.topbar {
    position: fixed;
    top: 0; left: 0; right: 0;
    /* Grow the bar up under the status bar / notch on viewport-fit=cover
       devices; the frosted surface extends into the inset while the
       bottom-aligned controls stay clear of it. env() = 0 in a browser tab. */
    height: calc(var(--topbar-h) + env(safe-area-inset-top, 0px));
    /* Bottom-aligned controls; bar height is --topbar-h (>=48px so the 40px
       house glyph clears the top edge). */
    padding: env(safe-area-inset-top, 0px) 16px 8px;
    display: flex;
    align-items: flex-end;
    gap: 12px;
    /* Frosted bar: a flat translucent canvas tint over a backdrop blur, so
       content scrolling under is BLURRED, not sharply see-through. Flat (not a
       gradient) on purpose — a tint that fades to transparent at the edge let
       content show through there; uniform tint + uniform blur keeps the whole
       bar consistent. No element mask (would fade the bottom-aligned buttons). */
    background: var(--bar-tint);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    /* Above the lens (z 15) so the bar stays visible/tappable while the lens
       is open and Home dismisses it. */
    z-index: 20;
    font-size: 13px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-faint);
    touch-action: manipulation;
}
/* Desktop: cap the bar (background included) to the 800px content column and
   center it, so the bar terminates at the reading width instead of running
   edge-to-edge. Reset padding to the mobile 16px now that the bar itself is
   constrained. Mobile keeps full-width with 16px padding. */
@media (min-width: 760px) {
    .topbar {
        /* Bar spans the 800px reading column with equal margins — right edge at
           the column edge (50%-400px), matching the left. The rail sits ~56px
           further out in the margin; the bar stops at the column, not the rail. */
        left: max(0px, calc(50% - 400px));
        right: max(0px, calc(50% - 400px));
        margin: 0;
        max-width: none;
        padding-left: 16px;
        padding-right: 16px;
    }
}
/* Year is the gilt focal element of the bar (§3.1). It also doubles as the
   flex spacer: flex:1 lets it grow to fill the gap after the lens switcher and
   push admin/back/bookmarks to the right edge. */
/* Empty flex spacer that replaced the year marker (removed for now). Just
   grows to push admin/back/bookmarks to the right edge. */
.topbar-spacer { flex: 1; }
.topbar-year {
    flex: 1;
    /* The buttons bottom-align (flex-end); the year instead centers in the
       FULL bar height. Stretch over the bar's content box and cancel the bar's
       8px bottom padding (margin-bottom:-8px) so "center" is the true vertical
       middle of the bar, not of the padded box — otherwise it reads high. */
    align-self: stretch;
    margin-bottom: -8px;
    display: flex; align-items: center;
    color: var(--gilt); font-size: 18px; letter-spacing: 0.04em;
    text-transform: none;
    padding-left: 4px;
}
.topbar-btn {
    color: var(--ink-dim);
    background: var(--canvas-deep);
    border: 1px solid var(--rule);
    /* Larger label (15px) + a bit wider; vertical padding trimmed to 8px so the
       box stays on the shared ~33px height metric (15 + 8+8 + 2 border). */
    padding: 8px 16px;
    border-radius: 9px;
    font-size: 15px;
    /* inline-flex + line-height:1 so this anchor's box height equals the
       bookmark/seg buttons exactly, all on one shared metric. */
    display: inline-flex; align-items: center;
    line-height: 1;
    text-decoration: none;
}
.topbar-btn-admin { color: var(--head); }
/* Vfy/Adm/Theme are icon-only buttons at EVERY size (no text labels): the SVG
   icon shows always (Vfy ✓, Adm ⚙, Theme sun/moon/hemisphere), the text label
   is hidden always. Compact box — far narrower than the text buttons — matched
   in height to the other controls, all three reading as one icon set. */
/* Theme toggle now renders inline SVG (`.tb-icon`) like the other icon buttons,
   so it shares their compact box — no font-size/glyph-nudge needed. (The old
   font-glyph sun depended on U+FE0E to suppress the emoji; phones ignored it.) */
.tb-theme { padding: 2.5px 7px; min-width: 0; justify-content: center; }
/* The icon inherits the button color via currentColor and centers as a vector —
   no glyph side-bearing, which is what made the earlier ⚙/✓ font glyphs drift. */
.tb-icon { display: block; width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
/* Flat-FILLED icons (the Icons/-set style the switcher established): filled
   silhouettes whose knocked-out detail (lens hole, gear hub, check, ferrule)
   needs the evenodd fill-rule. Overrides the stroked .tb-icon defaults. */
/* Same upsize the switcher got: the filled glyphs' viewBoxes hug their ink,
   and the 26px box (up from the stroked set's 20px) + 2.5px vertical padding
   holds the shared 33px control height (2.5 + 26 + 2.5 + 2 border). */
.tb-icon.tb-fill { width: 26px; height: 26px; fill: currentColor; stroke: none; fill-rule: evenodd; }
/* A solid silhouette lays down ~3-4x the ink of the old 1.7px strokes, so at
   the SAME color token the filled glyphs READ darker — in both themes (user-
   confirmed). Blend the idle fill toward the canvas to restore the old
   perceived shade (applies to the filled back-swoosh and dropdown trash
   too). Active states — the gilt search/edit toggles — keep full strength
   so "on" still pops. */
.tb-icon.tb-fill, .bk-swoosh, .bm-trash, .bm-glyph { opacity: 0.65; }
/* Gilt active toggles stay full. */
.active .tb-icon.tb-fill { opacity: 1; }
/* Compact icon-button box for Adm (no text label → no wide text padding).
   2.5px vertical padding holds the shared 33px box (2.5 + 26 + 2.5 + 2 border = 33). */
.tb-admin { padding: 2.5px 7px; min-width: 0; justify-content: center; }

/* Lens switcher [ Lanes · Map ] — one segmented pill matching the other
   controls' fill/border; the two segments share it with a hairline divider. */
.topbar-switcher {
    display: flex; align-items: center;
    background: var(--canvas-deep);
    border: 1px solid var(--rule);
    border-radius: 8px;
    overflow: hidden;
}
.tb-seg {
    /* Icon segments (the text labels Sum·Chro·Lns·Map became glyphs drawn in
       the Icons/-set style). The glyphs carry their own ~3px margins inside
       the 32-grid, so the box is upsized to 26px (20px read too small next to
       the old 18px text); 2.5px vertical padding holds the shared 33px
       switcher height (2.5 + 26 + 2.5 + 2 border). */
    color: var(--ink-faint);
    background: none; border: none; cursor: pointer;
    padding: 2.5px 11px;
    display: flex; align-items: center; justify-content: center;
}
/* The switcher glyphs are FILLED silhouettes (matching the flat Icons/ set),
   unlike the stroked .tb-icon default — flip fill/stroke and use evenodd so
   their knocked-out subpaths (text lines, event marks, place-hole) stay open. */
.tb-seg .tb-icon { width: 26px; height: 26px; fill: currentColor; stroke: none; fill-rule: evenodd; }
.tb-seg:hover { color: var(--ink-dim); }
.tb-seg.active { color: var(--gilt); }
/* Hairline divider between the two segments. */
.topbar-switcher .tb-seg + .tb-seg { border-left: 1px solid var(--rule); }
.topbar-pad { height: calc(var(--topbar-h) + env(safe-area-inset-top, 0px)); }

/* Tap feedback. Mobile Safari/Chrome paint a grey rectangle over a tapped
   element by default (-webkit-tap-highlight-color); it's ugly over our pill
   buttons and chevrons. Suppress it on all interactive controls and replace it
   with a press beat (dim + slight shrink). .chip already has its own :active
   scale (above), so it's excluded from the shrink here to avoid stacking two
   transforms.

   Timing is ASYMMETRIC so a quick tap is still visible: the press snaps in fast
   (the :active rule's 40ms transition), but the RELEASE fades out slowly (the
   base rule's 260ms), so the dim lingers and eases away even when finger-down
   and finger-up are nearly simultaneous. A symmetric fast transition made the
   beat almost invisible. */
button, a, [role="button"], .tb-seg, .chip {
    -webkit-tap-highlight-color: transparent;
}
.tb-seg, .topbar-btn, .tb-theme, .tb-admin, .topbar-back {
    transition: opacity 260ms ease, transform 260ms ease;
}
.tb-seg:active, .topbar-btn:active,
.tb-theme:active, .tb-admin:active, .topbar-back:active {
    opacity: 0.5;
    transform: scale(0.94);
    transition: opacity 40ms ease, transform 40ms ease;
}


/* Cards (solo events) */
.card {
    transition: opacity 220ms ease;
    padding: 18px 16px 22px 18px;
    border-left: 4px solid var(--accent);
    position: relative;
    display: flex;
    flex-direction: column;
    /* Programmatic jumps (bookmark flag / dropdown / back / return-anchor) use
       scroll_into_view(block:Start); reserve the fixed topbar height + a small
       gap so the card's year marker lands just below the bar, not tucked under
       it. Mirrors .jx-card's scroll-margin-top fix. */
    scroll-margin-top: 56px;
    /* Suppress iOS long-press text selection across the card so the
     * bookmark corner's gesture doesn't trigger native selection of
     * adjacent prose. */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
.card-ornament {
    position: absolute;
    left: 0;
    top: 28px;
    background: var(--canvas);
    color: var(--accent);
    font-size: 13px;
    line-height: 1;
    padding: 6px 3px;
    text-align: center;
    font-weight: bold;
    transform: translate(-50%, -50%);
}
.card-year {
    /* Fluid so a 4-digit year never overflows the ~250px inner column at 320px;
       upper bound unchanged at 60px on wide screens. */
    font-size: clamp(44px, 15vw, 60px);
    line-height: 0.95;
    letter-spacing: -0.02em;
    color: var(--accent);
    font-feature-settings: "lnum";
    font-family: var(--serif-display);
}
.card-title {
    margin-top: 6px;
    /* 18px keeps the headline clearly LARGER than the 17px body so it reads as
       a title, not body text (the uppercase + heavier osr cut already separate
       them; size reinforces the hierarchy). */
    font-size: 18px;
    color: var(--ink);
    letter-spacing: 0.01em;
    text-transform: uppercase;
    line-height: 1.18;
    /* Lead with the heavier 'osr' cut (real weight, no faux-bold) so the
       headline reads clearly heavier than the osl-cut body prose. */
    font-family: "osr", "osl", Georgia, serif;
    font-weight: 500;
}
/* Reserve a top-right gutter so the title's first line wraps clear of the
   bookmark corner mark; lower lines reflow to full width. */
.card-title::before {
    content: "";
    float: right;
    width: 34px;
    height: 26px;
}
.card-hairline {
    margin: 6px 0 10px;
    width: 42px;
    height: 1px;
    background: var(--accent);
    opacity: 0.6;
}
.card-body {
    /* Lighter display cut (osl) reads less heavy than osr, which felt too
       bold/large for the card prose. Applies to Normal cards; Heavy overrides
       size below but inherits this family. (Nudged 16->17px for readability.) */
    font-size: 17px;
    line-height: 1.7;
    color: var(--ink);
    font-family: var(--serif-display);
}
.card-dropcap {
    float: left;
    font-size: 66px;
    line-height: 0.82;
    color: var(--accent);
    padding: 6px 10px 0 0;
    font-family: var(--serif-display);
    font-weight: 700;
}
/* Drop-cap via ::first-letter on the summary text span (no text-splitting, so
   the summary stays one editable string). Same look as the old .card-dropcap. */
.card-body-text::first-letter {
    float: left;
    font-size: 66px;
    line-height: 0.82;
    color: var(--accent);
    padding: 6px 10px 0 0;
    font-family: var(--serif-display);
    font-weight: 700;
}
.card-citation {
    margin-top: 16px;
    font-size: 12px;
    color: var(--ink-faint);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}
.card-heavy { min-height: 88vh; min-height: 88dvh; padding: 60px 16px 40px 18px; }
.card-heavy .card-ornament { top: 80px; font-size: 15px; padding: 7px 3px; }
.card-heavy .card-year { font-size: clamp(64px, 22vw, 96px); letter-spacing: -0.025em; }
/* Heavy keeps the shared 20px headline size (no font-size override) — only its
   extra top margin differs. */
.card-heavy .card-title { margin-top: 14px; }
.card-heavy .card-hairline { margin: 10px 0 16px; width: 56px; }
/* Heavy body uses the lighter display cut (osl) at 17px so the large block of
   prose reads lighter and a touch smaller — osr (the regular body face) was too
   heavy/large for the near-fullscreen Heavy card. */
.card-heavy .card-body { font-size: 17px; line-height: 1.75; font-family: var(--serif-display); }
.card-heavy .card-dropcap { font-size: 74px; padding: 7px 12px 0 0; }
.card-heavy .card-body-text::first-letter { font-size: 74px; padding: 7px 12px 0 0; }
.card-heavy .card-citation { margin-top: 28px; font-size: 12px; }

/* Year cluster (multi-event year) */
.cluster { padding-bottom: 14px; scroll-margin-top: 56px; }
.cluster-head {
    padding: 24px 16px 6px 18px;
    border-left: 4px solid var(--ink-veryfaint);
    display: flex;
    align-items: center;   /* buttons vertically centered against the year block */
    gap: 12px;
    flex-wrap: wrap;
}
/* Year with its "N events" badge centered directly beneath it. */
.cluster-year-block {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
}
.cluster-year {
    font-size: clamp(44px, 15vw, 60px);
    line-height: 0.95;
    color: var(--head);
    font-feature-settings: "lnum";
    letter-spacing: -0.02em;
    font-family: var(--serif-display);
    font-weight: 600;
}
.cluster-count {
    display: inline-block;
    font-size: 11px;
    letter-spacing: 0.2em;
    color: var(--ink-faint);
    text-transform: uppercase;
    vertical-align: middle;
    padding: 4px 10px;
    border: 1px solid var(--rule);
    border-radius: 6px;
    background: var(--rule-soft);
}
.sub-card {
    transition: opacity 220ms ease;
    padding: 14px 16px 16px 18px;
    position: relative;
    border-left: 4px solid var(--accent);
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    /* Sub-cards carry data-event-id, so they're the scroll target for most
       events (the ~77% inside year-clusters). They MUST reserve the topbar like
       .card/.cluster or a jumped-to sub-card tucks under the bar. */
    scroll-margin-top: 56px;
}
.sub-card-ornament {
    position: absolute;
    left: 0;
    top: 22px;
    background: var(--canvas);
    color: var(--accent);
    font-size: 13px;
    line-height: 1;
    padding: 6px 3px;
    text-align: center;
    font-weight: bold;
    transform: translate(-50%, -50%);
}
.sub-card-title {
    /* Match the solo .card-title so the headline is one consistent size/weight
       everywhere (18px, uppercase, medium) — clearly larger than the 17px body. */
    font-size: 18px;
    color: var(--ink);
    letter-spacing: 0.01em;
    text-transform: uppercase;
    font-family: "osr", "osl", Georgia, serif;
    line-height: 1.18;
    font-weight: 500;
}
/* Reserve a top-right gutter so the title's first line(s) wrap clear of the
   bookmark corner mark; lower lines reflow to full width. */
.sub-card-title::before {
    content: "";
    float: right;
    width: 34px;
    height: 24px;
}
.sub-card-body {
    margin-top: 6px;
    /* Cluster sub-cards are the least-emphasized reading level. They keep a
       deliberately quieter body than solo cards (full --ink), but nudged up so
       the prose isn't a strain to read: 17px and a slightly-brighter-than-dim
       ink (#b0a691, a step between --ink-dim #998f7d and --ink #d8d0bf). The
       dimmer-than-solo color preserves the hierarchy. (Nudged 16->17px.) */
    font-size: 17px;
    color: var(--ink-soft);
    line-height: 1.6;
}
/* Sub-card drop cap: same treatment as the solo card, scaled to the 15px
   body so a stack of sub-cards doesn't shout. */
.sub-card-dropcap {
    font-size: 44px;
    line-height: 0.82;
    padding: 4px 7px 0 0;
}
/* Sub-card drop-cap via ::first-letter (scaled to the 15px sub-card body). */
.sub-card-body-text::first-letter {
    float: left;
    font-size: 44px;
    line-height: 0.82;
    color: var(--accent);
    padding: 4px 7px 0 0;
    font-family: var(--serif-display);
    font-weight: 700;
}

/* Quiet rule (gap between events) */
.quiet-rule {
    padding: 14px 30px;
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--ink-faint);
    font-size: 10px;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    border-left: 4px solid var(--rule);
}
.quiet-rule-label { white-space: nowrap; }
.quiet-rule-dash { flex: 1; height: 1px; background: var(--rule); }
.quiet-rule-empty { height: 0; }

.reader-main {
    padding-right: 36px;    /* reserve gutter for the edge scrubber */
    /* room for the chip bar — which now grows by the safe-area inset, so the
       last card still clears it on home-indicator iPhones. */
    padding-bottom: calc(100px + env(safe-area-inset-bottom, 0px));
}
/* Reclaim the bar's reserved gutter when collapsed (else collapsing frees no
   reading space). Match the padding transition to the bar slide so reflow +
   movement read as one motion. */
.reader-main { transition: padding .25s ease; }
/* Scale hidden: drop the reserved gutter to near-zero so the cards' own 16px
   inner padding becomes the only right margin (symmetric with the ~18px left),
   instead of leaving a wide dead strip where the rail used to be. */
body.scale-collapsed .reader-main { padding-right: 2px; }
body.chips-collapsed .reader-main { padding-bottom: calc(28px + env(safe-area-inset-bottom, 0px)); }
/* Desktop: cap the Reader card stream at 800px and center it so lines don't run
   the full width of a wide monitor. The Lanes (Juxtaposition) and Map lenses are
   SIBLINGS of .reader-main, not children, so they keep their full width — only
   the reader column is constrained. */
@media (min-width: 760px) {
    .reader-main {
        /* Cap at 800px, but in the narrow-desktop band (760–912px) shrink so the
           column + the scrubber's reserved lane (~112px = 2×56) still fit without
           the rail overlapping text. Above 912px this is a no-op (800px wins). */
        max-width: min(800px, calc(100% - 112px));
        margin-left: auto;
        margin-right: auto;
    }
}
.boot-splash {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 60vh;
    min-height: 60dvh;
    color: var(--ink-faint);
}

/* Edge scrubber (fixed right) */
.scrubber {
    position: fixed;
    /* Rail starts flush under the topbar (no extra gap — the prior 22px offset
       left a visible void at the top of the scale). The rail's 0%..100% span
       maps across this box. */
    top: var(--topbar-h);
    /* Extend the rail down to just above the chip bar's top edge (the chip bar is
       ~66px+inset tall); clears it without colliding. */
    bottom: calc(70px + env(safe-area-inset-bottom, 0px));
    right: 0;
    width: 56px;
    z-index: 9;
    /* Shading is contained to the RIGHT of the divider line (at right:47px):
       solid canvas near the edge, fading to transparent exactly at the line —
       visible gradient on the scale side, no bleed into the reading column. */
    background: linear-gradient(to left, var(--canvas) 30px, transparent 47px);
    touch-action: none;
    clip-path: inset(0 -100px 0 -100px);
    transition: bottom .25s ease;   /* smooth the reclaim when chips drop */
    /* A drag that starts on a year label must pan the rail, never begin a
       text selection of the labels (same guard the cards carry). */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
}
/* When the chip bar is collapsed (dropped off-screen) its 70px clearance is
   wasted, so let the rail extend down to the bottom edge to use it. Scoped below
   the desktop breakpoint: at >=760px collapse is disabled and the chip bar is
   forced visible, so the rail must keep its clearance there even if the body
   still carries a stale .chips-collapsed class. */
@media (max-width: 759px) {
    body.chips-collapsed .scrubber {
        bottom: calc(8px + env(safe-area-inset-bottom, 0px));
    }
}
/* Desktop: the scrubber's RIGHT edge lines up with the reading column's right
   edge (50%-400px) — same point the topbar's right edge stops at, so the rail
   and the status bar terminate together. The 56px rail extends leftward into the
   column's reserved padding-right gutter. max(0px,…) pins it to the viewport edge
   once the window is too narrow. */
@media (min-width: 760px) {
    .scrubber { right: max(0px, calc(50% - 400px)); }
}
/* Decorative left border drawn just inside the tick column, not at the
 * rail's left edge — the box itself is wider for touch comfort but the
 * visible line stays close to the ticks. */
.scrubber::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    right: 47px;   /* divider nudged a hair right toward the scale (was 48);
                      content gutter left untouched so nothing reflows */
    width: 1px;
    background: var(--rule);
    pointer-events: none;
}
.scrub-rail {
    position: absolute;
    top: 0; bottom: 0;
    width: 2px;
    background: var(--ink-veryfaint);
    right: 10px;
}
/* Gesture layer: rail_gestures slides this ONE element (composited
   translateY) during pans/flings instead of updating ~1100 children's `top`
   per pointer move; window_lo commits once when the gesture settles. Same
   box as the scrubber, so the children's % tops are unchanged. */
.scrub-layer {
    position: absolute;
    inset: 0;
    will-change: transform;
}
.scrub-tick {
    position: absolute;
    right: 7px;
    width: 8px;
    height: 1px;
    background: var(--ink-faint);
    transform: translateY(-50%);
    transition: top 220ms ease-out;
}
.scrub-tick-major {
    right: 5px;
    width: 12px;
    height: 2px;
    background: var(--ink-dim);
}
/* Major tick at a year label: clip to half, right end fixed. */
.scrub-tick-labeled {
    width: 6px;
}
.scrub-label {
    position: absolute;
    right: 15px;
    font-size: 11px;
    color: var(--ink-dim);
    transform: translateY(-50%);
    font-feature-settings: "lnum";
    font-weight: 600;
    font-family: var(--serif-body);
    letter-spacing: 0.02em;
    transition: top 220ms ease-out;
}
.scrub-dot {
    position: absolute;
    right: 6px;
    width: 8px; height: 8px;
    border-radius: 2px;
    transform: translateY(-50%);
    box-shadow: 0 0 0 2px var(--canvas);
    transition: top 220ms ease-out, opacity 0.2s;
}
.scrub-dot-heavy { right: 4px; width: 12px; height: 12px; border-radius: 3px; }
.scrub-dot-dim { opacity: 0.18; }
/* Bookmark flags: a small gilt PENNANT on a staff, on the LEFT of the rail
   (kind dots, ticks and labels all extend RIGHT, so the left side is
   collision-free). The element is a HORIZONTAL pole sticking left out from the
   rail; ::before is a pennant hanging DOWN off the pole's far (left) end, its
   long edge flush to the pole and tapering to a point — so it reads as a flag
   on a pole jutting out from the rail. Always rendered but hidden until
   .bm-flag-on (its event id is bookmarked): a stable CSS-toggled set, which is
   what keeps the rail clear of the unmount-on-click crash family. Same
   top-transition as the dots so it tracks zoom/pan smoothly. */
.scrub-bm-flag {
    position: absolute;
    right: 14px;             /* pole's RIGHT end sits just off the rail */
    width: 20px;             /* horizontal pole sticking LEFT out from the rail */
    height: 1px;             /* pole thickness */
    display: none;
    background: var(--gilt);
    opacity: 0.85;                 /* slightly translucent at rest */
    transform: translateY(-1px);   /* center the pole on the year line */
    transition: top 220ms ease-out, opacity 150ms ease;
    cursor: pointer;
}
.scrub-bm-flag::before {
    content: "";
    position: absolute;
    /* Pennant hangs DOWN off the far (left) end of the pole. Its long edge (the
       top) is flush against the pole and it tapers to a point at the bottom —
       a proper pennant, not a triangle pointing into the pole. */
    top: 0;
    left: 0;
    width: 0; height: 0;
    border-top: 10px solid var(--gilt);   /* filled top edge, flush to the pole */
    border-right: 11px solid transparent; /* taper to a point on the right/down */
}
.scrub-bm-flag.bm-flag-on { display: block; }
/* Transparent hit-expander: the visible pole+pennant is ~10px, well under a
   comfortable touch target. This ::after grows the tappable area to ~44px
   (centered on the pole) without changing the flag's look — same trick the
   scale grab-notch uses. Only present when the flag is shown. */
.scrub-bm-flag.bm-flag-on::after {
    content: "";
    position: absolute;
    top: 50%;
    right: -12px;
    width: 44px;
    height: 44px;
    transform: translateY(-50%);
}
/* At a labeled year, the pennant (which hangs DOWN off the pole) would hang into
   the year label sitting on that line. Tilt the whole flag 10° upward, pivoting
   about the pole's right end (where it meets the rail), so the free end + pennant
   swing up and clear the label while keeping the natural flag-on-a-pole look.
   Positive rotate() = clockwise; with the pivot at the rail (right) end, that
   lifts the left/free end UP. translateY(-1px) keeps the pole on the year line. */
.scrub-bm-flag.bm-flag-labeled {
    transform-origin: right center;
    transform: translateY(-1px) rotate(10deg);
}
/* A flag sitting EXACTLY on a label year is hidden entirely (the year label owns
   that line) — see is_on_label in scrubber.rs, which omits the bm-flag-on class.
   So there's no on-label tilt/long-pole rule: that whole collision is avoided by
   simply not drawing the flag there. */
.scrubber.pinching .scrub-bm-flag,
.scrubber.panning .scrub-bm-flag { transition: none; }
@media (hover: hover) {
    .scrub-bm-flag.bm-flag-on:hover { background: var(--head); opacity: 1; }
    .scrub-bm-flag.bm-flag-on:hover::before {
        border-top-color: var(--head);
    }
}
.scrubber.pinching .scrub-tick,
.scrubber.pinching .scrub-label,
.scrubber.pinching .scrub-dot,
.scrubber.pinching .scrub-head,
.scrubber.panning .scrub-tick,
.scrubber.panning .scrub-label,
.scrubber.panning .scrub-dot,
.scrubber.panning .scrub-head {
    transition: none;
}
.scrub-head {
    position: absolute;
    right: 2px;
    width: 18px;
    height: 18px;
    border-radius: 5px;
    background: transparent;
    border: 2px solid var(--head);
    box-shadow: 0 0 0 3px var(--canvas);
    transform: translateY(-50%);
    transition: top 0.12s ease-out, opacity 0.15s;
    z-index: 2;
    opacity: 1;
}
.scrub-head-hidden { opacity: 0; }
.scrub-head::after {
    content: '';
    position: absolute;
    left: 50%; top: 50%;
    width: 6px; height: 6px;
    background: var(--head);
    border-radius: 2px;
    transform: translate(-50%, -50%);
    opacity: 0.7;
}

/* Lane chip bar (fixed bottom) */
.chip-bar {
    position: fixed;
    bottom: 0; left: 0; right: 0;
    /* Frosted bar (mirror of .topbar): flat translucent tint + backdrop blur.
       Content under is blurred, not sharply see-through. */
    background: var(--bar-tint);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    /* Lift the chips above the iOS home-indicator / app-switcher gesture zone:
       the 20px base cushion PLUS the device's safe-area inset (0 on hardware
       without a home indicator; ~34px on Face-ID iPhones). viewport-fit=cover
       in index.html is what makes env() resolve to a real value here. */
    padding: 14px 12px calc(20px + env(safe-area-inset-bottom, 0px));
    z-index: 11;
    touch-action: pan-x;
}
/* Desktop: unlike the topbar/reader column (capped at 800px), the filter chips
   are allowed to use the full bar width and CENTER so they all fit on one row
   without horizontal scrolling whenever space is ample. They only fall back to
   scroll if the window is too narrow to hold every chip. */
@media (min-width: 760px) {
    .chip-bar {
        padding-left:  32px;
        padding-right: 32px;
    }
}
/* Collapsible bars: slide the bar off-screen; the grab pill is a separate fixed
   sibling so it stays put. transform-only (composited), reduced-motion safe. */
.chip-bar { transition: transform .25s ease; }
.chip-bar.collapsed { transform: translateY(100%); }

/* The scrubber has clip-path: inset(0 -100px 0 -100px) (100px horizontal bleed),
   so translateX(100%) (its 56px width) leaves a sliver — translate past width +
   bleed and drop the clip while hidden so nothing peeks at the right edge. */
.scrubber { transition: transform .25s ease; }
.scrubber.collapsed { transform: translateX(160px); clip-path: none; }

/* Grab-handle pills — small inset "bar" handles (no glyph), fixed siblings that
   stay tappable when their bar slides away. Frosted to match the bars. */
.bar-grab {
    position: fixed;
    z-index: 21;                /* above bars (chip-bar 11, scrubber 9, topbar 20). */
    border: none;
    /* Notch shares its bar's frosted surface so the seam touching the bar
       vanishes — per-variant rules below set the per-side rounding + hairline
       and (for the scale) swap the tint for the rail's --canvas. */
    background: var(--bar-tint);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    padding: 8px 12px;          /* generous hit area; per-variant tightens one axis */
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    touch-action: manipulation;
}
/* While a lens sheet (Lanes/Map, z 15) covers the stream, the reader's
   fixed chrome has no job — and some of it outright pokes through: the
   pills sit at z 21 (above the sheet), and on desktop the chip bar stuck
   out both sides of the Map's own bottom scrubber. Hide all of it; the
   Map's own controls are .map-scale (a different class, unaffected). */
body.lens-open .bar-grab,
body.lens-open .chip-bar,
body.lens-open .scrubber { display: none; }
/* Freeze the document scroll while a lens sheet covers the stream. The reader
   stream is unmounted on lens open (TimelinePage's <Show when=Reader>), so this
   is belt to that suspenders: it guarantees the body can't scroll (or rubber-
   band on iOS) behind the lens even for the frame between the class flip and the
   stream's teardown. The lens sheets carry their own inner scroll. */
body.lens-open { overflow: hidden; }
/* the visible "small bar" line inside the tap target */
.bar-grab-handle {
    display: block;
    width: 28px;
    height: 4px;
    border-radius: 2px;
    background: var(--ink-faint);
    /* The handle color is the only feedback signal these tabs had room for;
       animate it so hover/press read as a live control, not dead pixels. */
    transition: background .12s ease, transform .06s ease;
}

/* ── Grab-tab feedback ────────────────────────────────────────────────────
   The chips/scale pills AND the chronicle Contents grab are the same kind of
   control (a fixed grab-handle that hides a panel) and had NO hover, press, or
   focus state at all — every tap was silent, which reads as "did it even
   register?" and invites the re-tap the panel was reported for. One shared
   block gives all three: a strengthened handle on hover, a clear depress on
   :active (so a tap always answers, even before the .25s panel slide), and a
   keyboard focus ring. Kept fast + on the handle so it never lags the slide. */
.bar-grab:hover .bar-grab-handle,
.chron-toc-grab:hover .bar-grab-handle { background: var(--ink-dim); }
.bar-grab:active .bar-grab-handle,
.chron-toc-grab:active .bar-grab-handle {
    background: var(--ink); transform: scaleX(1.15);
}
.bar-grab:focus-visible,
.chron-toc-grab:focus-visible {
    outline: 2px solid var(--gilt, #b39250); outline-offset: 2px;
}
/* Remove the default focus outline only when NOT keyboard-focused (so the ring
   above is the sole focus affordance and mouse/touch presses stay clean). */
.bar-grab:focus:not(:focus-visible),
.chron-toc-grab:focus:not(:focus-visible) { outline: none; }

/* Filter notch: centered on the chip bar's TOP edge, fused (top corners
   rounded, bottom flush onto the bar). Horizontal handle. */
.bar-grab--chips {
    left: 16px;                     /* toward the left, not centered */
    /* Sit ON the bar's top edge. Bar height = 14 (pad-top) + 35 (chip) + 4
       (.chips pad) + 20 (pad-bottom) ≈ 73px + safe-area. Anchor a few px BELOW
       that so the notch overlaps onto the bar (no gap even if the estimate is
       off / a chip row is shorter); the overlap hides under the bar. */
    bottom: calc(66px + env(safe-area-inset-bottom, 0px));
    /* Match the scale notch dimensions (rotated 90°): depth axis 5/3, length
       axis 12 — equal tab size, just horizontal vs the scale's vertical. */
    padding: 5px 12px 3px;          /* shorter bottom: flush onto the bar */
    border-radius: 8px 8px 0 0;     /* round only the outer (top) corners */
    border-top: 1px solid var(--rule);
    transition: bottom .25s ease, padding .25s ease, border-radius .25s ease;
}
/* Horizontal handle = the scale handle (3×22) rotated. */
.bar-grab--chips .bar-grab-handle {
    width: 22px;
    height: 3px;
}
/* Enlarge the TAP zone without enlarging the visible notch (the scale/chron
   trick, which the chips pill was MISSING — its ~34×11px visible box was the
   whole hit target, well under a finger). A transparent overlay adds reach
   upward (over the bar's top edge, toward the thumb's travel) and sideways.
   A child, so pointer-events land on the button; the visible notch stays slim. */
.bar-grab--chips::before {
    content: "";
    position: absolute;
    top: -12px;
    bottom: -6px;
    left: -12px;
    right: -12px;
}
/* Collapsed: ride to the bottom screen edge as a thin centered lip. */
body.chips-collapsed .bar-grab--chips {
    bottom: 0;
    padding: 4px 12px calc(2px + env(safe-area-inset-bottom, 0px));
}

/* Scale notch: a tab on the rail's LEFT edge, toward the top, fused (left
   corners rounded, right flush onto the rail). Shares the rail's solid --canvas
   surface (the rail is canvas, not the frosted bar tint). Vertical handle. */
.bar-grab--scale {
    right: 47px;                    /* meet the rail's divider line (right:47px) */
    top: calc(var(--topbar-h) + 24px);  /* toward the top of the rail */
    background: var(--canvas);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    padding: 12px 3px 12px 5px;     /* shorter right: flush onto the rail */
    border-radius: 8px 0 0 8px;     /* round only the outer (left) corners */
    border-left: 1px solid var(--rule);
    transition: right .25s ease, padding .25s ease, border-radius .25s ease;
}
.bar-grab--scale .bar-grab-handle {
    width: 3px;
    height: 22px;
}
/* Enlarge the TAP zone without enlarging the visible tab: a transparent overlay
   that extends the clickable area left (toward the reading column) and a bit
   above/below. Clicks on it hit the button (it's a child, pointer-events auto).
   The visible --canvas tab stays slim. */
.bar-grab--scale::before {
    content: "";
    position: absolute;
    top: -14px;
    bottom: -14px;
    left: -22px;
    right: -4px;
}
/* Collapsed: ride to the right screen edge as a thin vertical lip. */
body.scale-collapsed .bar-grab--scale {
    right: 0;
    padding: 12px 2px 12px 4px;
}

/* Desktop (>=760px): collapse is a touch affordance — remove it. Hide the grab
   notches and force both bars to their expanded position regardless of any saved
   collapsed state (so a bar collapsed on mobile can't open hidden, with no handle
   to recover it, on a wide screen). The persisted state + Rust mounting are
   untouched; this is a CSS-only visual override that reverts below 760px. */
@media (min-width: 760px) {
    .bar-grab--chips,
    .bar-grab--scale { display: none; }
    .chip-bar.collapsed { transform: none; }
    .scrubber.collapsed { transform: none; clip-path: inset(0 -100px 0 -100px); }
}

@media (prefers-reduced-motion: reduce) {
    .chip-bar, .scrubber, .reader-main,
    .bar-grab--chips, .bar-grab--scale,
    .bar-grab-handle, .chron-toc-grab { transition: none; }
    /* Keep the press SIGNAL (color/scale is feedback, not decoration) but make
       it instant rather than eased. */
    .bar-grab:active .bar-grab-handle,
    .chron-toc-grab:active .bar-grab-handle { transform: none; }
}
.chips {
    display: flex;
    gap: 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 4px;
    touch-action: pan-x;
}
.chips::-webkit-scrollbar { display: none; }
/* Desktop: let the chips use the full bar width and CENTER so they all fit on one
   row without horizontal scrolling when space is ample; wrap to a second row
   rather than clip if the window is narrow (centered overflow is otherwise
   unscrollable on the left — the flex-centering clip bug). Placed AFTER the base
   `.chips` so it wins on source order. */
@media (min-width: 760px) {
    .chips {
        justify-content: center;
        flex-wrap: wrap;
        row-gap: 8px;
        overflow-x: visible;
    }
}
.chip {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    /* ≥44px touch target (the app's tab standard) without enlarging the text. */
    min-height: 44px;
    gap: 7px;
    padding: 7px 10px;
    border: 1px solid var(--rule);
    border-radius: 10px;
    color: var(--ink-dim);
    font-size: 19px;
    line-height: 1;
    background: var(--rule-soft);
    cursor: pointer;
    font-family: var(--serif-display);
    white-space: nowrap;
    justify-content: center;
    transition: transform 0.08s ease, background 0.15s, color 0.15s, border-color 0.15s;
}
.chip:active { transform: scale(0.96); }
.chip-ornament {
    font-size: 18px;
    line-height: 1;
    color: var(--chip-color, var(--ink-faint));
}
.chip[data-on="1"] {
    background: var(--chip-color, var(--head));
    color: var(--canvas);
    border-color: var(--chip-color, var(--head));
}
.chip[data-on="1"] .chip-ornament { color: var(--canvas); }
.chip-all[data-on="1"] {
    background: var(--head);
    color: var(--canvas);
    border-color: var(--head);
}

/* Filtered-out cards/sub-cards/dots */
/* Filtered-out events dim as ONE piece — body, citation, stamp, bookmark
   corner and buttons all fade together (composite opacity), so no stray
   chrome floats at full strength over a gutted card (the old treatment
   display:none'd the body and left dim stubs + bookmark marks behind). */
.card.filtered-out, .sub-card.filtered-out {
    opacity: 0.25;
}

/* ========================================================================
   Admin page — Slice 1 (tabs, refresh button, license table)
   ======================================================================== */

.admin-tabs {
    display: flex;
    gap: 8px;
    margin: 16px 0 24px;
    border-bottom: 1px solid var(--rule);
}
.admin-tab {
    background: transparent;
    border: none;
    color: var(--ink-dim);
    padding: 10px 16px;
    cursor: pointer;
    font-family: var(--serif-display);
    font-size: 15px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
}
.admin-tab.active {
    color: var(--head);
    border-bottom-color: var(--head);
}

.admin-refresh-row {
    display: flex;
    justify-content: flex-end;
    margin-top: 8px;
}
.admin-refresh-btn {
    background: var(--rule-soft);
    color: var(--head);
    border: 1px solid var(--rule);
    padding: 8px 16px;
    border-radius: 8px;
    font-family: var(--serif-display);
    font-size: 13px;
    letter-spacing: 0.04em;
    cursor: pointer;
}
.admin-refresh-btn[disabled] {
    opacity: 0.5;
    cursor: default;
}
.admin-refresh-btn.spinning::after {
    content: '';
    display: inline-block;
    width: 10px; height: 10px;
    margin-left: 8px;
    border: 2px solid var(--head);
    border-top-color: transparent;
    border-radius: 50%;
    animation: admin-spin 700ms linear infinite;
    vertical-align: middle;
}
@keyframes admin-spin { to { transform: rotate(360deg); } }

.admin-audit-placeholder {
    color: var(--ink-faint);
    text-align: center;
    padding: 60px 24px;
    font-size: 14px;
    letter-spacing: 0.04em;
}

/* ========================================================================
   Admin Licenses tab — Slice 1.1 (role-grouped collapsable cards)
   ======================================================================== */

.admin-license-groups {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-top: 8px;
}

.admin-license-group {
    border: 1px solid var(--rule);
    border-radius: 10px;
    background: var(--rule-soft);
    overflow: hidden;
}
.admin-license-group.empty {
    /* zero-user shelf: flat pill, no chevron, not interactive */
    opacity: 0.7;
}

.admin-license-group-header {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    background: transparent;
    border: none;
    color: var(--head);
    font-family: var(--serif-display);
    font-size: 14px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    cursor: pointer;
    min-height: 48px; /* mobile tap target */
}
.admin-license-group-header[disabled] {
    cursor: default;
}
.admin-license-group-label {
    flex: 1;
    text-align: left;
}
.admin-license-group-chevron {
    color: var(--ink-dim);
    font-size: 18px;
    line-height: 1;
}

.admin-license-group-body {
    border-top: 1px solid var(--rule);
}

.admin-license-user {
    /* divider between user rows within a group */
    border-bottom: 1px solid var(--rule);
}
.admin-license-user:last-child {
    border-bottom: none;
}

.admin-license-user-row {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
    padding: 14px 18px;
    background: transparent;
    border: none;
    cursor: pointer;
    text-align: left;
    min-height: 56px; /* mobile tap target */
}
.admin-license-user-name {
    color: var(--ink);
    font-family: var(--serif-display);
    font-size: 17px;
    line-height: 1.2;
}
.admin-license-user-email {
    color: var(--ink-dim);
    font-size: 13px;
}
.admin-license-user-locked {
    color: var(--oxblood);
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-left: 4px;
}

.admin-license-user-body {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: 16px;
    row-gap: 6px;
    margin: 0;
    padding: 0 18px 16px 18px;
}
.admin-license-user-body dt {
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 11px;
    align-self: baseline;
}
.admin-license-user-body dd {
    margin: 0;
    color: var(--ink);
    font-size: 14px;
}
.admin-license-user-body dd.license-key {
    font-family: ui-monospace, "SF Mono", "Cascadia Mono", "Consolas", monospace;
    letter-spacing: 0.04em;
    font-size: 13px;
}

.admin-license-user-revoke {
    margin-top: 12px;
    padding: 8px 14px;
    background: transparent;
    border: 1px solid var(--oxblood);
    border-radius: 6px;
    color: var(--oxblood);
    font-family: var(--serif-display);
    font-size: 13px;
    letter-spacing: 0.04em;
    cursor: pointer;
}
.admin-license-user-revoke:hover {
    background: var(--oxblood);
    color: var(--ink);
}
.admin-license-user-revoke[disabled] {
    opacity: 0.5;
    cursor: default;
}

/* Role-change buttons (viewer <-> contributor) — same shape as Revoke, with a
   moss accent for "promote" and a steel/muted accent for "demote". */
.admin-role-btn {
    margin-top: 12px;
    margin-right: 8px;
    padding: 8px 14px;
    background: transparent;
    border: 1px solid var(--rule);
    border-radius: 6px;
    color: var(--ink);
    font-family: var(--serif-display);
    font-size: 13px;
    letter-spacing: 0.04em;
    cursor: pointer;
}
.admin-role-btn.make-contributor { border-color: var(--moss); color: var(--moss); }
.admin-role-btn.make-contributor:hover { background: var(--moss); color: var(--canvas); }
.admin-role-btn.make-viewer { border-color: var(--steel); color: var(--steel); }
.admin-role-btn.make-viewer:hover { background: var(--steel); color: var(--canvas); }
.admin-role-btn[disabled] { opacity: 0.5; cursor: default; }

/* ============================================================
 * Bookmarks · corner indicator
 * ============================================================ */
.bm-corner {
    position: absolute;
    top: 0;
    right: 24px;
    width: 36px;
    height: 36px;
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
    z-index: 5;
    /* Suppress iOS long-press text selection + native callout sheet. */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    touch-action: manipulation;
}
.bm-corner::before {
    content: "";
    position: absolute;
    top: 2px;
    right: 2px;
    width: 20px;
    height: 20px;
    border-top: 2px solid var(--gilt-deep);
    border-right: 2px solid var(--gilt-deep);
    opacity: 0.55;
    pointer-events: none;
    /* Anchor at the bottom-right so the long-press scale-up throws the bracket
       UP-and-left, into the zone above the finger/thumb (which usually covers
       the area below). Right edge stays pinned so it still reads as a corner
       mark. */
    transform-origin: bottom right;
    transition: opacity 200ms ease, border-color 200ms ease, transform 200ms ease;
}
/* Long-press feedback: temporarily enlarge the existing corner mark while held
   so it reads around a fingertip — biased upward for visibility above the
   finger. Unbookmarked → the bracket (::before) scales up + brightens;
   bookmarked → the corner-fold triangle (::after) scales up. The extra
   translateY lifts it clear of the fingertip. */
.bm-corner.is-pressing::before {
    opacity: 1.0;
    border-top-color: var(--gilt);
    border-right-color: var(--gilt);
    transform: translateY(-6px) scale(2.6);
}
.bm-corner.is-bookmarked::before {
    border-color: transparent;
    opacity: 0;
}
.bm-corner.is-bookmarked::after {
    content: "";
    position: absolute;
    top: 2px;
    right: 2px;
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 0 20px 20px 0;
    border-color: transparent var(--gilt) transparent transparent;
    transform-origin: bottom right;
    transition: transform 200ms ease;
}
.bm-corner.is-bookmarked.is-pressing::after {
    transform: translateY(-6px) scale(2.6);
}
@media (hover: hover) and (pointer: fine) {
    .bm-corner::before {
        transition: opacity 150ms ease, border-color 150ms ease;
    }
    .bm-corner:hover::before {
        border-top-color: var(--gilt);
        border-right-color: var(--gilt);
        opacity: 0.8;
    }
}

/* ============================================================
 * Bookmarks · dropdown
 * ============================================================ */
.bm-dropdown-wrap {
    position: relative;
}
.bm-dropdown-btn {
    background: var(--canvas-deep);
    border: 1px solid var(--rule);
    color: var(--ink-dim);
    /* Tighter padding than the text buttons: this pill holds only a glyph + a
       digit. The 21px flag is now the tallest content, so vertical padding is
       trimmed to keep the box at the 33px height of .topbar-btn / .tb-seg
       (21px glyph + 2×5 padding + 2×1 border = 33). */
    padding: 5px 8px;
    border-radius: 9px;
    cursor: pointer;
    font-size: 18px;
    /* line-height:1 so the box height tracks the content, not a leaded line. */
    line-height: 1;
    letter-spacing: 0.04em;
    font-family: inherit;
    display: inline-flex;
    align-items: center;
    gap: 5px;
}
/* (.has-bookmarks used to gild the pill, but with bookmarks in regular use it
   made the button read permanently "selected" — the state class is still set
   on the element, just unstyled now.) */
/* Filled flag, upsized with the rest of the filled set (its viewBox hugs the
   ink); still capped so the pill matches the other topbar buttons' 33px. */
.bm-glyph { width: 17px; height: 21px; display: block; }
.bm-count {
    font-size: 18px;
    font-weight: 600;
    /* Match every other number in the reader (.card-year, .scrub-label,
       .bm-yr): the display cut with LINING figures, not the body font's
       old-style figures (which dip below the baseline and look off here). */
    font-family: var(--serif-display);
    font-feature-settings: "lnum";
    font-variant-numeric: tabular-nums;
}

/* Transparent full-viewport catcher: a click anywhere off the panel closes the
   dropdown. Sits just under the panel (z 49 < panel z 50) but above page content.
   Replaces the old hand-rolled document click/keydown listeners.
   Always in the DOM; shown only when .open (CSS toggle, NOT <Show> mount — so
   closing never disposes a click handler mid-dispatch; see bookmarks_dropdown.rs). */
.bm-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 49;
    background: transparent;
}
.bm-backdrop.open { display: block; }
.bm-panel {
    /* Hidden until .open — same always-rendered / CSS-toggle rationale as the
       backdrop above. */
    display: none;
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 6px;
    background: var(--canvas);
    border: 1px solid var(--gilt-deep);
    border-radius: 3px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.8);
    min-width: 280px;
    max-width: 320px;
    max-height: 480px;
    overflow: hidden;
    flex-direction: column;
    z-index: 50;
}
.bm-panel.open { display: flex; }
.bm-panel-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 12px;
    border-bottom: 1px dotted var(--rule);
    color: var(--ink-faint);
    font-size: 9.5px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    flex-shrink: 0;
}
.bm-count-pill { color: var(--gilt); }
.bm-list {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;   /* momentum scroll on iOS Safari */
    flex: 1;
}
/* Bookmark rows: "Ledger" treatment (chosen 2026-06-15, mockups/
   bookmarks-archetypes/row-redesign-options.html, option A). The year is a
   gilt left column ruled off from the title; rows are roomier with a solid
   hairline divider; the × delete is hidden until the row is hovered. */
.bm-era-head {
    color: var(--gilt-deep);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    padding: 8px 14px 4px;
    border-bottom: 1px solid var(--rule);
}
.bm-row {
    display: flex;
    align-items: baseline;
    padding: 10px 14px;
    border-bottom: 1px solid var(--rule-soft);
    font-size: 13px;
    cursor: pointer;
}
.bm-row:last-child { border-bottom: none; }
.bm-row:hover { background: rgba(179,146,80,0.06); }
.bm-yr {
    color: var(--gilt);
    width: 46px;
    flex-shrink: 0;
    font-size: 14px;
    letter-spacing: 0.02em;
    /* Match the card year: the display cut with lining figures. */
    font-family: var(--serif-display);
    font-feature-settings: "lnum";
    font-variant-numeric: tabular-nums;
    border-right: 1px solid var(--rule);
    margin-right: 12px;
}
.bm-ti {
    color: var(--head);
    flex: 1;
    line-height: 1.3;
    /* Echo the card title (reader/styles.css .card-title): heavier 'osr' cut,
       uppercase, slight tracking — so a bookmark reads as the same object as
       the card it points to, not generic body text. */
    font-family: "osr", "osl", Georgia, serif;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.01em;
    font-size: 12px;
}
.bm-del {
    color: var(--ink-veryfaint);
    padding: 0 2px 0 8px;
    cursor: pointer;
    transition: opacity 120ms ease, color 120ms ease;
    /* SVG trashcan instead of a × glyph; align it to the row's first text line
       (the row is align-items:baseline, which would otherwise drop the icon). */
    align-self: center;
    display: inline-flex;
    line-height: 0;
}
.bm-trash { width: 15px; height: 15px; display: block; }
.bm-del:hover { color: var(--oxblood); }
/* Reveal-on-hover only where hover actually exists — on touch devices (no
   hover) the × must stay visible or bookmarks can't be removed. */
@media (hover: hover) {
    .bm-del { opacity: 0; }
    .bm-row:hover .bm-del { opacity: 1; }
}
.bm-empty {
    padding: 16px 12px;
    color: var(--ink-faint);
    font-size: 11.5px;
    text-align: center;
    line-height: 1.5;
}

/* Mobile: anchor the panel to the viewport rather than the button,
 * so the 280px-wide panel doesn't clip off the left edge when the
 * button sits in the middle of the topbar. */
@media (max-width: 599px) {
    .bm-panel {
        position: fixed;
        top: var(--topbar-h);
        right: 12px;
        left: 12px;
        min-width: 0;
        max-width: none;
    }
    /* (The card settings popover is now position:fixed + viewport-centered at
       ALL widths in its base rule, so no mobile-specific override is needed —
       min(320px, 100vw-24px) already keeps it fully on-screen on phones.) */
    /* The bar is crowded on phones (switcher · spacer · Adm · Vfy · Theme ·
       Back · Bookmarks). Tighten the gap/padding AND collapse the two widest
       text labels (Adm, Vfy) to compact glyphs, mirroring the theme toggle,
       so everything fits even on a 320px screen. */
    .topbar { gap: 5px; padding-left: 9px; padding-right: 9px; }
    .topbar-btn { padding-left: 9px; padding-right: 9px; }
    /* Four segments (Summaries·Chronicle·Lanes·Map): the strip is the widest
       topbar item and was getting flex-shrunk, whose overflow:hidden CLIPPED
       the Map seg (seen on iPhone). No shrink + tighter horizontal padding so
       every seg stays whole; the 26px glyph + 2.5px vertical padding keeps the
       shared 33px control height (2.5 + 26 + 2.5 + 2 border). */
    .topbar-switcher { flex-shrink: 0; }
    .tb-seg { padding: 2.5px 5px; }

    /* (Adm/Rev/Edit are icon-only at every size now — the .tb-label spans and
       their show/hide rules are gone.) */

    /* The icon buttons (Admin ⚙, Theme ◐) share ONE fixed box so they read
       as a uniform set. The edit toggle now carries `.tb-admin` itself,
       so it's covered here automatically — no separate .tb-edit sizing. */
    .tb-admin, .tb-theme {
        box-sizing: border-box;
        width: 34px; height: 33px;
        padding: 0; min-width: 0;
        justify-content: center; align-items: center;
    }
}

/* ============================================================
 * Breadcrumbs — in-chart panel and collapsed-state pill
 * ============================================================ */
.bc-section {
    background: rgba(184, 168, 144, 0.05);  /* head color at 5% */
    border: 1px solid var(--rule);  /* pale-deep */
    border-radius: 2px;
    padding: 6px 8px 7px;
    margin-bottom: 8px;
}
.bc-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    color: var(--head);
    font-size: 8.5px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    margin-bottom: 4px;
}
.bc-count { color: var(--ink-faint); }
.bc-row {
    display: flex;
    gap: 6px;
    padding: 2px 0;
    font-size: 10px;
    cursor: pointer;
    align-items: baseline;
}
.bc-row:not(.is-current):hover { background: rgba(184, 168, 144, 0.08); }
.bc-row.is-current { cursor: default; }
.bc-yr { color: var(--ink-faint); width: 32px; flex-shrink: 0; }
.bc-ti { color: var(--head); flex: 1; line-height: 1.4; }
.bc-row.is-current .bc-ti {
    color: var(--moss);
    font-style: italic;
}

.bc-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    background: rgba(184, 168, 144, 0.08);
    border: 1px solid var(--rule);
    border-radius: 10px;
    color: var(--head);
    font-size: 9.5px;
    cursor: pointer;
}
.bc-pill-arrow { font-size: 11px; }

/* ============================================================
 * Back button (topbar) — styled to match bookmarks dropdown button
 * ============================================================ */
.topbar-back {
    background: var(--canvas-deep);
    border: 1px solid var(--rule);
    color: var(--ink-dim);
    /* 6px vertical holds the shared 33px box with the 19px-tall swoosh
       (6 + 19 + 6 + 2 border). */
    padding: 6px 12px;
    border-radius: 9px;
    cursor: pointer;
    font-size: 15px;
    /* line-height:1 (like .topbar-btn / .tb-seg) so Back's box is the shared
       33px height — without it the default ~1.2 line-height made it 2px taller. */
    line-height: 1;
    letter-spacing: 0.06em;
    font-family: inherit;
    display: inline-flex;
    align-items: center;
    gap: 5px;
}
/* Filled return swoosh (replaced the ↶ text glyph — font side-bearing
   drifted). Upsized with the rest of the filled set. */
.bk-swoosh { width: 22px; height: 19px; display: block; fill: currentColor; }
.bk-count { font-size: 10.5px; }

/* ===== Juxtaposition lens (Slice A) ===== */
/* (The .tb-home back chevron lived here; removed — the Summaries switcher
   segment covers every case it handled.) */

.jx-launch {
    /* Larger text, tighter internal padding so the label fills the button —
       the outline (border + radius) is unchanged; only text size and the
       text-to-edge margin change. margin-top (gap from the card body) kept. */
    margin-top: 12px; font-family: var(--serif-body); font-size: 14px;
    color: var(--ink-dim); background: var(--canvas-deep);
    border: 1px solid var(--rule); border-radius: 8px;
    padding: 6px 9px; cursor: pointer;
    /* ≥44px touch target without growing the text — inline-flex centers the
       label vertically in the taller box. */
    display: inline-flex; align-items: center; min-height: 44px;
}
.jx-launch:hover { color: var(--gilt); border-color: var(--gilt-deep); }
/* Cluster-header variant: aligned to the right of the year/count header row. */
.jx-launch-cluster { margin-top: 0; font-size: 13px; padding: 5px 8px; }

/* Sheet covering the reader content but sitting BELOW the persistent topbar,
   so the topbar's Home ⌂ dismisses the lens and the lit "Lanes" segment shows
   the active mode. Starts below the topbar (safe-area + topbar height); z-index
   above the cards (~9) but below the topbar (10) so the bar stays tappable. */
.jx-sheet {
    position: fixed;
    /* Cover the ENTIRE viewport (inset:0) so no scrolling body can ever show
       through — then inset the lens's own content below the topbar via
       padding-top (same --topbar-h). This avoids the iOS issue where a fixed
       element offset by a magic top exposes the scrolling body between it and
       the fixed topbar. */
    inset: 0;
    padding-top: var(--topbar-h);
    /* Above the reader cards AND the fixed scrubber (z 9), below the topbar
       (z 20) so the bar stays visible/tappable and Home dismisses the lens. */
    z-index: 15;
    display: flex; flex-direction: column;
    /* Match the reader page's text-area background (--page-bg, set on html/body
       in index.html), not the lighter --canvas used by the header. */
    background: var(--page-bg);
}

/* Column header lives INSIDE its lane (first child) so it pans horizontally
   with the column for free — they're the same flex item, so they can never
   desync. position:sticky pins it to the top vertically while the cards
   below scroll through time. z-index keeps it above scrolling cards. */
.jx-lh {
    position: sticky; top: 0; z-index: 1;
    display: flex; align-items: baseline; gap: 7px; color: var(--accent);
    /* Negative side margins cancel the lane padding so the header band
       spans the full column width; padding re-adds the inset for content. */
    margin: 0 -5px 12px; padding: 8px 12px 7px;
    background: var(--canvas-deep); border-bottom: 1px solid var(--rule);
}
.jx-lh-orn { font-size: 15px; }
.jx-lh-name { font-size: 10.5px; letter-spacing: .13em; text-transform: uppercase; }
.jx-lh-ct { font-size: 9px; color: var(--ink-veryfaint); margin-left: auto; }

.jx-wrap {
    flex: 1 1 auto; overflow-x: auto; overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none; -ms-overflow-style: none;
    /* Allow BOTH axes on touch (vertical = time scroll, horizontal = pan
       between threads). Without this, iOS/Android lock to the first-moved
       axis and trap the user. Mirrors the codebase's touch-action use on
       .chips (styles.css:589). */
    touch-action: pan-x pan-y;
    /* none (not contain) to also suppress the rubber-band bounce inside the
       lens scroll, matching the reader page. */
    overscroll-behavior: none;
}
.jx-wrap::-webkit-scrollbar { width: 0; height: 0; display: none; }
.jx-lanes { display: flex; align-items: flex-start; min-height: 100%; width: max-content; }
/* Trailing spacer flex-item: reliably extends the horizontal scroll width by
   ~25vw past the last column (padding on the flex container is ignored for
   scroll-width in Safari, so use a real item). */
.jx-lanes::after { content: ""; flex: 0 0 25vw; align-self: stretch; }
.jx-lane {
    flex: 0 0 292px; border-right: 1px solid var(--rule-soft);
    /* 25vw breathing room below the last card before the scroll bottoms out. */
    padding: 0 5px 25vw;
}

/* scroll-margin-top offsets the auto-scroll-to-anchor (block:Start) so the
   anchor card lands BELOW the sticky .jx-lh header band, not hidden behind it. */
.jx-card { border-left: 2px solid var(--accent); padding: 0 1px 0 6px; margin-bottom: 16px; cursor: pointer; scroll-margin-top: 44px; }
.jx-card-head { display: flex; align-items: baseline; gap: 6px; }
/* Year is a quiet sans-serif label, not a glowing serif number: matches the
   sans card text, dropped to a muted gilt and regular weight (was serif 18px/600
   hot-gilt). */
.jx-card-yr {
    font-family: var(--jx-sans); font-size: 14px;
    color: var(--gilt-deep); font-weight: 400; letter-spacing: .01em;
}
/* Sans-serif, regular weight card text — the most readable treatment at this
   size on the dark canvas (chosen from the A/B typeset test). */
.jx-card-ti { font-family: var(--jx-sans); font-size: 13px; line-height: 1.34; color: var(--ink); font-weight: 400; margin-top: 4px; }
.jx-card-loc { font-family: var(--jx-sans); font-size: 12.5px; color: var(--ink-faint); margin-top: 3px; }
.jx-anchor-card {
    background: rgba(201,162,74,.07); border-radius: 0 5px 5px 0;
    padding: 6px 4px 7px 6px;
}
.jx-anchor-card .jx-card-yr, .jx-anchor-card .jx-card-ti { color: var(--gilt); }

.jx-gap {
    font-size: 9px; color: var(--ink-veryfaint); letter-spacing: .14em;
    text-transform: uppercase; margin: 0 0 16px; padding-left: 6px;
}


/* Time-alignment highlight. Tap an event to focus its year; its contemporaries
   (within ALIGN_RADIUS years) across the threads stay full-strength while the
   non-aligned cards recede — so the aligned set reads as a row-across-threads.
   The recede dims the WHOLE card uniformly (border, year, ornament, title and
   location all muted to one faint grey) so it reads as clean de-emphasis, not a
   faded-text-under-bright-accents glitch. */
.jx-card.jx-focused .jx-card-yr { color: var(--gilt); }
.jx-wrap:has(.jx-focused) .jx-card:not(.jx-aligned) {
    border-left-color: var(--ink-veryfaint);
}
.jx-wrap:has(.jx-focused) .jx-card:not(.jx-aligned) .jx-card-yr,
.jx-wrap:has(.jx-focused) .jx-card:not(.jx-aligned) .jx-card-ti,
.jx-wrap:has(.jx-focused) .jx-card:not(.jx-aligned) .jx-card-loc {
    color: var(--ink-veryfaint);
}

/* ── Event-detail page (/event/:slug) — Slice B ─────────────────────── */

.detail-shell {
  min-height: 100vh;
  background: var(--canvas);
  color: var(--ink);
  padding: 1.25rem 0.85rem 4rem;
  /* 50rem = the app's 800px desktop column (topbar/stream/chronicle/map all
     cap there) — 46rem left this page visibly narrower than the bar above it. */
  max-width: 50rem;
  margin: 0 auto;
  font-family: var(--serif-body);
}

.detail-loading,
.detail-error { opacity: 0.75; font-style: italic; }

/* Back to the card stream — quiet text link above the article, with enough
   padding to be a comfortable touch target. */
.detail-back {
    display: inline-block;
    margin: 0 0 14px;
    padding: 6px 10px 6px 0;
    color: var(--ink-soft);
    text-decoration: none;
    font-size: 0.92rem;
}
.detail-back:hover { color: var(--ink); text-decoration: underline; }

/* Page-open entrance. Opening a different page (tap a card → this event detail)
   used to hard-cut the content in, which read as flashy. Ease it in instead: a
   gentle fade + small upward drift as the article mounts. ease-out so it
   decelerates into place (calm), not a bounce. Honored-once on mount; the
   detail article is freshly created per /event/:slug open, so it doesn't re-run
   on reactive updates. Disabled under prefers-reduced-motion (below). */
@keyframes page-enter {
  from { opacity: 0; }
  to   { opacity: 1; }
}
.detail-event {
  /* Opacity-only, ~150ms: a calm fade with NO movement, so it can never cause a
     layout shift (the earlier 380ms + translateY drift read as flashy/reflowy).
     Disabled under prefers-reduced-motion (below). */
  animation: page-enter 150ms ease both;
}

.detail-title {
  /* Heavier 'osr' cut (matches the card headline) so the title reads heavier
     than the chronicle body prose, not lighter. */
  font-size: 20px;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  line-height: 1.18;
  margin: 0 0 1rem;
  font-weight: 500;
  font-family: "osr", "osl", Georgia, serif;
}

.detail-meta {
  display: flex;
  gap: 0.75rem;
  align-items: baseline;
  color: var(--gilt);
  font-size: 1rem;
  margin-bottom: 0.3rem;
}
.detail-place::before { content: "· "; opacity: 0.6; }

.detail-section { margin: 1.6rem 0; }
.detail-section h2 {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  opacity: 0.6;
  margin: 0 0 0.5rem;
  font-weight: 600;
}
.detail-section h3 {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  opacity: 0.55;
  margin: 0 0 0.35rem;
}

.detail-source { opacity: 0.85; font-size: 0.95rem; margin: 0; }
.detail-tags p { opacity: 0.7; font-size: 0.9rem; margin: 0; }

/* Chronicle chapter blockquote (ported ChronicleBlockquote). */
.detail-further .story-quote {
  /* Give the chronicle source excerpt its own deeper surface (--canvas-deep) so
     it reads as a distinct, darker "source" panel rather than blending into the
     detail page (.detail-shell --canvas). Was transparent → showed the lighter
     page bg through it. --canvas-deep is the app's recessed-surface token, dark
     in both themes. Inset padding + radius so the bg reads as an intentional box
     (horizontal padding was 0). */
  background: var(--canvas-deep);
  padding: 0.9rem 1rem;
  margin: 0.5rem 0 0;
  border-radius: 10px;
}
.story-quote .quote-header {
  font-weight: 600;
  margin-bottom: 0.75rem;
}
/* quote-body now wraps one <p> per chronicle paragraph (Task 0). */
.story-quote .quote-body { margin: 0 0 0.75rem; }
.story-quote .quote-body p {
  /* Lighter 'osl' cut so the body reads lighter than the osr title. */
  font-family: "osl", "osr", Georgia, serif;
  font-size: 1.05rem;
  line-height: 1.7;
  margin: 0 0 0.9rem;
}
.story-quote .quote-body p:last-child { margin-bottom: 0; }
.story-quote .quote-attr {
  font-size: 0.8rem;
  opacity: 0.6;
  font-style: italic;
}

/* The reader card title link inherits color (no default browser blue). */
.card-title-link {
  color: inherit;
  text-decoration: none;
}
.card-title-link:hover { text-decoration: underline; text-decoration-color: var(--gilt); }

/* ── Map lens (/ overlay) — Slice C ─────────────────────────────────── */
.map-sheet {
  position: fixed;
  inset: var(--topbar-h, 48px) 0 0 0;   /* sit below the persistent topbar */
  background: var(--canvas);
  z-index: 40;
  overflow: hidden;
  /* The three-column atlas page (grid rules live in the atlas section at the
     end of this file — see "The atlas page"). */
}
/* Desktop: full-viewport, like the Lanes lens. (It was briefly capped to the
   800px app column, but a map earns a wide monitor — user call 2026-07-03.) */
.map-loading { color: var(--ink-soft); font-style: italic; padding: 1.5rem; }
.map-leaflet { flex: 1; width: 100%; height: 100%; background: var(--canvas); }
/* Voyager is a light style; nudge it toward the dark/gilt theme — LIGHT touch
   so its detail (rivers, terrain, labels) stays legible and the gilt pins pop.
   Tune at smoke (Task 6); fallback is the dark_all base (see plan Task 7). */
.map-leaflet .leaflet-tile-pane { filter: brightness(0.9) saturate(0.85) sepia(0.12); }
/* On-map REGION caps + NOTABLE-colony name labels were removed (2026-06-16):
   the CARTO basemap already labels places, so the overlaid names were redundant
   and collided. Their `.map-region-lbl` / `.map-colony-lbl` styles are gone with
   them. Colony names live in the tap popover. Only the arc from/to labels remain
   (styled below). */
/* Migration arcs (sourced mass-movements): gilt curved polylines, soft glow.
   The dashes MARCH from origin toward destination (animated stroke-dashoffset)
   so the eye reads the direction of travel. Opacity is set per arc by the arc
   Effect (draw-on / hold / fade); this styles + animates the stroke. The arc is
   drawn origin→destination, so a decreasing dashoffset flows toward the dest. */
/* One arc-march for ALL widths (desktop == mobile, operator choice). The former
   desktop variant (fine dashes 5 9, 2.6s) re-rasterized the SVG path every frame
   and stuttered on iOS Safari; this coarser/slower march (period 8+14=22, 5s,
   offset travel 44 = 2× the period for a seamless loop) is gentler everywhere
   and has far fewer dash-edge re-rasterizations per second. */
.map-arc {
  stroke-dasharray: 8 14;
  stroke-linecap: round;
  animation: map-arc-flow 5s linear infinite;
}
@keyframes map-arc-flow {
  from { stroke-dashoffset: 44; }
  to   { stroke-dashoffset: 0; }
}
/* Traced-journey route (journeys.rs): a longer, calmer dash than the migration
   arc — a documented route told in one line. Same deep-oxblood ink; the longer
   dash + numbered stops (below) keep it distinct from a migration arc. */
.map-journey {
  stroke-dasharray: 10 15;
  stroke-linecap: round;
  animation: map-arc-flow 6s linear infinite;
}
/* Numbered waypoint dot: an oxblood disc with a paper-white number, a paper halo
   so it reads over any terrain. The climax stop (Grant) is a filled star. */
.map-journey-dot {
  display: flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%;
  background: #7a1e1e; color: #f2ecdc;
  font: 700 11px/1 Georgia, serif;
  border: 1.5px solid #f7f2e8;
  box-shadow: 0 0 0 1px rgba(122,30,30,.85), 0 0 7px 2px rgba(247,242,232,.85);
  pointer-events: none;
}
.map-journey-dot.is-climax {
  width: 26px; height: 26px;
  background: #a82828; font-size: 15px;
  border-width: 2px;
  box-shadow: 0 0 0 1.5px rgba(74,16,16,.9), 0 0 9px 3px rgba(247,242,232,.9);
}
/* Transparent per-stop tap target — makes a small dot comfortably tappable
   without changing its look (same trick as .scrub-bm-flag's hit-expander). */
.map-journey-hit { cursor: pointer; }
/* Journey popover (the one Selected slot) — matches the raid/migration snippet
   look with the oxblood spine. */
.map-sel.is-journey .jrny-pop-snippet {
  font-style: italic; color: #b8b09e; line-height: 1.5;
  border-left: 2px solid #7a1e1e; padding-left: 9px; margin-top: 2px;
}
.map-sel.is-journey .jrny-pop-cite {
  color: #998f7d; font-size: 11px; margin-top: 6px;
  letter-spacing: 0.02em;
}
/* Leaflet ships a default `.leaflet-div-icon { background:#fff; border:1px solid
   #666 }` on EVERY divIcon — that was the white box behind our text labels (it's
   a different class on the same element, so the .map-arc-lbl rules never touched
   it). Strip it for all our map divIcons; each label/dot supplies its own look. */
.map-leaflet .leaflet-div-icon { background: none; border: none; }

/* Endpoint markers: a small ring at each arc end, and the from/to name labels.
   The arc's draw-on animation (@keyframes map-arc-flow) and its driver are
   UNCHANGED — these endpoints/labels only wear the same paper halo/pill every
   other mark on the plate wears, so the arc stops looking like a different
   piece of software (spec 2026-07-12). */
.map-arc-end {
  border: 1.5px solid #f7f2e8; border-radius: 50%;
  pointer-events: none;
  /* Outer ring tinted deep-oxblood to echo the route line (was neutral brown
     rgba(70,55,30,.8)); paper halo unchanged. */
  box-shadow: 0 0 0 1px rgba(122,30,30,.85), 0 0 8px 3px rgba(247,242,232,.8);
}
/* Destination = filled deep oxblood (#7a1e1e), matching the recolored route
   line (was gold rgba(216,176,74,.85)). */
.map-arc-end.is-to { background: rgba(122,30,30,.9); }
.map-arc-lbl-box {
  /* Positioning only: the fixed-size divIcon box that centers the inner chip
     over the endpoint. Same two-element pattern as .map-colony-lbl-box —
     painting the chip class directly on the divIcon stretched it to the full
     120px iconSize box (the clipped/oversized-chip bug). */
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.map-arc-lbl {
  /* Option C: a light parchment chip, deliberately DISTINCT from the dark
     colony pill (.map-colony-lbl) so a movement-label never reads as a
     place-label. Literal colors — the map is always light regardless of app
     theme. inline-block so it hugs its text inside the centering box.
     Palette folded to the plate's engraved-hand values (spec 2026-07-12). */
  display: inline-block;
  font-family: var(--serif-body); font-size: 11px; font-weight: 600;
  white-space: nowrap; pointer-events: none;
  padding: 2px 8px 2px 7px; border-radius: 3px;
  background: rgba(247,242,232,.94); color: #2b2722;
  border: 1px solid rgba(120,100,70,.35);
  box-shadow: 0 1px 3px rgba(0,0,0,.12);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.map-arc-lbl .dir {
  color: #9a7b3a; font-style: italic; font-weight: 400; margin-right: 3px;
}
/* Leaflet chrome themed to the reader (dark, gilt) */
.map-leaflet .leaflet-control-attribution {
  background: var(--overlay-bg); color: var(--ink-soft); font-size: 10px;
}
.map-leaflet .leaflet-control-attribution a { color: var(--gilt); }
.map-leaflet .leaflet-bar a {
  background: var(--overlay-bg); color: var(--gilt);
  border-bottom-color: rgba(179,146,80,.25);
}
.map-leaflet .leaflet-bar a:hover { background: rgba(179,146,80,.15); }
.map-leaflet .leaflet-popup-content-wrapper, .map-leaflet .leaflet-popup-tip { display: none; }
/* (The Map filmstrip scrubber — the old .map-strip / .map-band track and its
   2800px pixel-width run — was retired 2026-07-12. Its replacement, the
   engraved ruler, is the ".map-scale" section at the END of this file.) */
.card-launch-row { display: flex; gap: 0.5rem; margin-top: 0.6rem; }
/* Trash on its own line at the bottom of the card, pushed to the right. Used by
   BOTH renderers (EventCard + SubCard). The row only has content in edit mode
   (DeleteCard renders nothing otherwise), so it collapses to ~0 height while
   reading. justify-content:flex-end isolates the destructive control at the far
   right, away from the rest of the card. */
.card-delete-row { display: flex; justify-content: flex-end; }
.jx-launch-map { /* inherits .jx-launch; distinct hover if desired */ }

/* Respect the OS "reduce motion" setting. Users who ask for less motion get an
   instant page swap (no entrance fade/drift) and an instant tap state, never a
   crawl. Looping indicators (admin spinner, map-arc flow) are functional and
   left alone. This is the first reduced-motion handling in the sheet. */
@media (prefers-reduced-motion: reduce) {
  .detail-event { animation: none; }
  .tb-seg, .topbar-btn, .tb-theme, .tb-admin, .topbar-back,
  .tb-seg:active, .topbar-btn:active,
  .tb-theme:active, .tb-admin:active, .topbar-back:active {
    transition-duration: 0ms;
  }
}

/* ── Summary search ─────────────────────────────────────────────── */
/* Match the Adm/Theme icon buttons: same boxed treatment (.topbar-btn) and the
   same compact icon-only box + 20px shared .tb-icon (no stroke-width override,
   so the magnifier reads at the same line weight as its neighbors). */
.tb-search { padding: 2.5px 7px; min-width: 0; justify-content: center; }
.tb-search.active { color: var(--gilt); }

.search-overlay {
  position:fixed; left:0; right:0; top:var(--topbar-h); bottom:0;
  background:var(--page-bg); z-index:40;
  display:none; flex-direction:column;
}
.search-overlay.search-open { display:flex; }

.search-bar {
  position:sticky; top:0; display:flex; align-items:center; gap:10px;
  padding:10px 14px; background:var(--bar-tint);
  -webkit-backdrop-filter:blur(10px); backdrop-filter:blur(10px);
  border-bottom:1px solid var(--rule);
}
.search-input {
  /* font-size:16px is load-bearing: <16px triggers iOS focus auto-zoom. */
  flex:1; min-width:0; background:var(--canvas-deep); border:1px solid var(--gilt-deep);
  color:var(--ink); font-family:var(--serif-body); font-size:16px;
  padding:9px 12px; border-radius:8px; outline:none;
}
.search-input::placeholder { color:var(--ink-faint); }
.search-clear { background:none; border:none; color:var(--ink-dim); font-size:18px; cursor:pointer; padding:4px 8px; }

.search-results { overflow-y:auto; padding:4px 14px 40px; }
.search-empty { color:var(--ink-faint); font-size:14px; padding:24px 6px; text-align:center; }
.search-list { display:block; }
.search-hit {
  display:block; width:100%; text-align:left; background:none; border:none;
  border-bottom:1px solid var(--rule-soft); padding:13px 4px; cursor:pointer;
  color:inherit; font-family:var(--serif-body);
}
.search-hit:hover { background:rgba(179,146,80,.06); }
.search-hit-snip { color:var(--ink-soft); font-size:14px; line-height:1.45; }
.search-hit-snip mark { background:rgba(179,146,80,.28); color:var(--ink); border-radius:2px; padding:0 1px; }
.search-fuzzy {
  color:var(--gilt-deep); font-size:11px; letter-spacing:.04em;
  border:1px solid var(--gilt-deep); border-radius:4px; padding:1px 5px;
  margin-left:8px; text-transform:uppercase; vertical-align:middle;
}

/* Pulse the landed card after a search jump. The class lingers after the jump
   (no JS timer removes it — see jump_to_event); the animation runs once and
   ends transparent, so a lingering class is invisible. jump_to_event forces a
   reflow before re-adding the class so a repeat jump to the same card re-fires
   the animation. */
@keyframes search-pulse-kf {
  0%   { background:rgba(179,146,80,.18); }
  100% { background:transparent; }
}
@media (prefers-reduced-motion: no-preference) {
  .search-pulse { animation:search-pulse-kf 1.2s ease-out; }
}

.search-hit-head { display:flex; align-items:baseline; gap:8px; margin-bottom:5px; }
.search-hit-badge { width:8px; height:8px; border-radius:2px; flex:none; transform:translateY(1px); }
.search-hit-title { font-family:var(--serif-display); color:var(--head); font-size:15px; line-height:1.25; }
.search-hit-year { color:var(--ink-faint); font-size:13px; margin-left:auto; flex:none; }


/* Constrain the search overlay's inner content to the reader column width
   (matching .reader-main's 800px desktop cap) so the bar + results align with
   the card stream instead of running full-bleed on wide viewports. */
.search-bar, .search-results {
  width: 100%;
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
}


/* ── In-stream editing (edit-in-place) ──────────────────────────────
   Each field shows its own value; in edit mode a small pen sits INLINE
   right after the value. Tapping the pen turns the field's OWN text into a
   contenteditable span — same font/size/color/position, NO input box — and
   saving writes the new text back into the same spot. */
/* TopBar edit toggle carries `.topbar-btn .topbar-btn-admin .tb-admin` in the
   markup, so it inherits the Adm button's box/icon size/spec for free (desktop
   AND mobile). `.tb-edit` sets NO dimensions — it only colors the active state. */
.tb-edit.active { color: var(--gilt); }
.field-edit { display: inline-flex; align-items: center; gap: 4px; }
/* Inline pen: sits immediately after the field's value text. */
.field-pen {
  background: none; border: none; cursor: pointer; padding: 0 2px;
  color: var(--steel); opacity: .55; vertical-align: middle;
  display: inline-flex; align-items: center;
}
.field-pen:hover { opacity: 1; }
.pen-icon { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-width: 2; }
/* The gear is a FILLED silhouette (a solid toothed cog), not a single-stroke
   line like the pen — so it fills with currentColor and draws no stroke. Sized a
   touch larger so the teeth are legible. */
.gear-icon { fill: currentColor; stroke: none; width: 15px; height: 15px; }
/* Faint placeholder shown for an empty field while editing. */
.field-placeholder { opacity: .4; font-style: italic; }
/* The editable text itself: inherits the field's typography (the span carries
   the field's text-class), so it looks identical to the displayed text. NO
   border, NO background box. A dotted underline + caret are the only cues that
   it's live; the focus outline is suppressed in favor of the underline. */
.field-live {
  outline: none;
  /* Match the DISPLAYED text exactly. The span carries the field's text-class
     and sits inside the field's display container (.card-title / .card-body),
     but a contenteditable span was still picking up a different font at runtime;
     `font: inherit` pins family/size/weight/style to the parent so the editor
     looks identical to the saved text. (text-transform isn't a font property but
     is inherited from the parent anyway, so the title stays uppercase.) */
  font: inherit;
  border-bottom: 1px dotted var(--steel);
  caret-color: var(--accent, var(--ink));
  cursor: text;
  /* Separate the editable text from the ✓/✕ controls that follow it. */
  margin-right: 10px;
}
.field-live:focus { border-bottom-color: var(--accent, var(--steel)); }
/* Kind stays a real <select> (fixed choice list) — keep a light control look. */
.fe-input {
  font: inherit; padding: 1px 4px; border: 1px solid var(--steel);
  border-radius: 4px; background: var(--page-bg); color: var(--ink);
}
/* ✓ / ✕ save+cancel controls: roomy tap targets, clearly separated from each
   other and from the text being edited. */
.fe-ok, .fe-cancel {
  background: none; border: none; cursor: pointer;
  font-size: 1.15em; line-height: 1;
  padding: 4px 6px; margin-left: 6px;
  border-radius: 4px;
}
.fe-ok:hover { background: color-mix(in srgb, var(--moss) 18%, transparent); }
.fe-cancel:hover { background: color-mix(in srgb, var(--oxblood) 18%, transparent); }
.fe-ok { color: var(--moss); }
.fe-cancel { color: var(--oxblood); }

/* Per-card metadata form (year / kind / weight / location) — a gear button
   opens a popover so these "settings" never clutter the card face. */
.card-meta-form { position: relative; display: inline-flex; align-items: center; }
.meta-gear { margin-left: 8px; }
.meta-gear .pen-icon { width: 15px; height: 15px; }
/* Trash control: red-tinted so it reads as destructive, set apart from the gear. */
.delete-card { margin-left: 10px; color: var(--oxblood); }
.delete-card .pen-icon { width: 15px; height: 15px; }
.delete-card:hover { opacity: 1; }
/* Full-viewport transparent catcher: click outside the popover to dismiss.
   Always mounted; shown only with `.open` (CSS toggle, never <Show> — the
   backdrop's own click closes it, and unmounting a click target mid-dispatch
   crashes Leptos; see leptos_show_unmount_own_handler_crash). */
.meta-backdrop {
  position: fixed; inset: 0; z-index: 60; background: transparent;
  display: none;
}
.meta-backdrop.open { display: block; }
/* Settings popover — FIXED to the viewport (not absolute-to-the-gear) at ALL
   widths. Absolute+left:0 off the gear was fragile: from a gear that sits
   partway across the card, `left:0; width:280px` ran off the right edge on
   narrow viewports, AND because placeholder cards use opacity<1 (a stacking
   context) the popover's z-index couldn't beat sibling cards, so it hid behind
   the next card. position:fixed escapes BOTH: it clamps to the viewport (never
   overflows) and stacks against the viewport root (above any card's opacity
   context). Centered horizontally, pinned just below the topbar, height-capped
   with internal scroll. z-index sits above the topbar. */
.meta-popover {
  position: fixed; z-index: 200;
  top: calc(var(--topbar-h) + 10px);
  left: 50%; transform: translateX(-50%);
  width: min(320px, calc(100vw - 24px));
  max-width: calc(100vw - 24px);
  padding: 12px;
  background: var(--page-bg); color: var(--ink);
  border: 1px solid var(--steel); border-radius: 8px;
  box-shadow: 0 12px 32px rgba(0,0,0,.4);
  flex-direction: column; gap: 8px;
  font-family: var(--vfy-sans, inherit); font-size: .85rem;
  max-height: calc(100vh - var(--topbar-h) - 24px); overflow-y: auto;
  box-sizing: border-box;
  display: none;
}
.meta-popover.open { display: flex; }
/* A dimming backdrop behind the fixed popover so it reads as a focused modal
   (and taps outside are visually distinct). Reuses the always-mounted
   .meta-backdrop element; only its look changes for the fixed layout. */
.meta-backdrop.open { background: rgba(0,0,0,.28); }
/* Pin a compact control font INSIDE the popover. .fe-input uses `font: inherit`
   (the shorthand resets size+family) so without this the year/weight/etc inputs
   render at the card's big reading-serif size, blowing the popover up to ~700px
   tall and pushing Save/Cancel out past the card. Force a small sans control. */
.meta-popover .fe-input,
.meta-popover .meta-save,
.meta-popover .meta-cancel {
  font-family: var(--vfy-sans, system-ui, sans-serif);
  font-size: .85rem; line-height: 1.3;
}
.meta-row { display: flex; align-items: center; gap: 8px; }
.meta-label { flex: 0 0 64px; color: var(--ink-faint); font-size: .78rem; }
.meta-row .fe-input { flex: 1 1 auto; min-width: 0; padding: 4px 6px; }
/* (No card-lift needed now: the popover is position:fixed, so it stacks against
   the viewport root and is no longer trapped in a placeholder card's opacity
   stacking context.) */
/* Publish toggle row in the settings popover: a checkbox + label, not a flexed
   input. Left-aligned, compact, inherits the popover's pinned sans font. */
.meta-checkbox-row { gap: 8px; cursor: pointer; }
.meta-checkbox-row input[type="checkbox"] {
  width: 16px; height: 16px; margin: 0; flex: 0 0 auto; accent-color: var(--moss);
}
.meta-checkbox-row .meta-label { flex: 0 0 auto; }
.meta-actions {
  display: flex; gap: 8px; justify-content: flex-end;
  margin-top: 4px; padding-top: 8px;
  /* Keep the actions visibly attached to the form even when the body scrolls. */
  position: sticky; bottom: -12px;
  background: var(--page-bg); border-top: 1px solid var(--steel);
}
.meta-save, .meta-cancel {
  font-size: .85rem; padding: 5px 14px; border-radius: 6px; border: none; cursor: pointer;
}
.meta-save { background: var(--moss); color: var(--page-bg); }
.meta-cancel { background: none; color: var(--ink-faint); }

.add-entry-fab {
  position: fixed; right: 20px; bottom: 20px; z-index: 50;
  padding: 12px 18px; border-radius: 24px; border: none; cursor: pointer;
  background: var(--gilt); color: #1a1206; font-weight: 600;
  box-shadow: 0 4px 14px rgba(0,0,0,.3);
}

.card.placeholder, .sub-card.placeholder {
  /* NO `opacity` here. opacity<1 makes the ENTIRE card subtree translucent —
     INCLUDING the settings popover nested inside it — so the popover panel went
     see-through (card text bled through it) and its z-index got trapped in the
     card's stacking context. A child cannot opt out of a parent's opacity, and
     position:fixed does not escape it (still a DOM descendant). So placeholders
     are distinguished WITHOUT opacity: a slightly muted title color + the
     register pin / dup badge. */
}
/* Mute just the placeholder's own text (not descendants like the popover) so
   the card still reads as "faint/unpublished" without the opacity trap. */
.card.placeholder .sub-card-title, .sub-card.placeholder .sub-card-title,
.card.placeholder .card-title, .sub-card.placeholder .card-title { color: var(--ink-dim, #998f7d); }
.placeholder-pin {
  font-size: 0.78em;
  color: var(--steel-deep, #5a6b7a);
  white-space: nowrap;
  margin-top: 0.15rem;
}
.dup-badge { font-size: 0.7em; color: var(--gilt-deep, #8a6d3b); border: 1px solid currentColor; border-radius: 3px; padding: 0 0.25em; margin-left: 0.4em; }
.placeholder-detail-line { font-style: italic; }
.placeholder-detail-page { font-size: 0.85em; color: var(--steel-deep, #5a6b7a); }

/* Re-source ("Edit source") picker */
.sp-open-btn { font: inherit; padding: 0.4rem 0.8rem; border: 1px solid var(--line, #ccc); border-radius: 6px; background: transparent; cursor: pointer; }
.sp-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.35); opacity: 0; pointer-events: none; transition: opacity 0.15s; z-index: 200; }
.sp-backdrop.open { opacity: 1; pointer-events: auto; }
.sp-modal { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%) scale(0.98); width: min(680px, 92vw); max-height: 82vh; overflow: auto; background: var(--panel, #fff); border-radius: 10px; padding: 1rem; opacity: 0; pointer-events: none; transition: opacity 0.15s, transform 0.15s; z-index: 201; }
.sp-modal.open { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
.sp-filter { width: 100%; margin-bottom: 0.6rem; }
.sp-chapters { display: flex; flex-direction: column; gap: 2px; max-height: 60vh; overflow: auto; }
.sp-chapter { text-align: left; padding: 0.35rem 0.5rem; border: none; background: transparent; cursor: pointer; border-radius: 4px; font: inherit; }
.sp-chapter:hover { background: rgba(0,0,0,0.06); }
.sp-current { font-weight: 600; background: rgba(0,0,0,0.08); }
.sp-hint, .sp-note, .sp-err { font-size: 0.85rem; opacity: 0.8; }
.sp-err { color: #b00; }
.sp-paras { display: flex; flex-direction: column; gap: 4px; max-height: 55vh; overflow: auto; }
.sp-para { text-align: left; padding: 0.4rem 0.5rem; border: 1px solid transparent; border-radius: 4px; background: transparent; cursor: pointer; font: inherit; }
.sp-para:hover { border-color: var(--line, #ccc); }
.sp-para-sel { background: rgba(80,140,90,0.18); border-color: rgba(80,140,90,0.5); }
.sp-actions, .sp-stage1 > .fe-cancel { margin-top: 0.6rem; }
.sp-back { border: none; background: transparent; cursor: pointer; font: inherit; margin-bottom: 0.5rem; }

/* ── Settings-legend (now pinned at the top of the RIGHT cork board) ───────
   The legend is rendered by corkboard.rs inside .cork-right (it no longer
   hangs free in the margin — see 2026-07-02-contributor-corkboard-design.md,
   which supersedes the standalone pegboard). It scrolls internally so the
   long Weight paragraph never overflows a short board. Width gating comes
   from the same 1340px media query as the boards. */
.settings-legend { display: none; }   /* default hidden; the gate below shows it */

@media (min-width: 1340px) {
    .settings-legend {
        display: block;
        position: absolute;            /* within .cork-right */
        top: 14px;                     /* ＋ Note now lives on the LEFT board */
        left: 14px;
        right: 14px;
        max-height: calc(60vh - var(--topbar-h, 48px));
        overflow-y: auto;
        z-index: 2;
        padding: 14px 14px 16px;
        background: var(--canvas);
        border: 1px solid var(--rule);
        border-radius: 6px;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
        font-family: var(--serif-display, Georgia, serif);
        color: var(--ink);
    }
    .settings-legend-title {
        font-size: 12px;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        color: var(--ink-faint);
        border-bottom: 1px solid var(--rule);
        padding-bottom: 8px;
        margin-bottom: 10px;
    }
    .settings-legend-row { margin-bottom: 11px; }
    .settings-legend-row:last-child { margin-bottom: 0; }
    .settings-legend-field {
        font-size: 13px;
        font-weight: 600;
        color: var(--ink);
        margin-bottom: 2px;
    }
    .settings-legend-body {
        font-size: 12px;
        line-height: 1.5;
        color: var(--ink-soft, var(--ink));
    }
    /* Weight's fuller paragraph — a hair more separation so the long block reads
       as its own unit. */
    .settings-legend-row-wide { margin-top: 4px; margin-bottom: 13px; }
}

/* ── Contributor cork-boards + post-it notes (desktop, edit-mode) ──────────
   Two cork boards fill the dead margins outside the 800px column (component:
   reader/src/reader/corkboard.rs; data: D1 corkboard_notes, shared across
   contributors). Rendered only in edit mode + Reader lens (component gates);
   this media query is the width gate — no board below 1340px (that gate is
   in CSS px: browser ZOOM shrinks the CSS viewport, so zooming in far
   enough hides the boards by design). Notes are absolutely-positioned
   CHILDREN of their board, placed in percentages of the board (inline style
   from corkboard.rs) — the mockup's model; no viewport math anywhere.
   Design source: drafts/corkboard-mockup.html (gitignored). */
.corkboard { display: none; }

@media (min-width: 1340px) {
    .corkboard { display: block; }

    .cork {
        position: fixed;
        top: var(--topbar-h);
        bottom: 0;
        z-index: 6;                    /* below scrubber(9)/topbar(20) */
        /* visible: notes are CHILDREN of the board and may overhang the
           frame slightly (and ride over the column mid-drag) */
        overflow: visible;
        /* own compositor layer — the big gradient texture occasionally
           failed to repaint without it (right board showed bare) */
        will-change: transform;
        /* wood-plank texture: vertical seams every ~120px (plank joins) plus
           two offset layers of wavy grain lines (irregular spacing/angle so
           it doesn't read as a repeating pattern), over a warm base with a
           soft diagonal sheen. */
        background-color: var(--cork);
        background-image:
            repeating-linear-gradient(90deg,
                rgba(0,0,0,.22) 0 2px, transparent 2px 120px),
            repeating-linear-gradient(2deg,
                rgba(0,0,0,.09) 0 1px, transparent 1px 7px,
                rgba(0,0,0,.05) 7px 8px, transparent 8px 15px,
                rgba(0,0,0,.07) 15px 16px, transparent 16px 26px),
            repeating-linear-gradient(-1.4deg,
                rgba(255,255,255,.045) 0 1px, transparent 1px 19px,
                rgba(0,0,0,.04) 19px 20px, transparent 20px 31px),
            linear-gradient(100deg, rgba(255,255,255,.06) 0%, transparent 40%),
            linear-gradient(145deg, var(--cork) 0%, var(--cork-dark) 100%);
        box-shadow: inset 0 0 40px rgba(0,0,0,.25), inset 0 2px 4px rgba(0,0,0,.3);
        border: 6px solid var(--cork-frame);   /* wooden frame edge */
        border-radius: 3px;
    }
    .cork-left  { left: 0;  width: calc(50% - 400px); }
    .cork-right { right: 0; width: calc(50% - 400px); }

    .add-note {
        position: absolute;
        top: 12px;
        right: 12px;
        z-index: 3;
        font: inherit;
        font-size: 12px;
        letter-spacing: .03em;
        background: rgba(0,0,0,.28);
        color: #f4ead6;
        border: 1px solid rgba(0,0,0,.35);
        border-radius: 6px;
        padding: 5px 11px;
        cursor: pointer;
        box-shadow: 0 1px 2px rgba(0,0,0,.3);
    }
    .add-note:hover { background: rgba(0,0,0,.42); }

    /* ── Post-it note ── a CHILD of its board: inline percentages of the
       board (percent_style in corkboard.rs); these px min/max keep the
       rendered size readable regardless of board size. */
    .cork-note {
        position: absolute;
        z-index: 7;                    /* above the board texture */
        min-width: 110px;
        min-height: 90px;
        max-width: 340px;
        max-height: 420px;
        padding: 20px 12px 12px;       /* top room for the corner tab */
        font-family: var(--serif-body);
        font-size: 13px;
        line-height: 1.45;
        color: var(--sticky-ink, var(--sticky-yellow-ink));
        background: var(--sticky-bg, var(--sticky-yellow));
        box-shadow: 0 4px 10px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.25);
        border-radius: 2px;
        transform: rotate(var(--rot, -2deg));
        transition: box-shadow .12s ease;
        /* Native resize grip, bottom-right. overflow:hidden clips the grip to
           the rounded corner; the settled size is saved via ResizeObserver. */
        resize: both;
        overflow: hidden;
        display: flex;
        flex-direction: column;
    }
    .cork-note.dragging {
        box-shadow: 0 14px 26px rgba(0,0,0,.5);
        z-index: 40;                   /* over the column mid-drag is fine */
        transition: none;
    }

    /* corner tab = drag handle (folded-corner look) with a red pin */
    .cn-tab {
        position: absolute;
        top: 0; left: 0; right: 0;
        height: 16px;
        cursor: grab;
        border-radius: 2px 2px 0 0;
        background: linear-gradient(to bottom, rgba(0,0,0,.14), rgba(0,0,0,0));
        touch-action: none;            /* pointer events own the gesture */
        user-select: none;
        -webkit-user-select: none;
    }
    .cork-note.dragging .cn-tab { cursor: grabbing; }
    .cn-tab::before {
        content: "";
        position: absolute;
        top: 3px; left: 50%;
        transform: translateX(-50%);
        width: 8px; height: 8px;
        border-radius: 50%;
        background: radial-gradient(circle at 35% 30%, #fff6, var(--oxblood) 60%, var(--oxblood-deep));
        box-shadow: 0 1px 2px rgba(0,0,0,.4);
    }

    .cn-body { outline: none; flex: 1 1 auto; overflow: auto; }
    .cn-body:empty::before { content: "note…"; color: rgba(0,0,0,.35); }

    .cn-close {
        position: absolute;
        top: 2px; right: 4px;
        border: none; background: none; cursor: pointer;
        color: rgba(0,0,0,.4);
        font-size: 14px; line-height: 1; padding: 2px;
        opacity: 0;
        transition: opacity .12s ease;
    }
    .cork-note:hover .cn-close,
    .cork-note:focus-within .cn-close { opacity: 1; }
    .cn-close:hover { color: rgba(0,0,0,.8); }

    /* swatch palette — hidden at rest (height collapses: no empty gap);
       revealed on hover/focus-within. Pinned to the bottom as the note grows. */
    .cn-swatches {
        display: flex;
        gap: 5px;
        height: 0;
        margin-top: auto;
        overflow: hidden;
        opacity: 0;
        pointer-events: none;
        transition: opacity .12s ease, height .14s ease;
    }
    .cork-note:hover .cn-swatches,
    .cork-note:focus-within .cn-swatches {
        height: 13px;
        opacity: 1;
        pointer-events: auto;
    }
    .cn-sw {
        width: 13px; height: 13px;
        border-radius: 50%;
        cursor: pointer;
        border: 1px solid rgba(0,0,0,.25);
    }
    .cn-sw.active { outline: 2px solid rgba(0,0,0,.5); outline-offset: 1px; }
    .cn-sw-theme  { background: var(--sticky-theme-bg); }
    .cn-sw-yellow { background: var(--sticky-yellow); }
    .cn-sw-pink   { background: var(--sticky-pink); }
    .cn-sw-blue   { background: var(--sticky-blue); }
    .cn-sw-green  { background: var(--sticky-green); }

    /* color variants (class set reactively in Rust: cn-<color>) */
    .cork-note.cn-theme {
        --sticky-bg:  var(--sticky-theme-bg);
        --sticky-ink: var(--sticky-theme-ink);
        border: 1px solid var(--rule);
    }
    .cork-note.cn-yellow { --sticky-bg: var(--sticky-yellow); --sticky-ink: var(--sticky-yellow-ink); }
    .cork-note.cn-pink   { --sticky-bg: var(--sticky-pink);   --sticky-ink: var(--sticky-pink-ink); }
    .cork-note.cn-blue   { --sticky-bg: var(--sticky-blue);   --sticky-ink: var(--sticky-blue-ink); }
    .cork-note.cn-green  { --sticky-bg: var(--sticky-green);  --sticky-ink: var(--sticky-green-ink); }
    /* theme note: dark-alpha affordances are invisible on a dark card — use ink vars */
    .cork-note.cn-theme .cn-body:empty::before { color: var(--ink-faint); }
    .cork-note.cn-theme .cn-close { color: var(--ink-faint); }
    .cork-note.cn-theme .cn-close:hover { color: var(--ink); }
}
/* ^ closes @media (min-width: 1340px) — the corkboard width gate. The
   chronicle section below was once appended INSIDE that block (its closer
   drifted to EOF), scoping every .chron-* rule to ≥1340px and leaving
   mobile completely unstyled. styles_lint.rs now guards this. */

/* ── Chronicle reader (/chronicle) ─────────────────────────────── */

.chron-page {
    display: grid;
    grid-template-columns: 280px minmax(0, 1fr) 260px;
    gap: 28px;
    max-width: 1280px;
    margin: 0 auto;
    padding: calc(var(--topbar-h) + 20px) 20px 60px;
    align-items: start;
}

/* Sidebars: sticky, own scroll. */
.chron-toc, .chron-rail {
    position: sticky;
    top: calc(var(--topbar-h) + 16px);
    max-height: calc(100vh - var(--topbar-h) - 32px);
    overflow-y: auto;
    background: var(--canvas);
    border: 1px solid var(--rule-soft);
    border-radius: 10px;
    padding: 14px;
}

/* Text-size strip at the top of the TOC — two half-width steppers mirroring
   the volume tabs below, kept clearly apart from the contents by their own
   bottom rule (the stronger --rule, not --rule-soft) and breathing room. */
.chron-fontsize {
    display: flex; align-items: center; gap: 6px;
    padding-bottom: 12px; margin-bottom: 12px;
    border-bottom: 1px solid var(--rule);
}
/* Current size (px) between the steppers. */
.chron-fs-val {
    min-width: 2ch; text-align: center;
    font-size: 0.72rem; color: var(--ink-dim);
    font-feature-settings: "lnum";
}
.chron-fs-btn {
    flex: 1; height: 28px;
    display: flex; align-items: center; justify-content: center;
    background: none; border: 1px solid var(--rule-soft); border-radius: 6px;
    color: var(--ink-soft); cursor: pointer; line-height: 1;
}
.chron-fs-btn:hover:enabled { border-color: var(--gilt); color: var(--ink); }
.chron-fs-btn:disabled { opacity: 0.35; cursor: default; }
/* The two "A" glyphs telegraph direction by size. */
.chron-fs-down { font-size: 0.72rem; }
.chron-fs-up { font-size: 1.05rem; }

.chron-toc-tabs { display: flex; gap: 6px; margin-bottom: 10px; }
.chron-toc-tab {
    flex: 1; padding: 6px 4px; font-size: 0.72rem;
    background: none; border: 1px solid var(--rule-soft); border-radius: 6px;
    color: var(--ink-soft); cursor: pointer;
}
.chron-toc-tab.active { border-color: var(--gilt); color: var(--ink); }

.chron-toc-year {
    font-size: 0.68rem; letter-spacing: 0.08em; color: var(--ink-dim);
    margin: 10px 0 2px; text-transform: uppercase;
}
.chron-toc-row {
    display: block; width: 100%; text-align: left;
    padding: 5px 8px; border: 0; border-radius: 6px;
    background: none; color: var(--ink-soft);
    font-size: 0.82rem; line-height: 1.3; cursor: pointer;
}
.chron-toc-row:hover { background: var(--rule-soft); }
.chron-toc-row.current { background: var(--gilt); color: var(--page-bg); }
.chron-toc-row.marked::after { content: " §"; color: var(--gilt); }
.chron-toc-row.current.marked::after { color: var(--page-bg); }
/* Audio indicator: a small gilt speaker glyph before the title on chapters
   with an approved narration. Inline so it sits on the first text line and
   nudges the title over slightly; opacity keeps it a quiet cue, not a shout.
   On the current (gilt-filled) row it flips to the page background to stay
   legible against the gilt. */
.chron-toc-audio {
    display: inline-block; vertical-align: -1px; margin-right: 5px;
    color: var(--gilt); opacity: 0.9;
}
.chron-toc-row.current .chron-toc-audio { color: var(--page-bg); opacity: 1; }
.chron-toc-empty, .chron-loading { color: var(--ink-dim); padding: 12px; }
.chron-toc-toggle { display: none; }

/* Tap-outside dismissal for the mobile TOC drawer (bm-backdrop pattern:
   always in the DOM, toggled by class — never <Show>). Dim layer between
   the page and the drawer; hidden at every width by default, displayed
   only by the ≤599px drawer block below (desktop's TOC is a grid column,
   not an overlay, so it never gets a backdrop). */
.chron-toc-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    z-index: 40; /* over content + Contents button (40, earlier in DOM), under the drawer (41) */
    background: rgba(0,0,0,0.28);
}

/* TOC grab-tab — the timeline scale-tab pattern mirrored to the LEFT panel:
   fused to the TOC's right edge while expanded, rides to the screen's left
   edge when collapsed (the iPad ask: free the contents track for the text).
   A FIXED sibling of the panel it controls — never a child — so it stays
   tappable when the panel hides. Reuses .bar-grab-handle for the line.
   Base `left` = the 900–1339 band (20px padding + 280px track, centered
   above 1320 via the max() term); other bands override below. */
.chron-toc-grab {
    position: fixed;
    z-index: 21;
    top: calc(var(--topbar-h) + 24px);
    left: calc(max(0px, 50% - 640px) + 300px);
    background: var(--canvas);
    border: none;
    border-right: 1px solid var(--rule);
    border-radius: 0 8px 8px 0;
    padding: 12px 5px 12px 3px;   /* shorter left: flush onto the panel */
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    touch-action: manipulation;
    transition: left .25s ease;
}
.chron-toc-grab .bar-grab-handle { width: 3px; height: 22px; }
/* Enlarge the TAP zone without enlarging the visible tab (scale-tab trick). */
.chron-toc-grab::before {
    content: "";
    position: absolute;
    inset: -14px -22px -14px -6px;
}

/* Collapsed: hide the panel, free its grid track, park the tab at the screen
   edge. Scoped ≥600px — below that the TOC is the bottom drawer and a stale
   collapsed flag must never take it out. */
@media (min-width: 600px) {
    .chron-page.toc-collapsed .chron-toc { display: none; }
    .chron-page.toc-collapsed { grid-template-columns: minmax(0, 1fr) 260px; }
    .chron-page.toc-collapsed .chron-toc-grab { left: 0; }
}
/* 600–899 (rail already hidden): collapsing the TOC leaves one column. */
@media (min-width: 600px) and (max-width: 899px) {
    .chron-page.toc-collapsed { grid-template-columns: minmax(0, 1fr); }
}

/* Reading column. Fills its grid track (TOC edge → rail edge): the user
   preferred long lines over the classic ~68ch book measure. */
.chron-col { overflow-anchor: auto; }
.chron-prev, .chron-next {
    display: block; margin: 0 0 18px;
    background: none; border: 1px solid var(--rule-soft); border-radius: 8px;
    padding: 8px 14px; color: var(--ink-soft); cursor: pointer;
}
/* CSS-hide, never <Show>-unmount — a nav button may hide itself from its own
   click handler at the ends of the book (crash-family rule; see the note in
   the chronicle page module). */
.chron-prev.hidden, .chron-next.hidden, .chron-nav.hidden { display: none; }
/* Prev/next pairs above and below the chapter (one chapter shown at a time;
   these page through). */
.chron-nav {
    display: flex; justify-content: space-between; gap: 12px;
    margin: 34px 0 0;
}
.chron-nav.chron-nav-top { margin: 0 0 18px; }
.chron-nav .chron-prev, .chron-nav .chron-next { margin: 0; }
/* Keep Next on the right edge even when Prev is hidden (first chapter). */
.chron-nav .chron-next { margin-left: auto; }
.chron-chapter { margin-bottom: 44px; scroll-margin-top: calc(var(--topbar-h) + 12px); }
.chron-ch-head { display: flex; align-items: baseline; gap: 10px; }
.chron-ch-title {
    /* Match the timeline card headline (18px uppercase, heavier osr cut,
       medium weight) — the 1.9rem light chapter title read oversized against
       the 17px body. Scales with the text-size setting, keeping the 18:17
       card-headline proportion to the body (18px at the 17px default). */
    font-size: calc(var(--chron-fs, 17px) * 18 / 17);
    letter-spacing: 0.01em;
    text-transform: uppercase;
    line-height: 1.18;
    font-family: "osr", "osl", Georgia, serif;
    font-weight: 500;
    margin: 0 0 6px;
    color: var(--ink);
}
.chron-bm {
    background: none; border: 0; cursor: pointer; padding: 4px;
    color: var(--ink-faint); flex-shrink: 0;
}
.chron-bm svg { width: 14px; height: 18px; fill: none; }
.chron-bm:hover { color: var(--gilt); }
.chron-bm.marked { color: var(--gilt); }
.chron-bm.marked svg { fill: currentColor; }
.chron-ch-meta {
    font-size: 0.78rem; color: var(--ink-dim);
    border-bottom: 1px solid var(--rule-soft);
    padding-bottom: 10px; margin-bottom: 14px;
}
.chron-ch-body p {
    /* Lighter display cut (osl) — the regular osr body face read heavier/
       denser here than the same prose on the timeline cards. Size is reader-
       adjustable via the TOC's text-size strip (--chron-fs is set inline on
       .chron-page; the 17px fallback = the cards' body size). */
    font-family: var(--serif-display);
    font-size: var(--chron-fs, 17px); line-height: 1.75;
    margin: 0 0 1em; color: var(--ink);
    /* Search jumps scroll a hit paragraph to block:start — reserve the fixed
       topbar (same treatment as .chron-chapter / the reader's cards). The
       head gets it too, for chapter-header hits. */
    scroll-margin-top: calc(var(--topbar-h) + 12px);
}
.chron-ch-head { scroll-margin-top: calc(var(--topbar-h) + 12px); }
.chron-ch-pending { color: var(--ink-dim); }
.chron-attr {
    margin-top: 40px; padding-top: 12px;
    border-top: 1px solid var(--rule-soft);
    font-size: 0.75rem; color: var(--ink-dim);
}

/* Chronicle narration player — a compact, engraved strip between the chapter
   head and the body, keyed to the page rules so it reads as part of the plate,
   not a floating media widget. Only renders for chapters with approved audio;
   see pages/chronicle/audio.rs. */
.chron-audio {
    display: grid;
    /* Scrubber · Play/Pause · time — the Play button sits to the RIGHT of the
       scrubber, beside the readout (markup order matches). */
    grid-template-columns: minmax(120px, 1fr) auto auto;
    align-items: center;
    gap: 12px;
    margin: 4px 0 20px;
    padding: 12px 0;
    border-top: 1px solid var(--rule-soft);
    border-bottom: 1px solid var(--rule-soft);
}
/* Voice picker: a full-width row above the transport (chapters with >1 take). */
.chron-audio-voices {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
}
.chron-audio-voices-lbl {
    font-size: 0.68rem; letter-spacing: 0.09em; text-transform: uppercase;
    color: var(--ink-dim);
}
.chron-voice-btn {
    padding: 3px 11px;
    font-family: "osr", "osl", Georgia, serif;
    font-size: 0.8rem;
    color: var(--ink-dim);
    background: transparent;
    border: 1px solid var(--rule-soft);
    border-radius: 6px;  /* unified audio-button shape: rounded rectangle */
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
}
.chron-voice-btn:hover { color: var(--gilt); border-color: var(--gilt); }
.chron-voice-btn.sel {
    color: var(--page-bg);
    background: var(--gilt);
    border-color: var(--gilt);
}
.chron-audio-btn {
    min-width: 4.6rem;
    padding: 5px 14px;
    font-family: "osr", "osl", Georgia, serif;
    font-size: 0.95rem;  /* larger "Play"/"Pause" wording */
    color: var(--ink);
    background: var(--rule-soft);
    border: 1px solid var(--rule);
    border-radius: 6px;  /* unified audio-button shape: rounded rectangle (was 2px, too square) */
    cursor: pointer;
}
.chron-audio-btn:hover { color: var(--gilt); border-color: var(--gilt); }
.chron-audio-scrub {
    width: 100%;
    accent-color: var(--gilt);
    cursor: pointer;
}
.chron-audio-time {
    font-size: 0.8rem; color: var(--ink-dim);
    white-space: nowrap; font-variant-numeric: tabular-nums;
}
.chron-audio-error {
    grid-column: 1 / -1;
    font-size: 0.8rem; color: var(--ink-dim);
}
/* Failed load: dim the whole strip so it reads as unavailable, not broken. */
.chron-audio.error .chron-audio-btn,
.chron-audio.error .chron-audio-scrub { opacity: 0.45; pointer-events: none; }
@media (max-width: 520px) {
    /* Original two-row mobile layout, with Play now on the RIGHT of the scrubber:
       row 1 = scrubber (1fr) · Play (auto, compact — does NOT stretch); row 2 =
       time, full-width. Columns are scrubber|play, so the button keeps its
       natural width instead of filling a 1fr cell. */
    .chron-audio { grid-template-columns: 1fr auto; }
    .chron-audio-time { grid-column: 1 / -1; }
}

/* Floating transport bar — TOP-docked (below the fixed topbar), shown only while
   narration is active AND the head strip has scrolled away (audio.rs gates the
   render, so it appears exactly when the head Play button leaves the screen).
   Drives the same <audio> element as the head strip. Docked at the top rather
   than the bottom so it clears the iPhone bottom-edge swipe-up gesture. Solid
   --page-bg (NOT the never-defined --paper it used before, which rendered
   transparent and read as floating "in" the text). */
.chron-audio-float {
    position: fixed;
    left: 50%;
    /* Just below the fixed topbar (single source of truth --topbar-h) + its
       safe-area inset, with an 8px gap. */
    top: calc(var(--topbar-h) + env(safe-area-inset-top, 0px) + 8px);
    transform: translateX(-50%);
    /* Above the content, just BELOW the topbar (z 20) so a topbar dropdown
       (TOC, bookmarks) that expands downward covers the bar rather than
       peeking under it. */
    z-index: 19;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 7px 12px;
    background: var(--page-bg);
    border: 1px solid var(--rule);
    border-radius: 10px;  /* rounded rectangle: a touch larger than its 6px buttons */
    box-shadow: 0 4px 18px rgba(0, 0, 0, 0.28);
    /* Never wider than the viewport on small screens. */
    max-width: calc(100vw - 24px);
}
.chron-float-btn {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-family: "osr", "osl", Georgia, serif;
    font-size: 0.78rem;
    line-height: 1;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--rule-soft);
    border-radius: 6px;  /* unified audio-button shape: rounded rectangle */
    /* Snug h-padding so the skip buttons (icon + "15") stay compact next to the
       wider play button. Play overrides padding, so this only tightens the skips. */
    padding: 6px 8px;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s ease, border-color 0.15s ease;
}
.chron-float-btn:hover { color: var(--gilt); border-color: var(--gilt); }
/* Skip/play icons: flat-filled currentColor glyphs (the topbar icon idiom). */
.chron-float-ico {
    width: 17px; height: 17px;
    fill: currentColor;
    display: block;
}
.chron-float-num {
    font-size: 0.72rem; font-variant-numeric: tabular-nums;
}
/* Play/pause is the primary control — a filled gilt button so the eye lands on
   it. Wider (min-width + more h-padding) so its lone icon isn't pinched next to
   the "15" skip buttons. Its glyph is ZOOMED (22px vs the skips' 17px — the app's
   larger-primary-glyph style); vertical padding drops to 4px to absorb the taller
   icon so all three buttons stay the same height. */
.chron-float-play {
    color: var(--page-bg);
    background: var(--gilt);
    border-color: var(--gilt);
    padding: 2px 18px;
    min-width: 3.2rem;
    justify-content: center;
}
.chron-float-play .chron-float-ico {
    width: 26px; height: 26px;
}
.chron-float-play:hover { color: var(--page-bg); opacity: 0.9; }
.chron-float-time {
    font-size: 0.78rem; color: var(--ink-dim);
    white-space: nowrap; font-variant-numeric: tabular-nums;
    padding-left: 2px;
}
/* Tap-to-seek affordance: while narration is armed (started), sentences take a
   pointer cursor so the listener sees they can tap to jump there. Off while just
   reading, so plain prose stays normal text. */
.chron-ch-body span.seekable { cursor: pointer; }

/* Read-along highlight: the SENTENCE being narrated. An inline gilt wash on the
   active sentence's <span>; box-decoration-break:clone so a sentence wrapping
   across lines is washed on every line, not just the first. Restrained, with a
   gentle fade as it moves sentence to sentence. */
.chron-ch-body .reading {
    background: color-mix(in srgb, var(--gilt) 16%, transparent);
    border-radius: 2px;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
    transition: background-color 0.25s ease;
}
/* Dark canvas is near-black, so the same 16% wash barely lifts luminance —
   boost the mix (and lean on the lighter gilt) so the highlight reads as
   clearly as it does on light paper. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .chron-ch-body .reading {
        background: color-mix(in srgb, var(--gilt) 38%, transparent);
    }
}
:root[data-theme="dark"] .chron-ch-body .reading {
    background: color-mix(in srgb, var(--gilt) 38%, transparent);
}

/* Scriptures rail. */
.chron-rail-head {
    font-size: 0.7rem; letter-spacing: 0.1em; text-transform: uppercase;
    color: var(--ink-soft); margin-bottom: 6px;
}
.chron-rail-sub { font-size: 0.74rem; color: var(--ink-dim); margin-bottom: 10px; }
.chron-rail-list { list-style: none; margin: 0; padding: 0; }
.chron-rail-list li {
    padding: 5px 0; font-size: 0.85rem; color: var(--ink-soft);
    border-bottom: 1px solid var(--rule-soft);
}
/* Spec: the rail renders NOTHING when there are no refs (Vol II, undated,
   or a Vol I year with none) — hide the whole panel, don't show an empty box. */
.chron-rail.empty { display: none; }
/* And RELEASE its 260px grid track too — a hidden rail must not reserve a
   dead right gutter (scripture refs exist for only ~26 of Vol I's years, so
   most of the book reads with no rail). Scoped to the mid-width band where
   the rail still lives in the grid; ≥1340px it floats in the margin below. */
@media (min-width: 900px) and (max-width: 1339px) {
    .chron-page:has(.chron-rail.empty) { grid-template-columns: 280px minmax(0, 1fr); }
}

/* ≥1340px: the reading text occupies the app's 800px centered column — the
   SAME span as the topbar (left/right = 50% ∓ 400px), so the text's right
   edge ends exactly where the bar ends (the user's red line). TOC and rail
   float in the side margins hugging the column edges (the corkboard-margins
   pattern; 1340px is the same proven margin-width gate). Out-of-flow rail
   bonus: line length no longer shifts when a year has no refs. */
@media (min-width: 1340px) {
    .chron-page {
        display: block;
        max-width: 800px;
        margin: 0 auto;
        padding-left: 0;
        padding-right: 0;
    }
    .chron-toc {
        position: fixed;
        left: auto;
        right: calc(50% + 400px + 24px);   /* hug the column's left edge */
        width: min(300px, calc(50% - 400px - 44px));
        top: calc(var(--topbar-h) + 16px);
        max-height: calc(100vh - var(--topbar-h) - 32px);
    }
    /* Fuse the grab-tab to the floating panel's right edge (which sits at
       50% − 424px; see the .chron-toc right offset above). */
    .chron-toc-grab { left: calc(50% - 424px); }
    .chron-page.toc-collapsed .chron-toc-grab { left: 0; }
    .chron-rail {
        position: fixed;
        left: calc(50% + 400px + 24px);    /* hug the column's right edge */
        width: min(280px, calc(50% - 400px - 44px));
        top: calc(var(--topbar-h) + 16px);
        max-height: calc(100vh - var(--topbar-h) - 32px);
    }
}

/* ── Collapse is a TOUCH affordance — disable it on desktop (≥900px). ─────────
   The chip/scale bars already made this call (see the @media(min-width:760px)
   block above): a wide, width-capped, CENTERED layout gains nothing from hiding
   a side panel — the reading column can't widen into the freed space, so a
   collapse only strands the grab tab in the margin gutter at the screen edge
   (the reported "tucks away to the far left" bug; especially so ≥1340px, where
   the text is a fixed 800px column and the TOC floats in the margin).

   So above 900px we make .toc-collapsed a NO-OP and remove the grab entirely.
   This also neutralizes a PERSISTED collapsed flag (chron_toc_collapsed_v1)
   without touching localStorage or Rust — a state saved on a phone/iPad can no
   longer strand the desktop view. Collapse still works 600–899px (the iPad
   band) and the TOC is still a drawer ≤599px. Selectors mirror the collapse
   rules they revert, so they tie on specificity and win by later source order;
   the grab-hide is flagged to beat the per-band `left` rules unconditionally. */
@media (min-width: 900px) {
    /* No grab tab on desktop — nothing to strand, nothing to mis-tap. */
    .chron-toc-grab { display: none !important; }
    /* A stale collapsed flag can't hide the panel here. */
    .chron-page.toc-collapsed .chron-toc { display: block; }
}
/* Grid band (900–1339): restore the TOC's grid track a collapse had dropped. */
@media (min-width: 900px) and (max-width: 1339px) {
    .chron-page.toc-collapsed { grid-template-columns: 280px minmax(0, 1fr) 260px; }
    /* Keep the rail-empty variant honest (matches the base :has() rule above). */
    .chron-page.toc-collapsed:has(.chron-rail.empty) {
        grid-template-columns: 280px minmax(0, 1fr);
    }
}
/* ≤899px: drop the rail. */
@media (max-width: 899px) {
    .chron-page { grid-template-columns: 240px minmax(0, 1fr); }
    .chron-rail { display: none; }
    /* Narrower 240px track + 20px page padding. */
    .chron-toc-grab { left: 260px; }
    .chron-page.toc-collapsed .chron-toc-grab { left: 0; }
}

/* ≤599px (mobile): TOC becomes a drawer behind the Contents button. */
@media (max-width: 599px) {
    .chron-page { grid-template-columns: minmax(0, 1fr); padding-top: calc(var(--topbar-h) + 12px); }
    /* The TOC is the bottom drawer here; the collapse tab has no job. */
    .chron-toc-grab { display: none; }
    .chron-toc-toggle {
        display: block; position: fixed;
        bottom: 18px; right: 18px; z-index: 40;
        background: var(--canvas); color: var(--ink);
        border: 1px solid var(--rule); border-radius: 999px;
        padding: 10px 18px; box-shadow: 0 4px 14px rgba(0,0,0,0.25);
        cursor: pointer;
    }
    .chron-toc {
        position: fixed; inset: auto 0 0 0; top: 30vh; z-index: 41;
        border-radius: 14px 14px 0 0;
        transform: translateY(105%);
        transition: transform 0.25s ease;
        max-height: 70vh;
    }
    .chron-toc.open { transform: translateY(0); }
    .chron-toc-backdrop.open { display: block; }
}

/* Bookmarks dropdown: the "Chronicle" era-head hides via class (never <Show>). */
.bm-hidden { display: none; }

/* Event-detail excerpt: the read-in-context link, styled to belong to the
   quote panel (default browser blue clashes with the excerpt palette). */
.quote-context-link {
    display: inline-block; margin-top: 8px;
    font-size: 0.8rem; color: var(--gilt);
    text-decoration: none;
}
.quote-context-link:hover { text-decoration: underline; }

/* ── Stream windowing ─────────────────────────────────────────────────
   Spacers stand in for unmounted item runs (heights inline, estimated);
   .stream-item is a layout-neutral block wrapper carrying data-item-idx
   for measurement + pending-jump targeting. Cards inside keep their own
   margins — the wrapper adds none. */
.stream-item { display: block; }
.stream-spacer { display: block; width: 100%; }

/* ── Toast + session-expired banner ─────────────────────────────────── */
.toast {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  z-index: 300; max-width: min(92vw, 520px);
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border-radius: 10px;
  background: var(--canvas); color: var(--ink);
  border: 1px solid rgba(128,128,128,.35);
  box-shadow: 0 6px 24px rgba(0,0,0,.3); font-size: 14px;
}
.toast-error { border-left: 4px solid #d9534f; }
.toast-text { flex: 1; }
.toast-dismiss {
  background: none; border: none; color: inherit; cursor: pointer;
  font-size: 16px; line-height: 1; padding: 2px 4px;
}
.session-expired-banner {
  position: sticky; top: 0; z-index: 250;
  padding: 10px 16px; text-align: center;
  background: #d9534f; color: #fff; font-size: 14px;
}
.session-expired-banner a { color: #fff; text-decoration: underline; }

/* ── Map lens error overlay ─────────────────────────────────────────── */
.map-error {
  position: absolute; inset: 0; z-index: 1100;
  display: flex; align-items: center; justify-content: center;
  padding: 24px; text-align: center;
  background: var(--canvas); color: var(--ink);
  font-size: 15px;
}

/* ── Map raid layer (strikes, burns, badges) ────────────────────────── */
/* Strike line: dashed oxblood over the basemap (drawn by Leaflet's SVG
   renderer; className lands on the <path>). */
.raid-line { stroke-dasharray: 6 5; }

/* Riding/parked faction badge (24px divIcon). Pointer events stay ON for the
   parked (tappable) badge; the in-flight badge is created non-interactive. */
.raid-badge { filter: drop-shadow(0 1px 2px rgba(0,0,0,.35)); }

/* Origin tag: faction name (approx origins) or the named from-place. */
.raid-origin-lbl {
  font-family: Georgia, serif; font-style: italic; font-size: 10px;
  color: #a82828; text-align: center; white-space: nowrap;
  text-shadow: 0 1px 2px rgba(255,255,255,.75);
}

/* The Live result stack (inside one divIcon at the target). */
.raid-burn { position: relative; display: block; width: 100%; height: 100%; }
.raid-glow {
  position: absolute; left: 50%; bottom: -6px; width: 54px; height: 54px;
  transform: translate(-50%, 50%); border-radius: 50%;
  background: radial-gradient(circle, rgba(226,88,34,.4), rgba(226,88,34,0) 70%);
  animation: raid-glow-bloom .9s ease-out backwards,
             raid-glow-pulse 1.6s ease-in-out .9s infinite alternate;
}
@keyframes raid-glow-bloom {
  from { transform: translate(-50%, 50%) scale(.35); opacity: 0; }
  to   { transform: translate(-50%, 50%) scale(1); opacity: 1; }
}
@keyframes raid-glow-pulse { from { opacity: .55; } to { opacity: 1; } }
.raid-ring {
  position: absolute; left: 50%; bottom: -6px; width: 26px; height: 26px;
  transform: translate(-50%, 50%) scale(.2); border-radius: 50%;
  border: 2px solid rgba(168,40,40,.85); opacity: 0;
  animation: raid-ring-pop .7s ease-out 1 forwards;
}
@keyframes raid-ring-pop {
  0% { transform: translate(-50%, 50%) scale(.2); opacity: .9; }
  100% { transform: translate(-50%, 50%) scale(2.4); opacity: 0; }
}
/* Settlement glyph (18×13) standing on the target latlng (stack bottom). */
.raid-stlmt {
  position: absolute; left: 50%; bottom: 0; width: 18px; height: 13px;
  transform: translateX(-50%);
  /* The plate's halo, so the settlement floats off the terrain exactly like the
     colony marks do (spec 2026-07-12). Replaces the old flat drop-shadow. */
  filter: drop-shadow(0 0 1px rgba(70,55,30,.9)) drop-shadow(0 0 6px rgba(247,242,232,.95));
}
.raid-stlmt svg { display: block; width: 100%; height: 100%; }
/* Charred copy fades in over the alive glyph while the fire holds (burned). */
.raid-char-overlay {
  position: absolute; inset: 0; opacity: 0;
  animation: raid-charring 2.5s ease-in 1.6s 1 forwards;
}
@keyframes raid-charring { to { opacity: 1; } }

/* — burned · B5 "Ember bloom": the glow swells FIRST (heat before flame),
   then the tongues grow out of it. NO impact flash — rejected. — */
/* Growers: the wrapper carries the staggered entrance (scaleY from the base),
   the inner flame carries the flicker — two elements so the two transform
   animations never fight. */
.raid-fgrow {
  position: absolute; left: 50%; bottom: 9px;
  transform: translateX(-50%) scaleY(.1); transform-origin: 50% 100%;
  opacity: 0; animation: raid-flame-grow .7s ease-out forwards;
}
.raid-g1 { margin-left: -8px; animation-delay: .5s; }
.raid-g2 { animation-delay: .7s; }
.raid-g3 { margin-left: 8px; animation-delay: .95s; }
@keyframes raid-flame-grow {
  to { opacity: 1; transform: translateX(-50%) scaleY(1); }
}
.raid-flame {
  position: relative; display: block; width: 9px; height: 13px;
  background: linear-gradient(to top, #c33d12, #e8722a 55%, #f7c860);
  border-radius: 50% 50% 46% 46% / 68% 68% 32% 32%;
  transform-origin: 50% 100%;
  animation: raid-flick .32s ease-in-out infinite alternate;
}
.raid-flame::after {
  content: ""; position: absolute; left: 2.5px; bottom: 1px;
  width: 4px; height: 7px;
  background: linear-gradient(to top, #f3a13c, #ffe9a8);
  border-radius: 50% 50% 46% 46% / 68% 68% 32% 32%;
}
.raid-f1 { width: 11px; height: 16px; }
.raid-f2 { width: 7px; height: 10px; animation-delay: .12s; }
.raid-f3 { width: 7px; height: 11px; animation-delay: .2s; }
@keyframes raid-flick {
  from { transform: scaleY(.88); }
  to   { transform: scaleY(1.08); }
}
.raid-spark {
  position: absolute; left: 50%; bottom: 20px; width: 3px; height: 3px;
  border-radius: 50%; background: #f7c860; opacity: 0;
  animation: raid-spark-rise 1.4s linear 1.2s infinite;
}
.raid-sp1 { margin-left: -5px; }
.raid-sp2 { margin-left: 4px; animation-duration: 1.7s; animation-delay: 1.8s; }
@keyframes raid-spark-rise {
  0%   { transform: translate(0, 0); opacity: 0; }
  12%  { opacity: .95; }
  100% { transform: translate(5px, -26px); opacity: 0; }
}
.raid-smoke span {
  position: absolute; left: 50%; bottom: 22px; width: 9px; height: 9px;
  transform: translateX(-50%); border-radius: 50%;
  background: rgba(96,90,82,.4); filter: blur(2px);
  animation: raid-rise 2.8s 1.2s infinite;
}
.raid-smoke span:nth-child(2) { animation-delay: 2.6s; }
@keyframes raid-rise {
  0%   { transform: translate(-50%, 0) scale(.55); opacity: 0; }
  12%  { opacity: .55; }
  100% { transform: translate(calc(-50% + 9px), -40px) scale(1.7); opacity: 0; }
}

/* — plundered · P1 "Slash & scatter": fireless; the settlement survives. — */
.raid-jolt { animation: raid-jolt .55s ease-out .1s 1; }
@keyframes raid-jolt {
  0%, 100% { transform: translateX(-50%); }
  15% { transform: translate(calc(-50% - 3px), 1px); }
  35% { transform: translate(calc(-50% + 3px), -1px); }
  55% { transform: translate(calc(-50% - 2px), 1px); }
  75% { transform: translate(calc(-50% + 2px), 0); }
}
.raid-slash {
  position: absolute; left: 50%; bottom: 8px; width: 30px; height: 2.5px;
  margin-left: -15px;
  background: linear-gradient(90deg, transparent, #f6f3e8 30%, #cfd6da 55%, transparent);
  opacity: 0; transform-origin: 50% 50%;
}
.raid-sl1 { animation: raid-slash1 .5s ease-out 1; }
.raid-sl2 { animation: raid-slash2 .5s ease-out .18s 1; }
@keyframes raid-slash1 {
  0%   { opacity: 0; transform: rotate(38deg) scaleX(.15); }
  40%  { opacity: 1; transform: rotate(38deg) scaleX(1); }
  100% { opacity: 0; transform: rotate(38deg) scaleX(1.1); }
}
@keyframes raid-slash2 {
  0%   { opacity: 0; transform: rotate(-42deg) scaleX(.15); }
  40%  { opacity: 1; transform: rotate(-42deg) scaleX(1); }
  100% { opacity: 0; transform: rotate(-42deg) scaleX(1.1); }
}
.raid-dust {
  position: absolute; left: 50%; bottom: 2px; width: 14px; height: 9px;
  margin-left: -7px; border-radius: 50%;
  background: rgba(150,138,112,.5); filter: blur(2px); opacity: 0;
  animation: raid-dust .9s ease-out .1s 1;
}
@keyframes raid-dust {
  0%   { opacity: 0; transform: scale(.4); }
  25%  { opacity: .6; }
  100% { opacity: 0; transform: translate(-6px, -8px) scale(1.9); }
}
/* Loot container is rotated inline to the away-bearing (toward the raiders'
   origin); flecks tumble along its +x axis. */
.raid-loots { position: absolute; left: 50%; bottom: 6px; width: 0; height: 0; }
.raid-loot {
  position: absolute; width: 3.5px; height: 3.5px; border-radius: 1px;
  background: #d8b04a; box-shadow: 0 0 0 .5px rgba(61,58,53,.4); opacity: 0;
}
.raid-lt1 { animation: raid-loot-tumble 1.6s ease-in .25s 1; }
.raid-lt2 { top: -3px; animation: raid-loot-tumble 1.6s ease-in .5s 1; }
.raid-lt3 { top: 2px; animation: raid-loot-tumble 1.6s ease-in .75s 1; }
.raid-lt4 { top: -1px; animation: raid-loot-tumble 1.6s ease-in 1s 1; }
@keyframes raid-loot-tumble {
  0%   { opacity: 0; transform: translateX(0) rotate(0); }
  10%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(26px) rotate(260deg); }
}

/* — killed/captured · K1 "Mourning veil": somber; still after the toll. — */
.raid-dimmed {
  /* replaces .raid-stlmt's filter list wholesale (dim + keep the shadow) */
  filter: saturate(.25) brightness(.78) drop-shadow(0 1px 1px rgba(0,0,0,.3));
}
.raid-veil {
  position: absolute; left: 50%; bottom: -1px; width: 24px; height: 18px;
  margin-left: -12px; border-radius: 3px;
  background: linear-gradient(to bottom, rgba(38,34,29,.62), rgba(38,34,29,.4));
  opacity: 0; animation: raid-veil-settle 1.1s ease-out .15s 1 forwards;
}
@keyframes raid-veil-settle {
  from { opacity: 0; transform: translateY(-7px); }
  to   { opacity: 1; transform: translateY(0); }
}
.raid-toll {
  position: absolute; left: 50%; bottom: 0; width: 26px; height: 26px;
  margin-left: -13px; margin-bottom: -8px; border-radius: 50%;
  border: 1.5px solid rgba(47,42,36,.75); opacity: 0; transform: scale(.15);
  animation: raid-toll-ring 1.6s ease-out 1;
}
.raid-toll2 { animation-delay: 1.1s; }
@keyframes raid-toll-ring {
  0%   { opacity: 0; transform: scale(.15); }
  12%  { opacity: .75; }
  100% { opacity: 0; transform: scale(2.2); }
}

/* — spared/fled · F1 "Slip aside, come home". — */
.raid-ghost { animation: raid-ghost-aside 1.6s ease-out .3s 1 forwards; }
@keyframes raid-ghost-aside {
  from { transform: translateX(-50%); opacity: 1; }
  to   { transform: translate(calc(-50% + 8px), 6px); opacity: .6; }
}
/* Dashed stand-off ring where the strike stopped short (own divIcon). */
.raid-standoff {
  display: block; width: 14px; height: 14px; margin: 3px auto;
  border-radius: 50%; border: 1.5px dashed #a82828; opacity: .9;
}

/* — aftermath remnant (own divIcon, 22×17, bottom-anchored) — */
.raid-after {
  position: absolute; left: 50%; bottom: 0; width: 18px; height: 13px;
  transform: translateX(-50%);
  filter: drop-shadow(0 1px 1px rgba(0,0,0,.25));
}
.raid-after svg { display: block; width: 100%; height: 100%; }
.raid-dim {
  filter: saturate(.25) brightness(.8) drop-shadow(0 1px 1px rgba(0,0,0,.25));
}

/* Raid detail lines — now inside the left margin's ONE detail slot
   (.map-sel-note); the floating card they were written for is gone. */
.raid-pop-badge { width: 30px; height: 30px; margin-bottom: 4px; }
.raid-pop-outcome { font-size: 12px; color: #a82828; font-style: italic; }
.raid-pop-snippet { font-family: Georgia, serif; font-size: 12.5px; margin: 6px 0; }
.raid-pop-cite { font-size: 11px; opacity: .75; }
.raid-pop-link { display: inline-block; margin-top: 6px; font-size: 12.5px; }
/* Dark theme only: the default blue link stings on the dark card — swap to the
   warm gilt accent used everywhere else on dark. Light theme keeps blue (reads
   fine on cream). Covers both forced-dark and OS-dark-without-a-forced-theme. */
:root[data-theme="dark"] .raid-pop-link { color: var(--gilt, #b39250); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .raid-pop-link { color: var(--gilt, #b39250); }
}

/* Migration detail lines — same: they live inside .map-sel-note now. */
.mig-pop-route {
  font-weight: 600;
  margin-top: 2px;
  color: var(--map-pop-accent, #6b5a2e);
}
.mig-pop-snippet {
  font-style: italic;
  opacity: 0.85;
  margin-top: 6px;
  font-size: 0.92em;
}

/* Reduced motion: freeze everything into the mockup's static poses.
   (The full strike TRAVEL is downgraded in Rust via reduced_motion().) */
@media (prefers-reduced-motion: reduce) {
  .raid-flame, .raid-smoke span, .raid-glow, .raid-ring, .raid-spark,
  .raid-fgrow, .raid-char-overlay, .raid-jolt, .raid-slash, .raid-dust,
  .raid-loot, .raid-veil, .raid-toll, .raid-ghost {
    animation: none;
  }
  .raid-fgrow { opacity: 1; transform: translateX(-50%) scaleY(1); }
  .raid-glow { opacity: .8; }
  .raid-veil { opacity: 1; }
  .raid-sl1 { opacity: 1; transform: rotate(38deg); }
  .raid-sl2 { opacity: 1; transform: rotate(-42deg); }
  .raid-ghost { transform: translate(calc(-50% + 8px), 6px); opacity: .6; }
  .raid-char-overlay { opacity: 0; }
  .raid-ring, .raid-toll, .raid-dust, .raid-loot, .raid-spark { opacity: 0; }
}

/* ── Raid register (/raids) ─────────────────────────────────────────── */
/* The register column is sized to its content (~680px) and centered on the page
   (margin auto). Everything inside is LEFT-aligned; the whole block reads as a
   centered column. The edit form is a responsive grid that fits this width
   (collapsing to one field-column as needed). */
.raid-register {
  max-width: 680px; margin: 0 auto; padding: 16px 16px 64px;
}
.rr-title { font-size: 26px; margin: 10px 0 4px; }
.rr-intro { font-size: 14px; opacity: .8; margin: 0 0 18px; max-width: 720px; }
.rr-year { margin-top: 8px; }
.rr-year-h {
  font-size: 15px; letter-spacing: .08em; opacity: .7;
  border-bottom: 1px solid var(--rule, #d8d2c4);
  padding-bottom: 4px; margin: 22px 0 10px;
}
/* Row content fills the (narrow, centered) column: badge + text on the left,
   Edit pushed to the right edge. The dotted divider spans the column width. */
.rr-row {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 10px 0; border-bottom: 1px dotted var(--rule, #d8d2c4);
}
.rr-badge { width: 34px; height: 34px; flex-shrink: 0; }
.rr-main { flex: 1; min-width: 0; text-align: left; }
.rr-label { font-weight: 600; }
.rr-meta { font-size: 12.5px; opacity: .75; margin-top: 2px; }
.rr-snippet {
  font-family: Georgia, serif; font-size: 13px; margin: 6px 0;
  padding-left: 10px; border-left: 2px solid var(--rule, #d8d2c4);
}
.rr-cite { font-size: 11.5px; opacity: .65; }
.rr-conf { font-style: italic; }
.rr-edit {
  flex-shrink: 0; font-size: 12.5px; padding: 4px 10px; cursor: pointer;
}
.rr-add {
  margin-top: 18px; padding: 8px 14px; font-size: 14px; cursor: pointer;
}
/* The edit/add form: a responsive grid. auto-fill + minmax packs as many
   ~240px field columns as fit (two on a desktop form, one on a phone) with
   no per-field markup; wide fields opt into a full-row span below. */
.rr-form {
  margin: 8px 0 16px; padding: 16px 18px;
  border: 1px solid var(--rule, #d8d2c4); border-radius: 8px;
  display: grid; gap: 10px 16px;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  align-items: end;
}
.rr-form-h {
  font-weight: 600; font-size: 16px; margin-bottom: 2px;
  grid-column: 1 / -1;
}
.rr-field { display: block; margin-bottom: 0; min-width: 0; }
/* Inputs/selects/textarea fill their grid cell (they had no width, so the
   text was clipping — see the "Imperial troops strip…" cut-off). */
.rr-field .auth-input { width: 100%; box-sizing: border-box; }
.rr-field-name {
  display: block; font-size: 11px; letter-spacing: .04em;
  text-transform: uppercase; opacity: .65; margin-bottom: 4px;
}
/* Free-text and full-sentence fields read better across the whole row. */
.rr-field-wide, .rr-check, .rr-snippet-field { grid-column: 1 / -1; }
.rr-check {
  display: flex; gap: 8px; align-items: center; font-size: 13px;
  margin-top: 2px;
}
.rr-check input { margin: 0; }
.rr-snippet-input { min-height: 72px; resize: vertical; }
.rr-form-actions {
  grid-column: 1 / -1;
  display: flex; gap: 10px; margin-top: 6px;
  padding-top: 12px; border-top: 1px solid var(--rule, #d8d2c4);
}
.rr-save, .rr-cancel, .rr-retire { padding: 8px 16px; cursor: pointer; }
.rr-retire { margin-left: auto; color: #a82828; }

/* Map-register migrations section (Task 10). These rules MUST stay in the
   top-level (non-@media) scope — the css_section_swallowed_by_media_block trap. */
.rr-section-h {
  margin: 2.5rem 0 0.5rem;
  font-size: 1.15rem;
  font-weight: 600;
  border-top: 1px solid var(--rule, #d8cfc0);
  padding-top: 1.25rem;
}
/* The first section ("Migrations") sits right under the add-bar (which already
   has a bottom rule) — no redundant top divider or big gap on it. */
.rr-section-h:first-of-type {
  margin-top: 0.5rem;
  border-top: none;
  padding-top: 0;
}
.rr-row-mig .rr-meta { font-style: italic; }
.rr-add-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 14px 0 20px;   /* top: gap from intro; bottom: separate from the lists */
  padding-bottom: 16px;
  border-bottom: 1px solid var(--rule, #d8d2c4);
}
.rr-add-bar .rr-add { margin-top: 0; }   /* the bar owns the spacing now */
.rr-add-kind {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: var(--ink-faint);
}
/* Compact select sized to sit beside the "+ Add" button, not a full form field. */
.rr-kind-select {
  width: auto;
  padding: 8px 12px;
  font-size: 14px;
  font-family: inherit;
  color: var(--ink);
  background: var(--rule-soft);
  border: 1px solid var(--rule);
  border-radius: 8px;
  cursor: pointer;
}
.rr-view-control {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink-dim);
}
.rr-view-control .rr-kind-select:disabled {
  cursor: not-allowed;
  opacity: .55;
}

@media (max-width: 599px) {
  .raid-register { padding: 12px 10px 48px; }
  .rr-row { gap: 8px; }
  .rr-badge { width: 28px; height: 28px; }
  /* One column on a phone — the auto-fill min would already force this, but
     make it explicit so a 240px min never causes a sideways scroll. */
  .rr-form { grid-template-columns: 1fr; }
}

/* ── Map colony action-labels ──────────────────────────────────────────
   A name pill on colonies with an event this year (founded / ended / raided).
   TWO elements: `.map-colony-lbl-box` is the divIcon element add_label creates —
   a FIXED 160px-wide transparent box (iconSize), used ONLY to center the pill
   over the point. `.map-colony-lbl` is the visible pill: an inner <span> that
   shrink-wraps its text. Styling the box as the pill would paint a 160px bar,
   hence the split (the arc labels are bare text so they never needed this).
   The basemap is ALWAYS light regardless of app theme, so pill colors are
   LITERAL (like .map-arc-lbl), NOT theme tokens — a dark chip with light text
   reads over the light terrain. Leaflet's default .leaflet-div-icon chrome is
   already neutralized map-wide at .map-leaflet .leaflet-div-icon (styles.css). */
.map-colony-lbl-box {
  /* Positioning only: center the inner pill within the wide anchor box. */
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.map-colony-lbl {
  display: inline-block;
  background: rgba(24,20,16,.92); color: #e8e0cf;
  font-family: var(--serif-body); font-size: 12px; letter-spacing: .01em;
  white-space: nowrap; padding: 2px 8px; border-radius: 7px;
  border-left: 3px solid #b39250;
  box-shadow: 0 2px 8px rgba(0,0,0,.4);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.map-colony-lbl.is-founded { border-left-color: #d8b04a; } /* gold */
.map-colony-lbl.is-ended   { border-left-color: #a82828; } /* oxblood */
.map-colony-lbl.is-raided  { border-left-color: #a82828; } /* oxblood */
/* "Show all" extras (and a tapped colony with no real event): the same tan
   as the base pill — neutral, matching "Active settlement" in the Key, never
   gold/red for an event that didn't happen this year. */
.map-colony-lbl.is-plain   { border-left-color: #b39250; } /* tan, neutral */

/* ── Realm tint layer (spec 2026-07-09) ────────────────────────────────
   Year-reactive tinted realm polygons under the colony dots, with atlas-style
   name labels.
   Label colors are LITERAL per-realm hues inlined by view.rs (like
   .map-colony-lbl — the basemap is always light regardless of app theme). */

/* Realm name labels: the -box is the fixed-size transparent divIcon element
   (positioning only — same split as .map-colony-lbl-box); the inner span is
   the visible type. Reads as printed ON the base map: small caps, letter-
   spaced, translucent, light halo so it survives both Terrain and Satellite. */
.map-realm-lbl-box {
  display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.map-realm-lbl {
  font-family: var(--serif-body); font-size: 12px; font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  opacity: .55; white-space: nowrap;
  text-shadow: 0 1px 3px rgba(255,255,255,.55), 0 0 2px rgba(255,255,255,.35);
}

/* ── Period-map overlay chrome (spec 2026-07-10) ─────────────────────
   Toggle + then↔now fade now live in the RIGHT MARGIN (spec 2026-07-12) —
   see the atlas-page section at the end of this file. */

/* ── World-events layer (spec 2026-07-11) ─────────────────────────────
   The era's major events as real period prints framed on the map. The
   "world this year" list is a LEFT-MARGIN section now (.map-wrow, spec
   2026-07-12); only the on-plate art lives here. */

/* Framed period print — the picture-cartouche. The divIcon element
   (.map-world-frame-box) is a transparent positioning box; .world-frame is
   the visible vellum-matted frame (fixed picture window; the scan renders
   background-size:cover and the full sheet lives in the lightbox). LITERAL
   colors: this sits on the light basemap / sepia engraving in both themes
   (the .map-colony-lbl rationale). The parchment placeholder + title
   renders UNDER the image span — a 404'd print never paints and the
   placeholder shows through (no broken-image glyph, no inline JS). */
.map-world-frame-box {
  display: flex; align-items: center; justify-content: center;
}
.world-frame {
  position: relative; display: block;
  width: 180px; height: 130px;
  background: #e9dfc8;                      /* vellum mat */
  border: 1px solid #6b5b43;                /* engraved double border: outer */
  box-shadow:
    inset 0 0 0 4px #e9dfc8,                /* mat ring */
    inset 0 0 0 5px #8a755a,                /* inner rule */
    0 3px 10px rgba(0, 0, 0, .35);
  cursor: pointer;
  animation: world-unveil 1.4s ease-out both;
}
.world-frame-ph, .world-frame-img { position: absolute; inset: 7px; }
.world-frame-ph {
  display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 6px;
  background: #efe6d2;                      /* parchment placeholder */
  color: #5c4d38; font-family: var(--serif-body);
  font-size: 11px; line-height: 1.35;
}
.world-frame-img { background-size: cover; background-position: center; }
/* Ink-fade entrance (~1.4 s), then static — runs once per marker ADD (the
   draw Effect diffs by id, so scrub ticks never re-trigger it). */
@keyframes world-unveil {
  from { opacity: 0; filter: sepia(.9) contrast(.6); }
  to   { opacity: 1; filter: none; }
}
@media (max-width: 759px) {
  .world-frame { width: 140px; height: 101px; }
}

/* Wax-seal marker — same-year sibling moments; tap swaps which is framed
   (print-less seals render identically but are non-interactive). */
.map-world-seal-box {
  display: flex; align-items: center; justify-content: center;
}
.world-seal {
  width: 18px; height: 18px; border-radius: 50%;
  background: radial-gradient(circle at 35% 32%, #b8412e, #7e2417 68%, #5f1a10);
  border: 1px solid rgba(60, 18, 10, .85);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .4),
              inset 0 0 0 2.5px rgba(255, 240, 220, .18);
  cursor: pointer;
  animation: world-unveil 1.4s ease-out both;
}
/* Inert seals (print-less moments) are not tap targets — no pointer lie. */
.map-world-seal-box.is-quiet .world-seal { cursor: default; }

@media (prefers-reduced-motion: reduce) {
  .world-frame, .world-seal { animation: none; }
}

/* Lightbox — the full sheet. Modal over everything (popovers are 1000);
   its backdrop covers the map, so nothing beneath needs to yield. Mobile:
   a full-screen sheet. Frame palette matches .world-frame (literal —
   theme-independent print viewing). */
.world-lightbox {
  position: fixed; inset: 0; z-index: 1200;
  background: rgba(10, 8, 6, .82);
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
}
.world-lightbox-inner {
  position: relative; margin: 0;
  max-width: min(920px, 100%); max-height: 100%;
  overflow: auto;
  background: #e9dfc8; border: 1px solid #6b5b43;
  box-shadow: inset 0 0 0 3px #8a755a, 0 12px 40px rgba(0, 0, 0, .5);
  padding: 14px;
  display: flex; flex-direction: column; gap: 10px;
}
.world-lightbox-img { display: block; max-width: 100%; height: auto; }
.world-lightbox-cap { font-family: var(--serif-body); color: #3d3223; }
.world-lightbox-title { font-size: 15px; font-weight: 700; }
.world-lightbox-credit { font-size: 11.5px; color: #6b5b43; margin-top: 2px; }
/* ✕ pinned to the BACKDROP (viewport corner, lightbox convention) — never
   inside the scroller, so it can't scroll out of reach on tall prints. */
.world-lightbox-x {
  position: absolute; top: 14px; right: 14px; z-index: 1;
  width: 30px; height: 30px; border-radius: 8px;
  background: rgba(233, 223, 200, .9); border: 1px solid #6b5b43;
  color: #3d3223; font-size: 14px; line-height: 1;
  cursor: pointer;
}
@media (max-width: 759px) {
  .world-lightbox { padding: 0; }
  .world-lightbox-inner {
    width: 100%; height: 100%; max-height: none;
    border: none;
  }
  /* Clear a notch/Dynamic Island in standalone mode — the sheet is truly
     full-screen here. */
  .world-lightbox-x { top: calc(10px + env(safe-area-inset-top, 0px)); right: 10px; }
}

/* ── Legibility halo (spec 2026-07-11 §5) ──
   While the Period-map layer is ON, the data art gets a cream halo so
   pills / badges / flames / aftermath marks / arc labels read over the
   engraving. Values from the validated companion mock; tune at smoke.
   LITERAL color: the halo sits over the period scan, identical in both
   themes (the .map-colony-lbl literal-colors rationale). */
.map-sheet.period-on .map-colony-lbl,
.map-sheet.period-on .map-arc-lbl,
.map-sheet.period-on .raid-origin-lbl,
.map-sheet.period-on .raid-burn,
.map-sheet.period-on .raid-standoff {
  filter: drop-shadow(0 0 1.5px #f2ecdc) drop-shadow(0 0 3px #f2ecdc);
}
/* The badge keeps its existing dark drop-shadow; the halo wraps around it
   (this rule REPLACES the base .raid-badge filter while period-on). */
.map-sheet.period-on .raid-badge {
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .35))
          drop-shadow(0 0 1.5px #f2ecdc) drop-shadow(0 0 3px #f2ecdc);
}
/* Aftermath marks keep their own base shadow — and the dimmed variant its
   desaturation — under the halo (the .raid-badge treatment: filter values
   replace wholesale across the cascade, so each combination restates its
   base before appending the halo pair). */
.map-sheet.period-on .raid-after {
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .25))
          drop-shadow(0 0 1.5px #f2ecdc) drop-shadow(0 0 3px #f2ecdc);
}
.map-sheet.period-on .raid-after.raid-dim {
  filter: saturate(.25) brightness(.8) drop-shadow(0 1px 1px rgba(0, 0, 0, .25))
          drop-shadow(0 0 1.5px #f2ecdc) drop-shadow(0 0 3px #f2ecdc);
}

/* ── Map layer switches (spec 2026-07-11, re-homed 2026-07-12) ────────
   These rows once lived in a floating top-right drawer. The drawer is GONE:
   they are now bare rows in the right margin, under a "Layers" section head
   (.map-h) like everything else on that page. The PILL SWITCH is untouched —
   only the row's housing changed: full-width, transparent, no panel padding.
   The off-state is a pill switch, NOT a strikethrough (that reads "deleted"). */
/* Rows are <button>s: reset the UA chrome, keep the label/control split. */
.map-layers-row {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; gap: 12px; padding: 7px 0;
  background: none; border: 0; border-top: 1px solid var(--rule-soft);
  font-family: var(--serif-body); font-size: 12.5px; color: var(--ink-soft);
  text-align: left; cursor: pointer;
  transition: color .15s ease;
}
.map-layers-row:first-of-type { border-top: 0; }
.map-layers-row:hover { color: var(--gilt); }
.map-layers-row.is-off .map-layers-name { color: var(--ink-faint); }
.map-layers-name { white-space: nowrap; }

/* Pill switch: gilt track + knob right when on; dull track + knob left when off. */
.map-layers-track {
  position: relative; flex: none; width: 26px; height: 14px;
  border-radius: 7px; background: var(--rule);
  transition: background .15s ease;
}
.map-layers-track i {
  position: absolute; top: 2px; left: 2px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--page-bg);
  transition: left .15s ease;
}
.map-layers-track.is-on { background: var(--gilt); }
.map-layers-track.is-on i { left: 14px; }

/* Base map: a cycler, not a switch — it names the current mode instead. */
.map-layers-mode { font-size: 11px; color: var(--gilt); white-space: nowrap; }

/* The register entrance (contributors/admins only): oxblood marks it as a
   different KIND of row — a place you leave to, not a switch you flip. */
.map-layers-register {
  color: var(--oxblood); text-decoration: none;
}
.map-layers-register .map-layers-mode { color: var(--oxblood); }
.map-layers-register:hover,
.map-layers-register:hover .map-layers-mode { color: var(--oxblood-deep); }

@media (prefers-reduced-motion: reduce) {
  .map-layers-track, .map-layers-track i { transition: none; }
}

/* ── The engraved mark vocabulary (spec 2026-07-12) ───────────────────
   Every mark on the plate: a paper-white keyline, a dark hairline outside it,
   and a soft paper halo — so it carries its own background and floats off ANY
   basemap (relief, satellite, a 1570 engraving). The SILHOUETTE says what it
   is; the colour only reinforces. Replaces the flat 7px circleMarker dots that
   sank into the hillshade.

   LITERAL colours, not theme tokens: these sit on the basemap, which is always
   light regardless of the app theme — the same rationale as .map-colony-lbl. */
.map-mark-box { display: flex; align-items: center; justify-content: center;
                pointer-events: auto; cursor: pointer; }
.map-mark {
  display: block; box-sizing: border-box;
  border: 1.5px solid #f7f2e8;
  box-shadow: 0 0 0 1px rgba(70,55,30,.8), 0 0 8px 3px rgba(247,242,232,.8);
}
/* circle = founded this year */
.map-mark.is-founded { width: 15px; height: 15px; border-radius: 50%; background: #d8b04a; }
/* diamond = an active settlement */
.map-mark.is-active  { width: 12px; height: 12px; background: #b39250; transform: rotate(45deg); }
/* struck lozenge = ended this year */
.map-mark.is-ended {
  position: relative; width: 15px; height: 15px; border-radius: 2px;
  background: #a82828; transform: rotate(45deg);
}
.map-mark.is-ended::after {
  content: ""; position: absolute; inset: 3px; border: 1.5px solid rgba(247,242,232,.9);
}
/* ghost ring = not yet / gone */
.map-mark.is-gone {
  width: 8px; height: 8px; border-radius: 50%; background: transparent;
  border: 1.5px dotted rgba(60,50,35,.65);
  box-shadow: 0 0 5px 2px rgba(247,242,232,.55);
}
/* open ring + centre dot = a place we know, a year we don't (Status::Undated).
   Deliberately distinct from .is-gone, which it must never be confused with: the
   ghost's ring is DOTTED because we HAVE a date that rules the colony out of this
   year; this ring is SOLID because the place is real and it is only the DATE we
   lack. Drawn identically at every year — the constancy IS the signal. */
.map-mark.is-undated {
  position: relative; width: 9px; height: 9px; border-radius: 50%;
  background: transparent;
  border: 1.5px solid rgba(60,50,35,.7);
  box-shadow: 0 0 5px 2px rgba(247,242,232,.6);
}
.map-mark.is-undated::after {
  content: ""; position: absolute; inset: 1.5px; border-radius: 50%;
  background: rgba(60,50,35,.7);
}
/* The "Full size" Layers toggle: Pre/Gone/Undated grow to match Active's
   own 12px (never Founded/Ended's bigger 15px pulse — that stays a distinct,
   rarer signal). Silhouette/colour are UNCHANGED — still a hollow ring, still
   dotted vs solid — so a Gone colony still visibly reads as gone, just no
   longer shrunk to near-invisibility. */
.map-mark.is-gone.is-full { width: 12px; height: 12px; }
.map-mark.is-undated.is-full { width: 12px; height: 12px; }
/* the numeral cluster — several marks too close to separate */
.map-mark.is-cluster {
  width: 22px; height: 22px; border-radius: 50%; background: #f7f2e8;
  border: 1.5px solid #8a6e2e; color: #5a4820;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--serif-display); font-size: 11px; font-style: normal;
  box-shadow: 0 0 0 1px rgba(70,55,30,.4), 0 0 9px 3px rgba(247,242,232,.85);
}
/* a cluster holding a death keeps the oxblood keyline — never swallowed */
.map-mark.is-cluster.has-ended { border-color: #a82828; color: #7a1e1e; }

/* ── The "this one changed" swell ────────────────────────────────────
   Fires ONCE when the scrubber enters a year, on marks whose state changed
   THAT year (founded / ended). Unchanged marks never move — that contrast is
   the whole signal. 1.35x, ~620ms, slight overshoot, halo blooming with it so
   the mark lifts off the terrain for one beat. */
.map-mark.is-swell.is-founded { animation: mark-swell-round .62s cubic-bezier(.2,.9,.25,1) 1; }
.map-mark.is-swell.is-active  { animation: mark-swell-rot   .62s cubic-bezier(.2,.9,.25,1) 1; }
.map-mark.is-swell.is-ended   { animation: mark-swell-rot   .62s cubic-bezier(.2,.9,.25,1) 1; }
@keyframes mark-swell-round {
  0%   { transform: scale(1);
         box-shadow: 0 0 0 1px rgba(70,55,30,.8), 0 0 8px 3px rgba(247,242,232,.8); }
  28%  { transform: scale(1.35);
         box-shadow: 0 0 0 1px rgba(70,55,30,.85), 0 0 16px 7px rgba(247,242,232,.95); }
  60%  { transform: scale(.97); }
  100% { transform: scale(1);
         box-shadow: 0 0 0 1px rgba(70,55,30,.8), 0 0 8px 3px rgba(247,242,232,.8); }
}
/* the rotated shapes must keep their 45deg or they un-rotate mid-animation */
@keyframes mark-swell-rot {
  0%   { transform: rotate(45deg) scale(1);
         box-shadow: 0 0 0 1px rgba(70,55,30,.8), 0 0 8px 3px rgba(247,242,232,.8); }
  28%  { transform: rotate(45deg) scale(1.35);
         box-shadow: 0 0 0 1px rgba(70,55,30,.85), 0 0 16px 7px rgba(247,242,232,.95); }
  60%  { transform: rotate(45deg) scale(.97); }
  100% { transform: rotate(45deg) scale(1);
         box-shadow: 0 0 0 1px rgba(70,55,30,.8), 0 0 8px 3px rgba(247,242,232,.8); }
}
@media (prefers-reduced-motion: reduce) {
  .map-mark.is-swell { animation: none !important; }
}

/* ── The atlas page (spec 2026-07-12) ────────────────────────────────
   The map is a printed plate; the chrome is marginalia. Left margin = what you
   are reading, right margin = what you are controlling, the scale = when. The
   margins are PAPER, not panels: no card, no radius, no shadow, no blur —
   just --page-bg and one hairline against the plate. Panels float; margins
   belong. This replaces the eight floating surfaces the map used to carry. */
/* LEFT = the brethren. RIGHT = the world. The plate is the ground between them — so the
   two margins are EQUAL, and wide enough to read: at the old 244px a line of the year's
   prose is ~35 characters, half the comfortable measure. The ladder is the operator's,
   found with the mock's own overflow scan (2026-07-13-atlas-margins.html): 340px on a
   wide screen, stepping down so the plate never stops being a map. Below ~1000px the
   page reflows to one column (the phone block at the end of this file). */
.map-sheet {
  display: grid;
  grid-template-columns: 264px 1fr 264px;
  grid-template-rows: 1fr auto;
  grid-template-areas: "left plate right" "scale scale scale";
  min-height: 0;
}
@media (min-width: 1250px) {
  .map-sheet { grid-template-columns: 300px 1fr 300px; }
}
@media (min-width: 1500px) {
  .map-sheet { grid-template-columns: 340px 1fr 340px; }
}
.map-margin { background: var(--page-bg); padding: 20px 20px 0; min-height: 0;
              display: flex; flex-direction: column; }
.map-margin.is-left  { grid-area: left;  border-right: 1px solid var(--rule); }
.map-margin.is-right { grid-area: right; border-left: 1px solid var(--rule); }
/* one continuous scrolling column — a margin is a column, not a mode */
.map-margin-col { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden;
                  scrollbar-width: thin; padding-bottom: 20px; }
.map-plate { grid-area: plate; position: relative; min-width: 0; }
/* the Leaflet container fills the plate (it was flex:1 in the old column) */
.map-leaflet { position: absolute; inset: 0; width: auto; height: auto; }
/* the scale spans the foot of the page — see the ".map-scale" section at the
   end of this file, which carries `grid-area: scale`. */

/* the engraved double rule of a printed plate */
.map-frame {
  position: absolute; inset: 11px; pointer-events: none; z-index: 500;
  border: 1px solid rgba(90,72,45,.6);
  box-shadow: inset 0 0 0 3px rgba(247,242,232,.4), inset 0 0 0 4px rgba(90,72,45,.3);
}
/* the running head, printed ON the plate — not a floating caption */
.map-runhead {
  /* left clears Leaflet's zoom control (it sits at ~10px + 34px wide inside the
     plate and was printing straight through the running head). */
  position: absolute; left: 68px; right: 26px; top: 22px; z-index: 600;
  display: flex; justify-content: space-between; gap: 16px; pointer-events: none;
  font-family: var(--serif-display); font-size: 10px; letter-spacing: .18em;
  text-transform: uppercase;
}
.map-runhead .l { color: #6b5426; text-shadow: 0 1px 0 rgba(255,255,255,.45); }
.map-runhead .r { color: #8a8170; text-shadow: 0 1px 0 rgba(255,255,255,.45); }

/* ── THE ONE TYPOGRAPHIC IDIOM every margin section obeys ── */
.map-h {
  font-family: var(--serif-display); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--gilt);
  border-bottom: 1px solid var(--rule); padding-bottom: 5px;
  margin: 22px 0 10px;
}
/* A sub-group WITHIN one .map-h section (e.g. "Colonies" inside "Layers") —
   same idiom, tighter gap: it marks a division in one list, not a new one. */
.map-h-sub { margin-top: 14px; }
.map-margin-col > .map-h:first-child { margin-top: 0; }
.map-note { font-size: 13.5px; line-height: 1.62; color: var(--ink-soft); }
.map-note .lede { color: var(--ink); }
.map-dim { color: var(--ink-faint); }

/* the Vorsteher line — small caps key, name, tenure */
.map-vor { margin-top: 10px; font-size: 12px; line-height: 1.5; }
.map-vor-k { font-family: var(--serif-display); font-size: 9px; letter-spacing: .14em;
             text-transform: uppercase; color: var(--gilt); margin-right: 7px; }
.map-vor-n { color: var(--ink); }
.map-vor-t { color: var(--ink-faint); margin-left: 6px; font-variant-numeric: tabular-nums; }
.map-vor-note { display: block; color: var(--ink-faint); font-size: 11.5px; margin-top: 2px; }

/* the ONE detail slot — an oxblood spine, the event cards' kind-marker language */
.map-sel { border-left: 2px solid var(--oxblood); padding-left: 12px; }
.map-sel-name { font-family: var(--serif-display); font-size: 18px; line-height: 1.15;
                color: var(--ink); }
.map-sel-dates { font-size: 11.5px; color: var(--ink-faint); margin: 4px 0 7px;
                 font-variant-numeric: tabular-nums; }
.map-sel-now, .map-sel-note { font-size: 13.5px; line-height: 1.62; color: var(--ink-soft); }
.map-sel-note { margin-top: 5px; }
.map-sel-inferred { font-size: 11px; color: var(--ink-faint); margin-top: 5px; }
/* the raid / migration slots reuse the popovers' inner lines (.raid-pop-snippet,
   .raid-pop-cite, .raid-pop-link, .mig-pop-snippet — still emitted by margins.rs);
   they sit inside .map-sel-note, so they inherit the margin's paper, not a card. */

/* the world list — an index: title left, years right, a dagger on a moment */
.map-wrow {
  display: block; width: 100%; text-align: left; background: none; border: 0;
  border-bottom: 1px solid var(--rule-soft); padding: 8px 0; cursor: default;
  font-family: var(--serif-body); color: var(--ink-soft);
}
button.map-wrow { cursor: pointer; }
button.map-wrow:hover .t { color: var(--gilt); }
.map-wrow .t { font-size: 12.5px; color: var(--ink-soft); }
.map-wrow.is-moment .t { color: var(--ink); }
.map-wrow.is-moment .t::before { content: "\2020"; color: var(--oxblood); margin-right: 6px; }
.map-wrow .y { font-size: 10.5px; color: var(--ink-faint); white-space: nowrap;
               float: right; margin-left: 10px; font-variant-numeric: tabular-nums; }
.map-wrow-sum { font-size: 12px; line-height: 1.55; color: var(--ink-faint); margin-top: 3px; }
.map-wrow-act { display: inline-block; margin-top: 4px; font-size: 10.5px;
                letter-spacing: .06em; color: var(--gilt); }
.map-wrow-eff { font-size: 11.5px; line-height: 1.5; color: var(--ink-soft); margin-top: 4px;
                border-left: 2px solid var(--oxblood); padding-left: 8px; }
/* "For the brethren:" — the effect line's key (emitted by margins.rs) */
.map-wrow-eff-k { color: var(--oxblood); margin-right: 5px; }

/* the key — the same marks as the plate, at rest */
.map-krow { display: flex; align-items: center; gap: 9px; padding: 4px 0;
            font-size: 12.5px; color: var(--ink-soft); }
.map-kmark { width: 13px; height: 13px; border-radius: 3px; flex: none; }
.map-kmark.is-founded { background: #d8b04a; }
.map-kmark.is-active  { background: #b39250; }
.map-kmark.is-ended   { background: #a82828; }
.map-kmark.is-gone    { background: transparent; border: 2px dotted var(--ink-faint); }
.map-kmark.is-undated { background: transparent; border: 2px solid var(--ink-faint); }

/* the then↔now fade — a margin row now, not a floating bar over the scrubber */
.map-fade { display: flex; align-items: center; gap: 8px; padding: 10px 0 0; }
.map-fade-k { font-family: var(--serif-display); font-size: 9px; letter-spacing: .12em;
              text-transform: uppercase; color: var(--ink-faint); }
.map-fade input[type="range"] { flex: 1; min-width: 0; accent-color: var(--gilt); }
.map-period-line { font-size: 11.5px; line-height: 1.5; color: var(--ink-faint); margin-top: 8px; }


/* ── The scale, engraved (spec 2026-07-12) ───────────────────────────
   ONE coordinate system: every element is placed by scale::year_pct, so the era
   bands, the ticks, the playhead AND the drag agree by construction. Era bands
   above the axis, ticks below it, the year hung under a gilt playhead in the
   display face — the year is ON the scale, not floating beside it. Replaces the
   2800px filmstrip that translated under a fixed head. */
/* The scale is a Play GUTTER plus the ruler TRACK. Play used to sit inside the
   drag surface, where the moving year numeral printed straight through it at the
   left end of the window. The gutter fixes that by construction — and because the
   TRACK is the element the drag measures, the geometry (px/year, year-at-x) reads
   the ruler itself and stays exact. */
.map-scale {
  grid-area: scale; height: 88px; display: flex; align-items: stretch;
  background: var(--canvas); border-top: 1px solid var(--rule);
  user-select: none;
}
.map-track {
  position: relative; flex: 1; min-width: 0;
  touch-action: none; cursor: ew-resize;
}
.map-eras { position: absolute; left: 0; right: 0; top: 0; height: 24px; }
/* Each era CLIPS its own label, so a narrow band can never spill its name across
   the bands beside it (they are adjacent by construction — an absolutely-
   positioned label centred on a 2%-wide band always intersected its neighbours).
   The tint lives on ::before, NOT on the band's own `opacity`, or the label would
   fade with it. */
.map-era {
  position: absolute; top: 0; height: 24px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  border-right: 1px solid var(--rule);
}
.map-era::before {
  content: ""; position: absolute; inset: 0; background: var(--gilt); opacity: .07;
}
.map-era.is-live::before { opacity: .16; }
.map-era-lab {
  position: relative; z-index: 1; max-width: 100%; padding: 0 5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-family: var(--serif-display); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-faint); pointer-events: none;
}
.map-era.is-live .map-era-lab { color: var(--gilt); }
.map-axis { position: absolute; left: 0; right: 0; top: 24px; height: 1px; background: var(--rule); }
/* FOUR tick tiers — a printed scale is read at four depths: you glance for the
   labelled half-century, you count decades, the 5-year strokes give the eye its
   pace, and the year strokes show that a YEAR is the unit the scrubber steps in.
   A single tier read as an empty line. 141 strokes across the window. */
.map-tick { position: absolute; top: 24px; width: 1px; background: var(--rule); }
.map-tick.is-year   { height: 3px;  background: var(--ink-veryfaint); opacity: .8; }
.map-tick.is-fine   { height: 5px;  background: var(--ink-veryfaint); }
.map-tick.is-decade { height: 9px;  background: var(--ink-faint); opacity: .6; }
.map-tick.is-major  { height: 14px; background: var(--ink-faint); }
/* Every decade is labelled; the half-centuries are set louder so the eye still
   has anchors to jump between rather than a flat row of equal numerals. */
.map-ticklab {
  position: absolute; top: 39px; transform: translateX(-50%); pointer-events: none;
  font-size: 9px; color: var(--ink-faint); opacity: .7;
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
}
.map-ticklab.is-major {
  font-size: 10.5px; opacity: 1; color: var(--ink-dim);
}
.map-playhead { position: absolute; top: 24px; width: 1px; height: 33px; background: var(--gilt); }
/* The year, with the live era NAMED beside it — a narrow band (era II is three
   years wide; era VI, "The 1622 Expulsion", is nine) can only hold a numeral, so
   this is where the era gets said in full. Clamped in the Rust to [6%, 94%] so it
   can never run off the track's ends; the playhead hairline above stays exact. */
.map-playyr {
  position: absolute; top: 54px; transform: translateX(-50%); pointer-events: none;
  display: flex; align-items: baseline; gap: 9px; white-space: nowrap;
}
.map-playyr .y {
  font-family: var(--serif-display); font-size: 22px; color: var(--gilt);
  font-variant-numeric: tabular-nums; line-height: 1;
}
.map-playyr .e {
  font-family: var(--serif-display); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-faint); line-height: 1;
}
/* Play lives in its own gutter, OUTSIDE the track — the ruler cannot reach it,
   so nothing prints through it and its click cannot scrub.
   CENTRED, not left-aligned with a padding. The old `width:74px` +
   `padding-left:16px` + `text-align:left` held a ~38px word, so ~20px of slack
   ALL fell to the right of it and opened a gap before the next chevron — the
   uneven desktop spacing in the 2026-07-19 screenshot. Centring splits any
   remaining slack evenly, so the cluster's two gaps match by construction.
   The trailing letter-spacing (.16em hangs off the final letter) is cancelled
   with a matching text-indent, or the word sits a hair left of true centre. */
.map-play {
  flex: none; width: 66px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: none; border: 0; cursor: pointer; text-align: center;
  font-family: var(--serif-display); font-size: 10px; letter-spacing: .16em;
  text-indent: .16em;
  text-transform: uppercase; color: var(--gilt);
}
.map-play:hover { color: var(--ink); }
/* The play control says its word on a desktop and DRAWS its glyph on a phone. The
   glyph is an SVG in currentColor, never the characters ▶/⏸: iOS renders those with
   emoji presentation (a blue rounded square) that no CSS can recolour. */
.map-play-i { display: none; width: 13px; height: 15px; fill: currentColor; flex: none; }

/* The ‹ › single-year steppers, FLANKING Play as one transport cluster in the
   gutter outside the measured track. They were phone-only until 2026-07-19; the
   keyboard (←/→, Shift for ten) is the desktop twin of these, and both funnel
   through the same step_year() so neither can outrun autoplay: a manual step
   always pauses it first.

   THE CLUSTER IS A GRID, NOT A ROW OF PADDED BOXES. Each control is the same
   fixed width and CENTRES its own glyph, so the three gaps are equal by
   construction. The first cut gave Play a 38px box holding a 13px glyph plus an
   asymmetric padding-left — the 25px of slack all fell to the right of the
   triangle and shoved the next chevron away, which is the uneven spacing in the
   2026-07-19 phone screenshot. Never pad these; size them and centre. */
.map-step {
  flex: none; width: 34px; align-self: center; height: 34px;
  padding: 0; border: 0; background: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--gilt);
}
.map-step:hover { color: var(--ink); }
.map-step:active { color: var(--ink); }
/* The chevrons are STROKED (the play triangle is filled) — a filled chevron at
   this size reads as a blob; fill:none or the path closes into a wedge.
   2.8px with ROUND caps and joins is the weight the operator drew (2026-07-19):
   a confident mark you aim at, not the hairline the first cut shipped. It
   deliberately out-weighs the small-caps PLAY beside it — the arrows are the
   target, the word is a label. */
.map-step-i {
  width: 13px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2.8;
  stroke-linecap: round; stroke-linejoin: round;
}
/* The cluster's only outer inset — one value, on the leading edge. */
.map-step.is-prev { margin-left: 8px; }

/* ── PHONE-ONLY FURNITURE (spec 2026-07-14) ──────────────────────────
   Three pieces that exist ONLY below 759px, declared hidden here so the base is
   the desktop page and the phone block (at the END of this file) turns each one
   on. A desktop plate simply prints its credit, points at the year you point at,
   and IS its three margins side by side — none of these earn their space there. */
.map-credit { display: none; }   /* the collapsed attribution ⓘ */
.map-tabs   { display: none; }   /* Brethren · The world · Layers */
/* (.map-steps — the old right-hand ‹ › gutter — was retired 2026-07-19; the
   steppers moved into the left transport cluster and now show at every width.
   See the .map-step block above.) */

/* the invisible native range: keyboard + screen-reader control, layered over the
   ruler. pointer-events:none so it never swallows the drag (the ruler owns
   pointer events); it stays focusable and operable by keyboard. */
.map-range {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; -webkit-appearance: none; appearance: none;
  pointer-events: none; z-index: 1;
}

/* ── The narrating margins (spec 2026-07-13) ──────────────────────────
   The map is sitting on a complete, sourced, one-line-per-event narration of 140 years
   and has never spoken it. These are the marks it speaks with. */

/* the year, printed like a plate number */
.map-yr { font-family: var(--serif-display); font-size: 36px; line-height: 1;
          font-variant-numeric: tabular-nums; margin: 0; color: var(--ink); }
/* NB: `.map-era` is NOT free — it is the scrubber's era BAND. This is the margin's era. */
.map-era-name { font-size: 12.5px; color: var(--gilt); font-style: italic; margin: 2px 0 0; }
.map-eratext { font-size: 11.5px; color: var(--ink-soft); margin: 6px 0 0; line-height: 1.5; }

/* WHERE AM I IN THE STORY — the era never told you the year */
.map-pos { margin: 11px 0 0; height: 3px; background: var(--rule);
           position: relative; border-radius: 2px; }
.map-pos i { position: absolute; top: -3px; width: 3px; height: 9px; background: var(--ink); }
.map-posn { display: flex; justify-content: space-between; gap: 8px; margin-top: 4px;
            font-size: 9.5px; color: var(--ink-soft); }

/* the year's LEDE (the synthesis pass fills these later) */
.map-synth { font-size: 14px; line-height: 1.62; margin: 0; color: var(--ink); }

/* THE EMPTY CARTOUCHE — an atlas plate leaves its cartouche ruled and blank until it is
   engraved; it does not leave a hole. Honest, in the idiom, and it doubles as the
   authoring queue: the map itself tells you which years still need writing. */
.map-cartouche {
  border: 1px solid var(--rule); border-radius: 2px; padding: 9px 11px; margin: 0;
  position: relative;
  background: repeating-linear-gradient(-45deg, transparent 0 6px,
              rgba(201,189,166,.13) 6px 12px);
}
.map-cartouche:before { content: ""; position: absolute; inset: 3px;
  border: 1px solid rgba(201,189,166,.55); pointer-events: none; }
.map-cartouche b { display: block; font-family: var(--serif-display); font-size: 10px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--gilt); margin-bottom: 3px; }
.map-cartouche p { margin: 0; font-size: 11.5px; line-height: 1.5; font-style: italic;
  color: var(--ink-soft); position: relative; }

/* the RECORD, beneath the lede */
.map-rec { font-family: var(--serif-display); font-size: 9px; letter-spacing: .11em;
           text-transform: uppercase; color: var(--ink-soft); margin: 14px 0 5px; }
.map-fall { margin: 0; padding: 0; list-style: none; }
.map-fall-i { position: relative; padding: 6px 0 6px 13px; font-size: 12.5px;
              line-height: 1.45; border-bottom: 1px dotted var(--rule); }
.map-fall-i:last-child { border-bottom: 0; }
.map-fall-i:before { content: ""; position: absolute; left: 0; top: 12px;
  width: 4px; height: 4px; border-radius: 99px; background: var(--ink-soft); }
/* The kind vocabulary, borrowed from the marks on the plate. An unknown kind keeps the
   default bullet. ⚠ THE VARIABLE IS --oxblood: there is no --ox, and an undefined custom
   property would render these bullets TRANSPARENT, not red. */
.map-fall-i.k-martyrdom:before, .map-fall-i.k-dissolution:before { background: var(--oxblood); }
.map-fall-i.k-migration:before { background: var(--gilt); }
.map-fall-at { color: var(--ink-soft); font-size: 10.5px; font-style: italic; }
.map-more { display: inline-block; margin-top: 8px; padding: 0; border: 0;
  background: none; font: inherit; font-size: 11px; color: var(--gilt);
  text-decoration: underline dotted; cursor: pointer; }

/* THE FOLD IS A CSS TOGGLE, AND THAT IS LOAD-BEARING.
   Every title and both buttons are ALWAYS mounted; `is-folded`/`is-hidden` hide them. If
   the fold were a Rust conditional instead, clicking "+18 more" would unmount the very
   button being clicked and drop its Closure mid-dispatch — the "closure invoked after being
   dropped" family, which with panic=abort kills the app. (See margins.rs's comment, and the
   four defer_set/defer_close helpers this codebase already carries for it.)
   ⚠ The `6` is year_voice::TITLES_SHOWN + 1. If that constant moves, this moves. */
.map-fall.is-folded > .map-fall-i:nth-child(n + 6) { display: none; }
.map-fall.is-folded > .map-fall-i:nth-child(5) { border-bottom: 0; }
.map-more.is-hidden { display: none; }

/* a silent year, and a quiet world. Say it — an absent section reads as a bug. */
.map-quiet { color: var(--ink-soft); font-style: italic; font-size: 13px; margin: 0; }

/* THE ROAD, and the land it crossed */
.map-road { margin-bottom: 12px; }
.map-road-hd { font-size: 12px; margin: 0 0 4px; }
.map-road-note { margin: 2px 0 0; font-size: 12px; line-height: 1.45; font-style: italic; opacity: .78; }
.map-land-t { margin: 0 0 3px; font-size: 12.5px; line-height: 1.5; }
.map-land-c { margin: 0; font-size: 11px; opacity: .62; }
.map-road-prof { display: block; width: 100%; height: 38px;
  background: rgba(201,189,166,.2); border: 1px solid var(--rule); }
.map-road-prof .fill { fill: rgba(122,92,46,.15); }
.map-road-prof .line { fill: none; stroke: var(--gilt); stroke-width: 1.3; }
.map-road-ft { display: flex; justify-content: space-between; margin-top: 3px;
  font-size: 9.5px; color: var(--ink-soft); }

/* why the plate is here */
.map-why { font-size: 11.5px; color: var(--ink-soft); margin: 0; }

/* THE WORLD'S ENGRAVING, in the right margin. THE CAP IS LOAD-BEARING: without it a wider
   margin makes the White Mountain engraving TALLER, and widening the margins backfires. */
.map-wrow-print { display: block; width: 100%; max-height: 150px; object-fit: cover;
  object-position: center 38%; margin: 8px 0 3px; border: 1px solid var(--rule);
  filter: sepia(.2) contrast(1.03); }
.map-wrow-cred { font-size: 9px; color: var(--ink-soft); font-style: italic; margin: 0; }

/* ── The atlas page on a phone (spec 2026-07-12) ──────────────────────
   A narrow viewport has no margins to spare — and no sheet, no tabs, no
   gesture either. A page on a phone doesn't sprout a draggable drawer; it
   reflows into one column. Grid is off entirely: plate, then the scale,
   then the two margins' sections below, in the SAME order as the desktop
   margins (left, then right), all scrolled as one page. Nothing covers the
   plate; there is no mode to be in. This block sits at the end of the file
   (after the base .map-scale / .map-mark rules it overrides) so cascade
   order — not just specificity — puts it on top at narrow widths.
   (docs/superpowers/mockups/2026-07-12-mobile-fold.html, option B.) */
/* ── THE REFLOW (≤ 999px) ──
   Three real reading margins cannot hold in 900px, so the page drops to ONE column.
   This block carries ONLY the consequences of turning the grid off — it must NOT carry
   the phone's ERGONOMICS (touch targets, the shrunken scale, the hidden decade numerals):
   those would land on an iPad in portrait and a half-screen laptop window, which are not
   phones. They stay in the 759px block below. */
@media (max-width: 999px) {
  .map-sheet {
    display: block;
    /* the desktop rule pins position:fixed + overflow:hidden so the grid's
       columns can be viewport-height panes; here the PAGE scrolls instead. */
    overflow-y: auto;
    overflow-x: hidden;
  }
  /* the Leaflet plate collapses to zero height without one — it's
     position:relative holding an absolutely-positioned Leaflet container. */
  .map-plate { height: 46vh; min-height: 260px; }
  /* the margins stack, so their vertical hairline becomes a horizontal one */
  .map-margin.is-left, .map-margin.is-right {
    border-right: 0; border-left: 0;
    border-top: 1px solid var(--rule);
    padding: 14px 14px 0;
  }
  /* the page scrolls now, not the column */
  .map-margin-col { overflow: visible; }
  .map-margin.is-right { padding-bottom: 28px; }
  .map-margin.is-right .map-margin-col { padding-bottom: 24px; }
}

/* ── THE PHONE (≤ 759px) — ergonomics, AND (since 2026-07-14) its own layout ── */
@media (max-width: 759px) {
  /* ── THE PHONE PAGE (spec 2026-07-14) ──
     The ≤999px block above turns the grid OFF and lets the PAGE scroll. That is
     right for a tablet and wrong for a phone: the map scrolls away the moment you
     read anything, and a Layers switch below the fold is a control you cannot watch
     work. (That is the whole reason this became tabs and not a longer column.)

     So the grid comes back, VERTICAL, and only ONE row scrolls. The plate and the
     scale are always on screen; the tabs swap what is underneath them. Nothing
     floats, nothing is dismissed, and there is no gesture to learn. */
  .map-sheet {
    display: grid;
    grid-template-columns: 1fr;
    /* dvh, not vh: the mobile URL bar showing/hiding must not resize the plate.
       ⚠ minmax(0, 1fr), NOT 1fr. A bare `1fr` is minmax(AUTO, 1fr): its floor is the
       item's min-content height, so a long pane GROWS the row instead of scrolling
       inside it — the row pushes past the fixed sheet, `overflow:hidden` clips it,
       and the bottom of the pane becomes simply unreachable. That is exactly what
       "the lower data scroll stopped working" was. The 0 floor lets the row be
       smaller than its content, which is what makes the pane's own overflow scroll. */
    grid-template-rows: 52dvh auto auto minmax(0, 1fr);
    grid-template-areas: "plate" "scale" "tabs" "pane";
    overflow: hidden;            /* the PANE scrolls, not the sheet */
  }
  /* The ≤999px block hands the plate a fixed height because the page scrolls there.
     Here the grid row sizes it, so give it back. */
  .map-plate { grid-area: plate; height: auto; min-height: 0; }
  .map-scale { grid-area: scale; }
  .map-tabs  { grid-area: tabs; display: flex; border-top: 1px solid var(--rule); }

  /* Both margins occupy the SAME row. Exactly one is shown, chosen by the sheet's
     tab class; and inside the right margin, exactly one of its two panes. */
  .map-margin.is-left, .map-margin.is-right {
    grid-area: pane; min-height: 0;
    overflow-y: auto; -webkit-overflow-scrolling: touch;
    /* Without these, a touch-scroll near a Leaflet map (which owns its own
       pan/zoom gestures in the plate row above) can read as hesitant, and
       hitting the top/bottom of the pane can chain into a body-level bounce —
       both feel like "buggy" scroll even though the pane itself is fine. */
    touch-action: pan-y;
    overscroll-behavior-y: contain;
    border-top: 0; padding: 14px 14px 20px;
  }
  .map-margin-col { overflow: visible; }

  .map-sheet.tab-brethren .map-margin.is-right { display: none; }
  .map-sheet.tab-world    .map-margin.is-left  { display: none; }
  .map-sheet.tab-world    .map-pane.is-layers  { display: none; }
  .map-sheet.tab-layers   .map-margin.is-left  { display: none; }
  .map-sheet.tab-layers   .map-pane.is-world   { display: none; }

  .map-tab {
    flex: 1; padding: 12px 4px; min-height: 44px;
    background: none; border: 0; border-bottom: 2px solid transparent;
    cursor: pointer; color: var(--ink-faint);
    font-family: var(--serif-display); font-size: 11px; letter-spacing: .12em;
    text-transform: uppercase;
  }
  .map-tab.is-active { color: var(--gilt); border-bottom-color: var(--gilt); }

  /* the cartouche is prose, and prose on a phone is read at body size */
  .map-cartouche { padding: 11px 13px; }
  .map-cartouche b { font-size: 11px; }
  .map-cartouche p { font-size: 13px; line-height: 1.55; }

  /* (The 2026-07-12 phone scale — 76px, era names hidden, the year at top:50px —
     was REPLACED by the rebuilt scale at the foot of this block, where the year no
     longer collides with the half-century numerals. Its rules are gone rather than
     overridden: two sets of scale rules in one block is a lie about what is in
     force. `.map-era-lab { display: none }` went with them — the era BANDS are
     hidden outright now, so their labels have nothing to hide inside.) */

  /* ── The controls, on a phone ──
     A 26x14px pill is a desktop control shrunk onto a touchscreen. The rows
     become real touch targets (44px, the Apple/Android minimum) and the
     switches grow to match. The pill's proportions are unchanged — it is the
     same switch, at a size a thumb can actually hit. */
  .map-layers-row { padding: 13px 0; font-size: 15px; min-height: 44px; }
  .map-layers-name { font-size: 15px; }
  .map-layers-mode { font-size: 14px; }
  .map-layers-track { width: 44px; height: 26px; border-radius: 13px; }
  .map-layers-track i { width: 20px; height: 20px; top: 3px; left: 3px; }
  .map-layers-track.is-on i { left: 21px; }

  /* the key reads at body size too — it is prose, not chrome */
  .map-krow { font-size: 14.5px; padding: 6px 0; }
  .map-kmark { width: 15px; height: 15px; }

  /* the fade slider gets a real thumb, and clears the foot of the page */
  .map-fade { padding: 14px 0 8px; }
  .map-fade input[type="range"] { height: 28px; }
  .map-fade-k { font-size: 10px; }
  .map-runhead { left: 20px; right: 20px; top: 18px; }
  .map-mark.is-founded, .map-mark.is-ended { width: 14px; height: 14px; }
  .map-mark.is-active { width: 11px; height: 11px; }

  /* ── THE PLATE, DECLUTTERED (spec 2026-07-14) ──
     The phone screenshot of 1620 had seven floating chips, a zoom control over
     the running head and two lines of credit — and the engraved marks, the
     subject of the whole atlas, were the SMALLEST objects on their own plate.
     Everything that is not the map, the marks, or the plate's printed furniture
     comes off.

     This is CSS and not Rust ON PURPOSE: a Leaflet map is created ONCE, so an
     init flag would freeze the decision at load and get a rotation wrong. */

  /* Pinch and double-tap already zoom a phone; the buttons only covered the
     running head ("RAVIA · UNDER HABSBURG RULE"), which now gets its title back. */
  .map-leaflet .leaflet-control-zoom { display: none; }

  /* The arc from/to chips ("from Auspitz (Moravian heartland)" ran nearly the
     full screen width). The arc still marches origin→destination.
     ⚠ .map-arc-lbl-hit is NOT hidden — it is the transparent 120×24 tap target
     that makes a thin arc tappable, and tapping the arc is how a phone reader
     gets those names back. Hiding it would take the popover away with the label. */
  .map-arc-lbl-box { display: none; }

  /* The framed period engravings pinned on the map crowd the Moravian core. The
     same moments are rows in the World tab, and tapping a row still flies the
     map — nothing is lost; it just stops being printed on top of the map. */
  .map-world-frame-box, .map-world-seal-box { display: none; }

  /* THE CREDIT IS THE LICENCE, so it is COLLAPSED — never removed. Cliopatria is
     CC-BY 4.0 and the Esri/CARTO tiles carry their own terms: the credit must stay
     REACHABLE, not permanently printed. The ⓘ button (view.rs) toggles
     `.credit-open` on the sheet. */
  .map-leaflet .leaflet-control-attribution { display: none; }
  .map-sheet.credit-open .map-leaflet .leaflet-control-attribution {
    display: block; position: absolute; left: 8px; right: 8px; bottom: 8px;
    z-index: 700; padding: 8px 10px; line-height: 1.5; font-size: 10px;
    background: rgba(247,242,232,.96); color: #4a4030;
    border: 1px solid rgba(90,72,45,.5);
  }
  .map-credit {
    position: absolute; right: 14px; bottom: 14px; z-index: 700;
    width: 26px; height: 26px; padding: 0; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    background: rgba(247,242,232,.92); border: 1px solid rgba(90,72,45,.5);
    color: #5a4820; font-family: var(--serif-display); font-size: 13px;
    line-height: 1; cursor: pointer;
  }

  /* ── THE NAMES, ENGRAVED ──
     A colony name was the last FLOATING SURFACE left on the plate: an opaque dark
     chip with a blur, a shadow and a 3px gold border — a UI panel sitting on a
     printed page. The realm labels (.map-realm-lbl) already print the atlas way —
     ink, a paper halo, no chip — and the names now join them. Same words, a
     fraction of the ink, and the plate reads as cartography instead of as an app.

     The KIND survives as the colour of the INK, not as a border. Colours are
     LITERAL, not theme tokens: the basemap is always light regardless of the app
     theme (the .map-colony-lbl rationale). */
  .map-colony-lbl {
    background: none; border-left: 0; border-radius: 0; padding: 0;
    box-shadow: none; -webkit-backdrop-filter: none; backdrop-filter: none;
    font-size: 11.5px; font-weight: 700; letter-spacing: .02em;
    color: #3a3226;
    text-shadow: 0 1px 3px rgba(247,242,232,.95), 0 0 2px rgba(247,242,232,.9),
                 0 0 6px rgba(247,242,232,.7);
  }
  .map-colony-lbl.is-founded { color: #8a6a12; }  /* gold, in ink */
  .map-colony-lbl.is-ended,
  .map-colony-lbl.is-raided  { color: #8f2020; }  /* oxblood, in ink */
  /* The tapped colony: NEUTRAL ink. A colony tapped in a quiet year has no event,
     and colouring it as a founding would be a lie told in gold. */
  .map-colony-lbl.is-selected { color: #3a3226; }
  /* "Show all" extras: same neutral ink — no event to report, so no colour. */
  .map-colony-lbl.is-plain { color: #3a3226; }

  /* ── THE SCALE, ON A PHONE (rebuilt 2026-07-14) ──
     Three complaints, all real.
     (1) IT LOOKED BROKEN: the year sat at top:50px and the half-century numerals
         at top:39px in 10px type — they ran into each other. Every row now has its
         own band and they cannot touch.
     (2) IT ATE TOO MUCH HEIGHT: 76px. The ERA BANDS go — they are texture, and the
         live era is already named in full beside the year — which buys 24px back.
     (3) IT WAS HARD TO SCRUB: see the lifted readout and the ‹ › steppers below.

     56px, not the 48 the spec guessed: the ticks, the numerals and the year cannot
     fit in 48 without re-creating the very collision this rebuild exists to remove. */
  .map-scale { height: 56px; position: relative; }
  .map-eras { display: none; }            /* the bands: texture, not information */
  .map-axis { top: 0; }
  .map-tick { top: 0; }
  .map-ticklab { display: none; }         /* 14 decade numerals cannot fit at 2.5px/yr */
  .map-ticklab.is-major { display: block; top: 16px; font-size: 10px; }
  .map-playhead { top: 0; height: 30px; }
  .map-playyr { top: 30px; gap: 6px; }
  .map-playyr .y { font-size: 17px; }
  .map-playyr .e { font-size: 8px; letter-spacing: .1em; }
  /* The DRAWN glyph, not the word: "PLAY" in letter-spaced small caps cost 62px of a
     390px scale. The title/aria-label keep the control named for a screen reader. */
  /* ON A PHONE ALL THREE CONTROLS ARE THE SAME COLUMN and centre their own
     glyph — that is what makes the two gaps equal. Play drops its word, its
     74px width and its 16px left padding here: keeping the padding is what
     pushed the next chevron away in the 2026-07-19 screenshot.

     COMPACT: 30px columns, not 40. The cluster went 122px → 92px of a ~390px
     scale, giving ~30px back to the ruler — which is the scarce thing here
     (at ~2.5px/year every pixel of track is a year you can point at, and this
     whole phone scale was rebuilt once already for eating too much room).
     The HEIGHT stays 44px: that is the touch target, and it costs nothing
     because the scale is 56px tall regardless. A 30×44 button is still a
     comfortable hit — it is narrower than the 44px guidance, but these sit in
     a row with 2px between them and nothing else is tappable nearby, so a
     near-miss lands on the neighbouring step, never on something destructive. */
  .map-scale .map-play {
    width: 30px; padding: 0; justify-content: center; text-align: center;
  }
  .map-play-w { display: none; }
  /* ONE rule for the phone play glyph — its display flip and its size together.
     Split across two `.map-play-i` rules, the second silently depends on the
     first for `display:block`; anyone deleting either leaves the glyph hidden
     or full-size. The glyphs shrink WITH the columns (a 13px mark in a 30px
     box reads loose), but the stroke weight holds at 2.8 — that is the
     operator's drawn weight, and it is what keeps a small chevron reading as a
     confident mark rather than a hairline. */
  .map-play-i { display: block; width: 12px; height: 14px; }
  .map-step-i { width: 12px; height: 14px; }

  .map-step { width: 30px; height: 44px; }
  .map-step.is-prev { margin-left: 0; }

  /* THE READOUT LIFTS OUT FROM UNDER THE THUMB.
     While dragging, the year is the one thing you need to see and the one thing
     your finger is on top of. It rises above the band, onto the plate — which needs
     the scale to out-rank Leaflet's panes (z 400–600) for the duration. */
  .map-playyr.is-drag {
    top: -42px; z-index: 700;
    padding: 4px 10px; border-radius: 3px;
    background: rgba(247,242,232,.97);
    border: 1px solid rgba(90,72,45,.55);
    box-shadow: 0 3px 10px rgba(0,0,0,.25);
  }
  .map-scale:has(.map-playyr.is-drag) { z-index: 700; }
}

/* ── Map register: the three tabs + the Colonies queue ──────────────────────
   The tab bar replaces the old <h2 class="rr-section-h"> headings: the three
   registers (Colonies / Raids / Migrations) are now panels, and only the active
   one renders. The bar stays mounted while the panel beneath it swaps. */
.rr-tabs {
  display: flex;
  gap: 2px;
  margin: 18px 0 0;
  border-bottom: 1px solid var(--rule, #d8d2c4);
}
.rr-tab {
  appearance: none; background: none; cursor: pointer;
  font: inherit; font-size: 14px; letter-spacing: .04em;
  padding: 8px 14px; margin-bottom: -1px;
  color: inherit; opacity: .55;
  border: 1px solid transparent; border-bottom: none;
  border-radius: 6px 6px 0 0;
  /* Feedback the press wants: opacity/background/transform ease, but NOT the
     rebuild-lagged panel. Kept short so the highlight still reads as instant. */
  transition: opacity .12s ease, background-color .12s ease, transform .06s ease;
}
.rr-tab:hover { opacity: .85; }
/* A focus ring for keyboard/AT users (the buttons had none). --gilt is the
   theme-aware accent (--accent is only set inline on cards, not at :root). */
.rr-tab:focus-visible {
  outline: 2px solid var(--gilt, #b39250); outline-offset: 2px;
}
/* Press feedback: the button gives back on mousedown, so a click never reads as
   dead even in the tick before the deferred panel paints. */
.rr-tab:active { transform: translateY(1px); opacity: .9; }
.rr-tab.is-active {
  opacity: 1; font-weight: 600;
  border-color: var(--rule, #d8d2c4);
  background: var(--rule-soft);
}
/* Reserve the BOLD width up front so activating a tab doesn't widen its label and
   shove the neighbours sideways (the layout jump). A hidden bold twin sets the box;
   the visible text tracks weight. data-label is mirrored from the button text. */
.rr-tab::after {
  content: attr(data-label);
  display: block; height: 0; overflow: hidden; visibility: hidden;
  font-weight: 600; letter-spacing: .04em;
}

/* The count line: what we hold, and how much of it is still unknown. */
.rr-colony-count {
  font-size: 12.5px; opacity: .7; margin: 14px 0 4px;
}
.rr-colony-group-h {
  margin: 18px 0 2px;
  padding-bottom: 4px;
  border-bottom: 1px solid var(--rule, #d8d2c4);
  font-size: 14px;
  letter-spacing: .05em;
  color: var(--ink-dim);
}
.rr-colony-sorted { font-style: italic; opacity: .8; }

/* A colony row. The flag is the queue's rank made visible: ⚠ still needs a
   human, ✓ placed and dated. */
.rr-colony-row { align-items: center; }
.rr-colony-flag {
  width: 20px; flex-shrink: 0;
  font-size: 14px; opacity: .75; text-align: center;
}
.rr-colony-flag.is-done { opacity: .35; }
.rr-colony-id {
  font-size: 11px; opacity: .45; margin-left: 8px; font-weight: 400;
}
.rr-colony-coords {
  font-size: 12px; opacity: .7; margin-top: 2px;
  font-variant-numeric: tabular-nums;
}
/* No coordinate at all — one of the eleven the map's data could never carry. */
.rr-colony-coords.is-missing { font-style: italic; opacity: .5; }
.rr-colony-dates { font-size: 12px; opacity: .7; margin-top: 1px; }
/* An INFERRED end must READ as a machine guess, not as a fact — it is the
   inferencer's weak-1622 default, and it is nowhere in the sources. */
.rr-colony-inferred { font-style: italic; opacity: .5; }
.rr-colony-inferred-hint {
  grid-column: 1 / -1;
  font-size: 12px; font-style: italic; opacity: .7;
  padding: 8px 10px; margin-top: -2px;
  border-left: 2px solid var(--rule, #d8d2c4);
}

/* A SUGGESTED coordinate: cited, and never written until a human presses save. */
.rr-colony-suggestion {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  font-size: 12.5px;
  padding: 8px 10px;
  border: 1px dashed var(--rule, #d8d2c4); border-radius: 6px;
}
.rr-sugg-coord { font-variant-numeric: tabular-nums; font-weight: 600; }
.rr-sugg-town { opacity: .75; }
.rr-sugg-use {
  font: inherit; font-size: 12px; padding: 3px 10px; cursor: pointer;
  margin-left: auto;
}
/* A doubt we hold is a doubt the contributor must see. */
.rr-sugg-note {
  flex-basis: 100%;
  font-size: 11.5px; font-style: italic; opacity: .7; margin-top: 2px;
}

@media (max-width: 599px) {
  .rr-tabs { gap: 0; }
  .rr-tab { padding: 8px 10px; font-size: 13px; }
  .rr-sugg-use { margin-left: 0; }
}

/* Why this town — the IDENTIFICATION's source, a different claim from the
   coordinate's. Where the chronicle answers it, this quotes the chronicle. */
.rr-sugg-basis {
  flex-basis: 100%;
  font-size: 11.5px; opacity: .8; margin-top: 4px; line-height: 1.45;
  padding-left: 8px; border-left: 2px solid var(--rule, #d8d2c4);
}
