/* ==========================================================================
   Abby Sofia Doña — portfolio
   Single stylesheet. Mobile-first. No build step. No JavaScript.

   One dark ground throughout. The neutrals are warm (R > G > B) rather than
   blue-black, which keeps the palette related to the original cream direction
   instead of reading as cold default dark mode.

   Contrast, measured (WCAG 2.1 — text 4.5:1, large text and non-text UI 3:1):
       on-deep       on deep    17.64:1   AAA
       on-deep-mute  on deep     8.65:1   AAA
       rose          on deep     8.61:1   AAA
       rose-strong   on deep    10.39:1   AAA
       on-deep       on raise   16.47:1   AAA
       on-deep-mute  on raise    8.08:1   AAA
       rose          on raise    8.03:1   AAA
       deep          on rose     8.61:1   AAA  (button label)
   --line and --line-strong are decorative hairlines and never carry meaning.
   For controls that need a real 3:1 boundary (form fields on Contact), the
   lightest usable neutral is #6b645c at 3.21:1 — anything darker fails.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Webfonts — Lora (display) + Lato (body), self-hosted
   Lora ships as one variable file covering 400-700, so the whole display
   range is a single request. Lato is only ever used at 400 here (every bold
   thing on the page is either Lora or the mono stack), so one static weight
   is all that loads. Fallback metrics below are measured, not guessed.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Lora";
  src: url("/fonts/lora-latin-wght-normal.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Lato";
  src: url("/fonts/lato-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}


/* Soria — display face, large headings only.
   Served as the original unmodified .ttf, not a subset or converted .woff2:
   the font's embedded metadata declares CC BY-ND 4.0 (NoDerivatives), so it
   must not be altered. See fonts/README-LICENCES.md — the bundled licence
   file and the designer's own metadata disagree, and the footer carries the
   attribution CC BY requires.
   It maps only 182 codepoints, so it is scoped to display headings and never
   used for body copy. Notably it has no hyphen (U+002D). */
@font-face {
  font-family: "Soria";
  src: url("/fonts/soria.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* Metric-matched fallbacks so the swap does not re-wrap or shift anything.
   size-adjust / ascent / descent are computed from measured glyph widths
   against the actual local fallback, not estimated. */
@font-face {
  font-family: "Lora Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Times");
  font-weight: 100 900;
  size-adjust: 105.8%;
  ascent-override: 95.5%;
  descent-override: 25.5%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Soria Fallback";
  src: local("Georgia"), local("Times New Roman"), local("Times");
  font-weight: 100 900;
  size-adjust: 79.9%;
  ascent-override: 93.9%;
  descent-override: 31.3%;
  line-gap-override: 0%;
}

@font-face {
  font-family: "Lato Fallback";
  src: local("Helvetica Neue"), local("Arial"), local("Segoe UI"), local("Roboto");
  font-weight: 100 900;
  size-adjust: 95.6%;
  ascent-override: 103.6%;
  descent-override: 22.0%;
  line-gap-override: 0%;
}

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Ground */
  --deep: #141210;        /* page */
  --raise: #1d1917;       /* raised panel */

  /* Text */
  --on-deep: #fbf8f3;
  --on-deep-mute: #b9afa3;

  /* Accent — taken from the logo mark, hue 337° */
  --rose: #e49bb7;        /* sampled from her logo artwork */
  --rose-strong: #eeb0c7;

  /* Hairlines (decorative) */
  --line: #2e2825;
  --line-strong: #453d37;
  /* Neutral for real UI borders (form fields). Must clear 3:1 against the
     field interior AND the page ground — which the page light now lifts.
     #736c63 dropped to 2.99:1 against the lit ground once the light was added;
     this holds 3.46:1 there and 3.71:1 against the field interior. */
  --line-ui: #7d766c;

  /* Type */
  --sans: "Lato", "Lato Fallback", system-ui, -apple-system, "Segoe UI",
    Roboto, "Helvetica Neue", Arial, sans-serif;
  --display: "Lora", "Lora Fallback", Georgia, "Times New Roman", serif;
  /* Display face for the largest headings only — limited glyph set */
  --feature: "Soria", "Soria Fallback", "Lora", Georgia, serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", "Courier New", monospace;

  /* Rhythm */
  --measure: 34rem;
  --gutter: 1.5rem;
  --shell-max: 68rem;

  color-scheme: dark;
}

@media (min-width: 40em) { :root { --gutter: 2.5rem; } }
@media (min-width: 64em) { :root { --gutter: 4rem; } }

/* --------------------------------------------------------------------------
   2. Reset / base
   -------------------------------------------------------------------------- */

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

/* The ground lives on the root, not on body, so a light source can sit behind
   every page without adding markup to five files. A neutral warm lift rather
   than a rose wash: it raises luminance without tinting the page pink, and it
   costs less contrast (accent holds 5.41:1 against the lit hero ground, vs
   4.98:1 for an equivalent rose light).
   Deliberately NOT background-attachment: fixed — that forces a repaint every
   scroll frame on some mobile GPUs. The light belongs to the document, above
   its top edge, so a page-anchored gradient is also the truer metaphor.
   Horizontal extent is 90%, so the light falls off before the viewport edges
   and the content column reads as lit while the edges stay dark. */
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background-color: var(--deep);
  background-image:
    radial-gradient(90% 720px at 50% -90px, rgba(251, 248, 243, 0.055), transparent 70%);
  background-repeat: no-repeat;
}

body {
  margin: 0;
  background: transparent;      /* the root paints the ground; see html above */
  color: var(--on-deep);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.65;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 600;
  line-height: 1.14;
  letter-spacing: -0.012em;
  margin: 0;
  color: var(--on-deep);
  text-wrap: balance;
}

/* h3s sit below Soria h2s, so they step down a weight rather than matching. */
h3, .practice__term { font-weight: 500; }

p { margin: 0 0 1.25em; max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

a { color: var(--rose); }
a:hover { color: var(--rose-strong); }

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

::selection { background: var(--rose); color: var(--deep); }

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */

.wrap {
  width: 100%;
  max-width: var(--shell-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  background: var(--rose);
  color: var(--deep);
  font-family: var(--mono);
  font-size: 0.8125rem;
  padding: 0.75rem 1.25rem;
  text-decoration: none;
}

.skip-link:focus { left: 0; color: var(--deep); }

/* --------------------------------------------------------------------------
   4. Masthead
   -------------------------------------------------------------------------- */

.masthead { padding-block: 1.75rem 1.25rem; }

.masthead__inner {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  font-family: var(--display);
  font-size: 1.1875rem;
  font-weight: 600;
  color: var(--on-deep);
  text-decoration: none;
}

/* Intrinsic width/height are set on the element itself so the box is reserved
   before the image loads — no layout shift. */
.wordmark__mark {
  height: 2rem;
  width: auto;
  display: block;
}

.wordmark:hover { color: var(--rose); }
.wordmark .dot { color: var(--rose); }

.nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav a {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-deep-mute);
  text-decoration: none;
  padding-block: 0.375rem;
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.nav a:hover,
.nav a[aria-current="page"] {
  color: var(--on-deep);
  border-bottom-color: var(--rose);
}

@media (min-width: 40em) {
  .masthead { padding-block: 2.25rem 1.5rem; }
  .masthead__inner {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: 2rem;
  }
}

/* --------------------------------------------------------------------------
   5. Hero
   -------------------------------------------------------------------------- */

/* Ambient bloom: two soft pools of the accent at low alpha, drifting slowly.
   Peak alpha 0.14 puts the composited ground at #312527, where the quietest
   text on it still measures 6.83:1 — the drift cannot break AA at any point
   in its cycle. Animates transform only, so it composites on the GPU and
   costs nothing in layout or paint. */
.hero-band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.hero-band::before {
  content: "";
  position: absolute;
  inset: 0 -12%;
  z-index: -1;
  pointer-events: none;
  /* One pool behind the headline with a wide falloff. Two pools read as two
     visible blobs; a single broad one reads as light. */
  background:
    radial-gradient(58% 68% at 32% 44%, rgba(228, 155, 183, 0.14), transparent 76%);
  /* The masthead above and the first section below are separate elements, so
     `overflow: hidden` would cut the bloom off in hard horizontal lines at
     both joins. Fading both ends to zero — generously enough that the drift's
     scale and translate never push an opaque edge into the clip — keeps the
     whole page reading as one continuous dark surface. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 34%, #000 66%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 34%, #000 66%, transparent 100%);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-band::before {
    animation: bloom-drift 34s ease-in-out infinite alternate;
  }
}

@keyframes bloom-drift {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  to   { transform: translate3d(4%, 3%, 0) scale(1.14); }
}

.hero { padding-block: 2.25rem 2.25rem; }

@media (min-width: 40em) { .hero { padding-block: 3.5rem 2.5rem; } }
@media (min-width: 64em) { .hero { padding-block: 4rem 2.75rem; } }

.hero__eyebrow {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--rose);
  margin: 0 0 1.5rem;
}

.hero__title {
  font-family: var(--display);
  letter-spacing: -0.012em;
  font-size: clamp(2.25rem, 1.4rem + 4vw, 4.25rem);
  margin-bottom: 1.75rem;
}

@media (min-width: 48em) { .hero__title { max-width: 17ch; } }

.hero__rule {
  transform-origin: 0 50%;
  width: 3.5rem;
  height: 2px;
  background: var(--rose);
  border: 0;
  margin: 0 0 1.75rem;
}

.hero__lede {
  font-size: clamp(1.0625rem, 1rem + 0.4vw, 1.1875rem);
  line-height: 1.65;
  color: var(--on-deep-mute);
  max-width: 42ch;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 2rem;
  margin-top: 2rem;
}

/* -- spec strip ----------------------------------------------------------- */

.spec {
  margin: 0;
  padding-block: 1.75rem 2.5rem;
  border-top: 1px solid var(--line-strong);
  display: grid;
  gap: 1.25rem;
}

@media (min-width: 40em) {
  .spec {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 3rem;
    padding-block: 2rem 3.5rem;
  }
}

.spec__row { margin: 0; }

.spec__key {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
  margin-bottom: 0.375rem;
}

.spec__val {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--on-deep-mute);
  margin: 0;
}

/* --------------------------------------------------------------------------
   6. Buttons / inline links
   -------------------------------------------------------------------------- */

.btn {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 0.875rem 1.5rem;
  border-radius: 2px;
  background: var(--rose);
  border: 1px solid var(--rose);
  /* Set explicitly: without it the label inherits the accent link colour and
     disappears into its own button. */
  color: var(--deep);
  transition: background-color 0.15s ease, color 0.15s ease,
    border-color 0.15s ease;
}

.btn:hover {
  background: transparent;
  color: var(--rose);
  border-color: var(--rose);
}

.link-quiet {
  font-family: var(--mono);
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--on-deep);
  border-bottom: 1px solid var(--rose);
  padding-bottom: 3px;
}

.link-quiet:hover { color: var(--rose); }

/* --------------------------------------------------------------------------
   7. Section furniture
   -------------------------------------------------------------------------- */

.section { padding-block: 4rem; }
.section + .section { border-top: 1px solid var(--line); }

/* Opt-in tonal step, not :nth-child — nth silently reshuffles the whole page
   the moment a section is added or reordered.
   A soft pool rather than a flat fill: these sections carry .wrap, so a solid
   background would paint a floating 68rem rectangle instead of a full-width
   band. Making it fade out before its own edges hides the containment, and
   avoids the 100vw full-bleed trick that introduces horizontal overflow. */
.section--lift {
  background-image:
    radial-gradient(110% 90% at 50% 45%, rgba(251, 248, 243, 0.035), transparent 70%);
}

@media (min-width: 40em) { .section { padding-block: 5.5rem; } }

/* Soria sits at h2 only. It ships a single 400 weight, so these must not ask
   for more — requesting 600 makes the browser synthesise bold by smearing the
   outlines, which destroys the thin/thick contrast the face is built on.
   Keeping it off h1 also sidesteps its missing hyphens and dashes, which the
   longer headline text is far more likely to need. */
.section__title {
  font-family: var(--feature);
  font-weight: 400;
  letter-spacing: 0.005em;
  font-size: clamp(1.5rem, 1.2rem + 1.4vw, 2.125rem);
  margin-bottom: 2rem;
}

/* --------------------------------------------------------------------------
   8. Practice list
   -------------------------------------------------------------------------- */

.practice {
  display: grid;
  gap: 2.5rem;
  margin: 0;
  padding: 0;
}

@media (min-width: 48em) {
  .practice { grid-template-columns: repeat(3, 1fr); gap: 3rem 2.5rem; }
}

.practice__item {
  margin: 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line-strong);
}

.practice__term {
  font-family: var(--display);
  font-size: 1.1875rem;
  font-weight: 500;
  color: var(--on-deep);
  margin: 0 0 0.5rem;
}

.practice__desc {
  margin: 0;
  color: var(--on-deep-mute);
  font-size: 0.9875rem;
  max-width: 34ch;
}

/* --------------------------------------------------------------------------
   9. Metrics — compact stats grid closing the hero band
   Sits high on the page deliberately: the count is a one-shot animation on
   load rather than scroll-driven, so it has to be on screen when the page
   paints. Tying it to scroll made it hostage to how fast the reader scrolled.
   -------------------------------------------------------------------------- */

.metrics {
  list-style: none;
  margin: 0;
  padding-block: 2rem 2.75rem;
  border-top: 1px solid var(--line-strong);
  display: grid;
  gap: 1.75rem;
}

@media (min-width: 40em) {
  .metrics {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 3rem;
    padding-block: 2.5rem 3rem;
  }
}

.metric { margin: 0; }

.metric__val {
  display: block;
  font-family: var(--mono);
  font-size: clamp(1.875rem, 1.4rem + 2.2vw, 2.75rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--on-deep);
  line-height: 1;
}

.metric__key {
  display: block;
  margin-top: 0.75rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
}

/* One short qualifier, not a paragraph — the full story is on the Work page.
   Two stacked three-column blocks both carrying prose read as clutter. */
.metric__note {
  margin: 0.375rem 0 0;
  color: var(--on-deep-mute);
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.01em;
}

/* --------------------------------------------------------------------------
   10. Closing call to action — raised panel
   -------------------------------------------------------------------------- */

.closer {
  background: var(--raise);
  border-top: 1px solid var(--line-strong);
  padding-block: 4rem;
}

@media (min-width: 40em) { .closer { padding-block: 5.5rem; } }

.closer__title {
  font-family: var(--feature);
  font-weight: 400;
  letter-spacing: 0.005em;
  font-size: clamp(1.625rem, 1.3rem + 1.6vw, 2.25rem);
  margin-bottom: 1.25rem;
  max-width: 20ch;
}

.closer__text {
  color: var(--on-deep-mute);
  max-width: 42ch;
  margin-bottom: 2rem;
}

/* --------------------------------------------------------------------------
   11. Footer
   -------------------------------------------------------------------------- */

.footer {
  padding-block: 2.5rem 3rem;
  border-top: 1px solid var(--line);
}

.footer__inner {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--on-deep-mute);
}

.footer p { margin: 0; max-width: none; }

/* CC BY-ND 4.0 requires visible attribution for the display face. */
.footer__credit {
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  opacity: 0.75;
}
.footer a { color: var(--on-deep-mute); text-decoration-color: var(--rose); }
.footer a:hover { color: var(--rose); }

@media (min-width: 40em) {
  .footer__inner {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    gap: 2rem;
  }
}

/* --------------------------------------------------------------------------
   12. Scroll-driven reveal — CSS only, no JavaScript
   Gated three ways so it can never hide content it cannot then reveal:
     - only when the user has not asked for reduced motion
     - only where animation-timeline is actually supported
     - never applied to the hero, so the LCP element paints immediately
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .reveal {
      animation: reveal-in linear both;
      animation-timeline: view();
      /* Completes within the element's own entry phase rather than at a
         `cover` position, so a section sitting at the bottom of the document
         still finishes revealing when the page can scroll no further. */
      animation-range: entry 5% entry 70%;
    }
  }
}

@keyframes reveal-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* ==========================================================================
   13. Inner pages
   ========================================================================== */

/* -- page header (reuses .hero-band for the ambient bloom) ---------------- */

.pagehead { padding-block: 2.5rem 3rem; }

@media (min-width: 40em) { .pagehead { padding-block: 3.5rem 4rem; } }

.pagehead__title {
  font-family: var(--display);
  letter-spacing: -0.012em;
  font-size: clamp(2rem, 1.5rem + 2.4vw, 3.25rem);
  margin-bottom: 1.25rem;
  max-width: 20ch;
}

.pagehead__lede {
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.125rem);
  color: var(--on-deep-mute);
  max-width: 46ch;
}

/* -- case studies --------------------------------------------------------- */

.cases { list-style: none; margin: 0; padding: 0; }

.case {
  padding-block: 2.5rem;
  border-top: 1px solid var(--line);
}

.case:last-child { border-bottom: 1px solid var(--line); }

.case__meta {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
  margin: 0 0 0.75rem;
}

.case__title {
  font-size: clamp(1.25rem, 1.1rem + 0.7vw, 1.5rem);
  margin-bottom: 0.875rem;
}

.case__summary {
  color: var(--on-deep-mute);
  max-width: 58ch;
  margin-bottom: 1.25rem;
}

.case__points {
  margin: 0;
  padding: 0;
  list-style: none;
  max-width: 58ch;
}

.case__points li {
  position: relative;
  padding-left: 1.25rem;
  margin-bottom: 0.625rem;
  color: var(--on-deep-mute);
  font-size: 0.9375rem;
}

.case__points li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.7em;
  width: 0.5rem;
  height: 1px;
  background: var(--rose);
}

.case__points li:last-child { margin-bottom: 0; }

.case__link {
  display: inline-block;
  margin-top: 1.25rem;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid var(--rose);
  padding-bottom: 2px;
}

/* -- case study teaser ----------------------------------------------------
   A case with a full write-up behind it gets this panel instead of a plain
   text link: enough visual weight that a reader scanning the list registers
   there is something to open, without the detail page's bulk landing inline.
   ------------------------------------------------------------------------- */

.case__demo {
  display: block;
  margin-top: 1.5rem;
  text-decoration: none;
  color: inherit;
  border: 1px solid var(--line);
  background: var(--raise);
  transition: border-color 0.25s ease, background 0.25s ease;
}

.case__demo:hover,
.case__demo:focus-visible {
  border-color: var(--line-ui);
  background: #221d1b;
}

.case__demo:hover .case__demo__cue { color: var(--rose-strong); }
.case__demo:hover .case__demo__cue span { transform: translateX(3px); }

.demo-figures {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1px;
  background: var(--line);
  border-bottom: 1px solid var(--line);
}

@media (min-width: 34em) { .demo-figures { grid-template-columns: repeat(5, 1fr); } }

.demo-figure { background: var(--raise); padding: 0.875rem 0.75rem; }

.demo-figure b {
  display: block;
  font-family: var(--display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1;
  color: var(--on-deep);
}

.demo-figure span {
  display: block;
  margin-top: 0.375rem;
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.04em;
  color: var(--on-deep-mute);
}

.case__demo__foot {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: baseline;
  justify-content: space-between;
  padding: 0.75rem 1rem;
}

.case__demo__what {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--on-deep-mute);
  max-width: none;
}

.case__demo__cue {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--rose);
  white-space: nowrap;
}

.case__demo__cue span {
  display: inline-block;
  transition: transform 0.25s ease;
}

/* -- breadcrumb ------------------------------------------------------------ */

.crumb { margin-bottom: 1.25rem; }

.crumb ol {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--on-deep-mute);
}

.crumb li + li::before {
  content: "/";
  margin-right: 0.5rem;
  color: var(--line-ui);
}

.crumb a { text-decoration: none; border-bottom: 1px solid transparent; }
.crumb a:hover { border-bottom-color: var(--rose); }

@media (min-width: 56em) {
  .case {
    display: grid;
    grid-template-columns: 13rem 1fr;
    column-gap: 3rem;
    padding-block: 3rem;
  }
  .case__meta { grid-column: 1; grid-row: 1 / span 5; margin: 0; }
  .case__title,
  .case__summary,
  .case__points,
  .case__demo,
  .case__link { grid-column: 2; }
}

/* -- experience timeline -------------------------------------------------- */

.roles { list-style: none; margin: 0; padding: 0; }

.role {
  padding-block: 2rem;
  border-top: 1px solid var(--line);
}

.role:last-child { border-bottom: 1px solid var(--line); }

.role__when {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
  margin: 0 0 0.625rem;
}

.role__title {
  font-size: 1.1875rem;
  margin-bottom: 0.25rem;
}

.role__org {
  font-family: var(--mono);
  font-size: 0.8125rem;
  color: var(--on-deep-mute);
  margin: 0 0 0.875rem;
}

.role__desc {
  color: var(--on-deep-mute);
  font-size: 0.9375rem;
  max-width: 58ch;
  margin: 0;
}

@media (min-width: 56em) {
  .role {
    display: grid;
    grid-template-columns: 13rem 1fr;
    column-gap: 3rem;
  }
  .role__when { grid-column: 1; grid-row: 1 / span 3; margin: 0; }
  .role__title, .role__org, .role__desc { grid-column: 2; }
}

/* -- skill groups --------------------------------------------------------- */

.skills { display: grid; gap: 2rem; margin: 0; padding: 0; }

@media (min-width: 48em) {
  .skills { grid-template-columns: repeat(2, 1fr); gap: 2.25rem 3rem; }
}

.skills__group { margin: 0; }

.skills__label {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
  padding-top: 1rem;
  border-top: 1px solid var(--line-strong);
  margin-bottom: 0.625rem;
}

.skills__items {
  margin: 0;
  color: var(--on-deep-mute);
  font-size: 0.9375rem;
  max-width: 44ch;
}

/* -- credentials ---------------------------------------------------------- */

.creds { list-style: none; margin: 0; padding: 0; }

.creds li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1rem;
  padding-block: 1rem;
  border-top: 1px solid var(--line);
  font-size: 0.9375rem;
  color: var(--on-deep-mute);
}

.creds li:last-child { border-bottom: 1px solid var(--line); }

.creds__name { color: var(--on-deep); }

.creds__year {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  color: var(--rose);
  white-space: nowrap;
}

/* -- contact form --------------------------------------------------------- */

.contact-grid { display: grid; gap: 3rem; }

@media (min-width: 56em) {
  .contact-grid {
    grid-template-columns: 1fr 18rem;
    gap: 4rem;
    align-items: start;
  }
}

.form { display: grid; gap: 1.5rem; max-width: 34rem; }

.field { display: grid; gap: 0.5rem; }

.field label {
  font-family: var(--mono);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--on-deep-mute);
}

/* No placeholder text anywhere — placeholders routinely fail contrast and
   vanish on input. Every field has a persistent visible label instead. */
.field input,
.field textarea {
  font-family: var(--sans);
  font-size: 1rem;
  color: var(--on-deep);
  background: var(--raise);
  border: 1px solid var(--line-ui);
  border-radius: 2px;
  padding: 0.75rem 0.875rem;
  width: 100%;
}

.field textarea { min-height: 9rem; resize: vertical; }

.field input:focus-visible,
.field textarea:focus-visible {
  outline: 2px solid var(--rose);
  outline-offset: 2px;
  border-color: var(--rose);
}

.field__hint {
  font-size: 0.8125rem;
  color: var(--on-deep-mute);
  margin: 0;
}

.form button {
  font-family: var(--mono);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  background: var(--rose);
  color: var(--deep);
  border: 1px solid var(--rose);
  border-radius: 2px;
  padding: 0.875rem 1.5rem;
  cursor: pointer;
  justify-self: start;
  transition: background-color 0.15s ease, color 0.15s ease;
}

.form button:hover { background: transparent; color: var(--rose); }

/* -- direct contact details ----------------------------------------------- */

.details { margin: 0; display: grid; gap: 1.5rem; }

.details__key {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
  margin-bottom: 0.375rem;
}

.details__val {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--on-deep-mute);
  overflow-wrap: anywhere;
}

.details__val a { color: var(--on-deep); text-decoration-color: var(--rose); }
.details__val a:hover { color: var(--rose); }

/* -- portrait + bio ------------------------------------------------------- */

/* Portrait and credentials sit in one narrow column, the prose runs beside
   them. Previously the text sat below the whole block, which left a large
   empty field to the right of the photo on wide screens. */
.bio { display: grid; gap: 2rem; align-items: start; }

@media (min-width: 48em) {
  .bio {
    grid-template-columns: 17rem minmax(0, 1fr);
    gap: 3.5rem;
  }
}

@media (min-width: 68em) {
  .bio { grid-template-columns: 19rem minmax(0, 1fr); gap: 4.5rem; }
}

/* Sticky, so the portrait stays with the reader instead of leaving a 400px
   dead column behind it. Only from 48em up — below that the layout is a single
   column and sticky would pin the photo over the prose.
   This is the alternative to putting the bio in a scrolling box: nothing is
   hidden, no nested scroll region, and no keyboard trap. */
.bio__aside { display: grid; gap: 1.75rem; align-content: start; }

@media (min-width: 48em) {
  .bio__aside {
    position: sticky;
    top: 2.5rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bio__aside { position: static; }
}

/* Offset frame: a hairline box shifted down-left behind the photograph. Costs
   no asset and no request — it is one pseudo-element. `isolation` keeps the
   z-index:-1 inside this figure rather than punching through the page ground. */
.bio__figure {
  position: relative;
  isolation: isolate;
  margin: 0;
}

/* The offset always stays inside the page gutter — roughly half of it at each
   breakpoint. At a full 1.5rem offset against a 1.5rem gutter the frame landed
   exactly on the viewport edge, which reads as a mistake rather than a shift. */
.bio__figure::before {
  content: "";
  position: absolute;
  inset: 0.75rem 0.75rem -0.75rem -0.75rem;
  z-index: -1;
  border: 1px solid rgba(228, 155, 183, 0.45);
  border-radius: 4px;
  pointer-events: none;
}

@media (min-width: 40em) {
  .bio__figure::before { inset: 1.25rem 1.25rem -1.25rem -1.25rem; }
}

@media (min-width: 64em) {
  .bio__figure::before { inset: 2rem 2rem -2rem -2rem; }
}

/* Credential rail — the same mono label recipe as .case__meta, .role__when,
   .skills__label and .metric__key. Sits on the dark ground beside the photo,
   never over it, so there is no text-on-photograph contrast problem. */
.bio__creds {
  list-style: none;
  margin: 0;
  padding: 0;
}

.bio__creds li {
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--rose);
  padding-block: 0.875rem;
  border-top: 1px solid var(--line);
}

.bio__creds li:last-child { border-bottom: 1px solid var(--line); }

/* Monogram divider. Uses the masthead mark, so it is already in cache. */
.divider {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  margin-block: 3rem;
}

.divider::before,
.divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line-strong);
}

.divider img {
  height: 1.75rem;
  width: auto;
  display: block;
  opacity: 0.85;
}

/* An environmental portrait now, so it carries its own background — no plate,
   no cutout, no matte fringe to erode away. Its edges are mid-tone and meet
   the page ground at 4.6-8.5:1, which is a far gentler join than the old
   near-white plate managed at 17.64:1. */
.bio__portrait {
  display: block;
  width: 100%;
  height: auto;
  /* A full arch reads oddly once the column narrows on phones, so this is a
     plain soft rectangle instead, matching the 2-3px radii used elsewhere. */
  border-radius: 4px;
}

.bio__text p { max-width: 42ch; }
.bio__text p:last-child { margin-bottom: 0; }

/* ==========================================================================
   14. Motion — all CSS, no JavaScript
   Every effect below is gated twice: off entirely when the visitor asks for
   reduced motion, and only applied where scroll-driven timelines actually
   exist. Anything that hides content to reveal it must never run unless it
   can also finish, so the base styles stay fully visible on their own.
   ========================================================================== */

/* -- scroll progress bar -------------------------------------------------- */

.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--rose);
  transform: scaleX(0);          /* invisible without timeline support */
  transform-origin: 0 50%;
  z-index: 20;
  pointer-events: none;
}

@supports (animation-timeline: scroll()) {
  .progress {
    animation: progress-grow linear both;
    animation-timeline: scroll(root block);
  }
}

@keyframes progress-grow { to { transform: scaleX(1); } }

/* -- film grain ----------------------------------------------------------- */

/* A single 160px tile of fractal noise, repeated. Inline SVG, so it costs no
   request at all. Kept at 3.5% — measured, so it cannot move any text pair
   below its contrast threshold. */
.grain {
  position: fixed;
  inset: 0;
  z-index: 15;
  pointer-events: none;
  opacity: 0.035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

@media (prefers-reduced-motion: reduce) { .grain { opacity: 0.02; } }

/* -- headline staggered reveal -------------------------------------------- */

/* Each word of the h1 arrives in turn. Transforms need a block-ish box, so the
   words are inline-block; they still wrap on their spaces as normal text.
   The base state is fully visible — only the animation introduces opacity 0 —
   so if animations never run the headline simply renders.
   Deliberately quick: the h1 is the LCP element on every page, and every
   millisecond of stagger is a millisecond LCP is pushed back. First word
   starts at zero delay so painting begins immediately. */
.hero__title .w,
.pagehead__title .w { display: inline-block; }

@media (prefers-reduced-motion: no-preference) {
  .hero__title .w,
  .pagehead__title .w {
    animation: word-rise 0.5s cubic-bezier(0.22, 0.7, 0.2, 1) both;
  }
  .hero__title .w:nth-child(1),  .pagehead__title .w:nth-child(1)  { animation-delay: 0ms; }
  .hero__title .w:nth-child(2),  .pagehead__title .w:nth-child(2)  { animation-delay: 45ms; }
  .hero__title .w:nth-child(3),  .pagehead__title .w:nth-child(3)  { animation-delay: 90ms; }
  .hero__title .w:nth-child(4),  .pagehead__title .w:nth-child(4)  { animation-delay: 135ms; }
  .hero__title .w:nth-child(5),  .pagehead__title .w:nth-child(5)  { animation-delay: 180ms; }
  .hero__title .w:nth-child(6),  .pagehead__title .w:nth-child(6)  { animation-delay: 225ms; }
  .hero__title .w:nth-child(n+7),
  .pagehead__title .w:nth-child(n+7) { animation-delay: 260ms; }

  /* the rest of the hero follows the headline in */
  .hero__eyebrow { animation: word-rise 0.5s cubic-bezier(0.22,0.7,0.2,1) both; animation-delay: 0ms; }
  .hero__rule    { animation: rule-grow 0.5s cubic-bezier(0.22,0.7,0.2,1) both; animation-delay: 300ms; }
  .hero__lede,
  .pagehead__lede { animation: word-rise 0.5s cubic-bezier(0.22,0.7,0.2,1) both; animation-delay: 340ms; }
  .hero__actions  { animation: word-rise 0.5s cubic-bezier(0.22,0.7,0.2,1) both; animation-delay: 400ms; }
  .spec           { animation: word-rise 0.5s cubic-bezier(0.22,0.7,0.2,1) both; animation-delay: 460ms; }
}

@keyframes word-rise {
  from { opacity: 0; transform: translateY(0.35em); }
  to   { opacity: 1; transform: none; }
}

@keyframes rule-grow {
  from { opacity: 0; transform: scaleX(0); }
  to   { opacity: 1; transform: none; }
}

/* -- staggered reveal ----------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .stagger > * {
      animation: reveal-in linear both;
      animation-timeline: view();
    }
    /* Scroll-driven animations ignore animation-delay, so the offset has to
       come from each child starting its range a little later. */
    .stagger > *:nth-child(1)    { animation-range: entry 2% entry 58%; }
    .stagger > *:nth-child(2)    { animation-range: entry 8% entry 64%; }
    .stagger > *:nth-child(3)    { animation-range: entry 14% entry 70%; }
    .stagger > *:nth-child(4)    { animation-range: entry 20% entry 76%; }
    .stagger > *:nth-child(5)    { animation-range: entry 26% entry 82%; }
    .stagger > *:nth-child(n+6)  { animation-range: entry 30% entry 86%; }
  }
}

/* -- count-up metrics ----------------------------------------------------- */

@property --num {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}

@media (prefers-reduced-motion: no-preference) {
  /* The real number stays in the DOM and in the accessibility tree — it is
     only made transparent, never hidden — so assistive tech announces the
     final value while sighted readers see the tally. If animations never run,
     the plain number simply renders. */
  .count { position: relative; color: transparent; }
  .count::after {
    position: absolute;
    inset: 0;
    color: var(--on-deep);
    content: counter(num);
    counter-reset: num var(--num);
    animation-duration: 1.1s;
    animation-timing-function: cubic-bezier(0.2, 0.75, 0.25, 1);
    animation-fill-mode: both;
    animation-delay: 520ms;      /* lands just after the headline finishes */
  }
  .count--50::after  { animation-name: count-50; }
  .count--10::after  { animation-name: count-10; }
  .count--153::after { animation-name: count-153; }
}

@keyframes count-50  { from { --num: 0; } to { --num: 50; } }
@keyframes count-10  { from { --num: 0; } to { --num: 10; } }
@keyframes count-153 { from { --num: 0; } to { --num: 153; } }

/* -- "who you'd be working with" — the About entry point on Home ----------- */

/* About was previously reachable only from the top nav. This gives it a real
   route in, and puts a face on the home page, which does more for the
   "reliable" read than any amount of background treatment. Uses a dedicated
   240x300 thumbnail (19 KB) rather than the 72 KB About portrait, so the
   most-visited page does not carry the larger file. */

.who { display: grid; gap: 1.75rem; align-items: center; }

@media (min-width: 40em) {
  .who { grid-template-columns: 8.5rem 1fr; gap: 2.75rem; }
}

/* Circle with an offset ring. The About page uses an offset rectangular frame
   on the same accent — this rhymes with it rather than repeating it, so the
   two portraits read as one system without looking duplicated. The offset
   stays under half the page gutter so the ring never reaches the edge. */
.who__figure {
  position: relative;
  isolation: isolate;
  margin: 0;
  width: 8.5rem;
  max-width: 100%;
}

.who__figure::before {
  content: "";
  position: absolute;
  inset: 0.5rem 0.5rem -0.5rem -0.5rem;
  z-index: -1;
  border: 1px solid rgba(228, 155, 183, 0.5);
  border-radius: 50%;
  pointer-events: none;
}

.who__portrait {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
}

.who__text p { margin-bottom: 1.25rem; max-width: 48ch; }
