:root {
  --bone: #F3F1EC; --stone: #E4E0D8; --ink: #161714;
  --white: #FBFAF7; --ember: #D9873E; --quiet: #585B55;
  --paper: #D6CFC3;
  --sans: "Instrument Sans", system-ui, -apple-system, sans-serif;
  --serif: "Old Standard TT", Georgia, serif;
  color-scheme: light;
}
* { box-sizing: border-box; }
/* Clears the fixed header, measured at 66.56px now that it carries only the
   Enquire control. hero.js overwrites this with the live height; this value
   is what stands if the script never runs. */
html { scroll-padding-top: 72px; }
:focus-visible { outline: 2px solid var(--ember); outline-offset: 3px;
                 box-shadow: 0 0 0 5px rgba(10,12,12,.75); }
body { margin: 0; background: var(--bone); color: var(--ink);
       font: 400 1rem/1.65 var(--sans); }
/* The storyboard's type direction, which this stylesheet had inverted:
   "Type: the sheet's own (Old Standard TT) for headings; a plain sans for body
   copy." The serif was reaching only the map labels and the coordinates line,
   so the page read as a modern product landing page rather than the
   cartographic, editorial thing the storyboard describes. Weight 400 is not a
   choice: assets/fonts/old-standard-tt.woff2 ships that weight alone, and
   asking for 500 would make the browser synthesise a bold. */
/* SANS headlines. The spec asks for "clean sans-serif type for headlines
   and body", with the serif kept for the map; 64c6a36 put the serif on the
   whole page, which also meant the hero's live wordmark rendered in Old
   Standard TT against brand/logo/lockup/lockup.css, whose own --pv-font is
   Instrument Sans. The serif stays where it belongs: the map labels, the
   coordinates, and the hero tagline. */
h1, h2 { font-weight: 500; letter-spacing: -.02em; }

.top { position: fixed; inset: 0 0 auto 0; z-index: 20; display: flex;
       align-items: center; gap: 16px; color: var(--white);
       padding: calc(14px + env(safe-area-inset-top, 0px)) clamp(16px, 3vw, 40px) 14px;
       transition: background .3s, color .3s; }
.top.on-bone { background: rgba(243,241,236,.92); color: var(--ink);
               backdrop-filter: blur(8px); }
/* The lockup: the mark, then the name on two lines with "Resort" on a rule
   that dissolves outward rather than stopping - the same move the mark makes
   with its eased ends. Chosen 2026-10-15; the exploration is in
   experiments/lockup.html and experiments/lockup-d.html, and the portable
   copy, with the alternative ridge build, is in brand/logo/lockup/.

   ONE knob: --pv-w, the lockup's WIDTH. The type is sized from it, so the
   mark, "Pleasant Valley" and the ruled line share one left and one right
   edge. Sizing by the mark's height instead is what left the old one-line
   wordmark overhanging the mark by 8%. The ratios below are derived from the
   font's own metrics - 13.155em of tracked caps in "PLEASANT VALLEY" - so do
   not hand-edit them; see brand/logo/lockup/lockup.css. */
.lockup { --pv-l1: calc(var(--pv-w) * .07602);
          --pv-l2: calc(var(--pv-l1) * .871);
          --pv-mark: .62;          /* the ridge against the wordmark's width */
          display: grid; gap: calc(var(--pv-w) * .0356); width: var(--pv-w);
          text-decoration: none; color: inherit; }
/* The wordmark still sets the lockup's width - that is what keeps "Pleasant
   Valley" and the ruled line sharing one left and one right edge - but the
   ridge is a crest above the name rather than a band as wide as it, so it is
   scaled down and centred over it. --pv-mark is the only number to change. */
.lockup img { display: block; justify-self: center; height: auto;
              width: calc(var(--pv-w) * var(--pv-mark)); }
.lockup-wm { display: grid; gap: calc(var(--pv-l1) * .33); justify-items: stretch; }
.lockup-l1, .lockup-l2 { text-transform: uppercase; white-space: nowrap; line-height: 1; }
/* letter-spacing leaves a trailing gap after the last letter; pulling it back
   is what makes the line's box end where its ink ends. */
.lockup-l1 { font-size: var(--pv-l1); font-weight: 500; letter-spacing: .26em;
             margin-right: -.26em; }
.lockup-l2 { font-size: var(--pv-l2); font-weight: 400; letter-spacing: .3em;
             display: flex; align-items: center; gap: .85em; }
.lockup-word { margin-right: -.3em; }
/* A flex child defaults to its content width as the basis; for an empty span
   that is 0, but stating it keeps the rules from ever claiming space. */
.lockup-rule { flex: 1 1 0; width: 0; min-width: 4px; opacity: .55;
               height: max(1px, calc(var(--pv-l2) * .057)); }
.lockup-rule:first-child { background: linear-gradient(90deg, transparent, currentColor); }
.lockup-rule:last-child  { background: linear-gradient(90deg, currentColor, transparent); }
/* The lockup only ever sits over footage, so the lift is unconditional. The
   type needs the same lift as the mark or it floats free of it, and the rules
   are painted backgrounds rather than glyphs, so text-shadow never reaches
   them - a 1px bar at 55% opacity disappears entirely over a bright frame.
   They get their own. */
.lockup img { filter: drop-shadow(0 1px 3px rgba(10,12,12,.55)); }
.lockup-l1, .lockup-l2 { text-shadow: 0 1px 3px rgba(10,12,12,.55); }
.lockup-rule { filter: drop-shadow(0 1px 2px rgba(10,12,12,.6)); }
.nav { display: flex; gap: 10px; }
.nav a, .enquire { color: inherit; text-decoration: none;
           font-size: .78rem; font-weight: 500; letter-spacing: .14em;
           text-transform: uppercase; border: 1px solid currentColor;
           padding: 11px 18px; }
.nav a { text-shadow: 0 1px 3px rgba(10,12,12,.55); transition: background .2s, color .2s; }
.top.on-bone .nav a { text-shadow: none; }
/* currentColor for the fill would take the text with it; the invert has to
   name both sides, and the bar's two states invert opposite ways. */
.nav a:hover { background: var(--white); color: var(--ink); text-shadow: none; }
.top.on-bone .nav a:hover { background: var(--ink); color: var(--bone); }
.enquire { margin-left: auto; }

/* Centred on the BAR, not between its neighbours: absolute, so the nav on
   one side and Enquire on the other keep the width they had and the mark
   sits on the page's centre line wherever those end up. Hidden until the
   clearing's flight delivers it, and then it stays for the rest of the
   page. Laid out all along, because the flight animates to its measured
   rectangle and a display:none element has none. */
.top .brand { position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%); line-height: 0;
  opacity: 0; pointer-events: none; transition: opacity .25s ease; }
.top .brand img { display: block; width: 150px; height: auto; }
/* Narrower where Enquire is close. Centred at 390 px a 150 px lockup
   reaches within 5 px of the button's border and the final Y reads as
   touching it; 120 leaves a clear 20. The fold measures this element, so
   the mark arrives at whatever width the bar is giving it. */
@media (max-width: 440px) { .top .brand img { width: 120px; } }
html.brand-docked .top .brand { opacity: 1; pointer-events: auto; }

/* The lockup in flight: fixed, and ABOVE the header, so it crosses the bar
   rather than disappearing under it and lands on the mark the bar already
   holds. Positioned from its own centre, so one transform carries both the
   travel and the shrink. */
.iso-flight { position: fixed; left: 0; top: 0; z-index: 25; overflow: hidden;
  opacity: 0; pointer-events: none; will-change: transform, opacity; }
/* height:auto, and it is load-bearing. The <img> carries width="600"
   height="174" for the aspect box, and those attributes map to CSS
   presentation hints - so setting only style.width from the script left
   the height pinned at 174 px and the lockup drew 67% too tall, its
   letters sliced in half by the element's own clip. */
.iso-flight img { position: absolute; left: 0; top: 0; display: block;
  max-width: none; height: auto; }
/* The brand hides ONLY in the band where the nav and Enquire leave it no
   room. Below 620 the nav is gone and the room is back - and the brand has
   to be there, because it is what the valley folds into. Hiding it on a
   phone meant the picture dissolved on the way out with nothing arriving in
   its place. */
@media (min-width: 621px) and (max-width: 760px) {
  .top .brand { display: none; }
}
/* The Enquire control is a section link that happens to be the one that
   matters: same border, same weight, same translucency, placed opposite. */
.enquire:hover { background: var(--white); color: var(--ink); text-shadow: none; }
.top.on-bone .enquire:hover { background: var(--ink); color: var(--bone); }
.enquire { text-shadow: 0 1px 3px rgba(10,12,12,.55); transition: background .2s, color .2s; }
.top.on-bone .enquire { text-shadow: none; }
/* Below this the two section links and the Enquire control cannot share the
   bar without one of them wrapping. Enquire is the one that has to survive. */
@media (max-width: 620px) { .nav { display: none; } }
/* Draft copy waiting on a human - the host's note, how booking works. Scoped
   to the body text: the CTAs used to be marked this way too, which is what
   put an ember fill and a "number not set" label on the one control the page
   exists for. They look finished now in both states, and the safeguard moved
   somewhere a human cannot overlook: tools/check_assets.py --deploy fails
   while js/config.js still holds the placeholder number. */
p[data-placeholder] { outline: 1px dashed var(--ember); outline-offset: 3px; }
[aria-disabled="true"] { cursor: not-allowed; }
/* The film control is not on the page any more, for anyone using a mouse:
   no button, no hover reveal. It is still IN the page, clipped to a pixel
   the way a skip link is, because two things need it to exist.

   WCAG 2.2.2 asks for a way to stop motion that starts by itself, and a
   keyboard or screen-reader user reaches this one the ordinary way - it is
   the first thing after the header. And when a browser refuses autoplay, or
   reduced motion is set, this is the only way to start the film at all;
   without it those visitors have a still poster and no way in.

   Deleting the element instead would be a one-line change. It would also
   mean a hero that moves and cannot be stopped. */
.film-toggle { position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; }
/* Keyboard focus brings back the real control, where it used to sit. A
   focused thing the user cannot see is worse than no control at all. */
.film-toggle:focus-visible {
  width: auto; height: auto; margin: 0; clip-path: none; overflow: visible;
  right: clamp(16px, 3vw, 40px); bottom: calc(2vh + env(safe-area-inset-bottom, 0px));
  z-index: 5; min-width: 44px; min-height: 44px; padding: 0 14px;
  color: var(--white); background: rgba(10,12,12,.3);
  border: 1px solid rgba(251,250,247,.55); backdrop-filter: blur(6px);
  cursor: pointer; font: 500 .66rem var(--sans);
  letter-spacing: .14em; text-transform: uppercase; }

/* The hero is a scroll TRACK with a pinned stage in it, not a single
   viewport. With a one-viewport hero the fog's whole rise and the map's
   arrival happened over the same scroll, so the map won and the fog was
   never really seen. The second viewport is scroll that belongs to the fog.

   Pinned only where motion is welcome: with reduced motion there is no fog,
   and a second viewport of pinned nothing is just scrolling for its own
   sake. The base rule is the plain one-viewport hero. */
.hero { position: relative; height: 100svh; background: var(--ink); }
.hero-stage { position: relative; height: 100%; overflow: hidden; }
@media (prefers-reduced-motion: no-preference) {
  .hero { height: 200svh; }
  .hero-stage { position: sticky; top: 0; height: 100svh; }
}
#film, #atmosphere { position: absolute; inset: 0; width: 100%; height: 100%; }
#film { object-fit: cover; object-position: 50% 42%; }
#atmosphere { pointer-events: none; z-index: 2; }
/* Sits at the end of the pinned phase. In the plain one-viewport hero - with
   reduced motion, where nothing is pinned - that is simply the hero's own
   bottom edge, which is where the bar used to flip anyway. */
.hero-hinge { position: absolute; top: 100svh; left: 0; width: 1px; height: 1px; }
/* The scrim has to work harder than it did for a text headline. The white
   mark separates its five bands by OPACITY, not value - 0.22 at the back to
   1.0 at the front - so over a bright frame the back ridges simply are not
   there and the mark reads as a white smudge. It needs genuinely dark ground
   under it, not merely less-bright ground. Hence the lower stop moved up and
   deepened. Still a gradient, not a card: the storyboard's "text sits
   directly on imagery over a soft gradient" holds. */
.scrim { position: absolute; inset: 0;
         background:
           /* A soft pool under the lockup. A linear gradient alone cannot do
              this: it darkens a whole band of the frame to fix one object in
              the middle of it. Anchored where .hero-copy sits, so the mark
              always has ground whatever the film is doing behind it. */
           radial-gradient(64% 34% at 50% 82%, rgba(10,12,12,.62), rgba(10,12,12,0) 72%),
           /* The tagline sits half way up the left edge, where the vertical
              gradient has nothing to give it - its stops are at the top and
              the bottom. This is the prototype's left-hand wash, brought
              back for the same reason it existed there. */
           linear-gradient(90deg, rgba(10,12,12,.5), rgba(10,12,12,0) 46%),
           linear-gradient(180deg, rgba(10,12,12,.45), rgba(10,12,12,0) 30%,
                     rgba(10,12,12,0) 42%, rgba(10,12,12,.38) 70%, rgba(10,12,12,.66)); }
.hero-copy { position: absolute; left: 0; right: 0; bottom: 8vh; text-align: center;
             color: var(--white); padding: 0 16px; }
/* The headline is the mark itself, set as the h1 - built from the same real
   text as the header's lockup, so the heading still reads "Pleasant Valley
   Resort" to a screen reader and to a search engine. Only --pv-w changes. */
.hero-copy h1 { --pv-w: clamp(210px, 30vw, 420px); margin: 0 auto; }
/* The place, called out under the name. Tracked caps rather than the serif,
   so it reads as a label on the mark instead of a second headline. */
.eyebrow { margin: 16px 0 0; font-size: clamp(11px, .85vw, 13px);
           font-weight: 500; text-transform: uppercase;
           letter-spacing: .34em; text-indent: .34em; }
/* Half way up, on the left. Italic serif - and the italic is a real face:
   assets/fonts/old-standard-tt-italic-400.woff2, not the browser slanting
   the roman, which on a serif this fine reads as a printing fault. */
.hero-tagline { position: absolute; z-index: 3; margin: 0;
                left: clamp(16px, 3vw, 40px);
                top: 50%; transform: translateY(-50%);
                font: italic 400 clamp(16px, 1.5vw, 22px)/1.5 var(--serif);
                color: var(--white); max-width: 34ch;
                text-shadow: 0 1px 4px rgba(10,12,12,.7); }

/* The white the fog resolves into. Not shown by default: with reduced motion
   the hero is a plain one-viewport still, there is no fog, and this would be
   a screen of nothing to scroll past for no reason. */
/* THE CLEARING - what the fog lifts off.

   Without motion this is simply a figure: the finished render, its names,
   its caption, at the section's natural height. It used to be display:none
   there, which was right while it was a blank white breath between the film
   and the map and is wrong now that it carries the valley itself. Hiding a
   picture of the place from somebody who asked for less animation is not
   the same as showing them less animation.

   With motion it becomes a 360svh track: the fog that rose through the hero
   comes down, settles into a band, and hands over to the deck baked into
   the render. js/clearing.js drives it. */
.clearing { background: #fff; padding: clamp(3rem, 8vw, 6rem) 16px; }
.clearing-stage { display: grid; place-items: center; position: relative; }
/* The header is FIXED, so the sticky stage below it is the full viewport
   and the figure centres behind the bar: measured, the summit's top 25 px
   sat under it, and the bar is 92% opaque so the peak ghosted through
   rather than simply being hidden. The stage reserves the bar's height.

   --header-h is set from the real element by clearing.js, because the
   bar's height is content-driven - padding plus whatever the nav buttons
   come out as - and is not a number this stylesheet knows. The fallback
   is what it measures today; being wrong there costs the overlap back,
   nothing worse. */
:root { --header-h: 73px; }
.iso { margin: 0; }
.iso-frame { position: relative;
  /* THE HEIGHT CAP IS PART OF THE WIDTH, not a max-height.
     max-height does NOT shrink the width to match: with a definite width
     and an aspect-ratio, the height is computed from the ratio and then
     simply clamped, and the box keeps its width. The image, at 100% of
     both, then stretches to fill a box of the wrong shape - measured, the
     block was being squashed vertically by 25%, and nothing caught it
     because every label is positioned in PERCENT of the frame and a
     uniform squash moves them all together.

     So all three limits go in one min(), and the ratio appears in the
     width expression because it has to. It is not written out twice:
     clearing.js sets --iso-ratio from the label manifest, the same export
     that places the names, and the value below is the pre-script
     fallback. */
  --iso-ratio: 1.29685;
  /* 6rem of reserve, not 11: 11 was sized when a caption sat under the
     figure. What is left to leave room for is the two lowest names, which
     the stack is allowed to carry 6% below the picture. */
  width: min(1090px, 72vw,
             calc((100svh - var(--header-h) - 4rem) * var(--iso-ratio)));
  aspect-ratio: var(--iso-ratio); }
.iso-img { position: absolute; inset: 0; display: block;
           width: 100%; height: 100%; }
.clearing-text { max-width: 34ch; }
/* Set as the Stay section's head: eyebrow, sans headline, quiet lede. */
.clearing-eyebrow { margin: 0 0 10px; font-size: .72rem; line-height: 1;
  letter-spacing: .18em; text-transform: uppercase; color: var(--quiet); }
.clearing-text h2 { margin: 0 0 16px; font-size: clamp(28px, 2.9vw, 44px);
  line-height: 1.05; letter-spacing: -.015em; text-wrap: balance; }
.clearing-text p:not(.clearing-eyebrow) { margin: 0; color: var(--quiet);
  font-size: clamp(1rem, 1.15vw, 1.1rem); line-height: 1.6; }
/* NARROWER than 1100 px: the text goes ABOVE the block, centred. On a phone
   the block is width-bound and leaves half the screen empty, so there is
   room; the block's height cap gives up 11rem for the text so the two
   still fit one screen together on a tablet. */
@media (max-width: 1099px) {
  .clearing-stage { grid-template-rows: auto auto; align-content: center;
    row-gap: clamp(1.2rem, 4svh, 2.6rem); }
  .clearing-stage > .clearing-text { grid-row: 1; text-align: center; justify-self: center;
    padding: 0 16px; }
  .clearing-stage > .iso { grid-row: 2; }
  .clearing-text h2 { font-size: clamp(26px, 7vw, 36px); }
  .iso-frame { width: min(1090px, 72vw,
             calc((100svh - var(--header-h) - 4rem - 11rem) * var(--iso-ratio))); }
}
/* WIDE: the block moves left and text sits to its right (2026-10-19). The
   first column is the block plus a margin each side for the names that go
   out beside it (10vw each, measured: the longest needs ~104 px at 1200);
   the block itself is capped at 48vw. Below
   1100 px there is no room for both (see .clearing-text above). */
@media (min-width: 1100px) {
  .clearing-stage { grid-template-columns: 68vw minmax(0, 1fr);
    justify-items: center; align-items: center; }
  .clearing-stage > .iso { grid-column: 1; grid-row: 1; }
  .clearing-stage > .clearing-text { grid-column: 2; grid-row: 1;
    justify-self: start; padding: 0 clamp(16px, 4vw, 64px) 0 2vw; }
  .iso-frame { width: min(1090px, 48vw,
             calc((100svh - var(--header-h) - 4rem) * var(--iso-ratio))); }
}

/* The bottom edge is faded in the ASSET, not here.
   A white wash over the picture was the first attempt and it was wrong: it
   faded the block's own base along with the shadow, because nothing in CSS
   can tell one from the other once they are the same pixels. The export
   does the same job on the alpha instead, where the two ARE separable -
   see soften_shadow in map/scripts/logo_iso_export.py. The object keeps
   its outline; only what it casts is taken away. */
/* Above the images, so the two lowest names and their leaders stay crisp. */
.iso-leaders, .iso-labels { z-index: 2; }

/* Still: the finished render only. The bare plate and the fog exist for the
   landing, which does not run here. */
.iso-bare, .iso-fog, .iso-view { display: none; }
.iso-fog { position: absolute; inset: 0; width: 100%; height: 100%; }

/* NAMES.
   Each one is an anchor dot at the projected position plus a leader and the
   text, offset away from the middle of the picture so the text never covers
   the thing it names. The dot is the truth; the text is placed around it. */
.iso-labels { position: absolute; inset: 0; pointer-events: none; }
/* overflow:visible, because the leaders and the dots they point at live in
   different places: the dot is on the terrain, inside the box; the text is
   out in the page margin beside it. An SVG clips to its own bounds by
   default and would cut every leader at the edge of the picture. */
.iso-leaders { position: absolute; inset: 0; overflow: visible;
  pointer-events: none; }

/* OUTSIDE THE BLOCK.
   Seven names lying across rendered ground, each needing a white glow to be
   legible at all, is seven holes punched in the picture. Out in the margin
   they are simply dark text on white and need no help, and the block is
   left to be looked at. The resort keeps its place on the terrain: it is
   not a name among names, it is the subject, and it has the pin. */
.iso-label.is-out { margin: 0; text-shadow: 0 0 3px #fff, 0 0 8px #fff; }
.iso-label.is-out::before, .iso-label.is-out .lead { display: none; }
.iso-label.is-out.to-left { flex-direction: row; }
.iso-label.is-out.to-left .txt { text-align: right; }
/* ANCHORED ON THE DOT, not on the label.
   translate(-50%, -50%) centres the whole flex row - dot, leader and text -
   on the feature, which puts the dot half a label-width away from the thing
   it points at. The error scales with the text, so short river names looked
   fine and the resort's sat a clear 90 px adrift of the pin rendered at the
   same coordinate. Only the VERTICAL centring was wanted. */
.iso-label { position: absolute; display: flex; align-items: center; gap: 0;
  --dot: 5px;
  transform: translateY(-50%);
  margin-left: calc(var(--dot) / -2);
  font: 400 13px/1.15 var(--serif); color: var(--ink);
  /* The names lie across rendered ground that runs from near-white haze to
     near-black shadow within one word, so no single colour is legible on its
     own. A white glow gives every letter its own background; over the page
     outside the block it costs nothing, being white on white. */
  text-shadow: 0 0 2px #fff, 0 0 4px #fff, 0 0 11px rgba(255,255,255,.85);
  white-space: nowrap; }
.iso-label::before { content: ""; flex: none;
  width: var(--dot); height: var(--dot);
  border-radius: 50%; background: var(--ink); opacity: .75;
  box-shadow: 0 0 0 2px rgba(255,255,255,.7); }
.iso-label .lead { flex: none; height: 1px; width: var(--lead, 26px);
  background: currentColor; opacity: .45;
  box-shadow: 0 0 0 1px rgba(255,255,255,.55); }
.iso-label .txt { flex: none; }
/* Mirrored: the row is anchored by its RIGHT edge so it still grows away
   from the dot, and the margin flips with it. JS sets `right` for these; a
   row-reverse box positioned by `left` would run off to the right of its
   own anchor. */
.iso-label.to-left { flex-direction: row-reverse;
  margin-left: 0; margin-right: calc(var(--dot) / -2); }
.iso-label.to-left .txt { text-align: right; }

/* The resort already HAS a marker: a lit, shadow-casting sphere built into
   the render at this exact coordinate. A second ember dot on top of it is
   two markers for one place. The box is kept - visibility, not display - so
   the leader still starts where it did, at the edge of the pin. */
.iso-label.resort { font-family: var(--sans); font-weight: 600;
  font-size: 12px; letter-spacing: .07em; text-transform: uppercase;
  --dot: 11px; }
.iso-label.resort::before { visibility: hidden; }
.iso-label.peak { font-style: italic; }
/* The height goes UNDER the name, not after it: a peak's label is a name
   and a measurement, and running them together on one line reads as part
   of the name. Upright and tabular, because it is a number. */
/* A summit with no name anywhere is its height, and nothing else. Set
   like the height lines rather than like a name, because that is what it
   is - upright and quieter, not a word in italic serif. */
.iso-label.peak.unnamed { font-style: normal; font-size: 12px;
  letter-spacing: .02em; opacity: .72;
  font-variant-numeric: tabular-nums; }
.iso-label .ele { display: block; margin-top: .15em;
  font-style: normal; font-size: .78em; letter-spacing: .02em;
  opacity: .62; font-variant-numeric: tabular-nums; }
.iso-label.river { font-style: italic; color: #244652; }

@media (prefers-reduced-motion: no-preference) {
  .motion .reveal { opacity: 0; transform: translateY(18px); }
  .motion .pano img { will-change: transform; }

  /* 420svh. The track is travel + one viewport of pinned stage, so this is
     320svh of scrolling, split by js/clearing.js: 120 of landing, 80 of
     holding the finished picture, 120 of the fold.

     It was 520. The landing had 218svh and the cloud only finished 183svh
     in - nearly two screens of scrolling before there was a clear picture
     to look at, which is a long time to ask for on the strength of fog.
     The fold kept its length to the pixel (0.375 of 320 against 0.28 of
     420); every screen that came off came off the landing. */
  .clearing { height: 420svh; padding: 0; }
  /* THE OVERLAP: the clearing starts 30svh up inside the hero's tail, over
     the fog's white body, so the block rises into view sooner. Without it
     the reader scrolled about half a viewport of blank white after the fog
     before the summit appeared.

     30, not more, and measured at 1200x760: the frame's bottom only turns
     white at 0.8 of the hero's pinned travel (mean 248 at 0.8, 117 at 0.7),
     and the section must not reach the screen before that. At 30 it enters
     at 0.7 with its top still below the fold, and the summit first shows at
     0.9, when the fog is white down to 0.68 of the frame. At 40 the edge
     already lay over the film's bottom at 0.7.

     The background fades in over the overlap rather than starting solid, so
     the section's top edge cannot draw a line across the fog's last tint.
     Positioned and z-indexed to paint over the hero's sticky stage; the
     fold's fixed layers (.iso-view, .iso-flight) are outside this section,
     so the stacking context it creates does not trap them. */
  .clearing { position: relative; z-index: 1; margin-top: -30svh;
    background: linear-gradient(rgba(255,255,255,0), #fff 30svh); }
  .clearing-stage { position: sticky; top: 0; height: 100svh;
    padding-top: var(--header-h);
    overflow: hidden; }

  /* THE LANDING STRIP: the section after the clearing is pulled up UNDER
     the pinned stage, so it rises into the space the block vacates and is
     arriving as the mark docks.

     Without it the mark lands on a blank screen. The stage is pinned and
     exactly one viewport tall, so nothing below the clearing can be on
     screen while the pin holds - at 1200x760 the fold finished at scrollY
     4144 with the Stay headline still 1.26 viewports down, and the first
     word only became readable 424 px later. The reader watched the logo
     fly to the bar and then scrolled a whole screen of white.

     Three parts, each load-bearing:
     - the pull itself. 60svh leaves the headline entering the lower third
       at the docking frame, which is late enough that it never competes
       with the block and early enough that the two read as one move.
     - the bone back. The clearing is white and the page is bone, so
       overlapping the stage the band would have changed colour.
     - the stacking order. A sticky stage is a POSITIONED box and paints
       over a static sibling however late that sibling comes in the source.

     .has-fold rather than the media query alone: between 621 and 760 px
     the header brand is hidden, foldReady() returns null and the picture
     never leaves the stage. There is nothing to make room for there, and
     the pull would slide the band over the finished render. js/clearing.js
     sets the class from the same measurement the fold itself runs on. */
  .has-fold .clearing + section {
    position: relative; z-index: 1;
    margin-top: -60svh; background: var(--bone); }
  .iso-bare, .iso-fog { display: block; }
  /* The turn's window. Centred on the stage rather than carried by a
     transform: the size is the picture's size, so shrinking it moves
     nothing sideways and there is no transformed box to measure back. */
  /* Positioned in PIXELS by clearing.js, not centred by percentages: the
     window both shrinks and travels, and a transform would put its own
     output back into the measurement it is computed from. */
  /* FIXED, and above the bar. The window travels into the header, and the
     header is frosted glass - backdrop-filter samples everything painted
     BEHIND it, so at z-index 4 the arriving mark was blurred to a ghost and
     washed out by the bar's 92% bone. Sitting above the bar (under the
     flight's 25) it is sampled by nothing and arrives crisp. Fixed, not
     absolute: it is written in viewport pixels, and the stage it used to
     live in is a stacking context that clips to its own box. */
  .iso-view { display: block; position: fixed; left: 0; top: 0;
    overflow: hidden; z-index: 24; opacity: 0; pointer-events: none; }
  .iso-view img { position: absolute; left: 0; top: 0; display: block;
    max-width: none; height: auto; }
  .iso-drawn { opacity: 0; }

  /* Everything that the landing brings in starts hidden and is driven by
     clearing.js. If the script never runs, these rules never apply either -
     the block above is what is left, and it shows the finished render. */
  .clearing.is-live .iso-deck { opacity: 0; }
  .clearing.is-live .iso-label { opacity: 0; transition: opacity .5s ease; }
  /* Once a GSAP timeline exists it owns these, and a CSS transition on the
     same property would animate towards whatever the tween just set -
     two animations fighting over one number. */
  .clearing.is-drawn .iso-label { transition: none; }

  /* DRIFTING CLOUDS, over the rendered ones.
     Pure white, soft-edged. Over the white page they are invisible by
     construction and resolve only where they cross the terrain, so no mask
     or blend mode is needed to keep them off the paper.

     Deliberately faint, and held back until the landing is over. The render
     carries its own lit deck now, and these flat sprites cannot compete
     with it: at full weight a 2D wash slid across a 3D cloud bank and both
     looked worse. What they are still for is MOVEMENT, which a still render
     does not have. */
  .clouds { position: absolute; inset: -10% -20%; pointer-events: none;
    opacity: 0; transition: opacity 1.2s ease; }
  .clearing.is-landed .clouds { opacity: 1; }
  .clouds::before, .clouds::after {
    content: ""; position: absolute; background-repeat: no-repeat;
    background-image: url("../assets/fog/puff.png");
    background-size: 100% 100%; }
  .clouds-back { z-index: 0; }
  .clouds-front { z-index: 3; }
  /* The negative delays are not decoration. Each puff starts its loop at
     translate(-28%), which is off the left edge, so with no delay all four
     queue up outside the frame and the reader meets a clear sky. */
  .clouds-back::before  { width: 46%; height: 30%; left: 2%;  top: 22%;
    opacity: .34; animation: drift 71s linear infinite -11s; }
  .clouds-back::after   { width: 38%; height: 24%; left: 54%; top: 12%;
    opacity: .28; animation: drift 93s linear infinite -37s; }
  .clouds-front::before { width: 54%; height: 26%; left: -8%; top: 46%;
    opacity: .24; animation: drift 58s linear infinite -36s; }
  .clouds-front::after  { width: 34%; height: 20%; left: 46%; top: 60%;
    opacity: .18; animation: drift 84s linear infinite -71s; }
  /* Across and slightly up, the way cloud moves over a ridge, and the
     vertical term is deliberately not a multiple of the horizontal one so
     the loop does not read as a loop. */
  @keyframes drift {
    from { transform: translate(-28%, 0) }
    to   { transform: translate(128%, -7%) }
  }
}

/* Phone: five river names across 94vw is a thicket. The peaks and the
   resort are the three that answer "what am I looking at"; the rivers need
   width to be worth their ink. */
/* The river names are the long ones - "Chooriyode River" is 105 px of
   serif. Below about 900 px of viewport the margin beside the block is not
   wide enough to hold them, and inside the block is what this change exists
   to stop. The peaks are short enough to keep. */
@media (max-width: 900px) {
  .iso-label.river { display: none; }
}
@media (max-width: 760px) {
  .clearing { height: auto; }
  .iso-frame { width: 94vw; }
  .iso-label { font-size: 11px; }
  .iso-label.resort { font-size: 9px; letter-spacing: .05em; }
}
@media (prefers-reduced-motion: no-preference) and (max-width: 760px) {
  /* Was 440. Cut in the same proportion as the desktop track, so the beats
     - which are fractions of it - keep the pace they were tuned at. */
  .clearing { height: 360svh; }
}

/* Visually hidden, still read aloud. The strip's bars carry no text of
   their own - they are twelve divs - so each row name carries the span
   that says what its bars mean. Without this rule those spans RENDER,
   which is how the band first went up: every row label four times as
   long as the layout expected and the whole grid pushed sideways. */
.sr{position:absolute; width:1px; height:1px; margin:-1px; padding:0;
    overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0}

/* ------------------------------------------------------------------ */
/* SEASONS: THE NIGHT BAND                                             */
/* ------------------------------------------------------------------ */
/* The page's one dark beat. Two halves: the twelve-month STRIP, which
   is the only thing here that shows the seasons overlapping, and a RUN
   of four entries that each get room to be described and a photograph
   to be looked at. Chosen from three options in
   experiments/seasons.html; see docs/seasons.md.

   NOTE THE CLASS NAMES. `.label` and `.bar` were the experiment's, and
   both are taken on this page - `.label` is the zoom's map labels and
   is position:absolute, which would have thrown every row name of the
   strip into the top-left corner of the section. They are `.row-label`
   and `.yr` here. */
.night{position:relative; background:var(--ink); color:var(--bone);
       --paper:rgba(243,241,236,.92); --faint:rgba(243,241,236,.58);
       --hair:rgba(243,241,236,.14)}
.night-head{padding:clamp(4rem,9vw,8rem) clamp(16px,6vw,96px) clamp(2rem,4vw,3.5rem)}
.night .eyebrow{margin:0 0 10px; font:500 .72rem/1 var(--sans);
                letter-spacing:.18em; text-transform:uppercase; color:var(--faint)}
main .night h2{font-size:clamp(30px,4.6vw,56px); line-height:1.05;
               letter-spacing:-.02em; font-weight:500; margin:0 0 34px;
               color:var(--bone)}
.night-note{max-width:62ch; margin:26px 0 0; color:var(--faint);
            font-size:clamp(15px,1.15vw,17px)}

/* THE STRIP ------------------------------------------------------- */
.night .wrap-x{overflow-x:auto; overflow-y:hidden; margin:0 -4px; padding:0 4px}
.months{display:grid; grid-template-columns:auto repeat(12,1fr);
        gap:7px 8px; align-items:center; min-width:40rem}
.months .m{font-size:.76rem; letter-spacing:.1em; text-align:center;
           color:rgba(243,241,236,.45)}
.row-label{font-size:.93rem; padding-right:18px; white-space:nowrap;
           color:var(--paper)}
/* .19, not .11: an empty month still has to read AS a month, and over
   ink at .11 it measured 1.28:1 against the band behind it. */
.yr{height:13px; border-radius:2px; background:rgba(243,241,236,.19);
    position:relative}
.yr.on{background:var(--bone)}
.yr.soft{background:rgba(243,241,236,.42)}
.yr.ember{background:var(--ember)}
/* Dashed is the only honest mark for the misty-morning months, which
   are still unconfirmed - TODO.md. .62 because half a dashed bar is
   gaps, so its mean luminance is half a solid one's and it measured
   2.7:1 where a solid bar measured 15. */
.yr.dash{background:repeating-linear-gradient(90deg,
  rgba(243,241,236,.62) 0 3px, transparent 3px 6px)}

/* THE RUN --------------------------------------------------------- */
/* One row per season, and the photograph sits IN the row - so the
   plate's top and bottom ARE its entry's, at every width, with no
   script holding them together. The cell decides the crop: at 1500 a
   row is 750x529, which is the 4:3 that measured best anyway. */
.run{display:grid; gap:0;
     grid-template-columns:minmax(200px,.72fr) minmax(380px,1fr)
                           minmax(320px,1.72fr);
     grid-template-rows:repeat(4, auto)}
/* grid-row 1/-1 with explicit rows above it: an auto-placed item cannot
   span to -1 in an implicit grid, it silently becomes one row tall. */
.run .hold{grid-column:1; grid-row:1 / -1; position:sticky; top:0;
           align-self:start; height:100svh; display:flex;
           flex-direction:column; justify-content:flex-end;
           padding:clamp(2.5rem,5vw,4.5rem) clamp(16px,3vw,48px)}
.hold-note{max-width:34ch; color:var(--faint); font-size:.93rem}
.hold-note b{display:block; margin-bottom:8px; color:var(--paper);
             font:400 1.05rem/1.3 var(--serif); font-style:italic;
             letter-spacing:.01em}

.season{grid-column:2; min-height:62svh; display:flex; flex-direction:column;
        padding:clamp(2.2rem,4vw,3.6rem) clamp(20px,3vw,46px) clamp(2.6rem,5vw,4.4rem);
        border-left:1px solid var(--hair); border-bottom:1px solid var(--hair);
        background:rgba(243,241,236,.035)}
.season:last-of-type{border-bottom:0}
/* .season .num, not .num: `.season p` below sets margin and the muted
   body colour at the same specificity and later in the sheet, and a
   bare .num loses both - the number ends up grey and marooned in the
   middle of its cell instead of pinned to the top. */
.season .num{margin:0 0 auto; max-width:none; opacity:.92; color:var(--paper);
             font:400 clamp(34px,3.6vw,52px)/1 var(--serif); font-style:italic;
             letter-spacing:.01em}
.season .when{margin:0 0 7px; max-width:none; color:var(--ember);
              font:500 .68rem/1 var(--sans); letter-spacing:.16em;
              text-transform:uppercase}
.season h3{margin:0 0 12px; font:400 clamp(19px,1.7vw,24px)/1.25 var(--serif);
           font-style:italic; letter-spacing:.01em; color:var(--bone)}
.season p{margin:0; max-width:42ch; color:var(--faint)}

/* The plate is 1.72fr of 3.44fr, which is EXACTLY half the page - so
   the markup says sizes 50vw. It said 46 while this was an experiment,
   and an 8% under-declaration is not rounding: at 2x it put the browser
   one rung down the ladder. */
.plate{grid-column:3; overflow:hidden; background:#101109}
.plate img{width:100%; height:100%; object-fit:cover}

@media (max-width: 900px){
  /* One column, and the source order already alternates season, plate,
     season, plate - so each photograph lands under its own words with
     no reordering at all. */
  .run{grid-template-columns:1fr; grid-template-rows:none}
  .run .hold{grid-column:1; grid-row:auto; position:static; height:auto;
             display:block; padding-bottom:1.5rem}
  .season, .plate{grid-column:1}
  .season{border-left:0}
  .plate{height:46svh}
}

/* THE EMBERS ------------------------------------------------------ */
/* Confined to the Fireflies box (01), behind its text: each one flashes
   at a random spot inside it. js/seasons.js. */
.season--fx{position:relative; overflow:hidden}
/* The rain on the monsoon box (02): drawn on a canvas behind the text,
   landing on the top edge of the text block. js/seasons.js. */
.rain-fx{position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:0}
.season--fx > :not(.embers):not(.rain-fx):not(.mist-fx):not(.sun-fx){position:relative; z-index:1}
/* Mist on the misty-mornings box (03): a canvas of drifting cloud puffs,
   after drei's <Cloud>. js/seasons.js. */
.mist-fx{position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:0}
/* Low winter sun through a moving canopy on the cool-and-clear box (04):
   rays from beyond the top-right, swaying in the wind, and motes that glint
   only inside them. js/seasons.js. */
.sun-fx{position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:0}
.embers{position:absolute; inset:0; pointer-events:none; z-index:0}
.ff{position:absolute; left:0; top:0; width:0; height:0; will-change:transform}
.ff i{position:absolute; left:0; top:0; display:block; border-radius:50%;
      transform:translate(-50%,-50%)}
/* About 560 nm, which is what the insect actually emits - and it reads
   as a living thing against the ember bars, which are data. */
.ff .halo{width:calc(52px * var(--d)); height:calc(52px * var(--d));
  background:radial-gradient(circle closest-side,
    rgba(150,196,58,.5) 0%, rgba(150,196,58,.15) 38%, rgba(150,196,58,0) 100%);}
.ff .core{width:calc(4px * var(--d)); height:calc(4px * var(--d));
  background:#E8F5A8; box-shadow:0 0 8px 2px rgba(150,196,58,.7);}

/* The zoom section is a readable static stack by default: no JS, reduced
   motion, or a failed script all land here. The pinned, one-beat-at-a-time
   behaviour is opt-in: zoom-wire.js adds class "is-live" to #zoom, and only
   when motion is allowed. */
.zoom { position: relative; background: var(--paper);
        padding: clamp(3rem, 8vw, 6rem) clamp(16px, 6vw, 96px); }
.zoom-stage { position: relative; }
#zoomCanvas, #zoomOverlay { display: none; }
/* The map's type colours: the sheet's ink, with a paper outline. */
.zoom-stage { --map-ink: #241F1B; --map-ink-soft: #2F2924; --map-water: #1F4E57;
  --map-halo: rgba(244,236,223,1); --map-glow: rgba(244,236,223,.85); }
/* A paper wash at the closest view only, so the ink names stand off the
   valley's busy mid-tones where they are densest; zoom-wire.js takes it from
   full at 42 km to nothing by 60 km, within the first tenth of the zoom.
   Tried 2026-10-18: a wash held to 300 km white-washed the relief; a dark
   scrim with light names (38%, then 18%) read too dark. Only the opacity
   changes on scroll, which the compositor handles alone. */
.zoom-scrim { position: absolute; inset: 0; pointer-events: none; opacity: 0;
  background: rgba(244,236,223,.38); will-change: opacity; }
.label { position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
         font: 400 13px/1.3 var(--serif); color: var(--map-ink);
         /* Area names (ranges, parks, seas, rivers, states) take the sheet's
            own halo: a soft paper glow and no hard edge. Traced as a solid
            outline it was as thick as an italic's strokes, and "Cardamom
            Hills" read as outlined lettering. Place names override this
            below with a crisp 1px outline, which the small upright names
            close in need over busy relief. */
         --halo: var(--map-halo);
         text-shadow: 0 0 1px rgba(244,236,223,1), 0 0 1px rgba(244,236,223,1),
                      0 0 2.5px rgba(244,236,223,.95), 0 0 6px rgba(244,236,223,.6);
         white-space: nowrap; }
/* The sheet's type (map/scripts/sheet.py STYLES) at screen size. Sizes and
   tracking must match KIND in js/overlay.js, which estimates widths from them
   to cull collisions. */
.label--state   { font: 700 17px/1.3 var(--serif); letter-spacing: .45em; text-transform: uppercase; }
.label--water   { font: italic 400 15px/1.3 var(--serif); letter-spacing: .35em; text-transform: uppercase; }
.label--city    { font-size: 15px; letter-spacing: .02em; color: var(--map-ink); }
.label--town    { font-size: 14px; letter-spacing: .02em; color: var(--map-ink); }
.label--village { font-size: 12.5px; letter-spacing: .02em; color: var(--map-ink-soft); }
.label--range   { font: 400 14px/1.3 var(--serif); letter-spacing: .06em; color: var(--map-ink); }
.label--peak    { font: italic 400 12px/1.3 var(--serif); }
.label--feature { font: italic 400 12px/1.3 var(--serif); letter-spacing: .1em; }
.label--lake, .label--river { font: italic 400 13px/1.3 var(--serif); letter-spacing: .06em; color: var(--map-water); }
/* Ranges are set like the cities (2026-10-18): upright, Madurai's size and
   crisp outline, so the hills read as firmly as the places. */
.label--city, .label--town, .label--village, .label--range {
  text-shadow: 1px 0 var(--halo), -1px 0 var(--halo), 0 1px var(--halo), 0 -1px var(--halo),
               0 0 3px var(--halo), 0 0 6px var(--map-glow); }
/* A place's dot sits on its coordinate; the name runs off to one side. */
.label--city, .label--town, .label--village { padding-left: 8px; transform-origin: left center; }
.label.is-left { transform-origin: right center; }
/* an area name set beside its point (Silent Valley), with no dot */
.label--feature.is-left { padding-right: 8px; }
.label--city.is-left, .label--town.is-left, .label--village.is-left { padding: 0 8px 0 0; }
.label--city::before, .label--town::before, .label--village::before {
  content: ""; position: absolute; top: 50%; left: 0; width: 6px; height: 6px;
  margin: -3px 0 0 -3px; border-radius: 50%; background: var(--map-ink);
  box-shadow: 0 0 0 1.5px var(--map-halo); }
.label--village::before { width: 4.5px; height: 4.5px; margin: -2.25px 0 0 -2.25px; }
.is-left::before { left: auto; right: 0; margin: -3px -3px 0 0; }
.label--village.is-left::before { margin: -2.25px -2.25px 0 0; }

/* The sheet's furniture at full zoom-out (js/sheet-furniture.js). */
.sheet-furniture { position: absolute; inset: 0; pointer-events: none; z-index: 1;
  font: 400 10px/1.3 var(--serif); color: #3B3530; }
.sheet-tick { position: absolute; left: 0; top: 0; white-space: nowrap; letter-spacing: .04em; }
.sheet-legend { position: absolute; padding: 12px 14px;
  background: rgba(244,236,223,.85); }
.sheet-legend p { margin: 0; }
.sheet-legend-head { font: 700 11px/1 var(--serif); letter-spacing: .3em;
  text-transform: uppercase; margin-bottom: 9px !important; }
.sheet-legend-row { display: flex; align-items: center; gap: 9px; font-size: 10.5px;
  margin-bottom: 5px !important; }
.sheet-legend-row i { width: 22px; height: 12px; border: 1px solid #3B3530; flex: none; }
.sheet-scale { margin-top: 12px; }
.sheet-scale p { font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase; }
.sheet-scale-bar { display: flex; height: 5px; border: 1px solid #3B3530; margin: 3px 0 2px; }
.sheet-scale-bar i { flex: 1; }
.sheet-scale-bar i:nth-child(odd) { background: #3B3530; }
.sheet-scale-ends { display: flex; justify-content: space-between; width: 100%; }
@media (max-width: 760px) {
  /* The caption card owns the bottom of a phone. The grid, its ticks and the
     neat line stay; the legend does not fit beside anything. */
  .sheet-legend { display: none; }
}
/* The zoom's call-outs: type set straight on the map, no card - ported from
   the Stage 0 prototype (.copy, .eyebrow, .route in the artifact
   Mwxxvrg67wsf9Rb9cgRe27). A paper scrim from the left (.zoom-copy-scrim)
   is all the ground they get. These are also the static, no-JS stack. */
.beats { display: grid; gap: 28px; }
.beat { max-width: min(40rem, calc(100vw - 32px)); color: var(--ink); }
.beat-eyebrow { margin: 0; font: 500 .74rem/1 var(--sans); letter-spacing: .2em;
  text-transform: uppercase; opacity: .85; }
.beat h2 { font: 500 clamp(2rem, 4.6vw, 3.8rem)/1.02 var(--sans); letter-spacing: -.015em;
  margin: .8rem 0 .9rem; text-wrap: balance; }
.beat p { margin: 0; font-size: clamp(1rem, 1.4vw, 1.2rem); max-width: 34ch; }
.beat p.beat-eyebrow { font-size: .74rem; max-width: none; }
.beat-route { display: grid; gap: .2rem; margin-top: 1.2rem; }
.beat .beat-leg { font: 500 clamp(2.4rem, 6vw, 5rem)/1 var(--sans); letter-spacing: -.02em;
  max-width: none; }
.beat-leg span { color: #8E918C; font-weight: 400; }
/* The destination is the lockup itself, mark and name, on its own line and
   about as wide as "Coimbatore" so the two ends of the leg weigh the same. */
.beat-leg-logo { display: block; width: 4.1em; height: auto; margin-top: .18em; }
.beat-route dl { display: grid; grid-template-columns: repeat(3, auto); justify-content: start;
  gap: .2rem 2.2rem; margin: 1rem 0 0; }
.beat-route dl div { display: flex; flex-direction: column; gap: .35rem; }
.beat-route dt { font: 500 .7rem/1 var(--sans); letter-spacing: .18em; text-transform: uppercase;
  color: #8E918C; order: 2; }
.beat-route dd { margin: 0; font: 500 1.6rem/1.1 var(--sans); font-variant-numeric: tabular-nums;
  order: 1; }
/* Beat 3 is where a six-viewport set piece finally asks for the booking, so
   it gets a real button once a number exists. Until then the placeholder rule
   below paints it ember; the moment wireEnquire gives it an href, THIS is what
   is left, and a bare underlined text link is not how the page should ask. */
.beat a { color: var(--ink); }
.zoom.is-live .beat a {
  display: inline-block; margin-top: 14px; text-decoration: none;
  background: var(--ink); color: var(--bone);
  padding: 11px 20px; border: 1px solid var(--ink);
  font: 500 .72rem var(--sans); letter-spacing: .14em; text-transform: uppercase; }
.zoom.is-live .beat a[href]:hover { background: transparent; color: var(--ink); }

@media (prefers-reduced-motion: no-preference) {
  /* 730svh: the zoom itself takes the first 600 (82%, ZOOM_SHARE in
     zoom-wire.js) and the full sheet then holds for the last 130. */
  .zoom.is-live { height: 730svh; padding: 0; }
  .zoom.is-live .zoom-stage { position: sticky; top: 0; height: 100svh; overflow: hidden; }
  .zoom.is-live #zoomCanvas, .zoom.is-live #zoomOverlay {
    display: block; position: absolute; inset: 0; width: 100%; height: 100%; }
  .zoom.is-live #zoomOverlay { pointer-events: none; }
  /* The resort marker (zoom-wire.js). */
  .zoom-pin-disc { fill: #F4ECDF; stroke: #241F1B; stroke-width: 2; }
  .zoom-pin-core { fill: var(--ember); }
  .zoom-pin-pulse { stroke: var(--ember); }
  .zoom-pin-pulse-halo { stroke: #F4ECDF; }
  /* The resort's name over the marker, set as the header lockup sets it:
     Instrument Sans caps, the second line between two fading rules. Ink with
     the map labels' paper outline, since it sits on relief, not footage. */
  .zoom-pin-name { position: absolute; left: 0; top: 0; z-index: 2; pointer-events: none;
    display: grid; justify-items: stretch; gap: 3px; color: var(--map-ink);
    --halo: var(--map-halo);
    text-shadow: 1.5px 0 var(--halo), -1.5px 0 var(--halo), 0 1.5px var(--halo),
                 0 -1.5px var(--halo), 1px 1px var(--halo), -1px -1px var(--halo),
                 1px -1px var(--halo), -1px 1px var(--halo), 0 0 8px var(--map-glow); }
  .zoom-pin-l1 { font: 500 13px/1 var(--sans); letter-spacing: .26em; margin-right: -.26em;
    text-transform: uppercase; white-space: nowrap; }
  .zoom-pin-l2 { display: flex; align-items: center; gap: .85em;
    font: 400 9.5px/1 var(--sans); letter-spacing: .3em; text-transform: uppercase; }
  .zoom-pin-l2 i { flex: 1 1 0; height: 1px; opacity: .7;
    box-shadow: 0 0 0 1px var(--map-halo); }
  .zoom-pin-l2 i:first-child { background: linear-gradient(90deg, transparent, currentColor); }
  .zoom-pin-l2 i:last-child  { background: linear-gradient(90deg, currentColor, transparent);
    margin-left: -.3em; }
  /* The call-outs sit bottom-left, as the prototype's .copy did: left
     clamp(16px, 5vw, 72px), bottom clamp(48px, 12vh, 140px). */
  .zoom.is-live .beats { position: absolute; inset: 0; align-content: end; gap: 0;
         padding: 0 clamp(16px, 5vw, 72px) clamp(48px, 12vh, 140px);
         pointer-events: none; z-index: 2; }
  /* The prototype's #zoom .scrim: paper from the left, clear by mid-screen;
     from the bottom on a portrait screen. Under the call-outs and the labels'
     layer is above it, so names still read where it is light. */
  .zoom-copy-scrim { position: absolute; inset: 0; pointer-events: none; z-index: 1;
    background: linear-gradient(90deg, rgba(243,241,236,.82), rgba(243,241,236,0) 52%); }
  @media (orientation: portrait) {
    .zoom-copy-scrim { background: linear-gradient(0deg, rgba(243,241,236,.92),
        rgba(243,241,236,0) 55%); }
  }
  .zoom.is-live .labels { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
  .zoom.is-live .beat { grid-area: 1 / 1; align-self: end;
        opacity: 0; visibility: hidden; transition: opacity .45s, visibility 0s .45s;
        pointer-events: auto; }
  .zoom.is-live .beat[aria-current="true"] { opacity: 1; visibility: visible;
        transition: opacity .45s, visibility 0s; }
  @media (max-width: 760px) {
    /* The spec asks for a card at the BOTTOM on phones. align-self:end alone
       does not do it: the row is sized to its content and align-content:center
       centres that row, so the card lands mid-screen with nothing to end
       against. The grid has to place the row at the end too. */
  }
}

main .band { padding: clamp(4rem, 9vw, 8rem) clamp(16px, 6vw, 96px); }

/* START PLANNING - the closing section, after White Desert's: the place full
   bleed, a slow drift on it, one serif line in capitals and one outlined
   button. The scrim is darkest where the type sits, so the busy green frame
   still reads behind white type. */
.plan { position: relative; min-height: 100svh; display: grid; place-items: center;
  overflow: hidden; background: #1E2A1E; color: var(--white); text-align: center; }
.plan-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform-origin: 55% 60%; animation: plan-drift 30s ease-in-out infinite alternate; }
@keyframes plan-drift { from { transform: scale(1.04); } to { transform: scale(1.12) translate(-1.5%, -1%); } }
@media (prefers-reduced-motion: reduce) { .plan-img { animation: none; transform: scale(1.04); } }
/* The section opens out of the night band's own dark: the photo fades up
   from --ink over its first 22%, so it reads as a new scene and not as one
   more photograph in the Seasons stack, which ends on a picture too (on a
   phone the two used to butt together). */
.plan::after { content: ""; position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(180deg, #161714 0%, rgba(22,23,20,.85) 8%, rgba(22,23,20,0) 22%),
    radial-gradient(70% 55% at 50% 50%, rgba(8,14,8,.5), rgba(8,14,8,.15) 70%, rgba(8,14,8,.25)); }
.plan-eyebrow { margin: 0 0 1rem; font: 500 .74rem/1 var(--sans); letter-spacing: .24em;
  text-transform: uppercase; opacity: .9; }
.plan-copy { position: relative; z-index: 1; padding: 0 clamp(16px, 6vw, 96px); }
.plan h2 { margin: 0; font: 400 clamp(2.2rem, 5.2vw, 4.6rem)/1.05 var(--serif);
  letter-spacing: .02em; text-transform: uppercase; text-wrap: balance;
  text-shadow: 0 1px 18px rgba(0,0,0,.35); }
.plan-btn { display: inline-block; margin-top: clamp(1.8rem, 4vw, 2.8rem);
  padding: 1rem 2.6rem; border: 1px solid rgba(251,250,247,.55); color: var(--white);
  font: 400 1rem/1 var(--sans); text-decoration: none; letter-spacing: .01em;
  background: rgba(251,250,247,.06); backdrop-filter: blur(2px);
  transition: background .2s, border-color .2s; }
.plan-btn[href]:hover { background: rgba(251,250,247,.16); border-color: var(--white); }
main .band h2 { font-size: clamp(22px, 3vw, 34px); margin: 0 0 14px; }
main .band p { max-width: 54ch; color: var(--quiet); margin: 0; }
.grid { display: grid; gap: 14px; margin-top: 28px;
        grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid img { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; object-position: center;
            display: block; background: var(--stone); }
.facts { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px;
         max-width: 54ch; color: var(--quiet); }
.facts strong { color: var(--ink); }

/* FEATURE BANDS - Stay and the farm.
   These two carry the argument once the logo has docked, so they get a
   bigger headline, a quiet eyebrow and a wider lede. Seasons, How it works
   and Getting here stay plain bands. Chosen 2026-10-16; the options and the
   reasoning are in docs/stay-and-farm.md. */
.band.feature .eyebrow { margin: 0 0 10px; font-size: .72rem; line-height: 1;
  letter-spacing: .18em; text-indent: 0; color: var(--quiet); }
main .band.feature h2 { font-size: clamp(30px, 4.6vw, 56px); line-height: 1.05;
  margin: 0 0 16px; }
main .band.feature .lede { max-width: 52ch; margin: 0 0 30px;
  font-size: clamp(16px, 1.3vw, 19px); }

/* The numbers. Three facts that are true of the place rather than of a
   room, so they are stated once, above the photographs. */
.spec { display: flex; flex-wrap: wrap; gap: 0; margin: 0 0 26px; padding: 0;
  list-style: none; border-top: 1px solid var(--stone);
  border-bottom: 1px solid var(--stone); }
.spec li { padding: 18px 32px 18px 0; margin-right: 32px;
  border-right: 1px solid var(--stone); }
.spec li:last-child { border-right: 0; margin-right: 0; padding-right: 0; }
.spec b { display: block; letter-spacing: -.02em; font-weight: 500;
  font-size: clamp(26px, 3vw, 38px); line-height: 1; }
.spec span { display: block; margin-top: 6px; color: var(--quiet);
  font-size: .9rem; }

/* The amenities. A GRID, not a wrapping flex row: five items wrap 4+1 and
   look accidental. The glyphs are drawn in the sprite at the end of the
   document and are aria-hidden - the label beside each carries the
   meaning. Do not swap in an icon set without reading
   docs/stay-and-farm.md; the three that look odd were each chosen over
   something that read worse at 19 px. */
.amen { list-style: none; padding: 0; margin: 0; display: grid;
  gap: 11px 18px; grid-template-columns: repeat(4, minmax(0, auto));
  justify-content: start; }
.amen li { display: flex; align-items: center; gap: 8px; font-size: .94rem; }
.amen .ic { width: 19px; height: 19px; flex: none; fill: none;
  stroke: currentColor; stroke-width: 1.6; stroke-linecap: round;
  stroke-linejoin: round; color: var(--quiet); }
.both { margin: 8px 0 34px; color: var(--quiet); font-size: .88rem; }
@media (max-width: 760px) { .amen { grid-template-columns: repeat(2, minmax(0, auto)); } }

/* Every photograph is a button, so it is reachable by keyboard and
   announces itself as something that opens. height:auto is load-bearing:
   width="" and height="" are CSS presentation hints and beat aspect-ratio,
   which drew the farm's squares as tall columns. */
.shot { display: block; padding: 0; border: 0; background: none; width: 100%;
  text-align: left; font: inherit; color: inherit; cursor: zoom-in; }
.shot img { width: 100%; height: auto; background: var(--stone);
  transition: opacity .18s ease; }
.shot:hover img { opacity: .9; }
.shot:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.three { display: grid; gap: 18px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.three figure { margin: 0; }
.three .shot img { aspect-ratio: 3 / 2; object-fit: cover; }
.three figcaption { margin-top: 10px; font-size: .94rem; }
.three figcaption b { display: block; font-weight: 500; }
.three figcaption span { color: var(--quiet); }

/* The farm: a panorama the width of the page, then a strip of squares. */
.pano { position: relative; overflow: hidden;
  margin: 0 calc(-1 * clamp(16px, 6vw, 96px)) 30px; }
.pano .shot img { aspect-ratio: 3 / 1; object-fit: cover; }
.mist { position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(60% 120% at 20% 40%, rgba(255,255,255,.30), rgba(255,255,255,0) 62%),
    radial-gradient(70% 140% at 80% 60%, rgba(255,255,255,.20), rgba(255,255,255,0) 64%);
  background-size: 160% 160%, 150% 150%;
  background-position: -20% 40%, 120% 60%; background-repeat: no-repeat; }
.strip { display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.strip .shot img { aspect-ratio: 1 / 1; object-fit: cover; }
.crops { list-style: none; padding: 0; margin: 0 0 30px; display: flex;
  gap: 28px; flex-wrap: wrap; color: var(--quiet); }
.crops b { color: var(--ink); font-weight: 500; }

/* THE LIGHTBOX.
   <dialog> does the focus trap, Escape and the return of focus to the
   button that opened it. The scrim is on the DIALOG, not on ::backdrop:
   the backdrop paints behind the box, so a transparent box filling the
   viewport lets the page read straight through it. */
#lb { border: 0; padding: 0; width: 100vw; height: 100vh; max-width: 100vw;
  max-height: 100vh; overflow: hidden; background: rgba(12,14,12,.95); }
#lb::backdrop { background: rgba(12,14,12,.95); }
/* A flex column with min-height:0 on the image. As a grid, its row was
   sized by its own content, so max-height:100% measured against a row as
   tall as the photograph and the caption went off the bottom. */
.lb-wrap { position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px;
  padding: clamp(12px, 3vw, 40px);
  padding-bottom: calc(clamp(12px, 3vw, 40px) + 22px); }
#lb img { min-height: 0; flex: 0 1 auto; max-width: 100%; max-height: 100%;
  width: auto; height: auto; object-fit: contain; background: #0c0e0c; }
.lb-cap { flex: none; max-width: 60ch; text-align: center; color: #EDEAE3;
  font-size: .94rem; }
.lb-cap b { display: block; font-weight: 500; }
.lb-cap span { color: #A9A79F; }
.lb-btn { position: absolute; top: clamp(8px, 2vw, 20px);
  right: clamp(8px, 2vw, 20px); width: 44px; height: 44px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.3); background: rgba(0,0,0,.35);
  color: #fff; cursor: pointer; font-size: 20px; line-height: 1; }
.lb-nav { position: absolute; top: 50%; transform: translateY(-50%);
  width: 46px; height: 46px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,.26); background: rgba(0,0,0,.3);
  color: #fff; cursor: pointer; font-size: 20px; line-height: 1; }
.lb-prev { left: clamp(8px, 2vw, 20px); } .lb-next { right: clamp(8px, 2vw, 20px); }
.lb-btn:focus-visible, .lb-nav:focus-visible { outline: 2px solid #fff;
  outline-offset: 2px; }
.lb-count { position: absolute; bottom: clamp(8px, 2vw, 18px); left: 50%;
  transform: translateX(-50%); color: #8E8C85; font-size: .78rem;
  letter-spacing: .12em; }
body.lb-open { overflow: hidden; }


/* THE FOOTER, after White Desert's: paper ground ruled into columns by
   hairlines, italic serif heads over grey links, a column of tiles, and
   the name across the full width, in the mark's deepest green. */
.foot { --rule: rgba(22,23,20,.09); --green: #0B4B31;
  background: #E9E6DF; color: var(--ink); overflow: hidden;
  padding-bottom: env(safe-area-inset-bottom, 0px); }
.foot-grid { display: grid; grid-template-columns: 1fr 1.3fr 1.3fr 2fr;
  border-bottom: 1px solid var(--rule); }
.foot-grid > * { padding: clamp(1.6rem, 3vw, 2.6rem) clamp(16px, 2vw, 28px);
  border-left: 1px solid var(--rule); }
.foot-grid > :first-child { border-left: 0; }
.foot-mark { display: flex; justify-content: center; align-items: flex-start; }
.foot-mark img { width: min(190px, 90%); height: auto; }
.foot h2 { margin: 0 0 .7rem; font: italic 400 1.45rem/1.2 var(--serif); letter-spacing: .005em; }
.foot ul { list-style: none; margin: 0 0 1.9rem; padding: 0; display: grid; gap: .45rem; }
.foot li, .foot li a { color: #7C7F78; font-size: 1rem; text-decoration: none; }
.foot li a { cursor: pointer; transition: color .15s; }
.foot li a[href]:hover { color: var(--ink); }
.foot li a[aria-disabled="true"] { cursor: default; }
.foot-coords { font-family: var(--serif); letter-spacing: .02em; }
.foot-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-content: start; }
.foot-tile { display: grid; place-items: center; min-height: 8.5rem; position: relative;
  background: #F3F1EC; color: var(--ink); text-decoration: none; font-weight: 500;
  overflow: hidden; cursor: pointer; transition: background .2s; }
.foot-tile[href]:hover { background: #FBFAF7; }
.foot-tile--map { grid-column: 1 / -1; color: var(--white); }
.foot-tile--map img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  object-position: 55% 45%; transition: transform .6s ease; }
.foot-tile--map::after { content: ""; position: absolute; inset: 0; background: rgba(10,14,12,.32); }
.foot-tile--map span { position: relative; z-index: 1; text-shadow: 0 1px 6px rgba(0,0,0,.4); }
.foot-tile--map:hover img { transform: scale(1.04); }
/* The maps' credits, small and quiet, under the green line. */
.foot-credits { margin: 0; padding: .5rem clamp(16px, 2vw, 28px) .9rem;
  font-size: .74rem; color: #8E918C; max-width: none; text-align: center; }
.foot-credits a { color: inherit; }
/* The line across the full width at the bottom, as White Desert's name
   is: 28 characters, so 5.4vw with .04em tracking fills about 96% of the page. It sits
   on the bottom edge. */
.foot-name { margin: 0 -.04em 0 0; padding: .55em 1vw .1em; font: 500 5.4vw/.9 var(--sans);
  letter-spacing: .04em; text-transform: uppercase; text-align: center;
  white-space: nowrap; color: var(--green); }
@media (max-width: 900px) {
  .foot-grid { grid-template-columns: 1fr 1fr; }
  .foot-mark { grid-column: 1 / -1; justify-content: center; border-bottom: 1px solid var(--rule); }
  .foot-tiles { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--rule); }
}
@media (max-width: 520px) {
  .foot-grid { grid-template-columns: 1fr; }
  .foot-col { border-left: 0; border-top: 1px solid var(--rule); }
}

@media (prefers-reduced-motion: reduce) {
  #atmosphere { display: none; }
}

/* THE ENQUIRY FORM (js/enquiry-form.js): a dialog in the page's own type. */
.enq { padding: 0; border: 0; background: transparent; max-width: none; max-height: none;
  width: 100%; height: 100%; color: var(--ink); }
.enq::backdrop { background: rgba(22,23,20,.55); backdrop-filter: blur(3px); }
.enq[open] { display: grid; place-items: center; }
.enq-panel { position: relative; width: min(560px, calc(100vw - 32px)); max-height: calc(100svh - 32px);
  overflow: auto; background: var(--bone); padding: clamp(24px, 4vw, 40px); }
.enq-close { position: absolute; top: 10px; right: 12px; border: 0; background: none;
  font: 300 2rem/1 var(--sans); color: var(--quiet); cursor: pointer; padding: 4px 8px; }
.enq-close:hover { color: var(--ink); }
.enq-eyebrow { margin: 0 0 8px; font-size: .72rem; letter-spacing: .18em; text-transform: uppercase;
  color: var(--quiet); }
.enq h2 { margin: 0 0 8px; font-size: clamp(26px, 3vw, 36px); line-height: 1.1; }
.enq-lede { margin: 0 0 22px; color: var(--quiet); }
.enq form { display: grid; gap: 14px; }
.enq-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 520px) { .enq-row { grid-template-columns: 1fr; } }
.enq label { display: grid; gap: 6px; font-size: .78rem; letter-spacing: .06em; color: var(--quiet); }
.enq label i { font-style: normal; letter-spacing: 0; opacity: .8; }
.enq-done-msg { margin: 0 0 22px; font-size: 1.1rem; color: #0B4B31; }
.enq.is-sent form, .enq.is-sent .enq-lede { display: none; }
.enq input, .enq select, .enq textarea { font: 400 1rem/1.4 var(--sans); color: var(--ink);
  background: #FBFAF7; border: 1px solid #CFCBC2; border-radius: 0; padding: 10px 12px; width: 100%;
  box-sizing: border-box; }
.enq input:focus, .enq select:focus, .enq textarea:focus { outline: 2px solid var(--ember); outline-offset: 0;
  border-color: transparent; }
.enq textarea { resize: vertical; min-height: 6rem; }
.enq-hp { position: absolute !important; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.enq-captcha:empty { display: none; }
.enq-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 18px; margin-top: 4px; }
.enq-send { font: 500 .78rem/1 var(--sans); letter-spacing: .14em; text-transform: uppercase;
  background: var(--ink); color: var(--bone); border: 1px solid var(--ink); padding: 14px 22px; cursor: pointer; }
.enq-send:hover { background: transparent; color: var(--ink); }
.enq-send:disabled { opacity: .5; cursor: wait; }
.enq-status { margin: 0; font-size: .9rem; color: var(--quiet); }
.enq-status[data-kind="error"] { color: #9A3B1E; }
.enq-status[data-kind="ok"] { color: #0B4B31; }
[data-enquire] { cursor: pointer; }
