/* The Hội An Chronicles — museum at night.
   One stylesheet, no build step, no preprocessor. Design tokens first, then
   page furniture top-to-bottom in the order it appears in the markup. */

:root {
  --bg: #161006;
  --deep: #120d04;
  --panel: #1c1408;
  --ink: #f2e9d8;
  --ink-soft: rgba(242, 233, 216, 0.88);
  --ink-mute: rgba(242, 233, 216, 0.6);
  --ink-faint: rgba(242, 233, 216, 0.45);
  --amber: #e0a458;
  --cinnabar: #c0502e;
  --jade: #7fa98c;
  --bronze: #9c8a72;
  --hair: rgba(224, 164, 88, 0.2);
  --hair-soft: rgba(224, 164, 88, 0.12);

  --display: 'Fraunces', Georgia, serif;
  --body: 'Newsreader', Georgia, serif;
  --label: 'Be Vietnam Pro', system-ui, sans-serif;
  --cjk: 'Noto Serif TC', serif;

  /* 1180px is the measure *inside* the gutters, not including them: the story
     column and the 330px panel are laid out in that width. */
  --shell: 1180px;
  --gutter-l: 140px;
  --gutter-r: 64px;
  --measure: calc(var(--shell) + var(--gutter-l) + var(--gutter-r));
  --header-h: 58px;

  /* Every archival image is treated the same way, so uneven sources still read
     as one collection. Modern colour photographs get the same warm duotone. */
  --plate: sepia(0.55) saturate(0.72) contrast(1.06) brightness(0.88) hue-rotate(-8deg);
}

.t-person { --accent: var(--amber); }
.t-event { --accent: var(--cinnabar); }
.t-place { --accent: var(--jade); }
.t-era { --accent: var(--bronze); }

*, *::before, *::after { box-sizing: border-box; }

html { scroll-behavior: smooth; scroll-padding-top: 96px; }

/* All motion on this site is state feedback — a dot lighting up, a card
   arriving, a row warming under the cursor. None of it carries meaning, so
   none of it is missed when the reader asks for stillness. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--body);
  font-size: 20px;
  line-height: 1.7;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

::selection { background: rgba(224, 164, 88, 0.28); }

a {
  color: var(--amber);
  text-underline-offset: 3px;
  text-decoration-color: rgba(224, 164, 88, 0.4);
}
a:hover { color: var(--ink); text-decoration-color: rgba(242, 233, 216, 0.5); }
/* One hover speed for every link on the page, so the chrome, the ledger and
   the apparatus all answer the cursor at the same pace. */
a, .thread, .index-card { transition: color 0.2s ease, text-decoration-color 0.2s ease, background 0.25s ease; }

.grain {
  position: fixed;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--panel);
  color: var(--ink);
  font-family: var(--label);
  font-size: 13px;
  padding: 12px 18px;
  border: 1px solid var(--hair);
}
.skip-link:focus { left: 12px; top: 12px; }

/* ------------------------------------------------------------ site header */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 70;
  display: flex;
  align-items: center;
  gap: 28px;
  height: var(--header-h);
  padding: 0 28px;
  background: linear-gradient(180deg, rgba(22, 16, 6, 0.6), transparent);
  border-bottom: 1px solid transparent;
  transition: background 0.4s ease, border-color 0.4s ease;
}
body.scrolled .site-header {
  background: rgba(18, 13, 5, 0.92);
  backdrop-filter: blur(10px);
  border-bottom-color: rgba(224, 164, 88, 0.18);
}

.wordmark {
  display: flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--ink);
  font-family: var(--label);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.35em;
  white-space: nowrap;
}
/* The lantern mark replaces the amber dot that stood here. Sized by height so the
   portrait mark keeps its ratio; at this size the ribs read as texture, not lines,
   so the glow is what carries it. */
.wordmark-mark {
  display: block;
  flex: none;
  width: auto;
  height: 18px;
  filter: drop-shadow(0 0 7px rgba(224, 164, 88, 0.5));
}

.crumbs {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.2em;
  min-width: 0;
}
.crumbs a { color: rgba(242, 233, 216, 0.55); text-decoration: none; }
.crumbs a:hover { color: var(--ink); }
.crumb-sep { color: rgba(224, 164, 88, 0.5); }
.crumb-current {
  color: rgba(242, 233, 216, 0.9);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.header-spacer { flex: 1; }

.langs {
  display: flex;
  align-items: center;
  gap: 12px;
  font-family: var(--label);
  font-size: 11px;
  letter-spacing: 0.18em;
}
.lang { color: rgba(242, 233, 216, 0.5); text-decoration: none; }
.lang:hover { color: var(--ink); }
.lang.on { color: var(--amber); font-weight: 600; }
.lang-sep { width: 1px; height: 12px; background: rgba(224, 164, 88, 0.3); }

/* -------------------------------------------------- mobile progress thread */

.progress-thread { display: none; }

/* ---------------------------------------------------------------- the rail */

.rail {
  position: fixed;
  left: 34px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 50;
}
.rail ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 36px; }
.rail-spine {
  position: absolute;
  left: 4px;
  top: -28px;
  bottom: -28px;
  width: 1px;
  background: linear-gradient(180deg, transparent, rgba(224, 164, 88, 0.22) 12%, rgba(224, 164, 88, 0.22) 88%, transparent);
}
.rail a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
}
.rail-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
  background: rgba(224, 164, 88, 0.4);
  transition: background 0.5s ease, box-shadow 0.5s ease;
}
.rail-year {
  font-family: var(--label);
  font-size: 11px;
  letter-spacing: 0.22em;
  color: var(--ink-faint);
  white-space: nowrap;
  transition: color 0.5s ease, text-shadow 0.5s ease;
}
.rail li.is-active .rail-dot { background: var(--amber); box-shadow: 0 0 14px 4px rgba(224, 164, 88, 0.5); }
.rail li.is-active .rail-year { color: var(--amber); text-shadow: 0 0 10px rgba(224, 164, 88, 0.5); }
.rail a:hover .rail-year { color: var(--ink); }

/* ------------------------------------------------------------------- hero */

.hero {
  position: relative;
  height: 94vh;
  min-height: 640px;
}
.hero-media { position: absolute; inset: 0; }
/* The plate is treated exactly like every other image on the page — the room
   is darkened around it instead, by the veil below. */
.hero-slot { width: 100%; height: 100%; object-fit: cover; }
.hero-veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 30%, transparent 40%, rgba(22, 16, 6, 0.55) 100%),
    linear-gradient(180deg, rgba(22, 16, 6, 0.45) 0%, rgba(22, 16, 6, 0.08) 34%, rgba(22, 16, 6, 0.2) 55%, rgba(22, 16, 6, 0.9) 82%, var(--bg) 100%);
}
.hero-frame { position: absolute; inset: 22px; pointer-events: none; border: 1px solid rgba(224, 164, 88, 0.16); }
.hero-calligraphy {
  position: absolute;
  right: 9%;
  top: 8%;
  writing-mode: vertical-rl;
  font-family: var(--cjk);
  font-size: 132px;
  line-height: 1.1;
  color: rgba(242, 233, 216, 0.055);
  user-select: none;
  pointer-events: none;
}
.hero-exhibit { position: absolute; top: 78px; right: 48px; text-align: right; pointer-events: none; }
.hero-exhibit-no { font-family: var(--label); font-size: 11.5px; letter-spacing: 0.28em; color: var(--amber); }
/* Set in the reading face, italic and small: this is a caption in a book, not
   a label stamped on the wall. The tracked capitals it used to wear made a
   line of housekeeping look like part of the title. */
.hero-exhibit-credit {
  font-family: var(--body);
  font-style: italic;
  font-size: 13px;
  line-height: 1.5;
  letter-spacing: 0;
  /* On the wide layout this line lies on the plate itself, and a plate can be
     bright: the small italic needs both a lit colour and a shade under it to
     stay readable over a white sky. */
  color: var(--ink-mute);
  text-shadow: 0 1px 8px rgba(18, 13, 5, 0.9);
  margin-top: 6px;
  text-decoration-color: rgba(224, 164, 88, 0.22);
}
a.hero-exhibit-credit:hover { color: var(--ink); text-decoration-color: var(--amber); }
/* The block above is pointer-transparent so the plate stays clean under the
   cursor, which also meant the attribution link could never be clicked and
   the licence led nowhere. The link inside takes its own clicks back. */
.hero-exhibit a { pointer-events: auto; }
.hero-caption { position: absolute; inset: auto 0 7vh 0; pointer-events: none; }
.hero-inner { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter-r) 0 var(--gutter-l); }

.kicker {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--label);
  font-size: 12.5px;
  letter-spacing: 0.32em;
  color: color-mix(in srgb, var(--accent) 72%, var(--ink));
}
.kicker-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 80%, transparent);
}

.hero h1 {
  font-family: var(--display);
  font-weight: 420;
  font-size: clamp(52px, 7.4vw, 104px);
  line-height: 0.98;
  letter-spacing: -0.01em;
  color: #fff;
  margin: 22px 0 0;
  text-wrap: balance;
  /* The design breaks its title by hand; a measure does the same job for a
     title of any length, and keeps the hero stacked rather than banner-wide. */
  max-width: 15ch;
}
.hero-alt {
  font-family: var(--body);
  font-style: italic;
  font-size: 23px;
  color: rgba(242, 233, 216, 0.85);
  margin-top: 22px;
}
.hero-lede { font-size: 20px; line-height: 1.6; color: rgba(242, 233, 216, 0.68); max-width: 58ch; margin: 14px 0 0; }

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

.layout {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 24px var(--gutter-r) 0 var(--gutter-l);
  display: grid;
  grid-template-columns: minmax(0, 1fr) 330px;
  /* No row gap: the invitation below the story sets its own distance, and a
     second gap on top of that opens a hole the width of the page. */
  column-gap: 88px;
  row-gap: 0;
  align-items: start;
}
.story { max-width: 68ch; }

/* --------------------------------------------------------------- chapters */

.chapter { position: relative; padding-top: 104px; }
.ghost-year {
  position: absolute;
  top: 36px;
  right: -36px;
  font-family: var(--display);
  font-weight: 340;
  font-size: 148px;
  line-height: 1;
  color: rgba(242, 233, 216, 0.055);
  pointer-events: none;
  user-select: none;
  white-space: nowrap;
}
.chapter-head { position: relative; display: flex; align-items: center; gap: 16px; }
.eyebrow {
  font-family: var(--label);
  font-size: 12px;
  letter-spacing: 0.28em;
  color: var(--amber);
  white-space: nowrap;
}
.rule { flex: 1; height: 1px; background: var(--hair); }
.chapter-date {
  font-family: var(--label);
  font-size: 12px;
  letter-spacing: 0.22em;
  color: var(--ink-mute);
  white-space: nowrap;
}
.chapter h2 {
  position: relative;
  font-family: var(--display);
  font-weight: 460;
  font-size: 42px;
  line-height: 1.08;
  color: #fff;
  margin: 26px 0 0;
  text-wrap: balance;
}
/* Justified like a printed page, with the last line left as it falls — the
   block reads as a set column rather than a ragged web paragraph. Vietnamese
   does not hyphenate, so the words themselves have to give: `hyphens: auto`
   for the browsers that can, and a text column wide enough that the spacing
   between words stays even at the narrowest screen. */
.chapter p {
  position: relative;
  margin: 26px 0 0;
  color: var(--ink-soft);
  text-align: justify;
  text-align-last: left;
  hyphens: auto;
}

/* --------------------------------------------------- inline entity links */

.elink {
  position: relative;
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  text-decoration-color: color-mix(in srgb, var(--accent) 65%, transparent);
}
.elink:hover, .elink:focus-visible { color: var(--accent); }

.ecard {
  position: absolute;
  left: -8px;
  top: calc(100% + 14px);
  z-index: 30;
  width: 296px;
  background: var(--panel);
  border: 1px solid rgba(224, 164, 88, 0.32);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  padding: 16px;
  font-family: var(--body);
  font-style: normal;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-soft);
  /* `display: none`, not `visibility: hidden`: a hidden card still holds its
     place in the layout, and its place is past the right edge of a phone. The
     page then measures wider than the screen, Chrome on Android widens the
     layout viewport to cover it, and the fixed header — language switcher and
     all — is carried off the side of the screen the reader can never reach.
     The card fades in the same as before; `allow-discrete` lets a box that
     starts at `display: none` still animate. */
  display: none;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.18s ease, transform 0.18s ease, display 0.18s allow-discrete;
  pointer-events: none;
}
.elink:hover .ecard, .elink:focus-visible .ecard, .elink.open .ecard {
  display: block;
  opacity: 1;
  transform: none;
}
@starting-style {
  .elink:hover .ecard, .elink:focus-visible .ecard, .elink.open .ecard {
    opacity: 0;
    transform: translateY(-6px);
  }
}
/* Where a card sits sideways is now decided in one place — site.js measures
   the link against the screen and sets `left` — so the old `.flip` class,
   which only ever swapped one edge for the other, is gone. */

/* The plate sits beside the name, cropped to a card-index portrait. */
.ecard-head { display: flex; gap: 14px; }
.ecard-heading { display: block; min-width: 0; }
.ecard-thumb {
  display: block;
  width: 58px;
  height: 72px;
  flex-shrink: 0;
  overflow: hidden;
  background: #241a0c;
}
.ecard-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; filter: var(--plate); }
.ecard-kicker {
  display: block;
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.24em;
  color: var(--accent);
}
.ecard-title {
  display: block;
  font-family: var(--display);
  font-size: 20px;
  font-weight: 480;
  color: #fff;
  margin-top: 5px;
  line-height: 1.15;
}
.ecard-dates {
  display: block;
  font-family: var(--label);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--ink-mute);
  margin-top: 5px;
}
.ecard-lede { display: block; margin-top: 12px; font-size: 15.5px; color: rgba(242, 233, 216, 0.82); }

/* ------------------------------------------------- inline glossary terms */
/* Historical exonyms keep the source's word and carry their own ⓘ. Same
   hover/focus mechanism as .ecard: the text is in the markup, CSS only reveals. */

.gloss { position: relative; white-space: nowrap; }
.gloss-word { white-space: normal; }
.gloss-mark {
  appearance: none;
  margin: 0 1px 0 3px;
  padding: 0;
  width: 15px;
  height: 15px;
  vertical-align: 4px;
  border: 1px solid color-mix(in srgb, var(--accent) 72%, transparent);
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: color-mix(in srgb, var(--accent) 92%, #fff);
  font-family: var(--label);
  font-size: 9.5px;
  font-weight: 600;
  line-height: 13px;
  text-align: center;
  cursor: help;
}
.gloss-mark:hover, .gloss-mark:focus-visible {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: #fff;
}
.gcard {
  position: absolute;
  left: -8px;
  top: calc(100% + 12px);
  z-index: 30;
  width: 320px;
  white-space: normal;
  background: var(--panel);
  border: 1px solid rgba(224, 164, 88, 0.32);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  padding: 16px;
  font-family: var(--body);
  font-style: normal;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink-soft);
  /* Same reason as .ecard: a card waiting off the right edge widens the page. */
  display: none;
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 0.18s ease, transform 0.18s ease, display 0.18s allow-discrete;
  pointer-events: none;
}
.gloss:hover .gcard, .gloss:focus-within .gcard {
  display: block;
  opacity: 1;
  transform: none;
  /* The point of a quoted original is that a reader can take it away, so the
     open card has to be selectable. Safe because the card sits inside .gloss:
     the pointer moving into it keeps the card open. */
  pointer-events: auto;
}
@starting-style {
  .gloss:hover .gcard, .gloss:focus-within .gcard { opacity: 0; transform: translateY(-6px); }
}
.gcard-kicker {
  display: block;
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.2em;
  color: var(--accent);
}
.gcard-term {
  display: block;
  font-family: var(--display);
  font-size: 19px;
  font-weight: 480;
  color: #fff;
  margin-top: 5px;
  line-height: 1.15;
}
/* Every .chapter p is its own stacking context, so a card opened in one
   paragraph would be painted under the next paragraph's text. Raise the
   paragraph that currently owns an open card — applies to entity cards too. */
.chapter p:has(.gloss:hover), .chapter p:has(.gloss:focus-within),
.chapter p:has(.elink:hover), .chapter p:has(.elink:focus-visible) { z-index: 40; }

.gcard-goc {
  display: block;
  margin-top: 6px;
  font-family: var(--display);
  font-size: 18px;
  line-height: 1.35;
  color: #fff;
  user-select: text;
}
.gcard-note { display: block; margin-top: 10px; }
.gcard-source {
  display: block;
  margin-top: 10px;
  font-family: var(--label);
  font-size: 10px;
  letter-spacing: 0.14em;
  color: var(--ink-mute);
}

/* -------------------------------------------------------------- pullquote */

.pullquote { position: relative; margin: 60px 0; z-index: 20; }
.pullquote-ghost {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--cjk);
  font-size: 104px;
  letter-spacing: 0.1em;
  color: rgba(242, 233, 216, 0.05);
  white-space: nowrap;
  user-select: none;
  pointer-events: none;
}
.pullquote .hairline { position: relative; display: block; height: 1px; width: 64px; margin: 0 auto; background: rgba(224, 164, 88, 0.25); }
.pullquote .hairline:last-child { margin-top: 26px; }
.pullquote blockquote {
  position: relative;
  margin: 28px auto;
  max-width: 30ch;
  text-align: center;
  font-family: var(--display);
  font-style: italic;
  font-weight: 420;
  font-size: 30px;
  line-height: 1.3;
  color: var(--ink);
  text-wrap: balance;
}
.pullquote-attrib {
  position: relative;
  text-align: center;
  font-family: var(--label);
  font-size: 11px;
  letter-spacing: 0.28em;
  color: rgba(224, 164, 88, 0.9);
}

/* --------------------------------------------------------------- exhibits */

.exhibit {
  position: relative;
  margin: 44px 0 0;
  border: 1px solid var(--hair);
  background: var(--deep);
  padding: 16px 16px 0;
}
.exhibit-frame { position: relative; aspect-ratio: 3 / 2; }
/* Portrait plates are held to a plate width so they don't swallow the column. */
.exhibit.portrait .exhibit-frame { max-width: 440px; margin: 0 auto; }
.exhibit-wanted { border-style: dashed; border-color: rgba(224, 164, 88, 0.28); }
.exhibit-frame .slot { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Where the frame is already the picture's own shape, `contain` costs nothing
   and covers the two cases where the shape has to give: the floor a very wide
   plate hits on a phone, and a rounding difference at the last pixel. Neither
   crops — the picture is matted against the plate ground instead. */
.exhibit.has-plate .exhibit-frame .slot { object-fit: contain; }
.exhibit-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    /* The band the credit is written in. Plates are not all dark — a weather
       chart is white paper — and a caption in the corner of white paper needs
       ground under it or it is not there at all. */
    linear-gradient(180deg, rgba(18, 13, 5, 0.62) 0, rgba(18, 13, 5, 0.24) 44px, transparent 72px),
    radial-gradient(115% 100% at 50% 45%, transparent 55%, rgba(18, 13, 5, 0.45) 100%);
}
/* Museum label, in the order a label is read: who made the plate is written in
   the corner of the plate itself, and the line under it says what you are
   looking at. Set in the reading face, italic — a caption in a book. */
.exhibit figcaption {
  padding: 14px 2px 16px;
}
.exhibit-no { font-family: var(--label); font-size: 11px; letter-spacing: 0.24em; color: var(--amber); white-space: nowrap; }
/* Centred under every plate. A portrait plate is held to 440px in the middle
   of the column, so a caption ranged left sat under nothing — an orphan line
   away at the far edge of the picture it belongs to. */
.exhibit-caption {
  display: block;
  text-align: center;
  font-family: var(--body);
  font-style: italic;
  font-size: 14px;
  line-height: 1.55;
  color: rgba(242, 233, 216, 0.7);
}
/* The credit carries the licence, so it is the one line that may not be lost.
   In the corner of a photograph it needs its own shade to stay readable over
   a bright sky, and a width bound so a long attribution wraps inside the plate
   instead of running off the side of a phone. */
.exhibit-credit {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 2;
  max-width: min(72%, 420px);
  text-align: right;
  font-family: var(--body);
  font-style: italic;
  font-size: 12px;
  line-height: 1.45;
  color: rgba(242, 233, 216, 0.72);
  text-shadow: 0 1px 8px rgba(18, 13, 5, 0.95), 0 0 3px rgba(18, 13, 5, 0.8);
}
a.exhibit-credit { text-decoration: none; }
a.exhibit-credit:hover { color: var(--amber); text-decoration: underline; }
a.hero-exhibit-credit { display: block; text-decoration: none; }
a.hero-exhibit-credit:hover { color: var(--amber); }

/* The pair shares one credit, so the pair is what the credit sits on — and it
   needs the same band of ground the single plates get from their vignette. */
.compare { position: relative; display: flex; flex-wrap: wrap; gap: 14px; }
.compare::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 72px;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(18, 13, 5, 0.62) 0, rgba(18, 13, 5, 0.24) 44px, transparent 100%);
}
/* A comparison only works if both halves are the same height, so here the box
   stays fixed and the pictures are matted into it rather than cropped to it —
   two plates of different shapes, both whole, hung at the same height. */
.compare-half { position: relative; flex: 1; min-width: 220px; aspect-ratio: 4 / 3; }
.compare-half .slot { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.compare-badge {
  position: absolute;
  right: 10px;
  bottom: 10px;
  pointer-events: none;
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.22em;
  color: var(--ink);
  background: rgba(18, 13, 5, 0.78);
  border: 1px solid rgba(224, 164, 88, 0.25);
  padding: 4px 9px;
  white-space: nowrap;
}

.slot { filter: var(--plate); }

.slot-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  text-align: center;
  padding: 24px;
  filter: none;
  background:
    repeating-linear-gradient(135deg, rgba(224, 164, 88, 0.035) 0 12px, transparent 12px 24px),
    var(--panel);
  border: 1px dashed rgba(224, 164, 88, 0.22);
  color: rgba(242, 233, 216, 0.4);
}
.slot-mark { font-size: 20px; color: rgba(224, 164, 88, 0.5); }
.slot-text {
  font-family: var(--label);
  font-size: 11px;
  letter-spacing: 0.14em;
  line-height: 1.7;
  max-width: 34ch;
  text-transform: uppercase;
}

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

/* The panel follows the reader, so the chart stays lit beside the chapter it
   belongs to. A pinned column taller than the screen would strand its own
   bottom — the dossier could never be scrolled to — so it gets its own scroll
   only in that case. On a tall screen nothing here does anything. */
.panel {
  position: sticky;
  top: 78px;
  padding-top: 104px;
  max-height: calc(100vh - 78px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(224, 164, 88, 0.35) transparent;
}
.panel-head { display: flex; align-items: center; gap: 14px; }
.panel-title { font-family: var(--label); font-size: 12px; letter-spacing: 0.32em; color: var(--amber); white-space: nowrap; }
.panel-meta { font-family: var(--label); font-size: 10.5px; letter-spacing: 0.2em; color: var(--ink-mute); white-space: nowrap; }
.panel-toggle {
  background: none;
  border: none;
  color: var(--amber);
  font-family: var(--label);
  font-size: 16px;
  cursor: pointer;
  padding: 0 4px;
}

.constellation { width: 100%; margin-top: 18px; display: block; overflow: visible; }
/* Every label knocks a hole in whatever it crosses. Without this the edge
   lines run straight through the lettering, which is most of what makes a
   crowded star chart unreadable. */
.constellation text {
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: 5px;
  stroke-linejoin: round;
}
.constellation .edge { stroke: rgba(224, 164, 88, 0.3); stroke-width: 1; transition: stroke 0.5s ease; }
.constellation .edge-label {
  font-family: var(--label);
  font-size: 9.5px;
  letter-spacing: 1.5px;
  fill: rgba(224, 164, 88, 0.75);
  transition: fill 0.5s ease;
}
.constellation .node-halo { opacity: 0.14; transition: opacity 0.5s ease; }
.constellation .node-label { font-family: var(--label); font-size: 10px; letter-spacing: 1.2px; fill: rgba(242, 233, 216, 0.95); }
.constellation .node-label.self { letter-spacing: 1.4px; fill: var(--ink); }
.constellation .node-note { font-family: var(--label); font-size: 9px; letter-spacing: 1px; fill: rgba(242, 233, 216, 0.7); }
.constellation .node { transition: opacity 0.5s ease; }
.constellation .node a { cursor: pointer; }
.constellation .node-dot.self { stroke: rgba(242, 233, 216, 0.6); stroke-width: 1; }

/* With JS, the chapter you are reading is the one lit. Without it, every node
   stays legible — the chart is still a map, just not a cursor. */
.has-js .constellation .node { opacity: 0.5; }
.has-js .constellation .edge { stroke: rgba(224, 164, 88, 0.14); }
.has-js .constellation .edge-label { fill: rgba(224, 164, 88, 0.4); }
.has-js .constellation .node.lit { opacity: 1; }
.has-js .constellation .node.lit .node-halo { opacity: 0.3; }
.has-js .constellation .node.lit .node-note { fill: rgba(224, 164, 88, 0.95); }
.has-js .constellation .edge.lit { stroke: rgba(224, 164, 88, 0.65); }
.has-js .constellation .edge-label.lit { fill: rgba(224, 164, 88, 0.98); }
.has-js .constellation .node-self { opacity: 1; }

.constellation-caption {
  font-family: var(--label);
  font-size: 11px;
  letter-spacing: 0.22em;
  color: var(--ink-mute);
  margin-top: 14px;
}
.constellation-caption .glow-value { color: rgba(242, 233, 216, 0.85); }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--hair-soft);
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.18em;
  color: rgba(242, 233, 216, 0.7);
}
.legend-dot { width: 6px; height: 6px; border-radius: 50%; }

/* ---------------------------------------------------------------- dossier */

.dossier {
  margin-top: 30px;
  border: 1px solid rgba(224, 164, 88, 0.22);
  background: rgba(224, 164, 88, 0.03);
  padding: 18px 18px 8px;
}
/* The plan is a mounted drawing, not a picture: it keeps its own frame, its
   ward line and its scale bar, the way a survey sheet would. */
.dossier-plan {
  margin-top: 16px;
  border: 1px solid rgba(224, 164, 88, 0.16);
  background: rgba(10, 7, 3, 0.4);
}
.dossier-map { width: 100%; display: block; padding: 8px 8px 0; }
.map-label { font-family: var(--label); font-size: 8.5px; letter-spacing: 1.5px; fill: rgba(156, 138, 114, 0.8); }
.map-label.small { font-size: 7.5px; letter-spacing: 1.3px; fill: rgba(156, 138, 114, 0.45); }
.map-label.river { fill: rgba(127, 169, 140, 0.7); }
.map-label.north { font-size: 8px; letter-spacing: 0; fill: rgba(156, 138, 114, 0.7); }
.map-pin { font-family: var(--label); font-size: 9.5px; letter-spacing: 1.6px; fill: var(--ink); }
.dossier-plan-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 6px;
  padding: 8px 10px 9px;
  border-top: 1px solid var(--hair-soft);
  font-family: var(--label);
  font-size: 9px;
  letter-spacing: 0.16em;
  color: rgba(156, 138, 114, 0.85);
}
.plan-scale { display: flex; align-items: center; gap: 6px; letter-spacing: 0.14em; }
.plan-scale-bar {
  width: 32px;
  height: 3px;
  border-left: 1px solid rgba(156, 138, 114, 0.6);
  border-right: 1px solid rgba(156, 138, 114, 0.6);
  background: linear-gradient(rgba(156, 138, 114, 0.6), rgba(156, 138, 114, 0.6)) center / 100% 1px no-repeat;
}
.dossier-row { padding: 12px 0; border-top: 1px solid var(--hair-soft); }
.dossier-plan + .dossier-row, .panel-head + .dossier-row { margin-top: 14px; }
.dossier-label { font-family: var(--label); font-size: 10.5px; letter-spacing: 0.26em; color: rgba(156, 138, 114, 0.95); }
.dossier-value { font-size: 15.5px; line-height: 1.5; color: rgba(242, 233, 216, 0.92); margin-top: 5px; }

/* ------------------------------------------------------------------- tail */

.tail { margin-top: 130px; }
.tail-block + .tail-block { margin-top: 56px; }
.tail-head { display: flex; align-items: center; gap: 14px; }

/* A ledger, not a card wall: the predicate keeps its own column so the reader
   can run down the relations — built by, shrine to, named by — without having
   to read the names first. */
.threads { display: flex; flex-direction: column; margin-top: 14px; }
.thread {
  display: grid;
  grid-template-columns: 168px 1fr;
  gap: 20px;
  align-items: baseline;
  padding: 15px 10px;
  border-bottom: 1px solid rgba(224, 164, 88, 0.1);
  text-decoration: none;
  color: inherit;
}
.thread:hover, .thread:focus-visible { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.thread-predicate {
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.22em;
  line-height: 1.7;
  color: var(--accent);
}
.thread-body { display: flex; flex-direction: column; gap: 3px; }
.thread-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px; }
.thread-title {
  font-family: var(--display);
  font-size: 18.5px;
  font-weight: 420;
  line-height: 1.3;
  color: var(--ink);
}
.thread-kicker { font-family: var(--label); font-size: 9.5px; letter-spacing: 0.2em; color: rgba(242, 233, 216, 0.5); }
.thread-lede { font-size: 14px; line-height: 1.55; color: rgba(242, 233, 216, 0.62); }

/* ------------------------------------------------------------- apparatus
   Sources and licences: on the page, checkable, and deliberately quiet. */

.refs { margin-top: 90px; padding-top: 24px; border-top: 1px solid rgba(224, 164, 88, 0.15); }
.ref-group + .ref-group { margin-top: 22px; }
.ref-head { font-family: var(--label); font-size: 10px; letter-spacing: 0.28em; color: rgba(156, 138, 114, 0.9); }
.ref-list {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 6px 36px;
}
/* One line each: the title is the handle, the link carries the rest. */
.ref-list li {
  font-size: 12px;
  line-height: 1.5;
  color: rgba(242, 233, 216, 0.42);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ref-list a { color: rgba(242, 233, 216, 0.55); text-decoration-color: rgba(224, 164, 88, 0.25); }
.ref-list a:hover { color: var(--ink); text-decoration-color: var(--amber); }
.ref-note { color: rgba(156, 138, 114, 0.75); }

/* Last block on the page at every width. On the wide layout it stays under the
   story, in the story's column and at the story's measure; when the grid
   collapses it simply falls to the bottom, after the aside. */
.contribute {
  grid-column: 1;
  max-width: 68ch;
  margin-top: 110px;
  border: 1px solid rgba(224, 164, 88, 0.22);
  background: rgba(224, 164, 88, 0.04);
  padding: 30px 32px 32px;
}
/* The invitation is read the way the entry above it is read, so its paragraphs
   are set exactly like `.chapter p` — same size, colour, measure and justified
   block. Type it smaller and it reads as a footer notice instead of a note the
   archive is actually writing to you. */
.contribute-body {
  margin: 26px 0 0;
  color: var(--ink-soft);
  text-align: justify;
  text-align-last: left;
  hyphens: auto;
}
/* The address reads as part of the sentence, so it takes the archive's amber
   with a quiet rule under it rather than a browser-blue underline. */
.contribute-mail {
  color: var(--amber);
  text-decoration: underline;
  text-decoration-color: rgba(224, 164, 88, 0.45);
  text-underline-offset: 4px;
}
.contribute-mail:hover { color: var(--ink); text-decoration-color: rgba(242, 233, 216, 0.6); }
.contribute-cta {
  display: inline-block;
  margin-top: 20px;
  font-family: var(--label);
  font-size: 12.5px;
  letter-spacing: 0.24em;
  color: var(--amber);
  /* The rule under the label is drawn here, so the anchor's own underline
     would print a second line 3px below it. */
  text-decoration: none;
  border-bottom: 1px solid rgba(224, 164, 88, 0.5);
  padding-bottom: 4px;
}
.contribute-cta:hover { color: var(--ink); border-bottom-color: rgba(242, 233, 216, 0.7); }

/* ----------------------------------------------------------------- footer */

.site-footer { padding: 40px var(--gutter-r) 72px var(--gutter-l); margin-top: 120px; }
.footer-inner { max-width: var(--shell); margin: 0 auto; border-top: 1px solid var(--hair-soft); padding-top: 36px; text-align: center; }
.footer-mark { font-family: var(--label); font-size: 11px; letter-spacing: 0.4em; color: var(--amber); }
.footer-tagline { font-family: var(--label); font-size: 10.5px; letter-spacing: 0.24em; color: rgba(242, 233, 216, 0.55); margin-top: 10px; }
.footer-nav { display: flex; justify-content: center; flex-wrap: wrap; gap: 22px; margin-top: 22px; }
.footer-nav a {
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.24em;
  color: rgba(242, 233, 216, 0.5);
  text-decoration: none;
}
.footer-nav a:hover { color: var(--amber); }

/* ------------------------------------------------------------ index pages */

.index-hero {
  position: relative;
  max-width: var(--measure);
  margin: 0 auto;
  padding: 190px var(--gutter-r) 40px var(--gutter-l);
}
.index-hero.small { padding-bottom: 10px; }
.index-calligraphy {
  position: absolute;
  right: 6%;
  top: 90px;
  font-family: var(--cjk);
  font-size: 190px;
  line-height: 1;
  color: rgba(242, 233, 216, 0.05);
  user-select: none;
  pointer-events: none;
}
.index-hero h1 {
  font-family: var(--display);
  font-weight: 420;
  font-size: clamp(42px, 6vw, 84px);
  line-height: 1;
  color: #fff;
  margin: 0;
  text-wrap: balance;
}
.index-lede-top { font-size: 21px; line-height: 1.6; color: rgba(242, 233, 216, 0.7); max-width: 56ch; margin: 26px 0 0; }
.index-body { max-width: var(--measure); margin: 0 auto; padding: 0 var(--gutter-r) 0 var(--gutter-l); }

/* ------------------------------------------------------------ trang riêng tư
   Chữ nhỏ hơn thân bài và không căn đều: đây là văn bản để tra, không phải văn
   kể để đọc liền mạch. Khối chữ lấp hết bề ngang của khung như mọi trang khác
   — nhưng lấp bằng HAI CỘT, vì một dòng dài 1180px thì mắt lạc dòng. Mỗi mục
   là một khối không bị cắt đôi, nên thứ tự đọc vẫn là trên xuống rồi sang phải. */
.legal { padding-bottom: 40px; font-size: 17px; line-height: 1.65; }
.legal-updated {
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.24em;
  color: var(--ink-mute);
  margin: 20px 0 0;
}
.legal-lede { max-width: 62ch; margin: 34px 0 0; color: var(--ink-soft); font-size: 19px; line-height: 1.6; }
.legal-cols { margin-top: 44px; }
/* Lề nằm ở đáy khối, không ở đỉnh: khối mở đầu cột thứ hai phải bắt đầu ngang
   hàng với khối mở đầu cột thứ nhất, mà lề trên thì cột nào cũng nuốt mất. */
.legal-block { break-inside: avoid; margin-bottom: 40px; }
.legal h2 {
  font-family: var(--display);
  font-weight: 460;
  font-size: 24px;
  line-height: 1.15;
  color: #fff;
  margin: 0;
}
.legal p { margin: 14px 0 0; color: rgba(242, 233, 216, 0.78); }
.legal a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: rgba(224, 164, 88, 0.45);
  text-underline-offset: 4px;
  overflow-wrap: anywhere;
}
.legal a:hover { text-decoration-color: var(--amber); }
/* Chỉ chia cột khi khung đã đủ rộng để mỗi cột còn khoảng 55 chữ một dòng.
   Hẹp hơn ngưỡng này thì một cột chạy suốt vẫn là bố cục đúng. */
@media (min-width: 1240px) {
  .legal-cols { columns: 2; column-gap: 72px; }
}
.index-group { margin-top: 76px; }
.index-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1px; margin-top: 22px; }
.index-card { display: block; outline: 1px solid var(--hair-soft); background: var(--bg); padding: 24px 22px 28px; text-decoration: none; color: inherit; }
.index-card:hover { background: var(--panel); }
.index-kicker { display: block; font-family: var(--label); font-size: 10px; letter-spacing: 0.24em; color: var(--accent); }
.index-title {
  display: block;
  font-family: var(--display);
  font-size: 26px;
  font-weight: 460;
  line-height: 1.12;
  color: #fff;
  margin-top: 10px;
  text-wrap: balance;
}
.index-dates { display: block; font-family: var(--label); font-size: 10.5px; letter-spacing: 0.16em; color: var(--ink-faint); margin-top: 8px; }
.index-lede { display: block; font-size: 16px; line-height: 1.55; color: var(--ink-mute); margin-top: 12px; }

/* --------------------------------------------------------------- home page
   Một màn hình, không cuộn trên desktop: lời mở bên trái, chòm sao bên phải,
   không có gì bên dưới. Mọi tên mục là chữ thật trong HTML — chòm sao chỉ là
   cách xếp chỗ cho mười cái link, tắt CSS đi vẫn còn nguyên mười cái link. */

body.home {
  background: radial-gradient(120% 90% at 68% 8%, #1e1509 0%, #161006 48%, #0e0902 100%);
}
/* Chấm sao phải nổi trên nền tối hơn chữ trong bài, nên hai loại dùng sắc sáng
   hơn token của kho: thời đại lấy màu ngà, sự kiện lấy chu sa non. Đây là số đo
   của bản thiết kế trang chủ, không phải một lần "làm mới cho đẹp". */
body.home .t-era { --accent: #f2e9d8; }
body.home .t-event { --accent: #d96a45; }

.home-shell {
  position: relative;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding-top: var(--header-h);
  overflow: hidden;
}
.home-calligraphy {
  position: absolute;
  right: 3.5%;
  top: 12%;
  writing-mode: vertical-rl;
  font-family: var(--cjk);
  font-size: 140px;
  line-height: 1.15;
  color: rgba(242, 233, 216, 0.045);
  pointer-events: none;
  user-select: none;
}
.home-grid {
  flex: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0 40px;
  width: 100%;
  max-width: 1480px;
  margin: 0 auto;
  padding: 0 48px;
}
.home-intro {
  position: relative;
  z-index: 2;
  flex: 0 1 380px;
  min-width: 290px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 24px 0;
}
.home-kicker {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-family: var(--label);
  font-size: 11.5px;
  letter-spacing: 0.32em;
  color: #8fb89c;
}
.home-kicker-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--jade);
  box-shadow: 0 0 8px rgba(127, 169, 140, 0.8);
  flex-shrink: 0;
}
.home-title {
  font-family: var(--display);
  font-weight: 420;
  font-size: clamp(40px, 4.8vw, 72px);
  line-height: 0.98;
  letter-spacing: -0.01em;
  color: #fff;
  margin: 20px 0 0;
  /* KHÔNG dùng `text-wrap: balance` ở đây. Nó chia tên thành hai dòng đều nhau
     nên ở cột hẹp ra "The Hội / An Chronicles" — cắt "Hội An" làm đôi, mà đó
     là một tên riêng. Ngắt dòng thường ăn no dòng trên trước, cho "The Hội An /
     Chronicles" — đúng ý ở mọi bề ngang, vì cỡ chữ co theo `vw`. */
  text-wrap: wrap;
}
/* Hai khối chữ mở đầu canh đều hai bên, cho mép phải thẳng với mép cột chòm
   sao. `text-wrap: pretty` phải tắt: nó và `justify` tranh nhau chia chữ, và
   khi tranh thì dòng cuối bị kéo giãn. */
.home-lede {
  font-family: var(--body);
  font-style: italic;
  font-size: 20.5px;
  line-height: 1.4;
  color: var(--ink-soft);
  margin: 18px 0 0;
  text-align: justify;
  text-wrap: wrap;
}
.home-start {
  font-size: 16.5px;
  line-height: 1.6;
  color: rgba(242, 233, 216, 0.62);
  max-width: 42ch;
  margin: 14px 0 0;
  text-align: justify;
  text-wrap: wrap;
}
.home-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 20px;
  margin-top: 30px;
  font-family: var(--label);
  font-size: 10.5px;
  letter-spacing: 0.22em;
}
.home-legend a {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--ink-mute);
  text-decoration: none;
}
.home-legend a:hover, .home-legend a:focus-visible { color: var(--ink); }
.legend-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.home-map {
  position: relative;
  z-index: 2;
  flex: 1 1 560px;
  min-height: 460px;
  margin: 36px 0 56px;
}
/* Hai cột cần 48 + 380 + 40 + 560 + 48 = 1076px mới đứng cạnh nhau được. Hẹp
   hơn thì cột chòm sao rớt xuống dòng dưới và cột chữ ở lại đúng 380px, để hở
   bên phải một mảng trống bằng nửa màn hình. Bó hai cột lại cho chúng đứng
   cạnh nhau đã thử và tệ hơn: cột sao còn ~420px thì nhãn xuống bốn năm dòng.
   Nên giữ nguyên chuyện rớt dòng, chỉ nới cột chữ rộng thêm nửa phần (380 →
   570) để nó ăn hết chỗ trống ấy. Bỏ trần 42ch trong dải này, không thì chữ
   vẫn dừng ở chỗ cũ và cột rộng ra mà nhìn không khác gì. */
@media (min-width: 720px) and (max-width: 1100px) {
  .home-intro { flex: 0 1 570px; }
  .home-start { max-width: none; }
  /* Chữ 會安 vốn dán vào mép phải trang, hợp lúc chòm sao còn nằm bên cạnh.
     Khi chòm sao rớt xuống dưới thì cả dải bên phải cột chữ trống ra, chữ dán
     mép nhìn như bị đẩy văng ra rìa. Đưa nó về giữa dải trống ấy: dải chạy từ
     `48 + 570 + 40 = 658px` tới `100% − 48px`, nên tâm dải nằm ở
     `50% + 305px`. */
  .home-calligraphy {
    right: auto;
    left: calc(50% + 305px);
    transform: translateX(-50%);
  }
}

.web { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.web line {
  stroke: var(--amber);
  stroke-opacity: 0.12;
  stroke-width: 1;
  transition: stroke-opacity 0.3s ease, stroke 0.3s ease;
}
/* Sợi nối vào nút trung tâm đậm hơn một chút: cái khung xương của kho hiện ra
   ngay ở trạng thái nghỉ, phần còn lại của mạng để mờ như nền sao. */
.web line.spine { stroke-opacity: 0.2; }

/* Chỗ neo là TÂM CHẤM, không phải tâm cả cột. `translate(-50%,-50%)` canh giữa
   cả khối chấm-cộng-nhãn, nên chấm bị đẩy lên trên và mối sợi hội tụ rơi vào
   giữa dòng nhãn. Chấm nằm đầu cột flex nên tâm nó cách mép trên đúng một bán
   kính — kéo ngược lên chừng ấy là sợi và chấm gặp nhau đúng một điểm. */
.star {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, calc(var(--r) * -1px));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  z-index: 2;
  opacity: 1;
  transition: opacity 0.3s ease;
}
.star-dot {
  width: calc(var(--r) * 2px);
  height: calc(var(--r) * 2px);
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 calc(var(--r) * 3px) var(--accent);
  animation: lantern var(--beat) ease-in-out var(--wait) infinite;
  transition: box-shadow 0.3s ease;
  flex-shrink: 0;
}
.star:hover .star-dot, .star:focus-visible .star-dot {
  box-shadow: 0 0 calc(var(--r) * 5px) var(--accent);
}
@keyframes lantern {
  0%, 100% { opacity: 0.65; }
  50% { opacity: 1; }
}
.star-label { display: flex; flex-direction: column; align-items: center; gap: 4px; text-align: center; }
.star-kicker {
  font-family: var(--label);
  font-size: 10px;
  letter-spacing: 0.24em;
  color: var(--accent);
  white-space: nowrap;
}
.star-name {
  font-family: var(--display);
  font-size: 16.5px;
  font-weight: 460;
  line-height: 1.2;
  color: #fff;
  max-width: 170px;
  text-shadow: 0 2px 12px rgba(14, 9, 2, 0.9);
}
.star:hover .star-name, .star:focus-visible .star-name { color: var(--amber); }

/* ------------------------------------------------------------ narrow: aside
   drops below the story, gutters tighten, rail keeps its lane. */

@media (max-width: 1199px) {
  /* Matched gutters. The left one is wide because the rail lives in it; the
     right one used to be 40px, which left the whole page leaning left — the
     story, the constellation and the dossier all stacked against one margin
     with a hand's width of nothing beside them. */
  :root { --gutter-l: 96px; --gutter-r: 96px; }
  /* One column now, so everything in it shares one measure and one centre
     line: story, aside and invitation are the same column, in reading order. */
  .layout { display: block; }
  .story, .panel, .contribute { max-width: 68ch; margin-left: auto; margin-right: auto; }
  /* Nothing is pinned here, so the panel has no reason to hold its own
     scrollbar — it just runs on down the page. */
  .panel { position: static; padding-top: 0; margin-top: 80px; max-height: none; overflow: visible; }
  /* The chart is drawn for a 330px canvas; stretched across the full column it
     is just a bigger drawing with the same amount in it. Keep it its own size
     and put it in the middle of the column. */
  .constellation { max-width: 330px; margin-inline: auto; }
  /* Which chapter the chart is lit for only earns its line where the chart
     sits beside the story and the two can be read against each other. Below
     that the chart is a block in the flow, and the line is a caption for
     something the reader has already scrolled past. */
  .constellation-caption { display: none; }
  .ghost-year { right: 0; }
}

/* -------------------------------------------------- narrow: the rail's lane
   The rail is fixed furniture in the left margin and it reaches 109px into it
   — the widest label is a word ("TRƯỚC"), not a year. From 1024px up the story
   column is narrower than the space it is given, so it centres itself and
   leaves the rail 28px of air. Below that the column fills the space, starts
   at the gutter, and the rail prints straight through the first two words of
   every line. So this is where time stops being a rail and becomes the thread
   under the header — the same answer the phone gets, two breakpoints earlier.
   (960px was the first guess and it is 4px short: measure, do not estimate.) */

@media (max-width: 1023px) {
  .rail { display: none; }

  .progress-thread {
    display: block;
    position: fixed;
    top: var(--header-h);
    left: 0;
    right: 0;
    z-index: 65;
    pointer-events: none;
    height: 34px;
  }
  .progress-track { position: absolute; inset: 0 0 auto 0; height: 2px; background: rgba(224, 164, 88, 0.15); }
  .progress-fill {
    position: absolute;
    top: 0;
    left: 0;
    height: 2px;
    width: 0;
    background: var(--amber);
    box-shadow: 0 0 8px rgba(224, 164, 88, 0.7);
    transition: width 0.2s ease;
  }
  .progress-year {
    position: absolute;
    right: 14px;
    top: 10px;
    font-family: var(--label);
    font-size: 10.5px;
    letter-spacing: 0.22em;
    color: var(--amber);
    background: rgba(18, 13, 5, 0.85);
    border: 1px solid rgba(224, 164, 88, 0.25);
    padding: 3px 8px;
  }
}

/* ------------------------------------------------------------------ mobile
   Gutters close to the edge of the glass, and the constellation collapses to
   a disclosure. */

@media (max-width: 719px) {
  :root { --gutter-l: 20px; --gutter-r: 20px; --header-h: 52px; }

  body { font-size: 18px; }

  /* Điện thoại không đủ chỗ cho một chòm sao: nhãn tiếng Việt chồng lên nhau
     ngay ở 390px. Nên trên màn hình hẹp, chòm sao duỗi thẳng thành danh sách —
     vẫn đúng mười cái link ấy, cùng một khối HTML, chỉ đổi cách xếp chỗ. Mạng
     nối là hình trang trí nên bỏ đi được mà không mất chữ nào. */
  .home-shell { min-height: 0; overflow: visible; padding-bottom: 44px; }
  .home-grid { flex-direction: column; padding: 0 22px; gap: 0; }
  .home-intro { flex: none; min-width: 0; padding: 20px 0 0; }
  /* Chữ 會安 là hoa văn, nhưng ở 360px nó thò ra ngoài mép 2px và trang không
     có thanh cuộn ngang để với theo — kéo vào trong lề. */
  .home-calligraphy { font-size: 104px; top: 8%; right: 8%; }
  .home-map { flex: none; min-height: 0; margin: 28px 0 0; display: flex; flex-direction: column; }
  .home-map .web { display: none; }
  .star {
    position: static;
    transform: none;
    flex-direction: row;
    align-items: center;
    gap: 14px;
    padding: 13px 2px;
    border-top: 1px solid var(--hair-soft);
  }
  .star:last-child { border-bottom: 1px solid var(--hair-soft); }
  .star-dot { width: 9px; height: 9px; box-shadow: 0 0 7px var(--accent); animation: none; }
  .star-label { align-items: flex-start; text-align: left; gap: 3px; }
  .star-name { max-width: none; font-size: 19px; text-shadow: none; }

  .site-header { padding: 0 16px; gap: 14px; }
  .wordmark { font-size: 10.5px; letter-spacing: 0.24em; }
  .crumbs { display: none; }

  /* The caption drops out of the plate and sits under it. Everything that
     dresses the plate — veil, frame, exhibit line — has to stop at the plate's
     edge too, or it paints over the title: the veil is positioned, the caption
     is not, so the veil would win. */
  .hero {
    --plate-h: max(62vh, 380px);
    height: auto;
    min-height: 0;
    padding-bottom: 34px;
  }
  /* A phone is too narrow to crop a plate to a fixed frame and still leave the
     photograph legible — at 360px the 1964 flood plate showed one window and
     lost the flooded street it is a photograph of. So where the picture's own
     proportions are known (the build reads them out of the file), the plate
     takes that shape and the whole photograph is on the page.
     The floor keeps a scroll painting 3.6 times wider than it is tall from
     arriving as a smear; the ceiling keeps a portrait plate from filling the
     screen twice over before the title turns up. `contain` is what makes both
     ends safe: in a box that is not the picture's shape, the picture is matted
     rather than cropped, and nothing is lost either way. */
  .hero.has-plate { --plate-h: clamp(200px, calc(100vw / var(--plate-ar)), 84vh); }
  .hero.has-plate .hero-slot { object-fit: contain; }
  .hero-media { position: relative; height: var(--plate-h); background: var(--deep); }
  /* Here the title sits under the plate, not on it, so the veil has no space
     to clear — and at this size its heavy foot was darkening the bottom fifth
     of the photograph for nothing. What is left is the vignette: the room
     stays dark at the edges, the picture itself is the picture. */
  .hero-veil {
    inset: 0 0 auto 0;
    height: calc(var(--plate-h) + 1px);
    background:
      /* the band the header sits in, so the wordmark holds against a bright
         plate now that the plate is no longer darkened all over */
      linear-gradient(180deg, rgba(22, 16, 6, 0.62) 0, transparent calc(var(--header-h) + 22px)),
      radial-gradient(130% 100% at 50% 45%, transparent 58%, rgba(22, 16, 6, 0.42) 100%);
  }
  .hero-frame { inset: 12px auto auto 12px; right: 12px; height: calc(var(--plate-h) - 24px); }
  .hero-calligraphy { font-size: 84px; top: 6%; right: 6%; }
  /* The credit line runs to three lines in Vietnamese, so it goes into the
     flow under the plate instead of floating at its bottom edge. */
  .hero-exhibit { position: static; padding: 14px var(--gutter-r) 0 var(--gutter-l); }
  .hero-caption { position: relative; inset: auto; z-index: 1; padding-top: 20px; }
  .hero h1 { font-size: clamp(38px, 11vw, 54px); margin-top: 16px; }
  .hero-alt { font-size: 19px; margin-top: 16px; }
  .hero-lede { font-size: 18px; }

  .chapter { padding-top: 72px; }
  .ghost-year { font-size: 92px; top: 22px; right: -8px; }
  .chapter h2 { font-size: 31px; margin-top: 20px; }
  .chapter-head { flex-wrap: wrap; gap: 10px; }
  .eyebrow, .chapter-date { font-size: 11px; letter-spacing: 0.2em; }

  .pullquote { margin: 44px 0; }
  .pullquote blockquote { font-size: 24px; }
  .pullquote-ghost { font-size: 68px; }

  .exhibit { padding: 12px 12px 0; margin-top: 32px; }
  /* A plate four times wider than it is tall would arrive here as a 80px
     ribbon. Same floor the hero has, and the picture is matted, not cut.
     `max-width` is not decoration: with a floor under the height, the aspect
     ratio pays for it in width instead and the frame grew to 578px inside a
     359px column — the credit in its corner went off the side of the phone. */
  .exhibit.has-plate .exhibit-frame { min-height: 150px; max-width: 100%; }
  /* The caption is the only thing under the plate now, so it needs no help
     finding its own line; the credit gets a little more of the corner. */
  .exhibit-credit { top: 8px; right: 9px; max-width: 82%; font-size: 11.5px; }
  .compare-half { min-width: 100%; }

  .ecard { width: min(280px, calc(100vw - 48px)); left: -4px; }
  .gcard { width: min(300px, calc(100vw - 48px)); left: -4px; }

  .panel { margin-top: 80px; max-width: none; }
  .panel-toggle { display: block; }
  .connections.collapsed .connections-body { display: none; }

  .tail { margin-top: 96px; }
  /* 168px of predicate column is more than a phone can spare: the label goes
     back above the title it belongs to. */
  .thread { grid-template-columns: 1fr; gap: 6px; padding: 14px 0; }
  .refs { margin-top: 64px; }
  .contribute { margin-top: 72px; padding: 24px 20px 26px; }
  .index-grid { grid-template-columns: 1fr; }

  .index-hero { padding-top: 140px; }
  .index-calligraphy { font-size: 110px; top: 76px; }
  .index-lede-top { font-size: 19px; }
  .site-footer { margin-top: 80px; }
}
