/*
   EMMANUEL DE FEX, PORTFOLIO DESIGN SYSTEM

   ── THE PREMISE ──────────────────────────────────────────────
   Colour is light, never pigment.

   Three tiers, and nothing lives in two of them:

     VOID      the room.  Obsidian and indigo, AND the amount of
               emptiness. Backgrounds, depth, and every gap.
     MATERIAL  the only substance. The gold ramp. Struck objects:
               surfaces, numbers, the name, the CTA.
     LIGHT     emission. Cyan, violet, pink. Strokes, rims, edge
               strips, and the bloom they cast.
               NEVER a fill. NEVER a glyph.

   Text sits outside all three, because text is lit BY the room:
   it runs the neutral --ink ladder. The one exception is text that
   IS a metal object (the name, the struck numbers), which may
   carry the ramp.

   The void is an amount, not just a colour. An object reads as
   floating only when there is enough emptiness around it to fall
   through. Gold on a crowded page is not jewellery, it is
   decoration.

   There is deliberately no token called "the accent". Every choice
   is either --metal-* (a substance, legal on a struck object) or
   --light-* (emission, illegal on a glyph). The naming is the rule.
   ─────────────────────────────────────────────────────────────
*/

@import url('https://fonts.googleapis.com/css2?family=Kantumruy+Pro:ital,wght@0,300..700;1,300..700&display=swap');

/* Clesmont, Drizy Studio. Display only: numerals, index numbers, the wordmark.
   Single weight (400), so it is never asked for bold. The heavy look is the face
   itself, not a weight. Anything that needs 600/700 uses Kantumruy Pro. */
@font-face {
  font-family: 'Clesmont';
  src: url('assets/Clesmont.otf') format('opentype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* ═══ VOID ═══ the room, as colour */
  --void-black: #05060a;
  --void-deep: #0b0e1f;
  --void-indigo: #1a2050;
  --void-indigo-lift: #232b6b;

  --border-subtle: rgba(255, 250, 235, 0.12);
  --border-glow: rgba(240, 199, 94, 0.42);

  /* ═══ VOID ═══ the room, as amount.
     A tight scale with few steps, used strictly, spent generously.
     On near-black, emptiness recedes and reads as depth, so it can be
     spent freely. But darkness hides structure, so the rhythm has to be
     strict or the page stops feeling composed and starts feeling adrift. */
  --space-3xs: 0.25rem;  /*   4px */
  --space-2xs: 0.5rem;   /*   8px */
  --space-xs:  0.75rem;  /*  12px */
  --space-sm:  1rem;     /*  16px */
  --space-md:  1.5rem;   /*  24px */
  --space-lg:  2rem;     /*  32px */
  --space-xl:  2.5rem;   /*  40px */
  --space-2xl: 4rem;     /*  64px */
  --space-3xl: 6rem;     /*  96px */
  --space-4xl: 9rem;     /* 144px  standard section */
  --space-5xl: 14rem;    /* 224px  the long silence before #proof */

  --measure-wide: 1280px;    /* timeline, hero, deliberately full width */
  --measure-content: 1120px; /* everything else: a column in a field */

  /* ═══ MATERIAL ═══ the only substance.
     Flat steps are for borders, marks and icons, anything under ~2rem
     where a full ramp turns to mud. Objects use the ramps. */
  --metal-bright: #f7d472;
  --metal-mid: #e0ae3c;
  --metal-deep: #a9761f;
  /* 0.30, then 0.15, now 0.10. The comment here used to say the cursor
     glow was the one light the page carried loud; that glow is gone, and
     nothing has replaced it as a loud light. Every remaining use of this
     token is a small object (the brand bead, a timeline node) where the
     halo only has to say "this is lit", not "look at this". */
  --metal-bloom: rgba(255, 190, 40, 0.10);
  --on-metal: #2a1a05;

  /* The metal ramp. Reads as metal because of the dark band at 42-50%,
     where the reflection flips from the light above to the ground below,
     and the bounce that climbs back out of it. Flatten this and it goes
     straight back to paint. */
  --metal-ramp:
    #fff9e3 0%,
    #f6d878 14%,
    #e0ae3c 28%,
    #8f5c16 42%,
    #6d4109 50%,
    #a97620 58%,
    #f0cb62 72%,
    #ffeeb4 82%,
    #d0982c 92%,
    #7f5010 100%;

  /* A compressed ramp for small surfaces, where the full one has no room.
     The trough sits low on purpose: a button carries a label, and the dark
     band has to clear the text band or the contrast falls under AA.
     Where the label sits this holds ~7.7:1 against --on-metal. */
  /* For struck text under ~3rem. Same metal, but the dark band is lifted out
     of the black so the lower half of a glyph does not disappear into the
     room. Brighter overall, shorter throw between the light and dark ends. */
  --metal-ramp-legible:
    #fff8dd 0%,
    #f8dc8c 18%,
    #eec258 34%,
    #c99a35 48%,
    #b8862a 56%,
    #dcb04a 68%,
    #f7dc92 80%,
    #e3b856 92%,
    #b8862a 100%;

  --metal-ramp-tight:
    #fff4d0 0%,
    #f7d477 22%,
    #dda72f 52%,
    #c08a22 72%,
    #8a5a14 100%;

  /* ═══ LIGHT ═══ emission only.
     Never a fill. Never a glyph. Strokes, rims, edge strips, and bloom.

     ONE source, ONE temperature. The old rainbow (cyan, violet and pink
     chasing each other around a rotating ring) is retired. Three hues
     turning on a border reads as a lit-up peripheral, not as a lit
     object: real light has a colour and a direction, and it does not
     cycle through the spectrum while you watch.

     Cool on purpose, and that is what keeps the tier alive. MATERIAL is
     the only warm thing on this page, and gold only reads as warm
     because the light falling on it is not. Let LIGHT go gold and both
     tiers collapse into one temperature, and the metal goes flat. */
  --light-core: #f2f6ff;  /* the filament: brightest point of a strip */
  --light-body: #c9d6ef;  /* the strip itself, just off the filament */
  --light-fade: #6f80a6;  /* where it falls off into the room */
  --light-bloom: rgba(178, 199, 240, 0.13);

  /* Functional, not decorative: the contact form's only two colours that
     mean something (submitted / failed) rather than carrying the brand. */
  --state-success: #6fcf97;
  --state-error: #f5836a;

  /* The strip, as conic stops, describing a single source ABOVE the
     object. 0deg is twelve o'clock, so the filament sits on the top edge
     and both sides fall away to nothing across the bottom, where a real
     object would be in its own shadow.

     Static, and that is the point: these stops say where the light is.
     Turning them would mean the room is spinning. */
  --light-strip:
    var(--light-core) 0deg,
    var(--light-body) 42deg,
    var(--light-fade) 104deg,
    transparent 156deg,
    transparent 204deg,
    var(--light-fade) 256deg,
    var(--light-body) 318deg,
    var(--light-core) 360deg;

  /* The same source landing on metal: cool key across the top, and along
     the bottom the warm bounce the gold throws back up into its own rim.
     That warm/cool pair inside one ring is what stops a gold object
     reading as a yellow rectangle. */
  --light-strip-gold:
    var(--light-core) 0deg,
    var(--light-body) 44deg,
    rgba(201, 214, 239, 0.22) 108deg,
    #8a5f1c 160deg,
    #c9922b 180deg,
    #8a5f1c 200deg,
    rgba(201, 214, 239, 0.22) 252deg,
    var(--light-body) 316deg,
    var(--light-core) 360deg;

  /* The diffuse spill a strip casts onto the surface behind it. Falls to
     transparent instead of running edge to edge: a wash with no falloff
     is just a coloured rectangle, which is the exact look this retires. */
  --light-wash: linear-gradient(155deg,
    rgba(242, 246, 255, 0.85),
    rgba(160, 181, 222, 0.45) 55%,
    transparent 88%);

  /* The same wash cast by a metal object, carrying the metal's own colour
     rather than the key light's. */
  --light-wash-gold: linear-gradient(155deg,
    rgba(255, 240, 199, 0.80),
    rgba(214, 162, 54, 0.42) 55%,
    transparent 88%);

  /* A highlight running along a single edge, peaking where the light is
     strongest and dying at both ends. .edge-lit is a horizontal strip and
     cannot use the radial falloff above. */
  --light-edge: linear-gradient(90deg,
    transparent,
    var(--light-body) 22%,
    var(--light-core) 50%,
    var(--light-body) 78%,
    transparent);

  /* ═══ INK ═══ lit by the room. Neutral only.
     No colour belongs here. Text that needs to be gold is a struck
     object and takes --metal-ramp, not a colour from this ladder. */
  --ink-bright: #ffffff;
  --ink: #ecebf2;
  --ink-soft: #a9a8bb;
  --ink-faint: #6f6e85;

  /* Typography */
  --font-heading: 'Kantumruy Pro', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body: 'Kantumruy Pro', -apple-system, BlinkMacSystemFont, sans-serif;
  /* Display face. Weight axis stops at 400, so --weight-display goes with it. */
  --font-display: 'Clesmont', 'Kantumruy Pro', -apple-system, sans-serif;

  --leading-display: 1.05; /* the hero name only, at 5.4rem, one word per line */
  --leading-tight: 1.2;    /* section and card titles */
  --leading-body: 1.6;
  --leading-dense: 1.45;   /* labels, chips, dense lists */

  --track-display: -0.02em;
  --track-caps: 0.08em;
  --track-caps-wide: 0.12em;

  /* Nine steps. Everything above --text-lg is fluid, because display type
     is what actually needs to respond to viewport; body copy does not. */
  --text-xs:  0.75rem;
  --text-sm:  0.875rem;
  --text-md:  1rem;
  --text-lg:  1.125rem;
  --text-xl:  clamp(1.2rem, 2.2vw, 1.5rem);
  --text-2xl: clamp(1.5rem, 2.4vw, 1.95rem);
  --text-3xl: clamp(1.8rem, 3vw, 2.5rem);
  --text-4xl: clamp(2.2rem, 4.5vw, 3.4rem);
  --text-5xl: clamp(3rem, 6vw, 4.8rem);

  /* Struck numerals are their own role: they carry the metal ramp and need
     to hold their weight against a card, not against a text column. */
  --text-numeral: clamp(2.4rem, 3.5vw, 3rem);

  /* Structure */
  --nav-height: 80px;
  --radius-xs: 6px;
  --radius-pill: 999px;
  --radius-sm: 8px;
  --radius-md: 16px;
  --radius-lg: 24px;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* ═══ SURFACES ═══ material, not flat fill.
     Light in this room comes from above, so every surface says so: a
     top-lit sheen falling off down the face, a lit top edge, and a bottom
     edge that falls away. The sheen is identical across all three tiers so
     they read as one material at three heights, not as three materials. */
  --surface-sheen: linear-gradient(180deg,
    rgba(255, 255, 255, 0.055) 0%,
    rgba(255, 255, 255, 0.015) 40%,
    transparent 100%);

  --surface-1: var(--surface-sheen), rgba(11, 14, 31, 0.55);
  --surface-2: var(--surface-sheen), rgba(16, 20, 45, 0.72);
  --surface-3: var(--surface-sheen), rgba(26, 32, 72, 0.88);

  /* Edge physics. Applied together as the first two box-shadow layers. */
  --edge-lit:  inset 0 1px 0 rgba(255, 250, 235, 0.09);
  --edge-fall: inset 0 -1px 0 rgba(0, 0, 0, 0.40);

  /* Elevation. Warm-biased rather than pure black, so a shadow under a gold
     object does not read as a hole punched in the page. */
  --elev-1: 0 6px 18px rgba(8, 5, 4, 0.42);
  --elev-2: 0 12px 30px rgba(8, 5, 4, 0.50);
  --elev-3: 0 22px 48px rgba(8, 5, 4, 0.62);

  /* ═══ GLARE ═══ how a bright thing scatters.
     Every glow on this page used to be a single radius: 0 0 10px on the
     brand bead, 0 0 16px on a timeline node, one blur(34px) behind a
     card. One Gaussian is why they read as a drop shadow with the colour
     changed rather than as light, and no amount of opacity fixes it,
     because the fault is the falloff and not the amount.

     A renderer does not blur once. It builds a mip chain: the bright pass
     is downsampled repeatedly, each level blurred and added back, so what
     you get is a tight bright core sitting inside progressively wider and
     fainter halos, finishing in a very broad veil across the whole
     neighbourhood. That last level is the one people read as "rendered",
     veiling glare is what a lens does, and its absence is what makes a
     CSS glow stop at an edge you can point to.

     Four levels, radius roughly x3 and alpha roughly x0.65 per step. The
     two tight levels carry a 1px upward bias so the core of the glare
     sits on the lit edge: every rim on this page is lit from twelve
     o'clock, and glare that ignores where the light is would undo that.
     The two wide levels are centred, because scatter is not directional.

     Saturation is flat across the ladder and the outer levels step
     *down* in chroma, for the same reason --bloom-sat is 1: real spill
     loses colour as it spreads. Gaining it is the other classic tell. */
  --glow-cool:
    0 -1px 4px    rgba(214, 226, 250, 0.10),
    0 0    14px   rgba(178, 199, 240, 0.075),
    0 0    40px   rgba(150, 174, 224, 0.048),
    0 0    104px  rgba(128, 152, 204, 0.028);

  --glow-cool-lift:
    0 -1px 5px    rgba(228, 238, 255, 0.20),
    0 0    18px   rgba(192, 212, 248, 0.145),
    0 0    52px   rgba(158, 182, 232, 0.095),
    0 0    132px  rgba(134, 158, 210, 0.052);

  /* The same ladder struck in metal. Warmer at the core, and it loses its
     warmth outward faster than the cool one does, because a gold object
     is reflecting a cool key: the further the scatter travels from the
     surface, the less of the metal it carries. */
  --glow-gold:
    0 -1px 4px    rgba(255, 228, 156, 0.11),
    0 0    14px   rgba(240, 190, 70,  0.08),
    0 0    40px   rgba(214, 162, 54,  0.046),
    0 0    104px  rgba(178, 132, 44,  0.026);

  --glow-gold-lift:
    0 -1px 5px    rgba(255, 238, 190, 0.22),
    0 0    18px   rgba(246, 202, 96,  0.155),
    0 0    52px   rgba(224, 174, 60,  0.098),
    0 0    132px  rgba(190, 140, 46,  0.052);
}

/* Rotating conic rim light, registered so the angle can actually animate */
@property --rim-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

/* Deck card hue, inherits so the chip, bars and stats move with the card.
   Clamped to 196-262 (cyan through indigo to violet) so a fill never lands on pink. */
@property --h {
  syntax: '<number>';
  initial-value: 232;
  inherits: true;
}

/* Reset & Base */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
  color-scheme: dark;
  background-color: var(--void-black);
  font-family: var(--font-body);
  color: var(--ink);
  /* clip, not hidden: hidden was silently masking real overflow bugs
     (nav, case metrics, stats grid, the card deck) instead of the layout
     never producing any. Now that those are fixed at the source, clip
     still stops stray horizontal scroll without hiding the next one. */
  overflow-x: clip;
  font-size: 16px;
  /* A defensive floor, not a fix for anything specific: nothing on the
     page had overflow-wrap at all, and Spanish strings run 20-40% longer
     than their English source, so a long compound word finding a tight
     spot is latent rather than theoretical. Only engages as a last
     resort, after normal wrapping already failed. */
  overflow-wrap: break-word;
}

body {
  line-height: var(--leading-body);
  background: var(--void-black);
  position: relative;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Icon system, one stroke weight everywhere */
.icon-sprite { display: none; }

.icon {
  width: 1.25em;
  height: 1.25em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Typography Helpers */
h1, h2, h3, h4, h5 {
  font-family: var(--font-heading);
  letter-spacing: var(--track-display);
  color: #ffffff;
  line-height: var(--leading-tight);
}

/* The name: white where the light lands, falling away into struck gold */
.gradient-text {
  background-image: linear-gradient(135deg,
    #ffffff 28%,
    #fff3cf 42%,
    #f2c85c 55%,
    #a9761f 67%,
    #7f5010 75%,
    #dda72f 86%,
    #ffeeb4 94%,
    #c08a22 100%);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  filter: drop-shadow(0 0 26px rgba(224, 174, 60, 0.22));
}

@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .gradient-text {
    background-image: none;
    color: var(--metal-bright);
    -webkit-text-fill-color: currentColor;
  }
}

/* ─────────────────────────────────────────────
   STRUCK GOLD
   Metal for the few things big enough to hold it: the stat numbers, the
   primary button, the brand bead. Everything smaller keeps the flat token.
   Two layers on every metal surface: the ramp, and a specular band that
   sweeps across it so the object reads as lit rather than painted.
────────────────────────────────────────────── */
.metal-gold,
.stat-number,
.case-metric-value {
  background-image:
    linear-gradient(101deg,
      transparent 36%,
      rgba(255, 252, 236, 0.62) 47%,
      rgba(255, 252, 236, 0.28) 52%,
      transparent 62%),
    linear-gradient(174deg, var(--metal-ramp));
  background-size: 260% 100%, 100% 100%;
  /* Parked mid-sweep so the specular sits across the glyphs and stays
     there. It used to travel every 7s forever, which is what a chrome
     web-banner does; metal in a photograph holds one highlight and reads
     more expensive for it. */
  background-position: 50% 0, 0 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  /* The warm bloom it casts on the dark, plus a contact shadow for weight.
     Radius pulled in from 22px and the opacity down from 0.17: a wide soft
     halo behind a glyph is neon signage, a tight one is a struck object
     catching the room. The contact shadow is doing most of the work. */
  filter:
    drop-shadow(0 0 11px rgba(255, 196, 74, 0.10))
    drop-shadow(0 2px 1px rgba(60, 34, 4, 0.85));
}

/* The case metrics sit well below the stat numerals in size, so they take the
   lifted ramp. Same metal, readable at 2rem. */
.case-metric-value {
  background-image:
    linear-gradient(101deg,
      transparent 36%,
      rgba(255, 252, 236, 0.62) 47%,
      rgba(255, 252, 236, 0.28) 52%,
      transparent 62%),
    linear-gradient(174deg, var(--metal-ramp-legible));
}

/* goldSweep is gone. It travelled the specular across every struck
   numeral on a 7s loop, which meant the two brightest elements on the
   page were always moving with nothing prompting them. The highlight is
   now parked in .metal-gold's background-position instead. */

/* Firefox and anything without background-clip on text gets flat gold
   rather than an invisible element */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .metal-gold,
  .stat-number,
  .case-metric-value {
    background-image: none;
    color: var(--metal-bright);
    -webkit-text-fill-color: currentColor;
  }
}

/* ─────────────────────────────────────────────
   RIM
   The animated outline, as one reusable thing rather than a trick copied
   per component. Paints a conic gradient into a 1px ring by masking out
   everything inside the padding box.

     .rim        opt in, dark at rest, lights on hover
     .rim-live   always lit and always turning, for the one or two things
                 that should pull the eye without being pointed at

   Tune per element with --rim-stops, --rim-width, --rim-speed, --rim-rest.
   The host element needs a border-radius and its own stacking position.
────────────────────────────────────────────── */
.rim {
  position: relative;
  --rim-stops: var(--light-strip);
  --rim-width: 1px;
  /* Only ever spends this on hover now, so it can afford to be slow. At
     4.5s a turning ring is the loudest thing in the viewport; at 14s it
     reads as the object being turned in the light rather than the light
     running laps around the object. */
  --rim-speed: 14s;
  --rim-rest: 0;
  /* Parked at twelve o'clock so the filament in --light-strip lands on
     the top edge. Every rim on the page is lit from the same direction,
     which is the only reason they read as one room. */
  --rim-angle: 0deg;
}

.rim::before {
  content: '';
  position: absolute;
  inset: calc(var(--rim-width) * -1);
  border-radius: inherit;
  padding: var(--rim-width);
  background: conic-gradient(from var(--rim-angle), var(--rim-stops));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  opacity: var(--rim-rest);
  transition: opacity 0.45s ease;
  pointer-events: none;
}

/* Rotation is now the reward for pointing at something, not the resting
   state. Nothing on the page turns until asked, and because no rim is
   already mid-rotation there is no longer a live/idle split to special
   case: every rim starts its turn from the same parked angle. */
.rim:hover::before,
.rim:focus-visible::before {
  opacity: 1;
  animation: rimSpin var(--rim-speed) linear infinite;
}

/* Always lit, never turning. "Live" used to mean rotating; it now means
   the edge is permanently catching the key light while the rest of the
   page waits to be hovered. Six rings spinning in unison was the single
   loudest thing here, and it is what made the light read as cheap. */
.rim-live {
  --rim-rest: 1;
}

/* The third setting between "dark until pointed at" and "fully lit".
   .rim-live is right for the four struck stat cards and the two CTAs,
   where the edge IS the object's finish. It is too much for a page of
   quieter objects: eight fully-lit rings is how the light started
   reading as decoration rather than as a room.

   At 0.6 the edge is caught rather than lit. You read it as the same key
   light from twelve o'clock grazing one more surface, and it comes up to
   full under the cursor. Same stops, same direction, same source: only
   the exposure changes, which is why adding it to more objects makes the
   room more coherent rather than busier. The thing that made the old
   rims read as cheap was that they turned and gained saturation, not
   that they were visible. */
.rim-soft {
  --rim-rest: 0.6;
}

/* Off screen, so nothing to light. Set from JS by initLiveRimGating, which
   is the difference between six elements repainting a conic gradient every
   frame for the whole session and only doing it while they are visible. */
.rim-parked::before { animation-play-state: paused; }
.rim-parked::after  { animation-play-state: paused; }

/* Drives every rim on the page, including the How I Work cards below */
@keyframes rimSpin {
  to { --rim-angle: 360deg; }
}

/* Firefox builds without @property cannot animate the conic angle, so they
   get a static gradient rather than a stuck one */
@supports not (background: conic-gradient(from var(--rim-angle), red, blue)) {
  .rim::before {
    background: linear-gradient(180deg, var(--light-core), var(--light-fade) 60%, transparent);
  }
}

/* ─────────────────────────────────────────────
   BLOOM
   The other half of a light strip. A real strip in a dark room is a bright
   line, but it also washes colour onto the surface behind it. Without that
   wash the rim is a sticker; with it, the object is lit.

   Deliberately a STATIC gradient, never the rotating conic. A blurred
   static layer composites once, a blurred animating layer repaints every
   frame. At this blur radius the wash is far too diffuse to read rotation
   anyway, so only the thin rim turns and the bloom costs nothing.

     .bloom       washes on hover
     .bloom-live  washes at rest, brighter on hover

   Needs a free ::after on whatever carries it.
────────────────────────────────────────────── */
.bloom {
  position: relative;
  --bloom-spread: 24px;
  /* Blurred further than before. A bloom you can find the edges of is a
     coloured shape sitting behind the object; a bloom you cannot is light
     in the air. The extra radius costs nothing, since it composites once
     and never animates. */
  --bloom-blur: 34px;
  --bloom-rest: 0;
  --bloom-hover: 0.22;
  /* Was 1.5. Boosting saturation on a blurred colour field is the single
     clearest tell of a cheap glow: real spill loses saturation as it
     spreads, it does not gain it. 1 leaves the wash exactly as mixed. */
  --bloom-sat: 1;
}

.bloom::after {
  content: '';
  position: absolute;
  inset: calc(var(--bloom-spread) * -1);
  z-index: -1;
  border-radius: inherit;
  background: var(--bloom-tint, var(--light-wash));
  filter: blur(var(--bloom-blur)) saturate(var(--bloom-sat));
  opacity: var(--bloom-rest);
  transition: opacity 0.55s ease;
  pointer-events: none;
}

.bloom:hover::after,
.bloom:focus-visible::after { opacity: var(--bloom-hover); }

/* Was 0.3, then 0.15. A resting bloom is allowed to exist (it is static,
   and it is what stops a lit object looking pasted onto the dark) but it
   should read as the room being slightly brighter near the object, not as
   a halo you can point at. */
.bloom-live { --bloom-rest: 0.10; }

/* ─────────────────────────────────────────────
   EDGE
   A strip on one edge instead of a full ring, for rows and panels where a
   complete rim is too much jewellery. Same light, less of it.
────────────────────────────────────────────── */
.edge-lit {
  position: relative;
  --edge-thickness: 2px;
  --edge-rest: 0.55;
}

.edge-lit::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--edge-thickness);
  border-radius: inherit;
  background: var(--light-edge);
  opacity: var(--edge-rest);
  transition: opacity 0.4s ease;
  pointer-events: none;
}

.edge-lit:hover::before { opacity: 1; }

/* Emphasis inside a heading, weight-and-color, not another gradient */
/* Emphasis is weight and value, not colour. The heading body sits at --ink
   so the emphasised clause can step up to pure white and be the brightest
   thing in the line without any hue at all. */
.section-title em {
  font-style: normal;
  font-weight: 700;
  color: var(--ink-bright);
}

.section-head { margin-bottom: var(--space-2xl); }

/* A quiet document link under a section's subtitle (the résumé, in Career). */
.section-head-doc { margin-top: var(--space-sm); }

.section-title {
  font-size: var(--text-4xl);
  font-weight: 700;
  color: var(--ink);
  margin-bottom: var(--space-sm);
  text-wrap: balance;
}

.section-subtitle {
  color: var(--ink-soft);
  font-size: var(--text-lg);
  max-width: 62ch;
  margin-bottom: 0;
  line-height: var(--leading-body);
}

/* ──────────────────────────────────────────────
   SECTION FOLDS, glass in a dark room
   Services, How it goes, Career and References each rest as one pane of
   glass you can barely tell from the room: no outline, no shadow, no
   colour, only a breath of fill and a blur that softens what is behind it.
   Pointing at one sends a single pass of the key light across it, and the
   edge exists only where that light is crossing. Opening it is when the
   room gets light: the indigo glass, the glaze, the gold count and star.
   The markup is built by script (main.js, initFolds), so without it every
   section simply stays open.
────────────────────────────────────────────── */
:is(#skills, #process, #experience).fold {
  padding-top: var(--space-xs);
  padding-bottom: var(--space-xs);
  /* A nav jump lands the pane below the fixed nav, not behind it. */
  scroll-margin-top: 64px;
  transition: padding 0.6s var(--ease-out);
}

:is(#skills, #process, #experience).fold.is-open { padding-bottom: var(--space-2xl); }

.testimonials-block.fold {
  margin-top: var(--space-2xl);
  padding-top: 0;
  border-top: 0;
}

.fold {
  --fold-ink-rest: #7c7b92;
  --fold-ink-dim: #56556a;
  --fold-hairline: rgba(255, 250, 235, 0.07);
  --fold-radius: 20px;
  --fold-pass: 1.3s;
}

.fold-pane {
  position: relative;
  border-radius: var(--fold-radius);
  isolation: isolate;
  background: linear-gradient(180deg, rgba(242, 246, 255, 0.018), rgba(242, 246, 255, 0.008));
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  transition:
    background 0.9s ease,
    box-shadow 0.9s ease,
    -webkit-backdrop-filter 0.9s ease,
    backdrop-filter 0.9s ease;
}

/* Afterglow along the top edge: the key light, caught. Hidden at rest. */
.fold-pane::before {
  content: '';
  position: absolute;
  top: 0;
  left: 6%;
  right: 6%;
  height: 1px;
  background: var(--light-edge);
  opacity: 0;
  transition: opacity 0.7s ease;
  pointer-events: none;
  z-index: 3;
}

/* The pass of light. Two layers on one clock: the glint is the pane's edge
   painted only where the light is, the sheen is the light on the face.
   Once per hover, left to right. Never loops, never follows the pointer. */
.fold-glint,
.fold-sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background-repeat: no-repeat;
  background-size: 320% 100%;
  background-position: 100% 0;
}

.fold-glint {
  z-index: 3;
  padding: 1px;
  background-image: linear-gradient(105deg,
    transparent 40%,
    rgba(201, 214, 239, 0.35) 46%,
    rgba(242, 246, 255, 0.85) 50%,
    rgba(201, 214, 239, 0.35) 54%,
    transparent 60%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}

.fold-sheen {
  z-index: 0;
  mix-blend-mode: screen;
  background-image: linear-gradient(105deg,
    transparent 36%,
    rgba(242, 246, 255, 0.025) 44%,
    rgba(242, 246, 255, 0.09) 49.5%,
    rgba(255, 255, 255, 0.13) 50%,
    rgba(242, 246, 255, 0.05) 53%,
    transparent 62%);
}

.fold-pane.is-lit .fold-glint,
.fold-pane.is-lit .fold-sheen {
  animation: foldPass var(--fold-pass) cubic-bezier(0.45, 0.05, 0.25, 1) forwards;
}

@keyframes foldPass {
  from { background-position: 100% 0; }
  to   { background-position: 0% 0; }
}

/* What the light leaves while it is still on the pane. */
.fold-pane.is-lit {
  background: linear-gradient(180deg, rgba(242, 246, 255, 0.035), rgba(242, 246, 255, 0.012));
  transition-delay: 0.25s;
}
.fold-pane.is-lit::before { opacity: 0.28; transition-delay: 0.3s; }

/* Open: the room gets light. Colour and contrast arrive here. */
.fold-pane.is-open {
  background:
    radial-gradient(170% 140px at 50% -44px,
      rgba(242, 246, 255, 0.075) 0%,
      rgba(242, 246, 255, 0.025) 53%,
      transparent 54.5%),
    /* The indigo is a tint now, about a fifth of what it was, over a
       near-black glass. The frost does more of the work than the colour. */
    linear-gradient(180deg,
      rgba(35, 43, 107, 0.085) 0%,
      rgba(26, 32, 80, 0.06) 120px,
      rgba(26, 32, 80, 0.04) 100%),
    linear-gradient(180deg,
      rgba(10, 11, 16, 0.50) 0%,
      rgba(7, 8, 12, 0.62) 100%);
  -webkit-backdrop-filter: blur(28px);
  backdrop-filter: blur(28px);
  box-shadow:
    inset 0 -1px 0 rgba(0, 0, 0, 0.5),
    inset 0 -18px 28px -22px rgba(201, 214, 239, 0.16),
    var(--elev-3),
    var(--glow-cool);
  transition-delay: 0s;
}
.fold-pane.is-open::before { opacity: 0.55; transition-delay: 0s; }

/* On open, the light runs down the glass as it grows. Once. */
.fold-flood {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
}
.fold-flood::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: -140px;
  height: 140px;
  background: linear-gradient(180deg,
    transparent,
    rgba(242, 246, 255, 0.04) 45%,
    rgba(255, 255, 255, 0.08) 50%,
    rgba(242, 246, 255, 0.02) 58%,
    transparent);
  mix-blend-mode: screen;
  opacity: 0;
}
.fold-pane.is-open .fold-flood::before { animation: foldFlood 1.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s; }
@keyframes foldFlood {
  0%   { top: -140px; opacity: 0; }
  15%  { opacity: 1; }
  100% { top: 100%; opacity: 0; }
}

/* ── The head: one low line ─────────────────── */
/* Two classes deep so it outranks .testimonials-head's own flex row. */
.fold-pane > .fold-head {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center;
  column-gap: var(--space-lg);
  min-height: 76px;
  padding: 14px 28px;
  margin: 0;
}
.section-head.fold-head,
.testimonials-head.fold-head { margin-bottom: 0; }

.fold-label {
  display: flex;
  align-items: baseline;
  gap: 18px;
  min-width: 0;
}

.fold-head :is(.section-title, .testimonials-title) {
  flex: none;
  margin: 0;
  font-size: clamp(1.3rem, 2.2vw, 1.55rem);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: var(--leading-tight);
  color: var(--fold-ink-rest);
  transition: color 0.6s ease;
}

.fold-head .section-subtitle {
  min-width: 0;
  max-width: none;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 0.95rem;
  color: var(--fold-ink-dim);
  transition: color 0.6s ease;
}

.is-lit .fold-head :is(.section-title, .testimonials-title) { color: var(--ink-soft); transition-delay: 0.2s; }
.is-lit .fold-head .section-subtitle { color: var(--fold-ink-rest); transition-delay: 0.2s; }
.is-open .fold-head :is(.section-title, .testimonials-title) { color: var(--ink-bright); transition-delay: 0s; }
.is-open .fold-head .section-subtitle { color: var(--ink-soft); transition-delay: 0s; }

/* The PDF links ride the same line, above the toggle's hit area. */
.fold-head :is(.section-head-doc, .link-quiet) {
  position: relative;
  z-index: 2;
  margin: 0;
}

/* Monotone with the rest of the line until the light arrives. */
.fold-head .link-quiet { color: var(--fold-ink-dim); transition: color 0.6s ease; }
.is-lit .fold-head .link-quiet { color: var(--fold-ink-rest); }
.is-open .fold-head .link-quiet { color: var(--ink-soft); }
.fold-head .link-quiet:hover { color: var(--ink); }

/* The whole line is the button. Text stays in its own elements (the
   language toggle rewrites them); the button only sits over them. */
.fold-toggle {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  border-radius: var(--fold-radius);
  cursor: pointer;
}
.fold-toggle:focus-visible {
  outline: 1px dashed rgba(201, 214, 239, 0.35);
  outline-offset: 3px;
}

/* What is inside, as a count. Grey at rest; struck gold once lit. */
.fold-count {
  display: flex;
  align-items: baseline;
  gap: 8px;
  white-space: nowrap;
}
.fold-count b {
  position: relative;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.35rem;
  line-height: 1;
  color: var(--fold-ink-dim);
  font-variant-numeric: tabular-nums;
  transition: color 0.6s ease;
}
.fold-count b::after {
  content: attr(data-n);
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, var(--metal-ramp-legible));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  opacity: 0;
  transition: opacity 0.8s ease 0.15s;
}
.fold-count span {
  font-size: 0.7rem;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--fold-ink-dim);
  transition: color 0.6s ease;
}
.is-lit .fold-count b { color: var(--fold-ink-rest); transition-delay: 0.3s; }
.is-open .fold-count b::after { opacity: 1; }
.is-open .fold-count span { color: var(--ink-soft); }

/* The star: the only affordance. Still at rest. The pass reaches it last,
   so it flares and turns a quarter; opening strikes it gold. */
.fold-star {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
  color: var(--fold-ink-dim);
  transform: scale(0.8) rotate(0deg);
  transition: color 0.5s ease, transform 0.8s var(--ease-out);
}
.fold-star .s {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.fold-star .s-cool { fill: currentColor; }
.fold-star .s-gold {
  fill: url(#star-gold);
  opacity: 0;
  transition: opacity 0.6s ease, filter 0.6s ease;
}

.is-lit .fold-star {
  color: var(--light-core);
  transform: scale(1) rotate(90deg);
  transition-delay: calc(var(--fold-pass) * 0.7);
}
.is-lit .fold-star .s-cool {
  animation: foldTwinkle 0.9s ease-out calc(var(--fold-pass) * 0.7) both;
}
@keyframes foldTwinkle {
  0%   { filter: drop-shadow(0 0 0 rgba(242, 246, 255, 0)); }
  35%  { filter: drop-shadow(0 0 7px rgba(242, 246, 255, 0.85)) drop-shadow(0 0 2px rgba(255, 255, 255, 0.9)); }
  100% { filter: drop-shadow(0 0 3px rgba(242, 246, 255, 0.35)); }
}

.fold-pane.is-open .fold-star {
  color: var(--metal-bright);
  transform: scale(1.05) rotate(135deg);
  transition-delay: 0s;
  animation: foldStrike 0.7s var(--ease-out);
}
.fold-pane.is-open .fold-star .s-cool { animation: none; }
.fold-pane.is-open .fold-star .s-gold {
  opacity: 1;
  filter: drop-shadow(0 0 5px rgba(246, 202, 96, 0.45)) drop-shadow(0 0 14px rgba(224, 174, 60, 0.18));
}
@keyframes foldStrike {
  0%   { scale: 1; }
  40%  { scale: 1.35; }
  100% { scale: 1; }
}

/* ── The body ────────────────────────────────── */
.fold-body {
  position: relative;
  z-index: 1;
  height: 0;
  overflow: hidden;
}
.fold-body.is-open { height: auto; }

/* Clipping is only needed while the height is moving. At rest, rims and
   hover shifts are allowed to reach outside the box. */
.fold-body.is-settled { overflow: visible; }

/* flow-root keeps a first child's margin inside the box. */
.fold-inner {
  display: flow-root;
  margin: 0 20px;
  padding: var(--space-lg) 8px var(--space-xl);
  border-top: 1px solid var(--fold-hairline);
}

/* Jumps (shut on scroll-out, shut or open for a nav jump) must not animate. */
.fold.fold-instant,
.fold.fold-instant * { transition: none !important; animation: none !important; }

@media (max-width: 768px) {
  .fold-pane > .fold-head { column-gap: var(--space-sm); padding: 12px 20px; min-height: 68px; }
  .fold-head .section-subtitle { display: none; }
  .fold-head :is(.section-head-doc, .link-quiet) span { display: none; }
  .fold-inner { margin: 0 12px; padding: var(--space-md) 4px var(--space-lg); }
}

@media (prefers-reduced-motion: reduce) {
  .fold-pane.is-lit .fold-glint,
  .fold-pane.is-lit .fold-sheen,
  .fold-pane.is-open .fold-flood::before,
  .is-lit .fold-star .s-cool,
  .fold-pane.is-open .fold-star { animation: none; }
  .fold, .fold-pane, .fold-pane::before, .fold-star,
  .fold-head :is(.section-title, .testimonials-title, .section-subtitle),
  .fold-count b, .fold-count b::after { transition-delay: 0s !important; }
  .is-lit .fold-star { transform: scale(1); }
  .fold-pane.is-open .fold-star { transform: scale(1.05) rotate(45deg); }
}

/* Layout Container */
/* A column in a field, not a full-width sheet. Narrower measure with more
   air around it is what makes the content read as composed. */
.container {
  position: relative;
  z-index: 1;
  max-width: var(--measure-content);
  margin: 0 auto;
  padding: 0 var(--space-xl);
}

/* ─────────────────────────────────────────────
   AMBIENT MOTES, slow looping drift, decoration only
────────────────────────────────────────────── */
.ambient-motes {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* Motes are dust suspended in the room, so they are material, not light.
   Depth is carried by brightness and size rather than by a second hue,
   which also puts them in the same family as the hero's dust stream. */
.ambient-motes span {
  position: absolute;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--metal-deep) 0%, rgba(169, 118, 31, 0) 70%);
  /* Static. Ten points of dust rising on ten separate loops was permanent
     motion in the reader's peripheral vision with nothing prompting it,
     and peripheral motion is the thing the eye is least able to ignore.
     Suspended dust in still air does not travel: it hangs and catches the
     light. Depth now comes from the opacity ladder below, not from drift. */
  opacity: 0.3;
}

.ambient-motes span:nth-child(2n) {
  background: radial-gradient(circle, var(--metal-bright) 0%, rgba(240, 199, 94, 0) 70%);
  width: 6px;
  height: 6px;
}

.ambient-motes span:nth-child(3n) { width: 3px; height: 3px; }

/* The scatter is unchanged; what were animation-delay/duration pairs are
   now an opacity ladder, so the field still reads as depth (some grains
   nearer, some further back) rather than as ten identical dots. */
.ambient-motes span:nth-child(1)  { left:  6%; top: 72%; opacity: 0.34; }
.ambient-motes span:nth-child(2)  { left: 18%; top: 24%; opacity: 0.20; }
.ambient-motes span:nth-child(3)  { left: 31%; top: 86%; opacity: 0.40; }
.ambient-motes span:nth-child(4)  { left: 44%; top: 12%; opacity: 0.16; }
.ambient-motes span:nth-child(5)  { left: 57%; top: 64%; opacity: 0.30; }
.ambient-motes span:nth-child(6)  { left: 69%; top: 30%; opacity: 0.22; }
.ambient-motes span:nth-child(7)  { left: 78%; top: 80%; opacity: 0.38; }
.ambient-motes span:nth-child(8)  { left: 88%; top: 44%; opacity: 0.18; }
.ambient-motes span:nth-child(9)  { left: 95%; top: 18%; opacity: 0.26; }
.ambient-motes span:nth-child(10) { left: 24%; top: 52%; opacity: 0.32; }

/* Navigation */
#main-nav {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: var(--nav-height);
  z-index: 1000;
  transition: background 0.3s ease, backdrop-filter 0.3s ease, border-color 0.3s ease;
}

/* The blur and the shadow already separate the bar from the page. The rule
   under it was one line doing a job two other things were doing. */
#main-nav.scrolled {
  background: rgba(5, 6, 10, 0.92);
  backdrop-filter: blur(20px);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}

.nav-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 100%;
}

/* Clesmont runs much wider than the old heading face, so the wordmark is
   stepped down and pinned to one line. It is caps-only by design. */
.nav-brand {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 400;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: #fff;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* A gold bead, not a dot. At 12px a linear ramp is invisible, so it gets
   the treatment a sphere actually has: an offset specular, a saturated
   midtone, and a dark rim where it turns away from the light. */
.nav-brand .brand-badge {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 32% 26%,
      #fff8dc 0%,
      #f6d878 20%,
      #dda72f 50%,
      #a9761f 76%,
      #6d4109 100%);
  box-shadow:
    inset 0 -1px 2px rgba(60, 34, 4, 0.65),
    inset 0 1px 1px rgba(255, 250, 225, 0.5),
    0 0 14px rgba(240, 199, 94, 0.55),
    0 1px 3px rgba(0, 0, 0, 0.6);
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  list-style: none;
}

.nav-link {
  position: relative;
  color: var(--ink-soft);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 600;
  /* Two-word links break across lines at the wider display face. The row is
     one line of labels, so none of them wrap. */
  white-space: nowrap;
  transition: color 0.25s ease;
}

.nav-link::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -6px;
  width: 100%;
  height: 1px;
  background: linear-gradient(90deg, var(--light-core), var(--light-fade));
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--ease-out);
}

.nav-link:hover, .nav-link.active { color: var(--ink-bright); }
.nav-link:hover::after, .nav-link.active::after { transform: scaleX(1); }

/* Language Switcher */
.lang-switcher {
  display: flex;
  align-items: center;
  background: rgba(3, 4, 10, 0.5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  padding: 3px;
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.45);
}

.lang-btn {
  display: inline-flex;
  align-items: center;
  height: 28px;
  background: none;
  border: none;
  color: var(--ink-soft);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 0 12px;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: color 0.25s ease, background 0.25s ease, box-shadow 0.25s ease;
}

.lang-btn:hover { color: var(--ink); }

.lang-btn.active {
  background: var(--metal-bright);
  color: var(--on-metal);
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 235, 0.45),
    inset 0 -1px 0 rgba(60, 34, 4, 0.35),
    var(--glow-gold);
}

/* Hamburger */
.hamburger {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  flex-direction: column;
  gap: 5px;
  /* No z-index here: it never had position set, so a z-index couldn't do
     anything even if the "above the nav" intent it once implied were
     otherwise correct (it also wouldn't be -- z-index only competes with
     siblings inside #main-nav's own stacking context, not the nav's
     1000 itself). It paints correctly from plain DOM order alone. */
}

.hamburger span {
  width: 26px;
  height: 2px;
  background: #ffffff;
  transition: all 0.3s ease;
}

.hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger.open span:nth-child(2) { opacity: 0; }
.hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* Hero Section */
#hero {
  position: relative;
  width: 100%;
  min-height: 100vh;
  overflow: hidden;
}

/* The hero is the one place that does not use --measure-content. A centred
   1120px column leaves hundreds of pixels of dead margin on a wide screen.
   This goes full bleed with a gutter that scales with the viewport.

   Composition: a lockup of eyebrow and headline across the full width,
   then the lead + actions and the reel sharing the row underneath. The
   headline gets the whole frame because four words of Clesmont caps only
   read as a hero at a size half a column cannot hold. */
.hero-frame {
  position: relative;
  z-index: 2;
  /* Matches #hero's own 100vh floor exactly. */
  min-height: 100vh;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(360px, min(560px, 34vw));
  grid-template-areas:
    "eyebrow eyebrow"
    "title   title"
    "copy    reel";
  /* The composition centres as one block. The bottom padding is heavier
     than the top so it sits a little above true centre, where the eye
     reads centred. */
  align-content: center;
  column-gap: var(--space-2xl);
  padding-block: calc(var(--nav-height) + var(--space-lg)) var(--space-3xl);
  padding-inline: clamp(var(--space-xl), 7vw, var(--space-4xl));
  /* The headline sizes itself against this frame's real content width
     (cqi below), not the viewport, so it can run edge to edge without a
     scrollbar or gutter estimate pushing it over. */
  container-type: inline-size;
}

.hero-eyebrow {
  grid-area: eyebrow;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  justify-self: start;
  /* Clesmont's ascent sits well above its cap height, so the headline's line
     box starts higher than its glyphs look. Without this the eyebrow and the
     first line read as touching. */
  margin-bottom: var(--space-md);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-soft);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

.hero-copy {
  grid-area: copy;
  align-self: start;
  min-width: 0;
}

#hero-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

/* Static fallback if WebGL is unavailable */
.no-webgl #hero-canvas { display: none; }
.no-webgl #hero {
  /* No WebGL, so the band is painted rather than simulated: a gold lane
     raked lower left to upper right, with the same dark rift through it and
     the same lower left hold-back that keeps the headline readable. */
  background:
    linear-gradient(107deg, transparent 38%, rgba(5, 6, 10, 0.55) 50%, transparent 62%),
    linear-gradient(107deg,
      transparent 30%,
      rgba(240, 199, 94, 0.16) 46%,
      rgba(224, 174, 60, 0.24) 55%,
      rgba(169, 118, 31, 0.10) 66%,
      transparent 78%),
    radial-gradient(ellipse 75% 55% at 74% 26%, rgba(35, 43, 107, 0.42) 0%, transparent 72%),
    var(--void-black);
}

.hero-badge-dot {
  width: 8px;
  height: 8px;
  background: var(--metal-bright);
  border-radius: 50%;
  /* The smallest struck object on the page, and the first one a visitor
     sees. At 8px the ladder's wide levels do most of the work: the bead
     itself is barely there, so what says "this is lit" is the neighbourhood
     around it being fractionally warmer, not a ring drawn at 10px. */
  box-shadow: var(--glow-gold);
}

/* The claim: "More output. / Same team." Two lines, one per phrase, as
   large as the frame allows. Sized from measured Clesmont advances
   (tracking included), so each size is the widest line's width divided
   into the frame:
     EN  "More output."     8.25em    ES  "Más producción."  10.09em
   11.6cqi / 9.5cqi fill about 96% of the frame. The vh term stops a wide,
   short screen from pushing the lead and the reel below the fold; the rem
   cap stops ultrawides turning it into a billboard.
   Declared twice: the vw line is the fallback where cqi is unsupported. */
.hero-title.hero-title-claim {
  grid-area: title;
  font-size: min(10.2vw, 13.5vh, 10rem);
  font-size: min(11.6cqi, 13.5vh, 10rem);
  line-height: 0.95;
  margin-bottom: var(--space-xl);
}

/* Spanish capitals carry accents (MÁS, PRODUCCIÓN) that rise above the cap
   height, into the space the eyebrow gap was sized for. In em, so the room
   scales with the headline. */
:lang(es) .hero-title.hero-title-claim {
  font-size: min(8.4vw, 13.5vh, 10rem);
  font-size: min(9.5cqi, 13.5vh, 10rem);
  padding-top: 0.14em;
}

/* The payoff always takes its own line, whatever the width. On an
   ultrawide the whole claim would otherwise fit on one. fit-content keeps
   the gold gradient spread across the words rather than across the frame. */
.hero-title-claim .gradient-text {
  display: block;
  width: fit-content;
}

.hero-title {
  font-family: var(--font-display);
  font-size: var(--text-5xl);
  font-weight: 400;
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  margin-bottom: var(--space-md);
  text-wrap: balance;
}

/* One line, one idea. Held to a short measure so it reads as a statement
   rather than as a paragraph. */
.hero-lead {
  font-size: var(--text-xl);
  color: var(--ink-soft);
  font-weight: 400;
  line-height: var(--leading-dense);
  max-width: 34ch;
  text-wrap: pretty;
}

.hero-actions {
  margin-top: var(--space-xl);
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

.hero-actions .btn { white-space: nowrap; }

/* Room above the download lets the real PDF page overlap its top edge. */
.hero-links-row {
  margin-top: var(--space-xl);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-xl) var(--space-lg);
}

.hero-profiles {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.hero-social-link {
  --profile-color: #0a66c2;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  color: var(--ink-soft);
  font-size: var(--text-md);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  border-radius: var(--radius-sm);
}

.hero-social-artstation { --profile-color: #13aff0; }

.hero-social-icon {
  width: 28px;
  height: 28px;
  flex: none;
  object-fit: contain;
}

.hero-social-linkedin .hero-social-icon {
  background: #ffffff;
  border-radius: 3px;
}

.hero-social-arrow {
  width: 14px;
  height: 14px;
  color: var(--ink-faint);
  transform: rotate(-45deg);
}

.hero-social-link:is(:hover, :focus-visible) { color: var(--ink-bright); }
.hero-social-link:is(:hover, :focus-visible) .hero-social-arrow { color: var(--profile-color); }
.hero-social-link:focus-visible { outline-color: var(--profile-color); }

/* A warm outlined surface gives the résumé its own hierarchy below the CTAs.
   The preview and label form one download target, including the overlap. */
.hero-resume {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  width: 250px;
  min-height: 72px;
  padding: 14px 18px 14px 100px;
  border: 1px solid rgba(247, 212, 114, 0.35);
  border-radius: var(--radius-sm);
  background: linear-gradient(115deg, rgba(224, 174, 60, 0.12), rgba(224, 174, 60, 0.035)), var(--void-black);
  box-shadow: inset 0 1px 0 rgba(255, 244, 208, 0.08);
  color: var(--ink-bright);
  text-decoration: none;
}

.hero-resume-preview {
  position: absolute;
  left: 16px;
  bottom: 12px;
  width: 68px;
  aspect-ratio: 417 / 590;
  transform: rotate(-8deg);
  transform-origin: 50% 85%;
  border: 1px solid rgba(255, 255, 255, 0.5);
  border-radius: 4px;
  background: #e1e5e9;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}

.hero-resume-preview img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 3px;
}

.hero-resume-filetype {
  position: absolute;
  right: -7px;
  bottom: 8px;
  padding: 3px 5px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 3px;
  background: #9d3029;
  color: #ffffff;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.04em;
}

.hero-resume-copy { display: grid; gap: 3px; }
.hero-resume-title { font-size: var(--text-md); font-weight: 700; white-space: nowrap; }
.hero-resume-hint { font-size: var(--text-xs); color: var(--ink-soft); white-space: nowrap; }
.hero-resume-arrow { width: 17px; height: 17px; color: var(--metal-bright); transform: rotate(90deg); }
.hero-resume:is(:hover, :focus-visible) { border-color: var(--metal-bright); }
.hero-resume:focus-visible { outline-color: var(--metal-bright); }

/* Hover feedback only: no perpetual floating, and keyboard focus stays still. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .hero-social-icon { transition: transform 220ms var(--ease-out); }
  .hero-social-arrow { transition: transform 180ms var(--ease-out); }
  .hero-social-link:hover .hero-social-icon { transform: translateY(-2px) scale(1.06); }
  .hero-social-link:hover .hero-social-arrow { transform: translate(2px, -2px) rotate(-45deg); }
  .hero-social-link:active .hero-social-icon { transform: scale(0.96); }
  .hero-resume { transition: transform 240ms var(--ease-out); }
  .hero-resume-preview { transition: transform 320ms var(--ease-out); }
  .hero-resume-arrow { transition: transform 180ms var(--ease-out); }
  .hero-resume:hover { transform: translateY(-2px); }
  .hero-resume:hover .hero-resume-preview { transform: translateY(-4px) rotate(-3deg); }
  .hero-resume:hover .hero-resume-arrow { transform: translateY(3px) rotate(90deg); }
  .hero-resume:active { transform: translateY(0); }
}

@media (max-width: 560px) {
  .hero-links-row { flex-direction: column; align-items: flex-start; gap: var(--space-xl); }
  .hero-profiles { gap: var(--space-lg); }
  .hero-social-link { font-size: 14px; gap: 8px; }
  .hero-social-icon { width: 24px; height: 24px; }
  .hero-resume { width: min(100%, 280px); }
}

@media (max-width: 360px) {
  .hero-profiles { gap: var(--space-sm); }
}

/* Button Component */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding: var(--space-sm) var(--space-lg);
  font-family: var(--font-heading);
  font-size: var(--text-md);
  font-weight: 700;
  border-radius: var(--radius-sm);
  text-decoration: none;
  cursor: pointer;
  transition: all 0.3s var(--ease-out);
}

.btn-sm { padding: var(--space-3xs) var(--space-xs); font-size: var(--text-sm); white-space: nowrap; }
.btn-block { width: 100%; }

/* A struck gold surface carrying a live rim: the tight ramp, a lit top edge,
   a dark bottom edge, a warm bloom behind it, and the outline turning.
   The specular is a background layer rather than a pseudo, which keeps the
   border area clear for the rim and drops the need for overflow: hidden.
   Border is transparent and the gold is clipped to the padding box, so the
   rim has a 1px channel of its own to sit in. */
.btn-primary {
  --rim-stops: var(--light-strip-gold);
  --rim-speed: 5.5s;
  --bloom-tint: var(--light-wash-gold);
  --bloom-rest: 0.26;
  --bloom-hover: 0.55;
  background-image:
    linear-gradient(101deg,
      transparent 38%,
      rgba(255, 253, 240, 0.5) 48%,
      transparent 60%),
    linear-gradient(172deg, var(--metal-ramp-tight));
  background-size: 260% 100%, auto;
  background-position: 210% 0, 0 0;
  background-repeat: no-repeat;
  background-clip: padding-box;
  color: var(--on-metal);
  border: 1px solid transparent;
  /* The 0 8px 26px warm layer was this button's entire glow: one radius,
     offset downward, which is a drop shadow tinted gold rather than a
     solid gold object scattering light. Replaced by the ladder, with the
     black contact shadow kept -- that one is occlusion, not glare, and
     the two are different things. */
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 228, 0.8),
    inset 0 -1px 0 rgba(74, 44, 6, 0.55),
    var(--glow-gold),
    0 2px 6px rgba(0, 0, 0, 0.55);
  transition:
    transform 0.3s var(--ease-out),
    box-shadow 0.3s var(--ease-out),
    background-position 0.75s var(--ease-out);
}

/* Hover wipes the specular across the face and lifts the bloom */
.btn-primary:hover {
  transform: translateY(-2px);
  background-position: -140% 0, 0 0;
  box-shadow:
    inset 0 1px 0 rgba(255, 252, 236, 0.9),
    inset 0 -1px 0 rgba(74, 44, 6, 0.5),
    var(--glow-gold-lift),
    0 4px 12px rgba(0, 0, 0, 0.55);
}

.btn-primary:active {
  transform: translateY(0);
  box-shadow:
    inset 0 2px 5px rgba(74, 44, 6, 0.55),
    inset 0 -1px 0 rgba(255, 245, 210, 0.35),
    0 4px 14px rgba(224, 174, 60, 0.2),
    0 1px 3px rgba(0, 0, 0, 0.6);
}

.btn-secondary {
  background: rgba(255, 255, 255, 0.06);
  color: #ffffff;
  border: 1px solid var(--border-subtle);
  backdrop-filter: blur(12px);
}

.btn-secondary:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.3);
  transform: translateY(-2px);
}

/* One focus treatment for everything focusable, inputs and selects
   included. It previously covered only a and button, which left every
   field in the contact form with no keyboard affordance at all once
   .form-row's own `outline: none` had stripped the UA default. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--light-core);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}

.scroll-hint {
  position: absolute;
  bottom: var(--space-lg);
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2xs);
  color: var(--ink-faint);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--track-caps-wide);
  text-transform: uppercase;
  z-index: 2;
  transition: opacity 0.3s ease;
  /* Decorative: a scroll cue with no link and no handler (JS only fades it
     out past 80px). Absolutely positioned over the bottom of the hero, it
     was swallowing the lower half of the reel dots' new touch targets, so
     a tap just under a dot did nothing at all. Nothing here should ever
     take a pointer event. */
  pointer-events: none;
}

.scroll-mouse {
  width: 22px;
  height: 34px;
  border: 2px solid var(--border-subtle);
  border-radius: var(--radius-md);
  position: relative;
}

.scroll-mouse::after {
  content: '';
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  width: 4px;
  height: 7px;
  background: var(--metal-mid);
  border-radius: var(--radius-xs);
  animation: scrollAnim 1.8s infinite;
}

@keyframes scrollAnim {
  0% { transform: translate(-50%, 0); opacity: 1; }
  100% { transform: translate(-50%, 14px); opacity: 0; }
}

/* ─────────────────────────────────────────────
   PATTERN, the context setter (replaced #about)
   Typographic, deliberately NOT a card grid. The deleted "How I Work"
   section was six boxes of qualities, and the box grid was half of why it
   read as self-congratulatory. Prose in a column, one list, three closing
   lines. The only cards here are the stat strip, which is evidence.
────────────────────────────────────────────── */
#pattern {
  padding: var(--space-4xl) 0;
  position: relative;
  z-index: 2;
}

.pattern-body {
  /* Set in rem, not ch, so the lead, the rows and the caption resolve to
     the SAME column despite sitting at three different font sizes. In ch
     they drifted to 38 / 111 / 79 and the block stopped reading as one
     piece of typesetting. */
  max-width: 40rem;
}

.pattern-lead {
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--ink);
  line-height: var(--leading-dense);
  text-wrap: pretty;
  margin-bottom: var(--space-2xl);
}

/* --- The mechanism -------------------------------------------
   Five rows, one control. Emphasis between the two states is value and
   weight, never a colour swap: the problem sits at --ink-soft, the fix at
   --ink-bright, so the list visibly resolves.
------------------------------------------------------------- */
.pattern-mech {
  max-width: 42rem;
  margin-bottom: var(--space-3xl);
}

/* Focusable but not visible. Needed because the switch is driven by real
   radio inputs, which is where its keyboard behaviour comes from. */
.u-vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Same trough and physics as the contact form path toggle. Sized to its
   content rather than the column: this is a small control, and a
   full-width segment would read as navigation. */
.pattern-switch {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3xs);
  width: max-content;
  max-width: 100%;
  background: rgba(3, 4, 10, 0.5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: 4px;
  margin-bottom: var(--space-lg);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.45);
}

.pattern-seg {
  padding: var(--space-xs) var(--space-lg);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--leading-dense);
  color: var(--ink-soft);
  text-align: center;
  cursor: pointer;
  user-select: none;
  transition: color 0.25s ease, background 0.25s ease, box-shadow 0.25s ease;
}

.pattern-seg:hover { color: var(--ink); }

/* Raised and neutral, never gold. Gold on this page means "act on this",
   and a view selector is not the action: the same reasoning that took the
   metal off the contact form active path tab. */
#pattern-view-problem:checked ~ [for="pattern-view-problem"],
#pattern-view-fix:checked ~ [for="pattern-view-fix"] {
  background: var(--surface-3);
  color: var(--ink-bright);
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 235, 0.14),
    inset 0 -1px 0 rgba(0, 0, 0, 0.45),
    0 2px 6px rgba(0, 0, 0, 0.45);
}

/* The input carries focus but cannot show it, so the ring is drawn on the
   label it controls. */
#pattern-view-problem:focus-visible ~ [for="pattern-view-problem"],
#pattern-view-fix:focus-visible ~ [for="pattern-view-fix"] {
  outline: 2px solid var(--light-core);
  outline-offset: 3px;
}

.pattern-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  gap: var(--space-md);
}

.pattern-row {
  display: grid;
  /* The marker column has to clear the marker's GROWN width, not its
     resting one. At --space-xl the extended mark ended flush against the
     text with a 0px gap. */
  grid-template-columns: var(--space-2xl) 1fr;
  align-items: center;
}

/* The marker extends and warms as the row resolves. A mechanical movement,
   so the change reads as the line being completed rather than as a tint.
   --metal-deep, not --metal-mid: a 1px state marker should not carry the
   same weight as the rail or a node. */
.pattern-mark {
  height: 1px;
  width: var(--space-md);
  background: var(--ink-faint);
  transition:
    width 0.42s cubic-bezier(0.16, 1, 0.3, 1),
    background-color 0.42s ease;
  transition-delay: calc(var(--i) * 55ms);
}

/* Both states occupy the same cell, so the row is as tall as the taller of
   the two and the switch causes zero layout shift. align-items: center is
   not cosmetic here: a grid item stretches by default, so the shorter of
   the two states rendered at the top of a box sized by the longer one and
   dropped a visible hole under row 3, which read as a grouping break
   between rows 3 and 4. Centred, the spare height splits evenly and the
   run keeps one rhythm. */
.pattern-swap {
  display: grid;
  align-items: center;
}

.pattern-state {
  grid-area: 1 / 1;
  font-size: var(--text-lg);
  line-height: var(--leading-body);
  text-wrap: pretty;
  transition:
    opacity 0.34s cubic-bezier(0.16, 1, 0.3, 1),
    transform 0.42s cubic-bezier(0.16, 1, 0.3, 1),
    filter 0.42s cubic-bezier(0.16, 1, 0.3, 1),
    visibility 0s;
}

.pattern-state--problem { color: var(--ink-soft); }
.pattern-state--fix { color: var(--ink-bright); }

/* Outgoing. visibility flips only after the fade has finished, which both
   keeps it off the accessibility tree and stops it popping. */
.pattern-mech:has(#pattern-view-problem:checked) .pattern-state--fix,
.pattern-mech:has(#pattern-view-fix:checked) .pattern-state--problem {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  filter: blur(6px);
  transition-delay:
    calc(var(--i) * 55ms),
    calc(var(--i) * 55ms),
    calc(var(--i) * 55ms),
    calc(var(--i) * 55ms + 0.34s);
}

/* Incoming, held back so the row it replaces has cleared first. */
.pattern-mech:has(#pattern-view-problem:checked) .pattern-state--problem,
.pattern-mech:has(#pattern-view-fix:checked) .pattern-state--fix {
  opacity: 1;
  visibility: visible;
  transform: none;
  filter: blur(0);
  transition-delay: calc(var(--i) * 55ms + 0.12s);
}

.pattern-mech:has(#pattern-view-fix:checked) .pattern-mark {
  width: var(--space-xl);
  background: var(--metal-deep);
}

/* The fix paragraph, now the switch explanation. */
.pattern-caption {
  margin-top: var(--space-xl);
  max-width: 36rem;
  font-size: var(--text-md);
  color: var(--ink-soft);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

/* Load-bearing line. Carries the most air on the block and the brightest
   ink, because it is the conclusion the whole mechanism argues toward. */
.pattern-punch {
  font-size: var(--text-2xl);
  font-weight: 600;
  color: var(--ink-bright);
  line-height: var(--leading-dense);
  text-wrap: balance;
  max-width: 24ch;
  margin-bottom: var(--space-md);
}

.pattern-handoff {
  font-size: var(--text-lg);
  color: var(--ink-soft);
  line-height: var(--leading-body);
}

@media (max-width: 560px) {
  .pattern-row { grid-template-columns: var(--space-xl) 1fr; }
  .pattern-mark { width: var(--space-sm); }
  .pattern-mech:has(#pattern-view-fix:checked) .pattern-mark { width: var(--space-md); }
}

/* Motion is the argument here, not ornament, so reduced motion keeps the
   state change and drops only the travel, blur and stagger. */
@media (prefers-reduced-motion: reduce) {
  .pattern-state,
  .pattern-mark {
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
  }

  .pattern-state {
    transform: none !important;
    filter: none !important;
  }
}


/* The stat strip runs full width now, not inside a 0.75fr column, so it
   goes 4 across and can afford the padding .stat-card gave up back when it
   was squeezed into half a grid cell. */
.stats-grid.stats-strip {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-top: var(--space-3xl);
}

/* The strip has to out-specify .stats-grid (same single-class weight, but
   declared later in the file), which then means it also out-specifies the
   420px .stats-grid override further down. So it owns all three tiers here,
   in descending order, and the last matching one wins. */
@media (max-width: 992px) {
  .stats-grid.stats-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 420px) {
  .stats-grid.stats-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.stats-strip .stat-card { padding: var(--space-lg) var(--space-md); }

/* ORPHANED by the repositioning: .about-grid, .about-lead, .about-beats,
   .beat and .beat-punch below have no markup left to style since #about was
   replaced by #pattern. Kept this pass so the change stays cheap to revert
   during review. Delete them once the repositioning is accepted. */

.about-grid {
  display: grid;
  /* 1.25fr/0.75fr left too little room for four 2-column stat cards to
     hold their longest word ("Automatización" needs ~137px; 0.75fr left
     each card only ~115px of content width even after tightening its
     padding). The lead paragraph's own max-width (about-lead: 40ch,
     .beat: 52ch) was already well under either column width, so it reads
     identically at 1fr -- this column only ever had spare width to give. */
  grid-template-columns: 1fr 1fr;
  gap: var(--space-2xl);
  align-items: center;
}

.about-lead {
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--ink);
  line-height: var(--leading-dense);
  margin: var(--space-md) 0 var(--space-lg);
  max-width: 40ch;
}

/* The spine is gone. The three tick marks on the beats are enough to say these
   belong together. */
.about-beats {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding-left: var(--space-lg);
}

.beat {
  position: relative;
  font-size: var(--text-lg);
  color: var(--ink-soft);
  line-height: var(--leading-body);
  max-width: 52ch;
}

.beat::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--space-lg));
  top: 0.72em;
  width: 9px;
  height: 1px;
  background: var(--ink-faint);
}

.beat-punch {
  color: var(--ink);
  font-weight: 600;
}

.beat-punch::before {
  width: 17px;
  left: calc(-1 * var(--space-lg));
  height: 2px;
  background: linear-gradient(90deg, var(--light-core), transparent);
}

.stats-grid {
  display: grid;
  /* minmax(0, 1fr): a plain 1fr track can't shrink below its label's
     min-content, so "AUTOMATION" (and the longer Spanish equivalent)
     forces the row wider than the viewport instead of wrapping its text. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-xl);
}

.stat-card {
  background: var(--surface-2);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  /* .about-grid gives this column 0.75fr, split two ways again by
     .stats-grid -- --space-xl on all four sides left only ~83px of real
     content width at 1440px, not enough even for "Automation" alone
     (~102px), so it silently overflowed the card before overflow-wrap:
     break-word (added site-wide as a Spanish-string safety net) turned
     that into a visible mid-word break instead. --space-lg still isn't
     quite enough (~99px); --space-md gives every label's longest word
     real margin instead of squeaking by. */
  padding: var(--space-md);
  transition:
    transform 0.4s var(--ease-out),
    background 0.4s ease,
    border-color 0.4s ease,
    box-shadow 0.4s ease;
  /* Glare added to the edge physics. These are the four struck objects
     the surface rule kept a card for, so they are the page's clearest
     case of "metal, lit from above" and the only ones that carry the
     gold ladder at rest. */
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-2), var(--glow-gold);
  /* Rim runs at rest, but dim. Four cards at full brightness is four things
     shouting at once, and then none of them is emphasis. Hover takes the
     one you are pointing at to full. Declared after .rim-live so it wins. */
  --rim-rest: 0.4;
  --rim-speed: 7s;
  --bloom-hover: 0.42;
}

/* Staggered so the four rings are not locked to the same beat */
.stat-card:nth-child(2)::before { animation-delay: -1.75s; }
.stat-card:nth-child(3)::before { animation-delay: -3.5s; }
.stat-card:nth-child(4)::before { animation-delay: -5.25s; }

.stat-card:hover {
  border-color: var(--border-glow);
  transform: translateY(-4px);
  background: var(--surface-3);
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-3), var(--glow-gold-lift);
}

/* Metal comes from the STRUCK GOLD block above, this only sets the type */
.stat-number {
  font-family: var(--font-display);
  font-size: var(--text-numeral);
  font-weight: 400;
  line-height: 1;
  margin-bottom: var(--space-2xs);
  white-space: nowrap;
}

/* Offset per card so the four numbers catch the light in sequence
   rather than all flashing on the same beat */
.stat-card:nth-child(2) .stat-number { animation-delay: 0.35s; }
.stat-card:nth-child(3) .stat-number { animation-delay: 0.7s; }
.stat-card:nth-child(4) .stat-number { animation-delay: 1.05s; }

/* The AI card stays violet, so it opts out of the metal entirely */
/* The AI card is struck gold like its three neighbours. Its difference is
   carried by the label and the rim, not by a second accent colour. */
.stat-card-alt:hover { border-color: var(--border-glow); }

.stat-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
}

/* ─────────────────────────────────────────────
   PILLAR INDEX, expanding rows, not cards
────────────────────────────────────────────── */
#skills {
  padding: var(--space-4xl) 0;
  position: relative;
  z-index: 2;
  background: radial-gradient(ellipse at 50% 0%, rgba(224, 174, 60, 0.08) 0%, transparent 70%);
}

/* No rules between rows. The gradient sweep on hover is the only thing that
   marks a row, so at rest the list is five names on open ground. */
.pillar-index {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.pillar-row {
  position: relative;
}

/* The animated cyan → violet → pink sweep under the active row */
.pillar-row::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 1px;
  background: linear-gradient(90deg, var(--light-core), var(--light-fade) 60%, transparent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.55s var(--ease-out);
}

.pillar-row.is-open::after,
.pillar-row:hover::after { transform: scaleX(1); }

.pillar-head {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  width: 100%;
  padding: var(--space-md) var(--space-3xs);
  background: none;
  border: none;
  cursor: pointer;
  text-align: left;
  color: inherit;
  font-family: var(--font-heading);
}

.pillar-num {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 400;
  color: var(--ink-faint);
  letter-spacing: var(--track-caps);
  font-variant-numeric: tabular-nums;
  transition: color 0.3s ease, transform 0.4s var(--ease-out);
}

.pillar-name {
  flex: 1;
  font-size: var(--text-2xl);
  font-weight: 700;
  letter-spacing: var(--track-display);
  color: var(--ink-soft);
  line-height: var(--leading-tight);
  transition: color 0.3s ease, transform 0.4s var(--ease-out);
  text-wrap: balance;
}

/* Indent on hover/open, transform, so no layout is recalculated */
.pillar-head:hover .pillar-num,
.pillar-head:hover .pillar-name,
.pillar-row.is-open .pillar-num,
.pillar-row.is-open .pillar-name { transform: translateX(0.85rem); }

.pillar-head:hover .pillar-name,
.pillar-row.is-open .pillar-name { color: #ffffff; }

.pillar-head:hover .pillar-num,
.pillar-row.is-open .pillar-num { color: var(--metal-bright); }

/* The circled +/× is gone and stays gone: it was a button-shaped object
   sitting inside a row that is already entirely a button, and it added
   chrome to a list whose whole point is that it is five names on open
   ground.

   But removing it left the rows with no affordance at all. Five headings
   that happen to be clickable read as five headings, and "the row says it
   is open by opening" only helps a visitor who has already guessed to
   click. Four of the five are shut on arrival, so most of this section
   was invisible unless the visitor experimented.

   What replaces it is the smallest thing that can carry the meaning: two
   hairlines, no circle, no fill, no box. It rotates to a × on open, which
   is the affordance and the state in one mark. Built from the element's
   own borders rather than glyphs so it stays optically identical in both
   states and needs no font. */
.pillar-mark {
  position: relative;
  flex: none;
  width: 14px;
  height: 14px;
  opacity: 0.45;
  transition: opacity 0.3s ease, transform 0.45s var(--ease-out);
}

.pillar-mark::before,
.pillar-mark::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  background: currentColor;
  transform: translate(-50%, -50%);
}

/* The bar of the + */
.pillar-mark::before {
  width: 100%;
  height: 1px;
}

/* The stem, which is the half that disappears when the mark becomes a − */
.pillar-mark::after {
  width: 1px;
  height: 100%;
  transition: opacity 0.35s var(--ease-out);
}

.pillar-head:hover .pillar-mark { opacity: 1; }

/* 180deg, not 90: at 90 the surviving horizontal bar turns vertical and
   the mark reads as a stray "|". Half a turn leaves the bar where it
   started, so the mark lands on a clean − while the stem is visibly
   rotated out of it. The motion is the feedback; the bar is the state. */
.pillar-row.is-open .pillar-mark {
  opacity: 1;
  transform: rotate(180deg);
  color: var(--metal-bright);
}

.pillar-row.is-open .pillar-mark::after { opacity: 0; }

/* The icon inside the span is no longer used; the borders draw the mark. */
.pillar-mark .icon { display: none; }

.pillar-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.5s var(--ease-out);
}

.pillar-row.is-open .pillar-panel { grid-template-rows: 1fr; }

/* padding-bottom used to live here, and it was 64px of dead air under every
   CLOSED row. min-height: 0 governs the content box only, so padding is
   outside what grid-template-rows: 0fr can collapse: the row measured 64px
   tall while claiming to be shut, four times over in a five-row list. The
   spacing moved onto the last child instead, where the 0fr collapse can
   actually reach it. overflow: hidden already makes this a BFC, so that
   margin stays contained rather than escaping to the next row. */
.pillar-panel-inner {
  overflow: hidden;
  min-height: 0;
}

.pillar-line {
  font-size: var(--text-lg);
  line-height: var(--leading-body);
  color: var(--ink-soft);
  max-width: 56ch;
  margin: 0 0 var(--space-sm) var(--space-2xl);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.4s ease 0.1s, transform 0.4s var(--ease-out) 0.1s;
}

.pillar-row.is-open .pillar-line { opacity: 1; transform: translateY(0); }

/* Carries the panel's bottom spacing now (see .pillar-panel-inner above).
   As the last child inside the collapsing box, this margin goes to zero
   with the row instead of propping it open. */
.pillar-panel-inner .skill-tags {
  margin: 0 0 var(--space-2xl) var(--space-2xl);
}

.skill-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}

/* Same chip as Career's and the 3D section's: neutral ground, an inset
   ring for the edge, a pill. Gold tint removed, because gold here means
   "act on this" and a capability tag is not an action. */
.skill-tags span,
.timeline-tags span {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  padding: var(--space-3xs) var(--space-sm);
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px rgba(236, 235, 242, 0.08);
  border-radius: var(--radius-pill);
  color: var(--ink);
}

/* ─────────────────────────────────────────────
   PROOF, case studies
────────────────────────────────────────────── */
#proof {
  /* The two case studies are the only load-bearing evidence on the site.
     They arrive after a noticeably longer silence than anything else. */
  padding: var(--space-5xl) 0;
  position: relative;
  z-index: 2;
  background: radial-gradient(ellipse at 80% 10%, rgba(150, 174, 224, 0.06) 0%, transparent 60%);
}

/* The two cases are separated by space, not by rules. */
.case {
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  gap: var(--space-2xl);
  align-items: center;
  padding: var(--space-4xl) 0;
}

/* The first case follows the section head, not another case: the 144px is
   there to separate two cases from each other, and stacking it on top of
   the head's own margin leaves a void the section reads as a gap. */
.case:first-of-type { padding-top: var(--space-lg); }


.case-name {
  font-size: var(--text-3xl);
  font-weight: 700;
  margin-bottom: var(--space-3xs);
  text-wrap: balance;
}

.case-role {
  font-size: var(--text-md);
  font-weight: 600;
  color: var(--ink-soft);
  margin-bottom: var(--space-lg);
}

.case-beats {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  margin-bottom: var(--space-xl);
}

.case-beat dt {
  font-family: var(--font-heading);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink-soft);
  margin-bottom: var(--space-2xs);
}

.case-beat dd {
  font-size: var(--text-md);
  line-height: var(--leading-body);
  color: var(--ink-soft);
  max-width: 56ch;
}

.case-beats .case-beat:last-child dd {
  color: var(--ink);
  font-weight: 500;
}

/* Three columns rather than a wrapping flex row: the display face is wide
   enough that a 64px gap pushed the third metric onto its own line. Columns
   take their content's width, because equal thirds clipped "100k+" against
   the narrower body of the flipped case. */
.case-metrics {
  display: grid;
  grid-template-columns: repeat(3, auto);
  justify-content: start;
  gap: var(--space-xl);
}

.case-metric {
  display: flex;
  flex-direction: column;
  gap: var(--space-3xs);
}

/* A struck object, like the stat numbers. Metal comes from the shared
   selector list in the STRUCK GOLD section. */
/* Bumped a step. The metal ramp runs dark at the bottom, so a small numeral
   loses its lower half against the room before it loses its shape. */
.case-metric-value {
  font-family: var(--font-display);
  font-size: clamp(2.1rem, 3.4vw, 2.9rem);
  font-weight: 400;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.case-metric-label {
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: var(--ink-faint);
}

/* No plate behind them. The queue and the deck float on the room, which is
   the point of both figures: jobs shipping in turn, cards dealt on demand. */
.case-visual {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  min-height: 300px;
  padding: var(--space-xl);
  /* The deck cards translate ±132px off-centre (deckCycle / nth-child
     transforms below); on a narrow phone that pushes the outer cards past
     this box's own edge with nothing to clip them. */
  overflow: hidden;
}

.case-flip .case-visual { order: -1; }

.case-visual figcaption {
  font-size: var(--text-sm);
  color: var(--ink-faint);
  text-align: center;
  max-width: 34ch;
  line-height: var(--leading-dense);
}

/* Case 1 visual, the Lloyds queue.
   Real output, not a diagram: each item is one auction's pair, a wide
   rotator and a square tile, exported without the tracker around it. It
   replaced a nine-figure diagram that only illustrated the 9x already
   stated in the metrics beside it.

   The motion is a queue, deliberately not the GDS fan. The front pair
   ships (drops toward the viewer and fades), the two behind step forward,
   and a new one arrives at the back. Eight items, one step every 2.5s.

   All offsets are percentages of the item's own height, so the stack
   keeps its shape at every width without a breakpoint. */
.queue {
  position: relative;
  width: min(100%, 420px);
  /* In item heights: the back step's overhang (0.27), the front item (1)
     and the exit drop (0.16), on a 2.785:1 item. */
  aspect-ratio: 420 / 217;
}

.queue-item {
  position: absolute;
  left: 0;
  right: 0;
  /* Clears the back step's 0.27 item heights of overhang: 0.28 / 1.44. */
  top: 19%;
  /* The source corners are 48px on a 1903x684 export. */
  border-radius: 2.52% / 7.02%;
  box-shadow: var(--elev-2);
  opacity: 0;
  animation: queueStep 20s var(--ease-out) infinite;
}

.queue-item img {
  display: block;
  width: 100%;
  height: auto;
}

/* One step apart. With these delays the item in front at load is the
   first, and the queue behind it runs 8, 7, 6... */
.queue-item:nth-child(1) { animation-delay: 0s; }
.queue-item:nth-child(2) { animation-delay: -2.5s; }
.queue-item:nth-child(3) { animation-delay: -5s; }
.queue-item:nth-child(4) { animation-delay: -7.5s; }
.queue-item:nth-child(5) { animation-delay: -10s; }
.queue-item:nth-child(6) { animation-delay: -12.5s; }
.queue-item:nth-child(7) { animation-delay: -15s; }
.queue-item:nth-child(8) { animation-delay: -17.5s; }

/* One item's lap, in 12.5% steps (20s / 8). Every step starts with the
   front item shipping (0.4s, fading as it drops) and only then, 0.3s in,
   does the queue step up (0.7s). Moving both at once put the fading item
   over the arriving one and ghosted two banners into each other. After
   that everything holds, so the queue reads as discrete jobs, not a
   conveyor. The wait at the back is invisible, so the jump there from the
   exit position costs nothing. */
@keyframes queueStep {
  0%    { transform: translateY(-18%) scale(0.94); filter: brightness(0.7);  opacity: 1; z-index: 3; }
  1.5%  { transform: translateY(-18%) scale(0.94); filter: brightness(0.7);  opacity: 1; z-index: 3; }
  5%    { transform: translateY(0) scale(1);       filter: brightness(1);    opacity: 1; z-index: 4; }
  12.5% { transform: translateY(0) scale(1);       filter: brightness(1);    opacity: 1; z-index: 5; }
  14.5% { transform: translateY(15%) scale(1.02);  filter: brightness(1);    opacity: 0; z-index: 5; }
  20%   { transform: translateY(-42%) scale(0.82); filter: brightness(0.4);  opacity: 0; z-index: 1; }
  76.5% { transform: translateY(-42%) scale(0.82); filter: brightness(0.4);  opacity: 0; z-index: 1; }
  80%   { transform: translateY(-33%) scale(0.88); filter: brightness(0.5);  opacity: 1; z-index: 2; }
  89%   { transform: translateY(-33%) scale(0.88); filter: brightness(0.5);  opacity: 1; z-index: 2; }
  92.5% { transform: translateY(-18%) scale(0.94); filter: brightness(0.7);  opacity: 1; z-index: 3; }
  100%  { transform: translateY(-18%) scale(0.94); filter: brightness(0.7);  opacity: 1; z-index: 3; }
}

/* Case 2 visual, the live-data deck.
   Five real cards (his picks, rasterised from the Figma SVGs), not drawn
   stand-ins. The shuffle is still the argument: one system, dealing out
   variants on demand. What changed is the honesty of the depth cue. The
   mock cards faded with opacity and hue-cycled; real artwork does neither.
   Depth is brightness, so a card behind never shows through the card in
   front, and every card keeps its true colours. */
.deck {
  position: relative;
  width: 100%;
  height: 290px;
  display: grid;
  place-items: center;
}

.deck-card {
  position: absolute;
  width: 164px;
  /* The source is 400x575, and the corner radius is 17px of that 400. */
  aspect-ratio: 400 / 575;
  border-radius: 7px;
  box-shadow: var(--elev-3);
  animation: deckCycle 15s var(--ease-out) infinite;
}

.deck-card img {
  display: block;
  width: 100%;
  height: 100%;
}

/* One slot apart: the period divided by the card count. */
.deck-card:nth-child(1) { animation-delay: 0s; }
.deck-card:nth-child(2) { animation-delay: -3s; }
.deck-card:nth-child(3) { animation-delay: -6s; }
.deck-card:nth-child(4) { animation-delay: -9s; }
.deck-card:nth-child(5) { animation-delay: -12s; }

/* Five slots, far left to far right, then the back card slips behind the
   others to the far left again. The span stays inside .case-visual's own
   padding at every width: 132px of travel plus half a 164px card is 214px
   either side of centre. */
@keyframes deckCycle {
  0%   { transform: translate(-132px, 20px) rotate(-10deg) scale(0.84); filter: brightness(0.5);  z-index: 1; }
  20%  { transform: translate(-68px, 7px)   rotate(-5deg)  scale(0.92); filter: brightness(0.75); z-index: 2; }
  40%  { transform: translate(0, -6px)      rotate(0deg)   scale(1);    filter: brightness(1);    z-index: 3; }
  60%  { transform: translate(68px, 7px)    rotate(5deg)   scale(0.92); filter: brightness(0.75); z-index: 2; }
  80%  { transform: translate(132px, 20px)  rotate(10deg)  scale(0.84); filter: brightness(0.5);  z-index: 1; }
  100% { transform: translate(-132px, 20px) rotate(-10deg) scale(0.84); filter: brightness(0.5);  z-index: 1; }
}

.section-head-sub {
  margin-bottom: var(--space-2xl);
}

.section-title-sub {
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--ink);
  margin-bottom: var(--space-sm);
  text-wrap: balance;
}

/* Experience Timeline */
#experience {
  padding: var(--space-4xl) 0;
  position: relative;
  z-index: 2;
}

.timeline {
  position: relative;
  max-width: 980px;
  margin: 0 auto;
  padding: var(--space-lg) 0;
}

.timeline::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 32px;
  width: 1px;
  background: linear-gradient(180deg, var(--metal-mid) 0%, rgba(224, 174, 60, 0.15) 100%);
}

/* Was --space-2xl (64px). With .timeline-card's own 24px of padding on
   each side, that put 112px between two consecutive two-line entries, and
   ten roles spread over 2200px of page. The rail stopped reading as a
   career you can take in and started reading as ten unrelated rows that
   happen to share a line.

   --space-lg brings the gap to 80px, which is still well clear of the
   32px inside each entry (so the run still groups correctly) but lets
   more of the career sit in one screen. Space is still doing the
   separating; there is just less of it per row. */
.timeline-item {
  position: relative;
  padding-left: 85px;
  margin-bottom: var(--space-sm);
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.5s ease, transform 0.5s var(--ease-out);
}

/* Without this, .timeline::before (top: 0; bottom: 0) inherits the last
   item's own trailing margin as extra height, so the rail runs 64px past
   the final node with nothing on it. */
.timeline-item:last-child { margin-bottom: 0; }

.timeline-item.visible {
  opacity: 1;
  transform: translateY(0);
}

.timeline-node {
  position: absolute;
  left: 24px;
  top: calc(var(--space-sm) + 0.35em);
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 1px solid var(--metal-bright);
  background: radial-gradient(circle, var(--metal-bright) 0 26%, var(--void-black) 32%);
  /* Was a lone 0 0 16px. A node is a small bright ring on near-black, which
     is the one situation where a single-radius glow is most obviously a
     drop shadow: there is no wide falloff, so the light stops dead at
     16px and the node reads as a sticker on the rail. */
  box-shadow: var(--glow-gold);
  z-index: 2;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

/* The node changes temperature under the cursor, so its glare has to as
   well: a cool ring throwing a warm scatter would read as two lights. */
.timeline-item:hover .timeline-node {
  border-color: var(--light-core);
  background: radial-gradient(circle, var(--light-core) 0 26%, var(--void-black) 32%);
  box-shadow: var(--glow-cool-lift);
}

/* No card. Company, role and dates stay readable at rest so the run of ten is
   still scannable; the detail arrives on hover. */
.timeline-card {
  padding: var(--space-sm) 0;
  transition: transform 0.35s var(--ease-out);
}

.timeline-card:hover { transform: translateX(8px); }

/* The detail is held back until asked for, and it collapses rather than
   just fading: reserving the space left ten holes down the page that read
   as mistakes rather than as air. grid-template-rows: 0fr -> 1fr instead
   of a fixed max-height: a max-height cap clips silently the moment any
   description (or its Spanish translation) runs longer than the guess;
   a 1fr row always sizes to the real content, so there's no cap to
   outgrow. Same technique .pillar-panel already uses. */
.timeline-desc,
.timeline-tags {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition:
    grid-template-rows 0.45s var(--ease-out),
    opacity 0.3s ease,
    margin-top 0.45s var(--ease-out);
}

.timeline-desc > span,
.timeline-tags-inner {
  overflow: hidden;
  min-height: 0;
}

.timeline-tags { margin-top: 0; }

.timeline-card:hover .timeline-desc,
.timeline-card:focus-within .timeline-desc {
  grid-template-rows: 1fr;
  margin-top: var(--space-sm);
  opacity: 1;
}

.timeline-card:hover .timeline-tags,
.timeline-card:focus-within .timeline-tags {
  grid-template-rows: 1fr;
  margin-top: var(--space-sm);
  opacity: 1;
}

/* Touch has no hover, so on a phone the entries are simply open. */
@media (hover: none) {
  .timeline-desc,
  .timeline-tags { grid-template-rows: 1fr; opacity: 1; }
  .timeline-desc { margin-top: var(--space-sm); }
  .timeline-tags { margin-top: var(--space-sm); }
}

.timeline-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-2xs);
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.timeline-company {
  font-family: var(--font-heading);
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--ink-bright);
}

.timeline-period {
  font-size: var(--text-sm);
  color: var(--ink-faint);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* One step under the company: same family, softer ink, lighter weight,
   so the run reads company, role, date instead of two bold lines. */
.timeline-role {
  font-size: var(--text-md);
  color: var(--ink);
  font-weight: 600;
}

.timeline-desc {
  color: var(--ink-soft);
  font-size: var(--text-md);
  line-height: var(--leading-body);
  max-width: 62ch;
}

.timeline-tags-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
}



/* Contact */
/* ─────────────────────────────────────────────
   PROCESS, what a buyer actually purchases
   Three cards, not a numbered vertical list: this is the one block on the
   page where the steps are parallel and equally weighted, which is what a
   row of equal cards says and a stacked list does not.
   No overflow/contain here. Either one kills .rim's mask.
────────────────────────────────────────────── */
/* ─────────────────────────────────────────────
   SHIPPED
   His two own products, between the case studies and the brief Projects
   tier. Each is a stage: a desktop window and a phone onto the live site,
   captured full length, beside the path it took from idea to live.

   Motion is asked for, never ambient. At rest both screens hold the top of
   the page and the window sits turned slightly into the room. Hover, or
   the stage reaching the middle of the viewport (.is-lit, set by
   initShipFocus), squares the window up and scrolls both screens through
   the page once. Leaving puts them back. Nothing loops.

     .ship-stage   isolation for the ambient wash, perspective for the turn
     .ship-desk    .rim (::before). Carries the turn. Never clips, or the
                   rim at inset -1px is cut off.
     .ship-screen  the only element that clips. A size container, so the
                   image can scroll exactly its own overflow: -100% + 100cqh.
────────────────────────────────────────────── */
.shipped { margin-top: var(--space-4xl); }

/* The tier head reads at the case names' size, so Shipped arrives as its
   own group inside Selected Work rather than as a footnote to case two. */
.shipped .section-title-sub {
  font-size: var(--text-3xl);
  color: var(--ink-bright);
}

.ship {
  display: grid;
  /* The stage is the argument, so it takes the larger share of the row. */
  grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr);
  gap: var(--space-2xl);
  align-items: center;
  padding: var(--space-xl) 0;
}

.ship + .ship { margin-top: var(--space-3xl); }

.ship--flip { grid-template-columns: minmax(0, 1.28fr) minmax(0, 0.72fr); }
.ship--flip .ship-stage { order: -1; }

/* Copy */
.ship-kicker {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  color: var(--ink-faint);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  margin-bottom: var(--space-xs);
}

.ship-index {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--text-lg);
  letter-spacing: 0;
  color: var(--metal-bright);
}

.ship-name {
  font-size: var(--text-2xl);
  font-weight: 700;
  margin-bottom: var(--space-sm);
  text-wrap: balance;
}

.ship-lede {
  color: var(--ink-soft);
  font-size: var(--text-lg);
  line-height: var(--leading-body);
  max-width: 46ch;
}

/* The path. One line from idea to live, drawn once when the stage arrives:
   cool where it starts, struck gold where it lands, because the live node
   is the one thing here that is finished. */
.ship-path {
  --rail: 1.9rem;
  list-style: none;
  position: relative;
  display: grid;
  gap: var(--space-md);
  margin: var(--space-xl) 0;
  padding-left: var(--rail);
}

.ship-path::before {
  content: '';
  position: absolute;
  left: 5px;
  top: 0.55rem;
  bottom: 0.9rem;
  width: 1px;
  background: linear-gradient(180deg,
    var(--light-fade) 0%,
    var(--light-body) 55%,
    var(--metal-bright) 100%);
  transform: scaleY(0);
  transform-origin: top;
  transition: transform 1.4s var(--ease-out) 0.25s;
}

.ship.visible .ship-path::before { transform: scaleY(1); }

.ship-step {
  position: relative;
  display: grid;
  gap: 0.15rem;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.6s ease, transform 0.6s var(--ease-out);
}

.ship.visible .ship-step { opacity: 1; transform: none; }
.ship.visible .ship-step:nth-child(1) { transition-delay: 0.30s; }
.ship.visible .ship-step:nth-child(2) { transition-delay: 0.65s; }
.ship.visible .ship-step:nth-child(3) { transition-delay: 1.00s; }

/* The node: a ring in the cool light, sitting on the rail. */
.ship-step::before {
  content: '';
  position: absolute;
  left: calc(var(--rail) * -1);
  top: 0.3rem;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 1px solid var(--light-body);
  background: var(--void-black);
  box-shadow: var(--glow-cool);
}

.ship-step--live::before {
  border-color: var(--metal-bright);
  background: radial-gradient(circle, var(--metal-bright) 0 32%, var(--void-black) 38%);
  box-shadow: var(--glow-gold-lift);
}

/* Same metadata treatment as the case beats above it. Clesmont stays on
   numerals, where its own @font-face note puts it. */
.ship-step-label {
  font-family: var(--font-heading);
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-soft);
}

.ship-step-text {
  color: var(--ink-soft);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  max-width: 48ch;
}

.ship-link {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--ink);
  font-size: var(--text-sm);
  font-weight: 700;
  text-decoration: none;
  transition: color 0.3s ease;
}

.ship-link .icon { transition: transform 0.35s var(--ease-out); }
.ship-link:hover,
.ship-link:focus-visible { color: var(--metal-bright); }
.ship-link:hover .icon,
.ship-link:focus-visible .icon { transform: translateX(4px); }

/* Stage */
.ship-stage {
  position: relative;
  isolation: isolate;
  perspective: 1800px;
  /* Room for the phone to stand in front of the window's lower corner. */
  padding: 0 0 var(--space-2xl) var(--space-2xl);
  --turn: -8deg;
  --scroll-time: 13s;
}

.ship--flip .ship-stage {
  padding: 0 var(--space-2xl) var(--space-2xl) 0;
  --turn: 8deg;
}

/* The room a little brighter around the object, and the floor it stands
   on. Both static: they composite once. */
.ship-stage::before,
.ship-stage::after {
  content: '';
  position: absolute;
  z-index: -1;
  pointer-events: none;
}

.ship-stage::before {
  inset: -6% -4% 4%;
  background: radial-gradient(ellipse 60% 55% at 55% 45%,
    rgba(150, 174, 224, 0.13), transparent 70%);
  filter: blur(30px);
}

.ship-stage::after {
  left: 10%;
  right: 10%;
  bottom: 0;
  height: 40px;
  background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.7), transparent 70%);
  filter: blur(12px);
}

.ship-desk {
  display: block;
  border-radius: 14px;
  background: #0a0c16;
  text-decoration: none;
  box-shadow:
    var(--edge-lit),
    var(--edge-fall),
    var(--elev-3),
    var(--glow-cool);
  transform: rotateY(var(--turn)) rotateX(3deg);
  transform-origin: center;
  transition:
    transform 1.1s var(--ease-out),
    box-shadow 0.6s ease;
}

.ship-stage:hover .ship-desk,
.ship-stage.is-lit .ship-desk {
  transform: none;
  box-shadow:
    var(--edge-lit),
    var(--edge-fall),
    var(--elev-3),
    var(--glow-cool-lift);
}

.ship-bar {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  height: 2.3rem;
  padding: 0 var(--space-sm);
  border-bottom: 1px solid rgba(236, 235, 242, 0.07);
}

.ship-dots { display: flex; gap: 6px; flex: none; }
.ship-dots i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: rgba(236, 235, 242, 0.16);
}

.ship-url {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  flex: 1;
  min-width: 0;
  max-width: 17rem;
  margin: 0 auto;
  padding: 0.18rem var(--space-sm);
  border-radius: var(--radius-pill);
  background: rgba(5, 6, 10, 0.65);
  color: var(--ink-soft);
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
}

/* Static on purpose. A pulsing "live" dot is the oldest loop on the web. */
.ship-live {
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--state-success);
  box-shadow: 0 0 6px rgba(111, 207, 151, 0.55);
}

.ship-screen {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 10;
  container-type: size;
  border-radius: 0 0 14px 14px;
  background: #0a0c16;
  --edge-thickness: 1px;
  --edge-rest: 0.35;
}

/* Above the scrolling image, which is its own layer once it moves. */
.ship-screen.edge-lit::before { z-index: 1; }

/* Glass. One static specular from the same key light, top left, so the
   screen reads as a pane in the room rather than a picture pasted on it. */
.ship-screen::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(118deg,
    rgba(242, 246, 255, 0.07) 0%,
    rgba(242, 246, 255, 0.02) 28%,
    transparent 46%);
  pointer-events: none;
}

.ship-screen img {
  display: block;
  width: 100%;
  height: auto;
  transform: translateY(0);
  /* The way back is quick: it is a reset, not a second performance. */
  transition: transform 1.1s var(--ease-out);
}

.ship-stage:hover .ship-screen img,
.ship-stage.is-lit .ship-screen img {
  transform: translateY(calc(-100% + 100cqh));
  transition: transform var(--scroll-time) cubic-bezier(0.45, 0, 0.25, 1) 0.5s;
}

/* The phone trails the window by a beat, so the two read as one site seen
   twice rather than one animation duplicated. */
.ship-stage:hover .ship-phone .ship-screen img,
.ship-stage.is-lit .ship-phone .ship-screen img {
  transition-delay: 0.9s;
}

.ship-phone {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 2;
  width: 25%;
  min-width: 110px;
  aspect-ratio: 390 / 844;
  padding: 5px;
  border-radius: 26px;
  background: #05060a;
  box-shadow:
    inset 0 0 0 1px rgba(236, 235, 242, 0.10),
    var(--edge-lit),
    0 30px 50px -10px rgba(8, 5, 4, 0.72),
    var(--glow-cool);
  transform: translateY(24px) rotate(-2deg);
  opacity: 0;
  transition:
    transform 1.1s var(--ease-out) 0.35s,
    opacity 0.8s ease 0.35s;
}

.ship--flip .ship-phone {
  left: auto;
  right: 0;
  transform: translateY(24px) rotate(2deg);
}

.ship.visible .ship-phone { opacity: 1; transform: rotate(-2deg); }
.ship--flip.visible .ship-phone { transform: rotate(2deg); }

.ship-phone .ship-screen {
  height: 100%;
  aspect-ratio: auto;
  border-radius: 21px;
}

/* The camera island. */
.ship-phone::after {
  content: '';
  position: absolute;
  top: 11px;
  left: 50%;
  z-index: 3;
  width: 28%;
  height: 9px;
  border-radius: var(--radius-pill);
  background: #05060a;
  transform: translateX(-50%);
}

@media (max-width: 992px) {
  .ship,
  .ship--flip {
    grid-template-columns: 1fr;
    gap: var(--space-xl);
    padding: var(--space-lg) 0;
  }
  /* Stage first everywhere once stacked: the picture opens, the path explains. */
  .ship-stage { order: -1; }
  .ship-stage,
  .ship--flip .ship-stage { --turn: 0deg; }
  .shipped { margin-top: var(--space-3xl); }
}

@media (max-width: 600px) {
  .ship-stage { padding: 0 0 var(--space-xl) var(--space-lg); }
  .ship--flip .ship-stage { padding: 0 var(--space-lg) var(--space-xl) 0; }
  .ship-phone { width: 30%; min-width: 0; border-radius: 18px; padding: 4px; }
  .ship-phone .ship-screen { border-radius: 14px; }
  .ship-phone::after { top: 8px; height: 6px; }
  .ship-dots { display: none; }
  .shipped { margin-top: var(--space-2xl); }
}

@media (prefers-reduced-motion: reduce) {
  .ship-desk,
  .ship-stage:hover .ship-desk,
  .ship-stage.is-lit .ship-desk { transform: none; }
  .ship-stage:hover .ship-screen img,
  .ship-stage.is-lit .ship-screen img { transform: none; }
  .ship-phone,
  .ship--flip .ship-phone,
  .ship.visible .ship-phone,
  .ship--flip.visible .ship-phone { opacity: 1; transform: none; }
  .ship-step { opacity: 1; transform: none; }
  .ship-path::before { transform: none; }
}

/* ─────────────────────────────────────────────
   3D VISUALIZATION
   A second hero, full bleed. Four rows of real renders on a plane tilted
   back into the room, each row drifting the opposite way to the one
   above it, under a veil that leaves the copy side dark enough to read.
   The featured panel is the Holden loop, and it opens the full film.

     .viz-wall    clips the plane to the section
     .viz-plane   the tilt. One transform, never animated.
     .viz-track   two identical runs of tiles, so -50% is a seamless loop
     .viz-veil    the darkness the copy sits in, plus the fade into the
                  sections above and below

   Rows only run while the section is on screen (.is-on, from initViz).
────────────────────────────────────────────── */
.viz {
  position: relative;
  z-index: 2;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  align-items: center;
  min-height: clamp(680px, 100vh, 980px);
  padding: var(--space-4xl) 0;
  background: var(--void-black);
}

.viz-wall {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  perspective: 1400px;
}

.viz-plane {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 165%;
  display: grid;
  --tile-gap: 16px;
  gap: var(--tile-gap);
  transform:
    translate(-50%, -50%)
    rotateX(24deg)
    rotateZ(-9deg)
    scale(1.05);
  transform-style: preserve-3d;
}

.viz-row { overflow: visible; }

.viz-track {
  display: flex;
  gap: var(--tile-gap);
  width: max-content;
  animation: vizDrift var(--row-time, 50s) linear infinite;
  animation-play-state: paused;
}

.viz-row--rev .viz-track { animation-direction: reverse; }

.viz.is-on .viz-track { animation-play-state: running; }

/* One run plus one gap is exactly half the track plus half a gap. */
@keyframes vizDrift {
  to { transform: translateX(calc(-50% - var(--tile-gap) / 2)); }
}

.viz-tile {
  display: block;
  width: clamp(220px, 21vw, 340px);
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: 10px;
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 235, 0.10),
    0 14px 30px rgba(0, 0, 0, 0.55);
}

/* Readable copy on the left, the work showing through on the right, and
   no hard edge where the section meets the page above or below it. */
.viz-veil {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(180deg,
      var(--void-black) 0%,
      rgba(5, 6, 10, 0) 18%,
      rgba(5, 6, 10, 0) 82%,
      var(--void-black) 100%),
    linear-gradient(90deg,
      rgba(5, 6, 10, 0.94) 0%,
      rgba(5, 6, 10, 0.86) 38%,
      rgba(5, 6, 10, 0.55) 62%,
      rgba(5, 6, 10, 0.40) 100%);
}

.viz-inner {
  display: grid;
  grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
  gap: var(--space-3xl);
  align-items: center;
}

.viz-kicker {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  color: var(--ink-soft);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--track-caps-wide);
  text-transform: uppercase;
  margin-bottom: var(--space-md);
}

.viz-since {
  font-family: var(--font-display);
  font-weight: 400;
  color: var(--metal-bright);
  letter-spacing: var(--track-caps);
}

.viz-since::before {
  content: '';
  display: inline-block;
  width: 28px;
  height: 1px;
  margin-right: var(--space-sm);
  vertical-align: middle;
  background: var(--light-edge);
}

.viz-title {
  font-size: clamp(2.2rem, 3.6vw, 3.4rem);
  font-weight: 700;
  line-height: 1.08;
  color: var(--ink-bright);
  margin-bottom: var(--space-lg);
  text-wrap: balance;
  max-width: 18ch;
}

.viz-lede {
  color: var(--ink-soft);
  font-size: var(--text-lg);
  line-height: var(--leading-body);
  max-width: 50ch;
}

.viz-lede + .viz-lede {
  margin-top: var(--space-sm);
  color: var(--ink);
}

.viz-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin: var(--space-lg) 0;
}

.viz-tags li {
  padding: var(--space-3xs) var(--space-sm);
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.05);
  box-shadow: inset 0 0 0 1px rgba(236, 235, 242, 0.08);
  color: var(--ink);
  font-size: var(--text-xs);
  font-weight: 600;
  backdrop-filter: blur(8px);
}

.viz-studio {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--ink);
  font-size: var(--text-sm);
  font-weight: 700;
  text-decoration: none;
  transition: color 0.3s ease;
}

.viz-studio .icon { transition: transform 0.35s var(--ease-out); }
.viz-studio:hover,
.viz-studio:focus-visible { color: var(--metal-bright); }
.viz-studio:hover .icon,
.viz-studio:focus-visible .icon { transform: translateX(4px); }

/* The featured panel. The button is the rim host and never clips; the
   frame inside it is the only thing that does. */
.viz-feature {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.viz-reel {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  border-radius: 18px;
  background: #0a0c16;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  box-shadow:
    var(--edge-lit),
    var(--edge-fall),
    var(--elev-3),
    var(--glow-cool);
  transition:
    transform 0.6s var(--ease-out),
    box-shadow 0.6s ease;
}

.viz-reel:hover,
.viz-reel:focus-visible {
  transform: translateY(-4px);
  box-shadow:
    var(--edge-lit),
    var(--edge-fall),
    var(--elev-3),
    var(--glow-cool-lift);
}

.viz-reel-frame {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
  border-radius: inherit;
  --edge-thickness: 1px;
  --edge-rest: 0.4;
}

.viz-reel-frame.edge-lit::before { z-index: 1; }

.viz-loop {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The film's own call to action, sitting on the footage like a lower third. */
.viz-reel-cta {
  position: absolute;
  left: var(--space-md);
  bottom: var(--space-md);
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  padding: 0.4rem var(--space-sm) 0.4rem 0.4rem;
  border-radius: var(--radius-pill);
  background: rgba(5, 6, 10, 0.72);
  backdrop-filter: blur(12px);
  box-shadow: inset 0 0 0 1px rgba(236, 235, 242, 0.10);
  color: var(--ink-bright);
  font-size: var(--text-sm);
  font-weight: 700;
}

.viz-reel-icon {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: 50%;
  background: linear-gradient(174deg, var(--metal-ramp-legible, var(--metal-bright), #c9a961));
  color: #1a1206;
  transition: transform 0.4s var(--ease-out);
}

.viz-reel-icon .icon { width: 0.95rem; height: 0.95rem; fill: currentColor; stroke: none; margin-left: 2px; }

.viz-reel:hover .viz-reel-icon,
.viz-reel:focus-visible .viz-reel-icon { transform: scale(1.08); }

.viz-reel-time {
  color: var(--ink-soft);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Sits over the moving wall, so it carries its own shadow to stay legible. */
.viz-feature figcaption {
  color: var(--ink-soft);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9), 0 0 18px rgba(5, 6, 10, 0.95);
  font-size: var(--text-sm);
  line-height: var(--leading-dense);
  max-width: 56ch;
}

/* The full film. */
.viz-film {
  width: min(1200px, 92vw);
  max-width: none;
  margin: auto;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: visible;
}

.viz-film::backdrop {
  background: rgba(5, 6, 10, 0.88);
  backdrop-filter: blur(8px);
}

.viz-film video {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  background: #000;
  box-shadow: var(--elev-3), var(--glow-cool);
}

.viz-film-close {
  position: absolute;
  top: -3rem;
  right: 0;
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink-bright);
  cursor: pointer;
  transition: background 0.3s ease;
}

.viz-film-close:hover,
.viz-film-close:focus-visible { background: rgba(255, 255, 255, 0.16); }

@media (max-width: 992px) {
  .viz {
    min-height: 0;
    padding: var(--space-3xl) 0;
  }
  .viz-inner {
    grid-template-columns: 1fr;
    gap: var(--space-xl);
  }
  /* Stacked, the copy no longer has a side to itself, so the veil darkens
     evenly instead of left to right. */
  .viz-veil {
    background:
      linear-gradient(180deg,
        var(--void-black) 0%,
        rgba(5, 6, 10, 0.80) 12%,
        rgba(5, 6, 10, 0.62) 60%,
        rgba(5, 6, 10, 0.70) 85%,
        var(--void-black) 100%);
  }
}

@media (max-width: 600px) {
  .viz { padding: var(--space-2xl) 0; }
  .viz-tile { width: 180px; border-radius: 8px; }
  .viz-plane { width: 260%; --tile-gap: 10px; }
  .viz-reel-cta { left: var(--space-xs); bottom: var(--space-xs); font-size: var(--text-xs); }
  .viz-reel-icon { width: 1.7rem; height: 1.7rem; }
  .viz-film-close { top: -2.8rem; }
}

@media (prefers-reduced-motion: reduce) {
  .viz-track { animation: none; }
  .viz-reel:hover,
  .viz-reel:focus-visible { transform: none; }
}

#process {
  padding: var(--space-4xl) 0;
  position: relative;
  z-index: 2;
}

.process-track {
  list-style: none;
  padding: 0;
  margin: 0;
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2xl);
}

/* The vertical career rail, rotated. It fades out past the last node
   instead of running to the edge, for the same reason the vertical one
   fades downward: a rail that overshoots its final station stops reading
   as a sequence and starts reading as a border. */
.process-track::before {
  content: '';
  position: absolute;
  top: 8px;
  left: 0;
  right: 0;
  height: 1px;
  background: linear-gradient(90deg,
    var(--metal-mid) 0%,
    rgba(224, 174, 60, 0.18) 62%,
    transparent 100%);
}

.process-step {
  position: relative;
  padding-top: calc(17px + var(--space-lg));
}

.process-node {
  position: absolute;
  top: 0;
  left: 0;
  width: 17px;
  height: 17px;
  border-radius: 50%;
  border: 1px solid var(--metal-bright);
  background: radial-gradient(circle, var(--metal-bright) 0 26%, var(--void-black) 32%);
  box-shadow: var(--glow-gold);
  z-index: 2;
}

.process-name {
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--ink-bright);
  margin-bottom: var(--space-xs);
  text-wrap: balance;
}

.process-desc {
  font-size: var(--text-md);
  color: var(--ink-soft);
  line-height: var(--leading-body);
  text-wrap: pretty;
  max-width: 34ch;
}

/* Below the two-column breakpoint the rail stands back up, which is the
   same shape the real timeline takes on a phone. */
@media (max-width: 768px) {
  .process-track {
    grid-template-columns: 1fr;
    gap: var(--space-2xl);
  }

  .process-track::before {
    top: 4px;
    bottom: 0;
    left: 8px;
    right: auto;
    width: 1px;
    height: auto;
    background: linear-gradient(180deg,
      var(--metal-mid) 0%,
      rgba(224, 174, 60, 0.18) 62%,
      transparent 100%);
  }

  .process-step {
    padding-top: 0;
    padding-left: calc(17px + var(--space-lg));
    min-height: 17px;
  }

  /* On the stacked rail the ring sits on the name's first line. */
  .process-node { top: 0.55em; }

  .process-desc { max-width: none; }
}


#contact {
  padding: var(--space-4xl) 0;
  position: relative;
  z-index: 2;
  /* The gold wash that used to rise off the footer now marks this as the
     page's arrival point instead -- the footer below is just legal text. */
  background: radial-gradient(ellipse at 50% 100%, rgba(224, 174, 60, 0.1) 0%, transparent 70%);
}

.contact-grid {
  display: grid;
  /* minmax(0, …) on both tracks: the form panel now has real padding, and
     a plain fr can't shrink under its content's min-content width. */
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: var(--space-2xl);
  align-items: start;
}

/* The side column is 403px of content against an 872px form, so at rest it
   left 469px of dead column: over half the track empty, and the emptiness
   sat at the bottom where it read as something failing to load rather than
   as air. Sticking it to the top means the column travels with the form
   instead of ending under it, and the availability line (the single most
   useful sentence in the section) stays on screen for the whole scroll of
   the field set it belongs to.

   top clears the fixed nav. The max-height/overflow pair is the standard
   guard so a sticky column can never become taller than the viewport and
   trap its own overflow. */
.contact-side {
  position: sticky;
  top: calc(var(--nav-height) + var(--space-lg));
  max-height: calc(100vh - var(--nav-height) - var(--space-2xl));
  overflow-y: auto;
  scrollbar-width: none;
}

.contact-side::-webkit-scrollbar { display: none; }

/* Sticky only pays for itself when there is real scroll travel to track.
   Below the point where the grid collapses to one column it is actively
   wrong, so it is switched off there rather than left to fight the
   single-column stack. 992px, matching .contact-grid's own collapse
   further down the sheet: any other number leaves a band of widths where
   the column is stacked but still trying to stick. */
@media (max-width: 992px) {
  .contact-side {
    position: static;
    max-height: none;
    overflow-y: visible;
  }
}

/* Was a bare line of text with a glowing dot in front of it, sitting above
   an unrelated list. It is the single most valuable sentence in the
   section (three kinds of availability, stated plainly) and it was the
   least designed thing on the page. Now a panel, so it reads as a
   standing status rather than as a stray caption. */
.availability {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: var(--space-md);
  background: var(--surface-1);
  /* border dropped: .rim paints the edge now, and a 1px border under a
     1px rim gives the object two outlines a pixel apart. */
  border: 0;
  border-radius: var(--radius-md);
  box-shadow: var(--edge-lit);
  color: var(--ink);
  font-size: var(--text-md);
  line-height: var(--leading-dense);
  margin-bottom: var(--space-lg);
}

/* A status LED, which is exactly the problem: at full opacity behind a
   saturated green it was the brightest object in the section and read as
   a chat presence indicator. A containment ring holds the same meaning
   without emitting. */
.availability-dot {
  flex: none;
  width: 8px;
  height: 8px;
  /* Optically centred on the first line of the sentence beside it rather
     than on the block as a whole, which is where align-items: flex-start
     would otherwise leave it. */
  margin-top: 0.5em;
  border-radius: 50%;
  background: var(--state-success);
  box-shadow:
    0 0 0 3px rgba(111, 207, 151, 0.14),
    0 0 8px rgba(111, 207, 151, 0.22);
}

.contact-info-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.contact-info-item {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  color: var(--ink-soft);
  font-size: var(--text-md);
  line-height: var(--leading-dense);
}

/* Was --metal-bright at 20px: three small saturated gold marks stacked in
   a column, competing with the struck numerals elsewhere for the eye. The
   icons are wayfinding, not objects, so they step down to the mid step and
   let the text they label lead. */
.contact-icon {
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 0.1em;
  color: var(--metal-deep);
}

.contact-card {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border-subtle);
}

.contact-card h4 {
  margin-bottom: var(--space-2xs);
  color: var(--ink-bright);
  font-size: var(--text-lg);
  font-weight: 700;
}

.contact-card p {
  color: var(--ink-soft);
  font-size: var(--text-md);
  margin-bottom: var(--space-md);
  line-height: var(--leading-body);
}

/* Footer: links, socials and legal, nothing else */
#footer {
  padding: var(--space-xl) 0;
  position: relative;
  z-index: 2;
}

.social-links {
  display: flex;
  gap: var(--space-sm);
}

.social-btn {
  width: 48px;
  height: 48px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border-subtle);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  text-decoration: none;
  font-weight: 700;
  font-size: var(--text-sm);
  transition:
    transform 0.3s var(--ease-out),
    background 0.3s ease,
    border-color 0.3s ease,
    color 0.3s ease,
    box-shadow 0.3s ease;
}

.social-btn:hover {
  background: var(--metal-bright);
  border-color: var(--metal-mid);
  color: var(--on-metal);
  transform: translateY(-4px);
  box-shadow: var(--glow-gold-lift);
}

.social-btn .icon { width: 1.35em; height: 1.35em; }

/* Filled brand marks. .icon is a stroke system, so a glyph opts out of
   the stroke and takes the colour as fill instead. */
.icon-glyph { fill: currentColor; stroke: none; }
.social-btn .icon-glyph { width: 18px; height: 18px; }
.btn .icon-glyph { width: 1em; height: 1em; }

/* Nav profile pair. The same recessed pill as the language switcher beside
   it, so the right end of the nav reads as two sibling controls instead of
   two loose roundels next to a pill. Hover raises the segment the way the
   page's other selectors do, neutral rather than gold: gold is reserved
   for the thing to act on, and leaving for a profile is not that. */
.nav-profiles {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  background: rgba(3, 4, 10, 0.5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.45);
}

.nav-profile {
  display: grid;
  place-items: center;
  width: 34px;
  height: 28px;
  border-radius: var(--radius-pill);
  color: var(--ink-soft);
  transition: color 0.25s ease, background 0.25s ease, box-shadow 0.25s ease;
}

.nav-profile .icon { width: 15px; height: 15px; }

.nav-profile:hover,
.nav-profile:focus-visible {
  color: var(--ink-bright);
  background: var(--surface-3);
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 235, 0.14),
    inset 0 -1px 0 rgba(0, 0, 0, 0.45),
    0 2px 6px rgba(0, 0, 0, 0.45);
}

.nav-profile:focus-visible {
  outline: 2px solid var(--light-core);
  outline-offset: 2px;
}

.contact-card .btn + .btn { margin-top: var(--space-sm); }

.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-md);
}

.footer-legal {
  color: var(--ink-faint);
  font-size: var(--text-sm);
  text-align: right;
}

/* ─────────────────────────────────────────────
   CONTACT FORM
   No boxed chrome on the fields -- a bottom-weighted border and a violet
   focus state, the same "interactive" colour the nav underline already
   uses. The two-path toggle borrows its pill shape from .lang-switcher:
   it's the same interaction (pick one of two), so it should look like it.
────────────────────────────────────────────── */
/* The form is an object in the void like everything else here, not a set
   of fields floating on the page background. Giving it one surface, one
   edge and one shadow is most of what made it read as unfinished: the
   fields were the only part with any definition, so they looked like they
   had been dropped onto the section rather than built into it. */
.contact-form-wrap {
  padding: clamp(var(--space-lg), 3vw, var(--space-xl));
  background: var(--surface-1);
  /* See .availability: the rim is the edge, so the border would be a
     second one directly under it. */
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--edge-lit), var(--elev-2);
}

/* A real segmented control: one recessed trough, two segments, the active
   one raised out of it. The trough is darker than the panel it sits on so
   the raised segment has something to be raised out of. */
.path-toggle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3xs);
  background: rgba(3, 4, 10, 0.5);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: 4px;
  margin-bottom: var(--space-xl);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.45);
}

.path-tab {
  background: none;
  border: none;
  color: var(--ink-soft);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--leading-dense);
  padding: var(--space-sm) var(--space-xs);
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: center;
  transition: color 0.25s ease, background 0.25s ease;
}

.path-tab:hover { color: var(--ink); }

/* Was the full metal ramp, which put two gold objects in one panel: the
   segment and the Send button below it. Gold is the page's one substance
   and it means "this is the thing to act on", so spending it on a
   selector left the actual action competing with a control that merely
   says which set of fields is showing. The reader's eye went to the
   wrong one.

   The segment keeps the physics that made it read as raised (a lit top
   edge, a dark bottom edge, a shadow it casts into the trough) and gives
   up the substance. Raised and neutral is enough to say "selected"; it
   does not also need to be the most valuable material on the page. */
.path-tab.active {
  background: var(--surface-3);
  color: var(--ink-bright);
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 235, 0.14),
    inset 0 -1px 0 rgba(0, 0, 0, 0.45),
    0 2px 6px rgba(0, 0, 0, 0.45);
}

.path-tab.active:hover { color: var(--ink-bright); }

.contact-form { display: flex; flex-direction: column; gap: var(--space-lg); }

.form-row { display: flex; flex-direction: column; gap: var(--space-2xs); }

.form-row-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-lg);
}

/* Was --ink-soft. A 14px label is already the smallest text in the form;
   running it at the muted step as well left the two weakest signals
   stacked on the one element the reader needs first. */
.form-row label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
}

/* Marked, not guessed. The label carries data-i18n and setLanguage writes
   textContent, which would wipe any real element added inside it, so the
   marker has to be a pseudo to survive a language switch. */
.form-row:has(input:required, textarea:required) label::after {
  content: '*';
  margin-left: 0.25em;
  color: var(--metal-mid);
  font-weight: 700;
}

/* A recessed well rather than a 3% white tint. At rgba(255,255,255,0.03)
   over this ground the field and the panel were within a couple of values
   of each other, so the only thing saying "you can type here" was a 12%
   hairline. Darker than the surface it sits in, plus an inset shadow, and
   the field reads as cut into the panel. */
.form-row input,
.form-row select,
.form-row textarea {
  width: 100%;
  background-color: rgba(3, 4, 10, 0.55);
  /* --border-subtle (0.12 alpha) reads under 3:1 against this well on a
     screen with no hover state to escalate it, which is most phones.
     A dedicated resting value here, not a change to the shared token. */
  border: 1px solid rgba(255, 250, 235, 0.22);
  border-radius: var(--radius-sm);
  padding: var(--space-sm) var(--space-md);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: var(--leading-dense);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35);
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

/* Firefox dims placeholders by default, which on this ground would put
   them under the contrast floor. Pinned to a token instead. */
.form-row input::placeholder,
.form-row textarea::placeholder {
  color: var(--ink-faint);
  opacity: 1;
}

.form-row input:hover,
.form-row select:hover,
.form-row textarea:hover {
  border-color: rgba(255, 250, 235, 0.32);
}

.form-row textarea {
  resize: vertical;
  min-height: 8rem;
  line-height: var(--leading-body);
}

/* `outline: none` used to live here with nothing put back. The global
   :focus-visible rule now covers inputs, so the ring is handled there and
   this only has to say which field is live. */
.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {
  border-color: var(--light-core);
  background-color: rgba(8, 11, 24, 0.72);
}

/* :user-invalid, not :invalid. :invalid matches an empty required field
   from first paint, so the form would greet every visitor already marked
   wrong; :user-invalid waits until they have actually left the field. */
.form-row input:user-invalid,
.form-row textarea:user-invalid {
  border-color: var(--state-error);
}

/* The native control paints its own arrow in the UA's colour and its menu
   in the UA's palette, neither of which know about this page. */
.form-row select {
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23a9a8bb' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-md) center;
  padding-right: calc(var(--space-md) * 2 + 12px);
}

.form-row select option {
  background: var(--void-deep);
  color: var(--ink);
}

/* The hidden attribute is the source of truth for both halves of the path
   toggle, but any author display rule outranks the UA sheet's [hidden], so
   both carriers have to opt back in. .contact-info-item is display: flex,
   which is exactly the case that would otherwise leak. */
.path-field[hidden],
[data-path-show][hidden] { display: none; }

.btn[disabled] { opacity: 0.6; cursor: not-allowed; pointer-events: none; }

/* Reserves its line whether or not it is holding a message, so a success
   or error never pushes the submit button out from under the cursor. */
.form-status {
  min-height: 1.4em;
  font-size: var(--text-sm);
  line-height: var(--leading-dense);
  color: var(--ink-soft);
  margin: 0;
}

.form-status.is-success { color: var(--state-success); }
.form-status.is-error { color: var(--state-error); }
.form-status.is-error a { color: inherit; text-decoration: underline; }

/* A link that never wants to be mistaken for a CTA: the résumé under the
   Career head, and the references link beside the Testimonials title.
   Text, not a button, on purpose. */
.link-quiet {
  display: inline-flex;
  align-items: center;
  align-self: center;
  gap: var(--space-3xs);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-faint);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
  transition: color 0.2s ease, border-color 0.2s ease;
}

.link-quiet:hover,
.link-quiet:focus-visible {
  color: var(--ink-soft);
  border-color: var(--border-subtle);
}

.link-quiet .icon { width: 14px; height: 14px; }

/* ─────────────────────────────────────────────
   TESTIMONIALS / REFERENCES
   Was a 42s marquee of 320px cards. Three faults, all of them structural:

     1. It asked the reader to read moving text. The .band strip further
        down works because a logo is glanceable, a 50-word paragraph is
        not. You either chase it or you wait for it.
     2. align-items: stretch against quotes of 16, 45 and 50 words left
        the Ignite card 72% empty: 67px of text in a 328px box.
     3. The edge mask always clipped a half-card on both sides, so the
        component's resting state was a broken one.

   It is now a static three-up. These are signed letters from two
   Directors and a CEO, which is the strongest proof on the page, and
   proof that cannot be read is decoration.

   They are also the only objects here that had no light on them at all.
   They now take the same key light as everything else, at the .rim-soft
   exposure, so they join the room instead of sitting on top of it.
────────────────────────────────────────────── */
.testimonials-block {
  margin-top: var(--space-3xl);
  padding-top: var(--space-xl);
  border-top: 1px solid var(--border-subtle);
}

.testimonials-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-lg);
  flex-wrap: wrap;
}

.testimonials-title {
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--ink-bright);
}

/* auto-fit rather than a fixed 3, so the set reflows on its own at every
   width instead of holding three columns until a breakpoint rescues it.
   260px is the floor at which a quote still wraps to sensible line
   lengths rather than to three words a line. */
.testimonial-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-lg);
  align-items: stretch;
}

/* A letter, not a speech bubble. The quote sits at the top, the
   attribution is pinned to the bottom by margin-top: auto, so three
   quotes of very different lengths still line their signatures up on one
   baseline across the row. That single shared line is what makes an
   uneven set read as composed rather than as ragged. */
.testimonial-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-lg);
  background: var(--surface-1);
  border-radius: var(--radius-md);
  /* Cool ladder, not gold: the rim on these is the platinum strip, and a
     cool edge throwing a warm scatter would read as two light sources. */
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-1), var(--glow-cool);
  margin: 0;
  /* .rim paints its ring into a ::before at inset -1px, so the card must
     not also carry a 1px border, or the object gets two edges. */
  border: 0;
  transition: box-shadow 0.45s var(--ease-out), transform 0.45s var(--ease-out);
}

/* The rim already comes up to full on hover (.rim's own rule) but the
   glare did not follow it, so pointing at a card brightened its edge
   while the room around the card stayed exactly as dark. An edge that
   gets brighter without its scatter getting brighter is the sticker
   problem again, just on hover instead of at rest. */
.testimonial-card:hover {
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-2), var(--glow-cool-lift);
  transform: translateY(-3px);
}

/* The mark is an icon, not a glyph in the prose: it takes a flat MATERIAL
   step the way borders and marks are allowed to, and it is aria-hidden so
   a screen reader is not read a punctuation mark. */
.testimonial-quote-mark {
  display: block;
  font-family: var(--font-display);
  font-size: 2.25rem;
  line-height: 0.6;
  color: var(--metal-deep);
  opacity: 0.5;
  margin-bottom: var(--space-sm);
  user-select: none;
}

/* Set as a pull quote, not as body copy. All three references are now
   trimmed to a single line of praise each (9-14 words), and a short quote
   at body size in a card this wide reads as a caption that lost its
   picture. At this size each one fills its measure and carries its own
   weight, which is what a quote pulled out of a letter is for. */
.testimonial-card p {
  font-size: clamp(var(--text-md), 1.6vw, 1.35rem);
  line-height: var(--leading-dense);
  color: var(--ink-bright);
  margin: 0 0 var(--space-lg);
}

/* Attribution, with the hierarchy the run-on line did not have. It used
   to be "KOSALA ARAVINDA · DIRECTOR, / BLOCKSTARS TECHNOLOGY": one size,
   one weight, one colour, wrapping mid-title, with the company (which is
   the entire credential) as the least visible word in it. Three steps
   now, on three lines, strongest last. */
.testimonial-card cite {
  display: block;
  font-style: normal;
  margin-top: auto;
  padding-top: var(--space-md);
  border-top: 1px solid var(--border-subtle);
}

.testimonial-name {
  display: block;
  font-size: var(--text-md);
  font-weight: 700;
  color: var(--ink-bright);
  line-height: var(--leading-dense);
}

.testimonial-role {
  display: block;
  font-size: var(--text-sm);
  color: var(--ink-soft);
  line-height: var(--leading-dense);
}

/* The credential. Small caps and tracked, so it reads as a stamp on a
   letter rather than as more prose. */
.testimonial-org {
  display: block;
  margin-top: var(--space-3xs);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--track-caps-wide);
  text-transform: uppercase;
  color: var(--metal-deep);
}

@media (max-width: 620px) {
  .testimonial-card { padding: var(--space-md); }
}

/* ─────────────────────────────────────────────
   PLATE
   A screenshot is a rectangle. A plate is an object: an edge that catches
   light, a wash it throws on the room, and enough void around it to fall
   through. The diagram above it still carries the argument. The plate is
   only the receipt.

   Three nested elements, because the pseudo-element budget is exactly
   three and each level owns one:

     .plate        the .reveal. Its transform belongs to the reveal
                   transition and nothing else may touch it.
     .plate-float  .rim (::before) + .bloom (::after). Carries the scroll
                   transform. No overflow, or the bloom's spill is clipped
                   off, and no background, so the spill is all there is.
     .plate-crop   .edge-lit (::before) + the duotone tint (::after).
                   The only element that clips the raster.
────────────────────────────────────────────── */
.plate {
  /* Deliberately narrower than its column. Evidence for the figure above
     it, not a rival to it. */
  width: 100%;
  max-width: 340px;
  margin-top: var(--space-lg);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Was --space-2xs (8px), and .plate-float's scroll parallax translates
     it up to 26px DOWN at the start of its view range. A transform does
     not reflow, so the caption stays put while the plate slides over it:
     measured 3px of overlap at 390px. --space-sm leaves the caption clear
     across the whole parallax throw actually reached. */
  gap: var(--space-sm);
}

.plate-float {
  position: relative;
  width: 100%;
  border-radius: var(--radius-lg);
  box-shadow:
    var(--edge-lit),
    var(--edge-fall),
    /* The object's own metal edge, struck rather than emitted. */
    inset 0 0 0 1px rgba(247, 212, 114, 0.13),
    var(--elev-3),
    /* Gold ladder to match --light-strip-gold on the rim. The plate is the
       one object carrying both a warm rim and a warm wash, so its glare
       has to be warm too or the three disagree. */
    var(--glow-gold);
  --rim-stops: var(--light-strip-gold);
  /* An object in the void needs an edge at rest. A rim at 0 would make the
     plate a hole cut in the page rather than a thing sitting in it. */
  --rim-rest: 0.22;
  --bloom-spread: 26px;
  --bloom-blur: 30px;
  --bloom-tint: var(--light-wash-gold);
  --bloom-hover: 0.22;
}

.plate-crop {
  position: relative;
  overflow: hidden;
  border-radius: inherit;
  /* Pins the tint's blend backdrop to the image, so it can never reach
     through to #proof's own radial gradient behind it. */
  isolation: isolate;
  contain: paint;
  --edge-thickness: 1px;
  --edge-rest: 0.30;
}

/* ── DUOTONE ──────────────────────────────────
   A screenshot arrives in somebody else's palette. At rest it is remapped
   into this room's: metal where the light lands, void where it does not.
   In focus it returns to what it actually is, which is the only honest
   resting state for evidence.

   Every function here is a colour matrix or a component transfer, so Skia
   folds the chain into one matrix and keeps it on the compositor. The
   order and the count match the lit state exactly. That is the only reason
   filter can interpolate at all: change either and it snaps.
────────────────────────────────────────────── */
.plate-img,
.band-img,
.gallery-img {
  display: block;
  filter:
    grayscale(1)
    sepia(1)
    saturate(2.4)
    hue-rotate(-12deg)
    brightness(0.88)
    contrast(1.15);
  transition: filter 0.75s var(--ease-out);
}

.plate-img { width: 100%; height: auto; }

/* ── THE LLOYDS PLATE ─────────────────────────
   His whole "Tiles And Rotators" image (assets/Lloyds/Lloyds.jpg), uncropped
   at his request: the wall, the logo and the title together. 16:9 source in
   a 16:9 box, so nothing is cut, and it matches the Game Day Squad plate.
   Exported at 960px, about 2.8x the 340px plate, so the title stays crisp.

   The tickets in the wall still carry the old tracker header. At this size
   its text is about 4px of grey and cannot be read. He chose this image
   knowing that. */
.plate-crop--lloyds { aspect-ratio: 16 / 9; }

/* The band runs to a common height and lets width follow the artwork, the
   way a contact strip does. Forcing a common box instead would crop the
   posters to a sliver and the landscapes to a square. */
.band-img { height: 100%; width: auto; }

/* The plate answers scroll as well as hover, because it is evidence and has
   to work on a touch screen. The band answers hover only: it is already
   moving, and a whole row lighting up at once would make the quiet section
   the loudest thing on the page. */
.plate:hover .plate-img,
.plate.is-lit .plate-img,
.band-item:hover .band-img,
.gallery-item:hover .gallery-img,
.gallery-item.is-focus .gallery-img {
  filter:
    grayscale(0)
    sepia(0)
    saturate(1)
    hue-rotate(0deg)
    brightness(1)
    contrast(1);
}

/* Sepia's shadows are neutral near-black, not indigo, so the void tier
   arrives as its own layer. screen is the right blend precisely because it
   cannot darken: it lifts the black floor toward indigo and leaves the
   metal highlights alone. That lands the floor a hair above --void-black,
   which is the difference between an object in the room and a hole in it. */
.plate-crop::after,
.band-crop::after,
.gallery-crop::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, #12163a 0%, #0b0e1f 55%, #05060a 100%);
  mix-blend-mode: screen;
  opacity: 0.55;
  transition: opacity 0.75s var(--ease-out);
  pointer-events: none;
}

.plate:hover .plate-crop::after,
.plate.is-lit .plate-crop::after,
.band-item:hover .band-crop::after,
.gallery-item:hover .gallery-crop::after,
.gallery-item.is-focus .gallery-crop::after { opacity: 0; }

/* The rim and the wash answer the same signal the colour does, so a plate
   in focus is lit on every channel at once rather than in pieces. */
.plate.is-lit .plate-float::before {
  opacity: 1;
  animation: rimSpin var(--rim-speed) linear infinite;
}

.plate.is-lit .plate-float::after { opacity: var(--bloom-hover); }

/* ── THE GAME DAY SQUAD PAIR ──────────────────
   This plate arrived with its own two states, GDS Off and GDS Hover, and
   the off one is already art-directed into a dark room. Running the
   duotone over it would be remapping a remap, so the filter and the tint
   stand down and the two images do the job instead: the lit one fades in
   over the off one on the same signal (hover, or .is-lit on touch). Both
   share one grid cell, so the crossfade never reflows.

   Source is 989x557 after trimming the export's baked drop shadow, about
   2.9x the 340px plate. Crisp on a 2x screen. */
.plate-crop--gameday {
  display: grid;
  aspect-ratio: 989 / 557;
}

.plate-crop--gameday .plate-img { grid-area: 1 / 1; }

.plate.plate--pair .plate-img { filter: none; }
.plate--pair .plate-crop::after { content: none; }

.plate-img--on {
  opacity: 0;
  transition: opacity 0.75s var(--ease-out);
}

.plate:hover .plate-img--on,
.plate.is-lit .plate-img--on { opacity: 1; }

.plate-caption {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--ink-faint);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-align: center;
}

/* Parallax rides the middle element. The outer .plate is holding the
   reveal transition, and a continuous transform there would park the plate
   at its scroll offset before it ever revealed. A continuous animation
   and a reveal transition never share an element.

   The @supports gate is load-bearing, not manners. Where animation-timeline
   is unknown the shorthand still applies and falls back to the DOCUMENT
   timeline, so the plate would play its whole parallax once on load and
   stop. Gated, those browsers get a plate that sits still, which is fine. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .plate-float {
      animation: plateFloat linear both;
      animation-timeline: view();
      animation-range: entry 15% exit 45%;
    }
  }
}

@keyframes plateFloat {
  from { transform: translate3d(0, 26px, 0) scale(0.985); }
  to   { transform: translate3d(0, -22px, 0) scale(1.006); }
}

/* ─────────────────────────────────────────────
   HERO SHOWCASE CARD
   A single contained floating card on the right side of the hero, cycling
   through REEL_ITEMS one image at a time. Fully inside the viewport now --
   no bleed -- with its own caption tag overlapping the top edge and a row
   of dots centred underneath.
────────────────────────────────────────────── */

/* The outer wrapper aligns itself in the grid. Dots sit here, below the
   image, in normal flow -- .hero-reel-frame (not this element) is the
   positioned ancestor for the image + caption, so the dots row never gets
   pulled into that stacking context. */
.hero-showcase {
  grid-area: reel;
  position: relative;
  /* Tops align with the lead's first line, under the headline. */
  align-self: start;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Positioned ancestor for the image and its caption card only. */
.hero-reel-frame {
  position: relative;
  width: 100%;
}

/* The visible window: clips the reel strip so only one is shown at a
   time. clip-path duplicates the overflow:hidden + border-radius clip
   rather than replacing it: rounding all 4 corners (previously left-only,
   back when the right edge bled off the viewport) put a rounded corner
   right where the track's translateX seam is, and a plain overflow clip on
   a composited (will-change: transform) child doesn't always mask that
   corner precisely -- a sliver of the next slide could show through at
   the rounded edge. clip-path clips post-composite and doesn't have that
   gap. */
.hero-reel-viewport {
  position: relative;
  width: 100%;
  /* Close to the source photos' natural ~1.8:1 landscape ratio, so
     object-fit: cover only needs a gentle crop instead of the extreme zoom
     a tall/narrow box forced. */
  aspect-ratio: 16 / 9.2;
  overflow: hidden;
  border-radius: var(--radius-md);
  clip-path: inset(0 round var(--radius-md));
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-3);
}

/* The strip: one item per REEL_ITEMS entry, side by side. JS drives
   translateX to slide between them. */
.hero-reel-track {
  display: flex;
  height: 100%;
  will-change: transform;
  transition: transform 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}

/* overflow: hidden here too, not just on the viewport: each item's
   .hero-reel-parallax layer is deliberately oversized (116%, see below) for
   the pointer/scroll drift, and this is its own clip so that oversizing
   never has a chance to bleed into a neighbouring item's space before the
   viewport-level clip would catch it. */
.hero-reel-item {
  position: relative;
  overflow: hidden;
  flex: 0 0 100%;
  height: 100%;
}

/* Oversized parallax layer: 8% larger than its item on every side. main.js
   measures that 8% in real pixels at runtime and clamps drift to it, since
   the fixed pointer/scroll drift budget (PARALLAX_IMG_PX) can otherwise
   exceed 8% of a short viewport and expose a transparent edge. */
.hero-reel-parallax {
  position: absolute;
  inset: -8%;
  width: 116%;
  height: 116%;
}

.hero-reel-crop {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.hero-reel-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* Very subtle Ken-Burns breathing while the image is active, added via JS
     by toggling .is-active on .hero-reel-item. */
  transition: transform 6s ease-in-out;
  transform: scale(1.04);
}

.hero-reel-item.is-active .hero-reel-img {
  transform: scale(1);
}

/* -- GLASS CAPTION CARD ------------------------
   A small liquid-glass card per slide: tag + title, on backdrop-filter
   frost. Replaces a baked-SVG "glass word" that could never actually
   refract anything -- a flat exported graphic can't sample the pixels
   behind it, which is what the effect it was imitating requires. This
   card gets real refraction in Phase 3, once a shader exists to do that;
   until then it's honest frosted glass rather than a fake. */
/* .hero-glass-words is a sibling of .hero-reel-viewport inside
   .hero-reel-frame, not a child of the viewport, so the viewport's
   overflow: hidden never clips a card that pokes up past the image's
   top edge -- that overlap is intentional. */
.hero-glass-words {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

.hero-glass-word {
  position: absolute;
  left: clamp(1rem, 3vw, 1.5rem);
  top: clamp(-1.25rem, -2.4vw, -0.75rem);
  max-width: min(72%, 300px);
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  background: linear-gradient(155deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.03));
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  border: 1px solid rgba(255, 255, 255, 0.22);
  box-shadow:
    0 16px 34px rgba(0, 0, 0, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.25);
  opacity: 0;
  transition: opacity 0.5s var(--ease-out);
  will-change: transform, opacity;
}

.hero-glass-word.is-active { opacity: 1; }
.hero-glass-word.is-fading { opacity: 0; }

/* Legibility scrim, between the canvas (z-index: 0) and the text
   (z-index: 2). The Phase 1 gradient background gave the tag/title
   enough contrast on its own; the WebGL crop is busier and opaque, so
   the text needs its own contrast layer now rather than relying on
   whatever happens to be in frame behind it. */
.hero-glass-word::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(5, 6, 10, 0.4);
  border-radius: inherit;
  pointer-events: none;
}

.hero-glass-tag {
  position: relative;
  z-index: 2;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  /* The LIGHT tier is reserved for strokes, rims and bloom, never a flat
     glyph colour (see the LIGHT tokens' own comment in :root) -- ink-soft
     is the same role .project-tag's chip text already plays. */
  color: var(--ink-soft);
}

.hero-glass-title {
  position: relative;
  z-index: 2;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 400;
  color: var(--ink-bright);
  line-height: 1.2;
  text-wrap: balance;
}

/* WebGL upgrade for capable/mid-tier hardware: a small canvas rendering a
   real, locally-warped crop of the active hero photo (buildHeroCardRefraction
   in main.js). Sits behind the tag/title (z-index above), in front of the
   card's own background/backdrop-filter -- which stays as the complete
   fallback appearance when this canvas never gets created (low tier, no
   THREE, reduced motion). border-radius: inherit so it respects the card's
   own rounding without needing overflow: hidden on the card itself. */
.hero-glass-refract-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  z-index: 0;
}

/* Firefox and anything without backdrop-filter gets a solid frosted
   plate instead of a fully transparent one */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .hero-glass-word { background: rgba(12, 15, 28, 0.86); }
}

/* ── NAVIGATION DOTS ─────────────────────────
   Indicator row in normal flow below the card, centred under it -- not an
   overlay on the photo. */
.hero-dots {
  position: relative;
  margin-top: var(--space-md);
  z-index: 5;
  display: flex;
  /* 20px, not 8px. The dots are 8px wide and each one now claims 10px of
     dead space either side as a touch target (see .hero-dot::before), so
     the pitch has to be 28px for those targets to tile edge to edge
     instead of overlapping. Overlapping hit areas are worse than small
     ones: the tap lands on whichever dot happens to be painted last
     rather than the one under the finger. */
  gap: 1.25rem;
  align-items: center;
  justify-content: center;
}

.hero-dot {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.32);
  border: none;
  cursor: pointer;
  padding: 0;
  transition: background 0.35s ease, transform 0.35s ease;
}

/* The dot is 8x8, which is a fine size for a dot and an unusable size for
   a target: it failed WCAG 2.5.8 (24px) as well as 2.5.5 (44px), and the
   reel is one of the few things on the page a phone visitor is meant to
   drive by hand.

   An invisible ::before carries the target instead of padding, because
   padding would grow the dot's own box and push the row apart. This keeps
   the dot 8px, keeps the layout, and hands the finger 28x44. */
.hero-dot::before {
  content: '';
  position: absolute;
  inset: -18px -10px;
  border-radius: var(--radius-sm);
}

.hero-dot.is-active {
  background: #fff;
  transform: scale(1.35);
}

/* Old gallery styles, kept as dead rules so any external reference doesn't
   break the page. The elements no longer exist in the DOM. */
.hero-gallery { display: none; }
.gallery-track {}
.gallery-item  {}
.gallery-crop  {}
.gallery-img   {}
.gallery-glass { display: none; }


/* ─────────────────────────────────────────────
   BAND
   Work that does not carry the argument, present as evidence that it
   exists. No heading and no copy: if it needed announcing it would belong
   in Projects. It drifts because a still row of pictures reads as a
   gallery, and a gallery would be making a claim this section is not.
────────────────────────────────────────────── */
.band {
  position: relative;
  /* No heading of its own, so it does not need a section's full lead-in:
     Career's bottom padding plus another 144px left about 290px of dead
     room above the run, which read as the page ending early. The bottom
     keeps the full step into the footer. */
  padding: var(--space-xl) 0 var(--space-4xl);
  /* The band runs edge to edge while everything else holds the measure,
     which is what stops it reading as a section. */
  overflow: hidden;
  /* Fades the run out at both ends so it reads as passing through rather
     than starting and stopping at the viewport edge. */
  -webkit-mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}

.band-track {
  display: flex;
  align-items: center;
  gap: var(--space-2xl);
  width: max-content;
  animation: bandDrift 88s linear infinite;
}

/* Pause where the eye has stopped, so anything the pointer reaches can
   actually be looked at. */
.band:hover .band-track { animation-play-state: paused; }

.band-item { flex: none; }

.band-crop {
  position: relative;
  height: clamp(150px, 17vw, 210px);
  overflow: hidden;
  isolation: isolate;
  contain: paint;
  border-radius: var(--radius-md);
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-2);
  /* Sits behind the argument, so it rests dimmer than a plate does. Opacity
     rather than another filter function: it composites on the GPU and does
     not have to match the remap chain term for term to interpolate. */
  opacity: 0.8;
  transition: opacity 0.6s var(--ease-out);
}

.band-item:hover .band-crop { opacity: 1; }

/* On a phone a 150px-tall run showed one whole card and two slivers at the
   edges. Shorter cards and a tighter gap keep two or three whole marks in
   view, so the work reads as work rather than as fragments. */
@media (max-width: 600px) {
  .band-crop { height: 112px; }
  .band-track { gap: var(--space-lg); }
}

/* Half the run is a duplicate of the first half, so the reset at -50%
   lands on an identical frame and the loop has no seam. */
@keyframes bandDrift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* Old .showcase section has been removed from the page.
   These rules are kept as stubs to prevent any stale references from
   causing layout issues, but they target nothing. */
.showcase         { display: none; }
.showcase-track   {}
.showcase-item    {}
.showcase-crop    {}
.showcase-img     {}
.showcase-glass   {}


/* ─────────────────────────────────────────────
   MARKS
   Two logos, set beside the entries they belong to rather than gathered
   into a strip. Two is not a strip, it is an orphan pair, and a row of two
   marks under a heading would be asking the page to admire them.

   Source paths are outline-converted silhouettes, so fill:none plus a
   stroke draws the boundary of each solid form: contour, not fill. The
   fill attributes are stripped at the symbol, not here, because a
   presentation attribute on the path beats an inherited value and CSS
   cannot select into a <use> shadow tree.
────────────────────────────────────────────── */
.mark {
  flex: none;
  fill: none;
  stroke: currentColor;
  /* The sprite's single stroke weight. At 1 the contour survives the
     downscale in theory and reads as a smudge in practice. */
  stroke-width: 1.5;
  /* Without this, the stroke is scaled by the ratio between the viewBox and
     the rendered size, which for a 296-unit box at 46px is 0.23 device
     pixels: technically present, visually absent. This pins it to 1.5. */
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  color: var(--ink-faint);
  opacity: 0.7;
  transition: color 0.5s ease, opacity 0.5s ease;
}

/* A glyph, so it takes MATERIAL or INK, never LIGHT. This was --light-cyan,
   which broke the tier rule stated at the top of this file. */
.timeline-item:hover .mark {
  color: var(--metal-bright);
  opacity: 1;
}

.timeline-company.has-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
}

/* Sized to the smallest box where a hollow contour still resolves into a
   flame rather than a smudge. Below about 44px it stops being a mark. */
.has-mark .mark { width: 46px; height: 33px; }

/* Animations */
.reveal {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.7s ease, transform 0.7s var(--ease-out);
}

.reveal.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Responsive */
@media (max-width: 1100px) {
  .nav-links { gap: var(--space-sm); }
}

/* The hero stacks earlier than everything else on the page. Below this
   the copy column beside a 360px reel gets too narrow for the lead to read
   as a statement, so the row stops being two columns while there is still
   room to stack gracefully. */
@media (max-width: 1080px) {
  .hero-frame {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "eyebrow" "title" "copy" "reel";
    min-height: auto;
  }

  /* Compact, not hidden: the single-column grid above already stacks
     .hero-showcase below .hero-copy in source order. Swipe (touch) and the
     dots both work unchanged -- initHeroShowcase() doesn't know or care
     about viewport width. align-self: start, not stretch, so the box sizes
     to the image instead of the row. */
  /* 2xl, not xl: the caption card rises about 20px above the image, so
     xl left it nearly touching the buttons. */
  .hero-showcase {
    margin-top: var(--space-2xl);
    align-self: start;
  }

  /* No aspect-ratio override here: the hero photos were exported baked to
     exactly 16/9.2. Changing the ratio makes object-fit: cover re-crop
     images that needed zero cropping. */
  .hero-glass-word {
    max-width: min(80%, 240px);
    left: 0.75rem;
  }
}

/* Phones: one word per line. Two lines would have to fit "More output."
   across ~340px, which caps the claim near 40px, smaller than it deserves.
   Sized to the widest single word instead, the phrases wrap on their own
   and the claim nearly doubles:
     EN  "Output."  4.75em -> 20.2cqi     ES  "Producción."  7.44em -> 12.9cqi
   Neither phrase can fit whole on a line at these sizes (the shortest is
   "Mismo equipo.", 107% of the frame), so the four-line shape holds at
   every phone width. */
@media (max-width: 560px) {
  .hero-title.hero-title-claim {
    font-size: min(16vw, 4.75rem);
    font-size: min(20.2cqi, 4.75rem);
  }

  :lang(es) .hero-title.hero-title-claim {
    font-size: min(10.5vw, 4.75rem);
    font-size: min(12.9cqi, 4.75rem);
  }
}

@media (max-width: 992px) {
  .about-grid { grid-template-columns: 1fr; gap: var(--space-xl); }
  .contact-grid { grid-template-columns: 1fr; gap: var(--space-xl); }
  .form-row-pair { grid-template-columns: 1fr; gap: var(--space-lg); }
  /* padding steps down in two stages (144 -> 96 -> 64) instead of dropping
     144px to 40px in one jump at this breakpoint. */
  .case { grid-template-columns: 1fr; gap: var(--space-xl); padding: var(--space-3xl) 0; }
  .case:first-of-type { padding-top: var(--space-md); }
  .case-flip .case-visual { order: 0; }
  .case-visual { min-height: 260px; }
  /* Stacked, the plate is no longer sharing a row with the prose, so the
     cap that kept it from rivalling the figure is no longer doing work. */
  .plate { max-width: 420px; }

  /* The hero's own stacking now lives in the 1080px query above, because
     the image column's 420px floor cannot fit beside the text column at
     these widths. Everything below is unrelated to the hero. */

  /* Nav collapses here so it never has to survive the 769-991px band
     where the label row no longer fits but the drawer wasn't offered yet. */
  .nav-links {
    position: fixed;
    top: var(--nav-height);
    right: -100%;
    width: 80%;
    /* vh resolves to the toolbar-retracted height on mobile browsers, so the
       drawer overshoots and the last items (Contact, EN/ES) land under the
       address bar on first open. dvh tracks the real visible viewport;
       the vh line stays first as a fallback for browsers without dvh. */
    height: calc(100vh - var(--nav-height));
    height: calc(100dvh - var(--nav-height));
    background: rgba(5, 6, 10, 0.96);
    backdrop-filter: blur(25px);
    flex-direction: column;
    padding: var(--space-xl) var(--space-lg);
    align-items: flex-start;
    gap: var(--space-md);
    overflow-y: auto;
    transition: right 0.4s var(--ease-out);
  }

  .nav-links.open { right: 0; }
  .nav-link { font-size: var(--text-md); }
  .hamburger { display: flex; }
}

@media (max-width: 768px) {
  #pattern, #skills, #experience, #process { padding: var(--space-3xl) 0; }
  #proof, #contact, .band { padding: var(--space-4xl) 0; }
  /* .band carries no heading, unlike #proof/#contact right after it, so
     the same top padding those two use reads as dead air stacked on top
     of #experience's own bottom padding. Bottom stays, so the run into
     Contact keeps its rhythm. */
  .band { padding-top: var(--space-xl); }
  .section-head { margin-bottom: var(--space-xl); }
  .case { padding: var(--space-2xl) 0; }
  .footer-legal { text-align: left; }

  .timeline::before { left: 16px; }
  .timeline-item { padding-left: 48px; }
  .timeline-node { left: 8px; }

  /* The header is a space-between row with flex-wrap, which at this width
     means whether the date sits beside the company or drops to its own
     line is decided by how long the company name happens to be. Measured
     at 390px: 6 of 11 rows kept it inline, 5 pushed it down, so the run
     alternated between 31px and 61px headers with the date sometimes
     right-aligned and sometimes left. Nothing about the role changed --
     only the length of its name -- so the raggedness carried no meaning.

     Stacked deliberately instead, and with the period first. On a phone
     the timeline is read as a vertical list, and the years are the spine
     of a career: leading with them gives every row the same shape and
     something consistent to scan down. */
  .timeline-header {
    flex-direction: column-reverse;
    align-items: flex-start;
    justify-content: flex-start;
    flex-wrap: nowrap;
    gap: var(--space-3xs);
  }

  .pillar-head { gap: var(--space-sm); padding: var(--space-md) var(--space-3xs); }
  .pillar-line, .pillar-panel-inner .skill-tags { margin-left: 0; }

  /* Keep the metric rail on one line instead of stacking three tall blocks.
     minmax(0, 1fr): plain 1fr can't shrink below the value's min-content,
     so a wide numeral like "100k+" blows the track out past the viewport. */
  .case-metrics { display: grid; grid-template-columns: repeat(3, auto); justify-content: start; gap: var(--space-lg); }
  .case-metric { min-width: 0; }
  .case-metric-value { font-size: clamp(1.4rem, 6.4vw, 2.1rem); }
  .case-metric-label { letter-spacing: var(--track-caps); }

  .case-visual { padding: var(--space-lg); }
  /* A transform does not reflow, so the scaled deck used to keep its full
     290px layout height and leave a band of dead space under the cards.
     The height scales with it: 290 x 0.72. */
  .deck { height: 210px; transform: scale(0.72); }
  .stats-grid { gap: var(--space-sm); }
}

@media (max-width: 420px) {
  .container { padding: 0 var(--space-md); }
  .hero-actions .btn, .hero-profiles .btn { width: 100%; }

  /* Nothing steps down below the 768px tier otherwise, so a 380px phone
     carries the same 96/144px block padding as a tablet. */
  #about, #skills, #experience { padding: var(--space-2xl) 0; }
  #proof, #contact, .band { padding: var(--space-3xl) 0; }
  .band { padding-top: var(--space-lg); }
  .case { padding: var(--space-xl) 0; }
  .hero-frame {
    padding-block: calc(var(--nav-height) + var(--space-xl)) var(--space-3xl);
    padding-inline: var(--space-md);
  }
  .hero-lead { max-width: none; }

  /* The 2.1rem clamp floor still overflows a narrow value like "100k+"
     once minmax(0, 1fr) forces the track down to the space actually
     available. Drop the floor further for phones under 420px. */
  .case-metric-value { font-size: clamp(1.3rem, 6.4vw, 2.1rem); }
  .case-metrics { gap: var(--space-md); }

  /* Two columns still crowds "Automatización" at this width even with
     minmax(0, 1fr) unlocked -- one column reads better than a forced
     shrink of both label and number. */
  .stats-grid { grid-template-columns: 1fr; }
}

/* The fan is 434px wide at full size, rotated corners included. At 0.72 it
   stops fitting its box below about 380px (measured: flush with both edges
   at 360), so the narrowest phones step down once more. 0.6 holds to 320. */
@media (max-width: 380px) {
  .deck { height: 180px; transform: scale(0.6); }
}

/* Motion preferences */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  .reveal { opacity: 1; transform: none; }
  .timeline-item { opacity: 1; transform: none; }
  /* .ambient-motes no longer needs hiding here: the dust is static now, so
     there is nothing in it for a motion preference to object to. */
  .scroll-mouse::after { animation: none; }
  /* The deck holds its fan instead of shuffling: the five slots of
     deckCycle, one card parked in each, centre card in front. */
  .deck-card { animation: none; }
  .deck-card:nth-child(1) { transform: translate(-132px, 20px) rotate(-10deg) scale(0.84); filter: brightness(0.5);  z-index: 1; }
  .deck-card:nth-child(2) { transform: translate(-68px, 7px) rotate(-5deg) scale(0.92);    filter: brightness(0.75); z-index: 2; }
  .deck-card:nth-child(3) { transform: translate(0, -6px) scale(1);                         filter: none;             z-index: 3; }
  .deck-card:nth-child(4) { transform: translate(68px, 7px) rotate(5deg) scale(0.92);      filter: brightness(0.75); z-index: 2; }
  .deck-card:nth-child(5) { transform: translate(132px, 20px) rotate(10deg) scale(0.84);   filter: brightness(0.5);  z-index: 1; }
  /* The queue holds still: front item, and the two waiting behind it. */
  .queue-item { animation: none; }
  .queue-item:nth-child(1) { transform: none; opacity: 1; z-index: 4; }
  .queue-item:nth-child(8) { transform: translateY(-18%) scale(0.94); filter: brightness(0.7); opacity: 1; z-index: 3; }
  .queue-item:nth-child(7) { transform: translateY(-33%) scale(0.88); filter: brightness(0.5); opacity: 1; z-index: 2; }
  .pillar-line { opacity: 1; transform: none; }

  /* The global 0.01ms override would snap the remap rather than skip it,
     which is a flicker instead of a preference. Show the work as it is. */
  .plate-img, .band-img, .gallery-img { filter: none; }
  .plate-crop::after, .band-crop::after, .gallery-crop::after { opacity: 0; }
  /* Same rule for the paired plate: show the lit image, skip the fade. */
  .plate-img--on { opacity: 1; }
  .plate-float { animation: none; transform: none; }
  /* iteration-count 1 above would run the drift once and strand the track
     half a lap along, so the band has to be stopped outright. */
  .band-track { animation: none; }
  /* .testimonial-track was here. The references marquee is gone: they are
     a static grid now, so there is no motion left to suppress. */
  /* Old gallery-track and showcase-track no longer used but kept to avoid
     any stale-reference repaints */
  .gallery-track { animation: none; }
  .showcase-track { animation: none; }
  /* In reduced motion the reel doesn't cycle, so just show the first image,
     and neither the image layer nor the glass word drift. */
  .hero-reel-track { transform: none !important; transition: none; }
  .hero-reel-parallax { transform: none !important; }

  .gallery-crop { opacity: 0.8; }
  /* Glass card visible immediately in reduced motion: no fade-in, no drift */
  .hero-glass-word.is-active {
    opacity: 1;
    transition: none;
    transform: none !important;
  }
}
