/* ================================================================
   HANOK — ink on old paper.
   Two materials (paper, ink), one green, one red seal. Nothing else.
================================================================= */

:root{
  --paper:       #DFCEA5;   /* the sheet */
  --paper-light: #EBDDB9;   /* raised card faces, highlights */
  --paper-deep:  #C7AE7E;   /* aged edges, vignette */
  --ink:         #211B11;   /* warm near-black */
  --ink-soft:    #4E4636;   /* worn strokes, secondary text */
  --ink-faint:   #8A7C61;   /* captions, hairlines */
  --pine:        #5C6648;   /* roofs, trees */
  --seal:        #8E4A38;   /* dojang red — seal marks ONLY, never buttons */

  --font-display: "Song Myung", "Times New Roman", serif;
  --font-body:    "Gowun Batang", Georgia, serif;
  /* the hand that writes the ENGLISH on the map (VEN, 2026-08-27: "a
     handwritten font ... applied to all text in the journey section",
     then, of Nanum Pen Script: "I dont like this 'handwritten' font,
     try another one. and revert the font of the korean characters").
     So the Korean names stay in the display serif, and the caption
     and the note take one of three Latin hands — Caveat by default,
     `?hand=kalam`, `?hand=patrick`. Song Myung sits second in each
     stack so the 기와 inside the note still has a Hangul glyph. */
  --font-hand:    "Caveat", "Song Myung", cursive;
  --font-hand-2:  "Kalam", "Song Myung", cursive;
  --font-hand-3:  "Patrick Hand", "Song Myung", cursive;

  --w-copy: 62ch;
  --w-wide: 1080px;
}

*{ box-sizing: border-box; }

/* .motion-ok is set on <html> by an inline script when the visitor
   is fine with motion (see index.html). All animation CSS hangs off
   it so JS and CSS always agree. */
html.motion-ok{ scroll-behavior: smooth; }

body{
  margin: 0;
  background-color: var(--paper);
  /* mottled aged-paper wash under everything */
  background-image:
    radial-gradient(120% 90% at 50% 0%,   rgba(238,225,191,.55), rgba(238,225,191,0) 60%),
    radial-gradient(130% 100% at 50% 100%, rgba(151,124,82,.28),  rgba(151,124,82,0) 55%),
    radial-gradient(60% 40% at 12% 28%,   rgba(151,124,82,.14),  rgba(151,124,82,0) 70%),
    radial-gradient(50% 36% at 88% 64%,   rgba(151,124,82,.12),  rgba(151,124,82,0) 70%);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.75;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* Scrollbar: fully invisible, with no gutter reserved either, so the
   paper runs to the screen edge.

   Verified in Chrome 151 rather than assumed: forcing
   ::-webkit-scrollbar{width:30px} moved the layout gutter 13px -> 30px,
   so the webkit pseudo-elements ARE honoured here. The standard
   scrollbar-* properties used to be fenced behind
   @supports (-moz-appearance: none), which is Firefox-only — Chrome
   reports scrollbar-color:auto and never saw them. Both paths are set
   now, ungated, and both resolve to hidden so they cannot fight.

   Making only the TRACK transparent was not enough: the thumb stayed
   as a visible bead and the whole thing still read as unchanged. */
html{ scrollbar-width: none; }                 /* Chrome 121+, Firefox */
::-webkit-scrollbar{ width: 0; height: 0; background: transparent; }
::-webkit-scrollbar-track{ background: transparent; }
::-webkit-scrollbar-thumb{ background: transparent; }

/* paper grain — one fixed layer: fine tooth + low-frequency mottle
   (the mottle tiles at constant scale so mid-page never reads flat) */
.grain{
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: 60;
  pointer-events: none;
  mix-blend-mode: multiply;
  opacity: .5;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.55 0 0 0 0 0.47 0 0 0 0 0.33 0 0 0 0.5 0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1000' height='1000'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.004' numOctaves='2' seed='11' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0.45 0 0 0 0 0.37 0 0 0 0 0.24 0 0 0 0.17 0'/%3E%3C/filter%3E%3Crect width='1000' height='1000' filter='url(%23m)'/%3E%3C/svg%3E");
  background-size: 240px 240px, 1000px 1000px;
}

/* shared fixed landscape behind hero + journey (art/scene-bg.png).
   journey.js fades the whole layer out over p .86 -> 1.

   z-index:-1 makes this a stacking context, so the img's multiply
   would be isolated from the page — the paper backing below is what
   it actually multiplies against. Keep them in sync with --paper. */
.scene-backdrop{
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--paper);
}
.scene-backdrop img{
  display: block;   /* no inline baseline gap under the image */
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* square 1:1 art; on wide screens cover crops top+bottom, and the
     mountain sits above centre — bias the crop up so it stays in frame */
  object-position: center 38%;
  /* multiply is what marries the painting's cooler paper to ours —
     without it the image sits on the page like a pasted photo */
  mix-blend-mode: multiply;
}
/* paper wash over the lower screen: the tea terraces are the busiest
   part of the painting and that is exactly where the journey's near
   road and its labels live. Enough to let ink win, not enough to
   bleach the painting — the sky and mountain the hero title sits
   over are left alone. */
.scene-backdrop::after{
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 72%;
  background: linear-gradient(to bottom,
    rgba(223,206,165,0)   0%,
    rgba(223,206,165,.22) 40%,
    rgba(223,206,165,.45) 75%,
    rgba(223,206,165,.60) 100%);
}
/* Reduced motion (and no-JS): journey.js never runs its fade, so a
   fixed backdrop would ride down the whole page. Pin it to the first
   screen and let it dissolve at the fold instead. */
html:not(.motion-ok) .scene-backdrop{
  position: absolute;
  bottom: auto;
  height: 100vh;
  height: 100svh;
  -webkit-mask-image: linear-gradient(#000 62%, transparent 99%);
          mask-image: linear-gradient(#000 62%, transparent 99%);
}

h1,h2,h3{
  font-family: var(--font-display);
  font-weight: 400;
  line-height: 1.25;
  margin: 0;
  letter-spacing: .01em;
}
h2{ font-size: clamp(1.7rem, 3.6vw, 2.6rem); }

p{ margin: 0; }
a{ color: var(--ink); }

:focus-visible{
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

/* shared bits ---------------------------------------------------- */

.eyebrow{
  font-family: var(--font-display);
  font-size: .85rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: .9rem;
}
.eyebrow [lang="ko"]{
  letter-spacing: .3em;
  color: var(--ink-soft);
}

.section-head{
  max-width: var(--w-copy);
  margin: 0 auto;
  padding: 0 24px;
  text-align: center;
}
.section-note{
  margin-top: .8rem;
  color: var(--ink-soft);
  font-size: .95rem;
}

/* ink-drawn horizontal rule motif — a real brush dash, not a bar */
.section-head h2::after{
  content: "";
  display: block;
  width: 88px;
  height: 9px;
  margin: 1rem auto 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 88 9'%3E%3Cpath d='M2 5.5 C28 2.4 58 3 86 4.8' fill='none' stroke='%23211B11' stroke-width='3.2' stroke-linecap='round'/%3E%3Cpath d='M7 6.8 C33 4 60 4.6 81 6' fill='none' stroke='%23211B11' stroke-width='1.2' stroke-linecap='round' opacity='.45'/%3E%3C/svg%3E") center/contain no-repeat;
  opacity: .9;
}

/* scroll-in reveal (only when motion is welcome) */
.motion-ok .reveal{
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .9s ease, transform .9s ease;
}
.motion-ok .reveal.is-in{
  opacity: 1;
  transform: none;
}

/* seal stamp — pure CSS dojang */
.manifesto__seal,
.footer__seal{
  width: 46px;
  height: 46px;
  border-radius: 9px 6px 10px 5px;
  background: var(--seal);
  color: var(--paper-light);
  position: relative;
  transform: rotate(-3deg);
  box-shadow: 0 0 0 1px rgba(142,74,56,.35), inset 0 0 8px rgba(0,0,0,.18);
}
.manifesto__seal::after,
.footer__seal::after{
  /* THE SEAL GLYPH COMES FROM js/config.js, through --seal-glyph.
     CSS cannot read a query string and this rule has to agree with
     the hero stamp and the map markers, so config.js resolves it once
     (including `?seal=`) and hands it over as a custom property. The
     fallback here is only for a no-JS visitor. `--seal-scale` shrinks
     a two-character seal, which is how a two-character 도장 is cut.

     The favicon in index.html is a static data-URI and is the one
     copy that still has to be changed by hand alongside the config. */
  content: var(--seal-glyph, "韓");
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: calc(24px * var(--seal-scale, 1));
  letter-spacing: 0;
  color: var(--paper-light);
}

/* floating CA pill --------------------------------------------- */

/* The pill is fixed to the WINDOW, and on a phone the journey's note
   band is at the bottom of it — so the pill printed itself over the
   last line of Zico's copy (VEN's screenshot, 2026-08-25). It steps
   over the band while the band is standing and drops back the moment
   the journey lets go. --jnote-h is published by measure() in
   js/journey.js and is 0px everywhere the band is display:none, so
   this calc is a no-op on every other screen and section. */
.ca-float{
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 70;
  opacity: 0;
  visibility: hidden;   /* keeps the hidden button out of the tab order */
  transform: translateY(8px);
  transition: opacity .4s ease, transform .4s ease, visibility 0s linear .4s;
  pointer-events: none;
}
.ca-float.is-on{
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity .4s ease, transform .4s ease;
  pointer-events: auto;
}
.has-jnote .ca-float{
  bottom: calc(18px + var(--jnote-h, 0px));
  transition: opacity .4s ease, transform .4s ease, bottom .3s ease;
}
.ca-pill{
  display: flex;
  align-items: center;
  gap: 10px;
  border: 1.5px solid var(--ink);
  background: var(--paper-light);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: .85rem;
  padding: 8px 14px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 2px 9px rgba(33,27,17,.22);
}
.ca-pill:active{ transform: translateY(1px); box-shadow: 0 1px 4px rgba(33,27,17,.2); }
.ca-pill__label{
  font-family: var(--font-display);
  letter-spacing: .14em;
  font-size: .75rem;
}
.ca-pill__value{
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* LANGUAGE TOGGLE (js/i18n.js) — the same pill chrome as the CA,
   pinned top right where no fixed chrome lives. Always visible:
   a first-time Korean visitor has to find it before scrolling. */
.lang-toggle{
  position: fixed;
  top: 18px;
  right: 18px;
  z-index: 70;
  border: 1.5px solid var(--ink);
  background: var(--paper-light);
  color: var(--ink);
  font-family: var(--font-display);
  font-size: .8rem;
  letter-spacing: .06em;
  padding: 7px 14px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 2px 9px rgba(33,27,17,.22);
}
.lang-toggle:active{ transform: translateY(1px); box-shadow: 0 1px 4px rgba(33,27,17,.2); }

/* HERO ---------------------------------------------------------- */

.hero{
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  position: relative;
  padding: 48px 24px 150px;
  text-align: center;
}

.hero__eyebrow{
  font-family: var(--font-display);
  letter-spacing: .3em;
  text-transform: uppercase;
  font-size: .8rem;
  color: var(--ink-soft);
  margin-bottom: 2.2rem;
}
.hero__eyebrow [lang="ko"]{ color: var(--ink-soft); letter-spacing: .34em; }

/* The hero sits on the shared .scene-backdrop; this soft paper wash
   keeps the ink title off the painting's mountain ridge. It is
   full-bleed inside .hero on purpose: sized off the shrink-to-fit
   .hero__inner it overflowed the viewport on phones and the gradient
   got a hard vertical cut at the screen edge. Horizontal radius is
   50%, so it reaches zero exactly at the hero's own edges. */
.hero::before{
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;   /* longhands: iOS dislikes inset */
  pointer-events: none;
  background: radial-gradient(50% 40% at 50% 42%,
    rgba(223,206,165,.82) 0%,
    rgba(223,206,165,.62) 46%,
    rgba(223,206,165,.24) 72%,
    rgba(223,206,165,0) 100%);
}
.hero__inner{
  position: relative;
  z-index: 1;
}

/* THE BOX IS THE TYPE SIZE. The svg fills this width and keeps its
   own aspect, so the same number renders two words at wildly
   different sizes: 기와's viewBox is 302 units wide, $TILES's is
   674, so at one width the hangul draws more than twice the height.
   Set the size HERE when the wordmark changes, not in hero.js.

   Sized for $TILES since 2026-08-30, and the size is the ask, not a
   side effect: Zico wanted the mark smaller than the hangul it
   replaced — *"Doesnt need to be that big."* 기와 drew 181px-tall
   glyphs in a 460px box; $TILES at 560 draws ~88px caps in a box
   that is only 100px wider, so the mark reads as a wordmark rather
   than a billboard and the lore line under it is no longer whispered
   at by comparison.

   `?mark=N` overrides the width live (js/hero.js writes --mark-w),
   so this can be re-judged on the real page against the real
   painting without an edit — `?mark=460`, `?mark=660`.

   The vw ceiling is 84 rather than 74 because six letters need the
   phone's full measure where two hangul blocks did not.

   2026-09-30, Zico, the other way now that the crew stands on it:
   *"try again with the lettering being bigger also"* — 760px (caps
   ~134px, up from ~99) and 92vw on phones (+9.5% there: a phone is
   width-bound, so the gain is mostly desktop). Measured with the brush
   letters at 320-1440px wide: still under the eyebrow, over the lore
   line, seal on screen, no sideways scroll. A new FONT sets its own
   number here from a cap-height target (~130px on desktop). */
.hero__title{
  /* the third term keeps a TALL mark inside the first screen: Zico's
     lettering (with the S's long tail) is ~1.7:1 where the old letters
     were ~3.8:1, so at 760px wide it would stand ~450px tall. js/hero.js
     writes --mark-ar (the box's width/height) from the viewBox. */
  width: min(var(--mark-w, 760px), 92vw, calc(52svh * var(--mark-ar, 4)));
  margin: 0 auto;
}
.hero__title svg{ display: block; width: 100%; height: auto; overflow: visible; }
/* no-JS only: the drawn mark replaces this on first paint. Latin
   since 2026-08-30, and $TILES sets six characters where 기와 set
   two, so the size comes down to match the box above. */
.hero__title-fallback{
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 11vw, 5rem);
  letter-spacing: .1em;
}
.js .hero__title-fallback{ display: none; }

/* The lore line belongs to the mark above it: tight to the title, in
   the display face, spaced like the eyebrow so the two frame it.

   This slot used to hold the ticker ($XXX) — four spaced characters at
   1.9rem. Since 2026-08-29 the hero DRAWS the ticker as brush strokes
   and this line carries a sentence instead, so the size and tracking
   came down to suit prose: at the old 1.9rem/.22em a 45-character line
   ran the width of the screen and wrapped against the tagline under it.
   It is a shade SMALLER than .hero__tagline now and still reads above
   it, because the hierarchy under the mark is carried by tracking and
   ink weight rather than by size: spaced and full-strength --ink here,
   unspaced and --ink-soft there.

   Scaled down once more at VEN's word, 2026-08-29. 34ch is kept
   deliberately: it is the width that breaks the line at its own comma
   ("understand the narrative," / "understand the lore") instead of
   mid-clause.

   THE CLOSING MARKER OF THIS COMMENT WAS ONCE IN THE MIDDLE OF IT,
   four lines up, and CSS does not warn: everything after it became
   the prelude of an unparseable rule and took .hero__lore's whole
   block down with it, so the line rendered as plain body text for a
   session. If this rule ever looks like it is not applying, read the
   comment above it before touching the declarations. */
.hero__lore{
  margin-top: .8rem;
  margin-inline: auto;
  max-width: 34ch;
  font-family: var(--font-display);
  font-size: clamp(.9rem, 1.45vw, 1.12rem);
  letter-spacing: .14em;
  line-height: 1.5;
  color: var(--ink);
}

.hero__tagline{
  margin-top: 1.9rem;
  color: var(--ink-soft);
  font-size: clamp(1rem, 1.6vw, 1.15rem);
}

.hero__actions{
  margin-top: 2.2rem;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}

.btn{
  font-family: var(--font-display);
  font-size: .95rem;
  letter-spacing: .06em;
  text-decoration: none;
  color: var(--paper-light);
  background: var(--ink);
  padding: 12px 26px;
  border-radius: 4px 10px 4px 12px;   /* brush-cut corners */
  border: 1.5px solid var(--ink);
  box-shadow: 0 3px 10px rgba(33,27,17,.24);
  transition: transform .15s ease, box-shadow .15s ease;
}
.btn:hover{ transform: translateY(-1px); box-shadow: 0 5px 14px rgba(33,27,17,.28); }
.btn:active{ transform: translateY(1px); box-shadow: 0 1px 5px rgba(33,27,17,.22); }
.btn--quiet{
  background: transparent;
  color: var(--ink);
}
/* links not wired yet: dim with colour, not opacity — over the
   backdrop painting a see-through button reads as broken, not quiet */
.btn.is-tba{
  background: var(--ink-soft);
  border-color: var(--ink-soft);
  color: rgba(235,221,185,.78);
  cursor: default;
  box-shadow: none;
}
.btn--quiet.is-tba{
  background: transparent;
  color: var(--ink-soft);
  border-color: rgba(78,70,54,.55);
}
.btn.is-tba:hover{ transform: none; box-shadow: none; }

.hero__scroll{
  position: absolute;
  left: 50%;
  bottom: 26px;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: var(--ink-soft);
  font-size: .78rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  font-family: var(--font-display);
  opacity: .85;
}
.motion-ok .hero__scroll{
  opacity: 0;
  transition: opacity .8s ease;
}
.motion-ok .hero.is-written .hero__scroll{ opacity: .85; }
.hero__scroll svg{ width: 11px; height: 40px; }

/* JOURNEY -------------------------------------------------------- */

.journey{
  height: 460vh;   /* scroll budget for the sticky stage */
  position: relative;
}
.journey__sticky{
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
}
.journey__stage{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.journey__head{
  position: absolute;
  top: clamp(20px, 5vh, 56px);
  left: 0;
  right: 0;
  text-align: center;
  z-index: 2;
  transition: opacity .5s ease;
  padding: 0 24px;
}
.journey.is-under-way .journey__head{ opacity: 0; }
.journey__noscript{
  position: absolute;
  bottom: 24px;
  left: 0; right: 0;
  text-align: center;
  color: var(--ink-soft);
}

/* static fallback (reduced motion): panels in a simple column */
.journey--static{ height: auto; }
.journey--static .journey__sticky{ position: static; height: auto; overflow: visible; padding: 64px 0 32px; }
.journey--static .journey__head{ position: static; margin-bottom: 40px; }
.journey__figure{
  margin: 0 auto 56px;
  /* wider than the 460px it was: this column now carries the stop's
     paragraph as well as its painting, and 460px of prose at this size
     sets too narrow a measure */
  width: min(560px, 88vw);
  text-align: center;
}
.journey__figure svg{ display: block; width: 100%; height: auto; }
.journey__figure figcaption{
  margin-top: 10px;
  font-family: var(--font-display);
  color: var(--ink-soft);
}
.journey__figure figcaption [lang="ko"]{ color: var(--ink-soft); }
/* the stop's own copy, under its painting. Body face, not the display
   face the name uses — it is a paragraph, not a label. */
.journey__blurb{
  display: block;
  margin: 8px auto 0;
  max-width: 46ch;
  font-family: var(--font-body);
  font-size: .95rem;
  line-height: 1.6;
  color: var(--ink-soft);
  text-wrap: pretty;
}

/* JOURNEY — SPLIT MODE (?journey=split) ---------------------------
   Copy left, painting right, one stop at a time. Both columns stack
   all four stops in a single grid cell (grid-area 1/1) so they
   crossfade in place and the column height is the tallest stop —
   nothing reflows as the text changes. */

.journey--split .journey__sticky{ display: flex; align-items: center; }

/* Per-stop tonal wash. Full-bleed inside the sticky (NOT inside
   .jsplit, which is capped at --w-wide), stacked one per stop and
   crossfaded on the same curve as the paintings, so the whole field
   takes on the colour of the place you are looking at. */
.jsplit__washes{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  z-index: 0;
  pointer-events: none;
}
.jsplit__wash{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  opacity: 0;
}

/* hairline in the gutter, one tick per stop, brightening as you reach
   it — architecture for the layout and a position cue in one */
.jsplit__rule{
  position: absolute;
  top: 8%; bottom: 8%;
  left: 50%;
  width: 1px;
  background: linear-gradient(to bottom,
    rgba(138,124,97,0), rgba(138,124,97,.4) 14%,
    rgba(138,124,97,.4) 86%, rgba(138,124,97,0));
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: center;
}
.jsplit__tick{
  display: block;
  width: 11px; height: 1px;
  background: var(--ink-faint);
}

/* seat the keyed paintings — with the ground keyed out they otherwise
   float on the fill with nothing under them */
.journey--split.has-ground .jsplit__plate{ position: relative; }
.journey--split.has-ground .jsplit__plate::after{
  content: "";
  position: absolute;
  left: 12%; right: 12%; bottom: 2%;
  height: 5%;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(33,27,17,.20), rgba(33,27,17,0) 72%);
  pointer-events: none;
}

.jsplit{
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: clamp(20px, 5vw, 72px);
  width: 100%;
  max-width: var(--w-wide);
  margin: 0 auto;
  padding: 0 clamp(20px, 5vw, 56px);
}
.jsplit__eyebrow{ margin: 0 0 clamp(14px, 3vh, 28px); }

.jsplit__stops, .jsplit__art{ display: grid; }
.jsplit__stop, .jsplit__plate{ grid-area: 1 / 1; margin: 0; }

.jsplit__idx{
  font-family: var(--font-display);
  font-size: .74rem;
  letter-spacing: .32em;
  color: var(--ink-faint);
  margin: 0 0 .9rem;
}
.jsplit__idx span{ opacity: .55; }
.jsplit__stop h3{
  font-size: clamp(1.6rem, 3.2vw, 2.5rem);
  line-height: 1.14;
  margin: 0 0 .35rem;
}
.jsplit__ko{
  font-family: var(--font-display);
  color: var(--ink-soft);
  letter-spacing: .14em;
  margin: 0 0 clamp(12px, 2.4vh, 22px);
}
.jsplit__body{
  margin: 0;
  max-width: 36ch;
  color: var(--ink-soft);
}
.jsplit__plate svg{ display: block; width: 100%; height: auto; }

/* Phones: eyebrow, then the painting, then the copy — all three have
   to share one 100svh sticky. `display:contents` dissolves the text
   column so its two children can be ordered around the painting; the
   plate is capped against BOTH vw and svh, or a tall stop pushes the
   body copy off the bottom of the screen. */
@media (max-width: 860px){
  .jsplit{
    grid-template-columns: 1fr;
    gap: clamp(10px, 2vh, 20px);
    align-content: center;
  }
  .jsplit__text{ display: contents; }
  .jsplit__eyebrow{ order: 1; margin: 0; }
  .jsplit__art{ order: 2; justify-items: center; }
  .jsplit__stops{ order: 3; }
  .jsplit__plate{ width: min(58vw, 32svh); }
  .jsplit__body{ max-width: none; }
  /* no gutter to rule when there is only one column */
  .jsplit__rule{ display: none; }
}

/* JOURNEY — MAP MODE (?journey=map, the default) -------------------
   A sheet of map under the whole window, and scrolling walks it. The
   camera is ONE transform on .jmap__cam; everything else in here is
   static, so a frame costs a transform, a dashoffset and a handful of
   opacities. Same build-once discipline as split and road mode.

   The road, the markers and the seals are SVG over the painting —
   see the block comment in js/journey.js for why that split exists. */

.journey--map{
  /* Three journeys and no dwells (since 2026-08-20 part 3 the camera
     never stops — see DWELL in js/journey.js). At 560vh the scrollable
     part is 460vh, ~153vh of scrolling per leg, and every vh of it
     moves the camera. It was 640vh when ~a third of the section was
     spent standing at the stops; with the holds gone that length made
     each leg half again slower than the pacing that was tuned.
     Shorten this and the travel stops reading as travel; lengthen it
     and the section outstays its welcome.

     590vh since 2026-08-27: the last 30vh of the pinned scroll pans the
     camera on past Jeonju to the sheet's foot (TAIL 0.06 of 490vh in
     js/journey.js; the road itself stays inked to the seal), the legs
     keeping their 460vh. */
  height: 590vh;
}
/* NO opaque background on the sticky — load-bearing absence (part 9).
   The entry dissolve slides the semi-transparent map up OVER the
   hero's fixed landscape; a paper background here sat between them,
   so the pre-pin viewport read as a blank slab instead of a
   cross-dissolve (VEN's screenshot). The sheet itself covers the
   sticky edge-to-edge once drawn (its zoom is cover-floored), so
   nothing is lost by the transparency. */

.jmap{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  overflow: hidden;
}
/* THE CAMERA'S FRAME. On wide screens --jnote-h is 0 and this is the
   whole of .jmap, so nothing about the map has changed. On a phone
   the note band below takes its own height out of this box, which is
   the entire point: there is then no terrain behind the words at any
   scroll position, rather than a translucent wash over whatever ridge
   happens to be under them. js/journey.js measures the camera off
   THIS element, and publishes --jnote-h from the band's own height.

   IT DOES NOT CLIP, and that is deliberate rather than an oversight.
   .jmap already clips the section, and the band below is opaque
   paper, so the sheet's overhang is covered either way — while a
   SECOND overflow:hidden here wraps the camera's 2960x5304 layer in
   another clip, and with .jmap's entry mask over the top of it that
   was enough to hang the renderer outright on load. Same family of
   trap as the two blend dropouts in HANDOFF §7 and the note on
   .jmap__sheet above: this layer is far too big to be given any
   compositing work that is not strictly needed. */
.jmap__view{
  position: absolute;
  top: 0; left: 0; right: 0;
  bottom: var(--jnote-h, 0px);
}

/* the moving sheet. transform-origin 0 0 keeps the camera maths in
   js/journey.js honest: translate() then scale() about the corner. */
.jmap__cam{
  position: absolute;
  top: 0; left: 0;
  transform-origin: 0 0;
  will-change: transform;
}
.jmap__sheet{
  display: block;
  width: 100%;
  height: 100%;
  /* NO mix-blend-mode HERE, and that is load-bearing.

     .scene-backdrop multiplies, and copying it onto this element was
     the obvious thing to do — it is what marries a painting's paper to
     ours. It does not survive the camera. At ZOOM_IN this <img> is
     2960x5304 CSS px, and a blended element has to be rasterised into
     a texture before it can be composited: 15.7M pixels of one. In
     Chrome the whole layer then silently fails to paint and you get
     bare parchment with the SVG road and the seal still drawn on top
     of nothing — which reads exactly like a missing-image bug.

     Measured, because it looked like one: at p=0.20 (zoom 1.0, layer
     1600x2867) the sheet painted; at p=0.07 (zoom 1.85) it did not,
     and setting mixBlendMode to normal brought it straight back. Same
     family of trap as the two blend dropouts in HANDOFF §7.

     The marriage is done by .jmap__tone instead, which is one
     viewport in size — about a twenty-third of the area — and blends
     safely, the same way .grain has always done. */
}
/* the paper veil that the sheet's own multiply used to provide, moved
   onto a viewport-sized layer. ?maptone=0 turns it off; journey.js
   owns the value so it can be judged without an edit. */
.jmap__tone{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  background: var(--paper);
  mix-blend-mode: multiply;
  pointer-events: none;
}
.jmap__ink{
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* The road ahead as FOOTSTEPS (default since part 10) — small ink
   prints a stride apart, alternating sides, each turned to where the
   road goes. Geometry is generated in js/journey.js from the same
   arc-length table the camera uses; prints vanish under the inked
   line as they are walked. ?trail=dots restores the dotted line. */
.jmap__steps ellipse{
  fill: var(--ink-soft);
  /* .42 until 2026-08-27. On the cleared paper the prints were the
     only thing that said where the path went, and at .42 VEN could
     not see it: "make it obvious where the path is or reduce the size
     of each clearing." Darker prints, and the dotted line under them
     (.jmap__road--under) — the clearings stay. */
  opacity: .66;
}
/* the dotted road drawn UNDER the footsteps (the `foot` trail): a faint
   line so the way ahead reads as a line even where the prints are
   sparse or the paper is bare */
.jmap__road--under{ opacity: .2; }

/* The road AHEAD of you: dotted, faint (?trail=dots). The map
   convention, and what makes the sheet read as a route rather than as
   terrain. */
.jmap__road{
  fill: none;
  stroke: var(--ink-soft);
  stroke-width: 3.4;
  stroke-linecap: round;
  stroke-dasharray: 0.1 11;
  opacity: .38;
}
/* The road BEHIND you: inked in solid as you walk it. Revealed by
   stroke-dashoffset, which is the only thing about it that changes. */
.jmap__inked{
  fill: none;
  stroke: var(--ink);
  stroke-width: 2.8;
  stroke-linecap: round;
  opacity: .82;
}
.jmap__walker{
  fill: var(--ink);
  opacity: .9;
}

.jmap__x path{
  fill: none;
  stroke: var(--ink);
  stroke-width: 3;
  stroke-linecap: round;
}

/* The place names, written onto the sheet (default since 2026-08-20
   part 2 — the info cards are gone unless ?info=1). The glyphs are the
   display face; the HANDWRITING is the label's mask, a serpentine
   brush stroke revealed by dashoffset — see the label block in
   js/journey.js. Sized per viewport by measure(), not here. */
.jmap__label text{
  font-family: var(--font-display);
  fill: var(--ink);
  fill-opacity: .84;
}
/* the English caption: same display face (inherited from the rule
   above), revealed by its own mask stroke in js/journey.js — opacity
   here is its STANDING strength, the writing is the mask */
.jmap__label .jmap__en{
  fill: var(--ink-soft);
  fill-opacity: 1;
  letter-spacing: .2em;
  text-transform: uppercase;
  opacity: .8;
}
/* Zico's note on the sheet, under the caption. Sentence case and no
   tracking — it is prose, not a map label, and the caption above it
   keeps the small-caps job. Slightly lighter than the caption so the
   name still leads. */
.jmap__label .jmap__copy text{
  fill: var(--ink-soft);
  fill-opacity: 1;
  letter-spacing: .01em;
  text-transform: none;
  /* NO `opacity` HERE. The line-by-line fade in js/journey.js sets each
     line's opacity, and a standing value in a class rule beat the
     attribute it used to set — so the fade never showed. The standing
     strength is COPY_OP in js/journey.js, and it writes inline style. */
}
/* the phone's block (js/journey.js setBlock): the caption is a heading
   on a note here, not the name of the place, so its tracking eases to
   fit the box the tool cleared for it */
.jmap__enlab--phone .jmap__en{ letter-spacing: .12em; }
/* THE HAND (VEN, 2026-08-27). The caption and the note in a
   handwriting face — the Korean name stays in the display serif, at
   VEN's ask — and the caption set as a line of that hand rather than
   tracked small caps, which a pen does not do. `.jmap--hand` is on by
   default (Caveat); `.jmap--hand-2` / `-3` are Kalam and Patrick Hand;
   `?hand=0` is the serif as before. See --font-hand. */
.jmap--hand .jmap__enlab text{ font-family: var(--font-hand); }
.jmap--hand-2 .jmap__enlab text{ font-family: var(--font-hand-2); }
.jmap--hand-3 .jmap__enlab text{ font-family: var(--font-hand-3); }
.jmap--hand .jmap__label .jmap__en,
.jmap--hand .jmap__enlab--phone .jmap__en{
  text-transform: none;
  letter-spacing: .02em;
}

/* ZICO'S COPY, SECOND HOME — a band under the map.

   The sheet is where it lives on a desktop (VEN: *"I liked how it was
   before"*). But the sheet block is sized off the LABEL scale, and
   that scale floors at 34px: on a 390px phone the block would be a
   quarter of the sheet's width while its own text fell to about 7px.
   So phones get this instead. `?copy=strip` forces it at every width;
   `?copy=sheet` given by hand overrides it even on a phone, so the
   alternative can be looked at rather than described.

   A BAND IN THE FLOW, NOT A WASH ON TOP. It used to be absolutely
   positioned over the bottom of the map with a translucent gradient
   behind it, and on a phone that put every word over mountains,
   terraced fields or roofs — the three things VEN circled in red on
   2026-08-25. There is no position on this sheet that fixes that: at
   phone width the map covers the screen edge to edge, so the only
   clean paper is paper the map does not reach. Hence a real band,
   taking its height OUT of .jmap__view. */
.jmap__note{
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: none;
  pointer-events: none;
  z-index: 3;
  /* the page's own parchment, opaque — nothing of the sheet behind it */
  background: var(--paper);
  border-top: 1px solid rgba(138, 124, 97, .34);
}
/* `.jmap--strip` is set by measure() in js/journey.js — no breakpoint
   here any more. Since 2026-08-27 a phone gets the SHEET when the
   sheet has clearings to put the block in (MAPS.ink.clear: the block
   is re-flowed into the box the tool measured for it), and the band
   only when it does not (`?map=solid`, the pirate sheet) or when
   `?copy=strip` asks for it. With the band on, the copy and the
   English caption come off the sheet: the band prints the same name
   as its heading at a size that can be read. The Korean name stays —
   it is the thing that writes itself, the point of the labels. */
.jmap--strip .jmap__note{ display: grid; }
.jmap--strip .jmap__label .jmap__copy{ display: none; }
.jmap--strip .jmap__label.jmap__enlab{ display: none; }
/* All four notes share ONE grid cell, so they crossfade in place and
   the band's height is the tallest of them — fixed for the whole
   journey, so the map's frame never resizes as you scroll between
   stops (which would re-run measure() and jump the camera). */
.jmap__note-p{
  grid-area: 1 / 1;
  align-self: center;
  margin: 0;
  padding: .9rem clamp(1rem, 5vw, 2.2rem) 1rem;
  color: var(--ink-soft);
  /* SIZED FOR READING, not for fitting. This is the one piece of text
     on the journey whose size is not negotiated with the art: it is
     off the sheet, so nothing can be behind it however large it gets
     — the band simply takes another few pixels from .jmap__view. It
     was 12.5px on a phone because it used to be a wash over the map
     and every extra line was another line over a mountain. That
     constraint is gone; this is ~15px. */
  font-size: clamp(.92rem, 3.9vw, 1rem);
  line-height: 1.5;
  text-align: center;
  text-wrap: pretty;
  transition: opacity .2s linear;
}
.jmap__note-p b{
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  letter-spacing: .2em;
  text-transform: uppercase;
  font-size: .8rem;
  color: var(--ink);
  margin-bottom: .35rem;
}
/* The one place seal red is allowed outside the hero and footer
   stamps: a map marker IS a stamp. §4. */
.jmap__seal rect{ fill: var(--seal); }
.jmap__seal text{
  fill: var(--paper-light);
  font-family: var(--font-display);
  /* a CSS declaration beats a presentation attribute, so the size has
     to be shrunk for a two-character seal HERE — setting font-size on
     the <text> in js/journey.js would silently lose to this rule */
  font-size: calc(21px * var(--seal-scale, 1));
  text-anchor: middle;
}

/* The vistas — the four paintings, back by request (VEN 2026-08-20
   part 4): just the place, no copy, fading up as you arrive on it and
   away as you leave. Screen-pinned so they never scale with the
   camera; left side against the marker's right-of-centre focus. The
   keyed treatment means no card edge — the painting sits straight on
   the map's field the way the split plates sit on the paper. */
.jmap__vistas{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  display: grid;
  align-items: center;
  justify-items: start;
  padding: 0 clamp(20px, 6vw, 84px);
  pointer-events: none;
}
.jmap__vista{
  grid-area: 1 / 1;
  margin: 0;
  width: clamp(240px, 24vw, 400px);
}
.jmap__vista svg{ display: block; width: 100%; height: auto; }
@media (max-width: 860px){
  /* one column: the painting sits along the bottom and the camera's
     focus moves up to clear it — the same trade the cards made */
  .jmap__vistas{
    align-items: end;
    justify-items: center;
    padding-bottom: clamp(16px, 5vh, 48px);
  }
  .jmap__vista{ width: min(56vw, 30svh); }
}

/* the eyebrow — the one piece of screen-pinned chrome left in map
   mode. It sits outside the camera in the corner; everything else the
   section says, it says on the sheet. */
.jmap__eyebrow{
  position: absolute;
  top: clamp(18px, 4.5vh, 44px);
  left: clamp(20px, 5vw, 64px);
  margin: 0;
  z-index: 2;
}
/* ITS OWN CLEARING (2026-09-30, session 21). Zico: *"The "the journey"
   title in the "the journey" section is not visible that well in the
   background"*; VEN: *"Perhaps a clearing that is pinned to the "the
   journey" text rather than the background itself."* The sheet moves
   under the eyebrow, so no clearing cut into the map could stay beneath
   it — this one belongs to the eyebrow and moves with it: a soft oval of
   the SHEET's own paper (#E9D4BD, sampled off the render — warmer than
   --paper), feathered to nothing, so it reads as mist the ridges fade
   into, like the clearings round Zico's notes, not a label stuck on.
   It sits inside the eyebrow's stacking context (z-index -1): under the
   words, over the map. `?jhead=0` = the bare eyebrow. */
.jmap__eyebrow::before{
  content: "";
  position: absolute;
  top: -1.7em; bottom: -1.7em;
  left: -2.6em; right: -2.6em;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(233, 212, 189, .96) 58%,
    rgba(233, 212, 189, .78) 76%,
    rgba(233, 212, 189, 0));
}
.jmap__eyebrow--bare::before{ display: none; }

/* ---- the copy column — ?info=1 ONLY since 2026-08-20 part 2 --------
   VEN removed the per-location information; this block serves the way
   back. Pinned to the screen rather than to the sheet: it must not
   scale with the camera, so it lives outside .jmap__cam. The wash is a
   gradient rather than a card edge: a hard-edged panel over a painting
   is the exact thing the journey's paintings were treated to avoid
   (§6, "glossy tiles pasted onto the landscape"). */
.jmap__cards{
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 clamp(20px, 5vw, 64px);
  pointer-events: none;
}
.jmap__cards::before{
  content: "";
  position: absolute;
  top: 0; left: 0; bottom: 0;
  /* Capped at 62% of the window, not 78%. The camera puts the place
     you have arrived at at FOCUS_X (0.68 of the width), so a wash that
     runs to 78% reaches past the marker and drops a veil over the very
     seal the copy is describing. At 62% the two never meet: measured
     at 1024px the wash ends at 635px and the marker sits at 696px. */
  width: min(760px, 62%);
  /* Holds near-opaque across the whole text column and only lets go
     past it. The map under the copy is ink mountains at full contrast,
     which is the busiest thing on the sheet; at .88 the ridges read
     straight through a body paragraph. The copy is the content — it
     wins, and the sheet stays legible either side of it. */
  background: linear-gradient(to right,
    rgba(223,206,165,.97) 0%,
    rgba(223,206,165,.95) 44%,
    rgba(223,206,165,.72) 70%,
    rgba(223,206,165,0)   100%);
  pointer-events: none;
}
/* all four cards share one grid cell so they cross in place and the
   block never reflows as the copy changes — same as .jsplit__stop */
.jmap__stops{
  position: relative;
  display: grid;
  width: min(46ch, 46%);
}
.jmap__card{
  grid-area: 1 / 1;
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 0 clamp(14px, 2vw, 24px);
}
.jmap__plate{
  grid-column: 1;
  margin: 0;
  width: clamp(92px, 15vh, 156px);
}
.jmap__plate svg{ display: block; width: 100%; height: auto; }
.jmap__head{ grid-column: 2; }
.jmap__idx{
  font-family: var(--font-display);
  font-size: .74rem;
  letter-spacing: .32em;
  color: var(--ink-faint);
  margin: 0 0 .6rem;
}
.jmap__idx span{ opacity: .55; }
.jmap__card h3{
  font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  line-height: 1.14;
  margin: 0 0 .3rem;
}
.jmap__ko{
  font-family: var(--font-display);
  color: var(--ink-soft);
  letter-spacing: .14em;
  margin: 0;
}
.jmap__body{
  grid-column: 1 / -1;
  margin: clamp(12px, 2.2vh, 20px) 0 0;
  color: var(--ink-soft);
  text-wrap: pretty;
}

/* Phones: one column, the copy along the bottom and the place you have
   arrived at above it (journey.js moves the camera's focus up to match
   — see `narrow` in measure()). The wash flips to a bottom-up
   gradient, because there is no left column to sit under. */
@media (max-width: 860px){
  .jmap__cards{
    justify-content: flex-end;
    padding-bottom: clamp(18px, 4vh, 44px);
  }
  .jmap__cards::before{
    top: auto; right: 0;
    width: auto;
    height: 62%;
    background: linear-gradient(to top,
      rgba(223,206,165,.96) 0%,
      rgba(223,206,165,.90) 38%,
      rgba(223,206,165,.55) 70%,
      rgba(223,206,165,0)   100%);
  }
  .jmap__stops{ width: auto; }
  .jmap__plate{ width: clamp(76px, 11vh, 112px); }
  .jmap__body{ margin-top: clamp(8px, 1.6vh, 14px); }
}

/* MANIFESTO ------------------------------------------------------ */

.manifesto{
  padding: 130px 24px 150px;
  display: grid;
  place-items: center;
  text-align: center;
}
.manifesto__inner{ max-width: 46ch; }
.manifesto__ko{
  font-family: var(--font-display);
  color: var(--ink-soft);
  letter-spacing: .5em;
  font-size: .95rem;
  margin-bottom: 1.6rem;
}
.manifesto__text{
  font-size: clamp(1.15rem, 2.2vw, 1.45rem);
  line-height: 2;
}
.manifesto__seal{ margin: 2.4rem auto 0; }

/* HOW TO JOIN (index.html #join) — Namsangol's steps -------------
   Zico's text, 2026-09-30. A paper card in the ledger's hand, three
   steps across it (one column on phones). The settings are set in a
   monospace so 0/O and l/1 can't be misread, and a click selects a
   whole value (user-select: all) for copying — nothing else to it.
   `?join=stop` moves the whole section onto the map (.join--stop). */
.join{ padding: 120px 0 130px; }
.join__card{
  width: min(var(--w-wide), 94vw);
  margin: 44px auto 0;
  padding: clamp(18px, 3vw, 36px) clamp(16px, 3vw, 40px);
  background: var(--paper-light);
  border: 1.5px solid rgba(33,27,17,.55);
  border-radius: 3px 14px 3px 16px;
  box-shadow:
    0 5px 20px rgba(33,27,17,.15),
    inset 0 0 44px rgba(151,124,82,.16);
}
.join__steps{
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(22px, 3.4vw, 44px);
  counter-reset: join-step;
}
.join__step{ counter-increment: join-step; min-width: 0; }
.join__head{
  display: flex;
  align-items: baseline;
  gap: .55rem;
  font-size: 1.2rem;
  margin-bottom: .9rem;
}
.join__head::before{
  content: counter(join-step);
  font-size: 2.1rem;
  line-height: 1;
  color: var(--ink-soft);
}
/* label over value: a value gets the column's whole width, so a URL
   stays on one line to be read and copied (side by side, the explorer
   URL broke after "https://sepolia-" at every desktop width) */
.join__kv{ margin: 0; }
.join__kv > div + div{ margin-top: .6rem; }
.join__kv dt{
  font-family: var(--font-display);
  font-size: .7rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.join__kv dd{
  margin: .1rem 0 0;
  font-size: .9rem;
  overflow-wrap: anywhere;
}
.join__kv .join__v{
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: .84rem;
  -webkit-user-select: all;
          user-select: all;
  /* a 42-character 0x address is wider than a desktop column: two even
     halves rather than 41 characters and a lone last one */
  word-break: break-all;
  text-wrap: balance;
}
.join__step p{
  font-size: .95rem;
  line-height: 1.6;
  color: var(--ink-soft);
}
.join__step p + p{ margin-top: .45rem; }
.join__step p + .join__kv, .join__kv + p{ margin-top: .9rem; }
.join__k{
  font-family: var(--font-display);
  font-size: .74rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink);
}
.join a{ color: var(--ink); text-underline-offset: 3px; }
.join__step .join__warn{ font-style: italic; color: var(--ink); }
/* the Korean page (js/i18n.js sets <html lang="ko">): hangul labels
   without the latin small caps' tracking, and lines broken at spaces
   only — by default a line broke inside "브리지”는", leaving "는" alone */
:root[lang="ko"] .join{ word-break: keep-all; }
:root[lang="ko"] .join__kv dt,
:root[lang="ko"] .join__k{ letter-spacing: .04em; font-size: .78rem; }
@media (max-width: 860px){
  .join{ padding: 90px 0 100px; }
  .join__steps{ grid-template-columns: 1fr; }
}

/* ?join=stop — the same section, ON the map at Namsangol (desktop):
   js/journey.js moves it into .jmap inside .jmap__join and fades it by
   the camera's distance from the stop, like the old ?info=1 cards. The
   padding keeps it clear of the language pill (top) and the CA pill
   (bottom); on a short window the card scrolls inside itself. */
.jmap__join{
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 64px clamp(16px, 3vw, 44px) 76px;
  pointer-events: none;
  z-index: 3;
}
.join.join--stop{
  width: min(400px, 34vw);
  max-height: 100%;
  overflow-y: auto;
  padding: clamp(14px, 1.8vw, 22px) clamp(14px, 2vw, 24px);
  pointer-events: auto;
  background: var(--paper-light);
  border: 1.5px solid rgba(33,27,17,.55);
  border-radius: 3px 14px 3px 16px;
  box-shadow: 0 5px 20px rgba(33,27,17,.18), inset 0 0 44px rgba(151,124,82,.16);
}
.join--stop .section-head{ padding: 0; text-align: left; }
.join--stop .eyebrow{ font-size: .7rem; margin-bottom: .35rem; }
.join--stop h2{ font-size: 1.35rem; }
.join--stop .section-head h2::after{ width: 64px; margin: .55rem 0 0; }
.join--stop .join__card{ width: auto; margin: 14px 0 0; padding: 0; background: none; border: 0; box-shadow: none; }
.join--stop .join__steps{ grid-template-columns: 1fr; gap: 16px; }
.join--stop .join__head{ font-size: 1rem; margin-bottom: .5rem; }
.join--stop .join__head::before{ font-size: 1.5rem; }
.join--stop .join__step p{ font-size: .86rem; line-height: 1.5; }
.join--stop .join__kv .join__v{ font-size: .76rem; }
/* label beside value here — the card is a column of the screen, and
   stacked it ran ~100px past a 900px window; at .76rem the explorer
   URL still fits beside its label in the 400px card */
.join--stop .join__kv{
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: .7rem;
  row-gap: .3rem;
}
.join--stop .join__kv > div{ display: contents; }
.join--stop .join__kv > div + div{ margin-top: 0; }
.join--stop .join__kv dt{ padding-top: .2em; }
.join--stop .join__kv dd{ margin: 0; }

/* LEDGER --------------------------------------------------------- */

.ledger{ padding: 40px 0 140px; }
.ledger__frame{
  width: min(var(--w-wide), 94vw);
  margin: 44px auto 0;
  padding: clamp(14px, 3vw, 34px);
  background: var(--paper-light);
  border: 1.5px solid rgba(33,27,17,.55);
  border-radius: 3px 14px 3px 16px;
  box-shadow:
    0 5px 20px rgba(33,27,17,.15),
    inset 0 0 44px rgba(151,124,82,.16);
}
.ledger__frame svg{ display: block; width: 100%; height: auto; }
.ledger__legend{
  margin-top: 12px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  font-size: .82rem;
  color: var(--ink-soft);
}
.ledger__key{
  width: 10px; height: 14px;
  display: inline-block;
  border: 1.5px solid var(--ink);
  border-radius: 1px;
}
.ledger__key--up{ background: var(--ink); }
.ledger__key--down{ background: transparent; margin-left: 14px; }

/* VILLAGE -------------------------------------------------------- */

.village{ padding: 20px 0 120px; }

.village__stats{
  display: flex;
  justify-content: center;
  gap: clamp(20px, 6vw, 84px);
  margin: 40px auto 8px;
  padding: 0 24px;
  flex-wrap: wrap;
}
.stat{
  text-align: center;
  min-width: 120px;
}
.stat__label{
  display: block;
  font-family: var(--font-display);
  font-size: .75rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.stat__value{
  display: block;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 3.4vw, 2.2rem);
  margin-top: .35rem;
}

.village__field{
  width: min(var(--w-wide), 94vw);
  margin: 26px auto 0;
}

/* The plate stacks the painted layers over the inline SVG fallback.
   Its aspect-ratio is a starting value only — js/village.js replaces it
   with the base plate's own dimensions once that image loads, so the art
   sets the box rather than being cropped into it. */
.village__plate{
  position: relative;
  aspect-ratio: 900 / 380;
}

/* A painting on parchment dropped into a 1080px box shows its own four
   edges against the page — the same trap as the journey's untreated
   panels (HANDOFF §5, ?panels=). Feather them so the plate reads as a
   drawing ON the sheet rather than a tile pasted onto it. Two linear
   masks intersected: a radial would pull the corners in too far and
   crop the field. Only applied once the art is up (.has-art), so the
   inline SVG fallback is never softened. */
.village__plate.has-art{
  --edge: 4%;
  -webkit-mask-image:
    linear-gradient(to right,  transparent 0, #000 var(--edge), #000 calc(100% - var(--edge)), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 var(--edge), #000 calc(100% - var(--edge)), transparent 100%);
          mask-image:
    linear-gradient(to right,  transparent 0, #000 var(--edge), #000 calc(100% - var(--edge)), transparent 100%),
    linear-gradient(to bottom, transparent 0, #000 var(--edge), #000 calc(100% - var(--edge)), transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}
.village__plate > svg,
.village__plate > .v-base,
.village__plate > .v-state{
  position: absolute;
  /* longhands, not `inset` — older iOS Safari ignores the shorthand */
  top: 0; left: 0; right: 0; bottom: 0;
  display: block;
  width: 100%;
  height: 100%;
}
.village__plate > .v-base,
.village__plate > .v-state{ object-fit: contain; }

/* state-plate crossfade: the incoming frame fades in over the old
   one. The frames share every pixel except the new building, so only
   the building appears — under reduced motion js swaps instantly. */
.motion-ok .v-state{ transition: opacity 1.2s ease; }
.v-state.is-fading{ opacity: 0; }

/* A building stands ON its plot: anchored bottom-centre, so `top` is the
   ground line and the art can be any height. --flip mirrors it, which is
   how six PNGs cover fifteen plots without reading as six stamps. */
.v-house{
  --flip: 1;
  position: absolute;
  height: auto;
  transform: translate(-50%, -100%) scaleX(var(--flip));
  transform-origin: 50% 100%;
}
.v-house[hidden]{ display: none; }

/* VILLAGERS (js/villagers.js, crew-test branch). Placed by a per-frame
   translate from the plate's top-left; the frame box is the crew's own
   (.v-walker__win shares the .crew__inner rules and crew-frames, further
   down). They walk only while the plate is on screen (.v-walking) and
   stand still at a route's end (.is-idle). */
.v-walker{
  position: absolute;
  left: 0; top: 0;
  pointer-events: none;
  will-change: transform;
}
/* a soft pool of ink under the feet: tan figures on a tan path had
   nothing to separate them from the ground, and the painted houses
   all stand on a shadow of their own */
.v-walker::before{
  content: "";
  position: absolute;
  left: 8%; right: 8%;
  bottom: -4%;
  height: 16%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(52, 40, 22, .38), rgba(52, 40, 22, 0));
}
.motion-ok .v-walking .v-walker:not(.is-idle) .v-walker__win{ animation-play-state: running; }

/* a roof raised since the last render rises into place */
.motion-ok .v-house.is-rising{
  opacity: 0;
  transform: translate(-50%, calc(-100% + 12px)) scaleX(var(--flip));
  transition: opacity .9s ease, transform .9s ease;
}
.motion-ok .v-house.is-rising.is-in{
  opacity: 1;
  transform: translate(-50%, -100%) scaleX(var(--flip));
}

/* ---- sleeping Z's (PLAN phase 3B) ---------------------------------
   js/village.js decides WHO sleeps and each house's rhythm (--zt the
   cycle length, --zp the phase); everything about how a Z moves lives
   here. Three glyphs per house share one keyframe loop, staggered .55s
   apart, so during a burst they read as a trail rising off the roof —
   then the same loop holds them at opacity 0 for the rest of the
   cycle, which is where the 6-10s of silence comes from. All
   percentages, so the drift scales with the Z, which scales with the
   house. The container is a zero-height anchor at the roof peak;
   the glyphs are positioned off its bottom edge and rise above it. */
.v-zzz{
  position: absolute;
  transform: translateX(-50%);
  pointer-events: none;
}
.v-zzz[hidden]{ display: none; }
.v-zzz svg{
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  aspect-ratio: 12 / 14;
  opacity: 0;             /* only ever visible mid-keyframe */
  will-change: transform, opacity;
}
/* an ink Z over a faint paper halo — the halo is what keeps it legible
   for the frames where it crosses a dark roof edge. The stroke is fat
   on purpose: at 10-17px, weight is most of what reads as "cartoon". */
.v-zzz__halo{
  fill: none; stroke: #EBDDB9; stroke-width: 5.2;
  stroke-linecap: round; stroke-linejoin: round; opacity: .6;
}
.v-zzz__ink{
  fill: none; stroke: #211B11; stroke-width: 2.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* animations exist only while the plate is on screen (.v-live, set by
   an IntersectionObserver in village.js) and motion is allowed.
   The bezier is the smoothing: the ride starts at a drift and
   decelerates the whole way, like a bubble losing its push — `linear`
   travelled at one speed and stopped dead, which is what VEN read as
   not smooth. It applies per keyframe segment, and the transform runs
   0%..17% as ONE segment (the middle keyframes only touch opacity), so
   the whole rise is a single eased gesture. */
.motion-ok .v-live .v-zzz svg{
  animation: v-zzz-rise var(--zt, 10s) cubic-bezier(.25, .55, .4, 1) infinite;
  animation-delay: var(--zp, 0s);
}
.motion-ok .v-live .v-zzz svg:nth-child(2){ animation-delay: calc(var(--zp, 0s) + .55s); }
.motion-ok .v-live .v-zzz svg:nth-child(3){ animation-delay: calc(var(--zp, 0s) + 1.1s); }
/* the visible part is ~17% of the cycle: bloom in over ~0.4s, ride up,
   melt out over the last ~0.7s. Peak opacity .55 — a Z is a whisper on
   the painting, not a marker over it. Rotation stays inside ±6°; any
   more reads as wobble once the easing slows the ride down. */
@keyframes v-zzz-rise{
  0%   { opacity: 0;   transform: translate(2%, 12%) scale(.55) rotate(-6deg); }
  4%   { opacity: .52; }
  10%  { opacity: .48; }
  17%  { opacity: 0;   transform: translate(52%, -175%) scale(1.04) rotate(5deg); }
  100% { opacity: 0;   transform: translate(52%, -175%) scale(1.04) rotate(5deg); }
}
/* PLAN 3B named the risk when this was specced: on a 390px plate a
   legible Z is ~3px, and the answer is to not show them, not to make
   them bigger. Below this width the biggest Z in the field is ~6px. */
@media (max-width: 640px){
  .v-zzz{ display: none; }
}
.village__next{
  text-align: center;
  color: var(--ink-soft);
  font-size: .92rem;
  margin-top: 6px;
}

/* FOOTER --------------------------------------------------------- */

.footer{
  border-top: 1.5px solid rgba(33,27,17,.4);
  padding: 64px 24px 54px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
  text-align: center;
}
.footer__word{
  font-family: var(--font-display);
  letter-spacing: .5em;
  color: var(--ink-soft);
}
.footer__links{
  display: flex;
  gap: 26px;
  font-family: var(--font-display);
  letter-spacing: .08em;
}
.footer__links a{ text-decoration: none; border-bottom: 1.5px solid rgba(33,27,17,.5); padding-bottom: 2px; }
.footer__links a:hover{ border-bottom-color: var(--ink); }
.footer__fine{
  color: var(--ink-soft);
  font-size: .8rem;
  max-width: 46ch;
}

/* THE CREW (js/crew.js, crew-test branch) ------------------------
   Zico's villagers sat on the tops of the letters. The layer is laid
   exactly over the title svg and every figure is placed in % of it,
   so the crew scales with the title. Each figure is a box one frame
   wide whose BACKGROUND is a strip of --n frames, sized n boxes wide
   and stepped across by background-position: at p% the offset is
   -p·(n-1) frames, so steps(n, jump-none) from 0% to 100% lands on
   frame 0 and on frame n-1 exactly. */
/* three planes: the walkers behind the ink, the ink, everyone else in
   front of it */
.hero__title.has-crew{ position: relative; isolation: isolate; }
.hero__title.has-crew svg{ position: relative; z-index: 1; }
.crew{ position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.crew--behind{ z-index: 0; }
.crew__fig{ position: absolute; }
/* The frame box. NOT an <img> sliding inside a clipped window: that
   was the first build, and Chrome dropped the mirrored figures
   entirely (a 7920px strip hanging thousands of px off-screen, painted
   or not depending on where it sat). A background never lays out
   beyond its box, so a mirror is just scaleX(-1) on this small box. */
.crew__inner,
.v-walker__win{
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-size: calc(var(--n) * 100%) 100%;
  background-position: 0% 0;
}
.crew__inner.is-flipped,
.v-walker__win.is-flipped{ transform: scaleX(-1); }
.motion-ok .crew__inner,
.motion-ok .v-walker__win{
  animation: crew-frames var(--dur) steps(var(--n), jump-none) infinite;
  animation-play-state: paused;
}
.motion-ok .crew--live .crew__inner{ animation-play-state: running; }
@keyframes crew-frames{
  from{ background-position: 0% 0; }
  to  { background-position: 100% 0; }
}
/* they climb on after the title is written: a short drop onto the
   letter, as if hopping up onto it. Without motion they are simply
   there. */
.motion-ok .crew__fig{
  opacity: 0;
  transform: translateY(-14%);
  transition: opacity .5s ease, transform .6s cubic-bezier(.3, 1.5, .5, 1);
}
.motion-ok .crew.is-in .crew__fig{ opacity: 1; transform: none; }
/* PAINTERS (js/crew.js kind "paint") were painted against the letters, so
   they never hop onto them — a drop would lift the brush off the ink. Each
   fades in on the spot: the moment the stroke he works on is written (his
   own .is-in), or with the whole layer once the title is done. */
.motion-ok .crew__fig--paint{ transform: none; transition: opacity .45s ease; }
.motion-ok .crew .crew__fig--paint.is-in{ opacity: 1; }

/* CARRIERS (js/crew.js deliver()) are driven frame by frame from one
   clock: position, frame and fade are inline styles, so these boxes
   take no stepping animation, no entrance drop and no transitions —
   and they stay out of sight until the crew has climbed on. */
.crew__inner.is-js{ animation: none !important; }
.crew__fig--js{ transition: none !important; transform: none !important; }
.motion-ok .crew__fig--js{ opacity: 1; }
.motion-ok .crew:not(.is-in) .crew__fig--js{ visibility: hidden; }

/* small screens -------------------------------------------------- */

@media (max-width: 560px){
  body{ font-size: 16px; }
  .hero{ padding-bottom: 110px; }
  .ca-pill__value{ max-width: 96px; }
  .manifesto{ padding: 90px 24px 100px; }
  .ledger{ padding-bottom: 100px; }
}
