/* ==========================================================================
   HATCH WEDDINGS — "The Reel"
   Core system: tokens, reset, typography, atmosphere, chrome.

   Concept: the site is built like a film, not decorated like one.
   Letterboxing is real structure. Timecode is real UI. Grain is projected.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Strictly monochrome. Hierarchy comes from brightness alone — there is
     no hue anywhere in the interface. */

  /* Ground — a hair off pure #000, which renders flat and cheap on OLED */
  --ink:        #070707;
  --ink-1:      #0C0C0C;
  --ink-2:      #121212;
  --ink-3:      #191919;

  /* Type — a touch under pure white, which glares against black */
  --bone:       #EDEDEB;
  --bone-2:     rgba(237, 237, 235, 0.58);
  --bone-3:     rgba(237, 237, 235, 0.34);
  --bone-4:     rgba(237, 237, 235, 0.16);

  /* Brand gold, sampled straight off Connor's package sheet (the headings
     and the price figures both measure ~#E8C29C). Used sparingly, the way
     that sheet uses it: section marks, the active chapter, the live dot,
     the progress bar. Never as a large fill. */
  --accent:     #E8C29C;
  --accent-dim: rgba(232, 194, 156, 0.34);

  --hair:       rgba(237, 237, 235, 0.11);
  --hair-soft:  rgba(237, 237, 235, 0.06);

  /* Type families — Connor's brand fonts.
     Raleway does all the work here, at two extremes: hairline-light and very
     wide-tracked for display, regular for reading. The brand's second face,
     Amsterdam Four, is a licensed script and is NOT substituted with a
     lookalike — the wordmark uses the real logo artwork instead. */
  --serif: 'Raleway', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --sans:  'Raleway', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --mono:  ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;

  /* Rhythm */
  --gut:    clamp(1.25rem, 4.5vw, 3.5rem);   /* page gutter */
  --bay:    clamp(5rem, 13vh, 10rem);        /* section padding */
  --measure: 34rem;                          /* reading width */

  /* Motion — one shared easing language.
     Long, low-acceleration curves read as "expensive". */
  --ease:      cubic-bezier(0.22, 1, 0.36, 1);      /* out-expo-ish */
  --ease-io:   cubic-bezier(0.76, 0, 0.24, 1);      /* in-out, for curtains */
  --ease-soft: cubic-bezier(0.33, 1, 0.68, 1);
  --t-fast:   .34s;
  --t-med:    .7s;
  --t-slow:   1.15s;

  /* Anamorphic letterbox bar height, driven by JS on load */
  --bar: 0px;

  --z-grain: 90;
  --z-nav:   100;
  --z-veil:  200;
}

/* --------------------------------------------------------------------------
   2. Reset
   -------------------------------------------------------------------------- */

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

* { margin: 0; padding: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto; /* JS owns smooth scroll; native would fight it */
}

body {
  background: var(--ink);
  color: var(--bone);
  font-family: var(--sans);
  font-size: clamp(0.9375rem, 0.86rem + 0.32vw, 1.0625rem);
  font-weight: 400;
  line-height: 1.75;
  letter-spacing: 0.005em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  overscroll-behavior-y: none;
  text-rendering: optimizeLegibility;
}

/* Locked while the title card plays and while the player is open */
body.is-locked { overflow: hidden; }

/* The UA rule for [hidden] is only `display: none`, so ANY author display
   declaration beats it — a `.gate { display: grid }` keeps rendering while
   hidden. Every panel toggled with .hidden in this codebase relies on this. */
[hidden] { display: none !important; }

img, video, svg, canvas { display: block; max-width: 100%; height: auto; }

button, input, textarea, select {
  font: inherit; color: inherit; background: none; border: none; outline: none;
}
button { cursor: pointer; }

a { color: inherit; text-decoration: none; }

::selection { background: var(--accent); color: var(--ink); }

/* Focus: visible for keyboards, never for mouse. Non-negotiable for a11y. */
:focus-visible {
  outline: 1px solid var(--accent);
  outline-offset: 4px;
}

/* Thin, dark scrollbar — the default light one breaks the mood instantly */
::-webkit-scrollbar { width: 9px; }
::-webkit-scrollbar-track { background: var(--ink); }
::-webkit-scrollbar-thumb {
  background: rgba(237, 237, 235, 0.14);
  border: 3px solid var(--ink);
  border-radius: 99px;
}
::-webkit-scrollbar-thumb:hover { background: rgba(237, 237, 235, 0.26); }
html { scrollbar-width: thin; scrollbar-color: rgba(237, 237, 235,.14) var(--ink); }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* --------------------------------------------------------------------------
   3. Typography
   -------------------------------------------------------------------------- */

/* Display type is Raleway at its lightest weight with open tracking. A
   hairline sans at size reads as expensive in a way that a heavier cut
   never does — and it matches Connor's own package sheet, which sets
   everything but the wordmark in spaced capitals. */
.display,
h1, h2, h3 {
  font-family: var(--serif);
  font-weight: 200;
  line-height: 1.06;
  letter-spacing: 0.005em;
  text-wrap: balance;
}

/* Hero wordmark — spaced wide like a title card, the way the client drew it */
.display-xl {
  font-family: var(--serif);
  font-weight: 200;
  font-size: clamp(2.9rem, 11vw, 10rem);
  line-height: 0.98;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-indent: 0.16em; /* optically re-centre the trailing letter-space */
}

.display-l {
  font-size: clamp(2.2rem, 5.6vw, 4.6rem);
  line-height: 1.08;
  letter-spacing: 0.012em;
  text-wrap: balance;
}

.display-m {
  font-size: clamp(1.75rem, 3.6vw, 2.9rem);
  line-height: 1.14;
  letter-spacing: 0.014em;
  text-wrap: balance;
}

.display-s {
  font-size: clamp(1.2rem, 1.9vw, 1.6rem);
  line-height: 1.3;
  font-weight: 300;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  /* Often used on <p>, which misses the h1-h3 balance rule above */
  text-wrap: balance;
}

em, .italic {
  font-style: italic;
  font-synthesis: none;
}

/* The wide-tracked uppercase micro-label — the connective tissue of the
   whole design. Appears above every section, on every button, in every
   caption. Consistency here is what makes it feel like one object. */
.label {
  font-family: var(--sans);
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--bone-3);
}
.label--bone { color: var(--bone); }
.label--accent { color: var(--accent); }

.tc {
  font-family: var(--mono);
  font-size: 0.625rem;
  font-weight: 400;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--bone-3);
}

.lede {
  font-size: clamp(1.0625rem, 0.98rem + 0.4vw, 1.25rem);
  line-height: 1.68;
  color: var(--bone-2);
  max-width: var(--measure);
}

.body { color: var(--bone-2); max-width: var(--measure); }
.body p + p { margin-top: 1.15em; }

/* Rebalances the final lines so a paragraph never ends on a lone word.
   Where it isn't supported, app.js binds the last two words with a
   non-breaking space, which achieves the same thing everywhere. */
.body p, p.body, .lede, .pack__tag, .step__body p {
  text-wrap: pretty;
}

/* Short standalone paragraphs get the stronger treatment: balance evens
   every line rather than only rescuing the last one. Browsers cap balancing
   at a handful of lines, so it is scoped to the short blocks. */
.reason p.body,
.value p,
.pack__tag {
  text-wrap: balance;
}

/* A hairline rule with a white tick at its head — used as a section mark */
.rule {
  position: relative;
  height: 1px;
  background: var(--hair);
  border: 0;
}
.rule::before {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 2.5rem;
  background: var(--accent);
}

/* --------------------------------------------------------------------------
   4. Layout primitives
   -------------------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: 92rem;
  margin-inline: auto;
  padding-inline: var(--gut);
}

.shell--wide { max-width: 110rem; }

.bay { padding-block: var(--bay); }

.stack > * + * { margin-top: var(--s, 1.5rem); }

/* --------------------------------------------------------------------------
   5. Atmosphere — grain, vignette, letterbox, veil
   These four layers are what separate "dark website" from "projected film".
   -------------------------------------------------------------------------- */

/* 5a. Film grain.
   A tiled turbulence texture, re-positioned in discrete steps so it flickers
   at roughly 10fps — real film grain resolves anew every frame; stepping it
   reads as celluloid rather than as a static noise PNG. */
.grain {
  position: fixed;
  /* Just enough overhang to cover the ±9% drift in grain-shift below.
     At -150% this was a 4600×2500 backing store — 11 megapixels of
     permanently animating layer for an effect that is 5% opaque. */
  inset: -15%;
  z-index: var(--z-grain);
  pointer-events: none;
  opacity: 0.05;
  background-image: var(--grain-src);
  background-size: 220px 220px;
  will-change: transform;
  animation: grain-shift 0.8s steps(1) infinite;
}

@keyframes grain-shift {
  0%   { transform: translate3d(0, 0, 0); }
  10%  { transform: translate3d(-7%, -4%, 0); }
  20%  { transform: translate3d(4%, -9%, 0); }
  30%  { transform: translate3d(-9%, 6%, 0); }
  40%  { transform: translate3d(6%, 3%, 0); }
  50%  { transform: translate3d(-3%, 8%, 0); }
  60%  { transform: translate3d(8%, -6%, 0); }
  70%  { transform: translate3d(-6%, -8%, 0); }
  80%  { transform: translate3d(3%, 7%, 0); }
  90%  { transform: translate3d(-8%, 2%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* 5b. Vignette — applied to the imagery, not the page.

   As a fixed, page-wide overlay its radial falloff was visible as a soft arc
   across flat black sections: two subtly different blacks meeting on a curve.
   Sitting inside the media wrappers it does what it is for, darkening the
   edges of a picture, and leaves plain sections untouched. */
.hero__media::after,
.closer__media::after,
.fhero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    120% 95% at 50% 42%,
    transparent 40%,
    rgba(0, 0, 0, 0.32) 80%,
    rgba(0, 0, 0, 0.62) 100%
  );
}

/* 5c. Letterbox bars — real anamorphic masking. Retract on load, and close
   again during page transitions like a scene cut. */
.bar {
  position: fixed;
  left: 0;
  right: 0;
  height: var(--bar);
  background: var(--ink);
  z-index: 95;
  pointer-events: none;
  transition: height var(--t-slow) var(--ease-io);
}
.bar--t { top: 0; }
.bar--b { bottom: 0; }

/* 5d. The veil — full-bleed curtain for the title card and page cuts */
.veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-veil);
  background: var(--ink);
  display: grid;
  place-items: center;
  transition: opacity var(--t-med) var(--ease), visibility var(--t-med);
}
/* Without JS there is nothing to lift the curtain, so never draw it */
html:not(.js) .veil { display: none !important; }
.veil[hidden] { display: none !important; }
.veil.is-gone { opacity: 0; visibility: hidden; pointer-events: none; }

/* --------------------------------------------------------------------------
   6. Title card (the opening sequence)
   -------------------------------------------------------------------------- */

.card {
  text-align: center;
  display: grid;
  gap: 2rem;
  justify-items: center;
  padding-inline: var(--gut);
}

/* The signature writes itself.

   A clip-path wipe rather than an SVG stroke: the mark is supplied as
   artwork, and auto-tracing a filled script into a centreline path to
   animate stroke-dashoffset gives an outline that draws around each letter
   instead of through it — which reads as tracing, not writing.

   The keyframe stops are uneven on purpose. A pen moves fast through the
   long H sweep, slows across a-t-c, then flicks through the final stroke.
   A single easing curve makes it glide like a wipe; these make it feel
   written. */
.card__sig {
  width: min(22rem, 62vw);

  /* A feathered mask sliding across, not a hard-edged clip.

     The first stroke of this signature is a large flourish that travels
     right to left, so a crisp vertical wipe made the mark look like it was
     growing backwards. A soft gradient edge reads as ink appearing instead
     of as a shutter passing over, and mask-position animates everywhere,
     unlike an interpolated custom property. */
  -webkit-mask-image: linear-gradient(90deg, #000 38%, transparent 62%);
          mask-image: linear-gradient(90deg, #000 38%, transparent 62%);
  -webkit-mask-size: 260% 100%;
          mask-size: 260% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: 100% 0;
          mask-position: 100% 0;
  animation: sign 1.9s cubic-bezier(0.45, 0, 0.25, 1) 0.25s forwards;
}
.card__sig img { width: 100%; height: auto; }

@keyframes sign {
  to { -webkit-mask-position: 0% 0; mask-position: 0% 0; }
}

/* No motion: show the finished mark, never a half-written one */
@media (prefers-reduced-motion: reduce) {
  .card__sig {
    animation: none;
    -webkit-mask-image: none;
            mask-image: none;
  }
}

.card__mark {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(2rem, 6vw, 3.6rem);
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: var(--bone);
}

.card__sub {
  opacity: 0;
  animation: card-fade .9s var(--ease) .85s forwards;
}

@keyframes card-fade { to { opacity: 1; } }

/* Loading rule — fills as assets resolve */
.card__rail {
  width: min(17rem, 52vw);
  height: 1px;
  background: var(--hair);
  position: relative;
  overflow: hidden;
  opacity: 0;
  animation: card-fade .6s var(--ease) 1s forwards;
}
.card__fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  background: var(--accent);
  transition: width .45s var(--ease-soft);
}

.card__pct {
  position: fixed;
  right: var(--gut);
  bottom: calc(var(--gut) - 0.25rem);
  opacity: 0;
  animation: card-fade .6s var(--ease) 1s forwards;
}

/* --------------------------------------------------------------------------
   7. Navigation
   -------------------------------------------------------------------------- */

.nav {
  position: fixed;
  top: var(--bar);
  left: 0; right: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding: 1.6rem var(--gut);
  transition: transform var(--t-med) var(--ease),
              background var(--t-med) var(--ease),
              top var(--t-slow) var(--ease-io),
              padding var(--t-med) var(--ease);
}

/* Condenses and frosts once you leave the hero */
.nav.is-stuck {
  padding-block: 1.05rem;
  background: rgba(7, 7, 7, 0.72);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  border-bottom: 1px solid var(--hair-soft);
}

/* The lockup, built to match the Hatch Videography mark Connor already uses:
   the signature large, the word set small and widely tracked, tucked up under
   the script's descender and starting about halfway along it rather than
   sitting squarely underneath. The negative top margin is what does the
   tucking; the left indent is expressed against the mark's height so the two
   stay in proportion as the mark scales. */
.brand {
  display: inline-grid;
  gap: 0;
  line-height: 1;
  justify-items: start;
  /* Held here so the wordmark's indent can be derived from it: the two have
     to stay in proportion as the mark scales between breakpoints, and an em
     indent on the word would not, since the word's size is fixed. */
  --mark-h: clamp(2rem, 2.9vw, 2.6rem);
}

/* The real signature mark (Amsterdam Four), supplied as white-on-transparent */
.brand__mark {
  height: var(--mark-h);
  width: auto;
}

.brand__name {
  font-family: var(--serif);
  font-weight: 300;
  font-size: 1.24rem;
  letter-spacing: 0.3em;
  text-indent: 0.3em;
  text-transform: uppercase;
}

.brand__sub {
  font-size: 0.5rem;
  font-weight: 500;
  letter-spacing: 0.46em;
  text-indent: 0.46em;
  text-transform: uppercase;
  /* Was --bone-3, which is 34% of bone over near-black: about 2.6:1, under
     the 4.5:1 floor and visibly murky at this size. A wordmark should not be
     something you squint at. */
  color: var(--bone);

  /* The signature carries a long horizontal flourish at 89% of its height —
     its own underline. The word sits just beneath that, starting where the
     flourish does rather than square under the H, which is the arrangement
     on the Hatch Videography mark. 1.835 is the artwork's aspect ratio
     (378x206), so this stays at 44% of the mark's width at any size. */
  /* Clear of the signature's flourish rather than tucked into it. */
  margin-top: calc(var(--mark-h) * 0.17);
  margin-left: calc(var(--mark-h) * 1.835 * 0.44);
}

.nav__links {
  display: flex;
  align-items: center;
  gap: clamp(1.4rem, 2.6vw, 2.8rem);
  list-style: none;
}

.nav__link {
  position: relative;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--bone-2);
  padding-block: 0.4rem;
  transition: color var(--t-fast) var(--ease);
}

/* Underline wipes in from the left, out to the right — directional, not a
   symmetrical scale. Small thing; reads as considered. */
.nav__link::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform var(--t-fast) var(--ease);
}
.nav__link:hover { color: var(--bone); }
.nav__link:hover::after { transform: scaleX(1); transform-origin: left; }

.nav__link.is-here { color: var(--bone); }
.nav__link.is-here::after { transform: scaleX(1); background: var(--bone-4); }

/* Persistent primary action in the bar. Hidden on mobile, where the same
   link already sits in the full-screen menu. */
.nav__cta {
  flex: none;
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--bone);
  padding: 0.78rem 1.25rem;
  border: 1px solid var(--bone-4);
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.nav__cta::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--bone);
  transform: scaleY(0);
  transform-origin: bottom;
  transition: transform var(--t-med) var(--ease);
}
.nav__cta:hover { color: var(--ink); border-color: var(--bone); }
.nav__cta:hover::before { transform: scaleY(1); }

@media (max-width: 60rem) { .nav__cta { display: none; } }

/* Burger — three hairlines, collapses to an X */
.burger {
  display: none;
  width: 26px; height: 14px;
  position: relative;
  flex: none;
}
.burger span {
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: var(--bone);
  transition: transform var(--t-fast) var(--ease), opacity var(--t-fast);
}
.burger span:nth-child(1) { top: 0; }
.burger span:nth-child(2) { top: 50%; }
.burger span:nth-child(3) { bottom: 0; }

.burger.is-open span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.burger.is-open span:nth-child(2) { opacity: 0; }
.burger.is-open span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* Full-bleed mobile menu */
.menu {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: var(--ink);
  display: grid;
  align-content: center;
  gap: 0.4rem;
  padding: var(--gut);
  clip-path: inset(0 0 100% 0);
  transition: clip-path var(--t-med) var(--ease-io);
  pointer-events: none;
}
.menu.is-open { clip-path: inset(0 0 0 0); pointer-events: auto; }

.menu a {
  font-family: var(--serif);
  font-weight: 300;
  font-size: clamp(2.1rem, 9vw, 3.6rem);
  line-height: 1.18;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  opacity: 0;
  transform: translateY(0.4em);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}
.menu.is-open a { opacity: 1; transform: none; }
.menu.is-open a:nth-child(1) { transition-delay: .12s; }
.menu.is-open a:nth-child(2) { transition-delay: .18s; }
.menu.is-open a:nth-child(3) { transition-delay: .24s; }
.menu.is-open a:nth-child(4) { transition-delay: .30s; }
.menu.is-open a:nth-child(5) { transition-delay: .36s; }

@media (max-width: 60rem) {
  .nav__links { display: none; }
  .burger { display: block; }
}

/* --------------------------------------------------------------------------
   9. Buttons & links
   -------------------------------------------------------------------------- */

/* The fill wipes up from the bottom edge as a background-size transition on
   the element itself.

   It used to be an absolutely positioned ::before at z-index:-1, which relies
   on the button's stacking context surviving wherever it is placed. Any
   ancestor creating its own context, or a sibling painted at a negative
   z-index, could bury the fill behind the page. A background needs no
   stacking context at all, so nothing can hide it. */
.btn {
  --pad: 1.05rem;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 1.6rem;
  padding: var(--pad) 1.7rem;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--bone);
  border: 1px solid var(--bone-4);
  background-color: transparent;
  background-image: linear-gradient(var(--bone), var(--bone));
  background-repeat: no-repeat;
  background-position: 50% 100%;
  background-size: 100% 0%;
  transition: background-size var(--t-med) var(--ease),
              color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
}

.btn:hover {
  color: var(--ink);
  border-color: var(--bone);
  background-size: 100% 100%;
}

.btn__arrow { transition: transform var(--t-med) var(--ease); }
.btn:hover .btn__arrow { transform: translateX(5px); }

/* Inverted: solid at rest, wipes to ink on hover */
.btn--solid {
  background-color: var(--bone);
  background-image: linear-gradient(var(--ink), var(--ink));
  color: var(--ink);
  border-color: var(--bone);
}
.btn--solid:hover { color: var(--bone); }

/* Text-only variant. No horizontal padding, so it must never take .btn:hover's
   border or fill — the outline would sit hard against the text. */
.btn--bare {
  border-color: transparent;
  padding-inline: 0;
  background-image: none;
}
.btn--bare:hover {
  color: var(--bone);
  border-color: transparent;
  background-size: 100% 0%;
}

/* Text link with a hairline that wipes through */
.tlink {
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
}
.tlink::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  opacity: .4;
  transform-origin: right;
  transform: scaleX(1);
  transition: transform .5s var(--ease);
}
.tlink:hover::after { transform: scaleX(0); }

/* --------------------------------------------------------------------------
   10. Reveals
   Everything enters. Nothing pops. Driven by IntersectionObserver adding
   .is-in — the CSS here only describes the two states.

   Every hidden starting state is scoped to html.js, a class set by an inline
   script in the head. If the module ever fails to load — old browser, blocked
   script, CDN hiccup — the page renders fully visible instead of blank. An
   invisible site is a far worse failure than an unanimated one.
   -------------------------------------------------------------------------- */

/* Deliberately NO blanket `will-change` here.

   It used to sit on every [data-rv] element, which promoted hundreds of
   nodes to their own compositing layers for the life of the page. Chrome
   then has to squash the surrounding content into extra backing stores, and
   each store rounds the flat page black independently — so the seams showed
   up as faint rectangles of a very slightly different black across otherwise
   empty sections. On a good display in a dark room they read as stray lines.

   app.js now sets will-change on a single element just before it animates
   and clears it when the transition ends, which is what the property is for.
   Nothing about the motion changes; the layers simply stop outliving it. */

/* Fade + rise */
.js [data-rv="up"] {
  opacity: 0;
  transform: translateY(2.2rem);
  transition: opacity var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}

/* clip-path clips to the border box, so a descender hanging below the text
   box is sliced off even at inset(0). The padding gives the tails room and
   sits outside the .js scope so it can't cause a layout shift. */
[data-rv="mask"] { padding-bottom: 0.24em; }

/* Anamorphic mask — the signature reveal. Content is unveiled by a mask
   opening from the centre, the way a projector gate opens. */
.js [data-rv="mask"] {
  clip-path: inset(50% 0 50% 0);
  transition: clip-path 1.5s var(--ease-io);
}

/* Image scale-down: starts overscaled, settles. Reads as a camera settling. */
.js [data-rv="frame"] { overflow: hidden; clip-path: inset(0 0 100% 0); transition: clip-path 1.4s var(--ease-io); }
.js [data-rv="frame"] > img,
.js [data-rv="frame"] > video,
.js [data-rv="frame"] > picture {
  transform: scale(1.22);
  transition: transform 2s var(--ease);
}

/* Per-line text reveal. JS wraps each line in .ln > .ln__i

   The mask needs room for descenders. Display line-height is 1.04–1.14, so
   the g, y and p tails fall outside the line box and `overflow: hidden`
   sliced them off. The padding extends the clip box below the baseline and
   the equal negative margin keeps the visual line spacing identical. */
.ln {
  display: block;
  overflow: hidden;
  padding-bottom: 0.22em;
}

/* Compensation goes BETWEEN lines, never after the last one.

   As margin-bottom on every .ln, the final line's negative margin leaked out
   of the heading and the paragraph below climbed into its descenders — 27px
   of overlap on a phone, where the display sizes are large relative to the
   gap. Moving it to the top of each following line keeps the inter-line
   advance identical and leaves the heading's bottom edge where it belongs. */
.ln + .ln { margin-top: -0.22em; }
.js .ln__i {
  display: block;
  /* Past 100% so the line clears the added padding as well as its own box */
  transform: translateY(130%);
  transition: transform 1.1s var(--ease);
}

/* Per-word fade for the manifesto — words resolve as you scroll */
.wd { display: inline-block; }
.js .wd {
  opacity: 0.12;
  transition: opacity .5s var(--ease-soft);
}
.wd.is-lit { opacity: 1; }

[data-rv].is-in {
  opacity: 1;
  transform: none;
  clip-path: inset(0 0 0 0);
}
[data-rv="frame"].is-in > img,
[data-rv="frame"].is-in > video,
[data-rv="frame"].is-in > picture { transform: scale(1); }
.is-in .ln__i { transform: none; }

/* Stagger children by index — set --i in markup or JS */
[data-rv-stagger] > * { transition-delay: calc(var(--i, 0) * 90ms); }

/* --------------------------------------------------------------------------
   11. Forms
   -------------------------------------------------------------------------- */

.field { position: relative; display: block; }

.field input,
.field textarea,
.field select {
  width: 100%;
  padding: 1.15rem 0 0.85rem;
  background: transparent;
  border-bottom: 1px solid var(--hair);
  color: var(--bone);
  font-size: 0.95rem;
  transition: border-color var(--t-fast) var(--ease);
}
.field textarea { resize: vertical; min-height: 7rem; line-height: 1.7; }

.field select { cursor: pointer; padding-right: 1.5rem; }
.field select option { background: var(--ink-2); color: var(--bone); }

.field__label {
  display: block;
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  /* 34% of bone on near-black measures about 2.6:1 — under the 4.5:1 floor,
     and at 10px with a quarter-em of tracking it read as decoration rather
     than as the name of the field. Full bone is 17:1. */
  color: var(--bone);
  transition: color var(--t-fast) var(--ease);
}

/* The focus line is drawn on the control itself, not as a separate element.

   .field__bar used to do this: a span absolutely positioned at the bottom of
   the .field. But a .field is taller than the control inside it — the error
   message sits below — so the bar landed a few pixels under the input's own
   border and focus showed as two parallel gold lines with a gap between
   them. At 1px that read as a slightly odd shadow; at the 2px a focus
   indicator needs, it was plainly wrong.

   A background gradient sized from 0 to 100% draws the line exactly on the
   control's bottom edge, sweeps in from the left the way the bar did, and
   costs no layout shift. It is the same technique the buttons use for their
   fill. The spans are left in the markup doing nothing rather than stripped
   from five pages. */
.field__bar { display: none; }

.field input,
.field textarea,
.field select,
.sel__btn,
.dp__btn {
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 2px;
  transition: background-size .5s var(--ease),
              border-color var(--t-fast) var(--ease);
}

.field:focus-within input,
.field:focus-within textarea,
.field:focus-within select,
.field:focus-within .sel__btn,
.field:focus-within .dp__btn { background-size: 100% 2px; }

.field:focus-within .field__label { color: var(--accent); }

/* 16% of bone measures 1.4:1 — not dim, invisible. 58% is 6.1:1: plainly
   readable, and still clearly a prompt rather than an answer, because a
   value the visitor has typed is full bone. */
.field input::placeholder,
.field textarea::placeholder { color: var(--bone-2); }

/* With no colour available, an invalid field is marked by weight rather than
   hue — a doubled, full-brightness underline plus the message below it. */
.field.is-bad input,
.field.is-bad textarea {
  border-bottom-color: var(--bone);
  border-bottom-width: 2px;
}

.field__err {
  display: block;
  margin-top: .5rem;
  font-size: .6875rem;
  letter-spacing: .1em;
  color: var(--bone);
  min-height: 1em;
}

/* Honeypot — bots fill it, humans never see it */
.hp {
  position: absolute !important;
  left: -9999px !important;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* --------------------------------------------------------------------------
   12. Footer
   -------------------------------------------------------------------------- */

.foot { position: relative; border-top: 1px solid var(--hair-soft); }

.foot__grid {
  display: grid;
  grid-template-columns: 1.4fr 1fr 1fr;
  gap: clamp(2rem, 5vw, 4rem);
  padding-block: clamp(3.5rem, 7vh, 5.5rem);
}

.foot__col { display: grid; gap: 1rem; align-content: start; }
.foot__col a { color: var(--bone-2); transition: color var(--t-fast); font-size: .875rem; }
.foot__col a:hover { color: var(--bone); }

.foot__base {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 2rem;
  justify-content: space-between;
  align-items: center;
  padding-block: 1.6rem;
  border-top: 1px solid var(--hair-soft);
}

.socials { display: flex; gap: 1.15rem; align-items: center; }
.socials a {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border: 1px solid var(--hair);
  border-radius: 50%;
  color: var(--bone-2);
  transition: color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease),
              transform var(--t-fast) var(--ease);
}
.socials a:hover { color: var(--bone); border-color: var(--bone-4); transform: translateY(-2px); }
.socials svg { width: 15px; height: 15px; }

@media (max-width: 54rem) {
  .foot__grid { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------------------
   13. Reduced motion
   Strip transforms and masks, keep the design. Never ship motion that can
   make someone ill.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .grain { animation: none; opacity: .035; }
  [data-rv] { opacity: 1 !important; transform: none !important; clip-path: none !important; }
  [data-rv="frame"] > img,
  [data-rv="frame"] > video { transform: none !important; }
  .ln__i { transform: none !important; }
  .wd { opacity: 1 !important; }
}

/* Footer blurb: measured so it always falls to two lines, and balanced so
   those two lines come out the same length rather than one long, one short. */
/* Width is tuned to the sentence: `balance` splits it into two even lines,
   and the measure has to be wide enough that two is the natural count.

   Selector is `p.foot__blurb`, not `.foot__blurb`: the element also carries
   .body, and `p.body { text-wrap: pretty }` above outranks a lone class, so
   balance was being dropped and the line broke 426px / 186px. */
p.foot__blurb {
  font-size: 0.875rem;
  max-width: 28rem;
  text-wrap: balance;
}

/* No box on a field that is drawn as a line.

   These controls have no border except the rule under the text, so a 1px
   outline boxed them on focus and sat awkwardly across the .field__bar
   sweeping in underneath. The focus state is the underline instead: the rule
   brightens the instant focus lands, the bar draws across it in the brand
   gold, and the label above turns gold with it.

   This is a change of shape, not a removal. A focus indicator still has to
   be unmistakable: the bar is 2px across the full width of the control, and
   gold on near-black is roughly 12:1 — comfortably past the 3:1 that WCAG
   asks of a focus indicator. The border brightening is what carries the
   first frame, since the bar's sweep takes half a second and a keyboard user
   tabbing quickly should not have to wait for it. */
.field input:focus-visible,
.field textarea:focus-visible,
.field select:focus-visible,
.sel__btn:focus-visible,
.dp__btn:focus-visible {
  outline: none;
  border-bottom-color: var(--accent-dim);
}

/* --------------------------------------------------------------------------
   Custom select
   A native <select> draws its popup with the operating system, so CSS cannot
   restyle it — it dropped a bright system menu over the black page. The
   native element stays in the form and keeps submitting; this is the UI.
   -------------------------------------------------------------------------- */

.sel { position: relative; }

/* Present but invisible: still submits, no longer focusable or announced */
.sel__native {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}

.sel__btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.15rem 0 0.85rem;
  border-bottom: 1px solid var(--hair);
  color: var(--bone);
  font-size: 0.95rem;
  text-align: left;
  transition: border-color var(--t-fast) var(--ease);
}
.sel.is-empty .sel__btn { color: var(--bone-4); }
.sel__btn:hover { border-bottom-color: var(--bone-4); }

.sel__caret {
  width: 15px; height: 15px;
  flex: none;
  color: var(--bone-3);
  transition: transform var(--t-fast) var(--ease);
}
.sel.is-open .sel__caret { transform: rotate(180deg); }

.sel__panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 4px);
  left: 0; right: 0;
  list-style: none;
  background: var(--ink-3);
  border: 1px solid var(--hair);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .9);
  max-height: 16rem;
  overflow-y: auto;
}

.sel__opt {
  padding: 0.7rem 0.9rem;
  font-size: 0.875rem;
  color: var(--bone-2);
  cursor: pointer;
  transition: background var(--t-fast), color var(--t-fast);
}
.sel__opt:hover,
.sel__opt.is-active { background: rgba(237, 237, 235, .06); color: var(--bone); }
.sel__opt[aria-selected="true"] { color: var(--accent); }

/* --------------------------------------------------------------------------
   Custom date picker
   The native control draws its calendar with the operating system and puts a
   black glyph in the field, neither of which CSS can reach. The native input
   stays in the form and keeps the value; this is the interface.
   -------------------------------------------------------------------------- */

.dp { position: relative; }

.dp__native {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
/* Belt and braces: kill the OS glyph even if the input is ever shown */
input[type="date"]::-webkit-calendar-picker-indicator { display: none; }
input[type="date"]::-webkit-inner-spin-button { display: none; }

.dp__btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.15rem 0 0.85rem;
  border-bottom: 1px solid var(--hair);
  color: var(--bone);
  font-size: 0.95rem;
  text-align: left;
  transition: border-color var(--t-fast) var(--ease);
}
.dp.is-empty .dp__btn { color: var(--bone-4); }
.dp__btn:hover { border-bottom-color: var(--bone-4); }

.dp__icon { width: 16px; height: 16px; flex: none; color: var(--bone-3); }

.dp__panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 6px);
  left: 0;
  width: min(19rem, 84vw);
  padding: 1rem;
  background: var(--ink-3);
  border: 1px solid var(--hair);
  box-shadow: 0 22px 48px -20px rgba(0, 0, 0, .92);
}

.dp__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin-bottom: .9rem;
}
.dp__month {
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bone);
}
.dp__nav {
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border: 1px solid var(--hair);
  color: var(--bone-2);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.dp__nav:hover { color: var(--bone); border-color: var(--bone-4); }
.dp__nav svg { width: 13px; height: 13px; }

.dp__dow,
.dp__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }

.dp__dow {
  margin-bottom: .35rem;
}
.dp__dow span {
  text-align: center;
  font-size: .5625rem;
  font-weight: 600;
  letter-spacing: .12em;
  color: var(--bone-4);
  padding-block: .3rem;
}

.dp__day {
  aspect-ratio: 1;
  display: grid; place-items: center;
  font-size: .8125rem;
  color: var(--bone-2);
  font-variant-numeric: tabular-nums;
  transition: background var(--t-fast), color var(--t-fast);
}
.dp__day:hover { background: rgba(237, 237, 235, .08); color: var(--bone); }
.dp__day.is-today { color: var(--accent); }
.dp__day.is-sel { background: var(--accent); color: var(--ink); font-weight: 600; }
.dp__day:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }

/* Past, or already booked. Struck through rather than merely dimmed, so it
   reads as unavailable instead of as low contrast — and left un-hoverable so
   the cursor never suggests otherwise. */
.dp__day.is-off {
  color: var(--bone-4);
  cursor: not-allowed;
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}
.dp__day.is-off:hover { background: none; color: var(--bone-4); }

.dp__foot {
  margin-top: .8rem;
  padding-top: .7rem;
  border-top: 1px solid var(--hair-soft);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.dp__key {
  font-family: var(--mono);
  font-size: .5625rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--bone-4);
}
.dp__clear {
  font-size: .625rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--bone-3);
  transition: color var(--t-fast);
}
.dp__clear:hover { color: var(--bone); }
