/* ================================================================
   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;

  --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{
  content: "韓";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: 24px;
  color: var(--paper-light);
}

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

.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;
}
.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;
}

/* 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;
}

.hero__title{
  width: min(640px, 88vw);
  margin: 0 auto;
}
.hero__title svg{ display: block; width: 100%; height: auto; overflow: visible; }
.hero__title-fallback{
  font-family: var(--font-display);
  font-size: clamp(3rem, 11vw, 5.4rem);
  letter-spacing: .12em;
}
.js .hero__title-fallback{ display: none; }

.hero__tagline{
  margin-top: 2.4rem;
  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; }
}

/* 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; }

/* 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; }

/* 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;
}

/* 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; }
}
