/* site/css/site.css -- deep crimson ground, light warm ink, gold and rose.
   Self-contained by rule: no webfonts, no CDN, no image files. Anything
   decorative is inline SVG or CSS. */

:root {
  /* The ground the whole site stands on: a dark crimson, warm rather than
     purple, with the ink a paper-white pulled the same way. */
  --paper: #31111a;
  --ink: #f2e7e1;
  --sepia: #d9a86a;

  /* derived tints -- one place, so the whole site stays in one key */
  --card: #3d1822;
  --line: #5b2a35;
  --paper-deep: #240a11;
  --ink-soft: #c9a8ab;
  --sepia-soft: #b8848a;
  --sepia-bright: #f0c68e;
  --warn: #f0918a;

  /* The three ways light lands on that ground, named once rather than restated
     as an rgba() in every section: a focus ring in the gold, a drop shadow that
     is simply less light, and the two washes that tint a full-bleed surface. */
  --glow: rgba(217, 168, 106, 0.24);
  --shade: rgba(0, 0, 0, 0.5);
  --wash-warm: rgba(217, 168, 106, 0.07);
  --wash-cool: rgba(0, 0, 0, 0.22);
  /* THE LEDGER. The tree is the atlas's companion volume, and its cards are
     slips of aged paper laid on the crimson ground -- the one place on the site
     where the ink is dark and the field is light. The three paper tones are the
     plate's own --sepia (#d9a86a) carried up to paper lightness at the same
     hue, and the two ink tones are that hue carried down to near-black, so the
     ledger is the map's palette at the other end of its own range rather than a
     second palette pretending to match.

     Name ink on the slip measures 12.98:1 and dates 6.86:1; on the lighter
     living slip, 11.35:1 and 5.99:1. */
  --slip: #efe1c8;
  --slip-live: #e2d3b6;
  --slip-edge: #b79a70;
  --slip-rule: #d3bf9c;
  --slip-ink: #2a1a10;
  --slip-ink-soft: #5c4630;

  /* THE FOLD. The crease down the middle of the sheet: the gutter falling into
     it, the lit edge rising out of it, and the hairline between. */
  --fold-shade: #120407;
  --fold-lit: #d9a86a;
  --fold-crease: #f0c68e;

  /* A card floated over a moving surface (the tree, the plate) and still let
     through faintly. */
  --card-veil: rgba(61, 24, 34, 0.9);

  --serif: Charter, 'Bitstream Charter', 'Iowan Old Style', Georgia, serif;
  --sans: Avenir Next, Segoe UI, Helvetica Neue, Arial, sans-serif;
  --radius: 3px;
  --topbar-h: 58px;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--sepia); }

/* === gate === */

.gate {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--paper);
  /* faint plate tone, pure CSS -- no image files may ship */
  background-image:
    radial-gradient(circle at 30% 20%, var(--wash-warm), transparent 60%),
    radial-gradient(circle at 75% 80%, var(--wash-cool), transparent 55%);
}
.gate[hidden] { display: none; }

.gate-card {
  width: min(440px, calc(100vw - 48px));
  padding: 44px 40px 32px;
  text-align: center;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 1px 0 var(--wash-warm) inset, 0 18px 40px var(--shade);
}

.masthead {
  margin: 0;
  font-family: var(--serif);
  font-size: 34px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.15;
}

.rule {
  display: block;
  width: 240px;
  max-width: 100%;
  height: 12px;
  margin: 14px auto 18px;
  stroke: var(--sepia-soft);
  stroke-width: 1;
  fill: none;
}
.rule .rule-lozenge { fill: var(--sepia-soft); stroke: none; }

.gate-sub {
  margin: 0 0 22px;
  color: var(--ink-soft);
  font-size: 15px;
}

.gate-row { display: flex; gap: 8px; }

.gate-row input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 10px 12px;
  font: inherit;
  font-size: 16px;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.gate-row input:focus {
  outline: none;
  border-color: var(--sepia);
  box-shadow: 0 0 0 3px var(--glow);
}

.gate-row button {
  flex: 0 0 auto;
  padding: 10px 20px;
  font: inherit;
  color: var(--paper-deep);
  background: var(--sepia);
  border: 1px solid var(--sepia);
  border-radius: var(--radius);
  cursor: pointer;
}
.gate-row button:hover { background: var(--sepia-bright); }
.gate-row button:disabled { opacity: 0.6; cursor: default; }

.gate-msg {
  min-height: 1.4em;
  margin: 14px 0 0;
  color: var(--warn);
  font-family: var(--sans);
  font-size: 13px;
}

.shake { animation: shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-7px); }
  40%, 60% { transform: translateX(7px); }
}
@media (prefers-reduced-motion: reduce) {
  .shake { animation: none; }
}

/* === top bar === */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  height: var(--topbar-h);
  padding: 0 24px;
  background: var(--paper-deep);
  border-bottom: 1px solid var(--line);
}
.topbar[hidden] { display: none; }

.topbar-title {
  font-size: 21px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink);
  text-decoration: none;
}
.topbar-title:hover { color: var(--sepia); }

/* The search box is sized by the room the bar has, not by a number. 280px was
   written as a fixed `width` on the input, which is a floor as well as a
   ceiling: the flex item around it could shrink, the input inside it could not,
   so at phone widths the field kept its 280px and pushed itself out past the
   masthead and the nav. `flex: 0 1 280px` makes 280 the *preferred* width and
   lets the bar take it back when there is not enough to go round; `min-width: 0`
   is what actually permits that, since a flex item's automatic minimum is its
   content otherwise. */
.topbar-search {
  flex: 0 1 280px;
  min-width: 0;
}

.topbar-search input {
  width: 100%;
  min-width: 0;
  padding: 7px 11px;
  font: inherit;
  font-size: 15px;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.topbar-search input:focus {
  outline: none;
  border-color: var(--sepia);
  box-shadow: 0 0 0 3px var(--glow);
}

/* === app shell === */

#app {
  min-height: calc(100vh - var(--topbar-h));
}
#app[hidden] { display: none; }

.view-error {
  padding: 40px 24px;
  color: var(--ink-soft);
  font-family: var(--sans);
  font-size: 14px;
}
.view-error { color: var(--warn); }

/* === tree-view (Task 6) === */

.tree-wrap {
  position: relative;
  height: calc(100vh - var(--topbar-h));
  overflow: hidden;
  background: var(--paper);
  /* faint plate tone, matching the gate -- CSS only, no image files ship */
  background-image:
    radial-gradient(circle at 22% 14%, var(--wash-warm), transparent 62%),
    radial-gradient(circle at 82% 86%, var(--wash-cool), transparent 58%);
}

svg.tree {
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: none;          /* the pointer handlers own the gesture */
  user-select: none;
}
svg.tree.dragging { cursor: grabbing; }
/* The canvas is itself a tab stop (it owns the +/- zoom), so it needs a ring
   the reader can see when they land on it. */
svg.tree:focus { outline: none; }
svg.tree:focus-visible {
  outline: 2px solid var(--sepia);
  outline-offset: -3px;
}

.tree-hint {
  position: absolute;
  left: 20px;
  bottom: 16px;
  margin: 0;
  padding: 5px 11px;
  color: var(--ink-soft);
  font-family: var(--sans);
  font-size: 12px;
  background: var(--card-veil);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  pointer-events: none;
}

/* Branch selector: the same absolutely-positioned overlay layer as .tree-hint,
   in the opposite corner. Only one branch shares the canvas with the trunk at
   a time, and this is where the reader says which. */
.tree-branches {
  position: absolute;
  top: 16px;
  left: 20px;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 7px 5px 11px;
  background: var(--card-veil);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.tree-branches-label {
  color: var(--ink-soft);
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
.tree-branch {
  padding: 3px 10px;
  color: var(--ink-soft);
  font-family: var(--sans);
  font-size: 12px;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}
.tree-branch:hover { color: var(--sepia); border-color: var(--line); }
.tree-branch:focus-visible { outline: 1px solid var(--sepia); outline-offset: 1px; }
.tree-branch.current {
  color: var(--paper);
  background: var(--sepia);
  border-color: var(--sepia);
  cursor: default;
}

/* THE FOLD, which is the signature of this view: the same sheet the map is
   printed on, creased down the middle. The gradient stops carry no colour of
   their own -- they are classed and painted here, so the crease cannot drift
   out of key with the ground it is creasing. */
.fold-shade { stop-color: var(--fold-shade); }
.fold-lit { stop-color: var(--fold-lit); }
.fold-crease { stop-color: var(--fold-crease); }
.tree-fold { pointer-events: none; }
.tree-crease {
  stroke: var(--fold-crease);
  stroke-width: 1;
  opacity: 0.35;
  pointer-events: none;
}
/* The branch's name set along the crease, the way a title is set on a spine. */
.tree-side-label {
  fill: var(--sepia-bright);
  font-family: var(--sans);
  font-size: 16px;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--paper-deep);
  stroke-width: 4;
  stroke-linejoin: round;
}

/* THE SALIENCE BUDGET. Documented lineage prints in full sepia ink; a research
   edge is dashed, lighter and rose-muted, so it reads as a proposal; and the
   edge across the fold is the heaviest line on the canvas, because it is the
   one tie the two-column drawing exists to show. Three weights, and nothing
   else on the canvas competes for the eye. */
.edge-child {
  stroke: var(--sepia);
  stroke-width: 1.8;
  opacity: 0.85;
}
.edge-child.adopted {
  stroke: var(--sepia-soft);
  stroke-dasharray: 6 5;
}
/* One layer of halo beneath the crossing edge, in the ground's own colour --
   the same device the map plate uses under its Atlantic arcs. */
.edge-halo {
  stroke: var(--paper);
  stroke-width: 11;
  opacity: 0.85;
  fill: none;
}
.edge-child.crossing,
.edge-kin.crossing {
  stroke: var(--sepia-bright);
  stroke-width: 4;
  opacity: 1;
}
/* A dash pattern is only a dash pattern relative to the stroke that draws it.
   The crossing edge is 4px wide and the research edge's 7/5 dash is tuned for
   1.1px, so at the fold the two rules combined into a chain of heavy beads --
   the loudest and ugliest thing on the canvas, and the first thing the eye
   landed on. Scale the pattern with the weight and it reads as what it is: the
   heaviest line here, still unmistakably a proposal. */
.edge-kin.crossing {
  stroke-dasharray: 18 11;
}
/* An undocumented relationship -- a candidate biological parentage, a pointer
   at a probable birth family, a "same family?" tie between two clusters.
   Dashed more finely than the adoption edge and dimmer, so it reads as a
   proposal rather than a record.

   IT WAS 3/7 AT 1.6px UNTIL 2026-08-16, and at that ratio a 1.6px stroke draws
   as a row of beads rather than as a line: the dash is shorter than the stroke
   is wide, so each mark reads as a dot, and the weight that made the dots
   legible made them heavy. The proposal edge was the loudest thing on a canvas
   where it is meant to be the quietest -- backwards, against the salience
   budget three rules above. So: a longer mark, a shorter gap, a finer stroke.
   Still unmistakably dashed at reading zoom, and now quiet. */
.edge-kin {
  fill: none;
  stroke: var(--sepia-soft);
  stroke-dasharray: 7 5;
  stroke-width: 1.1;
  opacity: 0.65;
}
/* A kin edge the record DOES establish: siblings whose shared parents are
   unknown, so no parentage edge can carry them. Solid and at full strength, and
   still visibly not a parentage edge -- it is the lifted arc across a row, not
   the bracket down from a couple. */
.edge-kin.documented {
  stroke: var(--sepia);
  stroke-dasharray: none;
  stroke-width: 1.8;
  opacity: 0.85;
}
.edge-couple {
  stroke: var(--sepia);
  stroke-width: 1.6;
  opacity: 0.85;
}
/* The label sits on top of the dashed edge it names, so knock the line out
   from behind it rather than letting the two read as one smudge. */
/* Quiet but legible: the research edges' captions are the only text on the
   canvas that is neither a name nor the branch, and they must not read as
   loudly as the ties they annotate. */
.edge-label {
  fill: var(--sepia-soft);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.08em;
  stroke: var(--paper);
  stroke-width: 5;
  paint-order: stroke fill;
}

/* A LEDGER SLIP: an aged-paper face, a printed edge, and a second rule inset
   from it the way a ledger's border sits inside the sheet. No drop shadow --
   this is a printed page, not a stack of material cards. */
.person { cursor: pointer; }
.person .card {
  fill: var(--slip);
  stroke: var(--slip-edge);
  stroke-width: 1;
}
.person .card-rule {
  fill: none;
  stroke: var(--slip-rule);
  stroke-width: 1;
}
.person .card-halo {
  fill: none;
  stroke: var(--sepia-bright);
  stroke-width: 2.5;
  opacity: 0;
}
.person:hover .card-halo { opacity: 0.6; }
/* Keyboard focus is a full-strength ring, not the hover's hint: it has to be
   findable at a glance while tabbing a canvas of a hundred cards. */
.person:focus-visible .card-halo { opacity: 1; }
.person:hover .card { stroke: var(--slip-ink-soft); }
.person:focus { outline: none; }

.person .card-name {
  fill: var(--slip-ink);
  font-family: var(--serif);
  font-size: 15.5px;
}
.person .card-years {
  fill: var(--slip-ink-soft);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.02em;
}

/* Living people are minimized upstream to a name and a birth year; a lighter
   sheet with a dashed edge says so at a glance rather than looking like missing
   research. The distinction survives the new material -- it is the same paper,
   thinner. */
.person.living .card {
  fill: var(--slip-live);
  stroke-dasharray: 5 4;
}
.person.living .card-rule { stroke: var(--slip-live); }
.person.living .card-name { fill: var(--slip-ink-soft); }

/* THE ZOOM RAIL, quoted from the map's timeline scrubber (.map-scrub below):
   the same card ground, the same 3px rule, the same sepia fill, the same
   15px ringed handle, the same round end buttons. The map scrubs time along a
   rail; this scrubs scale along one, and they should be recognisably the same
   control. It sits bottom RIGHT -- clear of the hint chip bottom left and the
   branch selector top left, which is the corner the map's own rail logic would
   pick: below the canvas, out of the way of what the canvas is showing. */
.tree-zoom {
  position: absolute;
  right: 20px;
  bottom: 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  background: var(--card-veil);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.tree-zoom-step {
  flex: none;
  width: 26px;
  height: 26px;
  padding: 0;
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1;
  color: var(--sepia);
  background: var(--paper);
  border: 1px solid var(--sepia-soft);
  border-radius: 50%;
  cursor: pointer;
}
.tree-zoom-step:hover { border-color: var(--sepia); color: var(--sepia-bright); }
.tree-zoom-step:focus-visible { outline: 2px solid var(--sepia); outline-offset: 2px; }

.tree-zoom-rail {
  position: relative;
  width: 128px;
  height: 26px;
  cursor: pointer;
  touch-action: none;      /* the drag is ours */
}
.tree-zoom-rail::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 11px;
  height: 3px;
  background: var(--sepia-soft);
  border-radius: 2px;
}
.tree-zoom-fill {
  position: absolute;
  left: 0;
  top: 11px;
  height: 3px;
  background: var(--sepia);
  border-radius: 2px;
}
.tree-zoom-handle {
  position: absolute;
  top: 5px;
  width: 15px;
  height: 15px;
  margin-left: -7.5px;
  background: var(--paper);
  border: 2px solid var(--sepia);
  border-radius: 50%;
  box-shadow: 0 1px 3px var(--shade);
}
.tree-zoom-handle:focus-visible {
  outline: 2px solid var(--sepia);
  outline-offset: 2px;
}

.tree-recentre {
  flex: none;
  padding: 4px 12px;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
}
.tree-recentre:hover { color: var(--sepia); border-color: var(--sepia-soft); }
.tree-recentre:focus-visible { outline: 2px solid var(--sepia); outline-offset: 2px; }

/* A reader who has asked for less motion gets no transitions on this canvas
   either. Nothing here animates by default, so this is a guard rather than a
   correction -- but the fold's gradient and the card halos are exactly the
   things a future change would be tempted to fade. */
@media (prefers-reduced-motion: reduce) {
  .tree-wrap *,
  .tree-zoom * { transition: none !important; animation: none !important; }
}

/* === person (Task 7) === */

/* Two columns in a fixed PROPORTION -- the record, and the documents beside it
   at 1.2 times its width -- rather than a proportion sitting on top of a pixel
   floor. `minmax(420px, 1fr)` held the record column at 420px once the page
   dropped below about 970px, and because the second track is a free-space
   fraction it was the documents column that gave up every pixel: at 901px the
   record stood at 420 and the documents at 393, so the 1 : 1.2 the design asks
   for had quietly inverted. The floor never overflowed the page -- a fr track
   shrinks to nothing before it pushes anything out -- but it stopped the layout
   meaning what it says. minmax(0, ...) on both tracks is the ratio holding all
   the way down to the breakpoint, and the 0 minimum is the same permission
   `.person-main { min-width: 0 }` already grants the first column's contents. */
.person-page {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
  gap: 40px;
  align-items: start;
  max-width: 1320px;
  margin: 0 auto;
  padding: 34px 24px 64px;
}
/* A living person has no document column; the record should not sit in a
   half-width gutter with nothing beside it. */
.person-page.is-living { grid-template-columns: minmax(0, 720px); }

@media (max-width: 900px) {
  .person-page { grid-template-columns: minmax(0, 1fr); gap: 28px; }
}

.person-main { min-width: 0; }

.person-head { margin: 0 0 22px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.person-name {
  margin: 0;
  font-size: 32px;
  font-weight: 600;
  letter-spacing: 0.01em;
  line-height: 1.15;
}
.person-span { margin: 6px 0 0; color: var(--sepia); font-size: 17px; }
.person-aka { margin: 4px 0 0; color: var(--ink-soft); font-family: var(--sans); font-size: 13px; }

.person-section { margin: 0 0 26px; }
.person-h2 {
  margin: 0 0 10px;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.vitals {
  width: 100%;
  margin: 0 0 26px;
  border-collapse: collapse;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.vitals th,
.vitals td { padding: 8px 12px; text-align: left; vertical-align: top; font-size: 15px; }
.vitals th {
  width: 92px;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.vitals tr + tr th,
.vitals tr + tr td { border-top: 1px solid var(--line); }

.rel-row { display: flex; gap: 12px; margin: 0 0 6px; font-size: 15px; }
.rel-label {
  flex: 0 0 82px;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding-top: 3px;
}
.rel-list { min-width: 0; }
.rel-sep { color: var(--sepia-soft); }
.rel-note { color: var(--ink-soft); font-size: 13px; }
.person-link { text-decoration: none; border-bottom: 1px solid var(--sepia-soft); }
.person-link:hover { color: var(--ink); border-bottom-color: var(--ink); }

.narrative { margin: 0 0 12px; font-size: 16.5px; line-height: 1.62; }

.fact-list,
.correction-list { margin: 0; padding-left: 20px; font-size: 15px; }
.fact-list li,
.correction-list li { margin: 0 0 7px; }
.correction-list { color: var(--ink-soft); }

.fact-doc {
  padding: 0;
  font: inherit;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--sepia);
  background: none;
  border: none;
  border-bottom: 1px solid var(--sepia-soft);
  cursor: pointer;
}
.fact-doc:hover { color: var(--ink); border-bottom-color: var(--ink); }
.fact-missing { font-family: var(--sans); font-size: 12px; color: var(--warn); }

/* --- document column --- */

.person-docs {
  min-width: 0;
  padding: 18px;
  background: var(--paper-deep);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.doc-viewer { margin: 0 0 16px; }
.doc-viewer[hidden] { display: none; }
.doc-viewer-title { margin: 0 0 8px; font-size: 15px; font-weight: 600; }
.doc-frame {
  width: 100%;
  height: min(72vh, 780px);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.doc-image {
  display: block;
  width: 100%;
  max-height: min(72vh, 780px);
  object-fit: contain;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.doc-unsupported {
  margin: 0;
  padding: 22px 16px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.doc-actions { margin: 8px 0 0; font-family: var(--sans); font-size: 13px; }
.doc-size { color: var(--ink-soft); }
.doc-error { margin: 0; font-family: var(--sans); font-size: 13px; color: var(--warn); }

/* CSS-only spinner: no image files may ship (the dist whitelist enforces it). */
.doc-spinner-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  height: 180px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.doc-spinner {
  width: 18px;
  height: 18px;
  border: 2px solid var(--sepia-soft);
  border-top-color: transparent;
  border-radius: 50%;
  animation: doc-spin 0.8s linear infinite;
}
.doc-spinner-label { font-family: var(--sans); font-size: 13px; color: var(--ink-soft); }
@keyframes doc-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .doc-spinner { animation-duration: 2.4s; }
}

.doc-list { display: flex; flex-direction: column; gap: 8px; }

.doc-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: 100%;
  padding: 10px 12px;
  text-align: left;
  font: inherit;
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
}
.doc-row:hover { border-color: var(--sepia-soft); }
.doc-row.is-open { border-color: var(--sepia); box-shadow: 0 0 0 2px var(--glow); }

.doc-row-top { display: flex; align-items: baseline; gap: 8px; }
.doc-title { flex: 1 1 auto; min-width: 0; font-size: 15px; }
.doc-badge {
  flex: 0 0 auto;
  padding: 1px 7px;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--sepia);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.doc-meta { font-family: var(--sans); font-size: 12px; color: var(--ink-soft); }

.doc-empty { margin: 0; font-family: var(--sans); font-size: 13px; color: var(--ink-soft); }

.withheld {
  margin: 22px 0 0;
  padding: 12px 14px;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--sepia-soft);
  border-radius: var(--radius);
}

/* === map (v2) === */

:root {
  /* Plate colors, in the same key as the rest of the site: the sea is the
     paper gone a shade cooler and deeper, the land the paper itself lifted. */
  --map-sea: #1e070c;
  --map-land: #431d27;
  --map-hatch: rgba(242, 231, 225, 0.07);
  /* The halo under every arc is the ground itself: it knocks one crossing out
     from behind another without introducing a fourth color to the plate. */
  --map-halo: var(--paper);

  /* How long an arc takes to draw itself. */
  --arc-draw: 780ms;

  /* The generated plate's own aspect ratio, 1654/900. It is the one number
     the layout needs out of map-base.js, and it is here so the sizing rule
     below reads as arithmetic rather than as a magic width. */
  --plate-aspect: 1.838;
}

.topbar-nav {
  display: flex;
  gap: 18px;
  font-family: var(--sans);
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.topbar-nav a {
  color: var(--ink-soft);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
}
.topbar-nav a:hover { color: var(--sepia); border-bottom-color: var(--sepia-soft); }

/* The view: the ground the plate sits on, and the box every control that is
   not the plate is positioned against. */
.map-view {
  position: relative;
  min-height: calc(100vh - var(--topbar-h));
  padding: 18px 22px 26px;
  background: var(--paper);
  background-image:
    radial-gradient(circle at 18% 12%, var(--wash-warm), transparent 60%),
    radial-gradient(circle at 84% 88%, var(--wash-cool), transparent 58%);
}

/* The box the plate sits in. The world frame is 1.84:1 now -- tall enough to
   be shown whole on a laptop -- so this is no longer the scroller the reader
   travels along, it is an overflow valve for screens too narrow to hold the
   plate at a legible size. Only the plate lives in here; the rail, the hint
   and the panel are siblings below and beside it. */
.map-wrap {
  position: relative;
  overflow-x: auto;
  overflow-y: hidden;
  overscroll-behavior-x: contain;
}

svg.map {
  display: block;
  margin: 0 auto;
  /* Width first, capped by the height the page can spare. The plate takes the
     box's full width, and max-width stops it before it grows taller than the
     vertical allotment -- so on any viewport at least --map-min wide, the
     whole world is on screen with nothing to scroll to. The reserve is the
     rail, its hint line, and the padding.

     min-width is the escape hatch, and it beats max-width by CSS's own
     ordering: below it the plate stops shrinking, overflows the box, and the
     scroller above takes over. 720px is the least that still reads as a map. */
  width: 100%;
  min-width: 720px;
  max-width: calc((100vh - var(--topbar-h) - 160px) * var(--plate-aspect));
  height: auto;
  touch-action: pan-x pan-y;
}

.map-field { fill: var(--paper); }
.map-sea { fill: var(--map-sea); }
.map-sea-hatch { fill: url(#map-hatch); }
.map-hatch-line { stroke: var(--map-hatch); stroke-width: 1; }

.map-land path {
  fill: var(--map-land);
  stroke: var(--ink);
  stroke-width: 0.5;
  stroke-linejoin: round;
}

.map-rule-outer, .map-rule-inner {
  fill: none;
  stroke: var(--ink);
}
.map-rule-outer { stroke-width: 1.6; }
.map-rule-inner { stroke-width: 0.7; }
.map-ornament path { fill: var(--paper); stroke: var(--ink); stroke-width: 1; }
.map-ornament circle { fill: var(--sepia); }

.map-label {
  font-family: var(--serif);
  font-style: italic;
  /* Small caps are the engraver's convention for a region name, and the
     letter-spacing above was already reaching for the same effect. */
  font-variant: small-caps;
  letter-spacing: 0.22em;
  fill: var(--ink-soft);
  paint-order: stroke;
  stroke: var(--map-land);
  stroke-width: 3.5;
  stroke-linejoin: round;
  pointer-events: none;
}
.map-label-sea { fill: var(--sepia); stroke: var(--map-sea); }

.map-journey { cursor: pointer; }
.map-journey:focus-visible {
  outline: 2px solid var(--sepia);
  outline-offset: 3px;
}

.map-arc {
  fill: none;
  stroke: var(--sepia);
  stroke-width: 2;
  stroke-linecap: round;
}
/* One layer of halos beneath every arc, so two crossings that share the
   Atlantic corridor cannot white each other's line out. */
.map-halos { pointer-events: none; }
.map-arc-halo {
  fill: none;
  stroke: var(--map-halo);
  stroke-width: 7;
  stroke-linecap: round;
  opacity: 0.75;
}
/* The click target the halo used to provide by accident. */
.map-arc-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 14;
  stroke-linecap: round;
  /* Stated rather than left to visiblePainted: the stroke is transparent, and
     `stroke` is the one value every state rule below turns back on. */
  pointer-events: stroke;
}

.map-journey:hover .map-arc,
.map-journey:focus-visible .map-arc,
.map-journey.is-selected .map-arc { stroke-width: 3.2; stroke: var(--sepia-bright); }

.map-dot { fill: var(--card); stroke: var(--sepia); stroke-width: 1.6; }
.map-dot-halo { fill: transparent; }
.map-journey:hover .map-dot,
.map-journey.is-selected .map-dot { fill: var(--sepia); }

.map-place {
  font-family: var(--sans);
  font-size: 12px;
  fill: var(--ink);
  paint-order: stroke;
  stroke: var(--paper);
  stroke-width: 3.5;
  stroke-linejoin: round;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease-out;
}
.map-waypoint:hover .map-place,
.map-journey:focus-visible .map-place,
.map-journey.is-selected .map-place { opacity: 1; }

/* The ship's name belongs to its moment: it prints while the scrubber is on
   that journey's year, and while its panel is open, and not otherwise -- seven
   captions standing at once turned the Atlantic into a bibliography. */
.map-vessel {
  font-family: var(--serif);
  font-style: italic;
  font-size: 15px;
  fill: var(--sepia);
  paint-order: stroke;
  stroke: var(--paper);
  stroke-width: 3.5;
  stroke-linejoin: round;
  pointer-events: none;
  opacity: 0;
  transition: opacity 260ms ease-out;
}
.map-journey.is-active .map-vessel,
.map-journey.is-selected .map-vessel { opacity: 1; }

/* --- journey state, driven by scrubState via map-scrub.js ---------------- */

/* Each line carries its own measured length in --arc-len (set at render by
   getTotalLength), so one rule draws every one of them whatever its length.
   The halo is dashed on the same terms as the arc it sits under: undashed, a
   7px paper-colored stroke prints the whole route in advance of the line. */
.map-arc,
.map-arc-halo { transition: stroke-dashoffset var(--arc-draw) cubic-bezier(0.4, 0.05, 0.2, 1); }

.map-journey.is-pending .map-arc,
.map-arc-halo.is-pending { stroke-dashoffset: var(--arc-len, 0); }
.map-journey.is-active .map-arc,
.map-journey.is-drawn .map-arc,
.map-arc-halo.is-active,
.map-arc-halo.is-drawn { stroke-dashoffset: 0; }
/* Already-drawn journeys are not re-animated when the reader drags backwards
   past them and forwards again -- only the active one draws. */
.map-journey.is-drawn .map-arc,
.map-arc-halo.is-drawn { transition: none; }

/* A pending journey keeps its dots on the plate, faintly, so the route reads
   as "not yet" rather than as an empty ocean. */
.map-journey.is-pending { opacity: 0.16; }
.map-journey.is-pending .map-arc-hit { pointer-events: none; }

/* Selecting a journey from the panel shows it whatever year it is. */
.map-journey.is-selected { opacity: 1; }
/* Including its click target: is-pending switched pointer-events off, and
   selection has to switch it back on or the arc the reader just opened cannot
   be clicked. */
.map-journey.is-selected .map-arc-hit { pointer-events: stroke; }
.map-journey.is-selected .map-arc,
.map-arc-halo.is-selected { stroke-dashoffset: 0; }

/* The first two frames after render, while the arcs are being measured. */
.map-wrap.is-arming .map-arc,
.map-wrap.is-arming .map-arc-halo,
.map-wrap.is-arming .map-vessel { transition: none; }

/* --- the timeline scrubber ---------------------------------------------- */

/* A DOM sibling of the plate, never a group inside it: the SVG's own
   pointerdown/pointerup pair reads any press on the map as a journey click or
   a dismissal, and a drag handle inside that surface would close the panel on
   every scrub. It is also outside the scroller, not merely outside the SVG:
   the rail is the control the view is built around, and a rail that travelled
   with the plate would be somewhere off to the left the moment a reader
   scrolled to Bremerhaven. */
.map-scrub {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 14px 2px 0;
  padding: 10px 16px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.map-scrub-play {
  flex: none;
  width: 30px;
  height: 30px;
  padding: 0;
  background: var(--paper);
  border: 1px solid var(--sepia-soft);
  border-radius: 50%;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.map-scrub-play:hover { border-color: var(--sepia); }
/* Icons out of one box: a right-pointing triangle from borders, and the same
   box restyled into a pause bar while it runs. No glyph, no asset. */
.map-scrub-play span {
  width: 0;
  height: 0;
  margin-left: 3px;
  border-top: 6px solid transparent;
  border-bottom: 6px solid transparent;
  border-left: 10px solid var(--sepia);
}
.map-scrub-play.is-playing span {
  width: 9px;
  height: 11px;
  margin-left: 0;
  border: none;
  border-left: 3px solid var(--sepia);
  border-right: 3px solid var(--sepia);
}

.map-scrub-rail {
  position: relative;
  flex: 1;
  height: 30px;
  margin: 12px 10px 0;
  cursor: pointer;
  touch-action: none;      /* the drag is ours, not the scroller's */
}
/* The rule the handle rides, drawn as a pseudo-element so the rail itself can
   be a taller, easier press target than a 2px line. */
.map-scrub-rail::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 13px;
  height: 3px;
  background: var(--sepia-soft);
  border-radius: 2px;
}
.map-scrub-fill {
  position: absolute;
  left: 0;
  top: 13px;
  height: 3px;
  background: var(--sepia);
  border-radius: 2px;
}

.map-scrub-tick {
  position: absolute;
  top: 8px;
  width: 1px;
  height: 13px;
  margin-left: -0.5px;
  background: var(--ink-soft);
}
.map-scrub-tick i {
  position: absolute;
  top: 15px;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--sans);
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}

.map-scrub-handle {
  position: absolute;
  top: 7px;
  width: 15px;
  height: 15px;
  margin-left: -7.5px;
  background: var(--paper);
  border: 2px solid var(--sepia);
  border-radius: 50%;
  box-shadow: 0 1px 3px var(--shade);
}
.map-scrub-handle:focus-visible {
  outline: 2px solid var(--sepia);
  outline-offset: 2px;
}

.map-scrub-year {
  flex: none;
  min-width: 4.2ch;
  font-family: var(--serif);
  font-size: 20px;
  font-variant-numeric: tabular-nums;
  color: var(--sepia);
  text-align: right;
}

.map-hint {
  margin: 10px 0 0;
  font-family: var(--sans);
  font-size: 12px;
  color: var(--ink-soft);
}

.map-panel {
  position: absolute;
  top: 18px;
  right: 22px;
  width: min(360px, calc(100% - 44px));
  max-height: calc(100vh - var(--topbar-h) - 56px);
  overflow-y: auto;
  padding: 18px 20px 22px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 10px 28px var(--shade);
}
.map-panel[hidden] { display: none; }

.map-panel-close {
  float: right;
  margin: -6px -6px 0 8px;
  padding: 0 6px;
  font-family: var(--sans);
  font-size: 20px;
  line-height: 1.2;
  color: var(--ink-soft);
  background: none;
  border: none;
  cursor: pointer;
}
.map-panel-close:hover { color: var(--sepia); }

.map-panel-title { margin: 0 0 4px; font-size: 19px; font-weight: 600; }
.map-panel-meta {
  margin: 0 0 12px;
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--sepia);
}
.map-panel-summary { margin: 0 0 14px; font-size: 15px; line-height: 1.5; }
.map-panel-sub {
  margin: 0 0 6px;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.map-panel-people { margin: 0; padding-left: 18px; font-size: 15px; }
.map-panel-people a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--sepia-soft); }
.map-panel-people a:hover { color: var(--sepia); }

/* A reader who has asked for less motion gets the finished plate: every arc
   drawn, nothing transitioning, and no play button offering a sweep. The view
   also starts them at the end of the rail, so this only backstops the CSS. */
@media (prefers-reduced-motion: reduce) {
  .map-place,
  .map-arc,
  .map-arc-halo,
  .map-vessel { transition: none; }
  /* Matched at the same weight as the pending rules they have to beat. */
  .map-journey.is-pending .map-arc,
  .map-arc-halo.is-pending { stroke-dashoffset: 0; }
  .map-journey.is-pending { opacity: 1; }
  .map-journey.is-pending .map-arc-hit { pointer-events: stroke; }
  .map-scrub-play { display: none; }
}
