/* TENON — scroll film layout.
 *
 * tokens.css owns colour, family, scale, weight. This file only arranges.
 * There is not a single literal colour value below: every surface resolves
 * through a custom property so light and dark are both correct by construction.
 *
 * Mask gradients use rgb(0 0 0 / n) purely as an ALPHA channel — a mask reads
 * only alpha, never hue — so no palette colour is being introduced there.
 */

/* ---------- Reset ---------- */

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

/* No scrollbar. It is the one route to a resting position between two beats
 * that gesture handling cannot intercept, and this reads as a film rather than
 * a document. It also has to be in force before the first layoutGlass(), since
 * removing it changes the layout width by ~15px on Windows. */
html {
  -webkit-text-size-adjust: 100%;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

html::-webkit-scrollbar,
body::-webkit-scrollbar { width: 0; height: 0; display: none; }

body {
  margin: 0;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, canvas { display: block; max-width: 100%; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

ul { margin: 0; padding: 0; list-style: none; }

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

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

/* ---------- Layer order ----------
 * 1 canvas · 5 stats overlay · 6 sections · 8 hero
 * 10 header · 100 loader
 * The glass is not a layer of its own: its seven sub-layers sit inside each
 * section, painted before that section's own copy so the copy stays on top.
 */

/* ---------- Loader ---------- */

/* Pinned, not themed. The loader cross-fades straight into the hero, so if it
 * followed the theme while the hero did not, light mode would dissolve a white
 * panel into a black one at the worst possible moment. They are one surface. */
#loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--hero-ground);
  display: grid;
  place-items: center;
  transition: opacity 700ms ease, visibility 700ms ease;
}

#loader.is-done { opacity: 0; visibility: hidden; }

.loader-inner {
  display: grid;
  justify-items: center;
  gap: 20px;
  width: min(300px, 70vw);
}

.loader-brand {
  font-size: var(--size-caption);
  color: var(--hero-ink);
}

#loader-bar {
  width: 100%;
  height: 1px;
  background: var(--hero-line);
  overflow: hidden;
}

#loader-fill {
  display: block;
  width: 0%;
  height: 100%;
  background: var(--hero-ink);
  transition: width 220ms ease;
}

#loader-percent {
  font-size: var(--size-caption);
  color: var(--hero-ink-soft);
}

#loader-percent::after { content: "%"; }

/* ---------- Header ---------- */

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 26px 5vw;
}

/* The 0.18em tracking is load-bearing — do not let the nav squeeze it. */
.site-header .wordmark {
  font-size: var(--size-caption);
  color: var(--hero-ink);
  text-decoration: none;
  white-space: nowrap;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 26px;
}

.nav-link {
  font-family: var(--font-body);
  font-size: var(--size-caption);
  font-weight: var(--weight-label);
  color: var(--hero-ink);
  opacity: 0.72;
  padding: 4px 0;
  position: relative;
  transition: opacity 260ms ease;
}

.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 340ms cubic-bezier(0.4, 0, 0.2, 1);
}

.nav-link:hover { opacity: 1; }
.nav-link:hover::after { transform: scaleX(1); }

.nav-link-accent { opacity: 1; }
.nav-link-accent::after { transform: scaleX(1); }

.theme-toggle {
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border: 1px solid var(--hero-line);
  border-radius: 50%;
}

.theme-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--hero-ink);
  transition: transform 300ms ease;
}

.theme-toggle:hover .theme-dot { transform: scale(1.25); }

/* The header is the only chrome that crosses two surfaces, and BOTH of them are
 * now pinned: the dark opening above, then the bright film here. So it switches
 * between two fixed palettes and never consults the theme at all — which is what
 * it has to do, since the theme can no longer tell it anything about either
 * ground it is actually sitting on. The declarations above carry the opening's
 * ink; these take over once js/app.js sets data-over-film, which it does as a
 * pure function of scroll progress once the hero has faded. */
:root[data-over-film] .site-header .wordmark,
:root[data-over-film] .nav-link { color: var(--film-ink); }

:root[data-over-film] .theme-toggle { border-color: var(--film-line); }
:root[data-over-film] .theme-dot { background: var(--film-ink); }

/* ---------- Hero ---------- */

.hero-standalone {
  position: fixed;
  inset: 0;
  z-index: 8;
  background: var(--hero-ground);
  color: var(--hero-ink);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 104px 5vw 120px;
  will-change: opacity;
}

.hero-inner { max-width: min(94vw, 1240px); }

/* .section-label is --film-ink-soft, which is near-black by design: it was
 * written for the copy that sits on the bright film. On the pinned-dark hero
 * that is near-black on near-black, so the label needs the opening's own ink. */
.hero-standalone .section-label { color: var(--hero-ink-soft); }

.hero-heading {
  font-size: var(--size-hero);
  margin: 18px 0 0;
}

/* Word-split: each word masked by its own overflow box so it can rise into place. */
.word {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  padding-bottom: 0.06em;
  margin-right: 0.22em;
}

.word:last-child { margin-right: 0; }

.word > span {
  display: inline-block;
  will-change: transform;
}

.hero-tagline {
  margin: 26px 0 0;
  color: var(--hero-ink-soft);
  max-width: 34ch;
}

.scroll-cue {
  position: absolute;
  left: 5vw;
  bottom: 40px;
  display: flex;
  align-items: center;
  gap: 14px;
}

.scroll-cue-label {
  font-size: var(--size-caption);
  font-weight: var(--weight-label);
  color: var(--hero-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.scroll-cue-line {
  display: block;
  width: 64px;
  height: 1px;
  background: var(--hero-line);
  position: relative;
  overflow: hidden;
}

.scroll-cue-line::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--hero-ink);
  transform-origin: left;
  animation: cue 2.4s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

@keyframes cue {
  0%   { transform: scaleX(0); transform-origin: left; }
  45%  { transform: scaleX(1); transform-origin: left; }
  55%  { transform: scaleX(1); transform-origin: right; }
  100% { transform: scaleX(0); transform-origin: right; }
}

/* ---------- Film + glass ---------- */

.canvas-wrap {
  position: fixed;
  inset: 0;
  z-index: 1;
  /* Behind the film, and only visible if a frame has not painted yet — so it is
   * the opening's ground rather than the theme's, same as body. */
  background: var(--hero-ground);
  clip-path: circle(0% at 50% 50%);
  will-change: clip-path;
}

.canvas-wrap canvas {
  width: 100%;
  height: 100%;
}

/* The glass — progressive blur that follows the type.
 *
 * backdrop-filter samples whatever is actually behind the element, which
 * here is the film canvas — so the backdrop IS the colour of the video
 * itself rather than a tint standing in for it.
 *
 * WHY SEVEN LAYERS AND NOT ONE.
 * A single uniformly-blurred layer faded out with an alpha mask does not fade
 * its blur: inside the ramp you see a semi-transparent blurred copy sitting on
 * top of the sharp film, and that double image reads as a hard ring wherever
 * the footage has contrast. So the blur is built up instead. Each layer's
 * backdrop-filter samples everything painted below it — including the layers
 * before it — so the radius ACCUMULATES toward the type:
 *
 *     2 · 4 · 7 · 10 · 12 · 14 px
 *   ------
 *   ~23px effective on the type  (radii add in quadrature), ramping
 *         continuously to a genuine 0px by about 76px out.
 *
 * Six steps and not seven: seven measured a 20.8ms median frame during a scrub,
 * which is 48fps. Six lands back under 16.7ms for the same effective radius —
 * the ladder just takes slightly larger strides.
 *
 * THE RULE THAT KEEPS IT SMOOTH: every layer's feather is at least twice its
 * own blur radius, so each fade only ever dissolves a blur too weak to ghost.
 * The last column below is there to be checked.
 *
 * WHY THE SHAPE IS NOT A BOX.
 * The mask is an SVG built per section by layoutGlass(): one rounded pill per
 * LINE of text, unioned and then Gaussian-blurred. So the outline steps in and
 * out with the type — a short line gets a short pool — and there is no corner
 * and no straight edge anywhere in it. A Gaussian is also a smoother alpha ramp
 * than any hand-written gradient stop list.
 *
 * Underneath the pills sits the same set dilated much further at --halo-alpha,
 * which is what keeps the space between the lines and past a ragged line ending
 * from reading as empty: blurred there too, just less.
 *
 * They are SIBLINGS of the copy, not children. backdrop-filter is cancelled by
 * a transform, opacity or filter on any ancestor, and the copy block carries
 * the entrance transform — nesting made the blur silently do nothing. Measured:
 * an untransformed parent blurs, the element's own transform is harmless. So
 * every layer gets the same tween as the copy and the whole stack moves as one.
 */
.glass-layer,
.glass-tint {
  position: absolute;
  pointer-events: none;

  /* mask-image is written by layoutGlass() — it has to know where the lines
   * are, and only the DOM knows that. Everything else about the shape lives
   * here, in the custom properties below. */
  /* The mask SVG carries no fill colour — a mask reads alpha, and pinning the
   * mode here means that is true by declaration rather than by inference. */
  -webkit-mask-source-type: alpha;
          mask-mode: alpha;

  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* --core        how far the full-strength pill extends past a line of type.
 * --feather     stdDeviation of the Gaussian that softens it. The visible ramp
 *               is about 3x this, and that ramp is what must stay at or above
 *               twice the blur radius.
 * --halo        dilation of the weaker set underneath, which fills between the
 *               lines and past a short line's ending.
 * --halo-alpha  how much of this layer's blur survives out there.
 */
.glass-layer,
.glass-tint { --halo: 48px; --halo-alpha: 0.42; }

/*                              blur   core     feather   ramp / radius */
.glass-layer[data-glass="1"] { --core: 16px; --feather: 20px;   /*  2px  30.0x */
  -webkit-backdrop-filter: blur(2px);  backdrop-filter: blur(2px);
}
.glass-layer[data-glass="2"] { --core: 15px; --feather: 17px;   /*  4px  12.8x */
  -webkit-backdrop-filter: blur(4px);  backdrop-filter: blur(4px);
}
.glass-layer[data-glass="3"] { --core: 14px; --feather: 14px;   /*  7px   6.0x */
  -webkit-backdrop-filter: blur(7px);  backdrop-filter: blur(7px);
}
.glass-layer[data-glass="4"] { --core: 12px; --feather: 12px;   /* 10px   3.6x */
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}
.glass-layer[data-glass="5"] { --core: 11px; --feather: 11px;   /* 12px   2.8x */
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
.glass-layer[data-glass="6"] { --core: 10px; --feather: 10px;   /* 14px   2.1x */
  -webkit-backdrop-filter: blur(14px) saturate(1.15);
          backdrop-filter: blur(14px) saturate(1.15);
}

/* The wash.
 *
 * Deliberately weak and deliberately enormous, and those two go together.
 *
 * A translucent wash lifts a flat wall by a fixed number of levels no matter how
 * smooth its edge is, and the eye finds that step on a flat field easily: an
 * earlier attempt spent 26 levels across 40px and drew a plainly visible line
 * down the middle of the frame. Spread over a feather this wide the same 26
 * levels come out at roughly 0.2 levels per pixel, under the threshold where a
 * line becomes findable.
 *
 * Its job is the contrast floor over the sofa, which the copy unavoidably
 * overlaps — on the promise beat the copy block is 415px tall and the clean wall
 * above the sofa is about 400px. */
.glass-tint {
  --core: 16px;
  --feather: 55px;
  --halo: 90px;
  --halo-alpha: 0.55;
  background: var(--film-glass);
}

#dark-overlay {
  position: fixed;
  inset: 0;
  z-index: 5;
  opacity: 0;
  pointer-events: none;
  background: var(--scrim-stats);
  will-change: opacity;
}

/* ---------- Scroll container + sections ---------- */

/* overflow-x: clip because the glass is deliberately much wider than its text.
 * .glass-tint pads out by max(--core, --halo) + --feather * 3 = 255px on every
 * side, and nothing else on the page clips it, so its right edge was landing in
 * documentElement.scrollWidth: 727px at a 390px viewport, 1885 at 1600. Chrome
 * then shrink-to-fits a phone to 54%, at which point window.innerHeight reports
 * the scaled visual viewport (1574) while 100vh and ScrollTrigger keep using the
 * layout viewport (844) — and every segment stop lands 17% short of its beat.
 *
 * clip, not hidden: hidden would make this a scroll container of its own, which
 * the absolutely-positioned sections and the snap controller would then have to
 * agree with. clip only clips, and everything it removes is off the side of the
 * screen already, so the Gaussian tail is never cut where it can be seen.
 * Unlike transform/opacity/filter it does not become a backdrop root, so the
 * blur below still samples the film — see the note under this rule. */
#scroll-container {
  position: relative;
  z-index: 6;
  height: 1000vh;
  overflow-x: clip;
  overflow-y: visible;
}

/* Deliberately no transform and no opacity on this element.
 *
 * The glass inside it uses backdrop-filter, and a transform, opacity, filter or
 * will-change on ANY ancestor turns that ancestor into the backdrop root — the
 * blur would then sample an empty box instead of the film and silently do
 * nothing. So the half-viewport offset is folded into `top` by positionSections()
 * rather than done with translateY, and visibility does the hiding that opacity
 * would normally do. The fade lives on the two children instead. */
.scroll-section {
  position: absolute;
  left: 0;
  right: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  visibility: hidden;
  pointer-events: none;
}

.scroll-section.is-shown { visibility: visible; }
.scroll-section.is-active { pointer-events: auto; }

/* Text lives in the outer zones; the sofa runs along the lower centre-right of
 * every frame, so the padding box is biased upward to clear it. Biasing with
 * padding rather than a margin means tall content (the CTA) still centres
 * inside the box instead of overflowing off-screen. */
.align-left  { justify-content: flex-start; padding: 104px 50vw 19vh 5vw; }
.align-right { justify-content: flex-end;   padding: 104px 5vw 19vh 50vw; }

.section-inner {
  position: relative;
  max-width: 44vw;
}

.align-right .section-inner { text-align: right; }

.section-label {
  display: block;
  font-family: var(--font-body);
  font-size: var(--size-caption);
  font-weight: var(--weight-label);
  color: var(--film-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  margin-bottom: 22px;
}

.section-heading {
  font-size: var(--size-beat);
  margin: 0;
  color: var(--film-ink);
}

.section-body {
  font-size: var(--size-lead);
  color: var(--film-ink);
  max-width: 44ch;
  margin: 26px 0 0;
}

.align-right .section-body,
.align-right .spec-list { margin-left: auto; }

.section-note {
  font-family: var(--font-body);
  font-size: var(--size-caption);
  color: var(--film-ink-soft);
  margin: 22px 0 0;
  letter-spacing: 0.04em;
}

.spec-list {
  margin: 28px 0 0;
  max-width: 36ch;
  display: grid;
  gap: 12px;
}

.spec-list li {
  font-size: var(--size-body);
  color: var(--film-ink);
  padding-top: 12px;
  border-top: 1px solid var(--film-line);
}

/* ---------- Stats ----------
 * The one place centred text is right: a full-bleed overlay owns the viewport.
 */

/* The one beat that keeps the theme palette — a full-bleed overlay covers the
 * film here, so it is a page surface rather than a film surface. */
.section-stats .section-label,
.section-stats .stat-label { color: var(--muted); }

.section-stats {
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 0 5vw;
}

.stats-head { margin-bottom: 54px; }
.stats-head .section-label { margin-bottom: 0; }

.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 40px 4vw;
  width: min(1100px, 90vw);
}

.stat { display: grid; justify-items: center; gap: 12px; }

.stat-value-row {
  display: flex;
  align-items: baseline;
  gap: 4px;
}

/* Inter, deliberately. These figures animate up from zero, and tabular figures
 * are the whole reason they do not jitter while doing it — the display face
 * carries no tabular set. */
.stat-number {
  font-family: var(--font-body);
  font-weight: var(--weight-label);
  font-size: clamp(48px, 5.4vw, 92px);
  line-height: 1;
  color: var(--text);
  letter-spacing: -0.02em;
}

.stat-suffix {
  font-family: var(--font-body);
  font-size: var(--size-lead);
  color: var(--muted);
}

.stat-label {
  font-family: var(--font-body);
  font-size: var(--size-caption);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

/* ---------- Origin + CTA ---------- */

.origin-copy {
  font-size: var(--size-lead);
  max-width: 44ch;
}

.cta-price {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin: 30px 0 0;
  flex-wrap: wrap;
}

.price-from {
  font-size: var(--size-caption);
  color: var(--film-ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.price {
  font-family: var(--font-body);
  font-weight: var(--weight-label);
  font-size: var(--size-h2);
  color: var(--film-ink);
}

.price-lead {
  font-size: var(--size-caption);
  color: var(--film-ink-soft);
}

.cta-button {
  display: inline-block;
  margin-top: 34px;
  padding: 16px 34px;
  background: var(--film-accent);
  color: var(--film-on-accent);
  text-decoration: none;
  font-family: var(--font-body);
  font-weight: var(--weight-label);
  font-size: var(--size-body);
  letter-spacing: 0.03em;
  position: relative;
  overflow: hidden;
  transition: transform 320ms cubic-bezier(0.4, 0, 0.2, 1);
}

.cta-button::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--film-ink);
  opacity: 0;
  transition: opacity 320ms ease;
}

.cta-button:hover { transform: translateY(-2px); }
.cta-button:hover::after { opacity: 0.12; }

/* ---------- Mobile ----------
 * Side zones collapse. The sofa fills the lower half of a portrait crop, so the
 * pool becomes a top-anchored band rather than a left/right one.
 */

@media (max-width: 860px) {
  #scroll-container { height: 620vh; }

  .align-left, .align-right {
    justify-content: center;
    padding: 92px 7vw 22vh;
  }

  .section-inner,
  .align-right .section-inner {
    max-width: none;
    width: 100%;
    text-align: left;
  }

  .align-right .section-body,
  .align-right .spec-list { margin-left: 0; }

  .section-body { max-width: none; }

  /* Portrait needs no mask override. The pool is built from the line boxes,
   * and a phone's lines run nearly the full width of the screen, so the same
   * per-line shape produces the full-width band this used to hand-build —
   * without the straight top and bottom edges it used to have. */

  .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 36px 6vw; }

  .site-header { padding: 18px 7vw; }
  .site-nav { gap: 16px; }
  .nav-link:not(.nav-link-accent) { display: none; }

  .hero-inner { max-width: none; }
  .hero-tagline { max-width: none; }
}

@media (max-width: 520px) {
  .stats-grid { grid-template-columns: 1fr; }
}

/* ---------- Reduced motion ----------
 * The frame scrub stays: it is the content, not decoration. Everything that is
 * decoration stops.
 */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }

  .scroll-cue-line::after { animation: none; transform: scaleX(1); }
  .canvas-wrap { clip-path: none !important; }
}
