/* ==========================================================================
   Charles Sanderson — Product Design Portfolio
   File: css/styles.css
   WCAG 2.1 AA compliant | Security-conscious (no inline event handlers)
   ========================================================================== */

/* =========================================================================
   1. CSS CUSTOM PROPERTIES (Design Tokens)
   ========================================================================= */
:root {
  /* — Colors — */
  --bg-primary:    #030510;
  --bg-secondary:  #070b1f;
  --bg-surface:    #0d1235;
  --bg-card:       #0f152a;

  --accent-cyan:   #60a5fa;   /* Periwinkle blue — more blue than teal */
  --accent-gold:   #ffd166;
  --accent-violet: #c084fc;
  /* A pale rose (palevioletred #db7093 mixed 45% toward white, so it tints
     without pulling focus): the small eyebrow labels and résumé bullets.
     9.9-11.1:1 on every site background (WCAG AA small text needs 4.5:1). */
  --accent-rose:   #eab0c2;

  --text-primary:   #e0e8ff;
  --text-secondary: #a3b0cf;   /* was #8892b0 — only 1.17:1 against --text-muted, so the two tiers read as one. Now 1.77:1 below primary / 1.67:1 above muted: an even three-step ramp */
  --text-muted:     #7987a0;
  --text-heading:   #c6d0ec;   /* post body headings: between primary and secondary so h2-h6 sit below the h1 title */   

  --border-subtle: rgba(96, 165, 250, 0.08);
  --border-active: rgba(96, 165, 250, 0.3);
  /* Hairline under the sticky nav once the page scrolls — muted blue-grey,
     enough to separate the bar from content passing under it, quiet enough
     not to pull the eye. Raise the alpha if you want it more assertive. */
  --nav-border:    rgba(136, 146, 176, 0.28);

  /* — Fonts — */
  --font-display: 'Gravitas One', serif;
  --font-body:    'Mulish', sans-serif;
  --font-mono:    'JetBrains Mono', monospace;

  /* — Spacing Scale — */
  --space-xs: 0.5rem;
  --space-sm: 1rem;
  --space-md: 2rem;
  --space-lg: 4rem;
  --space-xl: 8rem;

  /* — Motion — */
  --ease-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94);
  --ease-sharp:  cubic-bezier(0.16, 1, 0.3, 1);
  --dur-fast: 150ms;
  --dur-med:  350ms;
  --dur-slow: 600ms;

  /* — Layout — */
  --max-width:  1200px;
  --nav-height: 80px;
  --radius-sm:  4px;
  --radius-md:  8px;
  --radius-lg:  16px;
}

/* =========================================================================
   2. RESET & BASE
   ========================================================================= */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

body {
  background-color: var(--bg-primary);
  color: var(--text-primary);
  font-family: var(--font-body);
  line-height: 1.6;
  overflow-x: hidden;
  cursor: none; /* Custom cursor enabled for pointer devices */
}

/* Restore default cursor on touch-only devices */
@media (hover: none) {
  body { cursor: auto; }
  #custom-cursor, #custom-cursor-ring { display: none; }
}

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

a {
  color: var(--accent-cyan);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-smooth);
}
a:hover { color: #fff; }

/* WCAG 2.1 §2.4.7 — Visible focus indicator for keyboard navigation */
:focus-visible {
  outline: 2px solid var(--accent-cyan);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

ul, ol { list-style: none; }

button {
  font-family: var(--font-body);
  cursor: none;
  border: none;
  background: transparent;
}
@media (hover: none) { button { cursor: pointer; } }

/* =========================================================================
   3. SKIP LINK — WCAG 2.1 §2.4.1
   ========================================================================= */
.skip-link {
  position: fixed;
  top: -100%;
  left: 1rem;
  z-index: 9999;
  padding: 0.75rem 1.5rem;
  background: var(--accent-cyan);
  color: var(--bg-primary);
  font-family: var(--font-display);
  font-weight: 700;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  transition: top var(--dur-fast) var(--ease-smooth);
}
.skip-link:focus { top: 0; outline: none; }

/* =========================================================================
   4. CUSTOM CURSOR
   ========================================================================= */
#custom-cursor {
  position: fixed;
  top: 0; left: 0;
  width: 10px; height: 10px;
  background: var(--accent-cyan);
  border-radius: 50%;
  pointer-events: none;
  z-index: 10000;
  transform: translate(-50%, -50%); /* main.js prepends the pointer position */
  transition: width var(--dur-fast) var(--ease-smooth),
              height var(--dur-fast) var(--ease-smooth);
  mix-blend-mode: difference;
}

#custom-cursor-ring {
  position: fixed;
  top: 0; left: 0;
  width: 38px; height: 38px;
  border: 1px solid rgba(96, 165, 250, 0.45);
  border-radius: 50%;
  pointer-events: none;
  z-index: 9999;
  transform: translate(-50%, -50%);
  transition: width 0.22s var(--ease-smooth),
              height 0.22s var(--ease-smooth),
              border-color 0.22s var(--ease-smooth);
}

/* Expanded cursor state on interactive elements */
body.cursor-hover #custom-cursor { width: 5px; height: 5px; }
body.cursor-hover #custom-cursor-ring { width: 54px; height: 54px; border-color: var(--accent-cyan); }

/* =========================================================================
   5. STAR CANVAS BACKGROUND
   ========================================================================= */
#star-canvas {
  position: fixed;
  top: 0; left: 0;
  /* Height is set in px by resize() in main.js, so iOS toolbar resizes don't stretch it. */
  width: 100%; height: 100%;
  z-index: 0;
  pointer-events: none;
}

/* =========================================================================
   6. NAVIGATION
   ========================================================================= */
.site-nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  padding: 0 var(--space-md);
  /* Declared transparent up front so .scrolled only animates the COLOUR.
     Without this the border starts at its initial value — currentColor, i.e.
     the near-white nav text — and the first frames flash a white hairline
     before settling. box-sizing: border-box keeps the bar's height fixed. */
  border-bottom: 1px solid transparent;
  transition: background var(--dur-slow) var(--ease-smooth),
              border-color var(--dur-slow) var(--ease-smooth);
}

/* Glassmorphism nav when page is scrolled */
.site-nav.scrolled {
  background: rgba(3, 5, 16, 0.88);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-bottom-color: var(--nav-border);
}

.nav-inner {
  max-width: var(--max-width);
  width: 100%;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Logo / wordmark */
.nav-logo {
  font-family: var(--font-display);
  font-size: 1.1rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-primary);
}
.nav-logo span { color: var(--accent-cyan); }
/* Thin ring around the wordmark: the size of the pointer's hover ring
   (#custom-cursor-ring, 54px) in the scrolled header's border colour. */
.nav-logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 54px;
  height: 54px;
  border: 1px solid var(--nav-border);
  border-radius: 50%;
  padding-left: 0.12em;   /* optical centring: the trailing "." makes the word look left-heavy */
  line-height: 1;
}

/* Desktop nav links */
.nav-links { display: flex; align-items: center; gap: var(--space-md); }

.nav-links a {
  font-family: var(--font-body);
  Text-transform: uppercase;
  font-size: .9rem;
  font-weight: 300;
  letter-spacing: 0.1em;
  color: var(--text-secondary);
  position: relative;
  padding: 0.25rem 0;
  transition: color var(--dur-med) var(--ease-smooth);
}

/* Animated underline on nav items */
.nav-links a::after {
  content: '';
  position: absolute;
  bottom: -2px; left: 0;
  width: 0; height: 1px;
  background: var(--accent-cyan);
  transition: width var(--dur-med) var(--ease-smooth);
}
.nav-links a:hover,
.nav-links a[aria-current="page"] { color: var(--text-primary); }
.nav-links a:hover::after,
.nav-links a[aria-current="page"]::after { width: 100%; }

/* CTA button in nav */
.nav-cta {
  font-family: var(--font-body) !important;
  font-size: 0.78rem !important;
  font-weight: 700 !important;
  letter-spacing: 0.12em !important;
  text-transform: uppercase !important;
  color: var(--bg-primary) !important;
  background: var(--accent-cyan) !important;
  padding: 0.55rem 1.3rem !important;
  border-radius: 2px !important;
  transition: background var(--dur-fast) var(--ease-smooth),
              transform var(--dur-fast) var(--ease-smooth) !important;
}
.nav-cta:hover { background: #fff !important; transform: translateY(-1px) !important; color: var(--bg-primary) !important; }
.nav-cta::after { display: none !important; }

/* Hamburger toggle (mobile only) */
.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 8px;
  cursor: none;
  z-index: 101;
}
.nav-toggle span {
  display: block;
  width: 24px; height: 1.5px;
  background: var(--text-primary);
  transition: transform var(--dur-med) var(--ease-smooth),
              opacity var(--dur-med) var(--ease-smooth);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* =========================================================================
   7. LAYOUT UTILITIES
   ========================================================================= */
.container {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--space-md);
  position: relative;
  z-index: 1;
}

.section { padding: var(--space-xl) 0; position: relative; }

.section-label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent-rose);
  margin-bottom: var(--space-sm);
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.section-label::before {
  content: '';
  display: block;
  width: 32px; height: 1px;
  background: currentColor;
}

.section-title {
  font-family: var(--font-display);
  font-size: clamp(2.2rem, 5vw, 3.5rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin-bottom: var(--space-md);
}
/* Homepage section headings sit a step smaller, under the hero headline. */
:is(#about-heading, #work-heading, #process-heading, #cta-heading).section-title {
  font-size: clamp(2rem, 4.5vw, 3rem);
}

/* Section heading row with a trailing action (e.g. "All Projects").
   Lives here, not in a style attribute: the public-page CSP has no
   'unsafe-inline' for styles, so inline style="" is silently dropped. */
.section-head { margin-bottom: var(--space-md); }

/* Orphan control: let the browser rebalance the last lines of body copy so
   a single word doesn't sit alone on the final line. */
p, li, blockquote, figcaption { text-wrap: pretty; }

.section-body {
  font-size: 1.1rem;
  color: var(--text-secondary);
  max-width: 600px;
  line-height: 1.75;
}

/* =========================================================================
   8. BUTTON COMPONENTS
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.9rem 2rem;
  border-radius: 2px;
  transition: all var(--dur-med) var(--ease-smooth);
  cursor: none;
}
@media (hover: none) { .btn { cursor: pointer; } }

.btn-primary {
  background: var(--accent-cyan);
  color: var(--bg-primary);
  border: 1px solid var(--accent-cyan);
}
.btn-primary:hover {
  background: transparent;
  color: var(--accent-cyan);
  transform: translateY(-2px);
  box-shadow: 0 8px 30px rgba(96, 165, 250, 0.2);
}

.btn-outline {
  background: transparent;
  color: var(--accent-cyan);
  border: 1px solid var(--border-active);
}
.btn-outline:hover {
  background: rgba(96, 165, 250, 0.05);
  border-color: var(--accent-cyan);
  transform: translateY(-2px);
}

/* Hero buttons in the planet's sunlight: a soft shadow cast away from the
   sun, a soft warm inner glow on the sun-facing edges (blurred, so it reads
   as light falling on the border rather than a solid stripe; inset offsets
   show on the side opposite the offset, hence the negated vector), and a
   faint radial sheen centred past the sun-facing corner so the light is
   strongest there and fades along the top and right. The primary's own
   hover glow is folded back in, since this rule outranks .btn-primary:hover. */
.hero-ctas .btn {
  --sun-cast: calc(var(--sun-x, 0.8) * -5px) calc(var(--sun-y, -0.6) * -5px) 14px rgba(1, 3, 12, 0.5);
  --sun-edge: inset calc(var(--sun-x, 0.8) * -2px) calc(var(--sun-y, -0.6) * -2px) 5px -2px rgba(255, 214, 165, 0.22);
  box-shadow: var(--sun-cast), var(--sun-edge);
  background-image: radial-gradient(circle at calc(50% + var(--sun-x, 0.8) * 55%) calc(50% + var(--sun-y, -0.6) * 140%),
    rgba(255, 226, 190, 0.16), transparent 70%);
}
/* The secondary (outline) button keeps the light on its border only. */
.hero-ctas .btn-outline { background-image: none; }
.hero-ctas .btn-primary:hover { box-shadow: var(--sun-cast), var(--sun-edge), 0 8px 30px rgba(96, 165, 250, 0.2); }

/* Tertiary — no border, normal case; for dense lists (nav links, sheet
   triggers) where the outline button's border/all-caps read as too heavy. */
.btn-tertiary {
  background: transparent;
  color: var(--accent-cyan);
  border: 1px solid transparent;
  text-transform: none;
  font-weight: 600;
  letter-spacing: normal;
}
.btn-tertiary:hover {
  background: rgba(96, 165, 250, 0.06);
  color: #fff;
}

.btn-arrow { display: inline-block; transition: transform var(--dur-fast) var(--ease-smooth); }

/* Compact size modifier — pair with .btn-primary/.btn-outline for tight
   spaces (nav lists, etc.) that don't need the full CTA-sized button. */
.btn-sm { padding: .5rem 1rem; font-size: .9rem; letter-spacing: .06em; }
.btn:hover .btn-arrow { transform: translateX(4px); }

/* Centred row of CTA buttons. Lives here rather than in a style="" attribute
   because Helmet's style-src has no 'unsafe-inline' — inline style attributes
   are blocked on public pages exactly like inline scripts are. */
.btn-row {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: var(--space-md);
}

/* =========================================================================
   9. HERO SECTION
   ========================================================================= */
.hero {
  min-height: 100vh;
  display: flex;
  align-items: center;
  padding-top: var(--nav-height);
  position: relative;
  overflow: hidden;
}

/* Planet behind the hero headline (planet.js). Fixed to the viewport: the
   planet's position is a shader uniform, which is what gives it scroll and
   pointer parallax. Same layer as #star-canvas (z-index 0) but later in the
   DOM, so it covers the stars and stays under the ship and every z-index:1
   container. */
#planet {
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 100vh;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  transition: opacity 1.2s ease;
}
#planet.is-ready { opacity: 1; }
#planet canvas { display: block; }
#planet .planet-flyby { position: absolute; top: 0; left: 0; }  /* comets, over the planet */

/* No-WebGL2 stand-in: same silhouette, dark disc, blue rim toward the sun. */
.planet-fallback {
  position: absolute;
  top: 0; left: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 45%, #060b18 0%, #04070f 70%, #0b1a33 100%);
  box-shadow:
    18px -6px 40px -10px rgba(96, 165, 250, 0.55),
    0 0 60px 4px rgba(96, 165, 250, 0.12);
}

/* Ambient nebula glows */
.hero::before {
  content: '';
  position: absolute;
  top: 15%; left: -8%;
  width: 600px; height: 600px;
  background: radial-gradient(circle, rgba(96, 165, 250, 0.04) 0%, transparent 65%);
  pointer-events: none;
}
.hero::after {
  content: '';
  position: absolute;
  bottom: 5%; right: -8%;
  width: 500px; height: 500px;
  background: radial-gradient(circle, rgba(192, 132, 252, 0.04) 0%, transparent 65%);
  pointer-events: none;
}

.hero-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--space-md);
  width: 100%;
  position: relative;
  z-index: 1;
}

/* Small monospace identifier above headline */
.hero-orbit-label {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--accent-rose);   /* full opacity: dimming it would drop the contrast below AA */
  margin-bottom: 1.5rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}
.hero-orbit-label::before {
  content: '';
  display: block;
  width: 48px; height: 1px;
  background: currentColor;
}

/* Giant display headline — each line wraps for JS-driven reveal */
.hero-headline {
  font-family: var(--font-display);
  font-size: clamp(2.75rem, 5.44vw, 5.1rem);
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -0.03em;
  margin-bottom: var(--space-md);
}
.hero-headline .line { display: block; overflow: hidden; }
/* Each line is a fixed phrase: never wrap it, or the line's overflow:hidden
   (needed for the rise-in) cuts the wrapped part off. */
.hero-headline .line-inner { white-space: nowrap; }

/* Sunlight on the headline, from the homepage planet's sun. planet.js
   publishes on <html>: --sun-x/--sun-y (unit vector toward the sun, y down)
   and --sun-angle (that direction as a CSS gradient angle, deg); the
   fallbacks point up and to the right, where the sun sits.

   Each run of text is wrapped in .lit so the effect touches glyphs only:
   the orbit flourish on the "o" sits OUTSIDE the .lit spans and is untouched.
   - Fill: a gradient clipped to the letters, a touch darker away from the
     sun and lifting to a warm, brighter tint toward it. planet.js sets
     --lit-w (the line's text width) and --lit-x (each span's offset in it)
     so the ramp runs continuously across bey|o|nd instead of restarting.
   - Edge + shadow: filters, not text-shadow — browsers paint text-shadow
     OVER a background-clip:text fill, which muddies it. The first
     drop-shadow is a 1px warm rim on the sun-facing edge; the second, the
     soft shadow cast away from the sun. */
.hero-headline .lit {
  -webkit-text-fill-color: transparent;
  background-image: linear-gradient(calc(var(--sun-angle, 50) * 1deg),
    color-mix(in srgb, currentColor 84%, #02040c) 0%,
    currentColor 48%,
    color-mix(in srgb, currentColor 68%, #fff1dc) 100%);
  background-size: var(--lit-w, 100%) 100%;
  background-position: calc(var(--lit-x, 0px) * -1) 0;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
  background-clip: text;
  filter:
    drop-shadow(calc(var(--sun-x, 0.8) * 1px) calc(var(--sun-y, -0.6) * 1px) 0 rgba(255, 228, 190, 0.55))
    drop-shadow(calc(var(--sun-x, 0.8) * -3px) calc(var(--sun-y, -0.6) * -3px) 6px rgba(1, 3, 12, 0.5));
}
.hero-headline .line-inner { display: block; transform: translateY(110%); opacity: 0; }

/* Entrance (initHeroAnimation in main.js adds .is-in and numbers the lines
   with --i): the headline lines rise one after another, then the description
   and the buttons follow. */
.hero-headline .line-inner {
  transition: transform 0.8s var(--ease-sharp), opacity 0.8s ease;
  transition-delay: calc(200ms + var(--i, 0) * 150ms);
}
.hero-desc, .hero-ctas {
  transition: transform 0.7s var(--ease-sharp), opacity 0.7s ease;
  transition-delay: calc(400ms + var(--hero-lines, 3) * 150ms);
}
.hero-ctas { transition-delay: calc(550ms + var(--hero-lines, 3) * 150ms); }
.hero.is-in .hero-headline .line-inner,
.hero.is-in .hero-desc,
.hero.is-in .hero-ctas { transform: none; opacity: 1; }

/* Orbit flourish on the "o" in "beyond" — sized entirely in em/rem so it
   scales with the fluid clamp()'d headline and stays glued to the letter at
   any viewport width (no fixed px/viewport coordinates involved).
   Prefixed hero-orbit-* (not orbit-*) to avoid colliding with the unrelated
   .orbit-ring/.orbit-dot classes used by the About section's profile rings
   further down this file.

   DEPTH — the moon passing behind the "o" is done by drawing the whole orbit
   TWICE, once in a layer behind the letter and once in front, each clipped to
   its own half of the tilted ellipse. It can't be done with z-index on the
   moon itself: .hero-orbit-scene carries a transform, and a transform creates
   a stacking context, which traps any descendant z-index inside it — so the
   orbit would always paint as one unit on top of the letter. The clip-path
   divider is angled to match the -9deg tilt, so the swap happens exactly
   where the moon crosses the ellipse's left/right edge.

   PATH — .hero-orbit-scene squashes a circle into a foreshortened ellipse and
   then tilts it (transforms apply right-to-left: squash first, then tilt).
   The moon moves via rotate()+translateX() on itself; the trail is a conic
   gradient masked to a thin ring, rotating at the same rate so its bright head
   stays glued to the moon and fades off BEHIND it in the direction of travel
   (travel is clockwise: right → bottom → left → top). */
/* z-index: auto, not 0: the letter must not be its own stacking context.
   Safari paints the next filtered .lit span ("nd") over everything in an
   earlier sibling context, which hid the moon entirely on iPhone and Mac
   Safari. Without one, the front layer (z 1) sits above all the line's text
   and the back layer (z -1) below it, in every engine. */
.hero-orbit-letter { position: relative; display: inline-block; z-index: auto; }

.hero-orbit-layer {
  position: absolute;
  top: 50%; left: 50%;
  width: 1.8em; height: 1.8em;
  margin: -0.9em 0 0 -0.9em;
  pointer-events: none;
}
/* Divider tracks the -16deg tilt (right edge higher): 50% ± (tan16°/2). */
.hero-orbit-layer.is-back  { z-index: -1; clip-path: polygon(0% 64.34%, 100% 35.66%, 100% 0%, 0% 0%); }
.hero-orbit-layer.is-front { z-index: 1;  clip-path: polygon(0% 64.34%, 100% 35.66%, 100% 100%, 0% 100%); }

.hero-orbit-scene {
  position: absolute; inset: 0;
  transform: rotate(-16deg) scaleY(.26);
}

.hero-orbit-trail {
  position: absolute;
  top: calc(50% - .7em); left: calc(50% - .7em);
  width: 1.4em; height: 1.4em;
  border-radius: 50%;
  /* Head baked at 180deg (not 90deg) purely to leave room for a longer tail
     before wrapping past 0deg; the -90deg rotation offset in the keyframes
     brings that head back onto the moon at 3 o'clock. */
  background: conic-gradient(
    rgba(226,232,240,0)   0deg,
    rgba(226,232,240,0)   70deg,
    rgba(226,232,240,.35) 150deg,
    rgba(226,232,240,.8)  180deg,
    rgba(226,232,240,0)   180.5deg,
    rgba(226,232,240,0)   360deg);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - .045em), #000 calc(100% - .045em));
  mask: radial-gradient(closest-side, transparent calc(100% - .045em), #000 calc(100% - .045em));
  animation: hero-orbit-trail 6s linear infinite;
}

.hero-orbit-moon-wrap {
  position: absolute; top: 50%; left: 50%;
  width: 0; height: 0;
  animation: hero-orbit-moon 6s linear infinite;
}
/* Counter-rotates the wrap's spin so the counter-squash below lands on the
   scene's own axis rather than a rotating one — without this the moon only
   reads as round at the left/right extremes and goes oval top and bottom. */
.hero-orbit-moon-fix {
  position: absolute; top: 0; left: 0;
  width: 0; height: 0;
  animation: hero-orbit-moon-fix 6s linear infinite;
}
.hero-orbit-moon {
  position: absolute; top: 0; left: 0;
  width: 0.12em; height: 0.12em;
  margin: -0.06em 0 0 -0.06em;
  border-radius: 50%;
  /* Offsetting the highlight toward the sun turns the flat dot into a lit
     sphere — one gradient, no extra element. It follows the same --sun-x/
     --sun-y planet.js sets, so the shadow always falls on the side away from
     the hero's sun. The counter-rotation above keeps the lit side fixed as it
     orbits, so the light source stays put. */
  background: radial-gradient(circle at calc(50% + var(--sun-x, 0.8) * 22%) calc(50% + var(--sun-y, -0.6) * 22%),
    #fdfefe 0%, #dfe6f0 34%, #6c7589 64%, #262a35 100%);
  box-shadow: 0 0 .28em .025em rgba(226,232,240,.4);
  animation: hero-orbit-eclipse 6s linear infinite;
}
/* Eclipse — timed to the stretch where the moon is actually passing behind
   the glyph (centred on 75%, the top of the arc), not tied to the clip
   divider. Keying it to the divider meant it dimmed for the whole far half
   and snapped on/off as it crossed that straight edge. */
@keyframes hero-orbit-eclipse {
  0%,  66%  { filter: brightness(1) saturate(1); }
  70%, 80%  { filter: brightness(.5) saturate(.7); }
  84%, 100% { filter: brightness(1) saturate(1); }
}

@keyframes hero-orbit-trail {
  from { transform: rotate(-90deg); }
  to   { transform: rotate(270deg); }
}
@keyframes hero-orbit-moon {
  from { transform: rotate(0deg)   translateX(.68em); }
  to   { transform: rotate(360deg) translateX(.68em); }
}
/* scaleY(1 / .26) undoes the scene squash; the rotation tracks the wrap in reverse. */
@keyframes hero-orbit-moon-fix {
  from { transform: rotate(0deg)    scaleY(3.846); }
  to   { transform: rotate(-360deg) scaleY(3.846); }
}
/* Phones: a touch larger, so the moon reads at the smaller headline size. */
@media (max-width: 480px) {
  .hero-orbit-moon { width: 0.16em; height: 0.16em; margin: -0.08em 0 0 -0.08em; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-orbit-trail { animation: none; transform: rotate(-90deg); }
  .hero-orbit-moon-wrap { animation: none; transform: rotate(0deg) translateX(.68em); }
  .hero-orbit-moon-fix { animation: none; transform: rotate(0deg) scaleY(3.846); }
  .hero-orbit-moon { animation: none; }
}

/* Color utilities */
.text-accent  { color: var(--accent-cyan); }
.text-gold    { color: var(--accent-gold); }
.text-muted-c { color: var(--text-secondary); }

/* Hero supporting copy */
.hero-desc {
  max-width: 520px;
  font-size: 1.1rem;
  color: var(--text-secondary);
  line-height: 1.8;
  margin-bottom: var(--space-lg);
  opacity: 0;
  transform: translateY(20px);
}

/* Hero CTA buttons row */
.hero-ctas {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex-wrap: wrap;
  opacity: 0;
  transform: translateY(20px);
}

/* =========================================================================
   9b. SPACESHIP SCROLL SEQUENCE (homepage only — see initSpaceship() in
       main.js for the scroll choreography; this block is just the static
       shell + the one thing CSS alone should own, prefers-reduced-motion).
   ========================================================================= */
#spaceship {
  position: fixed;
  left: 50%;
  top: 0;
  z-index: 1;
  width: 48px;
  height: 76px;
  pointer-events: none;
  opacity: 0; /* pre-JS fallback; initSpaceship() drives opacity itself
                 every frame (its own lerp already smooths it — a CSS
                 transition on top would fight that smoothing) */
  will-change: transform, opacity;
}
/* The transformed element. preserve-3d is what lets the cross plane and the
   end cap hold their own orientations inside the pitch instead of being
   flattened into it — without it the whole assembly collapses back to the
   single flat sprite this structure exists to avoid. */
#ship-body {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  will-change: transform;
}
.ship-face {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}
/* Turned 90deg about the ship's long axis, so it presents width exactly when
   the main face has none. */
.ship-face--cross {
  transform: rotateY(90deg);
  opacity: var(--edge, 0);
}
/* Perpendicular to the long axis, centred on the engine end. Percentages are
   of #spaceship's box: 79.6% down is the bell's base and 39.4% wide is its
   mouth, both taken from the artwork's viewBox. Percentage margins resolve
   against the WIDTH, which is what keeps the disc centred on that point at
   either breakpoint. */
.ship-cap {
  position: absolute;
  left: 50%;
  top: 79.6%;
  width: 39.4%;
  aspect-ratio: 1;
  margin-left: -19.7%;
  margin-top: -19.7%;
  border-radius: 50%;
  transform: rotateX(90deg);
  opacity: var(--edge, 0);
  background: radial-gradient(circle at 50% 42%,
    rgba(8,11,20,.98) 0 34%,
    rgba(32,39,58,.96) 52%,
    rgba(122,133,160,.96) 78%,
    rgba(58,65,86,.96) 92%,
    rgba(20,25,38,.96) 100%);
}
#spaceship svg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible; /* lets the ion trail paint past the viewBox's own edge */
  filter: drop-shadow(0 0 12px rgba(96,165,250,.28));
}

/* Ion plume — a small idle flicker so the engine reads as lit rather than
   painted on. transform-box:fill-box pins each path's origin to its own
   bbox top, which for these two shapes is the nozzle throat, so they pulse
   AWAY from the ship instead of about their own middles. The two run at
   different rates on purpose — synchronised pulsing reads mechanical.
   Group opacity is main.js's (engine cut-off on touchdown). */
#ship-plume .plume-outer,
#ship-plume .plume-inner {
  transform-box: fill-box;
  transform-origin: 50% 0;
}
#ship-plume .plume-outer { animation: plumeOuter 520ms ease-in-out infinite alternate; }
#ship-plume .plume-inner { animation: plumeInner 340ms ease-in-out infinite alternate; }
/* Gentle: a quiet shimmer rather than a pulsing blue beacon. */
@keyframes plumeOuter {
  from { transform: scale(1, .96);    opacity: .82; }
  to   { transform: scale(1.03, 1.06); opacity: .95; }
}
@keyframes plumeInner {
  from { transform: scale(.97, .95);   opacity: .9; }
  to   { transform: scale(1.02, 1.07); opacity: 1; }
}
.ship-nacelle-glow {
  transform-box: fill-box;
  transform-origin: 50% 0;
  animation: nacelleFlicker 430ms ease-in-out infinite alternate;
}
@keyframes nacelleFlicker {
  from { transform: scaleY(.9);   opacity: .45; }
  to   { transform: scaleY(1.06); opacity: .7; }
}
/* End-on exhaust (see the markup). main.js sets transform and opacity; the
   box is a 100px disc it scales to the projected plume. Lives outside
   #ship-body on purpose: it must stay facing the viewer through the pitch. */
/* The ship far off beyond the planet's limb: a point of sunlight before it
   resolves into a hull. main.js positions it and fades it out as the ship
   grows. */
#ship-glint {
  position: absolute;
  left: 0;
  top: 0;
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(255,252,246,1) 0%,
    rgba(255,236,212,.6) 50%,
    rgba(255,214,176,0) 100%);
  will-change: transform, opacity;
}
#ship-plume-glow {
  position: absolute;
  left: 0;
  top: 0;
  width: 100px;
  height: 100px;
  margin: -50px 0 0 -50px;
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(244,249,255,.72) 0%,
    rgba(214,234,255,.5) 26%,
    rgba(150,196,255,.26) 58%,
    rgba(96,165,250,0) 100%);
  will-change: transform, opacity;
}

/* Landing / launch dust. The container is a positioning anchor and a clip,
   nothing more — every visible pixel belongs to the single #dust-shape path
   that main.js rebuilds each frame (see the note in index.html).

   overflow:hidden with the box's BOTTOM edge parked on the footer's top
   border (main.js positions it) makes that edge the ground plane: anything
   billowing below it is clipped, which is both how real dust behaves and
   what keeps the cloud out of the footer. main.js deliberately runs the
   path's base BELOW that edge, so the clip — not the geometry — is what
   forms the bottom of the cloud, and it comes out dead flush with the
   border instead of scalloping along it.

   Note there is no transform scaling here and none applied by main.js. The
   cloud grows and disperses by its outline being remade; scaling the whole
   thing instead read as one flat picture being zoomed, which is exactly
   what it was. */
#spaceship-dust {
  /* Live tone: main.js rewrites this every frame. The mass reads near-white
     while it's young and thin, greys as it thickens, then dims toward the
     page background as it disperses — a cloud leaving by going dark and
     drifting apart, not by having its alpha turned down. The value here is
     only the pre-JS starting point. */
  --dust: #dfe2f0;
  position: fixed;
  left: 50%;
  top: 0;
  z-index: 1;
  width: 100vw;
  /* Tall enough for the mass to tower well up the viewport, because that IS
     the effect: a landing cloud that stops a little above the footer reads as
     a scuff on the floor. Bounded in px so it doesn't become absurd on very
     tall displays. */
  height: min(72vh, 560px);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
}
#dust-canvas { display: block; width: 100%; height: 100%; overflow: visible; }
/* Fluid cloud (ship-cloud.js). Hidden until main.js confirms WebGL2 and adds
   .gl, which also retires the SVG cloud. It draws its own flat dust colour. */
#dust-gl { display: none; position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
#spaceship-dust.gl #dust-gl { display: block; }
#spaceship-dust.gl #dust-canvas { display: none; }
/* Flat fill, no stroke — the silhouette is the whole drawing. shape-rendering
   stays at the default: the displacement filter is what gives the edge its
   character, and crispEdges would fight it. */
#dust-shape { fill: var(--dust); }
/* Hidden outright when main.js has nothing lit, so the layer costs nothing
   for the rest of the page's life. */
#spaceship-dust:not(.active) { visibility: hidden; }

/* Heat on the footer's top border under a landing. Sits ON the border line
   (main.js parks it there) and is transparent at both ends, so it reads as
   that border brightening in the middle rather than as a separate bar laid
   over it. */
#ground-scorch {
  position: fixed;
  left: 50%;
  top: 0;
  z-index: 1;
  width: 100vw;
  height: 2px;
  opacity: 0;
  pointer-events: none;
  background: linear-gradient(to right,
    rgba(224,232,255,0) 0%,
    rgba(224,232,255,.05) 26%,
    rgba(224,232,255,.26) 44%,
    rgba(233,240,255,.45) 50%,
    rgba(224,232,255,.26) 56%,
    rgba(224,232,255,.05) 74%,
    rgba(224,232,255,0) 100%);
  will-change: transform, opacity;
}

/* Exhaust deflected by the deck. A half-ellipse whose flat side IS the
   ground line: main.js parks the box's bottom edge on the footer's border
   and scales it out from there (origin at the bottom centre), wide and flat
   while the nozzle is low, collapsing as the plume clears the ground. The
   flicker is on a pseudo-element because the box's own transform belongs to
   main.js. */
#ground-jet {
  position: fixed;
  left: 50%;
  top: 0;
  z-index: 1;
  width: 320px;
  height: 20px;
  margin-left: -160px;
  opacity: 0;
  pointer-events: none;
  transform-origin: 50% 100%;
  will-change: transform, opacity;
}
#ground-jet::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 50% 100% at 50% 100%,
    rgba(223,238,255,.78) 0%,
    rgba(147,197,253,.44) 28%,
    rgba(96,165,250,.14) 58%,
    rgba(96,165,250,0) 100%);
  animation: groundJetFlicker 300ms ease-in-out infinite alternate;
}
@keyframes groundJetFlicker {
  from { opacity: .78; transform: scaleX(.96); }
  to   { opacity: 1;   transform: scaleX(1.04); }
}

@media (max-width: 640px) {
  #spaceship { width: 34px; height: 54px; }
  #spaceship-dust { height: min(58vh, 340px); }
}

/* Scroll-linked scale/rotation is exactly the kind of motion this query
   exists for — hide it rather than freeze it at some arbitrary keyframe. */
@media (prefers-reduced-motion: reduce) {
  #spaceship, #spaceship-dust, #ground-scorch, #ground-jet { display: none; }
}

/* =========================================================================
   10. ABOUT / INTRO SECTION
   ========================================================================= */
.about-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xl);
  align-items: center;
}

/* Animated orbit graphic */
.about-visual {
  position: relative;
  height: 440px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.orbit-ring {
  position: absolute;
  border: 1px solid var(--border-subtle);
  border-radius: 50%;
  animation: orbitSpin 20s linear infinite;
  pointer-events: none;
}
.orbit-ring-1 { width: 300px; height: 300px; border-color: rgba(96, 165, 250, 0.1); }
.orbit-ring-2 { width: 440px; height: 440px; border-color: rgba(192, 132, 252, 0.06); animation-direction: reverse; animation-duration: 35s; }
.orbit-dot { position: absolute; top: -4px; left: 50%; width: 8px; height: 8px; background: var(--accent-cyan); border-radius: 50%; transform: translateX(-50%); box-shadow: 0 0 12px var(--accent-cyan); }

@keyframes orbitSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }

/* ── About section: circular profile photo, sits inside inner orbit ring ── */
.about-avatar-circle {
  width: 220px;
  height: 220px;
  border-radius: 50%;
  overflow: hidden;
  position: relative;
  z-index: 1;
  /* Subtle blue glow ring matching accent colour */
  border: 2px solid rgba(96, 165, 250, 0.30);
  box-shadow: 0 0 35px rgba(96, 165, 250, 0.14),
              0 0 0 1px rgba(96, 165, 250, 0.08);
  background: var(--bg-surface);
}

.about-avatar-circle img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  display: block;
}

/* ── About section: the 3D astronaut portrait (public/js/astro-portrait.js) ──
   The stage sits over the circle, its bottom edge on the circle's bottom edge.
   The mask is the union of the circle (inside its 2px border) and a column of
   the circle's width above its centre line: the circle's sides run straight
   up (tangent, no corner), so only his helmet and shoulders break out over the
   top edge and nothing is sliced flat at the sides (the jetpack was).
   Stage 320x330, circle centre at (160, 220), radius 110. The still image
   (.about-astro-poster) uses the same box and mask. */
.about-astro-stage,
.about-astro-poster {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 320px;
  height: 330px;
  margin: -220px 0 0 -160px;
  z-index: 2;
  pointer-events: none;
  -webkit-mask: radial-gradient(circle 108px at 160px 220px, #000 99%, transparent 100%),
                linear-gradient(#000 0 220px, transparent 220px) 52px 0 / 216px 100% no-repeat;
          mask: radial-gradient(circle 108px at 160px 220px, #000 99%, transparent 100%),
                linear-gradient(#000 0 220px, transparent 220px) 52px 0 / 216px 100% no-repeat;
}
.about-astro-stage canvas { display: block; width: 100%; height: 100%; }
/* The still (first paint, slow networks, no WebGL) sits under the live view:
   the canvas fades in over it, then the still drops out behind it. */
/* max-width: none — the global img { max-width: 100% } would squeeze the
   320px still to its parent's width wherever that is narrower (the résumé
   sidebar), flashing a thin astronaut until the 3D view covers it. */
.about-astro-poster { display: block; max-width: none; }
.about-astro-stage { z-index: 3; opacity: 0; transition: opacity 0.7s var(--ease-smooth); }
.about-visual.is-astro-ready .about-astro-stage { opacity: 1; }
.about-visual.is-astro-ready .about-astro-poster { opacity: 0; transition: opacity 0s linear 0.75s; }

/* Résumé sidebar: the same portrait, no orbit rings, circle left-aligned with
   the name, at half size (a 110px circle, the old profile photo's size). The
   box is the stage's height (helmet room above the circle) and the circle's
   width; the 320px stage overhangs it by 50px each side, which is invisible
   because the mask only shows the circle and the column above. zoom (not a
   transform) so the layout box shrinks with it; astro-portrait.js maps
   pointer coordinates through the rendered size, so taps still land. */
.about-visual--resume {
  display: block;
  width: 220px;
  height: 330px;
  zoom: 0.5;
  margin-bottom: calc(var(--space-sm) * 2);   /* x2: zoom halves it too */
}
.about-visual--resume .about-avatar-circle { position: absolute; left: 0; top: 110px; }
.about-visual--resume .about-astro-stage,
.about-visual--resume .about-astro-poster { left: -50px; top: 0; margin: 0; }

/* Fallback initials rendered via data-initials attribute when photo fails to load */
.about-avatar-circle[data-initials]::after {
  content: attr(data-initials);
  position: absolute; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 3rem;
  font-weight: 800;
  color: var(--text-muted);
}

/* Legacy .about-avatar — kept only for any residual references */
.about-avatar {
  width: 200px; height: 200px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 3.5rem;
  font-weight: 800;
  color: var(--text-muted);
  position: relative;
  z-index: 1;
  overflow: hidden;
}
.about-avatar::before {
  content: '';
  position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(96, 165, 250, 0.06), transparent 60%);
}

/* Skill pills */
.skill-pills { display: flex; flex-wrap: wrap; gap: 0.45rem; margin-top: var(--space-sm); }
.skill-pill {
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--border-subtle);
  border-radius: 2px;
  color: var(--text-secondary);
  background: var(--bg-surface);
  transition: border-color var(--dur-fast) var(--ease-smooth),
              color var(--dur-fast) var(--ease-smooth);
}
.skill-pill:hover { border-color: var(--accent-cyan); color: var(--accent-cyan); }

/* =========================================================================
   11. PORTFOLIO GRID
   ========================================================================= */
.portfolio-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1.5rem;
}
.card-featured { grid-column: span 8; }
.card-side     { grid-column: span 4; }
.card-full     { grid-column: span 12; }
.card-half     { grid-column: span 6; }

.project-card {
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  overflow: hidden;
  position: relative;
  transition: border-color var(--dur-med) var(--ease-smooth),
              transform var(--dur-med) var(--ease-smooth),
              box-shadow var(--dur-med) var(--ease-smooth);
  cursor: none;
  display: block;
  color: inherit;
}
@media (hover: none) { .project-card { cursor: pointer; } }
.project-card:hover {
  border-color: var(--border-active);
  transform: translateY(-4px);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4),
              0 0 0 1px rgba(96, 165, 250, 0.1);
}

/* Shimmer sweep across card on hover */
.project-card::before {
  content: '';
  position: absolute;
  top: 0; left: -100%;
  width: 100%; height: 100%;
  background: linear-gradient(90deg, transparent, rgba(96, 165, 250, 0.03), transparent);
  transition: left var(--dur-slow) var(--ease-smooth);
  z-index: 0;
  pointer-events: none;
}
.project-card:hover::before { left: 100%; }

/* Card preview image area */
.card-image {
  width: 100%;
  aspect-ratio: 16/9;
  overflow: hidden;
  background: var(--bg-secondary);
  position: relative;
}
.card-image img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-slow) var(--ease-smooth); }
.project-card:hover .card-image img { transform: scale(1.04); }

/* Gradient placeholder when no image */
.card-image-placeholder {
  width: 100%; height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 4rem;
  font-weight: 800;
  position: relative;
  overflow: hidden;
}

.card-body { padding: var(--space-md); position: relative; z-index: 1; }
.card-category { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--accent-cyan); margin-bottom: 0.5rem; }
.card-title { font-family: var(--font-display); font-size: 1.4rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.65rem; line-height: 1.2; transition: color var(--dur-fast) var(--ease-smooth); }
.project-card:hover .card-title { color: var(--accent-cyan); }
.card-desc { font-size: 0.88rem; color: var(--text-secondary); line-height: 1.65; margin-bottom: var(--space-sm); }
.card-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.card-tag { font-family: var(--font-mono); font-size: 0.62rem; padding: 0.2rem 0.55rem; border: 1px solid var(--border-subtle); border-radius: 2px; color: var(--text-muted); }

/* Arrow icon badge on card */
.card-arrow {
  position: absolute;
  top: var(--space-md); right: var(--space-md);
  width: 38px; height: 38px;
  border: 1px solid rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Near-opaque light chip with a dark glyph: the chip's own contrast is
     fixed, so it reads on light and dark hero images alike. The thin dark
     ring + drop shadow edge it on light images; the halo lifts it on dark. */
  background: rgba(224, 232, 255, 0.88);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 0 0 1px rgba(3, 5, 16, 0.35), 0 2px 10px rgba(1, 3, 12, 0.55), 0 0 14px rgba(224, 232, 255, 0.25);
  color: var(--bg-primary);
  font-weight: 700;
  transition: all var(--dur-med) var(--ease-smooth);
  /* Above .card-lock-overlay (z-index 2) so protected cards don't blur it. */
  z-index: 3;
}
.project-card:hover .card-arrow,
.project-card:focus-visible .card-arrow,
.article-row:hover .card-arrow,
.article-row:focus-visible .card-arrow,
.scroll-card:hover .card-arrow,
.scroll-card:focus-visible .card-arrow {
  background: var(--accent-cyan);
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 1px rgba(3, 5, 16, 0.35), 0 2px 10px rgba(1, 3, 12, 0.55), 0 0 16px rgba(96, 165, 250, 0.55);
  color: var(--bg-primary);
  transform: rotate(45deg);
}
/* An SVG, not the U+2197 arrow character: iOS draws that as a blue emoji. */
.card-arrow svg { width: 16px; height: 16px; display: block; }
.card-arrow--sm svg { width: 13px; height: 13px; }
/* The same badge, a size down, on the smaller article thumbnails (articles
   list and the homepage strip). */
.card-arrow--sm {
  top: 0.6rem; right: 0.6rem;
  width: 30px; height: 30px;
  font-size: 0.85rem;
}

/* =========================================================================
   12. PROCESS / SERVICE CARDS
   ========================================================================= */
.process-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 1.5rem;
  margin-top: var(--space-lg);
}
.process-card {
  padding: var(--space-md);
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  position: relative;
  overflow: hidden;
  transition: border-color var(--dur-med) var(--ease-smooth),
              transform var(--dur-med) var(--ease-smooth);
}
.process-card:hover { border-color: var(--border-active); transform: translateY(-3px); }
.process-card::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 100px; height: 100px;
  background: radial-gradient(circle at 0 0, rgba(96, 165, 250, 0.08), transparent 70%);
  pointer-events: none;
}
.process-number { font-family: var(--font-display); font-size: 3.5rem; font-weight: 800; color: rgba(96, 165, 250, 0.06); line-height: 1; margin-bottom: var(--space-sm); transition: color var(--dur-med) var(--ease-smooth); }
.process-card:hover .process-number { color: rgba(96, 165, 250, 0.14); }
.process-title { font-family: var(--font-display); font-size: 1.15rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.5rem; }
.process-desc { font-size: 0.88rem; color: var(--text-secondary); line-height: 1.65; }

/* =========================================================================
   13. RESUME PAGE
   ========================================================================= */
.resume-layout {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: var(--space-xl);
  align-items: start;
  padding-top: calc(var(--nav-height) + var(--space-lg));
}

/* Resume sidebar avatar — circular, matching About section style, 110×110 */
.resume-avatar {
  width: 110px;
  height: 110px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid rgba(96, 165, 250, 0.28);
  box-shadow: 0 0 20px rgba(96, 165, 250, 0.10);
  margin-bottom: var(--space-sm);
  position: relative;
  background: var(--bg-surface);
  flex-shrink: 0;
}
.resume-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  display: block;
}
/* Fallback initials when photo absent */
.resume-avatar[data-initials]::after {
  content: attr(data-initials);
  position: absolute; inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 1.8rem;
  font-weight: 800;
  color: var(--text-muted);
}
.resume-name { font-family: var(--font-display); font-size: 1.4rem; font-weight: 800; color: var(--text-primary); margin-bottom: 0.2rem; }
.resume-title-tag { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent-cyan); margin-bottom: var(--space-md); }
.resume-contact-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: var(--space-md); }
.resume-contact-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.82rem; color: var(--text-secondary); }
.resume-contact-item a { color: var(--text-secondary); }
.resume-contact-item a:hover { color: var(--accent-cyan); }
.resume-section-heading { font-family: var(--font-display); font-size: 0.7rem; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; color: var(--text-muted); border-top: 1px solid var(--border-subtle); padding-top: var(--space-sm); margin-top: var(--space-sm); margin-bottom: var(--space-sm); }
.resume-download { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; font-family: var(--font-body); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-cyan); border: 1px solid var(--border-active); padding: 0.6rem 1.2rem; border-radius: 2px; margin-top: var(--space-sm); width: 100%; transition: all var(--dur-fast) var(--ease-smooth); }
.resume-download:hover { background: rgba(96, 165, 250, 0.05); color: var(--accent-cyan); }

.resume-main { padding-bottom: var(--space-xl); }

/* Experience timeline */
.timeline { position: relative; padding-left: 1.5rem; }
.timeline::before { content: ''; position: absolute; left: 0; top: 8px; width: 1px; height: calc(100% - 16px); background: linear-gradient(to bottom, var(--accent-cyan), transparent); }
.timeline-item { position: relative; padding-bottom: var(--space-lg); }
.timeline-item::before { content: ''; position: absolute; left: -1.5rem; top: 8px; width: 7px; height: 7px; background: var(--accent-cyan); border-radius: 50%; transform: translateX(-3px); box-shadow: 0 0 8px var(--accent-cyan); }
.timeline-period { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.15em; color: var(--accent-cyan); margin-bottom: 0.2rem; }
.timeline-role { font-family: 'Merriweather', serif; font-size: 1.15rem; font-weight: 700; line-height: 1.35; color: var(--text-primary); margin-bottom: 0.1rem; }
/* The scope line under a role: what the job covered beyond its title. */
.timeline-scope { font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em; line-height: 1.6; color: var(--accent-cyan); opacity: 0.85; margin: 0.3rem 0 0.75rem; }
.timeline-company:has(+ .timeline-scope) { margin-bottom: 0; }
/* Achievement bullets under each role. */
.timeline-points { list-style: none; display: grid; gap: 0.45rem; }
.timeline-points li { position: relative; padding-left: 1.1rem; font-size: 0.88rem; line-height: 1.65; color: var(--text-secondary); }
.timeline-points li::before { content: ''; position: absolute; left: 0.1rem; top: 0.68em; width: 5px; height: 5px; border-radius: 50%; background: var(--accent-rose); }
.resume-references { margin-top: var(--space-md); font-size: 0.85rem; line-height: 1.6; color: var(--text-secondary); }
.timeline-company { font-size: 0.88rem; color: var(--text-secondary); margin-bottom: 0.65rem; }

/* Education / extras cards */
.edu-card { padding: var(--space-sm) var(--space-md); background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); margin-bottom: 1rem; }
.edu-degree { font-family: var(--font-display); font-size: 0.95rem; font-weight: 700; color: var(--text-primary); margin-bottom: 0.2rem; }
.edu-school { font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-secondary); }
.edu-year { font-family: var(--font-mono); font-size: 0.65rem; color: var(--accent-cyan); margin-top: 4px; }

/* =========================================================================
   14. CONTACT PAGE
   ========================================================================= */
.contact-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-xl);
  align-items: start;
  padding-top: calc(var(--nav-height) + var(--space-lg));
}
.contact-lead { font-family: var(--font-display); font-size: clamp(2rem, 4vw, 3.5rem); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; margin-bottom: var(--space-md); color: var(--text-primary); }
.contact-body { font-size: 1rem; color: var(--text-secondary); line-height: 1.8; margin-bottom: var(--space-lg); max-width: 480px; }
.contact-links { display: flex; flex-direction: column; gap: 1.1rem; }
.contact-link-item { display: flex; align-items: center; gap: 1rem; padding: 0.9rem 1.1rem; background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); text-decoration: none; color: var(--text-secondary); transition: border-color var(--dur-med) var(--ease-smooth), transform var(--dur-med) var(--ease-smooth), color var(--dur-med) var(--ease-smooth); }
.contact-link-item:hover { border-color: var(--border-active); color: var(--text-primary); transform: translateX(4px); }
.contact-link-icon { width: 38px; height: 38px; background: var(--bg-surface); border: 1px solid var(--border-subtle); border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: border-color var(--dur-fast) var(--ease-smooth); }
.contact-link-item:hover .contact-link-icon { border-color: var(--accent-cyan); }
.contact-link-label { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent-cyan); display: block; margin-bottom: 2px; }
.contact-link-value { font-size: 0.88rem; font-weight: 500; }
/* Stacked layout (phones/tablets): the three full-width cards pushed the form
   a screen down. Show them as a row of icon buttons instead; the label and
   address stay in the DOM, visually hidden, as each link's accessible name. */
@media (max-width: 1024px) {
  .contact-body { margin-bottom: var(--space-md); }
  .contact-links { flex-direction: row; gap: 0.75rem; }
  .contact-link-item { padding: 0; background: none; border: none; }
  .contact-link-item:hover { transform: none; }
  .contact-link-icon { width: 52px; height: 52px; border-radius: 12px; color: var(--accent-cyan); }
  .contact-link-icon svg { width: 22px; height: 22px; }
  .contact-link-item > div:last-child {
    position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden;
    clip: rect(0, 0, 0, 0); white-space: nowrap;
  }
}

/* Contact form */
.contact-form-wrap { background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); padding: var(--space-lg); }

/* All form labels are visible — WCAG 1.3.1 */
.form-group { margin-bottom: var(--space-sm); }
.form-label { display: block; font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: 0.5rem; }
/* "(optional)" beside a label: dimmer and lighter than the label, still
   5:1 on the form card. Required fields carry no marker (the browser and
   aria-required flag them); only the exception is labelled. */
.form-optional { font-family: var(--font-body); font-weight: 300; font-size: 0.72rem; letter-spacing: 0.02em; text-transform: none; color: #7987a0; margin-left: 0.35rem; }
.form-input, .form-textarea, .form-select {
  width: 100%;
  background: var(--bg-secondary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.92rem;
  padding: 0.82rem 1rem;
  transition: border-color var(--dur-fast) var(--ease-smooth),
              box-shadow var(--dur-fast) var(--ease-smooth);
  appearance: none;
  -webkit-appearance: none;
}
.form-input::placeholder,
.form-textarea::placeholder { color: var(--text-muted); }
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
}
/* Custom select (initCustomSelects() in main.js): .form-select's box, a
   chevron, and a listbox styled to match the site instead of the OS picker. */
.cselect { position: relative; }
.cselect-trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  cursor: pointer; user-select: none; -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.cselect-trigger.is-placeholder .cselect-value { color: var(--text-secondary); }
.cselect-chevron { width: 18px; height: 18px; flex-shrink: 0; color: var(--text-secondary); transition: transform var(--dur-fast) var(--ease-smooth); }
.cselect.is-open .cselect-trigger { border-color: var(--accent-cyan); box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1); }
.cselect.is-open .cselect-chevron { transform: rotate(180deg); color: var(--accent-cyan); }
/* Keyboard focus gets a visible ring (WCAG 2.4.7); .form-select:focus already
   draws the border + glow, this adds an outline for keyboard users. */
.cselect-trigger:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; }
.cselect-list {
  position: absolute; z-index: 20;
  top: calc(100% + 6px); left: 0; right: 0;
  max-height: min(320px, 50vh); overflow-y: auto; overscroll-behavior: contain;
  scroll-margin-bottom: calc(var(--tab-bar-height, 0px) + env(safe-area-inset-bottom) + 12px);
  margin: 0; padding: 0.35rem; list-style: none;
  background: var(--bg-surface);
  border: 1px solid var(--border-active);
  border-radius: var(--radius-sm);
  box-shadow: 0 14px 40px rgba(1, 3, 12, 0.6);
  animation: cselect-in var(--dur-fast) var(--ease-smooth);
}
.cselect-list[hidden] { display: none; }
@keyframes cselect-in { from { opacity: 0; transform: translateY(-4px); } }
@media (prefers-reduced-motion: reduce) { .cselect-list { animation: none; } .cselect-chevron { transition: none; } }
.cselect-option {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  min-height: 44px;   /* touch target (WCAG 2.5.5) */
  padding: 0.6rem 0.8rem;
  border-radius: 3px;
  font-family: var(--font-body); font-size: 0.92rem;
  color: var(--text-primary);
  cursor: pointer;
}
.cselect-option.is-active { background: rgba(96, 165, 250, 0.14); }
.cselect-option[aria-selected="true"] { color: var(--accent-cyan); }
/* Check mark on the chosen option: shape, not only colour (WCAG 1.4.1). */
.cselect-option[aria-selected="true"]::after {
  content: ''; width: 0.4rem; height: 0.75rem; flex-shrink: 0; margin-right: 0.2rem;
  border: solid currentColor; border-width: 0 2px 2px 0; transform: rotate(45deg) translateY(-2px);
}
@media (hover: hover) { .cselect-option:hover { background: rgba(96, 165, 250, 0.14); } }
@media (forced-colors: active) {
  .cselect-option.is-active, .cselect-option[aria-selected="true"] { outline: 2px solid Highlight; }
}

/* WCAG 3.3.1 — Error identification */
.form-input[aria-invalid="true"],
.form-textarea[aria-invalid="true"] { border-color: #f87171; }
.form-error { font-size: 0.78rem; color: #f87171; margin-top: 0.3rem; display: none; }
.form-error.visible { display: block; }
.form-textarea { min-height: 130px; resize: vertical; line-height: 1.6; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }

/* Success state */
.form-note { margin-top: 1rem; font-size: 0.85rem; line-height: 1.6; color: var(--text-secondary); }
.form-note a { color: var(--accent-cyan); }

/* =========================================================================
   16. FOOTER
   ========================================================================= */
.site-footer { border-top: 1px solid var(--border-subtle); padding: var(--space-md) 0; position: relative; z-index: 1; }
.footer-inner {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 var(--space-md);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
}
/* Brand + tagline inline on same row */
.footer-brand-group {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.footer-brand { font-family: var(--font-display); font-size: 1rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-primary); line-height: 1; }
.footer-brand span { color: var(--accent-cyan); }
/* Vertical separator between brand and tagline */
.footer-sep { color: var(--border-active); font-size: 0.75rem; }
/* Tagline — italic serif feel, clearly different from mono links */
.footer-tagline {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-style: italic;
  font-weight: 300;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.footer-links { display: flex; align-items: center; gap: var(--space-md); }
.footer-links a { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-secondary); transition: color var(--dur-fast) var(--ease-smooth); }
.footer-links a:hover { color: var(--accent-cyan); }
/* Text links and the icon row (contact, LinkedIn, Behance) side by side on
   desktop; stacked and centred on phones (see the 768px block). */
.footer-nav { display: flex; align-items: center; gap: var(--space-md); }
.footer-social { display: flex; align-items: center; gap: 0.35rem; }
.footer-social a {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  color: #fff;
  opacity: 0.85;
  transition: color var(--dur-fast) var(--ease-smooth), opacity var(--dur-fast) var(--ease-smooth);
}
.footer-social a:hover, .footer-social a:focus-visible { color: var(--accent-cyan); opacity: 1; }
.footer-social svg { width: 18px; height: 18px; }
.footer-copy { font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-muted); }
@media (max-width: 768px) {
  .footer-brand-group { justify-content: center; }
  .footer-inner { justify-content: center; text-align: center; }
}

/* =========================================================================
   17. SCROLL REVEAL ANIMATIONS
   ========================================================================= */
.reveal { opacity: 0; transform: translateY(30px); transition: opacity var(--dur-slow) var(--ease-sharp), transform var(--dur-slow) var(--ease-sharp); }
.reveal.visible { opacity: 1; transform: translateY(0); }
.reveal-left { opacity: 0; transform: translateX(-30px); transition: opacity var(--dur-slow) var(--ease-sharp), transform var(--dur-slow) var(--ease-sharp); }
.reveal-left.visible { opacity: 1; transform: translateX(0); }
.delay-1 { transition-delay: 0.1s; } .delay-2 { transition-delay: 0.2s; } .delay-3 { transition-delay: 0.3s; }
.delay-4 { transition-delay: 0.4s; } .delay-5 { transition-delay: 0.5s; }

/* WCAG 2.1 §2.3.3 — Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .reveal, .reveal-left,
  .hero-headline .line-inner,
  .hero-desc, .hero-ctas {
    animation: none !important;
    transition: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
  .orbit-ring { animation: none; }
}

/* =========================================================================
   18. PAGE TRANSITIONS
   The browser cross-fades between pages itself (View Transitions API): no
   script, no delay before navigating, and browsers without support simply
   navigate normally.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

/* =========================================================================
   19. MOBILE FULLSCREEN MENU
   ========================================================================= */
.mobile-menu { position: fixed; inset: 0; background: var(--bg-primary); z-index: 99; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-sm); opacity: 0; pointer-events: none; transition: opacity var(--dur-med) var(--ease-smooth); }
/* Closed, it is also hidden from the keyboard and screen readers (WCAG 2.4.3):
   visibility flips after the fade out, and at once on the way in. */
.mobile-menu { visibility: hidden; transition: opacity var(--dur-med) var(--ease-smooth), visibility 0s linear var(--dur-med); }
.mobile-menu.open { opacity: 1; pointer-events: all; visibility: visible; transition: opacity var(--dur-med) var(--ease-smooth), visibility 0s; }
.mobile-menu a { font-family: var(--font-display); font-size: clamp(2rem, 8vw, 4rem); font-weight: 800; letter-spacing: -0.02em; color: var(--text-secondary); transition: color var(--dur-fast) var(--ease-smooth); }
.mobile-menu a:hover { color: var(--accent-cyan); }

/* =========================================================================
   20. UTILITY CLASSES
   ========================================================================= */
/* Screen-reader-only content — WCAG 1.3.1 */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border-width: 0; }
.text-center { text-align: center; }

/* Vertical rhythm helpers. These exist as real classes because Helmet's
   style-src has no 'unsafe-inline' — a style="margin-top:2rem" attribute is
   silently dropped by the browser on every public page. */
.mt-sm { margin-top: var(--space-sm); }   /* 1rem */
.mt-md { margin-top: var(--space-md); }   /* 2rem */
.divider { width: 100%; height: 1px; background: var(--border-subtle); margin: var(--space-lg) 0; }
.divider-glow { height: 1px; background: linear-gradient(to right, transparent, var(--accent-cyan), transparent); margin: var(--space-lg) 0; opacity: 0.3; }

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }

/* =========================================================================
   21. RESPONSIVE BREAKPOINTS
   ========================================================================= */
@media (max-width: 1024px) {
  .about-grid { grid-template-columns: 1fr; gap: var(--space-lg); }
  .about-visual { display: none; }
  .resume-layout { grid-template-columns: 1fr; }
  .contact-layout { grid-template-columns: 1fr; }
  .card-featured, .card-side, .card-half { grid-column: span 12; }
}

@media (max-width: 768px) {
  :root { --nav-height: 64px; --space-xl: 4rem; --space-lg: 3rem; }
  .nav-links { display: none; }
  .nav-toggle { display: flex; }
  .hero-ctas { flex-direction: column; align-items: flex-start; }
  .hero-ctas .btn { width: 100%; justify-content: center; }
  .portfolio-grid { grid-template-columns: 1fr; }
  .card-featured, .card-side, .card-half, .card-full { grid-column: span 1; }
  .process-grid { grid-template-columns: 1fr; }
  .footer-inner { flex-direction: column; text-align: center; }
  .footer-links { flex-wrap: wrap; justify-content: center; gap: 0.5rem var(--space-md); }
  .footer-nav { flex-direction: column; gap: 0.5rem; }
  .footer-social a { width: 44px; height: 44px; }   /* touch-sized targets */
  .footer-social svg { width: 20px; height: 20px; }
  .contact-form-wrap { padding: var(--space-md); }
  .form-row { grid-template-columns: 1fr; }
}

@media (max-width: 480px) {
  /* Scale the headline so its longest line, "the horizon.", fits the column
     (viewport less 2rem padding a side). It measures 8.71em in Safari/WebKit
     and 8.38em in Chrome; 9.2 leaves margin for the sunlight rim filter. */
  .hero-headline { font-size: min(2.6rem, calc((100vw - 4rem) / 9.2)); }

  /* Eyebrow: one line instead of two, and a dark halo so it stays legible
     where the planet's bright limb passes behind it. */
  .hero-orbit-label {
    font-size: 0.62rem;
    letter-spacing: 0.18em;
    gap: 0.6rem;
    white-space: nowrap;
    opacity: 1;
    text-shadow: 0 0 4px var(--bg-primary), 0 0 10px var(--bg-primary), 0 0 18px var(--bg-primary);
  }
  .hero-orbit-label::before { width: 20px; }
}

/* ==========================================================================
   CMS-DRIVEN CONTENT — added for dynamic portfolio loading
   ========================================================================== */

/* ── Password-protected card overlay ──────────────────────────────────────── */
/* The card image gets blurred; a lock icon overlays the centre */
.card-image--protected img {
  filter: blur(8px);
  transform: scale(1.04); /* hide blur edge artefacts */
  transition: filter var(--dur-slow) var(--ease-smooth);
}
.project-card:hover .card-image--protected img {
  filter: blur(6px); /* subtly reduce blur on hover as affordance */
}

/* Lock overlay — centred absolutely over the image */
/* Scrim is deliberately heavy: it makes the badge's backdrop predictable
   regardless of how light the hero image behind it is. */
.card-lock-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  background: rgba(3, 5, 16, 0.6);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
/* WCAG 2.1 §1.4.11 — the badge is an opaque disc, so the icon's contrast is
   fixed by these two colours rather than by whatever photo sits underneath:
   #e0e8ff on #030510 is 16.6:1, and the accent ring reads 8:1 against it. */
.card-lock-icon {
  width: 52px;
  height: 52px;
  background: var(--bg-primary);
  border: 2px solid var(--accent-cyan);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-primary);
  box-shadow: 0 0 0 1px rgba(3, 5, 16, 0.9), 0 0 22px rgba(96, 165, 250, 0.35);
  transition: transform var(--dur-med) var(--ease-smooth),
              border-color var(--dur-med) var(--ease-smooth);
}
.card-lock-icon svg { width: 24px; height: 24px; }
.card-lock-icon--sm { width: 40px; height: 40px; }
.card-lock-icon--sm svg { width: 19px; height: 19px; }

.project-card:hover .card-lock-icon,
.scroll-card:hover .card-lock-icon {
  transform: scale(1.08);
  border-color: #fff;
}

/* ── Password gate modal ─────────────────────────────────────────────────── */
/* Shared modal component used when clicking password-protected cards */
#pw-gate-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  z-index: 2000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
}
#pw-gate-overlay.open {
  display: flex;
  animation: fadeIn 0.2s var(--ease-smooth);
}
/* Same card as a full page: a whole-page-protected post (passwordGatePage()). */
.pw-gate-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: calc(var(--nav-height) + var(--space-md)) var(--space-md) var(--space-lg);
}
.pw-gate-card {
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  max-width: 440px;
  width: 100%;
  text-align: center;
  animation: fadeInUp 0.25s var(--ease-sharp);
}
/* The same line-drawn lock as the card badges (CMS_LOCK_SVG), in white:
   the 🔒 emoji drew in the OS's own gold artwork and couldn't be recoloured. */
.pw-gate-icon { margin-bottom: 1.25rem; color: #fff; }
.pw-gate-icon svg { width: 40px; height: 40px; display: block; margin: 0 auto; }
.pw-gate-title {
  font-family: 'Merriweather', serif;   /* project titles are long; matches the card titles */
  font-size: 1.2rem;
  font-weight: 700;
  line-height: 1.35;
  color: var(--text-primary);
  margin-bottom: 0.4rem;
}
.pw-gate-desc {
  font-size: 0.88rem;
  color: var(--text-secondary);
  line-height: 1.65;
  margin-bottom: 1.5rem;
}
.pw-gate-input {
  width: 100%;
  background: var(--bg-secondary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 0.95rem;
  padding: 0.82rem 1rem;
  margin-bottom: 0.75rem;
  text-align: center;
  letter-spacing: 0.05em;
  transition: border-color var(--dur-fast) var(--ease-smooth),
              box-shadow var(--dur-fast) var(--ease-smooth);
}
.pw-gate-input:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.12);
}
.pw-gate-error {
  font-size: 0.78rem;
  color: #f87171;
  margin-bottom: 0.65rem;
  min-height: 1.1em;
}
.pw-gate-actions {
  display: flex;
  gap: 0.6rem;
  justify-content: center;
}

/* ── Horizontal portfolio scroll strip (home page, above CTA) ────────────── */
.portfolio-scroll-section {
  padding: var(--space-lg) 0;
  position: relative;
  z-index: 1;
}
.scroll-strip-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
  gap: 0.75rem;
}

/* Outer wrapper clips the track and enables scrolling */
.scroll-track-outer {
  overflow-x: auto;
  overflow-y: visible;
  /* Custom scrollbar — subtle, on-brand */
  scrollbar-width: thin;
  scrollbar-color: var(--border-active) transparent;
  /* overflow-x:auto forces overflow-y to clip, so reserve headroom for the
     hover lift (translateY(-3px)); the negative margin keeps layout unchanged */
  padding-top: 0.5rem;
  margin-top: -0.5rem;
  padding-bottom: 0.5rem;
  /* Fade edges to hint at scrollability */
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 3%, black 97%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, black 3%, black 97%, transparent 100%);
}
.scroll-track-outer::-webkit-scrollbar { height: 4px; }
.scroll-track-outer::-webkit-scrollbar-track { background: transparent; }
.scroll-track-outer::-webkit-scrollbar-thumb { background: var(--border-active); border-radius: 2px; }

/* The inner flex track */
.scroll-track {
  display: flex;
  gap: 1rem;
  padding: 0 var(--space-md);
  /* Don't wrap — cards stay in a single row */
  width: max-content;
  align-items: stretch;
}

/* Each card in the scroll strip */
.scroll-card {
  width: 260px;
  flex-shrink: 0;
  background: var(--bg-card);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  overflow: hidden;
  text-decoration: none;
  color: inherit;
  display: block;
  transition: border-color var(--dur-med) var(--ease-smooth),
              transform var(--dur-med) var(--ease-smooth),
              box-shadow var(--dur-med) var(--ease-smooth);
  cursor: none;
  position: relative;
}
@media (hover: none) { .scroll-card { cursor: pointer; } }
.scroll-card:hover {
  border-color: var(--border-active);
  transform: translateY(-3px);
  box-shadow: 0 12px 36px rgba(0,0,0,0.35);
}

/* Image area in scroll card */
.scroll-card-image {
  width: 100%;
  aspect-ratio: 16/9;
  overflow: hidden;
  background: var(--bg-secondary);
  position: relative;
}
.scroll-card-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-smooth);
}
.scroll-card:hover .scroll-card-image img { transform: scale(1.05); }
.scroll-card-placeholder {
  width: 100%; height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.5rem;
  opacity: 0.1;
}

/* Body of scroll card */
.scroll-card-body { padding: 0.85rem 1rem; }
.scroll-card-cat {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin-bottom: 0.3rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.scroll-card-title {
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.25;
  margin-bottom: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--dur-fast) var(--ease-smooth);
}
.scroll-card:hover .scroll-card-title { color: var(--accent-cyan); }

/* Project card titles (homepage Selected Work, the Portfolio page grid) and
   the Articles strip teasers are long, multi-word titles at small sizes,
   which the display face doesn't carry well. Merriweather, the case-study
   heading face, reads properly. Same size everywhere (.card-title, 1.4rem). */
#selected-work-grid .card-title,
#portfolio-grid .card-title,
#articles-scroll-track .scroll-card-title {
  font-family: 'Merriweather', serif;
  font-weight: 700;
  letter-spacing: 0;
}
#selected-work-grid .card-title,
#portfolio-grid .card-title { line-height: 1.35; }
#articles-scroll-track .scroll-card-title { line-height: 1.4; }

/* Loading skeleton for CMS-driven sections */
.card-skeleton {
  background: linear-gradient(90deg, var(--bg-card) 25%, var(--bg-surface) 50%, var(--bg-card) 75%);
  background-size: 200% 100%;
  animation: skeletonShimmer 1.5s infinite;
  border-radius: var(--radius-md);
}
@keyframes skeletonShimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}
.skeleton-card { height: 280px; }

/* ── CMS empty state ─────────────────────────────────────────────────────── */
.cms-empty {
  grid-column: 1 / -1;
  text-align: center;
  padding: var(--space-lg);
  color: var(--text-muted);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .card-skeleton { animation: none; }
  .card-image--protected img { filter: blur(8px); }
}


/* ==========================================================================
   ERROR PAGES — 404 (Dark Side of the Moon) and 403 (Radar)
   All styles here because Helmet sends style-src 'self' as an HTTP header,
   which blocks inline <style> blocks on server-rendered pages.
   ========================================================================== */

/* ── Shared layout ── */
body:has(.error-scene) {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.error-scene {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 2rem;
  animation: errFadeUp .9s cubic-bezier(.16,1,.3,1) both;
}
@keyframes errFadeUp {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ── Shared text ── */
.err-code {
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--accent-cyan);
  margin-bottom: 1.25rem;
  opacity: .8;
}
.err-code--403 { color: #f87171; }

.err-headline {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 4vw, 2.4rem);
  font-weight: 800;
  color: var(--text-primary);
  line-height: 1.15;
  letter-spacing: -.02em;
  margin-bottom: 1.25rem;
  max-width: 520px;
}

.err-sub {
  font-family: 'Mulish', sans-serif;
  font-size: 1rem;
  color: var(--text-secondary);
  line-height: 1.7;
  margin-bottom: 0;
}
.err-sub a {
  color: var(--accent-cyan);
  text-decoration: none;
  border-bottom: 1px solid rgba(96,165,250,.35);
  transition: color .15s, border-color .15s;
}
.err-sub a:hover { color: #fff; border-color: rgba(255,255,255,.6); }

.err-btn {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-family: var(--font-display);
  font-size: .82rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
  color: #030510;
  background: var(--accent-cyan);
  padding: .85rem 2rem;
  border-radius: 2px;
  margin-top: 2rem;
  transition: background .15s, transform .15s;
}
.err-btn:hover { background: #fff; transform: translateY(-2px); }

/* ── Nebula glows (404) ── */
.nebula-left {
  position: fixed; top: 10%; left: -8%;
  width: 500px; height: 500px;
  pointer-events: none; z-index: 0;
  background: radial-gradient(circle, rgba(192,132,252,.04) 0%, transparent 65%);
}
.nebula-right {
  position: fixed; bottom: 5%; right: -8%;
  width: 400px; height: 400px;
  pointer-events: none; z-index: 0;
  background: radial-gradient(circle, rgba(96,165,250,.04) 0%, transparent 65%);
}

/* ── 404: Orbit ring ── */
.orbit-stage {
  position: relative;
  width: 280px; height: 280px;
  margin-bottom: 3rem;
}
.orbit-ring-404 {
  position: absolute; inset: 0;
  border: 1px solid rgba(96,165,250,.15);
  border-radius: 50%;
}
/* Cyan dot orbiting the ring */
.orbit-ring-404::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 8px; height: 8px;
  margin: -4px 0 0 -4px;
  background: var(--accent-cyan);
  border-radius: 50%;
  box-shadow: 0 0 12px var(--accent-cyan);
  transform-origin: 0 0;
  animation: orbitDot 8s linear infinite;
}
@keyframes orbitDot {
  from { transform: rotate(0deg)   translateX(140px); }
  to   { transform: rotate(360deg) translateX(140px); }
}

/* ── 404: Moon sphere ── */
.moon {
  position: absolute;
  top: 50%; left: 50%;
  width: 140px; height: 140px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(
    circle at 38% 38%,
    #d8cfc4 0%, #b8b0a4 30%, #8a8278 55%, #4a4440 80%, #1a1410 100%
  );
  box-shadow:
    -6px -6px 28px rgba(255,240,200,.18),
     8px  8px 24px rgba(0,0,0,.9),
     0    0   40px rgba(96,165,250,.06);
}
/* Dark-side shadow sweeping across — the phase animation */
.moon::before {
  content: '';
  position: absolute; inset: 0;
  border-radius: 50%;
  animation: moonPhase 12s ease-in-out infinite;
}
@keyframes moonPhase {
  0%   { box-shadow: inset  -2px 0 30px  4px rgba(2,4,14,.05); }
  25%  { box-shadow: inset -20px 0 40px 12px rgba(2,4,14,.55); }
  50%  { box-shadow: inset -50px 0 60px 20px rgba(2,4,14,.95); }
  75%  { box-shadow: inset -20px 0 40px 12px rgba(2,4,14,.55); }
  100% { box-shadow: inset  -2px 0 30px  4px rgba(2,4,14,.05); }
}
/* Crater details */
.moon::after {
  content: '';
  position: absolute; inset: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle at 30% 45%, rgba(0,0,0,.18) 0%, transparent 10%),
    radial-gradient(circle at 55% 30%, rgba(0,0,0,.14) 0%, transparent  7%),
    radial-gradient(circle at 65% 60%, rgba(0,0,0,.20) 0%, transparent 12%),
    radial-gradient(circle at 22% 65%, rgba(0,0,0,.12) 0%, transparent  6%),
    radial-gradient(circle at 72% 42%, rgba(0,0,0,.10) 0%, transparent  5%);
}

/* ── 403: Radar ── */
.radar-wrap {
  position: relative;
  width: 200px; height: 200px;
  margin-bottom: 3rem;
}
/* Outer ring + two inner rings via pseudo-elements */
.radar-circle {
  position: absolute; inset: 0;
  border: 1px solid rgba(96,165,250,.18);
  border-radius: 50%;
}
.radar-circle::before {
  content: '';
  position: absolute; inset: 20%;
  border: 1px solid rgba(96,165,250,.12);
  border-radius: 50%;
}
.radar-circle::after {
  content: '';
  position: absolute; inset: 42%;
  border: 1px solid rgba(96,165,250,.12);
  border-radius: 50%;
  background: rgba(96,165,250,.04);
}
/* Cross-hairs */
.radar-crosshair { position: absolute; inset: 0; }
.radar-crosshair::before {
  content: '';
  position: absolute;
  left: 50%; top: 0; bottom: 0; width: 1px;
  background: rgba(96,165,250,.1);
  transform: translateX(-50%);
}
.radar-crosshair::after {
  content: '';
  position: absolute;
  top: 50%; left: 0; right: 0; height: 1px;
  background: rgba(96,165,250,.1);
  transform: translateY(-50%);
}
/* Rotating sweep */
.radar-sweep {
  position: absolute; inset: 0;
  border-radius: 50%; overflow: hidden;
  animation: radarSpin 3s linear infinite;
}
@keyframes radarSpin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
/* Sweep line */
.radar-sweep::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 50%; height: 1px;
  transform-origin: left center;
  background: linear-gradient(to right, rgba(96,165,250,.9), transparent);
}
/* Fading wedge behind sweep */
.radar-sweep::before {
  content: '';
  position: absolute; inset: 0;
  background: conic-gradient(from 0deg, rgba(96,165,250,.1) 0deg, transparent 65deg);
}
/* Red ✗ pulsing in sync */
.radar-denied {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(.7);
  font-size: 1.8rem;
  color: #f87171;
  opacity: 0;
  animation: deniedPulse 3s ease-in-out .8s infinite;
}
@keyframes deniedPulse {
  0%, 60%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(.7); }
  72%, 85%      { opacity: 1; transform: translate(-50%, -50%) scale(1);  }
}

/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .moon::before, .orbit-ring-404::after,
  .radar-sweep, .radar-denied, .error-scene {
    animation: none;
  }
  .moon::before { box-shadow: inset -25px 0 45px 15px rgba(2,4,14,.7); }
}

/* ==========================================================================
   PORTFOLIO PAGE (public/portfolio.html)
   Lives here (not an inline <style> block) because Helmet sends
   style-src 'self' as an HTTP header on public pages, which blocks
   inline <style> blocks outright — see the ERROR PAGES note above.
   ========================================================================== */

/* Compact intro banner (not a full-viewport hero) — this is a listing page, the grid
   below is the actual content, so it shouldn't require a full scroll to reach.
   Clearance below the nav plus padding, not 100vh centering. */
.portfolio-hero {
  padding: calc(var(--nav-height) + var(--space-xl)) 0 var(--space-lg);
  position: relative;
  overflow: hidden;
}
/* Ambient nebula glow top-right */
.portfolio-hero::before {
  content: '';
  position: absolute;
  top: 10%; right: -10%;
  width: 500px; height: 500px;
  background: radial-gradient(circle, rgba(96,165,250,0.04) 0%, transparent 65%);
  pointer-events: none;
}

/* Filter tab row */
.filter-tabs {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: var(--space-lg);
}
.filter-tab {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 0.45rem 1rem;
  border: 1px solid var(--border-subtle);
  border-radius: 2px;
  background: transparent;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-smooth);
}
.filter-tab:hover,
.filter-tab.active {
  color: var(--accent-cyan);
  border-color: var(--border-active);
  background: rgba(96,165,250,0.04);
}
/* Stagger the reveal animation for each card */
.portfolio-grid .project-card:nth-child(1)  { transition-delay: 0s;    }
.portfolio-grid .project-card:nth-child(2)  { transition-delay: 0.06s; }
.portfolio-grid .project-card:nth-child(3)  { transition-delay: 0.12s; }
.portfolio-grid .project-card:nth-child(4)  { transition-delay: 0.18s; }
.portfolio-grid .project-card:nth-child(5)  { transition-delay: 0.24s; }
.portfolio-grid .project-card:nth-child(6)  { transition-delay: 0.30s; }
.portfolio-grid .project-card:nth-child(7)  { transition-delay: 0.36s; }
.portfolio-grid .project-card:nth-child(8)  { transition-delay: 0.42s; }

/* ==========================================================================
   ARTICLES PAGE (public/articles.html)
   Lives here for the same reason as the PORTFOLIO PAGE section above.
   ========================================================================== */

/* Compact intro banner — same fix as .portfolio-hero (was 100vh + centered,
   which pushes the actual content far below the fold on a listing page). */
.articles-hero {
  padding: calc(var(--nav-height) + var(--space-xl)) 0 var(--space-lg);
  position: relative;
  overflow: hidden;
}
.articles-hero::before {
  content: '';
  position: absolute;
  top: 10%; left: -8%;
  width: 500px; height: 500px;
  background: radial-gradient(circle, rgba(192,132,252,0.04) 0%, transparent 65%);
  pointer-events: none;
}

/* Article list — clean editorial layout */
.article-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Each article row */
.article-row {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  border-bottom: 1px solid var(--border-subtle);
  text-decoration: none;
  color: inherit;
  align-items: start;
  transition: background var(--dur-fast) var(--ease-smooth);
  position: relative;
  cursor: none;
}
@media (hover: none) { .article-row { cursor: pointer; } }

.article-row:hover .article-title { color: var(--accent-cyan); }

/* Left column — hero thumbnail (when the post has one) above the date */
.article-aside {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}
.article-thumb {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
}
/* The thumb's resting 1px border, drawn as an overlay rather than a real
   border so the hover line below can sit exactly on it (a real border is
   outside the padding box that overflow: hidden clips the overlay to, which
   left the blue line inside it and read as 2px). */
.article-thumb::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  border: 1px solid var(--border-subtle);
  border-radius: inherit;
  pointer-events: none;
}
.article-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--dur-med) var(--ease-smooth);
}
.article-row:hover .article-thumb img { transform: scale(1.04); }
/* Protected rows keep the blur soft on hover, matching the portfolio cards */
.article-row:hover .card-image--protected img { filter: blur(6px); }

/* Date column */
.article-date {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  color: var(--text-muted);
  padding-top: 0.2rem;
  white-space: nowrap;
}

/* Title + excerpt column */
.article-title {
  font-family: 'Merriweather', serif;   /* matches the portfolio card titles */
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--text-primary);
  line-height: 1.35;
  margin-bottom: 0.4rem;
  transition: color var(--dur-fast) var(--ease-smooth);
}
.article-excerpt {
  font-size: 0.88rem;
  color: var(--text-secondary);
  line-height: 1.65;
  margin-bottom: 0.6rem;
}
.article-cats {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--accent-cyan);
}

/* "Read more" call to action, on its own line under the summary */
.article-more {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0.2rem;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--text-secondary);
  transition: color var(--dur-med) var(--ease-smooth);
}
.article-arrow {
  display: inline-block;
  font-size: 1rem;
  letter-spacing: 0;
  transition: transform var(--dur-med) var(--ease-smooth);
}
.article-row:hover .article-more,
.article-row:focus-visible .article-more { color: var(--accent-cyan); }
.article-row:hover .article-arrow,
.article-row:focus-visible .article-arrow { transform: translateX(5px); }

/* Password-protected badge on article rows */
.article-lock svg { width: 1.15em; height: 1.15em; flex-shrink: 0; }
.article-lock {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  border: 1px solid var(--border-subtle);
  padding: 0.15rem 0.5rem;
  border-radius: 2px;
  margin-left: 0.5rem;
}

@media (max-width: 768px) {
  .article-row {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto;
  }
  .article-aside {
    grid-row: 1;
    grid-column: 1;
    margin-bottom: 0.25rem;
  }
  /* Full card width, like the portfolio cards, with the full-size arrow badge. */
  .article-thumb .card-arrow--sm { top: var(--space-sm); right: var(--space-sm); width: 38px; height: 38px; }
  .article-thumb .card-arrow--sm svg { width: 16px; height: 16px; }
  .article-content { grid-row: 2; grid-column: 1; }
}

/* ==========================================================================
   CONTACT PAGE — extra styles (public/contact.html)
   Base .contact-layout etc. already live in section 15 above; these are the
   ones that were stuck in contact.html's CSP-blocked inline <style> block.
   ========================================================================== */
.form-title {
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 800;
  color: var(--text-primary);
  /* Matches the gap between the last field and the submit button (1rem + 0.5rem). */
  margin-bottom: 1.5rem;
}

/* ==========================================================================
   RESUME PAGE — extra styles (public/resume.html)
   Base .resume-layout etc. already live in section 14 above; these are the
   ones that were stuck in resume.html's CSP-blocked inline <style> block.
   Tag pills are scoped to .tag-cloud since .cms-article also has a .tag
   class (different styling) — see CMS POST PAGE section below.
   ========================================================================== */
.resume-h2 { font-family: var(--font-display); font-size: 1.5rem; font-weight: 800; color: var(--text-primary); margin-bottom: var(--space-md); padding-bottom: 0.75rem; border-bottom: 1px solid var(--border-subtle); }
/* Extra top breathing room for all section headings except the first */
.resume-block + .resume-block .resume-h2 { margin-top: var(--space-md); }
.resume-block { margin-bottom: var(--space-lg); }
.resume-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.tag-cloud { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.tag-cloud .tag { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.06em; padding: 0.28rem 0.65rem; border: 1px solid var(--border-subtle); border-radius: 2px; color: var(--text-secondary); background: var(--bg-surface); }
.fun-fact { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.6rem 0; border-bottom: 1px solid var(--border-subtle); font-size: 0.88rem; color: var(--text-secondary); }
.fun-fact:last-child { border-bottom: none; }
.fun-fact-icon { color: var(--accent-cyan); font-size: 0.75rem; margin-top: 3px; flex-shrink: 0; }
@media (max-width: 768px) { .resume-grid-2 { grid-template-columns: 1fr; } }

/* ==========================================================================
   CMS POST PAGE (contentTemplate() in server.js — /portfolio/:slug, /blog/:slug)
   Was stuck in contentTemplate()'s own CSP-blocked inline <style> block, so
   every published case-study/blog post has been unstyled in production.
   Kept the original hardcoded font-family / color fallback values as-is
   (only the var(--nav-height) fallback is dropped — the real
   :root definition is right there in this same file now).
   Tag pills scoped to .cms-tags — see .tag-cloud .tag above for why.
   ========================================================================== */
/* Horizontal padding matches .container's own --space-md explicitly — this
   is on the same element as .container, and a bare "0" here used to
   silently override .container's padding (same specificity, declared later),
   leaving content flush against the screen edge. */
.cms-page { padding: calc(var(--nav-height) + 4rem) var(--space-md) 6rem; }
/* Grid columns adapt to which optional sidebars are present — see
   contentTemplate() in server.js for the has-toc/has-meta toggle classes.
   min-width:0 on the flexible track lets the article shrink smoothly as the
   viewport narrows, instead of blowing out past the fixed sidebar columns
   (the grid blowout bug — a wide table's intrinsic min-content width was
   forcing the whole row wider than the container, clipping the right sidebar). */
.cms-layout { display: grid; gap: 3rem; align-items: start; grid-template-columns: 1fr; }
.cms-layout.has-toc.has-meta  { grid-template-columns: 220px minmax(0,1fr) 220px; }
.cms-layout.has-toc:not(.has-meta)  { grid-template-columns: 220px minmax(0,1fr); }
.cms-layout.has-meta:not(.has-toc)  { grid-template-columns: minmax(0,1fr) 220px; }
/* A long table of contents scrolls inside the sidebar, so its last links can
   still be seen and reached (and a focused link scrolls into view). */
.cms-sidebar { position: sticky; top: calc(var(--nav-height) + 2rem); max-height: calc(100vh - var(--nav-height) - 3rem); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
.cms-sidebar-heading { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .18em; text-transform: uppercase; color: var(--text-muted); border-top: 1px solid rgba(96,165,250,.08); padding-top: .9rem; margin-bottom: 1.25rem; }
.cms-sidebar-meta { display: flex; flex-direction: column; gap: 1.5rem; }
/* Table-of-contents nav — reuses the site's .btn/.btn-tertiary/.btn-sm button
   component, stacked and left-aligned instead of the usual inline CTA shape. */
.cms-toc { display: flex; flex-direction: column; gap: .15rem; }
.cms-toc-link { width: 100%; justify-content: flex-start; text-align: left; white-space: normal; line-height: 1.3; }
/* h3 entries nest under their h2: smaller and indented. */
.cms-toc-link--sub { font-size: .78rem; padding-left: 1.5rem; }

/* Mobile toolbar (hidden on desktop) — replaces the two sidebars below the
   breakpoint with two tertiary-button triggers that open a bottom sheet. */
.cms-mobile-bar { display: none; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; }

@media (max-width: 860px) {
  .cms-mobile-bar { display: flex; }
  .cms-layout.has-toc.has-meta,
  .cms-layout.has-toc:not(.has-meta),
  .cms-layout.has-meta:not(.has-toc) { grid-template-columns: 1fr; gap: 0; }
  .cms-sidebar-desktop { display: none; }
  .cms-page { padding-left: 1rem; padding-right: 1rem; }
}
.cms-article { max-width: 820px; min-width: 0; }
.cms-article--solo { max-width: 920px; margin: 0 auto; }
.cms-body table { display: block; overflow-x: auto; }

/* Bottom sheet — shared by the "On This Page" and "Details" mobile triggers.
   See initContentSheets() in main.js. */
.cms-sheet-overlay { position: fixed; inset: 0; z-index: 200; display: flex; align-items: flex-end; justify-content: center; }
.cms-sheet-overlay[hidden] { display: none; }
.cms-sheet-backdrop { position: absolute; inset: 0; background: rgba(3,5,16,.7); opacity: 0; transition: opacity var(--dur-med) var(--ease-smooth); }
.cms-sheet-overlay.open .cms-sheet-backdrop { opacity: 1; }
.cms-sheet { position: relative; width: 100%; max-width: 560px; max-height: 75vh; overflow-y: auto; background: var(--bg-card); border: 1px solid var(--border-active); border-bottom: none; border-radius: 16px 16px 0 0; padding: .75rem 1.5rem 2rem; transform: translateY(100%); transition: transform var(--dur-med) var(--ease-smooth); }
.cms-sheet-overlay.open .cms-sheet { transform: translateY(0); }
.cms-sheet-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--border-active); margin: 0 auto .75rem; }
.cms-sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
.cms-sheet-title { font-family: var(--font-display); font-size: 1.1rem; font-weight: 800; color: var(--text-primary); }
.cms-sheet-close { background: none; border: none; color: var(--text-muted); font-size: 1.6rem; line-height: 1; cursor: pointer; padding: .25rem; }
.cms-sheet-close:hover { color: var(--accent-cyan); }
.cms-sheet .cms-toc-link { padding: .85rem .6rem; font-size: .95rem; border-radius: 6px; }
.cms-sheet .cms-toc-link--sub { padding-left: 1.6rem; font-size: .85rem; }
@media (prefers-reduced-motion: reduce) {
  .cms-sheet, .cms-sheet-backdrop { transition: none; }
}
.cms-back { display: inline-flex; align-items: center; gap: .5rem; font-family: var(--font-mono); font-size: .82rem; letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted); margin-bottom: 2.5rem; text-decoration: none; transition: color .15s; }
.cms-back:hover { color: var(--accent-cyan); }
.cms-cats { font-family: var(--font-mono); font-size: .82rem; letter-spacing: .2em; text-transform: uppercase; color: var(--accent-cyan); margin-bottom: .75rem; }
.cms-headline { font-family: var(--font-display); font-size: clamp(1.85rem, 1.2rem + 1.6vw, 2.25rem); font-weight: 800; color: var(--text-primary); letter-spacing: -0.025em; line-height: 1.1; margin-bottom: 1.25rem; }
/* Optional dek under the title — body font, larger and lighter than body copy.
   Pulled up toward the headline so the two read as one unit. */
.cms-dek { font-family: var(--font-body); font-size: clamp(1.25rem, 1.1rem + .5vw, 1.45rem); font-weight: 200; color: var(--text-primary); line-height: 1.45; margin: -.5rem 0 1.75rem; max-width: 680px; }
.cms-meta-item { display: flex; flex-direction: column; gap: .2rem; }
.cms-meta-label { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .18em; text-transform: uppercase; color: var(--text-muted); }
.cms-meta-value { font-size: .88rem; color: var(--text-primary); font-weight: 500; }
.cms-hero-img:not([data-gated-src]) { cursor: zoom-in; }
.cms-hero-img { width: 100%; border-radius: 8px; margin: 0 0 2.5rem; border: 1px solid rgba(96,165,250,.1); aspect-ratio: 16/7; object-fit: cover; }
.cms-body { color: var(--text-secondary); line-height: 1.85; font-size: 1.05rem; }
/* Section divider — replaces the browser's 2px inset border with a single
   1px muted-grey line that fades out to the right. */
.cms-body hr { height: 1px; border: 0; margin: 3.5rem 0; background: linear-gradient(to right, rgba(136,146,176,.35), rgba(136,146,176,0) 85%); }
/* scroll-margin-top — the "On This Page" sidebar (.cms-toc, both the desktop
   column and the mobile bottom-sheet version) links straight to these ids.
   Without this, the browser's anchor jump (and the page's own
   scroll-behavior:smooth) lands the heading flush with the viewport top,
   right under the fixed .site-nav. This shifts the landing spot down by the
   nav's height plus a little breathing room — nav-height already changes at
   the 768px breakpoint, so this tracks it with no extra media query. */
/* Post heading scale. The page title (.cms-headline, the only h1) is the one
   display-font heading and the only one that scales with the viewport — its
   1.85rem floor keeps it clearly above h2 on phones. Body headings are fixed
   sizes in Merriweather (Gravitas One is a poster face and smudges at small
   sizes) and a step dimmer than the title so they don't compete with it.
   A Markdown "#" in the body renders as h2 (see lib/content.js). */
.cms-body h2, .cms-body h3, .cms-body h4, .cms-body h5, .cms-body h6 { font-family: 'Merriweather', serif; font-weight: 700; line-height: 1.35; color: var(--text-heading); clear: both; scroll-margin-top: calc(var(--nav-height) + 24px); }
.cms-body h2 { font-size: 1.45rem;  margin: 3rem 0 1rem; }
.cms-body h3 { font-size: 1.275rem; margin: 2.25rem 0 .75rem; }
.cms-body h4 { font-size: 1.125rem; margin: 1.75rem 0 .6rem; }
.cms-body h5, .cms-body h6 { font-size: 1rem; margin: 1.5rem 0 .5rem; }
.cms-body p { margin-bottom: 1.25rem; }
.cms-body ul, .cms-body ol { margin: 0 0 1.25rem 1.5rem; }
.cms-body ul { list-style: disc; }
.cms-body ol { list-style: decimal; }
.cms-body li { margin-bottom: .5rem; }
.cms-body li::marker { color: var(--accent-gold); }
.cms-body img { max-width: 100%; border-radius: 6px; margin: 1.5rem 0; cursor: zoom-in; }
/* Figure wrapper — used whenever an inserted image or video carries a
   caption (the "+ Insert Image" / "+ Insert Video" modals add this
   automatically once a caption is typed). fit-content + max-width:100% is
   what makes "Auto" width behave like a bare <img>: the box shrinks to the
   media's natural size instead of stretching to the full column, and
   centers it — the .cms-max-w-N / .cms-w-full / .cms-float-* classes below
   apply to the figure itself (same specificity, declared later, so they
   still win) exactly as they already do for a bare <img>. */
.cms-body .cms-figure { width: fit-content; max-width: 100%; margin: 1.5rem auto; }
.cms-body .cms-figure img,
.cms-body .cms-figure video { display: block; width: 100%; margin: 0; border-radius: 6px; }
.cms-body .cms-figure img { cursor: zoom-in; }
.cms-body .cms-figure video { background: #000; }
.cms-body .cms-figure figcaption {
  margin-top: .6rem;
  text-align: center;
  font-family: var(--font-mono);
  font-size: .78rem;
  letter-spacing: .02em;
  line-height: 1.5;
  color: var(--text-muted);
}
/* Float — wrap body text around an image. Combine with a .cms-max-w-N class
   to size it (otherwise defaults to 45% of the column). Headings already
   clear:both above, so a float never bleeds into the next section. */
.cms-body .cms-float-left  { float: left;  max-width: 45%; margin: .25rem 1.5rem 1rem 0; }
.cms-body .cms-float-right { float: right; max-width: 45%; margin: .25rem 0 1rem 1.5rem; }
.cms-body .cms-float-none  { float: none; }
.cms-body .cms-clear       { clear: both; }
.cms-body blockquote { border-left: 3px solid var(--accent-cyan); padding: 1.1rem 1.5rem; margin: 2.25rem 0; background: linear-gradient(to right, rgba(96,165,250,.1), rgba(96,165,250,.02)); color: var(--text-primary); font-size: 1.12rem; line-height: 1.65; border-radius: 0 6px 6px 0; }
/* Pull quote — any blockquote carrying an attribution line (<small>) is a
   real person's words, so it gets an editorial treatment instead of the
   callout box: large serif italic, gold quote mark, no panel. */
.cms-body blockquote:has(small) { position: relative; border-left: 0; background: none; padding: 1.5rem 0 1.5rem 3.5rem; font-family: 'Merriweather', serif; font-style: italic; font-size: clamp(1.35rem, 1.1rem + .8vw, 1.7rem); line-height: 1.4; }
.cms-body blockquote:has(small)::before { content: "\201C"; position: absolute; left: 0; top: .6rem; font-family: 'Merriweather', serif; font-style: normal; font-size: 4.5rem; line-height: 1; color: var(--accent-gold); }
.cms-body blockquote small { display: block; margin-top: .75rem; font-family: var(--font-mono); font-style: normal; font-size: .72rem; letter-spacing: .16em; text-transform: uppercase; color: var(--text-secondary); }
/* Padding alone sets the inner spacing — drop the paragraph margin at the
   edges so top and bottom space match at any line count. */
.cms-body blockquote > :first-child { margin-top: 0; }
.cms-body blockquote > :last-child { margin-bottom: 0; }
.cms-body code { font-family: var(--font-mono); font-size: .85em; background: rgba(96,165,250,.08); padding: .15em .4em; border-radius: 3px; }
.cms-body pre { background: rgba(0,0,0,.3); border: 1px solid rgba(96,165,250,.1); border-radius: 6px; overflow-x: auto; margin-bottom: 1.25rem; padding: 1rem; }
.cms-body pre code { background: transparent; padding: 0; }
.cms-body table { width: 100%; border-collapse: collapse; margin: 0 0 1.25rem; font-size: .95rem; }
.cms-body th, .cms-body td { padding: .6rem .9rem; border: 1px solid rgba(96,165,250,.15); text-align: left; }
.cms-body th { font-family: var(--font-mono); font-size: .72rem; letter-spacing: .06em; text-transform: uppercase; color: var(--accent-cyan); background: rgba(96,165,250,.05); }
.cms-body tr:nth-child(even) td { background: rgba(96,165,250,.03); }
/* Layout utility classes for custom in-content HTML (embedded <div>s in the
   Markdown body). Use these instead of inline style="" — the site's CSP
   (style-src) has no 'unsafe-inline', so the browser silently drops inline
   style attributes on the published page. */
.cms-body .cms-row { display: flex; flex-wrap: wrap; gap: 1.5rem; margin: 1.75rem 0; }
.cms-body .cms-row > * { flex: 1 1 220px; }
.cms-body .cms-card { border: 1px solid rgba(96,165,250,.15); border-radius: 8px; padding: 1.25rem 1.5rem; background: rgba(96,165,250,.03); }
.cms-body .cms-card h3, .cms-body .cms-card h4 { font-family: var(--font-display); color: var(--text-primary); margin: 0 0 .5rem; }
.cms-body .cms-card p:last-child { margin-bottom: 0; }
/* Card label — small mono eyebrow for the top of a .cms-card. Used instead
   of a card heading, whose display face smudges at small sizes. */
.cms-body .cms-eyebrow { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .16em; text-transform: uppercase; color: var(--accent-cyan); margin-bottom: .5rem; }
.cms-body .cms-card > :first-child { margin-top: 0; }

/* At-a-glance strip — label/value pairs shown at the top of a case study.
   <div class="cms-glance"><div><span>Role</span>Lead UX</div>…</div> */
.cms-body .cms-glance { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; margin: 0 0 2.5rem; background: rgba(96,165,250,.12); border: 1px solid rgba(96,165,250,.12); border-radius: 8px; overflow: hidden; }
.cms-body .cms-glance > div { background: var(--bg-primary); padding: .9rem 1.1rem; color: var(--text-primary); font-size: .95rem; line-height: 1.45; }
/* Four across only where the article column is wide enough: single-column
   mobile layout from 600px, or desktop once both sidebars leave room. */
@media (min-width: 600px) and (max-width: 860px), (min-width: 1280px) {
  .cms-body .cms-glance { grid-template-columns: repeat(4, 1fr); }
}
.cms-body .cms-glance span { display: block; font-family: var(--font-mono); font-size: .66rem; letter-spacing: .16em; text-transform: uppercase; color: var(--text-secondary); margin-bottom: .3rem; }

/* Filters — a numbered list of native <details> disclosures. Each summary is
   a real control (it expands its own panel), keyboard-operable with no JS.
   Sharing a name="" attribute makes them an exclusive accordion where
   supported; elsewhere they simply open independently.
   <div class="cms-filters"><details name="f" open><summary><span>01</span>Label</summary><p>…</p></details>…</div> */
.cms-body .cms-filters { display: flex; flex-direction: column; gap: .4rem; }
.cms-body .cms-filters details { border: 1px solid rgba(96,165,250,.15); border-left-width: 2px; border-radius: 6px; background: rgba(96,165,250,.03); transition: border-color var(--dur-fast), background var(--dur-fast); }
.cms-body .cms-filters details[open] { border-left-color: var(--accent-cyan); background: rgba(96,165,250,.07); }
.cms-body .cms-filters summary { list-style: none; display: flex; align-items: center; gap: .75rem; padding: .7rem .9rem; cursor: pointer; color: var(--text-primary); font-size: .95rem; line-height: 1.35; border-radius: 6px; }
.cms-body .cms-filters summary::-webkit-details-marker { display: none; }
.cms-body .cms-filters summary span { font-family: var(--font-mono); font-size: .7rem; color: var(--accent-cyan); flex-shrink: 0; }
.cms-body .cms-filters summary::after { content: "+"; margin-left: auto; padding-left: .75rem; font-family: var(--font-mono); font-size: 1.1rem; color: var(--text-secondary); transition: transform var(--dur-fast); }
.cms-body .cms-filters details[open] summary::after { transform: rotate(45deg); color: var(--accent-cyan); }
.cms-body .cms-filters summary:hover { background: rgba(96,165,250,.06); }
.cms-body .cms-filters summary:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; }
.cms-body .cms-filters details > p { margin: 0; padding: 0 .9rem .85rem 2.6rem; font-size: .92rem; line-height: 1.6; color: var(--text-secondary); }
.cms-body .cms-hint { font-family: var(--font-mono); font-size: .68rem; letter-spacing: .14em; text-transform: uppercase; color: var(--text-secondary); margin: 0 0 .6rem; }
@media (prefers-reduced-motion: reduce) {
  .cms-body .cms-filters details, .cms-body .cms-filters summary::after { transition: none; }
}
/* A figure placed inside a .cms-row sits flush with its sibling column. */
.cms-body .cms-row > .cms-figure { margin: 0; }

/* Stepper — a numbered sequence laid out as connected steps; wraps on
   narrow screens. <ol class="cms-steps"><li>…</li></ol> */
.cms-body .cms-steps { list-style: none; counter-reset: cms-step; margin: 2rem 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1.25rem 1rem; }
.cms-body .cms-steps li { counter-increment: cms-step; position: relative; margin: 0; padding-top: 2.6rem; color: var(--text-primary); font-size: .95rem; line-height: 1.4; }
.cms-body .cms-steps li::before { content: counter(cms-step, decimal-leading-zero); position: absolute; top: 0; left: 0; width: 2rem; height: 2rem; display: grid; place-items: center; font-family: var(--font-mono); font-size: .72rem; color: var(--accent-cyan); border: 1px solid var(--border-active); border-radius: 50%; background: var(--bg-primary); }
.cms-body .cms-steps li::after { content: ""; position: absolute; top: 1rem; left: 2.4rem; right: -.6rem; height: 1px; background: linear-gradient(to right, var(--border-active), rgba(96,165,250,0)); }
.cms-body .cms-steps li:last-child::after { display: none; }
.cms-body .cms-steps li:last-child::before { background: var(--accent-cyan); color: var(--bg-primary); border-color: var(--accent-cyan); }
/* No-wrap modifier — pair with .cms-row to keep items on one line (scrolls
   instead of stacking on narrow screens). */
.cms-body .cms-nowrap { flex-wrap: nowrap; overflow-x: auto; }
/* Spacing utilities — pair with .cms-card (or any div) to override its
   default padding/margin. Scale matches the site's --space-* tokens. */
.cms-body .cms-p-0  { padding: 0; }
.cms-body .cms-p-xs { padding: var(--space-xs); }
.cms-body .cms-p-sm { padding: var(--space-sm); }
.cms-body .cms-p-md { padding: var(--space-md); }
.cms-body .cms-m-0  { margin: 0; }
.cms-body .cms-m-xs { margin: var(--space-xs); }
.cms-body .cms-m-sm { margin: var(--space-sm); }
.cms-body .cms-m-md { margin: var(--space-md); }
/* Gap utilities — pair with .cms-row to override the space *between* cards
   (padding/margin above only affect a card's own inside/outside spacing). */
.cms-body .cms-gap-0  { gap: 0; }
.cms-body .cms-gap-xs { gap: var(--space-xs); }
.cms-body .cms-gap-sm { gap: var(--space-sm); }
.cms-body .cms-gap-md { gap: var(--space-md); }
/* Width — a numbered rem scale (1-30) instead of named sizes (xs/sm/md/...):
   a named scale runs out of granularity fast, and a plain number needs no
   lookup (.cms-max-w-18 means max-width:18rem, not "medium-ish"). Combine
   min/max on the same element for a fixed range, e.g. a pull-quote box. */
.cms-body .cms-min-w-1 { min-width: 1rem; }
.cms-body .cms-min-w-2 { min-width: 2rem; }
.cms-body .cms-min-w-3 { min-width: 3rem; }
.cms-body .cms-min-w-4 { min-width: 4rem; }
.cms-body .cms-min-w-5 { min-width: 5rem; }
.cms-body .cms-min-w-6 { min-width: 6rem; }
.cms-body .cms-min-w-7 { min-width: 7rem; }
.cms-body .cms-min-w-8 { min-width: 8rem; }
.cms-body .cms-min-w-9 { min-width: 9rem; }
.cms-body .cms-min-w-10 { min-width: 10rem; }
.cms-body .cms-min-w-11 { min-width: 11rem; }
.cms-body .cms-min-w-12 { min-width: 12rem; }
.cms-body .cms-min-w-13 { min-width: 13rem; }
.cms-body .cms-min-w-14 { min-width: 14rem; }
.cms-body .cms-min-w-15 { min-width: 15rem; }
.cms-body .cms-min-w-16 { min-width: 16rem; }
.cms-body .cms-min-w-17 { min-width: 17rem; }
.cms-body .cms-min-w-18 { min-width: 18rem; }
.cms-body .cms-min-w-19 { min-width: 19rem; }
.cms-body .cms-min-w-20 { min-width: 20rem; }
.cms-body .cms-min-w-21 { min-width: 21rem; }
.cms-body .cms-min-w-22 { min-width: 22rem; }
.cms-body .cms-min-w-23 { min-width: 23rem; }
.cms-body .cms-min-w-24 { min-width: 24rem; }
.cms-body .cms-min-w-25 { min-width: 25rem; }
.cms-body .cms-min-w-26 { min-width: 26rem; }
.cms-body .cms-min-w-27 { min-width: 27rem; }
.cms-body .cms-min-w-28 { min-width: 28rem; }
.cms-body .cms-min-w-29 { min-width: 29rem; }
.cms-body .cms-min-w-30 { min-width: 30rem; }
.cms-body .cms-max-w-1 { max-width: 1rem; }
.cms-body .cms-max-w-2 { max-width: 2rem; }
.cms-body .cms-max-w-3 { max-width: 3rem; }
.cms-body .cms-max-w-4 { max-width: 4rem; }
.cms-body .cms-max-w-5 { max-width: 5rem; }
.cms-body .cms-max-w-6 { max-width: 6rem; }
.cms-body .cms-max-w-7 { max-width: 7rem; }
.cms-body .cms-max-w-8 { max-width: 8rem; }
.cms-body .cms-max-w-9 { max-width: 9rem; }
.cms-body .cms-max-w-10 { max-width: 10rem; }
.cms-body .cms-max-w-11 { max-width: 11rem; }
.cms-body .cms-max-w-12 { max-width: 12rem; }
.cms-body .cms-max-w-13 { max-width: 13rem; }
.cms-body .cms-max-w-14 { max-width: 14rem; }
.cms-body .cms-max-w-15 { max-width: 15rem; }
.cms-body .cms-max-w-16 { max-width: 16rem; }
.cms-body .cms-max-w-17 { max-width: 17rem; }
.cms-body .cms-max-w-18 { max-width: 18rem; }
.cms-body .cms-max-w-19 { max-width: 19rem; }
.cms-body .cms-max-w-20 { max-width: 20rem; }
.cms-body .cms-max-w-21 { max-width: 21rem; }
.cms-body .cms-max-w-22 { max-width: 22rem; }
.cms-body .cms-max-w-23 { max-width: 23rem; }
.cms-body .cms-max-w-24 { max-width: 24rem; }
.cms-body .cms-max-w-25 { max-width: 25rem; }
.cms-body .cms-max-w-26 { max-width: 26rem; }
.cms-body .cms-max-w-27 { max-width: 27rem; }
.cms-body .cms-max-w-28 { max-width: 28rem; }
.cms-body .cms-max-w-29 { max-width: 29rem; }
.cms-body .cms-max-w-30 { max-width: 30rem; }
.cms-body .cms-w-full { width: 100%; }
/* Border thickness (each includes a sensible default color) + color
   overrides — combine e.g. class="cms-card cms-border-2 cms-border-gold". */
.cms-body .cms-border-0 { border: 0; }
.cms-body .cms-border-1 { border-style: solid; border-width: 1px; border-color: rgba(96,165,250,.15); }
.cms-body .cms-border-2 { border-style: solid; border-width: 2px; border-color: rgba(96,165,250,.15); }
.cms-body .cms-border-3 { border-style: solid; border-width: 3px; border-color: rgba(96,165,250,.15); }
.cms-body .cms-border-cyan   { border-color: var(--accent-cyan); }
.cms-body .cms-border-gold   { border-color: var(--accent-gold); }
.cms-body .cms-border-violet { border-color: var(--accent-violet); }
.cms-body .cms-border-muted  { border-color: rgba(96,165,250,.15); }
/* Corner radius — all four corners at once, or one at a time (combine to
   override a single corner, e.g. class="cms-card cms-radius-lg cms-radius-tl-0"). */
.cms-body .cms-radius-0    { border-radius: 0; }
.cms-body .cms-radius-sm   { border-radius: var(--radius-sm); }
.cms-body .cms-radius-md   { border-radius: var(--radius-md); }
.cms-body .cms-radius-lg   { border-radius: var(--radius-lg); }
.cms-body .cms-radius-full { border-radius: 999px; }
.cms-body .cms-radius-tl-0  { border-top-left-radius: 0; }
.cms-body .cms-radius-tl-sm { border-top-left-radius: var(--radius-sm); }
.cms-body .cms-radius-tl-md { border-top-left-radius: var(--radius-md); }
.cms-body .cms-radius-tl-lg { border-top-left-radius: var(--radius-lg); }
.cms-body .cms-radius-tr-0  { border-top-right-radius: 0; }
.cms-body .cms-radius-tr-sm { border-top-right-radius: var(--radius-sm); }
.cms-body .cms-radius-tr-md { border-top-right-radius: var(--radius-md); }
.cms-body .cms-radius-tr-lg { border-top-right-radius: var(--radius-lg); }
.cms-body .cms-radius-bl-0  { border-bottom-left-radius: 0; }
.cms-body .cms-radius-bl-sm { border-bottom-left-radius: var(--radius-sm); }
.cms-body .cms-radius-bl-md { border-bottom-left-radius: var(--radius-md); }
.cms-body .cms-radius-bl-lg { border-bottom-left-radius: var(--radius-lg); }
.cms-body .cms-radius-br-0  { border-bottom-right-radius: 0; }
.cms-body .cms-radius-br-sm { border-bottom-right-radius: var(--radius-sm); }
.cms-body .cms-radius-br-md { border-bottom-right-radius: var(--radius-md); }
.cms-body .cms-radius-br-lg { border-bottom-right-radius: var(--radius-lg); }
/* Text formatting — alignment, weight, size, color.
   !important: headings have their own component-level color/weight rules
   (e.g. .cms-body h2/h3, .cms-card h3/h4) that are MORE specific than a
   plain two-class utility selector, so without this a text-* class silently
   loses when applied directly to a heading. Utilities are meant to always
   win when explicitly applied — same rationale as Bootstrap's .text-* helpers. */
.cms-body .cms-text-left   { text-align: left !important; }
.cms-body .cms-text-center { text-align: center !important; }
.cms-body .cms-text-right  { text-align: right !important; }
.cms-body .cms-text-bold   { font-weight: 700 !important; }
.cms-body .cms-text-sm     { font-size: .85em !important; }
.cms-body .cms-text-lg     { font-size: 1.2em !important; }
/* Text color */
.cms-body .cms-text-primary   { color: var(--text-primary) !important; }
.cms-body .cms-text-secondary { color: var(--text-secondary) !important; }
.cms-body .cms-text-muted     { color: var(--text-muted) !important; }
.cms-body .cms-text-cyan      { color: var(--accent-cyan) !important; }
.cms-body .cms-text-gold      { color: var(--accent-gold) !important; }
.cms-body .cms-text-violet    { color: var(--accent-violet) !important; }
.cms-references { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid rgba(96,165,250,.08); font-size: .8rem; line-height: 1.6; color: var(--text-secondary); }
.cms-references .cms-references-title { font-family: var(--font-mono); font-size: .7rem; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; color: var(--text-secondary); margin: 0 0 .75rem; }
.cms-references ol, .cms-references ul { padding-left: 1.25rem; margin: 0; }
.cms-references li { margin-bottom: .4rem; }
.cms-references a { color: var(--accent-cyan); word-break: break-word; }
.cms-tags { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px solid rgba(96,165,250,.08); }
.cms-tags .tag { font-family: var(--font-mono); font-size: .65rem; padding: .25rem .6rem; border: 1px solid rgba(96,165,250,.2); border-radius: 2px; color: var(--text-secondary); }
/* Live-type outcome callout */

/* Image lightbox — click any .cms-body image to view it larger. See initLightbox() in main.js. */
.cms-lightbox { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center; }
.cms-lightbox[hidden] { display: none; }
.cms-lightbox-backdrop { position: absolute; inset: 0; background: rgba(3,5,16,.9); opacity: 0; transition: opacity var(--dur-med) var(--ease-smooth); }
.cms-lightbox.open .cms-lightbox-backdrop { opacity: 1; }
.cms-lightbox-img { position: relative; max-width: 90vw; max-height: 88vh; border-radius: 8px; box-shadow: 0 20px 60px rgba(0,0,0,.6); opacity: 0; transform: scale(.96); transition: opacity var(--dur-med) var(--ease-smooth), transform var(--dur-med) var(--ease-smooth); cursor: default; }
.cms-lightbox.open .cms-lightbox-img { opacity: 1; transform: scale(1); }
.cms-lightbox-close { position: absolute; top: 1.5rem; right: 1.5rem; z-index: 1; background: rgba(15,21,42,.8); border: 1px solid rgba(96,165,250,.2); color: var(--text-primary); width: 40px; height: 40px; border-radius: 50%; font-size: 1.4rem; line-height: 1; cursor: pointer; transition: all var(--dur-fast) var(--ease-smooth); }
.cms-lightbox-close:hover { background: rgba(96,165,250,.15); border-color: var(--accent-cyan); }
@media (prefers-reduced-motion: reduce) {
  .cms-lightbox-backdrop, .cms-lightbox-img { transition: none; }
}

/* =========================================================================
   SHIP IT — the spaceship easter egg (public/js/arcade.js builds the DOM).
   z-index sits under the custom cursor (9999) and page transition (9000).
   ========================================================================= */
#spaceship.is-hot svg {
  filter: drop-shadow(0 0 14px rgba(96,165,250,.75)) brightness(1.15);
}
html.arcade-open { overflow: hidden; }
html.arcade-open #spaceship { visibility: hidden; }

/* Keyboard route in: invisible until it takes focus, like the skip link. */
.arcade-launch {
  position: fixed; left: 1rem; bottom: 1rem; z-index: 150;
  transform: translateY(calc(100% + 2rem));
  padding: .6rem 1rem;
  font: 500 .8rem var(--font-mono);
  letter-spacing: .04em;
  color: var(--bg-primary);
  background: var(--accent-cyan);
  border: 0; border-radius: var(--radius-sm);
}
.arcade-launch:focus-visible {
  transform: none;
  outline: 2px solid var(--text-primary); outline-offset: 3px;
}

.arcade {
  position: fixed; inset: 0; z-index: 5000;
  display: flex; align-items: center; justify-content: center;
  padding: 1rem;
  /* Opaque rather than backdrop-filter: a blur re-sampling the page's WebGL
     canvas every frame is what hung Chrome. Opacity + transform only, so the
     whole open animation stays on the compositor. */
  background: rgba(3,5,16,.94);
  opacity: 0;
  transition: opacity .3s var(--ease-smooth);
}
.arcade.is-open { opacity: 1; }
.arcade[hidden] { display: none; }

.arcade-dialog {
  display: flex; flex-direction: column;
  width: min(100%, 620px);
  height: min(100%, 800px);
  background: var(--bg-primary);
  border: 1px solid var(--border-active);
  border-radius: var(--radius-lg);
  box-shadow: 0 0 0 1px rgba(96,165,250,.06), 0 30px 80px rgba(0,0,0,.6), 0 0 60px rgba(96,165,250,.12);
  overflow: hidden;
  /* Grows out of wherever the ship was (--dx/--dy set by arcade.js). */
  transform: translate(var(--dx, 0), var(--dy, 0)) scale(.08);
  transition: transform .45s var(--ease-sharp);
}
.arcade.is-open .arcade-dialog { transform: none; }
.arcade-bar {
  display: flex; align-items: center; justify-content: space-between;
  padding: .5rem .5rem .5rem 1rem;
  border-bottom: 1px solid var(--border-subtle);
}
.arcade-bar-title {
  font: 500 .72rem var(--font-mono);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-muted);
}
.arcade-bar-btns { display: flex; gap: .25rem; }
.arcade-icon-btn {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  font: 600 .9rem var(--font-mono);
  color: var(--text-secondary);
  background: transparent;
  border: 1px solid transparent; border-radius: var(--radius-md);
  transition: color var(--dur-fast), border-color var(--dur-fast), background var(--dur-fast);
}
.arcade-icon-btn:hover { color: var(--text-primary); background: rgba(96,165,250,.08); }
.arcade-icon-btn svg {
  width: 20px; height: 20px;
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.arcade-icon-btn:focus-visible,
.arcade-btn:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; }

.arcade-stage {
  position: relative; flex: 1; min-height: 0;
  display: grid; place-items: center;
  padding: .75rem;
}
.arcade-canvas {
  grid-area: 1 / 1;
  display: block;
  border-radius: var(--radius-md);
  touch-action: none; /* drag steers the ship, never the page */
  user-select: none; -webkit-user-select: none;
}
.arcade-canvas:focus { outline: none; }
.arcade-screen {
  grid-area: 1 / 1;
  display: grid; place-items: center;
  padding: 1.5rem;
  background: rgba(3,5,16,.72);
  border-radius: var(--radius-md);
}
.arcade-screen[hidden] { display: none; }
.arcade-panel { max-width: 32ch; text-align: center; }
.arcade-eyebrow {
  margin: 0 0 .75rem;
  font: 500 .7rem var(--font-mono);
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--accent-cyan);
}
.arcade-title {
  margin: 0 0 1rem;
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 6vw, 2.4rem);
  font-weight: 400; line-height: 1.1;
  color: var(--text-primary);
}
.arcade-line { margin: 0 0 1rem; font-size: .92rem; line-height: 1.55; color: var(--text-secondary); }
.arcade-stat { margin: 0 0 1.5rem; font: 500 .75rem var(--font-mono); color: var(--accent-gold); }
.arcade-actions { display: flex; flex-direction: column; gap: 1rem; align-items: center; }
.arcade-btn {
  min-width: 12rem; min-height: 44px;
  padding: .7rem 1.4rem;
  font: 600 .8rem var(--font-mono);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--bg-primary);
  background: var(--accent-cyan);
  border: 1px solid var(--accent-cyan); border-radius: var(--radius-sm);
  transition: background var(--dur-fast), color var(--dur-fast);
}
.arcade-btn:hover { background: #93c5fd; }
.arcade-btn--ghost { color: var(--text-secondary); background: transparent; border-color: var(--border-active); }
.arcade-btn--ghost:hover { color: var(--text-primary); background: rgba(96,165,250,.08); }
.arcade-foot { margin: 1rem 0 0; font: 400 .7rem var(--font-mono); color: var(--text-muted); }

@media (max-width: 640px) {
  .arcade { padding: 0; }
  .arcade-dialog { width: 100%; height: 100%; border-radius: 0; border: 0; }
  .arcade-stage { padding: .25rem; }
}
@media (prefers-reduced-motion: reduce) {
  .arcade, .arcade-dialog, .arcade-launch { transition: none; }
}

/* =========================================================================
   PORTFOLIO ASTRONAUT — public/js/astronaut.js flies him; this is the shell.
   z-index 0, appended after the star canvas: above the stars, beneath the
   page's z-index:1 containers, so he passes BEHIND the text.
   ========================================================================= */
.astro-layer {
  position: absolute; top: 0; left: 0;
  width: 100%; height: 0;          /* JS: ends at the footer's top edge */
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}
.astro {
  position: absolute; top: 0; left: 0;
  width: 104px;                    /* 2:3 box, so ~156px tall */
  opacity: 0;
  will-change: transform;
  filter: drop-shadow(0 6px 14px rgba(0,0,0,.45)) drop-shadow(0 0 22px rgba(96,165,250,.12));
}
.astro.is-flying { opacity: 1; }

/* Weightless drift on top of the flight path */
.astro-bob { animation: astroBob 3.4s ease-in-out infinite; }
@keyframes astroBob {
  0%, 100% { transform: translateY(0) rotate(-2deg); }
  50%      { transform: translateY(-6px) rotate(2.5deg); }
}

/* 2D fallback: layers baked from the 3D astronaut (_lab/astro3d/tools/build-sprites.mjs),
   stacked in a 2:3 box that matches the 3D canvas, so the swap lines up. */
.astro-sprite { position: relative; aspect-ratio: 2 / 3; }
.astro-sprite img {
  position: absolute; inset: 0; width: 100%; height: 100%; display: block;
  pointer-events: none; user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
}

/* The wave: the baked forearm swings about the elbow, matching the 3D wave
   clip's on-screen swing (build-sprites.mjs measures it: +-19deg, 3.04 s a
   cycle). Origins are the baked elbow and nozzle points, in % of the box. */
.ast-fore { transform-origin: 32.08% 31.52%; animation: astroWave 1.52s ease-in-out infinite alternate; }
@keyframes astroWave {
  from { transform: rotate(-19deg); }
  to   { transform: rotate(19deg); }
}

/* Ion plumes, flickering from the nozzles; out of phase on purpose. */
.ast-plume--a { transform-origin: 38.99% 45.52%; animation: astroFlame 140ms ease-in-out infinite alternate; }
.ast-plume--b { transform-origin: 24.54% 45.52%; animation: astroFlame 180ms ease-in-out infinite alternate-reverse; }
@keyframes astroFlame {
  from { transform: scale(.86); opacity: .8; }
  to   { transform: scale(1.04); opacity: 1; }
}

/* --- Grabbed: press-and-hold / drag (astronaut.js) --- */
/* In front of the text while someone has him (below the nav at 100). */
.astro-layer.is-front { z-index: 60; }
html.astro-dragging, html.astro-dragging * { user-select: none; -webkit-user-select: none; }
.astro.is-hot { filter: drop-shadow(0 6px 14px rgba(0,0,0,.45)) drop-shadow(0 0 16px rgba(96,165,250,.55)); }

/* Held: he waves frantically, the bob quickens, and the plumes sputter
   (he's not flying himself any more). */
.astro.is-held .ast-fore { animation: astroWaveFast .34s ease-in-out infinite alternate; }
@keyframes astroWaveFast {
  from { transform: rotate(-22deg); }
  to   { transform: rotate(16deg); }
}
.astro.is-held.is-catching-breath .ast-fore { animation-play-state: paused; }
.astro.is-held .astro-bob { animation-duration: 1.3s; }
.astro.is-held .ast-plume--a { animation: astroSputter 1.1s steps(1, end) infinite; }
.astro.is-held .ast-plume--b { animation: astroSputter 1.1s steps(1, end) -.45s infinite; }
@keyframes astroSputter {
  0%   { transform: scale(.45); opacity: .45; }
  18%  { transform: scale(.65); opacity: .7; }
  30%  { transform: scale(.25); opacity: .2; }
  52%  { transform: scale(.55); opacity: .55; }
  64%  { transform: scale(.2);  opacity: .1; }
  82%  { transform: scale(.5);  opacity: .5; }
  100% { transform: scale(.45); opacity: .45; }
}
/* Sulking after being let go: no more waving. */
.astro.is-sulking:not(.is-held) .ast-fore { animation: none; transition: transform .6s ease; }


@media (max-width: 640px) {
  .astro { width: 74px; }
}
@media (prefers-reduced-motion: reduce) {
  .astro-layer { display: none; }
}

/* 3D astronaut (astro3d.js): a canvas replaces the sprites once it has loaded. */
.astro canvas.astro-3d { display: block; width: 100%; height: auto; }

/* =========================================================================
   FORMER INLINE STYLES
   The public-page CSP (style-src without 'unsafe-inline') silently drops
   style="" attributes, including ones inside HTML built by JS. These rules
   carry what those attributes were meant to do. Kept at the end of the file
   so they win over the base rules they modify at equal specificity.
   ========================================================================= */
.text-primary-c { color: var(--text-primary); }

.section.section--flush-top { padding-top: 0; }
.container--pad-bottom { padding-bottom: var(--space-xl); }
.divider-glow--end { margin-top: var(--space-lg); }
.section-label--center { justify-content: center; }

/* Portfolio / Articles page titles */
/* Same size as the homepage hero headline (.hero-headline). */
.section-title--page { font-size: clamp(2.75rem, 5.44vw, 5.1rem); line-height: 0.95; letter-spacing: -0.03em; max-width: 700px; }
/* Phones: the title lines are held together with &nbsp; ("moves people",
   "putting down."), so at the 2.75rem minimum they ran off the edge. Scale to
   the longest, "putting down." (9.89em in WebKit); about 2rem on an iPhone 12,
   the contact headline's size. Kept here, after the base rule, so it wins. */
@media (max-width: 480px) {
  .section-title--page { font-size: min(2.6rem, calc((100vw - 4rem) / 10.4)); }
}
.page-intro { margin-top: 1.5rem; max-width: 560px; }

/* Closing CTA blocks */
.cta-heading { max-width: 600px; margin: var(--space-lg) auto var(--space-md); }
.cta-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.cta-banner-title { font-size: clamp(2.5rem, 6vw, 5rem); max-width: 700px; margin: 0 auto var(--space-md); }
.cta-banner-lead { font-size: 1.1rem; color: var(--text-secondary); max-width: 480px; margin: 0 auto; line-height: 1.75; }

.btn-xs { font-size: .75rem; padding: .55rem 1.1rem; }
.btn-block { width: 100%; justify-content: center; margin-top: 0.5rem; }

.card-skeleton.card-skeleton--row { height: 80px; margin-bottom: 1px; border-radius: 0; }
.card-skeleton.card-skeleton--row:last-child { margin-bottom: 0; }

.card-image-placeholder--0 { background: linear-gradient(135deg, #070b1f 0%, #0d1235 50%, #1a2550 100%); }
.card-image-placeholder--1 { background: linear-gradient(135deg, #0d1235 0%, #1a1040 100%); }
.card-image-placeholder--2 { background: linear-gradient(135deg, #0f1535 0%, #0a2040 100%); }
.card-image-placeholder--3 { background: linear-gradient(135deg, #051020 0%, #0d2030 100%); }
.card-image-placeholder-glyph { font-size: 4rem; opacity: .1; }

/* Cancel (secondary, outline) and Unlock (primary) share the standard .btn type and size. */
.pw-gate-actions .btn { padding: .6rem 1.5rem; }

/* Résumé */
.resume-section-heading--spaced { margin-top: 1.5rem; }
.resume-summary { font-size: 1.05rem; color: var(--text-secondary); line-height: 1.8; }
.resume-grid-2--wide { gap: 2rem; }
.resume-skill-label { font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent-cyan); margin-bottom: 0.75rem; }
/* Key skills: a quiet inline list, items divided by small accent dots (lighter than tag chips).
   Each dot sits in the gap before its item; on the item that starts a line the dot falls left of
   the list's edge and overflow clips it, so no line begins or ends with a stray dot. */
.skill-list {
  list-style: none; margin: 0; padding: 0; overflow: hidden;
  display: flex; flex-wrap: wrap; column-gap: 1.25em;
  font-size: 0.92rem; line-height: 1.9; color: var(--text-secondary);
}
.skill-list li { position: relative; white-space: nowrap; }
.skill-list li::before {
  content: ""; position: absolute; left: -0.8em; top: 50%; width: 4px; height: 4px; margin-top: -2px;
  border-radius: 50%; background: var(--accent-cyan); opacity: 0.6;
}

/* ==========================================================================
   IMAGES-ONLY PASSWORD PROTECTION
   The server swaps gated media for a ~32px blurred stand-in (see
   lib/media-gate.js); these rules scale it up and soften it, and style the
   quiet one-line unlock field under the dek (initMediaUnlock, main.js).
   ========================================================================== */
.media-unlock { margin: -.75rem 0 1.75rem; }
.media-unlock-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .35rem .9rem;
}
.media-unlock-label {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-family: var(--font-mono);
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-secondary);
}
.media-unlock-label svg { width: 13px; height: 13px; flex: none; color: var(--accent-cyan); }
/* Underline-only field: reads as part of the header, not a form block. */
.media-unlock-row input {
  width: 13rem;
  max-width: 100%;
  padding: .3rem .1rem;
  font: inherit;
  font-size: .85rem;
  color: var(--text-primary);
  background: transparent;
  border: 0;
  border-bottom: 1px solid rgba(136, 146, 176, .35);
  border-radius: 0;
  transition: border-color var(--dur-fast) var(--ease-smooth);
}
.media-unlock-row input::placeholder { color: var(--text-secondary); opacity: .7; }
.media-unlock-row input:focus { outline: none; border-bottom-color: var(--accent-cyan); }
.media-unlock-row input:focus-visible { box-shadow: 0 1px 0 0 var(--accent-cyan); }
.media-unlock-row .btn { padding: .3rem .6rem; font-size: .85rem; }
.media-unlock.is-error .media-unlock-row input { border-bottom-color: #f87171; }
.media-unlock-msg { margin: .35rem 0 0; font-size: .78rem; color: #fca5a5; }
.media-unlock-msg:empty { display: none; }
.media-unlock.is-unlocked .media-unlock-msg { margin: 0; color: var(--accent-cyan); font-family: var(--font-mono); font-size: .68rem; letter-spacing: .1em; text-transform: uppercase; }

/* Locked stand-ins sit in a bordered frame (lockHtml / frame() in
   lib/media-gate.js) with the card lock badge over the centre. The frame
   clips the blur, so the edge stays crisp; the slight scale hides the soft
   fringe a CSS blur leaves at the image's own edges. */
.gated-frame {
  position: relative;
  display: block;
  width: 100%;
  margin: 1.5rem 0;
  overflow: hidden;
  border: 1px solid rgba(96, 165, 250, 0.35);
  border-radius: 8px;
  background: var(--bg-surface);
  box-shadow: 0 0 0 1px rgba(3, 5, 16, 0.6), 0 6px 24px rgba(1, 3, 12, 0.45);
  cursor: not-allowed;
}
.cms-body .cms-figure .gated-frame { margin: 0; }
.gated-frame--hero { margin: 0 0 2.5rem; }
.cms-body .gated-frame img,
.gated-frame .cms-hero-img {
  display: block;
  width: 100%;
  margin: 0;
  border: 0;
  border-radius: 0;
  filter: blur(14px) saturate(.9);
  transform: scale(1.08);
  cursor: not-allowed;
}
.cms-body img.is-revealed,
.cms-hero-img.is-revealed { animation: media-reveal .5s var(--ease-smooth) both; }
@keyframes media-reveal { from { opacity: 0; } to { opacity: 1; } }

.cms-body video[data-gated] {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--bg-surface);
  border-radius: 6px;
}
.gated-media-note {
  margin: -.75rem 0 1.5rem;
  font-family: var(--font-mono);
  font-size: .72rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
@media (prefers-reduced-motion: reduce) {
  .cms-body img.is-revealed, .cms-hero-img.is-revealed { animation: none; }
}

/* ==========================================================================
   BOTTOM TAB BAR — phone navigation (renderTabBar() in lib/layout.js)
   Touch screens up to 768px wide get a fixed bar of five tabs instead of the
   hamburger, so nothing covers the page. A narrow desktop window (fine
   pointer) keeps the hamburger. viewport-fit=cover in the viewport meta makes
   env(safe-area-inset-bottom) the height of the iPhone home indicator.
   ========================================================================== */
.tab-bar { display: none; }

@media (max-width: 768px) and (pointer: coarse) {
  :root { --tab-bar-height: 58px; }
  .nav-toggle, .mobile-menu { display: none !important; }
  body { padding-bottom: calc(var(--tab-bar-height) + env(safe-area-inset-bottom)); }

  .tab-bar {
    display: block;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 100;   /* with .site-nav; under the post sheets (200) and lightbox (300) */
    padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
    background: rgba(3, 5, 16, 0.86);
    -webkit-backdrop-filter: blur(14px) saturate(140%);
    backdrop-filter: blur(14px) saturate(140%);
    border-top: 1px solid var(--nav-border);
  }
  .tab-bar ul {
    display: flex;
    height: var(--tab-bar-height);
  }
  .tab-bar li { flex: 1; }
  .tab-bar a {
    position: relative;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
    -webkit-tap-highlight-color: transparent;
  }
  .tab-bar svg { width: 22px; height: 22px; }
  .tab-bar a[aria-current="page"] { color: var(--accent-cyan); }
  .tab-bar a[aria-current="page"]::before {
    content: '';
    position: absolute;
    top: -1px; left: 30%; right: 30%;
    height: 2px;
    border-radius: 0 0 2px 2px;
    background: var(--accent-cyan);
  }
  .tab-bar a:active { color: var(--text-primary); }
  .tab-bar a:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: -4px; }

  /* Out of the way while the on-screen keyboard is up (contact form, unlock fields). */
  body:has(input:focus, textarea:focus, select:focus) .tab-bar { display: none; }
}

/* viewport-fit=cover lets the page run under the iPhone notch in landscape;
   keep content and the fixed header clear of it (0 everywhere else). */
body { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
.site-nav { padding-left: calc(var(--space-md) + env(safe-area-inset-left)); padding-right: calc(var(--space-md) + env(safe-area-inset-right)); }
