/* ============================================================
   pasin.dev — stylesheet
   Visual language ported from the AuditYote design system
   (Sovereign theme). Tokens are copied verbatim from
   design-system/tokens/*.css so the site and the app stay in
   sync; the type SCALE is the one deliberate divergence, see
   the typography block.
   ============================================================ */

/* ------------------------------------------------------------
   1. FONTS — self-hosted woff2, latin subset.
   No third-party requests, which is what lets the CSP stay
   self-only with no external font origins.
   ------------------------------------------------------------ */

@font-face {
  font-family: "Spectral";
  src: url("fonts/spectral-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Spectral";
  src: url("fonts/spectral-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Libre Franklin";
  src: url("fonts/libre-franklin-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Libre Franklin";
  src: url("fonts/libre-franklin-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Libre Franklin";
  src: url("fonts/libre-franklin-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/ibm-plex-mono-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/ibm-plex-mono-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("fonts/ibm-plex-mono-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}

/* ------------------------------------------------------------
   2. TOKENS
   ------------------------------------------------------------ */

:root {
  /* ---- Warm stone neutral ramp ---- */
  --stone-900: #1C211D;
  --stone-800: #2A302A;
  --stone-700: #3A413A;
  --stone-600: #555E54;
  --stone-500: #6B7268;
  --stone-400: #9AA095;
  --stone-300: #C9C6BA;
  --stone-250: #D9D4C8;
  --stone-200: #E7E3DA;
  --stone-150: #EDEAE2;
  --stone-100: #F3F1EC;
  --stone-75:  #F7F5F0;
  --stone-50:  #FBFAF7;
  --white:     #FFFFFF;

  /* ---- Forest green ramp (primary / brand) ---- */
  --forest-900: #102B23;
  --forest-800: #173A30;
  --forest-700: #1F4A3D;   /* primary */
  --forest-600: #2A5E4D;
  --forest-500: #2F6B57;
  --forest-300: #8FB3A4;
  --forest-200: #C7D8CE;
  --forest-100: #ECF1ED;
  --forest-ink: #18271F;

  /* ---- Semantic aliases ---- */
  --bg-app:         var(--stone-100);
  --bg-canvas:      var(--stone-50);
  --surface-card:   var(--white);
  --surface-sunken: var(--stone-100);
  --surface-inset:  var(--stone-75);
  --surface-header: var(--forest-ink);

  --text-strong:    var(--stone-900);
  --text-body:      var(--stone-700);
  --text-muted:     var(--stone-500);
  --text-faint:     var(--stone-400);
  --text-on-dark:   #EEF0EA;
  --text-on-primary:var(--white);
  --text-link:      var(--forest-700);

  --border-subtle:  var(--stone-200);
  --border-default: var(--stone-250);
  --border-strong:  var(--stone-300);
  --border-focus:   var(--forest-500);

  --primary:        var(--forest-700);
  --primary-hover:  var(--forest-800);
  --primary-press:  var(--forest-900);
  --primary-soft:   var(--forest-100);
  --primary-soft-border: var(--forest-200);
  --ring: color-mix(in srgb, var(--forest-500) 32%, transparent);

  /* ---- Families ---- */
  --font-sans:    "Libre Franklin", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
  --font-display: "Spectral", Georgia, "Times New Roman", serif;
  --font-body:    var(--font-sans);

  /* ---- Type scale ----
     The one deliberate divergence from the app. AuditYote's scale
     is tuned for a dense dashboard (14px body, 34px display); a
     page you read at arm's length needs more air. The ratios and
     the families are kept, the sizes step up, and everything
     fluid clamps between mobile and desktop. */
  --fs-hero:    clamp(2.25rem, 1.4rem + 4.2vw, 4rem);
  --fs-h2:      clamp(1.5rem, 1.15rem + 1.6vw, 2.125rem);
  --fs-h3:      clamp(1.125rem, 1rem + 0.6vw, 1.375rem);
  --fs-lead:    clamp(1.0625rem, 0.98rem + 0.5vw, 1.3125rem);
  --fs-body:    1rem;
  --fs-body-sm: 0.9375rem;
  --fs-caption: 0.875rem;    /* was 0.8125rem */
  --fs-micro:   0.8125rem;   /* was 0.75rem; nothing sits below 13px now */

  /* ---- Weights ---- */
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;

  /* ---- Line heights ---- */
  --lh-tight:  1.15;
  --lh-snug:   1.3;
  --lh-normal: 1.5;
  --lh-prose:  1.65;   /* long-form body, looser than the app */

  /* ---- Letter spacing ---- */
  --ls-tight:  -0.01em;
  --ls-normal: 0;
  --ls-wide:   0.02em;
  --ls-label:  0.06em;

  /* ---- Spacing, 4px base grid ---- */
  --space-1: 2px;   --space-2: 4px;   --space-3: 6px;
  --space-4: 8px;   --space-5: 12px;  --space-6: 16px;
  --space-7: 20px;  --space-8: 24px;  --space-9: 32px;
  --space-10: 40px; --space-11: 48px; --space-12: 64px;
  /* page-level rhythm, above the app's largest step */
  --space-section: clamp(4rem, 2.5rem + 6vw, 7.5rem);

  /* ---- Radii ---- */
  --radius-xs: 3px;
  --radius-sm: 5px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  /* ---- Borders ---- */
  --border-hair: 1px;
  --border-thick: 2px;
  --rail-w: 3px;     /* the app's card / table-left accent strip */

  /* ---- Shadows ---- */
  --shadow-xs: 0 1px 1px rgba(15, 23, 42, 0.04);
  --shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 1px rgba(15, 23, 42, 0.04);
  --shadow-md: 0 2px 4px rgba(15, 23, 42, 0.06), 0 4px 12px rgba(15, 23, 42, 0.08);
  --shadow-lg: 0 8px 24px rgba(15, 23, 42, 0.12), 0 2px 6px rgba(15, 23, 42, 0.06);
  --focus-ring: 0 0 0 3px var(--ring);

  /* ---- Layout ---- */
  --container-max: 1080px;   /* narrower than the app's 1440 chrome */
  --measure: 68ch;           /* comfortable reading width */

  /* ---- Motion, the design system's 90-140ms band ---- */
  --dur-fast: 90ms;
  --dur-mid:  120ms;
  --dur-slow: 140ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);
}

/* ------------------------------------------------------------
   3. RESET + BASE
   ------------------------------------------------------------ */

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

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

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--lh-prose);
  color: var(--text-body);
  background: var(--bg-canvas);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: var(--fw-semibold);
  color: var(--text-strong);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  margin: 0;
}

p { margin: 0; }

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

a {
  color: var(--text-link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color var(--dur-mid) var(--ease);
}
a:hover { color: var(--primary-hover); }

:focus-visible {
  outline: var(--border-thick) solid var(--border-focus);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* ------------------------------------------------------------
   4. PRIMITIVES carried over from the design system
   ------------------------------------------------------------ */

/* Small uppercase label. The app's .cm-eyebrow. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* All numbers, IDs, and refs are tabular mono. Non-negotiable
   design rule, same as the app. */
.mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

.container {
  width: 100%;
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 0.5rem + 3vw, 2.5rem);
}

.measure { max-width: var(--measure); }

/* Section banding. Three tones carry the page rhythm: the canvas,
   a sunken stone, and one deep forest band used once. */
.band          { padding-block: var(--space-section); }
.band--canvas  { background: var(--bg-canvas); }
.band--sunken  {
  background: var(--surface-sunken);
  border-block: var(--border-hair) solid var(--border-subtle);
  /* stone-500 on stone-100 measures 4.40:1, marginally under AA.
     The app can live with it at dashboard density; a page meant to
     be read should not. One step darker clears it at 5.63:1 and is
     still a design-system token, not an invented value. */
  --text-muted: var(--stone-600);
}
.band--forest  { background: var(--surface-header); color: var(--text-on-dark); }
.band--forest h2 { color: var(--text-on-dark); }
.band--forest .eyebrow { color: var(--forest-300); }

/* Hairline card with the tight low-opacity shadow the system
   prefers over soft consumer-app elevation. */
.card {
  background: var(--surface-card);
  border: var(--border-hair) solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-slow) var(--ease),
              box-shadow var(--dur-slow) var(--ease),
              border-color var(--dur-slow) var(--ease);
}
.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
  border-color: var(--primary-soft-border);
}

/* Skip link, visible only on keyboard focus. */
.skip-link {
  position: absolute;
  left: var(--space-6);
  top: calc(-1 * var(--space-12));
  z-index: 100;
  padding: var(--space-4) var(--space-6);
  background: var(--primary);
  color: var(--text-on-primary);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: top var(--dur-mid) var(--ease);
}
/* Land the skip link below the fixed navbar rather than on top of it. */
.skip-link:focus { top: calc(var(--nav-h) + var(--space-4)); color: var(--text-on-primary); }

/* ------------------------------------------------------------
   5. MOTION — every transition and transform is opt-out.
   ------------------------------------------------------------ */

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

/* ------------------------------------------------------------
   6. PAGE LAYOUT
   Everything below composes the primitives above. No new colors
   or spacing values are invented here; it is all tokens.
   ------------------------------------------------------------ */

/* ---- Section heads ---- */
.band h2 {
  font-size: var(--fs-h2);
  margin-top: var(--space-4);
  margin-bottom: var(--space-10);
  max-width: 22ch;
}
.measure p + p { margin-top: var(--space-6); }

/* ---- Hero ---- */
.hero {
  position: relative;
  overflow: hidden;
  padding-block: clamp(5rem, 3rem + 9vw, 9rem);
}
.hero > .container { position: relative; z-index: 1; }
/* Keep the copy clear of the densest part of the landform. */
@media (min-width: 940px) {
  .hero-copy { max-width: 34rem; }
}

.hero-name {
  font-size: var(--fs-hero);
  margin-top: var(--space-5);
}
/* The signature: a short forest rule under the name. Step 5
   animates it drawing outward; it is static and correct here. */
.hero-name::after {
  content: "";
  display: block;
  width: 3.5rem;
  height: 3px;
  margin-top: var(--space-7);
  border-radius: var(--radius-pill);
  background: var(--primary);
  transform-origin: left center;
}
.hero-aka {
  margin-top: var(--space-5);
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}
.hero-lead {
  margin-top: var(--space-7);
  font-size: var(--fs-lead);
  line-height: var(--lh-snug);
  color: var(--text-body);
}
.hero .framework-chips { margin-top: var(--space-10); }
.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  margin-top: var(--space-10);
}

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;                 /* comfortable touch target */
  padding-inline: var(--space-9);
  border: var(--border-hair) solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
  text-decoration: none;
  transition: background-color var(--dur-mid) var(--ease),
              border-color var(--dur-mid) var(--ease),
              color var(--dur-mid) var(--ease);
}
.btn--primary { background: var(--primary); color: var(--text-on-primary); }
.btn--primary:hover { background: var(--primary-hover); color: var(--text-on-primary); }
.btn--secondary {
  background: var(--surface-card);
  border-color: var(--border-default);
  color: var(--text-strong);
}
.btn--secondary:hover { border-color: var(--primary); color: var(--primary); }

/* ---- Chips ---- */
.chips {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.chips li {
  padding: var(--space-3) var(--space-6);
  border: var(--border-hair) solid var(--border-subtle);
  border-radius: var(--radius-pill);
  background: var(--surface-card);
  color: var(--text-body);
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-wide);
}
.framework-chips li {
  border-color: var(--primary-soft-border);
  background: var(--primary-soft);
  color: var(--forest-700);
  font-weight: var(--fw-medium);
}
/* Planned credentials read as provisional on sight: dashed,
   muted, no fill. Never let one pass for earned. */
.chip--planned {
  border-style: dashed !important;
  border-color: var(--border-strong) !important;
  background: transparent !important;
  color: var(--text-muted) !important;
}

/* ---- Card internals ---- */
.card-media {
  margin: 0;
  overflow: hidden;
  aspect-ratio: 16 / 10;
  background: var(--surface-inset);
  border-bottom: var(--border-hair) solid var(--border-subtle);
}
.card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  transition: transform var(--dur-slow) var(--ease);
}
.card:hover .card-media img { transform: scale(1.03); }

/* Certificates are documents, so they are contained and padded
   rather than cropped. Cropping a certificate looks like damage. */
.card-media--cert {
  display: grid;
  place-items: center;
  padding: var(--space-7);
  background: var(--stone-150);
}
.card-media--cert img { object-fit: contain; }
.card:hover .card-media--cert img { transform: none; }

.card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  flex: 1;
  padding: var(--space-8);
}
.card-body h3 { font-size: var(--fs-h3); }
.card-body > .mono {
  font-size: var(--fs-caption);
  color: var(--text-muted);
}
.card-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-7);
  margin-top: auto;
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
}
/* Sits under the links, after their margin-top:auto has pushed the pair to the
   bottom of the card. Tells visitors the live site will let them in before they
   click and hit a login wall. */
.card-note {
  margin-top: var(--space-4);
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

/* ---- Featured projects ---- */
.project-list,
.credentials {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-9);
}
.project-list > li,
.credentials > li { display: flex; }
.project-list .card,
.credentials .card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  width: 100%;
}
@media (min-width: 900px) {
  .project-list,
  .credentials { grid-template-columns: 1fr 1fr; }
}
.more-link {
  margin-top: var(--space-10);
  font-size: var(--fs-body-sm);
  font-weight: var(--fw-medium);
}
.more-link a {
  display: inline-block;
  padding-block: var(--space-2);
  min-height: 24px;
}

/* ---- Skills ---- */
.skill-groups { margin: 0; display: grid; gap: var(--space-8); }
.skill-groups dt {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.skill-groups dd { margin: 0; }
@media (min-width: 820px) {
  .skill-groups {
    grid-template-columns: 190px 1fr;
    align-items: start;
    column-gap: var(--space-10);
    row-gap: var(--space-9);
  }
  .skill-groups dt { padding-top: var(--space-3); }
}

/* ---- Experience ---- */
.experience {
  display: grid;
  gap: var(--space-8);
  align-items: start;
  padding: var(--space-9);
}
/* Not a link, so it must not behave like one on hover. */
.experience:hover {
  transform: none;
  box-shadow: var(--shadow-sm);
  border-color: var(--border-subtle);
}
.org-mark { width: 100%; max-width: 180px; }
@media (min-width: 760px) {
  .experience { grid-template-columns: 200px 1fr; column-gap: var(--space-10); }
}
.experience .card-body { padding: 0; }

/* ---- Training and certifications ---- */
.planned { margin-top: var(--space-10); }
.planned h3 {
  font-size: var(--fs-h3);
  margin-bottom: var(--space-6);
}

/* ---- Contact, the one forest band ---- */
.band--forest .measure { color: var(--text-on-dark); }
.contact-list {
  list-style: none;
  margin: var(--space-10) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
  justify-items: start;
}
.contact-list a {
  display: inline-flex;
  align-items: center;
  min-height: 26px;
  color: var(--forest-200);
  font-size: var(--fs-body-sm);
  text-decoration: none;
  border-bottom: var(--border-hair) solid transparent;
  padding-bottom: var(--space-2);
  transition: color var(--dur-mid) var(--ease),
              border-color var(--dur-mid) var(--ease);
}
.contact-list a:hover {
  color: var(--white);
  border-bottom-color: var(--forest-300);
}
.band--forest :focus-visible { outline-color: var(--forest-300); }

/* ---- Footer ---- */
footer.band {
  padding-block: var(--space-11);
  border-top: var(--border-hair) solid var(--border-subtle);
}
footer .mono {
  font-size: var(--fs-caption);
  color: var(--text-muted);
}

/* ------------------------------------------------------------
   7. DECORATION (step 4b)
   Density in the app's own language: section icons, a filmstrip
   of real views, and a contour motif behind the hero.
   ------------------------------------------------------------ */

/* ---- Section eyebrows carry a line icon ---- */
.eyebrow--icon {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}
.sec-icon {
  flex: none;
  color: var(--primary);
  opacity: 0.85;
}

/* ---- Hero becomes two columns, copy beside the motif ---- */
@media (min-width: 940px) {
  .hero-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 22.5rem;
    align-items: center;
    column-gap: var(--space-12);
  }
}
.hero-copy { min-width: 0; }

/* ---- Filmstrip ---- */
.filmstrip { margin: var(--space-10) 0 0; }
.filmstrip ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-7);
  grid-template-columns: 1fr;
}
@media (min-width: 760px) {
  .filmstrip ul { grid-template-columns: repeat(3, 1fr); }
}
.filmstrip li {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.filmstrip img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top center;
  border: var(--border-hair) solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-inset);
  box-shadow: var(--shadow-xs);
  transition: box-shadow var(--dur-slow) var(--ease),
              border-color var(--dur-slow) var(--ease);
}
.filmstrip li:hover img {
  box-shadow: var(--shadow-md);
  border-color: var(--primary-soft-border);
}
.filmstrip figcaption {
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-wide);
  color: var(--text-muted);
}

/* A quiet hairline contour on the sunken bands, so they read as
   surfaces rather than flat blocks. */
.band--sunken { position: relative; overflow: hidden; }
.band--sunken::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    115deg,
    transparent 0 38px,
    var(--stone-200) 38px 39px
  );
  opacity: 0.5;
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent 62%);
          mask-image: linear-gradient(to bottom, #000, transparent 62%);
  pointer-events: none;
}
.band--sunken > .container { position: relative; z-index: 1; }

/* ---- Cards gain the design system's accent rail on hover ---- */
.project-list .card,
.credentials .card { position: relative; }
.project-list .card::before,
.credentials .card::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--rail-w, 3px);
  background: var(--primary);
  opacity: 0;
  transition: opacity var(--dur-mid) var(--ease);
  z-index: 2;
}
.project-list .card:hover::before,
.credentials .card:hover::before { opacity: 1; }


/* ------------------------------------------------------------
   8. SECTION TITLES
   The label IS the heading now. Sentence headings are gone, so
   these carry the section on their own and are scaled up to do
   it. Kept in the mono eyebrow idiom rather than switched to
   Spectral: it reads as a label rather than a statement, which
   is the point of dropping the sentences.
   ------------------------------------------------------------ */

.section-title {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  margin-bottom: var(--space-10);
  font-family: var(--font-mono);
  font-size: clamp(1.5rem, 1.18rem + 1.35vw, 2.125rem);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-tight);
  text-transform: none;          /* uppercase stays reserved for small eyebrows */
  color: var(--text-strong);
  line-height: var(--lh-snug);
  max-width: none;
}
.section-title .sec-icon {
  width: 28px;
  height: 28px;
  opacity: 1;
}
.band--forest .section-title,
.band--ink .section-title { color: var(--text-on-dark); }
.band--forest .sec-icon,
.band--ink .sec-icon { color: var(--forest-300); }

/* Experience keeps "Teaching" beneath its label. Tighten the title's
   own gap when a subheading follows, rather than pulling the sub up
   with a negative margin that has to be recalculated by hand. */
.section-title:has(+ .section-sub) { margin-bottom: var(--space-6); }
.section-sub {
  margin-bottom: var(--space-9);
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: var(--fw-semibold);
  color: var(--text-strong);
  letter-spacing: var(--ls-tight);
}

/* ------------------------------------------------------------
   9. STEP 5 — reference alignment
   Structure borrowed from the splum.org study, rendered in the
   AuditYote language: a fixed navbar, a full-viewport dark hero,
   a rotating headline, and a back-to-top control.
   ------------------------------------------------------------ */

:root {
  /* Two variables on purpose.
     --nav-base is static and drives LAYOUT (the hero's top padding). It
     must not change after load or the hero shifts, which is a real CLS
     hit measured at 0.032.
     --nav-h is synced from the measured bar by main.js and is used only
     where it cannot cause reflow: scroll offsets and the absolutely
     positioned skip link. The bar wraps to two rows on narrow screens,
     so anchor offsets need the real number, not a guess. */
  --nav-base: 56px;
  --nav-h: 56px;
}
@media (max-width: 700px) { :root { --nav-base: 100px; } }

/* Anchor targets must clear the fixed navbar. */
html { scroll-padding-top: calc(var(--nav-h) + var(--space-6)); }

/* ---- Fixed navbar ----
   Permanently the forest-ink surface: it merges into the dark hero at
   the top of the page and stays legible once it floats over the light
   sections below. No scroll-dependent colour change, so no JS is
   needed to keep it readable. */
.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 50;
  height: 56px;
  background: color-mix(in srgb, var(--forest-ink) 88%, transparent);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  border-bottom: var(--border-hair) solid color-mix(in srgb, var(--forest-300) 22%, transparent);
}
.nav-inner {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  height: 100%;
}
.nav-brand {
  display: inline-flex;
  align-items: center;
  min-height: 26px;
  font-size: var(--fs-micro);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
  color: var(--text-on-dark);
  text-decoration: none;
  white-space: nowrap;
}
.nav-brand:hover { color: var(--white); }

.nav-links {
  display: flex;
  gap: var(--space-7);
  margin: 0 0 0 auto;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
  /* Inherit a colour that belongs on the dark bar. Without this the list
     items carry body stone-700 against forest ink, which only goes
     unnoticed because every item happens to contain a link. */
  color: var(--forest-200);
}
.nav-links::-webkit-scrollbar { display: none; }
.nav-links a {
  display: block;
  padding-block: var(--space-3);
  font-size: var(--fs-caption);
  letter-spacing: var(--ls-wide);
  color: var(--forest-200);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: var(--border-thick) solid transparent;
  transition: color var(--dur-mid) var(--ease),
              border-color var(--dur-mid) var(--ease);
}
.nav-links a:hover { color: var(--white); }
/* Applied by the scroll observer in step 6. */
.nav-links a.is-active {
  color: var(--white);
  border-bottom-color: var(--forest-300);
}

.nav-social {
  display: flex;
  gap: var(--space-6);
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav-social a {
  /* 18px glyphs in a 24px box: the icon stays small, the target meets
     the 24x24 minimum. */
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  color: var(--forest-200);
  transition: color var(--dur-mid) var(--ease);
}
.nav-social a:hover { color: var(--white); }

@media (max-width: 860px) {
  .nav-brand { display: none; }
  .nav-links { margin-left: 0; }
}

/* Below 700px the links wrap onto a second row rather than scrolling
   sideways. A horizontally scrolling strip technically kept every link
   reachable, but WCAG 1.4.10 is about not needing to scroll sideways to
   get at content, and at 320px two of the six sat off-screen. */
@media (max-width: 700px) {
  .nav { height: auto; min-height: 56px; }
  /* The social links live in the contact section too, so drop them here
     rather than spend a whole row of a fixed bar on duplicates. */
  .nav-social { display: none; }
  .nav-inner {
    height: auto;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-4) var(--space-6);
    padding-block: var(--space-5);
  }
  .nav-links {
    overflow: visible;
    flex-wrap: wrap;
    justify-content: center;
    margin: 0;
    gap: var(--space-2) var(--space-5);
  }
  /* Tighter here so six links settle into two rows. At the desktop size
     they took three, and the bar was eating an eighth of a phone screen. */
  .nav-links a { font-size: var(--fs-micro); letter-spacing: var(--ls-normal); }
}

.nav :focus-visible { outline-color: var(--forest-300); }

/* ---- Full-viewport dark hero ---- */
.band--ink {
  background: var(--surface-header);
  color: var(--text-on-dark);
}
.hero {
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-height: 100svh;
  padding-block: calc(var(--nav-base) + var(--space-11)) var(--space-12);
}
.hero > .container { width: 100%; }

/* Contour motif: a generated topographic landform, nodding to the design
   system's habitat theme without reaching for the coyote mark (which
   belongs in branding and empty states, not behind body copy).
   Self-hosted SVG, so the strict self-only CSP still holds. */
.hero::before {
  content: "";
  position: absolute;
  top: 50%;
  right: -12%;
  width: min(58rem, 92%);
  aspect-ratio: 1;
  transform: translateY(-50%);
  background: url("images/hero-topo.svg") no-repeat center / contain;
  opacity: 1;
  /* Fade held back to 48% so the strokes stay clear of the text column.
     At 38% the brightest stroke pixels drifted under the eyebrow and
     pulled its measured contrast to 4.84:1 — an AA pass, but with almost
     no margin. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 48%);
          mask-image: linear-gradient(to right, transparent 0%, #000 48%);
  pointer-events: none;
}

.hero .eyebrow {
  color: var(--forest-300);
  font-size: var(--fs-caption);
}
.hero-name { color: var(--white); }
.hero-name::after { background: var(--forest-300); }
.hero-aka { color: var(--forest-200); opacity: 0.75; }
.hero-lead { color: var(--text-on-dark); }

/* Chips read as outlines on the dark ground rather than pale fills,
   which would shout louder than the name. */
.hero .framework-chips li {
  font-size: var(--fs-caption);
  padding: var(--space-4) var(--space-7);
  background: transparent;
  border-color: color-mix(in srgb, var(--forest-300) 45%, transparent);
  color: var(--forest-200);
}

/* Buttons invert: forest-700 on forest-ink has nowhere near enough
   contrast, so the primary becomes light-on-dark. */
.hero .btn--primary {
  background: var(--stone-50);
  color: var(--forest-900);
  border-color: var(--stone-50);
}
.hero .btn--primary:hover {
  background: var(--white);
  color: var(--forest-900);
}
.hero .btn--secondary {
  background: transparent;
  border-color: color-mix(in srgb, var(--forest-300) 55%, transparent);
  color: var(--text-on-dark);
}
.hero .btn--secondary:hover {
  border-color: var(--forest-300);
  color: var(--white);
}
.hero :focus-visible { outline-color: var(--forest-300); }

/* ---- Rotating headline ----
   Static first word until step 6 wires the rotation, which is also
   exactly the reduced-motion state. */
.hero-rotator {
  margin-top: var(--space-8);
  font-size: 1.0625rem;
  color: var(--forest-200);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-4);
}
.hero-rotator-lead { opacity: 0.8; }
.rotator {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  color: var(--white);
}
.rotator-word { transition: opacity var(--dur-slow) var(--ease); }
.rotator-word.is-swapping { opacity: 0; }
.rotator-caret {
  display: inline-block;
  width: 0.55em;
  height: 1.05em;
  translate: 0 0.15em;
  background: var(--forest-300);
}

/* ---- Back to top ----
   Rendered from the start; step 6 gates it on scroll position. */
.to-top {
  position: fixed;
  right: clamp(1rem, 0.4rem + 2vw, 2rem);
  bottom: clamp(1rem, 0.4rem + 2vw, 2rem);
  z-index: 40;
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  border: var(--border-hair) solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--text-body);
  box-shadow: var(--shadow-sm);
  transition: color var(--dur-mid) var(--ease),
              border-color var(--dur-mid) var(--ease),
              box-shadow var(--dur-mid) var(--ease);
}
.to-top:hover {
  color: var(--primary);
  border-color: var(--primary);
  box-shadow: var(--shadow-md);
}

/* ---- Certificate verification line ---- */
.cert-verify {
  font-size: var(--fs-micro);
  letter-spacing: var(--ls-wide);
  color: var(--text-muted);
}

@media (prefers-reduced-motion: reduce) {
  .rotator-word { transition: none; }
}

/* ------------------------------------------------------------
   10. STEP 6 — behaviour layer
   Animation states, the zoom affordance, and the lightbox.
   Every animated element is VISIBLE by default; the hidden start
   state only applies once JS adds .js-anim to <html>. If the
   script fails or is blocked, the page renders fully composed.
   ------------------------------------------------------------ */

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- Scroll reveal ---- */
.js-anim .reveal {
  opacity: 0;
  transform: translateY(10px);
}
.js-anim .reveal.in-view {
  opacity: 1;
  transform: none;
  transition: opacity 320ms var(--ease), transform 320ms var(--ease);
  transition-delay: var(--reveal-delay, 0ms);
}

/* ---- Hero entrance ---- */
.js-anim .hero-step {
  opacity: 0;
  transform: translateY(10px);
}
.js-anim .hero-step.is-in {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease), transform var(--dur-slow) var(--ease);
  transition-delay: var(--hero-delay, 0ms);
}
/* The signature rule draws outward instead of fading. */
.js-anim .hero-name::after { transform: scaleX(0); }
.js-anim .hero-name.is-in::after {
  transform: scaleX(1);
  transition: transform 420ms var(--ease) 380ms;
}

/* ---- Back to top, gated on scroll ---- */
.js-anim .to-top {
  opacity: 0;
  visibility: hidden;
  transform: translateY(6px);
  transition: opacity var(--dur-slow) var(--ease),
              transform var(--dur-slow) var(--ease),
              visibility 0s linear var(--dur-slow);
}
.js-anim .to-top.is-visible {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

/* ---- Rotating caret ---- */
@keyframes caret-blink { 0%, 45% { opacity: 1; } 55%, 100% { opacity: 0.15; } }
.js-anim .rotator-caret { animation: caret-blink 1.1s steps(1, end) infinite; }

/* ---- Zoom affordance ----
   The whole media frame is the click target. A small glyph appears on
   hover and, importantly, whenever the link has keyboard focus. */
.zoom {
  display: block;
  position: relative;
  cursor: zoom-in;
  color: inherit;
  text-decoration: none;
  height: 100%;
}
.card-media .zoom { height: 100%; }
.card-media--cert .zoom { display: grid; place-items: center; width: 100%; }
.card-media--cert .zoom img { max-height: 100%; object-fit: contain; }

.zoom::after {
  content: "";
  position: absolute;
  right: var(--space-5);
  bottom: var(--space-5);
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--stone-900) 62%, transparent) center / 16px 16px no-repeat;
  /* magnifier glyph, inlined so the CSP needs no extra image origin */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23fff' stroke-width='1.7' stroke-linecap='round'%3E%3Ccircle cx='9' cy='9' r='5.2'/%3E%3Cpath d='M12.8 12.8 17 17M9 6.9v4.2M6.9 9h4.2'/%3E%3C/svg%3E");
  opacity: 0;
  transition: opacity var(--dur-mid) var(--ease);
  pointer-events: none;
}
.zoom:hover::after,
.zoom:focus-visible::after { opacity: 1; }
.filmstrip .zoom::after { right: var(--space-4); bottom: var(--space-4); }

/* ---- Lightbox ----
   Native <dialog>, so focus trapping, Escape, focus restoration and
   top-layer stacking come from the platform rather than from code. */
.lightbox {
  /* fit-content, not auto. The UA stylesheet gives dialog left:0 and
     right:0, so width:auto stretches the box to fill instead of hugging
     its contents. */
  --pad: var(--space-6);
  --btn: 38px;
  --dlg-max: min(96vw, 1500px);
  width: fit-content;
  max-width: var(--dlg-max);
  max-height: 94vh;
  /* Top strip sized from the button itself, so the control sits above
     the image instead of over its corner. Guessing a value here left the
     button overlapping the artwork by a few pixels. */
  padding: calc(var(--pad) + var(--btn) + var(--space-4)) var(--pad) var(--pad);
  border: none;
  border-radius: var(--radius-lg);
  background: var(--surface-header);
  color: var(--text-on-dark);
  overflow: visible;
}
/* Opening animation.
   A <dialog> toggles `display`, so a plain transition cannot run on it.
   `allow-discrete` lets display and overlay participate, and
   @starting-style supplies the state to animate FROM on open. Without
   both, the dialog simply appears.

   180ms sits just above the design system's 90-140ms band. That band is
   tuned for small controls; a full-screen overlay moving that fast
   reads as a flash rather than a transition. */
.lightbox {
  opacity: 0;
  transform: scale(0.97) translateY(8px);
  transition: opacity 180ms var(--ease),
              transform 180ms var(--ease),
              overlay 180ms allow-discrete,
              display 180ms allow-discrete;
}
.lightbox[open] {
  opacity: 1;
  transform: none;
}
@starting-style {
  .lightbox[open] {
    opacity: 0;
    transform: scale(0.97) translateY(8px);
  }
}

.lightbox::backdrop {
  background: color-mix(in srgb, #0B0F0C 82%, transparent);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity 180ms var(--ease),
              overlay 180ms allow-discrete,
              display 180ms allow-discrete;
}
.lightbox[open]::backdrop { opacity: 1; }
@starting-style {
  .lightbox[open]::backdrop { opacity: 0; }
}


.lightbox-figure {
  --ar: 1.6;
  --frame-h: calc(94vh - 10rem);
  /* Derived from the dialog's own cap minus its padding, so the figure
     can never be wider than the box that holds it. Hardcoding the same
     1500px the dialog uses left no room for padding, and every image
     wide enough to hit that cap overflowed by 16px a side.
     No percentages: one resolves against a grid column that this element
     itself sizes, which either stops the dialog hugging or grows the
     column past it. */
  --box-w: min(
      calc(var(--dlg-max) - 2 * var(--pad)),
      calc(var(--frame-h) * var(--ar))
  );
  /* The figure carries the definite width. Left to size from its
     children, the dialog fell back to max-width, because the caption's
     unwrapped max-content is far wider than the image it describes. */
  width: var(--box-w);
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-5);
  justify-items: center;
}
/* The frame is what reserves space.
   An <img> with width:auto has NO intrinsic size until the file
   arrives, so aspect-ratio alone cannot hold the box open and the
   caption gets shoved down on load. Giving the frame a definite width,
   derived from the aspect ratio and the height available, means the
   final box exists from the first frame. */
.lightbox-frame {
  position: relative;
  overflow: hidden;
  width: 100%;
  aspect-ratio: var(--ar);
  border-radius: var(--radius-sm);
  background: var(--forest-900);
}

/* Blurred preview, painted from the thumbnail the page already has in
   cache, so the wait shows the actual image out of focus rather than an
   empty rectangle. Inset past the edges so the blur has no soft border. */
.lightbox-frame::before {
  content: "";
  position: absolute;
  inset: -8%;
  background-image: var(--preview, none);
  background-size: cover;
  background-position: center;
  filter: blur(14px) saturate(1.04);
  transform: scale(1.02);
  opacity: 0.9;
  transition: opacity 240ms var(--ease);
}

/* Sheen over the blur, the design system's skeleton treatment, so the
   frame reads as loading rather than as a deliberately blurry image. */
@keyframes lightbox-sheen {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.lightbox-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(90deg,
    transparent 0%, rgba(255, 255, 255, 0.10) 50%, transparent 100%);
  background-size: 200% 100%;
  animation: lightbox-sheen 1.3s ease-in-out infinite;
  opacity: 1;
  transition: opacity 200ms var(--ease);
  pointer-events: none;
}

.lightbox-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity 240ms var(--ease);
}

/* Set once the file has decoded: sharp image in, fog and sheen out. */
.lightbox-frame.is-ready .lightbox-img { opacity: 1; }
.lightbox-frame.is-ready::before { opacity: 0; }
.lightbox-frame.is-ready::after { opacity: 0; animation: none; }
.lightbox-cap {
  font-size: var(--fs-caption);
  color: var(--forest-200);
  text-align: center;
  /* Match the frame, so the caption never widens the dialog past the
     image it describes. */
  width: 100%;
  max-width: 70ch;
}
/* Sits inside the dialog box. Positioning it above the box put it off
   the top of the viewport whenever the image was tall. */
.lightbox-close {
  position: absolute;
  top: var(--pad);
  right: var(--pad);
  z-index: 2;
  background: color-mix(in srgb, #0B0F0C 62%, transparent);
  -webkit-backdrop-filter: blur(4px);
          backdrop-filter: blur(4px);
  display: grid;
  place-items: center;
  width: var(--btn);
  height: var(--btn);
  padding: 0;
  border: var(--border-hair) solid color-mix(in srgb, var(--forest-300) 40%, transparent);
  border-radius: var(--radius-sm);
  color: var(--text-on-dark);
  cursor: pointer;
  transition: color var(--dur-mid) var(--ease), border-color var(--dur-mid) var(--ease);
}
.lightbox-close:hover { color: var(--white); border-color: var(--forest-300); }
.lightbox :focus-visible { outline-color: var(--forest-300); }

/* ---- Reduced motion ----
   Kill every scripted animation and show the finished state. */
@media (prefers-reduced-motion: reduce) {
  .js-anim .reveal,
  .js-anim .hero-step { opacity: 1; transform: none; transition: none; }
  .js-anim .hero-name::after,
  .js-anim .hero-name.is-in::after { transform: scaleX(1); transition: none; }
  .js-anim .rotator-caret { animation: none; }
  .js-anim .to-top { transition: none; }
  .lightbox,
  .lightbox[open],
  .lightbox::backdrop,
  .lightbox[open]::backdrop { transition: none; transform: none; }
  .lightbox-img { transition: none; }
  .lightbox-frame::before,
  .lightbox-frame::after { transition: none; animation: none; }
}
