/*
   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);
}

/* 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;
}

/* 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); }

.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);
}

/* 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: #ffffff; }
.nav-link:hover::after, .nav-link.active::after { transform: scaleX(1); }

/* Language Switcher */
.lang-switcher {
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: 3px;
}

.lang-btn {
  background: none;
  border: none;
  color: var(--ink-soft);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 5px 12px;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all 0.25s ease;
}

.lang-btn.active {
  background: var(--metal-bright);
  color: #ffffff;
  box-shadow: 0 2px 10px rgba(224, 174, 60, 0.5);
}

/* 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
   and drops one dense block in the middle. This goes full bleed and anchors
   content to the frame instead: eyebrow at the top edge, statement at the
   optical centre, scroll cue at the bottom. The gutter scales with the
   viewport rather than turning into dead space. */
.hero-frame {
  position: relative;
  z-index: 2;
  /* Matches #hero's own 100vh floor exactly. This used to be 140vh "to give
     the two-column layout room", but nothing actually needed the extra 40vh
     -- it just centred the content in a row taller than the section below
     it expected, leaving a dead band between the hero and About. */
  min-height: 100vh;
  display: grid;
  /* Left column: hero text. Right column: a contained image card -- both
     columns padded symmetrically now, no bleed off the viewport edge. */
  /* The image column's max is capped by vw, not just a flat px ceiling: a
     flat ceiling grabs space fast enough at ~1200-1400px widths (common
     laptop range) that the text column gets squeezed to within a few px of
     "Emmanuel"'s rendered width. Tapering the growth with vw means it only
     reaches its full ceiling once the viewport is wide enough that the
     text column already has slack to spare.

     The 420px floor is why the hero now stacks at 1080px rather than 992:
     445 + 420 + 32 of gap needs 897px of track, and a 993px viewport only
     clears 854px after its own padding. Below 1080 there is no two-column
     layout for the floor to overflow. */
  grid-template-columns: minmax(445px, 1fr) minmax(420px, min(744px, 41vw));
  column-gap: var(--space-lg);
  padding-block: calc(var(--nav-height) + var(--space-2xl)) var(--space-4xl);
  padding-inline: clamp(var(--space-xl), 7vw, var(--space-4xl));
}

.hero-copy {
  display: grid;
  grid-template-rows: auto 1fr;
  min-width: 0;
}

.hero-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  justify-self: start;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink-soft);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
}

/* Sits just above true centre. The eye reads the optical centre as centred;
   true centre reads as slightly low. No max-width here: .hero-lead sets its
   own 38ch measure directly, and the title needs the full column width to
   hold "Emmanuel" on one line -- a cap here was clipping it down to 46ch
   (~442px) regardless of how wide the column actually was. */
.hero-statement {
  align-self: center;
  transform: translateY(-3%);
}

#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%;
  box-shadow: 0 0 10px var(--metal-bloom);
}

.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: 38ch;
  text-wrap: pretty;
}

.hero-actions {
  margin-top: var(--space-2xl);
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
}

/* 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;
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 228, 0.8),
    inset 0 -1px 0 rgba(74, 44, 6, 0.55),
    0 8px 26px rgba(255, 180, 30, 0.13),
    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),
    0 14px 34px rgba(240, 199, 94, 0.34),
    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;
}

.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; }
}

/* About Section */
#about {
  padding: var(--space-4xl) 0;
  position: relative;
  z-index: 2;
}

.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: all 0.3s ease;
  box-shadow: var(--edge-lit), var(--edge-fall), var(--elev-2);
  /* 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);
}

/* 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);
}

/* 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. The row already says it is open by opening. */
.pillar-mark { 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; }

.pillar-panel-inner {
  overflow: hidden;
  min-height: 0;
  padding-bottom: var(--space-2xl);
}

.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); }

.pillar-panel-inner .skill-tags {
  margin: 0 0 var(--space-lg) var(--space-2xl);
}

.skill-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2xs);
}

.skill-tags span {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  padding: var(--space-3xs) var(--space-xs);
  background: rgba(224, 174, 60, 0.1);
  border-radius: var(--radius-md);
  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(203, 166, 247, 0.07) 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: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-caps-wide);
  color: var(--ink-faint);
  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;
}

.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 crew and the deck float on the room, which is the
   point of both figures: one person carrying nine, cards generated 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 ±104px 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, one person holding twelve designers' output */
/* The rule that used to split the one from the nine is gone. The gap does that
   job now, which is the whole point of the figure. */
.crew {
  display: flex;
  align-items: center;
  gap: var(--space-2xl);
}

.crew-rule { display: none; }

/* Nothing holds these up any more, so they drift. The motion goes on the icon
   inside, because .crew-figure's own transform is already carrying the
   scroll reveal and an animation would override the transition. Offsets are
   staggered so the group floats rather than sliding as one block. */
@keyframes figureDrift {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}

.crew-figure.is-lit .icon { animation: figureDrift 5.5s var(--ease-out) infinite; }

.crew-block .crew-figure .icon { animation: figureDrift 7s var(--ease-out) infinite; }
.crew-block .crew-figure:nth-child(3n+2) .icon { animation-delay: -1.4s; }
.crew-block .crew-figure:nth-child(3n) .icon   { animation-delay: -2.9s; }

@media (prefers-reduced-motion: reduce) {
  .crew-figure .icon { animation: none !important; }
}

.crew-figure {
  display: grid;
  place-items: center;
  color: var(--ink-faint);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 0.5s ease, transform 0.5s var(--ease-out);
}

.crew-figure .icon { width: 32px; height: 32px; }

.crew-figure.is-lit {
  color: var(--metal-bright);
  filter: drop-shadow(0 0 14px var(--metal-bloom));
}

.crew-figure.is-lit .icon { width: 62px; height: 62px; stroke-width: 1.3; }

.crew-block {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-xs) var(--space-sm);
}

.case.visible .crew-figure { opacity: 0.42; transform: translateY(0); }
.case.visible .crew-figure.is-lit { opacity: 1; }

.case.visible .crew-block .crew-figure:nth-child(1) { transition-delay: 0.20s; }
.case.visible .crew-block .crew-figure:nth-child(2) { transition-delay: 0.26s; }
.case.visible .crew-block .crew-figure:nth-child(3) { transition-delay: 0.32s; }
.case.visible .crew-block .crew-figure:nth-child(4) { transition-delay: 0.38s; }
.case.visible .crew-block .crew-figure:nth-child(5) { transition-delay: 0.44s; }
.case.visible .crew-block .crew-figure:nth-child(6) { transition-delay: 0.50s; }
.case.visible .crew-block .crew-figure:nth-child(7) { transition-delay: 0.56s; }
.case.visible .crew-block .crew-figure:nth-child(8) { transition-delay: 0.62s; }
.case.visible .crew-block .crew-figure:nth-child(9) { transition-delay: 0.68s; }

/* Case 2 visual, the live-data deck */
.deck {
  position: relative;
  width: 100%;
  height: 250px;
  display: grid;
  place-items: center;
}

.deck-card {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  width: 152px;
  height: 210px;
  padding: var(--space-sm);
  border-radius: var(--radius-md);
  background: linear-gradient(160deg, hsl(var(--h) 42% 16%) 0%, hsl(var(--h) 46% 9%) 100%);
  border: 1px solid var(--border-glow);
  overflow: hidden;
  box-shadow: 0 14px 30px rgba(0, 0, 0, 0.5);
  animation: deckCycle 12s var(--ease-out) infinite, hueCycle 24s linear infinite;
}

.deck-card:nth-child(1) { animation-delay: 0s, 0s; }
.deck-card:nth-child(2) { animation-delay: -3s, -6s; }
.deck-card:nth-child(3) { animation-delay: -6s, -12s; }
.deck-card:nth-child(4) { animation-delay: -9s, -18s; }

.deck-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--light-core), var(--light-fade) 55%, var(--metal-mid));
}

.deck-chip {
  align-self: flex-start;
  font-family: var(--font-heading);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--track-caps-wide);
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius-xs);
  color: hsl(var(--h) 85% 88%);
  background: hsl(var(--h) 70% 72% / 0.18);
  border: 1px solid hsl(var(--h) 70% 72% / 0.35);
}

.deck-bar {
  height: 7px;
  border-radius: var(--radius-xs);
  background: hsl(var(--h) 60% 78% / 0.5);
}

.deck-bar.short { width: 58%; background: hsl(var(--h) 60% 78% / 0.28); }

.deck-stats {
  display: flex;
  gap: var(--space-2xs);
  margin-top: auto;
}

.deck-stats i {
  flex: 1;
  height: 22px;
  border-radius: var(--radius-xs);
  background: hsl(var(--h) 50% 70% / 0.16);
  border: 1px solid hsl(var(--h) 60% 75% / 0.22);
}

@keyframes deckCycle {
  0%   { transform: translate(-104px, 16px) rotate(-9deg) scale(0.88); opacity: 0.4; z-index: 1; }
  25%  { transform: translate(-36px, 4px)   rotate(-4deg) scale(0.95); opacity: 0.75; z-index: 2; }
  50%  { transform: translate(36px, -8px)   rotate(3deg)  scale(1);    opacity: 1;    z-index: 3; }
  75%  { transform: translate(104px, 8px)   rotate(8deg)  scale(0.93); opacity: 0.65; z-index: 2; }
  100% { transform: translate(-104px, 16px) rotate(-9deg) scale(0.88); opacity: 0.4; z-index: 1; }
}

@keyframes hueCycle {
  0%   { --h: 196; }
  33%  { --h: 228; }
  66%  { --h: 262; }
  100% { --h: 196; }
}

/* Secondary tier below the two case studies: lighter, in brief, no metrics.
   margin-top echoes .case's own padding rhythm rather than a rule -- same
   "separated by space" logic as the two cases above it. */
.proof-more {
  margin-top: var(--space-4xl);
}

.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: 2px;
  background: linear-gradient(180deg, var(--metal-mid) 0%, rgba(224, 174, 60, 0.15) 100%);
}

.timeline-item {
  position: relative;
  padding-left: 85px;
  margin-bottom: var(--space-2xl);
  opacity: 0;
  transform: translateY(20px);
  transition: all 0.5s ease;
}

/* 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: 22px;
  top: 4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--void-black);
  border: 4px solid var(--metal-mid);
  box-shadow: 0 0 16px var(--metal-bloom);
  z-index: 2;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}

.timeline-item:hover .timeline-node {
  border-color: var(--light-core);
  box-shadow: 0 0 14px var(--light-bloom);
}

/* 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-md) 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;
  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-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: #ffffff;
}

.timeline-period {
  font-size: var(--text-sm);
  color: var(--ink-faint);
  font-weight: 600;
  letter-spacing: 0.01em;
}

.timeline-role {
  font-size: var(--text-md);
  color: var(--ink-bright);
  font-weight: 700;
  margin-bottom: var(--space-sm);
}

.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-2xs);
}

.timeline-tags span {
  font-size: var(--text-xs);
  font-weight: 600;
  padding: var(--space-3xs) var(--space-xs);
  background: rgba(255, 255, 255, 0.05);
  border-radius: var(--radius-md);
  color: var(--ink);
}

/* Projects Gallery: the secondary tier inside #proof, not its own section */
.projects-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-lg);
}

/* An odd number of cards leaves the last one alone beside an empty half of
   the row. It closes the tier as a full-width feature instead, on a
   shallower crop so the extra width doesn't buy an unreasonable height.
   Self-correcting: add a sixth card and the rule stops applying. */
.projects-grid > .project-card:last-child:nth-child(odd) { grid-column: 1 / -1; }
.projects-grid > .project-card:last-child:nth-child(odd) .project-img-wrapper {
  aspect-ratio: 2.2 / 1;
}

/* The image is the object. No plate under it, no line around it: the radius on
   the image itself is all the containment it needs. */
.project-card {
  transition: transform 0.4s var(--ease-out);
  transform-style: preserve-3d;
}

.project-card:hover { transform: translateY(-6px); }

.project-img-wrapper {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-lg);
  /* .project-card's preserve-3d (for the pointer-tilt effect) plus a
     scaled image inside a rounded overflow: hidden box is exactly the
     combination Safari's corner-clipping bug shows up on. .plate-crop
     and .band-crop already carry this guard for the same reason. */
  isolation: isolate;
  contain: paint;
}

.project-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease-out);
}

.project-card:hover .project-img { transform: scale(1.06); }

.project-tag {
  position: absolute;
  top: 1.2rem;
  right: 1.2rem;
  background: rgba(5, 6, 10, 0.85);
  backdrop-filter: blur(12px);
  color: var(--ink);
  font-size: var(--text-xs);
  font-weight: 700;
  padding: var(--space-3xs) var(--space-xs);
  border-radius: var(--radius-lg);
}

.project-info { padding: var(--space-lg); }

.project-title {
  font-size: var(--text-xl);
  font-weight: 700;
  margin-bottom: var(--space-xs);
  text-wrap: balance;
}

.project-desc {
  color: var(--ink-soft);
  font-size: var(--text-md);
  line-height: var(--leading-body);
  max-width: 46ch;
}

/* Contact */
#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;
}

/* 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: 1px solid var(--border-subtle);
  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: all 0.3s ease;
}

.social-btn:hover {
  background: var(--metal-bright);
  border-color: var(--metal-mid);
  transform: translateY(-4px);
  box-shadow: 0 8px 20px rgba(240, 199, 94, 0.45);
}

.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);
  border: 1px solid var(--border-subtle);
  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); }

/* This was #ffffff on --metal-bright (#f7d472): about 1.5:1, which is not
   a style problem but an unreadable label. It is a struck object, so it
   takes the metal ramp and --on-metal, the pairing the token ladder
   already guarantees clears AA. The 10px gold drop shadow it used to
   carry is replaced by the same lit-top/dark-bottom edge pair the primary
   button uses, so the segment reads as raised rather than as glowing. */
.path-tab.active {
  background-image: linear-gradient(172deg, var(--metal-ramp-tight));
  color: var(--on-metal);
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 228, 0.7),
    inset 0 -1px 0 rgba(74, 44, 6, 0.5),
    0 2px 6px rgba(0, 0, 0, 0.45);
}

.path-tab.active:hover { color: var(--on-metal); }

.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: 1px solid var(--border-subtle);
  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.22);
}

.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; }

/* ─────────────────────────────────────────────
   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;
  gap: var(--space-2xs);
}

.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);
  --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 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); }

.plate-caption {
  font-size: var(--text-xs);
  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. Same rule as figureDrift
   sitting on the child .icon rather than on .crew-figure.

   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 {
  position: relative;
  align-self: center;
  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;
  gap: 0.5rem;
  align-items: center;
  justify-content: center;
}

.hero-dot {
  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;
}

.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;
  /* Every other section runs --space-4xl; this ran a step short of the
     rest of the page's rhythm for no stated reason. */
  padding: var(--space-4xl) 0;
  /* 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; }

/* 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, and it has to.
   Its two columns carry hard minimums (445px of text so "Emmanuel" holds
   one line, 420px of image so the card never drops below the size it was
   designed at) and those two numbers plus the gap stop fitting at about
   1045px. Rather than let one of them lose, the layout stops being two
   columns while there is still room to stack gracefully. */
@media (max-width: 1080px) {
  .hero-frame { grid-template-columns: 1fr; 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. */
  .hero-showcase {
    margin-top: var(--space-xl);
    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;
  }
}

@media (max-width: 992px) {
  .about-grid { grid-template-columns: 1fr; gap: var(--space-xl); }
  .projects-grid { grid-template-columns: 1fr; }
  .projects-grid > .project-card:last-child:nth-child(odd) .project-img-wrapper { aspect-ratio: 16 / 9; }
  .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) {
  #about, #skills, #experience { padding: var(--space-3xl) 0; }
  #proof, #contact, .band { padding: var(--space-4xl) 0; }
  .section-head { margin-bottom: var(--space-xl); }
  .case { padding: var(--space-2xl) 0; }
  .proof-more { margin-top: var(--space-2xl); }
  .footer-legal { text-align: left; }

  .timeline::before { left: 16px; }
  .timeline-item { padding-left: 48px; }
  .timeline-node { left: 7px; }

  .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, minmax(0, 1fr)); gap: var(--space-sm); }
  .case-metric { padding-left: 0.8rem; min-width: 0; }
  .case-metric-label { letter-spacing: var(--track-caps); }

  .case-visual { padding: var(--space-lg); }
  .deck { transform: scale(0.72); }
  .crew { flex-direction: column; gap: var(--space-sm); }
  .stats-grid { gap: var(--space-sm); }
}

@media (max-width: 420px) {
  .container { padding: 0 var(--space-md); }
  .hero-actions .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; }
  .case { padding: var(--space-xl) 0; }
  .proof-more { margin-top: var(--space-xl); }
  .hero-frame {
    padding-block: calc(var(--nav-height) + var(--space-xl)) var(--space-3xl);
    padding-inline: var(--space-md);
  }
  /* Optical centring is a large-canvas refinement. On a phone the frame is
     short enough that nudging up just crowds the eyebrow. */
  .hero-statement { transform: none; }
  .hero-lead { max-width: none; }

  /* The hard <br> between "Emmanuel" and "De Fex" (index.html) needs
     "Emmanuel" to fit alone on one line. --text-5xl's floor (3rem) is sized
     for the two-column desktop case and is still too wide for a ~260px
     phone column -- it forces the global overflow-wrap fallback to split
     the word itself ("EMMAN" / "UEL"). Give phones their own smaller cap. */
  .hero-title { font-size: clamp(2.1rem, 10vw, 2.4rem); }
  .crew-block { gap: var(--space-2xs); }

  /* 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.4rem, 8vw, 2.1rem); }

  /* 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; }
}

/* 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; }
  .deck-card:nth-child(1) { transform: translate(-104px, 16px) rotate(-9deg) scale(0.88); opacity: 0.55; }
  .deck-card:nth-child(2) { transform: translate(-36px, 4px) rotate(-4deg) scale(0.95); opacity: 0.8; }
  .deck-card:nth-child(3) { transform: translate(36px, -8px) rotate(3deg) scale(1); opacity: 1; }
  .deck-card:nth-child(4) { transform: translate(104px, 8px) rotate(8deg) scale(0.93); opacity: 0.7; }
  .crew-figure { opacity: 0.42; transform: none; }
  .crew-figure.is-lit { opacity: 1; }
  .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; }
  .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; }
  /* 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;
  }
}
