/* ============================================================
   MORELLO — styles.css
   Blocks: TOKENS · BASE · LAYOUT · SECTIONS · MOTION · DESKTOP
   Spec: docs/superpowers/specs/2026-08-05-morello-landing-page-design.md
   ============================================================ */

/* ---------- TOKENS ---------------------------------------- */
:root {
  --ink:      #1B0A0F;   /* page ground, type on cream, scrims */
  --cream:    #F4EBDF;   /* type on dark */
  --cherry:   #8E1B32;   /* accent only — rules, numbers */
  --red:      #5B0D1B;   /* intro curtain */
  --sand:     #DAC0B1;   /* intro word colour */
  --stop:     #C0405C;   /* the red full stop */

  /* De-emphasis is a COLOUR, never an opacity — opacity belongs to the
     motion system, and .reveal.is-in (0,3,0) would override it. */
  --muted:  #928681;   /* eyebrows, labels, numbers — 5.42:1 on --ink */
  --soft:   #B7ACA5;   /* body copy — 8.62:1 on --ink */

  /* Channel tokens so scrims and hairlines in later tasks can use
     rgb(var(--ink-rgb) / .78) instead of hardcoding the palette. */
  --ink-rgb:   27 10 15;
  --cream-rgb: 244 235 223;

  --gut: 1.5rem;         /* side gutter */
  --btm: 2.6rem;         /* headline baseline offset */
  --vh:  100dvh;         /* section height — dvh, never vh */
  --dwell: 60dvh;        /* the hold — see .pinwrap.holds */

  --h-slot: 2.9rem;      /* the headline slot size */
  --word: 42px;          /* the intro word size — main.js reads this back */
}

@media (min-width: 900px) {
  /* --h-slot was 3.4rem. Molly's section was sized on its own — her bio is
     the one place anyone reads more than two lines, so it got a scale that
     actually holds a wide column — and it read so much better than the
     shared one that it is now the shared one. Every section headline is
     hers. The two exceptions keep their own rules and are unaffected by
     this token: the hero h1 (signed off) and the CTA (spec §3's one
     deliberate break). */
  :root { --gut: 4rem; --btm: 3.4rem; --h-slot: clamp(4.2rem, 4vw, 5.6rem); --word: 64px; }
}

/* ---------- BASE ------------------------------------------ */
@font-face {
  font-family: 'Bodoni Moda';
  src: url('/fonts/bodoni-moda-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Bodoni Moda';
  src: url('/fonts/bodoni-moda-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Jost';
  src: url('/fonts/jost-300.woff2') format('woff2');
  font-weight: 300; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'Jost';
  src: url('/fonts/jost-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}

/* Metric-matched fallbacks. Bodoni runs ~5% wider than Georgia and Jost
   ~8.6% narrower than system-ui, so an unadjusted swap visibly reflows.
   No ascent/descent overrides — every text element sets an explicit
   line-height, so vertical metrics can't shift. */
@font-face {
  font-family: 'Bodoni fallback';
  src: local('Georgia'), local('Times New Roman');
  size-adjust: 105%;
}
@font-face {
  font-family: 'Jost fallback';
  src: local('Helvetica Neue'), local('Arial');
  size-adjust: 92%;
}

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

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

body {
  margin: 0;
  background: var(--ink);
  color: var(--cream);
  font-family: 'Jost', 'Jost fallback', system-ui, sans-serif;
  font-weight: 300;
  line-height: 1.75;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: 'Bodoni Moda', 'Bodoni fallback', Georgia, serif;
  font-weight: 400;
  line-height: .95;
  letter-spacing: -.02em;
  text-wrap: pretty;
  margin: 0;
}

p { margin: 0; }

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

a { color: inherit; }

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

.eyebrow {
  font-size: .54rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  font-weight: 400;
  color: var(--muted);
}

.body-text {
  font-size: .78rem;
  line-height: 1.75;
  font-weight: 300;
  letter-spacing: .01em;
  color: var(--soft);
  max-width: 34rem;
}

.stop { color: var(--stop); }

/* ---------- LAYOUT ---------------------------------------- */

/* The card mechanism.
   position:sticky ALWAYS creates a stacking context, regardless of z-index.
   Anything that must paint above a pin has to be a CHILD of that pin,
   never a sibling. */
.pinwrap { position: relative; }

.pin {
  position: sticky;
  top: 0;
  height: var(--vh);
  z-index: 0;
  overflow: hidden;
}

.over {
  position: relative;
  z-index: 2;
  background: var(--ink);
}

/* The hold.
   Every full-frame moment gets one extra flick of scroll where nothing moves,
   so a visitor can take the section in before the next one starts arriving.
   It is a DISTANCE, not a duration — nothing here touches the scroll itself,
   so a hard flick still passes straight through, and the page stays exactly
   as usable with a keyboard as with a thumb.

   Two shapes, because a wrapper holds in two different places:
     · BEFORE a card rises — a transparent gap above the .over. The pin is
       stuck behind it, so the gap reads as the pinned section sitting still.
     · AFTER the last thing in the wrapper has arrived — trailing room, which
       is what extends how long a sticky child may stay stuck.

   That trailing room is a generated BOX and has to be. A sticky element is
   confined to its containing block's CONTENT box, so neither of the two
   obvious ways to make the wrapper taller buys a single pixel of hold:
   padding-bottom on the wrapper is outside the content box, and a
   margin-bottom on the sticky child is its own margin, which is subtracted
   from the constraint rather than added to it. Both measured at 0.00
   viewports of travel against 0.60 for this rule. ::after rather than an
   empty spacer div only because the markup does not need to carry it.

   Either way the maths land flush: the moment a sticky child releases it
   still fills the frame exactly and the next section is sitting on the
   bottom edge. The hold costs a beat, never a visible gap. */
.pinwrap > .over { margin-top: var(--dwell); }
.pinwrap.holds::after { content: ''; display: block; height: var(--dwell); }

/* Full-bleed image frame.
   No unconditional `position` here. The hero is `class="pin shot"`, and
   `.shot` sits after `.pin` at equal specificity — declaring position:relative
   for every .shot would silently un-stick the pin and the whole card
   mechanism with it. A pin is already a containing block for its children
   (sticky positions it), so only a shot that is NOT a pin needs one. */
.shot { overflow: hidden; }
.shot:not(.pin) { position: relative; }
/* Both depths, because every shot's img is wrapped in a picture element for
   the AVIF/WebP sources. That wrapper is a real element, so `.shot > img`
   alone matches nothing. Without this the images fall back to static flow at their
   intrinsic aspect ratio and object-position goes inert (object-fit resolves
   to `fill`), which reads as a letterboxed sliver above a panel of empty ink.
   Kept as an explicit two-deep list rather than a descendant selector so it
   can never reach an image nested further inside a section. */
.shot > img, .shot > picture > img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE HEADLINE SLOT.
   Every full-viewport moment lands its headline here — same size, same
   distance off the bottom, same gutter. This is what makes a covering
   section read as the same slot changing its mind rather than as two
   unrelated sections.

   Two sanctioned exceptions, and ONLY two:
     · the hero H1 (5.2rem on desktop) — it is the hero, and it is the one
       block signed off as-is
     · the CTA (3.6rem mobile / 5rem desktop) — spec §3 calls this the
       one deliberate break in the pattern
   The desktop pull quote used to be a third. It was sized to clear the
   gutters at a 46rem measure back when it shared its frame with a picture
   column; full-bleed it has the room to take --h-slot like everything else.
   Everything else uses --h-slot. Do not add a third. */
.slot {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  /* viewport-fit=cover puts content under the home indicator. The audience
     is the iOS Instagram in-app browser, which stacks its own chrome on top
     of that, so every bottom-anchored block needs the inset. */
  padding: 0 var(--gut) calc(var(--btm) + env(safe-area-inset-bottom, 0px));
}
.slot h2, .slot h1 { font-size: var(--h-slot); }

/* Top-anchored variant, used where a section reads from the top edge */
.slot-top {
  position: absolute;
  left: 0; right: 0; top: 0;
  padding: 3rem var(--gut) 0;
}
.slot-top h2 { font-size: var(--h-slot); }

/* A section that fills the frame without pinning */
.full { height: var(--vh); position: relative; overflow: hidden; }

/* The nav.
   NOT space-between — that distributes free space equally, not items, so
   with "WE" ~16px and "MORELLO" ~53px the centre word lands ~18px off.
   A 1fr auto 1fr grid centres the middle cell regardless of word widths. */
.nav {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 7;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: calc(1.3rem + env(safe-area-inset-top, 0px)) var(--gut) 1.3rem;
  border-bottom: 1px solid rgba(244, 235, 223, 0);
  transition: border-color 700ms ease;
}
.is-live .nav { border-bottom-color: rgba(244, 235, 223, .16); }

.seat {
  /* same stack as the headings, including the metric-matched fallback —
     the intro flies these into place, so a mismatched fallback would
     shift the measured layout mid-animation */
  font-family: 'Bodoni Moda', 'Bodoni fallback', Georgia, serif;
  font-weight: 500;
  font-size: 9px;
  line-height: 1;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--cream);
  white-space: nowrap;
  /* cancels the trailing letter-space after the final glyph, so ARE is
     optically centred and MORELLO sits flush to the gutter */
  margin-right: -.22em;
  transform-origin: center center;
}
.seat:nth-child(1) { justify-self: start; }
.seat:nth-child(2) { justify-self: center; }
.seat:nth-child(3) { justify-self: end; }

@media (min-width: 900px) {
  .nav  { padding: calc(1.5rem + env(safe-area-inset-top, 0px)) var(--gut) 1.5rem; }
  .seat { font-size: 11px; letter-spacing: .24em; margin-right: -.24em; }
}

/* Conditional line breaks. Two non-overlapping queries, each only ever
   hiding — so neither has to restore a display value for <br>, which is
   fiddly to get right. */
@media (min-width: 900px) { .br-m { display: none; } }
@media (max-width: 899px) { .br-d { display: none; } }

/* A place name reads as one word, so the en dash in "Echuca–Moama" must not
   be a break opportunity. Without this, 390px wraps it to "Echuca– / Moama". */
.nowrap { white-space: nowrap; }

/* ---------- SECTIONS -------------------------------------- */

/* 1 · hero */
/* id only — a descendant selector here would also match the pull quote
   image (also inside a .pin.shot) and outrank its own .img-quote class,
   cropping cherry-3 to the hero's framing. */
#hero-img { object-position: 61% 22%; }
/* Mobile only — the desktop block replaces this background outright with a
   96deg wash, because the wide asset needs the copy lifted off the left
   rather than off the bottom.

   Both ends are doing a job. The top band separates the nav from whatever
   the photograph happens to be doing behind it; the bottom has to carry
   the h1 and the sub-copy, and this frame puts the desk, the laptop and
   the coupe of cherries exactly where that text lands. At .78 they read
   through the type as clutter. It closes to solid ink by the time the
   headline starts instead.

   Many stops rather than four: the browser interpolates linearly between
   them, so a curve this shaped has to be described rather than implied —
   four stops give a visible straight-line ramp across her face. */
.hero-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(27,10,15,.88) 0%, rgba(27,10,15,.62) 6%, rgba(27,10,15,.34) 13%,
    rgba(27,10,15,.14) 20%, rgba(27,10,15,.05) 27%, rgba(27,10,15,.04) 36%,
    rgba(27,10,15,.12) 45%, rgba(27,10,15,.30) 53%, rgba(27,10,15,.54) 60%,
    rgba(27,10,15,.76) 65%, rgba(27,10,15,.91) 69%, rgba(27,10,15,.97) 73%,
    var(--ink) 78%, var(--ink) 100%);
}

/* 2 · "Morello is" — content-height card, text at the TOP edge.
   If this is a full viewport with bottom-aligned text, ~500px of empty
   ink rises over the hero before a single word appears. */
.card-is {
  min-height: 0;
  /* Generous top padding so the card's leading edge is a band of pure ink
     as it rises over the hero. Without it the copy sits right on the edge
     and reads as colliding with the headline it is covering. */
  padding: 5rem var(--gut) 4rem;
}
.card-is .eyebrow { margin-bottom: 1rem; }
/* the headline only — the eyebrow is an h2 as well, and `.card-is h2`
   (0,1,1) outranks `.eyebrow` (0,1,0), so an unguarded rule here sets
   "MORELLO IS" in 2.9rem tracked-out caps. See h2.eyebrow below. */
.card-is h2:not(.eyebrow) { font-size: var(--h-slot); }
.card-is .body-text { margin-top: 1rem; }

/* 3 · expertise */
/* The 78%/1.35 pair existed to pull the cherries out of a 2.33:1 frame and
   into a portrait one. It cost more than it bought: covering a phone with
   that source scales by height, so the image already rendered ~5x the
   viewport width before the zoom, and the zoom took it past 4x magnification
   of a 3024px source. Portrait viewports load a portrait source now, which
   covers by width and renders at exactly 100vw — no crop to compensate for
   and nothing to zoom. Desktop keeps the wide frame and sets its own crop. */
.img-expertise { object-position: 50% 50%; transform: none; }
.scrim-expertise {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    var(--ink) 0%, rgba(27,10,15,0) 16%,
    rgba(27,10,15,0) 36%, rgba(27,10,15,.44) 58%,
    rgba(27,10,15,.88) 80%, var(--ink) 100%);
}

/* 4 · services */
.section-pad { padding: 3.4rem var(--gut) 3rem; background: var(--ink); }
/* The panel photograph is the right half of the DESKTOP frame. Mobile has no
   right half and its layout is the locked design, so it does not appear
   there at all. display:none with loading="lazy" also means phones never
   fetch it: a lazy image with no layout box never intersects the viewport,
   so the request is deferred indefinitely rather than merely hidden. */
.delivers-shot { display: none; }
.services { list-style: none; margin: 1.6rem 0 0; padding: 0; }
.services li {
  display: flex; justify-content: space-between; align-items: baseline;
  padding: .85rem 0;
  border-bottom: 1px solid rgba(244,235,223,.13);
  font-size: .82rem; font-weight: 300;
}
.services .num { font-size: .55rem; color: var(--muted); letter-spacing: .1em; }

/* 5 · pull quote.
   Scrim starts at SOLID ink so it welds to the section above with no
   seam — .97 leaves a visible full-width step. Fifteen stops, not four:
   the browser interpolates linearly, so the curve must be described. */
/* The two sentences are separate blocks so the second can carry a gap.
   em, not rem: headlines are clamped, so the space has to scale with the
   type or it reads tight on desktop and loose on a phone. */
.q-line { display: block; }
.q-line + .q-line { margin-top: .34em; }

.img-quote { object-position: 50% 62%; }
.scrim-quote {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    var(--ink) 0%, var(--ink) 4%, rgb(var(--ink-rgb) / .98) 9%, rgba(27,10,15,.94) 14%,
    rgba(27,10,15,.87) 19%, rgba(27,10,15,.78) 24%, rgba(27,10,15,.67) 29%,
    rgba(27,10,15,.55) 34%, rgba(27,10,15,.42) 39%, rgba(27,10,15,.30) 44%,
    rgba(27,10,15,.19) 49%, rgba(27,10,15,.10) 54%, rgba(27,10,15,.04) 59%,
    rgba(27,10,15,.01) 64%, rgba(27,10,15,0) 70%);
}

/* 6 · Molly. One full frame. Title sits straight on the wall with no
   scrim; the gradient carries the bio only, and sits BELOW the title in
   the DOM so it can never wash over it. */
/* Sticky, so she holds her own frame once she has finished covering the
   quote — the trailing box on her wrapper is the distance she holds for.
   The doubled selector is deliberate: `.shot:not(.pin)` is (0,2,0) and would
   otherwise win with position:relative and silently un-stick her. */
.over.card-molly { position: sticky; top: 0; height: var(--vh); }
.img-molly { object-position: 56% 34%; }
.scrim-molly {
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(27,10,15,0) 56%, rgba(27,10,15,.70) 71%,
    rgba(27,10,15,.93) 86%, rgba(27,10,15,.97) 100%);
}
.molly-title {
  position: absolute; left: 0; right: 0; top: 0;
  padding: 2.4rem var(--gut) 0;
  color: var(--ink);
}
.molly-title .eyebrow { color: rgb(var(--ink-rgb) / .82); font-size: .66rem; margin-bottom: .7rem; }
.molly-title h3 { font-size: var(--h-slot); }
.molly-bio .body-text { color: var(--soft); }
/* The bio inherits .slot's bottom anchor, which parks it in the very base of
   the frame — under the laptop lid, in the darkest, busiest part of the
   scrim. Lifted to sit about halfway up to the laptop's top edge, where the
   photograph stops competing with it. Equal specificity to .slot (0,1,0) and
   later in the file, so it wins on the bottom edge alone and leaves the
   gutters and the safe-area inset intact. Desktop resets .molly-bio to
   static with its own padding shorthand, so this is mobile-only. */
.molly-bio { padding-bottom: calc(var(--btm) + 2rem + env(safe-area-inset-bottom, 0px)); }

/* 7 · CTA — type only. The one deliberate break in the pattern. */
.cta {
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 var(--gut) env(safe-area-inset-bottom, 0px);
  background: var(--ink);
}
.cta h2 { font-size: 3.6rem; }
.cta > .body-text { margin-top: 1.3rem; max-width: 16.5rem; }
.rule {
  height: 1px; width: 2.6rem; border: 0;
  background: var(--cherry);
  margin: 2.4rem 0 1.6rem;
}
/* The email is the most important element on the page and must look
   like it — materially larger than phone and Instagram. */
.email {
  font-family: 'Bodoni Moda', 'Bodoni fallback', Georgia, serif;
  font-size: 1.32rem;
  line-height: 1.25;
  letter-spacing: -.01em;
  text-decoration: none;
  color: var(--cream);
}
.contact { margin-top: 1.5rem; display: flex; gap: 1.6rem; }
.contact a {
  font-size: .82rem; font-weight: 300; color: var(--soft);
  text-decoration: none;
  border-bottom: 1px solid rgba(244,235,223,.3);
  padding-bottom: 2px;
}

/* Spacing helpers.
   These exist as classes rather than inline style= attributes because the
   CSP sets style-src 'self' — an inline style attribute would be blocked
   in production while working fine locally, which is the worst kind of bug. */
/* The hero is the one block Molly has signed off, so it keeps its own type
   rather than following the site scale below. The two changes here are the
   only ones asked for: more air under the h1, and a sub that carries. */
.hero-sub   { margin-top: 1.7rem; font-size: .92rem; }
.bio-second { margin-top: 1rem; }
.cta-place  { margin-top: 1.4rem; }

/* 8 · footer */
footer {
  padding: 1.6rem var(--gut) calc(1.6rem + env(safe-area-inset-bottom, 0px));
  text-align: center;
  border-top: 1px solid rgba(244,235,223,.12);
  background: var(--ink);
}
.credit { color: var(--muted); font-size: .62rem; letter-spacing: .26em; }
.credit a { text-decoration: none; }

/* Eyebrows that are also section headings must not inherit display type */
h2.eyebrow, h3.eyebrow {
  font-family: 'Jost', 'Jost fallback', system-ui, sans-serif;
  font-weight: 400;
  line-height: 1.75;
  letter-spacing: .3em;
}

/* ---------- MOTION ---------------------------------------- */

/* Everything is visible by default. Motion is added by script, so a
   failed or slow script leaves a complete page. .js-on is set by boot.js
   before first paint, and stripped again after 3s unless main.js reports
   in — see boot.js for why both halves are required. */
.js-on .reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 900ms ease, transform 900ms cubic-bezier(.16,.8,.24,1);
}
.js-on .reveal.is-in { opacity: 1; transform: translateY(0); }

.js-on .reveal-now {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 900ms ease, transform 900ms cubic-bezier(.16,.8,.24,1);
}
.js-on .is-live .reveal-now { opacity: 1; transform: translateY(0); }

/* The hero settle — the single highest-value piece of motion here */
.js-on #hero-img { transform: scale(1.07); transition: transform 2000ms cubic-bezier(.16,.8,.24,1); }
.js-on .is-live #hero-img { transform: scale(1); }

/* The intro.
   The curtain and word live INSIDE the pinned hero, because
   position:sticky creates a stacking context and a sibling can never
   paint above the nav seats. */
/* Gated on .js-on. Without it, a visitor with JavaScript disabled — or on
   the boot.js failsafe path where main.js never arrives — gets a solid red
   panel over the hero photo, the headline and the sub-copy, permanently.
   Verified: elementFromPoint on the H1 returned the curtain.
   .is-live .curtain below is equal specificity and later, so it still wins. */
.js-on .curtain {
  position: absolute; inset: 0;
  background: var(--red);
  z-index: 5;
  /* visibility is switched at the END of the fade — the 800ms delay on a
     0s transition is the standard way to keep an element paintable while
     it fades and then genuinely remove it. Without this the curtain stays
     a full-screen invisible layer over the hero for the life of the page:
     still in the a11y tree, still hit-testable if pointer-events changed,
     and still "visible" to any test that checks for a bounding box. */
  transition: opacity 800ms ease, visibility 0s linear 800ms;
}
.is-live .curtain { opacity: 0; visibility: hidden; pointer-events: none; }

/* NOT gated on .js-on, unlike the curtain above. opacity:0 is what hides this
   word, so putting it behind the gate makes it VISIBLE with JS off — and the
   `min-width: 900px` font-size rule below is ungated, so it renders at 70px.
   The hero photo happens to paint over it (the word is in flow, the photo is
   absolute), which hides it right up until the photo is the thing that failed.
   Verified with JS off and /img/** blocked: a 70px "Hi" in the top-left of the
   hero, across the WE seat. A rule whose job is to hide something must not
   depend on the script arriving. */
.hiword {
  pointer-events: none;   /* a full-width band across the hero, even at opacity 0 */
  position: absolute; left: 0; right: 0; top: 50%;
  text-align: center;
  z-index: 6;
  font-family: 'Bodoni Moda', 'Bodoni fallback', Georgia, serif;
  font-weight: 400;
  font-size: 46px;
  line-height: 1;
  color: var(--sand);
  transform: translateY(-50%);
  opacity: 0;
}
@media (min-width: 900px) { .hiword { font-size: 70px; } }

/* THE INTRO WORDS.
   These are separate elements, not the nav seats scaled up, and the reason
   is rasterisation. WebKit rasterises a transformed layer once and reuses
   that texture for the whole animation, so a 9px nav seat blown up 4.7x is a
   9px texture magnified — soft for the entire flight and only resolving as
   it lands. Reported from an iPhone as "each word blurred a little before it
   fully animated in", which is exactly that shape.

   So the word is rendered at its true intro size and the transform only ever
   scales DOWN. Every frame is minification, which stays crisp. The nav seats
   are never transformed at all now; they cross-fade in at the end, onto
   geometry the flight has already matched.

   Not gated on .js-on, for the same reason .hiword isn't: opacity is what
   hides these, so behind the gate a visitor with the script blocked gets
   three 42px words stacked in the corner of the hero. */
.introwords {
  position: absolute; inset: 0;
  /* ABOVE the nav (7), not below it. The handoff is a fade of this layer
     alone: the seats are simply switched on at full opacity underneath, and
     the word fades off the top of them. Fading both in opposite directions
     instead would dip the composite to ~75% at the midpoint — two 50% copies
     of the same glyphs do not add up to one — and that reads as a flicker on
     the last frame of the intro. */
  z-index: 8;
  pointer-events: none;
  /* same trick as the curtain — stay paintable through the fade, then be
     genuinely gone rather than an invisible layer over the live page */
  transition: opacity 220ms ease, visibility 0s linear 220ms;
}
.introwords.is-done { opacity: 0; visibility: hidden; }

.introword {
  position: absolute; top: 0; left: 0;
  /* must be the seats' stack, fallback included — the flight lands one on
     the other, so a different fallback face would show as a jump at the
     handoff on any load where the webfont is late */
  font-family: 'Bodoni Moda', 'Bodoni fallback', Georgia, serif;
  font-weight: 500;
  font-size: var(--word);
  line-height: 1;
  letter-spacing: .02em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--sand);
  transform-origin: center center;
  opacity: 0;
}
.introword.is-flying {
  /* to --cream, so the colour has already arrived by the handoff and the
     cross-fade has nothing left to give away */
  color: var(--cream);
  transition:
    transform var(--fly) cubic-bezier(.16,.8,.24,1),
    letter-spacing var(--fly) cubic-bezier(.16,.8,.24,1),
    color var(--fly) ease,
    opacity var(--fade) ease;
}

/* No transition: see .introwords above — the seats come on instantly and the
   word layer fades off them. */
.js-on.intro-armed .seat { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .js-on .reveal,
  .js-on .reveal-now,
  .js-on #hero-img {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
  /* the curtain must vanish outright, not fade */
  .curtain { transition: none !important; }
}

/* ---------- DESKTOP --------------------------------------- */
@media (min-width: 900px) {

  /* hero — the wide asset is nearly 16:9 so there is almost no slack */
  /* id only. .pin.shot > picture > img would also match the pull quote
     image and outrank its own .img-quote class, undoing the measured crop. */
  #hero-img { object-position: 52% 46%; }
  .hero-scrim {
    background: linear-gradient(96deg,
      rgba(27,10,15,.94) 0%, rgba(27,10,15,.72) 34%, rgba(27,10,15,.06) 66%);
  }
  .slot h1 { font-size: clamp(5.2rem, 5.4vw, 7.4rem); }
  .slot .body-text { max-width: 32rem; }
  /* The hero's own sizes — it is signed off, so it does not inherit the site
     scale below. Only the two changes asked for: air under the h1, and a sub
     large enough to carry under a 5.2rem headline. */
  .hero-sub { margin-top: 2.4rem; font-size: clamp(1.15rem, .95vw, 1.4rem); }

  /* TYPE SCALE.
     Everything below was inherited from mobile and never re-sized: body
     copy sat at 12.48px and eyebrows at 8.64px on a 1440+ screen, which
     is genuinely hard to read. clamp so they keep growing past 1440
     rather than stranding a small block in a wide frame.
     Tracking is deliberately NOT touched — .eyebrow's .3em is what makes
     an 8px label read as a label, and it holds at 12px. */
  /* Molly's bio and eyebrow scale, promoted to the whole page — see the
     --h-slot note above. The hero sub keeps its own size a few rules down. */
  .body-text { font-size: clamp(1.25rem, 1.1vw, 1.55rem); line-height: 1.75; max-width: 34rem; }
  .eyebrow   { font-size: clamp(.86rem, .68vw, 1.05rem); }

  /* The promoted headline scale runs past the column at the narrow edge of
     the desktop range: "Strategy that drives results." wants ~917px at
     4.2rem and a 900px viewport leaves 772px inside its gutters, so it wraps
     and orphans "results." on a line of its own. These two take the short
     breaks the markup already carries until there is room for the long ones.
     Scoped to the two that need it — expertise already breaks twice and
     would go to four lines. */
  @media (max-width: 1023px) {
    .card-is .br-m, .slot-top .br-m { display: inline; }
  }

  .card-is { padding: 6rem var(--gut) 5rem; }
  .card-is h2:not(.eyebrow) { font-size: var(--h-slot); }
  .card-is .body-text { max-width: 42rem; }

  .img-expertise { object-position: 62% 50%; transform: none; }

  /* Services splits: the list stacks down the left, the photograph is the
     right panel. Padding moves off the section and onto the copy column so
     the picture bleeds to the top, bottom and right edges — the same
     full-height panel Molly's section uses, so the two read as one device.
     The list keeps margin-top:auto, which now sits it against the bottom of
     its own column rather than the section. */
  .section-pad { min-height: var(--vh); padding: 0;
                 display: grid; grid-template-columns: minmax(0, 1fr) 40vw; }
  .delivers-copy { display: flex; flex-direction: column;
                   padding: 4rem var(--gut) var(--btm); }
  .services { display: block; margin-top: auto; }
  .services li { font-size: clamp(1rem, .95vw, 1.35rem); padding: 1.15rem 0; }
  .services .num { font-size: clamp(.66rem, .55vw, .82rem); }
  .delivers-shot { display: block; position: relative; }
  .img-delivers { position: absolute; inset: 0;
                  width: 100%; height: 100%; object-fit: cover;
                  object-position: 50% 50%; }

  /* pull quote — desktop loads the 16:9 outpaint of the still, so the frame
     is filled by a source shaped for it. The 3:4 original could not do that
     job: covering this frame scaled its 1728px to 1800 CSS px and then
     scale(1.25) on top, past 2x on a retina display with no rung above 1728
     to reach for. It was being asked for detail it does not contain.

     Full-bleed, not a picture column. This section is pinned and Molly's
     card rises over it, and her picture owns the right half of that card —
     a column here would meet hers at a different edge, so the whole
     transition read as two mismatched rectangles sliding past each other.
     Both frames are edge-to-edge now, so one simply replaces the other.

     46% rather than centred: it lifts the cherry off the vertical middle
     and clear of the headline above it. */
  .img-quote { object-position: 50% 46%; transform: none; }

  /* The mobile scrim is a top-down wash tuned to a 3:4 crop, and over the
     wide frame it left the cherry sitting in full brightness against ink.
     This is the expertise section's treatment instead — dark at the top
     where the headline sits, open through the middle where the cherry is,
     closing back to ink at the bottom so it welds into Molly's card as she
     rises over it. */
  .scrim-quote {
    background: linear-gradient(180deg,
      var(--ink) 0%, rgba(27,10,15,.92) 10%, rgba(27,10,15,.72) 22%,
      rgba(27,10,15,.52) 34%, rgba(27,10,15,.42) 50%,
      rgba(27,10,15,.52) 66%, rgba(27,10,15,.78) 82%, var(--ink) 100%);
  }
  .slot-top { padding: 3.4rem var(--gut) 0; }
  /* No longer its own size. It had one because the split gave it half a
     frame to sit in; full-bleed it has the room to take the site scale like
     every other section headline. */
  .slot-top h2 { max-width: 60rem; }

  /* Molly splits. Her frame is 2:3 — forcing it into a 16:9 viewport
     reintroduces the chop. This is the one section that is not the
     mobile layout stretched.
     Explicit rows rather than a margin offset: the 1fr tracks top and
     bottom centre the pair, and title and bio each own a row, so they
     can never overlap at any type size. */
  .card-molly {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: 1fr auto auto 1fr;
  }
  .card-molly > picture {
    grid-column: 2; grid-row: 1 / -1;
    position: relative; overflow: hidden;
  }
  .img-molly { object-position: 52% 30%; }
  .scrim-molly { display: none; }
  .molly-title {
    position: static;
    grid-column: 1; grid-row: 2;
    padding: 0 var(--gut);
    color: var(--cream);
  }
  /* The mobile title sits on the light top of the photograph, so its eyebrow
     is ink. Here the title moves onto the ink column, where that same ink
     leaves "The Morello cherry" invisible while still holding its 30px of
     the row. Colour, not opacity — .reveal.is-in owns opacity. */
  /* font-size as well as colour: the mobile rule is (0,2,0), so it outranks
     the .eyebrow scale above and this eyebrow alone would stay at 10.5px
     while every other one grew. */
  .molly-title .eyebrow { color: var(--muted); font-size: clamp(.86rem, .68vw, 1.05rem); }

  /* This is the only section where the copy IS the left half of the frame
     rather than a block sitting under a headline, so the shared desktop
     scale left it stranded: 54px of "Molly Green" over 15px of bio, a 3.6x
     drop, in a 720px column with room to spare. The bio leads here — it is
     the one place on the page anyone reads more than two lines — so it is
     sized first and the title and eyebrow are sized to sit with it. */
  .molly-title h3 { font-size: clamp(4.2rem, 4vw, 5.6rem); }
  .molly-bio {
    position: static;
    grid-column: 1; grid-row: 3;
    padding: 1.9rem var(--gut) 0;
  }
  .molly-bio .body-text {
    font-size: clamp(1.25rem, 1.1vw, 1.55rem);
    line-height: 1.75;
    max-width: 34rem;
  }

  .cta h2 { font-size: clamp(5rem, 5.6vw, 7.6rem); }
  .cta > .body-text { max-width: 32rem; margin-top: 1.6rem; }
  .rule { width: 3.2rem; margin: 2.6rem 0 1.8rem; }
  .email { font-size: clamp(1.9rem, 2.2vw, 3rem); }
  .email-break { display: none; }
  .contact { gap: 2.2rem; }
  .contact a { font-size: clamp(.95rem, .8vw, 1.15rem); }
  .credit { font-size: clamp(.7rem, .58vw, .85rem); }
}
