/* ============================================================================
   Global tokens, reset, base typography, shared utilities.

   This file is loaded BEFORE every per-component CSS file (server.ts
   concatenates `styles.css` first, then everything in `public/css/*.css`,
   serving the result at `/static/bundle.css`). Anything that components rely
   on — CSS variables, base font sizing, the `.section` and `.glass`
   utilities — lives here.

   Responsive approach:
   - html { font-size: 16px } fixed (standard browser default, 1rem = 16px)
   - Font sizes: RFS-style clamp() — fluid below 1200px, fixed above
   - Spacing/layout: stepped at Bootstrap standard breakpoints, mobile-first
   - 1920px is the reference cap — nothing grows above it

   ========================== BREAKPOINT REFERENCE ===========================
   CSS custom properties cannot be used inside @media queries, so the px
   values below are hardcoded throughout the codebase. Every @media rule
   should carry an inline CSS comment after the width naming its tier
   (e.g. sm, md, lg, xl, xxl) so the intent is greppable.

     sm      576px   Minor grid bumps (disciplines 1→2 cols)
     md      768px   Default tablet transitions: section padding, 2-col grids,
                     hire item sizing, etc. Most common.
     lg      992px   Wider grids (hire 2-col, timeline alternating, hero meta
                     3-col in wide-portrait). Also hero H1/H2 piecewise mid.
     xl     1200px   3-col grids (skills, disciplines), xl section padding,
                     --margin step, marquee single-row ribbon.
     xxl    1400px   Desktop chrome activation (Nav, DotNav, MobileBar hide),
                     hero overlap layout, hero meta L-shape + 3-col landscape,
                     --margin cap, contact modal hide.
     (1920)         Hero H1 piecewise top-segment anchor only. Not a Bootstrap
                     tier — specific to the fluid ramp there.

   When adding a new @media rule, pick the closest existing tier. If the
   ideal number is within 40-50px of an existing tier, use the existing one.
   Don't introduce new breakpoint values without a reason; if you do, update
   this table.

   See docs/2-STYLEGUIDE.md for the design system reference.
   ============================================================================ */

/* --- 1. RESET ------------------------------------------------------------- */

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  /* Suppress the default blue/grey tap flash that iOS Safari and Android
     Chrome paint on interactive elements. We have our own hover/active
     states (and the lightbox open animation) as affordance — the flash
     competes with them and looks dated. Global because every interactive
     element on the site opts out. */
  -webkit-tap-highlight-color: transparent;
}

/* --- 2. ROOT FONT-SIZE ---------------------------------------------------- */

html {
  font-size: 16px;
}

/* Anchor-click smoothness is owned by public/js/anchor-scroll.js, NOT a
   CSS `scroll-behavior: smooth` rule. Reason: Lenis drives desktop scroll
   by calling `window.scrollTo({ top: X })` every animation frame; with
   `scroll-behavior: smooth` each of those per-frame calls gets re-smoothed
   by the browser, stacking its easing on top of Lenis's and producing
   stuttery wheel scroll. The JS interceptor instead routes anchor clicks
   through `lenis.scrollTo()` when Lenis is active (perfectly coupled) and
   native smooth scrollTo when it isn't (touch, reduced-motion). */


/* --- 3. DESIGN TOKENS ----------------------------------------------------- */

:root {
  /* Colours */
  --bg: #f6f5f1;
  --text: #000000;
  --muted: #777;
  --accent: #e00000;
  --accent-dark: #b00000;
  --accent-light: #ff1111;
  --info: #1565c0;             /* secondary action button (CV, Technical details) */
  --info-dark: #0d47a1;        /* hover state */
  --white: #ffffff;

  /* Opacity utilities */
  --rule: rgba(0, 0, 0, .08);
  --text-06: rgba(0, 0, 0, .06);
  --text-12: rgba(0, 0, 0, .12);
  --text-15: rgba(0, 0, 0, .15);
  --text-50: rgba(0, 0, 0, .5);
  --bg-60: rgba(246, 245, 241, .6);
  --bg-75: rgba(246, 245, 241, .75);
  --accent-20: rgba(224, 0, 0, .2);
  --accent-35: rgba(224, 0, 0, .35);
  --accent-70: rgba(224, 0, 0, .7);
  --white-15: rgba(255, 255, 255, .15);
  --white-50: rgba(255, 255, 255, .5);

  /* Avatar gradients (testimonial cards) */
  --avatar-1a: #ff6b6b;
  --avatar-1b: var(--accent);
  --avatar-2a: #60a5fa;
  --avatar-2b: #22d3ee;
  --avatar-3a: #f472b6;
  --avatar-3b: #fb923c;

  /* Typography */
  --heading: 'DM Serif Display', Georgia, serif;
  --body: 'Space Grotesk', system-ui, sans-serif;

  /* Motion */
  --ease: cubic-bezier(.14, 1, .34, 1);

  /* Spacing — mobile-first, stepped at Bootstrap breakpoints.
     Components reference var(--margin) for consistent page-edge padding. */
  --margin: 20px;

  /* --- Font size scale ---
     Fixed sizes (< 24px) for UI/label text. RFS sizes (≥ 24px) scale
     fluidly between 320px and 1200px viewport, then stay fixed above.
     Every text element in the site references one of these — no ad-hoc
     font-size values in component CSS. */
  --text-xs:       1rem;                                                    /* 16px  — meta labels, tags */
  --text-sm:       1.1rem;                                                  /* 17.6px — section labels, eyebrows */
  --text-base:     1.2rem;                                                  /* 19.2px — nav links, numbers, badges */
  --text-md:       1.4rem;                                                  /* 22.4px — hero name, company names */
  --text-body:     clamp(1.275rem, calc(1.193rem + 0.409vw), 1.5rem);      /* 24px  — body text, descriptions */
  --text-lead:     clamp(1.305rem, calc(1.125rem + 0.9vw), 1.8rem);        /* 28.8px — project brief, logos */
  --text-subtitle: clamp(1.345rem, calc(1.034rem + 1.555vw), 2.2rem);      /* 35.2px — hero subtitle */
  --text-h4:       clamp(1.375rem, calc(0.966rem + 2.045vw), 2.5rem);      /* 40px  — card/section headings */
  --text-h3:       clamp(1.445rem, calc(0.807rem + 3.191vw), 3.2rem);      /* 51.2px — project title, marks */
  --text-h2:       clamp(1.525rem, calc(0.625rem + 4.5vw), 4rem);          /* 64px  — large headings, philosophy */
  --text-h1:       clamp(1.625rem, calc(0.398rem + 6.136vw), 5rem);        /* 80px  — hero tagline, disciplines h2 */
  --text-hero:     clamp(2.325rem, calc(-1.193rem + 17.591vw), 12rem);      /* 192px — contact headline */
}

@media (min-width: 768px) /* md */  { :root { --margin: 32px; } }
@media (min-width: 1200px) /* xl */ { :root { --margin: 48px; } }
@media (min-width: 1400px) /* xxl */ { :root { --margin: 64px; } }

/* --- 4. BASE ELEMENTS ----------------------------------------------------- */

body {
  font-family: var(--body);
  background: var(--bg) url("data:image/svg+xml,%3Csvg width='8' height='4' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='.5' cy='1.5' r='.5' fill='%23ebebeb'/%3E%3Ccircle cx='4.5' cy='3.5' r='.5' fill='%23ebebeb'/%3E%3C/svg%3E");
  background-size: 16px 8px;
  color: var(--text);
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  font-size: 1rem;
  line-height: 1.55;
}

a {
  color: inherit;
  text-decoration: none;
  /* Disable drag-ghost on anchors site-wide. The default browser behavior
     makes `<a>` drag a URL preview when you click-and-hold, which collides
     with the DotNav dots and any other link a user might tap-hold. No link
     on this site benefits from being draggable — it's pure noise. Text
     selection inside links still works (user-select is untouched). */
  -webkit-user-drag: none;
}

img {
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

cite,
address {
  font-style: normal;
}

/* --- 5. SECTION UTILITIES ------------------------------------------------- */

.section {
  /* Fixed 6rem (96px) vertical padding at every viewport. Individual
     sections override when they need different vertical spacing (e.g.,
     .philosophy-section uses fluid `max(60px, 8vw)` for divider weight). */
  padding-top: 6rem;
  padding-bottom: 6rem;
  position: relative;
}

.section--fullscreen {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: 6rem;
  padding-bottom: 6rem;
}

/* Fullscreen height kicks in only at xl+ where the desktop layout is active.
   Below 1200 the section sizes to its content (mobile stacked layouts don't
   want a forced 100vh). Capped at 90rem so it never exceeds that height on
   very tall viewports. */
@media (min-width: 1200px) /* xl */ {
  .section--fullscreen {
    min-height: min(100vh, 90rem);
    max-height: 90rem;
  }
}

.section--padded {
  padding-left: var(--margin);
  padding-right: var(--margin);
}

/* --- 6. FROSTED GLASS ----------------------------------------------------- */

.glass {
  background: var(--bg-60);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--text-06);
}

/* --- 7. REVEAL FLASH PREVENTION ------------------------------------------- */
/* The inline <head> script in templates/shell.ts adds .js-reveal to <html>
   before body parses. Reveal elements only hide themselves under that class,
   so JS-disabled users see content fully visible. The .is-visible class is
   added by public/js/reveal.js on intersection. */

html.js-reveal [data-reveal] {
  opacity: 0;
  transform: translateY(30px);
  transition:
    opacity 1s var(--ease) calc(var(--d, 0) * 100ms),
    transform 1s var(--ease) calc(var(--d, 0) * 100ms);
}

html.js-reveal [data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* About — three large fading-in paragraphs, centered. Mobile-first. */

.about-inner {
  max-width: 70rem;
  margin: 0 auto;
}

.about-text {
  font-size: var(--text-h4);
  font-weight: 300;
  line-height: 1.5;
  color: var(--text);
  margin-bottom: 2rem;
}

@media (min-width: 768px) /* md */ {
  .about-text {
    margin-bottom: 3rem;
  }
}

.about-text strong {
  font-weight: 600;
  color: var(--text);
}

/* Contact modal — triggered by the MobileBar CTA. Fixed overlay with a
   blurred backdrop and a centered card of contact links. Toggled via
   .is-active by public/js/contact-modal.js. Hidden above xl (1200px)
   where the desktop Contact section is directly visible instead. */

.contact-modal {
  position: fixed;
  inset: 0;
  z-index: 300;
  pointer-events: none;
  visibility: hidden;
  /* Delay the hide until child fades finish so they can animate out. */
  transition: visibility 0s .3s;
}

.contact-modal.is-active {
  pointer-events: auto;
  visibility: visible;
  transition: visibility 0s;
}

/* Backdrop fades in from fully transparent + no blur to dark + blurred.
   Transitioning the filter and background directly makes the blur ramp
   smoothly rather than snapping in at render time. */
.contact-modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0);
  backdrop-filter: blur(0);
  -webkit-backdrop-filter: blur(0);
  transition:
    background .3s var(--ease),
    backdrop-filter .3s var(--ease),
    -webkit-backdrop-filter .3s var(--ease);
}

.contact-modal.is-active .contact-modal__backdrop {
  background: rgba(0, 0, 0, .45);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.contact-modal__panel {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(90vw, 420px);
  padding: 3rem 2.5rem 2.5rem;
  background: var(--bg);
  border-radius: 1.4rem;
  box-shadow: 0 24px 80px rgba(0, 0, 0, .25);
  opacity: 0;
  transform: translate(-50%, -45%);
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}

.contact-modal.is-active .contact-modal__panel {
  opacity: 1;
  transform: translate(-50%, -50%);
}

.contact-modal__close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  width: 2.8rem;
  height: 2.8rem;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--muted);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  transition: background .2s var(--ease), color .2s var(--ease);
}

.contact-modal__close:hover {
  background: var(--text-06);
  color: var(--text);
}

.contact-modal__close svg {
  width: 1.6rem;
  height: 1.6rem;
}

.contact-modal__label {
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: .6rem;
}

.contact-modal__title {
  font-family: var(--heading);
  font-size: var(--text-h4);
  line-height: 1.1;
  margin-bottom: 2rem;
}

.contact-modal__links {
  list-style: none;
  display: flex;
  flex-direction: column;
}

.contact-modal__links li + li {
  border-top: 1px solid var(--rule);
}

.contact-modal__links a {
  display: block;
  padding: 1rem 0;
  font-size: var(--text-body);
  color: var(--text);
  transition: color .2s var(--ease);
}

.contact-modal__links a:hover {
  color: var(--accent);
}

@media (min-width: 1200px) /* xl */ {
  .contact-modal {
    display: none;
  }
}

/* Contact section — scroll-driven runway reveal at all viewports.
   Shorter runway on mobile (200vh), longer on desktop (250vh). The
   sticky container pins the content while the user scrolls through
   the runway; contact.js maps scroll progress to staggered opacity. */

.contact-runway {
  position: relative;
  height: 200vh;
}

@media (min-width: 992px) /* lg */ {
  .contact-runway {
    height: 250vh;
  }
}

.contact-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  width: 100%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--text);
  color: var(--white);
}

.contact-scroll-hint {
  position: absolute;
  top: 6rem;
  left: 50%;
  transform: translateX(-50%);
  color: var(--white);
  animation: scroll-bounce 2s var(--ease) infinite;
  transition: opacity .4s ease;
  z-index: 1;
}

@media (min-width: 992px) /* lg */ {
  .contact-scroll-hint {
    top: 10rem;
  }
}

@keyframes scroll-bounce {
  0%, 100% { transform: translateX(-50%) translateY(0) }
  50%      { transform: translateX(-50%) translateY(12px) }
}

.contact-sticky .section-label {
  color: var(--accent-light);
}

.contact-inner {
  width: 100%;
  max-width: 90rem;
  padding: 0 var(--margin);
}

.contact-headline {
  font-family: var(--heading);
  /* Height-driven with a width safety cap.
     Primary:   clamp(3rem, 16vh, 12rem) — pinned to a 100vh sticky frame,
                so height is the binding constraint for most viewports
                (3rem floor @≤300vh, 12rem ceiling @≥1200vh, 16vh between).
     Secondary: the `min(..., 18vw)` wrapper caps the result against
                viewport width so the widest word ("together.", ~4.3em of
                DM Serif Display at -.02em tracking) doesn't clip on narrow
                viewports. 18vw × 4.3em ≈ 77% of viewport width, leaving
                room for the `var(--margin)` page padding on both sides.
     The two are combined with `min()` — whichever constraint is tighter
     wins. On narrow-tall phones the vw cap takes over; on wide-short
     laptops the vh clamp does; on typical desktops both agree at 12rem. */
  font-size: min(clamp(3rem, 16vh, 12rem), 18vw);
  line-height: .88;
  letter-spacing: -.02em;
  /* Gap between the three stacked headline divs expressed in `em` so it
     scales with the already-vh-aware font-size. No breakpoint override
     needed: at the 12rem ceiling this gives ~4.2rem (matching the old
     desktop 4rem), at the 5rem floor it gives ~1.75rem (matching the
     old mobile 2rem). When the font shrinks on a short viewport the
     gap shrinks in lockstep, preserving the visual rhythm. */
  margin-bottom: .35em;
  /* No `will-change` — the headlines animate only during the contact
     runway reveal, which is one short pass near the end of page scroll.
     Keeping them on permanent composite layers costs GPU memory for
     every visit. Browsers auto-promote during active opacity/transform
     changes, which is when it actually matters. */
}

.contact-headline .outline {
  color: transparent;
  -webkit-text-stroke: 1.5px var(--white);
}

.contact-headline .accent-dot {
  color: var(--accent-light);
}

.contact-links {
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap;
  /* No `will-change` — see the matching note on .contact-headline above. */
}

@media (min-width: 768px) /* md */ {
  .contact-links {
    gap: 2rem;
  }
}

.contact-links a {
  font-size: var(--text-body);
  color: rgba(255, 255, 255, .5);
  position: relative;
  transition: color .3s var(--ease);
}

.contact-links a::after {
  content: '';
  position: absolute;
  bottom: -6px;
  left: 0;
  width: 0;
  height: 1px;
  background: var(--accent-light);
  transition: width .4s var(--ease);
}

.contact-links a:hover {
  color: var(--accent-light);
}

.contact-links a:hover::after {
  width: 100%;
}

/* "End-to-end, from idea to production" — five-card grid on a black
   background. Mobile-first: 1-col → 2-col (sm) → 5-col (xl). */

.disciplines {
  background: var(--text);
  color: var(--white);
  padding-left: var(--margin);
  padding-right: var(--margin);
}

@media (min-width: 1200px) /* xl */ {
  .disciplines {
    padding-right: calc(var(--margin) + 64px);
  }
}

.disciplines .section-label {
  color: var(--accent-light);
}

.disciplines h2 {
  font-family: var(--heading);
  font-size: var(--text-h1);
  margin-bottom: 3rem;
  line-height: 1.1;
}

@media (min-width: 768px) /* md */ {
  .disciplines h2 {
    margin-bottom: 5rem;
  }
}

.disciplines-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
}

@media (min-width: 576px) /* sm */ {
  .disciplines-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 3rem;
  }
}

@media (min-width: 1400px) /* xxl */ {
  .disciplines-grid {
    grid-template-columns: repeat(5, 1fr);
  }
}

.disciplines-item {
  border-left: 2px solid var(--accent-light);
  padding-left: 1.5rem;
}

@media (min-width: 768px) /* md */ {
  .disciplines-item {
    padding-left: 2rem;
  }
}

.disciplines-num {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--accent-light);
  margin-bottom: 1rem;
}

.disciplines-title {
  font-family: var(--heading);
  font-size: var(--text-h4);
  margin-bottom: 1rem;
}

.disciplines-desc {
  font-size: var(--text-body);
  font-weight: 400;
  color: rgba(255, 255, 255, .7);
  line-height: 1.6;
}

.disciplines-desc + .disciplines-desc {
  margin-top: .8rem;
}

/* Right-side vertical dot navigation. Hidden by default (mobile-first),
   shown at xl (1200px) where the desktop layout starts. */

.dot-nav {
  display: none;
}

@media (min-width: 1200px) /* xl */ {
  .dot-nav {
    position: fixed;
    right: 2.25rem;
    top: 50%;
    transform: translateY(-50%);
    z-index: 150;
    display: flex;
    flex-direction: column;
    gap: 1.6rem;
    padding: 1.6rem 1.2rem;
    border-radius: 2rem;
  }

  .dot-nav a {
    width: 1rem;
    height: 1rem;
    border-radius: 50%;
    border: 1.5px solid var(--text-15);
    background: var(--white);
    transition: all .3s ease;
    display: block;
    position: relative;
  }

  .dot-nav a::after {
    content: '';
    position: absolute;
    inset: -1rem;
  }

  .dot-nav a::before {
    content: attr(aria-label);
    position: absolute;
    right: calc(100% + 1.2rem);
    top: 50%;
    transform: translateY(-50%) translateX(.4rem);
    font-family: var(--body);
    font-size: var(--text-sm);
    font-weight: 500;
    letter-spacing: .06em;
    white-space: nowrap;
    color: var(--text);
    background: var(--white);
    padding: .4rem 1rem;
    border-radius: .6rem;
    box-shadow: 0 .2rem 1.2rem rgba(0, 0, 0, .1);
    opacity: 0;
    pointer-events: none;
    transition: opacity .25s ease, transform .25s ease;
  }

  .dot-nav a:hover::before {
    opacity: 1;
    transform: translateY(-50%) translateX(0);
  }

  .dot-nav a.is-active {
    background: var(--accent);
    border-color: var(--accent);
    transform: scale(1.3);
  }
}

/* Experience timeline. Mobile-first: single column with dot on left.
   At xl (1200px): alternating left/right with a centre line. */

/* Skills ("What I Bring") and Experience ("Experience Summary") are a
   conceptual pair — skills, then the proof. Zero the boundary padding
   between them so they read as a single 6rem gap instead of the 12rem
   that two stacked `.section`s would otherwise produce. Scoped to the
   adjacent-sibling relationship so it has no effect if either section
   is moved. */
#skills + #experience {
  padding-top: 0;
}

.timeline {
  max-width: 90rem;
  margin: 0 auto;
}

.timeline-card {
  position: relative;
  padding: 2.4rem;
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 1.4rem;
  box-shadow: 0 8px 48px rgba(0, 0, 0, .03);
}

@media (min-width: 768px) /* md */ {
  .timeline-card {
    padding: 3.6rem;
    border-radius: 1.8rem;
  }
}

@media (min-width: 1200px) /* xl */ {
  .timeline-card {
    padding: 4.8rem;
  }
}

.timeline-line {
  display: none;
}

@media (min-width: 1200px) /* xl */ {
  .timeline-line {
    display: block;
    position: absolute;
    left: 50%;
    top: 4.8rem;
    bottom: 3.6rem;
    width: 2px;
    background: var(--rule);
    transform: translateX(-50%);
  }
}

.timeline-item {
  display: grid;
  grid-template-columns: 1.4rem 1fr;
  gap: 1.5rem;
  margin-bottom: 3rem;
}

@media (min-width: 1200px) /* xl */ {
  .timeline-item {
    grid-template-columns: 1fr 1.4rem 1fr;
    gap: 2.8rem;
    margin-bottom: 4.8rem;
  }
}

.timeline-item:last-child {
  margin-bottom: 0;
}

.timeline-dot {
  width: 1.4rem;
  height: 1.4rem;
  background: var(--accent);
  border: 3px solid var(--bg);
  border-radius: 50%;
  box-shadow: 0 0 0 4px var(--accent-20);
  margin-top: .3rem;
  /* Mobile: force the dot into col 1 row 1 regardless of HTML order.
     Entries with the `.timeline-left` content div come BEFORE the dot in
     the DOM (for desktop alternating layout) so without this override the
     auto-flow would put the content into the narrow dot column. */
  grid-column: 1;
  grid-row: 1;
}

@media (min-width: 1200px) /* xl */ {
  .timeline-dot {
    /* Desktop: 3-col alternating layout — let HTML order drive placement. */
    grid-column: auto;
    grid-row: auto;
  }
}

.timeline-year {
  font-size: var(--text-base);
  font-weight: 500;
  letter-spacing: .1em;
  color: var(--accent);
  margin-bottom: .6rem;
}

.timeline-title {
  font-family: var(--heading);
  font-size: var(--text-h4);
  margin-bottom: .3rem;
}

.timeline-company {
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--muted);
  margin-bottom: .6rem;
}

.timeline-desc {
  font-size: var(--text-body);
  font-weight: 400;
  line-height: 1.65;
  color: var(--muted);
}

.timeline-desc + .timeline-desc {
  margin-top: .8rem;
}

.timeline-item .timeline-left {
  text-align: left;
}

@media (min-width: 1200px) /* xl */ {
  .timeline-item .timeline-left {
    text-align: right;
  }
}

.timeline-spacer {
  display: none;
}

@media (min-width: 1200px) /* xl */ {
  .timeline-spacer {
    display: block;
  }
}

/* Footer — single centered © line on a white background. Mobile-first. */

.footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
  padding: 3rem var(--margin);
  text-align: center;
  border-top: 1px solid var(--rule);
  font-size: var(--text-sm);
  /* Darker than --muted (#777) because that token fails WCAG AA contrast
     against the white footer background (4.48:1, below the 4.5:1 AA floor
     for normal text). #585858 hits exactly 7:1 — the AAA threshold for
     normal text. Scoped to the footer rather than darkening --muted
     globally, since --muted on tinted/textured backgrounds elsewhere
     passes contrast fine. */
  color: #585858;
  letter-spacing: .04em;
  background: var(--white);
}

@media (min-width: 1200px) /* xl */ {
  .footer {
    padding: 5rem var(--margin);
  }
}

/* Hero — masked photo + text.
   - Mobile-first: stacked. Photo on top, text below.
   - Landscape at lg (992px+): overlap. Photo left, text right, both full height.
   - Portrait at any width: stacked (overlap doesn't work on tall-narrow shapes). */

/* --- Mobile defaults: stacked layout ------------------------------------- */

.hero {
  position: relative;
  overflow: hidden;
}

.hero-photo {
  display: none;
}

/* Circular mobile avatar — shown below the desktop overlap breakpoint.
   The desktop HeroPhoto (above) is a full-hero-height bleed image; on
   narrower viewports that layout collapses, and instead we show a
   generously-sized circular portrait above the name. Step sizing:
   10rem (160px) default → 14rem (224px) at md. Rem-based so it scales
   with any future root font-size change and keeps the "in-system" feel
   of the rest of the spacing scale. Hidden at the same `(xl landscape
   OR xxl)` query where the desktop photo takes over — the rule lives
   further down in this file, next to the desktop layout block. */
.hero-avatar {
  display: block;
  width: 15rem;
  height: 15rem;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--text-15);
}

@media (min-width: 768px) /* md */ {
  .hero-avatar {
    width: 22rem;
    height: 22rem;
  }
}

/* Avatar + name grouped as a single identity unit on mobile: avatar on
   top, name centered beneath it, and the whole block centered
   horizontally within hero-text. `width: fit-content` + `align-self:
   center` gives the block its natural width (the avatar's) and places
   it at the centre of the text column. On desktop the avatar is
   display:none; the wrapper then contains only the name and sits in the
   normal text flow (align-self still centers it, which is fine for a
   single short line). */
.hero-identity {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.2rem;
  width: fit-content;
  align-self: center;
  margin-bottom: 2rem;
}

.hero-text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 3rem var(--margin) 3rem;
}

.hero-name {
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--muted);
  /* Identity wrapper owns the gap to the tagline below. */
  margin-bottom: 0;
}

.hero-tagline {
  font-family: var(--heading);
  /* Piecewise fluid: 2rem @375 → 3rem @992 → 4rem @1920 → 5rem @2560 */
  font-size: clamp(2rem, calc(1.392rem + 2.593vw), 3rem);
  line-height: 1.15;
  color: var(--text);
  margin-bottom: 1.5rem;
}

@media (min-width: 992px) /* lg */ {
  .hero-tagline {
    font-size: clamp(3rem, calc(1.931rem + 1.724vw), 4rem);
  }
}

@media (min-width: 1920px) /* hero-anchor */ {
  .hero-tagline {
    font-size: clamp(4rem, calc(1rem + 2.5vw), 5rem);
  }
}

.hero-tagline em {
  font-style: normal;
  color: var(--accent);
}

.hero-subtitle {
  font-family: var(--body);
  /* Piecewise fluid: 1.5rem @375 → 1.8rem @992 → 2.2rem @2560 */
  font-size: clamp(1.5rem, calc(1.318rem + 0.778vw), 1.8rem);
  font-weight: 300;
  line-height: 1.5;
  color: var(--muted);
  margin-bottom: 3rem;
}

@media (min-width: 992px) /* lg */ {
  .hero-subtitle {
    font-size: clamp(1.8rem, calc(1.547rem + 0.408vw), 2.2rem);
  }
}

.hero-subtitle em {
  font-style: normal;
  font-weight: 500;
  color: var(--accent);
}

/* Meta layout rules across viewport shapes:
   1. Narrow (default, < 992w OR landscape 992-1199): single column.
      Availability inline with · dots at ≥576, stacked lines below.
   2. Tablet portrait (992-1199w AND aspect ≤ 1): 3-col row.
   3. Desktop (≥1200w, any aspect): 2×2 L-shape with item 3 in right col.
   4. Landscape xxl (≥1400w AND aspect ≥ 4:3): 3-col row.
   5. Really big (≥1680w AND ≥1440h): 3-col row — portrait at this size
      gets plenty of room for a single row, no need for L-shape. */

.hero-meta {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2.5rem 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--rule);
}

/* Narrow portrait (≥sm 576): Availability values inline separated by · dots.
   Below 576 they stack as block lines again (too narrow for comma-list). */
@media (min-width: 576px) /* sm */ {
  .hero-meta > div:nth-child(3) .meta-value {
    display: inline;
  }

  .hero-meta > div:nth-child(3) .meta-value + .meta-value::before {
    content: ' · ';
    color: var(--muted);
  }
}

.meta-label {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .3rem;
}

.meta-value {
  font-size: var(--text-md);
}

/* Char-split spans for the cascade animation (server-rendered). */
.split-char {
  display: inline-block;
}

/* --- Landscape side-by-side (≥992px, aspect ≥ 1:1) -----------------------

   Photo container has fluid width + 100vh height. The image inside uses
   `background-size: contain` (and matching mask) so it's always shown
   at full natural size — no cropping, scaled to fit the container's
   dimensions while preserving aspect ratio. May leave empty space inside
   the container if container aspect doesn't match image aspect, but the
   image itself is always complete.

   Text margin-left is its own clamp, independent of photo natural size,
   so the text never gets pushed by the photo. */

/* Desktop overlap layout applies when:
   - xl landscape (≥1200w AND aspect ≥ 1), OR
   - xxl any aspect (≥1400w) — portrait viewports this wide still use the
     desktop layout since there's enough room; no reason for mobile view. */
@media (min-width: 1200px) /* xl */ and (min-aspect-ratio: 1/1),
       (min-width: 1400px) /* xxl */ {
  .hero {
    /* min at 100vh but capped at 90rem (1440px) so the section never
       exceeds that height on very tall viewports. */
    min-height: min(100vh, 90rem);
    max-height: 90rem;
  }

  /* Desktop swaps in HeroPhoto (below) and hides the circular HeroAvatar.
     Reset identity centering so the name sits left-aligned in the text
     column as before. */
  .hero-avatar {
    display: none;
  }

  .hero-identity {
    align-self: stretch;
  }

  .hero-photo {
    /* Photo fills the entire hero section. Image inside uses `contain`
       so it scales with the hero dimensions, preserving aspect, never
       cropped. Anchored left so the figure sits against the left edge.
       The mask scales identically so the silhouette/fade aligns.
       Format fallback via image-set(): browser picks AVIF → WebP → JPEG
       based on what it can decode. Variants emitted by `npm run images`. */
    display: block;
    position: absolute;
    inset: 0;
    width: auto;
    height: auto;
    background-image: image-set(
      url('/static/img/portfolio-me-v3.avif') type('image/avif'),
      url('/static/img/portfolio-me-v3.webp') type('image/webp'),
      url('/static/img/portfolio-me-v3.jpg') type('image/jpeg')
    );
    background-position: left center;
    background-size: contain;
    background-repeat: no-repeat;
    -webkit-mask-image: url('/static/img/portfolio-me-alpha.png');
    mask-image: url('/static/img/portfolio-me-alpha.png');
    -webkit-mask-position: left center;
    mask-position: left center;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
  }

  .hero-text {
    position: relative;
    z-index: 1;
    /* Margin-left transitions with viewport HEIGHT: 36rem @1080vh, up to
       47rem at 1440vh (matching the hero's max-height cap). Above 1440vh
       the hero section stops growing, so the text margin caps too. */
    margin-left: clamp(36rem, calc(3rem + 48.889vh), 47rem);
    padding: 9rem 9rem 3rem 4.5rem;
    min-height: min(100vh, 90rem);
    max-width: 95rem;
  }

  .hero-meta {
    gap: 3rem;
    /* Meta padding-left has two components added together:
       1) Height-based: 0 @820vh → 6rem @1080vh (caps at 6rem above).
       2) Width-based: 3rem @1200vw → 0 @1400vw (caps at 3rem below,
          0 above). Adds a small extra push at narrow-desktop widths
          where the photo is proportionally wider. */
    padding-left: calc(
      clamp(0rem, calc(-18.923rem + 36.923vh), 6rem) +
      clamp(0rem, calc(21rem - 24vw), 3rem)
    );
  }
}

/* Tablet (992-1199w, any aspect): 3-col row. The aspect-ratio gate was
   removed — at this width there's room for 3 columns regardless of
   whether the viewport is landscape or portrait, and toggling between
   1-col and 3-col as the user tilts a device or resizes around 1:1 is
   jarring. Above 1200w the L-shape rule below takes over. */
@media (min-width: 992px) /* lg */ and (max-width: 1199px) {
  .hero-meta {
    grid-template-columns: repeat(3, auto);
    gap: 3rem;
  }
  /* Availability back to stacked block lines (no inline dots). */
  .hero-meta > div:nth-child(3) .meta-value {
    display: block;
  }
  .hero-meta > div:nth-child(3) .meta-value + .meta-value::before {
    content: none;
  }
}

/* Desktop (≥1200w, any aspect): 2×2 L-shape with item 3 in right column.
   Applies to both portrait and landscape at desktop widths. Overridden by
   the row rules below for landscape xxl and really-big viewports. */
@media (min-width: 1200px) /* xl */ {
  .hero-meta {
    grid-template-columns: repeat(2, 1fr);
  }
  .hero-meta > div:nth-child(3) {
    grid-column: 2;
  }
  .hero-meta > div:nth-child(3) .meta-value {
    display: block;
  }
  .hero-meta > div:nth-child(3) .meta-value + .meta-value::before {
    content: none;
  }
}

/* Landscape xxl (≥1400w AND aspect ≥ 4:3): 3-col row. */
@media (min-width: 1400px) /* xxl */ and (min-aspect-ratio: 4/3) {
  .hero-meta {
    grid-template-columns: repeat(3, auto);
  }
  .hero-meta > div:nth-child(3) {
    grid-column: auto;
  }
}

/* Really big (≥1680w AND ≥1440h): 3-col row regardless of aspect. Portrait
   at this size has plenty of room for a single row; L-shape feels cramped. */
@media (min-width: 1680px) and (min-height: 1440px) {
  .hero-meta {
    grid-template-columns: repeat(3, auto);
  }
  .hero-meta > div:nth-child(3) {
    grid-column: auto;
  }
}

/* Available for hire — text + workstation photo. Mobile-first:
   stacked 1-col → 2-col grid at lg (992px). Vertical padding comes
   from the shared `.section` class (6rem top/bottom). */

.hire-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 3rem;
  align-items: center;
}

@media (min-width: 992px) /* lg */ {
  .hire-layout {
    grid-template-columns: 1fr 1fr;
    gap: 4rem;
  }
}

.hire-image img {
  width: 100%;
  border-radius: 1.4rem;
  object-fit: cover;
}

.hire-list {
  max-width: 90rem;
}

.hire-item {
  display: grid;
  grid-template-columns: 4rem 1fr;
  gap: 1.5rem;
  padding: 2.5rem 0;
  border-bottom: 1px solid var(--rule);
  align-items: start;
  transition: transform .6s var(--ease);
}

@media (min-width: 768px) /* md */ {
  .hire-item {
    grid-template-columns: 6rem 1fr;
    gap: 2rem;
    padding: 3.5rem 0;
  }
}

.hire-item:first-child {
  border-top: 1px solid var(--rule);
}

.hire-item:hover {
  transform: translateX(1rem);
}

.hire-num {
  font-size: var(--text-base);
  font-weight: 500;
  letter-spacing: .1em;
  color: var(--accent);
  padding-top: .3rem;
}

.hire-title {
  font-family: var(--heading);
  font-size: var(--text-h4);
  margin-bottom: .6rem;
}

.hire-desc {
  font-size: var(--text-body);
  font-weight: 400;
  line-height: 1.65;
  color: var(--muted);
  max-width: 50rem;
}

/* Lightbox — fullscreen image overlay used by lightbox.js. Hidden by
   default; .is-active activates. Desktop only — gated in JS. */

.lightbox-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 1);
  z-index: 80;
  opacity: 0;
  pointer-events: none;
}

.lightbox-backdrop.is-active {
  opacity: 1;
  pointer-events: auto;
}

.lightbox-close {
  position: fixed;
  top: 2rem;
  right: 2rem;
  z-index: 1000;
  width: 4rem;
  height: 4rem;
  border: none;
  background: none;
  color: var(--white);
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity .3s ease;
}

.lightbox-close.is-active {
  opacity: 1;
  pointer-events: auto;
}

.lightbox-close svg {
  width: 100%;
  height: 100%;
}

/* Tech-skills marquee — black bar with dotted technology labels right
   after the hero. Mobile-first flex-wrap, center-aligned rows, no dangling
   dots, crisp text.

   Two-layer implementation:

   Layer 1 (.marquee-track--dots): contrast(9999) applied. Text set to
   transparent so it just contributes to the flex layout. Each item has a
   ::before (left) and ::after (right) red half-opacity dot in the middle
   of its adjacent gap. A SINGLE dot on the black bg darkens to ~rgb(82,0,0)
   — below the contrast midpoint (127.5) → thresholded to pure black,
   invisible. TWO overlapping dots (right of item A + left of item B in
   the same gap) composite to ~rgb(145,0,0) — above midpoint → contrast
   pushes to pure red, fully visible. Wrapped rows' first/last items have
   lone dots with no partner → invisible. No dangling dots, no JS.

   Layer 2 (.marquee-track--text): same DOM structure and layout rules,
   no filter. Crisp white text. Sits on top of the dots layer via grid
   stacking.

   Both layers produce identical flex-wrap layout because they share
   content, font properties, and container constraints. */

.marquee {
  padding: 24px 0;
  overflow: hidden;
  background: var(--text);
  /* Anchor-scroll lands here when the dot-nav "Expertise" dot is clicked.
     The marquee is a thin band; without an offset, the top nav overlaps
     and clips roughly half its content. Push the scroll target up so the
     marquee lands fully below the chrome. */
  scroll-margin-top: 6rem;
}

@media (min-width: 768px) /* md */  { .marquee { padding: 32px 0; } }
@media (min-width: 1200px) /* xl */ { .marquee { padding: 48px 0; } }
@media (min-width: 1400px) /* xxl */ { .marquee { padding: 64px 0; } }

/* Grid stack — both tracks occupy the same cell, overlapping perfectly.
   `justify-content: center` centers the cell within the full-width stack so
   the max-width cap on the tracks (below) reads as "constrained ribbon
   centered on the page" on very wide viewports. */
.marquee-stack {
  display: grid;
  justify-content: center;
}

.marquee-stack > * {
  grid-area: 1 / 1;
}

.marquee-track {
  --gap: 2.5rem;
  --dot: 8px;
  /* Alpha tuned so single-dot darkens below the contrast midpoint (invisible)
     and two overlapping dots combine above it (visible). */
  --dot-alpha: 0.4;
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--gap);
  row-gap: 1rem;
  justify-content: center;
  padding-inline: var(--margin);
  /* Cap the ribbon width so the row layout stays consistent on very wide
     viewports (4K+). Without this the items get distributed over too much
     horizontal space and wrap unpredictably. 105rem (1680px) matches the
     "really big" landscape band; the marquee-stack's justify-content centers
     the capped track within the viewport above this width. */
  max-width: 105rem;
}

@media (min-width: 768px) /* md */ {
  .marquee-track { --gap: 3rem; }
}

@media (min-width: 1200px) /* xl */ {
  .marquee-track { --gap: 4rem; }
}

.marquee-item {
  font-size: var(--text-body);
  font-weight: 500;
  white-space: nowrap;
}

/* --- Layer 1: filtered dots ---------------------------------------------- */

.marquee-track--dots {
  /* Solid black inside the filtered layer — the contrast threshold needs
     a uniform backdrop, otherwise single dots don't get fully crushed and
     paired dots don't fully pop. */
  background: black;
  filter: contrast(4);
  color: transparent; /* hide text but keep layout */
  pointer-events: none;
}

.marquee-track--dots .marquee-item {
  position: relative;
}

.marquee-track--dots .marquee-item::before,
.marquee-track--dots .marquee-item::after {
  content: '';
  position: absolute;
  top: 50%;
  width: var(--dot);
  height: var(--dot);
  border-radius: 50%;
  background: rgb(255 0 0 / var(--dot-alpha));
  transform: translate(-50%, -50%);
}

.marquee-track--dots .marquee-item::before {
  left: calc(var(--gap) / -2);
}

.marquee-track--dots .marquee-item::after {
  left: calc(100% + var(--gap) / 2);
}

/* --- Layer 2: crisp text ------------------------------------------------- */

.marquee-track--text {
  color: var(--white);
  position: relative;
  z-index: 1;
}

/* ≥1920: wider gap + larger dots (still allowed to wrap). */
@media (min-width: 1920px) /* hero-anchor */ {
  .marquee-track {
    --gap: 5rem;
    --dot: 10px;
  }
}

/* Mobile-only sticky top bar. Visible by default (mobile-first), hidden at
   xl (1200px) where the desktop Nav, DotNav, and hero overlap layout take over. */

.mobile-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: sticky;
  top: 0;
  z-index: 200;
  height: 64px;
  padding: 0 var(--margin);
  /* Override .glass's all-sides border — only bottom edge. */
  border: none;
  border-bottom: 1px solid var(--rule);
}

.mobile-bar__logo {
  font-family: var(--heading);
  font-size: 1.8rem;
}

.mobile-bar__logo span {
  color: var(--accent);
}

.mobile-bar__cta {
  font-family: inherit;
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: .04em;
  padding: 12px 24px;
  border: none;
  border-radius: 10rem;
  background: var(--accent);
  color: var(--white);
  cursor: pointer;
  transition: background .3s var(--ease);
}

.mobile-bar__cta:hover {
  background: var(--accent-dark);
}

@media (min-width: 1200px) /* xl */ {
  .mobile-bar {
    display: none;
  }
}

/* Short-viewport rescue: below 390px tall the 64px mobile bar eats too
   much of the viewport. Hide it regardless of width. Paired rule in
   nav.css hides the desktop top nav in the same regime. */
@media (max-height: 390px) {
  .mobile-bar {
    display: none;
  }
}

/* Desktop top nav — fixed top, frosted glass. Hidden by default (mobile-first),
   shown at xl (1200px) where the desktop layout starts. */

.nav {
  display: none;
}

@media (min-width: 1200px) /* xl */ {
  .nav {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 100;
    height: 6.4rem;
    padding: 0 var(--margin);
    display: flex;
    align-items: center;
    justify-content: space-between;
    border-radius: 0;
    transition: background .5s var(--ease);
  }

  .nav.is-scrolled {
    background: var(--bg-75);
  }

  .nav-logo {
    font-family: var(--heading);
    font-size: var(--text-lead);
  }

  .nav-logo span {
    color: var(--accent);
  }

  .nav-links {
    display: flex;
    gap: 2.5rem;
  }

  .nav-links a {
    font-size: var(--text-base);
    font-weight: 500;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted);
    transition: color .3s var(--ease);
  }

  .nav-links a:hover {
    color: var(--accent);
  }
}

/* Short-viewport rescue: below 390px tall there's not enough vertical room
   for both the fixed top nav (6.4rem) and any meaningful section content.
   Hide the nav entirely in that regime. The dot-nav is right-edge and
   doesn't eat vertical space, so it stays. */
@media (max-height: 390px) {
  .nav {
    display: none;
  }
}

/* 404 page body. */

.not-found {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--margin);
  text-align: center;
  gap: 2rem;
}

.not-found__code {
  font-family: var(--heading);
  font-size: clamp(8rem, 18vw, 22rem);
  line-height: 1;
  color: var(--accent);
}

.not-found__msg {
  font-size: 1.8rem;
  color: var(--muted);
}

.not-found__back {
  font-size: 1.4rem;
  font-weight: 500;
  letter-spacing: .04em;
  color: var(--accent);
  border-bottom: 1px solid var(--accent-20);
  transition: color .3s var(--ease), border-color .3s var(--ease);
}

.not-found__back:hover {
  color: var(--accent-dark);
  border-bottom-color: var(--accent);
}

/* Philosophy divider — dark slab with fiber-texture background, parallax
   offset driven by nav.js setting --phil-parallax. Mobile-first. */

.philosophy-section {
  background: var(--text);
  position: relative;
  overflow: hidden;
  /* Isolate the section's paint + layout work from the rest of the page.
     This lets the browser treat the section as its own composite surface
     and commit it earlier, reducing the first-paint hitch when scrolling
     in. The contents (heavy text-shadows, large transformed background)
     are expensive to rasterize the first time — `contain` means that
     work doesn't ripple out. */
  contain: layout paint;
  margin-left: var(--margin);
  margin-right: var(--margin);
  /* 6rem vertical margin doubles the gap to adjacent sections. Each
     neighbour is a `.section` with 6rem top/bottom padding, so 6rem
     padding + 6rem margin = 12rem of whitespace framing the dark slab
     on both sides. Philosophy is a divider, not an in-flow section —
     it needs the extra breathing room to read as a punctuation mark
     rather than a continuation. Margins don't collapse against the
     neighbours' padding, so both sides stack cleanly. */
  margin-top: 6rem;
  margin-bottom: 6rem;
  border-radius: 1rem;
  /* Philosophy is a divider — its vertical presence is part of its
     visual weight. Fluid padding gives it the intended generous height
     on wider viewports; the 5rem (80px) floor keeps it substantial on
     narrow viewports where 8vw would collapse to ~30px. Overrides the
     .section padding inherited from styles.css. */
  padding-top: max(5rem, 9vw);
  padding-bottom: max(5rem, 9vw);
}

@media (min-width: 768px) /* md */ {
  .philosophy-section {
    border-radius: 2rem;
  }
}

.philosophy-section::after {
  content: '';
  position: absolute;
  inset: -10%;
  /* Format fallback: AVIF → WebP → JPEG. Variants emitted by
     `npm run images`. The JPEG was downsampled to 1600px wide during
     generation, cutting the decoded RGBA footprint from ~25MB to ~5MB
     and eliminating the scroll-in hitch that the preload workaround
     used to mask. */
  background-image: image-set(
    url('/static/img/bg-fiber.avif') type('image/avif'),
    url('/static/img/bg-fiber.webp') type('image/webp'),
    url('/static/img/bg-fiber.jpg') type('image/jpeg')
  );
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  z-index: 0;
  transform: translateY(var(--phil-parallax, 0));
  will-change: transform;
}

.philosophy-section::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, .33);
  z-index: 0;
}

.philosophy {
  max-width: 70rem;
  margin: 0 auto;
  position: relative;
  z-index: 1;
  border-left: 4px solid var(--accent-light);
  padding-left: 1.5rem;
}

@media (min-width: 768px) /* md */ {
  .philosophy {
    padding-left: 3rem;
  }
}

.philosophy-text {
  font-family: var(--heading);
  font-size: var(--text-h2);
  font-weight: 400;
  line-height: 1.25;
  text-shadow:
    0 0 8px rgba(0, 0, 0, 1),
    0 0 16px rgba(0, 0, 0, 1),
    0 0 30px rgba(0, 0, 0, .9),
    0 0 50px rgba(0, 0, 0, .8),
    0 0 80px rgba(0, 0, 0, .6),
    0 0 120px rgba(0, 0, 0, .4);
}

.philosophy-text span {
  color: var(--accent-light);
}

.philosophy .section-label {
  color: var(--accent-light);
  text-shadow:
    0 0 2px rgba(0, 0, 0, 1),
    0 0 2px rgba(0, 0, 0, 1),
    0 0 5px rgba(0, 0, 0, 1),
    0 0 5px rgba(0, 0, 0, 1),
    0 0 10px rgba(0, 0, 0, 1),
    0 0 10px rgba(0, 0, 0, 1),
    0 0 15px rgba(0, 0, 0, 1),
    0 0 15px rgba(0, 0, 0, 1),
    0 0 20px rgba(0, 0, 0, 1),
    0 0 20px rgba(0, 0, 0, 1),
    0 0 25px rgba(0, 0, 0, .9),
    0 0 30px rgba(0, 0, 0, .8);
}

/* Projects section. Mobile-first: stacked info + vertical image stack.
   At md (768px): 2-col info grid. Image grid auto-fit. Vertical padding
   comes from the shared `.section` class (6rem top/bottom). */

.projects-label {
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 3rem;
  padding: 0 var(--margin);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

@media (min-width: 768px) /* md */ {
  .projects-label {
    margin-bottom: 6rem;
  }
}

/* .trust-badge lives in public/css/trust-badge.css — shared with Testimonials. */

.project {
  padding: 32px 0;
  border-bottom: 1px solid var(--rule);
}

@media (min-width: 768px) /* md */  { .project { padding: 48px 0; } }
@media (min-width: 1200px) /* xl */ { .project { padding: 6vw 0; } }

.project:first-of-type {
  border-top: 1px solid var(--rule);
}

.project-info {
  padding: 0 var(--margin);
  margin-bottom: 2rem;
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  align-items: start;
}

@media (min-width: 768px) /* md */ {
  .project-info {
    grid-template-columns: 6rem 1fr;
    gap: 2rem;
    margin-bottom: 3rem;
  }
}

.project-num {
  font-size: var(--text-base);
  font-weight: 500;
  letter-spacing: .1em;
  color: var(--accent);
  padding-top: .4rem;
}

.project-title {
  font-family: var(--heading);
  font-size: var(--text-h3);
  line-height: 1.1;
  margin-bottom: .8rem;
  max-width: 50rem;
  display: flex;
  align-items: center;
  gap: .3rem;
}

.project-header {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: .8rem;
}

@media (min-width: 768px) /* md */ {
  .project-header {
    gap: 2rem;
  }
}

.project-header .project-title {
  margin-bottom: 0;
}

.project-links {
  display: flex;
  gap: .8rem;
  flex-wrap: wrap;
}

.project-link {
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: .04em;
  padding: 8px 16px 8px 14px;
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  border-radius: 10rem;
  background: var(--accent);
  color: var(--white);
  transition: background .3s var(--ease);
}

.project-link:hover {
  background: var(--accent-dark);
}

.project-link--gh {
  background: var(--text);
}

.project-link--gh:hover {
  background: #555;
}

.section-label .project-link {
  vertical-align: middle;
  margin-left: 1rem;
  position: relative;
  top: -.15em;
}

.project-link--cv {
  background: var(--info);
}

.project-link--cv:hover {
  background: var(--info-dark);
}

.project-link--disabled {
  background: none;
  border: 1.5px solid var(--text-15);
  color: var(--muted);
  cursor: default;
}

.project-link--disabled:hover {
  background: none;
}

.project-favicon {
  width: 1.8em;
  height: 1.8em;
  aspect-ratio: 1;
  vertical-align: middle;
  margin-right: .6rem;
  border-radius: .3rem;
  border: 1.5px solid var(--text-15);
  background: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: .7em;
  line-height: 1;
  object-fit: contain;
}

.project-subtitle {
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 1rem;
}

@media (min-width: 768px) /* md */ {
  .project-subtitle {
    margin-bottom: 1.5rem;
  }
}

.project-brief {
  font-size: var(--text-lead);
  font-weight: 400;
  line-height: 1.5;
  color: var(--text);
  max-width: 50rem;
  margin-bottom: 1.2rem;
}

.project-desc {
  font-size: var(--text-body);
  font-weight: 400;
  line-height: 1.65;
  color: var(--muted);
  max-width: 50rem;
  margin-bottom: 1.5rem;
}

/* "Technical details" toggle hiding the technical-depth `desc` paragraphs.
   Native <details> — zero JS. Closed by default. The brief / desc split in
   content/projects.ts is the seam: brief stays inline as the punchy framing,
   desc collapses behind this. Googlebot still reads the contents.
   Side note: opening leaves nav.js's cached offsets slightly stale until the
   next resize. Acceptable for now.

   Open/close is animated via the grid-template-rows trick: the inner wrapper
   is a grid item whose row interpolates from 0fr (collapsed) to 1fr
   (expanded). `min-height: 0` + `overflow: hidden` on the item lets the row
   shrink to 0 cleanly. We apply `display: grid` to `::details-content` (the
   UA pseudo wrapping the non-summary content) so the trick lives in one
   place; the inner div is the sole grid item.

   We pin `content-visibility: visible` on `::details-content` to override
   the UA's hide on close — without this, Safari 26 drops the closing
   transition (it applies content-visibility:hidden ahead of the animation
   even with `transition-behavior: allow-discrete`). The <details> [open]
   attribute still drives the a11y "collapsed/expanded" semantic; we're only
   overriding the visual hide so the grid-rows transition runs in both
   directions. Acceptable because the desc content has no focusable children
   (plain <p> + <strong>), so leaving it in flow when collapsed has no a11y
   downside.

   Chosen over `interpolate-size: allow-keywords` + `block-size` because
   Safari 26 (the latest as of this writing) doesn't support
   `interpolate-size` yet but does support `::details-content`. The grid trick
   needs neither, just the long-supported `grid-template-rows` interpolation. */
.project-more {
  max-width: 50rem;
  margin-bottom: 1.5rem;
}

.project-more::details-content {
  display: grid;
  grid-template-rows: 0fr;
  content-visibility: visible;
  transition: grid-template-rows 500ms var(--ease);
}

.project-more[open]::details-content {
  grid-template-rows: 1fr;
}

.project-more__inner {
  overflow: hidden;
  min-height: 0;
}

/* Outlined blue pill — same family as the .project-link--cv (Download CV)
   solid blue pill, but outlined to read as a secondary disclosure action.
   Blue (not red) so it's distinct from the Live/Code primary action pills
   above it; skimmers should clock this as a button on first scan without
   it competing for primary-action attention. */
.project-more__summary {
  cursor: pointer;
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--info);
  list-style: none;            /* hide default disclosure triangle (FF/Chrome) */
  padding: 8px 16px 8px 14px;
  user-select: none;
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  width: fit-content;
  border: 1.5px solid var(--info);
  border-radius: 10rem;
  background: var(--bg);
  transition:
    background .25s var(--ease),
    color .25s var(--ease),
    border-color .25s var(--ease);
}
.project-more__summary::-webkit-details-marker { display: none; } /* Safari */

.project-more__summary::after {
  content: '+';
  font-weight: 400;
  font-size: 1.2em;
  line-height: 1;
}
.project-more[open] > .project-more__summary::after {
  content: '−';
}

.project-more__summary:hover {
  background: var(--info);
  color: var(--white);
  border-color: var(--info);
}

.project-more__inner > .project-desc:first-child {
  margin-top: 1rem;
}

/* Inline image embedded inside a brief/desc paragraph via HTML. Displays as
   a block below the surrounding text, with max-width matching the text
   column so it doesn't dominate. drop-shadow (not box-shadow) so the
   shadow follows the image's alpha channel — right for transparent PNGs. */
.project-inline-img {
  display: block;
  width: 100%;
  max-width: 50rem;
  height: auto;
  margin-top: 1.2rem;
  -webkit-filter: drop-shadow(0 6px 18px rgba(0, 0, 0, .18));
  filter: drop-shadow(0 6px 18px rgba(0, 0, 0, .18));
}

.project-tags {
  display: flex;
  flex-wrap: wrap;
  gap: .7rem;
  max-width: 50rem;
}

.project-tag {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: 6px 16px;
  border: 1px solid var(--accent-20);
  border-radius: 10rem;
  color: var(--accent-70);
  background: var(--bg);
}

/* Image grid. Below lg (992px): vertical stack, no interactivity — the
   single full-width presentation reads better on phones AND tablet
   portrait (down to ~768px the tablet-style 2-col grid crowded the
   captions/titles). At lg+: auto-fit grid with tilt + lightbox active.
   The pointer-events and grid switch move together so "stacked" always
   means "non-interactive" — avoids the odd state of full-width images
   with tilt applied. */

.carousel-wrap {
  position: relative;
  user-select: none;
  -webkit-user-select: none;
}

.carousel-track {
  display: flex;
  flex-direction: column;
  gap: 2.4rem;
  padding: 0 var(--margin);
}

@media (min-width: 992px) /* lg */ {
  .carousel-track {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 24px;
  }
}

.carousel-img {
  width: 100%;
  aspect-ratio: 3/2;
  border-radius: 8px;
  cursor: default;
  box-shadow: 0 4px 32px rgba(0, 0, 0, .12), 0 1px 6px rgba(0, 0, 0, .08);
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
}

@media (min-width: 992px) /* lg */ {
  .carousel-img {
    width: auto;
    border-radius: .6rem;
    cursor: pointer;
    pointer-events: auto;
    /* No `will-change: transform` here. Tilt only runs while the user
       is hovering a card — a tiny fraction of page-view time. A permanent
       `will-change` would keep a composite layer alive for every
       carousel image at all times. Browsers auto-promote during active
       transform animations, which is when promotion actually matters. */
  }
}

.carousel-img[data-no-zoom] {
  cursor: default;
}

/* Section eyebrow — small uppercase red label rendered above each section's
   heading. Used by SectionLabel and inlined directly in places that need
   adjacent inline content (e.g. the Experience CV download link). */

.section-label {
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 2rem;
}

@media (min-width: 768px) /* md */ {
  .section-label {
    margin-bottom: 3rem;
  }
}

@media (min-width: 1200px) /* xl */ {
  .section-label {
    margin-bottom: 4rem;
  }
}

/* Skills — six cards in a responsive grid. Hover lifts the card and slides
   in a top-border accent. Mobile-first: 1-col → 2-col (md) → 3-col (xl). */

.skills-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}

@media (min-width: 768px) /* md */ {
  .skills-grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 2rem;
  }
}

@media (min-width: 1400px) /* xxl */ {
  .skills-grid {
    grid-template-columns: repeat(3, 1fr);
    gap: 2.4rem;
  }
}

.skill-card {
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 1.4rem;
  padding: 2rem;
  position: relative;
  overflow: hidden;
  transition: box-shadow .4s var(--ease), transform .4s var(--ease);
}

@media (min-width: 768px) /* md */  { .skill-card { padding: 3rem; } }
@media (min-width: 1200px) /* xl */ { .skill-card { padding: 4.5rem; } }

html.js-reveal [data-reveal].is-visible.skill-card.settled {
  transition: box-shadow .4s var(--ease), transform .4s var(--ease);
}

html.js-reveal [data-reveal].is-visible.skill-card.settled:hover {
  box-shadow: 0 12px 64px rgba(0, 0, 0, .06);
  transform: translateY(-6px) !important;
}

.skill-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .4s var(--ease);
}

.skill-card:hover::before {
  transform: scaleX(1);
}

.skill-card h3 {
  font-family: var(--heading);
  font-size: var(--text-h4);
  margin-bottom: .4rem;
}

.skill-card-sub {
  font-size: var(--text-sm);
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 1.6rem;
}

.skill-card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: .8rem;
}

.skill-card li {
  font-size: var(--text-body);
  font-weight: 400;
  color: var(--muted);
  padding-left: 1.6rem;
  position: relative;
}

.skill-card li::before {
  content: '';
  position: absolute;
  left: 0;
  /* Vertically center the dot on the first line. 0.775em matches the
     line-box center (half of the inherited 1.55 line-height), scaling
     with the font size so the alignment holds across RFS clamp values. */
  top: 0.775em;
  transform: translateY(-50%);
  width: .6rem;
  height: .6rem;
  border-radius: 50%;
  background: var(--accent-35);
}

/* Testimonials grid. Mobile-first: 1-col → auto-fill grid at md (768px).
   Avatar gradient is selected via [data-gradient] (1 / 2 / 3). Vertical +
   horizontal padding comes from the shared `.section .section--padded`
   classes on the element. */

.testimonials h2 {
  font-family: var(--heading);
  font-size: var(--text-h2);
  margin-bottom: 2rem;
}

@media (min-width: 768px) /* md */ {
  .testimonials h2 {
    margin-bottom: 3rem;
  }
}

.testimonials-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.5rem;
}

@media (min-width: 768px) /* md */ {
  .testimonials-grid {
    grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
    gap: 2.4rem;
    /* Cap the grid width so auto-fill can't pack a 4th column on ultra-wide
       viewports. Math: 4 × 30rem + 3 × 2.4rem = 127.2rem (2035px) is the
       grid width at which a 4th column starts fitting. 120rem (1920px) is
       comfortably below that, yet still wide enough for 3 columns
       (3 × 30rem + 2 × 2.4rem = 94.8rem / 1517px). Content has 3
       testimonials + 1 CTA, so 3-col gives a clean 2-row layout. */
    max-width: 120rem;
    margin-left: auto;
    margin-right: auto;
  }
}

.testimonial {
  background: var(--bg);
  border: 1px solid var(--rule);
  border-radius: 2rem;
  padding: 2.4rem;
  transition: transform .4s var(--ease), box-shadow .4s var(--ease);
  display: flex;
  flex-direction: column;
}

@media (min-width: 768px) /* md */ {
  .testimonial {
    padding: 3.2rem;
  }
}

html.js-reveal [data-reveal].is-visible.testimonial.settled {
  transition: box-shadow .4s var(--ease), transform .4s var(--ease);
}

html.js-reveal [data-reveal].is-visible.testimonial.settled:hover {
  transform: translateY(-6px) !important;
  box-shadow: 0 24px 76px rgba(0, 0, 0, .05);
}


.testimonial-mark {
  font-family: var(--heading);
  font-size: var(--text-h3);
  color: var(--accent-20);
  line-height: 1;
  margin-bottom: .8rem;
}

.testimonial p {
  flex: 1;
  font-size: var(--text-body);
  font-weight: 400;
  color: var(--text-50);
  line-height: 1.7;
  font-style: italic;
  margin-bottom: 2rem;
}

.testimonial-author {
  display: flex;
  align-items: center;
  gap: 1.4rem;
}

.testimonial-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  padding: 4px;
  border: none;
}

.testimonial-avatar[data-gradient="1"] {
  background: linear-gradient(135deg, var(--avatar-1b), var(--avatar-1a));
}

.testimonial-avatar[data-gradient="2"] {
  background: linear-gradient(135deg, var(--avatar-2a), var(--avatar-2b));
}

.testimonial-avatar[data-gradient="3"] {
  background: linear-gradient(135deg, var(--avatar-3a), var(--avatar-3b));
}

.testimonial-name {
  font-size: var(--text-md);
  font-weight: 600;
}

.testimonial-role {
  font-size: var(--text-sm);
  color: var(--muted);
}

.testimonial--cta {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  border-style: dashed;
  border-color: var(--text-15);
}

.testimonial--cta:hover {
  transform: none;
  box-shadow: none;
}

.testimonial-placeholder {
  font-size: var(--text-body);
  font-weight: 400;
  color: var(--text-15);
  font-style: italic;
  display: flex;
  align-items: center;
  gap: 0;
  user-select: none;
  -webkit-user-select: none;
}

.testimonial-cursor {
  display: inline-block;
  width: 2px;
  height: 1.6rem;
  background: var(--muted);
  margin-right: .3rem;
  animation: cursor-blink 1s ease-in-out infinite;
}

@keyframes cursor-blink {
  0%, 100% { opacity: 1 }
  50% { opacity: 0 }
}

/* Trust-badge pill — the green "VPN Protected" / "Trusted by ..." label
   used in both Projects (project links row) and Testimonials (heading
   row). Lives in its own file because it's shared across sections;
   putting it back in projects.css or testimonials.css would imply
   ownership by one of them when it's genuinely a shared primitive.

   The #2e7d32 green is specific to this component and has no token. If
   it starts showing up elsewhere, promote it into :root in styles.css. */

.trust-badge {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: .04em;
  color: #2e7d32;
  padding: 8px 16px 8px 14px;
  border-radius: 10rem;
  border: 1.5px solid #2e7d32;
  background: var(--bg);
  text-transform: none;
  vertical-align: middle;
}

.trust-badge svg {
  stroke: #2e7d32;
  width: 21px;
  height: 21px;
}
