/* Southwest Missouri data-center entity map.
   Colour roles are defined once here and referenced by role everywhere else.
   The three categorical hues are a validated set: they clear the lightness band,
   chroma floor, all-pairs CVD separation and normal-vision floor in both modes.
   Shape and fill carry the remaining category distinctions, so no category is
   ever identified by colour alone.

   That set was validated against itself but not against what it sits on, which
   is a separate question and the one WCAG asks. Every token here now also
   clears its threshold against its own background: 4.5:1 for text (SC 1.4.3)
   and 3:1 for glyphs, edges, age rings and control borders (SC 1.4.11), in
   both modes. Re-check both properties together before moving any value —
   darkening a hue for the light plane is what pulls it toward its neighbours
   under simulated protanopia. */

:root {
  color-scheme: dark;

  --plane:        #0d0d0d;
  --surface:      #1a1a19;
  --surface-2:    #232321;
  --ink:          #ffffff;
  --ink-2:        #c3c2b7;
  --ink-muted:    #8b8984;
  --hairline:     #2c2c2a;
  --border:       rgba(255, 255, 255, 0.10);
  /* --border is decorative and doubles as a hover wash. A control's edge is
     information — SC 1.4.11 wants 3:1 for it — so it gets its own token. */
  --control:      rgba(255, 255, 255, 0.34);

  --hue-principals:    #3f8be6;
  --hue-counterparties:#d95926;
  --hue-public:        #199e70;
  --hue-places:        #8e8c85;

  --edge:         #6c6a62;
  --edge-strong:  #94918a;
  --accent:       #3f8be6;
  --cta:          #1d63b8;

  --age-old:      #61605c;
  --age-new:      #e8e7e0;

  /* The freshness halo. Achromatic, like the age ramp and for the same reason:
     time is not one of the three categorical hues, a fourth would have to
     re-clear the all-pairs colour-vision separation the others were chosen to
     pass, and a wash in a category's own colour would muddy the very glyph it
     is pointing at. --fresh-veil is the halo's peak opacity, which has to
     differ by mode — light on a dark plane carries much further than dark on
     a light one. */
  --fresh:        #fbf8ec;
  --fresh-veil:   0.46;

  /* The plate a company's logo is fitted onto, inside its glyph. Unlike every
     other colour here these two do NOT flip with the theme, because the thing
     they sit behind does not: a wordmark lifted from a dark site header is
     white artwork in both modes, and a plate that followed the theme would
     make it vanish in one of them. Each logo declares which of the two it was
     drawn for (`logo_on` in entities.yaml) and gets that plate in both modes.
     They are near-black and near-white rather than the real thing so the plate
     still reads as a surface of this page rather than a hole punched in it. */
  --logo-light:   #fbfbf9;
  --logo-dark:    #131315;

  --r:   10px;
  --r-s: 7px;
  --pad: 18px;
  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
}

:root[data-theme="light"] {
  color-scheme: light;
  --plane:        #f9f9f7;
  --surface:      #fcfcfb;
  --surface-2:    #f0efec;
  --ink:          #0b0b0b;
  --ink-2:        #52514e;
  --ink-muted:    #6e6c67;
  --hairline:     #e1e0d9;
  --border:       rgba(11, 11, 11, 0.10);
  --control:      rgba(11, 11, 11, 0.45);

  --hue-principals:    #266dc3;
  --hue-counterparties:#eb6834;
  --hue-public:        #19a371;
  --hue-places:        #6f6d66;

  --edge:         #929088;
  --edge-strong:  #6f6d66;
  --accent:       #266dc3;
  --cta:          #1d63b8;

  --age-old:      #91908b;
  --age-new:      #2e2d2a;

  /* On paper the loud end of an achromatic scale is the dark end, so the halo
     inverts with the plane and reads as a lift rather than a light. Kept well
     below the dark mode's peak: the same opacity there is a smudge. */
  --fresh:        #2f2d27;
  --fresh-veil:   0.3;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  /* Mobile browsers grow and shrink the viewport as their chrome slides in and
     out. dvh follows that; the vh above it is the fallback for anything that
     does not know the unit, and is what desktop uses either way. */
  height: 100dvh;
  overflow: hidden;
  /* No rubber-banding or pull-to-refresh behind a map you drag with a finger. */
  overscroll-behavior: none;
  background: var(--plane);
  color: var(--ink);
  font-family: var(--font);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

button, input, select { font: inherit; color: inherit; }
a { color: var(--accent); }

/* One focus indicator for everything, rather than a per-control improvisation.
   2px on an offset, so the ring sits on the background it has to contrast
   against — --accent clears 3:1 on every surface in both themes. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* Available to a screen reader, absent from the page. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden; white-space: nowrap;
}

/* The sidebar puts ~30 tab stops in front of the map. This is the way past. */
.skip-link {
  position: absolute; z-index: 100; top: 8px; left: 8px;
  padding: 10px 16px; min-height: 44px;
  display: inline-flex; align-items: center;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--control); border-radius: var(--r-s);
  text-decoration: none; font-size: 13px;
  transform: translateY(-160%);
  transition: transform 0.15s;
}
.skip-link:focus { transform: none; }
@media (prefers-reduced-motion: reduce) { .skip-link { transition: none; } }

/* ------------------------------------------------------------- layout --- */

#app {
  display: grid;
  grid-template-columns: 296px 1fr;
  grid-template-rows: 1fr auto;
  height: 100%;
}

#stage { position: relative; grid-row: 1; overflow: hidden; }
#canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#canvas.dragging { cursor: grabbing; }
/* The map takes keyboard focus as a whole; which entity the cursor is on is
   drawn inside the canvas, by _drawKeyboardRing in js/graph.js. */
#canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Never shown on screen — it exists only so the canvas's aria-describedby has
   something to read to a screen reader. A sighted reader gets the same keys
   from the Keyboard shortcuts link in the sidebar footer instead. */
#canvas-help {
  position: absolute; z-index: 24; left: 16px; bottom: 14px;
  max-width: 340px; padding: 9px 12px;
  background: var(--surface); border: 1px solid var(--control);
  border-radius: var(--r-s); color: var(--ink-2);
  font-size: 11.5px; line-height: 1.55;
  /* Clipped rather than display:none, so it stays in the accessibility tree
     for the canvas's aria-describedby while it is out of sight. */
  clip-path: inset(50%);
  pointer-events: none;
}

/* ------------------------------------------------------------ sidebar --- */

/* Below the breakpoint the sidebar leaves the grid and slides over the map as
   a drawer; these two exist only for that, and stay out of the way until then. */
#sidebar-close, #open-menu { display: none; }

#scrim {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0; pointer-events: none;
  transition: opacity 0.24s cubic-bezier(0.4, 0, 0.2, 1);
}
#scrim[hidden] { display: none; }

/* The sidebar is a stack of cards on the shell's own surface, not one strip of
   text divided by air. The masthead and each .group carry their own plane, so
   "which build", "find an entity" and "recent developments" read as three
   places rather than three paragraphs — the 10.5px labels no longer have to do
   that work alone. The shell's padding drops to the size of the gap between
   the cards, because the cards now carry the inset themselves; keeping
   var(--pad) here on top of their own padding would inset the text twice. */
#sidebar {
  grid-row: 1 / span 2;
  background: var(--surface);
  border-right: 1px solid var(--hairline);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* surface-2 on surface is 1.11:1 in dark and 1.12:1 in light — a tint, not an
   edge — so the fill alone would not say "card". The hairline is what draws
   the boundary, and it is the same surface-2 + --border + --r-s the plates and
   the modal card already use, one plane up from whatever it sits on. */
#sidebar .masthead, #sidebar .group {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-s);
  padding: 13px 14px 14px;
}

.masthead h1 {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: 1.3;
}
.masthead p {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-muted);
}
.masthead .counts { margin-top: 8px; font-variant-numeric: tabular-nums; }

.group > h2 {
  margin: 0 0 9px;
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-muted);
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
}
.group > h2 button {
  background: none; border: 0; cursor: pointer;
  /* SC 2.5.8 wants 24x24. The negative margin keeps the padding from moving
     the baseline it is aligned on. */
  padding: 6px 4px; margin: -6px -4px; min-height: 24px;
  font-size: 10.5px; letter-spacing: 0.04em; text-transform: none;
  color: var(--ink-muted); text-decoration: underline; text-underline-offset: 2px;
}
.group > h2 button:hover { color: var(--ink); }

/* Not a card. It is a row of meta links, not a section of the thing the page
   is about, and the rule above it already separates it. The side padding only
   lines its text up with the card text above; the rule itself spans the same
   column the cards do. */
.sidebar-foot {
  margin-top: auto;
  padding: 13px 15px 3px;
  border-top: 1px solid var(--hairline);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 7px;
  font-size: 11px;
  color: var(--ink-muted);
}
.sidebar-foot a, .sidebar-foot .link-btn {
  color: var(--ink-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.sidebar-foot .link-btn { background: none; border: 0; padding: 0; font: inherit; cursor: pointer; }
.sidebar-foot a:hover, .sidebar-foot .link-btn:hover { color: var(--ink); }

/* -------------------------------------------------------------- search --- */

.search-wrap { position: relative; }
#search {
  width: 100%;
  padding: 9px 34px 9px 11px;
  background: var(--surface-2);
  border: 1px solid var(--control);
  border-radius: var(--r-s);
}
#search:focus-visible { border-color: var(--accent); }
/* In the sidebar the field sits on a surface-2 card, which is its own fill, so
   it takes the shell's plane instead and stays a well rather than a rectangle
   drawn in its background's colour. It reads the same way in both modes: sunk
   in dark, a white field on grey paper in light. */
#sidebar #search { background: var(--surface); }
#search-clear {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  background: none; border: 0; cursor: pointer; color: var(--ink-muted);
  /* 26x26 — SC 2.5.8 is 24x24, and this one sits inside another target. */
  width: 26px; height: 26px; padding: 0; line-height: 1;
  display: grid; place-items: center; border-radius: 4px;
}
#search-clear:hover { color: var(--ink); }

#suggest {
  position: absolute; z-index: 40; left: 0; right: 0; top: calc(100% + 4px);
  background: var(--surface-2);
  border: 1px solid var(--control);
  border-radius: var(--r-s);
  max-height: 264px; overflow-y: auto;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.42);
}
#suggest:empty { display: none; }
#suggest [role="option"] {
  display: flex; align-items: center; gap: 9px;
  width: 100%; text-align: left; padding: 8px 11px;
  cursor: pointer; min-height: 24px;
}
#suggest [role="option"]:hover { background: var(--border); }
/* The visual cursor for aria-activedescendant: focus never leaves the input,
   so the active option has to look active on its own. */
#suggest [role="option"][aria-selected="true"] {
  background: var(--border);
  box-shadow: inset 2px 0 0 var(--accent);
}
#suggest .s-name { flex: 1; font-size: 13px; }
#suggest .s-cat { font-size: 11px; color: var(--ink-muted); }

/* ------------------------------------------------------------- legend --- */

.family { margin-bottom: 13px; }
/* Inside a padded card the last family's margin is the card's own bottom
   padding again, and the card reads bottom-heavy. */
#legend .family:last-child { margin-bottom: 0; }
.family-label {
  font-size: 11px; font-weight: 600; color: var(--ink-2);
  margin-bottom: 5px; display: block;
}

.check {
  display: flex; align-items: center; gap: 9px;
  padding: 4px 6px; margin: 0 -6px;
  border-radius: 5px; cursor: pointer; user-select: none;
  line-height: 1.35;
}
.check:hover { background: var(--border); }
.check input { position: absolute; opacity: 0; pointer-events: none; }
/* The input is visually hidden but still tabbable, so keyboard focus needs to
   be visible on the row that stands in for it. */
.check:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
.check .label { flex: 1; font-size: 12.5px; }
.check .n { font-size: 11px; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
.check.off .label, .check.off .n { color: var(--ink-muted); opacity: 0.55; }
.check.off .swatch { opacity: 0.3; }
.swatch, svg.line { flex: none; vertical-align: -0.18em; }

/* ---------------------------------------------------------- which build --- */
/* One of four, so the row carries a radio's mark rather than a checkbox's:
   a ring that fills. The rows are the map's first question, and they are the
   only control in the sidebar that changes what the map is about. */
.proj { gap: 10px; }
.proj::before {
  content: ""; flex: none; width: 11px; height: 11px;
  border-radius: 50%; border: 1.5px solid var(--edge-strong);
  /* The ring is hollow by knocking its middle out to whatever is behind it,
     which is the .group card, not the sidebar's own surface. */
  box-shadow: inset 0 0 0 3px var(--surface-2);
  background: transparent;
}
.proj:has(input:checked)::before {
  border-color: var(--accent);
  background: var(--accent);
}
.proj:has(input:checked) .label { color: var(--ink); font-weight: 600; }
.proj .label { color: var(--ink-2); }

/* -------------------------------------------------------------- filters --- */
.filters-open {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; margin-top: 11px; min-height: 34px;
  padding: 8px 11px;
  background: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: 6px;
  color: var(--ink-2); font: inherit; font-size: 12.5px; text-align: left;
}
.filters-open:hover { color: var(--ink); border-color: var(--edge-strong); }
.filters-open-label { display: flex; align-items: center; gap: 8px; }
/* The funnel says what the button is for at a glance; the word says it for
   everyone the glance is not available to. Neither is carried into the dialog
   it opens — a heading that repeats its own button's icon is decoration. */
.funnel { flex: none; opacity: 0.75; }
.filters-open:hover .funnel { opacity: 1; }
.filters-count {
  flex: none; min-width: 18px; padding: 1px 5px;
  border-radius: 9px; background: var(--accent); color: var(--plane);
  font-size: 10.5px; font-weight: 650; text-align: center;
  font-variant-numeric: tabular-nums;
}
.dialog[data-width="mid"] .modal-card { max-width: 560px; }
/* The same treatment as the sidebar, one plane up: these sit on .modal-card,
   which is itself var(--surface). The 26px of air that used to be the only
   thing between one h3 and the next becomes the gap between two contained
   blocks, so the card padding carries the breathing room instead of stacking
   on top of it. */
#filters-modal .group {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-s);
  padding: 15px 16px 16px;
}
/* .modal-card h3 carries a 26px top margin for the flow layout it was written
   for. Inside a padded card that margin is the card's own padding a second
   time, and it does not collapse through the padding. */
#filters-modal .group > h3 { margin-top: 0; }
#filters-modal .group + .group { margin-top: 10px; }
#filters-modal .modal-card h3 { display: flex; justify-content: space-between;
  align-items: baseline; gap: 8px; }
#filters-modal .modal-card h3 button {
  background: none; border: 0; cursor: pointer;
  padding: 6px 4px; margin: -6px -4px; min-height: 24px;
  font-size: 10.5px; letter-spacing: 0.04em; text-transform: none; font-weight: 400;
  color: var(--ink-muted); text-decoration: underline; text-underline-offset: 2px;
}
#filters-modal .modal-card h3 button:hover { color: var(--ink); }
.filters-foot {
  margin-top: 26px; padding-top: 16px; border-top: 1px solid var(--hairline);
}
#filters-reset {
  min-height: 34px; padding: 8px 14px;
  background: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: 6px;
  color: var(--ink-2); font: inherit; font-size: 12.5px;
}
#filters-reset:hover { color: var(--ink); border-color: var(--edge-strong); }

.tier-row .line { flex: none; width: 24px; height: 12px; }
.note { font-size: 11px; line-height: 1.5; color: var(--ink-muted); margin: 7px 0 0; }

.age-ramp { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--ink-muted); }
.age-ramp .bar {
  flex: 1; height: 7px; border-radius: 4px;
  background: linear-gradient(90deg, var(--age-old), var(--age-new));
  border: 1px solid var(--border);
}

/* ------------------------------------------- recent developments --- */
/* The one list on this page that is written rather than derived. It reads as
   prose — a title, a date, and the sentence that says what happened — because
   that is what it is; the entity rows elsewhere are indexes and are styled
   like indexes. Each row is a button onto its own dialog, so the whole row is
   the target, not a link buried in it. */

.dev-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.dev-go {
  width: 100%; text-align: left; cursor: pointer;
  background: none; border: 0; border-radius: var(--r-s);
  padding: 9px 8px 10px; margin: 0 -8px;
  display: grid; gap: 3px;
}
/* The same wash .check uses. It was var(--surface-2) when the row sat on the
   sidebar's plain surface; on a surface-2 card that is the card's own colour
   and the hover disappears, so it moves to the tint that reads on anything. */
.dev-go:hover { background: var(--border); }
.dev-go:hover .dev-title { color: var(--accent); }
/* The title gets the full column. A chip beside it would squeeze a seven-word
   headline into three lines to sit next to two words, so the chip goes on the
   meta line, where the date already is and where it costs nothing. */
.dev-title { font-size: 12.5px; font-weight: 600; line-height: 1.35; color: var(--ink); }
.dev-meta { display: flex; align-items: center; gap: 7px; }
/* An outline rather than a fill: the chip groups entries, it does not rank
   them, and a filled chip beside a NEW chip would read as the same kind of
   statement. */
.dev-kind {
  flex: none; font-size: 9.5px; font-weight: 650; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-muted);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 2px 7px; line-height: 1.4;
}
.dev-when {
  font-size: 11px; color: var(--ink-muted); font-variant-numeric: tabular-nums;
}
.dev-summary { font-size: 11.5px; line-height: 1.52; color: var(--ink-2); }

/* The panel's own marker. A filled chip, so it survives greyscale — it is the
   same statement the NEW chip makes on the canvas. It does not repeat how wide
   the window is, because the entity's exact date is the next line down. */
.p-new {
  background: var(--fresh); color: var(--plane);
  font-size: 10px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 2px 6px; border-radius: 999px; line-height: 1.5;
}

/* ---------------------------------------------------------- timeline --- */

#timeline {
  grid-column: 2;
  grid-row: 2;
  background: var(--surface);
  border-top: 1px solid var(--hairline);
  padding: 11px 20px 13px;
  display: flex;
  align-items: center;
  gap: 15px;
}
#play {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--control);
  cursor: pointer; display: grid; place-items: center; color: var(--ink);
}
#play:hover { border-color: var(--accent); }

.scrub { flex: 1; display: flex; flex-direction: column; gap: 3px; }
.scrub-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 11px; color: var(--ink-muted);
}
.scrub-head strong {
  color: var(--ink); font-size: 13px; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
#scrubber { width: 100%; accent-color: var(--accent); cursor: pointer; }

/* ------------------------------------------------------------- panel --- */

#panel {
  position: absolute; top: 0; right: 0; bottom: 0; width: 396px; max-width: 92vw;
  background: var(--surface);
  border-left: 1px solid var(--hairline);
  overflow-y: auto; overscroll-behavior: contain;
  transform: translateX(100%);
  transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 30;
  box-shadow: -18px 0 42px rgba(0, 0, 0, 0.3);
}
#panel.open { transform: none; }
.panel-inner { padding: 20px var(--pad) 40px; }

#panel-close {
  position: sticky; top: 10px; float: right;
  background: var(--surface-2); border: 1px solid var(--control);
  width: 28px; height: 28px; border-radius: 50%;
  cursor: pointer; display: grid; place-items: center; color: var(--ink-2);
  z-index: 2;
}
#panel-close:hover { color: var(--ink); border-color: var(--edge-strong); }

.p-kicker {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: 11px; color: var(--ink-muted); margin-bottom: 7px;
}
.p-title { margin: 0 0 3px; font-size: 19px; line-height: 1.25; font-weight: 650; letter-spacing: -0.015em; }
.p-title .p-verb { color: var(--ink-2); font-weight: 500; }
.p-date { font-size: 12px; color: var(--ink-muted); margin: 0 0 15px; }
/* The head of an entity panel. A person leads with a face — their portrait, or
   the blank avatar that says we have no picture of them. An organisation the
   file has a logo for leads with the logo. An organisation it does not has no
   mark at all — there is no blank logo, because an absent wordmark says
   nothing about a company — and the text simply takes the width back. */
.p-head {
  display: flex; align-items: flex-start; gap: 13px; margin: 0 0 15px;
}
.p-head-text { min-width: 0; flex: 1; }
.p-head .p-date { margin-bottom: 0; }
.p-face {
  position: relative; flex: none; width: 80px; height: 80px;
  border-radius: 50%; overflow: hidden; background: var(--surface-2);
}
.p-face img { display: block; width: 100%; height: 100%; object-fit: cover; }
.p-face .avatar { display: block; }
/* The hue stays as a ring around the portrait, the way it does on the map: a
   picture of someone never costs the node the colour it is read by. The blank
   avatar needs no ring — it is already drawn in that colour. */
.p-face-photo::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  box-shadow: inset 0 0 0 2px currentColor;
}

/* A company's wordmark. Not a circle and not cropped: a wordmark is wide and
   half of one identifies nobody, so the plate takes the mark's own proportions
   up to a limit and the mark is fitted whole inside it. The plate colour is
   fixed by the artwork rather than by the theme — see --logo-light above — and
   the hue stays as a ring around it exactly as it does around a portrait. */
.p-mark {
  position: relative; flex: none;
  height: 56px; max-width: 148px; padding: 7px 10px;
  border-radius: var(--r-s); overflow: hidden;
}
.p-mark img {
  display: block; height: 100%; width: auto; max-width: 100%;
  object-fit: contain;
}
.p-mark-light { background: var(--logo-light); }
.p-mark-dark  { background: var(--logo-dark); }
.p-mark::after {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-s);
  box-shadow: inset 0 0 0 2px currentColor;
}

.p-because {
  font-size: 12.5px; line-height: 1.5; color: var(--ink-2);
  margin: 0 0 15px; display: flex; gap: 6px;
}
.p-because-arrow { color: var(--ink-muted); flex: none; }
.p-body { font-size: 13.5px; line-height: 1.62; color: var(--ink-2); margin: 0 0 15px; }

/* A cited image, shown rather than only linked. The plates sit under the summary
   because that is the sentence they answer, and they are deliberately small: the
   panel is a reading column, and a photograph that fills it stops being evidence
   and starts being decoration. Full size is one click away, which is also where
   anything worth looking at closely has to be read anyway. */
.p-plates {
  display: grid; grid-template-columns: 1fr 1fr; gap: 9px;
  margin: 0 0 16px;
}
.p-plates-one { grid-template-columns: 1fr; }
.plate { margin: 0; min-width: 0; }
.plate a {
  display: block; border-radius: var(--r-s);
  border: 1px solid var(--border); overflow: hidden; background: var(--surface-2);
}
.plate a:hover { border-color: var(--edge-strong); }
.plate a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* One ratio for every plate, so two photographs that were not framed alike still
   make a tidy pair. The crop is cosmetic — the file behind the link is untouched. */
.plate img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover;
}
.p-plates-one .plate img { aspect-ratio: 16 / 9; }
.plate figcaption {
  margin-top: 6px; font-size: 10.5px; line-height: 1.4; color: var(--ink-muted);
  text-wrap: pretty;
}
.plate-date {
  display: block; font-variant-numeric: tabular-nums; opacity: 0.8;
}

.p-caveat {
  border-left: 2px solid var(--hue-counterparties);
  background: var(--surface-2);
  padding: 10px 13px; border-radius: 0 var(--r-s) var(--r-s) 0;
  font-size: 12.5px; line-height: 1.6; color: var(--ink-2); margin: 0 0 15px;
}
.p-caveat b { color: var(--ink); font-weight: 600; }

.p-section {
  margin: 20px 0 9px; font-size: 10.5px; font-weight: 650;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-muted);
}

.cite {
  border: 1px solid var(--border); border-radius: var(--r-s);
  padding: 11px 13px; margin-bottom: 9px; background: var(--surface-2);
}
.cite-head { display: flex; gap: 8px; align-items: baseline; justify-content: space-between; }
.cite-label { font-size: 12.5px; font-weight: 600; line-height: 1.4; }
.cite-date { font-size: 11px; color: var(--ink-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.cite blockquote {
  margin: 9px 0 0; padding-left: 10px;
  border-left: 2px solid var(--hairline);
  font-size: 12.5px; line-height: 1.6; color: var(--ink-2);
}
/* The archived copy, then the live one — stacked, so the order is the reading
   order and the long repository path never crowds the link beside it. */
.cite-links { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; margin-top: 9px; }
.cite a.doc {
  display: inline-block; font-size: 11.5px;
  word-break: break-all; text-decoration: none; color: var(--accent);
}
.cite a.doc:hover { text-decoration: underline; }
.cite .ext {
  display: inline-block; font-size: 11px; color: var(--ink-muted);
  border: 1px solid var(--border); border-radius: 4px; padding: 2px 7px;
}
/* A live url is the one part of a citation that can rot, so it is marked as
   what it is rather than left to look like the archived file beside it. */
.cite a.live, .doc-live {
  display: inline-flex; align-items: baseline; gap: 6px; font-size: 11.5px;
  text-decoration: none; color: var(--ink-2); word-break: break-word;
}
.cite a.live:hover, .doc-live:hover { color: var(--accent); text-decoration: underline; }
.live-tag {
  flex: none; font-size: 9.5px; font-weight: 650; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-muted);
  border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px;
}
.cite a.live:hover .live-tag, .doc-live:hover .live-tag { border-color: var(--accent); color: var(--accent); }

/* Connections are already drawn on the canvas, so the panel keeps its copy of
   them folded away rather than making the reader scroll past it to reach
   anything else. <details> carries the open/closed state and the keyboard. */
.p-fold { margin-top: 20px; }
.p-fold-head {
  margin: 0; cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 7px;
}
.p-fold-head::-webkit-details-marker { display: none; }
.p-fold-head::before {
  content: '';
  width: 5px; height: 5px; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); transition: transform 0.15s;
}
.p-fold[open] > .p-fold-head::before { transform: rotate(45deg); }
.p-fold-head:hover { color: var(--ink-2); }
.p-fold-head:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.p-fold-hint {
  font-weight: 500; letter-spacing: 0.04em; text-transform: none;
  font-size: 10px; opacity: 0.7;
}

/* The chain of title. Each step is one conveyance; the rule running down the
   left and the arrow on it are the flow of ownership from one holder to the
   next, which is the whole reason these are a sequence and not a list. */
.chain { list-style: none; margin: 0; padding: 0; }
.chain-step { position: relative; padding-left: 18px; }
.chain-step::before {
  content: ''; position: absolute; left: 3.5px; top: 6px; bottom: 6px;
  border-left: 1px solid var(--edge-strong); opacity: 0.45;
}
.chain-step:first-child::before { top: 14px; }
.chain-step:last-child::before { bottom: auto; height: 10px; }
/* The arrow sits on the rule between one step and the next, knocking a hole in
   it, so the ownership reads as one movement down the column. */
.chain-step + .chain-step::after {
  content: '↓'; position: absolute; left: 0; top: -14px;
  width: 8px; text-align: center;
  font-size: 12.5px; line-height: 1; color: var(--ink-2);
  background: var(--surface); padding: 2px 0;
}
.chain-step + .chain-step { margin-top: 20px; }
.chain-flow {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 5px;
  font-size: 12.5px; font-weight: 600; line-height: 1.4;
}
.chain-to { color: var(--ink-muted); font-weight: 400; flex: none; }
.chain-meta {
  font-size: 11px; color: var(--ink-muted); margin-top: 3px; line-height: 1.45;
  font-variant-numeric: tabular-nums;
}
.chain-note {
  font-size: 12px; line-height: 1.55; color: var(--ink-2);
  margin: 8px 0 0; padding-left: 9px;
  border-left: 2px solid var(--hue-counterparties);
}
.chain .cite { margin-top: 9px; margin-bottom: 0; }

.rel { display: block; width: 100%; text-align: left; background: none;
  border: 0; border-top: 1px solid var(--hairline); padding: 9px 2px; cursor: pointer; }
.rel:hover .rel-name { color: var(--accent); }
.rel-name { font-size: 13px; font-weight: 550; display: flex; align-items: center; gap: 7px; }
.rel-desc { font-size: 11.5px; color: var(--ink-muted); margin-top: 2px; line-height: 1.45; }

/* --------------------------------------------------------- tooltip --- */

#tooltip {
  position: absolute; z-index: 25; pointer-events: none;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-s); padding: 9px 12px; max-width: 292px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  opacity: 0; transition: opacity 0.1s;
}
#tooltip.show { opacity: 1; }
#tooltip .t-name { font-size: 13px; font-weight: 600; line-height: 1.35; }
#tooltip .t-meta { font-size: 11px; color: var(--ink-muted); margin-top: 3px; display: flex; gap: 7px; align-items: center; }
#tooltip .t-cites { font-size: 11px; color: var(--ink-2); margin-top: 6px; }
#tooltip .t-new {
  background: var(--fresh); color: var(--plane);
  font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 1px 5px; border-radius: 999px;
}

/* ------------------------------------------------------------ topbar --- */

#topbar {
  position: absolute; top: 14px; right: 16px; z-index: 35;
  display: flex; gap: 7px;
  transition: left 0.22s cubic-bezier(0.4, 0, 0.2, 1), right 0.22s cubic-bezier(0.4, 0, 0.2, 1);
}
/* The detail panel covers the right edge, so the controls move out from under it. */
#stage.panel-open #topbar { left: 16px; right: auto; }
#topbar button, #topbar a {
  background: var(--surface); border: 1px solid var(--control);
  border-radius: var(--r-s); padding: 7px 12px; cursor: pointer;
  font-size: 12px; color: var(--ink-2); text-decoration: none;
}
#topbar button:hover, #topbar a:hover { color: var(--ink); border-color: var(--edge-strong); }

#empty {
  position: absolute; inset: 0; display: none; place-items: center;
  pointer-events: none; color: var(--ink-muted); font-size: 13px;
}
#empty.show { display: grid; }

/* ------------------------------------------------------------ modal --- */

/* Every dialog is a .dialog, so a new panel needs an entry in data/panels.yaml
   and nothing here. The three widths are named rather than numbered because
   what a panel wants is "as wide as prose reads", not 560 pixels.
   #mobile-note keeps its own rules, at id specificity, and deliberately wins
   over these: it is the one dialog that is not a panel. */
.dialog {
  position: fixed; inset: 0; z-index: 60; display: none;
  background: rgba(0, 0, 0, 0.62); padding: 40px 20px; overflow-y: auto;
}
.dialog.open { display: block; }
.modal-card {
  max-width: 720px; margin: 0 auto; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r);
  padding: 30px 34px 36px;
}
.modal-close {
  float: right; margin: -6px -8px 0 12px;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--surface-2); border: 1px solid var(--control);
  color: var(--ink-2); font-size: 16px; line-height: 1;
  display: grid; place-items: center; cursor: pointer;
}
.modal-close:hover { color: var(--ink); border-color: var(--edge-strong); }
.modal-card h2 { margin: 0 0 6px; font-size: 21px; letter-spacing: -0.015em; }
.modal-card > p.lede { margin: 0 0 22px; color: var(--ink-2); font-size: 13.5px; line-height: 1.6; }
.modal-card h3 {
  margin: 26px 0 11px; font-size: 10.5px; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-muted);
}
.nc { border-top: 1px solid var(--hairline); padding: 11px 0; }
.nc b { display: block; font-size: 13px; margin-bottom: 3px; }
.nc span { font-size: 12.5px; line-height: 1.6; color: var(--ink-2); }
.modal-card h3 + .note { margin-top: 11px; }
.modal-card .panel-foot { margin: 22px 0 0; }

.dialog[data-width="narrow"] .modal-card { max-width: 420px; }
.shortcut-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 18px; border-top: 1px solid var(--hairline); padding: 10px 0;
}
.shortcut-row:first-child { border-top: 0; }
.shortcut-row .keys { display: flex; gap: 4px; flex-shrink: 0; }
.shortcut-row .keys kbd {
  font: inherit; font-weight: 600; color: var(--ink); background: var(--surface-2);
  border: 1px solid var(--control); border-radius: 3px; padding: 2px 6px;
}
.shortcut-row .desc { color: var(--ink-2); font-size: 12.5px; text-align: right; }

/* --------------------------------------------------------- sources --- */

/* The document pages scroll as documents. The map's shell pins the body to the
   viewport, which would leave these trapped in a scrolling body — a nested
   scroller as far as a mobile browser is concerned, so its address bar never
   retracts and the scroll never gets momentum. */
html:has(body.sources), body.sources {
  height: auto;
  min-height: 100%;
  overflow: visible;
  overscroll-behavior: auto;
}
.sources-wrap { max-width: 940px; margin: 0 auto; padding: 44px 20px 80px; }
.sources-wrap h1 { font-size: 26px; letter-spacing: -0.02em; margin: 0 0 8px; }
.sources-wrap .lede { color: var(--ink-2); font-size: 14px; line-height: 1.65; margin: 0 0 26px; max-width: 64ch; }
.doc-row { border-top: 1px solid var(--hairline); padding: 15px 0; }
.doc-row:last-child { border-bottom: 1px solid var(--hairline); }
.doc-path { font-size: 12.5px; word-break: break-all; text-decoration: none; }
.doc-path:hover { text-decoration: underline; }
.doc-live-row { margin-top: 5px; }
.doc-refs { margin-top: 7px; display: flex; flex-wrap: wrap; gap: 5px; }
.doc-ref {
  font-size: 11.5px; padding: 3px 9px; border-radius: 20px;
  border: 1px solid var(--border); background: var(--surface); color: var(--ink-2);
}
.doc-ref.edge { color: var(--ink-muted); }

#doc-filter {
  width: 100%; max-width: 420px; padding: 9px 12px; margin-bottom: 22px;
  background: var(--surface); border: 1px solid var(--control);
  border-radius: var(--r-s);
}
#doc-filter:focus-visible { border-color: var(--accent); }

/* ------------------------------------------------------ small screens --- */

/* A phone has one screen's worth of room and the map is what it is for, so the
   sidebar stops taking a share of it: it becomes a drawer over the map, the
   detail panel becomes a sheet rising from the bottom, and the canvas keeps
   everything else. Controls grow to finger size on the way. */
@media (max-width: 900px) {
  #app {
    grid-template-columns: 1fr;
    grid-template-rows: 1fr auto;
  }

  #stage { grid-row: 1; grid-column: 1; }

  /* --- the sidebar, as a drawer --- */
  #sidebar {
    position: fixed; z-index: 55;
    top: 0; bottom: 0; left: 0;
    width: min(86vw, 330px);
    border-right: 1px solid var(--hairline);
    transform: translateX(-100%);
    transition: transform 0.24s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 18px 0 44px rgba(0, 0, 0, 0.36);
    /* Clear of #sidebar-close, which ends at 47px: the masthead's top edge is
       a drawn line now, so it has to miss the button rather than merely keep
       its text out from under it. */
    padding-top: 52px;
  }
  #sidebar.open { transform: none; }
  #scrim.show { opacity: 1; pointer-events: auto; }

  #sidebar-close {
    display: grid; place-items: center;
    position: absolute; top: 9px; right: 9px;
    width: 38px; height: 38px; border-radius: 50%;
    background: var(--surface-2); border: 1px solid var(--control);
    color: var(--ink-2); cursor: pointer; font-size: 14px;
  }

  #open-menu { display: inline-flex; align-items: center; }

  /* --- room for a fingertip --- */
  .check { padding: 8px 6px; }
  .check .label { font-size: 13px; }
  /* Same treatment as the filter rows: a fingertip is the same size in this
     list as it is in the one above it. */
  .dev-go { padding: 11px 8px 12px; }
  .dev-title { font-size: 13px; }
  #suggest button { padding: 11px 11px; }
  /* Under 16px iOS zooms the whole page in on focus and leaves it there. */
  #search, #doc-filter { font-size: 16px; padding-top: 11px; padding-bottom: 11px; }
  .sidebar-foot { font-size: 12px; gap: 10px; }

  /* --- the floating toolbar --- */
  #topbar {
    top: 10px; left: 10px; right: 10px;
    flex-wrap: wrap; justify-content: flex-end; gap: 6px;
  }
  /* The sheet rises from the bottom rather than covering the right edge, so
     the toolbar has no reason to move out of its way. */
  #stage.panel-open #topbar { left: 10px; right: 10px; }
  #topbar button, #topbar a {
    padding: 9px 11px; min-height: 38px; display: inline-flex; align-items: center;
  }

  /* --- the detail panel, as a bottom sheet --- */
  #panel {
    position: fixed; z-index: 45;
    top: auto; right: 0; bottom: 0; left: 0;
    width: 100%; max-width: none;
    height: 74vh;
    height: 74dvh;
    border-left: 0;
    border-top: 1px solid var(--hairline);
    border-radius: var(--r) var(--r) 0 0;
    transform: translateY(101%);
    box-shadow: 0 -18px 44px rgba(0, 0, 0, 0.36);
  }
  .panel-inner { padding: 16px var(--pad) 40px; }
  #panel-close { top: 6px; width: 38px; height: 38px; }
  /* A bar across the top edge, so the panel reads as a sheet that pulls up. */
  #panel::before {
    content: ""; position: absolute; z-index: 1;
    top: 8px; left: 50%; transform: translateX(-50%);
    width: 42px; height: 4px; border-radius: 3px;
    background: var(--hairline);
  }

  /* --- the timeline --- */
  #timeline { grid-row: 2; grid-column: 1; padding: 9px 12px 11px; gap: 11px; }
  /* A range thumb is a small target; the track keeps its look and the row
     around it grows. */
  #scrubber { height: 26px; }

  .sources-wrap { padding: 28px 16px 60px; }
  .modal-card { padding: 24px 18px 30px; }
  .dialog { padding: 16px 12px; }
  /* Three insets deep on a phone — dialog, modal card, group card — so the
     innermost one gives a few pixels back to the rows it holds. */
  #filters-modal .group { padding: 13px 13px 14px; }
}

/* On a phone the five toolbar buttons do not fit a row at their full names, and
   two rows of chrome over a small map costs more than the words are worth. The
   accessible name is on aria-label, so only the visible text is shortened. */
@media (max-width: 560px) {
  #topbar [data-short] { font-size: 0; }
  #topbar [data-short]::after { content: attr(data-short); font-size: 12px; }
  #topbar button, #topbar a { padding: 9px 10px; }

  /* The readout carries the date; the sentence under it does not survive the
     width, and the same count is on the masthead. */
  #scrub-count { display: none; }
  .scrub-head { justify-content: flex-start; }

  .masthead h1 { font-size: 16px; }
  .modal-card h2 { font-size: 19px; }
  .sources-wrap h1 { font-size: 22px; }
  .p-title { font-size: 18px; }
  #consent-note .consent-actions { flex-direction: column; }
}

/* Landscape on a phone is mostly a strip of screen. The timeline is the one
   piece that can stand down to give the map its height back. */
@media (max-height: 460px) and (max-width: 900px) {
  #timeline { padding: 5px 12px 6px; gap: 9px; }
  /* The date the cursor is on is the point of the scrubber and stays. The
     sentence beside it is the one that can go. */
  #scrub-count { display: none; }
  .scrub-head { justify-content: flex-start; }
  .scrub-head strong { font-size: 12px; }
  #play { width: 30px; height: 30px; }
  #panel { height: 92vh; height: 92dvh; }
}

/* The consent note. It uses the ordinary .dialog mid width rather than a
   bespoke size, but it is short enough to center on the screen at any
   width — unlike the small-screen note below, which only centers itself
   under a breakpoint, this one gets that treatment unconditionally, since
   it is shown to every visitor and not just a small screen.

   Most dialogs dim the page behind them, because there is a page worth
   glimpsing through them. This one is the first thing anyone sees, so its
   backdrop is opaque, in the page's own plane colour rather than a black
   rectangle — nothing behind it, not even a dimmed map, until it is agreed
   to. Privacy and Terms of use get the same opaque backdrop: legal text
   read on its own, not over a dimmed map. */
#consent-note.open { display: grid; place-items: center; }
#consent-note, #privacy-modal, #terms-modal { background: var(--plane); }
#consent-note p { margin: 0 0 13px; font-size: 13.5px; line-height: 1.6; color: var(--ink-2); }
#consent-note p:last-of-type { margin-bottom: 0; }
/* Two buttons, not one: reading the terms in full is a way out of this note
   too, just not the one that agrees to anything, so it gets the quieter
   treatment and sits on the side reading order reaches first. */
#consent-note .consent-actions { display: flex; gap: 10px; margin-top: 20px; }
#consent-note .consent-actions button {
  flex: 1; padding: 12px 14px; min-height: 44px;
  border-radius: var(--r-s); font-weight: 600; cursor: pointer;
}
#consent-note-terms {
  background: none; border: 1px solid var(--control); color: var(--ink-2);
}
#consent-note-terms:hover { color: var(--ink); border-color: var(--edge-strong); }
#consent-note-agree { background: var(--cta); border: 1px solid var(--cta); color: #fff; }
#consent-note-agree:hover { filter: brightness(1.08); }

/* The small-screen note. The map works on a phone and that was worth building,
   but it is a whole web meant to be read as one picture, and a desktop browser
   shows more of it at once — so the note says so, once, and gets out of the way.
   It is display:none by default and only turns on under the breakpoint, so a
   window dragged wider takes it away rather than leaving it stranded. */
#mobile-note {
  position: fixed; inset: 0; z-index: 70; display: none;
  background: rgba(0, 0, 0, 0.62);
  padding: 20px 16px; overflow-y: auto;
}
#mobile-note .modal-card { max-width: 420px; padding: 24px 22px 22px; }
#mobile-note h2 { margin: 0 0 10px; font-size: 18px; letter-spacing: -0.015em; }
#mobile-note p { margin: 0 0 11px; font-size: 13.5px; line-height: 1.6; color: var(--ink-2); }
#mobile-note p:last-of-type { margin-bottom: 0; }
#mobile-note button {
  display: block; width: 100%; margin-top: 18px;
  padding: 12px 14px; min-height: 44px;
  /* --accent is tuned to carry text ON a surface; white sitting on it only
     reaches 3.6:1. --cta is the same blue darkened until white clears 4.5:1. */
  background: var(--cta); border: 1px solid var(--cta);
  border-radius: var(--r-s); color: #fff; font-weight: 600; cursor: pointer;
}
#mobile-note button:hover { filter: brightness(1.08); }

@media (max-width: 900px) {
  #mobile-note.open { display: grid; place-items: center; }
}

/* Someone who has asked for less motion should not be handed a sliding drawer,
   a sheet, or a camera that flies to a node. */
@media (prefers-reduced-motion: reduce) {
  #sidebar, #panel, #scrim, #topbar { transition: none; }
}
