/* A campaign chart: paper land on a slate sea, marked in oxblood. */
:root {
  /* The mark's rendered size, consumed exactly once. It was three literals --
     104 on the atlas, 88 in a masthead, 28 in a mode bar -- and a card page
     showed two of them in succession while the reader watched. */
  --mark:       28px;
  /* The bar's height, a LITERAL and not a measurement. Measuring a box whose
     height is var(--bar-h) is circular; resize() runs only after hydration, so
     in document mode and on the 404 the token would never be set and
     var(--bar-h) would compute to auto rather than to what it replaced; and the
     requirement is a constant, because a height that depends on content is a
     height that changes between routes. */
  --bar-h:      52px;
  /* The card panel's width, and the width the map gives up to stay centred
     beside it. A size rather than a colour, so it is defined here once and not
     restated in the two dark blocks below. Read by .card and by #stage, which
     is the whole reason it is a token: the two have to be the same number or
     the globe sits half a card off centre. */
  --card-w:     min(23rem, 92vw);
  --sea:        #d8e0e2;
  --sea-deep:   #ccd6d9;
  --land:       #efebe1;
  --land-edge:  #b9b3a4;
  --ink:        #171b1e;
  --ink-soft:   #5c666b;
  --ink-faint:  #8d979c;
  --accent:     #8e2c33;
  --accent-dim: #b4666b;
  --elsewhere:  #b26a1c;
  --birth:      #2f7d4f;
  --death:      #3d434b;
  /* The selection. Violet because it has to be separable from the accent at
     4.6px, and the accent is a warm red: a second warm colour would be a
     shade of the same thing at dot size. One token, used by the map for the
     chosen point and by the card panel that point opened, so the two are
     visibly the same object. */
  --pick:       #6b3fa0;
  --scroll:     #9c958a;
  --scroll-hot: #6f685e;
  --panel:      #f6f4ee;
  --panel-edge: #cfcabb;
  --rule:       #c6c0b1;
  --shadow:     0 1px 2px rgba(23,27,30,.10), 0 8px 24px rgba(23,27,30,.10);
  --focus:      #2f6f7d;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --sea:        #0d1215;
    --sea-deep:   #0a0e11;
    --land:       #1c2328;
    --land-edge:  #333d43;
    --ink:        #dce1e3;
    --ink-soft:   #94a0a5;
    --ink-faint:  #67737a;
    --accent:     #c9606a;
    --accent-dim: #8d444c;
    --pick:       #b085e8;
    --elsewhere:  #d8912f;
    --birth:      #5fae7f;
    --death:      #a7b0ba;
    --scroll:     #333e45;
    --scroll-hot: #4b5a63;
    --panel:      #151b1f;
    --panel-edge: #2b343a;
    --rule:       #2b343a;
    --shadow:     0 1px 2px rgba(0,0,0,.5), 0 10px 30px rgba(0,0,0,.45);
    --focus:      #6fb3c2;
  }
}
:root[data-theme="dark"] {
  --sea:        #0d1215;
  --sea-deep:   #0a0e11;
  --land:       #1c2328;
  --land-edge:  #333d43;
  --ink:        #dce1e3;
  --ink-soft:   #94a0a5;
  --ink-faint:  #67737a;
  --accent:     #c9606a;
  --accent-dim: #8d444c;
  --pick:       #b085e8;
  --elsewhere:  #d8912f;
  --birth:      #5fae7f;
  --death:      #a7b0ba;
  --scroll:     #333e45;
  --scroll-hot: #4b5a63;
  --panel:      #151b1f;
  --panel-edge: #2b343a;
  --rule:       #2b343a;
  --shadow:     0 1px 2px rgba(0,0,0,.5), 0 10px 30px rgba(0,0,0,.45);
  --focus:      #6fb3c2;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--sea);
  color: var(--ink);
  font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
  font-size: 14px;
  line-height: 1.5;
  overflow: hidden;
  height: 100vh;
}

#stage { position: fixed; inset: 0; }
/* THE MAP CENTRES IN THE SPACE THE CARD LEAVES IT -- but it keeps the whole
 * window to draw in, and that is the point.
 *
 * The stage used to give up the card's width, which centred both surfaces in
 * the strip and was half right. It also stopped the map at the card's edge, so
 * the panel sat on a black rectangle and the planet ended where the text began.
 * The card is a panel OVER a world, and below it -- the card is only as tall as
 * its content -- the world has to still be there.
 *
 * So the canvas keeps the window and the CAMERA moves instead: globesurface
 * shifts its lens left by half of what the card covers, which puts the sphere's
 * centre in the middle of the strip while the sphere itself carries on behind
 * the panel. The flat map has always done the same thing by biasing its view
 * longitude -- app.ts's offCentre -- and both now measure the overlap from one
 * function, cardOverlap in cardview.ts.
 *
 * Nothing here, therefore. The rule this replaces is kept in the history for
 * the reasoning; --card-w stays because the panel's own width is still one
 * number. Changed on the owner's instruction, 2026-09-03. */
#map { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; }
#map.dragging { cursor: grabbing; }
/* A reader who chose the globe last time does not get shown the flat map first.
   The opening inline script sets this when it asks for the globe's bundle, and
   mode.ts takes it off the moment the globe is mounted -- or has failed, in
   which case the flat map is what they get after all.

   visibility rather than display: the canvas keeps its box, so mapview measures
   the viewport it will actually be drawn into. Hiding it with display:none gave
   resize() a 0x0 canvas and a map that came back the wrong size. */
body.globe-first #map { visibility: hidden; }

/* Whose moments are on the map, with the way back to everyone.

   Top left, under the header, where the masthead's own words sit on the atlas
   -- the reader has replaced "every life" with "this life", so the label
   replaces it in the same place. Fixed, above the map and below the card,
   and it lets the pointer through except on the two things worth pressing. */
.backbar {
  position: fixed;
  top: calc(var(--bar-h) + 14px);
  left: 22px;
  z-index: 5;
  display: flex; align-items: baseline; gap: 10px;
  padding: 7px 12px;
  background: var(--panel);
  border: 1px solid var(--panel-edge);
  border-radius: 3px;
  box-shadow: var(--shadow);
  max-width: min(30rem, 70vw);
}
.backbar[hidden] { display: none; }
.backbar-out {
  flex: none;
  font-size: 11.5px;
  letter-spacing: .02em;
  color: var(--ink-soft);
  text-decoration: none;
  white-space: nowrap;
}
.backbar-out:hover, .backbar-out:focus-visible { color: var(--accent); }
.backbar-who {
  font-family: "EB Garamond", Georgia, serif;
  font-size: 17px;
  line-height: 1.1;
  color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (max-width: 720px) { .backbar { left: 16px; max-width: 64vw; } }

.masthead {
  position: fixed; top: 0; left: 0;
  /* Clear of the header, which is fixed at top 0 and would otherwise sit on the
     standfirst. Padding rather than `top`, so one property answers the question
     in both this rule and the document-mode one below. §2.3. */
  padding: calc(var(--bar-h) + 18px) 22px 20px;
  max-width: min(30rem, 78vw);
  pointer-events: none;
  z-index: 3;
}
.eyebrow {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10.5px;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 4px;
}
.masthead h1 {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  /* A name, not a sentence, so it can carry more size and less width. */
  font-size: clamp(1.9rem, 4vw, 2.9rem);
  line-height: 1.02;
  margin: 0;
  text-wrap: balance;
  letter-spacing: -.012em;
}
.standfirst {
  margin: 8px 0 0;
  color: var(--ink-soft);
  font-size: 13px;
  max-width: 30rem;
}

/* .mode is gone. It was a second fixed strip across the top -- the person's
   name, their dates, a count -- switched on when a marker was clicked, and it
   is what the one server-rendered header replaced: the name and the dates are
   crumbs and the trailing slot, present on the first byte rather than written
   by script after a click. Its mark was 28px here and 88px on the 404, which is
   the drift the single header exists to end. Removed 2026-08-27; the count it
   also held now lives in the timeline's head row as .tl-count. */

button {
  font: inherit; font-size: 12px;
  color: var(--ink); background: transparent;
  border: 1px solid var(--rule); border-radius: 2px;
  padding: 4px 10px; cursor: pointer;
}
button:hover { border-color: var(--accent); color: var(--accent); }
button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
button[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
/* After the hover rule, so a control with nothing to do does not light up under
   the pointer as though it had. */
button:disabled {
  opacity: .4; cursor: default;
  border-color: var(--rule); color: var(--ink-faint);
}

.timeline {
  position: fixed; bottom: 0; left: 0; right: 0;
  background: var(--panel);
  border-top: 1px solid var(--panel-edge);
  padding: 12px 22px 14px;
  z-index: 4;
  display: grid; gap: 9px;
}
.tl-head { display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap; min-width: 0; }
.window-read {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 13px; font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.window-read b { color: var(--accent); font-weight: 500; }
.window-read .to { color: var(--ink-faint); }
.tl-note { font-size: 11.5px; color: var(--ink-faint); }
/* The count the mode bar used to hold. No margin-left: auto -- .spans below
   claims the trailing space, and two auto margins in one row would split it. */
.tl-count { font-size: 12px; color: var(--ink-faint); }
.spans {
  margin-left: auto; display: flex; gap: 6px; align-items: center;
  flex-wrap: wrap; justify-content: flex-end;
}
#spanButtons { display: inline-flex; gap: 6px; }
/* Set apart from the run of window lengths on its right: it undoes the drawing,
   which has nothing to do with choosing how much time is on screen. */
.reset { margin-right: 10px; }

/* The handle and the button that moves it for you, on one line. */
.transport { display: flex; align-items: center; gap: 12px; }
.play {
  flex: none; width: 30px; height: 30px; padding: 0;
  display: grid; place-items: center;
  font-size: 11px; line-height: 1;
  /* The glyphs are a triangle and two bars, which sit differently in the box;
     nudging the play arrow is cheaper than finding a pair that agree. */
  letter-spacing: .08em;
}
/* `display: grid` above outranks the UA's [hidden] rule, so the attribute
   atlas.ts sets did nothing and the button sat on the atlas root offering to
   play the whole of human history. The third time this file has been caught by
   the same specificity -- .card-close had it, and .card-back after it.

   Gone rather than dimmed: only a life has a sequence to walk, so on the root
   there is nothing for the control to mean, and display:none lets the track
   take the width it leaves -- including the transport's own 12px gap, which a
   flex row does not draw beside an item that is not there. */
.play[hidden] { display: none; }
.track { position: relative; height: 34px; flex: 1; min-width: 0; }
.ticks { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
input[type="range"] {
  -webkit-appearance: none; appearance: none;
  position: absolute; left: 0; right: 0; top: 9px;
  width: 100%; height: 16px; background: transparent; margin: 0;
}
input[type="range"]::-webkit-slider-runnable-track { height: 3px; background: var(--rule); }
input[type="range"]::-moz-range-track { height: 3px; background: var(--rule); }
/* Three pixels of mark, two of grab on either side of it.
 
   The hairline is the design -- a wide handle covers the ticks it is there to
   stand between -- but a 3px target is one you have to aim at, and the cursor
   only turns to ew-resize over the thumb itself, so there was no warning you
   had missed until the drag failed to start. The transparent side borders widen
   the box the pointer must hit without widening the mark: background-clip keeps
   the paint on the 3px content box inside them. */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  box-sizing: border-box;
  width: 7px; height: 20px; margin-top: -9px;
  border: 0 solid transparent; border-left-width: 2px; border-right-width: 2px;
  background: var(--accent); background-clip: content-box;
  border-radius: 0;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
  cursor: ew-resize;
}
input[type="range"]::-moz-range-thumb {
  box-sizing: border-box;
  width: 7px; height: 20px;
  border: 0 solid transparent; border-left-width: 2px; border-right-width: 2px;
  background: var(--accent); background-clip: content-box;
  border-radius: 0; cursor: ew-resize;
}
input[type="range"]:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
/* Focus taken on the reader's behalf, so the arrow keys work after a comb click,
   should not also announce itself. The ring answers "where is my keyboard"; a
   click has already answered "where is the window". atlas.ts drops the class at
   the first keypress or on blur, so the ring is back before it is needed. */
input[type="range"].quiet:focus-visible { outline: none; }

.card {
  /* Below the one header, on every page. The header's right-hand end carries
     the finder everywhere now, and a panel at top:0 with z-index 6 lands
     squarely on the field -- which is exactly what the owner photographed on a
     server-rendered life. This began as an atlas-only override; it stopped
     being an override the day the person page grew the same search box. */
  position: fixed; top: var(--bar-h); right: 0;
  /* One number, because the map now gives up exactly this much to stay centred
     in what is left -- see #stage below. Two copies would drift, and the drift
     would be a globe half a card off centre. */
  width: var(--card-w);
  /* Stops above the timeline rather than running the full height of the window.
     --tl-h is measured at runtime by mapview -- the timeline's own text wraps,
     so a literal here would either overlap it or float above it -- and it is the
     same token the zoom buttons sit on. Full height put the card over the right
     end of the scrubber and the controls there could not be reached at all. */
  /* Down TO the timeline, not 14px shy of it. The card used to stop short so
     its hard bottom edge did not sit against the scrubber's top edge -- two
     lines a pixel apart looked like a mistake. The foot fades now, so there is
     no edge to keep clear of: the fade IS the gap, and the 14px it was buying
     was height the reader could have been reading in. The zoom buttons keep
     their own clearance below; that offset is theirs, not this one's. */
  max-height: calc(100vh - var(--bar-h) - var(--tl-h, 96px));
  overflow-y: auto;
  /* Thin and dark, so a card that has to scroll says so without a pale bar down
     its edge. scrollbar-color is the standard property; the ::-webkit rules
     below are what Safari and Chrome actually read. */
  scrollbar-width: thin;
  scrollbar-color: var(--ink-faint) transparent;
  background: var(--panel);
  /* The selection's colour, on the panel the selection opened. The map says
     which point is chosen and this says the panel is about that point -- one
     token, so they cannot disagree. 3px because a 1px violet line against
     --panel-edge is a difference nobody notices at the edge of the screen. */
  border-left: 3px solid var(--pick);
  box-shadow: var(--shadow);
  /* Half the top it used to have, on the owner's instruction: 20px above a
     control row only 26px tall read as a gap rather than as breathing room.
     The sides and the foot are unchanged. */
  /* 26px of bottom padding was the same idea one layer in: space so the last
     line did not end against the edge. The fade does that job and does it
     over more distance, so this keeps only enough to stop a descender
     touching the mask's steepest part. */
  padding: 10px 22px 10px;
  z-index: 6;
  transform: translateX(101%);
  transition: transform .26s cubic-bezier(.22,.61,.36,1);
}
.card.on { transform: none; }
.card::-webkit-scrollbar { width: 8px; }
.card::-webkit-scrollbar-track { background: transparent; }
.card::-webkit-scrollbar-thumb {
  background: var(--ink-faint);
  border-radius: 4px;
  /* Inset by a transparent border, so the thumb reads as a slim bar rather than
     as a block filling the gutter. */
  border: 2px solid transparent;
  background-clip: content-box;
}
.card::-webkit-scrollbar-thumb:hover { background-color: var(--ink-soft); background-clip: content-box; }
/* The card's share of the tour, along its top edge, in the colour the marker
   uses for it: red for a moment he was at, orange for one he was not.
 
   The card gives up its top padding while the clock is running and the clock
   gives back exactly as much below itself, so switching it on moves nothing --
   and sticky then works, which it could not against a negative top margin: a
   sticky box is clamped inside its containing block, so the margin pulled it up
   into the padding and the clamp put it straight back. Sticky matters on a card
   long enough to scroll, where a clock that scrolls away stops telling the time.
   Negative side margins take it out through the padding to the panel's edges. */
.card:has(.run:not([hidden])) { padding-top: 0; }
/* THE FOOT OF THE CARD FADES rather than stopping.

   The panel is often taller than the window allows, so it scrolls -- and
   whatever sat at the cut simply ended: a portrait sliced across the collar at
   the card's own edge, which reads as a rendering fault rather than as more to
   read. The last 56px dissolve into the panel's own colour instead, so the
   edge says "there is more" and not "this is broken".

   position: sticky, because .card is the scroller. A normally-positioned
   ::after would scroll away with the content it is meant to cover, and a fixed
   one would need the card's computed bottom edge, which max-height decides at
   runtime; sticky stays at the bottom of the scrollport without being told
   where that is. The negative margin lifts it back over the content rather
   than adding height of its own, and pointer-events: none keeps it from
   swallowing a click on the picture underneath. */
.card {
  /* The mask, not an overlay. Three attempts with a sticky ::after all landed
     short of the card's own bottom: sticky is clamped to its parent's CONTENT
     box, so the padding and a negative margin fight over the last 26px and the
     picture reappeared below the fade. A mask has no flow and no containing
     block -- it is applied to the element's rendered box, which is exactly the
     edge the reader sees cut. */
  --card-fade: linear-gradient(to bottom,
    #000 calc(100% - 46px),
    /* Still nearly opaque most of the way down, so the panel stays the panel
       and only the last sliver lets the map through at all. A mask fades to
       whatever is BEHIND -- it cannot fade to a colour -- so the curve is
       shaped to spend its softness on the content and almost none of it on
       the background. */
    rgb(0 0 0 / 82%) calc(100% - 16px),
    transparent);
  -webkit-mask-image: var(--card-fade);
  mask-image: var(--card-fade);
}
.run {
  position: sticky; top: 0; z-index: 2;
  height: 2px; margin: 0 -22px 18px;
  background: color-mix(in srgb, var(--ink) 10%, transparent);
}
.run i {
  display: block; height: 100%;
  background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  animation: run-out var(--dwell, 3s) linear forwards;
}
.run.absent i { background: var(--elsewhere); }
/* Not disabled under prefers-reduced-motion: this animation is the clock the
   tour runs on, and a clock with no duration would race through the dataset. */
@keyframes run-out { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@media (prefers-reduced-motion: reduce) { .card { transition: none; } }
/* A square x, floated into the corner. Floated rather than positioned because
   the title and the metadata should flow around it -- an absolute button forces
   a permanent 40px gutter down the whole head of the card for the sake of one
   22px glyph. */
/* The one way back: a glyph in the chevrons' own register, not a picture.
   It was the owner's red arrow as a 30px png, and a drawn image in a row of
   typographic controls read as an imported thing rather than part of the set --
   so it is an arrow the typeface draws, sized and coloured like the ‹ › beside
   it and lit by the accent only on hover, like everything else here.

   Pushed deeper into the corner than the row alone would put it: margin-left
   auto takes it to the row's right edge, and the negative margins spend part of
   the card's own 20px/22px padding so it sits in the corner rather than inset
   from it. Up, not back -- one press is one level, a moment to its life and the
   life to everyone. */
/* ONE SIZE FOR EVERY CONTROL ON THE BAR, and the size is the hit area.
   30px, up from 26. The glyph inside is 16px, so more than two thirds of what
   the reader can press is empty space around it -- which is the point: the
   complaint was having to aim, and a target you have to aim at is a target that
   is the size of its picture rather than the size of its box. The hover tint
   fills the whole square, so the area is visible before it is needed. */
:root { --card-control: 30px; }

.card-back {
  /* auto pushes it to the far right of the bar; the negative top margin lifts
     it level with the card's own top edge, as it always did. */
  margin: -6px -6px 0 auto;
  width: var(--card-control); height: var(--card-control); padding: 0;
  display: grid; place-items: center;
  border: 0; border-radius: 3px;
  font-family: "EB Garamond", Georgia, serif;
  font-size: 19px; line-height: 1;
  color: var(--ink-soft);
  text-decoration: none;
  transition: color 120ms ease, background-color 120ms ease;
}
.card-back:hover, .card-back:focus-visible {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: none;
}
/* display:grid above outranks the UA's [hidden] rule -- the same trap the
   close button had, and the card picture's rule after it. A moment's card
   hides nothing here, but a document rendered before the renderer filled the
   href does, and without this it would show an arrow pointing nowhere. */
.card-back[hidden] { display: none; }

/* One row, above everything it acts on. The star leads, the chevrons pair
   beside it, the position follows THEM rather than sitting between them, and
   the way back is at the far right on margin-left auto -- so the title below
   owns the card's full width and nothing in the heading can wrap.
   The count moved out from between the chevrons on 2026-09-05: it is empty on
   a person's card and "1 / 75" on a moment, so with it in the middle the next
   chevron stood somewhere different on each and moved under the pointer as the
   reader stepped in. */
.card-bar {
  display: flex; align-items: center; gap: 2px;
  margin: 0 0 10px;
}
/* Where this moment falls in the life. Tabular so the row does not twitch as
   the number changes while a tour steps through. */
.card-count {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11.5px; font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  padding: 0 4px;
  white-space: nowrap;
}
.card h2 {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500; font-size: 1.42rem; line-height: 1.14;
  margin: 0 0 8px; text-wrap: balance;
}
/* Step to the moment before or after this one.
   Inline, so the heading stays a block that wraps around the floated close
   button. Sized in em so they track the title rather than being set in pixels
   beside type that is not. */
/* In the control row now, not inside the heading, so there is no line box to
   protect and the negative margin that used to cancel the padding is gone. A
   26px square target around a glyph that reads as a control rather than as
   punctuation -- the first pass was 0.86em of --ink-faint and did neither. */
.card-step {
  width: var(--card-control); height: var(--card-control);
  padding: 0; margin: 0;
  display: grid; place-items: center;
  border: 0; background: none; border-radius: 3px;
  color: var(--ink-soft);
  cursor: pointer;
  transition: color 120ms ease, background-color 120ms ease;
}

/* THE CHEVRONS ARE DRAWN, NOT TYPED.
   They were &lsaquo; and &rsaquo; in EB Garamond at 22px, and a glyph is
   centred on its own metrics rather than on its box: side bearings and the
   font's baseline put the mark off the middle of the button by a couple of
   pixels, differently in each direction, and the way back was a ↑ from a
   different family again -- three controls in a row, three alignments, three
   weights. A path in a square viewBox has none of that. Each of these spans
   x 8.5..15.5 and y 5.5..18.5 about the centre of a 24-box, so what centres it
   is arithmetic rather than a typeface.
   currentColor throughout, so every state the buttons already had -- hover,
   focus, disabled, pressed -- goes on working without knowing about this. */
.chev {
  width: 16px; height: 16px; display: block;
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
/* :not([disabled]) because a disabled button still matches :hover, and an end
   of the life that lit up under the pointer would be offering a step it will
   not take. */
.card-step:not([disabled]):hover, .card-step:focus-visible {
  color: var(--accent);
  /* The tint is what makes the enlarged hit area discoverable: without it the
     only thing that changes is a glyph, and the reader cannot see how much of
     the space around it is theirs to click. Same mix .peek-one uses. */
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: none;
}
.card-step:focus-visible { box-shadow: inset 0 0 0 1px var(--accent); }
/* At either end of the life there is nothing to step to. Kept in place rather
   than hidden, so the title does not shift as the reader walks the route. */
/* A step that leaves one end of the life for the other. Dimmed rather than
   marked, because it still works -- it is the same action, just further -- and
   anything louder would read as a warning about pressing it. */
.card-step.wraps { color: var(--ink-faint); }
.card-step[disabled] {
  color: var(--ink-faint);
  opacity: 0.28;
  cursor: default;
}
.meta {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11.5px; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  margin: 0 0 12px;
}
/* Blocks, not a grid. A grid box establishes its own formatting context and so
   sidesteps the floated x instead of wrapping around it -- it would sit narrowed
   beside the button and stay narrow for the rest of its lines. */
.meta > * { display: block; }
.meta > * + * { margin-top: 2px; }
.card figure { margin: 0 0 12px; }
/* Hugs the picture rather than the panel: a portrait scaled to 400px tall is
   narrower than the card, and the credit line reads better under the picture
   it credits than under the whole width of the panel. */
.shot {
  position: relative;
  width: max-content; max-width: 100%;
  margin: 0 auto;
}
.card img {
  /* Capped at 400px tall, and scaled rather than cropped: width and height are
     both auto so the aspect ratio survives, which matters when the picture is a
     portrait and the subject is the middle of it. */
  display: block; margin: 0 auto;
  width: auto; max-width: 100%;
  height: auto; max-height: 400px;
  border: 1px solid var(--panel-edge); background: var(--sea-deep);
}
/* `display: block` above outranks the UA's [hidden] rule, so a card with no
   picture would otherwise keep an empty bordered box where one would go. */
.card img[hidden] { display: none; }
.card figcaption { font-size: 11px; color: var(--ink-faint); margin-top: 6px; line-height: 1.45; }
.card figcaption a { color: var(--ink-soft); }
.badge {
  display: inline-block;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid var(--accent-dim); color: var(--accent);
  padding: 1px 5px; border-radius: 2px; margin-right: 6px; vertical-align: 1px;
}
.flags { display: flex; flex-wrap: wrap; gap: 5px; margin: 12px 0 0; }
.flag {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10px; letter-spacing: .05em; color: var(--ink-soft);
  border: 1px solid var(--rule); border-radius: 2px; padding: 2px 6px;
}
.elsewhere {
  font-size: 12px; line-height: 1.45;
  color: var(--ink-soft);
  border-left: 2px solid var(--elsewhere);
  padding: 2px 0 2px 10px; margin: 0 0 14px;
}
.elsewhere b { color: var(--elsewhere); font-weight: 600; }
.noimage {
  font-size: 12px; color: var(--ink-faint);
  border: 1px dashed var(--rule); padding: 10px 12px; border-radius: 2px;
  margin-bottom: 12px;
}

/* ---- the hover container: one at a time, and it stays ---- */
.peek {
  position: fixed; z-index: 7;
  display: none;
  min-width: 14rem; max-width: 27rem;
  background: var(--panel);
  /* Quiet. A container that arrived on hover is following the pointer around,
     and an accent edge on every marker the cursor crosses turns the map into a
     string of alarms. The shadow is enough to lift it off the sea. */
  border: 1px solid var(--panel-edge);
  box-shadow: var(--shadow);
}
.peek.on { display: block; }
/* The card leads somewhere, so it has to look like it does and be reachable.
   pointer-events is stated rather than left to the default: the ghost beside it
   sets none, and a reader should not have to know which of the two they are
   looking at. The hover tint is the only thing that changes, because the card
   is anchored a few pixels off its dot and anything that resized it on hover
   would move it out from under the pointer. */
.peek.clickable {
  cursor: pointer;
  pointer-events: auto;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.peek.clickable:hover {
  /* The same tint .peek-one uses for a row of a cluster list, so the two ways
     of reaching a person out of this card highlight identically. */
  background: color-mix(in srgb, var(--accent) 12%, var(--panel));
  border-color: var(--accent);
}
/* Clicked into place, and the only thing that colours the edge. The border box
   is the same 1px in both states, so nothing moves as it lights up -- which
   matters here, where the container is anchored a few pixels from its marker. */
.peek.pinned { border-color: var(--accent); }
.peek.pinned.elsewhere-edge { border-color: var(--elsewhere); }

/* Top right of the container, out of the flow, so that one rule covers both
   what a single moment looks like and what a crowd looks like. */
.peek-x {
  position: absolute; top: 4px; right: 4px; z-index: 1;
  width: 20px; height: 20px; padding: 0; margin: 0;
  display: grid; place-items: center;
  font-size: 13px; line-height: 1;
  background: var(--panel);
}

/* The whole of a single-moment container is the target: a title with a date
   under it is one thing to click, not two. */
.peek-one {
  display: block; width: 100%; text-align: left;
  background: none; border: none; padding: 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer;
}
.peek-one:hover, .peek-one:focus-visible {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: none;
}
.peek-one:focus-visible { box-shadow: inset 2px 0 0 var(--accent); }

.peek-title {
  font-family: "EB Garamond", Georgia, serif;
  font-size: 1.05rem; line-height: 1.2;
  /* Right padding clears the x, which is painted over this corner. */
  padding: 8px 32px 2px 12px;
}
.peek-sub {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10.5px; color: var(--ink-faint);
  padding: 0 12px 9px; font-variant-numeric: tabular-nums;
}
.peek-sub .why { color: var(--elsewhere); }

/* ---- the attract rotation: one card cross-fades into the next ---- */
/* Atlas only. The person page's peek is instant on purpose: it chases the
   pointer, and a fade on something chasing the cursor reads as lag. Here the
   card arrives by itself every three seconds, and a cut between two boxes in
   two different places is a flicker rather than a change of subject. */
.peek.arriving { animation: peek-in var(--peek-fade, 420ms) ease both; }
/* The copy left behind at the old position while the new one comes up. */
.peek-ghost { pointer-events: none; }
.peek-ghost.fading { animation: peek-out var(--peek-fade, 420ms) ease both; }
@keyframes peek-in { from { opacity: 0 } to { opacity: 1 } }
@keyframes peek-out { from { opacity: 1 } to { opacity: 0 } }
/* The rotation still runs -- it is how the map introduces itself -- but it
   changes over rather than fading, which is the part that moves. */
@media (prefers-reduced-motion: reduce) {
  .peek.arriving, .peek-ghost.fading { animation-duration: 1ms; }
}

.peek-head {
  display: flex; align-items: center; gap: 6px;
  padding: 6px 32px 6px 12px;
  border-bottom: 1px solid var(--panel-edge);
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-faint);
}
/* Five rows on the map, where there is room for them, against three in the
   card. The rest scroll in both. */
.peek-list { max-height: 8.4rem; overflow-y: auto; padding: 3px 0; }

/* Thin, dark, and out of the way. A dozen moments at one place is a list you
   read, and the platform's default bar is wide enough to look like a component. */
.peek-list, .card {
  scrollbar-width: thin;                       /* Firefox and the standard */
  scrollbar-color: var(--scroll) transparent;
}
.peek-list::-webkit-scrollbar,
.card::-webkit-scrollbar { width: 7px; height: 7px; }
.peek-list::-webkit-scrollbar-track,
.card::-webkit-scrollbar-track { background: transparent; }
.peek-list::-webkit-scrollbar-thumb,
.card::-webkit-scrollbar-thumb {
  background: var(--scroll);
  border-radius: 4px;
  /* Inset from the edge, so the bar reads as a hint rather than a rail. */
  border: 1px solid transparent;
  background-clip: content-box;
}
.peek-list::-webkit-scrollbar-thumb:hover,
.card::-webkit-scrollbar-thumb:hover { background: var(--scroll-hot); background-clip: content-box; }
.peek-list::-webkit-scrollbar-corner,
.card::-webkit-scrollbar-corner { background: transparent; }

.row {
  display: flex; align-items: baseline; gap: 8px;
  padding: 4px 12px; width: 100%;
  border: none; background: none; text-align: left; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--ink);
  white-space: nowrap;
}
.row .t { overflow: hidden; text-overflow: ellipsis; }
.row .d {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10.5px; color: var(--ink-faint);
  margin-left: auto; flex: none; font-variant-numeric: tabular-nums;
}
.row:hover, .row:focus-visible {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  outline: none;
}
.row.absent .t { color: var(--elsewhere); }
.row[aria-selected="true"] { box-shadow: inset 2px 0 0 var(--accent); }

/* ---- zoom, bottom left, clear of the timeline ---- */
.zoomers {
  position: fixed; left: 22px; z-index: 5;
  /* The timeline's height is measured at runtime: its own text wraps, so a
     literal here would either overlap it or float above it. */
  bottom: calc(var(--tl-h, 96px) + 14px);
  /* Two columns: the buttons stack in the first, the scale sits in the second
     against the LAST of them, so it reads as belonging to the zoom rather than
     floating beside the map -- and so it ends where the buttons end, just clear
     of the timeline. It said `grid-row: 2` when there were two buttons, which
     was the last row then; the mode toggle arrived as a third and the scale
     stayed where it was, floating a button's height above the bottom of the
     cluster it belongs to. A row number is a claim about how many rows there
     are, so the scale names the toggle's row rather than a number of its
     own. */
  display: grid; gap: 4px 10px;
  grid-template-columns: 30px auto;
  align-items: end;
}
#zoomIn  { grid-column: 1; grid-row: 1; }
#zoomOut { grid-column: 1; grid-row: 2; }
/* The third row of the same column. 30x30 comes from `.zoomers button` below,
   so this only has to say where it sits -- and how the icon sits inside it.
   The other two buttons hold text, which a button centres for you; an svg is
   replaced content and sits wherever the box leaves it, so it needs saying.
   place-items centres it the way a text button centres its label. */
#modeToggle { grid-column: 1; grid-row: 3; display: grid; place-items: center; }
/* The row the scale hangs off. Named once here, so adding a fourth control
   moves both rather than leaving the scale behind again. */
#modeToggle, .scalebar { grid-row: 3; }
#modeToggle svg { display: block; }
/* Pressed means the globe is showing, and the control then offers the way back
   -- so it wears the accent rather than looking like an unpressed button that
   happens to have a different icon. */
#modeToggle[aria-pressed="true"] { color: var(--accent); }
.scalebar {
  grid-column: 2;
  display: grid; gap: 2px; justify-items: start;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10.5px; color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  pointer-events: none; user-select: none;
}
/* A bar, not a number: a length you can hold against the map is the reading,
   and the number only says what the length means. The end ticks are what make
   it measurable rather than decorative. */
.scalebar-line {
  height: 6px; min-width: 1px;
  border: 1px solid var(--ink-faint);
  border-top: 0;
  background: transparent;
}
.tl-res { font-size: 11.5px; color: var(--ink-faint); margin-right: 10px; }
.zoomers button {
  width: 30px; height: 30px; padding: 0;
  font-size: 15px; line-height: 1;
  background: var(--panel);
  font-variant-numeric: tabular-nums;
}


@media (max-width: 720px) {
  /* The bar's height is a constant, so at narrow widths something has to give
     width rather than the row gaining a line. The finder yields first: the
     crumbs say where you are, which is the header's job, and the search box is
     a thing you go to. §2.6.

     Not the magnifier-that-expands-on-focus the spec describes -- that needs a
     focus handler in atlas.ts, and this plan does not touch it. */
  .finder { width: min(180px, 34vw); }
  .crumb { font-size: 1rem; }
  #bar { padding: 0 16px; gap: 8px; }

  .masthead { padding: 12px 16px; max-width: 100%; }
  .masthead .standfirst { display: none; }
  .timeline { padding: 10px 16px 12px; }
}

/* ==========================================================================
   The document under the map

   Every page arrives with its content in the HTML -- see "Every page is
   server-rendered" in the repository's CLAUDE.md. For a reader who never runs
   the script, that content *is* the page: the instruments for a map that will
   not be drawn stand aside, and the moments are a list you can read.

   Two classes, not one. .hydrated means the map is up, and app.ts adds it in
   the start block. .booting means a script is running and will hydrate, and an
   inline script at the top of <body> adds it before the first paint -- which
   .hydrated cannot be, because app.ts is a module and a module is deferred by
   definition. Without that, the browser painted the whole document and the
   module then hid it: a flash of a different page on every navigation.

   So the document rules below stand down for either class, and a reader with no
   JavaScript sets neither and keeps the document.
   ========================================================================== */

body:not(.hydrated):not(.booting) { overflow: auto; height: auto; background: var(--panel); }
body:not(.hydrated):not(.booting) #stage,
body:not(.hydrated):not(.booting) .timeline,
body:not(.hydrated):not(.booting) .zoomers,
body:not(.hydrated):not(.booting) .peek { display: none; }
body:not(.hydrated):not(.booting) .masthead {
  position: static;
  /* A fixed header takes no space in flow, so the document reserves it or its
     first block renders under the bar. Folded into this rule's own padding
     rather than added as a separate padding-top, so one rule owns one box.
     .moments is deliberately not given it: .masthead is static here and always
     precedes it, so the height is already reserved, and padding .moments too
     would open a 52px gap mid-document. §2.3. */
  pointer-events: auto;
  max-width: 44rem;
  margin: 0 auto;
  padding: 34px 22px 8px;
}
/* "Drag the scrubber to move through time" is an instruction for a map that
   document mode does not have. It comes back with the map. */
body:not(.hydrated):not(.booting) .masthead .standfirst { display: none; }
body:is(.hydrated, .booting) .moments { display: none; }

.moments {
  max-width: 44rem;
  margin: 0 auto;
  padding: 8px 22px 64px;
}
.moments-head {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 18px 0 0;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--rule);
}
.moment-list { list-style: none; margin: 0; padding: 0; }
.moment {
  padding: 22px 0;
  border-bottom: 1px solid var(--rule);
}

/* The two ends of the life, marked the same way the map marks their dots.
   An inset edge rather than a border, so nothing shifts by a pixel. */
.moment.birth {
  box-shadow: inset 3px 0 0 var(--birth);
  padding-left: 14px;
}
.moment.death {
  box-shadow: inset 3px 0 0 var(--death);
  padding-left: 14px;
}
.moment-title {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: 1.35rem;
  line-height: 1.15;
  margin: 0;
  letter-spacing: -.008em;
}
.moment-title a { color: var(--ink); text-decoration: none; }
.moment-title a:hover { text-decoration: underline; }
.moment-when {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11px;
  color: var(--ink-faint);
  margin: 5px 0 0;
}
.moment-figure { margin: 12px 0 0; }
.moment-shot {
  display: block;
  max-width: 100%;
  /* A list of moments, not a gallery: a 1600px portrait would otherwise stand
     three screens tall and the list would stop reading as one. */
  max-height: 380px;
  width: auto;
  height: auto;
  border: 1px solid var(--panel-edge);
}
/* A picture with no author and no source is decoration. */
.moment-credit {
  font-size: 11px;
  color: var(--ink-faint);
  margin-top: 6px;
  line-height: 1.45;
}
.moment-credit a { color: var(--ink-soft); }
.moment-credit .badge { margin-right: 6px; }
.moment-body {
  margin: 10px 0 0;
  color: var(--ink-soft);
  max-width: 34rem;
}
.moment-absent {
  margin: 8px 0 0;
  font-size: 13px;
  color: var(--elsewhere);
  max-width: 34rem;
}

/* The card's own prose, in the panel. Server-rendered for a ?card= link and
   rewritten by paintCard on click -- the same words either way. */
.card-body {
  margin: 0 0 12px;
  color: var(--ink-soft);
  font-size: 13.5px;
}
.card-body[hidden] { display: none; }

/* ------------------------------------------------------------- the index */

body.index-page {
  overflow: auto;
  height: auto;
  background: var(--panel);
}
.index-head {
  max-width: 44rem;
  margin: 0 auto;
  padding: 56px 22px 0;
}
.index-head h1 {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: clamp(2rem, 5vw, 3.1rem);
  line-height: 1.02;
  margin: 0;
  letter-spacing: -.012em;
}
.entries {
  list-style: none;
  max-width: 44rem;
  margin: 0 auto;
  padding: 26px 22px 64px;
}
.entry {
  padding: 22px 0;
  border-top: 1px solid var(--rule);
}
.entry h2 {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500;
  font-size: 1.7rem;
  line-height: 1.1;
  margin: 0;
}
.entry h2 a { color: var(--ink); text-decoration: none; }
.entry h2 a:hover { text-decoration: underline; }
.entry-span,
.entry-count {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11px;
  color: var(--ink-faint);
  margin: 5px 0 0;
}
.entry-summary { margin: 8px 0 0; color: var(--ink-soft); max-width: 34rem; }

@media (max-width: 720px) {
  .moments, .entries, .index-head { padding-left: 16px; padding-right: 16px; }
}

/* The masthead is decorative once the map is up -- it lets clicks through to
   the canvas -- but the way back to the index is not decorative. */
.eyebrow a { color: inherit; text-decoration: none; pointer-events: auto; }
.eyebrow a:hover { text-decoration: underline; }

/* A genre states what the picture is. (The "unverified" badge and its warn
   colour were removed 2026-08-29 -- the verification state stays in the data,
   it just no longer wears a label on every card.) */
.badge-genre {
  border-color: var(--rule);
  color: var(--ink-faint);
}

/* ------------------------------------------------------------- the atlas */
/* The lit aperture: the bucket the handle is in, drawn at its own resolution.
   This is where the inexactness is declared -- 500 years wide at bucket 2, ten
   at bucket 62 -- so the concept's wider aperture for antiquity is under the
   reader's thumb as well as in the axis's warp. */
.aperture {
  position: absolute; top: 0; bottom: 14px;
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border-left: 1px solid color-mix(in oklab, var(--accent) 40%, transparent);
  border-right: 1px solid color-mix(in oklab, var(--accent) 40%, transparent);
  /* Draggable, and the whole band is the handle. The lit band already says
     "this is the selection", so it should be the thing you can grab -- a 7px
     thumb is a target you have to aim at, and the band is fifty times wider.
     ew-resize is the scrubber's own thumb cursor, so the two read as one
     control rather than two. */
  cursor: ew-resize;
  touch-action: none;
  /* Above the range input, which covered sixteen of these twenty pixels and made
     the paragraph above false for most of the band it describes. */
  z-index: 2;
}
.aperture.dragging {
  background: color-mix(in oklab, var(--accent) 20%, transparent);
}
/* The handle's exact sub-bucket position: the playhead is continuous even
   though the count it selects is not. */
.aperture::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: var(--hair, 50%);
  width: 1px; background: var(--accent);
}

/* On the atlas the comb is a control, not a decoration: clicking a bar centres
   the window on that bucket, which is the difference between reaching the 1500s
   in one click and dragging the aperture across sixty-five buckets. So it has to
   sit above the range input that covers it.

   The input keeps the state and the keyboard. What it gives up to the two things
   now stacked over it is a thumb-drag that was never a distinct gesture -- loB is
   derived from hiB at a fixed width, so dragging the thumb moved the whole window
   exactly as dragging the aperture does -- and a click-to-position that the comb
   click replaces with a better-aimed version of itself.

   Scoped to body.atlas because the person page shares this markup, has no comb
   handler, and would only swallow the clicks its own scrubber wants. */
body.atlas .ticks { pointer-events: auto; cursor: pointer; z-index: 1; }
.combtip {
  position: absolute; bottom: calc(100% + 7px); transform: translateX(-50%);
  z-index: 3; pointer-events: none; white-space: nowrap;
  padding: 3px 7px; border-radius: 3px;
  background: var(--panel); border: 1px solid var(--rule);
  font-size: 11.5px; color: var(--ink);
  box-shadow: 0 2px 10px rgb(0 0 0 / 16%);
}
.combtip b { font-weight: 600; }
.combtip .dim { color: var(--ink-faint); }

/* The 404 is a document, not a map: it scrolls and it has no canvas to fill. */
body.gone { overflow: auto; height: auto; background: var(--panel); }
.gone-doc {
  padding: calc(var(--bar-h) + 40px) 24px 64px;
  max-width: 44ch; margin: 0 auto;
}
.gone-head {
  font: 500 30px/1.2 "EB Garamond", Georgia, serif; margin: 8px 0 12px;
}
.gone-doc p { color: var(--ink-soft); margin: 0 0 10px; }
.gone-doc code { font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12.5px; }
.gone-doc a { color: var(--accent); }

/* The document is the page without JavaScript, and the map is the page with it
   -- the same handover the person page performs for its moment list. */
body:is(.hydrated, .booting) .atlas-doc { display: none; }
body.atlas:not(.hydrated):not(.booting) { overflow: auto; height: auto; background: var(--panel); }
body.atlas:not(.hydrated):not(.booting) #stage,
body.atlas:not(.hydrated):not(.booting) .timeline,
body.atlas:not(.hydrated):not(.booting) .zoomers,
body.atlas:not(.hydrated):not(.booting) .peek { display: none; }
.atlas-doc {
  padding: calc(var(--bar-h) + 32px) 24px 64px;
  max-width: 64ch; margin: 0 auto;
}
.atlas-head { font: 600 30px/1.2 "EB Garamond", Georgia, serif; margin: 0 0 8px; }
.atlas-doc .standfirst { color: var(--ink-soft); margin: 0 0 16px; }
.atlas-counts { font: 400 13px/1.5 "IBM Plex Mono", monospace; color: var(--ink-faint); }

/* ------------------------------------------------------------- the header */
/* One header, one mark, on every route. Its height is a constant so that
   nothing about it can change between pages or when a page hydrates -- which is
   the whole requirement. z-index 5 leaves the card panel (6) able to slide over
   the bar's empty right end rather than the bar burying the panel's close
   button. */
#bar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 5;
  height: var(--bar-h);
  display: flex; align-items: center; gap: 12px;
  padding: 0 22px;
  background: var(--panel);
  border-bottom: 1px solid var(--panel-edge);
}
.brand {
  display: block; flex: none; width: var(--mark); line-height: 0;
  /* Explicit, because .masthead refuses pointer events so the map can be
     dragged through it, and a link inside anything that does is decoration
     until it opts back in. */
  pointer-events: auto;
}
.brand img { display: block; width: 100%; height: auto; }
.brand:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

/* The crumb row. nowrap and an ellipsis, because the bar's height is a constant
   and a crumb that wrapped would break it. */
.crumbs {
  display: flex; align-items: baseline; gap: 8px; min-width: 0;
  white-space: nowrap; overflow: hidden;
}
.crumb {
  margin: 0; font-family: "EB Garamond", Georgia, serif; font-weight: 500;
  font-size: 1.1rem; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis;
}
.crumb-up { flex: 0 1 auto; color: var(--ink-soft); text-decoration: none; }
a.crumb-up:hover { color: var(--accent); }
.crumb-here { flex: 1 1 auto; color: var(--ink); }
.crumb-sep { flex: none; color: var(--ink-faint); font-size: 12px; }

/* THE TRAILING SLOT IS THE SEARCH BOX, on every route, and there is nothing
   else it can be. .bar-trail was the other thing it could be -- a life's dates,
   in mono, pushed right by the same margin-left:auto -- and a slot with two
   possible occupants is a header with two possible shapes. The dates moved to
   the subject card; the class is gone rather than left lying about for the next
   page to reach for. */
.finder {
  margin-left: auto; flex: none; position: relative;
  width: min(320px, 38vw);
}
.finder input {
  width: 100%; box-sizing: border-box;
  padding: 9px 12px; border-radius: 6px;
  border: 1px solid var(--panel-edge); background: var(--panel);
  color: var(--ink); font: 400 14px/1.2 "IBM Plex Sans", sans-serif;
  box-shadow: 0 2px 10px var(--shadow);
}
.finder input:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
.finder-note {
  /* Out of the bar's flow, so reporting what it found cannot change the bar's
     height -- which is a constant by design. */
  position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  margin: 0; padding: 6px 10px; border-radius: 5px;
  background: var(--panel); border: 1px solid var(--panel-edge);
  font: 400 12px/1.45 "IBM Plex Sans", sans-serif; color: var(--ink-soft);
}
/* NOT HIDDEN BEFORE HYDRATION, and it never should have been.
   The rule here read `body:not(.hydrated):not(.booting) .finder { display:
   none }`, which was meant to keep a dead search box off a page whose script
   had not run. But the finder is a plain GET form -- action="/search",
   method="get" -- so it works with no JavaScript at all, which is this
   project's whole rule about pages. And the pages that never hydrate are
   exactly the ones that need it most: the SEARCH RESULTS page has no map and
   no client script, so it lost its search box permanently, and the auth slot
   beside it collapsed leftward into the gap the missing box left -- 284px
   instead of 1360. One rule, two faults, on the one page a reader arrives at
   wanting to search again. */

/* A mark on the globe.
 *
 * DOM rather than geometry: forty cylinders at thirty-two segments each is a lot
 * of triangles to rebuild whenever the data changes, and this is in the end a
 * circle with a number in it. globe.gl projects the element and writes a
 * transform; the browser draws the circle the way it draws every other circle on
 * the page -- round at any size, crisp at any zoom, and free on the GPU.
 *
 * translate(-50%, -50%) because globe.gl positions the element's top-left corner
 * at the place, and a mark means its own centre. */
.globe-mark {
  position: absolute;
  /* globe.gl overlays the HTML layer in a container it sets pointer-events:none
     on, so that the map underneath can still be dragged through it. A mark has
     to opt back in, or its own click and hover listeners never fire -- which is
     the difference between a dot and a picture of a dot. */
  pointer-events: auto;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  display: grid; place-items: center;
  font: 600 10px "IBM Plex Sans", sans-serif;
  color: var(--panel);
  line-height: 1;
  cursor: pointer;
  /* A count is a label, not a passage of text. Drilling into a cluster is two
     clicks on the same spot, which is also the gesture for selecting a word --
     so every time the globe came apart, the number inside the badge lit up
     blue. The mark is a control; controls are not selected. */
  user-select: none;
  -webkit-user-select: none;
  /* The ring is what separates a mark from the land under it, at every zoom.
     box-shadow rather than border, so it does not change the element's size and
     the number stays centred in the circle it was measured for. */
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--panel) 70%, transparent);
  transition: opacity 120ms linear;
}
/* A cluster carries a count, so it needs room for it; a lone moment does not. */
.globe-mark.is-cluster { font-size: 10.5px; }
.globe-mark.is-selected {
  box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--pick);
  z-index: 2;
}
/* The hover halo, the same one the flat map draws: a soft band in the mark's own
   colour at sixteen per cent, and a ring around it at fifty-five -- mapview's
   drawHalo, in CSS. In the mark's colour rather than the accent, because a halo
   that does not match its dot does not say which dot it belongs to.

   Two selectors for one look. :hover answers the pointer with no round trip; the
   class carries the state the APP knows about -- the moment whose card is open,
   the marker the peek is anchored to -- which the pointer alone cannot say. */
.globe-mark:hover,
.globe-mark.is-hovered {
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--panel) 70%, transparent),
    0 0 0 7px color-mix(in srgb, var(--mark) 16%, transparent),
    0 0 0 8px color-mix(in srgb, var(--mark) 55%, transparent);
  z-index: 3;
}
.globe-mark.is-selected.is-hovered,
.globe-mark.is-selected:hover {
  box-shadow:
    0 0 0 2px var(--panel), 0 0 0 4px var(--pick),
    0 0 0 10px color-mix(in srgb, var(--pick) 16%, transparent),
    0 0 0 11px color-mix(in srgb, var(--pick) 55%, transparent);
}

/* ==========================================================================
   The search results: a document, not an app.

   body carries overflow:hidden for the map pages, and a results page is the
   one page that must scroll -- so the class puts normal document flow back
   before anything else is styled. */
body.search-page { overflow: auto; height: auto; }
.search-main {
  max-width: 44rem;
  margin: 0 auto;
  padding: calc(var(--bar-h) + 26px) 22px 64px;
}
.search-count { color: var(--ink-soft); font-size: 13px; margin: 0 0 14px; }
ol.results { list-style: none; margin: 0; padding: 0; }
.results .result { padding: 14px 0; border-bottom: 1px solid var(--rule); }
.result-title { font-family: "EB Garamond", Georgia, serif; font-size: 19px; }
.result-title a { color: var(--ink); text-decoration: none; }
.result-title a:hover, .result-title a:focus-visible { color: var(--accent); }
.result-sub {
  margin-left: 10px;
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-size: 11.5px; color: var(--ink-faint);
}
.result-of { margin: 2px 0 0; font-size: 12.5px; color: var(--ink-soft); }
.result-of a { color: var(--ink-soft); }
.result-snippet { margin: 4px 0 0; font-size: 13.5px; color: var(--ink-soft); }
.result-snippet b { color: var(--ink); font-weight: 600; }
.search-nearby-h { font-size: 14px; margin: 26px 0 8px; }
.search-nearby { margin: 0; padding: 0 0 0 18px; }
.search-nearby a { color: var(--accent); }
.search-pages { display: flex; gap: 18px; margin-top: 24px; }
.search-pages a { color: var(--accent); text-decoration: none; }

/* The auth slot: the header's last item, after the finder. One .bar-auth
   whatever the state -- signed out it is a link, signed in a menu -- because
   the one-header check blanks it by that name and a second class name would
   be a second thing to keep in step. */
.bar-auth { display: flex; align-items: center; margin-left: 10px; }
.bar-auth a, .bar-auth summary, .bar-auth button {
  font: inherit; color: var(--ink-soft); text-decoration: none;
  cursor: pointer; background: none; border: 0; padding: 4px 8px;
  border-radius: 3px;
}
.bar-auth a:hover, .bar-auth summary:hover, .bar-auth button:hover,
.bar-auth a:focus-visible, .bar-auth summary:focus-visible,
.bar-auth button:focus-visible {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: none;
}
.who { position: relative; }
/* The disclosure triangle is the browser's idea of a menu, not ours. */
.who summary { list-style: none; }
.who summary::-webkit-details-marker { display: none; }
.who-menu {
  position: absolute; right: 0; top: 100%; z-index: 40;
  display: flex; flex-direction: column; align-items: stretch;
  background: var(--paper); border: 1px solid var(--rule); border-radius: 4px;
  box-shadow: 0 6px 20px rgb(0 0 0 / 18%); min-width: 9rem;
}
.who-menu a, .who-menu button { text-align: left; white-space: nowrap; }

/* The auth pages: a column of text on the paper, no map and no globe. They
   scroll like an ordinary document, which body.search-page already establishes
   for the one other page of this kind. */
body.auth-page { overflow: auto; height: auto; }
/* Reserving the fixed header, the way .prose, .search-main, .atlas-doc and
   .gone-doc all do. It was `margin: 3rem auto` -- 48px against a 52px bar --
   so every one of these pages began four pixels underneath it. Written as the
   token rather than as a number that happens to clear it today. */
.auth {
  max-width: 26rem; margin: 0 auto;
  padding: calc(var(--bar-h) + 26px) 1rem 8px;
}
/* An <h2> wearing the size the <h1> had, for the same reason as .prose-head:
   the header's last crumb is the page's only <h1>. 2em of the body's 14px is
   what a UA gives an h1, which is what these pages showed before. */
.auth .auth-head { font-size: 2em; margin: 0 0 .4rem; }
.auth .standfirst { color: var(--ink-soft); margin: 0 0 1.4rem; }
.auth .hint {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border-radius: 4px; padding: .6rem .8rem; margin: 0 0 1rem;
}
.auth .sent { margin: 0 0 1rem; }
.auth .warn { color: var(--accent); margin: 0 0 1rem; }
.doors { display: flex; flex-direction: column; gap: .5rem; }
.door {
  display: block; text-align: center; padding: .7rem 1rem;
  border: 1px solid var(--rule); border-radius: 4px;
  text-decoration: none; color: var(--ink);
}
.door:hover, .door:focus-visible { border-color: var(--accent); color: var(--accent); }
.door-email { display: flex; flex-direction: column; gap: .4rem; margin: 1.4rem 0 0; }
.door-email input {
  font: inherit; padding: .6rem .7rem; border-radius: 4px;
  border: 1px solid var(--rule); background: var(--paper); color: var(--ink);
}
.door-email button {
  font: inherit; padding: .7rem 1rem; border-radius: 4px; cursor: pointer;
  border: 1px solid var(--accent); background: var(--accent); color: var(--paper);
}
.fineprint { color: var(--ink-soft); font-size: .85em; margin: 1.4rem 0 0; }

/* The star, in the card's control row beside the chevrons. Outlined when the
   card is not kept, filled when it is -- one glyph, two states, no second
   icon to keep in step. */
/* First on the bar, and the same square as everything else on it: the whole
   thing is the button, not the star inside it. */
.card-star {
  width: var(--card-control); height: var(--card-control);
  padding: 0; border: 0; background: none;
  border-radius: 3px; display: grid; place-items: center; cursor: pointer;
  font-size: 15px; line-height: 1; color: var(--ink-soft);
  -webkit-text-stroke: 0; transition: color 120ms ease, background-color 120ms ease;
}
.card-star[aria-pressed="true"] { color: var(--accent); }
.card-star:hover, .card-star:focus-visible {
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: none;
}
/* The saved list, and what is left when a saved thing has gone. */
.saved-list { padding-left: 1.1rem; }
.saved-list li { margin: .35rem 0; }
.saved-list .gone { color: var(--ink-soft); }
.saved-list .gone em { font-size: .85em; }

/* The invitation at the foot of a card. Quiet: it is an offer, not an action
   the reader came for. */
.card-suggest {
  display: inline-block; margin-top: .6rem;
  color: var(--ink-soft); font-size: .85em; text-decoration: none;
  border-bottom: 1px dotted currentColor;
}
.card-suggest:hover, .card-suggest:focus-visible { color: var(--accent); }
/* The correction form, and the inbox that reads it. */
.suggest-form { display: flex; flex-direction: column; gap: .5rem; }
.suggest-form textarea {
  font: inherit; padding: .6rem .7rem; border-radius: 4px; resize: vertical;
  border: 1px solid var(--rule); background: var(--paper); color: var(--ink);
}
.suggest-form button {
  font: inherit; padding: .6rem 1rem; border-radius: 4px; cursor: pointer;
  align-self: flex-start;
  border: 1px solid var(--accent); background: var(--accent); color: var(--paper);
}
.inbox { max-width: 42rem; }
.inbox-list { padding-left: 1.1rem; }
.inbox-row { margin: 0 0 1.4rem; }
.inbox-meta { color: var(--ink-soft); font-size: .85em; margin: 0 0 .3rem; }
.inbox-text { margin: 0 0 .5rem; white-space: pre-wrap; }
.inbox-row form { display: flex; gap: .4rem; flex-wrap: wrap; }
.inbox-row input, .inbox-row button {
  font: inherit; padding: .35rem .6rem; border-radius: 3px;
  border: 1px solid var(--rule); background: var(--paper); color: var(--ink);
}
.inbox-row button { cursor: pointer; }

/* ---------------------------------------------- the place query's AND offer */
/* A coordinate answers with everything at a point, and the way to narrow it --
   "48.8566,2.3522 AND Alexander" -- is a syntax nobody would guess. So the box
   says so, after the coordinates, in the ghost weight of a placeholder.

   It sits OVER the input rather than inside it, because an <input> has no
   inside: it cannot hold a clickable region, and a contenteditable pretending
   to be one gives up the mobile keyboard, the form submit and the browser's own
   memory of what you have searched. The input keeps its value and its
   behaviour; this is a label lying on top of it, positioned after the measured
   width of that value.

   pointer-events is the whole interaction. The hint ignores the pointer, so a
   click anywhere across it lands on the input underneath and the hint gets out
   of the way -- "click in the empty space to the left or to the right and let
   me type". Only the word AND takes the pointer back, and only it inserts
   anything. */
.finder-hint {
  position: absolute; top: 0; bottom: 0;
  display: flex; align-items: center; gap: 5px;
  pointer-events: none;
  font: 400 14px/1.2 "IBM Plex Sans", sans-serif;
  white-space: nowrap;
}
.finder-and {
  pointer-events: auto;
  margin: 0; padding: 1px 5px;
  border: 1px dashed var(--panel-edge); border-radius: 4px;
  background: none; color: var(--ink-soft);
  font: 500 11px/1.4 "IBM Plex Mono", monospace; letter-spacing: 0.04em;
  cursor: pointer;
}
/* It must read as something that will DO something before it is pressed. */
.finder-and:hover, .finder-and:focus-visible {
  border-style: solid; border-color: var(--accent);
  color: var(--accent); background: var(--accent-wash, transparent);
}
.finder-kw { color: var(--ink-faint, var(--ink-soft)); opacity: 0.55; }
/* Measures the input's own text so the hint can sit just after it. Off-screen
   rather than hidden: a display:none element has no width to read. */
.finder-ruler {
  position: absolute; visibility: hidden; white-space: pre;
  left: -9999px; top: 0;
}

/* The last row of a stack that would not fit: the way to all of it. An <a>
   among buttons, because it is the one row that goes somewhere -- middle-click
   and cmd-click have to work on it, and a button cannot offer that. */
.peek-all {
  display: block; text-decoration: none;
  border-top: 1px solid var(--panel-edge); margin-top: 3px; padding-top: 6px;
}
.peek-all .peek-title { color: var(--accent); }
.peek-all:hover .peek-title { text-decoration: underline; }

/* ------------------------------------------- a place's results, by the life */
/* A place is not a ranking: nothing at a point is more relevant than anything
   else there, so the page is a timeline read one life at a time. The heading is
   whose life, and the rows under it are that life's moments here in order. */
.result-group { list-style: none; padding: 22px 0 2px; }
.results-place .result-group:first-child { padding-top: 4px; }
.result-group h2 {
  margin: 0; font-family: "EB Garamond", Georgia, serif;
  font-size: 17px; font-weight: 600; letter-spacing: 0.01em;
}
.result-group h2 a { color: var(--accent); text-decoration: none; }
.result-group h2 a:hover, .result-group h2 a:focus-visible {
  text-decoration: underline;
}
/* Indented under their heading, and the rule between them is lighter than the
   one between two lives -- the group boundary is the louder of the two. */
.results-place .result-in-group { padding-left: 14px; }
.results-place .result-in-group + .result-group { border-top: 1px solid var(--rule); }

/* ------------------------------------------ the standing links, and the foot */
/* Two destinations on the header row, beside whatever else it carries. On the
   map pages this is the ONLY place they can be: the atlas is a full-height
   canvas with a timeline across the bottom, so a footer there is either
   floating over the map or below the fold for ever. */
.bar-nav {
  display: flex; align-items: center; gap: 14px;
  margin-left: 16px; flex: none;
  font: 500 13px/1 "IBM Plex Sans", sans-serif;
}
.bar-nav a {
  color: var(--ink-soft); text-decoration: none;
  padding: 4px 2px; border-radius: 3px;
}
.bar-nav a:hover, .bar-nav a:focus-visible { color: var(--accent); outline: none; }
/* One case, not two. These used to be a pair -- ".finder ~ .bar-nav" for the
   pages that had a search box and ".bar-nav:first-of-type:not(.finder ~
   .bar-nav)" for the pages that did not -- and the second never matched
   anything: .crumbs is a <nav> too and comes first, so .bar-nav is never
   nav:first-of-type. What it was written to do, it never did; what saved it
   was that .crumb-here grows to fill the row anyway. There is no second case
   now: the finder is in every header, it claims the trailing space with
   margin-left:auto, and the nav simply follows it at a fixed gap. */
/* Narrow windows: the search box is the thing worth the width. */
@media (max-width: 720px) { .bar-nav { display: none; } }

/* The foot of a page that has one. */
.page-foot {
  max-width: 44rem; margin: 56px auto 0; padding: 22px 24px 40px;
  border-top: 1px solid var(--rule);
}
.foot-links {
  display: flex; flex-wrap: wrap; gap: 18px;
  /* align-items and a stated line box, because the row now holds a picture as
     well as words. Left to stretch, a 24px egg would have set the row's height
     and shoved every link's baseline down with it -- which is exactly the
     "do not push anything" this is answering. The line box is 24px, the egg is
     24px, and the words centre in it: the row's height is now stated rather
     than whatever the tallest child happens to be. */
  align-items: center; min-height: 24px;
  font: 500 12.5px/24px "IBM Plex Sans", sans-serif;
}
.foot-links a { color: var(--ink-soft); text-decoration: none; }
.foot-links a:hover, .foot-links a:focus-visible { color: var(--accent); }
/* The sister atlas, in the same row at the same gap. A link that leaves the
   site, so it is the one thing in this footer that opens a new tab.
   Sized in the markup as well as here: width and height on the <img> reserve
   the box before the bytes arrive, so the row does not reflow as it loads. */
.foot-egg {
  display: inline-flex; align-items: center;
  line-height: 0;                 /* no descender space under the picture */
  opacity: 0.85; transition: opacity 120ms ease, transform 120ms ease;
}
.foot-egg img { display: block; width: 24px; height: 24px; }
.foot-egg:hover, .foot-egg:focus-visible { opacity: 1; transform: scale(1.06); }
.foot-egg:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .foot-egg { transition: none; } }

/* The sign-off, under the links and quieter than them. */
.foot-made {
  margin: 14px 0 0;
  font: 400 12.5px/1.5 "IBM Plex Sans", sans-serif;
  color: var(--ink-faint, var(--ink-soft));
}

/* ----------------------------------------------------------- a prose page */
/* about, mcp, terms, privacy: the same measure as the search results, so the
   two read as one site rather than two. */
/* --panel, spelled out. It said var(--bg), which is a token this stylesheet
   has never defined: the declaration was invalid at computed-value time and
   threw the background away, and what actually painted these pages was
   body:not(.hydrated):not(.booting), further down and more specific. It looked
   right by accident. */
.prose-page { background: var(--panel); }
.prose {
  max-width: 44rem; margin: 0 auto;
  /* Clear the fixed header, exactly as .search-main does. Written as the same
     expression rather than as a number that happens to match today: #bar is
     position:fixed, so a prose page without this starts its h1 underneath it --
     which is what the first screenshot of /about showed. */
  padding: calc(var(--bar-h) + 26px) 24px 8px;
  font: 400 15.5px/1.68 "IBM Plex Sans", sans-serif; color: var(--ink);
}
/* The page's title, as an <h2>: the one <h1> on any page of this site is the
   last crumb in the header. Selected by class rather than by tag so it keeps
   its own size instead of taking .prose h2's, which is for the headings
   inside the prose. */
.prose .prose-head {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 500; font-size: 2rem; line-height: 1.12;
  margin: 0 0 14px;
}
.prose h2 {
  font-family: "EB Garamond", Georgia, serif;
  font-weight: 600; font-size: 1.28rem; line-height: 1.2;
  margin: 32px 0 8px;
}
.prose p { margin: 0 0 12px; }
.prose .standfirst {
  font-size: 17px; line-height: 1.55; color: var(--ink);
  margin-bottom: 18px;
}
.prose ul { margin: 0 0 12px; padding-left: 1.15rem; }
.prose li { margin: 0 0 7px; }
.prose a { color: var(--accent); }
.prose code {
  font: 400 13px/1.4 "IBM Plex Mono", ui-monospace, monospace;
  background: var(--panel); padding: 1px 5px; border-radius: 3px;
}
