@charset "utf-8";
/* ==========================================================================
   Jannelle Orilla — Personal Portfolio · main.css

   01 Fonts → 02 Tokens → 03 Reset → 04 Nav → 05 Hero → 06 Buttons
   07 Motion → 08 Hero responsive → 09 Section shell → 10 About
   11 Selected work + device mockups → 12 Services → 13 Process
   14 Contact + footer → 15 Signature transition → 16 Rail → 17 Reel
   18 Reduced motion + print

   Palette discipline: burgundy dominates, blush supports, gold is a micro
   accent only. Every text colour below carries its measured contrast ratio
   against the ground it is actually used on.
   ========================================================================== */

/* --------------------------------------------------------------- 01. Fonts */

@font-face {
  font-family: "Archivo Display";
  src: url("fonts/archivo-display.woff2") format("woff2-variations");
  font-weight: 700 900;
  font-stretch: 62% 100%;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "Inter";
  src: url("fonts/inter-latin-normal.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Fraunces";
  src: url("fonts/fraunces-latin-normal.woff2") format("woff2-variations");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
/* The annotation voice. No script/handwriting face is licensed in this
   project, so the editorial italic serif carries those marginal notes. */
@font-face {
  font-family: "Fraunces";
  src: url("fonts/fraunces-latin-italic.woff2") format("woff2");
  font-weight: 400 600;
  font-style: italic;
  font-display: swap;
}

/* -------------------------------------------------------------- 02. Tokens */

:root {
  /* ---- header logo ---------------------------------------------------------
     One size for the JO mark in every page header: 28px on phones (≤640px),
     easing to 32px from 1280px up — the sizes the inner-page headers already
     used. Width follows the SVG's own 1.364:1 proportions. Defined identically
     in main.css and styles.css, which never load together; keep the two in
     sync. Footer marks are sized separately and do not use this. */
  --header-logo-h: clamp(1.75rem, 1.5rem + .625vw, 2rem);   /* also in styles.css */

  /* Surface ------------------------------------------------------------- */
  /* ---- type scale ---------------------------------------------------------
     Measured 14+ distinct sizes in use, six of them between 10.9 and 13px —
     sizes that are not perceptibly different and so read as one size rendered
     inconsistently. Seven deliberate steps replace them. The smallest step is
     also raised from 11px to 12px: 11px uppercase with wide tracking was the
     cause of the unreadable case caption. */
  --fs-2xs: clamp(.75rem,   .80vw, .8125rem);   /* micro labels, eyebrows      */
  --fs-xs:  clamp(.8125rem, .90vw, .875rem);    /* captions, chips, meta       */
  --fs-sm:  clamp(.875rem,  .98vw, .9375rem);   /* small body                  */
  --fs-md:  clamp(1rem,    1.10vw, 1.0625rem);  /* body                        */
  --fs-lg:  clamp(1.125rem, 1.3vw, 1.25rem);
  --fs-xl:  clamp(1.35rem,  1.8vw, 1.6rem);
  --fs-2xl: clamp(1.75rem,  2.4vw, 2.15rem);

  --porcelain:      #f8f2e9;   /* warm porcelain — the page ground          */
  --porcelain-hi:   #fdf9f3;
  --blush:          #f3d7d3;   /* pale blush — supporting surface           */
  --blush-deep:     #edc3bd;
  --blush-wash:     #f7e3df;

  /* Ink ----------------------------------------------------------------- */
  --plum:           #28131d;   /* near-black plum · 15.6:1 on porcelain     */
  --plum-70:        #6b5158;   /*                    6.4:1 on porcelain     */
  --plum-50:        #8a7178;   /*                    4.0:1 — large only     */

  /* Brand --------------------------------------------------------------- */
  --wine:           #5a1830;   /* deep wine burgundy · 11.7:1 on porcelain  */
  --wine-hi:        #6c2039;
  --wine-lo:        #431122;
  --wine-08:        rgba(90, 24, 48, .08);
  --wine-14:        rgba(90, 24, 48, .14);
  --wine-28:        rgba(90, 24, 48, .28);

  /* Accent — coral is restrained, and never the only signal --------------- */
  --coral:          #c95e51;   /* 3.6:1 — display type and fills only        */
  --coral-ink:      #b24a3a;   /* 4.8:1 — safe for body-size text            */
  --coral-15:       rgba(178, 74, 58, .15);
  --coral-30:       rgba(178, 74, 58, .3);

  /* Metal — decorative unless noted -------------------------------------- */
  --gold:           #d4a24c;   /* 5.6:1 on wine — text-safe there only       */
  --gold-lo:        #b8862f;   /* 2.9:1 on porcelain — rules and marks only  */
  --gold-hi:        #ecd3a4;
  --gold-ink:       #8a6216;   /* 4.9:1 on porcelain — gold-toned small text */

  /* Glass --------------------------------------------------------------- */
  --glass:          rgba(255, 255, 255, .34);
  --glass-edge:     rgba(255, 255, 255, .58);
  --glass-shadow:   0 1.5rem 3.5rem -1.75rem rgba(60, 18, 34, .32);

  /* Type ---------------------------------------------------------------- */
  --f-display: "Archivo Display", "Archivo Black", "Arial Black", system-ui, sans-serif;
  --f-body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-serif:   "Fraunces", Georgia, "Times New Roman", serif;

  /* Geometry ------------------------------------------------------------ */
  --pad:       clamp(1.25rem, 2.57vw, 3.25rem);
  --nav-h:     clamp(4rem, 8.4svh, 5.5rem);
  --wm-ratio:  4.9565;                      /* JANNELLE ink-width ÷ cap-height */
  --radius:    .5rem;
  --radius-lg: 1.25rem;
  /* Eight distinct radii were in use — 24.8px and 29.6px sat side by side,
     two clamps landing differently rather than a decision. Everything now maps
     to these three plus 50% for true circles; the device-mockup corners stay
     bespoke because they are drawing real hardware. */
  --radius-full: 100px;

  /* Nine distinct shadows were in use. Four of them were the same large soft
     lift differing by hundredths of a rem — indistinguishable on screen, and
     the kind of drift that reads as "assembled" rather than designed. */
  --shadow-lift:  0 1.35rem 2.75rem -1.6rem rgba(60, 18, 34, .44);
  --shadow-raise: 0 .9rem 1.6rem -.7rem rgba(60, 18, 34, .45);
  /* The content column. --measure-max is the raw length, kept separate because
     the centring maths in section 09 needs a value it can subtract from 100%;
     --measure is the same thing clamped for use as a child max-width. */
  --measure-max:   80rem;
  --measure:       min(100%, var(--measure-max));
  --measure-tight: min(100%, 46rem);

  /* Easing -------------------------------------------------------------- */
  --ease-out:    cubic-bezier(.16, 1, .3, 1);
  --ease-whoosh: cubic-bezier(.11, .86, .15, 1);
  --ease-soft:   cubic-bezier(.4, 0, .2, 1);

  /* The four-point sparkle from the JO monogram's counter, as a mask so it
     can be tinted. The logo file itself is never touched. */
  --sparkle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0c1.1 6.6 5.3 10.9 12 12-6.7 1.1-10.9 5.4-12 12-1.1-6.6-5.3-10.9-12-12C6.7 10.9 10.9 6.6 12 0z' fill='%23000'/%3E%3C/svg%3E");
}

/* --------------------------------------------------------------- 03. Reset */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1rem);
}

body {
  margin: 0;
  min-height: 100svh;
  background: var(--porcelain);
  color: var(--plum);
  font-family: var(--f-body);
  font-size: 1rem;
  line-height: 1.6;
  font-synthesis-weight: none;
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, ul, ol, figure, blockquote { margin: 0; }
ul, ol { padding: 0; list-style: none; }
img, svg, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; }

:where(a, button, [tabindex], summary):focus-visible {
  outline: 2px solid var(--coral-ink);
  outline-offset: 4px;
  border-radius: 3px;
}
/* On burgundy grounds the coral ring is too close in value to read. */
:where(.sec--contact, .svc--dark) :where(a, button):focus-visible {
  outline-color: var(--gold-hi);
}

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

.skip-link {
  position: absolute;
  left: var(--pad);
  top: 0;
  z-index: 99;
  padding: .7rem 1.1rem;
  background: var(--wine);
  color: #fff;
  font-size: .8125rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  border-radius: 0 0 var(--radius) var(--radius);
  transform: translateY(-110%);
  transition: transform .25s var(--ease-out);
}
.skip-link:focus-visible { transform: translateY(0); }

/* ------------------------------------------------------------------ 04. Nav */

.nav {
  position: absolute;
  inset: 0 0 auto;
  z-index: 20;
  padding-inline: var(--pad);
}

.nav__inner {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: clamp(1rem, 2vw, 2.5rem);
  min-height: var(--nav-h);
}

/* padding-block keeps the link's hit area at or above 24px once the name is
   hidden on phones and only the monogram is left. */
.nav__brand { display: flex; align-items: center; gap: .95rem; padding-block: .35rem; }

/* The JO logo mark — the same artwork as favicon.svg, in the tight crop every
   page header uses. Sized by the shared --header-logo-h token so the homepage
   and inner-page headers match. Vector, so it stays sharp at any density. */
.nav__monogram {
  display: block;
  flex: none;
  width: auto;
  height: var(--header-logo-h);
}

.nav__divider {
  width: 1px;
  height: 1.35rem;
  background: linear-gradient(180deg, transparent, var(--wine-28), transparent);
}

.nav__name {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--plum);
  white-space: nowrap;
}

.nav__links ul {
  display: flex;
  justify-content: center;
  gap: clamp(1.1rem, 2.6vw, 2.8rem);
}

.nav__links a {
  position: relative;
  display: block;
  padding-block: .35rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--plum);
  transition: color .3s var(--ease-soft);
}
/* Panel-only furniture. The desktop row is a plain inline list, so the
   eyebrows, chevrons and CTA pair stay out of it entirely. */
.menu__eyebrow,
.menu__ctas,
.menu__chev { display: none; }

.nav__links a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: .05rem;
  width: 100%;
  height: 1.5px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .45s var(--ease-out);
}
.nav__links a:hover,
.nav__links a[aria-current] { color: var(--coral-ink); }
.nav__links a:hover::after,
.nav__links a[aria-current]::after { transform: scaleX(1); transform-origin: left; }

.nav__cta { justify-self: end; }

.nav__toggle {
  display: none;
  justify-self: end;
  width: 2.75rem;
  height: 2.75rem;
  place-content: center;
  gap: .3rem;
  cursor: pointer;
}
.nav__toggle span {
  display: block;
  width: 1.35rem;
  height: 2px;
  margin-inline: auto;
  background: var(--plum);
  border-radius: 2px;
  transition: transform .4s var(--ease-out), opacity .25s;
}
.nav__toggle[aria-expanded="true"] span:first-child { transform: translateY(4px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:last-child  { transform: translateY(-4px) rotate(-45deg); }

/* ----------------------------------------------------------------- 05. Hero */

.hero {
  /* Width reserved for the identity block. The left floating card is placed
     from this value, so the two can never overlap at any viewport width. */
  --lead-reserve: 23rem;
  position: relative;
  display: grid;
  grid-template-rows: 1fr;
  min-height: 100svh;
  padding-inline: var(--pad);
  overflow: clip;
  isolation: isolate;
}

/* Canvas ----------------------------------------------------------------- */

.hero__canvas {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(62% 46% at 50% 2%, rgba(255, 252, 247, .7), transparent 70%),
    var(--porcelain);
}

/* Four warm masses drifting behind the hero.

   Each .hero__glow is a zero-size carrier parked at an anchor around the frame.
   It rotates 360deg forever at a constant rate, so the blob inside it — offset
   from the carrier's origin — travels a closed ellipse. It never slows, never
   reverses, and its end state is its start state, so there is no seam to land
   on.

   Three things here are load-bearing:

   1. The blob's offset is in vmin, NOT a percentage. The carrier is 0x0, so a
      percentage resolves against zero and the orbit radius collapses.
   2. No filter: blur(). The gradient's own soft stop does the softening, so
      only `rotate` and `scale` change and the compositor never touches the
      main thread.
   3. `scale` on the carrier applies AFTER `rotate`, so a non-uniform scale
      turns the circular orbit into an ellipse rather than skewing it.

   Periods share no factors (41/53/67/79s) and each starts at a different point
   in its cycle, so the four do not resync for weeks. */

.hero__glow {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 0;
  height: 0;
  scale: var(--sq);
  animation: glow-orbit var(--orbit) linear infinite;
  animation-delay: var(--phase);
}

.hero__glow > i {
  position: absolute;
  left: var(--rx);
  top: var(--ry);
  width: var(--size);
  aspect-ratio: 1;
  margin: calc(var(--size) / -2);
  border-radius: 50%;
  opacity: var(--o);
  background: radial-gradient(closest-side, var(--c), transparent 72%);
  animation: glow-breathe var(--breathe) ease-in-out infinite alternate;
  animation-delay: var(--breathe-phase);
}

/* Anchored around the edges, so the centre stays clear for the wordmark
   and the portrait. Tinted to the blush/coral end of the new palette. */
.hero__glow--1 { --x: -2%;  --y: 36%;  --orbit: 41s; --phase: -9s;  --sq: 1 .82;
                 --rx: 11vmin;  --ry: 7vmin;   --size: 64vmin; --o: .62;
                 --c: rgba(243, 215, 211, .98); --breathe: 23s; --breathe-phase: -5s;  --s: 1.14; }
.hero__glow--2 { --x: 102%; --y: 52%;  --orbit: 53s; --phase: -21s; --sq: 1 .74;
                 --rx: -13vmin; --ry: 9vmin;   --size: 58vmin; --o: .56;
                 --c: rgba(237, 195, 189, .95); --breathe: 29s; --breathe-phase: -14s; --s: 1.11;
                 animation-direction: reverse; }
.hero__glow--3 { --x: 28%;  --y: -8%;  --orbit: 67s; --phase: -33s; --sq: .86 1;
                 --rx: 8vmin;   --ry: -10vmin; --size: 48vmin; --o: .22;
                 --c: rgba(212, 162, 76, .8);   --breathe: 37s; --breathe-phase: -19s; --s: 1.16; }
.hero__glow--4 { --x: 76%;  --y: 106%; --orbit: 79s; --phase: -11s; --sq: 1 .9;
                 --rx: -9vmin;  --ry: -12vmin; --size: 52vmin; --o: .18;
                 --c: rgba(201, 94, 81, .62);   --breathe: 43s; --breathe-phase: -27s; --s: 1.09;
                 animation-direction: reverse; }

@keyframes glow-orbit   { to { rotate: 360deg; } }
@keyframes glow-breathe { to { scale: var(--s); } }

.hero__grain {
  position: absolute;
  inset: -50%;
  opacity: .14;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
}

/* Wordmark --------------------------------------------------------------- */

.hero__wordmark {
  --wm-x: -50%;
  position: absolute;
  top: calc(var(--nav-h) + .5svh);
  left: 50%;
  z-index: 1;
  width: min(100% - var(--pad) * 2, calc(28svh * var(--wm-ratio)));
  translate: var(--wm-x) 0;
  pointer-events: none;
}

.wordmark { width: 100%; height: auto; overflow: visible; }

.wordmark__text {
  font-family: var(--f-display);
  font-size: 294.1px;
  font-weight: 900;
  font-stretch: 62%;
  font-variation-settings: "wght" 900, "wdth" 62;
  fill: var(--wine);
}

/* Portrait --------------------------------------------------------------- */

.hero__portrait {
  position: absolute;
  top: 4.6svh;
  left: 50%;
  z-index: 2;
  height: 117.5svh;
  aspect-ratio: 669 / 1431;
  translate: -50% 0;
  pointer-events: none;
  will-change: transform;
}
.hero__portrait img {
  height: 100%;
  width: auto;
  max-width: none;
  object-fit: contain;
  object-position: top center;
  filter: drop-shadow(0 2.5rem 4rem rgba(60, 18, 34, .18));
}

/* Floating glass cards --------------------------------------------------- */

/* Above the portrait (z2), below the copy (z3, and later in the DOM so it
   still wins). The floats sit in the same band as the portrait's edge; behind
   it they lost their last two rows to her shoulder. Glass over the portrait is
   the intended layered look and keeps every row readable. */
.hero__floats {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}

.card {
  position: absolute;
  width: clamp(11.25rem, 13.5vw, 15.5rem);
  padding: clamp(1rem, 2.05svh, 1.7rem) clamp(1rem, 1.8vw, 1.9rem);
  background: var(--glass);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  box-shadow: var(--glass-shadow), inset 0 1px 0 rgba(255, 255, 255, .6);
  backdrop-filter: blur(16px) saturate(1.15);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
  pointer-events: auto;
  will-change: transform;
}

.card--traits { left: calc(var(--pad) + var(--lead-reserve) + clamp(2rem, 3.6vw, 4.25rem)); bottom: 49%; }
.card--skills { right: clamp(1.25rem, 19vw, 34rem); top: 37%; }

.card__badge {
  position: absolute;
  top: 0;
  left: 0;
  display: grid;
  place-items: center;
  width: clamp(2.5rem, 3.25vw, 3.4rem);
  aspect-ratio: 1;
  translate: -46% -46%;
  border-radius: 50%;
  background: linear-gradient(142deg, var(--gold-hi) 2%, var(--gold) 48%, var(--gold-lo) 100%);
  box-shadow: 0 .5rem 1.1rem -.35rem rgba(110, 70, 26, .5), inset 0 1px 1px rgba(255, 255, 255, .7);
}
.card__badge svg {
  width: 52%;
  height: auto;
  fill: none;
  stroke: #fffaf2;
  stroke-width: 1.35;
  stroke-linejoin: round;
  stroke-linecap: round;
}
/* Icon family: 1.35 stroke in porcelain, one soft tint fill per mark at most.
   The spark is the JO monogram's own four-point counter, not an emoji
   sparkle; the skills mark is a browser window with a pointer. */
.card--traits .card__badge svg path:first-child,
.about__note .card__badge svg path:first-child { fill: rgba(255, 250, 242, .22); }
.card__badge svg .card__cursor { fill: #fffaf2; }

.card__list li {
  position: relative;
  padding-block: clamp(.5rem, 1.16svh, .92rem);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .12em;
  line-height: 1.2;
  text-transform: uppercase;
  color: var(--plum);
}
.card__list li + li::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(90deg, var(--wine-28), rgba(90, 24, 48, .02));
}
.card__list--traits li { font-weight: 700; }

/* Content grid ----------------------------------------------------------- */

.hero__grid {
  position: relative;
  z-index: 3;
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  align-items: end;
  align-content: end;
  column-gap: clamp(2rem, 5vw, 6rem);
  padding-block-end: clamp(1.5rem, 4.5svh, 3rem);
}

/* The identity block gained a third line in the 2 Sep repositioning (role +
   secondary descriptor). The hero grid is bottom-aligned, so a taller block
   pushes upward — and at 1440 the name collided with the base of the JANNELLE
   wordmark. Trimming the gap below it puts the block back under the type. */
.identity { max-width: var(--lead-reserve); margin-block-end: clamp(1rem, 4.2svh, 2.5rem); }

.identity__name {
  font-family: var(--f-serif);
  font-size: clamp(1.0625rem, 1.34vw, 1.5rem);
  font-weight: 700;
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 22;
  letter-spacing: -.005em;
  line-height: 1.15;
  color: var(--plum);
}
.identity__role {
  margin-block-start: .42em;
  font-size: var(--fs-2xs);
  font-weight: 500;
  letter-spacing: .19em;
  text-transform: uppercase;
  color: var(--wine-hi);
}
/* The secondary descriptor. Deliberately quieter than .identity__role — the
   hierarchy is role first, breadth second, and reversing the two was the
   central positioning problem this rebrand fixes. */
.identity__sub {
  margin-block-start: .3em;
  font-size: clamp(.6875rem, .78vw, .8125rem);
  font-weight: 500;
  letter-spacing: .1em;
  color: var(--plum-70);
}

.identity__rule {
  display: block;
  width: clamp(3rem, 4.7vw, 4.9rem);
  height: 2px;
  margin-block-start: clamp(.75rem, 1.5vh, 1.15rem);
  border-radius: 2px;
  background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi));
}

.headline {
  font-family: var(--f-display);
  font-size: clamp(2.15rem, 4.2vw, 4.55rem);
  font-weight: 900;
  font-stretch: 89%;
  font-variation-settings: "wght" 900, "wdth" 89;
  letter-spacing: -.012em;
  line-height: .9;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--plum);
}
.headline__line { display: block; }
.headline__in   { display: block; }
.headline em    { font-style: normal; color: var(--wine); }

/* The contact pair (below) uses the CTA-group grid: equal tracks sized to
   the longer label, so both buttons match at every width.

   Side by side, the hero pair keeps its natural widths on purpose: matching
   "View résumé" to the longer primary pushed the outlined button ~65px across
   the portrait, with its border cutting over the jacket. Heights match (see
   .btn) and the gap never drops below 12px. Stacked on phones they are equal
   and full width — the 600px block below. */
.hero__ctas {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(.75rem, 1.15vw, 1.2rem);
  margin-block-start: clamp(1.35rem, 3.4svh, 2.1rem);
}

/* Third CTA and the availability line, kept deliberately quiet: employers and
   clients each get a loud button, and this carries the other route without
   turning the hero into three competing calls to action. */
.hero__quiet {
  margin-block-start: clamp(.85rem, 2svh, 1.2rem);
  font-size: clamp(.8125rem, .92vw, .9375rem);
  line-height: 1.6;
  color: var(--plum-70);
}
.quiet__label--narrow { display: none; }
.quiet__arrow { display: none; width: .78em; height: .78em; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

.hero__quiet a {
  display: inline-block;
  font-weight: 600;
  color: var(--wine);
  border-block-end: 1px solid var(--wine-28);
  /* WCAG 2.2 exempts inline links from the 24px target minimum, but the rest of
     this site clears it, so this one does too. */
  padding-block: .22rem .18rem;
  transition: color .25s var(--ease-out), border-color .25s var(--ease-out);
}
.hero__quiet a:hover { color: var(--wine-lo); border-color: currentColor; }

.hero__note {
  justify-self: end;
  max-width: 18.5rem;
  margin-block-end: clamp(1.25rem, 4.65svh, 3.5rem);
  margin-inline-end: clamp(0rem, 2.8vw, 3rem);
  padding-inline-start: clamp(1rem, 1.5vw, 1.5rem);
  border-inline-start: 2px solid;
  border-image: linear-gradient(180deg, var(--gold-hi), var(--gold-lo) 45%, rgba(184, 134, 47, 0)) 1;
  font-size: clamp(.9375rem, 1.05vw, 1.1rem);
  line-height: 1.75;
  color: var(--plum-70);
  text-wrap: pretty;
}
.hero__note strong { font-weight: 600; color: var(--wine); }

/* -------------------------------------------------------------- 06. Buttons */

/* justify-content, min-height and the transparent border keep every .btn the
   same shape: the outlined versions used to be 2px taller than the filled
   ones beside them (51 vs 53px in the hero pair). */
.btn {
  --btn-fg: #fff;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 3.25rem;
  border: 1px solid transparent;
  gap: clamp(1.1rem, 2.2vw, 2.45rem);
  padding: clamp(.9rem, 1.92svh, 1.2rem) clamp(1.25rem, 1.85vw, 1.9rem);
  border-radius: var(--radius);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .13em;
  line-height: 1;
  text-transform: uppercase;
  color: var(--btn-fg);
  white-space: nowrap;
  overflow: hidden;
  isolation: isolate;
  cursor: pointer;
  transition: transform .4s var(--ease-out), box-shadow .4s var(--ease-out);
}
.btn > span { position: relative; z-index: 1; }

.btn::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  translate: 0 101%;
  background: var(--coral-ink);
  transition: translate .5s var(--ease-out);
}
.btn:hover  { transform: translateY(-2px); box-shadow: var(--shadow-raise); }
.btn:hover::before { translate: 0 0; }
.btn:active { transform: translateY(0); }

.btn__arrow {
  position: relative;
  z-index: 1;
  width: 1rem;
  height: 1rem;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: translate .4s var(--ease-out);
}
.btn:hover .btn__arrow { translate: 2px -2px; }

.btn--ink   { background: var(--plum); }
.btn--wine  { background: linear-gradient(180deg, var(--wine-hi), var(--wine-lo)); }
.btn--gold  { --btn-fg: var(--plum); background: linear-gradient(160deg, var(--gold-hi), var(--gold) 55%, var(--gold-lo)); }
.btn--gold:hover { --btn-fg: #fff; }
.btn--ghost {
  --btn-fg: var(--plum);
  border: 1px solid var(--wine-28);
  background: transparent;
}
.btn--ghost:hover { --btn-fg: #fff; border-color: transparent; }
/* Ghost on a burgundy ground */
.btn--ghost-light {
  --btn-fg: var(--porcelain);
  border: 1px solid rgba(243, 215, 211, .42);
  background: transparent;
}
.btn--ghost-light::before { background: var(--porcelain); }
.btn--ghost-light:hover { --btn-fg: var(--wine); border-color: transparent; }

/* --------------------------------------------------------------- 07. Motion */

/* Property budget, so nothing fights:
     translate  → entrance + idle drift   (independent transform property)
     scale      → entrance                (independent transform property)
     transform  → pointer parallax only   (written by main.js)
     filter     → entrance blur                                             */

.js .r { opacity: 0; will-change: translate, opacity; }
.js .r--down { translate: 0 -.85rem; }
.js .r--rise { translate: 0 1.75rem; }
.js .r--wipe { translate: 0 2.4rem; }

.js .hero__wordmark { opacity: 0; }
/* The portrait is deliberately NOT hidden here. Chromium refuses a
   zero-opacity element as a Largest Contentful Paint candidate and never
   reconsiders it once it fades in — and SVG <text> is excluded by definition,
   so the wordmark cannot stand in. It paints on the first frame and arrives
   out of focus instead of out of nothing. */

.is-loaded .r       { animation: reveal .85s var(--ease-out) var(--d, 0s) both; }
.is-loaded .r--wipe { animation-duration: 1s; }

.is-loaded .hero__wordmark { animation: whoosh 1.35s var(--ease-whoosh) .06s both; }
.is-loaded .hero__portrait { animation: emerge 1.25s var(--ease-out) .58s both; }

@keyframes reveal {
  from { opacity: 0; }
  to   { opacity: 1; translate: 0 0; }
}
@keyframes whoosh {
  0%   { opacity: 0; translate: calc(var(--wm-x) + 115%) 0; filter: blur(7px); }
  18%  { opacity: 1; }
  100% { opacity: 1; translate: var(--wm-x) 0; filter: blur(0); }
}
@keyframes emerge {
  0%   { translate: -50% 6.5%; scale: 1.055; filter: blur(18px); }
  100% { translate: -50% 0;    scale: 1;     filter: blur(0); }
}

.is-loaded .card--traits { animation: reveal .85s var(--ease-out) var(--d) both, drift 9s ease-in-out 3s infinite alternate; }
.is-loaded .card--skills { animation: reveal .85s var(--ease-out) var(--d) both, drift 11s ease-in-out 3.4s infinite alternate-reverse; }

@keyframes drift {
  from { translate: 0 0; }
  to   { translate: 0 -9px; }
}

/* Section reveals wait for the scroll observer. Without this the page-wide
   `.is-loaded .r` rule fires every section's reveal on the hero's timeline,
   so by the time you scroll there its text has already played off-screen. */
.is-loaded .sec .r { animation: none; }
.is-loaded .sec .r.in { animation: reveal .85s var(--ease-out) var(--d, 0s) both; }

/* Section headings arrive a word at a time. main.js wraps each word in
   .w > .w__in and sets --wd; the parent holds still so only the words move. */
.w { display: inline-block; }
.w__in { display: inline-block; will-change: translate, opacity; }
.js .r .w__in { opacity: 0; translate: 0 .9em; }
.js .r.is-split { opacity: 1; translate: 0 0; }
.is-loaded .sec .r.is-split,
.is-loaded .sec .r.is-split.in { animation: none; }
.is-loaded .sec .r.is-split.in .w__in {
  animation: reveal .8s var(--ease-out) calc(var(--d, 0s) + var(--wd, 0s)) both;
}

/* Pointer parallax — main.js writes --px / --py. */
.hero__wordmark,
.hero__portrait,
.card { transform: translate3d(var(--px, 0px), var(--py, 0px), 0); }

/* ------------------------------------------------------ 08. Hero responsive */

/* Short viewports (720p and 768p laptops) squeeze the wordmark and the
   bottom-aligned identity together, because the wordmark is sized from svh
   while the content stack is not. Give the type a smaller share of the height
   there — it still spans ~70% of the width, so the composition holds. */
@media (min-width: 901px) and (max-height: 800px) {
  .hero__wordmark { width: min(100% - var(--pad) * 2, calc(25svh * var(--wm-ratio))); }
}

/* Laptop — the portrait grows proportionally, so move the floats out to the
   margins rather than letting them slide behind her. */
@media (min-width: 901px) and (max-width: 1280px) {
  .card { width: clamp(10.25rem, 17.5vw, 13.5rem); padding-block: clamp(.85rem, 1.7svh, 1.4rem); }
  .card--traits { left: calc(var(--pad) + var(--lead-reserve) + clamp(1.25rem, 2.4vw, 2.5rem)); top: auto; bottom: 50%; }
  .card--skills { right: var(--pad); top: 28.5%; }
  .hero__note { margin-inline-end: 0; margin-block-end: clamp(1.25rem, 5svh, 3rem); }
  .hero__grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); }}

/* Mobile CTA hierarchy. Inline, the tertiary CTA and the availability line are
   one sentence; on phones that wrapped awkwardly and blurred which half was
   clickable. Below 900px the sentence re-composes into a stacked group: link,
   then availability as supporting copy. Only one label is ever in the
   accessibility tree — the other is display:none. */
@media (max-width: 900px) {
  .hero__quiet {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: clamp(.45rem, 1.4svh, .8rem);
    margin-block-start: clamp(1.1rem, 2.6svh, 1.6rem);
  }
  .quiet__or,
  .quiet__dash,
  .quiet__label--wide { display: none; }
  .quiet__label--narrow { display: inline; }

  .hero__quiet .quiet__cta {
    display: inline-flex;
    align-items: center;
    gap: .45em;
    min-height: 44px;
    font-size: clamp(.9375rem, 3.9vw, 1.0625rem);
    color: var(--wine);
    border-block-end: 1px solid var(--wine-28);
  }
  .quiet__arrow { display: block; }

  .quiet__avail {
    display: block;
    font-size: clamp(.78125rem, 3.3vw, .875rem);
    line-height: 1.55;
    color: var(--plum-70);
  }
  .quiet__avail::first-letter { text-transform: uppercase; }
}

/* Tablet and below — the composition stacks: poster block on top, content
   beneath, capability panels as a real two-up grid at the end. Deliberately
   a different composition, not a shrunken desktop one. */
@media (max-width: 900px) {
  .nav__links, .nav__cta { display: none; }
  .nav__toggle { display: grid; }

  /* ---- Mobile menu -------------------------------------------------------
     A full-screen editorial panel rather than a centred link list: brand and
     close control stay in the header above it, the sections are labelled, and
     the two routes out sit at the bottom where a thumb reaches them.
     The panel scrolls if a short phone cannot fit it, so nothing is stranded. */
  .nav__links.is-open {
    position: fixed;
    inset: 0;
    z-index: 30;
    display: flex;
    flex-direction: column;
    background: var(--porcelain);
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    padding-inline: var(--pad);
    padding-block: calc(var(--nav-h) + env(safe-area-inset-top, 0px) + clamp(1rem, 4svh, 2.25rem))
                   calc(clamp(1.5rem, 5svh, 2.5rem) + env(safe-area-inset-bottom, 0px));
  }

  /* Section label: a short gold rule, then tracked small caps. */
  .nav__links.is-open .menu__eyebrow {
    display: flex;
    align-items: center;
    gap: .7rem;
    margin: 0 0 clamp(.6rem, 2svh, 1rem);
    font-family: var(--f-body);
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--wine);
  }
  .nav__links.is-open .menu__rule {
    display: block;
    flex: 0 0 auto;
    width: 1.85rem;
    height: 1px;
    background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi));
  }

  /* Rows: full-width targets, serif display type, hairline separators. */
  .nav__links.is-open ul {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin-block-end: clamp(1.5rem, 5svh, 2.75rem);
  }
  .nav__links.is-open li + li { border-block-start: 1px solid var(--wine-14); }

  .nav__links.is-open li a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding-block: clamp(.62rem, 2.1svh, 1rem);
    padding-inline: 0;
    min-height: 44px;
    font-family: var(--f-serif);
    font-size: clamp(1.6rem, 7.6vw, 2.3rem);
    font-weight: 400;
    font-variation-settings: "opsz" 32;
    letter-spacing: -.015em;
    line-height: 1.1;
    text-transform: none;
    color: var(--plum);
  }
  /* The desktop hover underline would sit across the row; the chevron carries
     the affordance here instead. */
  .nav__links.is-open li a::after { display: none; }
  .nav__links.is-open li a:hover,
  .nav__links.is-open li a[aria-current] { color: var(--wine); }

  .nav__links.is-open .menu__chev {
    display: block;
    flex: 0 0 auto;
    width: 1.15rem;
    height: 1.15rem;
    fill: none;
    stroke: var(--wine);
    stroke-width: 1.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    opacity: .55;
    transition: transform .3s var(--ease-out), opacity .3s var(--ease-out);
  }
  .nav__links.is-open li a:active .menu__chev,
  .nav__links.is-open li a:hover .menu__chev { opacity: 1; transform: translateX(3px); }

  /* Two routes out, full width, primary first. */
  .nav__links.is-open .menu__ctas {
    display: flex;
    flex-direction: column;
    gap: .75rem;
  }
  /* The second label travels with the CTA pair, so the two read as one block
     pinned to the bottom rather than the label floating mid-panel. */
  .nav__links.is-open .menu__eyebrow:nth-of-type(2) {
    margin-block-start: auto;
    padding-block-start: clamp(1rem, 4svh, 2rem);
  }
  /* .nav__links a sets display:block and its own padding/size, and out-specifies
     .btn — so the button layout has to be restored explicitly here. */
  .nav__links.is-open .menu__cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    gap: 1rem;
    padding: clamp(.95rem, 2.1svh, 1.15rem) clamp(1.15rem, 4.4vw, 1.5rem);
    font-size: var(--fs-2xs);
    font-weight: 700;
    letter-spacing: .13em;
    line-height: 1;
    text-transform: uppercase;
    min-height: 52px;
  }
  .nav__links.is-open .menu__cta.btn--wine { color: var(--porcelain); }
  .nav__links.is-open .menu__cta.btn--ghost { color: var(--wine); }

  /* Header sits above the panel so the monogram and the close control hold
     their position instead of being covered by it. */
  .nav.is-menu-open { z-index: 40; }
  /* The panel is a sibling of the brand and the toggle inside .nav, so its
     z-index paints over both. Lift them back above it — that keeps one control
     doing open and close, in the position the closed header already used. */
  .nav.is-menu-open .nav__brand,
  .nav.is-menu-open .nav__toggle { position: relative; z-index: 31; }

  /* Hamburger becomes a ringed close control. */
  .nav__toggle {
    border: 1px solid var(--wine-28);
    border-radius: var(--radius-full);
    transition: border-color .3s var(--ease-soft), background .3s var(--ease-soft);
  }
  .nav__toggle span { transition: transform .35s var(--ease-out), opacity .2s linear; }
  .nav__toggle[aria-expanded="true"] { border-color: var(--wine-28); background: var(--porcelain-hi); }
  .nav__toggle[aria-expanded="true"] span:first-child { transform: translateY(3.5px) rotate(45deg); }
  .nav__toggle[aria-expanded="true"] span:last-child  { transform: translateY(-3.5px) rotate(-45deg); }

  /* Open stagger: short, and never a gate — the rows are clickable immediately. */
  .nav__links.is-open .menu__eyebrow,
  .nav__links.is-open li,
  .nav__links.is-open .menu__ctas { animation: menuIn .42s var(--ease-out) both; }
  .nav__links.is-open li:nth-child(1) { animation-delay: .05s; }
  .nav__links.is-open li:nth-child(2) { animation-delay: .09s; }
  .nav__links.is-open li:nth-child(3) { animation-delay: .13s; }
  .nav__links.is-open li:nth-child(4) { animation-delay: .17s; }
  .nav__links.is-open li:nth-child(5) { animation-delay: .21s; }
  .nav__links.is-open .menu__ctas { animation-delay: .26s; }

  .hero {
    grid-template-rows: auto auto auto;
    min-height: 100svh;
    padding-block-end: clamp(2rem, 6svh, 3.5rem);
  }

  .hero__wordmark {
    --wm-x: 0%;
    order: 1;
    position: relative;
    top: auto;
    left: auto;
    width: 100%;
    margin-block-start: calc(var(--nav-h) + 2.2svh);
  }

  /* Cropped to head-and-shoulders and faded out, so it never eats the fold. */
  .hero__portrait {
    order: 1;
    top: calc(var(--nav-h) + 2.2svh + 1.5vw);
    height: min(60vw, 21.5rem);
    width: min(74vw, 26rem);
    aspect-ratio: auto;
    overflow: clip;
    -webkit-mask-image: linear-gradient(180deg, #000 72%, transparent 99%);
    mask-image: linear-gradient(180deg, #000 72%, transparent 99%);
  }
  .hero__portrait .stage__img {
    position: absolute; top: 0; left: 0; translate: none;
    height: auto; width: 100%; filter: none;
  }

  .hero__grid {
    order: 2;
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(1.35rem, 3.5svh, 2.25rem);
    margin-block-start: min(40vw, 14.5rem);
    padding-block-end: 0;
  }

  .headline { font-size: clamp(2.1rem, 6.5vw, 3.6rem); }
  .identity { margin-block-end: clamp(1.25rem, 3.4svh, 2.25rem); }
  .hero__note { justify-self: start; max-width: 34rem; margin-block-end: 0; }

  .hero__floats {
    order: 3;
    position: static;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    gap: clamp(.85rem, 3vw, 1.5rem);
    z-index: 3;
    margin-block-start: clamp(1.5rem, 4svh, 2.5rem);
  }
  .card {
    position: static;
    width: auto;
    backdrop-filter: blur(10px) saturate(1.1);
    -webkit-backdrop-filter: blur(10px) saturate(1.1);
  }
  .card__badge { translate: -34% -34%; }
  .is-loaded .card--traits,
  .is-loaded .card--skills { animation: reveal .85s var(--ease-out) var(--d) both; }}

/* Phone */
@media (max-width: 600px) {
  :root { --nav-h: 4rem; }

  .nav__name, .nav__divider { display: none; }

  .hero__portrait { width: 80vw; height: min(72vw, 17rem); }
  .hero__grid { margin-block-start: min(56vw, 12.5rem); }

  .headline {
    font-size: clamp(1.7rem, 8.9vw, 2.6rem);
    font-stretch: 79%;
    font-variation-settings: "wght" 900, "wdth" 79;
    letter-spacing: -.015em;
  }
  /* Stacked full width, 12px apart, label and arrow centred as one unit. */
  .hero__ctas { flex-direction: column; gap: .75rem; }
  .hero__ctas .btn { width: 100%; gap: .85rem; }

  .hero__floats { gap: .65rem; }
  .card { padding: .8rem .85rem; border-radius: var(--radius-lg); }
  .card__list li { padding-block: .4rem; font-size: var(--fs-2xs); letter-spacing: .08em; }
  .card__badge { width: 2.15rem; }}

/* Short viewports — protect the wordmark from crowding the content. The
   wordmark size is handled by the max-height: 800px rule above; this block only
   tightens the content stack. traits stays bottom-anchored, or setting both top
   and bottom would stretch the card. */
@media (min-width: 901px) and (max-height: 720px) {
  .identity { margin-block-end: 1.1rem; }
  .hero__ctas { margin-block-start: 1.15rem; }
  .card { padding-block: .8rem; }
  .card__list li { padding-block: .4rem; }
  .card--traits { top: auto; bottom: 52%; }
  .card--skills { top: 23.5%; }}

/* ------------------------------------------------------- 09. Section shell */

/* --pad-s is the section's own inline-start padding, published as a variable so
   the decorations that hang off the left of the content column — the gold
   thread, the giant J, the contact starburst — can track it instead of being
   pinned to the viewport edge. Section 16 widens it on large screens. */
.sec {
  --pad-s: var(--pad);
  position: relative;
  padding-inline-start: var(--pad-s);
  padding-inline-end: var(--pad);
  padding-block: clamp(4rem, 9svh, 7rem);
  overflow: clip;
  isolation: isolate;
}
/* 108px below one section plus 81px above the next measured as 189px of empty
   page between them at 900px tall. 81 + 63 keeps the editorial air without the
   gap reading as a missing block. */
.sec + .sec { padding-block-start: clamp(3.25rem, 7svh, 5.5rem); }

/* The burgundy-and-gold thread, running the height of every section. It draws
   itself once as the section arrives — `scale` only, so it costs nothing. */
.sec::before {
  content: "";
  position: absolute;
  left: calc(var(--pad-s) * .42);
  top: 0;
  bottom: 0;
  width: 2px;
  background: linear-gradient(180deg,
    rgba(90, 24, 48, 0), rgba(90, 24, 48, .32) 12%,
    rgba(184, 134, 47, .55) 50%, rgba(90, 24, 48, .32) 88%, rgba(90, 24, 48, 0));
  opacity: .85;
  transform-origin: top;
  pointer-events: none;
}
.js .sec::before { scale: 1 0; }
.js .sec--seen::before { scale: 1 1; transition: scale 1.4s var(--ease-out); }
@media (max-width: 900px) { .sec::before { display: none; }}

.sec__head { max-width: var(--measure); margin-block-end: clamp(2.5rem, 6svh, 4rem); }

.eyebrow {
  display: flex;
  align-items: center;
  gap: .8rem;
  margin-block-end: clamp(1rem, 2.4svh, 1.6rem);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--coral-ink);
}
.eyebrow__num {
  font-family: var(--f-serif);
  font-size: 1.55em;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--wine);
}
.eyebrow::after {
  content: "";
  flex: 1 1 auto;
  max-width: 5rem;
  height: 1px;
  background: linear-gradient(90deg, var(--gold-lo), rgba(184, 134, 47, 0));
}

.sec__title {
  font-family: var(--f-display);
  font-size: clamp(2rem, 4.4vw, 4rem);
  font-weight: 900;
  font-stretch: 89%;
  font-variation-settings: "wght" 900, "wdth" 89;
  letter-spacing: -.015em;
  line-height: .95;
  text-transform: uppercase;
  text-wrap: balance;
  color: var(--plum);
}

.sec__lede {
  max-width: 48ch;
  margin-block-start: clamp(1rem, 2.4svh, 1.5rem);
  font-size: clamp(1rem, 1.12vw, 1.15rem);
  line-height: 1.68;
  color: var(--plum-70);
  text-wrap: pretty;
}

.sec__close {
  max-width: var(--measure-tight);
  margin-block-start: clamp(2.5rem, 6svh, 4rem);
  font-size: clamp(1rem, 1.18vw, 1.15rem);
  line-height: 1.75;
  color: var(--plum-70);
  text-wrap: pretty;
}
.sec__close em { font-style: normal; font-weight: 600; color: var(--coral-ink); }

.text-link {
  display: inline-block;
  font-weight: 600;
  color: var(--wine);
  border-block-end: 1px solid var(--wine-28);
  padding-block-end: .1rem;
  transition: color .25s var(--ease-out), border-color .25s var(--ease-out);
}
.text-link:hover { color: var(--coral-ink); border-color: currentColor; }

/* Decorative sparkle, the JO monogram's counter redrawn as a mask. */
.spark {
  position: absolute;
  width: var(--sz, 1.1rem);
  aspect-ratio: 1;
  background: var(--gold-lo);
  -webkit-mask: var(--sparkle) center / contain no-repeat;
  mask: var(--sparkle) center / contain no-repeat;
  opacity: var(--op, .5);
  pointer-events: none;
}

/* ---------------------------------------------------------------- 10. About */

.sec--about { overflow: clip; }

/* The giant serif J, the section's watermark. Decorative — the heading and
   the brand link already announce the name. */
.about__mark {
  position: absolute;
  /* Hangs off the left of the content column, not the viewport. */
  left: calc(var(--pad-s) - clamp(4rem, 6vw, 7rem));
  top: 50%;
  z-index: 0;
  translate: 0 -50%;
  font-family: var(--f-serif);
  font-size: clamp(22rem, 46vw, 46rem);
  font-weight: 500;
  font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 1;
  line-height: .8;
  letter-spacing: -.05em;
  color: var(--wine);
  opacity: .07;
  pointer-events: none;
  user-select: none;
}

.about__grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.02fr) minmax(0, .98fr);
  gap: clamp(2rem, 5vw, 5.5rem);
  align-items: center;
  max-width: var(--measure);
}

.about__label {
  display: flex;
  align-items: center;
  gap: .8rem;
  margin-block-end: clamp(1rem, 2.4svh, 1.5rem);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--coral-ink);
}
.about__label::after {
  content: "";
  flex: 1 1 auto;
  max-width: 4.5rem;
  height: 1px;
  background: linear-gradient(90deg, var(--gold-lo), rgba(184, 134, 47, 0));
}

/* Serif, not the display face — this section is the reflective voice. */
.about__title {
  font-family: var(--f-serif);
  font-size: clamp(2.6rem, 6vw, 5.1rem);
  font-weight: 500;
  font-variation-settings: "opsz" 120, "SOFT" 0, "WONK" 0;
  letter-spacing: -.025em;
  line-height: .94;
  text-transform: uppercase;
  color: var(--wine);
  text-wrap: balance;
}

.about__rule {
  display: block;
  width: clamp(3rem, 5vw, 5.5rem);
  height: 2px;
  margin-block: clamp(1.3rem, 3svh, 2rem);
  border-radius: 2px;
  background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi));
}

.about__lede {
  max-width: 34ch;
  font-size: clamp(1.02rem, 1.24vw, 1.25rem);
  line-height: 1.72;
  color: var(--plum);
  text-wrap: pretty;
}
.about__body {
  max-width: 44ch;
  margin-block-start: 1.1rem;
  font-size: var(--fs-md);
  line-height: 1.72;
  color: var(--plum-70);
  text-wrap: pretty;
}

.about__chips { display: flex; flex-wrap: wrap; gap: .45rem .5rem; margin-block-start: clamp(1.4rem, 3.4svh, 2.1rem); }
.about__chips li {
  padding: .4rem .8rem;
  font-size: .75rem;
  font-weight: 500;
  letter-spacing: .04em;
  color: var(--plum-70);
  border: 1px solid var(--wine-14);
  border-radius: var(--radius-full);
}

.about__cta { margin-block-start: clamp(1.5rem, 3.6svh, 2.2rem); }

/* Figure column ---------------------------------------------------------- */

.about__figure {
  position: relative;
  justify-self: center;
  width: min(100%, 30rem);
  padding-block: clamp(1.5rem, 4svh, 3rem);
}

/* Soft blush panel behind the cutout — gives the portrait an editorial frame
   without boxing a transparent cutout inside a hard rectangle. */
.about__panel {
  position: absolute;
  inset: 0 12% 6% 4%;
  z-index: 0;
  border-radius: 14rem 14rem var(--radius-lg) var(--radius-lg);
  background: linear-gradient(165deg, var(--blush) 8%, var(--blush-wash) 62%, rgba(247, 227, 223, 0));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6);
}

.morphfig { position: relative; z-index: 1; }
.morphfig img {
  width: 100%;
  height: auto;
  filter: drop-shadow(0 2rem 3.5rem rgba(60, 18, 34, .18));
  /* The cutout ends mid-thigh, which reads as a mistake against the blush
     panel. A short fade at the extreme base turns the cut into a dissolve.
     Kept to the last 7% deliberately: the particle sampler reads the raw
     pixels, not the CSS mask, so any longer fade would show as a mismatch at
     the moment the cloud hands over to the photograph. */
  -webkit-mask-image: linear-gradient(180deg, #000 93%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 93%, transparent 100%);
}

/* The glass note, overlapping the portrait's lower-left as in the concept. */
.about__note {
  position: absolute;
  left: -4%;
  bottom: clamp(2rem, 8%, 5rem);
  z-index: 2;
  width: min(17.5rem, 62%);
  padding: clamp(1rem, 2vh, 1.4rem) clamp(1rem, 1.6vw, 1.35rem);
  background: var(--glass);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  box-shadow: var(--glass-shadow), inset 0 1px 0 rgba(255, 255, 255, .6);
  backdrop-filter: blur(16px) saturate(1.15);
  -webkit-backdrop-filter: blur(16px) saturate(1.15);
  font-size: var(--fs-xs);
  line-height: 1.62;
  color: var(--plum);
}
.about__note .card__badge { width: clamp(2.2rem, 2.9vw, 2.9rem); }

/* Marginal annotation, in the italic serif. */
.about__annot {
  position: absolute;
  right: -2%;
  top: clamp(1rem, 6%, 4rem);
  z-index: 2;
  max-width: 8.5rem;
  font-family: var(--f-serif);
  font-style: italic;
  font-size: clamp(.9375rem, 1.16vw, 1.15rem);
  line-height: 1.32;
  color: var(--coral-ink);
  transform: rotate(-6deg);
  text-align: center;
}
.about__annot::after {
  content: "";
  display: block;
  width: 2.6rem;
  height: 1px;
  margin: .5rem auto 0;
  background: linear-gradient(90deg, rgba(184, 134, 47, 0), var(--gold-lo), rgba(184, 134, 47, 0));
}

.about__figure .spark--1 { right: 12%; bottom: 14%; --sz: 1.35rem; --op: .55; }
.about__figure .spark--2 { right: 4%;  bottom: 8%;  --sz: .75rem;  --op: .38; }

@media (max-width: 900px) {
  .about__grid { grid-template-columns: minmax(0, 1fr); }
  .about__figure { order: -1; justify-self: start; width: min(100%, 24rem); }
  .about__mark { font-size: clamp(16rem, 60vw, 26rem); opacity: .05; top: 12%; translate: 0 0; }
  .about__note { width: min(15rem, 72%); left: 0; }
  .about__annot { right: 0; }}
@media (max-width: 600px) {
  .about__figure { width: 100%; }
  .about__note { position: static; width: 100%; margin-block-start: 1rem; }
  .about__annot { top: 0; font-size: .9375rem; }}

/* --------------------------------- 11. Selected work + device mockups */

.cases {
  max-width: var(--measure);
  display: grid;
  /* Was clamp(3.5rem, 10svh, 7rem) = 90px at 900px tall. The media column is
     ~106px shorter than the copy column and .case centres them, so half that
     slack already sits under the caption — 90px on top of it read as a hole. */
  gap: clamp(2.5rem, 6svh, 4.5rem);
}

.case {
  display: grid;
  gap: clamp(1.75rem, 4vw, 3.5rem);
  align-items: center;
}
@media (min-width: 1000px) {
  .case { grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); }
  /* Alternating rhythm: Gen Spa leads with the mockup, iBlast with the copy. */
  .case--flip .case__media { order: 2; }}

.case__tags { display: flex; flex-wrap: wrap; gap: .4rem .45rem; margin-block-end: clamp(.9rem, 2svh, 1.3rem); }
.case__tags li {
  padding: .34rem .7rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--wine);
  background: rgba(90, 24, 48, .06);
  border: 1px solid var(--wine-14);
  border-radius: var(--radius-full);
}

.case__title {
  font-family: var(--f-display);
  font-size: clamp(2rem, 4.6vw, 3.3rem);
  font-weight: 900;
  font-variation-settings: "wght" 900, "wdth" 84;
  letter-spacing: -.022em;
  line-height: .95;
  text-transform: uppercase;
  color: var(--wine);
}
.case__sub {
  margin-block-start: .55rem;
  font-family: var(--f-serif);
  font-size: clamp(1.05rem, 1.5vw, 1.35rem);
  font-weight: 600;
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 22;
  line-height: 1.3;
  color: var(--plum);
}
.case__body {
  max-width: 46ch;
  margin-block-start: clamp(.9rem, 2.2svh, 1.3rem);
  font-size: var(--fs-md);
  line-height: 1.75;
  color: var(--plum-70);
  text-wrap: pretty;
}

.case__role {
  margin-block-start: clamp(1.1rem, 2.6svh, 1.6rem);
  padding-inline-start: clamp(.9rem, 1.4vw, 1.35rem);
  border-inline-start: 2px solid;
  border-image: linear-gradient(180deg, var(--gold-hi), var(--gold-lo) 60%, rgba(184, 134, 47, 0)) 1;
  max-width: 44ch;
  font-size: clamp(.875rem, 1vw, .9688rem);
  line-height: 1.7;
  color: var(--plum-70);
}
.case__role b {
  display: block;
  margin-block-end: .2rem;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-ink);
}

.case__link {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  margin-block-start: clamp(1.2rem, 2.8svh, 1.8rem);
  padding-block: .5rem;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--wine);
  border-block-end: 1px solid var(--wine-28);
  transition: color .25s var(--ease-out), border-color .25s var(--ease-out);
}
.case__link svg {
  width: .95rem; height: .95rem; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  transition: translate .25s var(--ease-out);
}
.case__link:hover { color: var(--coral-ink); border-color: currentColor; }
.case__link:hover svg { translate: 3px -3px; }

/* Media column ----------------------------------------------------------- */

.case__media { position: relative; }
.case__media .spark--1 { left: -1.5%; top: 4%; --sz: 1.2rem; --op: .5; }

/* ---- Device frames, built in code so the screen stays replaceable ------- */

.device { position: relative; }
.device__screen { position: relative; overflow: clip; background: #16090f; }
.device__screen img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* Laptop */
.device--laptop .device__lid {
  padding: clamp(.4rem, .7vw, .6rem) clamp(.4rem, .7vw, .6rem) clamp(.5rem, .9vw, .75rem);
  background: linear-gradient(168deg, #4a2c37 0%, #2e1721 42%, #1d0e15 100%);
  border-radius: clamp(.6rem, 1vw, .95rem) clamp(.6rem, 1vw, .95rem) .25rem .25rem;
  box-shadow:
    0 1.6rem 3rem -1.4rem rgba(40, 19, 29, .55),
    inset 0 1px 0 rgba(255, 255, 255, .09);
}
.device--laptop .device__cam {
  width: 5px; height: 5px;
  margin: 0 auto clamp(.28rem, .5vw, .42rem);
  border-radius: 50%;
  background: #6b4a55;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .4);
}
.device--laptop .device__screen { aspect-ratio: 16 / 10; border-radius: .2rem; }
.device--laptop .device__base {
  position: relative;
  width: 116%;
  height: clamp(.6rem, 1.1vw, .95rem);
  margin-inline: -8%;
  background: linear-gradient(180deg, #efe6dc 0%, #d3c4b6 52%, #ab9a8e 100%);
  clip-path: polygon(0 0, 100% 0, 97.4% 100%, 2.6% 100%);
  filter: drop-shadow(0 .55rem .7rem rgba(40, 19, 29, .26));
}
.device--laptop .device__base::after {
  content: "";
  position: absolute;
  top: 0; left: 50%;
  translate: -50% 0;
  width: 15%;
  height: 32%;
  background: rgba(29, 14, 21, .18);
  border-radius: 0 0 .35rem .35rem;
}

/* Browser window on a stand */
.device--browser .device__chrome {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: clamp(.42rem, .72vw, .6rem) clamp(.55rem, .9vw, .8rem);
  background: linear-gradient(180deg, #f6efe6, #e7dcd0);
  border: 1px solid rgba(40, 19, 29, .12);
  border-block-end: 0;
  border-radius: clamp(.55rem, .9vw, .8rem) clamp(.55rem, .9vw, .8rem) 0 0;
}
.device__dots { display: flex; gap: .28rem; flex: none; }
.device__dots i { width: .48rem; height: .48rem; border-radius: 50%; background: rgba(40, 19, 29, .2); }
.device__dots i:first-child { background: rgba(201, 94, 81, .55); }
.device__url {
  flex: 1 1 auto;
  min-width: 0;
  padding: .18rem .6rem;
  background: rgba(255, 255, 255, .8);
  border-radius: var(--radius-full);
  font-size: var(--fs-2xs);
  letter-spacing: .04em;
  color: var(--plum-70);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.device--browser .device__screen {
  aspect-ratio: 16 / 10;
  border: 1px solid rgba(40, 19, 29, .12);
  border-block-start: 0;
  border-radius: 0 0 .3rem .3rem;
  box-shadow: var(--shadow-lift);
}
.device--browser .device__stand {
  width: 26%;
  height: clamp(.9rem, 1.8vw, 1.6rem);
  margin-inline: auto;
  background: linear-gradient(180deg, #ded2c6, #c0b0a3);
  clip-path: polygon(34% 0, 66% 0, 100% 100%, 0 100%);
}
.device--browser .device__foot {
  width: 42%;
  height: clamp(.35rem, .6vw, .5rem);
  margin-inline: auto;
  background: linear-gradient(180deg, #d6c9bc, #ab9a8e);
  border-radius: var(--radius-full);
  filter: drop-shadow(0 .4rem .55rem rgba(40, 19, 29, .22));
}

/* Phone */
.device--phone .device__body {
  padding: clamp(.22rem, .38vw, .34rem);
  background: linear-gradient(158deg, #4a2c37, #2a1520 55%, #1a0c12);
  border-radius: var(--radius-lg);
  box-shadow:
    0 1.4rem 2.6rem -1.1rem rgba(40, 19, 29, .5),
    inset 0 1px 0 rgba(255, 255, 255, .12);
}
.device--phone .device__screen {
  aspect-ratio: 390 / 844;
  border-radius: var(--radius-lg);
}
.device--phone .device__island {
  position: absolute;
  top: clamp(.28rem, .5vw, .45rem);
  left: 50%;
  translate: -50% 0;
  z-index: 2;
  width: 27%;
  height: clamp(.5rem, .85vw, .75rem);
  border-radius: var(--radius-full);
  background: #12070c;
}

/* Composition: phone overlapping the larger device, as in the concept. */
.stack { position: relative; }
.stack__phone {
  position: absolute;
  z-index: 3;
  width: 26%;
  min-width: 5.5rem;
}
.stack--left .stack__phone  { left: -3%;  bottom: -8%; }
.stack--right .stack__phone { right: -3%; bottom: -6%; }

/* Lemon Refresh sets its headline in the lower-left of the hero, exactly where
   the phone lands by default — it read as "…blog heat, …t your …ch." Moving the
   phone outboard rather than down clears the headline AND the caption beneath;
   dropping it instead just traded the headline for "LIVE SITE · LEM…". The
   section clips, so the overhang cannot create horizontal overflow. */
.stack--lowleft .stack__phone { left: -13%; bottom: -6%; width: 24%; }
@media (max-width: 999px) { .stack--lowleft .stack__phone { left: -8%; }}

/* Caption under a mockup — what the screen actually is.
   The phone in the stack hangs below the laptop on a negative offset, and at
   1440 its base measured 7px into this line, sitting over "LIVE SITE ·". The
   margin now clears it. Size was 11.5px at weight 500 with .13em tracking —
   contrast was fine at 6.4:1, but that combination is simply too small to
   read; 13px at 600 fixes the legibility rather than the colour. */
.case__cap {
  margin-block-start: clamp(2.1rem, 4.5vh, 3rem);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--plum-70);
}
.case__cap b { font-weight: 700; color: var(--wine); }

@media (max-width: 560px) {
  .stack__phone { width: 30%; }
  .stack--left .stack__phone  { left: -2%; bottom: -6%; }
  .stack--right .stack__phone { right: -2%; bottom: -5%; }
  .case__cap { margin-block-start: 2.4rem; }}

/* Video strip ------------------------------------------------------------ */

.reelhead {
  max-width: var(--measure);
  /* 81px of margin plus 45px of padding above the rule left a 130px void
     between the last case study and this divider. */
  margin-block-start: clamp(2.5rem, 6svh, 4rem);
  padding-block-start: clamp(1.5rem, 3.5svh, 2.25rem);
  border-block-start: 1px solid var(--wine-14);
}
.reelhead__k {
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--coral-ink);
}
.reelhead__t {
  max-width: 40ch;
  margin-block-start: .75rem;
  font-family: var(--f-serif);
  font-size: clamp(1.3rem, 2.2vw, 1.85rem);
  font-weight: 600;
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 40;
  line-height: 1.28;
  color: var(--plum);
  text-wrap: balance;
}

.reel {
  max-width: var(--measure);
  margin-block-start: clamp(1.75rem, 4svh, 2.5rem);
  display: grid;
  gap: clamp(.85rem, 2vw, 1.4rem);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 900px) { .reel { grid-template-columns: repeat(3, minmax(0, 1fr)); }}

/* Hidden by default; the carousel query below turns them on. Declared before
   that query so source order does not defeat it — both selectors are one
   class, so whichever comes last would otherwise win. */
.reel__dots { display: none; }

/* Phones get a swipe carousel instead of a stack. At two columns a clip was
   168x95 css px — too small to read a face in, let alone judge a grade. Six
   full-width clips stacked fixed that but cost about a thousand pixels of
   scrolling. One clip per swipe gives a 300px-wide tile, keeps the section to
   roughly a screen, and swiping is the gesture a phone actually wants.
   The carousel scrolls, not the page, so this adds no horizontal overflow. */
@media (max-width: 640px) {
  .reel {
    display: flex;
    gap: .75rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--pad, 1.25rem);
    padding-inline: var(--pad, 1.25rem);
    /* break out of the section's padding so tiles can run to both edges */
    margin-inline: calc(var(--pad, 1.25rem) * -1);
    /* ...and drop the desktop measure cap, which otherwise held the strip at
       335px on a 375px phone: the right 40px showed bare page, the tiles were
       cut short of the edge, and centre-snap landed visibly off-centre. */
    max-width: none;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .reel::-webkit-scrollbar { display: none; }
  .reel__item {
    /* vw, not %, because the percentage would resolve against the carousel's
       padded content box and came out 254px rather than the intended ~330. */
    flex: 0 0 min(84vw, 22rem);
    scroll-snap-align: center;
  }
  /* Position dots, filled in by JS. Purely an orientation aid. */
  .reel__dots {
    display: flex;
    justify-content: center;
    gap: .4rem;
    margin-block-start: .9rem;
  }
  .reel__dots i {
    width: .4rem; height: .4rem;
    border-radius: 50%;
    background: var(--wine-28);
    transition: background .25s var(--ease-out), scale .25s var(--ease-out);
  }
  .reel__dots i.on { background: var(--wine); scale: 1.5; }}

.reel__fig {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: clip;
  background: var(--plum);
  border: 1px solid var(--wine-14);
  box-shadow: var(--shadow-lift);
}
.reel__v {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--plum);
}
.reel__cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  /* The overlap this had with the sound button was vertical, not horizontal:
     on a 168x95 tile the caption's own box (2.2rem of gradient + two wrapped
     lines) grew to 94px and swallowed the button sitting at the top. Going one
     clip per row makes the tile ~197px tall, which resolves it. The right pad
     is belt-and-braces for a long caption at the two-column breakpoint. */
  padding: 2.2rem max(.8rem, 3.9rem) .7rem .8rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: #fff;
  background: linear-gradient(180deg, rgba(40, 19, 29, 0), rgba(40, 19, 29, .78));
  pointer-events: none;
}
.reel__sound {
  position: absolute;
  top: .55rem; right: .55rem;
  display: grid;
  place-items: center;
  /* 44px — the minimum comfortable touch target on a phone. */
  width: 2.75rem; height: 2.75rem;
  border-radius: 50%;
  cursor: pointer;
  color: #fff;
  /* The frame behind this is video, so it can be any luminance at any moment;
     the pill has to carry its own contrast rather than borrow it. At .82 the
     weakest case measures over 10:1. */
  background: rgba(40, 19, 29, .82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, .34);
  box-shadow: 0 2px 10px -3px rgba(20, 6, 12, .55);
  transition: background .25s var(--ease-out), scale .25s var(--ease-out);
}
.reel__sound:hover { background: rgba(40, 19, 29, .95); scale: 1.06; }
.reel__sound svg {
  width: 1.15rem; height: 1.15rem;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.reel__spk { fill: currentColor; stroke-width: 1.2; }
.reel__on  { display: none; }
.reel__sound[aria-pressed="true"] .reel__off { display: none; }
.reel__sound[aria-pressed="true"] .reel__on  { display: block; }
.reel__sound[aria-pressed="true"] { background: var(--coral-ink); border-color: transparent; }

/* ------------------------------------------------------------- 12. Services */

.sec--services { background: linear-gradient(180deg, var(--porcelain) 0%, var(--porcelain-hi) 55%, var(--porcelain) 100%); }

.svcs {
  max-width: var(--measure);
  display: grid;
  gap: clamp(.85rem, 1.7vw, 1.35rem);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 900px) {
  /* Four groups on a 2x2. The two burgundy cards take the top row, which is the
     hierarchy the repositioning asks for: websites and search carry the
     commercial weight, marketing and video support them. */
  .svcs { grid-template-columns: repeat(2, minmax(0, 1fr)); }}
@media (max-width: 620px) { .svcs { grid-template-columns: minmax(0, 1fr); }}

.svc {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: .55rem;
  padding: clamp(1.25rem, 2.4vw, 2rem);
  border-radius: var(--radius-lg);
  background: var(--porcelain-hi);
  border: 1px solid var(--wine-14);
  overflow: clip;
  transition: transform .45s var(--ease-out), box-shadow .45s var(--ease-out), border-color .45s var(--ease-out);
}
.svc::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .5s var(--ease-out);
}
.svc:hover, .svc:focus-within {
  transform: translateY(-4px);
  border-color: var(--wine-28);
  box-shadow: var(--shadow-lift);
}
.svc:hover::after, .svc:focus-within::after { transform: scaleX(1); }

/* Burgundy modules — the palette's dominant note, alternating through the
   grid so it carries the section rather than the cream. */
.svc--dark {
  background: linear-gradient(158deg, var(--wine-hi) 0%, var(--wine) 48%, var(--wine-lo) 100%);
  border-color: transparent;
  color: var(--porcelain);
}
.svc--dark .svc__n    { color: var(--gold); }
.svc--dark .svc__name { color: var(--porcelain); }
.svc--dark .svc__desc { color: rgba(248, 242, 233, .82); }
.svc--dark .svc__icon { border-color: rgba(243, 215, 211, .3); color: var(--gold); }

/* One line of evidence per capability group. The section used to assert and move
   on; naming the proof under each claim is what makes it read as a record
   rather than a menu. */
.svc__proof {
  margin-block-start: auto;
  padding-block-start: .85rem;
  border-block-start: 1px solid var(--wine-14);
  font-size: clamp(.8125rem, .9vw, .875rem);
  font-weight: 500;
  line-height: 1.6;
  color: var(--gold-ink);
}
.svc--dark .svc__proof { border-block-start-color: rgba(243, 215, 211, .24); color: var(--gold); }

.svc__top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-block-end: .3rem; }

.svc__n {
  font-family: var(--f-serif);
  font-size: var(--fs-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  color: var(--gold-ink);
}

.svc__icon {
  display: grid;
  place-items: center;
  width: clamp(2.4rem, 3.2vw, 3rem);
  aspect-ratio: 1;
  flex: none;
  border: 1px solid var(--wine-28);
  border-radius: var(--radius);
  color: var(--wine);
  transition: transform .5s var(--ease-out);
}
.svc__icon svg {
  width: 52%; height: auto;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
.svc:hover .svc__icon { transform: rotate(-6deg) scale(1.06); }

.svc__name {
  font-family: var(--f-serif);
  font-size: clamp(1.15rem, 1.55vw, 1.42rem);
  font-weight: 600;
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 30;
  line-height: 1.2;
  color: var(--plum);
  text-wrap: balance;
}
.svc__desc {
  font-size: var(--fs-sm);
  line-height: 1.68;
  color: var(--plum-70);
  text-wrap: pretty;
}

/* -------------------------------------------------------------- 13. Process */

.steps {
  --gap: clamp(2rem, 5vw, 2.5rem);
  --mark: 3.2rem;                 /* marker diameter — the connector's anchor */
  position: relative;
  max-width: var(--measure);
  display: grid;
  gap: var(--gap);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 820px) {
  /* Subgrid, so the four steps share row tracks and every band — marker,
     number, name, description, deliverable — lands on the same line across all
     four. Flex could equalise the step heights but not the bands inside them:
     whichever child absorbed the slack pushed the "You get" rule off the line
     its neighbours sat on, by 20px at ~900px where one deliverable wraps to a
     third line. Browsers without subgrid fall back to auto rows, which is the
     behaviour these steps had before. */
  .steps {
    --gap: clamp(1.25rem, 3vw, 2.5rem);
    grid-template-columns: repeat(4, minmax(0, 1fr));
    grid-template-rows: auto auto auto 1fr auto;
    /* Column gutter only. There is one row of steps at this width, so a row
       gap between the parent's tracks is not separation between steps — it is
       separation between the bands inside every step, and it pushed 18px
       between each number, name and description. */
    column-gap: var(--gap);
    row-gap: 0;
  }
  .step {
    grid-row: span 5;
    grid-template-rows: subgrid;
    row-gap: 0;
  }}

/* The connector: one line behind the row of markers, drawn as the section
   arrives. On the stacked layout it runs vertically instead. */
/* Stacked: one segment per step, drawn from the bottom of that step's marker
   to the top of the next one. Spanning the whole list with a single rule ran
   it past the last marker to the container's bottom edge — which is wherever
   step 04's text happens to end, ~200px too far. Segments chain exactly and
   need no knowledge of the copy length. */
.step:not(:last-child)::before {
  content: "";
  position: absolute;
  z-index: 0;
  left: calc(var(--mark) / 2 - 1px);
  top: var(--mark);
  height: calc(100% - var(--mark) + var(--gap));
  width: 2px;
  background: linear-gradient(180deg, var(--gold-hi), var(--gold-lo));
  opacity: .5;
  transform-origin: top;
  scale: 1 0;
  transition: scale .9s var(--ease-out);
}
.sec--seen .step:not(:last-child)::before { scale: 1 1; }
.sec--seen .step:nth-child(2)::before { transition-delay: .12s; }
.sec--seen .step:nth-child(3)::before { transition-delay: .24s; }
@media (min-width: 820px) {
  /* Run the connector through the centre of marker 1 to the centre of marker 4.
     With four equal columns and gap G, one column is 25% - 0.75G wide, so the
     last marker's centre sits that far in from the right edge, plus half a
     marker. Getting this from the geometry rather than eyeballing a percentage
     is what keeps the line on the icons at every width. */
  .steps::before {
    content: "";
    position: absolute;
    z-index: 0;
    left: calc(var(--mark) / 2);
    right: calc(25% - (var(--gap) * .75) - (var(--mark) / 2));
    top: calc(var(--mark) / 2);
    width: auto;
    height: 2px;
    background: linear-gradient(90deg, var(--gold-lo), var(--gold-hi) 50%, var(--gold-lo));
    opacity: .5;
    transform-origin: left;
    scale: 0 1;
    transition: scale 1.6s var(--ease-out) .15s;
  }
  .sec--seen .steps::before { scale: 1 1; }
  /* The stacked per-step segments belong to the one-column layout only. */
  .step:not(:last-child)::before { display: none; }}

/* Stacked layout: marker in its own left gutter, copy in the column beside it.
   The connector runs down the centre of that gutter at calc(var(--mark) / 2),
   so with the copy starting after the gutter the line can never cross a glyph.
   Before this the copy began at the container edge and the rule was drawn
   straight through the words. It also reclaims the marker's full height per
   step, which is what made the section feel so sparse on a phone. */
.step {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: var(--mark) minmax(0, 1fr);
  column-gap: clamp(.9rem, 3.5vw, 1.15rem);
  align-items: start;
}
/* `1 / -1` rather than a counted span — a fourth child was added to each step
   and a `span 3` marker would have stopped short of it. */
.step__mark { grid-row: 1 / -1; margin-block-end: 0; }
.step__n, .step__name, .step__desc, .step__out { grid-column: 2; }

@media (min-width: 820px) {
  /* Four across — marker sits above its copy again. Stays a grid (see the
     subgrid rule above) rather than reverting to block. */
  .step { grid-template-columns: minmax(0, 1fr); }
  .step__mark { grid-row: auto; margin-block-end: clamp(1rem, 2.4svh, 1.5rem); }
  .step__n, .step__name, .step__desc, .step__out { grid-column: 1; }}

.step__mark {
  display: grid;
  place-items: center;
  width: var(--mark);
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--porcelain);
  border: 1px solid var(--wine-28);
  color: var(--wine);
  transition: background .4s var(--ease-out), color .4s var(--ease-out), border-color .4s var(--ease-out);
}
.step__mark svg {
  width: 46%; height: auto;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
.step:hover .step__mark { background: var(--wine); border-color: transparent; color: var(--gold-hi); }

.step__n {
  font-family: var(--f-serif);
  font-size: var(--fs-sm);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--gold-ink);
}
.step__name {
  margin-block-start: .3rem;
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--wine);
}
.step__desc {
  max-width: 30ch;
  margin-block-start: .7rem;
  font-size: var(--fs-sm);
  line-height: 1.7;
  color: var(--plum-70);
  text-wrap: pretty;
}
/* The lede promises "something you can look at and decide on at the end of each
   one" and then no stage said what that something was. This is the promise
   being kept: one named artefact per stage, set apart from the description so
   it reads as a commitment rather than more prose. */
.step__out {
  max-width: 30ch;
  margin-block-start: .85rem;
  padding-block-start: .75rem;
  border-block-start: 1px solid var(--wine-14);
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--plum);
  text-wrap: pretty;
}
.step__out span {
  display: block;
  margin-block-end: .25rem;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--gold-ink);
}


/* Capability ribbon ------------------------------------------------------ */

.ribbon {
  max-width: var(--measure);
  margin-block-start: clamp(3rem, 8svh, 5rem);
  padding-block: clamp(.85rem, 2vh, 1.15rem);
  border-radius: var(--radius-full);
  background: linear-gradient(100deg, var(--wine-lo), var(--wine) 45%, var(--wine-hi) 100%);
  overflow: clip;
  box-shadow: var(--shadow-lift);
}
.ribbon__track {
  display: flex;
  width: max-content;
  animation: ribbon 38s linear infinite;
}
.ribbon:hover .ribbon__track { animation-play-state: paused; }
.ribbon__set { display: flex; flex: none; }
.ribbon__set li {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 3vw, 2.75rem);
  padding-inline-end: clamp(1.25rem, 3vw, 2.75rem);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--porcelain);
  white-space: nowrap;
}
.ribbon__set li::after {
  content: "";
  width: .3rem;
  height: .3rem;
  flex: none;
  border-radius: 50%;
  background: var(--gold);
}
@keyframes ribbon { to { translate: -50% 0; } }

/* --------------------------------------------------- 14. Contact + footer */

.sec--contact {
  background: linear-gradient(168deg, var(--wine-hi) 0%, var(--wine) 42%, var(--wine-lo) 100%);
  color: var(--porcelain);
  padding-block-end: clamp(2.5rem, 6svh, 4rem);
}
.sec--contact::before {
  background: linear-gradient(180deg,
    rgba(212, 162, 76, 0), rgba(212, 162, 76, .45) 14%,
    rgba(236, 211, 164, .7) 50%, rgba(212, 162, 76, .45) 86%, rgba(212, 162, 76, 0));
}
.sec--contact .eyebrow { color: var(--gold); }
.sec--contact .eyebrow__num { color: var(--gold-hi); }
.sec--contact .eyebrow::after { background: linear-gradient(90deg, var(--gold), rgba(212, 162, 76, 0)); }

.contact__grid {
  position: relative;
  z-index: 1;
  max-width: var(--measure);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, .82fr);
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}
@media (max-width: 900px) { .contact__grid { grid-template-columns: minmax(0, 1fr); }}

.contact__title {
  font-family: var(--f-serif);
  font-size: clamp(2.4rem, 5.6vw, 4.6rem);
  font-weight: 500;
  font-variation-settings: "opsz" 120, "SOFT" 0, "WONK" 0;
  letter-spacing: -.025em;
  line-height: .96;
  text-transform: uppercase;
  color: var(--porcelain);
  max-width: 13ch;
  text-wrap: balance;
}
.contact__lede {
  max-width: 44ch;
  margin-block-start: clamp(1.2rem, 3svh, 1.9rem);
  font-size: clamp(1rem, 1.12vw, 1.125rem);
  line-height: 1.72;
  color: rgba(248, 242, 233, .84);
  text-wrap: pretty;
}
.contact__actions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  width: fit-content;
  max-width: 100%;
  gap: clamp(.75rem, 1.15vw, 1.2rem);
  margin-block-start: clamp(1.6rem, 3.6svh, 2.4rem);
}
@media (max-width: 600px) {
  .contact__actions { grid-auto-flow: row; width: 100%; gap: .75rem; }
  .contact__actions .btn { width: 100%; gap: .85rem; }}
.contact__direct { margin-block-start: clamp(1.6rem, 3.4svh, 2.2rem); }
.contact__direct a {
  font-family: var(--f-serif);
  font-size: clamp(1.05rem, 1.5vw, 1.45rem);
  font-weight: 600;
  color: var(--gold-hi);
  border-block-end: 1px solid rgba(236, 211, 164, .4);
  padding-block-end: .2rem;
  transition: color .3s var(--ease-soft), border-color .3s var(--ease-soft);
}
.contact__direct a:hover { color: #fff; border-color: currentColor; }

/* The starburst behind the headline, echoing the concept's ornament. */
.contact__burst {
  position: absolute;
  left: calc(var(--pad-s) - clamp(3rem, 5vw, 6rem));
  top: clamp(-2rem, -4vh, -1rem);
  z-index: 0;
  width: clamp(9rem, 16vw, 15rem);
  aspect-ratio: 1;
  opacity: .3;
  pointer-events: none;
}
.contact__burst svg { width: 100%; height: 100%; fill: none; stroke: var(--gold); stroke-width: .7; }
@media (max-width: 900px) { .contact__burst { display: none; }}

/* Portrait, right edge, treated so it reads as a silhouette against the
   burgundy rather than a repeat of the hero photograph. */
.contact__portrait {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 0;
  width: clamp(14rem, 24vw, 24rem);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(200deg, #000 42%, transparent 88%);
  mask-image: linear-gradient(200deg, #000 42%, transparent 88%);
  mix-blend-mode: luminosity;
  opacity: .38;
}
.contact__portrait img { width: 100%; height: auto; }
@media (max-width: 1100px) { .contact__portrait { display: none; }}

/* Form ------------------------------------------------------------------- */

.inquiry {
  position: relative;
  z-index: 1;
  padding: clamp(1.4rem, 3svh, 2.2rem) clamp(1.2rem, 2.2vw, 2rem);
  background: rgba(255, 255, 255, .07);
  border: 1px solid rgba(243, 215, 211, .22);
  border-radius: var(--radius-lg);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
.inquiry__title {
  margin-block-end: clamp(1rem, 2.4svh, 1.5rem);
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold);
}
.field { margin-block-end: clamp(.85rem, 2svh, 1.15rem); }
.field label {
  display: block;
  margin-block-end: .4rem;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--porcelain);
}
/* Marks the one optional text field. Weight, not a dimmer colour, carries the
   difference — the label sits on the wine gradient, where lowering opacity
   would cost contrast. */
.field__opt { font-weight: 400; }
/* min-height, not padding: the control's height used to fall out of its font
   size, so mapping the form onto the type scale quietly took the select to
   43px — one pixel under a comfortable touch target. Pinning it here means the
   target no longer depends on typography. */
.field input, .field select, .field textarea {
  min-height: 44px;
  width: 100%;
  padding: .7rem .85rem;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--plum);
  background: rgba(255, 255, 255, .9);
  border: 1px solid rgba(243, 215, 211, .3);
  border-radius: var(--radius);
  transition: box-shadow .25s var(--ease-soft), background .25s var(--ease-soft);
}
.field textarea { resize: vertical; min-height: 6rem; }
/* iOS Safari zooms the whole viewport when a control smaller than 16px takes
   focus, and never zooms back out — on the one form that matters. --fs-sm
   resolves to 14px on a phone, so lift the controls to exactly 16px on touch
   devices. The fix is the control's own size, not a viewport lock: pinch-zoom
   stays available. Labels, spacing and every other type size are untouched. */
@media (pointer: coarse), (max-width: 900px) {
  .field input, .field select, .field textarea { font-size: 1rem; }
}
.field :is(input, select, textarea):focus-visible {
  outline: 2px solid var(--gold-hi);
  outline-offset: 2px;
  background: #fff;
}
.inquiry .btn { width: 100%; justify-content: space-between; margin-block-start: .4rem; }

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

.endmark {
  position: relative;
  z-index: 1;
  max-width: var(--measure);
  margin-block-start: clamp(3.5rem, 9svh, 6rem);
  padding-block-start: clamp(2rem, 5svh, 3rem);
  border-block-start: 1px solid rgba(243, 215, 211, .2);
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
  align-items: center;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 860px) { .endmark { grid-template-columns: auto 1fr auto; }}

.endmark__brand { display: flex; align-items: center; gap: .9rem; }
.endmark__mono {
  font-family: var(--f-serif);
  font-size: clamp(1.9rem, 3vw, 2.6rem);
  font-weight: 600;
  letter-spacing: -.045em;
  line-height: .9;
  color: var(--gold-hi);
}
.endmark__name {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--porcelain);
}
.endmark__role {
  margin-block-start: .28rem;
  font-size: .75rem;
  letter-spacing: .05em;
  color: rgba(248, 242, 233, .72);
}

.endmark__links { display: flex; flex-wrap: wrap; gap: .85rem 1.6rem; }
@media (min-width: 860px) { .endmark__links { justify-content: center; }}
.endmark__links a, .endmark__place {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding-block: .45rem;
  font-size: .8125rem;
  letter-spacing: .04em;
  color: rgba(248, 242, 233, .88);
}
.endmark__links a { transition: color .25s var(--ease-out); }
.endmark__links a:hover { color: var(--gold-hi); }
/* .endmark__place shares the layout rule above but used to miss this one, so its
   pin had no width and no `fill: none` — an inline SVG with a viewBox and no
   size stretches to fill its container, and the default fill is solid black.
   It rendered as a ~140x175px black blob over the footer on phones. */
.endmark__links svg,
.endmark__place svg {
  width: 1rem; height: 1rem; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
  color: var(--gold);
}
.endmark__place { color: rgba(248, 242, 233, .72); }

/* -------------------------------------------- 15. Signature transition */

/* Progress out of the hero, 0 → 1, written on <html> by main.js so the whole
   hero can read it. The ramps below slice that one value into hand-off stages. */
:root {
  --t: 0;
  --cloud-in:   clamp(0, calc((var(--t) - .015) / .070), 1);
  --photo-out:  clamp(0, calc((var(--t) - .085) / .050), 1);
  --photo-in:   clamp(0, calc((var(--t) - .870) / .055), 1);
  --cloud-out:  clamp(0, calc((var(--t) - .925) / .045), 1);
  --settle:     clamp(0, calc((var(--t) - .930) / .055), 1);
}

/* Hero exit. opacity needs !important: the entrance keyframes use fill-mode
   `both`, which pins opacity at 1 above any normal declaration. transform is
   free — the property budget reserves it for parallax, and the --px/--py terms
   are carried through so the two compose instead of fighting. */

.hero-exit .hero .hero__wordmark {
  transform: translate3d(calc(var(--px, 0px) - var(--t) * 46vw), var(--py, 0px), 0);
  opacity: calc(1 - var(--t) * 1.9) !important;
}
.hero-exit .hero .identity {
  transform: translate3d(calc(var(--t) * -6rem), 0, 0);
  opacity: calc(1 - var(--t) * 2.7) !important;
}
.hero-exit .hero .headline__in { opacity: calc(1 - var(--t) * 2.4) !important; }
.hero-exit .hero .headline__line:nth-child(1) .headline__in { transform: translate3d(0, calc(var(--t) * -5.5rem), 0); }
.hero-exit .hero .headline__line:nth-child(2) .headline__in { transform: translate3d(0, calc(var(--t) * -7.2rem), 0); }
.hero-exit .hero .headline__line:nth-child(3) .headline__in { transform: translate3d(0, calc(var(--t) * -8.9rem), 0); }
.hero-exit .hero .hero__ctas {
  transform: translate3d(0, calc(var(--t) * -10.5rem), 0);
  opacity: calc(1 - var(--t) * 2.9);
}
.hero-exit .hero .hero__note {
  transform: translate3d(0, calc(var(--t) * -7rem), 0);
  opacity: calc(1 - var(--t) * 2.5) !important;
}
.hero-exit .hero .card { opacity: calc(1 - var(--t) * 2.3) !important; }
.hero-exit .hero .card--traits { transform: translate3d(var(--px, 0px), calc(var(--py, 0px) - var(--t) * 12rem), 0); }
.hero-exit .hero .card--skills { transform: translate3d(var(--px, 0px), calc(var(--py, 0px) - var(--t) * 15rem), 0); }

.hero-gone .hero .hero__wordmark,
.hero-gone .hero .hero__floats,
.hero-gone .hero .hero__grid { visibility: hidden; }

.stage__img {
  position: absolute;
  top: 0;
  left: 50%;
  height: 100%;
  width: auto;
  max-width: none;
  translate: -50% 0;
  object-fit: contain;
  object-position: top center;
}
.stage__img--a { filter: drop-shadow(0 2.5rem 4rem rgba(60, 18, 34, .18)); }

.morph-on .stage__img--a {
  filter: drop-shadow(0 2.5rem 4rem rgba(60, 18, 34, .18)) blur(calc(var(--cloud-in) * 7px));
  opacity: calc(1 - var(--photo-out));
}

#morph {
  position: fixed;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
}
/* The hand-off runs in sequence rather than as a crossfade — two half-opaque
   copies would let a quarter of the porcelain through and the figure would go
   pale. The layer underneath is always fully opaque; only the top layer moves:

     .015 → .085   cloud fades UP over the still-solid hero photo
     .085 → .135   hero photo stands down, now hidden by the cloud
     .870 → .930   landing photo comes up UNDER the cloud (unseen, but ready)
     .930 → 1.00   cloud fades DOWN, revealing a photo already opaque         */
.morph-on #morph {
  opacity: calc(var(--cloud-in) * (1 - var(--cloud-out)));
  visibility: visible;
}

/* The About portrait is the particle cloud's landing site. The gate below is
   scoped to .morph-ready, which main.js adds only once it has confirmed the
   engine can actually run — otherwise a no-JS visitor, a tainted canvas or
   reduced motion would leave the portrait permanently invisible, because --t
   never rises and --photo-in stays at 0. Below 1100px there is no cloud, so
   the portrait is simply visible. */
@media (min-width: 1100px) {
  .morph-ready .morphfig img {
    /* She arrives out of focus and settles into it, so the cloud resolves into
       a photograph instead of being replaced by one. Both effects are also
       multiplied by --photo-in, so while she is invisible they are exactly
       zero — a 9px blur still costs a composite every frame at opacity 0. */
    opacity: var(--photo-in);
    filter: drop-shadow(0 2rem 3.5rem rgba(60, 18, 34, .18))
            blur(calc(var(--photo-in) * (1 - var(--settle)) * 9px));
    scale: calc(1 + var(--photo-in) * (1 - var(--settle)) * .014);
  }
  /* The blush panel and the notes do not travel with the particles, so they
     arrive on the ordinary scroll reveal instead of waiting for --photo-in. */
}

/* Fallback when pixel access is unavailable (page opened straight from disk).
   Not the particle transport, but still a dissolve — never a hard cut. */
.morph-fallback .stage__img--a {
  filter: blur(calc(var(--t) * 26px));
  scale: calc(1 - var(--t) * .06);
}
.morph-fallback .morphfig img,
.morph-fallback.morph-ready .morphfig img { opacity: 1; filter: drop-shadow(0 2rem 3.5rem rgba(60, 18, 34, .18)); scale: 1; }

/* ------------------------------------------------------------- 16. Rail */

.rail {
  position: fixed;
  left: clamp(.6rem, 1.4vw, 1.5rem);
  top: 50%;
  z-index: 30;
  translate: 0 -50%;
  display: grid;
  gap: 1.1rem;
  padding: .85rem .5rem;
  background: rgba(255, 255, 255, .58);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-full);
  box-shadow: var(--glass-shadow);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  opacity: 0;
  visibility: hidden;
  transform: translateX(-.6rem);
  transition: opacity .45s var(--ease-out), transform .45s var(--ease-out), visibility .45s;
}
.rail.is-on { opacity: 1; visibility: visible; transform: translateX(0); }

.rail__mono {
  font-family: var(--f-serif);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -.04em;
  text-align: center;
  color: var(--wine);
}
/* The visible marks are 7px dots, but the hit area must clear the WCAG 2.2
   24x24 minimum — so the dots keep their spacing via a negative margin while
   each link carries a full-size target. */
.rail__list { display: grid; gap: .2rem; }
.rail__list a {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  font-size: 0;
  color: var(--plum-70);
}
.rail__list i {
  display: block;
  width: .42rem;
  height: .42rem;
  border-radius: 50%;
  background: var(--wine-28);
  transition: background .3s var(--ease-soft), scale .3s var(--ease-out);
}
.rail__list a:hover i { background: var(--coral-ink); scale: 1.3; }
.rail__list a[aria-current="true"] i { background: var(--wine); scale: 1.45; }

@media (max-width: 1099px) { .rail { display: none; }}
@media (min-width: 1100px) {
  /* Two jobs at once.
     1. Clear the side rail: the content never starts closer to the left edge
        than --rail-gutter.
     2. Centre the content column once the viewport is wider than it. Without
        this the column stays pinned to the left gutter and everything past
        ~1250px is dead space — which is exactly what a 1900px window showed. */
  .sec {
    --rail-gutter: clamp(6.5rem, 9vw, 10rem);
    --pad-s: max(var(--rail-gutter), (100% - var(--measure-max)) / 2);
    padding-inline-end: max(var(--pad), (100% - var(--measure-max)) / 2);
  }
  /* The thread rides just outside the content column rather than the viewport. */
  .sec::before { left: calc(var(--pad-s) - clamp(1.9rem, 2.6vw, 3rem)); }}

/* ============================================================================
   17b. MOBILE REFINEMENTS
   Everything here was written against measurements taken at 320/360/375/390/
   430px, not guessed at. It sits before the reduced-motion block on purpose,
   so that block still gets the last word on anything that moves.
   ========================================================================= */

/* ---------------------------------------------- tap targets and legibility */

/* A control smaller than ~44px is hard to hit with a thumb. These were the
   nine that measured under it: both case-study links (164x36), the work
   library link (181x31), the skip link, the brand lockup, and the footer
   rows. Rather than inflate the visible type, the hit area is grown with
   padding and a pseudo-element so the design is unchanged but the target
   is not. */
@media (max-width: 900px) {
  .case__link,
  .text-link,
  .endmark__links a,
  .reelhead a {
    position: relative;
    min-height: 44px;
  }
  /* These three measured under 44 and were missed by the group above: the
     brand lockup (31x34), the skip link (43 by one pixel), and the direct
     email link, which lives in .contact__direct rather than the footer list
     and is display:inline — where min-height does nothing at all. */
  .skip-link { min-height: 44px; }
  /* min-width too — with the name hidden on phones the lockup is just "JO",
     which measured 31px across. */
  .nav__brand { min-height: 44px; min-width: 44px; align-items: center; }
  .contact__direct a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }
  /* Invisible hit area, centred on the link, without disturbing layout. */
  .case__link::after,
  .text-link::after,
  .endmark__links a::after {
    content: "";
    position: absolute;
    inset: 50% 0 auto 0;
    height: 44px;
    translate: 0 -50%;
  }

  /* The two case-study links are the primary conversion action on the page.
     On a phone they read as a plain underlined string; give them the weight
     of a real control. */
  .case__link {
    padding: .85rem 1.15rem;
    border: 1px solid var(--wine-28);
    border-radius: var(--radius-full);
    background: rgba(90, 24, 48, .045);
  }
  .case__link::after { display: none; }   /* the padding already carries it */

  /* Text that measured 11px. Phones need a floor; 12.5-13px keeps the
     letter-spaced uppercase style without shrinking it out of readability. */
  .nav__name,
  .nav__cta,
  .card__list li { font-size: .78rem; }
  .reel__cap     { font-size: .72rem; }}

/* Pointer-coarse devices have no hover, so :active is the only feedback a
   thumb ever gets. Without this every control feels dead on touch. */
@media (hover: none) {
  .btn:active,
  .case__link:active,
  .svc:active,
  .reel__fig:active { scale: .985; }
  .btn, .case__link, .svc, .reel__fig { transition: scale .12s ease-out; }}

/* ------------------------------------------------------- sticky mobile CTA */

/* One persistent way to act, from the moment the hero leaves until the
   contact form arrives. On a 13-screen page a visitor should never have to
   scroll back to find the ask. Hidden once #contact is on screen, because
   offering "start a project" on top of the project form is noise. */
.cta-dock {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 60;
  display: flex;
  gap: .75rem;
  padding: .6rem .9rem calc(.6rem + env(safe-area-inset-bottom, 0px));
  background: rgba(248, 242, 233, .93);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-block-start: 1px solid var(--wine-14);
  translate: 0 110%;
  transition: translate .38s var(--ease-out);
}
.cta-dock.is-up { translate: 0 0; }
.cta-dock a {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  min-height: 48px;
  padding: .5rem .8rem;
  border-radius: var(--radius-full);
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}
/* Below 375px "Start a project" needed 137px and had 112, so it broke onto two
   lines and grew the dock. A touch less size and tracking keeps both labels on
   one line; nowrap guarantees it. */
.cta-dock a { white-space: nowrap; }
@media (max-width: 374px) {
  .cta-dock a { padding-inline: .55rem; font-size: .7rem; letter-spacing: .06em; }}
/* Transparent border on the filled button only, so it matches the outlined
   one exactly. (On .cta-dock a it out-specified .cta-dock__ghost and erased
   the ghost button's outline.) */
.cta-dock__primary { background: var(--wine); color: var(--porcelain); border: 1px solid transparent; }
.cta-dock__ghost   { border: 1px solid var(--wine-28); color: var(--wine); }
.cta-dock a:active { scale: .97; }
@media (min-width: 901px) { .cta-dock { display: none; }}

/* Keep the last section clear of the dock. */
@media (max-width: 900px) {
  .sec--contact { padding-block-end: calc(clamp(4rem, 12svh, 7rem) + 4.5rem); }}

/* ------------------------------------------- expandable case-study detail */

/* The work section measured 4.1 screens on a phone, most of it the "My role"
   paragraph. Collapsed by default on small screens so the two projects can be
   scanned in one thumb-sweep, with the full text one tap away. This is a real
   <button aria-expanded>, so it is keyboard reachable and announced. */
.case__more {
  display: none;
  align-items: center;
  gap: .45rem;
  margin-block-start: .9rem;
  padding: .7rem 0;
  min-height: 44px;
  font: inherit;
  font-size: .78rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--wine);
  background: none;
  border: 0;
  border-block-end: 1px solid var(--wine-28);
  cursor: pointer;
}
.case__more svg {
  width: .85rem; height: .85rem;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  transition: rotate .28s var(--ease-out);
}
.case__more[aria-expanded="true"] svg { rotate: 180deg; }

/* Everything below is scoped to .js — set by a synchronous script in <head>,
   so there is no flash. Without JavaScript the button cannot open anything, so
   collapsing the copy would simply delete it from the page for that visitor and
   for any crawler that does not execute scripts. Measured: with JS disabled the
   panel was 0px tall and unreachable. Now it just stays open. */
@media (max-width: 760px) {
  .js .case__more { display: inline-flex; }
  /* Collapsed with the grid 0fr→1fr trick rather than a fixed max-height, so
     it animates from nothing without a magic number that would go stale the
     moment the copy changes. The wrapper is the grid; the <p> inside is the
     item that gets clipped. */
  .js .case__reveal {
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    transition: grid-template-rows .34s var(--ease-out), opacity .28s var(--ease-out);
  }
  .js .case__reveal > * { overflow: hidden; min-height: 0; }
  .js .case__reveal.is-open { grid-template-rows: 1fr; opacity: 1; }
  /* The <p>'s own top margin would show while collapsed — move it inside. */
  .js .case__reveal .case__role { margin-block-start: 0; }
  .js .case__reveal.is-open .case__role { margin-block-start: clamp(1rem, 2.4svh, 1.4rem); }}

/* ------------------------------------------------ process step activation */

/* Each marker lights as its step reaches the middle of the screen, so the
   stacked timeline reads as a journey being walked rather than four cards.
   Compositor-only — background and colour, no layout. */
.step__mark {
  transition: background .45s var(--ease-out), border-color .45s var(--ease-out),
              color .45s var(--ease-out), scale .45s var(--ease-out);
}
.step.is-live .step__mark {
  background: var(--wine);
  border-color: transparent;
  color: var(--gold-hi);
  scale: 1.06;
}

/* ------------------------------------------------------------ video tiles */

/* The whole tile toggles playback on touch. The sound button sits above it
   and stops the event, so the two never fight. */
.reel__fig { position: relative; }
.reel__tap {
  position: absolute;
  inset: 0;
  z-index: 1;
  border: 0;
  background: none;
  cursor: pointer;
}
.reel__tap span {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  opacity: 0;
  transition: opacity .3s var(--ease-out);
}
.reel__fig.is-held .reel__tap span { opacity: 1; }
.reel__tap span::before {
  content: "";
  width: 3.4rem; aspect-ratio: 1;
  border-radius: 50%;
  background: rgba(40, 19, 29, .62);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, .3);
}
.reel__sound { z-index: 2; }

/* ============================================================================
   04 · CONTENT & SOCIAL
   Ground: blush wash — the supporting surface in this palette, so the section
   reads as its own chapter between two porcelain neighbours without introducing
   a colour. Cards are porcelain-hi on top of it.
   ========================================================================= */

/* Blush at 40%, not full strength. The eyebrow is coral-ink, which measures
   4.80:1 on porcelain — almost no headroom — and full blush-wash dropped it to
   4.32:1, i.e. this section's ground would have broken an element that passes
   everywhere else on the page. 40% keeps the tonal shift and keeps the eyebrow
   above 4.5:1. Verified against rendered pixels, not against the token values. */
.sec--social {
  background: linear-gradient(180deg,
    var(--porcelain) 0%,
    rgba(243, 215, 211, .4) 16%, rgba(243, 215, 211, .4) 84%,
    var(--porcelain) 100%);
}
.js .chart__bar { scale: 1 0; }
.js .sec--seen .chart__bar {
  scale: 1 1;
  transition: scale .65s var(--ease-out);
  transition-delay: calc(var(--i, 0) * .045s);
}

/* --- services list ------------------------------------------------------- */

/* Each service carries one line saying what it actually involves. Four bare
   nouns read as a menu; the line under each reads as someone who has done it. */
/* Distributes into whatever height the row hands it, the same way the queue
   does — the tools card beside it is taller and there is no honest way to
   invent a fifth service. Self-adjusting, so it stays right if the tools list
   grows or shrinks again. */
.mini {
  display: grid;
  gap: clamp(.7rem, 1.5svh, .95rem);
  flex: 1 1 auto;
  align-content: space-between;
}
.mini li {
  position: relative;
  padding-inline-start: 1.15rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--plum);
}
.mini li b { display: block; font-weight: 600; }
.mini li span {
  display: block;
  margin-block-start: .1rem;
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--plum-70);
  text-wrap: pretty;
}
.mini li::before {
  content: "";
  position: absolute;
  left: 0; top: .78em;
  width: .55rem; height: 1px;
  background: var(--gold-lo);
}
.js .track__s:not(:last-child)::after { scale: 1 0; }
.js .sec--seen .track__s:not(:last-child)::after { scale: 1 1; transition: scale .7s var(--ease-out); }
.js .sec--seen .track__s:nth-child(2)::after { transition-delay: .1s; }
.js .sec--seen .track__s:nth-child(3)::after { transition-delay: .2s; }

/* Four across only once the card is genuinely wide. Below 1080 the preview
   card is half the board, and four tiles measured 72px square at 768px and
   100px at 1024px — small enough that the work in them stops being visible,
   which defeats the point of showing it. Two across holds ~170-250px. */
@media (min-width: 760px) {
  /* Services runs the full width at this breakpoint, so its four lines split
     into two columns rather than running down one side of a 728px card. */
  .board__c--svc .mini { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: clamp(1rem, 2.4vw, 2rem); }}

/* ------------------------------------------------------- measured results */

/* The two verified figures, lifted out of the role paragraph. They were true
   before; as prose they read as a claim, and set like this they read as
   evidence. Deliberately restrained — no cards, no gradient, no counter
   animation. The date underneath is the part that makes it credible. */
.proof {
  margin-block-start: clamp(1.2rem, 2.8svh, 1.7rem);
  padding-block-start: clamp(1rem, 2.2svh, 1.4rem);
  border-block-start: 1px solid var(--wine-14);
  display: grid;
  gap: clamp(.7rem, 1.6svh, 1rem) clamp(1.5rem, 3vw, 2.5rem);
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  max-width: 30rem;
}
.proof__item { display: grid; gap: .3rem; }
.proof__k {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--gold-ink);
}
.proof__v {
  margin: 0;
  display: flex;
  align-items: baseline;
  gap: .45rem;
  font-family: var(--f-display);
  font-size: var(--fs-2xl);
  line-height: 1;
  letter-spacing: -.02em;
}
.proof__was   { color: var(--plum-50); font-weight: 400; }
.proof__arrow { color: var(--gold-lo); font-size: .6em; }
.proof__now   { color: var(--wine); font-weight: 700; }
.proof__note {
  grid-column: 1 / -1;
  margin: 0;
  font-size: var(--fs-2xs);
  line-height: 1.6;
  /* --plum-50 measures 4.0:1 on porcelain, below AA for text this size. */
  color: var(--plum-70);
}

/* ----------------------------------------------- card entrance on scroll */

/* The service cards, case studies and process steps get a deliberate lift
   rather than the page-wide fade: further travel, a touch of scale, and a
   shadow that settles as the card lands. Compositor-only — translate, scale
   and opacity — so it stays smooth on a phone.

   The stagger comes from --d, which main.js sets per element from its index
   among its siblings. Doing it there rather than from the IntersectionObserver
   batch is what makes the wave survive slow scrolling. */
.js .svcs .r--rise,
.js .cases .r--rise,
.js .steps .r--rise,
.js .board .r--rise,
.js .track .r--rise {
  translate: 0 2.75rem;
  scale: .965;
}
.is-loaded .sec .svcs .r.in,
.is-loaded .sec .cases .r.in,
.is-loaded .sec .steps .r.in,
.is-loaded .sec .board .r.in,
.is-loaded .sec .track .r.in {
  animation: card-rise .78s cubic-bezier(.22, .68, .32, 1) var(--d, 0s) both;
}

@keyframes card-rise {
  from { opacity: 0; translate: 0 2.75rem; scale: .965; }
  to   { opacity: 1; translate: 0 0;       scale: 1; }
}

/* ------------------------------------------------------ vertical rhythm */

/* Section padding was tuned on a desktop canvas: 101px below one section plus
   76px above the next is 177px of dead space between them, which on an 844px
   phone screen is a fifth of the viewport — and there are five such gaps, so
   nearly a full screen of the page was empty. Tightened on phones only; the
   editorial breathing room is intact from 700px up. */
@media (max-width: 699px) {
  .sec { padding-block: clamp(3.25rem, 7svh, 4.5rem); }
  .sec + .sec { padding-block-start: clamp(3rem, 6svh, 4rem); }}

/* ------------------------------------------------------- reading comfort */

@media (max-width: 560px) {
  /* Long-form paragraphs were running to the full 350px column at 17px, which
     is a comfortable measure, but the leading was tuned for desktop. */
  .case__lede, .step__desc, .svc__desc, .sec__lede { line-height: 1.62; }}

/* ------------------------------------------- hero exit: desktop only ------ */

/* The exit is a desktop device: the hero content lifts away as the particles
   carry the portrait into the About section. It assumes the hero is about one
   viewport tall, which is true at 1440 (900px hero in a 900px window) — the
   next section is already arriving as the content leaves.

   On a phone it is not. The hero measures 1031px against an 844px viewport,
   and the exit finishes at --t 0.55, i.e. scrollY ~480. From there to 1031 the
   screen showed an empty hero: measured 80% blank with a 646px unbroken run of
   background at scrollY 400. Below 900px the hero simply scrolls away like
   normal content, which is what a phone expects anyway.

   --t still runs, so the particle hand-off is unaffected. */
@media (max-width: 900px) {
  .hero-exit .hero .hero__wordmark,
  .hero-exit .hero .identity,
  .hero-exit .hero .headline__in,
  .hero-exit .hero .hero__ctas,
  .hero-exit .hero .hero__note,
  .hero-exit .hero .card,
  .hero-exit .hero .card--traits,
  .hero-exit .hero .card--skills {
    transform: none;
    opacity: 1 !important;
  }
  .hero-gone .hero .hero__wordmark,
  .hero-gone .hero .hero__floats,
  .hero-gone .hero .hero__grid { visibility: visible; }}


/* ---------------------------------------- 17b. Content & social work

   Replaced the fabricated dashboard on 2 Sep 2026. Four published campaign
   graphics, square, in a plain grid — no chrome, no invented UI. The work is
   the argument; a frame around it would only dilute it. */

.promo {
  max-width: var(--measure);
  display: grid;
  gap: clamp(.85rem, 2vw, 1.4rem);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 860px) { .promo { grid-template-columns: repeat(4, minmax(0, 1fr)); }}

.promo__fig {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: clip;
  background: var(--porcelain-hi);
  border: 1px solid var(--wine-14);
  box-shadow: 0 1rem 2.25rem -1.5rem rgba(60, 18, 34, .38);
  transition: transform .45s var(--ease-out), box-shadow .45s var(--ease-out);
}
.promo__i:hover .promo__fig {
  transform: translateY(-4px);
  box-shadow: 0 1.4rem 2.8rem -1.5rem rgba(60, 18, 34, .45);
}
.promo__fig img { width: 100%; height: auto; aspect-ratio: 1; object-fit: cover; }

.promo__cap {
  padding: .7rem .85rem .8rem;
  font-size: clamp(.6875rem, .8vw, .75rem);
  font-weight: 600;
  letter-spacing: .07em;
  line-height: 1.4;
  color: var(--plum-70);
}

/* The scope note. Deliberately understated — it exists to keep the claim
   narrow, not to draw attention to itself. */
.socialnote {
  max-width: var(--measure-tight);
  margin-block-start: clamp(2rem, 5svh, 3rem);
  padding-inline-start: clamp(1rem, 1.5vw, 1.5rem);
  border-inline-start: 2px solid;
  border-image: linear-gradient(180deg, var(--gold-hi), var(--gold-lo) 55%, rgba(184, 134, 47, 0)) 1;
}
.socialnote__k {
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-ink);
}
.socialnote__t {
  margin-block-start: .5rem;
  font-size: clamp(.9375rem, 1.05vw, 1.03rem);
  line-height: 1.72;
  color: var(--plum-70);
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .promo__fig, .promo__i:hover .promo__fig { transition: none; transform: none; }}


/* ---------------------------------------------- 17c. Work with me

   Two routes, given equal visual weight deliberately: the moment one card
   looks like the real offer and the other like a fallback, the site has picked
   an audience, and this portfolio serves two. */

.paths {
  max-width: var(--measure);
  display: grid;
  gap: clamp(1rem, 2.4vw, 1.75rem);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 820px) { .paths { grid-template-columns: repeat(2, minmax(0, 1fr)); }}

.path {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .75rem;
  padding: clamp(1.5rem, 3vw, 2.25rem);
  border-radius: var(--radius-lg);
  background: var(--porcelain-hi);
  border: 1px solid var(--wine-14);
  transition: border-color .4s var(--ease-out), box-shadow .4s var(--ease-out);
}
.path:hover, .path:focus-within {
  border-color: var(--wine-28);
  box-shadow: 0 1.2rem 2.6rem -1.7rem rgba(60, 18, 34, .35);
}
.path__k {
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--gold-ink);
}
.path__t {
  font-family: var(--f-serif);
  font-size: clamp(1.3rem, 1.9vw, 1.65rem);
  font-weight: 600;
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 32;
  line-height: 1.2;
  color: var(--wine);
}
.path__d {
  font-size: clamp(.9375rem, 1.02vw, 1.03rem);
  line-height: 1.72;
  color: var(--plum-70);
  text-wrap: pretty;
}
.path .btn { margin-block-start: auto; }

@media (prefers-reduced-motion: reduce) { .path { transition: none; }}

/* --------------------------------------------- 18. Reduced motion + print */

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

  .js .r { opacity: 1; translate: 0 0; }
  .js .r .w__in { opacity: 1; translate: 0 0; }
  .is-loaded .sec .r.in,
  .is-loaded .sec .r.is-split.in .w__in { animation: none; }
  /* The card entrance carries a scale, which `.js .r` above does not reset,
     and its selector is more specific than the `animation: none` on the line
     above — so both need saying explicitly or the cards stay at 0.965 and
     still animate. */
  .js .svcs .r--rise,
  .js .cases .r--rise,
  .js .steps .r--rise,
  .js .board .r--rise,
  .js .track .r--rise { scale: 1; translate: 0 0; }
  .is-loaded .sec .svcs .r.in,
  .is-loaded .sec .cases .r.in,
  .is-loaded .sec .steps .r.in,
  .is-loaded .sec .board .r.in,
  .is-loaded .sec .track .r.in { animation: none; }
  .js .hero__wordmark { opacity: 1; translate: -50% 0; }
  .js .hero__portrait { opacity: 1; translate: -50% 0; scale: 1; }
  .is-loaded .r,
  .is-loaded .card--traits,
  .is-loaded .card--skills,
  .is-loaded .hero__wordmark,
  .is-loaded .hero__portrait { animation: none; }

  .hero__wordmark, .hero__portrait, .card { transform: none; }
  .hero__glow, .hero__glow > i { animation: none; }
  .btn, .btn::before, .btn__arrow, .nav__links a::after,
  .svc, .svc::after, .svc__icon, .step__mark, .rail { transition: none; }

  .js .sec::before { scale: 1 1; transition: none; }
  .steps::before,
  .step:not(:last-child)::before { scale: 1 1; transition: none; }
  /* These three carry a `.js` prefix in their live rules, so a bare-element
     reset here loses on specificity and the bars simply stay collapsed. Reset
     at the same weight as the rule being undone — including the `.sec--seen`
     variant, which is heavier still. */
  .js .chart__bar,
  .js .sec--seen .chart__bar,
  .js .track::before,
  .js .sec--seen .track::before,
  .js .track__s:not(:last-child)::after,
  .js .sec--seen .track__s:not(:last-child)::after { scale: 1 1; transition: none; }
  .ribbon__track { animation: none; }
  /* The dock still appears — it is a control, not decoration — but without
     the slide. Step markers stay in their resting state (JS skips them). */
  .cta-dock { transition: none; }
  .case__reveal { transition: none; }
  .case__more svg { transition: none; }

  /* No particle transport: the hero portrait simply fades and the About
     portrait is present from the start. */
  .js .stage__img--a { opacity: calc(1 - var(--t) * 1.9); }
  .morphfig img,
  .morph-ready .morphfig img { opacity: 1; filter: drop-shadow(0 2rem 3.5rem rgba(60, 18, 34, .18)); scale: 1; }
  .reel__sound { display: none; }}

@media print {
  .nav__toggle, .hero__glow, .hero__grain, .rail,
  .contact__portrait, .contact__burst, .about__mark, .social__mark { display: none; }
  .js .r, .js .hero__wordmark, .js .hero__portrait { opacity: 1; translate: 0 0; animation: none; }
  .hero { min-height: auto; }
  .sec { padding-block: 1.5rem; }
  .sec--contact, .svc--dark, .ribbon { background: none; color: #000; }}
/* ---------------------------------------------- Mobile menu reveal */
@keyframes menuIn {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .nav__links.is-open .menu__eyebrow,
  .nav__links.is-open li,
  .nav__links.is-open .menu__ctas { animation: none; }
  .nav__links.is-open .menu__chev { transition: none; }
}
