/* ==========================================================================
   NETTORRA — base design system
   Brand: navy #0A1633 · cyan #47D6F6 · blue #2E5BEB · purple #7B3BE9
   ========================================================================== */

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

:root {
  /* Brand core */
  --navy-950: #050C1E;
  --navy-900: #070F27;
  --navy-800: #0A1633;
  --navy-700: #0E1D42;
  --navy-600: #152852;
  --navy-500: #1D3468;

  --cyan: #47D6F6;
  --blue: #2E5BEB;
  --purple: #7B3BE9;

  --white: #ffffff;
  --text: #E9EFFC;
  --text-soft: #B9C7E6;
  --text-dim: #8195BD;

  /* Signature gradient — used for the ribbon N, buttons, glows */
  --grad: linear-gradient(115deg, var(--cyan) 0%, var(--blue) 48%, var(--purple) 100%);
  --grad-soft: linear-gradient(115deg, rgba(71, 214, 246, .18) 0%, rgba(46, 91, 235, .16) 48%, rgba(123, 59, 233, .18) 100%);
  --grad-line: linear-gradient(90deg, transparent, var(--cyan), var(--blue), var(--purple), transparent);

  /* Glass */
  --glass-bg: rgba(255, 255, 255, .045);
  --glass-brd: rgba(255, 255, 255, .11);
  --glass-brd-lit: rgba(255, 255, 255, .2);
  --glass-blur: 22px;

  /* Type */
  --font-display: "Outfit", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Inter", "Helvetica Neue", Arial, sans-serif;
  --font-serif: "Cormorant Garamond", Georgia, "Times New Roman", serif;

  /* Rhythm */
  --shell: 1240px;
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --section-y: clamp(5rem, 11vw, 9.5rem);
  --radius-s: 10px;
  --radius: 18px;
  --radius-l: 28px;

  /* Motion */
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-io: cubic-bezier(.65, 0, .35, 1);

  /* Elevation */
  --shadow-lift: 0 24px 60px -20px rgba(0, 0, 0, .65);
  --shadow-glow: 0 0 0 1px rgba(71, 214, 246, .28), 0 22px 70px -24px rgba(46, 91, 235, .75);
}

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

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--navy-800);
  color: var(--text);
  font-family: var(--font-body);
  font-size: clamp(1rem, .96rem + .2vw, 1.0625rem);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

body.nav-open { overflow: hidden; }

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

img { height: auto; }

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

button,
input,
textarea,
select { font: inherit; color: inherit; }

button { background: none; border: 0; cursor: pointer; }

h1, h2, h3, h4, h5 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -.025em;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }

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

:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
  border-radius: 4px;
}

::selection {
  background: rgba(71, 214, 246, .3);
  color: var(--white);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: .8rem 1.2rem;
  background: var(--navy-700);
  border-radius: 0 0 var(--radius-s) 0;
}
.skip-link:focus { left: 0; }

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

/* --- 3. Layout --------------------------------------------------------- */

.shell {
  width: min(100% - (var(--gutter) * 2), var(--shell));
  margin-inline: auto;
}

.section { padding-block: var(--section-y); position: relative; }

.section-head { margin-bottom: clamp(2.5rem, 5vw, 4rem); }

/* --- 4. Typography helpers -------------------------------------------- */

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  font-family: var(--font-display);
  font-size: .74rem;
  font-weight: 600;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--cyan);
  margin-bottom: 1.15rem;
}
.eyebrow::before {
  content: "";
  width: 26px;
  height: 1px;
  background: var(--grad);
}
.h-display { font-size: clamp(2.55rem, 6.2vw, 4.6rem); font-weight: 600; }
.h1 { font-size: clamp(2.1rem, 4.6vw, 3.4rem); }
.h2 { font-size: clamp(1.75rem, 3.4vw, 2.6rem); }
.h3 { font-size: clamp(1.2rem, 1.9vw, 1.45rem); font-weight: 600; }

/* --- 5. Buttons -------------------------------------------------------- */

.btn {
  --btn-py: .95rem;
  --btn-px: 1.7rem;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  padding: var(--btn-py) var(--btn-px);
  font-family: var(--font-display);
  font-size: .96rem;
  font-weight: 600;
  letter-spacing: -.005em;
  border-radius: 100px;
  white-space: nowrap;
  isolation: isolate;
  transition: transform .35s var(--ease-out), box-shadow .35s var(--ease-out), color .25s, border-color .25s, background-color .25s;
}

.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn--primary {
  color: var(--white);
  background: var(--grad);
  background-size: 160% 160%;
  background-position: 0% 50%;
  box-shadow: 0 12px 34px -12px rgba(46, 91, 235, .85);
  transition: transform .35s var(--ease-out), box-shadow .35s var(--ease-out), background-position .6s var(--ease-out);
}
.btn--primary:hover {
  background-position: 100% 50%;
  box-shadow: 0 20px 46px -14px rgba(123, 59, 233, .9);
}

.btn--ghost {
  color: var(--text);
  border: 1px solid var(--glass-brd);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.btn--ghost:hover {
  border-color: var(--glass-brd-lit);
  background: rgba(255, 255, 255, .085);
}

.btn--sm { --btn-py: .68rem; --btn-px: 1.25rem; font-size: .88rem; }

.btn .arw { transition: transform .35s var(--ease-out); }
.btn:hover .arw { transform: translateX(4px); }

/* --- 6. Placeholder image blocks --------------------------------------
   Every image slot on the site renders as one of these until the final
   Higgsfield visual is dropped in. data-ph carries the art direction note.
   ---------------------------------------------------------------------- */

.ph {
  position: relative;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  border-radius: var(--radius);
  background:
    radial-gradient(120% 130% at 18% 12%, rgba(71, 214, 246, .3) 0%, transparent 55%),
    radial-gradient(110% 120% at 85% 25%, rgba(123, 59, 233, .34) 0%, transparent 58%),
    radial-gradient(140% 120% at 55% 100%, rgba(46, 91, 235, .32) 0%, transparent 62%),
    linear-gradient(160deg, var(--navy-700), var(--navy-950));
  isolation: isolate;
}

/* Fine grid texture so empty slots read as "designed", not broken */
.ph::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, .045) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .045) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: radial-gradient(90% 90% at 50% 50%, #000 20%, transparent 78%);
  mask-image: radial-gradient(90% 90% at 50% 50%, #000 20%, transparent 78%);
  pointer-events: none;
}

/* Slow sheen drift */
.ph::after {
  content: "";
  position: absolute;
  inset: -40%;
  background: conic-gradient(from 210deg at 50% 50%, transparent 0deg, rgba(255, 255, 255, .07) 60deg, transparent 130deg);
  animation: ph-sheen 14s linear infinite;
  pointer-events: none;
}

@keyframes ph-sheen { to { transform: rotate(360deg); } }

.ph__tag {
  position: relative;
  z-index: 2;
  margin: 1rem;
  padding: .45rem .8rem;
  font-family: var(--font-display);
  font-size: .68rem;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .8);
  background: rgba(5, 12, 30, .5);
  border: 1px solid rgba(255, 255, 255, .13);
  border-radius: 100px;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}

/* Final art. Drop an <img> inside any .ph and it takes over: the gradient
   stays behind as the loading colour, and the sheen and slot label switch off.
   Nothing else about the slot needs to change. */
.ph > img {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.ph:has(> img)::after { content: none; }
.ph:has(> img) > .ph__tag { display: none; }

/* Cut-out art — the product hero and the four exploded layers. The page draws
   the backdrop these sit on, so they arrive as transparent PNGs and have to be
   fitted whole rather than cropped to fill. */
.ph > img.is-contain { object-fit: contain; }

/* And once a cut-out has landed, the slot must stop drawing anything of its
   own. The gradient and grid are there to make an empty slot look deliberate,
   which is right behind a photograph that covers them — but a transparent PNG
   lets them straight through, and they resolve as a hard-edged coloured tile
   floating behind the product. The page already supplies the backdrop these
   sit on; the slot's job is finished the moment the art arrives. */
.ph:has(> img.is-contain) {
  background: none;
  overflow: visible;
}
.ph:has(> img.is-contain)::before { content: none; }

.ph--1x1  { aspect-ratio: 1 / 1; }

/* --- 7. Header --------------------------------------------------------- */

.hdr {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  transition: border-color .45s, padding .45s var(--ease-out);
  border-bottom: 1px solid transparent;
  padding-block: 1.15rem;
}

/* The stuck bar's tint and blur are painted by this pseudo-element rather than
   by .hdr itself, and that is load-bearing rather than tidiness.

   A backdrop-filter other than `none` makes the element a containing block for
   every position:fixed descendant it has. .hdr has exactly one — .hdr__nav,
   which under 980px is the slide-in drawer and is authored `inset: 0 0 0 auto`
   expecting to be measured against the viewport. With the filter on .hdr it was
   measured against the header instead, so the drawer stopped being 844px tall
   and became 128px: a stub showing three clipped links. It only did this once
   .is-stuck had landed, which is scrollY > 24 — so the menu worked at the very
   top of a page and was broken everywhere below it, which is most of the time.

   Moving the filter onto ::before keeps .hdr a plain fixed box. The pseudo sits
   at z-index -1, behind the header's own content but still inside the stacking
   context .hdr already creates at z-index 100, so it cannot fall behind the
   page. Cross-fading its opacity replaces the old background-color and
   backdrop-filter transitions, which is also cheaper to animate. */
.hdr::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: rgba(7, 15, 39, .78);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  opacity: 0;
  transition: opacity .45s var(--ease-out);
}
.hdr.is-stuck::before { opacity: 1; }

.hdr.is-stuck {
  border-bottom-color: rgba(255, 255, 255, .08);
  padding-block: .72rem;
}

.hdr__inner {
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.hdr__logo { flex-shrink: 0; display: block; }
.hdr__logo img {
  height: 38px;
  width: auto;
  transition: height .45s var(--ease-out);
}
.hdr.is-stuck .hdr__logo img { height: 33px; }

.hdr__nav {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: clamp(1.1rem, 2.2vw, 2.1rem);
}

.hdr__link {
  position: relative;
  font-family: var(--font-display);
  font-size: .95rem;
  font-weight: 500;
  color: var(--text-soft);
  padding-block: .35rem;
  transition: color .3s;
}
.hdr__link::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 2px;
  border-radius: 2px;
  background: var(--grad);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .4s var(--ease-out);
}
.hdr__link:hover { color: var(--white); }
.hdr__link:hover::after,
.hdr__link[aria-current="page"]::after { transform: scaleX(1); transform-origin: left; }
.hdr__link[aria-current="page"] { color: var(--white); }

.hdr__actions {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.hdr__phone {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: .93rem;
  color: var(--text);
  white-space: nowrap;
  transition: color .3s;
}
.hdr__phone:hover { color: var(--cyan); }
.hdr__phone svg { width: 15px; height: 15px; flex-shrink: 0; }

/* Mobile toggle */
.hdr__burger {
  display: none;
  width: 44px; height: 44px;
  margin-left: auto;
  place-items: center;
  border-radius: 12px;
  border: 1px solid var(--glass-brd);
  background: var(--glass-bg);
}
.hdr__burger span {
  display: block;
  width: 19px; height: 1.8px;
  border-radius: 2px;
  background: var(--text);
  transition: transform .38s var(--ease-out), opacity .25s;
}
.hdr__burger span + span { margin-top: 5px; }
body.nav-open .hdr__burger span:nth-child(1) { transform: translateY(6.8px) rotate(45deg); }
body.nav-open .hdr__burger span:nth-child(2) { opacity: 0; }
body.nav-open .hdr__burger span:nth-child(3) { transform: translateY(-6.8px) rotate(-45deg); }

@media (max-width: 980px) {
  .hdr__burger { display: grid; }

  .hdr__nav {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(340px, 86vw);
    margin: 0;
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: .35rem;
    padding: 5.5rem 2rem 2.5rem;
    background: rgba(7, 15, 39, .96);
    -webkit-backdrop-filter: blur(26px);
    backdrop-filter: blur(26px);
    border-left: 1px solid rgba(255, 255, 255, .08);
    transform: translateX(101%);
    /* Off-screen is not the same as gone: without visibility the five links
       stay in the tab order and keyboard focus vanishes off the right edge.
       Transitioning it keeps the slide-out visible — visibility holds its
       `visible` value for the whole duration when heading to hidden. */
    visibility: hidden;
    transition: transform .5s var(--ease-out), visibility .5s;
    overflow-y: auto;
  }
  body.nav-open .hdr__nav { transform: translateX(0); visibility: visible; }

  .hdr__link { font-size: 1.32rem; padding-block: .75rem; }
  .hdr__link::after { display: none; }

  .hdr__actions {
    flex-direction: column;
    align-items: stretch;
    gap: .85rem;
    margin-top: 1.75rem;
    padding-top: 1.75rem;
    border-top: 1px solid rgba(255, 255, 255, .1);
  }
  .hdr__phone { justify-content: center; font-size: 1.02rem; }
}

.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 99;
  background: rgba(5, 12, 30, .6);
  opacity: 0;
  visibility: hidden;
  transition: opacity .45s, visibility .45s;
}
body.nav-open .nav-scrim { opacity: 1; visibility: visible; }

/* --- 8. Footer --------------------------------------------------------- */

.ftr {
  position: relative;
  background: var(--navy-950);
  border-top: 1px solid rgba(255, 255, 255, .07);
  padding-block: clamp(3.5rem, 7vw, 5.5rem) 2rem;
}

.ftr__grid {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1.2fr;
  gap: clamp(2.25rem, 5vw, 4.5rem);
  align-items: start;
}

.ftr__logo img { height: 44px; width: auto; }

@media (max-width: 800px) {
  .ftr__grid { grid-template-columns: 1fr 1fr; }
  .ftr__grid > :first-child { grid-column: 1 / -1; }
}
@media (max-width: 520px) {
  .ftr__grid { grid-template-columns: 1fr; }
}

/* --- 9. Scroll reveal -------------------------------------------------- */

/* No will-change here, and its absence is the point.

   This rule used to carry `will-change: opacity, transform`, which reads as a
   free hint and is not one. will-change is a standing instruction: the element
   gets its own compositor layer and keeps it for the life of the page, whether
   or not anything is still moving. There are 31 of these on the homepage and 20
   on the Holt & Co. page — so the browser was being told to hold twenty to
   thirty full-width textures, at device resolution, permanently, for elements
   that animate exactly once for under a second and then never again.

   A desktop absorbs that. A phone does not: at 3x on a 390px screen a
   full-width block is a 1170px-wide texture, and a page's worth of them is well
   past what the compositor will keep resident. So it evicts and re-rasterises
   under memory pressure, which is visible as the page hitching as new content
   composites in — text arriving late or flickering as you scroll past the hero,
   which is precisely the report this came from.

   It also cost the film. The hero scrub steps down a resolution tier after six
   consecutive seeks slower than 150ms, and it measures the wait for the `seeked`
   event, so a compositor busy re-rasterising layers looks exactly like a decoder
   that cannot keep up. The phone was being talked down to the 540p cut by its
   own layer pressure, which is the other half of the same report: the film
   looking soft.

   Nothing is lost by dropping it. Browsers already promote an element for the
   duration of an opacity or transform transition and release the layer when the
   transition ends — which is the behaviour wanted here, arrived at by letting
   the browser manage the lifetime instead of overriding it with a hint that
   never expires. The handful of will-change declarations elsewhere in the CSS
   stay: those sit on one to four elements each and animate continuously while
   the page scrolls, which is what the property is for. */
[data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  transition:
    opacity .95s var(--ease-out),
    transform .95s var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
[data-reveal="left"]  { transform: translateX(-34px); }
[data-reveal="right"] { transform: translateX(34px); }
[data-reveal="scale"] { transform: scale(.955); }
[data-reveal="fade"]  { transform: none; }

[data-reveal].is-in {
  opacity: 1;
  transform: none;
}

/* --- 9b. Router page transition ---------------------------------------- */

#page {
  transition: opacity .24s var(--ease-out), transform .24s var(--ease-out);
}
body.is-leaving #page {
  opacity: 0;
  transform: translateY(10px);
}

/* Scenes settle at rest without JS; the engine drives them when it loads. */
[data-scene] { --p: 1; }
.js [data-scene] { --p: 0; }

/* --- 10. Misc utilities ------------------------------------------------ */

.center { text-align: center; }

/* --- 11. Reduced motion ------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }

  [data-reveal] { opacity: 1 !important; transform: none !important; }

  /* Parallax layers are driven by JS; it checks this query too. */
}

/* ==========================================================================
   12. Chrome — shared header and footer
   Re-skinned to the editorial navy direction: hairline rules, small mono and
   Space Grotesk labels, no gradients or glass. Deliberately last in the file
   so it overrides sections 7 and 8 on source order rather than by piling on
   specificity. Applies on every page — the chrome is Nettorra's, even when
   the page below it belongs to a demo brand.
   ========================================================================== */

.hdr,
.ftr {
  --chrome-display: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  --chrome-mono: "DM Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --chrome-line: rgba(174, 198, 248, .17);
}

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

.hdr {
  padding-block: 0;
  border-bottom: 1px solid rgba(255, 255, 255, .1);
  background: transparent;
}
/* Retints the pseudo-element from section 7. The blur has to stay off .hdr for
   the reason documented there — it would trap the mobile drawer. */
.hdr::before {
  background: rgba(11, 20, 39, .82);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
}
.hdr.is-stuck {
  padding-block: 0;
  border-bottom-color: rgba(255, 255, 255, .1);
}

/* Chrome rides the wide 1320 measure, not the 1240 default, so the logo lands
   directly above the hero headline and the footer closes on the same edge.
   Both carry .shell, so overriding its two inputs is enough. */
.hdr__inner, .ftr__grid { --shell: 1320px; --gutter: 24px; }

.hdr__inner { min-height: 96px; gap: 1.5rem; }
.hdr.is-stuck .hdr__inner { min-height: 74px; }

.hdr__logo img { height: 30px; }
.hdr.is-stuck .hdr__logo img { height: 27px; }

.hdr__nav { gap: clamp(1.25rem, 3vw, 3.1rem); }

.hdr__link {
  font-family: var(--chrome-display);
  font-size: .73rem;
  font-weight: 500;
  letter-spacing: 0;
  color: rgba(255, 255, 255, .78);
  padding-block: .3rem;
}
/* The gradient underline belongs to the old direction. Current page is marked
   by weight and colour instead, which is quieter at this size. */
.hdr__link::after { display: none; }
.hdr__link:hover,
.hdr__link[aria-current="page"] { color: #F2F5FF; }

.hdr__actions { gap: .9rem; }

.hdr__phone {
  font-family: var(--chrome-mono);
  font-size: .62rem;
  font-weight: 400;
  letter-spacing: .04em;
  color: rgba(255, 255, 255, .72);
}
.hdr__phone:hover { color: #27B8E8; }
.hdr__phone svg { width: 13px; height: 13px; }

/* The header CTA drops the gradient fill for an outline that inverts on
   hover — the same move the nav links make, one step louder. */
.hdr .btn--primary {
  min-height: 42px;
  padding: 0 1rem;
  border: 1px solid rgba(255, 255, 255, .46);
  border-radius: .5rem;
  background: none;
  box-shadow: none;
  font-family: var(--chrome-display);
  font-size: .72rem;
  font-weight: 500;
  color: #F2F5FF;
}
.hdr .btn--primary::before { display: none; }
.hdr .btn--primary:hover,
.hdr .btn--primary:focus-visible {
  transform: none;
  color: #0B1427;
  background: #F2F5FF;
  border-color: #F2F5FF;
  box-shadow: none;
}

.hdr__burger { border-radius: .35rem; border-color: rgba(255, 255, 255, .32); background: none; }

/* Two measures on desktop collapse to one on a phone — 16px, the same gutter
   the page content uses, so the logo lines up with the headline under it. */
@media (max-width: 760px) {
  .hdr__inner, .ftr__grid { --gutter: 16px; }
}

@media (max-width: 980px) {
  .hdr__inner { min-height: 76px; }
  .hdr.is-stuck .hdr__inner { min-height: 68px; }
  .hdr__nav { background: rgba(11, 20, 39, .96); }
  .hdr__link { font-size: 1.1rem; }
  .hdr__phone { font-size: .78rem; justify-content: center; }
  .hdr .btn--primary { min-height: 46px; font-size: .8rem; }
}

/* --- Footer ------------------------------------------------------------ */

/* Reduced to a single bar. Everything it used to repeat — nav, contact,
   socials — is one screen up in the contact section, and saying it twice was
   only ever filling space. */
.ftr {
  background: var(--ink, #0B1427);
  border-top: 1px solid var(--chrome-line);
  padding-block: 0;
}

.ftr__grid {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem 2rem;
  min-height: 100px;
  grid-template-columns: none;
}

.ftr__logo img { height: 26px; }

.ftr__meta {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  color: #ADB8D0;
  font-family: var(--chrome-mono);
  font-size: .58rem;
  letter-spacing: .05em;
  text-transform: uppercase;
}

.ftr__top {
  color: #ADB8D0;
  font-family: var(--chrome-display);
  font-size: .69rem;
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: rgba(255, 255, 255, .35);
  text-underline-offset: .25rem;
  transition: color .2s ease;
}
.ftr__top:hover { color: #F2F5FF; }

@media (max-width: 620px) {
  .ftr__grid { flex-wrap: wrap; justify-content: center; min-height: 0; padding-block: 2rem; text-align: center; }
  .ftr__meta { flex-basis: 100%; justify-content: center; gap: 1rem; }
}

/* ==========================================================================
   13. The reel — a scroll-driven film hero

   Shared by the homepage and both of the showcase pages that open on a
   flight. The scroll bar is the transport: the reader's thumb runs the
   camera, and the picture holds still the moment they stop. Then the film
   lands and the page opens out around it — the full-bleed frame contracts
   into a panel sitting on the site's own ground, and the copy arrives in the
   room the contraction just made.

   The whole thing runs off one number. data-scene="sticky" writes --p (0→1)
   across the section's scroll travel; js/site.js measures the same travel to
   pick the video frame. Two readings of one scroll position, so the picture
   and everything drawn around it cannot drift apart.

   --open-at is where the flight ends and the opening begins. It must match
   the film's data-film-end attribute, which is the same instant expressed to
   the other half of the machinery.
   ========================================================================== */

.reel {
  --open-at: .76;
  /* The reciprocal of what is left after --open-at, so the two are a pair and
     changing one means changing the other. Written as a multiply rather than a
     divide-by-custom-property, which is the one calc() form browsers have
     historically disagreed about — and this is the hero. */
  --open-rate: 4.167;
  /* 0 for the whole flight, then 0→1 across the dwell at the end. */
  --open: clamp(0, calc((var(--p, 0) - var(--open-at)) * var(--open-rate)), 1);

  /* How far the frame pulls in once it has landed. Declared here rather than
     hidden in a var() fallback so a page can both override it and read it back:
     content laid out inside the frame has to fit the frame at its smallest, and
     that is this much shorter than the pin. */
  --reel-inset-y: clamp(1rem, 3.4vh, 2.4rem);
  --reel-inset-x: clamp(1rem, 2.6vw, 2.6rem);

  position: relative;
  /* Travel, not height. 300svh of section is 200svh of scrolling, which is
     roughly two flicks of a thumb over an eight-second film — slow enough to
     read as a camera move rather than a scrub bar being dragged. */
  height: 300svh;
  background: var(--reel-ground, var(--navy-950));
}

/* No overflow clip here. The frame does its own clipping, and once it has
   contracted its shadow needs to spread past the inset — hidden on the pin
   would cut the lift off at 2rem and the panel would read as a hole again. */
.reel__pin {
  position: sticky;
  top: 0;
  height: 100svh;
}

/* The frame. Full bleed while the camera is moving; inset and rounded once it
   has landed. Insets are animated rather than a transform scale because the
   film has to keep filling the box exactly as the box changes shape —
   scaling it would shrink the picture and the frame together and reveal
   nothing. */
.reel__frame {
  position: absolute;
  inset: calc(var(--open) * var(--reel-inset-y))
         calc(var(--open) * var(--reel-inset-x));
  overflow: hidden;
  /* The frame is a composed picture, so it stacks as one. Without this a page
     that layers its hero with negative z-index — the homepage does, to get the
     artwork behind its washes — would drop those layers through the frame and
     out the back of the section's own ground. */
  isolation: isolate;
  border-radius: calc(var(--open) * var(--radius-l));
  /* Lifts off the ground as it contracts, so the panel reads as sitting on
     the page rather than as a hole cut in it. */
  box-shadow: 0 calc(var(--open) * 40px) calc(var(--open) * 90px)
              calc(var(--open) * -30px) rgba(0, 0, 0, calc(var(--open) * .8));
}

.reel__film,
.reel__still {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* One geometry, shared. The still and the film are the same picture and they
   have to land in the same place, or the hand-off between them is a jump.

   This used to read `.reel__still { object-fit: cover }` against the film's
   `contain`, and the comment defended it: a still is a photograph, you can take
   the top and bottom off it, so let it fill the frame. That is a fine argument
   about photographs and the wrong argument about this element. The still is not
   decoration underneath the film — it is the film's first frame, standing in
   until the file arrives. It is on screen for the second or so before `is-ready`
   lands and never again.

   So the only thing it has to do in that second is be indistinguishable from
   the frame that replaces it. Measured on the Lantern House hero at 1512x900,
   `cover` painted the still 1512x1129 against the film's 1512x851: a fifth of
   the picture cropped away, and then the whole thing visibly shrank into
   letterbox as the film faded up. Same picture, two geometries, one very
   obvious glitch on every page load.

   The ground is what fills the bands. Without it they would be a sliver of
   whatever is behind, sitting still while the film moves through it, which
   reads as a seam. Solid ground reads as a frame. */
.reel__film,
.reel__still {
  /* contain, at every window shape, with no exceptions.

     There used to be a `cover` default here and a `@media (min-aspect-ratio:
     7/5)` that switched to contain only on wide windows. The reasoning was that
     letterboxing is cheap on a landscape window and ruinous on a phone, so
     narrow windows should take the crop instead. The reasoning was wrong, and it
     was wrong in a way that measuring the wrong thing kept hiding.

     What the crop actually cost, measured:

       window        shape   fit      width of the frame you could see
       1440x900      1.60    contain  100%
       1280x1000     1.28    cover     71%
       390x844       0.46    cover     26%

     Twenty-six per cent. Three quarters of every flight thrown away on a phone,
     and a quarter thrown away on any desktop window that is not especially wide
     — and 7/5 is a high bar, so an ordinary un-maximised browser window fell the
     wrong side of it. This is what "too zoomed in" meant, on every page at once.

     The bands the old comment worried about are real, and they are the cheaper
     problem. They are also not bare: the element carries the section's own
     ground, so what sits above and below the picture is the same colour as the
     page around it and reads as a frame rather than as a video that failed. A
     thin picture you can see all of beats a tall one you can see a quarter of.

     The lesson, since it cost two rounds: `cover` reports a perfectly full box.
     Any check that asks "does the picture fill its box" will pass while the
     picture is being destroyed. Ask what fraction of the SOURCE reaches the
     screen, and clip that to the viewport, not to the box. */
  object-fit: contain;
  background: var(--reel-ground, var(--navy-950));
}

/* The still is the hero until the film can prove it is running. It stays in
   the markup underneath as the real fallback — no autoplay, no file, reduced
   motion, a codec the browser will not take — so the hero is a finished thing
   in every one of those cases and the film is the enhancement.

   is-ready is set by the scrub engine once it knows the duration, which is
   the first moment the film can honestly claim to be scrubbable. */
.reel__film { opacity: 0; transition: opacity .6s var(--ease-out); }
.reel__film.is-ready { opacity: 1; }

/* Two ramps. The top only has to keep the fixed header legible, so it stays
   light or it reads as a black bar; the bottom carries the copy and has to
   hold against whatever the flight lands on. */
.reel__scrim {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(3, 6, 16, .68) 0%, rgba(3, 6, 16, .2) 20%, transparent 38%),
    linear-gradient(0deg,   rgba(3, 6, 16, .92) 0%, rgba(3, 6, 16, .66) 30%,
                            rgba(3, 6, 16, .24) 58%, transparent 80%);
}

/* The block itself is always here. What it holds back is inside .reel__open. */
.reel__body {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0 clamp(1.6rem, 5vw, 4rem) clamp(2rem, 6vh, 4rem);
}

/* Held back for the whole flight. A headline read over a moving camera is a
   headline nobody reads, and copy that arrives with the landing reads as the
   flight delivering the page rather than the page waiting its turn.

   One line is exempt, and it sits outside this wrapper: the standing brand
   line. A film with no name on it is a screensaver — the reader needs to know
   whose house they are being flown around while they are being flown around it,
   and small caps at the foot of the frame say that without competing with the
   shot. It is laid out in its final position from the first frame, so the
   landing adds and never shifts: the headline fades in above a line that has
   not moved. Holding it back with everything else would leave the hero
   anonymous for its whole first screen, which is a worse trade than the two
   seconds of quiet it buys. */
.reel__open {
  opacity: var(--open);
  transform: translateY(calc((1 - var(--open)) * 26px));
}

/* The one instruction the hero needs, and only while it is still true. */
.reel__cue {
  position: absolute;
  z-index: 2;
  /* Pinned to both edges and centred by the flex box, not by left:50% and a
     translate. Anchored at the midpoint it could only ever be half the frame
     wide, so on a phone the line wrapped and the two rules ended up flanking a
     two-line block. */
  left: 0;
  right: 0;
  bottom: clamp(1.4rem, 4vh, 2.4rem);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .7rem;
  color: rgba(255, 255, 255, .72);
  font-family: var(--font-display);
  font-size: .64rem;
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  /* Gone by the time the camera has travelled a fifth of the way — by then
     the reader has worked out that scrolling drives it. */
  opacity: clamp(0, calc(1 - var(--p, 0) * 5), 1);
  pointer-events: none;
}
.reel__cue::before,
.reel__cue::after {
  content: "";
  width: clamp(1.6rem, 5vw, 3.4rem);
  height: 1px;
  background: currentColor;
  opacity: .5;
}

/* A hairline that draws itself along the frame as the film runs — the only
   readout of how much flight is left. Sits inside the frame so it contracts
   with it, and is gone by the time the page has opened. */
.reel__bar {
  position: absolute;
  z-index: 2;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background: rgba(255, 255, 255, .14);
  opacity: calc(1 - var(--open));
}
.reel__bar::after {
  content: "";
  display: block;
  height: 100%;
  background: var(--reel-accent, var(--cyan));
  transform: scaleX(var(--p, 0));
  transform-origin: 0 50%;
}

@media (max-width: 860px) {
  /* Less travel on a phone: the same film, but two hundred vh of scrolling on
     a short screen is a long time to spend before the page says anything. */
  .reel { height: 230svh; }
}

/* Tall windows: the band flies centred, then composes as it lands.

   A 16:9 film across a 390px phone is 219px tall and cannot be any taller —
   not without cropping it, which is the thing that broke this hero three times.
   So the picture's size is settled and the only question left is where in the
   other 625px it sits, and when.

   Both answers, because they are different questions at different moments.
   During the flight there is no copy — that is the whole point of the flight —
   so the band belongs in the middle of the screen, which is where a film goes
   when a film is all there is. Once the camera stops and the page opens, the
   band lifts under the header and the copy takes the floor it just left.

   This used to be one answer. The band was pulled up from the first paint and
   --open was pinned at 1 to bring the copy up with it, on the reasoning that a
   held-back hero on a phone is 83% flat navy with a ribbon floating in it and
   reads as a page that failed to load. That reasoning was sound and it is what
   the centred band above fixes: centred and full strength with the cue under it,
   the flight looks like a flight, which is exactly how the two showcase heroes
   that never took this override have always looked on a phone.

   Guarded on shape, not width, because this is about the window being taller
   than it is wide. A landscape phone already fits the film nearly edge to edge
   and wants none of this. */
@media (max-aspect-ratio: 1/1) {
  /* object-position, not a shorter box.

     The obvious way to lift the band is to stop the element being full-bleed —
     give it `top: 13svh; height: auto; aspect-ratio: 16/9`. That works for one
     frame and then shows you the still, because the still is a sibling
     underneath and what had been hiding it was the film element's own opaque
     background covering the whole frame. Shrink the element and two strips of a
     different picture appear above and below the film, sitting still while it
     moves.

     Keeping the element full-bleed and moving the picture inside it costs
     nothing and keeps the ground where it needs to be.

     The still takes the same offset, and has to. It used to be excluded here —
     the note said so, on the grounds that a film which never arrives should
     leave a full-bleed photograph behind it. That was true when the still was a
     different photograph; it is not true now that it is the film's own opening
     frame. Give the two different offsets and the picture jumps up the screen
     the moment the film fades in, which on a phone is the most visible version
     of the glitch this pair exists to avoid.

     The composed offset is a length, not a percentage. A percentage there is a
     share of the leftover space, so the gap under the header would grow and
     shrink with the phone — and the first pass at this used 26%, which on an
     844pt screen dropped the band into the middle of the headline. A length
     holds the picture a fixed distance below the header on every handset.

     The flight offset is a percentage, and has to be: 50% of the leftover space
     is the definition of centred, at any frame size, which matters because the
     frame contracts by --reel-inset-y as it opens and the leftover space
     contracts with it. Interpolating between the two on --open means the band
     walks up out of the centre exactly as the copy walks in underneath — one
     number driving both, so they cannot arrive at different times.

     At --open 0 the second term is zero and this is a plain `50% 50%`; at 1 the
     first term is `0%`, which is zero at any size, and this is the composed
     length it always was. If --open is ever unresolvable the whole declaration
     is dropped and object-position falls back to its initial `50% 50%` — the
     centred flight, which is the safe half of the pair to be stuck in. */
  .reel__film,
  .reel__still {
    object-position:
      50% calc(50% * (1 - var(--open)) + clamp(4.5rem, 9svh, 7rem) * var(--open));
  }
}

@media (prefers-reduced-motion: reduce) {
  /* No flight, so no travel to make room for — the section collapses to the
     one screen it would have landed on. The engine parks --p at 1, which puts
     --open at 1, so what is left is the opened state: a framed still with the
     copy in place. That is a finished hero, not a fallback that looks like one. */
  .reel { height: auto; }
  .reel__pin { position: static; height: auto; min-height: min(86svh, 720px); }
  .reel__film,
  .reel__cue,
  .reel__bar { display: none; }
  .reel__open { opacity: 1; transform: none; }
}

/* The still's own half of that restatement, and it needs its own query because
   the offset it is restating only exists on a portrait window — asserted flat it
   would push the picture down on a reduced-motion desktop, where the frame is
   the film and centred is correct.

   Same argument as .reel__open above: --open does resolve to 1 here once the
   engine parks --p, and a hero should not need that to have happened to put its
   picture where its copy was laid out against. Centred with the copy on is the
   headline standing on the film, which is the one state this hero is not
   allowed to be in. */
@media (prefers-reduced-motion: reduce) and (max-aspect-ratio: 1/1) {
  .reel__still { object-position: 50% clamp(4.5rem, 9svh, 7rem); }
}
