/* Reserved: the experience layer (prototype).
   One system, taken from the product itself: candlelight becomes active,
   two availabilities converge into one held window, the day advances
   toward the table, and the reservation is held. The hero is settled on
   load (fixes7: no load transition, no pointer parallax).

   Structure:
   - Static composition first (hero field, overlap device, day-of stage): these
     rules apply with or without motion, so the reduced-motion and no-JS pages
     are complete, composed states.
   - Everything that moves lives under `html.fx` (set before first paint, and
     absent under prefers-reduced-motion).
   - Animation is transform/opacity first. clip-path is used only for the
     later sections' one-time scroll reveals (Price/Safety, unchanged).
     Scroll-linked values arrive as custom properties set once per frame
     by experience.js. */

:root {
  --lantern: 233, 168, 91;       /* amber as r,g,b for light layers */
}

html { overflow-x: clip; }

/* =============== Shared =============== */

.bar { position: relative; z-index: 6; }

/* Fixes10 (addendum B): the header wordmark carries the logo art's
   amber sliver (assets/profile-reserved-a.png: a slim vertical amber
   slit after the final letter). Set after "Reserved" at about the
   wordmark's cap height, a few px wide, with a letterspaced gap; it
   is drawn inside the header wordmark's own box, after the text, so
   the wordmark's left edge and the eyebrow do not move. The footer
   wordmark (not in the bar) is unchanged. No glow: a flat rule in
   the page's amber. */
.bar .wordmark::after {
  content: "";
  display: inline-block;
  width: 3px;
  height: .72em;
  margin-left: .24em;
  background: var(--amber);
  vertical-align: baseline;
}

.aperture picture {
  position: absolute;
  inset: 0;
  display: block;
}

/* Living flame (fixes8 desktop, fixes9 mobile): the hero photograph as
   the owner's clip in stacked elements (full ignition, then
   a ping-pong loop file). The still above stays
   as poster/fallback and paints instantly; only the active video
   (`.is-active`) shows under `.is-live` — a hard cut on an identical
   frame at each turn, no fade — so unsupported, blocked, save-data and
   reduced-motion states land on exactly the static build. On mobile
   (fixes9) the same clip set runs in the frame at the clip's own 3:2
   proportions, so no crop is added and the table stays whole. */
.hero-video {
  display: block;
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
}

.aperture--hero.is-live .hero-video.is-active { opacity: 1; }

/* Fixes14: never Safari's play button over the hero, whatever the policy. */
.hero-video::-webkit-media-controls-start-playback-button { display: none !important; -webkit-appearance: none; }

/* Fixes10: while the gated flame is pending (class set before first
   paint by the head script, removed when the clip goes live or when
   the flame dies), the lit still is withheld so it cannot flash
   ahead of frame 0; the aperture shows the dark page ground, then
   frame 0 below. No-video paths never carry the class. */
html.flame-pending .aperture--hero picture { opacity: 0; }

/* Fixes10: the clip's own frame 0 as the pre-play state. The underlay
   sits above the lit still and below the videos, hidden until the
   flame system arms it once the gated clip fetches are in flight
   (`.is-armed`, set in experience.js when the underlay is decoded); it carries the same
   object geometry as the video so the swap is a hard cut between
   identical frames, with no fade. Without `.is-armed` (no-JS, reduced
   motion, save-data, unsupported, or any failure) it never shows and
   the lit still underneath stands exactly as before. */
.aperture--hero .hero-frame0 {
  z-index: 1;
  object-position: 50% 50%;
  opacity: 0;
  pointer-events: none;
}

.aperture--hero.is-armed .hero-frame0 { opacity: 1; }

.fx .hero .hero-frame0 {
  transform: translate3d(0, calc(var(--hp, 0) * 5%), 0) scale(calc(1.03 + var(--hp, 0) * .07));
}

/* The clip shares the still's scroll-linked depth only (fixes7: the
   pointer terms are gone; --hp is scroll, kept unchanged). */
.fx .hero .hero-video {
  transform: translate3d(0, calc(var(--hp, 0) * 5%), 0) scale(calc(1.03 + var(--hp, 0) * .07));
  will-change: transform;
}

/* =============== The thread =============== */

.thread { display: none; }

.fx .thread {
  display: block;
  position: fixed;
  z-index: 40;
  pointer-events: none;
  left: max(14px, calc(50vw - 36rem + var(--pad) - 2.5rem));
  top: 14vh;
  bottom: 14vh;
  width: 1px;
  background: #5a4f44;
  /* Fixes7: present from load with the settled page; no staged arrival. */
  opacity: 1;
}

.thread__fill {
  position: absolute;
  inset: 0;
  background: rgb(var(--lantern));
  transform-origin: 50% 0;
  transform: scaleY(var(--p, 0));
}

.thread__dot {
  position: absolute;
  z-index: 2;
  left: -4px;
  top: -4px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--amber-hi);
  /* The ring exists only while the point rests at or past the held station
     (--held is 1 there, 0 elsewhere), so it never shows at the journey's start. */
  outline: 1px solid color-mix(in srgb, var(--amber-hi) calc(var(--held, 0) * 100%), transparent);
  outline-offset: 3px;
  transform: translate3d(0, calc(var(--p, 0) * (100vh - 28vh)), 0) scale(calc(1 + var(--held, 0) * .7));
  transition: transform .45s cubic-bezier(.16, 1, .3, 1);
}

/* One tick per section. Reached ticks take the amber; the station the point
   is resting at grows, so arriving at a section registers. */
.thread__tick {
  position: absolute;
  left: -3px;
  top: calc(var(--t) * 100%);
  width: 7px;
  height: 1px;
  background: #6d6152;
  transform: scaleX(1);
  transition: background-color .5s ease-out, transform .5s cubic-bezier(.16, 1, .3, 1);
}

.thread__tick.is-reached { background: var(--amber); }
.thread__tick.is-here { transform: scaleX(2.4); }
/* The card's station is the held reservation: its tick stays enlarged and
   bright, and the travelling point carries a ring, from the hold onward. */
.thread__tick.is-held { background: var(--amber-hi); transform: scaleX(3.2); }

/* Score-fix (adapt): the current station's name. Mobile only (owner
   licence 2026-11-06, DESIGN (o)); the text is the section's own
   existing heading, verbatim, set by experience.js from the DOM.
   Vertical in the frame gutter beside the rail, so it never reaches
   the text column or the reservation card's fields. Desktop stations
   stay unlabelled (k). Fix pass 2: the point rides at the word's end
   on a solid night strip (below) — centred, it ate the middle
   letters. */
.thread__station { display: none; }

@media (max-width: 47.999rem) {
  /* On a phone the thread keeps to the screen's edge. Score-fix
     (adapt): it is now the licensed mobile wayfinding element —
     station ticks show (they were hidden here), the point matches
     the desktop 9px, and the current station's heading rides
     vertically in the 20px frame gutter (label box ~11px at
     abs. x 7-18px), clear of the text column and the card. */
  .fx .thread { left: 5px; top: 10vh; bottom: 10vh; background: #4a4038; }
  .thread__dot { left: -4px; top: -4px; width: 9px; height: 9px; transform: translate3d(0, calc(var(--p, 0) * (100vh - 20vh)), 0) scale(calc(1 + var(--held, 0) * .7)); }
  .thread__station {
    display: block;
    position: absolute;
    /* Fix pass 2: 0 (was 2px) so the backed strip's box — abs. x 5–20px
       at 390 — ends at the frame gutter's own edge, never on the card. */
    left: 0;
    top: 0;
    writing-mode: vertical-rl;
    font-family: var(--sans);
    font-size: .6875rem;
    font-weight: 600;
    line-height: 1;
    letter-spacing: .06em;
    white-space: nowrap;
    color: var(--ink-soft);
    /* Fix pass 2 (adapt; owner-confirmed on his phone): the word sits on
       a solid night backing strip in the gutter, so photographs and the
       paper card stop interfering with its letters. The strip is the
       gutter's own ground (Warm Night), padded a step beyond the first
       and last letters and 2px to each side — the box stays inside the
       20px frame gutter. The text-shadow it replaces is gone. */
    background: var(--night);
    /* Logical axes in vertical-rl: block is horizontal (the strip's
       sides), inline is vertical (beyond the first/last letters). */
    padding-block: 2px;
    padding-inline: .45rem;
    /* Travels exactly as the point does, but the point now rides at the
       word's end, not its centre: the label ends 16px above the point's
       centre, clear of the point at its largest (held scale 1.7 = 15.3px
       plus its 3px-offset ring reaches ~11px above centre). The word
       reads top to bottom into the arriving point. */
    transform: translate3d(0, calc(var(--p, 0) * (100vh - 20vh)), 0) translateY(calc(-100% - 16px));
    transition: transform .45s cubic-bezier(.16, 1, .3, 1), opacity .45s ease-out;
  }
  .thread__station.is-empty { opacity: 0; }
  /* Polish (fixes17): no title indent here. Round 8 indented the FAQ
     title 1.25rem so the travelling dot would clear its first letter,
     which started that one title off the shared axis (round-8 P3).
     Measured on the root page at 390/360/320: the dot's right edge is
     7px and the title text starts at the 20px frame padding (13px
     clear); in the held state the dot and its ring reach 13.5px
     (6.5px clear). The dot lives in the frame gutter as it does for
     every other title, so the FAQ title keeps the shared axis. */
}

@media (prefers-reduced-motion: reduce) {
  .thread__dot, .thread__tick, .thread__station { transition: none; }
}

/* =============== Hero: settled on load (fixes7) =============== */

/* No aperture opening, no ignition veil, no masked rise: the hero is
   simply there in its settled state. The ignition is the clip's own
   content. */
.hero { position: relative; }

/* ---- Desktop: the photograph carries the right side of the hero ---- */
@media (min-width: 48rem) {
  .hero { --bleed: calc((100vw - min(100vw, 72rem)) / 2); }

  /* A field, not a card: from the very top of the page to the band's foot and
     out to the viewport's right edge. Its left edge softens into the type;
     the table sits in its lower half, so the foot only dissolves at the end. */
  .aperture--hero {
    position: absolute;
    z-index: 1;
    top: -4.7rem;
    bottom: 0;
    left: 55%;
    right: calc(-1 * var(--bleed));
    grid-column: auto;
    grid-row: auto;
    height: auto;
    margin: 0;
    aspect-ratio: auto;
    box-shadow: none;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 30%), linear-gradient(to bottom, #000 90%, transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image: linear-gradient(to right, transparent 0, #000 30%), linear-gradient(to bottom, #000 90%, transparent 100%);
    mask-composite: intersect;
  }

  .aperture--hero img { object-position: 50% 50%; }

  .hero__copy,
  .hero__free,
  h1 { position: relative; z-index: 3; }
}

/* Fixes12: the near-square field made `cover` crop a third of the 3:2 clip
   and upscale the rest, losing the whole table and its sharpness. The
   field now takes the clip's shape at the hero's foot, so nothing is
   cropped and a 1440 Retina screen draws it close to 1:1. The clip's dark
   room dissolves upward into the page ground.
   The hero fills the window below the bar and no more, so the copy and the
   whole table are seen without scrolling: the headline and the field both
   answer to the window's height as well as its width. */
@media (min-width: 64rem) {
  .hero {
    --hero-h: calc(100svh - 4.4rem);
    --field-left: 38%;
    min-height: var(--hero-h);
    align-content: center;
    padding-block: clamp(1rem, 3vh, 2.5rem);
  }

  h1 { font-size: clamp(3.4rem, min(7.4vw, 12vh), 6rem); }

  .aperture--hero {
    top: auto;
    left: auto;
    width: min(calc(100% + var(--bleed) - var(--field-left)), calc(var(--hero-h) * 1.35));
    aspect-ratio: 3 / 2;
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 18%), linear-gradient(to bottom, transparent 0, #000 22%, #000 92%, transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 18%), linear-gradient(to bottom, transparent 0, #000 22%, #000 92%, transparent 100%);
  }
}

/* Layout (fixes2, fix 2): on wide viewports the hero alone takes a wider
   measure, so its left block (thread rail + text column) moves toward
   the left edge and the screen's width is used. The photograph's right
   edge still lands exactly on the viewport edge (--bleed is recomputed
   for the wider measure) and its crop is unchanged; the sections below
   keep the 72rem measure, and mobile is untouched. */
@media (min-width: 80rem) {
  .hero {
    max-width: 80rem;
    justify-content: start;
    --bleed: calc((100vw - min(100vw, 80rem)) / 2);
  }
  .fx .thread { left: max(14px, calc(50vw - 40rem + var(--pad) - 2.5rem)); }
}

/* Layout (fixes3): above the verified 1440 measure the centred cap itself
   becomes the dead band, so the hero stage spans the full viewport width.
   The photograph field extends with it (its right edge stays on the
   viewport edge: --bleed falls to zero as the stage reaches it), and the
   rail + copy block anchor to a left gutter tied to the viewport rather
   than to the cap. Sections below keep their 72rem measure and axis; at
   1440 and below nothing in this block applies. */
@media (min-width: 90.0625rem) {
  .hero {
    max-width: none;
    margin-inline: 0;
    padding-inline-start: clamp(4.5rem, 6.5vw, 8.5rem);
    --bleed: 0px;
  }
  .fx .thread { left: calc(clamp(4.5rem, 6.5vw, 8.5rem) - 2.5rem); }
}

/* Layout (fixes4): the header row takes the hero's measure, so the
   wordmark stands on the headline's axis and the eyebrow meets the
   symmetric right gutter. Above 1280px the bar shares the hero's
   80rem stage; above the 1440 measure it spans the full width on the
   hero's viewport gutter. At 1280px and below the bar keeps its
   centred measure exactly as before. */
@media (min-width: 80rem) {
  .bar { max-width: 80rem; }
}

@media (min-width: 90.0625rem) {
  .bar {
    max-width: none;
    margin-inline: 0;
    padding-inline: clamp(4.5rem, 6.5vw, 8.5rem);
  }
}

/* 1024–1279px: the lede reaches further across the frame, so the field starts
   later and the plate never sits under its last words. */
@media (min-width: 64rem) and (max-width: 79.999rem) {
  .hero { --field-left: 46%; }
}

/* 1280–1439px (Fixes12): the wider stage clears the lede, so the field can
   reach under the copy's dark side: at 1280 it is 819px wide, which a 2x
   screen draws at the clip's own 1620 pixels. */
@media (min-width: 80rem) and (max-width: 89.999rem) {
  .hero { --field-left: 36%; }
}

/* ---- Mobile: text first; the tall table is the hero's lower room ---- */
@media (max-width: 47.999rem) {
  /* Fixes10 (addendum A) filled the viewport and anchored the frame
     to the hero's foot. Score-fix (adapt, 2026-11-06 critique P1:
     dead-black hero gap on a 7,064px mobile page): the hero then took
     its natural height — no 100svh min-height, no 1fr stretch row —
     so the dark ground between the free line and the photograph was
     one deliberate 1.75rem interval, not leftover screen height, and
     the top interval tightened 4.5rem -> 2.25rem.
     Fix pass 2 (owner finding, 2026-11-06, from his 400x869 screen:
     the natural-height stack composed only by accident at one height
     — copy hugging the top, a void beneath it, the photograph landing
     where the stack left it): the first screen is now a composed
     frame at every height. The hero fills exactly the viewport below
     the header bar (100svh, so mobile browser chrome behaves; 100vh
     first as the fallback), the aperture is anchored to its foot, and
     two equal 1fr intervals — one above the copy block, one below it
     — hold the copy equidistant between the header and the video.
     --bar-h is the header's measured height (58.4px: 1rem padding top
     and bottom around its tallest item; re-measured in BUILD-NOTE-18).
     min-height, not height, so a viewport too short for the copy lets
     the hero grow past the first screen rather than crush it. The
     "How it works" title no longer peeking at the foot of the first
     screen is the accepted consequence (owner spec, 2026-11-06); it
     supersedes the score-fix peek-as-progress-cue at mobile. The
     frame's 3:2 shape, crops, fades and the video inside it are
     unchanged; copy, wording and order unchanged.
     Fix pass 3 (owner direction, 2026-11-06, from his phone on the
     pass-2 build: the hero text read small and cramped in the left
     axis column; his words: "On mobile it can be full width and
     centered"): below 48rem only, the hero copy takes the frame's
     full content width and centres — headline, lede, the action on
     its own line, and the free line — and the type steps up so the
     hero leads the page again (headline ~48px at 390, lede 17px at
     a looser leading — fix pass 4 calibrates the lede down to 15px
     at the same 1.62 leading, owner direction (his 16px instruction
     replaced within the hour, never shipped); nothing else in the
     hero moves). The pass-2 composed frame stands: hero
     exactly the screen below the bar, aperture anchored to its
     foot, copy equidistant between header and aperture. What
     gives when the larger copy and the 3:2 aperture cannot both
     fit a short screen is the aperture's HEIGHT, never the type:
     its height is capped by the screen left over after a reserve
     for the copy block (--copy-reserve: the measured copy + free
     line, two minimum intervals, and one extra lede line for
     Safari's wider metrics), and floored at --aperture-floor.
     The clip inside is object-fit: cover anchored to its foot
     (below), so a shortened frame crops only the dark room above
     the table. Desktop (>=48rem) is untouched. */
  .hero {
    --bar-h: 3.65rem;
    --copy-reserve: 31rem;
    --aperture-floor: 10rem;
    min-height: calc(100vh - var(--bar-h));
    min-height: calc(100svh - var(--bar-h));
    grid-template-rows: 1fr auto auto 1fr auto;
    padding-top: 0;
    padding-bottom: 0;
  }

  .hero__copy { grid-row: 2; text-align: center; }
  .hero__free { grid-row: 3; text-align: center; }

  /* Full width: no measure cap holds the headline in the old axis
     column; the frame's own gutter (--pad, 20px at 390) is the side
     interval, so glyphs start inside it and the thread rail/label
     in that gutter (box ends at 20px) is never under the text. */
  .hero h1 {
    font-size: clamp(2.75rem, 12.3vw, 3.25rem);
    max-width: none;
    margin-inline: auto;
  }

  .hero .lede {
    font-size: .9375rem;
    line-height: 1.62;
    margin-inline: auto;
  }

  .hero__action { justify-items: center; }

  .hero__copy,
  .hero__free { position: relative; z-index: 3; }

  /* Fixes9: the frame takes the clip's own 3:2 proportions (1620x1080),
     so at full bleed (390px wide, 260px tall) the clip shows uncropped —
     table, lantern, both settings and both chair backs whole, as in the
     clip frame itself. The top fade that blends the frame into the
     page ground is kept as it was (the tuck under the text block is
     superseded by fixes10 addendum A, below); the height follows the 3:2 shape instead of the old
     portrait still's 4:5. The portrait still remains underneath as the
     fallback for reduced-motion, save-data, unsupported and hidden-tab
     states. Its top is the dark room, so the fade melts it into the
     text block above, and its dark foot fades into the next band below. */
  /* Fixes10 (addendum A): the frame is anchored toward the hero's
     bottom (align-self: end in the 1fr row) instead of tucked up
     under the text block; its shape is unchanged (3/2, zero crop,
     table whole) and the top fade still blends it into the dark
     ground above it. The portrait still fallback occupies this
     same lowered frame. */
  .aperture--hero {
    order: 2;
    /* Fix pass 2: the last row of the composed frame — anchored to the
       hero's foot, which is the viewport's foot. No top margin: the
       interval above the frame is the second 1fr row, equal to the
       interval above the copy by construction.
       Fix pass 3: the height flexes. It is the 3:2 height
       (100vw * 2/3 — the aperture bleeds to both screen edges, so
       its width is the viewport's) wherever that fits beside the
       enlarged copy; below that it takes what the screen leaves
       after --copy-reserve, down to --aperture-floor (10rem: at
       390 wide a 2.44:1 band foot-anchored on the clip still shows
       its lower three-fifths — lantern, both glasses, both
       settings whole; shorter than that, the frame's own top fade
       starts eating the glasses). No aspect-ratio here: the clamp's
       cap IS the 3:2 statement, and a ratio beside a definite
       height would pull the width in from its full bleed when the
       frame flexes (measured: 240px wide at 390x667) — width stays
       the stretched full bleed at every height. */
    grid-row: 5;
    align-self: end;
    aspect-ratio: auto;
    height: clamp(var(--aperture-floor), calc(100vh - var(--bar-h) - var(--copy-reserve)), calc(100vw * 2 / 3));
    height: clamp(var(--aperture-floor), calc(100svh - var(--bar-h) - var(--copy-reserve)), calc(100vw * 2 / 3));
    margin-inline: calc(-1 * var(--pad));
    margin-top: 0;
    box-shadow: none;
    z-index: 1;
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 24%, #000 84%, transparent 100%);
    mask-image: linear-gradient(to bottom, transparent 0, #000 24%, #000 84%, transparent 100%);
  }

  .aperture--hero img { object-position: 50% 66%; }
  /* Fix pass 3: the landscape clip sources (the video and its frame-0
     still) anchor to their foot when the frame flexes, so the crop
     takes only the dark room above the table and the held moment —
     flame, lantern, settings — keeps its exact place at the
     aperture's foot. At the full 3:2 height there is no crop and
     the position has no effect. The portrait lit still above keeps
     its established 66% crop. */
  .aperture--hero .hero-video,
  .aperture--hero .hero-frame0 { object-position: 50% 100%; }
  .aperture--hero::after { display: none; }
}

/* ---- Hero motion (fixes7): scroll-linked only ---- */
/* The load choreography is gone entirely: no masks, no clip opening, no
   veil, no staged fade/rise. What remains is scroll-linked depth: the
   photograph and the type separate a little as the hero scrolls out
   (--hp is set from scrollY in experience.js; no pointer term remains). */
.fx .hero picture {
  transform: translate3d(0, calc(var(--hp, 0) * 5%), 0) scale(calc(1.03 + var(--hp, 0) * .07));
  will-change: transform;
}
.fx .hero__copy { transform: translate3d(0, calc(var(--hp, 0) * -26px), 0); }

/* =============== Availability: the overlap device =============== */

.overlap { position: relative; }

/* Score-fix (layout, 2026-11-06 critique P1): the bar and its hairline
   sit at the centre of the 2.75rem touch track instead of its foot, so
   on phones each label stands ~24px from the bar it names, not ~42px
   (measured before). The track keeps its full 44px hit height; the
   hairline is drawn at the bar's foot (::before) where the old
   border-bottom put it relative to the bar. Words/readouts untouched. */
.overlap__track {
  height: 2.75rem;
  display: block;
  touch-action: pan-y;
  cursor: ew-resize;
  border-bottom: 0;
}

.overlap__track::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: calc(50% + .2rem);
  height: 1px;
  background: var(--line);
}

.overlap__span {
  left: 0;
  bottom: auto;
  top: calc(50% - .2rem);
  width: 100%;
  height: .4rem;
  background: var(--track);
  transform-origin: 0 50%;
  transform: translate3d(calc(var(--l, .06) * 100%), 0, 0) scaleX(var(--w, .52));
  cursor: grab;
}

.overlap__span--a { --l: .06; --w: .52; left: 0; }
.overlap__span--b { --l: .42; --w: .48; left: 0; }

.overlap__span:active { cursor: grabbing; }

/* A grip at each availability's start shows the bar can be moved. */
@property --nx { syntax: "<length>"; inherits: false; initial-value: 0px; }

/* Score-fix (layout): grips 1.35rem -> 1.75rem (2rem on phones), centred
   on the centred bar; experience.js clamps --gx so a grip dragged to
   either end stays inside the track instead of overhanging its edge. */
.overlap__grip {
  position: absolute;
  left: 0;
  bottom: calc(50% - .875rem);
  width: 1.75rem;
  height: 1.75rem;
  margin-left: -.875rem;
  border-radius: 50%;
  background: var(--band-lift);
  border: 2px solid var(--amber);
  transform: translate3d(calc(var(--gx, 0px) + var(--nx)), 0, 0);
  pointer-events: none;
}

@media (max-width: 47.999rem) {
  .overlap__grip {
    bottom: calc(50% - 1rem);
    width: 2rem;
    height: 2rem;
    margin-left: -1rem;
  }
}

/* Once the availabilities have settled, the grips nudge once: these bars move. */
@keyframes ex-nudge {
  0%, 100% { --nx: 0px; }
  25% { --nx: 7px; }
  50% { --nx: 0px; }
  75% { --nx: 4px; }
}
.fx .overlap.is-nudge .overlap__grip { animation: ex-nudge 1.1s cubic-bezier(.45, 0, .2, 1) .2s 1; }

.overlap__span:focus-visible { outline: none; }
.overlap__track:has(.overlap__span:focus-visible) { outline: 2px solid var(--ink); outline-offset: 2px; }
/* A pointer drag is not keyboard navigation: while the visitor is dragging,
   the keyboard focus box stays off (keyboard use removes .is-pointer). */
.overlap__track.is-pointer:has(.overlap__span:focus-visible) { outline: none; }
/* The grips grow under hover or keyboard focus, so movability is recognised
   before the first drag, not only from the one-time nudge. */
.overlap__track:hover .overlap__grip,
.overlap__track:focus-within .overlap__grip { transform: translate3d(calc(var(--gx, 0px) + var(--nx)), 0, 0) scale(1.18); }
@media (min-width: 48rem) {
  /* Score-fix (layout): labels centre on their centred bars (the old
     end-alignment matched bars at the tracks' feet). */
  .overlap__row { align-items: center; }
  .overlap__label { padding-bottom: 0; }
}

.overlap__meet {
  height: .4rem;
  position: relative;
}

/* The held window is drawn twice: the bar, and a second hairline beneath it
   (light falling below, as a drawn rule rather than a blurred glow). */
.overlap__held::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + .35rem);
  height: 1px;
  background: rgb(var(--lantern));
  opacity: calc(var(--held, 1) * .55);
}

.overlap__held {
  left: 0;
  width: 100%;
  background: rgb(var(--lantern));
  transform-origin: 0 50%;
  transform: translate3d(calc(var(--hl, .42) * 100%), 0, 0) scaleX(var(--hw, .16));

}

/* The shared window: two hairlines through all three rows, lit when the
   availabilities meet. */
/* Score-fix (layout): the shared-window edges are contained between the
   first bar and the held bar. They no longer run the full device height
   through the day axis and readout (measured overflow before: the lines
   crossed both). Bottom stops at the held bar: axis + readout + gaps
   below it measure 5.5rem at both sizes; tops start at the first bar's
   centre (phone labels stack above their tracks, desktop labels sit
   beside them). The gradient's faded ends absorb a few px of drift. */
.overlap__edge {
  position: absolute;
  top: 1.625rem;
  bottom: 5.5rem;
  left: 0;
  width: 1px;
  background: linear-gradient(to bottom, transparent, rgba(var(--lantern), .9) 18%, rgba(var(--lantern), .9) 82%, transparent);
  /* Fix pass 2 (harden): hidden until experience.js arms the device
     (.is-armed, set on the first measured apply). Without the script
     --ex never leaves its 0px default and the edges used to stand as
     a stray amber vertical at the track's start; the settled static
     drawing (spans + held bar in styles.css) needs no edges. */
  opacity: 0;
  transform: translate3d(var(--ex, 0px), 0, 0);
  pointer-events: none;
}

.overlap.is-armed .overlap__edge {
  opacity: calc(var(--held, 1) * .75);
}

@media (max-width: 47.999rem) {
  /* Phone: first bar centre = device padding + label + row gap + half track. */
  .overlap__edge { top: 3.15rem; }
}

.fx .overlap__span { transition: background-color .5s ease-out; }
.fx .overlap.is-lit .overlap__span { background: #8a7b63; }

/* =============== Day of the date: all three frames at once =============== */
/* Layout (fixes5): the pinned dissolving stage stays gone and all three
   frames stay visible simultaneously (fixes2), but the band under the
   beats is removed. Desktop returns to two columns: the beats on their
   rail in the step-text column, and the three short-wide frames stacked
   in the axis (slit) column beside them, morning, on the way, arrived,
   dividing the list's height equally (the styles.css day-of grid).
   Mobile has no beside layout: the three frames stay together as one
   full-width stack after the beats, at their native 2:1, in the same
   order. No pinning, no dissolve, no scroll machinery. */

.aperture--arrived img { object-position: 47% 62%; }

/* Fixes9: amber dividers between the day-of frames. A 1px hairline in
   the page's amber (#e9a85b, var(--amber)) — the same 1px weight as the
   page's other rules (the day-of rail, statement rules) — centred in
   the breathing gap between consecutive frames, so the three dark
   frames read as three photographs. The gap is split evenly either
   side of the rule (.375rem + 1px + .375rem), keeping the total
   inter-frame spacing at the previous .75rem rhythm. */
.dayof__divider {
  display: block;
  height: 1px;
  background: var(--amber);
}

/* Score-fix pass (2026-11-05, COPY.md M8): each frame is captioned
   with the moment it pictures, in the beats' own approved words, set
   like the overlap drawing's row labels. The frame and its caption
   travel together (grid cell on desktop, swipe item on phones). */
.dayof__frame {
  display: grid;
  gap: .45rem;
  align-content: start;
  min-width: 0;
}
.dayof__frame .aperture--dayof { width: 100%; }
.dayof__caption {
  /* Reconcile (Mode 3 captions): serif caption register. The M8 words
     and their case are frozen copy; only the face changes. */
  font-family: var(--serif);
  font-size: 1rem;
  line-height: 1.35;
  color: var(--ink-soft);
}

/* Fixes17: the frames take their photographs' own 2:1, so no subject is
   cropped, and read in time order: a row under the beats on desktop, a
   strip to swipe on phones, with the amber rules standing between them. */
@media (min-width: 48rem) {
  .dayof { row-gap: clamp(2rem, 4vw, 3rem); }
  .dayof__frames {
    grid-column: 1 / -1;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr);
    gap: .75rem;
  }
  .dayof__frames .aperture--dayof { aspect-ratio: 2 / 1; height: auto; min-height: 0; }
  .dayof__divider { width: 1px; height: auto; }
}

@media (max-width: 47.999rem) {
  .dayof__frames {
    display: flex;
    gap: .75rem;
    margin-inline: calc(-1 * var(--pad));
    padding-inline: var(--pad);
    scroll-padding-inline: var(--pad);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .dayof__frames::-webkit-scrollbar { display: none; }
  /* The swipe item is the frame-plus-caption wrapper (M8); the
     aperture keeps its 2:1 inside it at full width. */
  .dayof__frames .dayof__frame {
    flex: 0 0 78vw;
    scroll-snap-align: start;
  }
  .dayof__frames .aperture--dayof {
    aspect-ratio: 2 / 1;
    height: auto;
  }
  .dayof__divider { flex: 0 0 1px; height: auto; }
}



/* The beats keep their static rail and dots from styles.css; nothing in
   the day-of is scroll-driven any more. */

/* =============== Price: a scene that arrives =============== */

.fx .card--price .ledger__row {
  border-top-color: transparent;
  position: relative;
  opacity: 0;
  transform: translate3d(0, 12px, 0);
}

.fx .card--price .ledger__row::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: 0 50%;
}

.fx .card--price .ledger__row:first-child::before { display: none; }

.fx .card--price.is-read .ledger__row {
  opacity: 1;
  transform: none;
  transition: opacity .6s ease-out calc(var(--i, 0) * .12s + .05s), transform .9s cubic-bezier(.16, 1, .3, 1) calc(var(--i, 0) * .12s + .05s);
}

.fx .card--price.is-read .ledger__row::before {
  transform: none;
  transition: transform 1.2s cubic-bezier(.16, 1, .3, 1) calc(var(--i, 0) * .22s);
}

.figure { position: relative; }

.figure { color: var(--amber); }
.fx .card--price .figure { display: inline-block; opacity: 0; transform: translate3d(0, .18em, 0); }

.fx .card--price.is-read .figure {
  opacity: 1;
  transform: none;
  transition: opacity 1.2s ease-out .5s, transform 1.4s cubic-bezier(.16, 1, .3, 1) .5s;
}

.fx .card--price .aperture--price img {
  transform: translate3d(0, calc(var(--sp, 0) * 4%), 0) scale(1.1);
  will-change: transform;
}

/* The price line carries weight: a 2px amber rule above "$29" (static too). */
.card--price .ledger__row--figure { border-top: 2px solid var(--amber); }
.fx .card--price .ledger__row--figure { border-top-color: transparent; }
.fx .card--price .ledger__row--figure::before { height: 2px; top: -2px; background: var(--amber); }

/* Light travels the price line once: from the held card's side, along the
   rule, and comes to rest at the figure's end of the ledger. */
.fx .card--price .ledger__row--figure::after {
  content: "";
  position: absolute;
  top: -2px;
  right: 0;
  width: 6rem;
  height: 2px;
  background: linear-gradient(90deg, rgba(243, 191, 126, 0), var(--amber-hi), rgba(243, 191, 126, 0));
  opacity: 0;
  pointer-events: none;
}

@keyframes ex-travel {
  0% { opacity: 0; transform: translate3d(14rem, 0, 0); }
  12% { opacity: 1; }
  88% { opacity: 1; }
  100% { opacity: 0; transform: translate3d(calc(-100% - 26rem), 0, 0); }
}

.fx .card--price.is-read .ledger__row--figure::after { animation: ex-travel 2.2s cubic-bezier(.45, 0, .2, 1) 1.3s both; }

/* The Price photograph bleeds exactly to the viewport's right edge:
   experience.js measures the shortfall and sets --price-bleed on the card. */
@media (min-width: 48rem) {
  .card--price .aperture--price { margin-inline-end: calc(-1 * var(--price-bleed, calc((100vw - min(100vw, 72rem)) / 2 + var(--pad)))); }
}

.fx .card--price .aperture--price { clip-path: inset(0 0 0 100%); }
.fx .card--price.is-in .aperture--price { clip-path: inset(0); transition: clip-path 1.6s cubic-bezier(.16, 1, .3, 1) .1s; }

/* =============== Safety: a threshold =============== */

.aperture--safety::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: #030202;
  opacity: 0;
  pointer-events: none;
}

.fx .card--safety .aperture--safety::before { opacity: .55; }
/* The lit window rides higher in its tall slit, so the photograph does not
   sink into the deepest black at the foot of the section. */
@media (min-width: 48rem) {
  .card--safety .aperture--safety img { object-position: 50% 64%; }
}
.fx .card--safety .aperture--safety { clip-path: inset(36% 32% 36% 32%); }
.fx .card--safety.is-in .aperture--safety { clip-path: inset(0); transition: clip-path 2.6s cubic-bezier(.16, 1, .3, 1) .2s; }
.fx .card--safety.is-in .aperture--safety::before { opacity: 0; transition: opacity 2.4s ease-in-out .5s; }

/* After the window lights, its light reaches each measure in turn (the amber
   disc of each row), then the statement's rule is drawn from the window's side.
   The text itself is present throughout; only the light moves. */
.fx .card--safety .measures__row::before { opacity: .22; transform: scale(.55); }
.fx .card--safety.is-read .measures__row::before {
  opacity: 1;
  transform: none;
  transition: opacity .7s ease-out calc(.25s + var(--i, 0) * .32s), transform .7s cubic-bezier(.16, 1, .3, 1) calc(.25s + var(--i, 0) * .32s);
}

.card--safety .card__statement { border-top-width: 2px; }
.fx .card--safety .card__statement { border-top-color: transparent; position: relative; }
.fx .card--safety .card__statement::before {
  content: "";
  position: absolute;
  top: -2px;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--amber);
  transform: scaleX(0);
  transform-origin: 100% 50%;
}
.fx .card--safety.is-read .card__statement::before { transform: none; transition: transform 1.4s cubic-bezier(.16, 1, .3, 1) 1.7s; }



/* =============== The reservation card =============== */

.reservation {
  --fill: 0;
  --focus: 0;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: background-color .9s ease, color .9s ease, box-shadow .8s ease;
  background-color: color-mix(in srgb, var(--paper) calc(100% - var(--fill) * 9%), #f1d8ab);
}

.reservation > * { position: relative; z-index: 2; }

/* The card's light: a lit rule along its foot that fills as the form does
   (a drawn line, not a glow). */
.reservation::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  z-index: 1;
  pointer-events: none;
  background: var(--amber);
  transform-origin: 0 50%;
  transform: scaleX(calc(var(--fill) * .92 + var(--focus) * .08));
  transition: transform .7s cubic-bezier(.16, 1, .3, 1);
}

.held {
  position: absolute;
  inset: 0;
  z-index: 0 !important;
  margin: 0;
  opacity: 0;
  pointer-events: none;
  clip-path: inset(0 50% 0 50%);
}

.held img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 55%; display: block; }
.held::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(8, 7, 6, .94) 0, rgba(8, 7, 6, .8) 44%, rgba(8, 7, 6, .05) 62%); }
/* Fixes15: the lantern stands clear of the message, unscrimmed; the
   photograph grows from its left foot to carry it into the right third. */
@media (min-width: 48rem) {
  .held img { inset: auto auto 0 0; width: 135%; height: 135%; max-width: none; }
}

@media (max-width: 47.999rem) {
  .held::after { background: linear-gradient(0deg, rgba(8, 7, 6, .95) 0, rgba(8, 7, 6, .75) 55%, rgba(8, 7, 6, .3) 100%); }
}

/* Valid submit: the paper card becomes the held table. */
.reservation.is-held { background-color: var(--night); color: var(--ink); }
.reservation.is-held .held { opacity: 1; clip-path: inset(0); transition: clip-path 1.1s cubic-bezier(.16, 1, .3, 1); }
.reservation.is-held .aperture--close { opacity: 0; transition: opacity .6s ease-out; }
.reservation.is-held .confirmation__message { color: var(--ink); transition: color .9s ease; }

/* The held state takes the card: the form's heading yields (kept for
   assistive technology, which still names the section by it), so the two
   approved lines never stack. */
.reservation.is-held .reservation__title {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.reservation.is-held { align-content: center; }
/* The held message sits at the card's vertical centre, over the lit table,
   so the card does not read as a message over a dark lower third. */
@media (min-width: 48rem) {
  .reservation.is-held .waitlist__main { align-self: center; grid-row: 1 / -1; }
}
.reservation.is-held .confirmation { border-top-color: rgb(var(--lantern)); }
.reservation.is-held :focus-visible { outline-color: var(--ink); }
/* Focus lands here by script so readers announce it; it is not a control. */
.confirmation:focus-visible { outline: none; }

/* Mobile held state: photograph first, full-bleed across the card's head, the
   approved message directly under it; the card's height is released (no pinned
   1150px slab) and the script brings its top into view. */
@media (max-width: 47.999rem) {
  .reservation.is-held { min-height: 0 !important; align-content: start; }
  .reservation.is-held .aperture--close { display: none; }
  .reservation.is-held .held {
    position: relative;
    inset: auto;
    order: -1;
    aspect-ratio: 2 / 1;
    margin: calc(-1 * var(--card-pad)) calc(-1 * var(--card-pad)) 0;
  }
  .held img { object-position: 50% 50%; }
  .held::after { background: linear-gradient(0deg, rgba(8, 7, 6, .9) 0, rgba(8, 7, 6, 0) 45%); }
}

@media (prefers-reduced-motion: reduce) {
  .reservation,
  .reservation.is-held .held,
  .reservation.is-held .aperture--close { transition: none; }
}

/* ---- Owner-approved rule relaxations (2026-11-04): week axis + held edit path ---- */
.overlap__axis {
  position: relative;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  margin-top: .5rem;
  padding-top: .45rem;
  font-size: .875rem;
  line-height: 1.4;
  color: var(--ink-soft);
}
/* Score-fix pass (2026-11-05, COPY.md M7): every day is named, centred
   in its own seventh of the track, so a window's position reads against
   weekdays directly and "Sun" names the last day rather than the
   track's end point. Replaces the end-only Mon/Sun labels of M4. */
.overlap__day { text-align: center; }
/* Day-boundary marks: six marks dividing the week into seven days.
   Polish (round 8, P3): strengthened within the system — wider, taller,
   and in Quiet Taupe (ink-quiet, ~6.7:1 on the lifted band) instead of
   the near-invisible 1x8px track taupe. */
.overlap__daytick {
  position: absolute;
  top: 0;
  left: calc(var(--d) * 100% / 7);
  width: 2px;
  margin-left: -1px;
  height: .8rem;
  background: var(--ink-quiet);
}
.overlap__readout {
  margin-top: .35rem;
  min-height: 1.4em;
  font-size: .875rem;
  line-height: 1.4;
  color: var(--amber);
  text-align: right;
}
@media (min-width: 48rem) {
  /* The axis starts where the tracks start: after the label column
     (9rem + 1.5rem since the score-fix layout pass tightened it). */
  .overlap__axis { margin-left: calc(9rem + 1.5rem); }
}
.confirmation__edit {
  margin-top: .6rem;
  padding: .7rem .25rem;
  background: none;
  border: 0;
  font: inherit;
  font-size: .95rem;
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: .25em;
  cursor: pointer;
}
.confirmation__edit:hover { color: var(--ink); }

/* =============== Cast light on display text (owner direction, round 8) ===============
   Colorize/polish: display text adjacent to a lit photograph reads as lit
   by it — the photograph's amber cast across the letters, warmest nearest
   each image's own light, falling off with distance. Text only: page
   grounds stay as reverted under amendment (b); body copy, form text,
   labels and errors stay flat ink. The light is a state, not motion, so
   it is present with or without `fx` and under reduced motion.
   Contrast floor: every stop stays at or above the round-6 measured
   floor of 6.57:1 on its ground (the farthest stop, #d9cfbf, is ~13:1
   on Warm Night, ~12:1 on the lifted band, ~13:1 on Deepest Black);
   where the glow would cost contrast, the glow yields — Price and
   Safety, whose titles sit farthest from their photographs, take a
   weaker cast than the hero. */
.cast {
  --cast-near: #f3bf7e;
  --cast-mid: #eed3a8;
  --cast-far: #d9cfbf;
  --cast-dir: to left;
  /* Static (owner ruling, 2026-11-05, DESIGN (m)): the hero clip
     plays on its own and no surface takes light from it, so the
     cast stands exactly as round 8 set it, at all times. */
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .cast {
    /* The stops sit at round 8's 42/82, fixed. */
    background-image: linear-gradient(var(--cast-dir), var(--cast-near) 0%, var(--cast-mid) 42%, var(--cast-far) 82%);
    /* Fixes17: Price and Safety set --cast-size so the falloff runs over
       a length longer than the title box, anchored at the far side
       (default position): their titles sit far from their photographs
       (round-8 P3), so the letters sample only the shallow far stretch
       of the same gradient instead of its full near-to-far span across
       the title's own width. Sampled colours stay between the far and
       mid stops, so no stop drops below the contrast floor above. The
       hero and held casts, genuinely adjacent to their light, keep the
       full span. */
    background-size: var(--cast-size, auto);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}
/* Hero: on desktop the lantern sits in the photograph to the right of
   the headline, so the cast warms from the right; on mobile the
   photograph stands below the text, so it warms from below. */
.cast--held { --cast-near: #f3bf7e; --cast-mid: #e9cf9f; --cast-far: #d9cfbf; }
/* On the two surfaces genuinely beside their light, an oversized
   gradient (160%) is centred on the letters, so the sampled span
   is ~19%–81% of the gradient, a near-match for the old full span. */
.cast--hero, .cast--held { --cast-size: 160% 100%; background-position: 50% 50%; }
.cast--price { --cast-near: #eed3a8; --cast-mid: #e2d2b8; --cast-far: #d9cfbf; --cast-size: 250% 100%; }
.cast--safety { --cast-near: #e6d3ae; --cast-mid: #ddd0ba; --cast-far: #d9cfbf; --cast-size: 250% 100%; }
/* The two weakest casts (2026-11-05): How it works is lit from
   below (the overlap's amber and the day-of row stand under it);
   FAQ from above (the reservation card's lantern and held table).
   Both sample only the gradient's far stretch, like Price/Safety. */
.cast--how { --cast-near: #e6d3ae; --cast-mid: #ddd0ba; --cast-far: #d9cfbf; --cast-dir: to top; --cast-size: 100% 250%; }
.cast--faq { --cast-near: #e6d3ae; --cast-mid: #ddd0ba; --cast-far: #d9cfbf; --cast-dir: to bottom; --cast-size: 100% 250%; background-position: 50% 100%; }
@media (max-width: 47.999rem) {
  .cast--hero, .cast--price, .cast--safety { --cast-dir: to top; }
  .cast--price, .cast--safety { --cast-size: 100% 250%; }
  /* On mobile the held photograph stands above the message. */
  .cast--held { --cast-dir: to bottom; }
  /* The oversized gradient turns vertical with the direction,
     centred on the letters. */
  .cast--hero { --cast-size: 100% 160%; background-position: 50% 50%; }
  .cast--held { --cast-size: 100% 160%; background-position: 50% 50%; }
}

/* One flame, on its own (owner ruling, 2026-11-05, DESIGN (m)):
   the hero clip is the page's only moving light. No surface
   outside the hero takes light from it, and no appearance on
   the page depends on time. */

/* =============== Fixes15 =============== */

/* Tablet: the lede stops short of the field, so it never crosses the glass. */
@media (min-width: 48rem) and (max-width: 63.999rem) {
  .lede { max-width: 22rem; }
}

/* Short phones: the table stays on the first screen. Fix pass 2 set
   the copy one step tighter here (smaller display type and lede) so
   copy + the full 3:2 frame fit 390x667. Fix pass 3 (owner: the type
   must stop reading small; the aperture flexes instead): the type
   sizes and leading no longer drop on short screens — only the
   copy's own gaps tighten a step, the reserve shrinks by the same
   amount, and the aperture's height takes the rest of the
   adjustment down to its 10rem floor. Wording and order unchanged. */
@media (max-width: 47.999rem) and (max-height: 700px) {
  .hero { --copy-reserve: 28.5rem; }
  .hero__copy { gap: .55rem; }
  .hero h1 { margin-bottom: .3rem; }
  .hero__action { margin-top: 0; }
  .hero .action { padding-block: .8rem; }
  .hero__free { margin-top: .45rem; }
}

/* Browser autofill keeps the ledger field's paper, not its own tint. */
.reservation .field input:-webkit-autofill,
.reservation .field input:-webkit-autofill:hover,
.reservation .field input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--paper-ink);
  box-shadow: inset 0 0 0 100px var(--paper);
  caret-color: var(--paper-ink);
}

/* The address as entered, in the card's own ink for either state. */
.confirmation__email {
  margin-top: 1rem;
  font-size: 1.0625rem;
  line-height: 1.4;
  overflow-wrap: anywhere;
}

/* The closing action, for whoever read the FAQ first; gone once joined. */
.faq__action { justify-self: start; }
body:has(#waitlist-form[hidden]) .faq__action { display: none; }

/* The typo hint: reads as a note on the ledger line, not an error. */
.field__hint {
  margin-top: .35rem;
  font-size: .9375rem;
  line-height: 1.4;
  color: var(--paper-soft);
  overflow-wrap: anywhere;
}
.field__suggest {
  padding: .75rem .1rem;
  margin-block: -.75rem;
  background: none;
  border: 0;
  font: inherit;
  font-weight: 600;
  color: var(--paper-ink);
  text-align: left;
  text-decoration: underline;
  text-underline-offset: .2em;
  cursor: pointer;
}

/* The second paragraph of a longer FAQ answer. */
.faq details p + p { margin-top: .75rem; }

