/*
  bnfldt — style.css
  ─────────────────────────────────────────────────────────────────────────────
  1.  Custom properties
  2.  Reset & base
  3.  Utilities
  4.  Site logo (fixed, JS-animated)
  5.  Site header (glass bg + nav only)
  6.  Hero section
  7.  Container & section base
  8.  Services section
  9b. Gallery section
  9.  Thinking section
  10. CTA section
  11c. Gallery overlay + viewer
  11. Site footer
  12. Contact page
  13. Keyframe animations
  14. Reduced motion
  15. Print
*/


/* ─── 0. Fonts ─────────────────────────────────────────────────────────────── */

/* Absans replaces the two Google-hosted families, and with them the last
   external request: no preconnects, no third-party stylesheet, and a CSP that
   is now 'self' only.

   One static face at weight 400. The weight range says 300–500 so the browser
   maps every weight the stylesheet asks for onto this file rather than
   synthesising one. The font-weight declarations further down are therefore
   inert today, and deliberately kept: they still say what the design intends,
   and they start working again the day a Light or Medium is added. */
@font-face {
  font-family: 'Absans';
  src: url('/assets/fonts/Absans-Regular.woff2') format('woff2');
  font-weight: 300 500;
  font-style: normal;
  font-display: swap;
}


/* ─── 1. Custom properties ─────────────────────────────────────────────────── */

:root {
  color-scheme: light dark;

  /* Brand palette */
  --brand-dark:  #1D1D1D;
  --brand-smoke: #E7EDEB;
  --brand-bone:  #FCFAF8;

  /* Theme tokens — light default. Dark lives in the
     prefers-color-scheme: dark block below. */
  --color-bg:           #FCFAF8;
  --color-bg-section:   #EBEAE2;
  --color-bg-elevated:  #FFFFFF;
  --color-canvas-bg:    #FCFAF8;
  --color-text:         #1D1D1D;
  --color-text-muted:   rgba(29, 29, 29, 0.68);
  --color-text-faint:   rgba(29, 29, 29, 0.62);
  --color-border:       rgba(29, 29, 29, 0.10);
  --color-border-hover: rgba(29, 29, 29, 0.30);
  --color-header-bg:    rgba(252, 250, 248, 0.88);
  --color-footer-bg:    #B7B4A2;
  --color-card-panel:   rgba(252, 250, 248, 0.74);
  --color-scrim:        rgba(235, 234, 226, 0.95);
  /* Accent — #ff854d, from the reference palette (pxr.law).
     Two tokens on purpose. The raw orange gives only 2.3:1 on the light
     ground: fine for a fill or a border, too low for anything read.
     --color-accent is for surfaces, --color-accent-ink for text.
     --color-accent-on is the ink that sits ON an accent fill; it is the same
     in both themes, because #ff854d is light enough (L=0.39) that dark ink is
     the only readable choice either way. */
  --color-accent:      #ff854d;
  --color-accent-ink:  #AD4C1F;                  /* hsl(19 70% 40%) — 4.5:1+ on every light surface */
  --color-accent-soft: rgba(255, 133, 77, 0.18);
  --color-accent-on:   #1D1D1D;                  /* 7.0:1 on #ff854d */

  /* Frost — the selection colour, and the ground for the two floating
     surfaces. Deliberately the same in both themes: they read as panes of
     frosted glass laid over the page, not as part of it. Kept translucent so
     the backdrop-filter behind them still shows. */
  --color-frost:       rgba(231, 237, 235, 0.90);
  --color-frost-solid: #E7EDEB;
  /* Pill controls — theme toggle + language switch */
  --switch-track:        #E7EDEB;
  --switch-track-border: rgba(0, 0, 0, 0.10);
  --switch-knob:         #1a1a1a;
  --switch-knob-ink:     #E7EDEB;
  --switch-ink-muted:    rgba(29, 29, 29, 0.50);
  --switch-ink-pending:  rgba(29, 29, 29, 0.28);
  --pill-hover-bg:       rgba(29, 29, 29, 0.05);
  --pill-hover-border:   rgba(29, 29, 29, 0.40);
  --switch-track-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.07), 0 1px 2px rgba(255, 255, 255, 0.8);
  --switch-knob-shadow:  0 1px 4px rgba(0, 0, 0, 0.25), 0 0 0 0.5px rgba(0, 0, 0, 0.08);

  /* Typography */
  --font-display: 'Absans', sans-serif;
  --font-body:    'Absans', sans-serif;

  /* Spacing */
  --space-sm:  1rem;
  --space-md:  2rem;
  --space-lg:  4rem;
  --space-xl:  7rem;
  --space-2xl: 12rem;

  /* Easing */
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out:   cubic-bezier(0.4, 0, 0.2, 1);
  /* Pure ease-out — for reactions to input (hover, press, toggles). Feedback
     starts at full speed; slow-start easing is reserved for entrances. */
  --ease-fast:     cubic-bezier(0, 0, 0.2, 1);

  /* ── Radius ────────────────────────────────────────────────────────────────
     Fire trin og en pille. Et nyt hjørne på sitet skal falde i et af dem —
     grammatikken var her allerede, den stod bare aldrig skrevet ned, og fem
     værdier var drevet fra den.

     To ting med vilje uden for skalaen, fordi de ikke ER grammatik:
       - 50% er cirkler, ikke radier.
       - .ni-shell morfer mellem 1.75rem og 1.25rem. De to tal er koreografi;
         de animerer mod hinanden og skal kunne justeres uafhængigt af alt
         andet på siden.
       - .header-nav .nav-cta bruger calc(--switch-h / 2) — bevidst mindre
         end en fuld pille, så den matcher kontakternes krumning. Se dens
         egen kommentar. */
  --radius-hairline: 2px;      /* fokusringe på inline-tekst og ikoner */
  --radius-chip:     0.5rem;   /* småt chrome: tooltip, skip-link */
  --radius-inner:    0.75rem;  /* flader nede i et kort: foto, listepunkt */
  --radius-card:     1.5rem;   /* selve kortet */
  --radius-pill:     999px;    /* alt der skal læse som en pille */

  /* ── Kantlys på profilkortet ──────────────────────────────────────────────
     Varigheden er eneste sandhedskilde: initCardTrace i main.js læser den
     herfra, så CSS-animationen og JS' oprydnings-timer ikke kan drifte fra
     hinanden. Ændr varigheden her, aldrig i JS.

     Farven skal sættes i alle fire tema-tilstande. I lyst tema er den blæk,
     ikke lys — hvidt lys er usynligt på et hvidt kort. */
  --trace-dur:   0.9s;
  --trace-color: rgba(255, 133, 77, 0.95);
  --trace-glow:  rgba(255, 133, 77, 0.28);

  /* Header dimensions — used by JS for logo landing position */
  --header-pad-x: 2.5rem;    /* 40px at 16px base */
  --header-pad-y: 0.875rem;  /* 14px — tightened for the stacked control cluster */

  /* Control cluster geometry.
     The theme toggle and the language switch share one footprint; the CV pill
     is exactly as tall as both of them plus the gap between them. */
  --switch-w:          3.25rem;   /* 52px */
  --switch-h:          1.625rem;  /* 26px */
  --switch-stack-gap:  0.375rem;  /* 6px */
}

/* Dark mode override — light is now the primary theme */
@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: dark;

    --color-bg:           #0a0a0a;
    --color-bg-section:   #0e0e0e;
    --color-bg-elevated:  #141414;
    --color-canvas-bg:    #0a0a0a;
    --color-text:         #E7EDEB;
    --color-text-muted:   rgba(231, 237, 235, 0.65);
    --color-text-faint:   rgba(231, 237, 235, 0.52);
    --color-border:       rgba(231, 237, 235, 0.08);
    --color-border-hover: rgba(231, 237, 235, 0.22);
    --color-header-bg:    rgba(10, 10, 10, 0.78);
    --color-footer-bg:    #080808;
    --color-card-panel:   rgba(10, 10, 10, 0.66);
    --color-scrim:        rgba(6, 6, 6, 0.94);
    /* Accent — see :root for why ink is a separate token */
    --color-accent:      #ff854d;
    --color-accent-ink:  #ff854d;                  /* 8.2:1 on #0a0a0a */
    --color-accent-soft: rgba(255, 133, 77, 0.14);
    --color-accent-on:   #1D1D1D;                  /* 7.0:1 on #ff854d */
    /* Pill controls — theme toggle + language switch */
    --switch-track:        #111111;
    --switch-track-border: rgba(231, 237, 235, 0.12);
    --switch-knob:         #E7EDEB;
    --switch-knob-ink:     #1a1a1a;
    --switch-ink-muted:    rgba(231, 237, 235, 0.55);
    /* DA is not published yet — it reads a step below the inactive label */
    --switch-ink-pending:  rgba(231, 237, 235, 0.30);
    --pill-hover-bg:       rgba(231, 237, 235, 0.07);
    --pill-hover-border:   rgba(231, 237, 235, 0.40);
    --switch-track-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6), 0 1px 0 rgba(255, 255, 255, 0.04);
    --switch-knob-shadow:  0 1px 4px rgba(0, 0, 0, 0.55), 0 0 0 0.5px rgba(0, 0, 0, 0.15);
  }
}

/* Manual theme overrides — set via data-theme attribute on <html> */
/* These beat the media query via higher specificity (attribute selector) */
html[data-theme="dark"] {
  color-scheme: dark;


  --color-bg:           #0a0a0a;
  --color-bg-section:   #0e0e0e;
  --color-bg-elevated:  #141414;
  --color-canvas-bg:    #0a0a0a;
  --color-text:         #E7EDEB;
  --color-text-muted:   rgba(231, 237, 235, 0.65);
  --color-text-faint:   rgba(231, 237, 235, 0.52);
  --color-border:       rgba(231, 237, 235, 0.08);
  --color-border-hover: rgba(231, 237, 235, 0.22);
  --color-header-bg:    rgba(10, 10, 10, 0.78);
  --color-footer-bg:    #080808;
  --color-card-panel:   rgba(10, 10, 10, 0.66);
  --color-scrim:        rgba(6, 6, 6, 0.94);

  /* Accent — see :root for why ink is a separate token */
  --color-accent:      #ff854d;
  --color-accent-ink:  #ff854d;                  /* 8.2:1 on #0a0a0a */
  --color-accent-soft: rgba(255, 133, 77, 0.14);
  --color-accent-on:   #1D1D1D;                  /* 7.0:1 on #ff854d */

  /* Pill controls — theme toggle + language switch */
  --switch-track:        #111111;
  --switch-track-border: rgba(231, 237, 235, 0.12);
  --switch-knob:         #E7EDEB;
  --switch-knob-ink:     #1a1a1a;
  --switch-ink-muted:    rgba(231, 237, 235, 0.55);
  /* DA is not published yet — it reads a step below the inactive label */
  --switch-ink-pending:  rgba(231, 237, 235, 0.30);
  --pill-hover-bg:       rgba(231, 237, 235, 0.07);
  --pill-hover-border:   rgba(231, 237, 235, 0.40);
  --switch-track-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6), 0 1px 0 rgba(255, 255, 255, 0.04);
  --switch-knob-shadow:  0 1px 4px rgba(0, 0, 0, 0.55), 0 0 0 0.5px rgba(0, 0, 0, 0.15);
}

html[data-theme="light"] {
  color-scheme: light dark;


  --color-bg:           #FCFAF8;
  --color-bg-section:   #EBEAE2;
  --color-bg-elevated:  #FFFFFF;
  --color-canvas-bg:    #FCFAF8;
  --color-text:         #1D1D1D;
  --color-text-muted:   rgba(29, 29, 29, 0.68);
  --color-text-faint:   rgba(29, 29, 29, 0.62);
  --color-border:       rgba(29, 29, 29, 0.10);
  --color-border-hover: rgba(29, 29, 29, 0.30);
  --color-header-bg:    rgba(252, 250, 248, 0.88);
  --color-footer-bg:    #B7B4A2;
  --color-card-panel:   rgba(252, 250, 248, 0.74);
  --color-scrim:        rgba(235, 234, 226, 0.95);

  /* Accent — ink is darkened to clear WCAG on the light ground */
  --color-accent:      #ff854d;
  --color-accent-ink:  #AD4C1F;                  /* hsl(19 70% 40%) — 4.5:1+ on every light surface */
  --color-accent-soft: rgba(255, 133, 77, 0.18);
  --color-accent-on:   #1D1D1D;                  /* 7.0:1 on #ff854d */

  /* Pill controls — theme toggle + language switch */
  --switch-track:        #E7EDEB;
  --switch-track-border: rgba(0, 0, 0, 0.10);
  --switch-knob:         #1a1a1a;
  --switch-knob-ink:     #E7EDEB;
  --switch-ink-muted:    rgba(29, 29, 29, 0.50);
  --switch-ink-pending:  rgba(29, 29, 29, 0.28);
  --pill-hover-bg:       rgba(29, 29, 29, 0.05);
  --pill-hover-border:   rgba(29, 29, 29, 0.40);
  --switch-track-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.07), 0 1px 2px rgba(255, 255, 255, 0.8);
  --switch-knob-shadow:  0 1px 4px rgba(0, 0, 0, 0.25), 0 0 0 0.5px rgba(0, 0, 0, 0.08);
}


/* ─── 2. Reset & base ──────────────────────────────────────────────────────── */

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html { scroll-behavior: smooth; }

body {
  background-color: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 1rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

::selection {
  background: var(--color-frost-solid);
  color: var(--color-accent-on);
}

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

ul { list-style: none; }
a  { color: inherit; text-decoration: none; }


/* ─── 3. Utilities ─────────────────────────────────────────────────────────── */

/* Visually hidden — for screen readers / SEO h1 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  z-index: 9999;
  padding: 0.5rem 1rem;
  background: var(--color-text);
  color: var(--color-bg);
  font-size: 0.85rem;
  font-family: var(--font-body);
  border-radius: 0 0 var(--radius-chip) var(--radius-chip);
  transition: top 0.2s var(--ease-fast);
}
.skip-link:focus { top: 0; outline: none; }


/* ─── 3b. Press feedback ────────────────────────────────────────────────────
   Apple's Designing Fluid Interfaces §1: feedback belongs on pointer-down,
   not on release, and :active (not :hover) is what fires without a mouse.
   One shared block for every interactive control on the site, so a tap
   reads as a press before the result arrives — not silence until it does.

   scale(0.97) is Apple's own value. Elements that already carry a transform
   on interaction (.gc-cover img's hover zoom, .ni-shell's width/height morph)
   are deliberately left out here and compose the press explicitly at their
   own rule instead, so this block never clobbers an existing transform.
────────────────────────────────────────────────────────────────────────── */

.nav-links a,
.nav-cta,
.nav-burger,
.sheet-cta,
.mobile-sheet ul a,
.gallery-card-btn,
.career-trigger,
.ni-pill,
.ni-item,
.ni-min,
.ni-close,
.gov-close,
.gov-item,
.gv-close,
.gv-nav,
.footer-pill,
.footer-logo,
.location-tag,
.profile-socials a,
.header-social a,
.mail-pill,
.profile-skill-btn,
.thinking-range li {
  touch-action: manipulation; /* drop the ~300ms tap delay where it still exists */
}

/* :active, never :hover — this has to work on touch, which has no hover */
.nav-links a:active,
.nav-cta:active,
.nav-burger:active,
.sheet-cta:active,
.mobile-sheet ul a:active,
.gallery-card-btn:active,
.career-trigger:active,
.ni-pill:active,
.ni-item:active,
.ni-min:active,
.ni-close:active,
.gov-close:active,
.gov-item:active,
.gv-close:active,
.gv-nav:active,
.footer-pill:active,
.footer-logo:active,
.location-tag:active,
.profile-socials a:active,
.header-social a:active,
.mail-pill:active,
.thinking-range li:active {
  transform: scale(0.97);
}

/* Every selector above needs `transform` in its own transition list so the
   press-down has something to animate along; where a rule already declares
   a transition without `transform`, that base rule adds it directly rather
   than being overridden here (specificity/ordering would make an addition
   here unreliable). See each component's own rule for `transform 100ms
   var(--ease-fast)` folded into its existing transition. */


/* ─── 3c. First-appearance reveal ───────────────────────────────────────────
   The hero has a full entrance — logo morph, photo morph, delayed tagline,
   kantlys on the card. Everything below it simply existed. Apple's §7
   (spatial consistency) is the argument: content should arrive from
   somewhere, or the page reads as two designs stitched together.

   Deliberately quiet. Opacity plus a 14px rise, nothing else. No scale, no
   overshoot — in this codebase bounce means "you flicked this", and it is
   reserved for the sheet and the gallery. An entrance nobody asked for gets
   critical damping.

   THE `.js` SCOPE IS WHAT MAKES THIS SAFE. Targets start invisible, so
   without JS the page below the hero would be blank. The class is set by the
   inline head script in index.html and 404.html; no script, no class, no
   hiding. Same bargain the nav island and the gallery already take.

   Reduced motion needs no block of its own. The global one (§13) rewrites
   transition-property to the opacity/colour whitelist with !important, so
   `transform` drops out and .is-revealed becomes a plain 200ms fade with the
   transform snapping straight to none. That is the correct degradation —
   which is why the revealed state must say `transform: none` explicitly
   rather than merely omitting a transform.
────────────────────────────────────────────────────────────────────────── */

.js [data-reveal] {
  opacity: 0;
  /* 24px, not 14. The element is travelling hundreds of pixels up the screen
     under the reader's own scroll while this plays; a 14px lift is simply
     swallowed by that motion and the reveal reads as nothing happening.
     Measured: at 14px the animation was invisible in every section. */
  transform: translateY(24px);
  /* --reveal-delay is written per element by initReveal, so the stagger does
     not have to be spelled out as nth-child rules the way .competency-pill's
     is. Adding a tenth card there means editing CSS; here it does not. */
  transition:
    opacity   0.7s var(--ease-out-expo) var(--reveal-delay, 0ms),
    transform 0.7s var(--ease-out-expo) var(--reveal-delay, 0ms);
}

.js [data-reveal].is-revealed {
  opacity: 1;
  transform: none;
}


/* ─── 4. Site logo — fixed, CSS-transition animated ───────────────────────── */
/*
  Always position:fixed anchored at left:0; top:0.
  JS measures both states once (after fonts load) and injects a <style> block
  with the computed translate/scale values. A CSS class toggle then triggers
  the transition — which runs entirely on the GPU compositor (no JS per frame).

  Font-size is set once as an inline style (never changes during animation).
  Only `transform` and `opacity` are transitioned — zero layout cost.
*/

.site-logo {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 200;
  font-family: var(--font-display);
  font-weight: 300;
  line-height: 1;
  white-space: nowrap;             /* keep jacob beenfeldt on one line */
  color: var(--color-text);
  text-decoration: none;
  user-select: none;
  pointer-events: none;
  transform-origin: 0 0;           /* scale from top-left corner */
  will-change: transform, opacity; /* promote to compositor layer */

  opacity: 0;
  /* Initial + scrolled transforms are injected by JS */
  transition:
    transform 0.82s cubic-bezier(0.16, 1, 0.3, 1),
    opacity   0.7s  ease 0.15s;
}

.logo-first,
.logo-last {
  display: inline; /* single line in hero; beenfeldt fades on scroll */
}

.logo-last {
  transition: opacity 0.55s ease;
}

.site-logo.logo-ready    { opacity: 1; }
.site-logo.logo-scrolled { pointer-events: auto; }

.site-logo.logo-scrolled .logo-last {
  opacity: 0;
}

.site-logo:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 4px;
}


/* ─── 4b. Header social icons — fixed, revealed on second scroll ──────────── */
/*
  Lives as a direct <body> child (like mobile-sheet) so backdrop-filter on
  .site-header doesn't trap it. JS positions left/top from measured logo data.
  Hidden until body.icons-visible; 0.6s delay lets the logo morph finish first.
*/

.header-social {
  position: fixed;
  z-index: 201;          /* above .site-logo (200) */
  display: flex;
  align-items: center;
  gap: 0.55rem;
  opacity: 0;
  pointer-events: none;
  transform: translateX(-5px);
  /* Quick fade-out when hiding (no delay) */
  transition:
    opacity   0.2s ease,
    transform 0.2s ease;
}

/* Show state — 0.6s delay so logo finishes its 0.82s morph before icons appear */
.header-social.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: translateX(0);
  transition:
    opacity   0.4s ease                          0.6s,
    transform 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.6s;
}

.header-social a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  color: var(--color-text-faint);
  transition: color 0.2s var(--ease-fast), transform 0.2s var(--ease-fast);
}
.header-social a:active { transition-duration: 100ms; }

/* Social icons take the raw --color-accent in both themes, not the ink —
   fully orange by choice. In light theme that is 2.3:1 against the ground,
   below the 3:1 for non-text graphics, but hover is a supplementary state and
   the resting colour carries the contrast. */
.header-social a:hover {
  color: var(--color-accent);
  transform: scale(1.2);
}

.header-social a:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
  border-radius: var(--radius-hairline);
}

.header-social a svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* Mail pill — overrides the icon link sizing */
.header-social .mail-pill {
  width: auto;
  height: auto;
  padding: 0.18rem 0.6rem;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: 0.7rem;
  letter-spacing: 0.07em;
  color: var(--color-text-muted);
  line-height: 1;
  transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
}
.header-social .mail-pill:hover {
  color: var(--color-accent-ink);
  transform: none;
  border-color: var(--color-accent);
}

/* Stagger: icons appear sequentially after LinkedIn */
.header-social.is-visible a:nth-child(1) {
  transition: color 0.2s var(--ease-fast), transform 0.2s var(--ease-fast), opacity 0.35s ease 0.6s;
}
.header-social.is-visible a:nth-child(2) {
  transition: color 0.2s var(--ease-fast), transform 0.2s var(--ease-fast), opacity 0.35s ease 0.68s;
}
.header-social.is-visible a:nth-child(3) {
  transition: color 0.2s var(--ease-fast), transform 0.2s var(--ease-fast), opacity 0.35s ease 0.76s;
}

/* Hide on mobile — icons live in the burger sheet instead */
@media (max-width: 768px) {
  .header-social { display: none !important; }
}

/* No component-local override needed — the global reduced-motion block
   (§13) already forces this element's transition-property to the
   opacity/colour whitelist, which drops the transform-based reveal and
   keeps the opacity crossfade. */


/* ─── 5. Site header — glass + nav only ───────────────────────────────────── */
/*
  No logo in the DOM — .site-logo floats over the left side.
  All state changes driven by body.site-scrolled class (toggled by JS).
*/

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: var(--header-pad-y) var(--header-pad-x);
  gap: 1.25rem; /* only matters on 404, where a logo shares the row */
  backdrop-filter: blur(18px) saturate(1.5);
  -webkit-backdrop-filter: blur(18px) saturate(1.5);
  background: var(--color-header-bg);

  opacity: 0;
  pointer-events: none;
  /* No delay when hiding (reverse scroll) */
  transition: opacity 0.4s var(--ease-out-expo);
}

/* Scroll edge — a soft fade where the floating glass meets content, instead
   of a hard 1px divider (Apple's "scroll edge effects, not hard dividers").
   Sits just below the header's own box; .site-header is already the
   containing block (position: fixed), so no extra positioning is needed. */
.site-header::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 1.25rem;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--color-border), transparent);
}

/* Appear after logo is halfway to the header */
body.site-scrolled .site-header {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.4s var(--ease-out-expo) 0.28s;
}

/*
  404 carries a plain text logo in the header — index.html instead floats the
  morphing #site-logo over the same spot. Sized to match where that one lands.
*/
.header-logo {
  margin-right: auto;
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 1.1rem;
  letter-spacing: -0.02em;
  white-space: nowrap;
  color: var(--color-text);
}

.header-nav {
  display: flex;
  align-items: center;
  gap: 2rem;
  position: relative; /* anchor for mobile dropdown */
  opacity: 0;
  transition: opacity 0.3s ease;
}

/* Nav fades in after the glass panel settles */
body.site-scrolled .header-nav {
  opacity: 1;
  transition: opacity 0.3s ease 0.55s;
}

.nav-links {
  display: flex;
  align-items: center;
  list-style: none;
  gap: 2rem;
}

.nav-links li {
  display: flex;
  align-items: center;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 1.25rem;
}

/*
  Vertical rule separating the nav links from the control cluster.
  Desktop only — .nav-links is hidden below 768px, so the rule goes with it.
*/
.nav-divider {
  width: 1px;
  height: 2.25rem;
  background: var(--color-border-hover);
  flex-shrink: 0;
}

/* Theme toggle stacked on top of the language switch */
.nav-switches {
  display: flex;
  flex-direction: column;
  gap: var(--switch-stack-gap);
}

/* Burger — hidden on desktop */
.nav-burger {
  display: none;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
  width: 3.25rem;
  height: 1.625rem;
  padding: 0;
  background: none;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  cursor: pointer;
  color: var(--color-text);
  transition: border-color 0.25s var(--ease-fast), transform 100ms var(--ease-fast);
  flex-shrink: 0;
}
.nav-burger:hover         { border-color: var(--color-text-muted); }
.nav-burger:focus-visible { outline: 2px solid var(--color-text); outline-offset: 3px; }

.burger-bar {
  display: block;
  width: 16px;
  height: 1px;
  background: currentColor;
  /* Ikke et radius-token: en linjekap på en 1px-streg, afledt af stregens
     egen højde. Samme for .ls-hint::before nedenfor. */
  border-radius: 1px;
  transition: transform 0.25s var(--ease-out-expo), opacity 0.2s var(--ease-fast);
  transform-origin: center;
}
.nav-burger[aria-expanded="true"] .burger-bar:nth-child(1) { transform: translateY(3px)  rotate(45deg);  }
.nav-burger[aria-expanded="true"] .burger-bar:nth-child(2) { transform: translateY(-3px) rotate(-45deg); }


/* ── Mobile nav — bottom sheet ── */

/*
  .nav-links lives inside .site-header which has backdrop-filter.
  backdrop-filter creates a new containing block, trapping position:fixed children.
  So on mobile we hide .nav-links and use .mobile-sheet (a direct <body> child) instead.
*/
.mobile-sheet {
  display: none; /* hidden on desktop */
}

@media (max-width: 768px) {
  /* Hide desktop inline links */
  .nav-links { display: none; }
  .nav-webdev { display: none; }

  /* Show burger */
  .nav-burger { display: flex; }


  /* Bottom sheet — direct <body> child, free from any stacking context */
  .mobile-sheet {
    display: flex;
    flex-direction: column;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 300;
    padding: 1rem var(--header-pad-x) calc(1.75rem + env(safe-area-inset-bottom, 0px));
    background: var(--color-frost);
    backdrop-filter: blur(28px) saturate(1.8);
    -webkit-backdrop-filter: blur(28px) saturate(1.8);
    border-top: 1px solid var(--color-border);
    /* hidden — below viewport */
    transform: translateY(100%);
    visibility: hidden;
    pointer-events: none;
    /* visibility delays until after the closing transform completes */
    transition: transform 0.4s var(--ease-out-expo), visibility 0s 0.4s;
  }

  .mobile-sheet.is-open {
    transform: translateY(0);
    visibility: visible;
    pointer-events: auto;
    /* visibility turns on immediately when opening */
    transition: transform 0.4s var(--ease-out-expo), visibility 0s 0s;
  }

  /* drag handle pip */
  .mobile-sheet::before {
    content: '';
    display: block;
    width: 2rem;
    height: 3px;
    border-radius: var(--radius-hairline);
    background: var(--color-border-hover);
    margin: 0 auto 1.25rem;
    flex-shrink: 0;
  }

  .mobile-sheet ul {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  .mobile-sheet li { display: block; }

  .mobile-sheet ul a {
    display: block;
    padding: 1rem 0;
    font-family: var(--font-display);
    font-weight: 300;
    font-size: 1.2rem;
    letter-spacing: -0.02em;
    border-bottom: 1px solid var(--color-border);
    color: var(--color-text-muted);
    transition: color 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
  }
  .mobile-sheet ul a:hover          { color: var(--color-text); }
  .mobile-sheet li:last-child a     { border-bottom: none; }

  /* Social icons row at the bottom of the sheet */
  .sheet-social {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    margin-top: 1.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--color-border);
  }

  .sheet-social a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    color: var(--color-text-faint);
    transition: color 0.2s var(--ease-fast), transform 0.2s var(--ease-fast);
  }
  .sheet-social a:hover {
    color: var(--color-accent);
    transform: scale(1.15);
  }
  .sheet-social a:focus-visible {
    outline: 2px solid var(--color-text);
    outline-offset: 3px;
    border-radius: var(--radius-hairline);
  }
  .sheet-social a svg {
    width: 100%;
    height: 100%;
    display: block;
  }

  /* CV — lives in the sheet on mobile, opens the PDF in the browser viewer */
  .sheet-cta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: 1.25rem;
    padding: 0.85rem 1.15rem;
    border: 1px solid var(--color-border-hover);
    border-radius: var(--radius-pill);
    font-family: var(--font-body);
    font-weight: 300;
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    color: var(--color-text);
    transition: background 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
  }
  .sheet-cta:hover {
    background: var(--color-accent-soft);
    border-color: var(--color-accent);
    color: var(--color-accent-ink);
  }
  .sheet-cta:focus-visible {
    outline: 2px solid var(--color-text);
    outline-offset: 3px;
  }
  .sheet-cta svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
    opacity: 0.7;
  }

  /* Mail pill in mobile sheet — pushed to the right */
  .sheet-social .mail-pill {
    margin-left: auto;
    width: auto;
    height: auto;
    padding: 0.25rem 0.75rem;
    border: 1px solid var(--color-border-hover);
    border-radius: var(--radius-pill);
    font-family: var(--font-body);
    font-size: 0.75rem;
    letter-spacing: 0.07em;
    color: var(--color-text-muted);
    line-height: 1;
    transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
  }
  .sheet-social .mail-pill:hover {
    color: var(--color-accent-ink);
    border-color: var(--color-accent);
    transform: none;
  }
}

.header-nav a {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  transition: color 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
}
.header-nav a:hover            { color: var(--color-text); }
.header-nav a[aria-current="page"] { color: var(--color-text); }

/*
  CV block — same footprint as the switch stack above it (two switches plus the
  gap between them), and the same corner radius as a single switch, so the three
  controls read as one family instead of one pill floating next to two.
  Selector carries .header-nav so it outranks `.header-nav a`, which would
  otherwise win on font-size.
*/
.header-nav .nav-cta {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  width: var(--switch-w);
  height: calc(2 * var(--switch-h) + var(--switch-stack-gap));
  padding: 0;
  border: 1px solid var(--color-border-hover);
  /* Half of --switch-h: matches the curvature of the pills, not a full stadium */
  border-radius: calc(var(--switch-h) / 2);
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--color-text) !important;
  flex-shrink: 0;
  transition: background 0.25s var(--ease-fast), border-color 0.25s var(--ease-fast), transform 100ms var(--ease-fast) !important;
}
/* The !important mirrors the base rule's — .nav-cta sets its colour that way,
   so the hover has to match to win. The download arrow is currentColor under
   an opacity shift, so it follows the ink without a rule of its own. */
.header-nav .nav-cta:hover {
  background: var(--color-accent);
  color: var(--color-accent-on) !important;
  border-color: var(--color-accent) !important;
}

/* Download arrow — the extra height is what makes room for it */
.nav-cta-arrow {
  width: 0.8125rem;
  height: 0.8125rem;
  opacity: 0.55;
  transition: transform 0.25s var(--ease-out-expo), opacity 0.25s var(--ease-fast);
}
.header-nav .nav-cta:hover .nav-cta-arrow {
  opacity: 0.9;
  transform: translateY(2px);
}

@media (prefers-reduced-motion: reduce) {
  /* The global block already kills the transition; this stops the actual
     hover VALUE (translateY) from applying at all under reduced motion. */
  .header-nav .nav-cta:hover .nav-cta-arrow { transform: none; }
}

/*
  Language switch — segmented pill sharing the theme toggle's footprint.
  The knob slides to whichever language is active; both labels stay readable.
  Selectors use two classes (0,2,0) so they outrank `.header-nav a` (0,1,1).
*/
.lang-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: var(--switch-w);
  height: var(--switch-h);
  padding: 0;
  border: 1px solid var(--switch-track-border);
  border-radius: var(--radius-pill);
  background: var(--switch-track);
  box-shadow: var(--switch-track-shadow);
  flex-shrink: 0;
}

/* Sliding knob — sits behind the labels */
.lang-switch .ls-thumb {
  position: absolute;
  left: 3px;
  width: calc(50% - 3px);
  height: 20px;
  border-radius: var(--radius-pill);
  background: var(--switch-knob);
  box-shadow: var(--switch-knob-shadow);
  transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s var(--ease-fast);
}
.lang-switch[data-lang="da"] .ls-thumb { left: 50%; }

.lang-switch .ls-opt {
  position: relative;
  z-index: 1;
  flex: 1;
  /* DA is a <button>; strip the UA chrome so it matches the EN <span> exactly */
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.5625rem;   /* 9px */
  letter-spacing: 0.06em;
  line-height: 1;
  color: var(--switch-ink-muted);
  transition: color 0.25s var(--ease-fast);
  user-select: none;
  -webkit-user-select: none;
}

/* Active language reads against the knob */
.lang-switch .ls-opt[aria-current="true"] { color: var(--switch-knob-ink); }

/* Inactive language, once it is a real link */
.lang-switch a.ls-opt:hover { color: var(--color-text); }
.lang-switch a.ls-opt:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
  border-radius: var(--radius-pill);
}

/*
  Not yet published. DA sits a step below the inactive-language colour, so the
  switch reads as "one of these is not ready" at a glance, before anyone clicks.
  Still an inert span — no pointer affordance, nothing to activate.
*/
.lang-switch .ls-opt.is-pending {
  cursor: default;
  color: var(--switch-ink-pending);
}

/*
  The reason, for whoever asks for it. Hover and keyboard focus are pure CSS, so
  the hint survives with JS off; initLangHint adds .is-visible for touch, where
  there is no hover to speak of.

  Anchored to the pill's right edge rather than centred on DA: the bubble is
  wider than the whole 52px pill, and the header sits flush right in both the
  desktop column and the mobile row. Growing leftwards is the only safe direction.
*/
.lang-switch .ls-hint {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 2;
  padding: 0.3125rem 0.5rem;
  border-radius: var(--radius-chip);
  background: var(--switch-knob);
  color: var(--switch-knob-ink);
  box-shadow: var(--switch-knob-shadow);
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.5625rem;   /* 9px — matches the labels it explains */
  letter-spacing: 0.04em;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-3px);
  pointer-events: none;   /* never eat a click meant for the pill */
  transition: opacity 0.2s var(--ease-fast), transform 0.2s var(--ease-out-expo), visibility 0.2s;
}

/* Arrow — a rotated corner of the bubble, sat under DA's half of the pill.
   13px from the right edge is the centre of the DA label in a 52px pill. */
.lang-switch .ls-hint::before {
  content: '';
  position: absolute;
  bottom: 100%;
  right: 10px;
  width: 6px;
  height: 6px;
  background: var(--switch-knob);
  transform: translateY(50%) rotate(45deg);
  border-radius: 1px;
}

/*
  .is-dismissed is how JS says "put away". Without it, tapping DA gives it focus,
  :focus-visible latches on, and the hover/focus rule below keeps the bubble up —
  so the second tap (and Escape) would appear to do nothing. It is cleared again
  as soon as the pointer or focus leaves, restoring plain hover behaviour.
*/
.lang-switch .ls-opt.is-pending:hover ~ .ls-hint:not(.is-dismissed),
.lang-switch .ls-opt.is-pending:focus-visible ~ .ls-hint:not(.is-dismissed),
.lang-switch .ls-hint.is-visible {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}

/* Focus ring on DA itself — it is tabbable so the hint is reachable by keyboard */
.lang-switch .ls-opt.is-pending:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
  border-radius: var(--radius-pill);
}

@media (prefers-reduced-motion: reduce) {
  /* The global block handles the transition timing; these keep the actual
     slide VALUE from applying so the hint doesn't move under reduced motion. */
  .lang-switch .ls-hint  { transform: none; }
  .lang-switch .ls-opt.is-pending:hover ~ .ls-hint:not(.is-dismissed),
  .lang-switch .ls-opt.is-pending:focus-visible ~ .ls-hint:not(.is-dismissed),
  .lang-switch .ls-hint.is-visible { transform: none; }
}


/* Theme toggle — pill switch */
.theme-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: var(--switch-w);
  height: var(--switch-h);
  border-radius: var(--radius-pill);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  background: var(--switch-track);
  border: 1px solid var(--switch-track-border);
  box-shadow: var(--switch-track-shadow);
  transition: background 0.3s var(--ease-fast), border-color 0.3s var(--ease-fast), box-shadow 0.3s var(--ease-fast);
}
/* Thumb — the track follows the theme, only the knob's side is stateful */
.tt-thumb {
  position: absolute;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  left: 3px;
  background: var(--switch-knob);
  box-shadow: var(--switch-knob-shadow);
  transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
              background 0.3s var(--ease-fast),
              box-shadow 0.2s var(--ease-fast);
}
.theme-toggle[aria-checked="false"] .tt-thumb,
.theme-toggle:not([aria-checked]) .tt-thumb { left: 3px; }
.theme-toggle[aria-checked="true"]  .tt-thumb { left: calc(100% - 23px); }
/* Icons */
.tt-icon {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 12px;
  height: 12px;
  pointer-events: none;
  transition: opacity 0.2s var(--ease-fast);
}
.tt-moon { right: 6px; }
.tt-sun  { left: 6px; }
.theme-toggle[aria-checked="false"] .tt-moon,
.theme-toggle:not([aria-checked]) .tt-moon { opacity: 0.65; color: #e7edeb; }
.theme-toggle[aria-checked="false"] .tt-sun,
.theme-toggle:not([aria-checked]) .tt-sun  { opacity: 0; }
.theme-toggle[aria-checked="true"]  .tt-moon { opacity: 0; }
.theme-toggle[aria-checked="true"]  .tt-sun  { opacity: 0.65; color: #1a1a1a; }
.tt-icon svg { width: 100%; height: 100%; display: block; }
/* States */
.theme-toggle:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}
/* No local override needed — the global reduced-motion block already
   drops the position/transform-driven parts of these transitions
   (.tt-thumb's left, .burger-bar's rotate, .mobile-sheet's slide) while
   keeping their colour/background crossfades. */


/* ── Control cluster on mobile ──
   Must come after the desktop rules above: same specificity, later wins. */
@media (max-width: 768px) {
  /* Keep the header at roughly its previous height */
  :root { --header-pad-y: 1.125rem; }

  /* The divider belongs to the desktop link row */
  .nav-divider { display: none; }

  /* Switches sit side by side so the header stays one row tall */
  .nav-switches {
    flex-direction: row;
    gap: 0.5rem;
  }

  /* CV moves into the burger sheet */
  .header-nav .nav-cta { display: none; }

  .header-logo { font-size: 0.95rem; }

  /* 404 has no burger sheet, so its CV pill stays — compact, one switch tall.
     No vertical room for the arrow at this size, so it drops out. */
  .header-nav .nav-cta--keep {
    display: inline-flex;
    width: var(--switch-w);
    height: var(--switch-h);
    font-size: 0.72rem;
  }
  .header-nav .nav-cta--keep .nav-cta-arrow { display: none; }
}


/* ─── 6. Hero section ──────────────────────────────────────────────────────── */

.hero {
  position: relative;
  width: 100%;
  height: calc(100vh + 180px);
  min-height: 780px;
  display: flex;
  align-items: flex-start;   /* content anchors to top; space below for card */
  justify-content: center;
  overflow: hidden;
  background: var(--color-bg);
}

.hero-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* Grain texture overlay.
   Bleeds 15% past the hero top and bottom so the parallax shift in
   initLogoMorph never drags a hard edge of the repeat into view. The
   pattern repeats, so the extra area costs nothing. */
.hero-grain {
  position: absolute;
  inset: -15% 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 180px 180px;
  opacity: 0.045;
  pointer-events: none;
  mix-blend-mode: overlay;
}

/* Fade into next section */
.hero::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 200px;
  background: linear-gradient(to bottom, transparent, var(--color-bg));
  pointer-events: none;
  z-index: 1;
}

.hero-content {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.75rem;
  margin-top: clamp(5rem, 18vh, 11rem); /* push below header, leave room for card */
}

/*
  Ghost placeholder — invisible text clone of .site-logo at full hero size.
  Occupies the same footprint as the real fixed logo so the tagline lines up
  naturally below it. Never shown visually, never read by screen readers.
*/
.hero-logo-ghost {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(2.5rem, 13vw, 8rem);
  letter-spacing: -0.04em;
  line-height: 1;
  white-space: nowrap;
  visibility: hidden;
  pointer-events: none;
  user-select: none;
}

.hero-logo-ghost span {
  display: inline;
}

.hero-tagline {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(0.95rem, 1.6vw, 1.1rem);
  line-height: 1.65;
  letter-spacing: -0.005em;
  /* Centred to match the wordmark above it; balance evens the three lines so
     the ragged edges read as deliberate rather than accidental. */
  text-align: center;
  text-wrap: balance;
  max-width: 34em;              /* ~62 chars — keeps the line length readable */
  margin-inline: auto;
  color: var(--color-text-muted);
  opacity: 0;
  transition: opacity 0.7s ease 0.35s; /* delayed appear — matches logo/photo entry */
}

/* The one accent word in the hero. The tagline is body-sized (max 1.1rem at
   weight 300), not display type, so this takes --color-accent-ink rather than
   the raw orange — on the light ground #ff854d would read at 2.3:1.
   Static by choice: the reference rotates the word through four phrases, and
   rotating adjectives in a headline are on the forbidden list in JACOB.md. */
.hero-accent {
  font-style: normal;
  color: var(--color-accent-ink);
}

/* Quiet line under the tagline — carries the label the old title used to */
.hero-subline {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(0.68rem, 1vw, 0.8rem);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  text-align: center;
  text-wrap: balance;           /* evens the two lines when it wraps on phones */
  margin-top: 0.35rem;
  margin-bottom: 0.9rem;
  opacity: 0;
  transition: opacity 0.7s ease 0.5s;
}

@media (max-width: 480px) {
  .hero-subline { letter-spacing: 0.12em; }
}

.hero-subline.tagline-ready {
  opacity: 1;
}

body.site-scrolled .hero-subline {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}

.hero-tagline.tagline-ready {
  opacity: 1;
}

body.site-scrolled .hero-tagline {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease; /* fast exit, no delay */
}

/*
  Hero photo ghost — invisible placeholder that holds the same footprint as
  #site-photo (128px circle) so hero-content spacing is preserved.
*/
.hero-photo-ghost {
  width: 128px;
  height: 128px;
  border-radius: 50%;
  visibility: hidden;
  pointer-events: none;
  user-select: none;
  flex-shrink: 0;
}

/*
  #site-photo — fixed element, same pattern as .site-logo.
  Always position:fixed anchored at left:0; top:0; transform-origin:0 0.
  JS measures the ghost (hero state) and card avatar (landed state),
  injects a <style> block with the computed translate/scale values.
  Only transform and opacity are transitioned — compositor-only, 60fps.
*/
.site-photo {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 150;
  width: 128px;
  height: 128px;
  border-radius: 50%;
  object-fit: cover;
  object-position: center top;
  border: 1px solid var(--color-border);
  transform-origin: 0 0;
  will-change: transform, opacity;
  pointer-events: none;
  user-select: none;
  opacity: 0;
  transition:
    transform 0.82s cubic-bezier(0.16, 1, 0.3, 1),
    opacity   0.7s  ease 0.15s;
}
.site-photo.photo-ready { opacity: 1; }
/* .photo-scrolled transform injected by JS */

@media (prefers-reduced-motion: reduce) {
  /* Safety net: guarantees the photo is visible even if .photo-ready's
     opacity crossfade (kept by the global block) never fires. */
  .site-photo.photo-ready { opacity: 1 !important; }
}

/* Scroll indicator */
.scroll-indicator {
  position: fixed;
  bottom: 2.5rem;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  opacity: 0;
  animation: fadeIn 0.6s ease 1s forwards;
  transition: opacity 0.4s ease;
}

body.site-scrolled .scroll-indicator {
  opacity: 0 !important;
  transition: opacity 0.4s ease;
}

.scroll-label {
  font-family: var(--font-body);
  font-size: 0.7rem;
  font-weight: 300;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-text);
  opacity: 0.7;
}

.scroll-indicator svg {
  color: var(--color-text);
  opacity: 0.7;
  animation: scrollBounce 1.8s ease-in-out infinite;
}


/* ─── 7. Container & section base ─────────────────────────────────────────── */

.container {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 2.5rem;
}

/* Ankermål — headeren er position: fixed og scroll-behavior er smooth, så
   uden dette lander browseren overskriften bag glasset. Højden er headerens
   egen: 2 x --header-pad-y plus kontrolklyngen (58px), plus luft.

   :where() holder specificiteten på 0, så ingen af sektionsreglerne nedenfor
   bliver overskygget. .career-item er med, fordi Experience-pillen på
   profilkortet scroller til en enkelt akkordeonrække, ikke til sektionen. */
:where(main > section, .career-item) { scroll-margin-top: 6.5rem; }

@media (max-width: 768px) {
  /* --header-pad-y er 1.125rem her, og CV-pillen er væk — headeren er lavere */
  :where(main > section, .career-item) { scroll-margin-top: 5rem; }
}

.section-label {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.78rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  margin-bottom: 1.5rem;
}

.section-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4.5vw, 3.5rem);
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: var(--color-text);
  margin-bottom: var(--space-lg);
}


/* ─── 8. Profile card overlay — fixed, direct <body> child ────────────────── */
/*
  Appears simultaneously with the logo morph on first scroll.
  Must be a direct <body> child — same reason as .mobile-sheet and .header-social
  (backdrop-filter on .site-header would trap position:fixed children).
*/

.profile-card-overlay {
  position: fixed;
  z-index: 90;  /* below header (100) so card scrolls behind it */
  /* left, top, and translateX(-50%) set dynamically by JS based on photo position */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.45s var(--ease-out-expo) 0.15s;
}

.profile-card-overlay.is-visible {
  opacity: 1;
  pointer-events: auto;
}

/* Card locked — settles into place, subtle shadow deepens */
.profile-card-overlay.is-locked .profile-card {
  box-shadow:
    0 8px 32px rgba(0, 0, 0, 0.28),
    0 2px 8px  rgba(0, 0, 0, 0.18);
  transition: box-shadow 0.5s var(--ease-out-expo);
}

.profile-card {
  position: relative;
  width: 340px;
  background: var(--color-bg-elevated);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  overflow: visible;
  cursor: default;
  transition:
    box-shadow 0.35s var(--ease-fast);
  will-change: transform;
}

/* ── Kantlys ────────────────────────────────────────────────────────────────
   To hårfine striber løber fra top-centrum hver sin vej langs kortets kant,
   mødes i bund-centrum og slukker. Fyres når det flyvende foto lander.

   Mekanik: hver halvdel er et SVG-stroke med pathLength="100", så dash-
   matematikken er uafhængig af kortets faktiske omkreds. `stroke-dasharray:
   15 100` giver én stribe på 15% af halvdelen; dashoffset animeres fra 15
   (stregen ligger før path-start, usynlig) til -100 (løbet af enden). Den
   slukker altså af sig selv i bunden — ingen særskilt exit-logik.

   Ingen `filter: drop-shadow` på den animerende streg: et CSS-filter
   re-renderes hver frame og er dyrt på telefon, og animationen fyrer præcis
   mens brugeren scroller. Gløden er i stedet en bredere path med lav opacitet.

   `d` sættes af initCardTrace i main.js — se markup-kommentaren i index.html. */

.card-trace {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;            /* over .profile-card-inner (1), på kortets kant */
  pointer-events: none;
  overflow: visible;     /* gløden må bløde en anelse uden for kanten */
}

.card-trace path {
  fill: none;
  stroke-linecap: round;
  stroke-dasharray: 15 100;
  stroke-dashoffset: 15; /* parkeret før start = usynlig i hvile */
  opacity: 0;
}

.card-trace-line { stroke: var(--trace-color); stroke-width: 1.5; }
.card-trace-glow { stroke: var(--trace-glow);  stroke-width: 5;   }

/* --ease-in-out, ikke --ease-out-expo: expo lægger to tredjedele af strækningen
   i de første par hundrede millisekunder og kravler resten. Det læses som et vip,
   ikke som lys der løber langs kanten. */
.profile-card-overlay.is-tracing .card-trace path {
  animation: cardTrace var(--trace-dur) var(--ease-in-out) forwards;
}

/* Gentagen landing — samme bevægelse, dæmpet. Fuld styrke er reserveret til
   første gang billedet lander i dette sidebesøg. */
.profile-card-overlay.is-tracing.trace-repeat .card-trace-line { stroke-opacity: 0.5;  }
.profile-card-overlay.is-tracing.trace-repeat .card-trace-glow { stroke-opacity: 0.35; }

@keyframes cardTrace {
  0%   { stroke-dashoffset:   15; opacity: 0; }
  12%  {                          opacity: 1; }
  78%  {                          opacity: 1; }
  100% { stroke-dashoffset: -100; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .card-trace { display: none; }
}

/* Radial glow on hover */
.profile-card::before {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 0%, rgba(231, 237, 235, 0.06) 0%, transparent 65%);
  opacity: 0;
  transition: opacity 0.4s var(--ease-fast);
  pointer-events: none;
  z-index: 0;
}
.profile-card:hover::before { opacity: 1; }

.profile-card-inner {
  position: relative;
  z-index: 1;
  padding: 2rem 1.75rem 1.75rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  text-align: center;
}

.profile-avatar {
  width: 112px;
  height: 112px;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid var(--color-border);
  flex-shrink: 0;
  transition: transform 0.4s var(--ease-out-expo);
}
.profile-card:hover .profile-avatar { transform: scale(1.04); }

/* Initially hidden — #site-photo covers the slot while morphing.
   JS sets opacity:1 after the flying photo lands (~860ms). */
.profile-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  display: block;
  opacity: 0;
  transition: opacity 0.25s ease;
}

.profile-name {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.5rem;
  letter-spacing: -0.03em;
  color: var(--color-text);
  line-height: 1.1;
  margin: 0;
}

.profile-badge {
  display: inline-block;
  padding: 0.22rem 0.75rem;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

.profile-location {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-family: var(--font-body);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: var(--color-text-faint);
}

.profile-skills {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.45rem;
}

.profile-skill {
  display: inline-block;
  padding: 0.2rem 0.65rem;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), background 0.2s var(--ease-fast),
              transform 0.25s var(--ease-out-expo);
}

.profile-skill-btn {
  background: none;
  cursor: pointer;
  font-family: inherit;
  touch-action: manipulation;
  transition: transform 100ms var(--ease-fast);
}

/* Both pills are buttons, and nothing said so. They fill solid on hover and
   lift 1px. The earlier note here said a fill "would shout on a quiet card" —
   that judgement is deliberately reversed: the card is where the accent should
   be loudest. The ::after chevron is currentColor, so it follows the ink. */
.profile-skill-btn:hover,
.profile-skill-btn:focus-visible {
  background: var(--color-accent);
  color: var(--color-accent-on);
  border-color: var(--color-accent);
  transform: translateY(-1px);
}

.profile-skill-btn:active {
  /* Composes with the hover lift instead of overriding it: pressed-while-hovered
     keeps 0 lift, pressed-without-hover (touch) still reads the press. */
  transform: translateY(0) scale(0.97);
}

.profile-skill-btn::after {
  content: '+';
  margin-left: 0.3em;
  font-size: 0.9em;
  opacity: 0.5;
  display: inline-block;
  transition: transform 0.25s var(--ease-out-expo), opacity 0.2s var(--ease-fast);
}

.profile-skill-btn:hover::after,
.profile-skill-btn:focus-visible::after {
  opacity: 0.9;
}

.profile-skill-btn[aria-expanded="true"]::after {
  transform: rotate(45deg);
  opacity: 0.8;
}

/* Experience scrolls to Career instead of expanding in place, so a '+' promises
   the wrong thing. The arrow nudges down towards where the click takes you. */
.profile-skill-jump::after {
  content: '\2193';
}

.profile-skill-jump:hover::after,
.profile-skill-jump:focus-visible::after {
  transform: translateY(2px);
}

.profile-skill-btn:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}

/* Touch has no hover — without this the lift sticks after a tap. */
@media (hover: none) {
  .profile-skill-btn:hover {
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The global block already gives color/border-color/opacity their 200ms
     crossfade; these value overrides stop the actual lift/rotate motion. */
  .profile-skill-btn:hover,
  .profile-skill-btn:focus-visible,
  .profile-skill-jump:hover::after,
  .profile-skill-jump:focus-visible::after {
    transform: none;
  }
  .profile-skill-btn[aria-expanded="true"]::after {
    transform: rotate(45deg);
  }
}

/* Competency pills — direct child of .profile-card */
/* Desktop: absolute to the left. Mobile: static flow inside card. */
.competency-pills {
  position: absolute;
  top: 1.5rem;
  right: calc(100% + 1rem);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
  width: max-content;
  pointer-events: none;
}

.competency-pill {
  padding: 0.22rem 0.8rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  white-space: nowrap;
  opacity: 0;
  transform: translateX(10px);
  transition:
    opacity      0.25s ease,
    transform    0.35s var(--ease-out-expo),
    color        0.2s var(--ease-fast),
    border-color 0.2s var(--ease-fast),
    background   0.2s var(--ease-fast);
}

/* Tags pick up the accent on hover. Scoped to .is-open because the pills only
   have pointer-events once the card has unfolded them. */
.competency-pills.is-open .competency-pill:hover {
  background: var(--color-accent);
  color: var(--color-accent-on);
  border-color: var(--color-accent);
}

.competency-pills.is-open {
  pointer-events: auto;
}

.competency-pills.is-open .competency-pill {
  opacity: 1;
  transform: translateX(0);
}

/* Desktop stagger — pills 2 & 3 are mobile-only (display:none), so skip them */
.competency-pills.is-open .competency-pill:nth-child(1) { transition-delay: 0.00s; }
.competency-pills.is-open .competency-pill:nth-child(2) { transition-delay: 0.04s; }
.competency-pills.is-open .competency-pill:nth-child(3) { transition-delay: 0.08s; }
.competency-pills.is-open .competency-pill:nth-child(4) { transition-delay: 0.12s; }
.competency-pills.is-open .competency-pill:nth-child(5) { transition-delay: 0.16s; }
.competency-pills.is-open .competency-pill:nth-child(6) { transition-delay: 0.20s; }
.competency-pills.is-open .competency-pill:nth-child(7) { transition-delay: 0.24s; }
.competency-pills.is-open .competency-pill:nth-child(8) { transition-delay: 0.28s; }
.competency-pills.is-open .competency-pill:nth-child(9) { transition-delay: 0.32s; }

@media (max-width: 768px) {
  .competency-pills {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: flex-start;
    align-content: flex-start;
    gap: 0.45rem;
    width: auto;
    /* Collapsed — no space taken */
    max-height: 0;
    overflow: hidden;
    padding: 0 1.75rem;
    pointer-events: none;
    transition: max-height 0.5s var(--ease-out-expo);
  }

  .competency-pills.is-open {
    /* ~4.5 pill rows visible: each row ≈ 29px, 4.5 × 29 = 130px + 1rem top padding */
    max-height: 146px;
    overflow-y: auto;
    overflow-x: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none; /* Firefox */
    padding-top: 1rem;
    padding-bottom: 1rem;
    pointer-events: auto;
  }

  .competency-pills.is-open::-webkit-scrollbar {
    display: none; /* Chrome/Safari */
  }


  /* Disable per-pill animation on mobile — container handles reveal */
  .competency-pill {
    white-space: normal;
    text-align: center;
    opacity: 1;
    transform: none;
    transition: none;
  }
}

.profile-socials {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  margin-top: 0.15rem;
  padding-top: 1rem;
  border-top: 1px solid var(--color-border);
  width: 100%;
}

.profile-socials a {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  color: var(--color-text-faint);
  transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), transform 0.2s var(--ease-fast);
  flex-shrink: 0;
}
.profile-socials a:active { transition-duration: 100ms; }
.profile-socials a:hover {
  color: var(--color-accent);
  border-color: var(--color-accent);
  transform: translateY(-2px);
}
.profile-socials a:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}
.profile-socials a svg {
  width: 13px;
  height: 13px;
  display: block;
}

@media (max-width: 480px) {
  .profile-card { width: calc(100vw - 2rem); max-width: 340px; }
}


/* ─── 9. Services section ──────────────────────────────────────────────────── */

.section-services {
  background: var(--color-bg-section);
  padding: var(--space-xl) 0;
}

.highlights-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1.25rem;
}

/* Explicit 2x2 placement keeps column 1 on NIMO and column 2 on ReGaia,
   while source order stays reading order. */
.highlight-card:nth-child(1) { grid-column: 1; grid-row: 1; }
.highlight-card:nth-child(2) { grid-column: 1; grid-row: 2; }
.highlight-card:nth-child(3) { grid-column: 2; grid-row: 1; }
.highlight-card:nth-child(4) { grid-column: 2; grid-row: 2; }

.highlight-card {
  position: relative;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-bg-elevated);
  overflow: hidden;               /* clips the shader canvas to the radius */
  transition: border-color 0.35s var(--ease-in-out);
}

.highlight-card:hover {
  border-color: var(--color-border-hover);
}

/*
  .has-shader is set by impact-shaders.js only once a WebGL2 context really
  exists. The shader canvas mounts at z-index:-1, so the card itself has to be
  transparent or it would paint over its own shader; the panel below supplies
  the surface instead. With no JS or no WebGL2 the class never lands and the
  card keeps its solid background.
*/
.highlight-card.has-shader {
  background: transparent;
}

.highlight-card-inner {
  position: relative;
  height: 100%;
  padding: var(--space-md);
}

.highlight-card.has-shader .highlight-card-inner {
  background: var(--color-card-panel);
}

.highlight-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  color: var(--color-text);
  margin-bottom: 1.25rem;
}

.highlight-bullets {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.highlight-bullets li {
  position: relative;
  padding-left: 1.1rem;
  font-size: 0.95rem;
  line-height: 1.7;
  color: var(--color-text-muted);
}

.highlight-bullets li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.68em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--color-text-faint);
}

@media (max-width: 640px) {
  .highlights-grid {
    grid-template-columns: 1fr;
  }
  .highlight-card,
  .highlight-card:nth-child(1),
  .highlight-card:nth-child(2),
  .highlight-card:nth-child(3),
  .highlight-card:nth-child(4) {
    grid-column: auto !important;
    grid-row: auto !important;
  }
  .highlight-card-inner {
    padding: 1.5rem;
  }
}


/* ─── 9b. Gallery section ──────────────────────────────────────────────────── */
/*
  An index of photo galleries. One row per gallery: a 4:5 cover with two sheets
  fanned behind it, and the gallery's details alongside. The stack is the whole
  affordance — it says there is a pile of photographs under the cover, which a
  single flat image does not.

  The row form is deliberate. With one gallery a lone portrait card would read
  as an accident in a 1200px column; as a row it reads as an index that grows
  by stacking, which is what happened when the second gallery landed.

  Below 720px the stack turns on its side and becomes a carousel — see the end
  of this section for why.
*/

.section-gallery {
  background: var(--color-bg);
  padding: var(--space-xl) 0;
}

.gallery-list {
  display: flex;
  flex-direction: column;
  /* Uden rammer er der intet, der adskiller to poster, andet end luften
     mellem dem. 1.25rem var kanten mellem to kasser; dette er afstanden
     mellem to objekter på samme flade. */
  gap: 4.5rem;
  /* Capped: a single row stretched across 1200px reads as a bar with a hole in
     it. At this measure the card is an object, and a second gallery stacks
     underneath at the same width. */
  max-width: 58rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/*
  No frame and no fill. The stack of photographs is the only object on the
  surface, and a bordered panel around it was a second object competing for
  the same attention — DESIGN.md's first rule is that the UI recedes so the
  photograph can speak. What used to be the card's edge is now just the page.
*/
.gallery-card {
  background: none;
}

.gallery-card-btn {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 3.5rem;
  width: 100%;
  /* The frame is gone, so the padding that held content off it goes too —
     the photograph now lines up with the section heading above it. */
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  color: var(--color-text);
  text-align: left;
  transition: transform 100ms var(--ease-fast);
}

/* ── The stack ── */

/* Whitespace is the photograph's pedestal, and so is size. At 15rem the
   stack was an illustration next to the text; at 19rem it is the subject
   and the text is the caption. */
.gc-stack {
  position: relative;
  width: 19rem;
  aspect-ratio: 4 / 5;
  flex-shrink: 0;
}

.gc-sheet,
.gc-cover {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-inner);
}

/* Sheets peek out to the right and below; the cover sits square on top.
   Same surface as the card with a lit edge — a stack of prints reads by its
   edges, and a darker fill would just look like a second shadow.

   Which is exactly what the shadow they used to carry was. The system allows
   one drop-shadow and it belongs to the photograph; the sheets keep their
   hairline edge, which is what the comment above always said should be doing
   the work. */
.gc-sheet {
  border: 1px solid var(--color-border-hover);
  background: var(--color-bg-elevated);
  transition: transform 0.45s var(--ease-out-expo);
}

.gc-sheet--2 { transform: translate(0.45rem, 0.35rem) rotate(1.5deg); }
.gc-sheet--3 { transform: translate(0.9rem, 0.7rem)  rotate(3deg); }

.gallery-card-btn:hover .gc-sheet--2,
.gallery-card-btn:focus-visible .gc-sheet--2 { transform: translate(0.8rem, 0.5rem) rotate(2.5deg); }
.gallery-card-btn:hover .gc-sheet--3,
.gallery-card-btn:focus-visible .gc-sheet--3 { transform: translate(1.6rem, 1rem) rotate(4.5deg); }

/* The one shadow in the system, on the one thing entitled to it: a
   photograph resting on a surface. Apple's own value, unmodified. */
.gc-cover {
  overflow: hidden;
  border: 1px solid var(--color-border-hover);
  box-shadow: rgba(0, 0, 0, 0.22) 3px 5px 30px;
}

.gc-cover img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s var(--ease-out-expo);
}

.gallery-card-btn:hover .gc-cover img { transform: scale(1.03); }

/* ── The details ── */

.gc-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.7rem;
  min-width: 0;
}

.gc-eyebrow {
  font-size: 0.72rem;
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-text-faint);
}

.gc-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.4rem, 2.6vw, 2rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--color-text);
}

.gc-sub {
  max-width: 46ch;
  font-size: 0.95rem;
  font-weight: 300;
  line-height: 1.65;
  color: var(--color-text-muted);
}

.gc-foot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.9rem;
  margin-top: 0.6rem;
  font-size: 0.8rem;
  color: var(--color-text-faint);
}

/* Hairline separators between the two facts, not bullet characters */
.gc-span {
  padding-left: 0.9rem;
  border-left: 1px solid var(--color-border-hover);
}

.gc-open {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--color-text);
}

.gc-open svg {
  width: 0.875rem;
  height: 0.875rem;
  transition: transform 0.35s var(--ease-out-expo);
}

.gallery-card-btn:hover .gc-open svg { transform: translateX(3px); }

/* offset is now positive: with no card frame the ring has nothing to sit
   inside, so it sits around the row instead of cutting into the photograph. */
.gallery-card-btn:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 8px;
  border-radius: var(--radius-card);
}

@media (max-width: 860px) {
  .gallery-card-btn {
    grid-template-columns: 1fr;
    gap: 1.75rem;
  }
  /* Follows the desktop stack up: the photograph stays the subject when the
     row folds into a column. */
  .gc-stack { width: min(100%, 20rem); }
  .gc-sub   { max-width: none; }
}

/* ── The index turns on its side ── */
/*
  Stacked, each card is most of a phone screen, so two of them make #gallery a
  wall to scroll past on the way to the rest of the page — and it grows by a
  screen with every gallery added. As a snap carousel the section stays one
  screen tall however many there are.

  Cards are 88% of the scroller's content box, so the next one's edge always
  shows: the peek is the affordance, which is why there are no dots and no
  script. Tab order is untouched — each card holds a button, and the browser
  scrolls a focused button into view by itself.
*/

@media (max-width: 720px) {
  .gallery-list {
    flex-direction: row;
    /* Wider than it was: with the frames gone, this gap is the only thing
       telling one gallery from the next. Still far below the 4.5rem the
       stacked layout uses, because the carousel's peek does most of the
       separating. */
    gap: 1.75rem;
    max-width: none;
    /* Out through .container's 2.5rem so a card can reach the screen edge,
       then the same padding back on the inside, so the first card still lines
       up with the heading above it */
    margin-inline: -2.5rem;
    padding-inline: 2.5rem;
    /* overflow-x makes overflow-y compute to auto, which would clip the
       fanned sheets' shadows and hang a second scrollbar off the list */
    padding-block: 0.5rem;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 2.5rem;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .gallery-list::-webkit-scrollbar { display: none; }

  .gallery-list > li {
    flex: 0 0 min(88%, 26rem);
    scroll-snap-align: start;
  }

  /* Equal height, so the carousel does not resize under the thumb */
  .gallery-card,
  .gallery-card-btn { height: 100%; }

  /* The card is now taller than its content; hold the content at the top
     instead of letting the grid centre it in the leftover space */
  .gallery-card-btn { align-content: start; }
}


/* ─── 9. Thinking section ──────────────────────────────────────────────────── */

.section-thinking {
  background: var(--color-bg);
  padding: var(--space-xl) 0;
}

.thinking-layout {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--space-xl);
  align-items: start;
}

.thinking-heading {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2rem, 4.5vw, 3.5rem);
  letter-spacing: -0.03em;
  line-height: 1.05;
  color: var(--color-text);
  margin-top: 0.35rem;
}

.thinking-thesis {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.2rem, 2.2vw, 1.6rem);
  letter-spacing: -0.025em;
  line-height: 1.25;
  color: var(--color-text);
  margin-bottom: var(--space-md);
}

.thinking-paras {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: var(--space-lg);
}

.thinking-paras p {
  font-size: 0.95rem;
  line-height: 1.85;
  color: var(--color-text-muted);
}

/* Range strip — breadth shown as domains, in the same visual language as
   .competency-pill so the two read as one system. */
.thinking-range {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-md);
}

.thinking-range li {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.8rem;
  letter-spacing: -0.005em;
  line-height: 1;
  color: var(--color-text-muted);
  padding: 0.5rem 0.85rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-pill);
  white-space: nowrap;
  transition:
    border-color 0.3s var(--ease-in-out),
    color        0.3s var(--ease-in-out),
    background   0.3s var(--ease-in-out),
    transform    100ms var(--ease-fast);
}

/* Solid fill. This reverses the text-and-border-only version shipped
   earlier the same day. */
.thinking-range li:hover {
  background: var(--color-accent);
  color: var(--color-accent-on);
  border-color: var(--color-accent);
}

.thinking-close {
  font-family: var(--font-body);
  font-size: 0.925rem;
  line-height: 1.8;
  color: var(--color-text-faint);
  font-style: italic;
}

@media (max-width: 900px) {
  .thinking-layout {
    grid-template-columns: 1fr;
    gap: var(--space-md);
  }
}

@media (max-width: 480px) {
  .thinking-range li {
    white-space: normal;
  }
}


/* ─── 10. Career section ──────────────────────────────────────────────────── */

.section-career {
  background: var(--color-bg-section);
  padding: var(--space-xl) 0;
}

/* The overlap line sits between the heading and the list, so the heading gives
   up its own bottom margin and the paragraph carries the gap to the list
   instead. Muted and small on purpose: it is a pointer at the dates below,
   not a claim of its own. --color-text-muted is defined in all three themes. */
.section-career .section-title {
  margin-bottom: 0.9rem;
}

.career-intro {
  max-width: 44ch;
  margin: 0 0 var(--space-lg);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.95rem;
  line-height: 1.65;
  color: var(--color-text-muted);
}

.career-list {
  border-top: 1px solid var(--color-border);
}

.career-item {
  border-bottom: 1px solid var(--color-border);
}

/* Accordion trigger — full-width button styled as the row */
.career-trigger {
  width: 100%;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) 0;
  background: var(--color-bg-section);
  -webkit-appearance: none;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  color-scheme: inherit;
  border: none;
  cursor: pointer;
  color: inherit;
  text-align: left;
  transition: transform 100ms var(--ease-fast);
}

.career-trigger:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 4px;
  border-radius: var(--radius-hairline);
}

.career-trigger-main {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: var(--space-md);
  align-items: start;
}

.career-meta {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding-top: 0.15rem;
}

.career-period {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.78rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-text-faint);
}

.career-company {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.925rem;
  color: var(--color-text-muted);
}

.career-role {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: 1.15rem;
  letter-spacing: -0.02em;
  color: var(--color-text);
  margin-bottom: 0.4rem;
  transition: color 0.2s var(--ease-fast);
}

.career-role-sub {
  font-style: normal;
  color: var(--color-text-muted);
  font-size: 0.95em;
}

/* Hover now carries the accent instead of a brightness step. One token covers
   both themes: --color-accent-ink is the raw orange on dark, the darkened one
   on light, so the per-theme overrides this rule used to need are gone. */
.career-trigger:hover .career-role { color: var(--color-accent-ink); }

.career-tagline {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--color-text-faint);
  font-style: italic;
}

/* Chevron */
.career-chevron {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  color: var(--color-text-faint);
  flex-shrink: 0;
  transition: border-color 0.25s var(--ease-fast), color 0.25s var(--ease-fast);
}

.career-chevron svg {
  width: 14px;
  height: 14px;
  display: block;
  transition: transform 0.35s var(--ease-out-expo);
}

.career-trigger:hover .career-chevron {
  border-color: var(--color-border-hover);
  color: var(--color-text-muted);
}

.career-trigger[aria-expanded="true"] .career-chevron svg {
  transform: rotate(180deg);
}

/* Accordion body — grid-template-rows for smooth height animation */
.career-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.45s var(--ease-out-expo);
}

.career-body.is-open {
  grid-template-rows: 1fr;
}

.career-body-inner {
  overflow: hidden;
  min-height: 0;
  padding-bottom: 0;
  transition: padding-bottom 0.45s var(--ease-out-expo);
}

.career-body.is-open .career-body-inner {
  padding-bottom: var(--space-md);
}

/* Bullet list */
.career-bullets {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.career-bullets li {
  position: relative;
  padding-left: 1.1rem;
  font-size: 0.95rem;
  line-height: 1.75;
  color: var(--color-text-muted);
}

.career-bullets li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.68em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--color-text-faint);
}

/* Impact label */
.career-impact-label {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.75rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  margin-top: 1.25rem;
  margin-bottom: 0.75rem;
}

@media (max-width: 640px) {
  .career-trigger-main {
    grid-template-columns: 1fr;
    gap: 0.75rem;
  }
}


/* ─── 11. CTA section ──────────────────────────────────────────────────────── */

.section-cta {
  background: var(--color-bg-section);
  padding: var(--space-xl) 0 var(--space-2xl);
  text-align: center;
}

.cta-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(2.5rem, 6vw, 5rem);
  letter-spacing: -0.04em;
  line-height: 1.05;
  color: var(--color-text);
  margin-bottom: var(--space-md);
}

/* Body copy below heading */
.cta-body {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  color: var(--color-text-muted);
  max-width: 36rem;
  margin: 0 auto var(--space-md);
  line-height: 1.75;
}

/* Action row */
.cta-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
  flex-wrap: wrap;
}

.cta-button {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 1.4rem;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  color: var(--color-text-muted);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.82rem;
  letter-spacing: 0.04em;
  transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), background 0.2s var(--ease-fast);
}
/* These were hardcoded rgba(231, 237, 235, …) — light-on-dark values that did
   almost nothing in light theme, where a near-white tint on a near-white
   ground is invisible. The accent tokens are theme-aware, so this fixes that
   as well as carrying the accent. */
.cta-button:hover {
  color: var(--color-accent-ink);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}
.cta-button:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}
.cta-button:active { background: var(--color-accent-soft); }

.cta-button--filled {
  background: rgba(231, 237, 235, 0.1);
  color: var(--color-text);
  border-color: rgba(231, 237, 235, 0.2);
}
/* The primary action is the one element that fills solid, so the page has a
   single loudest button. Same treatment as the footer pill. */
.cta-button--filled:hover {
  background: var(--color-accent);
  color: var(--color-accent-on);
  border-color: var(--color-accent);
}
.cta-button--filled:active { background: var(--color-accent); }


/* Both frosted surfaces are light grounds in *both* themes, so they carry the
   light ink set locally. Without this the dark theme would paint #E7EDEB text
   onto an #E7EDEB pane. Same reasoning as ::selection, which pairs the frost
   with dark ink for exactly that. */
/* The inks are tuned for the WORST composite, not the nominal frost colour.
   Being translucent, the pane picks up what is behind it: over the dark theme
   it lands near #D1D6D4 rather than #E7EDEB, and the site's normal light inks
   fall to 4.4:1 and 3.8:1 there. These values clear 4.5:1 against that darkest
   case, and simply have more contrast over a light page. */
.ni-shell,
.mobile-sheet {
  /* The element adopts the scoped ink itself, not just its descendants:
     without this the pane inherits the theme's colour, and any future child
     that does not set its own would be #E7EDEB text on an #E7EDEB pane. */
  color: var(--color-text);

  --color-text:         #1D1D1D;
  --color-text-muted:   rgba(29, 29, 29, 0.72);
  --color-text-faint:   rgba(29, 29, 29, 0.68);
  --color-border:       rgba(29, 29, 29, 0.10);
  --color-border-hover: rgba(29, 29, 29, 0.30);
  --color-accent-ink:   #98421B;   /* hsl(19 70% 35%) — the site ink is too
                                      light on the darkest composite */
  --pill-hover-bg:      rgba(29, 29, 29, 0.05);
}


/* ─── 11b. Nav island — fixed, direct <body> child ─────────────────────────── */
/*
  A floating pill showing the section you are currently in plus overall scroll
  progress; tapping it expands into the full section list.

  Must be a direct <body> child — same reason as .mobile-sheet and .header-social
  (backdrop-filter on .site-header would trap position:fixed children).

  z-index 250 sits above the header (100) and the profile card (150), but below
  .mobile-sheet (300) so the burger sheet covers it instead of fighting for space.

  Every colour here reuses an existing token, so all three themes work with no
  new custom properties.
*/

.nav-island {
  position: fixed;
  bottom: 1.75rem;
  left: 50%;
  z-index: 250;
  display: block;

  /* Distance from the viewport edge when minimised. Read by JS, which turns it
     into the exact pixel shift from centre (--ni-min-x). */
  --ni-edge: 24px;

  /* Reveal transition — the island is translated out of view until scrolled past hero */
  transform: translate(-50%, 150%);
  opacity: 0;
  visibility: hidden;
  transition:
    transform  0.55s var(--ease-out-expo),
    opacity    0.35s var(--ease-in-out),
    visibility 0s linear 0.55s;
}

.nav-island[hidden] { display: none; }

.nav-island.is-visible {
  transform: translate(-50%, 0);
  opacity: 1;
  visibility: visible;
  transition:
    transform  0.55s var(--ease-out-expo),
    opacity    0.35s var(--ease-in-out),
    visibility 0s linear 0s;
}

/*
  Minimised — the island parks against the left edge as a bare progress puck.
  --ni-min-x is written by initNavIsland: the exact px offset from the centre
  line to --ni-edge, so the puck lands on the gutter and not on 50vw, which
  counts the scrollbar and would sit a few px too far left.
  These rules follow .is-visible so they win the transform on equal specificity.
*/
.nav-island[data-state="min"] {
  transform: translate(var(--ni-min-x, -50%), 150%);
}
.nav-island[data-state="min"].is-visible {
  transform: translate(var(--ni-min-x, -50%), 0);
}

/* The morphing container — width/height animate between the two states */
.ni-shell {
  position: relative;
  overflow: hidden;
  width:  var(--ni-w, 15rem);
  height: 3.25rem;
  border-radius: 1.75rem;
  background: var(--color-frost);
  border: 1px solid var(--color-border);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  transition:
    width         0.5s var(--ease-out-expo),
    height        0.5s var(--ease-out-expo),
    border-radius 0.5s var(--ease-out-expo),
    border-color  0.3s var(--ease-in-out);
}

.nav-island[data-state="open"] .ni-shell {
  width:  var(--ni-w-open, 17.5rem);
  height: var(--ni-h-open, 15rem);
  border-radius: 1.25rem;
}

/* Collapsed all the way down to the ring — a circle the width of its own height */
.nav-island[data-state="min"] .ni-shell {
  width:  3.25rem;
  height: 3.25rem;
  border-radius: 50%;
}

/* ── Collapsed pill ── */

.ni-pill {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0 1.15rem;
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
  color: var(--color-text);
  text-align: left;
  transition:
    opacity    0.3s var(--ease-in-out),
    padding    0.5s var(--ease-out-expo),
    background 0.25s var(--ease-in-out),
    transform  100ms var(--ease-fast);
}

.ni-pill:hover .ni-label { color: var(--color-accent-ink); }

.nav-island[data-state="open"] .ni-pill {
  opacity: 0;
  pointer-events: none;
}

.ni-dot {
  flex-shrink: 0;
  width: 0.4rem;
  height: 0.4rem;
  border-radius: 50%;
  background: var(--color-text);
}

.ni-label {
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: 0.875rem;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--color-text);
  transition: opacity 0.25s var(--ease-in-out), flex-basis 0.5s var(--ease-out-expo);
}

/* Swapped out by JS when the active section changes */
.ni-label.is-swapping { opacity: 0; }

/*
  Minimise — parked over the dot, so revealing it costs no width. Fades in on
  hover; on touch, where there is no hover, it is quietly always there.
*/
.ni-min {
  position: absolute;
  left: 0.5rem;
  top: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  margin-top: -0.875rem;
  padding: 0;
  background: none;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  color: var(--color-text-muted);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s var(--ease-in-out), color 0.2s var(--ease-in-out), transform 100ms var(--ease-fast);
}

.ni-min svg { width: 0.875rem; height: 0.875rem; }

.ni-shell:hover .ni-min,
.ni-min:focus-visible {
  opacity: 1;
  pointer-events: auto;
}

.ni-min:hover { color: var(--color-text); }

/* The dot steps aside for it — same spot, one at a time */
.ni-shell:hover .ni-dot { opacity: 0; }

.ni-dot { transition: opacity 0.25s var(--ease-in-out); }

@media (hover: none) {
  .ni-min { opacity: 0.55; pointer-events: auto; }
  .ni-shell:hover .ni-dot { opacity: 1; }
  .nav-island[data-state="pill"] .ni-dot { opacity: 0; }
}

/* No minimising from the open panel or from the puck itself.
   visibility, not just opacity, so the button leaves the tab order too. */
.nav-island[data-state="open"] .ni-min,
.nav-island[data-state="min"] .ni-min {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* ── Minimised puck ── */

/* Label and dot collapse to nothing; the ring keeps its own size and centres */
.nav-island[data-state="min"] .ni-pill {
  gap: 0;
  padding: 0;
  justify-content: center;
}

.nav-island[data-state="min"] .ni-dot,
.nav-island[data-state="min"] .ni-label {
  flex: 0 0 0;
  width: 0;
  min-width: 0;
  opacity: 0;
}

.nav-island[data-state="min"] .ni-ring {
  width: 1.6rem;
  height: 1.6rem;
}

/* Hover on the puck reads as "this opens again" — clipped to the circle by the shell */
.nav-island[data-state="min"] .ni-pill:hover {
  background: var(--color-accent-soft);
}

/* ── Progress ring ── */

.ni-ring {
  flex-shrink: 0;
  width: 1.35rem;
  height: 1.35rem;
  transform: rotate(-90deg);
  transition: width 0.5s var(--ease-out-expo), height 0.5s var(--ease-out-expo);
}

.ni-ring-track {
  fill: none;
  stroke: var(--color-border-hover);
  stroke-width: 2.25;
}

.ni-ring-fill {
  fill: none;
  stroke: var(--color-text);
  stroke-width: 2.25;
  stroke-linecap: round;
  /* dasharray set by JS from the circle's real circumference */
  transition: stroke-dashoffset 0.15s linear;
}

/* ── Expanded panel ── */

.ni-panel {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s var(--ease-in-out);
}

.nav-island[data-state="open"] .ni-panel {
  opacity: 1;
  pointer-events: auto;
  transition-delay: 0.1s;
}

.ni-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  padding: 1rem 1rem 0.5rem 1.25rem;
}

.ni-panel-title {
  font-family: var(--font-body);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--color-text-faint);
}

.ni-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  background: none;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  color: var(--color-text-muted);
  transition: color 0.2s var(--ease-in-out), background 0.2s var(--ease-in-out), transform 100ms var(--ease-fast);
}

.ni-close svg { width: 0.875rem; height: 0.875rem; }

.ni-close:hover {
  color: var(--color-accent-ink);
  background: var(--color-accent-soft);
}

.ni-list {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 0;
  padding: 0 0.5rem 0.6rem;
  list-style: none;
}

.ni-list li { display: block; }

.ni-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.6rem 0.75rem;
  background: none;
  border: none;
  border-radius: var(--radius-inner);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.875rem;
  text-align: left;
  color: var(--color-text-faint);
  transition:
    background 0.25s var(--ease-in-out),
    color      0.25s var(--ease-in-out),
    transform  100ms var(--ease-fast);
}

/* Only :hover takes the accent. The [aria-current] rule below keeps
   --pill-hover-bg on purpose: it is a persistent "you are here" state, and an
   accent fill there would leave one item permanently orange. */
.ni-item:hover {
  background: var(--color-accent-soft);
  color: var(--color-accent-ink);
}

.ni-item[aria-current="true"] {
  background: var(--pill-hover-bg);
  color: var(--color-text);
}

.ni-item-text {
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* Marker dot — only on the active item */
.ni-item-dot {
  flex-shrink: 0;
  width: 0.35rem;
  height: 0.35rem;
  border-radius: 50%;
  background: var(--color-text);
  opacity: 0;
  transform: scale(0.4);
  transition: opacity 0.25s var(--ease-in-out), transform 0.25s var(--ease-out-expo);
}

.ni-item[aria-current="true"] .ni-item-dot {
  opacity: 1;
  transform: scale(1);
}

/* Keyboard focus — matches the rest of the site's focus treatment */
.ni-pill:focus-visible,
.ni-close:focus-visible,
.ni-min:focus-visible,
.ni-item:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: -2px;
}

/* ── Mobile ── */

@media (max-width: 640px) {
  .nav-island {
    bottom: 1.15rem;
    --ni-edge: 16px;
    /* Never wider than the viewport, minus a comfortable gutter */
    max-width: calc(100vw - 2rem);
  }

  .ni-shell { width: var(--ni-w-mobile, 13.5rem); }

  .nav-island[data-state="open"] .ni-shell {
    width: min(var(--ni-w-open, 17.5rem), calc(100vw - 2rem));
  }
}

/* The burger sheet owns the screen when open — get out of its way entirely.
   Sibling selector works because .nav-island follows #mobile-sheet in the DOM;
   initBurger toggles .is-open on the sheet itself, not on <body>. */
#mobile-sheet.is-open ~ .nav-island {
  opacity: 0;
  pointer-events: none;
  transform: translate(-50%, 150%);
}

/* No local override needed — the global reduced-motion block already
   drops the island's width/height/transform morph and the ring's rotate,
   while keeping .ni-item/.ni-pill's background/colour crossfades. */

/* ─── 11c. Gallery overlay + viewer — fixed, direct <body> children ────────── */
/*
  Two layers, both outside the header for the same backdrop-filter reason as
  .mobile-sheet:

    .gallery-overlay — the grid of every photograph in one gallery. Solid
      ground, because photographs need a neutral surface, not a tinted one.
    .gallery-viewer  — one photograph, uncropped, over a scrim.

  z-index sits above .mobile-sheet (300): opening a gallery is a full takeover,
  and nothing on the page should compete with it.
*/

.gallery-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  flex-direction: column;
  background: var(--color-bg);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity    0.35s var(--ease-in-out),
    visibility 0s linear 0.35s;
}

.gallery-overlay[hidden] { display: none; }

.gallery-overlay.is-open {
  opacity: 1;
  visibility: visible;
  transition:
    opacity    0.35s var(--ease-in-out),
    visibility 0s linear 0s;
}

/* Page behind must not scroll while either layer is up */
body.is-gallery-open { overflow: hidden; }

/* ── Overlay header ── */

/* Hovedet står stille, mens .gov-body scroller under det — altså floating
   chrome over indhold i bevægelse, præcis det tilfælde hvor Apples §12 siger
   scroll edge frem for hård divider. Samme behandling som .site-header::after
   allerede får; den hårde 1px-streg her var den sidste af slagsen.
   position: relative, så ::after har hovedet som containing block. */
.gov-head {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
  flex-shrink: 0;
  padding: 1.75rem 2.5rem 1.25rem;
}

.gov-head::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 1.25rem;
  pointer-events: none;
  background: linear-gradient(to bottom, var(--color-border), transparent);
}

.gov-eyebrow {
  font-size: 0.72rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--color-text-faint);
  margin-bottom: 0.4rem;
}

.gov-title {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(1.35rem, 3vw, 2rem);
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--color-text);
}

.gov-close,
.gv-close,
.gv-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 50%;
  cursor: pointer;
  color: var(--color-text-muted);
  transition:
    color        0.2s var(--ease-in-out),
    border-color 0.2s var(--ease-in-out),
    background   0.2s var(--ease-in-out),
    transform    100ms var(--ease-fast);
}

.gov-close svg,
.gv-close svg,
.gv-nav svg { width: 1rem; height: 1rem; }

.gov-close:hover,
.gv-close:hover,
.gv-nav:hover {
  color: var(--color-text);
  border-color: var(--color-border-hover);
  background: var(--pill-hover-bg);
}

/* ── The grid ── */

.gov-body {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 1.5rem 2.5rem 4rem;
}

.gov-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 0.75rem;
  max-width: 100rem;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.gov-item {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  padding: 0;
  overflow: hidden;
  background: var(--color-bg-section);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-inner);
  cursor: pointer;
  transition: border-color 0.25s var(--ease-in-out), transform 100ms var(--ease-fast);
}

.gov-item:hover { border-color: var(--color-border-hover); }

.gov-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s var(--ease-out-expo);
}

.gov-item:hover img { transform: scale(1.04); }

/*
  The set is a build sequence, so the date is information, not decoration.
  It stays out of the way until the tile is pointed at.
*/
.gov-date {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1.75rem 0.7rem 0.55rem;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: #E7EDEB;
  text-align: left;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0));
  opacity: 0;
  transition: opacity 0.25s var(--ease-in-out);
}

.gov-item:hover .gov-date,
.gov-item:focus-visible .gov-date { opacity: 1; }

/* ── Single photograph ── */

.gallery-viewer {
  position: fixed;
  inset: 0;
  z-index: 410;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4.5rem 5.5rem;
  background: var(--color-scrim);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity    0.3s var(--ease-in-out),
    visibility 0s linear 0.3s;
}

.gallery-viewer[hidden] { display: none; }

.gallery-viewer.is-open {
  opacity: 1;
  visibility: visible;
  transition:
    opacity    0.3s var(--ease-in-out),
    visibility 0s linear 0s;
}

.gv-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  max-width: 100%;
  max-height: 100%;
  margin: 0;
}

/* Contained, never cropped — the whole point of the full view */
.gv-figure img {
  max-width: 100%;
  max-height: calc(100vh - 11rem);
  object-fit: contain;
  border-radius: var(--radius-inner);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}

.gv-caption {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  font-size: 0.82rem;
  color: var(--color-text-muted);
  text-align: center;
}

.gv-date {
  color: var(--color-text-faint);
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.gv-counter {
  padding-left: 0.75rem;
  border-left: 1px solid var(--color-border-hover);
  color: var(--color-text-faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.gv-close {
  position: absolute;
  top: 1.5rem;
  right: 1.75rem;
}

.gv-nav {
  position: absolute;
  top: 50%;
  margin-top: -1.25rem;
}

.gv-prev { left: 1.25rem; }
.gv-next { right: 1.25rem; }

.gov-close:focus-visible,
.gov-item:focus-visible,
.gv-close:focus-visible,
.gv-nav:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 2px;
}

@media (max-width: 720px) {
  .gov-head { padding: 1.25rem 1.25rem 1rem; }
  .gov-body { padding: 1rem 1.25rem 3rem; }
  .gov-grid { grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); }

  .gallery-viewer { padding: 4.5rem 1rem 5.5rem; }
  .gv-figure img  { max-height: calc(100vh - 13rem); }

  /* Arrows drop to the bottom edge — nothing sits over the photograph */
  .gv-nav  { top: auto; bottom: 1.25rem; margin-top: 0; }
  .gv-prev { left: calc(50% - 3.25rem); }
  .gv-next { right: calc(50% - 3.25rem); }

  /* Dates are always on at touch sizes: there is no hover to reveal them */
  .gov-date { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  /* The global block already gives .gallery-overlay/.gallery-viewer/.gov-date
     their opacity crossfade instead of killing it outright; these value
     overrides stop the actual hover motion on the stacked sheets and covers. */
  .gallery-card-btn:hover .gc-sheet--2,
  .gallery-card-btn:hover .gc-sheet--3,
  .gallery-card-btn:hover .gc-cover img,
  .gallery-card-btn:hover .gc-open svg,
  .gov-item:hover img {
    transform: none;
  }
}


/* ─── 12. Site footer ──────────────────────────────────────────────────────── */

/* The beige footer (#B7B4A2) is a darker ground than the rest of the light
   theme, so the muted inks are re-tuned locally: at the global values they
   fall to 4.0:1 and 3.5:1 against it. Dark theme is unaffected — its footer
   is #080808 and the global values already clear. */
@media (prefers-color-scheme: light) {
  html:not([data-theme="dark"]) .site-footer {
    --color-text-muted: rgba(29, 29, 29, 0.86);   /* 6.1:1 on #B7B4A2 */
    --color-text-faint: rgba(29, 29, 29, 0.78);   /* 5.1:1 on #B7B4A2 */
  }
}
html[data-theme="light"] .site-footer {
  --color-text-muted: rgba(29, 29, 29, 0.86);
  --color-text-faint: rgba(29, 29, 29, 0.78);
}

.site-footer {
  background: var(--color-footer-bg);
  border-top: 1px solid var(--color-border);
  padding: var(--space-lg) 0;
}

.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
}

.footer-logo {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: 1rem;
  letter-spacing: -0.02em;
  color: var(--color-text);
  transition: opacity 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
}
.footer-logo:hover { opacity: 0.6; }

.footer-nav { display: flex; gap: 0.65rem; flex-wrap: wrap; }

.footer-pill {
  padding: 0.28rem 0.85rem;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  transition: color 0.2s var(--ease-fast), border-color 0.2s var(--ease-fast), background 0.2s var(--ease-fast), transform 100ms var(--ease-fast);
}
/* The one pill that fills rather than tints. --color-accent-ink reads only
   2.6:1 on the beige, so the accent cannot be the text here — but as a fill
   with dark ink on top it clears 7.0:1, and that is what the reference does
   for its buttons too. */
.footer-pill:hover {
  background: var(--color-accent);
  color: var(--color-accent-on);
  border-color: var(--color-accent);
}
.footer-pill:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}

.footer-copy {
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.75rem;
  color: var(--color-text-faint);
  letter-spacing: 0.03em;
}

@media (max-width: 640px) {
  .footer-inner { flex-direction: column; align-items: flex-start; gap: 1.25rem; }
}

/* Location tag */
.location-tag {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.28rem 0.85rem;
  border: 1px solid var(--color-border-hover);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--color-text-muted);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: 0.8rem;
  letter-spacing: 0.01em;
  cursor: default;
  transition: border-color 0.3s var(--ease-fast), color 0.3s var(--ease-fast), background 0.3s var(--ease-fast), transform 100ms var(--ease-fast);
}
/* The last hover that was still hardcoded rgba(231, 237, 235, …) — invisible
   in light theme, where a near-white tint sits on a near-white ground. */
.location-tag:hover {
  background: var(--color-accent);
  color: var(--color-accent-on);
  border-color: var(--color-accent);
}
.location-tag:focus-visible {
  outline: 2px solid var(--color-text);
  outline-offset: 3px;
}

/* Pulse dot */
.lt-pulse {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 8px;
  height: 8px;
  flex-shrink: 0;
}
.lt-pulse-dot {
  position: relative;
  width: 7px;
  height: 7px;
  background: #10b981;
  border-radius: 50%;
  z-index: 1;
}
.lt-pulse-ring {
  position: absolute;
  inset: 0;
  background: #10b981;
  border-radius: 50%;
  opacity: 0.7;
  animation: lt-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite;
}

/* Text slide */
.lt-text-wrap {
  position: relative;
  overflow: hidden;
  height: 1.2em;
  min-width: 11rem;
}
.lt-location,
.lt-time {
  display: block;
  white-space: nowrap;
  transition: transform 0.4s var(--ease-out-expo), opacity 0.35s ease;
}
.lt-location {
  transform: translateY(0);
  opacity: 1;
}
.lt-time {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(100%);
  opacity: 0;
}
.location-tag:hover .lt-location {
  transform: translateY(-100%);
  opacity: 0;
}
.location-tag:hover .lt-time {
  transform: translateY(0);
  opacity: 1;
}

/* Arrow */
.lt-arrow {
  flex-shrink: 0;
  opacity: 0.35;
  transition: transform 0.3s var(--ease-fast), opacity 0.3s var(--ease-fast);
}
.location-tag:hover .lt-arrow {
  transform: translateX(2px) rotate(-45deg);
  opacity: 0.8;
}

@keyframes lt-ping {
  0%   { transform: scale(1);   opacity: 0.7; }
  70%  { transform: scale(2.2); opacity: 0;   }
  100% { transform: scale(2.2); opacity: 0;   }
}

@media (prefers-reduced-motion: reduce) {
  /* Fully removes the looping ping (the global block's near-zero duration
     already makes it imperceptible, but this is the clean, explicit stop
     for a decorative infinite animation). Text-slide/arrow transitions need
     no local override — the global block already drops their translateY
     while keeping the opacity crossfade. */
  .lt-pulse-ring { animation: none; }
}


/* ─── 12. Keyframe animations ──────────────────────────────────────────────── */

@keyframes fadeIn {
  to { opacity: 1; }
}

@keyframes scrollBounce {
  0%, 100% { transform: translateY(0);    }
  50%       { transform: translateY(7px); }
}



/* ─── 12b. Reduced transparency & high contrast ────────────────────────────
   Four glass surfaces on the site lean on backdrop-filter: .site-header,
   .mobile-sheet, .ni-shell, .gallery-viewer. Neither media feature needs a
   new token — both reuse the existing solid/near-solid tokens that already
   resolve correctly in all three themes (:root, the dark media query, and
   both html[data-theme] overrides), so one block each covers every theme.
────────────────────────────────────────────────────────────────────────── */

@media (prefers-reduced-transparency: reduce) {
  .site-header {
    background: var(--color-bg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .mobile-sheet {
    background: var(--color-frost-solid);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .ni-shell {
    background: var(--color-frost-solid);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .gallery-viewer {
    /* --color-scrim is already ~95% opaque — dropping the blur is what
       makes it read as solid, no new colour needed. */
    background: var(--color-scrim);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

@media (prefers-contrast: more) {
  .site-header {
    background: var(--color-bg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-bottom: 1px solid var(--color-border-hover);
  }
  /* Under høj kontrast er en gradient ikke en kant. Begge scroll edges
     falder tilbage til den hårde streg, ligesom headeren ovenfor. */
  .site-header::after,
  .gov-head::after { display: none; }
  .gov-head { border-bottom: 1px solid var(--color-border-hover); }
  .mobile-sheet {
    background: var(--color-frost-solid);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-top: 1px solid var(--color-border-hover);
  }
  .ni-shell {
    background: var(--color-frost-solid);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-color: var(--color-border-hover);
  }
  .gallery-viewer {
    background: var(--color-scrim);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}


/* ─── 12c. Gesture hooks ────────────────────────────────────────────────────
   CSS-only groundwork for the gesture-wiring wave: while a sheet or the
   photo viewer is being dragged 1:1 under a pointer, the resting transition
   must get out of the way or it fights the live drag every frame. No JS
   here — .is-dragging is toggled by that later wave.
────────────────────────────────────────────────────────────────────────── */

.mobile-sheet.is-dragging,
.gallery-viewer.is-dragging {
  transition: none !important;
}


/* ─── 13. Reduced motion (renumbered from 13 — contact section removed) ────── */
/*
  Apple's Designing Fluid Interfaces §14: reduced motion should kill
  transform-driven movement and looping animation, but keep opacity/colour
  crossfades as the gentler substitute — not flatten every transition to
  0.01ms, which was the previous approach here and silently killed the
  cross-fades too. The ~10 component-local reduced-motion blocks elsewhere
  in this file were pruned of anything this block now covers; what remains
  there does something this block does not (hides an element outright, or
  turns off a specific keyframe animation by name).
*/

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
  html { scroll-behavior: auto; }

  /* Kill transform-based motion and looping/entrance animation; keep
     opacity/colour crossfades as the gentler substitute. */
  *, *::before, *::after {
    transition-property: opacity, color, background-color, border-color, box-shadow !important;
    transition-duration: 200ms !important;
  }

  /* Ensure elements that animate-in are still visible */
  .scroll-indicator { opacity: 0.4 !important; }
  .hero-tagline     { opacity: 1 !important; }
  .site-logo        { opacity: 1 !important; }
  .hero-subline     { opacity: 1 !important; }

  /* Direct manipulation (dragging a sheet or a photo under the finger) is
     not vestibular motion the same way an unprompted animation is — keep
     the live 1:1 follow, only strip the spring/settle afterwards. */
  .mobile-sheet.is-dragging,
  .gallery-viewer.is-dragging {
    transition: none !important;
  }
}


/* ─── 14. Print ────────────────────────────────────────────────────────────── */

@media print {
  body { background: #fff; color: #000; }

  .site-header,
  .scroll-indicator,
  .hero-grain,
  .hero-canvas,
  .site-logo { display: none; }

  .hero {
    height: auto;
    background: #fff;
    padding: 2rem 0;
  }

  .cta-button { border-color: #000; color: #000; }

  a[href]::after {
    content: " (" attr(href) ")";
    font-size: 0.75em;
    color: #666;
  }

  .section-services,
  .section-work,
  .section-cta { break-inside: avoid; }
}
