/* ============================================================
   Medelo TouchTable Hub — ported (near-verbatim) from the Claude
   Design source's styles.css. Dark-mode/density/palette tweak
   hooks are left in place as static defaults; the design-time
   Tweaks panel itself isn't part of this build.
   ============================================================ */

:root {
  --purple: oklch(58% 0.105 305);
  --purple-deep: oklch(48% 0.115 305);
  --purple-soft: oklch(72% 0.075 305);
  --purple-wash: oklch(92% 0.025 305);
  --cream: oklch(97% 0.012 85);
  --cream-2: oklch(94% 0.018 80);
  --ink: oklch(22% 0.025 305);
  --ink-soft: oklch(40% 0.025 305);
  --ink-mute: oklch(58% 0.02 305);
  --line: oklch(82% 0.02 305);
  --white: #ffffff;
  --gold: oklch(75% 0.13 75);
  --gold-wash: oklch(95% 0.045 82);

  --surface: var(--cream);
  --surface-2: var(--cream-2);
  --on-surface: var(--ink);
  --on-surface-soft: var(--ink-soft);
}

* { box-sizing: border-box; }

html, body {
  margin: 0; padding: 0; width: 100%; height: 100%;
  overflow: hidden;
  background: #000;
  font-family: "Outfit", "Helvetica Neue", system-ui, sans-serif;
  color: var(--on-surface);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  user-select: none;
}

button { font-family: inherit; cursor: pointer; }

/* ---------- Scaling stage (3840x2160 -> viewport) ---------- */
#stage-wrap { position: fixed; inset: 0; background: #000; overflow: hidden; display: flex; align-items: center; justify-content: center; }
#stage-shrink { position: relative; flex-shrink: 0; }
/* Width and height are set by fitStage() in hub.js — the canvas height is
   fixed at 2160 design px but its width follows the display's aspect ratio,
   so a non-16:9 screen isn't letterboxed. These values are only a fallback
   for the first paint. */
#stage {
  width: 3840px; height: 2160px;
  transform-origin: 0 0;
  background: var(--surface);
  position: absolute; left: 0; top: 0;
  overflow: hidden;
}

/* ============================================================ HEADER */
.tt-header {
  position: absolute; top: 0; left: 0; right: 0; height: 160px;
  padding: 0 80px;
  display: flex; align-items: center; justify-content: space-between;
  z-index: 50;
}
.tt-header-left { display: flex; align-items: center; gap: 32px; }
.tt-header-right { display: flex; align-items: center; gap: 24px; }

.tt-brand { display: flex; align-items: center; gap: 24px; cursor: pointer; padding: 16px 28px; border-radius: 200px; transition: background 0.2s; }
.tt-brand:active { background: var(--purple-wash); }
.tt-brand-mark {
  width: 76px; height: 76px; border-radius: 50%;
  background: var(--purple);
  display: grid; place-items: center;
  color: white; font-weight: 600; font-size: 38px; letter-spacing: -1px;
  position: relative; overflow: hidden;
}
.tt-brand-mark::after { content: ""; position: absolute; inset: 8px; border: 2px solid rgba(255,255,255,0.35); border-radius: 50%; }
.tt-brand-text { display: flex; flex-direction: column; line-height: 1; }
.tt-brand-name { font-size: 36px; font-weight: 600; color: var(--on-surface); letter-spacing: -0.02em; }
.tt-brand-sub { font-size: 20px; color: var(--on-surface-soft); margin-top: 8px; font-family: "Source Serif 4", Georgia, serif; font-style: italic; }

.tt-lang { display: flex; background: var(--surface-2); border: 1.5px solid var(--line); border-radius: 200px; padding: 6px; gap: 4px; }
.tt-lang button { border: none; background: transparent; padding: 16px 28px; font-size: 24px; font-weight: 500; color: var(--on-surface-soft); border-radius: 200px; letter-spacing: 0.05em; transition: all 0.15s; min-width: 80px; }
.tt-lang button.active { background: var(--purple); color: white; }

.tt-clock { font-size: 28px; font-weight: 500; color: var(--on-surface-soft); font-variant-numeric: tabular-nums; padding: 0 16px; }

/* ============================================================ HOME */
.home { position: absolute; inset: 0; padding: 200px 120px 80px; display: grid; grid-template-rows: auto 1fr auto; gap: 56px; }
.home-hero { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 80px; padding-bottom: 8px; }
.home-eyebrow { font-size: 28px; font-weight: 500; color: var(--purple); letter-spacing: 0.2em; text-transform: uppercase; margin-bottom: 28px; }
.home-title { font-size: 130px; line-height: 0.95; letter-spacing: -0.035em; font-weight: 500; color: var(--on-surface); margin: 0; font-family: "Source Serif 4", Georgia, serif; }
.home-title em { color: var(--purple); font-style: italic; font-weight: 400; }
.home-lede { font-size: 32px; line-height: 1.4; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; max-width: 1100px; margin-top: 36px; }
.home-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; color: var(--on-surface-soft); font-size: 24px; padding-bottom: 24px; }
.home-meta-row { display: flex; align-items: center; gap: 16px; }
.home-meta-dot { width: 12px; height: 12px; border-radius: 50%; background: oklch(70% 0.15 145); box-shadow: 0 0 0 4px oklch(70% 0.15 145 / 0.25); }

/* Eight apps in nine cells: the featured Tijdlijn spans two, so the 3x3 grid
   fills exactly. This is a hard cap — a ninth app needs ten cells and its tile
   drops off the bottom of the screen, which is why Gebouwen was folded into
   the Kaart. Anything new here has to displace something. */
.home-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 36px; }

.tile {
  position: relative; background: var(--white); border: 1.5px solid var(--line); border-radius: 36px;
  padding: 56px 56px 48px; display: flex; flex-direction: column; justify-content: space-between;
  min-height: 380px; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer;
}
.tile:active { transform: scale(0.985); }
.tile.tile-featured { background: var(--purple); color: white; border-color: var(--purple-deep); grid-column: span 2; grid-row: span 1; }
.tile.tile-featured .tile-num, .tile.tile-featured .tile-sub, .tile.tile-featured .tile-title { color: white; }
.tile.tile-featured .tile-sub { color: rgba(255,255,255,0.78); }
.tile.tile-featured .tile-num { color: rgba(255,255,255,0.55); }

.tile-head { display: flex; justify-content: space-between; align-items: flex-start; }
.tile-num { font-size: 26px; font-weight: 500; color: var(--on-surface-soft); letter-spacing: 0.1em; font-variant-numeric: tabular-nums; }
.tile-icon { width: 96px; height: 96px; border-radius: 24px; background: var(--purple-wash); display: grid; place-items: center; color: var(--purple); }
.tile.tile-featured .tile-icon { background: rgba(255,255,255,0.15); color: white; }
.tile-icon svg { width: 56px; height: 56px; }

.tile-title { font-size: 56px; font-weight: 500; letter-spacing: -0.025em; line-height: 1; margin: 0; color: var(--on-surface); font-family: "Source Serif 4", Georgia, serif; }
.tile-sub { font-size: 24px; line-height: 1.35; color: var(--on-surface-soft); margin-top: 16px; max-width: 90%; }
.tile-cta { display: inline-flex; align-items: center; gap: 12px; font-size: 24px; font-weight: 500; margin-top: 28px; color: var(--purple); }
.tile.tile-featured .tile-cta { color: white; }
.tile-cta svg { width: 28px; height: 28px; }

.home-footer { display: flex; justify-content: space-between; align-items: center; color: var(--on-surface-soft); font-size: 22px; padding-top: 16px; border-top: 1.5px solid var(--line); }
/* The maker credit. The mark is loaded from Content/homescreen (see that
   folder's LEES-MIJ.txt), so it can be replaced without touching the app; if
   it fails to load hub.js drops the <img> and the line stands on its own. */
.home-credit { display: flex; align-items: center; gap: 16px; }
/* The Lions Alliance mark. Gold artwork on the brand's own deep navy
   (#16232a, sampled from the supplied logo): the gold averages rgb(233,209,161)
   and would all but vanish laid straight onto the cream footer, so the mark
   keeps the dark ground it was drawn for and reads as a small badge. The file
   is the transparent cut, trimmed and scaled; the tile is drawn here so the
   padding and the corner stay under the stylesheet's control. */
.home-credit-logo { height: 64px; width: auto; flex: none; object-fit: contain; display: block;
  background: #16232a; padding: 8px 10px; border-radius: 10px; }
[data-mode="dark"] .home-credit-logo { box-shadow: 0 0 0 1.5px var(--line); }

/* ============================================================ Screen plumbing (not in the design source — routing glue) */
.screen { position: absolute; inset: 0; background: var(--surface); }
.screen.hidden { display: none; }

/* An app that failed to build says so, rather than leaving the previous screen
   up pretending to be this one. See renderOnce() in hub.js — the reason is in
   the console. */
.app-failed {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: "Source Serif 4", Georgia, serif; font-size: 40px;
  color: var(--on-surface-soft); font-style: italic; text-align: center; padding: 0 200px;
}

/* ============================================================ TIMELINE — a side-scrolling white line with era thumbnails
   spaced along it, matching the real OmniTapps MediaTimeline app.
   The cream/purple/cream vertical ratio (~29.6% / 40.8% / 29.6%)
   is measured from the real TRU-tijdlijn-digi reference graphic,
   so the "zoom level" here matches the physical printed timeline. */
.timeline-view { position: absolute; inset: 0; padding-top: 160px; display: flex; flex-direction: column; background: var(--surface); }
.tl-list { display: flex; flex-direction: column; height: 100%; }
.tl-top { flex: 296 0 0; display: flex; flex-direction: column; justify-content: center; }
.tl-intro { padding: 0 120px; }
.tl-intro .tl-eyebrow { font-size: 24px; font-weight: 500; color: var(--purple); letter-spacing: 0.18em; text-transform: uppercase; margin-bottom: 20px; }
.tl-intro h1 { font-family: "Source Serif 4", Georgia, serif; font-size: 84px; font-weight: 500; letter-spacing: -0.03em; line-height: 1; margin: 0; color: var(--on-surface); }
.tl-intro h1 em { color: var(--purple); font-style: italic; }

/* .tl-rail is just a layout slot for the band's height ratio — it does NOT
   clip. The purple color lives on .tl-rail-bg (a plain background layer,
   sized to the slot) so .timeline-strip is free to be taller than the band
   and let its thumbnails visually escape above/below it. */
.tl-rail { position: relative; flex: 408 0 0; }
.tl-rail-bg { position: absolute; inset: 0; background: var(--purple); }
.tl-bottom { flex: 296 0 0; }
.tl-rail-line { position: absolute; left: 60px; right: 60px; top: 50%; height: 6px; background: white; border-radius: 3px; box-shadow: 0 2px 6px rgba(0,0,0,0.2); z-index: 1; }

/* height set inline via JS (must equal the thumbnail height) — overflow-x:auto
   forces overflow-y to clip too (CSS spec), so the box has to already be
   tall enough that clipping never actually triggers. */
.timeline-strip { position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%); display: flex; align-items: center; gap: 0; overflow-x: auto; overflow-y: hidden; padding: 0 120px; scrollbar-width: none; touch-action: pan-x; z-index: 2; }
.timeline-strip::-webkit-scrollbar { display: none; }
.timeline-node { flex: 0 0 auto; display: flex; align-items: center; height: 100%; cursor: pointer; }
.timeline-node-thumb {
  display: block; width: auto; height: 100%; /* fills the (already correctly sized) strip */
  filter: drop-shadow(0 10px 24px rgba(0,0,0,0.35));
  transition: transform 0.15s;
}
.timeline-node.active .timeline-node-thumb { filter: drop-shadow(0 0 0 4px var(--gold)) drop-shadow(0 10px 24px rgba(0,0,0,0.35)); }
.timeline-node:active .timeline-node-thumb { transform: scale(0.96); }

/* ---------- "Dive in" scattered detail view ---------- */
.leaves-view { position: absolute; inset: 0; padding-top: 160px; background: var(--surface); overflow: hidden; }
.leaves-back {
  position: absolute; top: 50%; left: 60px; transform: translateY(-50%); z-index: 20;
  width: 96px; height: 96px; display: grid; place-items: center;
  background: var(--white); border: 1.5px solid var(--line); border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.15); color: var(--on-surface);
}
.leaves-back svg { width: 40px; height: 40px; }
.leaves-canvas { position: absolute; inset: 0; }

.leaf-card {
  position: absolute; top: 0; left: 0;
  touch-action: none;
  cursor: grab;
}
.leaf-card.active-gesture { cursor: grabbing; z-index: 10; }
.leaf-card-media {
  width: 100%; height: 100%; background: var(--cream-2); background-size: cover; background-position: center;
  overflow: hidden; position: relative; box-shadow: 0 16px 40px rgba(0,0,0,0.25);
}
.leaf-card.active-gesture .leaf-card-media { box-shadow: 0 24px 56px rgba(0,0,0,0.35); }
.leaf-card-media video { width: 100%; height: 100%; object-fit: contain; background: #000; }

/* ---------- Caption and credit on a scattered picture ----------
   Both are opt-in (`captions` in scatterLeaves) and both come from the source
   document: the caption is what the article printed under that photograph, the
   credit is which article it was. Only Kaart van Meijel turns them on.

   The caption hangs BELOW the card instead of sitting inside it, so the card's
   box stays exactly the picture — leafSize derives it from the image's own
   proportions, and the corner rotate handles stay on the photo's corners. It
   still rotates and scales with the card, the way writing under a photograph
   belongs to the photograph. */
.leaf-card-caption {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 14px 18px 34px;
  background: linear-gradient(to top, oklch(97% 0.012 85 / 0.94) 0%,
                                      oklch(97% 0.012 85 / 0.90) 62%,
                                      oklch(97% 0.012 85 / 0) 100%);
  font-family: "Source Serif 4", Georgia, serif;
  font-size: 21px; line-height: 1.3; color: var(--purple-deep);
  /* Three lines, then it stops. A few of these captions run to three hundred
     characters and would bury the photograph they describe; the card pinches
     open like every other card on the table when someone wants the rest. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  overflow: hidden;
}
/* Where the picture was found. Bottom-right as asked, over the caption's own
   band, so the caption reserves room for it rather than running underneath. */
.leaf-card-credit {
  position: absolute; right: 0; bottom: 0; z-index: 2;
  max-width: 82%; padding: 5px 12px;
  background: oklch(22% 0.025 305 / 0.60); color: oklch(98% 0 0);
  font-size: 15px; letter-spacing: 0.01em; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  border-top-left-radius: 10px;
}

/* ---------- The source article under the summary ----------
   Verbatim Medelo text, so it is set apart from the museum's own four-language
   summary above it rather than blending in. See renderArticle in collection.js. */
.coll-detail-article {
  margin-top: 44px; padding-top: 32px; border-top: 1.5px solid var(--line);
  /* Same size as the summary above it. This is the museum's main text
     now, not a footnote — setting it smaller because there is more of it
     is exactly the mistake the PDF pages were making. */
  font-size: 28px; line-height: 1.5; color: var(--on-surface);
}
.coll-article-source { font-size: 22px; color: var(--purple); margin: -6px 0 6px; }
.coll-article-note { font-size: 20px; color: var(--ink-mute); font-style: italic; margin-bottom: 14px; }
.coll-detail-article p { margin: 0 0 26px; }
.coll-detail-article p:last-child { margin-bottom: 0; }

/* Locatie / Kunstenaar / Jaar / Eigendom, as the chapel articles print them. */
.coll-detail-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 8px 24px;
  margin: 24px 0 0; padding: 22px 28px;
  background: var(--purple-wash); border-radius: 20px;
}
.coll-detail-facts dt { font-size: 20px; color: var(--purple-deep); font-weight: 500; }
.coll-detail-facts dd { font-size: 20px; margin: 0; color: var(--on-surface-soft); }

/* Invisible hit-zones at the four corners for the rotate gesture — no
   visible marker, per the "remove the purple corner markers" request. */
.corner-handle { position: absolute; width: 56px; height: 56px; margin: -28px; touch-action: none; background: transparent; }
.corner-handle.tl { top: 0; left: 0; cursor: nwse-resize; }
.corner-handle.tr { top: 0; right: 0; left: auto; margin-right: -28px; margin-left: 0; cursor: nesw-resize; }
.corner-handle.bl { bottom: 0; left: 0; margin-bottom: -28px; margin-top: 0; cursor: nesw-resize; }
.corner-handle.br { bottom: 0; right: 0; left: auto; margin: -28px; cursor: nwse-resize; }

/* ---------- Video controls on a leaf card (videocontrols.js) ----------
   Reels/TikTok vocabulary — tap the picture to play or pause, scrub along the
   bottom edge, a speaker for the sound.

   The bar used to be dimmed rather than hidden, on the reasoning that a visitor
   with thirty seconds at a museum table does not know controls are a tap away
   the way a phone's owner does. What answers that now is `.is-paused`: a film
   that is not running keeps its bar up permanently and wears a play button the
   size of a fist, so the affordance is never something you have to know about.
   Only once it is actually playing do the controls clear off the picture, and
   the next tap brings them back rather than stopping the film.

   The whole overlay is pointer-transparent except the bar and that button, so a
   tap on the footage still reaches the card. */
.vc { position: absolute; inset: 0; pointer-events: none; }

.vc-flash {
  position: absolute; top: 50%; left: 50%;
  width: 180px; height: 180px; margin: -90px 0 0 -90px;
  display: grid; place-items: center;
  border-radius: 50%; background: rgba(0,0,0,0.55); color: #fff;
  opacity: 0; transform: scale(0.7);
}
.vc-flash svg { width: 88px; height: 88px; }
.vc-flash.is-on { animation: vc-flash 700ms ease-out forwards; }
@keyframes vc-flash {
  0%   { opacity: 0;    transform: scale(0.7); }
  18%  { opacity: 0.95; transform: scale(1); }
  100% { opacity: 0;    transform: scale(1.25); }
}

.vc-bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: 18px;
  padding: 18px 22px 16px;
  pointer-events: auto;
  /* The four invisible .corner-handle rotate zones are later siblings of
     .leaf-card-media, so they paint over it — including over the ends of this
     bar, where they would quietly eat the edge of the play and sound buttons.
     .leaf-card-media is position:relative with z-index:auto and so opens no
     stacking context, which means this one line is enough to put the bar
     above them. On a film card the controls outrank rotating from the bottom
     corners; the top two corners and the outer half of the bottom two are
     still there to turn it by. */
  z-index: 2;
  touch-action: none;
  color: #fff;
  background: linear-gradient(to top, rgba(0,0,0,0.72), rgba(0,0,0,0.36) 55%, rgba(0,0,0,0));
  opacity: 0;
  visibility: hidden;
  /* Delay `visibility` by the length of the fade so the bar is still hittable
     on its way out, then genuinely gone — an invisible bar that still eats
     taps along the bottom edge of a film is worse than one you can see. */
  transition: opacity 220ms ease, visibility 0s linear 220ms;
}
.vc.is-revealed .vc-bar,
.vc.is-paused .vc-bar {
  opacity: 1;
  visibility: visible;
  transition: opacity 220ms ease, visibility 0s linear 0s;
}

/* The one control a stopped film really needs, at a size you can hit without
   looking for it. Shown only while paused — which, now that nothing autoplays,
   is how every card on a year canvas starts out. */
.vc-bigplay {
  position: absolute; top: 50%; left: 50%;
  width: 180px; height: 180px; margin: -90px 0 0 -90px;
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgba(0,0,0,0.55); color: #fff;
  pointer-events: auto; cursor: pointer; z-index: 3;
  opacity: 0; visibility: hidden;
  transition: opacity 220ms ease, visibility 0s linear 220ms, transform 160ms ease;
}
.vc.is-paused .vc-bigplay {
  opacity: 1; visibility: visible;
  transition: opacity 220ms ease, visibility 0s linear 0s, transform 160ms ease;
}
.vc-bigplay svg { width: 88px; height: 88px; }
.vc-bigplay:active { transform: scale(0.94); }
/* Footage the table cannot decode gets the message, not a button that lies. */
.vc.is-broken .vc-bigplay { display: none; }

.vc-btn {
  flex: none; width: 64px; height: 64px;
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgba(255,255,255,0.14); color: #fff;
  cursor: pointer;
}
.vc-btn svg { width: 32px; height: 32px; }
.vc-btn:active { background: rgba(255,255,255,0.28); }

.vc-time {
  flex: none; min-width: 84px;
  font-size: 24px; font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
  color: rgba(255,255,255,0.88);
}
.vc-duration { text-align: right; }

/* The hit area is the full 64px band; only the rail inside it is drawn. */
.vc-track { position: relative; flex: 1 1 auto; height: 64px; touch-action: none; cursor: pointer; }

/* Zero-size probes at the rail's two ends. videocontrols.js reads their rects
   to map a finger onto the rail — the card is rotated, so a bounding box on
   the rail itself would be a box drawn around a tilted line, not the line. */
.vc-mark { position: absolute; top: 50%; width: 0; height: 0; }
.vc-mark-a { left: 0; }
.vc-mark-b { left: 100%; }

.vc-rail {
  position: absolute; left: 0; right: 0; top: 50%;
  height: 6px; transform: translateY(-50%);
  border-radius: 999px; background: rgba(255,255,255,0.3);
  overflow: hidden;
  transition: height 160ms ease;
}
.vc.is-revealed .vc-rail { height: 10px; }
.vc.is-scrubbing .vc-rail { height: 14px; }
.vc-fill { height: 100%; width: 0; background: var(--gold); border-radius: 999px; }

.vc-knob {
  position: absolute; top: 50%; left: 0;
  width: 26px; height: 26px; margin: 0;
  transform: translate(-50%, -50%) scale(0);
  border-radius: 50%; background: var(--gold);
  box-shadow: 0 2px 10px rgba(0,0,0,0.45);
  transition: transform 160ms ease;
}
.vc.is-revealed .vc-knob { transform: translate(-50%, -50%) scale(1); }
.vc.is-scrubbing .vc-knob { transform: translate(-50%, -50%) scale(1.3); }

.vc-error {
  position: absolute; left: 0; right: 0; top: 50%; transform: translateY(-50%);
  padding: 0 32px; text-align: center;
  font-size: 30px; line-height: 1.3; color: #fff;
  text-shadow: 0 2px 12px rgba(0,0,0,0.8);
}
.vc.is-broken .vc-track, .vc.is-broken .vc-play { opacity: 0.35; }

/* ============================================================ STUB APP SCREENS (ported) */
.stub-view { position: absolute; inset: 0; padding: 200px 120px 80px; display: flex; flex-direction: column; background: var(--surface); overflow-y: auto; }
.stub-head { display: grid; grid-template-columns: auto 1fr; align-items: end; gap: 64px; margin-bottom: 64px; }
.stub-head h1 { font-family: "Source Serif 4", Georgia, serif; font-size: 120px; margin: 0; font-weight: 500; letter-spacing: -0.035em; line-height: 0.95; color: var(--on-surface); }
.stub-head h1 em { color: var(--purple); font-style: italic; }
.stub-lede { font-size: 28px; line-height: 1.4; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; max-width: 1200px; align-self: end; padding-bottom: 24px; }

/* Photo archive */

/* ============================================================ KAART VAN MEIJEL
   A real map now: Content/Kaart/kaartlagen holds forty-seven editions of
   Kadaster's maps of this square of the Peel, 1815-2025, and views/kaartlagen.js
   draws them as a tile pyramid with a year slider over the top. Pins are placed
   from Rijksdriehoek coordinates in JS (left/top in px), NOT from percentages —
   every layer shares one RD frame, so one coordinate is right on all of them.
   The .pm-canvas block below is the placeholder that came before, kept as the
   fallback for a build whose kaartlagen folder was never harvested.
   See views/placemap.js. */

.pm-view { position: absolute; inset: 0; display: grid; grid-template-columns: minmax(0, 1fr) 820px; gap: 48px; }
.pm-stage { position: relative; min-width: 0; }

/* ---------- Placeholder, for a build with no harvested map ---------- */
/* A surveyor's grid, not a borrowed photograph: an unrelated archive image
   behind real pins would read as the village seen from above. */
.pm-canvas {
  position: absolute; inset: 0; border-radius: 32px; overflow: hidden;
  border: 1.5px solid var(--line);
  background-color: oklch(95.5% 0.014 85);
  background-image:
    repeating-linear-gradient(0deg,  oklch(58% 0.105 305 / 0.07) 0 1px, transparent 1px 90px),
    repeating-linear-gradient(90deg, oklch(58% 0.105 305 / 0.07) 0 1px, transparent 1px 90px);
}
.pm-note {
  position: absolute; left: 28px; right: 28px; bottom: 24px; z-index: 2;
  background: oklch(100% 0 0 / 0.86); border-radius: 200px; padding: 14px 28px;
  font-size: 20px; font-style: italic; color: var(--on-surface-soft);
  font-family: "Source Serif 4", Georgia, serif; text-align: center;
}

/* ---------- The map ---------- */
.km-map {
  position: absolute; inset: 0; overflow: hidden; border-radius: 32px;
  border: 1.5px solid var(--line); background: var(--cream-2);
  touch-action: none;
}
.km-plane { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
/* Two layers, so a change of year can dissolve rather than blink. */
.km-layer { position: absolute; left: 0; top: 0; transition: opacity 260ms ease; }
.km-map.is-scrubbing .km-layer { transition: none; }
/* z0 sits under everything: one 640px image of the whole frame, so a tile that
   has not arrived yet shows a blurry version of the right place, never a white hole. */
.km-base { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
.km-tile { position: absolute; display: block; }
/* Nothing in the plane may take a touch — the surface above it owns every gesture. */
.km-plane, .km-base, .km-tile { pointer-events: none; user-select: none; }

.km-surface { position: absolute; inset: 0; touch-action: none; }

.km-pins { position: absolute; inset: 0; pointer-events: none; }
.km-pins > * { pointer-events: auto; }

/* ---------- Pins ---------- */
/* .km-pin is placed in px by JS from an RD coordinate; .pm-pin is the older
   percentage-placed pin the placeholder canvas still uses. Same shape. */
.km-pin, .pm-pin {
  position: absolute; width: 60px; height: 60px; padding: 0;
  border-radius: 50% 50% 50% 0; background: var(--purple); border: 4px solid white;
  transform: rotate(-45deg) translate(-50%, -100%); transform-origin: center;
  box-shadow: 0 6px 16px rgba(0,0,0,0.22); display: grid; place-items: center;
  cursor: pointer; transition: background 0.15s, box-shadow 0.15s, opacity 0.25s;
}
.pm-pin-num { transform: rotate(45deg); color: white; font-weight: 600; font-size: 22px; }
.km-pin.gone, .pm-pin.gone { background: oklch(52% 0.16 25); }
/* The street is known, the spot on it is not — an open ring says so. */
.km-pin.approx { background: oklch(100% 0 0 / 0.9); border-color: var(--purple); }
.km-pin.approx .pm-pin-num { color: var(--purple-deep); }
/* Out of its time for the year on the slider: still there, still tappable,
   visibly not part of the map you are looking at. */
.km-pin.is-future, .km-pin.is-past { opacity: 0.26; }
.km-pin.is-active, .km-pin:active, .pm-pin.is-active, .pm-pin:active {
  background: var(--gold); opacity: 1;
  box-shadow: 0 0 0 12px oklch(75% 0.13 75 / 0.32), 0 6px 16px rgba(0,0,0,0.22);
}
.km-pin.is-active .pm-pin-num, .km-pin:active .pm-pin-num,
.pm-pin.is-active .pm-pin-num, .pm-pin:active .pm-pin-num { color: var(--ink); }

/* ---------- Map chrome ---------- */
/* The credit is the licence condition, not decoration: the historical maps are
   free to use *under* it. Do not remove it. */
.km-credit {
  position: absolute; left: 28px; top: 24px; max-width: 1000px;
  background: oklch(100% 0 0 / 0.82); border-radius: 200px; padding: 10px 24px;
  font-size: 18px; color: var(--on-surface-soft);
}
.km-scalebar { position: absolute; left: 28px; bottom: 250px; display: flex; align-items: center; gap: 14px; }
.km-scalebar-bar { height: 10px; border: 2px solid var(--ink); border-top: none; background: oklch(100% 0 0 / 0.55); }
.km-scalebar span { font-size: 19px; font-weight: 500; color: var(--ink); text-shadow: 0 0 6px white, 0 0 6px white; }

.km-controls { position: absolute; right: 28px; top: 24px; display: flex; flex-direction: column; gap: 16px; }
.km-btn {
  width: 92px; height: 92px; border-radius: 50%; border: 1.5px solid var(--line);
  background: oklch(100% 0 0 / 0.92); color: var(--on-surface);
  font-size: 44px; line-height: 1; display: grid; place-items: center;
  box-shadow: 0 4px 14px rgba(0,0,0,0.12);
}
.km-btn:active { background: var(--purple); border-color: var(--purple); color: #fff; }
.km-btn svg { width: 40px; height: 40px; }

/* ---------- The time slider ---------- */
.km-time {
  position: absolute; left: 28px; right: 28px; bottom: 26px; z-index: 3;
  display: flex; align-items: center; gap: 32px;
  background: oklch(100% 0 0 / 0.9); border: 1.5px solid var(--line);
  border-radius: 36px; padding: 24px 36px 18px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.14);
}
.km-play {
  flex: none; display: flex; align-items: center; gap: 14px; height: 86px; padding: 0 32px;
  border-radius: 200px; border: 1.5px solid var(--line); background: var(--white);
  color: var(--on-surface); font-size: 24px; font-weight: 500;
}
.km-play svg { width: 32px; height: 32px; }
.km-play.is-playing { background: var(--purple); border-color: var(--purple); color: #fff; }
.km-time-body { flex: 1; min-width: 0; }
.km-readout { display: flex; align-items: baseline; gap: 20px; margin-bottom: 14px; }
.km-readout strong { font-size: 58px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1; color: var(--purple-deep); }
.km-readout span { font-size: 22px; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; }
.km-track { position: relative; height: 20px; border-radius: 200px; background: var(--surface-2); border: 1.5px solid var(--line); touch-action: none; }
.km-track-fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 200px; background: var(--purple); }
.km-track-ticks { position: absolute; inset: 0; }
/* One tick per edition: the marks are the moments the map of Meijel was
   actually redrawn, which is worth seeing before you even drag. */
.km-tick { position: absolute; top: -7px; width: 2px; height: 30px; background: oklch(48% 0.115 305 / 0.4); }
.km-knob {
  position: absolute; top: 50%; width: 52px; height: 52px; border-radius: 50%;
  background: var(--white); border: 4px solid var(--purple); box-shadow: 0 4px 12px rgba(0,0,0,0.2);
  transform: translate(-50%, -50%); pointer-events: none; transition: transform 0.12s;
}
.km-map.is-scrubbing .km-knob { transform: translate(-50%, -50%) scale(1.25); }
.km-track-labels { position: relative; height: 34px; margin-top: 10px; }
.km-track-labels i {
  position: absolute; transform: translateX(-50%); font-style: normal;
  font-size: 19px; color: var(--ink-mute); font-variant-numeric: tabular-nums;
}

/* ---------- The list beside it ---------- */
.pm-list { display: flex; flex-direction: column; gap: 18px; overflow-y: auto; align-content: start; }
.pm-row {
  flex: none; text-align: left; background: var(--white); border: 1.5px solid var(--line);
  border-radius: 24px; padding: 26px 32px; display: grid; grid-template-columns: auto 1fr;
  gap: 26px; align-items: center; cursor: pointer; transition: border-color 0.15s, background 0.15s;
}
.pm-row.is-active, .pm-row:active { border-color: var(--purple); background: var(--purple-wash); }
.pm-row-num {
  width: 60px; height: 60px; border-radius: 50%; background: var(--purple-wash);
  color: var(--purple-deep); display: grid; place-items: center; font-weight: 600; font-size: 24px;
}
.pm-row.is-active .pm-row-num { background: var(--purple); color: white; }
.pm-row-num.unplaced { background: var(--surface-2); color: var(--ink-mute); }
.pm-row-head { display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; }
.pm-row-head h3 { font-family: "Source Serif 4", Georgia, serif; font-size: 32px; margin: 0; font-weight: 500; color: var(--on-surface); }
.pm-badges { display: flex; gap: 10px; flex-wrap: wrap; }
.pm-badge { display: inline-flex; align-items: center; gap: 8px; padding: 7px 18px; border-radius: 200px; font-size: 17px; font-weight: 500; letter-spacing: 0.04em; }
.pm-badge svg { width: 19px; height: 19px; }
.pm-badge.gone { background: oklch(60% 0.18 25 / 0.14); color: oklch(45% 0.14 25); }
.pm-badge.model { background: var(--purple-wash); color: var(--purple-deep); }
.pm-row-period { font-size: 21px; color: var(--purple); font-variant-numeric: tabular-nums; margin-top: 6px; }
.pm-row-sub { font-size: 21px; color: var(--on-surface-soft); margin-top: 6px; font-family: "Source Serif 4", Georgia, serif; line-height: 1.3; }
.pm-row-approx { font-size: 18px; color: var(--ink-mute); margin-top: 8px; font-style: italic; }


/* ================================================ MARKDOWN-AUTHORED ARTICLES
   A place on the Kaart (and a person in Personen) keeps its text as Markdown in
   a folder of its own; ContentScanner renders it and the web layer drops it in
   as `bodyHtml`. A picture alone in a paragraph becomes a <figure> with its alt
   text as the caption, so a caption sits with the sentence it belongs to —
   which is the whole reason the captions came off the scattered cards.
   See ContentScanner.ScanEntityFolders. */

/* Every picture in an article is drawn at this share of the size the source
   document gave it. One number, deliberately: the pictures are all on the
   free-move canvas at full size for anyone who wants to look properly, so in
   the text they are illustrations rather than plates. Raise it towards 1 to
   have the article match the PDF's proportions exactly. */
.coll-detail-body { --fig-scale: 0.6; }

.coll-detail-body figure { margin: 30px 0; width: calc(100% * var(--fig-scale)); }
.coll-detail-body figure img {
  display: block; width: 100%; height: auto;
  border-radius: 20px; border: 1.5px solid var(--line); margin: 0;
}
.coll-detail-body figcaption {
  margin-top: 12px;
  font-family: "Source Serif 4", Georgia, serif;
  font-size: 21px; line-height: 1.35; color: var(--purple-deep);
}
/* ---- Placement, as the source document had it ----
   `![Bijschrift](beeld-02.jpg){.links .w45}` in the Markdown puts the picture
   in the left of the column at 45% of the measure, with the text running past
   it — which is how these articles were set, and why reading them as a plain
   column of pictures and paragraphs lost the thread. No class means full width.
   The classes land on the <figure>, so the caption travels with its picture. */
.coll-detail-body figure.links,
.coll-detail-body figure.rechts { width: calc(46% * var(--fig-scale)); }
.coll-detail-body figure.links  { float: left;  margin: 8px 40px 22px 0; }
.coll-detail-body figure.rechts { float: right; margin: 8px 0 22px 40px; }

.coll-detail-body figure.w25 { width: calc(25% * var(--fig-scale)); }
.coll-detail-body figure.w30 { width: calc(30% * var(--fig-scale)); }
.coll-detail-body figure.w35 { width: calc(35% * var(--fig-scale)); }
.coll-detail-body figure.w40 { width: calc(40% * var(--fig-scale)); }
.coll-detail-body figure.w45 { width: calc(45% * var(--fig-scale)); }
.coll-detail-body figure.w50 { width: calc(50% * var(--fig-scale)); }
.coll-detail-body figure.w55 { width: calc(55% * var(--fig-scale)); }
.coll-detail-body figure.w60 { width: calc(60% * var(--fig-scale)); }
.coll-detail-body figure.w65 { width: calc(65% * var(--fig-scale)); }
.coll-detail-body figure.w70 { width: calc(70% * var(--fig-scale)); }
.coll-detail-body figure.w75 { width: calc(75% * var(--fig-scale)); }
.coll-detail-body figure.w80 { width: calc(80% * var(--fig-scale)); }
.coll-detail-body figure.w85 { width: calc(85% * var(--fig-scale)); }
.coll-detail-body figure.w90 { width: calc(90% * var(--fig-scale)); }
.coll-detail-body figure.w95 { width: calc(95% * var(--fig-scale)); }

/* A floated picture and a floated aside must not end up shoulder to shoulder in
   a column this narrow — one of them gets pushed below the other instead. */
.coll-detail-body figure.links  { clear: left; }
.coll-detail-body figure.rechts { clear: right; }

/* A picture's own note, folded into its <figure> by ContentScanner and printed
   under the caption. This is where "ca 1840:" and its three lines belong: they
   describe the map above them. Set as caption text, not as a box — a slab of
   purple beside every picture was heavy, and floating it opposite the picture
   squeezed the body text into a ribbon between the two. */
.coll-detail-body .fig-note { margin-top: 10px; }
.coll-detail-body .fig-note blockquote {
  float: none; width: auto; margin: 0; padding: 0;
  background: none; border: none; border-radius: 0;
  font-family: "Source Serif 4", Georgia, serif;
  font-size: 20px; line-height: 1.35; color: var(--on-surface-soft);
}
.coll-detail-body .fig-note ul { margin: 6px 0 0; padding-left: 24px; }
.coll-detail-body .fig-note li { margin-bottom: 5px; }
.coll-detail-body .fig-note strong { color: var(--purple-deep); font-weight: 600; }

/* An aside that belongs to no picture — the author talking beside the text
   rather than about a photograph. A rule and a change of voice, not a box. */
.coll-detail-body blockquote {
  float: right; clear: right; width: 38%;
  margin: 6px 0 24px 40px; padding: 4px 0 4px 24px;
  border: none; border-left: 3px solid var(--purple-soft);
  background: none; border-radius: 0;
  font-family: "Source Serif 4", Georgia, serif;
  font-size: 21px; line-height: 1.45; color: var(--on-surface-soft);
}
.coll-detail-body blockquote p { margin: 0 0 10px; }
.coll-detail-body blockquote p:last-child { margin-bottom: 0; }
.coll-detail-body blockquote ul { margin: 0; padding-left: 26px; }
.coll-detail-body blockquote strong { color: var(--purple-deep); }
/* A heading must not tuck itself in beside a floated aside. */
.coll-detail-body h1, .coll-detail-body h2,
.coll-detail-body h3, .coll-detail-body h4 { clear: right; }
.coll-detail-body::after { content: ""; display: block; clear: both; }

/* ============================================================ COLLECTION APPS
   Shared chrome for Verhalen / Personen / Gebouwen / Tradities (collection.js).
   One grid, one detail overlay, one chip style — each app's own character comes
   from its signature view, not from a different card. */

.coll-view { position: absolute; inset: 0; padding: 200px 120px 80px; display: flex; flex-direction: column; background: var(--surface); overflow: hidden; }
.coll-view .stub-head { flex: none; }


.coll-body { flex: 1; min-height: 0; position: relative; }
.coll-grid-wrap { position: absolute; inset: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 40px; }

.coll-search { display: flex; align-items: center; gap: 24px; background: var(--white); border: 1.5px solid var(--line); border-radius: 200px; padding: 8px 40px; flex: none; max-width: 1000px; }
.coll-search svg { width: 40px; height: 40px; color: var(--on-surface-soft); flex: none; }
.coll-search input { border: none; outline: none; background: transparent; font-family: inherit; font-size: 32px; color: var(--on-surface); padding: 24px 0; width: 100%; }

.coll-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 40px; align-content: start; padding-bottom: 40px; }
.coll-empty { grid-column: 1 / -1; text-align: center; padding: 120px 0; font-size: 36px; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; font-style: italic; }

.coll-card { background: var(--white); border: 1.5px solid var(--line); border-radius: 28px; overflow: hidden; display: flex; flex-direction: column; height: 640px; transition: transform 0.15s, border-color 0.15s; }
.coll-card:active { transform: scale(0.985); border-color: var(--purple); }
.coll-card-image { flex: 1; min-height: 0; background: var(--cream-2); background-size: cover; background-position: center; }
.coll-card-meta { padding: 36px 40px; }
.coll-card-period { font-size: 22px; color: var(--purple); font-weight: 500; font-variant-numeric: tabular-nums; }
.coll-card-name { font-family: "Source Serif 4", Georgia, serif; font-size: 38px; margin: 6px 0 0; font-weight: 500; color: var(--on-surface); line-height: 1.05; }
.coll-card-sub { font-size: 21px; color: var(--on-surface-soft); margin-top: 14px; line-height: 1.4; font-family: "Source Serif 4", Georgia, serif; }

/* Wider cards (films, buildings) read better three-up; portraits four-up. */
.coll-grid:has(.coll-card-wide) { grid-template-columns: repeat(3, 1fr); }
.coll-card-portrait .coll-card-image { flex: none; height: 420px; }

/* Verhalen: the provenance of a film is never hidden behind a tap. */
.story-kind { display: inline-block; margin-top: 20px; padding: 10px 24px; border-radius: 200px; font-size: 19px; font-weight: 500; letter-spacing: 0.04em; }
.story-kind.archival { background: var(--purple-wash); color: var(--purple-deep); }
.story-kind.ai { background: oklch(75% 0.13 75 / 0.22); color: oklch(45% 0.09 75); }
/* The third kind: a written article from the Medelo archive, read rather than
   watched. Green, so it is not mistaken at a glance for either film badge. */
.story-kind.document { background: oklch(72% 0.10 150 / 0.22); color: oklch(40% 0.08 150); }

/* ---------- Detail overlay ---------- */
.coll-detail { position: absolute; inset: 0; background: var(--surface); z-index: 20; display: flex; }
/* Same position and shape as the timeline's .leaves-back — "back" always lives
   on the left edge, vertically centred, wherever you are on the table. */
.coll-detail-close {
  position: absolute; top: 50%; left: 60px; transform: translateY(-50%); z-index: 2;
  width: 96px; height: 96px; border-radius: 12px; border: 1.5px solid var(--line);
  background: var(--white); color: var(--on-surface); display: grid; place-items: center;
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.coll-detail-close svg { width: 40px; height: 40px; }
/* Proportional, not a fixed 1400px hero — the canvas width varies with the
   display's aspect ratio (see fitStage in hub.js). */
.coll-detail-inner { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 80px; width: 100%; overflow: hidden; padding: 200px 120px 80px 220px; }
.coll-detail-hero { background: var(--cream-2); background-size: cover; background-position: center; border-radius: 32px; }
.coll-detail-text { overflow-y: auto; padding-right: 40px; }
.coll-detail-period { font-size: 28px; color: var(--purple); font-weight: 500; font-variant-numeric: tabular-nums; }
.coll-detail-name { font-family: "Source Serif 4", Georgia, serif; font-size: 88px; line-height: 1; letter-spacing: -0.03em; margin: 12px 0 0; font-weight: 500; color: var(--on-surface); }
.coll-detail-sub { font-size: 30px; color: var(--on-surface-soft); margin-top: 20px; font-family: "Source Serif 4", Georgia, serif; font-style: italic; }
.coll-detail-body { font-size: 28px; line-height: 1.5; color: var(--on-surface); margin: 40px 0 0; }
.coll-detail-body:empty { display: none; }
.coll-detail-body p { margin: 0 0 26px; }
.coll-detail-body p:last-child { margin-bottom: 0; }

/* Markdown-rendered bodies (see ContentScanner's ScanEntityFolders) can carry
   headings, tables, lists and inline images — richer than the plain-text
   paragraphs the fields above were styled for — so give every tag Markdig can
   produce a sane look inside the same reading column. */
.coll-detail-body h1, .coll-detail-body h2, .coll-detail-body h3, .coll-detail-body h4 {
  font-family: "Source Serif 4", Georgia, serif; font-weight: 500; color: var(--on-surface);
  line-height: 1.2; margin: 40px 0 18px;
}
.coll-detail-body h1 { font-size: 40px; } .coll-detail-body h2 { font-size: 34px; } .coll-detail-body h3 { font-size: 30px; } .coll-detail-body h4 { font-size: 28px; }
.coll-detail-body > :first-child { margin-top: 0; }
.coll-detail-body ul, .coll-detail-body ol { margin: 0 0 26px; padding-left: 44px; }
.coll-detail-body li { margin-bottom: 10px; }
.coll-detail-body blockquote {
  margin: 0 0 26px; padding: 4px 32px; border-left: 6px solid var(--purple-soft);
  color: var(--on-surface-soft); font-style: italic;
}
.coll-detail-body table { border-collapse: collapse; width: 100%; margin: 0 0 26px; font-size: 24px; }
.coll-detail-body th, .coll-detail-body td { text-align: left; padding: 14px 20px; border-bottom: 1.5px solid var(--line); }
.coll-detail-body th { font-weight: 600; color: var(--on-surface-soft); }
.coll-detail-body img { max-width: 100%; border-radius: 20px; margin: 0 0 26px; display: block; }
.coll-detail-body a { color: var(--purple); text-decoration-color: var(--purple-soft); }
.coll-detail-body hr { border: none; border-top: 1.5px solid var(--line); margin: 40px 0; }

/* ---------- Split detail: folder-sourced material beside its text ----------
   The layout Personen (and any other collection app with a scanned entity
   folder) gets instead of the hero + media-strip above: the free-manipulation
   canvas confined to the left of the screen, matching the timeline's own
   "leaves" view but boxed rather than full-stage, with the (Markdown) text
   reading normally beside it — never underneath the scattered cards. */
.coll-detail-split-inner {
  display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  width: 100%; height: 100%; overflow: hidden;
}
/* Media-fed canvas (Tradities): half the screen each. A scanned person folder
   holds one to four files and reads fine in the narrower column above; a
   tradition holds five to eighteen photographs, and they need the room. */
.coll-detail-split-even .coll-detail-split-inner {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}
/* A place carrying the whole source article needs the reading column wider
   than the picture canvas: 40/60 rather than half and half. The photographs
   lose a little room and stay perfectly handleable; the article gains a
   measure it can actually be read at. */
.coll-detail-split-article .coll-detail-split-inner {
  grid-template-columns: minmax(0, 0.4fr) minmax(0, 0.6fr);
}
.coll-detail-canvas {
  position: relative; overflow: hidden; background: var(--surface-2);
  border-right: 1.5px solid var(--line);
}
.coll-detail-textpane { overflow-y: auto; padding: 200px 100px 80px 72px; }
.coll-detail-textpane .coll-detail-text { max-width: 1300px; }
/* A full source article runs to thousands of words, so it gets a little more
   measure than a short biography: 1500 design pixels is about sixty characters
   a line at this size, and it leaves less of the pane empty. The stage is 3840
   design pixels wide on a 16:9 screen and 3456 on a 16:10 one, so this column
   is the same on both — only the margin around it changes. */
.coll-detail-split.has-article .coll-detail-text { max-width: 1500px; }
/* The article runs long, so it gets the full width of its pane rather than the
   1300px measure the short summaries use. */
.coll-detail-split-article .coll-detail-textpane .coll-detail-text { max-width: 1600px; }

/* A name inside the text, linked to that person — the encyclopedia move that
   replaced the tijdgenoten star. It has to sit in the running text without
   breaking the line rhythm, so it inherits everything and is marked only by
   colour and a soft underline; a button reset would otherwise give it the
   browser's own font and baseline. */
.coll-link {
  font: inherit; line-height: inherit; color: var(--purple);
  background: none; border: none; padding: 0; margin: 0;
  text-decoration: underline; text-decoration-color: var(--purple-soft);
  text-underline-offset: 5px; text-decoration-thickness: 2px;
}
.coll-link:active { background: var(--purple-wash); border-radius: 6px; }

/* ---------- A block of Mééls beside the text ----------
   Not a translation — the language buttons do not reach it. It is dialect
   quoted from the source, set apart so nobody mistakes it for the Dutch, with
   the reading underneath in the smaller, quieter type of a footnote. Gold
   rather than purple: this is the one place on the table where the village
   speaks in its own words. */
.coll-meels {
  margin-top: 44px; padding: 40px 44px;
  background: var(--gold-wash); border-radius: 28px;
  border-left: 10px solid var(--gold);
}
.coll-meels-head { display: flex; align-items: baseline; gap: 20px; flex-wrap: wrap; margin-bottom: 28px; }
.coll-meels-tag {
  flex: none; font-size: 19px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: oklch(42% 0.09 75); background: oklch(75% 0.13 75 / 0.32); border-radius: 200px; padding: 8px 22px;
}
.coll-meels-lede { font-size: 24px; color: var(--on-surface-soft); font-style: italic; font-family: "Source Serif 4", Georgia, serif; }

.coll-meels-lines { display: flex; flex-direction: column; gap: 26px; }
.coll-meels-text {
  font-family: "Source Serif 4", Georgia, serif; font-size: 32px; line-height: 1.25;
  color: var(--on-surface);
}
.coll-meels-gloss { font-size: 21px; color: var(--on-surface-soft); margin-top: 6px; }

.coll-meels-note {
  margin-top: 32px; padding-top: 24px; border-top: 1.5px solid oklch(75% 0.13 75 / 0.35);
  font-size: 20px; line-height: 1.4; color: var(--on-surface-soft); font-style: italic;
}

.coll-section-label { font-size: 22px; font-weight: 500; color: var(--on-surface-soft); letter-spacing: 0.18em; text-transform: uppercase; margin: 56px 0 24px; }

.coll-media-strip { display: flex; gap: 24px; flex-wrap: wrap; }
.coll-media-thumb { width: 260px; height: 200px; border-radius: 20px; background: var(--cream-2); background-size: cover; background-position: center; border: 1.5px solid var(--line); }
.coll-media-thumb:active { border-color: var(--purple); }

.coll-chips { display: flex; gap: 20px; flex-wrap: wrap; }
.coll-chip {
  display: inline-flex; align-items: center; gap: 20px;
  background: var(--white); border: 1.5px solid var(--line); border-radius: 200px;
  padding: 22px 36px; text-align: left;
}
.coll-chip:active { border-color: var(--purple); background: var(--purple-wash); }
.coll-chip-app { font-size: 18px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--purple); font-weight: 500; }
.coll-chip-name { font-size: 26px; color: var(--on-surface); font-family: "Source Serif 4", Georgia, serif; }
.coll-chip svg { width: 28px; height: 28px; color: var(--on-surface-soft); }

/* A tapped media item becomes the same draggable/pinchable/rotatable card as
   a timeline leaf, so the gesture vocabulary stays identical across the table. */
.coll-media-overlay { position: absolute; inset: 0; background: oklch(22% 0.025 305 / 0.55); z-index: 30; }
.coll-media-canvas { position: absolute; inset: 0; }
.coll-media-close {
  position: absolute; left: 60px; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 120px; height: 120px; border-radius: 28px; border: none;
  background: var(--white); color: var(--on-surface); display: grid; place-items: center;
}
.coll-media-close svg { width: 52px; height: 52px; }

/* ============================================================ PERSONEN — GENERATIEWAND
   Replaced the levenslijnen (and the tijdgenoten star beside the text). One
   shelf per decade of birth; a crowded decade wraps onto more lines instead of
   pushing 34 overlapping bars onto 34 separate rows. See views/generations.js. */
.gw-view { position: absolute; inset: 0; display: flex; flex-direction: column; }

.gw-filters { flex: none; display: flex; gap: 12px; margin-bottom: 32px; flex-wrap: wrap; }
.gw-filter {
  display: inline-flex; align-items: center; gap: 14px;
  background: var(--white); border: 1.5px solid var(--line); border-radius: 200px;
  padding: 16px 32px; font-size: 24px; color: var(--on-surface-soft);
}
.gw-filter span {
  font-size: 19px; font-variant-numeric: tabular-nums;
  background: var(--surface-2); border-radius: 200px; padding: 4px 14px; color: var(--on-surface-soft);
}
.gw-filter.active { background: var(--purple); border-color: var(--purple); color: #fff; }
.gw-filter.active span { background: rgba(255,255,255,0.22); color: #fff; }

.gw-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 20px; }
.gw-shelves { display: flex; flex-direction: column; gap: 72px; padding-bottom: 40px; }

.gw-shelf-head { display: flex; align-items: center; gap: 28px; margin-bottom: 28px; }
.gw-decade {
  font-family: "Source Serif 4", Georgia, serif; font-size: 56px; font-weight: 500;
  color: var(--purple); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; flex: none;
}
.gw-decade-undated { font-size: 38px; font-style: italic; color: var(--on-surface-soft); }
.gw-rule { flex: 1; height: 2px; background: var(--line); }
.gw-count {
  flex: none; font-size: 24px; color: var(--on-surface-soft); font-variant-numeric: tabular-nums;
  background: var(--surface-2); border-radius: 200px; padding: 8px 24px;
}

.gw-cards { display: flex; flex-wrap: wrap; gap: 40px 36px; }

.gw-card {
  width: 240px; flex: none; background: transparent; border: none; padding: 0;
  display: flex; flex-direction: column; align-items: flex-start; text-align: left;
}
.gw-card:active .gw-portrait { border-color: var(--purple); }

.gw-portrait {
  width: 200px; height: 200px; border-radius: 24px;
  background: var(--cream-2); background-size: cover; background-position: center top;
  border: 4px solid var(--white); box-shadow: 0 10px 26px rgba(0,0,0,0.16);
}
/* No portrait in the source document, so the card shows the person's initials.
   Borrowing an unrelated archive scan to fill the hole would read as a
   photograph of them, which it is not. */
.gw-portrait.is-empty {
  display: grid; place-items: center;
  background: var(--purple-wash); border-color: var(--white);
  color: var(--purple-deep); font-family: "Source Serif 4", Georgia, serif; font-size: 64px;
}

.gw-name {
  font-family: "Source Serif 4", Georgia, serif; font-size: 28px; line-height: 1.15;
  color: var(--on-surface); margin-top: 20px;
}
.gw-years { font-size: 21px; color: var(--purple); font-variant-numeric: tabular-nums; margin-top: 6px; }

/* Fixed pixels-per-year, so one life is directly comparable to the one beside
   it without an axis to read against. An unknown or open end fades out rather
   than stopping square, because the source does not say where it stops. */
.gw-bar { height: 8px; border-radius: 200px; background: var(--purple); opacity: 0.75; margin-top: 12px; }
.gw-bar.open-ended { background: linear-gradient(to right, var(--purple) 55%, transparent 100%); }

.gw-role {
  font-size: 20px; color: var(--on-surface-soft); margin-top: 12px; line-height: 1.25;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

.gw-hint {
  flex: none; padding-top: 20px; font-size: 22px; color: var(--on-surface-soft);
  font-style: italic; font-family: "Source Serif 4", Georgia, serif;
}

/* ============================================================ TRADITIES — JAARWIEL
   The one cyclical view in a museum full of linear ones.

   The wheel is a DOME: its centre sits just below the bottom edge, so only the
   top half shows. `.yw-view` deliberately breaks out of `.coll-view`'s padding
   (the negative inset) so the dome runs the full width and touches the bottom
   of the screen — at a table you stand at, the thing you spin belongs under
   your hands, not floating in the middle.

   The SVG uses a centre-origin viewBox with 0,0 at the wheel's centre, so every
   radius in views/yearwheel.js reads as a real distance. */

.yw-view { position: absolute; inset: 0 -120px -80px; display: flex; flex-direction: column; }
.yw-top { flex: none; padding: 0 120px; display: flex; flex-direction: column; gap: 24px; }
.yw-dome { flex: 1; min-height: 0; position: relative; }
.yw-svg { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }

/* ---- The seasonal band. Segment fills are computed per five-day step in JS
   (see colourAtDay) so the year fades rather than steps; only the edges and
   ticks are styled here. ---- */
.yw-ring path { stroke: none; }
.yw-band-edge { fill: none; stroke: oklch(62% 0.04 300 / 0.30); stroke-width: 2; }
.yw-tick { stroke: oklch(55% 0.05 300 / 0.35); stroke-width: 3; stroke-linecap: round; }
.yw-month-div { stroke: var(--surface); stroke-width: 3; stroke-opacity: 0.75; }

/* Set on the curve of the band (a <textPath>), which is what makes it read as
   an engraved dial. `dominant-baseline: central` centres the glyphs on the arc
   instead of hanging them off it as a baseline. */
.yw-month-label {
  font-family: "Outfit", sans-serif; font-size: 34px; font-weight: 500;
  fill: oklch(42% 0.03 300 / 0.78); letter-spacing: 0.16em; text-transform: uppercase;
  dominant-baseline: central;
}

/* ---- Markers: a tradition and "vandaag" are the same object, differently
   dressed — stem, pin, upright name plate. ---- */
.yw-marker { cursor: pointer; }
.yw-stem { stroke: var(--purple); stroke-opacity: 0.45; stroke-width: 4; stroke-linecap: round; }
.yw-dot { fill: var(--purple); stroke: var(--white); stroke-width: 6; transition: r 0.15s ease, fill 0.15s ease; }

.yw-plate {
  fill: var(--white); stroke: var(--line); stroke-width: 2;
  transition: fill 0.15s ease, stroke 0.15s ease;
}
.yw-plate-label {
  font-family: "Source Serif 4", Georgia, serif; font-size: 32px; fill: var(--ink);
  transition: fill 0.15s ease;
}

/* The tradition nearest the apex leans forward, so spinning has a running
   answer on the wheel itself and not only in the hub readout. */
.yw-marker.is-near .yw-dot { fill: var(--purple-deep); r: 23; }
.yw-marker.is-near .yw-plate { fill: var(--purple); stroke: var(--purple); }
.yw-marker.is-near .yw-plate-label { fill: var(--white); }

.yw-marker:active .yw-dot { fill: var(--gold); r: 25; }
.yw-marker:active .yw-plate { fill: var(--gold-wash); stroke: var(--gold); }
.yw-marker:active .yw-plate-label { fill: var(--ink); }
.yw-marker:focus-visible { outline: none; }
.yw-marker:focus-visible .yw-plate { stroke: var(--purple); stroke-width: 5; }

/* Today is a marker like any other, but ringed rather than recoloured, so it
   reads as "you are here" instead of as one more festivity. */
.yw-marker-today .yw-dot { fill: var(--gold); }
.yw-marker-today .yw-stem { stroke: var(--gold); stroke-opacity: 0.85; }
.yw-today-halo { fill: none; stroke: var(--gold); stroke-width: 5; stroke-opacity: 0.55; animation: yw-pulse 3.2s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.yw-marker-today .yw-plate { fill: var(--gold); stroke: var(--gold); }
.yw-marker-today .yw-plate-label { fill: var(--ink); font-family: "Outfit", sans-serif; font-size: 28px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; }
@keyframes yw-pulse {
  0%, 100% { transform: scale(1);    stroke-opacity: 0.55; }
  50%      { transform: scale(1.18); stroke-opacity: 0.18; }
}

/* ---- Hub: the half-disc at the bottom, reading out whatever the apex is
   pointing at. ---- */
.yw-hub-disc { stroke: oklch(62% 0.04 300 / 0.28); stroke-width: 2; }
.yw-hub-season {
  font-family: "Outfit", sans-serif; font-size: 30px; font-weight: 500;
  fill: var(--ink-mute); letter-spacing: 0.3em; text-transform: uppercase;
}
.yw-hub-date { font-family: "Outfit", sans-serif; font-size: 74px; font-weight: 500; fill: var(--purple); letter-spacing: -0.01em; }
.yw-hub-name { font-family: "Source Serif 4", Georgia, serif; font-size: 38px; fill: var(--ink); }

/* ---- Apex: fixed pointer marking the date the wheel has been turned to. ---- */
.yw-apex { pointer-events: none; }
.yw-apex-point { fill: var(--purple); }
.yw-apex-stem { stroke: var(--purple); stroke-width: 7; stroke-linecap: round; }
.yw-apex-line { stroke: var(--purple); stroke-opacity: 0.30; stroke-width: 3; stroke-dasharray: 10 12; }

.yw-undated { display: flex; flex-wrap: wrap; gap: 16px; }
.yw-undated:empty { display: none; }
.yw-undated .coll-section-label { width: 100%; margin: 0 0 8px; }
.yw-hint { font-size: 24px; color: var(--on-surface-soft); font-style: italic; font-family: "Source Serif 4", Georgia, serif; }

/* Decade stack + audio in the tradition detail */
.td-extra { margin-bottom: 8px; }
.td-audio { margin-bottom: 8px; }
.td-audio-play { display: inline-flex; align-items: center; gap: 18px; padding: 22px 40px; background: var(--purple); color: white; border: none; border-radius: 200px; font-size: 26px; font-weight: 500; }
.td-audio-play svg { width: 32px; height: 32px; }
.td-audio-play.playing { background: var(--purple-deep); }
.td-stack { display: flex; gap: 20px; overflow-x: auto; padding-bottom: 12px; }
.td-stack-item { flex: none; border: none; background: transparent; padding: 0; text-align: center; }
.td-stack-image { width: 280px; height: 210px; border-radius: 20px; background: var(--cream-2); background-size: cover; background-position: center; border: 1.5px solid var(--line); }
.td-stack-year { font-size: 24px; color: var(--purple); font-variant-numeric: tabular-nums; margin-top: 12px; font-weight: 500; }

/* ============================================================ VERHALEN — CINEMA
   One story read large on the left, the index of all thirty scrolling on the
   right. The provenance strip is not decoration and must never be made
   dismissible or collapsed — see views/storyplayer.js for why. In the cinema
   it rides in the title row so the reading pane keeps the height; it is still
   on screen the entire time, which is the only thing that matters. */

.sp-cinema { position: absolute; inset: 0; display: grid; grid-template-columns: minmax(0, 1fr) 700px; gap: 56px; }
.sp-cinema-stage { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 24px; }
.sp-cinema-title { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 40px; }
.sp-cinema-heading { min-width: 0; }
.sp-cinema-title h2 { font-family: "Source Serif 4", Georgia, serif; font-size: 52px; margin: 0; font-weight: 500; line-height: 1.05; color: var(--on-surface); }
.sp-cinema-sub { font-size: 25px; color: var(--on-surface-soft); font-style: italic; font-family: "Source Serif 4", Georgia, serif; margin-top: 6px; }
.sp-cinema-title .sp-provenance { flex: none; max-width: 46%; padding: 14px 28px; gap: 18px; }

.sp-player { flex: 1; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) 620px; gap: 48px; }
/* The cinema variant: no side column, because the side column is the index. */
.sp-player-inline { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; gap: 24px; }
.sp-main { min-width: 0; display: flex; flex-direction: column; gap: 20px; }
/* The screen is the area the pane gives us; what shape the picture takes inside
   it depends on what is playing. Note the flex centring rather than grid: a
   percentage height resolves against a flex container with a definite height,
   but not against an auto-sized grid row, and that difference is what let the
   films overflow their box and lose their top and bottom to overflow:hidden. */
.sp-screen { flex: 1; min-height: 0; background: var(--ink); border-radius: 28px; overflow: hidden; display: flex; align-items: center; justify-content: center; position: relative; }
/* A film: the screen is only the area, and the 16:9 frame inside it is the
   window. The archive films are 4:3, so contain pillarboxes them within the
   frame — the whole picture, nothing cropped. */
.sp-screen.is-film { background: transparent; overflow: visible; border-radius: 0; }
.sp-frame {
  position: relative; flex: 0 0 auto;
  height: 100%; width: auto; max-width: 100%; aspect-ratio: 16 / 9;
  background: var(--ink); border-radius: 28px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.sp-video { width: 100%; height: 100%; object-fit: contain; background: var(--ink); }

.sp-pending { text-align: center; padding: 80px; }
.sp-pending-title { font-family: "Source Serif 4", Georgia, serif; font-size: 48px; color: var(--cream); }
.sp-pending-sub { font-size: 26px; color: oklch(80% 0.02 305); margin-top: 20px; }

/* Always-visible provenance. */
.sp-provenance { flex: none; display: flex; align-items: center; gap: 24px; padding: 20px 32px; border-radius: 20px; }
.sp-provenance.archival { background: var(--purple-wash); }
.sp-provenance.ai { background: oklch(75% 0.13 75 / 0.2); }
.sp-provenance.document { background: oklch(72% 0.10 150 / 0.18); }
.sp-provenance-tag { font-size: 20px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; white-space: nowrap; }
.sp-provenance.archival .sp-provenance-tag { color: var(--purple-deep); }
.sp-provenance.ai .sp-provenance-tag { color: oklch(42% 0.09 75); }
.sp-provenance.document .sp-provenance-tag { color: oklch(38% 0.08 150); }
.sp-provenance-note { font-size: 23px; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; font-style: italic; }

.sp-transport { flex: none; display: flex; align-items: center; gap: 32px; }

/* ---- Document story: the pages, turned like a book ----
   Two fits, and the default is the unusual one. `page` shows the whole sheet,
   which inside this pane lands around 900px wide — smaller than the paper it
   was printed on, and the reason these articles read badly before. `width`
   fills the pane and scrolls, so the type arrives at the size the pages were
   rasterised for (2856px, see rerender-pages). The dark .sp-screen behind the
   sheet reads as the table the page is lying on. */
.sp-page {
  position: absolute; inset: 0; overflow: auto; overscroll-behavior: contain;
  display: flex; justify-content: center; background: var(--ink);
}
.sp-page-img { display: block; }
.sp-page[data-fit="width"] { align-items: flex-start; touch-action: pan-y; }
.sp-page[data-fit="width"] .sp-page-img { width: 100%; height: auto; }
.sp-page[data-fit="page"] { align-items: center; touch-action: none; }
.sp-page[data-fit="page"] .sp-page-img { max-width: 100%; max-height: 100%; width: auto; height: auto; margin: auto; }
.sp-page::-webkit-scrollbar { width: 16px; }
.sp-page::-webkit-scrollbar-thumb { background: oklch(100% 0 0 / 0.35); border-radius: 200px; border: 5px solid transparent; background-clip: content-box; }
.sp-page-fit {
  flex: none; margin-left: auto; height: 84px; padding: 0 34px; border-radius: 200px;
  border: 1.5px solid var(--line); background: var(--white); color: var(--on-surface); font-size: 24px;
}
.sp-page-prev, .sp-page-next {
  width: 84px; height: 84px; flex: none; display: grid; place-items: center;
  border: 1.5px solid var(--line); border-radius: 50%; background: var(--white); color: var(--on-surface);
}
.sp-page-prev svg, .sp-page-next svg { width: 36px; height: 36px; }
.sp-page-prev:disabled, .sp-page-next:disabled { opacity: 0.3; }
.sp-page-count { font-size: 26px; color: var(--on-surface-soft); font-variant-numeric: tabular-nums; min-width: 120px; text-align: center; }
.sp-play { width: 108px; height: 108px; border-radius: 50%; border: none; background: var(--purple); color: white; display: grid; place-items: center; flex: none; }
.sp-play svg { width: 44px; height: 44px; }
.sp-scrub { flex: 1; min-width: 0; padding: 30px 0; }
.sp-scrub-track { position: relative; height: 20px; border-radius: 200px; background: var(--surface-2); border: 1.5px solid var(--line); touch-action: none; }
.sp-scrub-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: 200px; background: var(--purple); }
.sp-scrub-ticks { position: absolute; inset: 0; }
.sp-tick { position: absolute; top: 50%; transform: translate(-50%, -50%); width: 44px; height: 44px; border-radius: 50%; border: none; background: transparent; }
.sp-tick::after { content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 12px; height: 32px; border-radius: 200px; background: var(--gold); box-shadow: 0 0 0 3px var(--surface); }
.sp-time { font-size: 26px; color: var(--on-surface-soft); font-variant-numeric: tabular-nums; white-space: nowrap; flex: none; }
/* Sound starts off. A table stands in a room with other people in it. */
.sp-sound { width: 84px; height: 84px; flex: none; display: grid; place-items: center; border: 1.5px solid var(--line); border-radius: 50%; background: var(--white); color: var(--on-surface); }
.sp-sound svg { width: 36px; height: 36px; }
.sp-sound.on { background: var(--purple); border-color: var(--purple); color: #fff; }

.sp-rail { min-width: 0; display: flex; flex-direction: column; }
.sp-rail .coll-section-label { margin-top: 0; }
.sp-rail-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 24px; }
.sp-source { background: var(--white); border: 1.5px solid var(--line); border-radius: 24px; overflow: hidden; }
.sp-source-image { height: 380px; background: var(--cream-2); background-size: cover; background-position: center; }
.sp-source-compact .sp-source-image { height: 200px; }
.sp-source-meta { padding: 24px 28px; }
.sp-source-app { font-size: 18px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--purple); font-weight: 500; }
.sp-source-label { font-size: 25px; color: var(--on-surface); font-family: "Source Serif 4", Georgia, serif; margin-top: 8px; line-height: 1.3; }

/* ---- The index: every story, on the right, scrolling ----
   Thirty rows do not fit across the bottom of anything, and a horizontal strip
   hid all but four of them behind a swipe. A vertical list shows a dozen at a
   glance and costs the reading pane nothing but its own column. */
.sp-index { min-height: 0; display: flex; flex-direction: column; border-left: 1.5px solid var(--line); padding-left: 44px; }
.sp-index-list { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 14px; padding-right: 14px; }
.sp-index-group { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin: 26px 0 4px; }
.sp-index-group:first-child { margin-top: 0; }
.sp-index-group span { font-variant-numeric: tabular-nums; opacity: 0.55; }
.sp-item {
  display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 24px; align-items: center;
  text-align: left; padding: 14px; border: 1.5px solid transparent; border-radius: 22px; background: transparent;
}
.sp-item-thumb { width: 132px; height: 132px; border-radius: 16px; background: var(--cream-2); background-size: cover; background-position: center top; }
.sp-item-text { min-width: 0; }
.sp-item-name { font-family: "Source Serif 4", Georgia, serif; font-size: 28px; line-height: 1.2; color: var(--on-surface); }
.sp-item-meta { font-size: 21px; color: var(--on-surface-soft); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sp-item .story-kind { margin: 10px 0 0; font-size: 16px; padding: 6px 14px; }
.sp-item.active { background: var(--white); border-color: var(--purple); }
.sp-item.active .sp-item-name { color: var(--purple-deep); font-weight: 500; }

/* Inline rail: the cross-links lie along the bottom of the reading pane as a
   thin strip. storyplayer removes the rail outright when it would be empty. */
.sp-player-inline .sp-rail { flex: none; min-height: 0; }
.sp-player-inline .sp-rail .coll-section-label { margin: 0 0 14px; }
.sp-player-inline .sp-rail-body { flex-direction: row; overflow-x: auto; overflow-y: hidden; gap: 20px; padding-bottom: 6px; }
.sp-player-inline .sp-source { flex: none; width: 430px; display: flex; align-items: center; border-radius: 20px; }
.sp-player-inline .sp-source-image { width: 80px; height: 80px; flex: none; border-radius: 12px; margin: 10px; }
.sp-player-inline .sp-source-meta { padding: 0 22px 0 4px; min-width: 0; }
.sp-player-inline .sp-source-app { font-size: 16px; }
.sp-player-inline .sp-source-label { font-size: 22px; margin-top: 2px; }

/* In the detail overlay the player sits in the narrower text column. */
.coll-detail-extra .sp-player { grid-template-columns: minmax(0, 1fr); height: auto; }
.coll-detail-extra .sp-screen { height: 520px; flex: none; }
.coll-detail-extra .sp-rail { margin-top: 32px; }
.coll-detail-extra .sp-rail-body { max-height: 640px; }

/* ============================================================ 3D / TURNTABLE
   Two viewer tiers per building: a GLB orbited in three.js, or a photo
   turntable when the model isn't finished. Used by the Kaart's detail view —
   this outlived the Gebouwen app it was written for. See views/building3d.js. */

.b3-extra { margin-bottom: 8px; }
.b3-viewer { position: relative; height: 720px; border-radius: 28px; overflow: hidden; background: var(--cream-2); border: 1.5px solid var(--line); touch-action: none; }
.b3-canvas { position: absolute; inset: 0; }
.b3-canvas canvas { display: block; width: 100%; height: 100%; }
.b3-frame { position: absolute; inset: 0; background-size: contain; background-position: center; background-repeat: no-repeat; }
.b3-status { position: absolute; inset: 0; display: grid; place-items: center; font-size: 28px; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; font-style: italic; text-align: center; padding: 40px; }
.b3-hint { position: absolute; left: 32px; bottom: 28px; font-size: 22px; color: var(--on-surface-soft); background: oklch(97% 0.012 85 / 0.85); padding: 12px 24px; border-radius: 200px; pointer-events: none; }
.b3-reset { position: absolute; right: 32px; bottom: 28px; display: inline-flex; align-items: center; gap: 14px; padding: 16px 28px; border: 1.5px solid var(--line); background: var(--white); color: var(--on-surface); border-radius: 200px; font-size: 22px; }
.b3-reset svg { width: 26px; height: 26px; }

/* ============================================================ STAMBOOM
   Search first, tree second — a visitor arrives looking for their own name.
   See views/stamboom.js. */

.st-view { position: absolute; inset: 0; padding: 200px 120px 80px; background: var(--surface); display: flex; flex-direction: column; }
.st-body { flex: 1; min-height: 0; position: relative; }

/* Fabricated demo data must be impossible to mistake for the real records. */
.st-demo-banner {
  flex: none; margin-bottom: 32px; padding: 22px 40px; border-radius: 20px;
  background: oklch(60% 0.18 25 / 0.14); color: oklch(42% 0.14 25);
  font-size: 24px; font-weight: 600; letter-spacing: 0.06em; text-align: center;
}

.st-empty { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; text-align: center; }
.st-empty-title { font-family: "Source Serif 4", Georgia, serif; font-size: 48px; color: var(--on-surface); }
.st-empty-sub { font-size: 28px; color: var(--on-surface-soft); font-family: "Source Serif 4", Georgia, serif; font-style: italic; max-width: 1400px; }


.sm-search-field { flex: none; display: flex; align-items: center; gap: 28px; background: var(--white); border: 2px solid var(--line); border-radius: 200px; padding: 10px 48px; max-width: 1800px; }
.sm-search-field svg { width: 52px; height: 52px; color: var(--on-surface-soft); flex: none; }
.sm-search-field input { border: none; outline: none; background: transparent; font-family: inherit; font-size: 46px; color: var(--on-surface); padding: 30px 0; width: 100%; }



/* Match quality is always visible: a suggestion must never read as a fact. */
.st-match { font-size: 19px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; padding: 8px 20px; border-radius: 200px; white-space: nowrap; }
.st-match-exact { background: var(--purple); color: white; }
.st-match-variant { background: var(--purple-soft); color: white; }
.st-match-prefix { background: var(--surface-2); color: var(--on-surface-soft); }
.st-match-phonetic { background: oklch(75% 0.13 75 / 0.28); color: oklch(42% 0.09 75); }
.st-match-fuzzy, .st-match-contains { background: var(--surface-2); color: var(--on-surface-soft); }


/* ---- Tree ---- */


.st-node {
  position: absolute; width: 400px; height: 104px; padding: 0 28px;
  background: var(--white); border: 2px solid var(--line); border-radius: 18px;
  display: flex; flex-direction: column; justify-content: center; align-items: flex-start; gap: 4px;
  text-align: left; box-shadow: 0 4px 14px rgba(0,0,0,0.06);
}

.st-panel-card { background: var(--white); border: 1.5px solid var(--line); border-radius: 28px; padding: 44px; }
.st-panel-name { font-family: "Source Serif 4", Georgia, serif; font-size: 46px; color: var(--on-surface); line-height: 1.05; }
.st-panel-years { font-size: 28px; color: var(--purple); font-variant-numeric: tabular-nums; margin-top: 10px; }
.st-panel-place { font-size: 24px; color: var(--on-surface-soft); margin-top: 8px; }
.st-panel-private { font-size: 24px; color: var(--on-surface-soft); font-style: italic; margin-top: 16px; font-family: "Source Serif 4", Georgia, serif; }
.st-rel { margin-top: 36px; }
.st-rel-label { font-size: 19px; font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--on-surface-soft); margin-bottom: 12px; }
.st-rel-name { font-size: 26px; color: var(--on-surface); font-family: "Source Serif 4", Georgia, serif; padding: 6px 0; }
.st-rel-name span { color: var(--purple); font-size: 21px; font-family: "Outfit", sans-serif; font-variant-numeric: tabular-nums; margin-left: 10px; }

/* ---------- Stamboom: the whole village as a map (views/stamboom.js) ---------- */
.sm-wrap { position: absolute; inset: 0; }

.sm-stage {
  position: absolute; inset: 0;
  overflow: hidden; touch-action: none;
  background: var(--white); border: 1.5px solid var(--line); border-radius: 28px;
}
.sm-canvas { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }

.sm-edges { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.sm-edge { fill: none; stroke-linejoin: round; }
.sm-edge-descent { stroke: var(--purple); stroke-opacity: 0.30; stroke-width: 3; }
.sm-edge-marriage { stroke: var(--gold); stroke-opacity: 0.9; stroke-width: 7; }

/* One element per person — the years ride in data-years and are drawn by
   ::after, which halves the element count over a few thousand people and makes
   hiding them at low zoom a class swap rather than a DOM walk. Width and height
   must match NODE_W/NODE_H in stamboom.js; that layout never measures the DOM. */
.sm-node {
  position: absolute; width: 190px; height: 56px;
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center;
  padding: 0 14px; gap: 2px;
  background: var(--white); border: 2px solid var(--line); border-radius: 12px;
  font-family: "Source Serif 4", Georgia, serif; font-size: 21px; line-height: 1.05;
  color: var(--on-surface); text-align: left;
  overflow: hidden; white-space: nowrap;
}
.sm-node::after {
  content: attr(data-years);
  font-family: "Outfit", sans-serif; font-size: 16px; font-variant-numeric: tabular-nums;
  color: var(--purple); opacity: 0.85;
}
.sm-node.is-m { border-color: oklch(72% 0.07 250); }
.sm-node.is-f { border-color: oklch(74% 0.09 20); }
.sm-node.is-private { background: var(--surface-2); border-style: dashed; color: var(--on-surface-soft); font-style: italic; }
.sm-node.is-lit { border-color: var(--purple); background: var(--purple-wash); z-index: 2; }
.sm-node.is-selected { border-color: var(--purple-deep); border-width: 4px; background: var(--white); box-shadow: 0 0 0 8px var(--purple-wash); z-index: 3; }

/* Tapping someone dims the village so their line reads through it. */
.sm-canvas.has-selection .sm-node { opacity: 0.22; }
.sm-canvas.has-selection .sm-node.is-lit,
.sm-canvas.has-selection .sm-node.is-selected { opacity: 1; }
.sm-canvas.has-selection .sm-edge { stroke-opacity: 0.09; }

/* Level of detail: drop the years, then the names. Below that zoom the text is
   a grey smear costing layout for nothing, and the shape of the village is what
   is being read at that distance anyway. */
.sm-canvas.lod-mid .sm-node::after { display: none; }
.sm-canvas.lod-mid .sm-node { justify-content: center; font-size: 24px; }
.sm-canvas.lod-far .sm-node { font-size: 0; padding: 0; border-radius: 6px; border-width: 4px; }

/* ---- Search, docked over the map ---- */
.sm-search { position: absolute; top: 28px; left: 28px; width: 760px; z-index: 6; display: flex; flex-direction: column; gap: 16px; }
.sm-search .sm-search-field { background: var(--white); box-shadow: 0 12px 32px rgba(0,0,0,0.14); padding: 6px 32px; max-width: none; }
.sm-search .sm-search-field input { font-size: 34px; padding: 20px 0; }
.sm-search .sm-search-field svg { width: 38px; height: 38px; }

.sm-results { display: none; flex-direction: column; gap: 12px; max-height: 1100px; overflow-y: auto; }
.sm-wrap.is-searching .sm-results { display: flex; }
.sm-result { background: var(--white); border: 1.5px solid var(--line); border-radius: 20px; box-shadow: 0 12px 32px rgba(0,0,0,0.12); overflow: hidden; }
.sm-result-head { display: flex; align-items: center; gap: 16px; width: 100%; padding: 22px 28px; background: var(--surface-2); border: none; text-align: left; }
.sm-result-name { font-family: "Source Serif 4", Georgia, serif; font-size: 32px; color: var(--on-surface); text-transform: capitalize; flex: 1; }
.sm-result-count { font-size: 20px; color: var(--on-surface-soft); font-variant-numeric: tabular-nums; flex: none; }
.sm-result-people { display: flex; flex-direction: column; }
.sm-result-person { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 18px 28px; background: transparent; border: none; border-top: 1px solid var(--line); font-family: "Source Serif 4", Georgia, serif; font-size: 26px; color: var(--on-surface); text-align: left; }
.sm-result-person:active { background: var(--purple-wash); }
.sm-result-years { font-family: "Outfit", sans-serif; font-size: 21px; color: var(--purple); font-variant-numeric: tabular-nums; flex: none; }
.sm-noresult { background: var(--white); border: 1.5px solid var(--line); border-radius: 20px; padding: 28px; box-shadow: 0 12px 32px rgba(0,0,0,0.12); }
.sm-noresult-title { font-family: "Source Serif 4", Georgia, serif; font-size: 30px; color: var(--on-surface); }
.sm-noresult-sub { font-size: 22px; color: var(--on-surface-soft); margin-top: 8px; font-style: italic; }

/* ---- Tools and hint ---- */
.sm-tools { position: absolute; top: 28px; right: 28px; z-index: 6; display: flex; align-items: center; gap: 24px; }
.sm-tool { display: inline-flex; align-items: center; gap: 16px; background: var(--white); border: 1.5px solid var(--line); border-radius: 200px; padding: 20px 36px; font-size: 24px; color: var(--on-surface); box-shadow: 0 12px 32px rgba(0,0,0,0.12); }
.sm-tool svg { width: 30px; height: 30px; color: var(--purple); }
.sm-tool:active { background: var(--purple-wash); }
.sm-count { font-size: 21px; color: var(--on-surface-soft); background: var(--white); border-radius: 200px; padding: 16px 28px; box-shadow: 0 12px 32px rgba(0,0,0,0.10); }

.sm-hint { position: absolute; left: 28px; bottom: 24px; z-index: 5; font-size: 22px; color: var(--on-surface-soft); font-style: italic; font-family: "Source Serif 4", Georgia, serif; background: oklch(97% 0.012 85 / 0.86); border-radius: 200px; padding: 12px 28px; }

/* ---- Selected-person panel ---- */
/* Sits below .sm-tools rather than over it — the panel outranks the tools on
   z-index, and covering the 'Hele stamboom' button the moment someone is
   selected would strand a visitor zoomed in on one family. */
.sm-panel { position: absolute; top: 136px; right: 28px; width: 620px; max-height: calc(100% - 250px); overflow-y: auto; z-index: 7; }
.sm-panel:empty { display: none; }
.sm-panel .st-panel-card { box-shadow: 0 20px 48px rgba(0,0,0,0.18); position: relative; padding-right: 96px; }
.sm-panel-close { position: absolute; top: 28px; right: 28px; width: 64px; height: 64px; display: grid; place-items: center; background: var(--surface-2); border: none; border-radius: 50%; color: var(--on-surface); }
.sm-panel-close svg { width: 30px; height: 30px; }
.sm-panel-gen { font-size: 21px; color: var(--on-surface-soft); margin-top: 12px; letter-spacing: 0.04em; }
.sm-panel .st-rel-name { display: block; width: 100%; text-align: left; background: transparent; border: none; }
.sm-panel .st-rel-name:active { color: var(--purple); }

.st-jump { margin-bottom: 24px; }
.st-jump svg { width: 30px; height: 30px; color: var(--purple); }

/* ============================================================ ON-SCREEN KEYBOARD
   The table has no physical keyboard. Keys are sized for fingers, not mice.
   See keyboard.js. */

.kb-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 90;
  background: var(--surface-2); border-top: 2px solid var(--line);
  padding: 36px 60px 44px; box-shadow: 0 -20px 60px rgba(0,0,0,0.14);
}
.kb-rows { display: flex; flex-direction: column; gap: 18px; align-items: center; }
.kb-row { display: flex; gap: 18px; justify-content: center; }
.kb-key {
  min-width: 132px; height: 124px; padding: 0 20px;
  border: 1.5px solid var(--line); background: var(--white); color: var(--on-surface);
  border-radius: 20px; font-family: "Outfit", sans-serif; font-size: 46px; font-weight: 500;
  display: inline-flex; align-items: center; justify-content: center; gap: 12px;
  box-shadow: 0 4px 0 var(--line);
}
.kb-key.down { background: var(--purple-wash); border-color: var(--purple); transform: translateY(3px); box-shadow: none; }
.kb-key svg { width: 46px; height: 46px; }
.kb-key span { font-size: 22px; color: var(--on-surface-soft); }
.kb-wide { min-width: 240px; }
.kb-space { min-width: 720px; font-size: 26px; color: var(--on-surface-soft); }
.kb-fn { background: var(--surface); font-size: 30px; }
.kb-done { background: var(--purple); color: white; border-color: var(--purple); font-size: 30px; box-shadow: 0 4px 0 var(--purple-deep); }
.kb-accents .kb-key { min-width: 108px; height: 104px; font-size: 40px; }

.kb-active { border-color: var(--purple) !important; }

/* Pròt mèr Mééls — the vendored dialect trainer, shown in a frame.
   `top` is set inline from HEADER_HEIGHT so the hub header (home, language,
   clock) stays reachable above it; the app brings its own full-bleed layout
   and sizes itself off the frame's viewport in vh/vw, so it needs no design
   pixels of its own here. */
.pmm-view { position: absolute; inset: 0; background: var(--surface); }
.pmm-frame { display: block; width: 100%; height: 100%; border: 0; }

/* The schoolbord is the other kind of vendored app: it brings its own header
   — brand, clock, language switcher, and a labelled way back to the
   Medelotapps home screen — so it takes the whole stage rather than sitting
   under the hub's. Two headers stacked, each with its own idea of "home", is
   what this avoids. `inset: 0` on the stage, not the viewport, so it still
   scales with fitStage() like everything else. */
.embed-view { position: absolute; inset: 0; background: var(--surface); }
.embed-full { z-index: 60; }   /* over .tt-header, which sits at 50 */
