/* ==========================================================================
   agbjordan.me
   Hand-authored, no build step — what is written here is what ships.
   Baseline target: Chrome/Edge 120+, Safari 17.2+, Firefox 117+.
   Features used: native nesting (always with an explicit `&`), :is()/:where(),
   clamp(), logical properties, color-mix(), custom-property theming.
   Contents
     1. Tokens
     2. Base
     3. Layout
     4. Primitives      <- reusable; new components should need markup only
     5. Components
     6. Themes
     7. At-rules
   JS contracts — see README of the inline <script> on each page:
     .fade-in / .visible          IntersectionObserver
     .back-to-top / .back-home    scroll toggle (.visible + inert)
     [data-parallax] .hero-bg     JS writes inline background-position-y
     [data-parallax] .hero-phone-shot  JS writes inline transform
   Do not rename these, and do not move .fade-in's hidden state out of the
   prefers-reduced-motion: no-preference guard (the page would blank if JS fails).
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */
:root {
  /* --- Colour -------------------------------------------------------------
     --papaya is the brand orange, and --papaya-text deliberately matches it.

     DO NOT DARKEN --papaya-text. At 2.52:1 on white it does not meet the 4.5:1
     WCAG AA minimum for text, and darkening it to something that does (#B35600
     and similar) is a rejected change -- the brand orange is the brand orange.
     This is an accepted, documented exception, not an oversight, and the a11y
     gate is configured around it: .pa11yci.js excludes the specific selectors
     that draw with this token. Change the value and that exclusion list goes
     stale.

     Lighthouse cannot be scoped the same way, so its color-contrast audit
     fails on these elements and drags the accessibility category to 0.96
     against a 0.95 threshold. That is expected and still passes. The thin
     margin is deliberate: any NEW accessibility problem drops it below 0.95
     and fails CI, which is what the gate is for.

     The two tokens stay separate so a darker text-only value can be swapped in
     from one place if that call is ever revisited. --ink-tertiary is the token
     to reach for when text needs to be quiet AND pass AA. */
  --papaya: #ff8000;
  --papaya-text: #ff8000; /*  2.52:1 on white -- see the note above */
  --papaya-text-hover: color-mix(in oklab, var(--papaya-text) 85%, black);
  --ink: #111111; /* 18.9:1 on white */
  --ink-secondary: #555555; /*  7.3:1 */
  /* Sits on white and on the tinted case-study surfaces (--surface,
     --surface-blue, #eee8f5, #f5f0f0). #767676 cleared 4.5:1 on white only
     (4.54:1) and dropped to 3.78:1 on the darkest of those; #666666 holds
     4.78:1 on all of them. */
  --ink-tertiary: #666666; /*  5.74:1 on white, 4.78:1 worst case */
  --white: #ffffff;
  --surface: #f9f8f6;
  --surface-blue: #f0f4ff;
  --rule: #e8e6e2;

  /* --- Typography --------------------------------------------------------- */
  --font-serif: "Cormorant Garamond", Georgia, serif;
  --font-sans: "Geist", system-ui, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, "SF Mono", "Fira Code", monospace;

  /* Root stays at 100% so the user's browser font-size preference is honoured;
     body opts into 18px. 1rem = 16px. */
  --text-2xs: 0.75rem; /* 12px */
  --text-xs: 0.8125rem; /* 13px */
  --text-sm: 0.875rem; /* 14px */
  --text-md: 1rem; /* 16px */
  --text-lg: 1.125rem; /* 18px — body */

  /* Serif display ramp. Four steps because the design genuinely uses four. */
  --display-sm: clamp(1.6875rem, 3vw, 2.8125rem);
  --display-md: clamp(2.025rem, 5vw, 2.8125rem);
  --display-lg: clamp(2.025rem, 5vw, 3.375rem);
  --display-xl: clamp(2.25rem, 5vw, 3.375rem);

  /* --- Space --------------------------------------------------------------
     xs..2xl is the component scale; the three --space-block/section values are
     the vertical rhythm that used to be hardcoded as 32/40/64px. */
  --space-xs: 8px;
  --space-sm: 16px;
  --space-md: 24px;
  --space-lg: 48px;
  --space-xl: 80px;
  --space-2xl: 120px;
  --space-block: 32px;
  --space-block-lg: 40px;
  --space-section: 64px;

  /* --- Shape & motion ----------------------------------------------------- */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 100px;
  --ease: 0.15s ease;
  --ease-slow: 0.25s ease;
  --focus-ring: 2px solid var(--papaya-text);

  /* --- Layout -------------------------------------------------------------
     Breakpoints (mobile-first, min-width only; em so they ignore --text-*):
       30em = 480px   35em = 560px
     Media queries cannot read custom properties, so these are documented here
     and written literally below. */
  --column: 860px;
}

/* ==========================================================================
   2. BASE
   ========================================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
html {
  font-size: 100%;
  /* iOS Safari never shipped the unprefixed property, so this is the only
     form that works there. */
  -webkit-text-size-adjust: 100%;
}
body {
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  line-height: 1.6;
  background: var(--white);
  color: var(--ink);
}
:where(img, svg, video) {
  max-width: 100%;
  display: block;
}
:where(a) {
  color: inherit;
  text-decoration: none;
}
:where(ul, ol):where([class]) {
  list-style: none;
}
:where(button) {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
}

/* One focus style for everything; :focus-visible keeps it off mouse clicks. */
:where(a, button, [tabindex]):focus-visible {
  outline: var(--focus-ring);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Scroll-driven reveal. The hidden state lives inside no-preference so that
   reduced-motion users — and anyone whose JS fails — always see the content. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
  .fade-in {
    opacity: 0;
    transform: translateY(16px);
    transition:
      opacity 0.5s ease,
      transform 0.5s ease;
    &.visible {
      opacity: 1;
      transform: none;
    }
  }
}
.skip-link {
  position: absolute;
  top: -100%;
  left: var(--space-md);
  z-index: 1000; /* above the sticky header (100) */
  padding: var(--space-xs) var(--space-sm);
  background: var(--ink); /* white-on-papaya was 2.5:1 */
  color: var(--white);
  font-size: var(--text-sm);
  font-weight: 500;
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  transition: top 0.2s;
  &:focus {
    top: 0;
  }
}

/* ==========================================================================
   3. LAYOUT
   ========================================================================== */
.column {
  max-width: var(--column);
  margin-inline: auto;
  padding-inline: var(--space-md);
}

/* Vertical rhythm + hairline shared by every top-level section. */
.section {
  padding-block: var(--space-xl);
  border-bottom: var(--section-rule, 1px solid var(--rule));
}
header[role="banner"] {
  position: sticky;
  top: 0;
  z-index: 100;
  width: 100%;
  background: var(--white);
  padding-block: var(--space-md) 0;
}

/* Full-bleed white banner, but the hairline is drawn on .header-inner so it
   lines up with the section rules inside the column measure. */
.header-inner {
  position: relative;
  max-width: var(--column);
  margin-inline: auto;
  padding: 0 var(--space-md) var(--space-lg);
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-md);
  &::after {
    content: "";
    position: absolute;
    left: var(--space-md);
    right: var(--space-md);
    bottom: 0;
    border-bottom: 1px solid var(--rule);
  }
}
.logo {
  flex-shrink: 0;
  transform: translateY(12px);
  & img {
    width: auto;
    height: 88px;
  }
}
.header-contact {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-xs);
  padding-top: 4px;
}
footer[role="contentinfo"] {
  border-top: 1px solid var(--rule);
  padding-block: var(--footer-pad, var(--space-lg));
}
.footer-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-sm);
}
.footer-copy {
  font-size: var(--text-sm);
  color: var(--ink-tertiary);
}
.footer-links {
  display: flex;
  gap: var(--space-md);
}

/* ==========================================================================
   4. PRIMITIVES
   Reusable across both page types. Variation is carried by modifiers and
   custom properties, so a new component needs markup, not new CSS.
   ========================================================================== */

/* --- Muted link that warms on hover (header, footer, back-nav) ------------- */
.link-muted {
  font-size: var(--text-sm);
  color: var(--ink-secondary);
  transition: color var(--ease);
  &.link-muted--faint {
    color: var(--ink-tertiary);
  }
  &:hover {
    color: var(--papaya-text);
  }
}

/* --- Underlined link ------------------------------------------------------ */
.link-underline {
  display: inline-block;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  transition:
    color var(--ease),
    border-color var(--ease),
    opacity var(--ease);
  &:hover {
    color: var(--papaya-text);
    border-color: var(--papaya-text);
  }

  /* Already-accented: carries the brand colour at rest. */
  &.link-underline--accent {
    color: var(--papaya-text);
    border-color: var(--papaya-text);
    font-weight: 500;
  }

  /* Serif, display-sized (next-project link). */
  &.link-underline--display {
    font-family: var(--font-serif);
    font-size: 2.025rem;
    font-weight: 400;
  }

  /* Outbound, with an arrow affordance. */
  &.link-underline--external {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--text-sm);
    margin-top: var(--space-sm);
    &::after {
      content: "\2197";
      font-size: var(--text-2xs);
    }
  }
}

/* Inline text link inside a paragraph. */
.link-inline {
  color: var(--papaya-text);
  transition: color var(--ease);
  &:hover {
    color: var(--papaya-text-hover);
  }
}

/* --- Uppercase eyebrow label ---------------------------------------------- */
.eyebrow {
  font-size: var(--eyebrow-size, var(--text-sm));
  font-weight: 500;
  letter-spacing: var(--eyebrow-tracking, 0.1em);
  text-transform: uppercase;
  color: var(--ink-tertiary);
  margin-bottom: var(--eyebrow-gap, 0);
  &.eyebrow--accent {
    color: var(--papaya-text);
  }
  &.eyebrow--wide {
    --eyebrow-tracking: 0.12em;
  }
  &.eyebrow--gap {
    --eyebrow-gap: var(--space-sm);
  }
  &.eyebrow--gap-lg {
    --eyebrow-gap: 20px;
  }
  &.eyebrow--gap-xs {
    --eyebrow-gap: 4px;
  }
}

/* --- Serif display heading ------------------------------------------------ */
:where(h1, h2),
.display {
  font-family: var(--font-serif);
  font-weight: var(--display-weight, 200);
  font-size: var(--display-size, var(--display-md));
  line-height: var(--display-leading, 1.2);
  letter-spacing: -0.01em;
  font-variant-numeric: lining-nums;
  text-wrap: balance;
  margin-bottom: var(--display-gap, var(--space-lg));
}

/* Per-context ramp: property-only rules, no repeated declarations. */
.intro h1 {
  --display-size: var(--display-lg);
  --display-gap: var(--space-md);
}
.hero h1 {
  --display-size: var(--display-xl);
  --display-weight: 400;
  --display-leading: 1.15;
  --display-gap: var(--space-md);
}
body.case-study h2 {
  --display-size: var(--display-sm);
  --display-weight: 400;
  --display-gap: 20px;
}

/* --- Serif italic quote/label voice --------------------------------------- */
.quote {
  font-family: var(--font-serif);
  font-weight: 300;
  font-style: italic;
}

/* --- Plain grid ----------------------------------------------------------- */
.grid {
  display: grid;
  gap: var(--grid-gap, var(--space-sm));
  grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr));
}

/* --- Hairline card grid ---------------------------------------------------
   The 1px gap over a --rule background draws the dividers, so cells need no
   borders of their own. Base is a single column: that is the whole reason the
   three max-width:480px overrides could be deleted. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(var(--cols, 1), minmax(0, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-block: var(--space-block);
  & > * {
    background: var(--white);
    padding: var(--card-pad, 20px var(--space-sm));
  }

  /* Fills the row with as many cells as fit — no breakpoint needed. */
  &.card-grid--auto {
    grid-template-columns: repeat(auto-fit, minmax(var(--card-min, 120px), 1fr));
  }
}
@media (min-width: 30em) {
  .grid--2,
  .card-grid--2 {
    --cols: 2;
  }
  .grid--3,
  .card-grid--3 {
    --cols: 3;
  }
}

/* --- Bordered, rounded, clipped container --------------------------------- */
.panel {
  border: 1px solid var(--rule);
  border-radius: var(--panel-radius, var(--radius-md));
  overflow: hidden;
}

/* --- Pill ----------------------------------------------------------------- */
.pill {
  font-size: var(--pill-size, var(--text-sm));
  font-weight: 200;
  color: var(--ink-secondary);
  background: var(--surface);
  padding: var(--pill-pad, 8px 14px);
  border-radius: var(--radius-pill);
  letter-spacing: 0.05em;
  &.pill--sm {
    --pill-size: var(--text-2xs);
    --pill-pad: 4px 12px;
    font-weight: 400;
    letter-spacing: normal;
  }
}
.pill-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: var(--pill-list-gap, var(--space-lg));
  &.pill-list--gap {
    --pill-list-gap: var(--space-md);
  }
}

/* ==========================================================================
   5. COMPONENTS — HOMEPAGE
   ========================================================================== */
.intro {
  & h1 span {
    color: var(--papaya-text);
  }
}
.intro-meta {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
  margin-top: var(--space-lg);
}

/* --- Work grid ------------------------------------------------------------ */
.work-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-xl);
}
.work-item {
  display: block;
  cursor: pointer;
  &:hover .work-thumb {
    transform: translateY(-3px) scale(1.02);
  }
}
.work-thumb {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--thumb-bg, var(--surface));
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: var(--space-sm);
  /* Two layers: a tight ambient plus a wider key with negative spread, so the
     falloff clears the card edge instead of hiding under it.
     Strength is a knob because an identical cast does not read identically —
     against a near-black card (Fraction, Winga) it looks like a crisp lift,
     against a white one (Payconnect, Yoma) the same grey looks like a heavy
     haze. The light cards dial these down in THEMES to match by eye, which is
     the only match that matters here.
     Defaults live in the var() fallbacks, not as declarations on this rule --
     same idiom as --thumb-bg above. A declaration here would sit on the
     element itself and beat the value Yoma inherits from its .work-item. */
  box-shadow:
    0 2px 6px rgb(0 0 0 / var(--thumb-shadow-ambient, 0.06)),
    0 18px 45px -12px rgb(0 0 0 / var(--thumb-shadow-key, 0.28));
  transition: transform var(--ease-slow);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Only the Yoma card is a plain thumb; every other card is covered edge to
   edge by .hero-bg, so it needs no background of its own. */
.work-item[data-project="yoma"] {
  --thumb-bg: #f5f0f0;
  /* Same reason as Payconnect — a light card needs less shadow to read level
     with the dark ones. Set here rather than on .work-thumb so it inherits. */
  --thumb-shadow-ambient: 0.04;
  --thumb-shadow-key: 0.16;
  & .work-thumb-label {
    color: rgb(180 30 30 / 0.15);
  }
}
.work-thumb-label {
  font-family: var(--font-serif);
  font-size: 2.8125rem;
  font-weight: 300;
  font-style: italic;
  color: var(--ink-tertiary);
  letter-spacing: 0.02em;
}
.work-title {
  font-size: var(--text-lg);
  font-weight: 400;
  margin-bottom: 4px;
}
.work-tags {
  font-size: var(--text-sm);
  color: var(--ink-tertiary);
}

/* --- Animated work hero ---------------------------------------------------
   Layered web background + real iPhone frame with a scroll-driven screenshot.
   Every knob is a custom property so a new project is a class in the markup
   plus four lines in the THEMES section.
   JS writes inline background-position-y on .hero-bg and transform on
   .hero-phone-shot; inline styles beat these rules, which is the intent. */
.work-thumb--hero {
  --hero-bg-image: none;
  --hero-bg-fill: #0a0a0a; /* matches the page bg so the letterbox is seamless */
  --hero-phone-left: auto; /* set one of left/right; leave the other auto */
  --hero-phone-right: 6%;
  --hero-phone-top: 26%;
  --hero-phone-width: 40%;
  --hero-screen-fill: #0a0a0a;
  background-color: var(--hero-bg-fill);
  z-index: -10;
}
.hero-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Longhands, not the shorthand: JS sets background-position-y and a later
     shorthand would silently reset it. */
  background-color: var(--hero-bg-fill);
  background-image: var(--hero-bg-image);
  background-position: top center; /* fit full-width, anchored to the top */
  background-size: 100% auto;
  background-repeat: no-repeat;
  will-change: background-position;
}
.hero-phone {
  position: absolute; /* bleeds off the bottom of the card */
  z-index: 2;
  width: var(--hero-phone-width);
  left: var(--hero-phone-left);
  right: var(--hero-phone-right);
  top: var(--hero-phone-top);
  aspect-ratio: 480 / 988; /* matches mobile-frame.png */
}
.hero-phone-frame {
  position: absolute;
  inset: 0;
  z-index: 2; /* frame + island sit over the screenshot */
  width: 100%;
  height: 100%;
  pointer-events: none;
  filter: drop-shadow(0 24px 38px rgb(0 0 0 / 0.5));
}
.hero-phone-screen {
  position: absolute; /* aligned to the frame's transparent cutout */
  inset: 6% 4% 0;
  z-index: 1;
  overflow: hidden;
  background: var(--hero-screen-fill);
}
.hero-phone-shot {
  position: absolute;
  top: 0; /* frame supplies the 9:41 status bar */
  left: 0;
  width: 100%;
  height: auto; /* JS reads offsetHeight to size the travel */
  display: block;
  will-change: transform;
}

/* --- Experience ----------------------------------------------------------- */
.exp-item {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2px;
  padding-block: var(--space-md);
  border-bottom: 1px solid var(--rule);
  &:last-child {
    border-bottom: none;
  }
}
@media (min-width: 30em) {
  .exp-item {
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: var(--space-sm);
  }
}
.exp-role {
  font-size: var(--text-lg);
  font-weight: 400;
}
.exp-company {
  font-size: var(--text-md);
  color: var(--ink-secondary);
}
.exp-location {
  color: var(--ink-tertiary);
}
.exp-dates {
  font-size: var(--text-md);
  color: var(--ink-tertiary);
  white-space: nowrap;
}

/* --- Contact -------------------------------------------------------------- */
.contact {
  --section-rule: none;
  padding-bottom: var(--space-2xl);
}
.contact-links {
  display: flex;
  gap: var(--space-lg);
  flex-wrap: wrap;
  align-items: center;
  margin-top: var(--space-md);
}

/* ==========================================================================
   5b. COMPONENTS — CASE STUDY
   ========================================================================== */
body.case-study {
  --footer-pad: var(--space-block);
}

/* Prose defaults for case-study body copy.
   These are wrapped in :where() so they carry specificity 0. Without it,
   `body.case-study .content p` scores (0,2,2) and silently beats every
   component class on a <p> inside <article class="content"> — .stat-number,
   .stat-label, .feature-title, .feature-desc, .team-role, .sprint-step-label
   and .eyebrow were all losing to it. Prose is the default; components win. */
:where(body.case-study) h3 {
  font-size: var(--text-md);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: var(--space-sm);
  margin-top: var(--space-block-lg);
  &:first-child {
    margin-top: 0;
  }
}
:where(body.case-study .content) p {
  font-size: var(--text-lg);
  font-weight: 300;
  color: var(--ink-secondary);
  line-height: 2;
  margin-bottom: var(--space-sm);
  text-wrap: pretty;
  &:last-child {
    margin-bottom: 0;
  }
  & strong {
    color: var(--ink);
    font-weight: 500;
  }
}
.nav-back {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-md);
  color: var(--ink-tertiary);
  margin-bottom: 2.8125rem;
  transition: color var(--ease);
  &::before {
    content: "\2190";
  }
  &:hover {
    color: var(--papaya-text);
  }
}
.hero {
  padding-block: var(--space-xl) var(--space-section);
  border-bottom: 1px solid var(--rule);
}
.hero-summary {
  font-size: var(--text-lg);
  color: var(--ink-secondary);
  line-height: 2;
  margin-bottom: var(--space-block-lg);
  & a {
    color: var(--papaya-text);
  }
}
.meta-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-block);
  padding-top: var(--space-block);
  border-top: 1px solid var(--rule);
}
.meta-value {
  font-size: var(--text-md);
}

/* Hero image — per-case colours come from the THEMES section. */
.hero-image {
  aspect-ratio: 16 / 10;
  background: var(--case-hero-bg, var(--surface));
  border: var(--case-hero-border, none);
  border-radius: var(--radius-md);
  margin-block: var(--space-lg);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  & a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
  }
  & img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}
.hero-image-placeholder {
  font-family: var(--font-serif);
  font-size: 2.25rem;
  font-weight: 300;
  font-style: italic;
  color: var(--case-hero-placeholder, var(--ink-tertiary));
}
.content {
  padding-block: var(--space-section);
}
.content-section {
  margin-bottom: var(--space-section);
  &:last-child {
    margin-bottom: 0;
  }
}

/* --- Stats (card-grid) ---------------------------------------------------- */
.stats {
  --card-pad: 24px var(--space-sm);
  --card-min: 160px;
  margin-block: var(--space-block-lg);
}
.stat-number {
  font-family: var(--font-serif);
  font-size: var(--stat-size, 2.8125rem);
  font-weight: 300;
  color: var(--papaya-text);
  line-height: 1;
  margin-bottom: var(--space-md);
  font-variant-numeric: lining-nums;
}
.stat-label {
  font-size: var(--text-sm);
  color: var(--ink-tertiary);
  line-height: 1.4;
}

/* --- Pullquote ------------------------------------------------------------ */
.pullquote {
  border-inline-start: 3px solid var(--papaya);
  padding: 4px 0 4px var(--space-md);
  margin-block: var(--space-block-lg);
  & p {
    font-size: 1.4625rem;
    color: var(--ink);
    line-height: 1.5;
  }
}

/* --- Images --------------------------------------------------------------- */
.img-placeholder {
  aspect-ratio: 16 / 9;
  background: var(--img-bg, var(--surface));
  border-radius: 6px;
  margin-block: var(--space-block);
  display: flex;
  align-items: center;
  justify-content: center;
  &.dark {
    --img-bg: #0d0d0d;
  }
  &.blue {
    --img-bg: var(--surface-blue);
  }
  &.purple {
    --img-bg: #eee8f5;
  }
  &.red {
    --img-bg: #f5f0f0;
  }
  & span {
    font-size: var(--text-2xs);
    color: var(--ink-tertiary);
    letter-spacing: 0.05em;
  }
  &.dark span {
    color: rgb(255 255 255 / 0.35);
  }
}
.img-row {
  margin-block: var(--space-block);
  & .img-placeholder {
    margin-block: 0;
  }
}

/* --- Next project --------------------------------------------------------- */
.next-project {
  border-top: 1px solid var(--rule);
  padding-block: var(--space-section);
}

/* --- Floating buttons -----------------------------------------------------
   Hidden state is opacity + visibility; the markup ships `inert` and the page
   script toggles it, so these are never reachable while invisible. */
.back-to-top,
.back-home {
  position: fixed;
  bottom: var(--space-md);
  z-index: 100;
  width: 44px;
  height: 44px;
  background: var(--papaya-text);
  color: var(--white);
  border-radius: var(--radius-md);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(8px);
  transition:
    opacity 0.2s ease,
    transform 0.2s ease,
    background var(--ease);
  &.visible {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: none;
  }
  &:hover {
    background: var(--papaya-text-hover);
  }
  &:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 3px;
  }
  & svg {
    width: 18px;
    height: 18px;
    display: block;
  }
  &[data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 8px);
    right: 0;
    padding: 6px 8px;
    background: var(--ink);
    color: var(--white);
    font-size: var(--text-2xs);
    font-weight: 400;
    line-height: 1;
    white-space: nowrap;
    border-radius: var(--radius-sm);
    opacity: 0;
    pointer-events: none;
    transform: translateY(4px);
    transition:
      opacity var(--ease),
      transform var(--ease);
  }
  &:hover[data-tooltip]::after,
  &:focus-visible[data-tooltip]::after {
    opacity: 1;
    transform: none;
  }
}
.back-to-top {
  right: var(--space-md);
}
.back-home {
  right: calc(var(--space-md) + 56px);
}

/* --- Comparison (card-grid) ----------------------------------------------- */
.comparison {
  --card-pad: 28px var(--space-md);
  margin-block: var(--space-block-lg);
}
.comparison-side {
  & ul {
    list-style: none; /* this list carries no class, so the base reset misses it */
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  & li {
    position: relative;
    padding-left: var(--space-sm);
    font-size: var(--text-sm);
    color: var(--ink-secondary);
    line-height: 1.5;
    &::before {
      content: var(--marker, "\2013");
      position: absolute;
      left: 0;
      color: var(--marker-color, var(--ink-tertiary));
    }
  }
  &.after li {
    --marker: "\2192";
    --marker-color: var(--papaya-text);
  }
}

/* --- Team structure ------------------------------------------------------- */
.team-grid {
  margin-block: var(--space-block-lg);
}
.team-office {
  --panel-radius: 6px;
}
.team-office-header {
  background: var(--surface);
  padding: 12px var(--space-sm);
  border-bottom: 1px solid var(--rule);
}
.team-office-body {
  padding: var(--space-sm);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.team-role {
  font-size: var(--text-xs);
  color: var(--ink-secondary);
  & span {
    font-weight: 500;
    color: var(--papaya-text);
  }
}

/* --- Sprint flow (card-grid) ---------------------------------------------- */
.sprint-flow {
  --card-min: 120px;
  margin-block: var(--space-block);
}
.sprint-step {
  --stat-size: 2.025rem;
  text-align: center;
}
.sprint-step-label {
  font-size: var(--text-2xs);
  color: var(--ink-tertiary);
  line-height: 1.4;
}

/* --- Stack decisions table ------------------------------------------------ */
.decisions {
  margin-block: var(--space-block);
}
.decision-row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  border-bottom: 1px solid var(--rule);
  &:last-child {
    border-bottom: none;
  }
  /* Scoped to .decision-row: a bare `.header` rule would also hit this row's
     own class list. */
  &.header {
    background: var(--surface);
  }
}
.decision-cell {
  padding: 14px var(--space-sm);
  font-size: var(--text-xs);
  color: var(--ink-secondary);
  border-right: 1px solid var(--rule);
  line-height: 1.4;
  &:last-child {
    border-right: none;
  }
  & strong {
    color: var(--ink);
    font-weight: 500;
  }
}
/* .decision-cell sets font-size and colour at the same specificity as .eyebrow
   and comes later, so the header cells restate just those two. */
.decision-row.header .decision-cell {
  --eyebrow-tracking: 0.08em;
  font-size: var(--text-sm);
  color: var(--ink-tertiary);
}

/* --- Feature grid (card-grid) --------------------------------------------- */
.feature-grid {
  --card-pad: var(--space-md) 20px;
  margin-block: var(--space-block);
}
.feature-title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--ink);
  margin-bottom: 6px;
}
.feature-desc {
  font-size: var(--text-xs);
  color: var(--ink-tertiary);
  line-height: 1.5;
}

/* --- Scope grid (card-grid) ----------------------------------------------- */
.scope-grid {
  --card-min: 140px;
  margin-block: var(--space-block);
}
.scope-item {
  font-size: var(--text-xs);
  color: var(--ink-secondary);
  line-height: 1.4;
}

/* --- Code block ----------------------------------------------------------- */
.code-block {
  background: var(--ink);
  border-radius: 6px;
  padding: var(--space-md);
  margin-block: var(--space-block);
  overflow-x: auto;
  & pre {
    margin: 0;
  }
  & code {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: rgb(255 255 255 / 0.85);
    line-height: 1.6;
    white-space: pre;
  }
}
.code-comment {
  color: rgb(255 255 255 / 0.55);
} /* 0.35 composited to 3.2:1 */
.code-keyword {
  color: var(--papaya);
}
.code-string {
  color: #98d982;
}

/* ==========================================================================
   6. THEMES
   Per-project custom-property overrides. Adding a project means adding a
   block here — no new rules anywhere else.
   ========================================================================== */

/* Homepage work-card heroes */
.work-thumb--fraction {
  --hero-bg-image: url("../static/fraction-hero-bg.jpg");
  --hero-bg-fill: #0d0d0d;
}
.work-thumb--payconnect {
  --hero-bg-image: url("../static/payconnect-hero-bg.jpg");
  --hero-bg-fill: #ffffff;
  /* White card: the shared shadow reads far heavier here than on the dark
     cards, so it is pulled back to sit level with them by eye. */
  --thumb-shadow-ambient: 0.04;
  --thumb-shadow-key: 0.15;
}
.work-thumb--winga {
  --hero-bg-image: url("../static/winga-hero-bg.jpg");
  --hero-bg-fill: #030712;
}
.work-thumb--levelup {
  --hero-bg-image: url("../static/levelup-hero-bg.jpg");
  --hero-bg-fill: #4b0082;
}

/* Case-study pages */
body.cs-fraction {
  --case-hero-bg: #0d0d0d;
  --case-hero-placeholder: rgb(255 255 255 / 0.15);
}
body.cs-payconnect {
  --case-hero-bg: var(--surface-blue);
  --case-hero-placeholder: rgb(30 60 180 / 0.15);
}
body.cs-scb {
  --case-hero-bg: #eee8f5;
  --case-hero-placeholder: rgb(75 0 130 / 0.12);
  & .stats {
    --card-min: 140px;
  }
}
body.cs-winga {
  --case-hero-bg: var(--surface);
  --case-hero-border: 1px solid var(--rule);
  --case-hero-placeholder: rgb(0 0 0 / 0.1);
  & .img-placeholder {
    border: 1px solid var(--rule);
  }
}
body.cs-yoma {
  --case-hero-bg: #f5f0f0;
  --case-hero-placeholder: rgb(180 30 30 / 0.12);
}

/* ==========================================================================
   7. AT-RULES
   ========================================================================== */

/* Every motion in one place. `!important` on the parallax targets is the
   deliberate kill-switch for the transforms JS writes as inline styles. */
@media (prefers-reduced-motion: reduce) {
  .hero-bg,
  .hero-phone-shot {
    transform: none !important;
  }
  .work-item:hover .work-thumb {
    transform: none;
  }
  .back-to-top,
  .back-home {
    transition: opacity 0.01s linear;
    transform: none;
    &.visible {
      transform: none;
    }
    &[data-tooltip]::after {
      transition: opacity 0.01s linear;
      transform: none;
    }
  }
}
@media print {
  .work-thumb {
    background: #eee !important;
  }
  .back-to-top,
  .back-home,
  .skip-link {
    display: none;
  }
}
