/* =====================================================================
   ALI EZBAYEL — PORTFOLIO
   Design system: clean neutral-grayscale base + one sharp accent
   (vermillion — matches the 3D skyline's "signal candle" color exactly,
   so the 2D chrome and the WebGL scene read as one system). No warm
   paper/ink editorial texture, no serif — a single confident sans
   (Manrope) at varying weight does headlines and body both, mono stays
   for data/labels only.

   NOTE: --gold/--gold-bright/--teal/--rose are kept as the live variable
   NAMES (inline style="--accent:var(--gold)" refs throughout index.html,
   plus script.js's achievementMemories color keys) — only the VALUES
   changed. --teal/--rose are now neutral graphite/taupe tones, not
   distinct hues — the only real color on the site is the vermillion
   accent; everything else is grayscale.
   ===================================================================== */

:root {
  --paper: #ffffff;
  --paper-dim: #f6f7f9;
  --paper-panel: #eef0f3;
  --paper-panel-strong: #e2e5ea;
  --line: rgba(15, 17, 21, 0.1);
  --line-strong: rgba(15, 17, 21, 0.2);
  /* WCAG 1.4.11 wants 3:1 for the visual boundary of an interactive
     control. --line-strong composites to ~1.6:1 on paper, which is fine
     for a decorative row divider but too faint to be a button's only
     affordance. This is used ONLY on controls (buttons, tabs, option
     pills, the nav toggle, zoom buttons) and clears 3:1 on every surface
     they sit on; decorative rules keep the lighter tokens above. */
  --line-ui: rgba(15, 17, 21, 0.5);

  --ink: #101216;
  --ink-dim: #565b66;
  /* Was #686c75. That measured 4.61:1 on --paper-panel — over AA's 4.5
     but with almost no margin, and it's the token used for the SMALLEST
     type on the site (0.6–0.72rem mono labels, the divider marquees, the
     scroll cue). One step darker takes the worst case to 5.27:1 and the
     common case (on white) to 6.02:1, which is real headroom, while
     staying visibly "faint" against --ink-dim above it. */
  --ink-faint: #5f636c;

  /* the one accent color on the site — matches the 3D scene's GOLD exactly */
  --gold: #c13a20;
  --gold-bright: #ff6b3d;
  /* secondary/tertiary are neutral tones now, not distinct hues — kept only for wayfinding */
  --teal: #454952;
  --rose: #7c7568;

  --card-ink: #0d0f13;
  --card-ink-2: #191c22;

  --font-serif: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  --font-sans: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

  --max-width: 1140px;
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --section-pad: clamp(3rem, 7vw, 6rem);

  /* ---- Spacing scale -------------------------------------------------
     One step = 0.25rem. The root here is 106.25% (17px), so a step is
     4.25px and every token below lands on a strict 4-step grid — the same
     rhythm as a 4/8px system, but expressed in rem so it still scales
     with a visitor's own browser font-size setting instead of freezing at
     one pixel size. Values that were previously hand-picked (0.65rem,
     1.1rem, 1.75rem, 0.85rem…) are snapped to the nearest step, which is
     a sub-pixel-to-3px move each time — spacing gets consistent without
     the layout shifting. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;
  --space-6: 1.5rem;
  --space-7: 1.75rem;
  --space-8: 2rem;
  --space-10: 2.5rem;
  --space-12: 3rem;

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 18px;
  --radius-pill: 999px;

  --shadow-md: 0 1px 2px rgba(15, 17, 21, 0.05), 0 12px 24px -12px rgba(15, 17, 21, 0.14);
  --shadow-lg: 0 4px 10px rgba(15, 17, 21, 0.06), 0 28px 56px -20px rgba(15, 17, 21, 0.22);
  --shadow-card: 0 30px 60px -24px rgba(8, 9, 12, 0.5), 0 8px 20px -6px rgba(8, 9, 12, 0.35);
  /* A hairline top highlight + a tight contact shadow. Reads as a card
     lifted a millimetre off the page rather than a drop-shadowed box. */
  --shadow-raise: inset 0 1px 0 rgba(255, 255, 255, 0.7), 0 1px 2px rgba(15, 17, 21, 0.04), 0 8px 20px -12px rgba(15, 17, 21, 0.16);

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  /* A quick, slightly overshooting settle — for the moment something
     responds to a touch (a press, a release), not for page-level reveals
     (those stay on --ease-out, which only decelerates, never overshoots).
     The distinction is deliberate: a big reveal that overshot would read
     as sloppy, but a button with zero spring to it reads as inert. */
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  /* One duration/curve for every hover, active and focus change on the
     site, so no two controls settle at different speeds. */
  --transition: 0.2s ease-in-out;

  /* Focus ring: a white separator ring hugging the element, then the
     accent ring outside it. On paper the white is invisible; on the dark
     surfaces (KPI strip, credit card, nav toggle when open) it's what
     keeps the vermillion ring from disappearing into the dark. */
  --focus-ring: var(--gold);
  --focus-halo: rgba(255, 255, 255, 0.92);
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* The navbar is sticky at top:0 and ~82px tall, so an in-page jump
     ("Experience", "Contact"…) used to park the section's eyebrow and
     heading UNDERNEATH it — you landed mid-section with no title in
     sight. This offsets every scroll target, including the ones the
     browser picks when restoring a #hash on load. */
  scroll-padding-top: 6.5rem;
  font-size: 106.25%;
  scrollbar-width: none;
}

html::-webkit-scrollbar {
  display: none;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--ink);
  background: var(--paper);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Manrope ships slightly loose by default at body sizes. A hair of
     negative tracking tightens words into blocks without touching the
     mono labels (which set their own positive tracking). */
  letter-spacing: -0.003em;
}

/* Avoids the one-word-alone-on-the-last-line ("orphan") breaks that show
   up all over a narrow column — supported browsers rebalance, the rest
   are unaffected. */
p {
  text-wrap: pretty;
}

/* Numbers that animate (KPI count-up, career stats) change digit by digit.
   Proportional figures make the whole line jitter wider and narrower on
   every frame; tabular figures hold one fixed advance width so the count
   ticks in place. */
.kpi-number,
.career-stat-value,
.cc-number,
.education-date,
.ach-card-year,
.ach-modal-year {
  font-variant-numeric: tabular-nums;
}

::selection {
  background: var(--gold);
  color: var(--paper);
}

a {
  color: inherit;
}

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

/* Two concentric rings instead of one flat outline. The box-shadow sits
   flush against the element as a light separator, the outline sits 2px
   outside it in the accent — so the ring stays legible whether the
   control is on paper, on the dark KPI strip, or on the credit card.
   The old rule also set `border-radius: var(--radius-sm)` here, which
   silently re-rounded the corners of any focused element that didn't
   declare its own radius; the ring now follows each element's real shape. */
:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  box-shadow: 0 0 0 2px var(--focus-halo);
}

/* Mouse users never see the ring; keyboard users always do. */
:focus:not(:focus-visible) {
  outline: none;
}

/* First tab stop on the page: lets a keyboard or switch user jump the
   nav and the intro chrome straight to the content. Off-canvas until
   focused, then it slides into the top-left corner. */
.skip-link {
  position: fixed;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 600;
  transform: translateY(-160%);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-sm);
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-decoration: none;
  box-shadow: var(--shadow-lg);
  transition: transform var(--transition);
}

.skip-link:focus-visible {
  transform: translateY(0);
}

h1, h2, h3 {
  margin: 0 0 0.5em 0;
  font-family: var(--font-serif);
  font-weight: 800;
  color: var(--ink);
  /* Optical tracking: the bigger the type, the more the counters need
     closing up. A single -0.02em applied from a 5.2rem h1 down to a
     1.05rem h3 leaves the h1 loose and the h3 cramped, so each level
     gets its own value below. */
  letter-spacing: -0.022em;
  /* Headings inherit body's 1.6 otherwise, which is a paragraph leading —
     far too airy once a title wraps to two lines. */
  line-height: 1.15;
  /* Evens out wrapped headings instead of leaving one short word stranded. */
  text-wrap: balance;
}

h1 {
  letter-spacing: -0.032em;
}

h3 {
  letter-spacing: -0.012em;
  line-height: 1.25;
}

.letter-char {
  display: inline-block;
  will-change: transform, opacity;
}

/* =====================================================================
   Global backdrop — a real 3D financial skyline (Three.js/WebGL), the
   camera flying through it as the page scrolls, plus a subtle mouse-
   driven drift on desktop. Desktop + WebGL only: the canvas is created
   and sized by script.js only when both are available; everywhere else
   (mobile, no WebGL, prefers-reduced-motion-without-fallback) this stays
   a plain, cheap, gradient-tinted paper background — never a broken or
   blank canvas.
   ===================================================================== */
.global-backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 30%, var(--paper-panel) 0%, var(--paper) 70%);
}

.global-backdrop::after {
  content: "";
  position: absolute;
  inset: -10%;
  opacity: 0.05;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

.skyline-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 1.2s ease;
}

.skyline-canvas.is-ready {
  opacity: 1;
}

/* ---- Readability scrim over the 3D scene ---------------------------
   #experience, #achievements and the hero are transparent sections, so
   their body copy sits directly on the WebGL skyline. The towers span
   the full viewport width (7 lanes at ±60 world units against a ~±49
   visible half-width), roughly one in five is a vermillion accent, and
   text over one of those measures well under 4.5:1 — a contrast level
   that changes as the camera flies, which is the worst kind.

   A soft paper veil, strongest down the centre where the 1140px content
   column sits and clearing completely at the edges where the towers
   actually read, lifts that floor without flattening the scene. It's
   gated on .has-skyline (set by script.js only once the renderer is
   actually painting), so the mobile / no-WebGL flat gradient — which
   already passes everywhere — is left exactly as it was. */
.has-skyline .page::before {
  content: "";
  position: fixed;
  inset: 0;
  /* .page is z-index:1 + position:relative, i.e. its own stacking
     context, so -1 puts this above the backdrop but below all content. */
  z-index: -1;
  pointer-events: none;
  /* Measured, not eyeballed. Against the worst realistic backdrop — a
     near-field vermillion candle at full saturation, directly behind the
     content column — this veil takes body copy from 1.26:1 to 5.52:1 and
     the small mono labels from 1.12:1 to 4.88:1. Dial the 0.86 down for
     a bolder skyline and up for more headroom; every 0.02 is worth
     roughly 0.07 of contrast ratio on the faint text. */
  background: radial-gradient(
    ellipse 62% 96% at 50% 50%,
    rgba(255, 255, 255, 0.86) 0%,
    rgba(255, 255, 255, 0.82) 46%,
    rgba(255, 255, 255, 0.4) 76%,
    rgba(255, 255, 255, 0) 100%
  );
}

/* =====================================================================
   Custom scrollbar
   ===================================================================== */
.site-scrollbar {
  position: fixed;
  top: 0;
  right: 3px;
  width: 4px;
  height: 100vh;
  z-index: 500;
  pointer-events: none;
}

.site-scrollbar-thumb {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  min-height: 28px;
  border-radius: 3px;
  background: var(--gold);
  opacity: 0.6;
}

.page {
  position: relative;
  z-index: 1;
}

/* =====================================================================
   Intro — the one deliberately dark moment on the whole site: a two-
   second ink curtain that wipes open to reveal the paper-light page
   beneath, like house lights coming up. Contrast, not a dark theme.
   ===================================================================== */
.intro-overlay {
  position: fixed;
  inset: 0;
  z-index: 500;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.8rem;
  background: radial-gradient(ellipse at 50% 40%, var(--card-ink-2) 0%, var(--card-ink) 65%, #06070a 100%);
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}

.intro-overlay.is-leaving {
  animation: intro-wipe 0.9s cubic-bezier(0.65, 0, 0.35, 1) forwards;
  pointer-events: none;
}

.intro-overlay.is-hidden {
  display: none;
}

@keyframes intro-wipe {
  0% { clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%); }
  60% { clip-path: polygon(0% 0%, 0% 0%, 100% 100%, 0% 100%); }
  100% { clip-path: polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%); }
}

.intro-rig {
  position: relative;
  display: flex;
  align-items: center;
  gap: 1.1rem;
}

.intro-guide {
  width: 34px;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 107, 61, 0.5));
}

.intro-guide:last-child {
  transform: scaleX(-1);
}

.intro-chip {
  position: relative;
  width: 58px;
  height: 42px;
}

.intro-chip-base {
  position: absolute;
  inset: 0;
  border-radius: 7px;
  background: linear-gradient(150deg, var(--card-ink-2), var(--card-ink));
  border: 1px solid rgba(255, 107, 61, 0.3);
}

.intro-chip-glow {
  position: absolute;
  inset: -14px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 107, 61, 0.35), transparent 70%);
  animation: chip-pulse 1.8s ease-in-out infinite;
}

@keyframes chip-pulse {
  0%, 100% { opacity: 0.5; transform: scale(0.9); }
  50% { opacity: 1; transform: scale(1.08); }
}

.intro-chip-lid {
  position: absolute;
  inset: 3px 3px auto 3px;
  height: 16px;
  border-radius: 5px;
  background: linear-gradient(150deg, var(--gold-bright), var(--gold));
  display: flex;
  align-items: center;
  justify-content: center;
  animation: chip-lift 2s ease-in-out infinite;
  transform-origin: 50% 100%;
}

@keyframes chip-lift {
  0%, 100% { transform: rotateX(0deg); }
  50% { transform: rotateX(-18deg); }
}

.intro-chip-mono {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  color: var(--card-ink);
}

.intro-cycle {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(251, 249, 244, 0.7);
  min-height: 1.2em;
}

.intro-bar {
  width: 140px;
  height: 2px;
  background: rgba(251, 249, 244, 0.15);
  border-radius: 2px;
  overflow: hidden;
}

.intro-bar-fill {
  display: block;
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--gold), var(--gold-bright));
  animation: intro-bar-fill 1.9s var(--ease-out) forwards;
}

@keyframes intro-bar-fill {
  to { width: 100%; }
}

/* =====================================================================
   Top ticker + section dividers — thin, restrained marquees, real
   content only (no fake market data)
   ===================================================================== */
.top-ticker {
  position: relative;
  z-index: 50;
  background: var(--ink);
  overflow: hidden;
  white-space: nowrap;
}

.top-ticker-track {
  display: inline-flex;
  gap: 0.9rem;
  padding: 0.4rem 0;
  animation: scroll-left 32s linear infinite;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  color: rgba(251, 249, 244, 0.78);
}

.top-ticker-track span:nth-child(odd) {
  color: var(--gold-bright);
}

@keyframes scroll-left {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

/* WCAG 2.2.2 (Pause, Stop, Hide) applies to anything that moves
   automatically for more than five seconds — these four marquees all run
   forever. Hovering or tabbing into one halts it, so the text can
   actually be read, and reduced-motion stops them outright further down. */
.top-ticker:hover .top-ticker-track,
.top-ticker:focus-within .top-ticker-track,
.section-divider:hover .section-divider-content,
.trade-ticker:hover .trade-ticker-track,
.cc-security-ticker:hover .cc-security-track {
  animation-play-state: paused;
}

.section-divider {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-4) var(--gutter);
}

.section-divider-arrow {
  flex-shrink: 0;
  font-size: 0.6rem;
  color: var(--gold);
  opacity: 0.7;
}

.section-divider-track {
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  -webkit-mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent);
}

.section-divider-content {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  color: var(--ink-faint);
  animation: scroll-left 28s linear infinite;
}

/* =====================================================================
   Navbar
   ===================================================================== */
.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  /* Was rgba(251,249,244,.86) — a warm cream left over from the earlier
     paper palette, which now reads as a visible yellow band sitting on a
     pure white page. Neutral white at a slightly lower alpha lets more of
     the skyline through while matching --paper exactly. */
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(14px) saturate(1.4);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  border-bottom: 1px solid var(--line);
  /* A whisper of depth under the bar so content scrolling beneath it
     passes *under* something, rather than colliding with a flat edge. */
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6), 0 6px 20px -14px rgba(15, 17, 21, 0.5);
}

.navbar-inner {
  position: relative;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: var(--space-4) var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

.logo {
  font-family: var(--font-serif);
  font-weight: 800;
  font-size: 1.2rem;
  color: var(--ink);
}

.logo em {
  font-style: normal;
  font-weight: 500;
  color: var(--gold);
}

.navbar nav {
  display: flex;
  gap: clamp(1rem, 2.5vw, 2rem);
}

.navbar nav a {
  position: relative;
  display: inline-flex;
  align-items: center;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-dim);
  padding-bottom: 3px;
  /* The colour change was instant while the underline animated — the two
     halves of one hover arriving at different times. */
  transition: color var(--transition);
}

.navbar nav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1.5px;
  background: var(--gold);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.3s var(--ease-out);
}

.navbar nav a:hover,
.navbar nav a:focus-visible {
  color: var(--ink);
}

/* Keyboard focus now gets the same underline sweep the mouse gets, on top
   of the focus ring — the hover affordance was previously mouse-only. */
.navbar nav a:hover::after,
.navbar nav a:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  border: 1px solid var(--line-ui);
  border-radius: var(--radius-sm);
  background: var(--paper);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  transition: background var(--transition), border-color var(--transition);
}

.nav-toggle:hover {
  background: var(--paper-panel);
  border-color: var(--ink);
}

.nav-toggle:active {
  background: var(--paper-panel-strong);
}

.nav-toggle span {
  display: block;
  width: 18px;
  height: 1.5px;
  background: var(--ink);
  transition: transform 0.3s var(--ease-out), opacity 0.3s var(--ease-out);
}

.nav-toggle.is-open span:nth-child(1) {
  transform: translateY(6.5px) rotate(45deg);
}

.nav-toggle.is-open span:nth-child(2) {
  opacity: 0;
}

.nav-toggle.is-open span:nth-child(3) {
  transform: translateY(-6.5px) rotate(-45deg);
}

/* =====================================================================
   Buttons
   ===================================================================== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  /* Snapped from 0.85rem/1.6rem onto the scale. Also guarantees a 44px
     tall target on its own, without relying on the coarse-pointer block. */
  padding: var(--space-3) var(--space-6);
  min-height: 44px;
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition),
    color var(--transition), box-shadow var(--transition), transform var(--transition);
}

/* Every control on the site now depresses very slightly on press, so a
   tap or click confirms itself even before the page reacts. */
.btn:active {
  transform: translateY(1px);
}

.btn-primary {
  background: var(--gold);
  color: var(--paper);
  box-shadow: 0 10px 24px -10px rgba(193, 58, 32, 0.55);
}

.btn-primary:hover,
.btn-primary:focus-visible {
  background: #a6301a;
}

/* Deliberately hover-only: the global :focus-visible rule paints its halo
   with box-shadow, so re-declaring box-shadow on :focus-visible here would
   overwrite the ring's inner separator and break it on dark surfaces. */
.btn-primary:hover {
  box-shadow: 0 14px 30px -10px rgba(193, 58, 32, 0.65);
}

.btn-secondary {
  background: transparent;
  border-color: var(--line-ui);
  color: var(--ink);
}

.btn-secondary:hover,
.btn-secondary:focus-visible {
  border-color: var(--ink);
  background: var(--paper-panel);
}

/* =====================================================================
   Section shell + reveal-on-scroll
   ===================================================================== */
.section {
  position: relative;
  padding: var(--section-pad) 0;
}

/* Belt-and-braces alongside html{scroll-padding-top}: Safari has been
   inconsistent about applying scroll-padding to same-document anchor
   jumps, and scroll-margin on the target itself is honoured everywhere. */
section[id],
#about {
  scroll-margin-top: 6.5rem;
}

.section-alt {
  background: var(--paper-dim);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.section-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold);
  margin: 0 0 var(--space-3) 0;
}

.section-inner > h2 {
  font-size: clamp(2rem, 4vw, 2.9rem);
  perspective: 500px;
  /* The three-part section header (eyebrow → title → sub) is one block.
     0.5em of h2 margin at 2.9rem is ~24px, which floated the title away
     from its own eyebrow; a scale step ties them together. */
  margin-bottom: var(--space-3);
}

.section-sub {
  color: var(--ink-dim);
  font-size: 1.02rem;
  /* Snapped from 2.5rem. This is the gap between a section's header block
     and its content — the single biggest rhythm value on the page, now
     identical in all five sections. */
  margin: 0 0 var(--space-10) 0;
  max-width: 46ch;
  line-height: 1.55;
}

/* Education has no .section-sub, so its heading sat hard against the
   timeline while every other section had 2.5rem of air. Same gap, applied
   to whatever directly follows the header block. */
.section-inner > h2:last-of-type + .education-list {
  margin-top: var(--space-10);
}

.reveal {
  opacity: 0;
  /* Scale + blur added on top of the original translateY, for a "settling
     into focus" arrival instead of a flat slide-up — closer to how Apple's
     product pages bring content in. Both interpolate to nothing by the
     time .in-view lands (see below for why that specifically matters here). */
  transform: translateY(28px) scale(0.96);
  filter: blur(6px);
  transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out), filter 0.8s var(--ease-out);
}

.reveal.in-view {
  opacity: 1;
  /* transform: none (not translateY(0) or scale(1)) — any non-none
     transform on an ancestor creates a new containing block for
     position:fixed descendants (e.g. .ach-modal-overlay lives inside a
     .reveal section), which would pin "fixed" elements to this section's
     box instead of the viewport. */
  transform: none;
  /* filter has the SAME hazard and it's much less well known: a filter
     value other than the literal keyword "none" ALSO creates a containing
     block for fixed/sticky descendants, identically to transform — a
     lingering blur(0px) here would be just as broken as a lingering
     translateY(0) above, even though it looks visually identical to "off".
     #contact uses .reveal too, and .cc-sticky (the credit card's reveal)
     lives inside it — this is not a hypothetical. */
  filter: none;
}

/* =====================================================================
   Hero
   ===================================================================== */
.hero {
  position: relative;
  padding: clamp(6.5rem, 14vw, 9rem) 0 clamp(4rem, 8vw, 6rem);
  overflow: hidden;
}

.hero-chart {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -10px;
  width: 100%;
  height: 130px;
  opacity: 0.5;
  pointer-events: none;
}

.hero-chart-teal polyline {
  fill: none;
  stroke: var(--teal);
  stroke-width: 1.4;
  opacity: 0.4;
}

.hero-chart-gold polyline {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.4;
  opacity: 0.5;
}

.hero-echo-chip {
  position: absolute;
  top: 8%;
  right: 6%;
  width: 130px;
  height: 96px;
  opacity: 0.5;
  pointer-events: none;
  display: none;
}

.hero-echo-base {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-md);
  border: 1px solid var(--line-strong);
  background: var(--paper-panel);
}

.hero-echo-glow {
  position: absolute;
  inset: -20px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(193, 58, 32, 0.14), transparent 70%);
}

.hero-echo-lid {
  position: absolute;
  inset: 6px 6px auto 6px;
  height: 34px;
  border-radius: var(--radius-sm);
  background: linear-gradient(150deg, var(--gold), #a6301a);
  transform-origin: 50% 100%;
}

.hero-inner {
  position: relative;
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--gutter);
  opacity: 0;
  /* Same "settling into focus" language as .reveal below — the very first
     thing on the page should set the tone for the same motion vocabulary
     everything else uses. */
  transform: translateY(16px) scale(0.97);
  filter: blur(5px);
  transition: opacity 0.9s var(--ease-out), transform 0.9s var(--ease-out), filter 0.9s var(--ease-out);
}

.hero-inner.is-revealed {
  opacity: 1;
  transform: none;
  filter: none;
}

.hero-top {
  display: flex;
  align-items: flex-end;
  gap: clamp(1.5rem, 4vw, 3rem);
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
  flex-wrap: wrap;
}

.hero-id-badge {
  position: relative;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
}

.photo-wrap {
  position: relative;
  /* Fluid instead of a fixed size + breakpoint jump — was 128px on
     desktop (small and disconnected next to a heading that can run
     80px+ tall) and only 104px on mobile (a token-sized photo sitting
     alone in a lot of empty space with nothing beside it). This scales
     smoothly and reads as a deliberately-sized photo at every width. */
  width: clamp(148px, 20vw, 190px);
  height: clamp(148px, 20vw, 190px);
  padding: 6px;
  background: var(--paper);
  border: 1.5px solid var(--ink);
  box-shadow: var(--shadow-md);
}

.photo-wrap::before,
.photo-wrap::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  border: 1.5px solid var(--gold);
}

.photo-wrap::before {
  top: -1.5px;
  left: -1.5px;
  border-right: none;
  border-bottom: none;
}

.photo-wrap::after {
  bottom: -1.5px;
  right: -1.5px;
  border-left: none;
  border-top: none;
}

@keyframes about-photo-pulse {
  0% { box-shadow: var(--shadow-md), 0 0 0 0 rgba(193, 58, 32, 0.45); }
  70% { box-shadow: var(--shadow-md), 0 0 0 20px rgba(193, 58, 32, 0); }
  100% { box-shadow: var(--shadow-md), 0 0 0 0 rgba(193, 58, 32, 0); }
}

.photo-wrap.is-pulsing {
  animation: about-photo-pulse 0.9s var(--ease-out);
}

.profile-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Default 50% 50% centered on the full head-to-chest composition,
     leaving the face small with a lot of empty shoulder/torso below it
     in a small square frame — biased up to crop tighter on the face
     (measured against the source photo's actual head position). */
  object-position: 50% 24%;
  filter: grayscale(0.15) contrast(1.03);
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  background: var(--paper-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 0.4rem 0.85rem;
  white-space: nowrap;
}

.status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--gold);
  box-shadow: 0 0 0 0 rgba(193, 58, 32, 0.5);
  animation: status-pulse 2s infinite;
}

@keyframes status-pulse {
  0% { box-shadow: 0 0 0 0 rgba(193, 58, 32, 0.5); }
  70% { box-shadow: 0 0 0 7px rgba(193, 58, 32, 0); }
  100% { box-shadow: 0 0 0 0 rgba(193, 58, 32, 0); }
}

.hero-heading h1 {
  font-size: clamp(2.75rem, 6.4vw, 5.2rem);
  line-height: 0.98;
}

.tagline {
  font-size: clamp(1rem, 1.6vw, 1.2rem);
  color: var(--ink-dim);
  margin: 0.6rem 0 0 0;
  /* The rotating word (see .tagline-rotate) types/erases character by
     character, and at narrow widths that constantly flips this line
     between wrapping to one line and two — reserving 2 lines' worth of
     height (line-height 1.6 × 2) up front stops the whole page below it
     from jumping up and down on every keystroke of the animation. */
  min-height: 3.2em;
}

.tagline-rotate {
  color: var(--gold);
  font-weight: 600;
}

.tagline-cursor {
  color: var(--gold);
  animation: caret-blink 1s step-end infinite;
}

@keyframes caret-blink {
  50% { opacity: 0; }
}

.hero-columns {
  display: block;
}

.hero-flow {
  position: relative;
  /* Semi-transparent + blurred instead of flat --paper-dim: the panel now
     picks up the skyline moving behind it, which ties the hero's one
     opaque block into the backdrop instead of stamping a grey rectangle
     over it. Falls back to a solid tint where backdrop-filter is absent. */
  background: var(--paper-dim);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-7);
  box-shadow: var(--shadow-raise);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .hero-flow {
    background: rgba(246, 247, 249, 0.82);
    backdrop-filter: blur(12px) saturate(1.3);
    -webkit-backdrop-filter: blur(12px) saturate(1.3);
  }
}

.hero-config-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: var(--space-4);
}

.flow-grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.flow-node {
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  /* 0.65/0.7rem were near-identical but not equal, so the nodes' inner
     text sat a pixel off-centre horizontally versus vertically. */
  padding: var(--space-3);
  box-shadow: var(--shadow-md);
}

.flow-node-bio {
  grid-column: 1;
}

.flow-node-actions {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.3rem;
}

.flow-actions-hint {
  margin: 0 0 0.5rem 0;
  font-size: 0.78rem;
  color: var(--ink-faint);
}

.btn-arrow {
  display: inline-block;
  transition: transform 0.25s var(--ease-out);
}

.btn:hover .btn-arrow {
  transform: translateX(3px);
}

.flow-bio-text {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--ink-dim);
  margin: 0;
}

.flow-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.flow-node-actions .btn {
  width: 100%;
  justify-content: space-between;
  padding: 0.7rem 1rem;
  font-size: 0.7rem;
}

.flow-settings {
  grid-column: 1 / -1;
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}

.flow-settings .flow-node {
  flex: 1 1 140px;
}

.config-node-title {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.5rem;
}

.config-node-options {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.config-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  text-align: left;
  font-family: var(--font-sans);
  font-size: 0.76rem;
  color: var(--ink-dim);
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  /* Was 0.3rem 0.4rem (~28px tall). WCAG 2.5.8 sets a 24px floor even for
     a mouse; this clears it with room and stays compact on desktop, while
     the pointer:coarse block takes it to a full 44px on touch. */
  padding: var(--space-2) var(--space-2);
  min-height: 32px;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}

.config-option::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 1.5px solid var(--line-ui);
  flex-shrink: 0;
  transition: border-color var(--transition), background var(--transition), transform var(--transition);
}

/* The radio dot itself acknowledges the press — the only moving part
   small enough to read as a real switch being thrown. */
.config-option:active::before {
  transform: scale(0.85);
}

/* A brief "light switching on" glow the moment a pick becomes active —
   the deselected option's dot already fades out smoothly via the
   transition above, this is the "on" half. .is-flashing is added by JS
   right when the click is handled and removed once the animation ends. */
@keyframes option-light-on {
  0% { box-shadow: 0 0 0 0 rgba(193, 58, 32, 0.55); }
  60% { box-shadow: 0 0 0 9px rgba(193, 58, 32, 0); }
  100% { box-shadow: 0 0 0 0 rgba(193, 58, 32, 0); }
}

.config-option.is-flashing::before {
  animation: option-light-on 0.55s ease-out;
}

.config-option:hover,
.config-option:focus-visible {
  background: var(--paper-panel);
  color: var(--ink);
}

.config-option.is-active {
  color: var(--ink);
  font-weight: 600;
}

.config-option.is-active::before {
  border-color: var(--gold);
  background: var(--gold);
}

.config-wires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  overflow: visible;
  pointer-events: none;
}

.config-wire {
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1;
  stroke-dasharray: 3 4;
}

.config-wire-dot {
  fill: var(--gold);
  filter: drop-shadow(0 0 4px var(--gold-bright));
}

.config-output {
  position: relative;
  z-index: 2;
  /* Distinguished from the four INPUT cards above (Bio/Actions/Focus/
     Sector — all plain --paper, --shadow-md, --line-strong all round)
     with a tinted background, an accent top edge, and a stronger shadow.
     This is the one card that's an OUTPUT — whatever gets picked above
     resolves here — and looking identical in weight to the controls
     feeding it undersold that "here's your answer" moment, especially on
     mobile where all five cards just stack in one plain column with
     nothing to set the result apart from the inputs. */
  background: var(--paper-dim);
  border: 1px solid var(--line-strong);
  border-top: 2px solid var(--gold);
  border-radius: var(--radius-md);
  /* Snapped from 1.1rem/1.2rem — near-equal values that read as a
     mistake rather than a decision. Equal padding all round. */
  padding: var(--space-5);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.config-output::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, 0.55) 48%, transparent 62%);
  transform: translateX(-120%);
  animation: cc-sweep 6s ease-in-out infinite;
  pointer-events: none;
}

.config-output-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  color: var(--gold);
  margin-bottom: 0.7rem;
}

.config-output-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--gold);
  animation: status-pulse 2s infinite;
}

.config-output-headline {
  font-family: var(--font-serif);
  font-size: 1.1rem;
  margin: 0 0 0.5rem 0;
  color: var(--ink);
}

.config-output-blurb {
  font-size: 0.86rem;
  color: var(--ink-dim);
  margin: 0 0 0.9rem 0;
}

.config-output-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.9rem;
}

.config-chip {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  background: var(--paper-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  padding: 0.25rem 0.6rem;
}

.config-output-meta {
  font-size: 0.72rem;
  color: var(--ink-faint);
  margin: 0;
  padding-top: 0.8rem;
  border-top: 1px dashed var(--line-strong);
}

.scroll-cue {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  width: max-content;
  /* Was a ~30px-tall text link. Padding (rather than a fixed height) keeps
     the label optically where it was while giving it a real 44px target. */
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  margin: clamp(3rem, 7vw, 4.5rem) auto 0;
  text-decoration: none;
  color: var(--ink-faint);
  transition: color var(--transition), transform var(--transition);
}

.scroll-cue:hover,
.scroll-cue:focus-visible {
  color: var(--ink);
}

.scroll-cue:hover {
  transform: translateY(2px);
}

.scroll-cue-text {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.2em;
}

.scroll-cue-arrow {
  font-size: 1rem;
  animation: scroll-cue-bounce 1.8s ease-in-out infinite;
}

@keyframes scroll-cue-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(6px); }
}

/* =====================================================================
   KPI strip
   ===================================================================== */
.kpi-strip {
  padding: clamp(2.5rem, 5vw, 3.5rem) 0;
  background: var(--ink);
}

.kpi-strip .eyebrow {
  color: var(--gold-bright);
}

.ticker-board {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 1px;
  background: rgba(251, 249, 244, 0.1);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.ticker-cell {
  background: var(--card-ink);
  padding: var(--space-6) var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.ticker-arrow {
  color: var(--gold-bright);
  font-size: 0.75rem;
}

.kpi-number {
  font-family: var(--font-serif);
  font-size: clamp(1.8rem, 3vw, 2.4rem);
  color: var(--paper);
}

.kpi-label {
  font-size: 0.78rem;
  color: rgba(251, 249, 244, 0.55);
  line-height: 1.35;
}

.kpi-bar {
  display: block;
  height: 2px;
  background: rgba(251, 249, 244, 0.12);
  margin-top: 0.5rem;
  border-radius: 2px;
  overflow: hidden;
}

.kpi-bar-fill {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--gold-bright);
}

.ticker-dots {
  display: none;
  justify-content: center;
  /* gap:0 — each dot is now a 24px (44px on touch) button that carries its
     own spacing internally, so the visible dots stay evenly pitched while
     their hit areas sit edge to edge instead of overlapping. */
  gap: 0;
  margin-top: var(--space-4);
}

/* Shared pagination-dot shape. The button is the target; the ::before is
   the dot you actually see, so the visual size and the touch size are
   decoupled. Previously these were 6px <span>s with click handlers and a
   -19px ::after hit area — which made every target 44px wide on a 12.8px
   pitch, so adjacent targets overlapped by ~70% and the last dot in the
   row swallowed taps meant for its neighbours. */
.ticker-dot,
.career-dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: none;
  padding: 0;
  background: transparent;
  border-radius: 50%;
  cursor: pointer;
  transition: transform var(--transition);
}

.ticker-dot::before,
.career-dot::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  transition: background var(--transition), transform var(--transition);
}

.ticker-dot:hover::before,
.career-dot:hover::before {
  transform: scale(1.35);
}

.ticker-dot:active,
.career-dot:active {
  transform: scale(0.9);
}

.ticker-dot::before {
  background: rgba(251, 249, 244, 0.35);
}

.ticker-dot.is-active::before {
  background: var(--gold-bright);
  transform: scale(1.2);
}

/* =====================================================================
   Experience — career explorer (drag / zoom / wire mechanic preserved
   from script.js; this is a visual re-skin only)
   ===================================================================== */
.career-live-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-left: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  color: var(--gold);
}

.career-live-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--gold);
  animation: status-pulse 2s infinite;
}

.career-explorer {
  position: relative;
}

.career-edge-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: 2;
}

/* Sized to .career-canvas (its parent), not the whole explorer — see the
   HTML comment above it. z-index: -1 paints it above .career-canvas's OWN
   background (negative z-index always clears its stacking context's own
   background/border) but below every normal-flow and z-index:auto/positive
   descendant in that same context — which is every other thing in here:
   .career-canvas-scroll, .career-nav, .career-node, .career-nav-scroll,
   .career-controls (z-index:6). That ordering is enforced by the negative
   number itself, not by this element coming first in the markup — so even
   if a future edit reorders these children, the line stays tucked behind
   the nodes and in front of the tray. It only needs to beat its own
   local siblings; .career-canvas's own z-index:3 (below) is what already
   handles its standing against elements OUTSIDE this box. */
.career-chain-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  z-index: -1;
}

.career-chain-edge {
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1;
  stroke-dasharray: 3 4;
}

.career-edge-path {
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.5;
}

.career-edge-dot {
  fill: var(--gold);
}

.career-node-toolbar {
  position: absolute;
  z-index: 5;
  transform: translate(-50%, calc(-100% - 14px));
  background: var(--ink);
  color: var(--paper);
  border-radius: var(--radius-md);
  padding: 0.55rem 0.8rem;
  min-width: 160px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s;
  box-shadow: var(--shadow-md);
}

.career-node-toolbar.is-visible {
  opacity: 1;
}

.career-node-toolbar-title {
  display: block;
  font-family: var(--font-serif);
  font-size: 0.88rem;
}

.career-node-toolbar-meta {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: rgba(251, 249, 244, 0.6);
  margin-top: 0.15rem;
}

.career-node-toolbar-arrow {
  position: absolute;
  bottom: -5px;
  left: 50%;
  transform: translateX(-50%) rotate(45deg);
  width: 9px;
  height: 9px;
  background: var(--ink);
}

.career-canvas {
  position: relative;
  /* Sits above .career-edge-svg (z-index 2, now just the one dynamic wire
     to the output panel) as a WHOLE UNIT, which is the part the original
     node-level fix (z-index:3 on .career-node, still below) missed:
     .career-nav has its own transform for the zoom feature, and
     .career-canvas itself picks up a leftover transform matrix from the
     scrollReveal() GSAP tween below (it animates scale, and never clears
     the property once settled). Either one alone already creates a
     stacking context, and a stacking context TRAPS a descendant's
     z-index — .career-node's z-index:3 only wins fights inside
     .career-nav, never against a sibling svg sitting outside this box.
     Only an explicit z-index on the ancestor that actually competes with
     that svg fixes it, at every zoom level and whether or not GSAP has
     touched this element yet. The static per-row chain (.career-chain-svg)
     is a DIFFERENT fix for a DIFFERENT reason: it's nested inside this
     box specifically so it CAN'T be resolved this way — see its own rule
     for why raising the whole box, background included, would just hide
     the chain instead of layering it correctly. */
  z-index: 3;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--paper-panel);
  padding: var(--space-5) 0;
  /* An inset top highlight reads as a recessed tray the nodes sit inside,
     which is what the drag/zoom interaction implies — it was previously a
     flat grey rectangle with no indication it was a surface at all. */
  box-shadow: inset 0 1px 2px rgba(15, 17, 21, 0.06), inset 0 0 0 1px rgba(255, 255, 255, 0.5);
}

/* Mobile-only "there's more to swipe" hint for the role row — desktop
   shows all six nodes on a draggable canvas instead, no hint needed. */
.career-nav-scroll {
  display: none;
}

/* Generic "there's more to swipe" cue — used for the role-chip row, the
   role card carousel, and the KPI strip, all independently (each has its
   own show/hide logic in script.js, since they're separate scroll
   containers). Renamed from .career-swipe-cue once a second, unrelated
   section started reusing it verbatim — the rule itself was always
   generic (flex row + typography, nothing career-specific in it), the
   name just hadn't caught up. */
.swipe-cue {
  display: none;
}

/* The section intro's two alternate sentences (see index.html) — only
   one is ever shown. Default state is desktop's, flipped below 820px. */
.career-sub-mobile {
  display: none;
}

@keyframes career-nav-hint-nudge {
  0%,
  100% {
    transform: translateX(0);
  }
  50% {
    transform: translateX(4px);
  }
}

.career-canvas-scroll {
  overflow: auto;
  scrollbar-width: thin;
}

.career-controls {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  gap: var(--space-2);
  z-index: 6;
}

.career-control-btn {
  /* Was 28px — under WCAG 2.5.8's 24px floor once you account for the
     1px border, and well under a comfortable target. 32px on a mouse,
     44px on touch (see the pointer:coarse block). */
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-ui);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1;
  transition: background var(--transition), border-color var(--transition),
    color var(--transition), transform var(--transition);
}

.career-control-btn:hover,
.career-control-btn:focus-visible {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--paper);
}

.career-control-btn:active {
  transform: scale(0.92);
}

.career-nav {
  position: relative;
  width: 880px;
  height: 170px;
  transform-origin: 0 0;
  transform: scale(var(--career-zoom, 1));
}

.career-node {
  position: absolute;
  /* Above .career-edge-svg (z-index 2). Both are positioned, so without
     this the connector lines paint over the node cards and their logos. */
  z-index: 3;
  width: 128px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  padding: 0.7rem 0.6rem;
  cursor: grab;
  transition: border-color var(--transition), box-shadow var(--transition),
    background var(--transition), transform var(--transition);
  --accent: var(--gold);
}

.career-node:active,
.career-node.is-dragging {
  cursor: grabbing;
}

.career-node:hover,
.career-node:focus-visible {
  border-color: var(--accent);
  /* Lifting on hover tells you the node is draggable before you try it —
     a border colour change alone read as "link", not "handle". */
  box-shadow: var(--shadow-md);
}

/* Not on .is-dragging: the node is already following the pointer there,
   and a second transform would fight the left/top the drag handler sets. */
.career-node:hover:not(.is-dragging) {
  transform: translateY(-2px);
}

/* Same specificity as the hover rule above, declared after it, so a press
   (:active fires together with :hover on a normal click) replaces the
   lift with a press-scale rather than the two fighting silently. Skipped
   during an actual drag for the same reason the lift is: JS already owns
   this element's position via left/top at that point. */
.career-node:active:not(.is-dragging) {
  transform: scale(0.96);
  transition-duration: 0.1s;
}

.career-node.is-active {
  background: var(--ink);
  border-color: var(--ink);
  box-shadow: var(--shadow-md);
}

.career-node-handle {
  display: none;
}

.career-node-logo {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--paper-panel-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3px;
  overflow: hidden;
}

.career-node-logo.logo-dark {
  background: var(--ink);
}

.career-node-logo.logo-dark img,
.career-panel-logo.logo-dark img {
  object-fit: cover;
  object-position: left center;
}

.career-node-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.career-node-code {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.08em;
  color: var(--ink-dim);
}

.career-node.is-active .career-node-code {
  color: var(--paper);
}

.career-panel {
  position: relative;
  /* Same fix, same reason as .career-canvas above: this also gets a
     transform (the persistent 3D tilt set in script.js, plus the same
     GSAP reveal tween), which traps its z-index and lets the connector
     wire's tip draw over its top edge instead of tucking behind it. */
  z-index: 3;
  max-width: 560px;
  margin: var(--space-4) auto 0;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  /* 1.4/1.5rem snapped to one equal value — the 1.7px difference was
     invisible as intent but visible as a slightly off-centre card. */
  padding: var(--space-6);
  --accent: var(--gold);
  box-shadow: var(--shadow-lg);
}

.career-panel-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.14em;
  color: var(--accent);
  margin-bottom: 0.6rem;
}

.career-panel-tag-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
}

.career-panel-handle {
  position: absolute;
  top: -1px;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
}

.career-panel-loadbar {
  height: 2px;
  width: 0;
  background: var(--accent);
  margin-bottom: 1rem;
}

.career-panel-loadbar.is-loading {
  animation: loadbar-run 0.7s var(--ease-out);
}

@keyframes loadbar-run {
  from { width: 0; }
  to { width: 100%; }
}

.career-panel-progress {
  display: none;
}

.career-panel-item {
  position: relative;
  display: none;
  overflow: hidden;
}

.career-panel-item.is-active {
  display: block;
  animation: panel-fade-in 0.5s var(--ease-out);
}

@keyframes panel-fade-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.career-panel-brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

.career-panel-logo {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--paper-panel-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  overflow: hidden;
}

.career-panel-logo.logo-dark {
  background: var(--ink);
}

.career-panel-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.career-panel-code {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  color: var(--accent);
}

.career-panel-item h3 {
  font-size: 1.25rem;
  margin-bottom: var(--space-1);
}

.career-panel-meta {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--ink-faint);
  margin: 0 0 var(--space-4) 0;
  /* Job title + employer + dates is one identity block; letting the meta
     line wrap mid-place-name broke that reading. */
  line-height: 1.5;
}

.career-panel-stats {
  display: flex;
  gap: 1.4rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}

.career-stat {
  display: flex;
  flex-direction: column;
}

.career-stat-value {
  font-family: var(--font-serif);
  font-size: 1.7rem;
  color: var(--accent);
}

.career-stat-label {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.career-panel-text {
  color: var(--ink-dim);
  max-width: 62ch;
}

.career-text-chunk {
  display: block;
  margin-bottom: 0.85em;
}

.career-text-chunk:last-child {
  margin-bottom: 0;
}

.job-gallery {
  display: flex;
  gap: 0.6rem;
  margin-top: 1.2rem;
  flex-wrap: wrap;
}

.job-gallery:empty {
  display: none;
}

.job-gallery img {
  width: 76px;
  height: 76px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
}

.career-dots {
  display: none;
  justify-content: center;
  gap: 0;
  margin-top: var(--space-5);
}

.career-dot::before {
  background: var(--line-ui);
}

.career-dot.is-active::before {
  background: var(--gold);
  transform: scale(1.2);
}

/* =====================================================================
   Skills — editorial index (replaces the trading-watchlist look; tab
   filter + data-cat plumbing in script.js is untouched)
   ===================================================================== */
.trade-terminal {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--paper);
  box-shadow: var(--shadow-md);
}

/* The terminal's four bands (topbar, ticker, tabs, table) each had their
   own horizontal padding — 1.4rem in three places and 1.4rem-ish in the
   fourth — so the left edges of the title, the ticker text, the tabs and
   the row numbers didn't line up. One shared gutter aligns all four. */
.trade-topbar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-6);
  background: var(--ink);
  color: var(--paper);
}

.terminal-dots {
  display: none;
}

.trade-topbar-title {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
}

.trade-holdings {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.08em;
  color: var(--gold-bright);
  border: 1px solid rgba(255, 107, 61, 0.4);
  border-radius: var(--radius-pill);
  padding: 0.2rem 0.65rem;
}

.trade-ticker {
  border-bottom: 1px solid var(--line);
  background: var(--paper-dim);
  overflow: hidden;
}

.trade-ticker-track {
  display: inline-flex;
  gap: var(--space-6);
  padding: var(--space-2) var(--space-6);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  animation: scroll-left 40s linear infinite;
  white-space: nowrap;
}

.trade-tabs {
  display: flex;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-6) 0;
  flex-wrap: wrap;
}

.trade-tab {
  --accent: var(--gold);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-pill);
  border: 1px solid var(--line-ui);
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition),
    color var(--transition), transform var(--transition);
}

.trade-tab:hover,
.trade-tab:focus-visible {
  border-color: var(--accent);
  color: var(--ink);
  background: var(--paper-panel);
}

.trade-tab:active {
  transform: translateY(1px);
}

.trade-tab.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--paper);
}

/* Without this the active tab's own hover rule (above) repainted it with
   the inactive panel background mid-hover — the selected tab appeared to
   deselect itself under the cursor. */
.trade-tab.is-active:hover,
.trade-tab.is-active:focus-visible {
  background: var(--accent);
  color: var(--paper);
}

.trade-table {
  /* .trade-row's -0.5rem side margin is cancelled exactly by its own
     0.5rem side padding, so row content still begins on this gutter —
     the same 1.5rem as the topbar title and the tab row. Only the hover
     wash extends past it, which is the point. */
  padding: var(--space-2) var(--space-6) var(--space-6);
  counter-reset: skill-row;
}

.trade-row {
  --accent: var(--gold);
  display: none;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--space-2);
  /* Pulls the row's own padding back out to the table's edge so the
     hover wash below spans the full width instead of insetting. */
  margin: 0 calc(var(--space-2) * -1);
  /* No border-radius here on purpose: a rounded box with a border-bottom
     curves the divider up at both ends into a shallow smile. */
  border-bottom: 1px solid var(--line);
  counter-increment: skill-row;
  transition: background var(--transition);
}

/* An index of 17 rows with no hover feedback gave no sense of where the
   eye was. A near-invisible wash is enough to track a line across. */
.trade-row:hover {
  background: var(--paper-dim);
}

.trade-row.is-visible {
  display: flex;
  animation: panel-fade-in 0.4s var(--ease-out);
}

.trade-row:last-child {
  border-bottom: none;
}

.trade-row::before {
  content: counter(skill-row, decimal-leading-zero);
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--ink-faint);
  width: 1.6em;
  flex-shrink: 0;
}

.trade-symbol {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--accent);
  width: 4.2em;
  flex-shrink: 0;
}

.trade-name {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: 1.02rem;
  color: var(--ink);
  flex: 1;
}

.trade-spark-wrap {
  display: none;
}

.trade-trend {
  display: none;
}

/* =====================================================================
   Achievements
   ===================================================================== */
.ach-flow {
  position: relative;
  height: 46px;
  max-width: 420px;
  margin: 0 auto 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.ach-flow-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.ach-flow-edge {
  fill: none;
  stroke: var(--line-strong);
  stroke-width: 1;
  stroke-dasharray: 3 4;
}

.ach-flow-node {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--accent);
  z-index: 1;
  transition: transform 0.3s, box-shadow 0.3s;
}

.ach-flow-node.is-journey-current {
  transform: scale(1.5);
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 18%, transparent);
}

.ach-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}

.ach-card {
  --accent: var(--gold);
  position: relative;
  text-align: left;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-7) var(--space-6) var(--space-6);
  font-family: inherit;
  transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition);
  will-change: transform;
  overflow: hidden;
}

/* Compresses very slightly on press — the whole card is one stretched
   button now (see .ach-card-btn below), so this is the tactile "I heard
   you" feedback for what reads, on a touchscreen, as tapping a physical
   object. Harmless where the desktop pinned journey (script.js) is also
   driving this same transform via GSAP: that writes a real inline style,
   which always outranks a stylesheet rule regardless of :active, so the
   two never actually fight — this just has no visible effect while the
   journey owns the card, same as it would anywhere an inline style wins. */
.ach-card:active {
  transform: scale(0.98);
  transition-duration: 0.1s;
}

/* The card is a <div> now, not a <button>: it contained an <h3> and two
   <p>s, which are flow content and are not valid inside a button's
   phrasing-only content model — and it flattened the whole card into one
   run-on accessible name. The real control is the button wrapped in the
   h3, stretched over the card face by the ::after below. That keeps the
   entire card clickable, keeps the three h3s in the document outline, and
   reduces the accessible name to just the milestone title. */
.ach-card-btn {
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  background: none;
  border: 0;
  padding: 0;
  text-align: left;
  cursor: pointer;
}

.ach-card-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* The stretched button has no box of its own to ring, so the focus ring
   is drawn on the card. :has() is the only way to reach the parent. */
.ach-card-btn:focus-visible {
  outline: none;
  box-shadow: none;
}

.ach-card:has(.ach-card-btn:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  box-shadow: 0 0 0 2px var(--focus-halo);
  border-color: var(--accent);
}

.ach-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, 0.5) 48%, transparent 62%);
  transform: translateX(-120%);
  animation: cc-sweep 7s ease-in-out infinite;
  pointer-events: none;
}

.ach-card:nth-child(2)::before {
  animation-delay: 1.4s;
}

.ach-card:nth-child(3)::before {
  animation-delay: 2.8s;
}

.ach-card:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-md);
}

.ach-card::after {
  content: "\2713";
  position: absolute;
  /* Paints above the stretched .ach-card-btn::after, so without this the
     badge would swallow clicks in the card's top-right corner — a dead
     30px patch that used to be clickable when the card itself was the
     button. Decorative only; it must never take the pointer. */
  pointer-events: none;
  top: var(--space-4);
  right: var(--space-4);
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  opacity: 0.5;
  transform: rotate(10deg);
}

.ach-card-year {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  color: var(--accent);
  margin-bottom: 0.75rem;
}

.ach-card-name {
  font-size: 1.2rem;
  margin: 0 0 0.5rem 0;
  padding-right: 2rem;
}

.ach-card-meta {
  font-size: 0.85rem;
  color: var(--ink-faint);
  margin: 0 0 1.3rem 0;
}

.ach-card-hint {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  transition: color var(--transition);
}

.ach-card-hint span {
  transition: transform var(--transition);
}

.ach-card:hover .ach-card-hint,
.ach-card:has(.ach-card-btn:focus-visible) .ach-card-hint {
  color: var(--accent);
}

.ach-card:hover .ach-card-hint span,
.ach-card:has(.ach-card-btn:focus-visible) .ach-card-hint span {
  transform: translateX(4px);
}

.ach-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: rgba(15, 17, 21, 0.5);
  backdrop-filter: blur(3px);
  opacity: 0;
  pointer-events: none;
  /* visibility, not just opacity: an opacity-0 overlay is still in the
     accessibility tree and its close button stays in the tab order, so
     keyboard and screen-reader users would land inside an invisible
     dialog (whose title is empty until JS fills it on open). Transitioned
     with a delay so the fade-out still plays before it's hidden. */
  visibility: hidden;
  transition: opacity 0.3s, visibility 0s linear 0.3s;
}

.ach-modal-overlay.is-open {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition: opacity 0.3s, visibility 0s;
}

.ach-modal {
  --accent: var(--gold);
  position: relative;
  max-width: 480px;
  width: 100%;
  background: var(--paper);
  border-radius: var(--radius-lg);
  padding: clamp(2rem, 5vw, 3rem);
  box-shadow: var(--shadow-lg);
  transform: scale(0.94) translateY(10px);
  transition: transform 0.35s var(--ease-out);
}

.ach-modal-overlay.is-open .ach-modal {
  transform: scale(1) translateY(0);
}

.ach-modal-close {
  position: absolute;
  top: var(--space-4);
  right: var(--space-4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1px solid var(--line-ui);
  background: transparent;
  color: var(--ink-dim);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  /* The only control in the dialog, and it had no transition at all —
     the one element where a state change most needs to be legible. */
  transition: background var(--transition), border-color var(--transition),
    color var(--transition), transform var(--transition);
}

.ach-modal-close:hover,
.ach-modal-close:focus-visible {
  border-color: var(--ink);
  background: var(--paper-panel);
  color: var(--ink);
}

.ach-modal-close:active {
  transform: scale(0.92);
}

.ach-modal-year {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  border: 2px solid var(--accent);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: 0.95rem;
  margin-bottom: 1.2rem;
  transform: scale(0.7) translateY(8px);
  opacity: 0;
}

.ach-modal-overlay.is-open .ach-modal-year {
  animation: year-in 0.4s 0.1s var(--ease-out) forwards;
}

@keyframes year-in {
  to { transform: scale(1) translateY(0); opacity: 1; }
}

.ach-modal-title {
  font-size: clamp(1.4rem, 3vw, 1.8rem);
  margin-bottom: 0.5rem;
}

.ach-modal-meta {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: var(--ink-faint);
  margin: 0 0 1.3rem 0;
}

.ach-modal-detail {
  color: var(--ink-dim);
  font-size: 1.02rem;
  margin: 0;
}

/* =====================================================================
   Education
   ===================================================================== */
.education-list {
  position: relative;
  padding-left: 2.2rem;
}

.education-rail {
  position: absolute;
  left: 6px;
  top: 6px;
  bottom: 6px;
  width: 2px;
  background: var(--line-strong);
}

.education-rail-fill {
  display: block;
  width: 100%;
  height: 0%;
  background: var(--gold);
}

.education-item {
  position: relative;
  --accent: var(--gold);
  display: flex;
  align-items: center;
  gap: var(--space-5);
  padding: var(--space-6) 0;
  border-bottom: 1px solid var(--line);
}

.education-item:last-child {
  border-bottom: none;
}

.education-dot {
  position: absolute;
  left: -2.2rem;
  top: 50%;
  transform: translateY(-50%);
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--paper);
  border: 2px solid var(--line-strong);
  transition: border-color 0.4s, background 0.4s;
}

.education-item.is-reached .education-dot {
  border-color: var(--accent);
  background: var(--accent);
}

.education-logo {
  flex-shrink: 0;
  width: 46px;
  height: 46px;
  border-radius: var(--radius-sm);
  background: var(--paper-panel);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 7px;
}

.education-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.education-info {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1;
}

.education-degree {
  font-family: var(--font-serif);
  font-size: 1.08rem;
  font-weight: 700;
  /* It's a heading in everything but tag name; body's 1.6 leading left a
     wrapped degree title looking like two unrelated lines, and the
     regular weight gave it no more emphasis than the school below it. */
  line-height: 1.3;
  letter-spacing: -0.012em;
  color: var(--ink);
}

.education-school {
  font-size: 0.88rem;
  color: var(--ink-dim);
  line-height: 1.45;
}

.education-detail {
  font-size: 0.82rem;
  color: var(--ink-faint);
  line-height: 1.5;
  margin-top: var(--space-1);
}

.education-date {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* =====================================================================
   Contact — the credit card is the one other deliberately dark object,
   like a product photograph on a light backdrop
   ===================================================================== */
.contact-inner {
  text-align: center;
}

.contact-intro {
  max-width: 46ch;
  /* 2.25rem was the odd one out: every other section leaves --space-10
     between its header block and its content, so Contact's stage started
     ~4px higher than the rest for no reason. Same gap everywhere now. */
  margin: 0 auto var(--space-10);
  color: var(--ink-dim);
  line-height: 1.55;
}

#contact {
  position: relative;
  /* No overflow:hidden — it would make this a scroll container and
     silently break the position:sticky reveal inside it. #contact::before
     is a soft radial that fades out well before its own edges anyway. */

  /* The sticky frame already centres the card in a full viewport, so a
     section pad below it was ~100px of dead scroll after the reveal had
     finished — and it pushed the card up out of frame at the page's very
     bottom. The frame is the breathing room. */
  padding-bottom: 0;
}

/* Sits behind the heading, not the card — the section is several screens
   tall now, so a percentage-positioned glow would drift far down it. */
#contact::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 220px;
  width: 60vw;
  height: 60vw;
  max-width: 720px;
  max-height: 720px;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(193, 58, 32, 0.14), transparent 68%);
  pointer-events: none;
}

/* Gives the reveal its scroll distance. Shorter than the reference spec's
   220vh: the reveal reads as complete well before then, and the extra was
   just empty scrolling. */
.cc-scroll {
  position: relative;
  height: 150vh;
}

/* Holds the card still mid-viewport while .cc-scroll passes behind it. */
.cc-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.25rem, 3vh, 1.9rem);
  overflow: hidden;
}

/* Backdrop for the card: a blueprint grid fading out toward the edges,
   plus a warm pool of light behind the card itself. Without these the
   card sat alone in a blank viewport — the space around it now reads as
   a deliberate backdrop rather than an empty page. */
.cc-sticky::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: radial-gradient(ellipse 62% 58% at 50% 46%, #000 8%, transparent 72%);
  mask-image: radial-gradient(ellipse 62% 58% at 50% 46%, #000 8%, transparent 72%);
  pointer-events: none;
}

.cc-sticky::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 46%;
  width: min(78vw, 760px);
  height: min(78vw, 760px);
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(193, 58, 32, 0.13), rgba(193, 58, 32, 0.05) 42%, transparent 70%);
  pointer-events: none;
}

/* Keeps the card and its pocket above the backdrop layers. */
.cc-wrap {
  position: relative;
  z-index: 1;
  width: clamp(320px, 44vw, 460px);
  height: clamp(390px, 46vh, 470px);
  margin: 0 auto;
}

/* The pocket sits IN FRONT of the card (higher z-index) and is what
   genuinely occludes it while tucked — no clip-path or opacity trick.
   Kept in the site's cool dark palette rather than the reference's
   leather brown; the stripe texture and dashed stitching carry over. */
.cc-wallet {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: clamp(150px, 19vh, 190px);
  border-radius: var(--radius-lg);
  background: linear-gradient(155deg, #24272e 0%, #16181d 65%, #0e0f13 100%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 24px 48px -18px rgba(8, 9, 12, 0.5);
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
}

.cc-wallet-texture {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 2px, transparent 2px, transparent 9px);
}

.cc-wallet-border {
  position: absolute;
  inset: 9px;
  border-radius: calc(var(--radius-lg) - 6px);
  border: 2px dashed rgba(255, 255, 255, 0.2);
}

/* The stitch line at the pocket's lip — the edge the card passes under. */
.cc-wallet-stitch {
  position: absolute;
  left: 16%;
  right: 16%;
  top: 18px;
  height: 0;
  border-top: 2px dashed rgba(255, 255, 255, 0.32);
}

.credit-card {
  position: absolute;
  left: 50%;
  /* 24px, not the reference's 18px: the wider card here dips further at
     its -4deg rest tilt, and this is the margin that keeps its bottom
     corner inside the pocket. See CARD_TUCK_Y in script.js. */
  bottom: 24px;
  width: clamp(290px, 39vw, 400px);
  z-index: 1;
  aspect-ratio: 1.586 / 1;
  border-radius: var(--radius-lg);
  /* Smooths the hover tilt only; scroll updates are already continuous,
     and a longer duration would visibly lag the card behind the scroll. */
  transition: transform 0.15s ease-out;
  padding: clamp(1.4rem, 3.6vw, 1.9rem) clamp(2rem, 4.8vw, 2.5rem) clamp(1.4rem, 3.6vw, 1.9rem) clamp(1.4rem, 3.6vw, 1.9rem);
  background:
    radial-gradient(circle at 85% 10%, rgba(255, 107, 61, 0.14), transparent 55%),
    linear-gradient(150deg, var(--card-ink-2) 0%, var(--card-ink) 45%, #06070a 100%);
  color: var(--paper);
  /* .contact-inner sets text-align:center for the section around this
     card (heading, intro paragraph) — without overriding it here, every
     line of card content inherited that centering instead of reading
     left-aligned like an actual card. */
  text-align: left;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
}

/* Plain opacity, never mix-blend-mode. A blend mode here, inside the
   position:sticky + nested-transform ancestor chain above, is what made
   the whole card black out mid-scroll on some GPUs previously. */
.cc-shine {
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, 0.35) 45%, transparent 60%);
  background-size: 250% 250%;
  animation: shineSweep 4s ease-in-out infinite;
  opacity: 0.12;
  pointer-events: none;
}

@keyframes shineSweep {
  0% { background-position: 130% 130%; }
  50% { background-position: -10% -10%; }
  100% { background-position: 130% 130%; }
}

/* Used by .config-output and .ach-card::before, not by the card. */
@keyframes cc-sweep {
  0%, 40% { transform: translateX(-120%); }
  60%, 100% { transform: translateX(120%); }
}

.cc-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  position: relative;
  z-index: 1;
}

.cc-chip {
  width: 42px;
  height: 31px;
  border-radius: 6px;
  background: linear-gradient(135deg, var(--gold-bright), var(--rose));
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 1px;
  padding: 5px;
  flex-shrink: 0;
}

.cc-chip span {
  border-right: 1px solid rgba(0, 0, 0, 0.25);
  border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}

.cc-chip span:nth-child(2n) {
  border-right: none;
}

.cc-chip span:nth-child(n + 3) {
  border-bottom: none;
}

.cc-linkedin {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--paper);
  color: var(--card-ink);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  flex-shrink: 0;
  transition: transform 0.2s var(--ease-out), background 0.2s, color 0.2s;
}

.cc-linkedin svg {
  width: 16px;
  height: 16px;
}

.cc-linkedin:hover,
.cc-linkedin:focus-visible {
  background: var(--gold-bright);
  color: var(--paper);
  transform: scale(1.08);
}

.cc-mid {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.cc-title {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: rgba(251, 249, 244, 0.82);
}

.cc-number {
  font-family: var(--font-mono);
  font-size: clamp(1.05rem, 2.6vw, 1.3rem);
  letter-spacing: 0.06em;
  color: var(--paper);
  text-decoration: none;
  width: max-content;
}

/* Runs along the right edge, clear of the padding used by every other
   row — decorative only (aria-hidden, the same fact already sits in the
   Education section), so a small size is fine here same as the magnetic
   badge's orbiting text elsewhere on this page. */
.cc-side-label {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%) rotate(180deg);
  writing-mode: vertical-rl;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  /* 0.32 composited over the card's darkest gradient stop is only ~2.8:1;
     0.55 clears 4.5:1 while still reading as a quiet side marking. */
  color: rgba(251, 249, 244, 0.55);
  white-space: nowrap;
  z-index: 1;
  pointer-events: none;
}

.cc-bottom-group {
  position: relative;
  z-index: 1;
}

.cc-divider {
  display: block;
  height: 2px;
  background: linear-gradient(90deg, var(--gold), transparent);
  border-radius: 2px;
  margin-bottom: 0.7rem;
}

.cc-bottom {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.cc-label {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.14em;
  color: rgba(251, 249, 244, 0.62);
}

.cc-holder {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  letter-spacing: 0.03em;
  color: var(--paper);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}

/* The two live links on the card had an instant colour swap and no
   underline — nothing said "this is a link" until you hovered it, and
   even then it snapped. */
.cc-number,
.cc-holder {
  transition: color var(--transition), text-decoration-color var(--transition);
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 0.22em;
  text-decoration-thickness: 1px;
}

.cc-number:hover,
.cc-number:focus-visible,
.cc-holder:hover,
.cc-holder:focus-visible {
  color: var(--gold-bright);
  text-decoration-color: currentColor;
}

.cc-security-ticker {
  width: min(100% - 2 * var(--gutter), 420px);
  overflow: hidden;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.cc-security-track {
  display: inline-flex;
  gap: 1.2rem;
  padding: 0.7rem 0;
  font-family: var(--font-mono);
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  color: var(--ink-faint);
  animation: scroll-left 26s linear infinite;
  white-space: nowrap;
}

/* =====================================================================
   Footer
   ===================================================================== */
.footer {
  padding: var(--space-8) var(--gutter);
  text-align: center;
  border-top: 1px solid var(--line);
  /* The footer previously sat on nothing — the credit-card stage above it
     ends in mid-air, so the page just stopped. An opaque paper base gives
     the page a floor to land on. Deliberately no position/z-index: the
     sticky credit-card frame is a preceding sibling, and giving this a
     stacking order it doesn't need is exactly the kind of thing that has
     broken that reveal before. Normal flow already paints it above the
     z-index:-1 scrim. */
  background: var(--paper);
}

.footer p {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--ink-faint);
}

/* =====================================================================
   Back to top — mobile only (desktop already has a sticky nav within
   easy reach). Hidden until scrolled past roughly one screen, so it
   never sits in the way at the top of the page. z-index stays below the
   achievement modal (400) so it can't peek through an open dialog.
   ===================================================================== */
.back-to-top {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: var(--ink);
  color: var(--paper);
  font-size: 1.15rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-lg);
  cursor: pointer;
  z-index: 390;
  opacity: 0;
  transform: translateY(10px) scale(0.92);
  pointer-events: none;
  transition: opacity 0.3s var(--ease-out), transform 0.3s var(--ease-out), background 0.2s;
}

.back-to-top.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
  pointer-events: auto;
}

.back-to-top:hover,
.back-to-top:focus-visible {
  background: var(--gold);
}

/* Extra .is-visible for specificity — needs to beat .is-visible's own
   scale(1), which a bare :active wouldn't reliably outrank. */
.back-to-top.is-visible:active {
  transform: translateY(0) scale(0.88);
  transition-duration: 0.1s;
}

@media (min-width: 861px) {
  .back-to-top {
    display: none;
  }
}

/* =====================================================================
   Responsive
   ===================================================================== */
@media (max-width: 900px) {
  .hero-columns {
    grid-template-columns: 1fr;
  }

  .ticker-board {
    grid-template-columns: repeat(3, 1fr);
  }

  .config-wires {
    display: none;
  }

  /* Bio, Actions, and Settings (Focus+Sector, already merged into one
     card below) now render as ONE card instead of three — cutting two
     full sets of card chrome (border + padding + shadow + inter-card
     gap) that were adding up fast: the whole "Configure the Analyst"
     widget measured 1243px tall on a 375px-wide phone before this — 1.53x
     the viewport — before even reaching the KPI strip below it. Nothing
     is removed, just consolidated: the same content, divided by
     border-top rules instead of by being three separate boxes. */
  .flow-grid {
    grid-template-columns: 1fr;
    background: var(--paper);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
    padding: var(--space-4);
    gap: 0;
  }

  .flow-node-bio,
  .flow-node-actions {
    grid-column: 1;
    background: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
  }

  .flow-node-bio {
    padding: 0 0 var(--space-3);
  }

  .flow-node-actions {
    padding: var(--space-3) 0;
    border-top: 1px solid var(--line);
  }

  .flow-settings {
    flex-direction: column;
    gap: var(--space-3);
    /* No longer its own card — just the last section of the merged one
       above, separated the same way every other section in it is. */
    background: none;
    border: none;
    border-top: 1px solid var(--line);
    border-radius: 0;
    padding: var(--space-3) 0 0;
    box-shadow: none;
  }

  .flow-settings .flow-node {
    flex-basis: auto;
    background: none;
    border: none;
    border-radius: 0;
    padding: 0;
    box-shadow: none;
  }

  /* Focus/Sector as a compact wrapped pill row instead of a tall stack of
     full-width rows — the original layout meant 6 rows of ~50px each
     (300px+) just to make two picks, on top of the Bio/Actions/Output
     cards above and below it. This cuts that considerably without losing
     any option. */
  .config-node-options {
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--space-2);
  }

  .config-option {
    flex: 0 0 auto;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--line-ui);
    border-radius: var(--radius-pill);
    background: var(--paper);
  }

  .config-option.is-active {
    border-color: var(--gold);
    background: var(--paper-panel);
  }

  /* Side by side instead of stacked — two 45px+ full-width buttons plus
     their gap cost ~98px stacked; side by side costs one row. Tested
     that "View Experience" (the longer label) wraps to two lines inside
     its own button rather than overflowing, at this width. */
  .flow-actions {
    flex-direction: row;
    gap: var(--space-2);
  }

  .flow-node-actions .btn {
    width: auto;
    flex: 1;
    justify-content: center;
    gap: var(--space-1);
    padding: 0.6rem var(--space-2);
    font-size: 0.62rem;
  }

  /* A touch tighter than the desktop 1.5 — this paragraph is the single
     largest block of static text in the widget, and every notch of
     line-height compounds across five lines. */
  .flow-bio-text {
    line-height: 1.42;
  }

  /* Output is the tallest single piece of the widget (blurb+chips+meta,
     each with desktop-generous margins meant for a card with room to
     spare) — tightened here, mobile only, rather than in the base rules,
     since desktop's spacing is fine as-is. */
  .config-output {
    padding: var(--space-4);
  }

  .config-output-blurb {
    margin-bottom: var(--space-3);
    line-height: 1.45;
  }

  .config-output-chips {
    margin-bottom: var(--space-3);
  }

  .config-output-meta {
    padding-top: var(--space-3);
  }

  .flow-grid {
    margin-bottom: 1rem;
  }

  .hero-flow {
    padding: 1.4rem;
  }
}

@media (max-width: 860px) {
  .nav-toggle {
    display: flex;
  }

  /* #primaryNav's job below this width is now .mobile-nav's (see its own
     rules further down) — the inline desktop row just gets out of the
     way entirely rather than being reshaped into a dropdown. */
  .navbar nav {
    display: none;
  }
}

/* =====================================================================
   Mobile nav — a card dropping from the header over a dimmed backdrop
   (see the HTML comment above #mobileNav for why this is a separate
   element from #primaryNav rather than that element reshaped, and why it
   lives outside .navbar). This replaced an earlier full-screen takeover
   version: at 6 short links, a full 100vh panel left roughly 40% of the
   screen as dead space below the last link, and read as more "wall" than
   "menu" — the actual iOS pattern for exactly this (an action sheet, a
   popover) is a sized-to-content card over a dimmed scrim, not a
   full-bleed panel, and that's what this is now. */
.mobile-nav {
  display: none;
}

@media (max-width: 860px) {
  .mobile-nav {
    position: fixed;
    inset: 0;
    /* Just under .navbar's 100 — enough to cover everything else in
       .page (main, footer — both z-index:auto) while the real navbar,
       with its logo and the existing hamburger/X toggle, stays visibly
       pinned above this rather than being covered by it. */
    z-index: 95;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    /* Clears the sticky navbar's own ~76px height (measured, not
       guessed) so the card starts right below it instead of overlapping
       — the navbar stays on top by z-index regardless, but without this
       the card would open underneath that top strip. */
    padding: calc(76px + var(--space-4)) var(--gutter) var(--space-4);
    /* The dimmed backdrop itself — a scrim over the page behind, not a
       wall replacing it. Tapping it (script.js: anywhere outside
       .mobile-nav-panel) closes the menu, same as any iOS/Android sheet. */
    background: rgba(15, 17, 21, 0.4);
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.3s var(--ease-out), visibility 0s linear 0.3s;
  }

  .mobile-nav.is-open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity 0.3s var(--ease-out), visibility 0s;
  }

  .mobile-nav-panel {
    width: 100%;
    max-height: 100%;
    overflow-y: auto;
    background: rgba(255, 255, 255, 0.97);
    backdrop-filter: blur(20px) saturate(1.4);
    -webkit-backdrop-filter: blur(20px) saturate(1.4);
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--space-2);
    /* One clean "card arriving" entrance for the whole panel — the
       previous version staggered each link individually on top of its
       own blur/scale, which was a lot of simultaneous motion for six
       short words. Simpler reads calmer, not less polished. */
    transform: translateY(-12px) scale(0.97);
    opacity: 0;
    transition: transform 0.35s var(--ease-out), opacity 0.35s var(--ease-out);
  }

  .mobile-nav.is-open .mobile-nav-panel {
    transform: none;
    opacity: 1;
  }

  .mobile-nav-panel a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    text-decoration: none;
    color: var(--ink);
    padding: var(--space-4);
    border-radius: var(--radius-md);
    border-bottom: 1px solid var(--line);
    font-family: var(--font-serif);
    font-weight: 700;
    font-size: 1.3rem;
    letter-spacing: -0.012em;
    transition: background var(--transition), color var(--transition);
  }

  .mobile-nav-panel a:last-child {
    border-bottom: none;
  }

  .mobile-nav-panel a:active {
    background: var(--paper-panel);
    color: var(--gold);
  }

  .mobile-nav-arrow {
    color: var(--gold);
    font-family: var(--font-mono);
    font-size: 1rem;
    opacity: 0;
    transform: translateX(-4px);
    transition: opacity var(--transition), transform var(--transition);
  }

  .mobile-nav-panel a:active .mobile-nav-arrow {
    opacity: 1;
    transform: translateX(0);
  }
}

/* The scroll-to-top button floating above the dimmed nav backdrop would
   look broken — nothing to scroll past while the menu's open, and body
   scroll is locked anyway. html.nav-is-open is set by script.js exactly
   while #mobileNav is open. A root-level class rather than a CSS sibling
   selector because .mobile-nav (inside .page) and .back-to-top (a
   sibling of .page, outside it) aren't adjacent in the DOM. */
html.nav-is-open .back-to-top {
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 820px) {
  .career-panel {
    perspective: none !important;
    transform: none !important;
  }

  /* NOT display:none — .career-nav (the role quick-jump chips) lives
     inside this wrapper, and hiding the wrapper hid the chips along with
     it, silently breaking the only way to jump straight to a role on
     mobile (the "Auto-rotating... click a role to jump in" hint became a
     dead promise — nothing clickable was actually visible). Only the
     desktop-only drag/zoom container behavior needs neutralizing here;
     .career-nav's own mobile rule below already turns it into a normal
     horizontal-scroll chip row. */
  .career-canvas-scroll {
    overflow: visible;
  }

  .career-canvas {
    padding: 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    background: transparent;
    border: none;
  }

  .career-controls,
  .career-node-toolbar,
  .career-edge-svg,
  .career-chain-svg {
    display: none;
  }

  .career-nav {
    display: flex;
    gap: 0.5rem;
    width: auto;
    height: auto;
    overflow-x: auto;
    padding: 0.2rem 0.2rem 0.8rem;
    transform: none;
    /* Fades the last ~40px of chips toward transparent, so a row that
       continues past the edge reads as "more here" rather than looking
       like it simply ends there. */
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);
    mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);
  }

  /* A slim scroll indicator under the logo row instead of a floating
     arrow: an arrow only pointed one way, sat on top of the chips, and
     covered whichever logo was at the edge. This sits clear of them and
     shows both how far along the row you are and how much is left. */
  .career-nav-scroll {
    display: block;
    width: 96px;
    height: 3px;
    margin: 0.1rem auto 0;
    border-radius: 999px;
    background: var(--line-strong);
    overflow: hidden;
  }

  .career-nav-scroll-thumb {
    display: block;
    height: 100%;
    width: 40%;
    border-radius: 999px;
    background: var(--gold);
    transform: translateX(0);
    transition: transform 0.12s linear;
  }

  .career-node {
    position: static;
    flex: 0 0 auto;
    width: auto;
    min-width: 88px;
    justify-content: center;
    flex-direction: row;
    padding: 0.5rem 0.8rem;
  }

  .career-panel {
    display: flex;
    /* Default (stretch) forces every card to the height of the TALLEST
       one — Nadher Media, with two stat chips and three paragraphs — so
       Poly's two-sentence blurb sat in a box with ~300px of blank paper
       below it (measured). This alone doesn't fix that: a flex row's own
       height is still set by its tallest child regardless of align-items,
       so the track is exactly as tall either way — but it's what makes
       each item's OWN scrollHeight report its real content height instead
       of the stretched one, which script.js's syncCareerPanelHeight()
       needs to be accurate before it can lock the whole track to whichever
       card is actually showing. The two changes only work together. */
    align-items: flex-start;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    border-radius: var(--radius-lg);
    border-top: 1px solid var(--line);
    -webkit-overflow-scrolling: touch;
    /* Animates the swipe-to-swipe resize syncCareerPanelHeight() drives —
       without it, the track would snap to each card's height instantly,
       which read as a layout jolt rather than a deliberate transition.
       Same curve as everything else that settles into place on this site. */
    transition: height 0.35s var(--ease-out);
    /* The desktop padding (1.4rem 1.5rem) lives on this element, but on
       desktop only one .career-panel-item is ever visible at once
       (display swap), so it reads fine. Here every item sits side by
       side at flex:0 0 100% — and that 100% is measured against this
       container's content box, i.e. width MINUS this padding. With the
       padding still here, each "full width" card actually rendered ~48px
       narrower than the visible scroll viewport, letting a sliver of the
       next card bleed into view at the right edge on every swipe. Padding
       moves onto each card below instead, so 100% is really 100%. */
    padding: 0;
  }

  /* These three are meant as persistent header chrome sitting above the
     swipeable cards — but as plain (non-absolute) children of a now-flex
     .career-panel, they were becoming their own tiny flex items in the
     scroll-snap row, landing off-screen depending on scroll position.
     Mobile already has its own dot pagination, so just drop them here. */
  .career-panel-tag,
  .career-panel-handle,
  .career-panel-loadbar {
    display: none;
  }

  .career-panel-item {
    display: block !important;
    flex: 0 0 100%;
    scroll-snap-align: start;
    animation: none !important;
    padding: var(--space-6);
  }

  .career-dots {
    display: flex;
  }

  .career-sub-desktop {
    display: none;
  }

  .career-sub-mobile {
    display: inline;
  }

  /* An explicit written cue plus an arrow. The mask fade and the dots
     alone were too subtle — the row genuinely looked like it ended at the
     screen edge, so nothing told a first-time visitor there were more
     roles unless they happened to read the section's intro copy. */
  .swipe-cue {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    margin-top: 0.9rem;
    font-family: var(--font-mono);
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-dim);
    transition: opacity 0.3s var(--ease-out);
  }

  .swipe-cue.is-hidden {
    opacity: 0;
    pointer-events: none;
  }

  .swipe-cue-arrow {
    color: var(--gold);
    font-size: 1.1rem;
    line-height: 1;
    animation: career-nav-hint-nudge 1.6s ease-in-out infinite;
  }

  /* #kpiSwipeCue also carries .swipe-cue (see index.html), but its own
     carousel doesn't activate until 680px (.ticker-board below) — between
     that and this wider 820px breakpoint the KPI strip is still a plain
     3-column grid with nothing to swipe, so it needs to stay hidden
     longer than the two role-explorer cues sharing the rule above. Undone
     again inside the 680px block further down, which — being narrower —
     comes later in this file and so wins the cascade once it applies. */
  #kpiSwipeCue {
    display: none;
  }

}

@media (max-width: 780px) {
  .ach-grid {
    grid-template-columns: 1fr;
    gap: 1.1rem;
  }

  .ach-flow {
    display: none;
  }
}

@media (max-width: 720px) {
  .hero-top {
    align-items: flex-start;
  }
}

@media (max-width: 680px) {
  .ticker-board {
    grid-template-columns: 1fr;
    scroll-snap-type: x mandatory;
    display: flex;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .ticker-cell {
    scroll-snap-align: start;
    flex: 0 0 100%;
  }

  .ticker-dots {
    display: flex;
  }

  /* Re-enables the cue that the 820px block turned off for this specific
     instance — see the comment there. This is where the KPI strip's own
     carousel actually starts (directly above), so this is where its
     "there's more" cue becomes true. */
  #kpiSwipeCue {
    display: flex;
  }

  .career-panel-stats {
    gap: 1.4rem;
  }
}

@media (max-width: 600px) {
  :root {
    --section-pad: 3.5rem;
  }

  .navbar-inner {
    padding: 0.9rem var(--gutter);
  }

  .hero-id-badge {
    align-self: flex-start;
  }

  .global-backdrop {
    opacity: 0.7;
  }

  .trade-row {
    flex-wrap: wrap;
    gap: 0.4rem 0.8rem;
  }

  .trade-name {
    flex-basis: 100%;
    order: 3;
  }
}

@media (max-width: 480px) {
  /* The base clamp's floor (6.5rem) leaves a wide dead gap between the
     sticky navbar and the photo before anything else fills it in — this
     tightens just that floor for narrow phones. */
  .hero {
    padding-top: 3.5rem;
  }

  .hero-heading h1 {
    font-size: 2.5rem;
  }

  .hero-flow {
    padding: 1.1rem;
  }

  .btn {
    width: 100%;
    justify-content: center;
  }

  /* Was flex-direction: column here, re-stacking the two hero action
     buttons this narrow — predates the compact side-by-side sizing added
     for them at the 900px breakpoint (.flow-node-actions .btn), which is
     more specific than the .btn rule above and already overrides its
     width:100% on its own. Tested at 375px with no overflow; removed
     rather than kept "just in case" since it was silently cancelling
     that change out below 480px. */
}

@media (max-width: 420px) {
  .section-inner > h2 {
    font-size: 1.7rem;
  }

  .ach-card-name {
    font-size: 1.05rem;
  }
}

/* =====================================================================
   Touch accessibility — WCAG 2.5.5 / platform guidance calls for a
   minimum 44x44px target on real touch input, regardless of viewport
   width (a narrow desktop window is not a touchscreen; a large tablet
   in portrait can be) — detected via pointer:coarse rather than a width
   breakpoint for that reason. Visual size of small elements (dots) is
   left alone; only the actual tappable area is expanded.
   ===================================================================== */
@media (pointer: coarse) {
  .config-option {
    min-height: 44px;
    padding: var(--space-3);
  }

  .trade-tab {
    min-height: 44px;
  }

  .ach-modal-close {
    width: 44px;
    height: 44px;
  }

  /* Was a -19px ::after on a 6px dot: a 44px target on a 12.8px pitch, so
     every dot's hit area overlapped its neighbours' by roughly 70% and
     the later sibling won — taps landed on the wrong stat. The dot is now
     a real 44px button (see the shared rule further up) and the row's
     gap is 0, so the targets tile edge to edge with no overlap at all. */
  .ticker-dot,
  .career-dot {
    width: 44px;
    height: 44px;
  }

  /* Slightly larger visible dot so a 44px-pitched row still reads as a
     considered set of dots rather than scattered specks. */
  .ticker-dot::before,
  .career-dot::before {
    width: 8px;
    height: 8px;
  }

  /* Desktop-sized chrome that a touchscreen laptop or tablet can still
     reach — none of these are inside a width-based mobile breakpoint, so
     they'd otherwise stay at their mouse sizes on a real touchscreen. */
  .career-control-btn {
    width: 44px;
    height: 44px;
  }

  .navbar nav a {
    min-height: 44px;
  }

  .cc-number,
  .cc-holder {
    display: inline-block;
    padding: 13px 0;
    margin: -13px 0;
  }

  .cc-linkedin {
    /* The padding+negative-margin trick used elsewhere on this page
       doesn't work here: this element has an explicit width/height and
       box-sizing:border-box (the site-wide default), so padding eats
       into the icon's own space instead of growing the tappable area —
       the box stayed exactly 32px regardless. Sizing it up directly is
       the only thing that actually works for a fixed-size box like this. */
    width: 44px;
    height: 44px;
  }

  .nav-toggle {
    width: 44px;
    height: 44px;
  }
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .top-ticker-track,
  .section-divider-content,
  .trade-ticker-track,
  .cc-security-track,
  .cc-shine,
  .status-dot,
  .career-live-dot,
  .swipe-cue-arrow,
  .scroll-cue-arrow {
    animation: none !important;
  }

  .hero-inner,
  .reveal {
    opacity: 1 !important;
    transform: none !important;
    /* Without this, anything below the fold is force-shown at full
       opacity by the rule above (correct — reduced motion means "don't
       make me wait to see it") but keeps the base rule's filter:blur(6px)
       until the IntersectionObserver actually reaches it and adds
       .in-view — which, for reduced-motion, defeats the entire point:
       visible-but-illegible content that only sharpens once scrolled to,
       instead of just being there. */
    filter: none !important;
  }

  /* The card renders fully out and static here (see script.js), so the
     tall spacer and sticky frame would just be empty scrolling past an
     animation that never plays. Collapse both to normal flow. */
  .cc-scroll {
    height: auto !important;
  }

  .cc-sticky {
    position: static !important;
    height: auto !important;
    padding: clamp(2rem, 6vw, 3.5rem) 0;
  }
}
