/* ============================================================
   Ankita Menon — portfolio
   Riso coral: warm paper, a halftone field, one flat disc,
   Montserrat for headings, Karla for body, mono for data.
   ============================================================ */

:root {
  /* Surface — "Veil" direction. --paper/--card keep their names (every
     existing rule already reads them) but carry the new values, so this
     one block re-skins the whole site without touching each rule. */
  --bg:     #F5F5FC;
  --paper:  var(--bg);
  --panel:  rgba(255, 255, 255, .62);
  --card:   var(--panel);
  --card-edge: var(--line);

  /* Ink */
  --ink:    #292A4D;
  --ink-2:  #5B5D80;
  --body:   var(--ink-2);   /* paragraph copy */
  --mono-muted: var(--ink-2);

  /* Accent — one accent does every job (labels, links, marks); the old
     bright/text split doesn't exist in this palette. */
  --acc:        #5D5EB8;
  --accent:     var(--acc);
  --accent-ink: var(--acc);

  --line: rgba(41, 42, 77, .14);

  /* more_stuff_about_me.json — an inverted console, not a light-mode tint.
     Orange has to go very dark to pass contrast as text on a light ground,
     and dark orange on an orange field reads brown; on a dark ground the
     same warmth reads clean at full brightness instead. */
  --console:   #101A33;
  --code-bg:   var(--console);
  --code-edge: #1D2A4D;
  --code-key:  #7CC4FF;
  --code-str:  #FFB067;
  --code-body: #C9D4F0;

  /* Type */
  --display: "Montserrat", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Karla", "Helvetica Neue", Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, Menlo, monospace;

  /* Space */
  --gutter: clamp(24px, 5vw, 64px);
  --nav-h: 64px;
  --section-y: clamp(2.5rem, 5vw, 4.5rem);
  --maxw: 1320px;
  --radius: 16px;   /* one corner radius for the whole site */

  /* the case study label/body split — used by the facts list AND every
     section, so a value can never start on a different line than the
     Overview paragraph */
  --case-split: minmax(0, 3fr) minmax(0, 9fr);
  --case-split-gap: clamp(1.25rem, 3.4vw, 3.5rem);

  --ease: cubic-bezier(.22, .68, .32, 1);
  /* One inner padding for EVERY tile — glass cards, work cards, the JSON
     panel, Featured in, Contact. Boxes drifted to five different clamps. */
  --card-pad: clamp(22px, 2.4vw, 34px);
}

/* ------------------------------------------------------------ base */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }

body {
  margin: 0;
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.75;
  font-weight: 500;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  background: var(--paper);
}

/* ---- the background: paper, shapes, halftone on top ---- */
.bg {
  position: fixed; inset: 0; z-index: -1;
  pointer-events: none; overflow: hidden;
  background: var(--paper);
}
/* The halftone dot field is gone in the Veil direction — the ground is
   clean paper; all texture now comes from the WebGL veil where it runs. */
/* Three faces, each with one job: the serif for headings, Karla for
   everything else, and the mono only inside stuff.json. */
h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 700; letter-spacing: -.025em; line-height: 1.15; margin: 0;
}
p { margin: 0 0 1.25em; }
p:last-child { margin-bottom: 0; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }

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

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
  position: absolute; left: 50%; top: 0; transform: translate(-50%, -140%);
  z-index: 200; background: var(--ink); color: var(--paper);
  padding: .75rem 1.25rem; border-radius: 0 0 var(--radius) var(--radius);
}
.skip-link:focus { transform: translate(-50%, 0); }

.section__title { font-family: var(--display); font-weight: 700; font-size: clamp(1.5rem, 2.6vw, 2.125rem); letter-spacing: -.025em; color: var(--ink); }
.dot { color: var(--acc); }

/* ============================================================
   BACKGROUND CANVAS
   One shared WebGL context. On the home page it's a fixed,
   full-viewport veil behind everything. On a converted case study
   it's repositioned (by JS) to live only inside that page's end-nav.
   Hidden entirely — falls back to the flat --bg — if WebGL is
   unavailable or the visitor prefers reduced motion.
   ============================================================ */
#gl { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
/* main needs a stacking context above the canvas. The nav does NOT belong
   in this rule: header.nav outranks .nav in specificity, so putting
   position:relative here silently defeated the nav's position:fixed and
   sent the whole bar (progress hairline included) scrolling off-screen.
   The nav's own rule already carries position:fixed + z-index:100.
   Case articles live OUTSIDE <main> (they're hash-routed siblings), so
   they need the same lift — without it the opaque canvas paints over
   their content the moment the veil runs on a case page. */
main, article.case { position: relative; z-index: 1; }

/* ============================================================
   CURSOR
   An organic blob that follows the pointer, morphing slowly so it
   reads as part of the veil rather than a sharp system cursor.
   Only on devices with a real pointer; the native cursor stays
   under reduced motion and on touch.
   ============================================================ */
@media (hover: hover) and (pointer: fine) {
  body.fine, body.fine a, body.fine button { cursor: none; }

  .cur {
    position: fixed; left: 0; top: 0; width: 26px; height: 26px;
    border: 1.5px solid var(--ink); pointer-events: none; z-index: 9999;
    transform: translate(-50%, -50%); display: grid; place-items: center;
    opacity: 0;
    transition: width .28s cubic-bezier(.2,.7,.2,1), height .28s cubic-bezier(.2,.7,.2,1),
                background .28s, border-color .28s, opacity .3s;
  }
  /* Two offset chromatic rings — faint magenta and cyan echoes of the main
     outline, one nudged right and one left, so the cursor reads as a lens
     with slight colour fringing. They ride along in every state, including
     the enlarged View/More discs. */
  .cur::before, .cur::after {
    content: ""; position: absolute; inset: -2px;
    border: 1px solid rgba(255, 60, 150, .55);
    transform: translate(1px, 0);
  }
  .cur::after { border-color: rgba(60, 200, 255, .55); transform: translate(-1px, 0); }
  .cur.on { opacity: 1; }
  .cur span {
    font-family: var(--mono); font-size: .6875rem; font-weight: 600;
    letter-spacing: .14em; text-transform: uppercase; color: var(--ink);
    opacity: 0; transition: opacity .2s;
  }
  .cur.view, .cur.link {
    background: rgba(255, 255, 255, .55); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    border-color: rgba(255, 255, 255, .8);
  }
  .cur.view { width: 88px; height: 88px; }
  .cur.link { width: 64px; height: 64px; }
  .cur.view span, .cur.link span { opacity: 1; transition-delay: .12s; }
  /* over a carousel arrow: the cursor snaps to a clean outline circle
     sized (from JS) to hug that exact button. Outline ONLY — any fill or
     backdrop blur here sits on top of the button and hides the arrow it's
     meant to be framing. The button supplies its own white + arrow; the
     cursor just draws the ring around it. Morph suspended because a
     wobbling outline can't "fit" a round button. */
  .cur.ring { background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; border-color: var(--ink); }
  .cur.ring, .cur.ring::before, .cur.ring::after { border-radius: 50%; animation: none; }
  .cur.ring span { opacity: 0; }
  /* the blob morph, shared by ring and echoes with staggered phase */
  .cur, .cur::before, .cur::after {
    border-radius: 58% 42% 55% 45% / 45% 58% 42% 55%;
    animation: cur-morph 7s ease-in-out infinite;
  }
  .cur::before { animation-delay: -2.3s; }
  .cur::after { animation-delay: -4.6s; }
  @keyframes cur-morph {
    0%, 100% { border-radius: 58% 42% 55% 45% / 45% 58% 42% 55%; }
    33% { border-radius: 45% 55% 40% 60% / 60% 40% 58% 42%; }
    66% { border-radius: 52% 48% 62% 38% / 40% 55% 45% 60%; }
  }
}
@media (pointer: coarse) { .cur { display: none; } }

/* A trailing cursor is motion; the native one stays under reduced motion. */
@media (prefers-reduced-motion: reduce) {
  body.fine, body.fine a, body.fine button { cursor: auto; }
  .cur { display: none !important; }
}

/* ------------------------------------------------------------- nav */
/* Veil direction: gradient scrim instead of a hard blurred bar, plus a
   scroll-progress hairline. Deliberately KEPT position:fixed rather than
   the reference's position:sticky — sticky removes the nav from flow
   compensation, which means every page's top padding (hero, case-study
   headers) would need reworking at the same time. Fixed + the existing
   compensating padding still delivers the same visual, and the other five
   case studies aren't converted yet — revisit when they are. */
.nav {
  position: fixed; inset: 0 0 auto 0; z-index: 100;
  padding-inline: var(--gutter);
  /* liquid glass: a translucent gradient that lets the veil seep through,
     with a backdrop blur so whatever passes underneath goes frosted
     rather than fighting the nav links for legibility */
  background: linear-gradient(to bottom,
    rgba(245, 245, 252, .88) 0%,
    rgba(245, 245, 252, .55) 65%,
    rgba(245, 245, 252, .12) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
}
.nav__inner {
  max-width: var(--maxw); margin-inline: auto;
  height: var(--nav-h);
  display: flex; align-items: center; gap: clamp(1rem, 2.4vw, 2rem);
  font-family: var(--font-body); font-weight: 500; font-size: .875rem;
  position: relative;
}
.nav__logo { font-family: var(--display); font-weight: 700; font-size: 1.125rem; letter-spacing: -.02em; line-height: 1; }
.nav__links { margin-left: auto; display: flex; gap: clamp(1.25rem, 2.6vw, 2.5rem); }
.nav__links a {
  font-size: .875rem; font-weight: 500; letter-spacing: 0;
  text-transform: none; color: var(--ink); opacity: .8;
  position: relative; padding-block: .25rem; transition: opacity .3s var(--ease);
}
.nav__links a::after {
  content: ""; position: absolute; left: 0; right: 100%; bottom: -6px; height: 1px;
  background: currentColor;
  transition: right .3s cubic-bezier(.2,.7,.2,1);
}
.nav__links a:hover { opacity: 1; }
.nav__links a:hover::after { right: 0; }
.nav__prog {
  position: absolute; left: calc(-1 * var(--gutter)); right: calc(-1 * var(--gutter)); bottom: 0;
  height: 2px; background: var(--line);
}
.nav__prog i { display: block; height: 100%; width: 0; background: var(--acc); }

/* ------------------------------------------------------------ hero */
/* ---- hero ----
   The old three-square SVG module is gone; its content survives as the
   callouts list (lead phrases) and the vertical rails (the sr-only years-
   of-experience / open-for-roles text, now actually visible). */
.hero {
  position: relative; margin-top: var(--nav-h);
  /* Full first viewport: the case study grid stays below the fold and the
     scroll cue does the inviting. Content centres vertically, with a
     slightly heavier bottom pad so it sits a touch above optical centre. */
  min-height: calc(100svh - var(--nav-h));
  display: flex; flex-direction: column; justify-content: center;
  /* heavy bottom pad lifts the block above optical centre */
  padding: clamp(2.75rem, 6vh, 5rem) var(--gutter) clamp(6rem, 15vh, 9rem);
}
/* Narrower than --maxw so the hero sits clear of the side rails. The
   second column's left edge stays put anyway: with 1fr/1fr + 56px gap on
   a centred container, col 2 always starts at page-centre + 28px, which
   is exactly where the second work card starts too. */
.hero__inner { max-width: 1120px; margin-inline: auto; width: 100%; }
.hero__grid {
  /* IDENTICAL column definition to .wgrid (1fr 1fr, 56px column gap) so
     "My designs" starts on exactly the same vertical line as the second
     case study card below it. Change one, change both. */
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 56px; align-items: start;
}
.hero__txt, .hero__form { display: grid; grid-template-rows: 96px 44px auto; gap: 22px 0; align-content: start; }
.hero__name { display: contents; }
.hero__name picture { grid-row: 1; }
.hero__name img {
  width: 96px; height: 96px; border-radius: 50%; object-fit: cover;
  border: 1px solid var(--line); display: block;
}
.hero__mark {
  grid-row: 2; align-self: end;
  font-family: var(--display); font-weight: 700;
  font-size: clamp(1.5rem, 2.6vw, 2.25rem);
  letter-spacing: -.02em; line-height: 1.1; margin: 0;
}
.hero__lead {
  grid-row: 3; max-width: 44ch; margin: 0;
  font-size: 1rem; line-height: 1.7; color: var(--ink-2); font-weight: 400;
}
/* "My designs" as a quiet mono eyebrow (chosen layout A) — an annotation
   beside the intro, not a second heading competing with the name. It sits
   in the same 44px baseline row the h1 uses, so the two still align. */
.mine {
  grid-row: 2; align-self: end;
  font-family: var(--mono); font-weight: 600;
  font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase;
  color: var(--acc); margin: 0;
}
/* Tight enough to read as one connected paragraph rather than three
   separate blocks, and wide enough that the longest line ("Adopted by
   10 million+ devices across 100+ countries") never wraps — a widow on a
   three-item list reads as broken, not just untidy. */
.callouts { grid-row: 3; list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; max-width: 34rem; align-self: start; }
.callouts li { position: relative; padding-left: 20px; font-size: .9375rem; line-height: 1.5; color: var(--ink); white-space: nowrap; }
.callouts .more > p { white-space: normal; }
.callouts li::before {
  content: ""; position: absolute; left: 0; top: 10px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--acc); opacity: .55;
  transition: transform .35s cubic-bezier(.2,.7,.2,1), opacity .3s;
}
.callouts li:hover::before { transform: scale(1.7); opacity: 1; }
.callouts li b { font-family: var(--display); font-weight: 700; font-size: inherit; }
/* pointer, not none: the body.fine scope hides the native cursor when the
   custom one is running; hardcoding none here left reduced-motion users
   with no cursor at all over these buttons */
.callouts button { all: unset; display: block; width: 100%; cursor: pointer; }
.callouts .line { display: block; transition: transform .35s cubic-bezier(.2,.7,.2,1); }
.callouts .line b { transition: color .25s; }
.callouts button:hover .line { transform: translateX(8px); }
.callouts button:hover .line b { color: var(--acc); }
.callouts button:focus-visible { outline: 2px solid var(--acc); outline-offset: 6px; border-radius: 6px; }
.callouts .more { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .45s cubic-bezier(.2,.7,.2,1); }
.callouts .more > p {
  overflow: hidden; margin: 0; color: var(--ink-2); font-size: .875rem; line-height: 1.55;
  opacity: 0; transform: translateY(6px);
  transition: opacity .35s ease, transform .45s cubic-bezier(.2,.7,.2,1);
}
.callouts li.open .more { grid-template-rows: 1fr; }
.callouts li.open .more > p { opacity: 1; transform: none; transition-delay: .12s; }

/* rails — fixed in the gutter, hidden below 900px, home page only */
.rail {
  position: fixed; top: 0; bottom: 0; width: var(--gutter); z-index: 4;
  /* was display:grid;place-items:center — a single grid cell centers every
     child independently, so the dot and the text were stacking on top of
     each other instead of sitting next to each other. flex-column stacks
     them in sequence, close together via the dot's own margin. */
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  pointer-events: none;
  font-family: var(--mono); font-size: .6875rem; font-weight: 500; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-2);
  opacity: var(--railo, 1); transition: opacity .5s;
}
.rail-l { left: 0; } .rail-r { right: 0; }
body.is-case .rail { display: none; }
.rail span { white-space: nowrap; }
.rail-l span { writing-mode: vertical-rl; transform: rotate(180deg); }
.rail-r span { writing-mode: vertical-rl; }
.rail-r i {
  display: block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--acc); margin-bottom: 10px; animation: rail-pulse 2.4s ease infinite;
}
@keyframes rail-pulse { 50% { opacity: .35; } }
@media (max-width: 900px) { .rail { display: none; } }
@media (prefers-reduced-motion: reduce) { .rail-r i { animation: none; } }

/* Fixed to the viewport, not absolute within .hero — the hero is content-
   sized now (see the padding fix above), not forced to fill the screen, so
   anchoring to ITS bottom edge would put this wherever the content happens
   to end rather than at the bottom of the screen where it's meant to read
   as "there's more below." Hidden entirely once a case study is open. */
.scrollcue {
  position: fixed; bottom: 26px; z-index: 4; opacity: 1;
  display: flex; align-items: center; gap: 10px; background: none; border: 0; cursor: pointer;
  color: var(--ink-2); font-family: var(--mono); font-size: .6875rem; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; padding: 0;
  transition: color .25s, opacity .4s;
}
.scrollcue--l { left: var(--gutter); }
.scrollcue--r { right: var(--gutter); flex-direction: row-reverse; }
.scrollcue:hover { color: var(--acc); }
.scrollcue svg { display: block; }
body.is-case .scrollcue { display: none; }
@media (max-width: 767px) { .scrollcue--r { display: none; } }

/* -------------------------------------------------------- sections */
.section { padding: var(--section-y) var(--gutter); position: relative; }
.work, .about, .contact { scroll-margin-top: var(--nav-h); }

.section__head {
  max-width: var(--maxw); margin: 0 auto clamp(1.5rem, 3vw, 2.25rem);
  display: flex; align-items: center; gap: 1.5rem;
}

/* -------------------------------------------------------- work grid */
.wgrid {
  list-style: none; margin: 0 auto; padding: 0; max-width: var(--maxw);
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 72px 56px;
}

/* Card reads top-down: title and destination first, then the work.
   The media bleeds to the card's own bottom edge rather than floating
   inside it, so the image feels like part of the panel. Glassy at rest —
   this is the frosted-panel look, not just a hover state — and it stands
   up into place as it scrolls into view (transform only; opacity is
   reserved for the hover de-emphasis below, never mixed with scroll-in). */
.wcard {
  --pad: var(--card-pad);
  display: flex; flex-direction: column;
  background: rgba(255, 255, 255, .78);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  border: 1px solid var(--card-edge);
  border-radius: var(--radius);
  padding: var(--pad);
  overflow: hidden;
  transform: translateY(var(--ty, 18px));
  transition: opacity .35s ease, transform .6s cubic-bezier(.2,.7,.2,1);
}

/* Hovering one card pulls the others back so attention lands in one
   place — a soft de-emphasis, not a wash-out. Keyboard focus gets the
   same treatment. */
@media (hover: hover) {
  .wgrid li { transition: opacity .35s var(--ease); }
  .wgrid:has(.wcard:hover) li:not(:has(.wcard:hover)),
  .wgrid:has(.wcard:focus-visible) li:not(:has(.wcard:focus-visible)) { opacity: .55; }
}
@media (prefers-reduced-motion: reduce) { .wcard { transform: none; } }

/* one to three tags, then the title. The descriptor moved into the hover
   panel so the image can take the space it used to occupy. */
.wcard__head { display: grid; gap: .6rem; }
.wcard__tags {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .55rem;
}
/* plain labels separated by a dot — a pill reads as tappable, and these
   are not */
.wcard__tags li {
  display: inline-flex; align-items: center; gap: .55rem;
  font-size: .75rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent-ink);
}
.wcard__tags li:not(:last-child)::after {
  content: ""; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent);
}
.wcard__title {
  font-family: var(--display); font-weight: 700;
  font-size: clamp(1.25rem, 1.9vw, 1.625rem);
  letter-spacing: -.022em; color: var(--ink); margin: 0;
}

/* hairline under the head, then the media running full bleed to the
   card's own edges — the card's overflow:hidden clips it to the radius */
.wcard__media {
  position: relative; isolation: isolate;
  margin: var(--pad) calc(var(--pad) * -1) calc(var(--pad) * -1);
  /* Every card teaser is sourced from its case study's own first slide,
     and all six happen to share this ratio (see build brief) — matching
     the frame to it means object-fit has nothing to crop. */
  aspect-ratio: 1600 / 1039;
  background: linear-gradient(150deg, #E9ECF3 0%, #D6DCEA 100%);
  display: grid; place-items: center;
  /* no overflow:hidden here — .wcard__reveal is nested inside this box and
     deliberately extends above its top edge (see .wcard__reveal) to sit
     flush with the title; this box clipping its own overflow was cutting
     that part of the panel off. The image itself never overflows (its
     ratio always matches this box exactly), and .wcard's own
     overflow:hidden still clips everything to the card's rounded corners. */
}
.wcard__media picture { display: contents; }
.wcard__media img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }
.wcard__ph {
  display: grid; justify-items: center; gap: .55rem;
  font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink);
}
.wcard__ph svg { width: 28px; fill: none; stroke: var(--ink); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }

.wcard__reveal {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  /* .wcard__media carries its own top margin (var(--pad)) to space it from
     the head above. Pulling the panel up by that same amount puts its top
     edge flush with the bottom of the title instead of doubling the gap. */
  top: calc(-1 * var(--pad));
  display: flex; flex-direction: column; justify-content: flex-start;
  /* the media bleeds to the card edge, so the panel pads back by --pad to
     put this copy on the same left margin as the title above it */
  padding: 1.1rem var(--pad) var(--pad);
  /* Frosted, not solid — rgba(255,255,255,.9) with an 8px blur reads as
     glass over the image rather than a flat card. This is safe against the
     bleed-through bug from the solid-white version's history: the panel
     shows/hides with visibility (an instant snap, never a fade), so at any
     moment it's either fully absent or already at its final, static alpha
     — there's no in-between low-opacity frame for the image to leak
     through. Only the text inside it (below) fades in on top. */
  background: rgba(255, 255, 255, .9);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  visibility: hidden;
}
.wcard:hover .wcard__reveal, .wcard:focus-visible .wcard__reveal { visibility: visible; }
.wcard__lede {
  margin: 0; max-width: 44ch; font-weight: 600;
  /* The ledes are all different lengths, so no single measure keeps every one
     of them off a one-word last line — widening one card stranded a word on
     another. text-wrap: balance evens the lines out per card instead. */
  text-wrap: balance;
  font-size: clamp(1rem, 1.2vw, 1.125rem); line-height: 1.4; color: var(--ink);
  opacity: 0; transition: opacity .3s var(--ease);
}
.wcard__meta {
  margin: .9rem 0 0; padding-top: .9rem;
  border-top: 1px solid var(--line);
  display: grid; gap: .65rem;
  opacity: 0; transition: opacity .3s var(--ease);
}
.wcard:hover .wcard__lede, .wcard:focus-visible .wcard__lede,
.wcard:hover .wcard__meta, .wcard:focus-visible .wcard__meta { opacity: 1; }
.wcard__meta dt {
  font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-ink); margin-bottom: .2rem;
}
.wcard__meta dd {
  margin: 0; max-width: 44ch;
  font-size: .8125rem; line-height: 1.5; color: var(--body);
}

.wcard:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 4px; }

/* closing line under the grid */
.work__note {
  max-width: var(--maxw); margin: clamp(1.5rem, 2.6vw, 2.25rem) auto 0;
  padding-bottom: clamp(1.5rem, 2.6vw, 2.25rem);
  border-bottom: 1px solid var(--line);
  font-family: var(--display); font-weight: 700; letter-spacing: -.02em;
  font-size: clamp(1rem, 1.3vw, 1.1875rem); color: var(--accent-ink);
}

/* -------------------------------------------------- panels & cards */
.contact__card { max-width: var(--maxw); margin-inline: auto; }

/* ------------------------------------------------------------ about */
/* About sits directly on the paper — no panel behind it. Two equal halves
   with the same gap as the work grid, so both sections share one rhythm and
   the outer edges line up. */
.about__grid {
  max-width: var(--maxw); margin-inline: auto;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.6vw, 2.25rem);
  /* stretch is deliberate here: it makes the portrait take the row height,
     so it ends exactly where the last Featured In button ends */
  align-items: stretch;
}
.portrait { position: relative; margin: 0; height: 100%; }
.portrait__frame {
  height: 100%;
  border-radius: var(--radius); overflow: hidden;
  background: linear-gradient(155deg, #CBD4EA 0%, #94A6D2 100%);
  display: grid; place-items: center;
}
.portrait__frame picture { display: contents; }
.portrait__frame img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }

.about__copy { min-width: 0; }

/* ---- stuff.json ---- */
.jbox {
  margin-top: 0;
  width: 100%;
  background: var(--code-bg);
  border: 1px solid var(--code-edge);
  border-radius: var(--radius);
  padding: .95rem 1.1rem;
}
.jbox__hd {
  display: flex; align-items: center; gap: .5rem; margin: 0 0 .7rem;
  font-family: var(--mono); font-size: .75rem; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase; color: var(--code-key);
}
.jbox.washed::before { background: var(--acc); opacity: .16; }
.jbox__hd::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--code-str); }
.jcode {
  margin: 0; font-family: var(--mono);
  font-size: clamp(.625rem, .78vw, .75rem); line-height: 1.55;
  color: var(--code-body); white-space: pre; overflow-x: auto;
}
.jcode .k { color: var(--code-key); }
.jcode .s { color: var(--code-str); }
.jcode .c { color: #8FA0C8; font-style: italic; opacity: .9; }
.jcode .p { color: rgba(201, 212, 240, .45); }
/* the type-in caret (see the console type-in block in main.js) */
.jcaret {
  display: inline-block; width: 8px; height: 1.1em;
  background: var(--code-str); vertical-align: -2px;
  animation: jblink 1s steps(1) infinite;
}
@keyframes jblink { 50% { opacity: 0; } }

/* ---- featured in ---- */
.press { margin-top: clamp(1.25rem, 2.2vw, 1.75rem); }
.press__label {
  font-family: var(--font-body); font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-ink); margin: 0 0 .7rem;
}
/* A ruled list now, not stacked buttons — see .linkrow below. No gap: the
   hairline between two rows belongs to both of them at once, so a gap
   would leave it floating, touching neither. */
.linkstack {
  list-style: none; margin: 0; padding: 0;
  border-top: 1px solid var(--line);
}
.linkrow__label { flex: 0 1 auto; transition: color .25s; }
/* Filled areas share the hero's marker treatment: the fill sits on a
   pseudo-element so the displacement filter roughens the colour without
   touching the text, and the border stays a crisp drawn line. */
/* --fill drives a diagonal mask so the colour strokes itself in the way the
   hero squares do. Registered so it can be transitioned at all — an
   unregistered custom property jumps rather than animates. initial-value is
   1, so if the script never runs the colour is simply there. */
@property --fill {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

.washed { position: relative; isolation: isolate; }
.washed::before {
  content: ""; position: absolute; inset: 1px; z-index: -1;
  background: var(--accent); opacity: .16;
  /* no filter: the roughened-edge SVG filter (#hmEdge) left with the old
     hero module, and a broken filter reference stops Chrome painting the
     element at all — a flat fill matches the Veil direction anyway */
  border-radius: inherit;
  -webkit-mask-image: linear-gradient(-22deg,
    #000 calc(var(--fill) * 132%), transparent calc(var(--fill) * 132%));
          mask-image: linear-gradient(-22deg,
    #000 calc(var(--fill) * 132%), transparent calc(var(--fill) * 132%));
}
/* held empty until it scrolls into view */
.washed.wash-wait { --fill: 0; }
/* steps() is what turns a sweep into six separate passes */
/* Four passes over half a second. The hero can afford six slow ones because
   it animates SVG transforms, which stay on the compositor. These are HTML
   boxes animating a mask over a filtered background, so every step forces a
   repaint — the longer it runs, the more visible that becomes. */
.washed.wash-go { --fill: 1; transition: --fill .5s steps(4, end); }
/* only interactive elements react to hover — a static panel that lights up
   reads as a link and isn't one */
a.washed:hover::before, button.washed:hover::before { opacity: .26; }

/* Ruled list rows, not bordered pills — replaces the old cobalt-bordered
   box (its border color was a hardcoded cobalt rgba that clashed outright
   with the new palette). Featured-in and Contact share this base and
   differ only in weight/size below; the .washed marker-fill this class
   used to carry is switched off for both — a diagonal accent wash behind
   a borderless row read as a stray smear with nothing to contain it. */
.linkrow.washed::before { content: none; }
.linkrow {
  display: flex; align-items: center; gap: 1rem;
  padding: .85rem 0;
  border-bottom: 1px solid var(--line);
  font-weight: 500; color: var(--ink);
  transition: padding-left .3s cubic-bezier(.2, .7, .2, 1);
}
.linkrow:hover { padding-left: 10px; }
.linkrow__go {
  width: 15px; height: 15px; margin-left: auto; flex: 0 0 auto;
  fill: none; stroke: var(--acc); stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform .3s cubic-bezier(.2, .7, .2, 1);
}
.linkrow:hover .linkrow__go { transform: translateX(6px); }
.linkrow:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; border-radius: 4px; }

.press .linkrow { font-size: .9375rem; padding: .5rem 0; }
.press .linkstack .linkrow:hover { padding-left: 8px; }
.contact .linkrow {
  font-family: var(--display); font-weight: 600; font-size: 1.125rem;
  padding: 1rem 0;
}
/* The size rules above set padding with a shorthand, which — being later
   in the file at equal specificity — was silently zeroing the hover
   indent's padding-left. Three class-level selectors outrank both. */
.linkstack .linkrow:hover { padding-left: 12px; }
.linkstack .linkrow:hover .linkrow__label { color: var(--acc); }

/* ---------------------------------------------------------- contact */
.about { padding-bottom: 0; }
.about__rule {
  max-width: var(--maxw); margin: var(--section-y) auto 0;
  height: 1px; background: var(--line);
}
.contact__inner { max-width: 46rem; }
.contact__grid .contact__inner { max-width: none; }
.contact__title { font-family: var(--display); font-weight: 700; font-size: clamp(1.7rem, 3vw, 2.375rem); letter-spacing: -.028em; margin: 0 0 clamp(1.25rem, 2.4vw, 1.75rem); }
.contact__lead { max-width: 44ch; color: var(--body); font-size: clamp(1rem, 1.3vw, 1.1875rem); text-wrap: pretty; }
.contact__lead + .contact__lead { margin-top: 1rem; }

/* two halves on the same grid as Work and About */
.contact__grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.6vw, 2.25rem); align-items: start;
}

/* ----------------------------------------------------------- footer */
.footer { padding: 1.6rem var(--gutter); border-top: 1px solid var(--line); }
.footer__inner { max-width: var(--maxw); margin-inline: auto; display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.footer__inner p { margin: 0 0 0 auto; font-size: .875rem; color: var(--mono-muted); }

/* --------------------------------------------------------- reveals */
.reveal { opacity: 0; transform: translateY(18px); }
.reveal.is-in { opacity: 1; transform: none; transition: opacity .8s var(--ease), transform .8s var(--ease); }

/* ============================================================
   CASE STUDY VIEW
   ============================================================ */
.case {
  --case-max: 1100px;
  padding: calc(var(--nav-h) + clamp(2rem, 5vw, 3.5rem)) var(--gutter) var(--section-y);
}
.case[hidden] { display: none; }

.case-hero { max-width: var(--case-max); margin: 0 auto clamp(2rem, 4vw, 3.5rem); }
.case-title { font-family: var(--display); font-weight: 700; font-size: clamp(1.8rem, 3.3vw, 2.625rem); letter-spacing: -.028em; margin-bottom: 1.15rem; }
.case-title:focus { outline: none; }
.case-title:focus-visible { outline: 2px solid var(--accent-ink); outline-offset: 6px; }

/* Title, then the facts, then the rule. The value column is capped so the
   skills line breaks at a readable length rather than running the full width
   of the page. */
.case-facts {
  margin: clamp(1.5rem, 2.8vw, 2rem) 0 0;
  display: grid; gap: .75rem;
  padding-bottom: clamp(1.5rem, 2.8vw, 2.25rem);
  border-bottom: 1px solid var(--line);
}
.case-facts > div {
  display: grid; grid-template-columns: var(--case-split);
  gap: var(--case-split-gap); align-items: baseline;
}
.case-facts dt {
  font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-ink);
}
.case-facts dd {
  margin: 0; max-width: 54ch;
  font-size: .9375rem; line-height: 1.55; color: var(--ink);
}

.case-car { position: relative; }
.case-car__viewport { position: relative; overflow: hidden; border-radius: var(--radius); }
/* Icon only — no chip behind it. A drop-shadow (not a solid background)
   keeps it legible over both light and dark parts of a photo. */
.case-car__expand {
  position: absolute; top: 1.5rem; right: 1rem; z-index: 3;
  width: 22px; aspect-ratio: 1;
  border: 0; background: none; padding: 0; cursor: pointer;
}
.case-car__expand svg {
  width: 100%; fill: none; stroke: var(--ink); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
  /* A soft light halo, not a solid chip — keeps the (dark, same as every
     other icon on the site) stroke readable over a busy photo without
     boxing it in. --fit carousels letterbox to a near-white background,
     which is exactly why this can't just be a light stroke instead. */
  filter: drop-shadow(0 0 3px rgba(255, 255, 255, .9)) drop-shadow(0 1px 1px rgba(255, 255, 255, .9));
}
.case-car__expand:hover svg { stroke: var(--accent-ink); }
.case-car__track { list-style: none; margin: 0; padding: 0; display: flex; transition: transform .55s var(--ease); }
.case-car__slide { flex: 0 0 100%; }
.case-car__slide .case-fig__frame { aspect-ratio: 21 / 9; border-radius: 0; }
/* .case-car--fit: real screenshots that shouldn't be cropped to the wide
   21/9 strip above. Frame ratio comes from --fit-ratio, set inline per
   carousel to its own first slide's real dimensions (falls back to
   showcase-1-1.jpg's ratio if a --fit carousel doesn't set one) — object-fit:
   contain then means nothing is ever cut off, whatever the frame ratio;
   a different-ratio slide in the same carousel just letterboxes more. */
.case-car--fit .case-fig__frame { aspect-ratio: var(--fit-ratio, 1600 / 1039); background: var(--paper); }
.case-car--fit .case-fig__frame img { object-fit: contain; }
/* Buttons flank the image left/right, in normal flow — not layered on
   top of it. On narrow viewports (where the case-sec label/body columns
   have already collapsed to one stacked column, see the 1023px query
   below) there's no room outside the image, so they sit just inside its
   edges instead, and the image gives up a little width for them. */
.case-car__row { position: relative; display: flex; align-items: center; gap: .6rem; }
.case-car__row .case-car__viewport { flex: 1 1 auto; min-width: 0; }
/* Same family as .case-paging__arrow — identical border, icon size and
   stroke, hover — sized 38px to the paging links' 44px. */
.car-btn {
  position: static; flex: 0 0 auto;
  width: 38px; aspect-ratio: 1;
  border: 1px solid var(--line); border-radius: 50%;
  background: rgba(255, 255, 255, .9);
  display: grid; place-items: center; cursor: pointer;
  transition: background-color .3s var(--ease), border-color .3s var(--ease), opacity .3s var(--ease);
}
.car-btn svg { width: 17px; fill: none; stroke: var(--ink); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.car-btn:hover { background: #FFF; border-color: var(--acc); }
/* first slide has no "previous", last has no "next" — the arrow leaves
   rather than looping around the back (set from JS) */
.car-btn.is-end { opacity: 0; pointer-events: none; }
.case-car__count { margin: .8rem 0 0; text-align: center; font-family: var(--font-body); font-size: .8125rem; font-weight: 600; color: var(--mono-muted); }

/* A single-image carousel has nothing to page through — the prev/next
   buttons and the "1 / 1" caption are dead controls, not navigation.
   .is-single is set in JS once the real slide count is known. */
.case-car.is-single .car-btn,
.case-car.is-single .case-car__count { display: none; }

/* Desktop: the case-sec label/body columns are side by side (see the
   1023px query below). The image spans the same width as the paragraph
   text; the buttons pull out into the margin beyond it rather than
   shrinking the image to make room. On the left, that margin IS
   --case-split-gap (the same fixed-width gutter between the sticky label
   column and the body column, --case-split-gap: clamp(1.25rem,3.4vw,3.5rem))
   — at the low end of that range (~35px at the 1024px breakpoint) the
   normal 40-50px button plus any margin doesn't fit without overlapping
   the label column, so this button is sized specifically to always clear
   it: clamp(24px,2vw,32px) + .4rem margin stays comfortably under
   --case-split-gap's own minimum at every width from 1024px up. The right
   side has the much more generous outer page gutter to work with, but
   uses the same size for visual symmetry. */
@media (min-width: 1024px) {
  /* Buttons straddle the image edge (half in, half out — the reference
     build's treatment). This also frees them from the old left-gutter
     squeeze that forced them down to 24px: half-outside only needs ~19px
     of margin room, so they keep the full 38px size at every width. */
  .case-car__row .car-btn {
    position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  }
  .case-car__row .car-btn--prev { left: -19px; }
  .case-car__row .car-btn--next { right: -19px; }
}

.case-sec {
  max-width: var(--case-max); margin: 0 auto;
  padding-block: clamp(1.75rem, 3.4vw, 3rem);
  display: grid; grid-template-columns: var(--case-split);
  gap: var(--case-split-gap);
}
.case-sec__label {
  margin: 0; font-family: var(--font-body); font-size: .8125rem; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--accent-ink);
  position: sticky; top: calc(var(--nav-h) + 1.5rem); align-self: start;
}
.case-sec__body { max-width: 68ch; }
.case-sec__body > p { line-height: 1.8; color: var(--body); margin-bottom: 1.4rem; }
/* A carousel reads as belonging to the text immediately above it, not
   whatever heading happens to follow — so it sits close to that paragraph
   and gets real breathing room before the next heading, not the reverse. */
.case-sec__body > p:has(+ .case-car) { margin-bottom: .5rem; }
.case-car { margin-bottom: 2.75rem; }
/* When a section has several slideshows back to back — with or without a
   heading between them (Design & execution sections do either) — each one
   but the last needs clearer separation from the next than 2.75rem gives.
   The last slideshow in a group already gets enough room from the section
   boundary that follows it, so it's deliberately left alone. */
.case-car:has(~ .case-car) { margin-bottom: 3.5rem; }
.case-after-list { margin-top: 1.6rem; }
.case-h { font-family: var(--display); font-weight: 700; font-size: 1.125rem; letter-spacing: -.02em; color: var(--ink); margin-bottom: 1rem; }

.case-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .75rem; }
.case-list li { position: relative; padding-left: 1.4rem; font-size: 1rem; line-height: 1.65; color: var(--body); }
.case-list li::before {
  content: ""; position: absolute; left: 0; top: .62em;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
}
.case-cols { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(1.5rem, 3vw, 2.75rem); }

.case-fig { margin: 2.25rem 0 0; }
.case-fig__frame {
  aspect-ratio: 16 / 10; border-radius: var(--radius); overflow: hidden;
  background: linear-gradient(150deg, #E9ECF3 0%, #D6DCEA 100%);
  display: grid; place-items: center;
}
.case-fig__frame img { width: 100%; height: 100%; object-fit: cover; }
.case-fig__ph {
  display: grid; justify-items: center; gap: .55rem;
  font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink);
}
.case-fig__ph svg { width: 28px; fill: none; stroke: var(--ink); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.case-fig figcaption { margin-top: .8rem; font-size: .8125rem; color: var(--mono-muted); }

/* ------------------------------------------------------- lightbox --- */
/* One shared instance, reused by every carousel's expand button. Deliberately
   independent of .case-fig__frame's own aspect-ratio rules (those are scoped
   to .case-car__slide/.case-car--fit ancestors that don't exist once content
   is cloned in here) — the lightbox always shows the image at its own
   natural ratio via object-fit: contain, uncropped, which is the entire
   point of "let me see it bigger." */
.lightbox {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(20, 24, 31, .92);
  display: flex; align-items: center; justify-content: center;
  padding: clamp(1rem, 4vw, 3rem);
}
.lightbox[hidden] { display: none; }
body.has-lightbox { overflow: hidden; }

.lightbox__stage { max-width: min(92vw, 1600px); max-height: 82vh; display: grid; place-items: center; }
.lightbox__stage picture { display: contents; }
.lightbox__stage img {
  display: block; max-width: 100%; max-height: 82vh; width: auto; height: auto;
  object-fit: contain; border-radius: var(--radius);
}
/* A "Coming Soon" slide has no intrinsic size to size the stage by —
   give it one, at the same gradient/label treatment used everywhere else. */
.lightbox__stage .case-fig__ph {
  width: min(92vw, 900px); aspect-ratio: 16 / 10;
  background: linear-gradient(150deg, #E9ECF3 0%, #D6DCEA 100%);
  border-radius: var(--radius); color: var(--paper);
}
.lightbox__stage .case-fig__ph svg { width: 40px; stroke: var(--paper); }

.lightbox__close, .lightbox__nav {
  position: absolute; z-index: 1;
  border: 1px solid rgba(246, 247, 245, .3); border-radius: 50%;
  background: rgba(246, 247, 245, .12);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  display: grid; place-items: center; cursor: pointer;
}
.lightbox__close:hover, .lightbox__nav:hover { background: rgba(246, 247, 245, .22); }
.lightbox__close svg, .lightbox__nav svg {
  fill: none; stroke: var(--paper); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}
.lightbox__close { top: clamp(.75rem, 3vw, 1.75rem); right: clamp(.75rem, 3vw, 1.75rem); width: 44px; aspect-ratio: 1; }
.lightbox__close svg { width: 45%; }
.lightbox__nav { top: 50%; transform: translateY(-50%); width: 48px; aspect-ratio: 1; }
.lightbox__nav svg { width: 40%; }
.lightbox__nav--prev { left: clamp(.5rem, 3vw, 2rem); }
.lightbox__nav--next { right: clamp(.5rem, 3vw, 2rem); }

.lightbox__count {
  position: absolute; bottom: clamp(.75rem, 3vw, 1.75rem); left: 0; right: 0;
  text-align: center; font-family: var(--font-body); font-size: .8125rem; font-weight: 600;
  color: var(--paper); opacity: .8;
}
.lightbox.is-single .lightbox__nav, .lightbox.is-single .lightbox__count { display: none; }
.lightbox__nav { transition: opacity .3s var(--ease), background-color .3s var(--ease); }
.lightbox__nav.is-end { opacity: 0; pointer-events: none; }

@media (max-width: 767px) {
  .lightbox__nav { width: 40px; }
}

.case-stats {
  list-style: none; margin: 1.75rem 0 2.25rem; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 1.75rem 2rem;
}
.case-stats li { display: grid; gap: .45rem; align-content: start; }
/* same size as .case-h, so Delivery & Impact doesn't shout over the rest */
.case-stats strong {
  font-family: var(--display); font-weight: 700; font-size: 1.125rem;
  letter-spacing: -.02em; color: var(--ink); line-height: 1.25;
}
.case-stats span { font-size: .875rem; line-height: 1.5; color: var(--body); }

/* the skills row runs long, so it spans the meta grid */
/* the one pull-quote moment in the case study */
.case-callout {
  margin: clamp(1.75rem, 3vw, 2.5rem) 0 0;
  padding: clamp(1.1rem, 2.2vw, 1.6rem) clamp(1.2rem, 2.4vw, 1.75rem);
  border-left: 2px solid var(--accent);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.case-callout__label {
  font-size: .8125rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-ink); margin: 0 0 .5rem;
}
.case-callout__body {
  margin: 0; font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: 1.55; color: var(--ink); font-weight: 500;
}

/* the process line — steps separated by arrows */
.case-flow {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem .55rem;
  margin: 0 0 1.75rem; padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--line);
  font-size: .875rem; font-weight: 500; color: var(--ink);
}
.case-flow i { font-style: normal; color: var(--accent); }

.case-list li b { font-weight: 600; color: var(--ink); }
.case-sec__body > p b { font-weight: 600; color: var(--ink); }
.case-car + p { margin-top: 1.75rem; }

.case-paging {
  max-width: var(--case-max); margin: clamp(2rem, 4vw, 3.5rem) auto 0;
  padding-top: 2rem; position: relative;
  display: flex; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap;
}
.case-paging::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 1px;
  background: var(--line);
}
.case-paging__link { display: flex; align-items: center; gap: 1rem; }
.case-paging__link--prev { text-align: left; }
.case-paging__link--next { text-align: right; flex-direction: row-reverse; }
.case-paging__arrow {
  width: 44px; aspect-ratio: 1; flex: 0 0 auto;
  border: 1px solid var(--line); border-radius: 50%;
  display: grid; place-items: center; transition: background-color .3s var(--ease);
}
.case-paging__arrow svg { width: 17px; fill: none; stroke: var(--ink); stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.case-paging__link:hover .case-paging__arrow { background: #FFF; }
.case-paging__label { display: block; font-family: var(--font-body); font-size: .75rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--accent-ink); margin-bottom: .3rem; }
.case-paging__title { display: block; font-weight: 600; font-size: clamp(1rem, 1.6vw, 1.25rem); color: var(--ink); }
.case-paging__link:hover .case-paging__title { color: var(--accent-ink); }

/* ===================================================== BREAKPOINTS */
@media (max-width: 1023px) {
  .hero__grid { grid-template-columns: 1fr; }
  .about__grid { grid-template-columns: 1fr; }
  .portrait { max-width: 420px; }
  .case-facts > div { grid-template-columns: 1fr; gap: .25rem; }
  .case-sec { grid-template-columns: 1fr; gap: 1rem; }
  .case-sec__label { position: static; }
}
@media (max-width: 767px) {
  body { font-size: 1rem; }
  .contact__grid { grid-template-columns: 1fr; }
  /* backdrop blur is the most expensive paint here and barely visible at
     this size — a flat translucent band costs nothing */
  .car-btn { -webkit-backdrop-filter: none; backdrop-filter: none; }
  .nav__links { gap: 1rem; }
  .wgrid { grid-template-columns: 1fr; gap: 1.75rem; }
  .case-cols, .case-stats { grid-template-columns: 1fr; }
  .case-car__slide .case-fig__frame { aspect-ratio: 4 / 3; }
  .case-car--fit .case-fig__frame { aspect-ratio: var(--fit-ratio, 1600 / 1039); }
  .case-paging { flex-direction: column; }
  .case-paging__link--next { justify-content: flex-end; text-align: left; }
  .footer__inner { justify-content: center; text-align: center; }
  .footer__inner p { margin-left: 0; }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  .reveal { opacity: 1; transform: none; }
}

/* ============================================================
   GLASS HERO — one glass family across every home-page card,
   plus the nav Personalize panel (colour / text size / background).
   ============================================================ */

/* ---- text-size preference (accessibility). Scaling the root scales
        every rem on the page — type, gaps and padding together. ---- */
html[data-textsize="lg"] { font-size: 112.5%; }
html[data-textsize="xl"] { font-size: 125%; }

/* ---- background-intensity preference ---- */
/* opacity is set directly from JS, NOT through a custom property: an
   unregistered custom property jumps rather than animates, so the
   background snapped off instead of fading. */
#gl { transition: filter .8s var(--ease), opacity .5s var(--ease); }

/* ---- the shared glass recipe ----
   .gcard  = hero panes, the lightest (content is text on open space)
   .gpanel = supporting panels, mid weight
   .wcard  = case studies, heaviest — screenshots need a solid ground
   All share: blur + saturate, a hairline, the same radius, and a
   top edge-light. Cards that clip their own overflow get that
   edge-light as an inset shadow instead of a ::after, since a
   pseudo-element at top:-1px would be clipped away. */
.gcard, .gpanel {
  position: relative;
  background: rgba(255, 255, 255, .2);
  -webkit-backdrop-filter: blur(9px) saturate(1.15); backdrop-filter: blur(9px) saturate(1.15);
  border: 1px solid rgba(255, 255, 255, .45);
  border-radius: 24px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .85), 0 14px 40px rgba(41, 42, 77, .06);
  transition: background .35s var(--ease), transform .35s var(--ease);
}
.gcard { padding: var(--card-pad); display: flex; flex-direction: column; align-items: flex-start; }
.gcard::after, .gpanel::after {
  content: ""; position: absolute; top: -1px; left: 12%; right: 12%; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .95), transparent);
  opacity: .7; transition: opacity .3s; pointer-events: none;
}
.gcard:hover, .press.gpanel:hover { transform: translateY(-4px); background: rgba(255, 255, 255, .3); }
.gcard:hover::after, .gpanel:hover::after { opacity: 1; }

/* ---- hero ---- */
.hero { padding-bottom: clamp(5rem, 12vh, 8rem); }
.hero__inner { max-width: var(--maxw); position: relative; }
/* A real 2x2 grid, not two independent stacks. Row 1 is portrait | impact,
   row 2 is About me | belief — so the two big cards share a row and therefore
   share a top edge and a height. Two flex columns could never guarantee that:
   the card's top depended on whatever sat above it in its own stack. Source
   order stays portrait, About me, impact, belief so the single-column layout
   and screen readers read sensibly; placement is done here. */
.hero__grid--glass {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto 1fr;
  gap: clamp(26px, 2.6vw, 36px) 56px;
  align-items: stretch;
}
.hero__grid--glass > .gcard__portrait { grid-column: 1; grid-row: 1; }
.hero__grid--glass > .gcard--about    { grid-column: 1; grid-row: 2; }
.hero__grid--glass > .gcard--stats    { grid-column: 2; grid-row: 1; }
.hero__grid--glass > .gcard--ethos    { grid-column: 2; grid-row: 2; }
.hero__grid--glass > .gcard__portrait { height: clamp(150px, 14vw, 200px); max-height: none; min-height: 0; align-self: center; }
.gstack { display: flex; flex-direction: column; gap: 28px; }
/* The right column now carries three cards, so the left one has slack. Give it
   to the gap between the portrait and the card rather than letting the card
   stretch and open a void above the status line — the portrait stays small and
   the two columns still end level. */
/* The left column now carries three blocks to the right column's two, so it is
   the taller one — space-between here would only push Featured in off-screen.
   The portrait is allowed to shrink instead, and the belief card centres its
   copy so the slack on the right reads as air rather than a dead corner. */
.gcard--ethos { justify-content: center; }
.gstack .gcard:last-child { flex: 1 1 auto; }
.gcard--about .hero__mark { margin-top: 0; font-size: clamp(1.5rem, 2.4vw, 2rem); align-self: flex-start; }
/* text-wrap:pretty makes the browser rebalance the last lines so a
   single word never strands on its own — the general fix for the
   widow problem rather than hand-tuning each max-width. */
.gcard--about .hero__lead { margin-top: 14px; max-width: none; text-wrap: pretty; }

/* My design impact now lives inside the intro card, so its label needs the
   space a separate pane used to give it, and the list must not stretch the
   card's flex layout. */
/* The portrait sits ABOVE the About card, centred on it, as its own element in
   the left column — not inside the card. It is the only flexible box in that
   column, so it grows into whatever height the right-hand stack has and the
   circle's diameter follows; the card itself keeps its natural height.
   Clamped at both ends so it never collapses or balloons at extreme widths. */
.gcard__portrait {
  flex: 1 1 auto; min-height: 132px; max-height: 274px;
  width: 100%; min-width: 0;
  display: flex; align-items: center; justify-content: center;
}
.gcard__portrait picture { position: relative; display: block; height: 100%; aspect-ratio: 1 / 1; }
/* Two offset outlines that wobble on the same curve as the blob cursor, so the
   portrait reads as hand-drawn rather than stamped. cur-morph itself lives
   inside the fine-pointer media query, so the keyframes are redeclared here at
   top level; the photo stays a true circle and only the rings move. */
.gcard__portrait picture::before,
.gcard__portrait picture::after {
  content: ""; position: absolute; inset: -11px;
  border: 1.5px solid rgba(41, 42, 77, .30);
  border-radius: 58% 42% 55% 45% / 45% 58% 42% 55%;
  animation: scribble 9s ease-in-out infinite;
  pointer-events: none;
}
.gcard__portrait picture::after {
  inset: -18px; border-color: rgba(93, 94, 184, .26); animation-delay: -3.4s;
}
@keyframes scribble {
  0%, 100% { border-radius: 58% 42% 55% 45% / 45% 58% 42% 55%; }
  33%      { border-radius: 45% 55% 40% 60% / 60% 40% 58% 42%; }
  66%      { border-radius: 52% 48% 62% 38% / 40% 55% 45% 60%; }
}
@media (prefers-reduced-motion: reduce) {
  .gcard__portrait picture::before, .gcard__portrait picture::after { animation: none; }
}
.gcard__face {
  height: 100%; width: auto; aspect-ratio: 1 / 1;
  border-radius: 50%; object-fit: cover; object-position: center top;
  border: 1px solid var(--line); display: block;
}
.gcard__status {
  margin: auto 0 0; padding-top: 24px;
  display: flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-size: .6875rem; font-weight: 500;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-2);
}
.gcard__status i { width: 7px; height: 7px; border-radius: 50%; background: var(--acc); animation: rail-pulse 2.4s ease infinite; }
.glabel {
  margin: 0 0 16px; font-family: var(--mono); font-size: .6875rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--acc);
}
.gstats { list-style: none; margin: 0; padding: 0; display: grid; gap: 13px; width: 100%; }
.gstats li { position: relative; padding-left: 20px; font-size: .9375rem; line-height: 1.55; color: var(--ink); text-wrap: pretty; }
.gstats li::before {
  content: ""; position: absolute; left: 0; top: 9px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--acc); opacity: .55;
  transition: transform .35s cubic-bezier(.2,.7,.2,1), opacity .3s;
}
.gstats--expand li:hover::before { transform: scale(1.6); opacity: 1; }
.gstats li b { font-family: var(--display); font-weight: 700; }
.gstats--expand button { all: unset; display: block; width: 100%; cursor: pointer; }
.gstats--expand button:focus-visible { outline: 2px solid var(--acc); outline-offset: 4px; border-radius: 6px; }
.gstats--expand .line { display: block; transition: transform .35s cubic-bezier(.2,.7,.2,1); }
.gstats--expand button:hover .line { transform: translateX(6px); }
.gstats--expand button:hover .line b { color: var(--acc); }
.gstats--expand .line b { transition: color .25s; }
.gstats--expand .more { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .45s cubic-bezier(.2,.7,.2,1); }
.gstats--expand .more > p {
  overflow: hidden; margin: 0; color: var(--ink-2); font-size: .8438rem; line-height: 1.6;
  max-width: 52ch; opacity: 0; transform: translateY(6px);
  transition: opacity .35s ease, transform .45s cubic-bezier(.2,.7,.2,1);
}
.gstats--expand li.open .more { grid-template-rows: 1fr; }
.gstats--expand li.open .more > p { opacity: 1; transform: none; transition-delay: .1s; padding-top: 8px; }
/* Same face, weight and size as the <b> figures in .gstats — these two lists
   read as one system, so they must not drift apart typographically. */
.gethos__t {
  margin: 0 0 10px; font-family: var(--display); font-weight: 700;
  font-size: .9375rem; line-height: 1.55; letter-spacing: -.025em;
  color: var(--ink); max-width: 26ch; text-wrap: balance;
}
.gethos { margin: 0; font-size: .9375rem; line-height: 1.7; color: var(--ink-2); max-width: 54ch; text-wrap: pretty; }

/* Three beliefs stacked in one pane. Titles and bodies reuse .gethos__t /
   .gethos so the pane keeps a single type rhythm; only the stacking is new. */
.gbeliefs {
  list-style: none; margin: 0; padding: 0; display: grid;
  gap: clamp(18px, 1.7vw, 26px);
}
.gbeliefs > li { position: relative; padding-left: 20px; }
.gbeliefs > li::before {
  content: ""; position: absolute; left: 0; top: .5em;
  width: 7px; height: 7px; border-radius: 50%; background: var(--acc); opacity: .55;
}
.gbeliefs .gethos__t { margin: 0 0 6px; max-width: none; text-wrap: nowrap; }
.gbeliefs .gethos { max-width: none; }
/* Only force the title onto one line where there is room for it; below the
   two-column breakpoint the pane is too narrow and it must wrap normally. */
@media (max-width: 1099px) { .gbeliefs .gethos__t { text-wrap: pretty; } }

/* The one call to action under the work grid. It is a .gpanel, so the glass,
   hairline, radius and top edge-light all come from the shared card rule —
   and a .linkrow__go arrow, so it moves like every other link on the site.
   Nothing here invents a new surface; it only makes the panel a pill. */
.workcta {
  display: inline-flex; align-items: center; gap: 1rem;
  padding: 1rem clamp(20px, 2vw, 28px);
  border-radius: 999px;
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1rem, 1.2vw, 1.125rem);
  color: var(--ink); text-decoration: none;
}
.workcta:hover { transform: translateY(-4px); background: rgba(255, 255, 255, .3); }
.workcta:hover .linkrow__label { color: var(--acc); }
.workcta:hover .linkrow__go { transform: translateX(6px); }
@media (prefers-reduced-motion: reduce) { .workcta:hover { transform: none; } }

/* ---- case study cards: heaviest glass in the family. The screenshots
        are the content here, so this keeps a solid ground; the edge-light
        is an inset shadow because .wcard clips its own overflow. Hover
        lift is deliberately NOT applied — .wcard already animates
        transform for its scroll stand-up, and the two would fight. ---- */
.wcard {
  background: rgba(255, 255, 255, .72);
  -webkit-backdrop-filter: blur(16px) saturate(1.25); backdrop-filter: blur(16px) saturate(1.25);
  border: 1px solid rgba(255, 255, 255, .55);
  border-radius: 22px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9), 0 18px 46px rgba(41, 42, 77, .07);
}

/* ---- more_stuff_about_me.json: SMOKED glass, not light glass.
        The syntax palette (amber strings, sky keys) only clears WCAG on a
        dark ground — that is exactly why this panel went dark earlier in
        the build. At .88 alpha the measured contrast is 6.9:1 for the
        amber and 8.4:1 for the body, and the veil still shifts visibly
        behind it, so it reads as glass without breaking the type. ---- */
.jbox {
  background: rgba(16, 26, 51, .88);
  -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 24px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 18px 46px rgba(41, 42, 77, .12);
  padding: 1.15rem 1.35rem;
  position: relative;
  transition: background .35s var(--ease), transform .35s var(--ease);
}
.jbox:hover { transform: translateY(-4px); }

/* ---- Featured in + Contact join the family ---- */
.press.gpanel { padding: var(--card-pad); margin-top: 28px; }
/* In the hero stack the column gap already spaces it, and the belief is now a
   single paragraph, so it takes the card's full measure like every other one. */
.gstack .press.gpanel { margin-top: 0; }
.gcard--ethos .gethos { max-width: none; }
.press.gpanel .linkstack { border-top: 0; }
.press.gpanel .linkrow:last-of-type { border-bottom: 0; }
.portrait__frame { box-shadow: inset 0 1px 0 rgba(255,255,255,.5), 0 18px 46px rgba(41,42,77,.08); border-radius: 24px; }

/* ---- scroll cue ---- */
.scrollcue--solo { left: 50%; right: auto; transform: translateX(-50%); flex-direction: column; gap: 7px; bottom: 20px; }
.mousey__wheel { animation: mousey-wheel 1.8s ease-in-out infinite; }
@keyframes mousey-wheel {
  0%, 100% { transform: translateY(0); opacity: 1; }
  55% { transform: translateY(9px); opacity: 0; }
  75% { transform: translateY(0); opacity: 0; }
}

/* ============================================================
   PERSONALIZE PANEL — trigger sits in the right gutter, top-aligned
   with the "My design experience" card; the panel drops down and
   right-aligns to the trigger so it never overflows the viewport.
   ============================================================ */
/* Lives in the nav, so every preference is reachable from the home page
   AND from any case study — the nav is outside <main> and never unmounts,
   and the prefs it sets (root font-size, canvas hue/opacity) are global. */
.gctl { position: relative; margin-left: clamp(1rem, 2vw, 1.75rem); z-index: 120; }
.gctl__trigger {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  height: 34px; padding: 0 14px 0 12px; border-radius: 999px;
  background: rgba(255, 255, 255, .55);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, .8);
  box-shadow: 0 4px 14px rgba(41, 42, 77, .07);
  font-family: var(--font-body); font-size: .8125rem; font-weight: 600;
  color: var(--ink); white-space: nowrap;
  transition: background .25s var(--ease), border-color .25s var(--ease), color .25s var(--ease);
}
.gctl__trigger svg { width: 15px; flex: 0 0 auto; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; }
.gctl__trigger:hover { background: #fff; border-color: var(--acc); }
.gctl__trigger[aria-expanded="true"] { background: var(--ink); border-color: var(--ink); color: #fff; }

.gctl__panel {
  position: absolute; top: 44px; right: 0; width: 250px;
  padding: 18px 18px 16px;
  background: rgba(255, 255, 255, .72);
  -webkit-backdrop-filter: blur(22px) saturate(1.4); backdrop-filter: blur(22px) saturate(1.4);
  border: 1px solid rgba(255, 255, 255, .8);
  border-radius: 18px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9), 0 22px 50px rgba(41, 42, 77, .14);
  display: grid; gap: 15px;
  animation: gctl-in .28s cubic-bezier(.2,.7,.2,1);
}
/* the [hidden] attribute's display:none comes from the UA stylesheet, which
   ANY author rule outranks — without this the display:grid above kept the
   panel permanently open. Class+attribute beats the bare class. */
.gctl__panel[hidden] { display: none; }
@keyframes gctl-in { from { opacity: 0; transform: translateY(-6px); } }
.gctl__title {
  margin: 0; font-family: var(--mono); font-size: .625rem; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--acc);
}
.gctl__row { display: grid; gap: 8px; }
.gctl__key { font-size: .8125rem; font-weight: 600; color: var(--ink); }
.gctl__dots { display: flex; gap: 10px; }
.gctl__dot {
  width: 20px; height: 20px; border-radius: 50%; cursor: pointer;
  background: var(--c); border: 2px solid rgba(255, 255, 255, .9);
  box-shadow: 0 0 0 1px var(--line); transition: transform .25s var(--ease), box-shadow .25s;
}
.gctl__dot:hover { transform: scale(1.2); }
.gctl__dot.is-on { box-shadow: 0 0 0 2px var(--ink); }
.gctl__seg { display: flex; gap: 5px; }
.gctl__sbtn {
  flex: 1 1 auto; min-height: 30px; padding: 4px 6px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 9px;
  background: rgba(255, 255, 255, .55);
  font-family: var(--mono); font-size: .625rem; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2);
  display: grid; place-items: center;
  transition: all .22s var(--ease);
}
.gctl__sbtn:hover { border-color: var(--acc); color: var(--ink); }
.gctl__sbtn.is-on { background: var(--ink); border-color: var(--ink); color: #fff; }

@media (prefers-reduced-motion: reduce) {
  .mousey__wheel, .gcard__status i { animation: none; }
  .gctl__panel { animation: none; }
  .gctl__trigger:hover { transform: none; }
}
@media (max-width: 900px) {
  .hero__grid--glass { grid-template-columns: 1fr; grid-template-rows: none; }
  .hero__grid--glass > .gcard__portrait,
  .hero__grid--glass > .gcard--about,
  .hero__grid--glass > .gcard--stats,
  .hero__grid--glass > .gcard--ethos { grid-column: 1; grid-row: auto; }
}
@media (max-width: 620px) {
  /* nav gets tight — the icon carries it, the word drops out */
  .gctl__trigger { padding: 0; width: 34px; justify-content: center; gap: 0; }
  .gctl__trigger .gctl__word { display: none; }
}

/* The portrait's natural aspect ratio was taller than the JSON + Featured
   column, so IT set the row height and the right column stretched to match.
   Taking the frame out of flow (absolute) means the right column decides the
   row height and the portrait fills whatever that is — cropping from the top
   via object-position so the face is never the part that gets cut. */

/* The Work/About divider is a border-bottom on .work__note ("More projects
   coming soon."), not a standalone rule element — which is why it survived
   removing .about__rule. Its padding stays; only the line goes. */
.work__note { border-bottom: 0; }

/* .about__rule carried `margin: var(--section-y) auto 0`, so deleting it
   also deleted the gap between About and Contact (.about has
   padding-bottom:0). Put that spacing back on .about itself. */
.about { padding-bottom: var(--section-y); }

/* contact panel — full width, so it brightens on hover rather than lifting */
.contact__card.gpanel { padding: var(--card-pad); }
.contact__card.gpanel:hover { background: rgba(255, 255, 255, .3); }

.portrait { position: relative; height: auto; min-height: 0; }
.portrait__frame { position: absolute; inset: 0; height: auto; }
.portrait__frame img { width: 100%; height: 100%; object-fit: cover; object-position: center top; }

/* Featured in lives inside the About card now, so it needs the same standoff
   the impact list had, and the rows keep the tighter rhythm from .press. */
.glabel--stacked { margin-top: 30px; }
.gcard--about .linkstack { width: 100%; border-top: 0; }
.gcard--about .linkrow { font-size: .9375rem; padding: .5rem 0; }
.gcard--about .linkstack .linkrow:hover { padding-left: 8px; }
.gcard--about .linkrow:last-of-type { border-bottom: 0; }

.gethos + .gethos { margin-top: .9em; }
