/* ============================================================
   LOOP8 — design system
   Light only. Neutral grotesque type, sentence case, generous
   whitespace, hairline rules, minimal chrome.
   ============================================================ */

/* TT Squares Condensed Thin (TypeType) — this is the DEMO cut, licensed for
   personal use only. Fine for local preview, but check TypeType's EULA (or
   buy a webfont license) before this ships on the live site — foundry demo
   fonts are almost never cleared for web embedding or commercial use. */
@font-face {
  font-family: 'TT Squares Condensed Thin DEMO';
  src: url('../assets/fonts/tt-squares-condensed-thin-demo.woff2') format('woff2');
  font-weight: 100;
  font-style: normal;
  font-display: swap;
}

/* DearPlotter (Licia He, eyesofpanda.com/project/dearplotter_font) — the
   "chalk-newsprint" generated variant, exported as TTF. Clean to use: "the
   generator is open source, and fonts generated by the system are free to
   use for any purposes" per the project page.

   dearplotter-chalk-newsprint.woff2 below is that export, unmodified —
   kept as the source for tools/facet-font.py, not used directly anywhere.

   dearplotter-faceted.woff2 is what's actually live: the same font run
   through tools/facet-font.py --simplify 22 --jitter 0, which decimates
   its already-dense straight-line outlines (confirmed via fontTools: the
   export has zero curves, every point on-curve) down to fewer, longer
   edges — the same low-poly technique Licia He's own site uses for its
   headings. jitter 0 was the chosen result after comparing 0/8/16/24 side
   by side — see git history for that comparison and the rejected values. */
@font-face {
  font-family: 'DearPlotter Chalk Newsprint';
  src: url('../assets/fonts/dearplotter-chalk-newsprint.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
/* Hershey Simplex — the actual 1967 US Naval Weapons Laboratory vector font
   data (public domain), decoded from Lingdong Huang's MIT-licensed
   p5.hershey.data.js port (tools/hershey-data/) with tools/hershey-to-font.py,
   which buffers each pen stroke into a filled ribbon via shapely (see that
   script's header for the full technique).

   This is the "Pen" variant: --pen-sim mode, which additionally subdivides
   every stroke into short sub-segments, nudges each point's position and
   width slightly at random (simulating a real pen dragged over paper), and
   drops a circle at the start of every discrete stroke to mark the pen
   touching down. Final settings, after several rounds of live comparison:
   --segment-length 1.0 --xy-jitter 0.12 --width-jitter 0.3 --tip-scale 1.39
   (tip-scale is the touchdown circle's size as a multiple of the local
   stroke width right there — tried 1.15/1.25/1.5 first; 1.39 was the pick).

   Superseded TT Squares Faceted as the title font — see git history if
   that one (or the CSS text-stroke weight approach that went with it) is
   ever needed again; it doesn't layer sensibly on top of these organic,
   already-irregular pen-sim outlines, so it isn't applied here. */
@font-face {
  font-family: 'Hershey Simplex Pen';
  src: url('../assets/fonts/hershey-simplex-pen.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Brand spots — used as details against white, never as the ground */
  --coral:   #DF594F;
  --blue:    #0075B2;
  --sand:    #FFE4BD;

  /* Surfaces — white page, like the reference */
  --bg:      #FFFFFF;
  --surface: #FAFAFA;   /* quiet panel fill */
  --raised:  #FFFFFF;
  --band:    #FFE4BD;   /* sand, for the occasional full-width band */
  --line:    #E6E6E6;
  --line-2:  #CFCFCF;

  /* Ink — matching the reference: #333 body, #aaa muted */
  --ink:     #333333;
  --ink-2:   #6E6E6E;
  --ink-3:   #AAAAAA;

  /* The 3D stage stays a neutral grey so part colours read true
     rather than picking up a cast from the cream page. */
  --stage-1: #F1EFEC;
  --stage-2: #DEDBD6;

  /* The reference sets Akkurat Pro (Lineto, commercial) falling back to
     Helvetica/Arial. Inter stands in as the closest freely-licensed neutral
     grotesque — swap it here if Akkurat gets licensed. */
  --font: 'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  --font-squares: 'TT Squares Condensed Thin DEMO', var(--font);
  --font-title: 'Hershey Simplex Pen', var(--font);

  /* A little extra title weight on top of the pen-sim font's own
     irregularity, requested after seeing it at 0. Still just a dial —
     change (or zero out) this one value any time. */
  --title-stroke-width: 1.5px;

  --nav-h: 70px;
  --gutter: 20px;
  --maxw: 1200px;
}

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

html {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-title);   /* sitewide default, not just headings —
                                        everything inherits this unless a
                                        rule overrides it (var(--font),
                                        i.e. Inter, is now the exception) */
  font-size: 20px;   /* reference body size, 30px leading */
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  scroll-behavior: smooth;
}

body { min-height: 100vh; }
body.viewer-expanded { overflow: hidden; }

img, svg, canvas { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

/* Regular weight, no tracking games, no uppercase — the reference sets
   text-transform:none explicitly and leans on size and space instead.
   font-family: Hershey Simplex Pen (see the @font-face block up top for
   how it was generated) — chosen for headings after comparing it live.
   text-stroke-width reads from --title-stroke-width, 0 by default — the
   option to add weight stays live (just set that one variable) without a
   uniform stroke muddying these already-irregular pen-sim edges by default.
   text-stroke-color: currentColor, not a hardcoded --ink, so the same
   stroke works correctly wherever a heading's own color is overridden —
   e.g. white text over a photo needs a white stroke, not an ink one; this
   way it just follows whatever color is already set, everywhere. */
h1, h2, h3, h4 {
  font-family: var(--font-title);
  font-weight: 400;
  -webkit-text-stroke-width: var(--title-stroke-width);
  -webkit-text-stroke-color: currentColor;
  letter-spacing: -0.01em;
  line-height: 1.14;
  color: var(--ink);
}

h1 { font-size: clamp(32px, 3.6vw, 52px); letter-spacing: -0.02em; }
h2 { font-size: clamp(25px, 2.2vw, 34px); }
h3 { font-size: 20px; letter-spacing: 0; line-height: 1.35; }

p { max-width: 62ch; }

/* Apply where wanted: <h1 class="font-squares">, a span, etc. Thin weight
   only (this cut has no other weights), so pair it with a decent size —
   it disappears at body-copy sizes. */
.font-squares { font-family: var(--font-squares); font-weight: 100; }

.eyebrow {
  display: inline-block;
  font-size: 15px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--coral);
  margin-bottom: 14px;
}

.container {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* ── NAV ── */
.site-nav {
  position: sticky;
  top: 0;
  z-index: 200;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.site-nav .container {
  display: flex;
  align-items: center;
  gap: 40px;
  width: 100%;
}

.brand {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.brand img {
  display: block;
  height: 36px;
  width: auto;
}

.nav-links {
  display: flex;
  gap: 30px;
  margin-left: auto;
  align-items: center;
}

.nav-links a {
  font-size: 17px;
  color: var(--ink-2);
  transition: color 0.15s;
  position: relative;
  padding: 4px 0;
}
.nav-links a:hover { color: var(--ink); }
.nav-links a.active { color: var(--ink); }
.nav-links a.active::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 1.5px;
  background: var(--coral);
}

.nav-toggle {
  display: none;
  margin-left: auto;
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink);
  font-family: inherit;
  font-size: 14px;
  padding: 8px 18px;
  cursor: pointer;
}

/* ── BUTTONS ── */
.btn {
  /* <button> doesn't inherit font-family from the page by default (the
     browser's own UI font wins unless told otherwise) — same reason
     .nav-toggle below needs this explicitly too. */
  font-family: inherit;
  font-size: 17px;
  letter-spacing: 0;
  padding: 12px 26px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
  transition: all 0.18s;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  white-space: nowrap;
}
.btn:hover { border-color: var(--ink); }

.btn.primary {
  background: var(--coral);
  border-color: var(--coral);
  color: #fff;
}
.btn.primary:hover { background: #c94a41; border-color: #c94a41; }

.btn.active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bg);
}

.btn:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; }

/* ── PRICE ── */
.price {
  font-size: 32px;
  font-weight: 400;
  letter-spacing: -0.02em;
  color: var(--ink);
}
.price-line { margin-top: 28px; display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.price-note { font-size: 13px; color: var(--ink-3); }
.price-row { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; margin-bottom: 22px; }

/* ── SECTIONS ── */
.section { padding: 90px 0; border-bottom: 1px solid var(--line); }
.section.tight { padding: 70px 0; }
/* One sand band per page at most — the spot colour as a full-width moment
   rather than a background wash. */
.section.on-surface { background: var(--band); border-bottom-color: #EFD4AE; }
.section.on-surface .card { background: var(--band); }
.section.on-surface .grid { background: #EFD4AE; border-color: #EFD4AE; }

.section-head { max-width: 760px; margin-bottom: 56px; }
.section-head p { color: var(--ink-2); margin-top: 18px; }

/* ── HERO ── */
/* No longer the first thing on the page — the media hero above holds the
   full viewport, so this just needs room to breathe. */
.hero {
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--line);
}

.hero .container {
  display: grid;
  grid-template-columns: 1.05fr 1fr;
  gap: 72px;
  align-items: center;
  padding-top: 80px;
  padding-bottom: 80px;
}

.hero-copy p { color: var(--ink-2); margin-top: 22px; }
.hero-copy .btn-row { margin-top: 36px; }

.hero-stage {
  aspect-ratio: 1 / 1;
  border: 1px solid var(--line);
  background: var(--raised);
  position: relative;
}
.hero-stage:has(.viewer-panel.expanded) { border: none; background: none; }

/* ── CARD GRID ── */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
}
/* For a grid whose card count is known and fixed (not auto-fit), so an odd
   count never leaves a dangling empty cell at in-between widths. */
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 700px) {
  .grid.cols-4 { grid-template-columns: repeat(2, 1fr); }
}
.card { background: var(--bg); padding: 40px 34px; }

/* #post-grid grows over time (one card per journal post) and uses plain
   auto-fit .grid, not a fixed .cols-N -- so unlike every other .grid on
   the site, its post count can't be guaranteed to fill a whole row. The
   normal .grid hairline trick (cards sit on a shared gray background,
   showing through the 1px gaps between them) then also shows that gray
   through any UNFILLED cells in a short last row, since there's no card
   there to cover it. Giving cards their own border instead, with the gap
   collapsed to 0, means an empty grid track has nothing drawing into it
   at all -- transparent, not gray -- while real cards still show the
   same hairline via touching/overlapping borders. */
#post-grid { background: transparent; gap: 0; }
#post-grid .card { border: 1px solid var(--line); margin: -1px 0 0 -1px; }

/* Same fix, same reason: #cart-features-grid has a fixed count (4) but
   auto-fit's column count still depends on viewport width, so at some
   widths (3 columns here, for instance) the last row comes up short and
   the shared-background hairline trick shows gray through the gap. Any
   .grid with a card count that isn't guaranteed to fill whatever column
   count auto-fit lands on needs this same treatment -- see #post-grid's
   own comment above for the full mechanism. */
#cart-features-grid { background: transparent; gap: 0; }
#cart-features-grid .card { border: 1px solid var(--line); margin: -1px 0 0 -1px; }
.card .eyebrow { margin-bottom: 18px; }
.card p { color: var(--ink-2); font-size: 15px; margin-top: 10px; }
.card .spec-val { font-size: 22px; font-weight: 400; letter-spacing: -0.01em; color: var(--ink); }

/* Bleeds to the card's edges, ignoring the card's own padding — swap the
   placeholder for a real photo by replacing the file, no markup change. */
.card-media { margin: -40px -34px 24px; }
.card-media img,
.card-media video { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; }

/* Scannable at this size on a normal phone. The radius matches the rounded
   corner already baked into Signal's own export, hiding the pale square
   corners left behind by cropping the tile out of it. */
.qr-code {
  display: block;
  width: 132px;
  height: auto;
  margin-top: 18px;
  border-radius: 11px;
}

/* ── PRESET ROW — sits across the top of the viewer, one click sets every
   colour at once. Each swatch mimics a striped cassette-spine label. ── */
.preset-row {
  display: flex;
  gap: 8px;
  padding: 14px;
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
}

.preset-btn {
  flex: 1 1 0;
  min-width: 84px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 7px;
  background: transparent;
  border: 1px solid var(--line-2);
  border-radius: 4px;
  padding: 7px;
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.preset-btn:hover { border-color: var(--ink-2); }
.preset-btn.active { border-color: var(--coral); box-shadow: 0 0 0 1px var(--coral); }

.preset-swatch {
  display: flex;
  width: 100%;
  height: 20px;
  border-radius: 2px;
  overflow: hidden;
}
.preset-swatch span { flex: 1; }

.preset-name {
  font-size: 12px;
  color: var(--ink-2);
  text-align: center;
}
.preset-btn.active .preset-name { color: var(--ink); }

/* ── VIEWER ── */
.viewer-panel { border: 1px solid var(--line); background: var(--raised); position: relative; }

.viewer-stage {
  width: 100%;
  aspect-ratio: 4 / 3;
  position: relative;
  cursor: grab;
  background: radial-gradient(circle at 50% 42%, var(--stage-1) 0%, var(--stage-2) 72%);
}
.viewer-stage:active { cursor: grabbing; }
.viewer-stage canvas { width: 100% !important; height: 100% !important; display: block; }

.viewer-loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  color: var(--ink-3);
  pointer-events: none;
}

.viewer-controls {
  display: flex;
  gap: 10px;
  padding: 16px;
  border-top: 1px solid var(--line);
  flex-wrap: wrap;
  justify-content: space-between;
}
.viewer-controls .btn-row { flex-wrap: wrap; align-items: center; gap: 8px; }
.viewer-controls .btn { font-size: 13px; padding: 9px 17px; }

.view-hint { font-size: 13px; color: var(--ink-3); align-self: center; }

.btn.icon-btn { padding: 9px 12px; line-height: 0; }
.btn.icon-btn svg { display: block; }

/* Expand — model only, page stays visible behind it */
.viewer-panel.expanded {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 92vw;
  height: 90vh;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  background: transparent;
  border: none;
}
.viewer-panel.expanded .viewer-stage { flex: 1; aspect-ratio: auto; background: none; }
.viewer-panel.expanded .viewer-controls { background: transparent; border-top: none; }

.viewer-close {
  display: none;
  position: absolute;
  top: 0;
  right: 0;
  width: 46px;
  height: 46px;
  align-items: center;
  justify-content: center;
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink);
  cursor: pointer;
  z-index: 2;
}
.viewer-panel.expanded .viewer-close { display: flex; }

.hero-viewer-panel { border: none; height: 100%; display: flex; flex-direction: column; }
.hero-viewer-panel .viewer-stage { flex: 1; aspect-ratio: auto; }

/* ── EXPLODE ── */
.explode-row { display: flex; align-items: center; gap: 16px; padding: 0 16px 16px; }

.explode-row input[type="range"],
.slider-row input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 2px;
  background: var(--line-2);
  cursor: pointer;
}
.explode-row input[type="range"]::-webkit-slider-thumb,
.slider-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 15px; height: 15px;
  border-radius: 999px;
  background: var(--coral);
  border: none;
  cursor: pointer;
}
.explode-row input[type="range"]::-moz-range-thumb,
.slider-row input[type="range"]::-moz-range-thumb {
  width: 15px; height: 15px;
  border-radius: 999px;
  background: var(--coral);
  border: none;
  cursor: pointer;
}
.explode-value { font-size: 13px; color: var(--ink-3); min-width: 40px; text-align: right; }

/* ── ADMIN SLIDER PANEL ── */
.slider-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
}
.slider-row:last-child { border-bottom: none; }
.slider-row label { flex: 0 0 150px; font-size: 13px; color: var(--ink-2); }
.slider-row .slider-value { font-size: 13px; color: var(--ink-3); min-width: 42px; text-align: right; }

/* ── CONFIGURATOR ── */
.configurator { display: grid; grid-template-columns: 1fr 380px; gap: 28px; align-items: start; }

.config-panel { border: 1px solid var(--line); background: var(--surface); }
.config-panel-head { padding: 22px 24px; border-bottom: 1px solid var(--line); }
.config-panel-head .sub { font-size: 14px; color: var(--ink-3); margin-top: 6px; line-height: 1.45; }

/* Name on the left, its palette inline on the right. The swatch is the whole
   interaction — no selection step, no separate palette, no colour names. */
.colour-list { padding: 6px 0; }

.colour-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 11px 24px;
  transition: background 0.3s;
}
.colour-row + .colour-row { border-top: 1px solid var(--line); }
.colour-row.flash { background: rgba(223, 89, 79, 0.12); }

.colour-row-name {
  font-size: 16px;
  color: var(--ink);
  white-space: nowrap;
}

.colour-row-swatches {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.colour-chip {
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.16);
  border-radius: 3px;
  cursor: pointer;
  position: relative;
  transition: transform 0.12s;
}
.colour-chip:hover { transform: scale(1.18); }
.colour-chip:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Selected gets a ring that reads on both light and dark swatches. */
.colour-chip.selected {
  border-color: var(--ink);
  box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px var(--ink);
}

@media (max-width: 420px) {
  .colour-row { flex-direction: column; align-items: flex-start; gap: 10px; }
  .colour-row-swatches { justify-content: flex-start; }
}

/* ── INLINE SLIDESHOW ── */
.slideshow {
  position: relative;
  width: 100%;
  max-width: 512px;
  background: var(--surface);
  border: 1px solid var(--line);
}

.slideshow-track {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
}

.slideshow-track img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 0.7s ease;
}
.slideshow-track img.current { opacity: 1; }

.slideshow-status {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  color: var(--ink-3);
}

.slideshow-dots {
  display: flex;
  gap: 6px;
  justify-content: center;
  flex-wrap: wrap;
  padding: 12px;
  border-top: 1px solid var(--line);
}
.slideshow-dots button {
  width: 6px;
  height: 6px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: var(--line-2);
  cursor: pointer;
  transition: background 0.2s;
}
.slideshow-dots button:hover { background: var(--ink-3); }
.slideshow-dots button.active { background: var(--coral); }

/* Click zones — invisible, full height, half the width each. The arrows are
   purely decorative; the zone behind them is what takes the click. */
.slideshow-zone {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  z-index: 3;
  -webkit-tap-highlight-color: transparent;
}
.slideshow-zone.prev { left: 0; }
.slideshow-zone.next { right: 0; }
.slideshow-zone:focus-visible { outline: 2px solid var(--coral); outline-offset: -4px; }

.slideshow-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 52px;
  height: 52px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.32);
  color: #fff;
  pointer-events: none;   /* the zone underneath handles it */
  opacity: 0;
  transition: opacity 0.2s;
  z-index: 4;
  backdrop-filter: blur(3px);
}
.slideshow-arrow.prev { left: 22px; }
.slideshow-arrow.next { right: 22px; }

.slideshow:hover .slideshow-arrow { opacity: 1; }
.slideshow-zone.prev:hover ~ .slideshow-arrow.prev,
.slideshow-zone.next:hover ~ .slideshow-arrow.next { background: rgba(0, 0, 0, 0.55); }

/* Touch has no hover, so keep them permanently visible there. */
@media (hover: none) {
  .slideshow-arrow { opacity: 0.85; }
}

/* ── VIDEO — click-to-load facade, see js/video-embed.js. The poster is
   self-hosted so nothing is requested from YouTube until play is pressed. ── */
.video-embed {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #111;
  border: 1px solid var(--line);
  overflow: hidden;
}
.video-embed img,
.video-embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.video-embed img { object-fit: cover; }

.video-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 76px;
  height: 76px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: rgba(223, 89, 79, 0.92);
  color: #fff;
  cursor: pointer;
  transition: background 0.2s, transform 0.2s;
}
.video-play:hover { background: var(--coral); transform: translate(-50%, -50%) scale(1.06); }
.video-play:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
/* Optical centring — a triangle looks off-centre when geometrically centred. */
.video-play svg { display: block; margin-left: 4px; }

@media (max-width: 560px) {
  .video-play { width: 60px; height: 60px; }
}

/* ── MECHANISM DEMO ──────────────────────────────────────────
   Three panes over one canvas. Portrait stacks the crank views
   under the eyepiece; from 900px the eyepiece takes the left and
   the two crank views stack beside it. The scissor rectangles in
   js/demo.js are read from the panes' live bounds, so the layout
   can move without the renderer knowing anything about it. ── */
.mech {
  position: relative;
  display: grid;
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  grid-template-areas:
    "eye persp"
    "jog jog";
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  aspect-ratio: 3 / 4;
  max-width: 560px;
  margin: 0 auto;
  overflow: hidden;
}

@media (min-width: 900px) {
  .mech {
    grid-template-areas:
      "eye persp"
      "eye jog";
    grid-template-columns: 1.35fr 1fr;
    aspect-ratio: 16 / 9;
    max-width: none;
  }
}

.mech-pane { position: relative; z-index: 1; min-width: 0; min-height: 0; }

/* The eyepiece is opaque and covers the canvas — nothing is rendered behind
   it. The two 3D panes MUST stay transparent: they sit above the canvas and
   only exist to be measured for scissor rectangles and to catch pointer
   events. Give either of them a background and it hides the model. */
.mech-eye {
  grid-area: eye;
  background: #0b0b0b;
  overflow: hidden;
  /* Centres .mech-frame when its locked aspect ratio doesn't match this
     pane's own box shape — see .mech-frame below for why that's routine,
     not a bug. */
  display: flex;
  align-items: center;
  justify-content: center;
}
.mech-persp { grid-area: persp; background: transparent; }
.mech-jog {
  grid-area: jog;
  background: transparent;
  cursor: grab;
  touch-action: none;      /* no scroll/pan stealing the gesture */
  user-select: none;       /* a text-selection drag also interrupts it */
  -webkit-user-select: none;
}
.mech-jog:active { cursor: grabbing; }

/* Sits behind the panes and draws only into the two scissored rectangles.
   Its own background is what shows around the model in those panes. */
.mech-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  background: var(--raised);
}

/* Black mat frame, sized and centred exactly like the old .mech-frame did —
   locked to the real Super 8 gate ratio (5.46 x 4.01mm, as 546:401 to keep
   the fraction exact) rather than stretching to fill .mech-eye's box.
   width:100% + max-height:100% (not height:100%, which would over-constrain
   and disable aspect-ratio entirely) lets it shrink to fit and centre via
   the flex parent on .mech-eye. tools/build-demo-atlas.sh crops source
   frames to this exact ratio, so this rule and that script must be changed
   together. */
.mech-frame-outer {
  position: relative;
  aspect-ratio: 546 / 401;
  width: 100%;
  max-height: 100%;
  background: #000;
}

/* The sprite window, and the one non-obvious block of CSS in this file.
   Sized as a hand-computed PERCENTAGE OF .mech-frame-outer, not via
   aspect-ratio + max-height + auto — tried three variations of that
   (flex item, absolute+margin:auto, plain block with only max-width/
   max-height) and none of them correctly shrank a non-replaced element's
   WIDTH back down after MAX-HEIGHT clamped its aspect-ratio-derived
   height. Each left it stretched to fill the available box instead of
   letterboxing within it. A fixed percentage sidesteps that class of bug
   entirely — plain definite width/height on an absolutely positioned,
   inset:0 + margin:auto box is about as reliably-supported as CSS gets.

   The trade this makes, and it is a real one: a flat pad-of-width inset
   on a 546:401 box does NOT leave a 546:401 opening behind — top/bottom
   lose the same absolute amount as left/right, which is a bigger bite
   out of the shorter dimension, leaving an opening wider than the
   image's true 1.36:1. Stretching the sprite to fill that opening would
   silently reintroduce the exact distortion the aspect-ratio work
   elsewhere on this page exists to prevent — so instead the image keeps
   its true ratio and ends up letterboxed: at the pad below, LEFT/RIGHT
   mat reads a little wider than TOP/BOTTOM (opening is height-bound, so
   only the sides pick up the extra). Tuned live with a temporary slider
   (see git history around the point that slider was removed) rather
   than picked by eye — 6.5% read as the right balance of image area vs
   visible mat.

   The maths (outer width/height treated as 1 unit, R = 546/401,
   pad = 0.065 — recompute by hand if either changes, or bring the
   slider back temporarily):
     opening       = (1 - 2*pad)  x  (1/R - 2*pad)        = 0.87 x 0.6044
     opening ratio = 0.87 / 0.6044 = 1.439  ( > R, so HEIGHT-bound )
     frame size    = 1 - 2*pad*R  = 82.2993%  of outer width AND height */
/* No `filter` here on purpose. An always-on SVG filter forces every repaint
   of this element through the filter pipeline — expensive, since the
   background is scaled to 600%. js/demo.js attaches it only while the gate
   blur is actually non-zero. */
.mech-frame {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 82.2993%;
  height: 82.2993%;
  border-radius: 4%;        /* small, and scales with the frame rather than
                                looking chunky on mobile / tiny on desktop */
  overflow: hidden;         /* is what actually rounds the visible corners */
  background-repeat: no-repeat;
  background-size: 600% 600%;
  image-rendering: auto;
}

/* "24/108"-style readout, upper-left of the film frame itself (not the
   wider eyepiece pane) so it sits over the image the way an in-camera
   counter would. tabular-nums keeps it from jittering width as digits
   change; 50% opacity + pointer-events:none per request — a readout, not
   a control. */
.mech-frame-counter {
  position: absolute;
  top: 8px;
  left: 10px;
  z-index: 2;
  color: #fff;
  opacity: 0.5;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* Loading pinwheel, centred over the frame. The low-res preview atlas
   (see previewUrl() in demo.js) is rough enough on its own — mid-motion
   blockiness reads as broken rather than "still loading" — so this spins
   on top of it until the matching full-res atlas has actually loaded.
   js/demo.js toggles .visible per atlas, in showFrame(). z-index matches
   .mech-frame-counter — both are non-interactive overlays that only make
   sense once .mech-dark's opaque boot veil (z-index 3) has faded out. */
.mech-frame-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 28px;
  height: 28px;
  margin: -14px 0 0 -14px;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.mech-frame-spinner.visible { opacity: 1; }
.mech-frame-spinner i {
  position: absolute;
  top: 0;
  left: 50%;
  width: 3px;
  height: 8px;
  margin-left: -1.5px;
  border-radius: 2px;
  background: #fff;
  transform-origin: 1.5px 14px;
  animation: mech-spin-fade 1s linear infinite;
}
.mech-frame-spinner i:nth-child(1) { transform: rotate(0deg);   animation-delay: -0.875s; }
.mech-frame-spinner i:nth-child(2) { transform: rotate(45deg);  animation-delay: -0.75s; }
.mech-frame-spinner i:nth-child(3) { transform: rotate(90deg);  animation-delay: -0.625s; }
.mech-frame-spinner i:nth-child(4) { transform: rotate(135deg); animation-delay: -0.5s; }
.mech-frame-spinner i:nth-child(5) { transform: rotate(180deg); animation-delay: -0.375s; }
.mech-frame-spinner i:nth-child(6) { transform: rotate(225deg); animation-delay: -0.25s; }
.mech-frame-spinner i:nth-child(7) { transform: rotate(270deg); animation-delay: -0.125s; }
.mech-frame-spinner i:nth-child(8) { transform: rotate(315deg); animation-delay: 0s; }
@keyframes mech-spin-fade {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.15; }
}
@media (prefers-reduced-motion: reduce) {
  .mech-frame-spinner i { animation: none; opacity: 0.55; }
}

/* Shown until a cartridge is loaded — makes the insertion mean something. */
.mech-dark {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0b0b0b;
  color: var(--ink-3);
  font-size: 13px;
  transition: opacity 0.45s ease;
}
.mech.has-cartridge .mech-dark { opacity: 0; pointer-events: none; }

.mech-hint {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 2;
  font-size: 13px;
  color: var(--ink-3);
  pointer-events: none;
  transition: opacity 0.3s;
}
.mech-hint.hidden { opacity: 0; }

.mech-loading {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  color: var(--ink-3);
  font-size: 14px;
}

.mech-readout {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 16px;
  margin-top: 20px;
}
/* Clip picker buttons stay compact — there can be several, and the row
   already competes with the sound toggle for width on narrow screens.
   #clip-row now holds one .clip-cat-group per category that actually has
   a clip in it (stacked, not side-by-side) — see js/demo.js renderClipPicker(). */
#clip-row.btn-row { flex-direction: column; align-items: flex-start; gap: 10px; }
#clip-row .btn { font-size: 13px; padding: 9px 17px; }

.clip-cat-group { display: flex; flex-direction: column; gap: 6px; }
.clip-cat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-3); }
.clip-cat-buttons { display: flex; gap: 8px; flex-wrap: wrap; }

/* Pastel per-category fills, keyed to assets/demo/categories.json (id ->
   cat-<id> class, applied by js/demo.js). Text stays --ink on every one of
   these — they're all light enough that the default dark ink reads fine,
   no per-category text color needed. */
.cat-vintage-stock       { background: #F5D9A8; border-color: #E4BD79; }
.cat-vintage-super8-fill { background: #F4C2A1; border-color: #E39F72; }
.cat-diy-fills           { background: #BFE3C7; border-color: #8FCC9E; }
.cat-memories            { background: #D9CBEF; border-color: #B7A0DD; }
.cat-art                 { background: #F4C6DC; border-color: #E497BE; }
.cat-zines               { background: #BEE3EC; border-color: #8AC7D6; }
/* #clip-row-scoped and id-qualified so it beats the .cat-* rules above on
   specificity no matter where either sits in the file — the selected clip's
   button should always read as the existing dark "active" style, not its
   category pastel. */
#clip-row .btn.active { background: var(--ink); border-color: var(--ink); color: var(--bg); }

/* Hidden for now per request — the keyboard/assistive-tech route to crank
   position (see the label's own comment in demo.html) stays in the DOM and
   fully wired, just not shown. Re-instate by switching this back to flex. */
.mech-a11y { display: none; align-items: center; gap: 14px; margin-top: 18px; }

/* Lower-page attribution note (currently: DIY fills' wedding-clip sourcing).
   Matches .fine-print's treatment of links in a small ink-3 note. */
.demo-note { font-size: 13px; color: var(--ink-3); line-height: 1.6; margin-top: 24px; max-width: 62ch; }
.demo-note a { color: var(--blue); }
.demo-note a:hover { text-decoration: underline; }
.mech-a11y span { font-size: 13px; color: var(--ink-3); flex-shrink: 0; }
.mech-a11y input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  appearance: none;
  height: 2px;
  background: var(--line-2);
  cursor: pointer;
}
.mech-a11y input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 15px; height: 15px;
  border-radius: 999px;
  background: var(--coral);
  border: none;
  cursor: pointer;
}
.mech-a11y input[type="range"]::-moz-range-thumb {
  width: 15px; height: 15px;
  border-radius: 999px;
  background: var(--coral);
  border: none;
  cursor: pointer;
}

/* ── FULL-BLEED HERO SLIDESHOW ── */
.hero-media {
  position: relative;
  width: 100%;
  height: calc(100vh - var(--nav-h));
  min-height: 420px;
  max-height: 860px;
  overflow: hidden;
  background: #111;
}

.hero-media .slideshow {
  max-width: none;
  height: 100%;
  border: none;
  background: none;
}
.hero-media .slideshow-track {
  aspect-ratio: auto;
  height: 100%;
}
.hero-media .slideshow-track img { object-fit: cover; }

/* Self-hosted looping background video, same full-bleed role as the
   slideshow above -- js/main.js starts it (see the inline script next to
   #watch in index.html) rather than the autoplay attribute, so a
   prefers-reduced-motion visitor gets the static poster frame instead of
   forced motion. */
.hero-media .hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* 20 dots would be noise over a hero image. */
.hero-media .slideshow-dots { display: none; }
.hero-media .slideshow-status { color: #999; }

.hero-media-title {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 90px 0 44px;
  pointer-events: none;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.62) 0%, rgba(0, 0, 0, 0.28) 45%, transparent 100%);
}
/* #watch reuses this overlay with an <h2> (not <h1> -- see index.html) to
   keep the page's one true <h1> on the photo-gallery hero further down, so
   this covers both. */
.hero-media-title h1,
.hero-media-title h2 {
  color: #fff;
  margin: 0;
}
.hero-media-title .eyebrow { color: rgba(255, 255, 255, 0.85); }

/* Subtitle line, between the h1 and the availability/price line below. */
.hero-media-tagline {
  margin-top: 14px;
  font-size: 19px;
  color: rgba(255, 255, 255, 0.92);
  max-width: 46ch;
}

/* Availability line under the hero title. The title block is pointer-events:
   none so drags fall through to the slideshow behind it — the link has to opt
   back in, or it can't be clicked. */
.hero-media-meta {
  margin-top: 14px;
  font-size: 15px;
  color: rgba(255, 255, 255, 0.88);
  max-width: none;
}
.hero-media-meta a {
  color: #fff;
  pointer-events: auto;
  border-bottom: 1px solid rgba(255, 255, 255, 0.55);
}
.hero-media-meta a:hover { border-bottom-color: #fff; }

@media (max-width: 820px) {
  .hero-media { height: 62vh; min-height: 340px; }
  .hero-media-title { padding: 60px 0 28px; }
}

/* Section that pairs a block of text with a slideshow beside it. */
.split {
  display: grid;
  grid-template-columns: 1fr 512px;
  gap: 56px;
  align-items: start;
}
.split-copy p { margin-top: 18px; color: var(--ink-2); }
.split-copy .btn-row { margin-top: 30px; }

@media (max-width: 980px) {
  .split { grid-template-columns: 1fr; gap: 32px; }
  .slideshow { max-width: none; }
}

/* ── FEEL — the experience section. Deliberately not another spec grid:
   these read as a rhythm, one after another, so they stack full width with
   hairline rules instead of sitting in cards like the build specs do. ── */
.feel-lead {
  font-size: clamp(19px, 1.7vw, 23px);
  line-height: 1.5;
  color: var(--ink);
  max-width: 46ch;
}

.feel-list { border-top: 1px solid var(--line); }

.feel-item {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 40px;
  padding: 30px 0;
  border-bottom: 1px solid var(--line);
  align-items: baseline;
}
.feel-item p { color: var(--ink-2); font-size: 16px; }

@media (max-width: 820px) {
  .feel-item { grid-template-columns: 1fr; gap: 10px; padding: 24px 0; }
}

/* ── GALLERY ── */
.gallery-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--surface);
  border: 1px solid var(--line);
  overflow: hidden;
}
.gallery-stage img { width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.3s ease; }
.gallery-stage img.loaded { opacity: 1; }

.gallery-empty, .gallery-status {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 28px;
  font-size: 14px; color: var(--ink-3);
}

.gallery-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 46px; height: 46px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255, 251, 245, 0.86);
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink);
  cursor: pointer;
  transition: all 0.15s;
  backdrop-filter: blur(4px);
}
.gallery-nav:hover { border-color: var(--ink); }
.gallery-nav.prev { left: 18px; }
.gallery-nav.next { right: 18px; }

.gallery-counter {
  position: absolute; right: 18px; bottom: 18px;
  font-size: 13px; color: var(--ink-2);
  background: rgba(255, 251, 245, 0.86);
  border: 1px solid var(--line-2);
  border-radius: 999px;
  padding: 5px 13px;
  backdrop-filter: blur(4px);
}

.gallery-thumbs {
  display: flex;
  gap: 10px;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 16px 2px 6px;
  scroll-behavior: smooth;
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
}
.gallery-thumbs::-webkit-scrollbar { height: 8px; }
.gallery-thumbs::-webkit-scrollbar-track { background: transparent; }
.gallery-thumbs::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 999px; }

.gallery-thumb {
  flex: 0 0 auto;
  width: 146px;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface);
  cursor: pointer;
  opacity: 0.6;
  overflow: hidden;
  transition: opacity 0.15s, border-color 0.15s;
}
.gallery-thumb img { width: 100%; height: 100%; object-fit: cover; }
.gallery-thumb:hover { opacity: 0.9; }
.gallery-thumb.active { opacity: 1; border-color: var(--coral); box-shadow: 0 0 0 1px var(--coral); }

/* ── FORMS ── */
.field { margin-bottom: 20px; }
.field label { display: block; font-size: 14px; color: var(--ink-2); margin-bottom: 9px; }

.field-row { display: flex; gap: 16px; }
.field-row .field { flex: 1; }
@media (max-width: 560px) {
  .field-row { flex-direction: column; gap: 0; }
}

.field input, .field textarea {
  width: 100%;
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: 4px;
  color: var(--ink);
  font-family: inherit;
  font-size: 16px;
  padding: 13px 15px;
  transition: border-color 0.15s;
}
.field input:focus, .field textarea:focus { outline: none; border-color: var(--blue); }
.field textarea { resize: vertical; min-height: 150px; }

.form-panel { border: 1px solid var(--line); background: var(--surface); padding: 30px 26px; }
.form-note { font-size: 14px; color: var(--ink-3); margin-top: 18px; line-height: 1.5; }

.form-status { font-size: 14px; margin-top: 18px; display: none; }
.form-status.show { display: block; }
.form-status.ok { color: var(--blue); }
.form-status.err { color: var(--coral); }

/* ── SHIPPING ── */
.shipping-options { display: flex; flex-direction: column; gap: 8px; }

.shipping-option {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  padding: 11px 13px;
  border: 1px solid var(--line-2);
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 0.15s;
}
.shipping-option:hover { border-color: var(--ink-2); }
.shipping-option:has(input:checked) { border-color: var(--coral); background: rgba(223, 89, 79, 0.05); }

.shipping-option input { margin-top: 3px; accent-color: var(--coral); flex-shrink: 0; }
.shipping-option-body { display: flex; flex-direction: column; gap: 2px; }
.shipping-option-name { font-size: 15px; color: var(--ink); }
.shipping-option-note { font-size: 13px; color: var(--ink-3); }

/* ── PDF / secondary action divider inside the order form ── */
.form-divider {
  border: none;
  border-top: 1px solid var(--line);
  margin: 22px 0;
}

/* ── FINE PRINT ── */
.fine-print {
  padding: 44px 0 60px;
  border-top: 1px solid var(--line);
}
.fine-print p {
  font-size: 13px;
  color: var(--ink-3);
  line-height: 1.6;
  max-width: 62ch;
}
.fine-print p + p { margin-top: 12px; }
.fine-print a { color: var(--blue); }

/* ── ROADMAP ── */
.level { display: grid; grid-template-columns: 190px 1fr 140px; gap: 40px; padding: 48px 0; border-bottom: 1px solid var(--line); align-items: start; }
.level:last-child { border-bottom: none; }
.level-tag { font-size: 15px; color: var(--ink-3); padding-top: 4px; }
.level.now .level-tag { color: var(--coral); }
.level-date { font-size: 12px; color: var(--ink-3); margin-top: 8px; }
.level-body p { color: var(--ink-2); margin-top: 14px; }
.level-body a { color: var(--blue); }

/* Compact square slideshow per level — reuses the .slideshow component
   (js/slideshow.js) with the dots/border stripped down for a small box. */
.level-media { width: 140px; height: 140px; flex-shrink: 0; }
.level-media .slideshow { max-width: none; height: 100%; border: 1px solid var(--line); background: var(--raised); }
.level-media .slideshow-track { aspect-ratio: 1 / 1; height: 100%; }
.level-media .slideshow-track img { object-fit: cover; }
.level-media .slideshow-dots { display: none; }
.level-media .slideshow-status { font-size: 12px; padding: 8px; text-align: center; }

.status-pill {
  display: inline-block;
  font-size: 12px;
  letter-spacing: 0;
  padding: 4px 13px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink-3);
  margin-bottom: 10px;
}
.status-pill.now { color: #fff; background: var(--coral); border-color: var(--coral); }
.status-pill.diy { color: #fff; background: var(--blue); border-color: var(--blue); }

/* ── FOOTER ── */
.site-footer { padding: 56px 0; color: var(--ink-3); font-size: 14px; }
.site-footer a:hover { color: var(--ink); }

/* ── UTIL ── */
.warn-note {
  border-left: 2px solid var(--coral);
  padding: 16px 22px;
  color: var(--ink-2);
  font-size: 16px;
  background: #FFF6E9;   /* sand at low strength */
  margin-top: 36px;
}
/* Inside a sand band it needs to lift, not blend. */
.section.on-surface .warn-note { background: rgba(255, 255, 255, 0.55); }

/* ── RESPONSIVE ── */
@media (max-width: 980px) {
  .configurator { grid-template-columns: 1fr; }
  .hero .container { grid-template-columns: 1fr; gap: 48px; }
  .level { grid-template-columns: 1fr; gap: 12px; }
  .level-media { margin-top: 4px; }
}

@media (max-width: 820px) {
  :root { --gutter: 24px; --nav-h: 66px; }
  html { font-size: 16px; }
  .section { padding: 88px 0; }
  .section-head { margin-bottom: 48px; }

  .nav-links {
    position: absolute;
    top: var(--nav-h);
    left: 0; right: 0;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 8px;
    display: none;
  }
  .nav-links.open { display: flex; }
  .nav-links a { padding: 14px 16px; font-size: 16px; }
  .nav-links a.active::after { display: none; }
  .nav-toggle { display: block; }

  .viewer-panel.expanded { width: 96vw; height: 88vh; }
}

@media (max-width: 560px) {
  .grid, .grid.cols-3, .grid.cols-4 { grid-template-columns: 1fr; }
  .bar-options { grid-template-columns: repeat(2, 1fr); }
  .gallery-thumb { width: 108px; }
  .gallery-nav { width: 40px; height: 40px; }
  .gallery-nav.prev { left: 10px; }
  .gallery-nav.next { right: 10px; }
}

/* ── JOURNAL ── generated by tools/build-journal.py from content/journal/*.md
   into site/journal/ — .card/.grid/.eyebrow above are reused as-is for the post
   listing; everything here is just what's specific to a post: the
   category badge, and readable defaults for the elements markdown
   produces that the sitewide *{margin:0} reset otherwise strips bare
   (list indents, blockquote rule, etc). */
.post-head { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.post-badge {
  display: inline-block;
  font-size: 12px;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid transparent;
  color: var(--ink);
}
.post-date { font-size: 13px; color: var(--ink-3); }
.post-back { display: inline-block; font-size: 13px; color: var(--ink-3); margin-bottom: 20px; }
.post-back:hover { color: var(--ink); }

.card .post-head { margin-bottom: 12px; }

/* Each post's default leading visual is a short muted loop rather than a
   still (see genesis.md's own journal entry on why) -- js/loop-video.js
   starts it, same prefers-reduced-motion gating as the homepage hero. */
.post-video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 4px;
  margin-top: 28px;
}

.post-body p { margin-top: 18px; }
.post-body p:first-child { margin-top: 0; }
.post-body h2, .post-body h3 { margin-top: 40px; margin-bottom: 4px; }
.post-body ul, .post-body ol { margin: 18px 0 18px 22px; }
.post-body li { margin-bottom: 8px; color: var(--ink-2); }
.post-body li:last-child { margin-bottom: 0; }
.post-body li strong { color: var(--ink); }
.post-body blockquote {
  margin: 24px 0;
  padding-left: 18px;
  border-left: 2px solid var(--coral);
  color: var(--ink-2);
}
.post-body strong { font-weight: 600; color: var(--ink); }
.post-body code {
  font-family: ui-monospace, 'SF Mono', Consolas, monospace;
  font-size: 0.9em;
  background: var(--raised);
  padding: 2px 6px;
  border-radius: 4px;
}
.post-body a { color: var(--blue); border-bottom: 1px solid rgba(0, 117, 178, 0.35); }
.post-body a:hover { border-bottom-color: var(--blue); }
.post-body img {
  display: block;
  width: 100%;
  height: auto;
  margin: 32px 0;
  border-radius: 4px;
}
/* A markdown image followed immediately by an italic line, with no blank
   line between, is treated as a caption -- confirmed against actual
   python-markdown output that the two land as siblings inside the SAME
   <p> ("![](img.jpg)\n*caption text*" -- a single \n is just a line
   break, not a new paragraph), so a plain adjacent-sibling selector
   reaches it correctly. Tightened to a small gap between image and
   caption rather than stacking both elements' full margins. */
.post-body img:has(+ em) { margin-bottom: 8px; }
.post-body img + em {
  display: block;
  margin-bottom: 32px;
  font-size: 13px;
  font-style: normal;
  color: var(--ink-3);
  text-align: center;
}
