/* ============================================================================
   CYBERTOWN MALL — components.

   Reads tokens.css. Every colour here is a var(); a literal hex in this file is
   a bug, because build/check-contrast.mjs measures tokens and would not see it.

   Composition follows the chosen Stitch direction, PORTAL // MIDNIGHT_NEON:
   full-bleed photographic hero, `> MARKER` mono section heads, image-led tiles,
   near-black ground with hairline separation instead of raised panels. What is
   deliberately absent is the previous pass's bordered .card around every unit
   of content — sections sit on shared ground and are separated by a rule.
   ============================================================================ */

/* ---------------------------------------------------------------- reset -- */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--surface-0);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  /* Nothing on this site may scroll the page sideways. Wide content gets its
     own overflow-x container instead. */
  overflow-x: hidden;
}

img,
svg { max-width: 100%; }

img { height: auto; }

h1, h2, h3, h4 {
  margin: 0;
  line-height: var(--leading-tight);
  font-weight: 650;
  letter-spacing: -0.01em;
}

p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0; padding: 0; }

a { color: var(--cyan); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--cyan-hover); }

/* One focus treatment, everywhere, and it is never removed. */
:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring);
  border-radius: var(--radius-sm);
}

code, pre, kbd {
  font-family: var(--font-mono);
  /* 0.92em inside a 13.5px line lands at 12.42px, under the 13px floor. */
  font-size: max(13px, 0.92em);
}

::selection { background: var(--cyan); color: var(--fg-on-cyan); }

/* ------------------------------------------------------------ utilities -- */

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip {
  position: absolute;
  left: var(--space-3);
  top: calc(-1 * var(--space-10));
  z-index: 50;
  padding: var(--space-3) var(--space-5);
  background: var(--gold);
  color: var(--fg-on-gold);
  font-weight: 700;
  text-decoration: none;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  /* NOT transitioned. A keyboard user pressing Tab needs the skip link now, not
     140ms from now — and `top` is a layout property, which the motion rules say
     never to animate. QA caught this by measuring the link's position the
     instant it received focus and finding it still off-screen. */
}
.skip:focus { top: 0; }

main:focus { outline: none; }

.wrap {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--space-5);
}
.wrap-wide { max-width: var(--page-wide); }

.prose { max-width: var(--measure); }

/* The mono label used for markers, metadata and signage. */
.mono,
.sec-marker,
.tile-meta,
.badge,
.auth,
.stat-label,
.fact dt {
  font-family: var(--font-mono);
}

.sep { opacity: 0.5; padding-inline: 0.4em; }

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

.masthead {
  position: sticky;
  top: 0;
  z-index: 30;
  background: color-mix(in srgb, var(--surface-0) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.masthead-in {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding: var(--space-3) var(--space-5);
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--space-5);
}
.masthead-in.is-wide { max-width: var(--page-wide); }

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--fg);
  text-decoration: none;
  min-width: 0;
}
.brand img { flex: none; border-radius: 50%; }

.brand-text { display: flex; flex-direction: column; min-width: 0; }

.brand-name {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  letter-spacing: var(--tracking-sign);
  text-transform: uppercase;
  color: var(--gold);
  white-space: nowrap;
}
.brand-sub {
  font-size: var(--text-xs);
  color: var(--fg-muted);
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* On a phone the seven nav items do not fit, so the strip scrolls. A scrolling
   strip with a hard edge looks like a strip that ends, so the last few pixels
   fade — the only signal that Archive and Checker are further along. */
.nav {
  min-width: 0;
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 28px), transparent 100%);
}
@media (min-width: 1280px) {
  /* Everything fits above this width; masking there would fade a nav item for
     no reason.

     WAS 1081px, RAISED WITH THE CHECKER LINK (2026-09-10). Six items fitted
     beside the brand and the search box from 1081px up. Seven do not — measured
     at 1081, 1100 and 1200, where the strip scrolled AND the fade was switched
     off, so Checker was cut off the end with nothing to say it was there. The
     two breakpoints move together: see the stacked masthead below, which now
     gives the nav its own row up to 1279px. */
  .nav { -webkit-mask-image: none; mask-image: none; }
}
.nav ul {
  display: flex;
  gap: var(--space-1);
  list-style: none;
  overflow-x: auto;
  scrollbar-width: thin;
}
.nav a {
  display: block;
  padding: var(--space-3) var(--space-4);
  min-height: 44px;
  display: flex;
  align-items: center;
  color: var(--fg-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  white-space: nowrap;
  border-bottom: 2px solid transparent;
}
.nav a:hover { color: var(--fg); background: var(--surface-1); }
.nav a[aria-current='page'] {
  color: var(--cyan);
  border-bottom-color: var(--cyan);
}

.masthead-search { display: flex; align-items: center; gap: var(--space-2); }
.masthead-search label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.masthead-search input {
  width: 15rem;
  min-height: 40px;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font: inherit;
  font-size: var(--text-sm);
}
.masthead-search input::placeholder { color: var(--fg-faint); }
.masthead-search button {
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--cyan);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  cursor: pointer;
}
.masthead-search button:hover { background: var(--cyan-quiet); }

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

/*
  The first viewport. A photograph of the Mall spans the frame; the title sits
  INSIDE it in a calm zone at the lower left, never on top of a face or a sign.
  The scrim is a horizontal-plus-vertical gradient rather than a flat overlay so
  the right-hand third of the image stays legible.
*/

.hero {
  position: relative;
  isolation: isolate;
  min-height: min(76vh, 620px);
  display: grid;
  align-items: end;
  overflow: hidden;
  border-bottom: 1px solid var(--border);
}

.hero-media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  /* Makes this box the size container the live frame measures itself against.
     Without it `cqw`/`cqh` on .hero-live resolve against the viewport and the
     frame stops covering on any page whose hero is not the full window. */
  container-type: size;
}
.hero-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Tuned for the 2026-08-20 master (docs/images/mall.png, 16:9). The fountain
     stands at 53% across and 41% down the frame, so those are the anchor
     points: a viewport crop then takes its slice around the fountain instead of
     drifting off it. The vertical figure is biased slightly above the fountain's
     own centre because the top 9% of the master is empty world background, and
     that is the band worth spending on the crop. Re-measure if the master is
     replaced -- the previous value (70% 42%) was tuned for a 2.85:1 master that
     had its subject on the right. */
  object-position: 53% 38%;
  display: block;
}

/*
  THE LIVE MALL, FADED IN OVER THE STILL.

  js/hero.js appends this iframe once /world/hero.html has proved it drew real
  pixels. It sits in the same box as the poster, under the same scrim, so the
  swap moves nothing on the page.

  IT IS 16:9 AND IT IS COVERED, NOT STRETCHED. A VRML `Viewpoint` applies its
  `fieldOfView` to the SMALLER side of the viewport, so a frame stretched to the
  hero band's 2.3:1 would widen the horizontal view and show more of the Mall
  than the poster does — the swap would visibly jump, and the culled world would
  run out of geometry at the edges. Instead the frame keeps 16:9 and is sized to
  cover the band, then anchored on the same 53%/38% point the poster's
  `object-position` uses. Poster and live frame are then the same crop of the
  same shot.

  `pointer-events: none` is why the page still scrolls over the hero. It is the
  outer of two independent guards; the inner one is `NavigationInfo type "NONE"`
  in the world itself.
*/
.hero-live {
  position: absolute;
  top: 38%;
  left: 53%;
  translate: -53% -38%;
  width: max(100cqw, calc(100cqh * 16 / 9));
  height: max(100cqh, calc(100cqw * 9 / 16));
  border: 0;
  display: block;
  pointer-events: none;
  opacity: 0;
  /* `--dur` is already 1ms under prefers-reduced-motion (tokens.css), so the
     fade becomes a cut for anyone who asked for that, with no rule here. */
  transition: opacity var(--dur) var(--ease);
}
.hero-media.is-live .hero-live { opacity: 1; }

/* No container queries: fill the band rather than leave a gap. The frame then
   shows a slightly wider view than the poster, which is a worse match and still
   a perfectly good picture of the Mall. */
@supports not (width: 1cqw) {
  .hero-live { inset: 0; width: 100%; height: 100%; translate: none; }
}

.hero-scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  /* The left column has to be dark enough to read a wordmark on, and the right
     third has to stay bright enough to show the crowd and the SHOPPING CENTER
     sign. That is why this is a five-stop horizontal ramp and not a flat wash. */
  background:
    linear-gradient(to right,
      color-mix(in srgb, var(--surface-0) 97%, transparent) 0%,
      color-mix(in srgb, var(--surface-0) 92%, transparent) 24%,
      color-mix(in srgb, var(--surface-0) 62%, transparent) 46%,
      color-mix(in srgb, var(--surface-0) 14%, transparent) 74%,
      color-mix(in srgb, var(--surface-0) 34%, transparent) 100%),
    linear-gradient(to bottom,
      color-mix(in srgb, var(--surface-0) 55%, transparent) 0%,
      transparent 30%,
      color-mix(in srgb, var(--surface-0) 88%, transparent) 100%);
}

.hero-in {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding: var(--space-9) var(--space-5) var(--space-7);
}

.hero-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-sign);
  text-transform: uppercase;
  color: var(--lime);
  margin-bottom: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.hero-eyebrow::before {
  content: '';
  width: 28px;
  height: 1px;
  background: var(--lime);
  flex: none;
}

.hero-title {
  font-family: var(--font-mono);
  font-size: var(--text-hero);
  font-weight: 400;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  line-height: 0.98;
  margin: 0 0 var(--space-4);
  /* The wordmark has to survive being read against a lit photograph. A tight
     text-shadow does that without the neon bloom the brief rules out. */
  text-shadow: 0 2px 18px var(--surface-0), 0 1px 3px var(--surface-0);
}
.hero-title .is-revival { display: block; color: var(--gold); font-size: 0.42em; letter-spacing: var(--tracking-sign); }

.hero-lede {
  max-width: 46ch;
  font-size: var(--text-md);
  color: var(--fg);
  text-shadow: 0 1px 10px var(--surface-0);
  margin-bottom: var(--space-6);
}

.hero-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }

/* A peek at the next section, so the fold promises something. */
.hero-peek {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-top: var(--space-7);
  padding-top: var(--space-4);
  border-top: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg-muted);
}
.hero-peek strong { color: var(--fg); font-weight: 400; }
.hero-peek a { margin-left: auto; white-space: nowrap; }

/* -------------------------------------------------------------- buttons -- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 46px;
  padding: var(--space-3) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  /* Tactile push, not a glow. */
  transition: transform var(--dur-quick) var(--ease), background var(--dur-quick) var(--ease);
}
.btn:active { transform: translateY(1px); }

.btn-primary { background: var(--lime); color: var(--fg-on-lime); font-weight: 700; }
.btn-primary:hover { background: color-mix(in srgb, var(--lime) 84%, white); color: var(--fg-on-lime); }
.btn-primary:focus-visible { box-shadow: var(--focus-ring-on-light); }

.btn-ghost { border-color: var(--border-strong); color: var(--fg); background: color-mix(in srgb, var(--surface-0) 60%, transparent); }
.btn-ghost:hover { border-color: var(--cyan); color: var(--cyan); }

.btn-quiet { color: var(--fg-muted); padding-inline: var(--space-3); }
.btn-quiet:hover { color: var(--fg); }

/* --------------------------------------------------------- section head -- */

/*
 * SECTION SEPARATION. LSS asked on 2026-09-07 for major sections to read as
 * clearly separate while scrolling. The design language is unchanged -- still a
 * marker plus a hairline, still no bordered panels -- it is the spacing and the
 * weight of the rule that changed:
 *
 *   padding      --space-8 (64px) -> --space-9 (96px)
 *   the rule     --border -> --border-strong, which is the same hairline at a
 *                contrast that survives a laptop screen in daylight
 *
 * A sunk section already separates itself by its own fill, so it drops the rule
 * rather than carrying both.
 */
.section { padding-block: var(--space-9); }
.section + .section { border-top: 1px solid var(--border-strong); }
.section-sunk { background: var(--surface-sunk); }
.section-sunk + .section,
.section + .section-sunk { border-top: 0; }

.sec-head {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: var(--space-3) var(--space-5);
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}

.sec-marker {
  grid-column: 1 / -1;
  margin: 0 0 var(--space-1);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-sign);
  text-transform: uppercase;
  color: var(--lime);
}
.sec-marker::before { content: '> '; opacity: 0.7; }

.sec-title { font-size: var(--text-xl); grid-column: 1; }

.sec-more {
  grid-column: 2;
  grid-row: 2;
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  white-space: nowrap;
  text-decoration: none;
}
.sec-more:hover { text-decoration: underline; }

.sec-lede {
  grid-column: 1 / -1;
  margin: var(--space-2) 0 0;
  max-width: var(--measure);
  color: var(--fg-muted);
}

/* ---------------------------------------------------------------- tiles -- */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr));
  gap: var(--space-5);
  list-style: none;
}
/* Grid children must be allowed to shrink or intrinsic content pushes the
   track past the container and the page scrolls sideways on mobile. */
.tiles > * { min-width: 0; }

/* The drop rail: an editorial sequence, not four equal boxes. The first tile
   is given twice the room because it is the newest thing in the Mall. */
.tiles-rail {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 210px), 1fr));
}

.tile { display: flex; flex-direction: column; min-width: 0; }

.tile-img {
  display: block;
  position: relative;
  text-decoration: none;
  color: inherit;
  aspect-ratio: 1 / 1;
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
  margin-bottom: var(--space-3);
}
.tile:hover .tile-img { border-color: var(--border-strong); }

.tile-img .thumb { width: 100%; height: 100%; }

.tile-body { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }

.tile-name { font-size: var(--text-lg); overflow-wrap: anywhere; }
.tile-name a { color: var(--fg); text-decoration: none; }
.tile-name a:hover { color: var(--cyan); text-decoration: underline; }

.tile-meta {
  margin: 0;
  font-size: var(--text-xs);
  color: var(--fg-muted);
  overflow-wrap: anywhere;
}
.tile-creator { color: var(--fg); }
.tile-price .price { color: var(--gold); }

/* The one object given room to breathe on the homepage. */
.feature {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: var(--space-7);
  align-items: start;
}
.feature-img {
  display: block;
  max-width: 420px;
  text-decoration: none;
  color: inherit;
  aspect-ratio: 1 / 1;
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}
.feature-img .thumb { width: 100%; height: 100%; }
.feature h3 { font-size: var(--text-2xl); margin-bottom: var(--space-3); overflow-wrap: anywhere; }
.feature h3 a { color: var(--fg); text-decoration: none; }
.feature h3 a:hover { color: var(--cyan); }

/* ------------------------------------------------------------ thumbnail -- */

.thumb {
  display: block;
  object-fit: cover;
  background: var(--surface-2);
  /* Fade in over a reserved box so a grid never flashes grey then jumps. */
  animation: thumb-in var(--dur) var(--ease) both;
}

@keyframes thumb-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/*
  The missing-image state. It says what it is: the object's real name in the
  signage face on its district's fill, plus the reason in mono. It is NOT two
  initials on a coloured tile pretending to be artwork.
*/
.thumb-empty {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: var(--space-4);
  background:
    radial-gradient(120% 90% at 50% 30%,
      color-mix(in srgb, var(--tile-tint, var(--surface-indigo)) 92%, white) 0%,
      color-mix(in srgb, var(--tile-tint, var(--surface-indigo)) 70%, black) 62%,
      color-mix(in srgb, var(--tile-tint, var(--surface-indigo)) 34%, black) 100%);
}

/* An inset hairline, so the plate reads as a deliberate label rather than as an
   image that failed to load. */
.thumb-plate {
  flex: 1;
  display: grid;
  place-items: center;
  padding: var(--space-3);
  border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
  text-align: center;
  min-height: 0;
}
.thumb-name {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  line-height: 1.25;
  color: var(--fg);
  overflow-wrap: anywhere;
  text-wrap: balance;
  /* Long object names must not spill out of the plate. */
  display: -webkit-box;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.thumb-note {
  margin-top: var(--space-3);
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
}

/* District fills, so a grid is still scannable without inventing artwork. */
.thumb-empty[data-district='home-habitat'] { --tile-tint: #2B1E52; }
.thumb-empty[data-district='gallery-row'] { --tile-tint: #123A4A; }
.thumb-empty[data-district='grounds'] { --tile-tint: #17402C; }
.thumb-empty[data-district='occasions'] { --tile-tint: #46203F; }
.thumb-empty[data-district='transit-frontier'] { --tile-tint: #402A16; }
.thumb-empty[data-district='concourse'] { --tile-tint: #1E1440; }

.thumb-sm { width: 72px; height: 72px; flex: none; }
.thumb-sm .thumb-name { font-size: var(--text-xs); -webkit-line-clamp: 3; line-clamp: 3; }
.thumb-sm .thumb-note { display: none; }

/* ----------------------------------------------------------------- rows -- */

.rows { list-style: none; }

/* Prose entries — announcements, notes. Full width, no thumbnail column. */
.notes { list-style: none; display: grid; gap: var(--space-4); }
.notes > li {
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--border);
  max-width: var(--measure);
}
.notes p { margin: 0 0 var(--space-2); }
.notes p:last-child { margin-bottom: 0; }

.row {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-2);
  border-bottom: 1px solid var(--border);
}
.row:hover { background: var(--surface-1); }
.row > * { min-width: 0; }

.row-img { display: block; text-decoration: none; color: inherit; }
.row-name { color: var(--fg); font-weight: 600; text-decoration: none; overflow-wrap: anywhere; }
.row-name:hover { color: var(--cyan); text-decoration: underline; }
.row-meta { margin: 0; font-size: var(--text-xs); color: var(--fg-muted); font-family: var(--font-mono); overflow-wrap: anywhere; }
.row-price { margin: 0; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--gold); white-space: nowrap; }
.row-date { margin: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-muted); white-space: nowrap; }
.row .badges { grid-column: 2 / -1; }

/* --------------------------------------------------------------- badges -- */

.badges { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; }

/* TIGHTENED, NOT SHRUNK. `--text-xs` is 13px and 13px is the floor -- a
   low-vision accommodation, not a style setting (see tokens.css). The width
   these badges were spending came from tracking and padding, not from type
   size, so that is what came off: 0.06em of letter-spacing on a mono face is
   most of a character per word. Shorter labels did the rest. Do not reach for
   font-size here. */
.badge {
  display: inline-block;
  padding: 1px 6px;
  border: 1px solid currentColor;
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  line-height: 1.45;
  white-space: nowrap;
}
.badge-in-mall { color: var(--lime); background: var(--lime-quiet); }
.badge-sold-out { color: var(--neutral); background: var(--neutral-quiet); }
.badge-removed { color: var(--neutral); background: var(--neutral-quiet); }
.badge-new { color: var(--magenta); background: var(--magenta-quiet); }
.badge-limited { color: var(--magenta); background: var(--magenta-quiet); }
.badge-award { color: var(--gold); background: var(--gold-quiet); }

/* ------------------------------------------------------ authority label -- */

.auth {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.auth-kind {
  padding: 2px var(--space-2);
  border: 1px solid currentColor;
  border-radius: var(--radius-sm);
}
/*
 * A marker on its own line, above or below the prose it labels.
 *
 * LSS asked for this on 2026-09-07: as an inline-flex the pill sat mid-sentence,
 * read as part of the sentence, and at a narrow width wrapped into the middle of
 * a line of body text. `display: flex` gives it the whole line; `align-items:
 * flex-start` stops the outline stretching to the row height; the margin is what
 * makes the separation visible rather than implied.
 */
.auth-standalone {
  display: flex;
  align-items: flex-start;
  margin-block: var(--space-3);
}
.auth-extra { text-transform: none; letter-spacing: normal; color: var(--fg-muted); }
.auth-current { color: var(--lime); }
.auth-historical { color: var(--amber); }
.auth-creator { color: var(--cyan); }
.auth-editorial { color: var(--neutral); }
.auth-conflict { color: var(--magenta); }

/* ---------------------------------------------------------------- notes -- */

.note {
  padding: var(--space-4) var(--space-5);
  background: var(--surface-1);
  border-left: 3px solid var(--border-strong);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-size: var(--text-sm);
  color: var(--fg-muted);
  max-width: var(--measure);
}
.note-title { font-size: var(--text-base); color: var(--fg); margin-bottom: var(--space-2); }
.note-coverage { border-left-color: var(--amber); }
.note-conflict { border-left-color: var(--magenta); }
.note-info { border-left-color: var(--cyan); }
.note-warn { border-left-color: var(--amber); }
.note p:last-child { margin-bottom: 0; }

/* A note that carries a list needs the list to read as a list. Without this the
   paragraph after it butts straight up against the last bullet and reads as a
   continuation of it, which is exactly what it is not. */
.note ul { margin: var(--space-3) 0; padding-left: var(--space-5); }
.note li { margin-bottom: var(--space-2); }
.note li:last-child { margin-bottom: 0; }
.note ul + p { margin-top: var(--space-4); }

/* -------------------------------------------------------- signs & shops -- */

/*
  The shop plate. Both frames ship; the swap on hover/focus is the interaction
  the Mall itself shipped in 1999. Under reduced motion the crossfade becomes an
  instant swap, but the swap still happens — it is information, not decoration.
*/
.sign {
  position: relative;
  display: block;
  width: 166px;
  max-width: 100%;
  aspect-ratio: 166 / 126;
  flex: none;
}
.sign img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.sign-over { opacity: 0; transition: opacity var(--dur-quick) var(--ease); }
.shop-link:hover .sign-over,
.shop-link:focus-visible .sign-over,
.district:hover .sign-over,
.district a:focus-visible .sign-over { opacity: 1; }
.shop-link:hover .sign-idle,
.shop-link:focus-visible .sign-idle,
.district:hover .sign-idle,
.district a:focus-visible .sign-idle { opacity: 0; }
.sign-idle { transition: opacity var(--dur-quick) var(--ease); }

.sign-sm { width: 110px; }
.sign-lg { width: 220px; }

/* The lettered stand-in for a shop with no plate in the world. Same box, same
   idle green on black, obviously type rather than a picture. */
.sign-blank {
  display: grid;
  place-items: center;
  padding: var(--space-3);
  background: #000;
  border: 1px solid var(--border);
  text-align: center;
}
.sign-blank > span {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.2;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--lime);
  overflow-wrap: anywhere;
}
.shop-link:hover .sign-blank { border-color: var(--cyan); }
.shop-link:hover .sign-blank > span { color: var(--cyan); }

.shops {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 200px), 1fr));
  gap: var(--space-5) var(--space-4);
  list-style: none;
}
.shops > * { min-width: 0; }

.shop-link {
  display: grid;
  /* Fixed plate row, then the name: every tile in a district row starts and
     ends at the same place regardless of which shops have a period plate. */
  grid-template-rows: auto auto;
  gap: var(--space-2);
  justify-items: start;
  color: var(--fg);
  text-decoration: none;
  min-height: 44px;
}
.shop-link .sign,
.shop-link .sign-blank { width: 100%; max-width: 166px; aspect-ratio: 166 / 126; }
.shop-name {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: 0.06em;
  overflow-wrap: anywhere;
}
.shop-link:hover .shop-name { color: var(--cyan); text-decoration: underline; }
.shop-count { margin: var(--space-1) 0 0; font-size: var(--text-xs); font-family: var(--font-mono); color: var(--fg-muted); }
.stock-live { color: var(--lime); }
.stock-none { color: var(--fg-faint); }
.shop.is-former .shop-name { color: var(--fg-muted); }

/* Districts: a low wide strip, not a card grid. */
/* Exactly three columns, because there are exactly six districts: an auto-fit
   track count leaves empty cells that show the 1px gap colour as a large block. */
.districts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  list-style: none;
  background: var(--border);
  border: 1px solid var(--border);
}
.district {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  background: var(--surface-1);
  min-width: 0;
}
.district:hover { background: var(--surface-2); }
.district h3 { font-size: var(--text-lg); overflow-wrap: anywhere; }
.district h3 a { color: var(--fg); text-decoration: none; }
.district h3 a:hover { color: var(--cyan); text-decoration: underline; }
.district-blurb { margin: 0; font-size: var(--text-sm); color: var(--fg-muted); }
.district-count { margin: auto 0 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-muted); }

/* Locate-in-Mall: a real link into the running game. */
.locate {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--lime);
  border-radius: var(--radius-sm);
  color: var(--lime);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  text-decoration: none;
}
.locate:hover { background: var(--lime-quiet); color: var(--lime); }
.locate-mark { opacity: 0.8; }
.locate-note { color: var(--fg-muted); font-size: var(--text-xs); }
.locate-sm { min-height: 36px; padding: var(--space-1) var(--space-3); }
.locate-sm .locate-note { display: none; }

/* ----------------------------------------------------------------- stats */

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  list-style: none;
  margin: 0;
}
.stat-value { display: block; font-family: var(--font-mono); font-size: var(--text-xl); color: var(--gold); line-height: 1.1; }
.stat-label { display: block; font-size: var(--text-xs); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--fg-muted); }

/* ------------------------------------------------------- forms & search -- */

.searchbar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.searchbar input[type='search'] {
  min-height: 52px;
  padding: var(--space-3) var(--space-5);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font: inherit;
  font-size: var(--text-md);
  min-width: 0;
}
.searchbar input[type='search']::placeholder { color: var(--fg-faint); }
.searchbar .btn { min-height: 52px; }

/* Quick filters: the four that matter, always visible, as links not controls. */
.quickfilters { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; margin-bottom: var(--space-4); }
.quickfilters a {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--fg-muted);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  text-decoration: none;
}
.quickfilters a:hover { border-color: var(--border-strong); color: var(--fg); }
.quickfilters a[aria-current='true'] { border-color: var(--cyan); color: var(--cyan); background: var(--cyan-quiet); }

/*
  The advanced drawer. The previous pass had twelve controls permanently open
  above the results, which pushed the objects below the fold on every viewport.
  Everything is still here and still a plain GET form; it is just closed until
  asked for.
*/
.advanced {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-6);
  background: var(--surface-1);
}
.advanced > summary {
  padding: var(--space-3) var(--space-5);
  min-height: 46px;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg);
  cursor: pointer;
  list-style: none;
}
.advanced > summary::-webkit-details-marker { display: none; }
.advanced > summary::before { content: '+'; color: var(--cyan); font-size: 1.1em; }
.advanced[open] > summary::before { content: '−'; }
.advanced > summary:focus-visible { box-shadow: var(--focus-ring); }

.advanced-body {
  padding: var(--space-5);
  border-top: 1px solid var(--border);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: var(--space-4);
}
.field { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
/* The one-line contract under a filter that accepts more than one shape of
   input. --text-xs is the 13px floor, never below it. */
.field-hint { margin: 0; font-size: var(--text-xs); color: var(--fg-muted); }
.field label { font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--fg-muted); }
.field select,
.field input {
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font: inherit;
  font-size: var(--text-sm);
  min-width: 0;
}
.advanced-actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
.advanced-actions .count { margin-left: auto; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg-muted); }

.results-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.results-head h2 { font-size: var(--text-xl); }
.viewswitch { margin-left: auto; display: flex; gap: var(--space-2); font-family: var(--font-mono); font-size: var(--text-sm); }
.viewswitch a { min-height: 40px; display: inline-flex; align-items: center; padding-inline: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-sm); text-decoration: none; color: var(--fg-muted); }
.viewswitch a[aria-current='true'] { border-color: var(--cyan); color: var(--cyan); }

/* --------------------------------------------------------------- chips  -- */

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); list-style: none; margin-bottom: var(--space-5); }
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 36px;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-family: var(--font-mono);
  text-decoration: none;
  color: var(--fg);
}
.chip:hover { border-color: var(--magenta); color: var(--magenta); }
.chip .x { color: var(--fg-muted); }
.chip-clear { color: var(--fg-muted); }

/* --------------------------------------------------------------- pager  -- */

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-7);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}
.pager a,
.pager > span {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding-inline: var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  text-decoration: none;
}
.pager a { color: var(--cyan); border-color: var(--border); }
.pager a:hover { border-color: var(--cyan); }
.pager [aria-current='page'] { border-color: var(--cyan); color: var(--cyan); background: var(--cyan-quiet); }
.pager [aria-disabled='true'] { color: var(--fg-faint); }
.pager .pager-status { margin-left: auto; color: var(--fg-muted); border: 0; min-width: 0; }
.pager .gap { border: 0; min-width: 0; color: var(--fg-faint); }

/* ---------------------------------------------------------- item detail -- */

.detail {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 5fr);
  gap: var(--space-7);
  align-items: start;
}
.detail-img {
  max-width: 460px;
  aspect-ratio: 1 / 1;
  background: var(--surface-2);
  border: 1px solid var(--border);
  overflow: hidden;
}
.detail-img .thumb { width: 100%; height: 100%; }

.page-title { font-size: var(--text-2xl); margin-bottom: var(--space-3); overflow-wrap: anywhere; }
.page-lede { color: var(--fg-muted); max-width: var(--measure); margin-bottom: var(--space-5); }

.facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr)); gap: var(--space-4); margin: 0 0 var(--space-6); }
.fact { min-width: 0; padding-top: var(--space-2); border-top: 1px solid var(--border); }
.fact dt { margin: 0 0 var(--space-1); font-size: var(--text-xs); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--fg-muted); }
.fact dd { margin: 0; font-size: var(--text-sm); overflow-wrap: anywhere; }
.unknown { color: var(--fg-faint); font-style: italic; }

/*
 * The upload rules on /desk.
 *
 * Body size, not table size. This list is the one thing on the page a creator
 * has to follow exactly, so it may not be the smallest type on it -- see the
 * low-vision accommodation note in tokens.css. The term is 15px and NOT
 * uppercased: "Object name" is a label the creator will see on the form itself,
 * and shouting it does not help them match it.
 */
.uprules { margin: 0; display: grid; gap: var(--space-4); }
.uprule { min-width: 0; padding-top: var(--space-3); border-top: 1px solid var(--border); }
.uprule dt { margin: 0 0 var(--space-1); font-size: var(--text-base); font-weight: 600; color: var(--fg); }
.uprule dd { margin: 0; display: grid; gap: var(--space-1); }
.uprule-rule { font-size: var(--text-base); }
.uprule-detail { font-size: var(--text-base); color: var(--fg-muted); max-width: var(--measure); }

/* The completeness footnote under a tutorial credit. Its own element so it can
   sit on a line of its own on a phone without a second paragraph's margins. */
.tut-completeness { display: block; margin-top: var(--space-1); }

.table-scroll { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th, td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
th { font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--fg-muted); }

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

.site-foot {
  margin-top: var(--space-9);
  background: var(--surface-sunk);
  border-top: 1px solid var(--border);
  padding-block: var(--space-8);
}
.foot-in {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--space-5);
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--space-7);
}
.foot-col > * { min-width: 0; }
.foot-col h2 {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-sign);
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: var(--space-4);
}
.foot-col p { font-size: var(--text-sm); color: var(--fg-muted); }
.foot-col ul { list-style: none; display: flex; flex-direction: column; gap: var(--space-2); }
.foot-col a { font-size: var(--text-sm); min-height: 32px; display: inline-flex; align-items: center; }

/* ----------------------------------------------------------- responsive -- */

/* RAISED FROM 1080px WITH THE CHECKER LINK (2026-09-10). The nav takes its own
   full-width row here, and seven items fit in it comfortably from about 640px
   up — so the header that used to hide the last tab between 1081 and 1279px now
   shows all of them. Above 1280px the single row is back and still fits. */
@media (max-width: 1279px) {
  .masthead-in {
    grid-template-columns: 1fr auto;
    row-gap: var(--space-2);
  }
  .brand { grid-row: 1; grid-column: 1; }
  .masthead-search { grid-row: 1; grid-column: 2; }
  .nav { grid-row: 2; grid-column: 1 / -1; }
  .masthead-search input { width: 11rem; }
}

@media (max-width: 900px) {
  .districts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 860px) {
  .feature,
  .detail { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .feature-img,
  .detail-img { max-width: 420px; }
  .foot-in { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .foot-brand { grid-column: 1 / -1; }
  .row { grid-template-columns: 56px minmax(0, 1fr); }
  .row-price, .row-date { grid-column: 2; font-size: var(--text-xs); }
  .thumb-sm { width: 56px; height: 56px; }
}

@media (max-width: 640px) {
  .hero { min-height: min(88vh, 640px); }
  .hero-media img { object-position: 58% 45%; }
  .hero-in { padding: var(--space-8) var(--space-4) var(--space-5); }
  .hero-actions .btn { flex: 1 1 100%; }
  .hero-peek { flex-direction: column; align-items: flex-start; gap: var(--space-1); }
  .hero-peek a { margin-left: 0; }

  .wrap { padding-inline: var(--space-4); }
  /* One step down from the desktop --space-9, not two: a phone is where the
     sections run together worst, so it keeps the separation it was given. */
  .section { padding-block: var(--space-8); }

  /* Two columns of objects on a phone, which is what a shopper expects. */
  .tiles,
  .tiles-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); }
  .tile-name { font-size: var(--text-base); }

  .shops { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sign { width: 100%; }
  .districts { grid-template-columns: minmax(0, 1fr); }
  .thumb-name { font-size: var(--text-sm); }

  .searchbar { grid-template-columns: minmax(0, 1fr); }

  /* At 360px a nowrap "All 18 in this drop →" beside a title cannot fit, and
     the 1fr/auto grid grew the page 27px past the viewport. Stack instead. */
  .sec-head { grid-template-columns: minmax(0, 1fr); }
  .sec-title,
  .sec-more { grid-column: 1; }
  .sec-more { grid-row: auto; white-space: normal; }
  .foot-in { grid-template-columns: minmax(0, 1fr); gap: var(--space-6); }
  .facts { grid-template-columns: minmax(0, 1fr); }
  .masthead-search input { width: 100%; min-width: 0; }
  .masthead-in { grid-template-columns: minmax(0, 1fr); }
  .masthead-search { grid-row: 2; grid-column: 1; }
  .nav { grid-row: 3; }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------------ /staff ---- */

/*
  Three roles, each a card. The NAME is what somebody came for, so it is the
  largest thing in the card and the role label sits above it in the same
  label-then-value order every record page on this site uses.

  No badge, no date, no source. See views/staff.mjs.
*/
.staff-roles {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  gap: var(--space-5);
  margin: var(--space-6) 0 0;
}

.staff-role {
  min-width: 0;
  padding: var(--space-5);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-left: 3px solid var(--gold);
  border-radius: var(--radius-sm);
}

.staff-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-muted);
  margin: 0 0 var(--space-2);
}

.staff-people { list-style: none; margin: 0; }
.staff-person {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  color: var(--fg);
  overflow-wrap: anywhere;
}

/* A staffed role whose holders are not named. `--fg`, never `--fg-muted`:
   "Currently staffed." is the Mall's answer, not a footnote about it. */
.staff-status {
  font-size: var(--text-md);
  color: var(--fg);
  margin: 0;
}

.staff-blurb {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  color: var(--fg-muted);
}

/* ------------------------------------------------------------ /mall ----- */

.viewer-hero { padding-block: var(--space-7) var(--space-8); }

/*
  A fixed aspect box. The poster and the live world occupy exactly the same
  rectangle, so activating the viewer moves nothing on the page — CLS from this
  swap is zero by construction rather than by luck.
*/
.viewer {
  position: relative;
  aspect-ratio: 16 / 9;
  max-height: 74vh;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  overflow: hidden;
}
@media (max-width: 640px) {
  .viewer { aspect-ratio: 4 / 3; max-height: 62vh; }
}

.viewer-poster { position: absolute; inset: 0; }
.viewer-poster img { width: 100%; height: 100%; object-fit: cover; object-position: 62% 45%; display: block; }
.viewer-poster[hidden] { display: none; }

.viewer-cta {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--space-3);
  padding: var(--space-5);
  text-align: center;
  background:
    radial-gradient(70% 70% at 50% 50%,
      color-mix(in srgb, var(--surface-0) 40%, transparent) 0%,
      color-mix(in srgb, var(--surface-0) 78%, transparent) 100%);
}
.viewer-cta .btn { min-height: 56px; padding-inline: var(--space-7); font-size: var(--text-base); }
.viewer-weight {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-muted);
  text-shadow: 0 1px 6px var(--surface-0);
}

.viewer-frame { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

.viewer.is-loading .viewer-cta { opacity: 0.55; }
/* The one hero motion the design allows: the poster fades out as the world
   fades in, once. */
.viewer.is-live .viewer-frame { animation: thumb-in var(--dur) var(--ease) both; }

.viewer-error { margin-top: var(--space-4); }

.viewer-alt {
  margin-top: var(--space-4);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg-muted);
}

/* Two columns that collapse cleanly, used by /mall's guide and provenance. */
.split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
  gap: var(--space-7);
  align-items: start;
}
.split > * { min-width: 0; }

.vp-list { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); }
.vp-list code {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--lime);
}

.plain-list { list-style: none; display: grid; gap: var(--space-3); max-width: var(--measure); }
.plain-list li { padding-left: var(--space-5); position: relative; color: var(--fg-muted); font-size: var(--text-sm); }
.plain-list li::before { content: '—'; position: absolute; left: 0; color: var(--fg-faint); }

.locate-list {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
  gap: var(--space-3) var(--space-5);
}
.locate-list > li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-width: 0;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.locate-name { font-family: var(--font-mono); font-size: var(--text-sm); overflow-wrap: anywhere; }

.hash { overflow-wrap: anywhere; color: var(--fg-muted); }

/* -------------------------------------------------------- record pages -- */

.crumbs {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg-muted);
  margin-bottom: var(--space-5);
}

.shop-head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-7);
  align-items: start;
  margin-bottom: var(--space-6);
}
.shop-head:has(.shop-head-sign:empty),
.shop-head > :only-child { grid-template-columns: minmax(0, 1fr); }
.shop-head-sign { display: grid; gap: var(--space-3); max-width: 240px; }
.shop-head-sign .hint { text-transform: none; letter-spacing: normal; margin: 0; }

@media (max-width: 700px) {
  .shop-head { grid-template-columns: minmax(0, 1fr); gap: var(--space-5); }
  .shop-head-sign { max-width: 180px; }
}

/* The viewer CTA sits on a rendered world, so it needs its own ground rather
   than relying on the image being dark in the right place. */
.viewer-cta .btn { box-shadow: 0 8px 30px rgb(0 0 0 / 0.55); }
.viewer-weight {
  padding: var(--space-2) var(--space-4);
  background: color-mix(in srgb, var(--surface-0) 88%, transparent);
  border-radius: var(--radius-sm);
}


/* Layout primitives the record and document pages use. */
.stack { display: grid; gap: var(--space-4); align-content: start; min-width: 0; }
.stack-lg { display: grid; gap: var(--space-6); align-content: start; min-width: 0; }
.cluster { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
.grid-wide {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: var(--space-5);
}
.grid-wide > * { min-width: 0; }
.row-main { display: grid; gap: var(--space-1); min-width: 0; }

/* Secondary metadata: the mono line under a name, a date, a source path. */
.card-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-muted);
  overflow-wrap: anywhere;
}
.muted { color: var(--fg-muted); }
.field-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-muted);
}

/* A notice is the same object as .note; the two names exist because half the
   pages predate the rename. Both resolve here so they cannot look different. */
.notice,
.panel {
  padding: var(--space-4) var(--space-5);
  background: var(--surface-1);
  border-left: 3px solid var(--border-strong);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-size: var(--text-sm);
  color: var(--fg-muted);
  max-width: var(--measure);
}
.notice h2, .notice h3,
.panel h2, .panel h3 {
  font-size: var(--text-base);
  color: var(--fg);
  margin-bottom: var(--space-2);
}
.notice p:last-child, .panel p:last-child { margin-bottom: 0; }
.notice-amber { border-left-color: var(--amber); }
.notice-cyan { border-left-color: var(--cyan); }

/* The archive's evidence block: a source path, a hash, a capture URL. */
.evidence {
  padding: var(--space-4);
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-muted);
  overflow-wrap: anywhere;
}

/* The timeline. A single rule with the year gutter beside it, rather than a
   card per entry — a hundred bordered boxes is what made the old one unreadable. */
.timeline { list-style: none; display: grid; gap: var(--space-5); }
.timeline > li {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.timeline > li > * { min-width: 0; }
.when {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--amber);
  white-space: nowrap;
}
@media (max-width: 640px) {
  .timeline > li { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
}

/* The remaining multi-control forms (archive filter, rules search). Same shape
   as .advanced-body so a control looks the same wherever it appears. */
.filters {
  padding: var(--space-5);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-6);
  display: grid;
  gap: var(--space-4);
}
.filter-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: var(--space-4);
}
.filter-row > * { min-width: 0; }
.filters label { display: grid; gap: var(--space-2); min-width: 0; }
.filters label > span {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-muted);
}
.filters input,
.filters select {
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font: inherit;
  font-size: var(--text-sm);
  min-width: 0;
}
.filters input::placeholder { color: var(--fg-faint); }
.filter-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); align-items: center; }
.filter-actions .count { margin-left: auto; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg-muted); }

/* The dense row's small plate carries no name — at 72px a clamped four-line
   name reads as noise. The district tint alone is enough to tell rows apart,
   and the name is right beside it in full. */
.thumb-sm .thumb-plate { border: 0; }
.thumb-sm .thumb-name { display: none; }

/* --------------------------------------------------------------- drops -- */

/*
  A drop entry is date-on-the-left, objects-on-the-right. It is NOT an object
  row: reusing `.row` here put the date inside a 72px thumbnail column and every
  date wrapped into a four-line fragment.
*/
.drops { list-style: none; display: grid; gap: var(--space-6); }

.drop {
  display: grid;
  grid-template-columns: 13rem minmax(0, 1fr);
  gap: var(--space-6);
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--border);
}
.drop > * { min-width: 0; }
.drop:last-child { border-bottom: 0; padding-bottom: 0; }

.drop-when { display: grid; gap: var(--space-2); align-content: start; }
.drop-date { font-family: var(--font-mono); font-size: var(--text-lg); font-weight: 400; letter-spacing: 0.02em; }
.drop-date a { color: var(--fg); text-decoration: none; }
.drop-date a:hover { color: var(--cyan); text-decoration: underline; }
.drop-count { margin: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-muted); }

.drop-body { display: grid; gap: var(--space-3); align-content: start; }

/* The strip scrolls inside itself rather than wrapping: a drop is a sequence,
   and eighteen objects reflowing into five ragged rows stops reading as one. */
.drop-strip {
  list-style: none;
  display: flex;
  gap: var(--space-2);
  overflow-x: auto;
  scrollbar-width: thin;
  padding-bottom: var(--space-2);
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 24px), transparent 100%);
}
.drop-strip > li { flex: none; }
.drop-strip a { display: block; text-decoration: none; color: inherit; }

.drop-more a {
  display: grid;
  place-items: center;
  width: 104px;
  height: 104px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--fg-muted);
}
.drop-more a:hover { border-color: var(--cyan); color: var(--cyan); }

.drop-who { margin: 0; font-size: var(--text-sm); color: var(--fg-muted); overflow-wrap: anywhere; }
.drop-label {
  display: inline-block;
  min-width: 3.2em;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--fg-faint);
}
.drop-open { margin: var(--space-1) 0 0; font-family: var(--font-mono); font-size: var(--text-sm); }

/* The strip thumbnail: big enough that the object's name is readable in the
   missing-image plate, small enough that eight fit. */
.thumb-strip {
  width: 104px;
  height: 104px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.drop-strip a:hover .thumb-strip { border-color: var(--border-strong); }
.thumb-strip .thumb-plate { border: 0; padding: var(--space-2); }
.thumb-strip .thumb-name {
  font-size: var(--text-xs);
  -webkit-line-clamp: 4;
  line-clamp: 4;
  /* `anywhere` is right for a wide tile and wrong for a 104px one — it split
     "Celestial" into "Celesti / al". Break only words that genuinely cannot
     fit, and let the clamp handle the rest. */
  overflow-wrap: break-word;
  word-break: normal;
  hyphens: auto;
}
.thumb-strip .thumb-note { display: none; }

@media (max-width: 760px) {
  .drop { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
}

/* --------------------------------------------------------- creator tools -- */

.toolgroup { margin-bottom: var(--space-7); }
.toolgroup h3 {
  font-family: var(--font-mono);
  font-size: var(--text-base);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: var(--space-2);
}
.toolgroup > .prose { margin-bottom: var(--space-5); color: var(--fg-muted); }

.tools {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: var(--space-5);
}
.tool {
  min-width: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.tool-name { font-size: var(--text-lg); margin-bottom: var(--space-1); overflow-wrap: anywhere; }
.tool-name a { color: var(--fg); text-decoration: none; }
.tool-name a:hover { color: var(--cyan); text-decoration: underline; }
.tool-tags {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-muted);
}
.tool-cost { color: var(--lime); }
.tool-nolink { color: var(--amber); }
.tool-what { margin: 0 0 var(--space-2); font-size: var(--text-sm); }
.tool-caveat { margin: 0; font-size: var(--text-sm); color: var(--fg-muted); }
.tool-caveat-label {
  display: inline-block;
  margin-right: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--amber);
}

/* What kind of link this is. Amber for the ones that need a second thought --
   a third-party copy, a Wayback capture, no download at all -- and the muted
   meta colour for the ones that go where you would expect. The distinction is
   the point, so it must not be carried by colour alone: the text says it. */
.tool-kind { color: var(--fg-muted); }
.tool-kind-community,
.tool-kind-archived,
.tool-kind-none { color: var(--amber); }
.tool-kind-hosted,
.tool-kind-ours { color: var(--lime); }

.tool-dl {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin: var(--space-3) 0 0;
}
.tool-dl-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-muted);
}
.tool-dl-meta code { font-size: max(13px, 0.92em); }

/* ------------------------------------------------------ reading & links -- */

/* Same shape as .tools deliberately. A reading list and a tool list are the
   same kind of object -- a name, what it is, who made it, a caveat -- and
   giving them two different looks would imply a distinction that is not there. */
.reads {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
  gap: var(--space-5);
}
.read {
  min-width: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.read-title { font-size: var(--text-lg); margin-bottom: var(--space-1); overflow-wrap: anywhere; }
.read-title a { color: var(--fg); text-decoration: none; }
.read-title a:hover { color: var(--cyan); text-decoration: underline; }
.read-tags {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-muted);
}
.read-kind { color: var(--gold); text-transform: uppercase; letter-spacing: var(--tracking-label); }
.read-what { margin: 0 0 var(--space-2); font-size: var(--text-sm); }

/* ------------------------------------------------- return to the top -- */

/* THE STICKY MASTHEAD USED TO EAT EVERY ANCHOR TARGET.

   `.masthead` is `position: sticky; top: 0`, so a jump to `#worldinfo` put the
   section's top edge at y=0 and the masthead then painted over it: measured
   2026-08-20, the "World Info" heading landed at y=65 under a 73px header on
   desktop and at y=49 under a 178px header at 390px. Every one of the sixteen
   `/rules` contents links dropped the reader onto a hidden heading, a page and
   a bit into the wrong content.

   `scroll-margin-top` is the whole fix, and it belongs on the targets rather
   than on the links, so it covers the contents list, the new return links and
   anything anchored later. The value clears the tallest masthead with a little
   air; over-reserving costs nothing because it only applies to scroll
   anchoring. */
:target,
#main,
main section[id],
.tut-page {
  scroll-margin-top: calc(var(--space-6) + 5.5rem);
}
@media (max-width: 900px) {
  :target,
  #main,
  main section[id],
  .tut-page { scroll-margin-top: calc(var(--space-6) + 11.5rem); }
}

/* Deliberately a hairline affordance and not a widget: no fixed position, no
   floating button, nothing that covers content or needs script. It sits at the
   end of a section, in the reading flow, where a reader who has finished one
   already is. 13px is the type floor, and it is a floor, not a target -- the
   tap area is padded out to 44px rather than the text being shrunk to suit. */
.to-top {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  min-height: 44px;
  padding: var(--space-2) 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  color: var(--fg-muted);
  text-decoration: none;
}
.to-top:hover { color: var(--cyan); text-decoration: underline; }

/* Right-aligned at the end of its section, so it never sits in the reading
   column and never competes with the next heading. `width: fit-content` with an
   auto left margin does the alignment WITHOUT stretching the anchor: a
   full-width link would give the page a 1,300px-wide invisible click target
   along every section boundary. */
.section > .to-top,
.tut-page > .to-top {
  width: fit-content;
  margin-top: var(--space-5);
  margin-left: auto;
}

/* ------------------------------------------------------------ tutorials -- */

/* The hairline grid travels with the CARDS, not behind them.

   This used to paint `background: var(--border)` on the container and let it
   show through a 1px gap. That works only when the last row is full. There are
   eleven tutorials, so at three columns the last row holds two and at two
   columns it holds one -- and the leftover slot showed the container's own
   #2A1F4A straight through, a filled purple rectangle exactly the size of a
   card. It read as a twelfth tutorial that had failed to load.

   Giving each card a 1px outset shadow instead means an empty slot has nothing
   to paint it. Adjacent cards' shadows meet inside the 1px gap, and the cards on
   the outside draw the grid's outer edge themselves -- which is why the
   container's own border went too. A border on the container would still have
   framed the leftover slot, so the last row would read as a card that had lost
   its contents rather than as a row that simply ends. Every seam is still
   exactly 1px. No placeholder element, no fake card, nothing focusable added. */
.tuts {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
  gap: 1px;
}
.tuts > li {
  min-width: 0;
  background: var(--surface-1);
  box-shadow: 0 0 0 1px var(--border);
}

.tut-link {
  display: grid;
  grid-template-columns: 3.4rem minmax(0, 1fr);
  gap: var(--space-4);
  padding: var(--space-4) var(--space-5);
  min-height: 44px;
  color: inherit;
  text-decoration: none;
}
.tut-link:hover { background: var(--surface-2); }
.tut-num {
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  color: var(--gold);
  line-height: 1.1;
}
.tut-body { display: grid; gap: var(--space-1); min-width: 0; }
.tut-title { font-weight: 650; overflow-wrap: anywhere; }
.tut-link:hover .tut-title { color: var(--cyan); text-decoration: underline; }
.tut-summary { font-size: var(--text-sm); color: var(--fg-muted); }
.tut-meta { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-faint); }

/* The reading page. Prose is measured; code is not, because VRML lines are
   long and a reflowed snippet is a snippet that lies. */
.tut-page .tut-body-copy > p,
.tut-page .tut-list { max-width: var(--measure); }
.tut-body-copy > p { margin-bottom: var(--space-4); }
.tut-h {
  font-size: var(--text-lg);
  color: var(--gold);
  margin: var(--space-7) 0 var(--space-3);
}
.tut-h:first-child { margin-top: 0; }
.tut-list { margin: 0 0 var(--space-4) var(--space-5); display: grid; gap: var(--space-2); }
.tut-list li { padding-left: var(--space-1); }

.tut-callout {
  margin: var(--space-5) 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--lime);
}

.tut-code {
  margin: 0 0 var(--space-5);
  overflow-x: auto;
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-left: 3px solid var(--lime);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
.tut-code pre { margin: 0; padding: var(--space-4) var(--space-5); }
.tut-code code {
  display: block;
  white-space: pre;
  font-size: max(13px, 0.86em);
  line-height: 1.55;
  color: var(--fg);
}

@media (max-width: 640px) {
  .tut-link { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
  .tut-code pre { padding: var(--space-3); }
}

/* -------------------------------------------------------- archive posts -- */

.post-wrap { display: grid; gap: var(--space-2); min-width: 0; }
.post-title { font-size: var(--text-base); font-weight: 650; overflow-wrap: anywhere; }
.post-meta { margin: 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-muted); }
.post-source { margin: var(--space-3) 0 0; font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-faint); overflow-wrap: anywhere; }

/* The no-JavaScript path. `<details>` is what the server sends and what a
   reader gets if public/js/archive.js never arrives. */
details.post > summary {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--cyan);
  cursor: pointer;
  list-style: none;
}
details.post > summary::-webkit-details-marker { display: none; }
details.post > summary::before { content: '▸ '; }
details.post[open] > summary::before { content: '▾ '; }
details.post .post-content { padding-top: var(--space-3); max-width: var(--measure); }
details.post .post-content p { margin-bottom: var(--space-3); }

/* The /mall edit log, disclosed rather than dropped. Styled off the same
   pattern as `details.post` above so the two disclosures on this site look like
   one idea; 44px here rather than that one's 40px because this is new. */
.prov-details { margin-top: var(--space-4); }
.prov-details > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--cyan);
  cursor: pointer;
  list-style: none;
}
.prov-details > summary::-webkit-details-marker { display: none; }
.prov-details > summary::before { content: '▸'; }
.prov-details[open] > summary::before { content: '▾'; }
/* The count is not colour-coded and not an icon: it is the words "16 changes",
   so the summary says how much is behind it without being opened. */
.prov-count { color: var(--fg-muted); font-size: var(--text-xs); }
.prov-details > .table-scroll { margin-top: var(--space-4); }

/* The enhanced path: the same words, in a dialog. */
.post-open {
  align-self: start;
  min-height: 40px;
  padding: var(--space-1) var(--space-3);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--cyan);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  text-align: left;
  cursor: pointer;
}
.post-open::before { content: '▸ '; }
.post-open:hover { border-color: var(--cyan); background: var(--cyan-quiet); }

.post-dialog {
  width: min(72ch, calc(100vw - 2 * var(--space-5)));
  max-height: min(80dvh, 900px);
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface-1);
  color: var(--fg);
  box-shadow: var(--shadow-2);
}
.post-dialog::backdrop { background: rgb(0 0 0 / 0.72); backdrop-filter: blur(3px); }
.post-dialog-in { display: grid; gap: var(--space-4); padding: var(--space-6); }
.post-dialog-head { display: flex; align-items: center; gap: var(--space-4); }
.post-dialog-kind {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--amber);
}
.post-dialog-close {
  margin-left: auto;
  min-height: 40px;
  padding: var(--space-1) var(--space-4);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  cursor: pointer;
}
.post-dialog-close:hover { border-color: var(--cyan); color: var(--cyan); }
.post-dialog-title { font-size: var(--text-lg); overflow-wrap: anywhere; }
.post-dialog-body { overflow-y: auto; max-height: 56dvh; }
.post-dialog-body:focus { outline: none; }
.post-dialog-body p { margin-bottom: var(--space-4); }
.post-dialog-source {
  margin: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg-faint);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------ archive sections -- */

.chapters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: var(--space-5);
  margin-top: var(--space-5);
}
.chapter {
  min-width: 0;
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  display: grid;
  gap: var(--space-2);
  align-content: start;
}
.chapter-num {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--amber);
}

.credits {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
.credit-name {
  display: inline-block;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: var(--text-base);
  color: var(--gold);
  overflow-wrap: anywhere;
}
.credit-design { color: var(--fg-muted); font-size: var(--text-sm); }

/* The tool lineage: three entries, a year gutter, no boxes. */
.lineage { list-style: none; display: grid; gap: var(--space-5); }
.lineage > li {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  gap: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.lineage > li:last-child { border-bottom: 0; padding-bottom: 0; }
.lineage > li > * { min-width: 0; }
.lineage-when { margin: 0; font-family: var(--font-mono); font-size: var(--text-lg); color: var(--amber); }
.lineage h3 { font-size: var(--text-lg); margin-bottom: var(--space-2); }
.lineage p { max-width: var(--measure); color: var(--fg-muted); }

/* The old front page's news feed: a date gutter and a post, no boxes. */
.feed { list-style: none; display: grid; gap: var(--space-5); margin-bottom: var(--space-6); }
.feed-post {
  display: grid;
  grid-template-columns: 11rem minmax(0, 1fr);
  gap: var(--space-5);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--border);
}
.feed-post > * { min-width: 0; }
.feed-post:last-child { border-bottom: 0; padding-bottom: 0; }
.feed-when { margin: 0; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--amber); }

@media (max-width: 640px) {
  .feed-post { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
}

.front-notice {
  margin: 0;
  padding: var(--space-5) var(--space-6);
  background: var(--surface-sunk);
  border: 1px solid var(--border);
  border-left: 3px solid var(--amber);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  max-width: var(--measure);
}
.front-notice p { margin: 0 0 var(--space-2); }
.front-dateline {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--amber);
  margin-bottom: var(--space-4) !important;
}
.front-notice footer { margin-top: var(--space-4); font-family: var(--font-mono); font-size: var(--text-xs); color: var(--fg-faint); }

@media (max-width: 640px) {
  .lineage > li { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); }
  .post-dialog { width: 100vw; max-width: 100vw; max-height: 92dvh; border-radius: 0; border-inline: 0; }
  .post-dialog-in { padding: var(--space-5) var(--space-4); }
}

/* ============================================================================
   TRANSITIONAL — classes still used by pages not yet moved onto sectionHead().
   These exist so the site is coherent between phases, not as a second system.
   Every rule here is deleted as its page is redesigned; if this block is empty,
   delete the block.
   ============================================================================ */

.eyebrow {
  margin: 0 0 var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-sign);
  text-transform: uppercase;
  color: var(--lime);
}
.eyebrow::before { content: '> '; opacity: 0.7; }

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3) var(--space-5);
  margin-bottom: var(--space-5);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
.section-head h2,
.section-head h3 { font-size: var(--text-xl); }
.section-head .more { margin-left: auto; font-family: var(--font-mono); font-size: var(--text-sm); }

.lede { color: var(--fg-muted); max-width: var(--measure); }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 240px), 1fr));
  gap: var(--space-5);
}
.grid > * { min-width: 0; }

.hint { display: block; font-family: var(--font-mono); font-size: var(--text-xs); letter-spacing: var(--tracking-label); text-transform: uppercase; color: var(--fg-muted); margin-bottom: var(--space-2); }

.empty { padding: var(--space-7) var(--space-5); text-align: center; color: var(--fg-muted); border: 1px dashed var(--border); border-radius: var(--radius-md); }

/* ------------------------------------------------------------ rule lists -- */

/*
  LISTS UNDER A HEADING, INDENTED; THE HEADING FLUSH (Ryan/LSS, 2026-09-08).

  The global reset above zeroes list padding so that the many lists this site
  styles as grids -- tiles, shops, chips -- start from nothing. Prose lists
  were caught by the same reset: on /help every bullet marker sat at the
  heading's own left edge and a wrapped rule wrapped back under the marker,
  which is what the owner's screenshots showed. This block gives a prose list
  its indent back with ordinary padding on the list, so the marker hangs in the
  gutter and every wrapped line aligns with the first line of the rule text.
  No negative margin anywhere: the heading is not pulled left to meet the
  bullets; the bullets are pushed right under the heading.

  `--space-5` (24px) is enough for a two-digit ordered marker at 15px body size
  and still leaves a 390px phone with a readable measure. Nested lists step in
  by the same amount and lose their outer margin so they read as part of the
  item above them.
*/
.prose ul,
.prose ol,
.panel ul,
.panel ol,
.advanced ul,
.advanced ol,
.toc {
  list-style-position: outside;
  padding-left: var(--space-5);
  margin: 0 0 var(--space-4);
}
.prose li,
.panel li,
.advanced li,
.toc li { margin-bottom: var(--space-2); }
.prose li:last-child,
.panel li:last-child,
.advanced li:last-child,
.toc li:last-child { margin-bottom: 0; }
.prose ul ul, .prose ul ol, .prose ol ul, .prose ol ol,
.panel ul ul, .panel ol ol,
.advanced ul ul, .advanced ol ol { margin: var(--space-2) 0 0; }
.prose ul:last-child, .prose ol:last-child { margin-bottom: 0; }

/* The heading above a rule list is a subheading of the section, not a list
   item, so it keeps the section's left edge and sits close to its list. */
.rules h3 { margin: var(--space-5) 0 var(--space-2); }
.rules h3:first-child { margin-top: 0; }
.rules > ul + h3, .rules > ol + h3, .rules > p + h3 { margin-top: var(--space-5); }

/* The "see also" line under a FAQ answer: links, separated, at body size. */
.faq-see { font-size: var(--text-sm); margin-top: calc(-1 * var(--space-2)); }
