/* ==========================================================================
   Tiffany Quen — Portfolio
   Design system: warm editorial-gallery meets product-design case study.
   Fluid-first: clamp() typography/spacing, auto-fit grids, minimal hard
   breakpoints (only where the structure genuinely needs to change).
   ========================================================================== */

/* ---------- Fonts ---------- */
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400;1,9..144,500&display=swap');

/* ---------- Tokens ---------- */
:root{
  /* Global color system. Every color used across the site — nav, footer,
     cards, buttons, links, hover/active/focus states, case-study UI —
     resolves to one of these tokens (or a color-mix()/opacity variant
     of one) rather than a one-off value chosen per page, so the whole
     portfolio reads as one consistent system. Project screenshots and
     prototype videos are untouched by this system — it governs
     interface chrome only.
     --card and --paper-dim are derived from --paper via color-mix()
     rather than separate hardcoded hexes, so the surface hierarchy
     (page → card → dim) always stays in proportion if --paper changes. */

  /* surfaces */
  --paper:#FFFCF7;                                            /* main page background — the site-wide default */
  --card: color-mix(in srgb, var(--paper) 65%, white);         /* card / contained-module background (figures, callouts, tiles), lighter than the page */
  --paper-dim: color-mix(in srgb, var(--paper) 90%, var(--ink)); /* alternate/section background (device screens, tile fills), a touch dimmer than the page */

  /* text */
  --ink:#17171B;         /* primary text + headings */
  --ink-soft:#57544E;    /* secondary/body text */
  --ink-faint:#8B8778;   /* muted/tertiary text (labels, captions, meta) */

  /* borders + dividers */
  --line:rgba(23,23,27,0.12);
  --line-strong:rgba(23,23,27,0.22);

  /* accent — the primary link/navigation color: main nav, page-to-page
     links (case-nav, work list, footer links), buttons, active/hover/
     focus states, and small emphasis (numerals, tag borders, chips) */
  --accent:#CE4996;
  --accent-ink: #CE4996;   /* accent on hover / higher-contrast accent text — same as --accent per brand color pass */
  --accent-soft: color-mix(in srgb, #CE4996 16%, var(--paper)); /* accent-tinted fill (chips, tags) */

  --shadow: 0 24px 60px -30px rgba(23,23,27,0.35);
  --shadow-sm: 0 10px 30px -18px rgba(23,23,27,0.35);

  --font-display:"Fraunces", "Iowan Old Style", Georgia, serif;
  --font-body:"Helvetica Neue", Helvetica, Arial, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* fluid type scale — display sizes (hero/h1/h2/h3) trimmed roughly
     20% total across two passes so headings throughout the site read
     noticeably smaller without changing the overall type hierarchy. */
  --fs-hero: clamp(2rem, 2.6vw + 1.3rem, 4.4rem);
  --fs-h1: clamp(1.65rem, 1.6vw + 1.2rem, 2.6rem);
  --fs-h2: clamp(1.2rem, 0.8vw + 0.95rem, 1.65rem);
  --fs-h3: clamp(0.95rem, 0.4vw + 0.8rem, 1.1rem);
  --fs-lede: clamp(1.1rem, 0.5vw + 0.95rem, 1.35rem);
  --fs-body: clamp(1rem, 0.15vw + 0.94rem, 1.09rem);
  --fs-small: clamp(0.82rem, 0.1vw + 0.78rem, 0.92rem);
  --fs-micro: clamp(0.72rem, 0.1vw + 0.7rem, 0.78rem);

  /* fluid spacing */
  --space-section: clamp(4.5rem, 6vw + 2rem, 9rem);
  --space-block: clamp(2.5rem, 3vw + 1rem, 5rem);
  --gutter: clamp(1.25rem, 4.5vw, 4rem);
  --radius: 14px;
  --radius-sm: 8px;

  --container: 1280px;
  --container-narrow: 860px;

  --ease: cubic-bezier(.16,.84,.44,1);
  --dur: .6s;
}

/* ---------- Reset ---------- */
*, *::before, *::after{ box-sizing: border-box; }
/* A single, intentional focus treatment in the accent color for every
   link/button/control, instead of leaving keyboard focus to fall back
   to the browser's default (unrelated) blue outline. */
a:focus-visible, button:focus-visible{
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}
html{
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Custom paintbrush cursor, site-wide. Hotspot (5 27) is the bristle
     tip in the 32x32 icon, so the actual click point lines up with
     the visual point of the brush. This exact cursor is re-declared
     (never swapped for a different one) on every element that browsers
     otherwise force their own cursor onto — links, buttons, and the
     lightbox's zoom-in gallery figures — so it stays active everywhere
     on desktop/laptop rather than falling back to a pointer/hand/zoom
     cursor. Touch devices have no cursor, so this has no effect there. */
  cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto;
}
body{
  margin:0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: clip;
}
img, video{ max-width: 100%; display:block; height:auto; }
/* Links don't reliably inherit a custom cursor from an ancestor —
   browsers special-case anchors with an href and swap in their own
   pointing-hand cursor regardless of what's inherited, so the
   paintbrush needs to be declared directly on `a` (and again on
   :hover/:active/:focus, since those pseudo-classes are where browsers
   are most likely to reassert the native hand cursor) rather than
   relying on inheritance from html. */
a{ color: inherit; text-decoration: none; cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto; }
a:hover, a:active, a:focus{ cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto; }
ul{ list-style:none; margin:0; padding:0; }
h1,h2,h3,h4,p,figure{ margin:0; }
button{ font: inherit; color: inherit; background:none; border:none; cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto; padding:0; }
button:hover, button:active, button:focus{ cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto; }
svg{ display:block; }

/* Belt-and-suspenders: some browsers momentarily swap in their own
   native cursor (arrow/hand) at the exact instant of mousedown/click
   on an interactive element, rather than only on plain hover, which
   the rules above don't catch. This universal, maximum-specificity
   rule re-asserts the SAME paintbrush cursor (identical file, hotspot,
   and appearance as everywhere else on the site) on every element in
   every state, so nothing can ever fall back to a different cursor,
   at any point of interaction, anywhere on desktop/laptop. It has no
   effect on touch devices (no cursor exists there) and does not alter
   any hover/click behavior, only which cursor graphic is shown. */
*, *::before, *::after,
*:hover, *:active, *:focus, *:focus-visible, *:visited{
  cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto !important;
}

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

/* ---------- Layout helpers ---------- */
.wrap{
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow{ max-width: var(--container-narrow); }
.section{ padding-block: var(--space-section); }
.section--tight{ padding-block: clamp(2.5rem, 4vw, 4.5rem); }

/* Other Experience list (home): a repeated title+org / description row,
   separated by divider lines, rather than a card grid — matches the
   lighter, resume-like presentation this content calls for compared to
   the deeper case-study rows above it. */
.exp-list{
  margin-top: clamp(1.6rem, 3vw, 2.2rem);
  border-top: 1px solid var(--line);
}
.exp-row{
  display:grid;
  grid-template-columns: minmax(0,1fr) minmax(0,1.5fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  padding-block: clamp(1.6rem, 3vw, 2.2rem);
  border-bottom: 1px solid var(--line);
  align-items:start;
}
.exp-row-title{
  font-family: var(--font-display);
  font-weight:500;
  font-size: var(--fs-h3);
  line-height:1.25;
}
.exp-row-org{
  margin-top:.4rem;
  font-size: var(--fs-small);
  font-weight:600;
  color: var(--accent-ink);
}
.exp-row-desc{
  font-size: var(--fs-body);
  color: var(--ink-soft);
}
@media (max-width: 640px){
  .exp-row{ grid-template-columns: 1fr; gap:.5rem; }
}

.eyebrow{
  font-family: var(--font-body);
  font-size: var(--fs-micro);
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent-ink);
  display:flex;
  align-items:center;
  gap:.6em;
}
.eyebrow::before{
  content:"";
  width: 1.3em;
  height: 1px;
  background: var(--accent);
  display:inline-block;
}

.tag{
  display:inline-flex;
  align-items:center;
  font-size: var(--fs-micro);
  letter-spacing:.1em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--ink-soft);
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  padding: .5em 1em;
  background: var(--card);
}

/* ---------- Reveal-on-scroll ----------
   Default is a gentle fade+rise. A few variants exist so scroll reveal
   doesn't read as one repeated animation everywhere: small/frequent
   text (labels, meta) can fade in place with no movement, and major
   visual moments (hero imagery, key case-study screenshots, decision
   numerals) get a touch more presence via a slower, slightly larger
   entrance. Applied selectively, not on every element. */
[data-reveal]{
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 1s var(--ease), transform 1s var(--ease);
  transition-delay: var(--reveal-delay, 0s);
}
[data-reveal].is-visible{ opacity:1; transform:none; }

/* Fade only, no rise — for small inline labels/meta where a vertical
   shift would feel fussy. */
[data-reveal="fade"]{ transform:none; }

/* Slightly larger, slower entrance with a soft scale — for hero
   imagery and the single most important visual in a section. */
[data-reveal="feature"]{
  transform: translateY(36px) scale(.98);
  transition-duration: 1.3s;
}

/* Numerals / badges — scale in rather than rise, since they read as
   marks/stamps rather than lines of text. */
[data-reveal="mark"]{
  transform: scale(.85);
  transition: opacity .8s var(--ease), transform .8s var(--ease);
  transition-delay: var(--reveal-delay, 0s);
}

/* No base opacity/transform hidden-state at all — for wrappers whose
   own children handle the reveal themselves (e.g. the letter-by-letter
   painted phrase), so the wrapper doesn't fade/rise as a block on top
   of its children's own animation. Still observed by the same scroll
   trigger, purely to add .is-visible at the right moment. */
[data-reveal="none"]{ opacity:1; transform:none; }

/* ---------- Skip link ---------- */
.skip-link{
  position:absolute; left:1rem; top:-4rem;
  background: var(--ink); color: var(--paper);
  padding: .75rem 1.25rem; border-radius: var(--radius-sm);
  z-index: 999; transition: top .25s var(--ease);
}
.skip-link:focus{ top: 1rem; }

/* ---------- Case-study reading progress ----------
   A hairline bar fixed to the very top of the viewport, above the nav's
   own z-index, filling with the accent color as the visitor scrolls
   through a case study. Quiet by design — 2px tall, no label — it's a
   small signal that the page is a structured narrative rather than an
   infinite scroll, not a UI element demanding attention. */
.progress-bar{
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 2px;
  z-index: 700;
  background: transparent;
}
.progress-bar-fill{
  display:block;
  height:100%;
  width:0%;
  background: var(--accent);
}

/* ==========================================================================
   Nav
   ========================================================================== */
.site-header{
  position: sticky;
  top: 0;
  z-index: 500;
  padding-top: clamp(.9rem, 1.4vw, 1.4rem);
  padding-bottom: clamp(.9rem, 1.4vw, 1.4rem);
}
.site-header::before{
  content:"";
  position:absolute;
  inset:0;
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  border-bottom: 1px solid transparent;
  transition: border-color .4s var(--ease);
}
.site-header.is-scrolled::before{ border-bottom-color: var(--line); }

.nav{
  position:relative;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap: 1rem;
}
.nav-logo{
  display:flex;
  align-items:center;
  transition: opacity .25s var(--ease);
}
.nav-logo img{
  display:block;
  height: clamp(1.9rem, 1.4rem + 1vw, 2.3rem);
  width:auto;
}
.nav-logo:hover{ opacity:.75; }

.nav-links{
  display:flex;
  align-items:center;
  gap: clamp(1.4rem, 2.2vw, 2.6rem);
}
/* The Work/About and Creative/Resume groups are wrapped in spans in the
   markup so desktop can lay them out as two independent, equal-width
   grid tracks (see the min-width:781px block below) — true centering
   needs real DOM grouping, not just visual order. At every other
   viewport those wrapper spans should be invisible to layout, so the
   mobile flex column still sees the four links directly. */
.nav-links-l, .nav-links-r{ display:contents; }
.nav-links a{
  position:relative;
  font-size: var(--fs-small);
  font-weight: 500;
  letter-spacing:.02em;
  padding-block: .3rem;
  color: var(--ink-soft);
  transition: color .3s var(--ease);
}
.nav-links a::after{
  content:"";
  position:absolute; left:0; right:100%; bottom:-2px;
  height:1px; background: var(--accent);
  transition: right .35s var(--ease);
}
.nav-links a:hover, .nav-links a[aria-current="page"]{ color: var(--accent); }
.nav-links a:hover::after, .nav-links a[aria-current="page"]::after{ right:0; }
.nav-links a.btn-resume{
  color: var(--paper);
  background: var(--ink);
  padding: .55em 1.1em;
  border-radius: 100px;
  transition: background .3s var(--ease), transform .3s var(--ease);
}
.nav-links a.btn-resume::after{ display:none; }
.nav-links a.btn-resume:hover{ background: var(--accent); color: var(--paper); transform: translateY(-1px); }

.nav-toggle{
  display:none;
  width: 42px; height: 42px;
  border-radius: 50%;
  align-items:center; justify-content:center;
  border: 1px solid var(--line-strong);
  z-index: 60;
}
.nav-toggle span, .nav-toggle span::before, .nav-toggle span::after{
  content:""; display:block; width: 16px; height:1px; background: var(--ink);
  position:relative; transition: transform .3s var(--ease), opacity .3s var(--ease);
}
.nav-toggle span::before{ position:absolute; top:-5px; }
.nav-toggle span::after{ position:absolute; top:5px; }
.nav-toggle[aria-expanded="true"] span{ background: transparent; }
.nav-toggle[aria-expanded="true"] span::before{ top:0; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] span::after{ top:0; transform: rotate(-45deg); }

/* Desktop/tablet only: logo centered in the bar with the nav links
   split to either side of it, uppercase and letter-spaced, echoing the
   reference layout.

   Two prior approaches both turned out unreliable: unequal-width link
   groups spread with justify-content:space-between (the logo's true
   center didn't line up with the visual midpoint between two unevenly
   sized groups), and nesting display:contents two levels deep — .nav
   as a grid with .nav-links unwrapped via contents so its two grouping
   spans became grid items — which some rendering engines apparently
   don't promote reliably, stacking the logo above the links instead
   of beside them.

   This version avoids nested contents entirely. .nav-logo is taken out
   of the flex flow with position:absolute and centered on .nav's own
   box via left:50%/transform — a single, well-supported centering
   mechanism with no dependency on sibling widths at all. .nav-links
   (the real <nav> element already in the markup, not unwrapped) is
   left as the only in-flow child and stretched to .nav's full width,
   so its own true center lines up exactly with .nav's center — i.e.
   exactly where the logo sits. Inside it, a plain two-column grid
   (1fr 1fr) holds the two link-group spans, each pushed inward toward
   the middle where the logo overlays. */
@media (min-width: 781px){
  .nav-logo{
    position:absolute;
    left:50%;
    top:50%;
    transform: translate(-50%, -50%);
    z-index:1;
  }
  .nav-links{
    display:grid;
    grid-template-columns: 1fr 1fr;
    align-items:center;
    width:100%;
    column-gap: clamp(2.6rem, 6vw, 5rem);
  }
  .nav-links-l, .nav-links-r{
    display:flex;
    align-items:center;
    gap: clamp(1.4rem, 2.2vw, 2.6rem);
  }
  .nav-links-l{ justify-content:flex-end; }
  .nav-links-r{ justify-content:flex-start; }
  .nav-links a{
    text-transform:uppercase;
    letter-spacing:.14em;
    font-size: var(--fs-micro);
    font-weight:600;
  }
  /* Plain text like the other links here, not the pill-button treatment
     mobile still uses. */
  .nav-links a.btn-resume{
    color: var(--ink-soft);
    background:none;
    padding-block: .3rem;
    padding-inline: 0;
    border-radius:0;
  }
  .nav-links a.btn-resume::after{ display:block; }
  .nav-links a.btn-resume:hover{
    background:none;
    color: var(--accent);
    transform:none;
  }
}

@media (max-width: 780px){
  .nav-toggle{ display:flex; }
  .nav-links{
    position: fixed; inset:0;
    flex-direction: column;
    justify-content:center;
    align-items:flex-start;
    gap: 1.6rem;
    padding: 6rem var(--gutter) 3rem;
    background: var(--paper);
    transform: translateY(-100%);
    transition: transform .5s var(--ease);
  }
  .nav-links[data-open="true"]{ transform: translateY(0); }
  .nav-links a{
    font-size: clamp(1.4rem, 6vw, 2rem); font-family: var(--font-display); color: var(--ink);
    opacity:0;
    transform: translateY(14px);
    transition: opacity .4s var(--ease), transform .4s var(--ease);
  }
  /* Mobile menu: Resume reads as a plain link like Work/About/Creative,
     not the pill-button treatment the desktop bar deliberately drops
     (see the min-width:781px override above) — same reasoning applies
     here, just resetting the base .btn-resume pill styles instead. */
  .nav-links a.btn-resume{
    color: var(--ink);
    background:none;
    padding-block: .3rem;
    padding-inline: 0;
    border-radius:0;
  }
  .nav-links a.btn-resume::after{ display:block; }
  .nav-links a.btn-resume:hover{
    background:none;
    color: var(--accent);
    transform:none;
  }
  /* Links step in one after another once the menu is open, instead of
     the whole panel just being visible at once — a small sense of
     sequence rather than a static list appearing. */
  .nav-links[data-open="true"] a{ opacity:1; transform:none; }
  .nav-links[data-open="true"] .nav-links-l a:nth-child(1){ transition-delay: .1s; }
  .nav-links[data-open="true"] .nav-links-l a:nth-child(2){ transition-delay: .16s; }
  .nav-links[data-open="true"] .nav-links-r a:nth-child(1){ transition-delay: .22s; }
  .nav-links[data-open="true"] .nav-links-r a:nth-child(2){ transition-delay: .28s; }
}

/* ==========================================================================
   Hero (home)
   ========================================================================== */
/* Since the hero lost its illustration column, its content is now just
   a few centered lines — short enough that on many screens the work
   list's first card started peeking in at the very bottom of the
   initial view before any scrolling. min-height (roughly viewport
   height minus the sticky header) plus centering the content within
   it keeps the hero occupying its own full screen, so the work list
   only comes into view once the visitor actually scrolls. */
.hero{
  min-height: calc(100vh - 5.5rem);
  min-height: calc(100svh - 5.5rem);
  display:flex;
  align-items:center;
  justify-content:center;
  /* Top/bottom padding matched exactly (both used to clamp to slightly
     different maximums — 6rem vs 5rem — which skewed the centered
     content a little off true middle at wider viewports). */
  padding-top: clamp(3rem, 6vw, 5rem);
  padding-bottom: clamp(3rem, 6vw, 5rem);
}
/* Single column at every width — the hero used to split into a second
   column holding the monitor/canvas illustration on wide viewports,
   but that illustration has been removed entirely, so the two-column
   split (and the empty second track it would now leave behind) is
   gone too. Only .hero-main remains, so this is effectively a plain
   wrapper at this point, kept in case the hero gains a second element
   again later. */
.hero-grid{
  display:grid;
  grid-template-columns: 1fr;
  align-items:center;
  gap: clamp(2rem, 5vw, 3.5rem);
  width:100%;
}
.hero-main{ min-width:0; text-align:center; }

/* ---------- Hero statement (redesigned) ----------
   Replaces the previous name + short bio line with a single large,
   casual statement (per reference), plus a compact location/school
   meta line and a pill CTA that scrolls straight to the work list.
   Intentionally sans-serif (the site's --font-body) rather than the
   display serif used for headings elsewhere — this is a one-off, more
   casual treatment for this one element, not a site-wide type change.
   Capitalization follows normal sentence case, written directly in the
   markup rather than forced via CSS. */
.hero-statement{
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(1.7rem, 1.5vw + 1.3rem, 2.7rem);
  line-height: 1.4;
  letter-spacing: -0.005em;
  color: var(--ink);
  max-width: 46rem;
  margin-inline: auto;
}
/* Two small "in progress" moments inside the statement: "fine arts" gets
   a hand-painted highlight swipe (a rough, brush-shaped block of accent
   color scaling in behind the words), and "product design" types itself
   out letter by letter with a blinking caret. Both trigger off the same
   [data-reveal] scroll-into-view class main.js already adds to the
   parent statement, so no extra JS is needed — reduced-motion users get
   the site-wide override further up this file, which collapses these
   to their instant end state. */
/* "fine arts" uses the real Brittany Signature font, not a system font
   stack: the glyph outlines were pre-extracted (via fontTools, offline,
   at build time) into the path data embedded below, split into their
   real contours exactly as the font defines them (e.g. the "i" body
   plus its separate dot terminal are two distinct contours in the font
   itself). Each visible contour (.hl-stroke, filled with that exact
   outline) is masked by an SVG <mask> containing a stroked copy of the
   same outline; main.js reveals that stroke via stroke-dasharray /
   stroke-dashoffset, which traces the contour's own boundary in order
   rather than growing an axis-aligned rectangle over it (the earlier
   approach, which fragmented non-monotonic swashes like the "f"). The
   underlying fill is never altered, so what's ultimately shown is
   always the unmodified font geometry. A small dot (.hl-brush-tip)
   rides the leading edge of whichever contour is currently being
   revealed, standing in for a brush. The real word lives in an
   .sr-only span alongside it so screen readers/copy-paste still get
   plain text, and the SVG height ties to 1em so it scales with the
   rest of the sentence automatically. */
.hero-statement .hl-paint{
  display:inline-flex;
  align-items:flex-end;
  /* The viewBox includes room above and below the font's own baseline
     (ascender swashes, descender loops), so the glyphs' true baseline
     sits partway down the box rather than at its bottom edge. This
     offset pulls the box down so that inner baseline lines up with the
     surrounding text's baseline instead of floating high above it. */
  vertical-align: -0.66em;
  /* At 1.65em tall against the sentence's 1.4 line-height, this inline
     box was taller than a normal line box — inflating just the line it
     sits on and making the gap around that line read as bigger than the
     even spacing between every other line. Because vertical-align:-0.66em
     pushes the box DOWN relative to baseline, nearly all of that excess
     falls below the baseline (widening the gap to the line beneath this
     one specifically), not above it — so the correction is asymmetric:
     pull up mostly via margin-bottom, leave margin-top close to
     untouched. Neither touches the vertical-align baseline math above or
     clips the SVG's own rendered content (overflow stays visible). */
  margin-top: -0.03em;
  margin-bottom: -0.22em;
}
.hero-statement .hl-paint-svg{
  height: 1.65em;
  width:auto;
  display:block;
  overflow:visible;
}
.hero-statement .hl-stroke{
  fill: var(--accent-ink);
  /* A modest stroke in the same color bulks up the letterforms'
     weight without touching the underlying font geometry at all —
     the path data (proportions, curves, spacing) is untouched; this
     just adds a bit of ink around the exact same shape. */
  stroke: var(--accent-ink);
  stroke-width: 24;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.hero-statement .hl-brush-tip{
  fill: var(--accent-ink);
  opacity:0;
  transition: opacity .3s var(--ease);
}
/* The characters themselves are revealed by JS (main.js), one at a time,
   directly into .hl-type-text's real text content — not a CSS width/ch
   trick — so spacing always matches the text's true rendered width
   exactly, with no estimation gap before or after it. .hl-caret is a
   separate small bar that only blinks for the duration of typing, then
   fades away rather than sitting there afterward. */
.hero-statement .hl-type{ color: var(--accent); white-space: nowrap; }
.hero-statement .hl-type-text{ white-space:nowrap; }
/* The trailing comma right after "product design" belongs to the
   sentence, not the highlighted word, so it's reset back to the body
   ink color rather than inheriting .hl-type's accent pink. It still
   lives inside .hl-type (rather than as a separate sibling text node)
   purely so the parent's white-space:nowrap keeps it glued to the
   caret — on narrow/mobile widths, a comma sitting just outside that
   nowrap span could otherwise get orphaned onto its own line. */
.hero-statement .hl-type-comma{ color: var(--ink); }
.hero-statement .hl-caret{
  display:inline-block;
  width: .07em;
  height: .95em;
  margin-left: .04em;
  vertical-align: -0.12em;
  background: var(--accent);
  opacity:0;
}
.hero-statement .hl-type.is-typing .hl-caret{
  opacity:1;
  animation: hl-caret-blink .8s steps(1) infinite;
}
.hero-statement .hl-type.is-done .hl-caret{
  opacity:0;
  animation:none;
  transition: opacity .4s var(--ease);
}
@keyframes hl-caret-blink{ 50%{ opacity:0; } }
.hero-meta-lines{
  margin-top: clamp(1.4rem, 2vw, 1.8rem);
  text-transform: lowercase;
  font-size: var(--fs-lede);
  color: var(--ink);
}
.hero-meta-lines p + p{ margin-top: .3rem; color: var(--ink-faint); }
.hero-meta-lines a{
  transition: color .25s var(--ease);
}
.hero-meta-lines a:hover{ color: var(--accent-ink); }
.arrow-nudge{
  display:inline-block;
  font-size: .85em;
  transition: transform .25s var(--ease);
}
.hero-meta-lines a:hover .arrow-nudge{ transform: translate(2px,-2px); }

.hero-cta{
  display:inline-flex;
  align-items:center;
  gap:.6em;
  margin-top: clamp(1.8rem, 2.8vw, 2.4rem);
  padding: .9em 1.7em;
  border-radius: 100px;
  border: 1px solid var(--line-strong);
  background: var(--card);
  color: var(--ink);
  font-size: var(--fs-body);
  font-weight: 500;
  text-transform: lowercase;
  transition: background .3s var(--ease), transform .3s var(--ease), border-color .3s var(--ease);
}
.hero-cta:hover{ background: var(--accent-soft); border-color: var(--accent-ink); transform: translateY(-2px); }
/* The arrow is a child of .hero-cta, so it already rises with the whole
   button on hover. Giving it its own small extra lift in the *same*
   direction (rather than the opposite one, which used to cancel the
   button's own motion out to ~zero) keeps it moving together with
   "See my work" instead of looking static against it. */
.hero-cta .arrow-nudge--down{ transition: transform .3s var(--ease); }
.hero-cta:hover .arrow-nudge--down{ transform: translateY(-2px); }

/* Anchor target for the CTA above — offset so the section doesn't land
   flush under the sticky header when scrolled to directly. */
.work-list{ scroll-margin-top: 6rem; }

/* ---------- Hero canvas: draw your own ----------
   Replaces the old brush-animation with a blank canvas visitors can
   actually draw on themselves (mouse or touch), instead of watching a
   fixed animation play. The canvas element's pixel buffer is sized to
   match its own rendered box in main.js (devicePixelRatio-aware, so
   strokes stay crisp), and a small "Clear" link resets it. The site's
   existing custom paintbrush cursor (applied globally in the universal
   cursor rule further down) already shows over this element, so no
   extra cursor styling is needed here — it just happens to fit.

   Framed inside the same .device--mac laptop mockup used for case-study
   prototypes (shell, traffic-light bar, base) rather than sitting in a
   plain dashed box, so the drawing area reads as "a laptop screen" —
   reusing the existing component instead of a bespoke frame. Only the
   screen's aspect-ratio and the canvas's own fill behavior need
   overriding here, since the shared .device-mac__screen rule normally
   expects an img/video (which has its own intrinsic size) rather than
   a canvas (which doesn't, so it needs an explicit ratio to sit in). */
.hero-note{
  position: relative;
  justify-self: center;
  width: 100%;
  max-width: 31rem;
}
/* Below the two-column breakpoint the hero stacks into a single column,
   so the monitor mockup sits directly under the hero paragraph. Centering
   it there (the desktop behavior, balanced against the left text column)
   left it floating with mismatched edges against that full-width text.
   Left-align it instead so its left edge matches the text above it. */
@media (max-width: 899px){
  .hero-note{ justify-self: start; }
  .device.hero-laptop{ width: 82%; }
}
.hero-canvas-wrap{
  display:flex;
  flex-direction:column;
  gap: .8rem;
}
.device.hero-laptop{ position:relative; width:92%; max-width:none; }
.hero-laptop .device-monitor__screen{ aspect-ratio: 8/5; }

/* A little hand-drawn signature on the canvas frame — a clothing tag
   dangling off the corner, nodding to the thrifting/fashion side
   mentioned on the About page rather than a generic device mockup.
   Pure SVG so it stays crisp at any size; a slow sway keeps it reading
   as something actually hanging rather than a static sticker. */
.hero-tag{
  position:absolute;
  top: .21rem;
  left: -3.24rem;
  width: 13.1rem;
  height:auto;
  z-index: 2;
  overflow: visible;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.18));
  transform-origin: 1% 1%;
  animation: hero-tag-sway 5s ease-in-out infinite;
}
.hero-tag-string{
  fill:none;
  stroke: var(--ink-faint);
  stroke-width: 1.6;
  stroke-linecap: round;
}
/* The main hanging cord gets a heavier stroke than the little knot tail
   so it reads as a proper looped ribbon instead of a thin thread. */
.hero-tag-ribbon{
  stroke-width: 2.2;
}
/* The small tied-off blob sitting right at the punch hole — reads as a
   loose knot rather than the string just stopping at the tag's edge. */
.hero-tag-knot{
  fill: var(--ink-faint);
  opacity: .85;
}
.hero-tag-body rect{
  fill: var(--card);
  stroke: var(--line-strong);
  stroke-width: 1.6;
}
/* A second tag peeking out from behind the front one, on the same
   string — a nod to how real stacked product tags look, and an excuse
   for a second, more playful pass at the wordmark in the display font. */
.hero-tag-body--back rect{
  fill: var(--accent-soft);
  stroke: rgba(23,23,27,0.15);
  stroke-width: 1.2;
}
.hero-tag-script{
  font-family: var(--font-display);
  font-style: italic;
  font-size: 6.5px;
  fill: var(--accent);
}
/* The back tag's own "ONE OF ONE" — same label as the front tag, but in
   ink instead of accent so it still reads clearly against the pink fill. */
.hero-tag-label--back{
  font-size: 4px;
  fill: var(--ink-soft);
}
.hero-tag-hole{
  fill: var(--paper);
  stroke: var(--line-strong);
  stroke-width: 1.6;
}
.hero-tag-logo{
  opacity: .88;
}
.hero-tag-label{
  font-family: var(--font-body);
  font-size: 5px;
  font-weight:700;
  letter-spacing: .075em;
  fill: var(--accent);
}
.hero-tag-rule{
  stroke: var(--line);
  stroke-width: .6;
}
.hero-tag-url{
  font-family: var(--font-body);
  font-size: 3.4px;
  letter-spacing: .03em;
  fill: var(--ink-faint);
}
@keyframes hero-tag-sway{
  0%, 100%{ transform: rotate(-3deg); }
  50%{ transform: rotate(3deg); }
}
@media (prefers-reduced-motion: reduce){
  .hero-tag{ animation:none; }
}
@media (max-width: 640px){
  .hero-tag{ width: 8.5rem; left: -2.1rem; }
}
.hero-canvas{
  position:absolute;
  inset:0;
  display:block;
  width:100%;
  height:100%;
  touch-action: none;
}
.hero-canvas-ui{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap: 1rem;
  margin-top: .6rem;
  padding-top: .6rem;
  border-top: 1px solid var(--line);
}
.hero-canvas-hint{
  font-size: var(--fs-micro);
  letter-spacing:.1em;
  text-transform:uppercase;
  color: var(--ink-faint);
}
.hero-canvas-clear{
  font-size: var(--fs-micro);
  letter-spacing:.1em;
  text-transform:uppercase;
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: .2em;
  transition: color .25s var(--ease);
}
.hero-canvas-clear:hover{ color: var(--accent); }

/* Mobile only: the hero's own bottom padding plus the work list's full
   section-level top margin stack on top of each other below the hero
   text, reading as an oversized gap before the first project. Tightened
   just for this transition, not the hero or the work list's spacing in
   general elsewhere on the page. */
@media (max-width: 640px){
  .hero{ padding-bottom: clamp(1.6rem, 5vw, 2.4rem); }
  .work-feature-list{ margin-top: clamp(2.2rem, 7vw, 3rem); }
}

/* ---------- Selected Work: featured case-study imagery ----------
   Each project pairs a moderately large, uncropped image (own intrinsic
   aspect ratio, roughly half the content grid rather than full-bleed)
   with its title and role/year/focus metadata sitting beside it — an
   editorial split rather than an image stacked above a paragraph, and
   rather than a full-width hero. This keeps each image clearly the
   dominant element in its row without any one project consuming the
   full viewport height. Rows alternate image left/right for rhythm;
   the reading order (title → meta → description) stays the same on
   every project so scanning down the list stays predictable. */
.work-feature-list{
  margin-top: var(--space-section);
  display:flex;
  flex-direction:column;
  gap: clamp(3.5rem, 6vw, 6rem);
}
.work-feature{
  display:flex;
  align-items:center;
  gap: clamp(2rem, 5vw, 4.5rem);
  color:inherit;
}
.work-feature:nth-child(even){ flex-direction: row-reverse; }
.work-feature-media{
  position:relative;
  display:block;
  flex: 1 1 52%;
  max-width:52%;
  border-radius: var(--radius);
  overflow:hidden;
  transition: box-shadow .5s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .work-feature:hover .work-feature-media{ box-shadow: var(--shadow); }
}
.work-feature-media img{
  width:100%;
  height:auto;
  display:block;
  transition: transform 1.1s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .work-feature:hover .work-feature-media img{ transform: scale(1.035); }
}
/* Small "View case study" cue in the image's corner — the clearest
   possible signal the whole image is clickable, without laying a heavy
   overlay across the work. Hidden until hover on pointer/hover-capable
   devices; shown at rest on touch, since there's no hover moment to
   reveal it there. */
.work-feature-view{
  position:absolute;
  left: clamp(.8rem, 2vw, 1.4rem);
  bottom: clamp(.8rem, 2vw, 1.4rem);
  display:inline-flex;
  align-items:center;
  gap:.55em;
  padding:.6em 1.05em;
  border-radius:100px;
  background: color-mix(in srgb, var(--paper) 90%, transparent);
  color: var(--ink);
  font-size: var(--fs-micro);
  font-weight:600;
  letter-spacing:.06em;
  text-transform: uppercase;
  backdrop-filter: blur(6px);
}
.work-feature-view svg{ width:13px; height:13px; stroke:currentColor; }
@media (hover:hover) and (pointer:fine){
  .work-feature-view{
    opacity:0;
    transform: translateY(10px);
    transition: opacity .45s var(--ease), transform .45s var(--ease);
  }
  .work-feature:hover .work-feature-view{ opacity:1; transform:none; }
}
.work-feature-body{
  flex: 1 1 auto;
  min-width:0;
}
.work-feature-label{
  display:flex;
  align-items:baseline;
  gap:.9em;
  margin-bottom: .9rem;
  transition: transform .5s var(--ease);
}
.work-feature:hover .work-feature-label{ transform: translateX(.3rem); }
.work-feature-index{
  font-family: var(--font-display);
  font-size: .95rem;
  color: var(--ink-faint);
  transition: color .35s var(--ease);
}
.work-feature:hover .work-feature-index{ color: var(--accent); }
.work-feature-eyebrow{
  font-size: var(--fs-micro);
  letter-spacing:.14em;
  text-transform: uppercase;
  color: var(--accent-ink);
  font-weight:600;
}
.work-feature-title{
  display:block;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 1.8vw, 2.15rem);
  font-weight:400;
  letter-spacing:-0.01em;
  line-height:1.15;
  transition: color .35s var(--ease);
}
.work-feature:hover .work-feature-title{ color: var(--accent-ink); }
.work-feature-meta{
  display:block;
  margin-top:.6rem;
  font-size: var(--fs-small);
  color: var(--ink-faint);
}
.work-feature-desc{
  display:block;
  margin-top:.9rem;
  color: var(--ink-soft);
  max-width:38ch;
  font-size: var(--fs-small);
}

@media (max-width: 860px){
  .work-feature, .work-feature:nth-child(even){
    flex-direction:column;
    align-items:stretch;
    gap: 1.5rem;
  }
  .work-feature-media{ flex:1 1 auto; max-width:100%; }
  .work-feature-desc{ max-width:56ch; }
}

/* ==========================================================================
   Section headings
   ========================================================================== */
.section-head{
  display:flex;
  flex-direction:column;
  gap: 1rem;
  margin-bottom: var(--space-block);
  max-width: 62ch;
}
.section-head h2{
  font-family: var(--font-display);
  font-weight:400;
  font-size: var(--fs-h1);
  letter-spacing:-0.01em;
  line-height:1.08;
}
.section-head p{ color: var(--ink-soft); font-size: var(--fs-lede); }

/* ==========================================================================
   Buttons / links
   ========================================================================== */
.btn{
  display:inline-flex;
  align-items:center;
  gap:.6rem;
  font-size: var(--fs-small);
  font-weight:600;
  letter-spacing:.02em;
  padding: .95em 1.7em;
  border-radius: 100px;
  border: 1px solid var(--ink);
  transition: background .35s var(--ease), color .35s var(--ease), transform .35s var(--ease), border-color .35s var(--ease);
}
.btn--solid{ background: var(--ink); color: var(--paper); }
.btn--solid:hover{ background: var(--accent); border-color: var(--accent); transform: translateY(-2px); }
.btn--ghost:hover{ border-color: var(--accent); color: var(--accent-ink); transform: translateY(-2px); }

.link-inline{
  position:relative;
  color: var(--accent-ink);
  font-weight:600;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat:no-repeat;
  background-position: 0 100%;
  background-size: 100% 1px;
  padding-bottom:.05em;
  transition: background-size .3s var(--ease);
}
.link-inline:hover{ background-size: 0% 1px; }
/* Keeps the trailing arrow glued to the last word before it (e.g.
   "photography →") so a line wrap never strands the arrow alone on its
   own line — the same soft-wrap problem as the hero statement's comma,
   fixed the same way: group them in a no-wrap span. */
.link-inline-nowrap{ white-space:nowrap; }

/* ==========================================================================
   Footer
   ========================================================================== */
.site-footer{
  border-top: 1px solid var(--line);
  padding-block: clamp(2rem, 1.6vw + 1.2rem, 3.6rem); /* smaller, dedicated footer padding, not the full section rhythm used elsewhere */
  background: var(--paper-dim);
}
.footer-cta{ text-align:center; }
.footer-cta h2{
  font-family: var(--font-display);
  font-weight:400;
  font-size: clamp(1.6rem, 2.6vw, 2.6rem);
  letter-spacing:-0.01em;
}
.footer-cta .email-link{
  display:inline-block;
  margin-top: 1rem;
  font-family: var(--font-display);
  font-style:italic;
  font-size: clamp(1.1rem, 1.8vw, 1.5rem);
  color: var(--accent-ink);
  border-bottom: 1px solid var(--line-strong);
  transition: border-color .3s var(--ease), color .3s var(--ease);
}
.footer-cta .email-link:hover{ border-color: var(--accent); color: var(--accent); }

/* Three-column grid instead of flex space-between: with unequal-width
   siblings (a small logo, three icons, a copyright line), space-between
   only ever coincidentally centers the middle group. A grid with equal
   outer columns guarantees the social icons sit at the true horizontal
   center every time, regardless of how wide the logo or copyright text
   are on a given page. */
.footer-bottom{
  margin-top: clamp(1.4rem, 2.6vw, 2.2rem);
  padding-top: clamp(1rem, 1.6vw, 1.3rem);
  border-top: 1px solid var(--line);
  display:grid;
  grid-template-columns: 1fr auto 1fr;
  align-items:center;
  gap: 1rem 1.5rem;
}
.footer-bottom .footer-brand{ justify-self:start; }
.footer-bottom .social-row{ justify-self:center; }
.footer-bottom .footer-meta{ justify-self:end; text-align:right; }
@media (max-width: 600px){
  .footer-bottom{
    grid-template-columns: 1fr;
    justify-items:center;
    text-align:center;
    gap: 1.2rem;
  }
  .footer-bottom .footer-brand,
  .footer-bottom .social-row,
  .footer-bottom .footer-meta{
    justify-self:center;
  }
}
.footer-brand{
  display:flex;
  align-items:center;
  transition: opacity .25s var(--ease);
}
.footer-brand img{
  display:block;
  height: 1.9rem;
  width:auto;
}
.footer-brand:hover{ opacity:.75; }
.footer-meta{
  color: var(--ink-faint);
  font-size: var(--fs-micro);
  letter-spacing:.04em;
}
.social-row{ display:flex; gap: .85rem; }
.social-row a{
  width: 2.4rem; height:2.4rem;
  border-radius:50%;
  border: 1px solid var(--line-strong);
  display:flex; align-items:center; justify-content:center;
  transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease);
}
/* Previously swapped to a solid --ink background with the icon flipped to
   --paper on hover — but the accompanying `fill: var(--paper)` also
   overrode each icon's own fill="none", so the rect/circle shapes behind
   the strokes filled in solid and the icon's own details (the Instagram
   lens, the LinkedIn "in") disappeared into one flat blob. The hover now
   enhances instead of covers: a soft accent-tinted background, an accent
   border, and a small lift/scale, while the icon itself only shifts to
   the accent color and keeps its normal fill:none the whole time. */
.social-row a:hover{
  background: var(--accent-soft);
  border-color: var(--accent);
  transform: translateY(-2px) scale(1.06);
}
.social-row a:hover svg{ stroke: var(--accent-ink); color: var(--accent-ink); }
.social-row svg{
  width:16px; height:16px;
  stroke: var(--ink); color: var(--ink);
  transition: stroke .3s var(--ease), color .3s var(--ease);
}

.back-to-top{
  position: fixed;
  right: clamp(1rem, 3vw, 2.4rem);
  bottom: clamp(1rem, 3vw, 2.4rem);
  width: 3rem; height:3rem;
  border-radius:50%;
  background: var(--ink);
  color: var(--paper);
  display:flex; align-items:center; justify-content:center;
  opacity:0; pointer-events:none;
  transform: translateY(10px);
  transition: opacity .35s var(--ease), transform .35s var(--ease), background .3s var(--ease);
  z-index: 400;
}
.back-to-top.is-visible{ opacity:1; pointer-events:auto; transform:none; }
.back-to-top:hover{ background: var(--accent); }
.back-to-top svg{ width:16px; height:16px; stroke: var(--paper); }

/* ==========================================================================
   Project case-study page
   ========================================================================== */
.case-hero{ padding-top: clamp(2.5rem, 5vw, 4rem); }
.case-hero-cover{
  margin-top: clamp(2rem, 4vw, 3.2rem);
}
.case-hero-cover img{ width:100%; height:auto; }
.case-title{
  font-family: var(--font-display);
  font-weight:400;
  font-size: var(--fs-hero);
  line-height:1.03;
  letter-spacing:-0.02em;
  max-width: 18ch;
}
/* Lenovo's title ("Redesigning product discovery for Gen Z") is short
   enough to comfortably fit one line at this font size on typical
   desktop widths — the shared 18ch cap (sized for the other, longer
   case titles) was wrapping it to two lines unnecessarily. Scoped to
   this page only so Opportuna/CClubs/Rovin keep their current wrap. */
.page-lenovo .case-title{ max-width: none; }

/* Role / Team / Timeline: a compact, balanced group centered under the
   hero cover, not a full-bleed grid stretched edge to edge. Each column
   holds a shared minimum width so all three read as one even group; the
   group itself centers with equal whitespace on both sides. Shared
   across every case study page for a consistent presentation. */
.case-meta{
  margin-top: clamp(2.4rem, 4vw, 3.6rem);
  display:flex;
  flex-wrap:wrap;
  justify-content:center;
  gap: clamp(2rem, 5vw, 4.5rem);
  padding-top: clamp(1.6rem, 3vw, 2.2rem);
  border-top: 1px solid var(--line);
}
.case-meta > div{
  text-align:center;
  min-width: 8rem;
}
.case-meta dt{
  font-size: var(--fs-micro);
  text-transform:uppercase;
  letter-spacing:.14em;
  color: var(--ink-faint);
  font-weight:600;
  margin-bottom:.6rem;
}
.case-meta dd{
  margin:0;
  font-size: var(--fs-body);
  color: var(--ink);
}
.case-meta dd + dd{ margin-top:.15rem; }
@media (max-width: 640px){
  .case-meta{
    flex-direction:column;
    /* Each project page overrides .case-meta > div's text-align to
       left (see the .page-lenovo/.page-opportuna/.page-cclubs rules
       below), but this container was still centering each div as a
       flex item by its own shrink-to-fit width — since "Role",
       "Team", and "Timeline" are different widths, that left each
       block's text starting at a different x position even though
       the text itself was left-aligned. align-items:flex-start keeps
       every block flush to the same left edge instead. */
    align-items:flex-start;
    gap: clamp(1.2rem, 4vw, 1.8rem);
  }
}

.case-block{
  padding-block: var(--space-block);
  border-top: 1px solid var(--line);
}
.case-block-grid{
  display:grid;
  grid-template-columns: minmax(0,.9fr) minmax(0,1.6fr);
  gap: clamp(2rem, 5vw, 5rem);
}
@media (max-width: 860px){
  .case-block-grid{ grid-template-columns: 1fr; gap: 1.5rem; }
}
.case-block h3{
  font-family: var(--font-display);
  font-weight:400;
  font-size: var(--fs-h2);
  line-height:1.15;
}
/* Was scoped to .case-block .body-copy, so any body-copy paragraph
   sitting inside a .decision section instead (Decisions 01-04 across
   all three case studies) or .case-block-grid (home page) never
   matched and fell back to full-dark ink instead of this soft gray —
   made unconditional so every body-copy paragraph gets it regardless
   of its container. */
.body-copy{ color: var(--ink-soft); font-size: var(--fs-body); max-width: 68ch; }
.body-copy + .body-copy{ margin-top: 1.1em; }
.body-copy strong{ color: var(--ink); font-weight:600; }

/* Every case-study page (Lenovo, Opportuna, CClubs) runs its content
   through .case-col, a single fixed-width column that headings, images,
   and .pull-question all fill edge to edge. .body-copy's own 68ch
   readability cap is narrower than that column, though, so plain
   paragraphs sat visibly short of everything around them instead of
   sharing one consistent measure. Removed the cap specifically inside
   .case-col so every case-study paragraph fills the same column as its
   neighbors; .body-copy's 68ch default stays intact everywhere else
   (e.g. the About page bio), where there's no wider sibling forcing a
   match. */
.case-col .body-copy{ max-width:none; }

/* NDA-protected case study: a quiet, centered card standing in for
   content that can't be shown, so the page still reads as a deliberate,
   finished case study rather than an empty placeholder. */
.nda-card{
  margin-top: clamp(2rem, 4vw, 2.8rem);
  padding: clamp(2rem, 5vw, 3rem);
  text-align:center;
  background: var(--card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}
.nda-card-icon{ width: 2.6rem; height: 2.6rem; margin-inline:auto; color: var(--ink-faint); }
.nda-card h2{ margin-top: 1.1rem; font-family: var(--font-display); font-weight:400; font-size: var(--fs-h2); letter-spacing: -0.01em; }
.nda-card .body-copy{ margin-top: .8rem; margin-inline:auto; max-width: 42ch; }
.nda-card .body-copy strong{ display:block; margin-top: .5rem; }
.nda-card .email-link{
  display:inline-block; margin-top: 1.4rem;
  font-family: var(--font-display); font-style:italic;
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  color: var(--accent-ink);
  border-bottom: 1px solid var(--line-strong);
  transition: border-color .3s var(--ease), color .3s var(--ease);
}
.nda-card .email-link:hover{ border-color: var(--accent); color: var(--accent); }

/* ---------- Editorial image system ----------
   Rule: no cards. Images are never wrapped in a bordered box, a filled
   background, a drop shadow, or rounded "card" corners — every image on
   the site sits directly on the page's own background, at its true
   aspect ratio, full quality, uncropped and undistorted. Organization
   comes entirely from spacing, alignment, and width, not from framing.

   Rule: never crop. Every image keeps width:100%/height:auto inside its
   frame, so the complete image is always visible at its true aspect
   ratio — a portrait mockup and a landscape dashboard simply end up
   different heights, which is expected, not a bug.

   Consistency instead comes from WIDTH:
   - A standalone .figure gets a deliberate max-width for its context
     (small mockup vs. wide diagram) and is centered with margin:auto.
   - Figures inside .compare-item / .option-card / .shot-cluster fill
     an equal-width grid column, so every image in that row reads at
     the same width even though heights vary with content.
   - Exception: .option-card figures share a fixed height (see the
     option-card-specific override below) so a row of option cards
     reads as one matched set, since they're compared side by side as
     interchangeable choices rather than read individually like other
     figures — width is left to scale automatically off that shared
     height, so nothing is ever cropped or boxed in to get there. */
.figure{
  display:block;
  width:100%;
  max-width: 23rem;
  margin-top: clamp(1.4rem, 2.5vw, 2.1rem);
  margin-inline:auto;
}
.figure img, .figure video{
  display:block;
  width:100%;
  height:auto;
}
/* A small lift on hover for standalone project imagery — the whole
   figure (image plus any overlaid pin-markers, which are its children
   and move with it) rises 3px rather than the image alone scaling,
   so annotated screenshots never drift out of sync with their markers.
   Opted out for option-card figures below, since those sit in a fixed,
   clipped card frame where a shift would just peek the card's own
   background at the edge instead of reading as a lift. */
@media (hover:hover) and (pointer:fine){
  .figure{ transition: transform .4s var(--ease); }
  .figure:hover{ transform: translateY(-3px); }
  .option-card .figure:hover{ transform:none; }

  /* Case study pages only: restrict the hover-lift to just the accent
     (pink-bordered) "after"/"final"/"chosen" images — the highlighted
     comparison result — instead of every screenshot on the page.
     Covers every pink-border pattern used across the three pages:
     Lenovo's .figure.is-after and .compare-grouped-fig.is-after,
     Opportuna's chosen ideation option, and CClubs' .figure-final.
     Decision 02's .decision-compact-row .is-after thumbnails (the
     right-side "After" images in the top two-column comparison rows)
     are intentionally excluded below — no hover-lift on those. */
  .page-lenovo .figure:hover,
  .page-opportuna .figure:hover,
  .page-cclubs .figure:hover{
    transform:none;
  }
  .page-lenovo .figure.is-after,
  .page-lenovo .compare-grouped-fig.is-after,
  .page-opportuna .idea-option.is-chosen .figure,
  .page-cclubs .figure-final{
    transition: transform .4s var(--ease);
  }
  .page-lenovo .figure.is-after:hover,
  .page-lenovo .compare-grouped-fig.is-after:hover,
  .page-opportuna .idea-option.is-chosen .figure:hover,
  .page-cclubs .figure-final:hover{
    transform: translateY(-3px) !important;
  }

  /* Decision 02's entire .decision-compact-row (both the plain Before
     side and the accent-bordered After side) stays put on hover —
     pinned down explicitly, !important, so no other rule can apply a
     lift to either column of these two-image comparison rows. */
  .page-lenovo .decision-compact-row .pin-figure,
  .page-lenovo .decision-compact-row .pin-figure:hover{
    transform:none !important;
  }

  /* The chosen option-card on Lenovo/CClubs Decision 01 carries its
     pink border on the whole card, not just the image inside — so the
     hover-lift applies to the whole card too (image, spec chip, and
     text all rise together) rather than just the screenshot moving
     inside a card that stays put. */
  .page-lenovo .option-card.is-chosen,
  .page-cclubs .option-card.is-chosen{
    transition: border-color .3s var(--ease), transform .4s var(--ease);
  }
  .page-lenovo .option-card.is-chosen:hover,
  .page-cclubs .option-card.is-chosen:hover{
    transform: translateY(-3px) !important;
  }
}
.figure--sm{ max-width: 17rem; }
.figure--lg{ max-width: 42rem; }
/* Opportuna/CClubs' Ideation figures (low-fi comparison, flow map) sat at
   the same default 23rem cap as smaller single-screenshot figures
   elsewhere, which read as undersized for wide reference images. Scoped
   to .case-block-grid, which only Opportuna and CClubs use — Lenovo's
   case-col figures are unaffected. */
.case-block-grid .figure{ max-width: 30rem; }
/* On Lenovo, the final-prototype overview sits directly above the device
   row and should read as the same column width, not a narrower card
   floating above a wider one. Scoped to .case-col so Opportuna's use of
   .figure--lg (in its own two-column layout) is unaffected. */
.case-col .figure--lg{ max-width:none; }

/* Wide banners / flow diagrams meant to read at full column width. */
.figure--fill{ max-width:none; }

.figure-caption{
  margin-top: .9rem;
  font-size: var(--fs-small);
  color: var(--ink-faint);
  text-align:center;
  max-width: 60ch;
  margin-inline:auto;
}
/* The lenovo.com browser-frame screenshot on the Problem section is a
   plain (non --lg/--fill) .figure, so it's capped at the standard
   23rem figure width — narrower than the caption's own generic 60ch
   cap, which let the caption below it wrap wider than the image it's
   describing. Matched the caption to that same 23rem so the two share
   one width instead of the caption overhanging both sides. */
.browser-frame + .figure-caption{ max-width: 23rem; text-align:left; }

/* Grid contexts: the column supplies equal width, image supplies its
   own natural height — no forced aspect ratio, no cropping. */
.compare-item .figure,
.option-card .figure,
.shot-cluster .figure{
  max-width:none;
  width:100%;
  margin-top:0;
}
/* Option cards need a shared image height so all three read as one
   matched set, without ever cropping the source screenshot or boxing it
   in with a background. Each image is set directly to a fixed height
   with width left to auto, so it scales at its own true aspect ratio
   with no letterboxing needed — three different-shaped screenshots
   still land at the same height, sitting directly on the card's own
   surface. */
.option-card .figure{
  height: clamp(11rem, 22vw, 14.5rem);
  display:flex;
  align-items:center;
  justify-content:center;
}
.option-card .figure img{
  height:100%;
  width:auto;
  max-width:100%;
  /* object-fit:contain is the safety net for when a narrow grid column
     forces max-width to kick in alongside the explicit height:100% —
     without it, the browser honors both constraints literally and
     stretches/squishes the image instead of preserving its aspect
     ratio, which is what was happening on CClubs' Option 1/2 cards. */
  object-fit: contain;
}
/* Lenovo-only: "01 Simplified Cards" option images fill the full card
   width edge to edge, with height following each image's own true
   aspect ratio rather than being forced into a shared fixed-height box
   — nothing cropped, nothing letterboxed, nothing stretched. The three
   cards themselves still land at the same overall height as each other
   (via .option-grid's grid-stretch plus .option-body's flex:1, both
   already in place), so a taller image just leaves less room below it
   for the option-body text rather than the cards drifting out of sync.
   Scoped to .page-lenovo so CClubs' identical option-card markup keeps
   the current site-wide treatment untouched. */
.page-lenovo .option-card .figure{
  width:100%;
  height:auto;
  display:block;
}
.page-lenovo .option-card .figure img{
  display:block;
  width:100%;
  height:auto;
}
/* On mobile, all three option cards now sit in one row instead of
   stacking full-width — card padding, badge/heading/list type, and gaps
   are all scaled down together so the row fits the screen without any
   single card growing wide enough to force a wrap. Scoped to
   .page-lenovo so CClubs' option-card grid keeps its own site-wide
   mobile behavior untouched. */
@media (max-width: 640px){
  .page-lenovo .option-grid{
    grid-template-columns: repeat(3, 1fr);
    gap: .5rem;
    max-width: 100%;
    margin-inline: 0;
  }
  .page-lenovo .option-body{
    padding: .55rem .5rem .7rem;
  }
  .page-lenovo .option-body .kicker{
    font-size: .55rem;
    letter-spacing:.04em;
  }
  .page-lenovo .option-body h4{
    margin-top:.3rem;
    font-size: .78rem;
    line-height:1.25;
  }
  .page-lenovo .option-body ul{
    margin-top:.4rem;
    gap:.25rem;
  }
  .page-lenovo .option-body li{
    font-size: .62rem;
    line-height:1.3;
    padding-left: .9em;
  }
}
/* CClubs: same fix as Lenovo above — the shared fixed-height, centered
   .option-card .figure box was letterboxing (visible empty space above/
   below) whenever an image's aspect ratio didn't exactly fill that box.
   Let each image fill the card width edge to edge instead, with height
   following its own true aspect ratio, so the card hugs the image with
   no gaps. Scoped to .page-cclubs so Opportuna (which doesn't use
   .option-card) is unaffected. */
.page-cclubs .option-card .figure{
  width:100%;
  height:auto;
  display:block;
}
.page-cclubs .option-card .figure img{
  display:block;
  width:100%;
  height:auto;
}
/* Same treatment as Lenovo's option-grid: all three cards sit in one
   row on mobile instead of stacking full-width, with card padding and
   badge/heading/list type scaled down together so the row fits without
   wrapping. Scoped to .page-cclubs so Opportuna (no option-card) and
   the desktop grid are untouched. */
@media (max-width: 640px){
  .page-cclubs .option-grid{
    grid-template-columns: repeat(3, 1fr);
    gap: .5rem;
    max-width: 100%;
    margin-inline: 0;
  }
  .page-cclubs .option-body{
    padding: .55rem .5rem .7rem;
  }
  .page-cclubs .option-body .kicker{
    font-size: .55rem;
    letter-spacing:.04em;
  }
  .page-cclubs .option-body h4{
    margin-top:.3rem;
    font-size: .78rem;
    line-height:1.25;
  }
  .page-cclubs .option-body ul{
    margin-top:.4rem;
    gap:.25rem;
  }
  .page-cclubs .option-body li{
    font-size: .62rem;
    line-height:1.3;
    padding-left: .9em;
  }
}

/* Groups of related supporting shots — replaces "one giant image after
   another" with a tidy, aligned cluster. Each item takes an equal-width
   column (grid default stretch) so mixed shots line up consistently. */
.shot-cluster{
  margin-top: clamp(1.4rem, 2.5vw, 2.1rem);
  display:grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: clamp(1rem, 2vw, 1.6rem);
  align-items:start;
}

/* Small brand/competitor logo strip, as an infinite horizontal
   marquee. .logo-marquee is the full-bleed, clipping viewport (same
   break-out-of-.wrap technique as the hero banners); .logo-marquee-
   track is the flex row that actually translates; it holds two
   identical .logo-marquee-set groups back to back. Because the second
   set is pixel-identical to the first (same logos, same gaps, same
   trailing spacer), animating the track exactly -50% always lands the
   second set precisely where the first one started — a seamless loop
   with no visible jump, reset, or gap. The second set is aria-hidden
   and its images carry empty alt text, since it exists purely for the
   animation and would otherwise double up screen-reader content. */
.logo-marquee{
  margin-top: clamp(1.2rem, 2vw, 1.8rem);
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, black 6%, black 94%, transparent);
  mask-image: linear-gradient(to right, transparent, black 6%, black 94%, transparent);
}
.logo-marquee-track{
  display:flex;
  align-items:center;
  width: max-content;
  animation: logo-marquee-scroll 34s linear infinite;
  /* Hints the browser to composite this on its own GPU layer instead of
     repainting every frame — without it, the continuous translateX can
     visibly stutter/skip on some browsers, especially while other
     images on the page are still decoding. */
  will-change: transform;
}
.logo-marquee-set{
  display:flex;
  align-items:center;
  flex-shrink:0;
  gap: clamp(2.25rem, 5.5vw, 4.25rem);
  /* Trailing spacer instead of a gap on the track: this keeps the
     spacing between the last logo of one set and the first logo of
     the next identical to the spacing everywhere else, and keeps the
     track's total width at exactly 2x one set's width — required for
     translateX(-50%) to line up perfectly. */
  padding-right: clamp(2.25rem, 5.5vw, 4.25rem);
}
.logo-marquee img{
  display:block;
  height: clamp(1.4rem, 2.6vw, 1.9rem);
  width:auto;
  max-width: 7.5rem;
  object-fit:contain;
  filter: grayscale(1);
  opacity:.62;
  transition: opacity .3s var(--ease), filter .3s var(--ease);
}
.logo-marquee img:hover{ opacity:1; filter:grayscale(0); }
@keyframes logo-marquee-scroll{
  from{ transform: translateX(0); }
  to{ transform: translateX(-50%); }
}
/* Respect reduced-motion: freeze the scroll and fall back to a single
   clean static row instead of a moving one. The duplicate set (already
   hidden from assistive tech) is also hidden visually so it doesn't
   just sit there motionless as a second copy. */
@media (prefers-reduced-motion: reduce){
  .logo-marquee{ overflow:visible; }
  .logo-marquee-track{ animation:none; width:auto; }
  .logo-marquee-set{
    flex-wrap:wrap;
    justify-content:center;
    padding-right:0;
  }
  .logo-marquee-set[aria-hidden="true"]{ display:none; }
}
.brand-strip{
  margin-top: clamp(2.6rem, 4.4vw, 3.5rem);
  display:flex; align-items:center;
  padding: clamp(.9rem, 2vw, 1.4rem) clamp(1.1rem, 2.4vw, 1.6rem);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  max-width: 24rem;
}
/* Lenovo's content spine (.case-col) is itself centered in the wrap, but a
   narrower max-width child still needs its own auto margins to sit centered
   rather than flush against the column's left edge. Scoped to .case-col so
   Opportuna's brand-strip (placed in a two-column grid cell) is untouched. */
.case-col .brand-strip{ margin-inline:auto; }
/* Opportuna's brand-strip lives in the second column of .case-block-grid
   and had the same missing-centering issue as Lenovo's — same fix,
   scoped so CClubs (no brand-strip) and Lenovo (already scoped above)
   are unaffected. */
.case-block-grid .brand-strip{ margin-inline:auto; }
.brand-strip img{ width:100%; height:auto; display:block; }

/* ==========================================================================
   Device frames — Mac + phone shells for prototype video playback.
   Pure CSS, no images. Every dimension is set in em against the frame's
   own clamp()-driven font-size, so bezel/dock/notch thickness, dot size
   and corner radii all scale together with the frame as one unit,
   whether it's shrinking with the viewport or sitting smaller in a
   multi-device composition.
   ========================================================================== */
.device{ width:100%; margin-inline:auto; }
.device-caption{
  margin-top: 1rem;
  text-align:center;
  font-size: var(--fs-small);
  color: var(--ink-faint);
}

.device--mac{
  max-width: 44rem;
  font-size: clamp(7px, 1.05vw, 15px);
}
.device-mac__shell{
  background: linear-gradient(180deg, color-mix(in srgb, var(--ink) 92%, #000 8%), var(--ink));
  border-radius: 1.1em 1.1em .35em .35em;
  padding: .55em .55em 0;
  box-shadow: var(--shadow);
}
.device-mac__bar{
  display:flex;
  align-items:center;
  gap: .5em;
  padding: .7em 1em;
}
.device-mac__dot{
  width: .62em; height:.62em;
  border-radius:50%;
  background: color-mix(in srgb, var(--paper) 22%, transparent);
}
.device-mac__screen{
  position:relative;
  overflow:hidden;
  border-radius: .4em .4em 0 0;
  background: var(--paper-dim);
}
.device-mac__screen video,
.device-mac__screen img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain; /* show the complete recording, never crop it */
  display:block;
}
.device-mac__base{
  position:relative;
  height: .85em;
  background: linear-gradient(180deg, var(--ink), color-mix(in srgb, var(--ink) 78%, #000 22%));
  border-radius: 0 0 .55em .55em;
}
.device-mac__base::after{
  content:"";
  position:absolute; left:50%; top:0; transform:translateX(-50%);
  width: 16%; height:.2em;
  background: rgba(0,0,0,.28);
  border-radius: 0 0 .3em .3em;
}

/* Desktop-monitor mockup for the home-page hero canvas: an even bezel
   around the screen (no laptop-style top bar/traffic lights), with a
   short stand and a flat foot beneath it instead of a merged keyboard
   deck. Kept as its own component rather than reusing .device--mac so
   the case-study laptop mockups elsewhere are unaffected. */
.device--monitor{
  max-width: 44rem;
  font-size: clamp(7px, 1.05vw, 15px);
}
.device-monitor__shell{
  background: var(--paper-dim);
  border: 1px solid var(--line-strong);
  border-radius: .6em;
  padding: .5em;
  box-shadow: var(--shadow-sm);
}
.device-monitor__screen{
  position:relative;
  overflow:hidden;
  border-radius: .2em;
  background: var(--paper);
  border: 1px solid var(--line);
}
.device-monitor__screen video,
.device-monitor__screen img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain; /* show the complete recording, never crop it */
  display:block;
}
.device-monitor__stand{
  width: 9%;
  height: 1.5em;
  margin-inline:auto;
  background: var(--paper-dim);
  border-inline: 1px solid var(--line-strong);
  clip-path: polygon(28% 0, 72% 0, 100% 100%, 0 100%);
}
.device-monitor__foot{
  width: 32%;
  height: .55em;
  margin-inline:auto;
  border-radius: 999px;
  background: var(--paper-dim);
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-sm);
}

.device--phone{
  max-width: 17rem;
  font-size: clamp(7px, 1.05vw, 15px);
}
.device-phone__shell{
  position:relative;
  background: linear-gradient(160deg, color-mix(in srgb, var(--ink) 92%, #000 8%), var(--ink));
  border-radius: 2.6em;
  padding: .55em;
  box-shadow: var(--shadow);
}
.device-phone__island{
  position:absolute;
  top: .95em; left:50%; transform:translateX(-50%);
  width: 30%; height: .5em;
  background: rgba(0,0,0,.55);
  border-radius: 100px;
  z-index:2;
}
.device-phone__screen{
  position:relative;
  overflow:hidden;
  border-radius: 2.1em;
  background: var(--paper-dim);
  aspect-ratio: 9/19.5;
}
.device-phone__screen video,
.device-phone__screen img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:contain; /* show the complete recording, never crop it */
  display:block;
}
.device-phone__home{
  position:absolute;
  bottom: .7em; left:50%; transform:translateX(-50%);
  width: 32%; height:.22em;
  border-radius:100px;
  background: color-mix(in srgb, var(--paper) 50%, transparent);
  z-index:2;
}

/* Multi-device compositions: desktop and mobile prototypes stack
   vertically, one composition after another, each centered in the
   content column with its own intentional scale — rather than
   competing for width side by side. */
.device-row{
  display:flex;
  flex-direction:column;
  align-items:center;
  gap: clamp(2.5rem, 5vw, 4rem);
  margin-top: clamp(1.8rem, 3vw, 2.6rem);
}
.device-row .device--mac{ max-width: 42rem; }
.device-row .device--phone{ max-width: 18rem; }
/* Lenovo's device-row: let each video define its own screen shape from
   its real intrinsic aspect ratio instead of a guessed fixed ratio, so
   the screen area always matches the actual recording exactly — no
   gaps, no stretching, nothing cropped, nothing floating outside the
   frame. Scoped to .device-row (Lenovo-only — Opportuna/CClubs present
   a single solo device each with their own tuned aspect-ratio and are
   left untouched). */
.device-row .device-mac__screen,
.device-row .device-phone__screen{
  aspect-ratio: auto;
}
.device-row .device-mac__screen video,
.device-row .device-phone__screen video{
  position:static;
  display:block;
  width:100%;
  height:auto;
  object-fit:initial;
}

/* ==========================================================================
   Pinned annotations v2 — rail layout.
   The image and its notes are separate flex columns of equal (stretched)
   height, so a dot at --y% on the image always lines up with its note at
   the same --y% in the rail beside it — the connection is spatial, not
   just numeric. A dashed leader line is free to cross the image (from
   the dot to the media column's edge); the note text itself lives
   entirely in the rail column, so it can never sit on top of the work.
   Both --x/--y are percentages, so the whole thing scales as one unit
   with the image at any width instead of relying on fixed coordinates.
   Below ~640px there's no room for a side rail, so the layout stacks:
   image on top, notes below as a plain list, dots still on the image.
   ========================================================================== */
.annotated{
  display:flex;
  align-items:stretch;
  gap: clamp(1rem, 2.5vw, 1.6rem);
  margin-inline:auto; /* centers a narrower standalone instance (e.g. the
    Consolidated Duplicates close-up); a no-op where .annotated already
    fills its grid column, so pin/dot/line/rail placement is unaffected */
}
.annotated-media{
  position:relative;
  flex: 1 1 auto;
  min-width:0;
}
.annotated-media img{
  width:100%; height:auto; display:block;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--accent);
}
.annotated-rail{
  position:relative;
  flex: 0 0 clamp(9.5rem, 21vw, 12.5rem);
}

/* Target-dot + straight leader line on the image, numbered marker +
   label out in the rail — modeled on the pin style already used on
   tiffanyquen.com's Lenovo before/afters, translated into this site's
   ink/cream/brass system instead of its original brand color. */
.pin{ --x:50%; --y:50%; }
.pin-dot{
  position:absolute;
  left: var(--x); top: var(--y);
  transform:translate(-50%,-50%);
  z-index:3;
  width: .68rem; height:.68rem;
  border-radius:50%;
  background: var(--paper);
  border: 2px solid var(--ink);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--paper) 75%, transparent);
}
.pin-line{
  position:absolute;
  top: var(--y);
  left: var(--x);
  right: 0;
  height: 0;
  border-top: 1.5px solid color-mix(in srgb, var(--ink) 60%, transparent);
  transform: translateY(-50%);
  z-index:2;
}

.pin-note{
  position:absolute;
  left:0; right:0;
  top: var(--y, 50%);
  transform: translateY(-50%);
  display:flex;
  align-items:flex-start;
  gap:.5rem;
  padding-left: .85rem;
}
.pin-note::before{
  content:"";
  position:absolute;
  left:0; top:.6rem;
  width:.6rem; height:1.5px;
  background: color-mix(in srgb, var(--ink) 55%, transparent);
}
.pin-note-num{
  flex-shrink:0;
  margin-top:.05rem;
  width: 1.3rem; height:1.3rem;
  border-radius:50%;
  background: var(--accent);
  color: var(--paper);
  display:flex; align-items:center; justify-content:center;
  font-size: .66rem; font-weight:700;
}
.pin-note p{ margin:0; font-size: var(--fs-micro); line-height:1.45; color: var(--ink-soft); }

@media (max-width: 640px){
  .annotated{ flex-direction:column; }
  .annotated-rail{ flex-basis:auto; margin-top: .25rem; }
  .pin-note{
    position:static;
    transform:none;
    padding-left:0;
    margin-top: .65rem;
  }
  .pin-note::before{ display:none; }
  .pin-line{ display:none; }
}

/* ---------- Consolidated content spine (Lenovo) ----------
   One consistent column that every section's text, images, and
   decisions align to, so nothing jogs left or right as the page
   scrolls. Centered in the viewport with balanced whitespace on both
   sides. Scoped as a standalone utility — doesn't touch the shared
   .case-block-grid/.decision layout other case studies still use. */
.case-col{
  max-width: 46rem;
  margin-inline: auto;
}
.case-col > .eyebrow{ margin-bottom: .9rem; }
/* Statement-style h2 (Key Decisions intro, Final Prototype intro) inside
   the content spine — same treatment already used inline on Lenovo's
   Final Prototype heading, promoted to a shared rule so every case-col
   h2 (including Opportuna/CClubs') renders consistently. */
.case-col h2{
  font-family: var(--font-display);
  font-weight:400;
  font-size: var(--fs-h1);
  letter-spacing:-0.01em;
  line-height:1.1;
  margin-top:.6rem;
}

/* Compact before/after + annotation: the "after" state is shown once,
   annotated in place, instead of repeating the same screenshot larger
   further down the page. */
.compare-annotated{
  display:grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.7fr);
  gap: clamp(1.25rem, 3vw, 2rem);
  align-items:start;
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}
@media (max-width: 560px){
  .compare-annotated{ grid-template-columns: 1fr; }
}
.compare-annotated .figure{ margin-top:0; max-width:none; }

/* Compact module for a decision that needs a smaller, more contained
   footprint than the standard full-column treatment. No card background
   or border around the module or its images — spacing alone (the
   max-width plus the gap between rows) keeps it feeling contained. */
.decision-compact{
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
  margin-inline:auto;
  max-width: 27rem;
}
.decision-compact-row{
  display:grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.6rem;
}
.decision-compact-row + .decision-compact-row{ margin-top: 1.2rem; }
.decision-compact-row img{
  width:100%; height:auto; display:block;
}
.decision-compact-label{
  display:flex; align-items:center; gap:.4rem;
  font-size: var(--fs-micro);
  font-weight:700;
  letter-spacing:.1em;
  text-transform:uppercase;
  color: var(--ink-faint);
  margin-bottom:.5rem;
}
.decision-compact-label.is-after{ color: var(--accent-ink); }
.decision-compact-label .chip{
  width: 1.3em; height:1.3em; border-radius:50%;
  background: var(--paper-dim);
  display:flex; align-items:center; justify-content:center;
  font-size:.85em;
}
.decision-compact-label.is-after .chip{ background: var(--accent-soft); }
/* Decision 02's "After" label above the standalone merged-card figure
   below stays flush-left at the full column width — matching the
   "Before" label earlier in this same decision — rather than matching
   the narrower, centered 18rem figure beneath it. The figure is
   intentionally narrower and centered on its own; the label isn't
   meant to track its width. */
/* Mobile only: the gap between the Before/After label row and the image
   row beneath it (margin-top on the second row, plus margin-bottom on
   the labels themselves) read as too loose at narrow widths. Tightened
   both down, mobile only — desktop spacing is untouched. */
@media (max-width: 640px){
  .decision-compact-row + .decision-compact-row{ margin-top: .5rem; }
  .decision-compact-label{ margin-bottom: .25rem; }
}

/* problem tiles */
.tile-grid{
  display:grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow:hidden;
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}
.tile{
  background: var(--card);
  padding: clamp(1.4rem, 2.5vw, 2rem);
}
.tile-num{
  font-family: var(--font-display);
  font-style:italic;
  font-size: 1.6rem;
  color: var(--accent);
}
.tile h4{ margin-top: .6rem; font-size: var(--fs-h3); font-weight:600; font-family: var(--font-body); }
.tile p{ margin-top:.5rem; font-size: var(--fs-small); color: var(--ink-soft); }
/* CClubs' 5-item Research tile-grid: on mobile, auto-fit's 200px min
   column was still fitting two narrow columns, wrapping headings onto
   multiple lines and leaving the 5th (odd) tile alone in its row at a
   different width than the rest. One column instead — every tile fills
   the full row width (so they're all the same size) and reads shorter
   since headings and copy no longer wrap as tightly. Scoped to
   .page-cclubs so Lenovo's own tile-grid usage is untouched. */
@media (max-width: 640px){
  .page-cclubs .tile-grid{ grid-template-columns: 1fr !important; }
  .page-cclubs .tile{ padding: 1.3rem 1.4rem; }
  .page-cclubs .tile-num{ font-size: 1.3rem; }
  .page-cclubs .tile h4{ margin-top: .4rem; }
  .page-cclubs .tile p{ max-width: 34ch; line-height:1.5; }
}
/* The rule above targets grid-template-columns, but a later, always-on
   rule (further down this file) actually switches this same .tile-grid
   to display:flex with flex-wrap:wrap and each .tile pinned to a
   clamp(9rem,23vw,14rem) width — which is what was really causing two
   per row on mobile; grid-template-columns has no effect once display
   is flex, so the fix above was a no-op. Overriding the flex behavior
   directly here instead: stack in a single column and let each tile
   take the full row width. !important guarantees this wins regardless
   of the two rules' source order. */
@media (max-width: 640px){
  .page-cclubs .tile-grid{ flex-direction: column !important; }
  .page-cclubs .tile{ width: 100% !important; }
}

/* Opportuna-only: the shared .tile-grid above draws a single bordered
   container with 1px divider lines between cells (used as-is on Lenovo
   and CClubs). Opportuna's 5-tool problem list instead needs 5
   independent cards: 3 centered on a top row, 2 centered on a second
   row as their own group. A wrapping flex row with justify-content:
   center does this natively — items that don't fit the first line
   drop to a second line and are centered there too, with no extra
   markup or media queries needed. Every .tile gets the exact same
   width (not flex-grow) so sizing stays identical whether a card ends
   up in the 3-card row or the 2-card row. Scoped to .page-opportuna so
   Lenovo/CClubs' existing merged-table tile-grid is untouched. */
/* Scattered composition: five cards arranged like loose sticky notes
   instead of a tidy grid, visually reinforcing "fund reporting,
   scattered across five tools." Each card's displacement from the
   container's center is set once as --dx/--dy/--rot custom properties
   (a single source of truth per card); the actual transform multiplies
   those by --scatter-scale, so every breakpoint below just dials one
   number down instead of redefining five cards' worth of positions. */
.page-opportuna .tile-grid{
  position:relative;
  display:block;
  background:none;
  border:none;
  border-radius:0;
  overflow:visible;
  min-height: clamp(14.5rem, 22vw, 18rem);
  margin-top: clamp(0.6rem, 1.2vw, 1rem);
  --scatter-scale: 1;
}
.page-opportuna .tile{
  position:absolute;
  left:50%;
  top:50%;
  width:max-content;
  max-width: 12rem;
  padding: .85rem 1.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: var(--shadow-sm);
  display:flex;
  align-items:center;
  justify-content:center;
  text-align:center;
  transform:
    translate(-50%, -50%)
    translate(calc(var(--dx) * var(--scatter-scale)), calc(var(--dy) * var(--scatter-scale)))
    rotate(calc(var(--rot) * var(--scatter-scale)));
  transition: transform .4s var(--ease), background .3s var(--ease), border-color .3s var(--ease);
}
.page-opportuna .tile p{ margin-top:0; text-align:center; }
.page-opportuna .tile:nth-child(1){ --dx:-6.5rem; --dy:-0.8rem; --rot:-6deg; z-index:3; }
.page-opportuna .tile:nth-child(2){ --dx:0.5rem;  --dy:-4.2rem; --rot:4deg;  z-index:2; }
.page-opportuna .tile:nth-child(3){ --dx:6.5rem;  --dy:-1rem;   --rot:5deg;  z-index:4; }
.page-opportuna .tile:nth-child(4){ --dx:-6rem;   --dy:3.2rem;  --rot:-4deg; z-index:1; }
.page-opportuna .tile:nth-child(5){ --dx:2.8rem;  --dy:4.2rem;  --rot:3deg;  z-index:5; }

/* Per-card color variation, pulled from the existing token set (accent /
   accent-soft / paper-dim / line-strong) rather than new colors, so the
   scatter reads as an assortment of sticky notes instead of five identical
   tiles while staying inside the established palette. */
.page-opportuna .tile:nth-child(2){ background: var(--accent-soft); border-color: var(--accent); box-shadow: var(--shadow); }
.page-opportuna .tile:nth-child(3){ border-color: var(--line-strong); }
.page-opportuna .tile:nth-child(4){ background: var(--paper-dim); }
.page-opportuna .tile:nth-child(5){ border-color: var(--accent); box-shadow: var(--shadow); }

/* Tablet: dial the whole scatter down to about half strength — less
   horizontal reach, less rotation, less overlap — rather than letting
   full-strength offsets crowd a narrower column. */
@media (max-width: 900px){
  .page-opportuna .tile-grid{
    --scatter-scale: 0.7;
    min-height: clamp(11.5rem, 23vw, 14rem);
  }
}

/* Mobile: a scattered absolute cluster stops being legible at this
   width, so the cards drop back into normal document flow as a
   vertical stack, alternating slight left/right offsets and a small
   residual rotation so it still reads as "staggered," not a plain
   list. */
@media (max-width: 640px){
  .page-opportuna .tile-grid{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap: 1.9rem;
    min-height:0;
    margin-top: clamp(3rem, 8vw, 3.8rem) !important;
  }
  .page-opportuna .tile{
    position:static;
    width:100%;
    max-width: 16rem;
    padding: 1.05rem 1.3rem;
    transform: rotate(calc(var(--rot) * 0.3));
  }
  .page-opportuna .tile:nth-child(odd){ align-self:flex-start; }
  .page-opportuna .tile:nth-child(even){ align-self:flex-end; }
  .page-opportuna .tile-grid + .body-copy{ margin-top: 2.6rem !important; }
}

/* Opportuna-only: the shared .decision-head is flex + flex-wrap:wrap,
   which works fine for Lenovo's short headings but, once Opportuna's
   longer headings can't fit next to "01" on one line, wraps the
   *entire* heading down onto its own line below the number instead of
   keeping them side by side. Turning off item-level wrapping keeps
   the number and heading on the same row always; the heading's own
   text still wraps normally within its remaining space right next to
   the number, matching Lenovo's side-by-side look without changing
   the existing gap. */
.page-opportuna .decision-head{
  flex-wrap:nowrap;
}
.page-opportuna .decision-head h3{
  min-width:0;
}

.pull-question{
  margin-top: clamp(2rem, 4vw, 3rem);
  font-family: var(--font-display);
  font-style: italic;
  font-weight:400;
  font-size: clamp(1.4rem, 2.4vw, 2.1rem);
  line-height:1.3;
  color: var(--ink);
  border-left: 2px solid var(--accent);
  padding-left: clamp(1.2rem, 2vw, 1.8rem);
}

/* research card grid */
.insight-grid{
  display:grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(1.25rem, 2.5vw, 1.75rem);
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}
.insight-card{
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.5rem, 2.5vw, 2.1rem);
}
.insight-card .eyebrow{ margin-bottom: 1rem; }
.insight-card h4{ font-family: var(--font-display); font-weight:500; font-size: var(--fs-h3); }
.insight-card p{ margin-top:.6rem; color: var(--ink-soft); font-size: var(--fs-small); }

/* Small icon row standing in for each named platform's logo — simple
   line pictograms (not literal brand marks) in the same hand-drawn SVG
   style already used for the footer's Instagram/LinkedIn icons, so the
   card reads as more than a plain text block without borrowing anyone's
   actual trademarked logo. */
.insight-icons{
  display:flex;
  gap:.5rem;
  margin-bottom: 1rem;
}
.insight-icon{
  width: 2.1rem;
  height: 2.1rem;
  border-radius:50%;
  background: var(--paper-dim);
  border: 1px solid var(--line);
  display:flex;
  align-items:center;
  justify-content:center;
  flex-shrink:0;
}
.insight-icon svg{ width:1.05rem; height:1.05rem; stroke: var(--ink-soft); color: var(--ink-soft); }

/* decisions */
.decision{
  padding-block: var(--space-block);
  border-top: 1px solid var(--line);
}
.decision-head{
  display:flex;
  align-items:baseline;
  gap: 1rem;
  flex-wrap:wrap;
}
.decision-num{
  font-family: var(--font-display);
  font-style: italic;
  color: var(--accent);
  font-size: var(--fs-h2);
  flex-shrink:0;
  display:inline-block;
  /* Stamps in a beat after the rest of the heading, rather than rising
     with the same motion as everything else — reads as a mark being
     set down, echoing the "Decision 01/02/03" pacing of the case study
     narrative. Tied to the parent .decision section's own reveal state
     so it needs no separate scroll observer. */
  opacity:0;
  transform: scale(.6);
  transition: opacity .6s var(--ease) .18s, transform .6s var(--ease) .18s;
}
.decision.is-visible .decision-num{ opacity:1; transform:scale(1); }
.decision-head h3{ font-family: var(--font-display); font-weight:400; font-size: clamp(1.35rem, 1.3vw + 0.95rem, 2.1rem); letter-spacing:-0.01em; }
.decision-intro{ margin-top: 1rem; max-width: 68ch; color: var(--ink-soft); }
/* Same width-matching issue as .body-copy: .decision-intro sits inside
   .case-col too (each Decision's lede paragraph, above its option-grid
   or before/after row), and its own 68ch cap left it narrower than the
   full-width option cards and comparison rows sitting right below it.
   Removed the cap inside .case-col so it fills the same column as
   everything else on the page. */
.case-col .decision-intro{ max-width:none; }

.compare{
  margin-top: clamp(2rem, 3.5vw, 2.8rem);
  display:grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(1rem, 2vw, 1.5rem);
  align-items:start; /* images keep their natural height — no stretch-cropping */
}
@media (max-width: 640px){ .compare{ grid-template-columns: 1fr; } }
.compare-item{ min-width:0; }
/* Before settles in first, After arrives a beat later — a small,
   restrained echo of "here's where we started, here's where we
   landed" instead of both sides appearing identically at once. Tied to
   the parent .decision's own reveal state, same mechanism as the
   decision numeral above. */
.decision .compare-item{
  opacity:0;
  transform: translateY(16px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.decision.is-visible .compare-item{ opacity:1; transform:none; }
.decision.is-visible .compare-item:nth-child(1){ transition-delay:.05s; }
.decision.is-visible .compare-item:nth-child(2){ transition-delay:.24s; }
/* Text-vs-image compare rows (e.g. a short step list beside a full
   screenshot) look lopsided at an even 1fr/1fr split — the text side
   never fills its column, leaving an oversized gap before the image
   starts. Giving the image side more room and centering both columns
   vertically closes that gap and lets the image read at a fuller,
   better-balanced size. */
.compare--quickview{
  grid-template-columns: 0.72fr 1fr;
  /* The Before column is a short numbered step list; the After column
     is a much taller screenshot. align-items:start (the .compare
     default) left both top-anchored, but centering the whole column
     (an earlier attempt) dragged the "BEFORE" label itself down out
     of line with "AFTER" above the image. Instead: stretch both
     columns to the row's full height so the labels stay level at the
     top, and only the step list's own content area (below its label)
     centers within the leftover space — landing the list's vertical
     midpoint near the image's midpoint without moving the label. */
  align-items: stretch;
}
.compare--quickview .compare-item{
  display:flex;
  flex-direction:column;
}
.compare--quickview .step-flow{
  flex:1;
  justify-content:center;
}
@media (max-width: 640px){ .compare--quickview{ grid-template-columns: 1fr; } }
.compare-label{
  display:flex; align-items:center; gap:.5rem;
  font-size: var(--fs-micro); font-weight:700; letter-spacing:.12em; text-transform:uppercase;
  color: var(--ink-faint); margin-bottom:.7rem;
}
.compare-label.is-after{ color: var(--accent-ink); }
.compare-label .chip{
  width: 1.4em; height:1.4em; border-radius:50%;
  background: var(--paper-dim);
  display:flex; align-items:center; justify-content:center;
  font-family: var(--font-body);
}
.compare-label.is-after .chip{ background: var(--accent-soft); }

/* Grouped compare: two "before" states shown alongside a single "after"
   result, for decisions where several earlier explorations converge on
   one outcome rather than a strict 1-to-1 before/after pairing. Before
   and After get the identical thin-border-plus-shadow treatment, with
   the After image picking up an accent-colored border to signal it's
   the outcome, so the two sides read as one cohesive comparison instead
   of two differently styled sections. Every image (before and after
   alike) is set to the same displayed height via .compare-grouped-row;
   width is left to auto so each image scales proportionally at its own
   true aspect ratio, never cropped or stretched. */
.compare-grouped{
  margin-top: clamp(2rem, 3.5vw, 2.8rem);
  display:flex;
  flex-wrap:wrap;
  align-items:flex-start;
  justify-content:center;
  gap: clamp(2rem, 4vw, 3.5rem);
}
.compare-grouped-group{
  display:flex;
  flex-direction:column;
  align-items:center;
}
.compare-grouped-group .compare-label{ align-self:flex-start; }
.compare-grouped-row{
  display:flex;
  align-items:center;
  justify-content:center;
  gap: clamp(.75rem, 2vw, 1.25rem);
  height: clamp(15.5rem, 33vw, 22.5rem);
}
.compare-grouped-fig{
  display:block;
  height:100%;
  margin:0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow:hidden;
  box-shadow: var(--shadow-sm);
  background: var(--card);
}
.compare-grouped-fig.is-after{ border-color: var(--accent); }
.compare-grouped-fig img{
  display:block;
  height:100%;
  width:auto;
  max-width:100%;
}
@media (max-width: 900px){
  /* Sized to match Decision 02's .decision-compact module: capped at
     the same 27rem max-width and centered, instead of stretching
     edge-to-edge with the column, so the two grouped-compare decisions
     read at a consistent scale against each other on mobile.

     Threshold bumped from 640px to 900px: below the width where Before
     (two images) and After (one image) can comfortably sit side by
     side, .compare-grouped's desktop behavior (flex-wrap, each group
     sized to its own content and centered independently on whatever
     line it wraps to) let the two groups land at different left
     edges — Before's wider box starting further left than After's
     narrower one, even though each group's own label is flush-left
     within its own box. Switching to this always-stacked, always
     full-width layout earlier removes that dead zone: both groups now
     share the exact same 100%-wide box at every width in this range,
     so "Before" and "After" labels always share the same left edge. */
  .compare-grouped{ flex-direction:column; align-items:flex-start; gap: clamp(1.6rem, 4vw, 2.4rem); max-width: 27rem; margin-inline:auto; }
  .compare-grouped-group{ width:100%; align-items:flex-start; }
  .compare-grouped-group .compare-label{ align-self:flex-start; }
  .compare-grouped-row{ height:auto; width:100%; justify-content:center; }
  .compare-grouped-fig{ height:auto; width:auto; flex:1 1 0; min-width:0; }
  .compare-grouped-fig img{ height:auto; width:100%; max-width:100%; }
  /* The single After image sized to match "The merged card, up close"
     figure below (Decision 02), which caps at 18rem — instead of
     growing to fill the full 27rem group width like the two Before
     images beside it. It stays centered within its row (per the
     justify-content:center above) rather than flush-left, since the
     image itself should read as centered even though the label above
     it is flush-left. */
  .compare-grouped-fig.is-after{ flex: 0 1 18rem; max-width: 18rem; }
}

.callout-list{
  margin-top: clamp(1.5rem, 2.5vw, 2rem);
  display:grid;
  gap: 1rem;
}
.callout{
  display:flex; gap: 1rem;
  align-items:flex-start;
  padding: 1.1rem 1.3rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.callout-num{
  flex-shrink:0;
  width: 1.8rem; height:1.8rem; border-radius:50%;
  background: #CE4996; color: var(--paper);
  display:flex; align-items:center; justify-content:center;
  font-size: var(--fs-micro); font-weight:700;
}
.callout p{ font-size: var(--fs-small); color: var(--ink-soft); margin:0; }

/* Step flow: a connected vertical sequence for "before" pain-point
   walkthroughs (click through, scroll, go back, repeat) — a thin line
   linking each numbered circle down to the next communicates a tedious
   repeated process better than isolated boxes that read the same as
   the feature-callout component used everywhere else. */
.step-flow{
  margin-top: clamp(1.4rem, 2.5vw, 2rem);
  display:flex;
  flex-direction:column;
}
.step-flow-item{
  position:relative;
  display:flex;
  align-items:flex-start;
  gap: 1rem;
  padding-bottom: clamp(1.4rem, 2.5vw, 1.9rem);
}
.step-flow-item:last-child{ padding-bottom:0; }
.step-flow-item:not(:last-child)::after{
  content:"";
  position:absolute;
  left: .9rem;
  top: 1.8rem;
  bottom: 0;
  width:1px;
  background: var(--line);
}
.step-flow-num{
  position:relative;
  z-index:1;
  flex-shrink:0;
  width: 1.8rem; height:1.8rem; border-radius:50%;
  background: var(--paper);
  border: 1.5px solid var(--ink-faint);
  color: var(--ink-soft);
  display:flex; align-items:center; justify-content:center;
  font-size: var(--fs-micro); font-weight:700;
}
.step-flow-item p{
  margin:0;
  padding-top:.25rem;
  font-size: var(--fs-small);
  color: var(--ink-soft);
}

/* options row (ideation) */
.option-grid{
  display:grid;
  grid-template-columns: repeat(auto-fit, minmax(220px,1fr));
  gap: clamp(1.25rem, 2.5vw, 1.75rem);
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
  align-items:stretch; /* every card in a row matches the tallest one, instead of each hugging its own content height */
}
.option-card{
  display:flex;
  flex-direction:column;
  height:100%;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow:hidden;
  background: var(--card);
  transition: border-color .3s var(--ease), transform .3s var(--ease);
}
.option-card.is-chosen{ border-color: var(--accent); box-shadow: var(--shadow-sm); }
.option-body{ padding: 1.2rem 1.3rem 1.4rem; flex: 1 1 auto; display:flex; flex-direction:column; }
.option-body .kicker{ font-size: var(--fs-micro); font-weight:700; letter-spacing:.08em; text-transform:uppercase; color: var(--ink-faint); }
.option-card.is-chosen .kicker{ color: var(--accent-ink); }
.option-body h4{ margin-top:.4rem; font-family: var(--font-display); font-weight:500; font-size: 1.15rem; }
.option-body ul{ margin-top:.7rem; display:grid; gap:.35rem; }
.option-body li{ font-size: var(--fs-small); color: var(--ink-soft); padding-left: 1.1em; position:relative; }
.option-body li::before{ content:"–"; position:absolute; left:0; color: var(--ink-faint); }

.reflection{
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
  font-size: var(--fs-body);
  color: var(--ink-soft);
  max-width: 68ch;
}
.reflection strong{ color: var(--ink); font-weight: 600;}
/* Same width-matching issue again: .reflection is the closing paragraph
   right below a .body-copy paragraph in the same .case-col, and its own
   68ch cap left it narrower than that now-full-width neighbor directly
   above it. Removed the cap inside .case-col so the two paragraphs read
   as one consistent column instead of the second one stepping in. */
.case-col .reflection{ max-width:none; }
/* The "if I had more time" line uses .body-copy + .mt-lg (a big
   section-level spacer, meant for gaps between whole blocks) which
   reads as far more space than the rhythm used everywhere else
   between paragraphs in this column. Tightened to match .reflection's
   own gap above it instead, so the spacing above and below reads the
   same. Same fix for the CClubs chat-intro paragraph right after the
   texting caption, which had the identical oversized gap. */
.case-col .reflection + .body-copy.mt-lg,
.case-col .chat-caption + .body-copy.mt-lg{ margin-top: clamp(1.6rem, 3vw, 2.4rem); }

.case-nav{
  border-top: 1px solid var(--line);
  padding-block: clamp(2.5rem, 4vw, 4rem);
  display:flex;
  justify-content:space-between;
  gap: 1.5rem;
  flex-wrap:wrap;
}
.case-nav a{
  display:flex; flex-direction:column; gap:.5rem;
  max-width: 22rem;
  transition: transform .35s var(--ease);
}
.case-nav .dir{ font-size: var(--fs-micro); text-transform:uppercase; letter-spacing:.12em; color: var(--ink-faint); transition: color .3s var(--ease); }
.case-nav .name{ font-family: var(--font-display); font-size: 1.4rem; transition: color .3s var(--ease); }
.case-nav a:hover .name{ color: var(--accent-ink); }
.case-nav a:hover .dir{ color: var(--accent); }
.case-nav a.next{ text-align:right; align-items:flex-end; margin-left:auto; }
/* A small nudge in the direction of travel on hover — the arrows
   already live inside the "dir" text ("Next project →"), so the whole
   link leaning that way reinforces where it's taking you rather than
   adding a second separate icon. */
@media (hover:hover) and (pointer:fine){
  .case-nav a.next:hover{ transform: translateX(4px); }
  .case-nav a.prev:hover{ transform: translateX(-4px); }
}

/* ==========================================================================
   About page
   ========================================================================== */
.about-grid{
  display:grid;
  grid-template-columns: minmax(240px, 380px) 1fr;
  gap: clamp(2.5rem, 6vw, 6rem);
  align-items:start;
}
@media (max-width: 780px){ .about-grid{ grid-template-columns: 1fr; } }
.about-portrait{
  position: sticky;
  top: 6.5rem;
}
@media (max-width: 780px){ .about-portrait{ position:static; max-width: 22rem; margin-inline:auto; } }
.about-portrait img{ width:100%; height:auto; display:block; }
.about-facts{
  display:flex; flex-wrap:wrap; gap: .6rem;
  margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
}
.about-body h2{
  font-family: var(--font-display);
  font-weight:400;
  font-size: var(--fs-hero);
  letter-spacing:-0.02em;
  line-height:1.05;
  margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
}
.about-body .body-copy{ font-size: var(--fs-body); color: var(--ink-soft); }
.about-body .body-copy + .body-copy{ margin-top: 1.2em; }

/* ==========================================================================
   Creative page
   ========================================================================== */
.creative-tabs{
  display:flex;
  gap: .6rem;
  flex-wrap:wrap;
  margin-top: clamp(2rem, 3vw, 2.6rem);
}
.creative-tab{
  padding: .7em 1.3em;
  border-radius: 100px;
  border: 1px solid var(--line-strong);
  font-size: var(--fs-small);
  font-weight:600;
  color: var(--ink-soft);
  transition: background .3s var(--ease), color .3s var(--ease), border-color .3s var(--ease);
}
.creative-tab:hover{ border-color: var(--accent); color: var(--accent-ink); }
.creative-tab[aria-selected="true"]{ background: var(--accent); border-color: var(--accent); color: var(--paper); }

.creative-panel{ margin-top: clamp(2.5rem, 4vw, 3.5rem); }
.creative-panel[hidden]{ display:none; }

.masonry{
  columns: 3 250px;
  column-gap: clamp(1.1rem, 2.2vw, 2rem);
}
.masonry-item{
  break-inside: avoid;
  margin-bottom: clamp(1.1rem, 2.2vw, 2rem);
  cursor: url("../assets/img/cursor-paintbrush.svg") 5 27, auto;
}
.masonry-item img{
  width:100%;
  display:block;
  transition: transform .6s var(--ease);
}
.masonry-item:hover img{ transform: scale(1.02); }

/* Lightbox */
.lightbox{
  position: fixed; inset:0; z-index:1000;
  display:flex; align-items:center; justify-content:center;
  padding: clamp(1.2rem, 5vw, 4rem);
  background: color-mix(in srgb, var(--ink) 92%, transparent);
  opacity:0; pointer-events:none;
  transition: opacity .35s var(--ease);
}
.lightbox.is-open{ opacity:1; pointer-events:auto; }
.lightbox img{
  max-width: 100%; max-height: 84vh;
  width:auto; height:auto;
  border-radius: var(--radius-sm);
  box-shadow: 0 40px 90px rgba(0,0,0,.5);
  transform: scale(.97);
  transition: transform .35s var(--ease);
}
.lightbox.is-open img{ transform:none; }
.lightbox-close, .lightbox-prev, .lightbox-next{
  position:absolute;
  width: 3rem; height:3rem;
  border-radius:50%;
  background: color-mix(in srgb, var(--paper) 12%, transparent);
  display:flex; align-items:center; justify-content:center;
  color: var(--paper);
  transition: background .3s var(--ease);
}
.lightbox-close:hover, .lightbox-prev:hover, .lightbox-next:hover{ background: color-mix(in srgb, var(--paper) 25%, transparent); }
.lightbox-close{ top: clamp(1rem,3vw,2rem); right: clamp(1rem,3vw,2rem); }
.lightbox-prev{ left: clamp(.6rem,2vw,1.6rem); top:50%; transform: translateY(-50%); }
.lightbox-next{ right: clamp(.6rem,2vw,1.6rem); top:50%; transform: translateY(-50%); }
.lightbox svg{ width:18px; height:18px; stroke: currentColor; }

@media (max-width: 900px){ .masonry{ columns: 2 220px; } }
@media (max-width: 520px){ .masonry{ columns: 1 100%; } }

/* ==========================================================================
   Lenovo-only overrides
   Scoped entirely to body.page-lenovo so Opportuna, CClubs, the Creative
   page, and every other page keep the site-wide "no card" image system
   exactly as it is. On Lenovo specifically, every image outside the
   hero banner and the "01 Simplified Cards" section (which keeps its
   own separately-restored treatment above) gets a thin, refined border
   and a soft, subtle shadow directly on the image itself — no padding,
   no background fill, no card body, just a quiet frame. All of this is
   plain CSS (border/shadow/radius) so it scales fluidly with the
   existing responsive layout; nothing here touches image dimensions,
   cropping, or aspect ratio. */
/* Single shared corner-radius token for every static image on this page
   (screenshots, before/after pairs, research and UI imagery). Excludes
   the hero banner and the prototype device/video mockups, which keep
   their own separate treatment untouched. Change this one value and
   every applicable image below stays in sync automatically. */
.page-lenovo{ --lenovo-figure-radius: 8px; }
.page-lenovo .figure img{
  border: 1px solid var(--line);
  border-radius: var(--lenovo-figure-radius);
  box-shadow: var(--shadow-sm);
}
/* "Is-after" figures (chosen/final result) pick up the same accent
   border already used for After images in the compare-grouped and
   compare-label components, so a standalone closeup figure can carry
   that same "this is the result" signal. */
.page-lenovo .figure.is-after img{
  border-color: var(--accent);
}
/* Browser-chrome frame for the Problem section's lenovo.com screenshot:
   a light window bar (traffic-light dots + a plain address field
   showing the URL) sitting above the screenshot, inside one shared
   bordered/shadowed frame — reads as "this is a real website" rather
   than a bare cropped image. The frame itself carries the border and
   shadow the generic .figure img rule above would otherwise put
   directly on the image, so that's turned off here to avoid a double
   border around both the bar and the screenshot separately. */
.page-lenovo .browser-frame{
  border: 1px solid var(--line);
  border-radius: var(--lenovo-figure-radius);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  background: var(--paper-dim);
}
.page-lenovo .browser-frame img{
  border: none;
  border-radius: 0;
  box-shadow: none;
  border-top: 1px solid var(--line);
}
.browser-frame-bar{
  display:flex;
  align-items:center;
  gap: 1.25rem;
  padding: 1rem 1.25rem;
}
.browser-frame-dots{
  display:flex;
  gap: .4rem;
  flex-shrink:0;
}
.browser-frame-dots span{
  width: .55rem;
  height: .55rem;
  border-radius: 50%;
  background: var(--line-strong);
}
.browser-frame-url{
  flex: 0 1 20rem;
  padding: .5rem 1rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--paper);
  font-family: var(--font-display);
  font-size: var(--fs-small);
  color: var(--ink-soft);
}
/* "01 Simplified Cards" option images keep their bare, borderless
   treatment (no frame or shadow, matching their own restored look),
   but now share the same corner radius as every other image on the
   page instead of the square corners they had before. The option-card
   container itself (which clips the image's top corners via its own
   overflow:hidden) is brought down from the site-wide --radius (14px)
   to this same shared value on Lenovo only, so the container's clip
   curve and the image's own corner radius are identical — otherwise
   the two mismatched curves would leave a sliver of the card's
   background visible in the top corners. */
.page-lenovo .option-card{
  border-radius: var(--lenovo-figure-radius);
}
.page-lenovo .option-card .figure img{
  border:none;
  border-radius: var(--lenovo-figure-radius);
  box-shadow:none;
}
.page-lenovo .decision-compact-row img{
  border: 1px solid var(--line);
  border-radius: var(--lenovo-figure-radius);
  box-shadow: var(--shadow-sm);
}
/* This row's After column no longer carries the accent border — with
   the "After" label removed from the top thumbnail pair (the single
   "After" tag now lives only by the full merged card below), a pink
   border up here read as an orphaned signal with nothing labeling it. */
/* Decision 01's grouped Before/After thumbnails (compare-grouped-fig)
   previously used the larger --radius (14px) token, which read as a
   noticeably rounder corner than every other image on the page. Bring
   it in line with the shared Lenovo figure radius. compare-grouped is
   only used on this page, but the override is scoped explicitly for
   clarity and to protect against future reuse elsewhere. */
.page-lenovo .compare-grouped-fig{
  border-radius: var(--lenovo-figure-radius);
}
/* Competitor logos stay borderless — a thin card outline around each
   small brand mark read as boxy rather than refined, so this one image
   type is left out of the page-wide border/shadow treatment. */
.page-lenovo .logo-marquee img{
  border:none;
  box-shadow:none;
}
/* The hero banner stays exactly as-is: no border, no shadow. Stated
   explicitly since .case-hero-cover doesn't use the .figure class, it
   would never have picked up the rule above, but this keeps the intent
   on record for future edits. */
.page-lenovo .case-hero-cover img{ border:none; box-shadow:none; }

/* Hero banner: break out of .case-hero's .wrap max-width/padding so the
   image spans the full viewport edge-to-edge, instead of being capped
   to the content column like the rest of the page. Lenovo-only. */
.page-lenovo .case-hero-cover{
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.page-lenovo .case-hero-cover img{ width:100%; height:auto; display:block; }

/* Laptop prototype: fill the same content column as the surrounding
   text (.case-col) instead of its own narrower cap, so the laptop's
   left/right edges line up with the paragraph and heading edges above
   it. The phone mockup below keeps its own smaller, natural width. */
.page-lenovo .device-row .device--mac{ max-width:none; width:100%; }

/* Role/Team/Timeline: left-align the text within each column (so "2
   PMs" and "6 Product Designers" share the same left edge as "TEAM"
   instead of each line centering independently under it), while
   leaving .case-meta's own justify-content:center untouched — the
   group of three columns stays centered together in the middle of
   the page, it's just the text inside each column that's no longer
   individually centered. */
.page-lenovo .case-meta > div{ text-align:left; }

/* Decision 03's Before/After (Reorganized Entry Point): the two source
   screenshots turned out to have noticeably different real aspect
   ratios despite looking similar at a glance, so letting each follow
   its own natural width:100%/height:auto left them rendering at
   visibly different heights — the taller (Before) column ran well
   past the shorter (After) one. Matched here the same way the grouped
   Before/After thumbnails elsewhere on this page are: both figures
   share one fixed height and each image is contain-fit within it
   (full image always visible, nothing cropped or stretched), so the
   pair's bottom edges land together regardless of source proportions. */
.page-lenovo .compare--matched-height{ align-items: stretch; }
.page-lenovo .compare--matched-height .compare-item{
  display:flex;
  flex-direction:column;
}
/* The figure box no longer stretches to fill the full column width —
   that was leaving pillarbox gaps around the border whenever an
   image's natural width at the shared height came in narrower than
   the column (object-fit:contain fitting inside an oversized box).
   align-self:flex-start + width:auto lets the box shrink-to-fit the
   image's own rendered width instead, so the border sits flush
   against the image with no gap, and it stays left-aligned under the
   label above it rather than floating centered. */
.page-lenovo .compare--matched-height .compare-item .figure{
  align-self:flex-start;
  display:block;
  width:auto;
  max-width:100%;
  height: clamp(13rem, 24vw, 19rem);
  /* overflow:hidden here was clipping the image's own box-shadow (drawn
     outside its border box) since the image is sized to fill this box
     exactly with no actual overflow to hide. Switched to visible so the
     shadow already defined on .figure img actually renders — matches
     the mobile version of this rule further down, which already uses
     overflow:visible. */
  overflow:visible;
}
.page-lenovo .compare--matched-height .compare-item .figure img{
  display:block;
  width:auto;
  height:100%;
  max-width:100%;
}
/* On mobile the pair stacks instead of sitting side by side, so the
   shared-height matching above (which exists to keep two side-by-side
   columns visually level) no longer serves a purpose — and because
   Before/After have slightly different source aspect ratios, matching
   height at a narrow viewport left one image (whichever has the lower
   ratio) rendering narrower than the other once the taller one hit its
   max-width cap. Switched to width:100%/height:auto here so both
   Before and After fill the exact same width, full-bleed to the column,
   stacked at their own natural (and possibly differing) heights. */
@media (max-width: 640px){
  .page-lenovo .compare--matched-height{
    max-width: 27rem !important;
    margin-inline: auto !important;
  }
  .page-lenovo .compare--matched-height .compare-item .figure{
    align-self:stretch !important;
    display:block !important;
    width:100% !important;
    max-width:100% !important;
    height:auto !important;
    overflow:visible !important;
  }
  .page-lenovo .compare--matched-height .compare-item .figure img{
    display:block !important;
    width:100% !important;
    max-width:100% !important;
    height:auto !important;
  }
}

/* Pin markers, same component as Opportuna's Decision screenshots:
   numbered circle badges positioned with percentage-based left/top so
   they track each image's own responsive scaling. Applied here as a
   reusable "pin-figure" wrapper across every Decision image on this
   page; no .pin-marker spans are placed yet since exact positions are
   still pending — add them per-image once coordinates are provided. */
.page-lenovo .pin-figure{ position:relative; }
.page-lenovo .pin-marker{
  position:absolute;
  transform: translate(-50%, -50%);
  width: 1.4rem; height:1.4rem;
  border-radius:50%;
  background: #CE4996;
  color: var(--paper);
  border: 2px solid var(--paper);
  box-shadow: var(--shadow-sm);
  display:flex; align-items:center; justify-content:center;
  font-size: var(--fs-micro); font-weight:700;
}

/* Section divider lines: border-top on a .wrap element draws at the
   very edge of its own box, which is wider than the padded content
   area — that's why the lines were reading as noticeably wider than
   the hero banner and body content sitting inside that padding. A
   background line sized to the same content width (100% minus the
   gutter on each side) and centered replaces the border, so every
   divider now matches the banner's width exactly. */
.page-lenovo .case-block,
.page-lenovo .decision,
.page-lenovo .case-nav{
  border-top: none;
  background-image: linear-gradient(var(--line), var(--line));
  background-repeat: no-repeat;
  background-size: calc(100% - 2 * var(--gutter)) 1px;
  background-position: center top;
}

/* Same fix, Opportuna-only. The hero banner is now full-bleed (see the
   .page-opportuna .case-hero-cover rule below), so the divider width
   is sized to 100vw instead of the content-column width, to keep
   matching the banner exactly. Since these .wrap sections are
   themselves centered on the page, a 100vw background centered on the
   element's own box lands exactly on the viewport edges. */
.page-opportuna .case-block,
.page-opportuna .decision,
.page-opportuna .case-nav{
  border-top: none;
  background-image: linear-gradient(var(--line), var(--line));
  background-repeat: no-repeat;
  background-size: 100vw 1px;
  background-position: center top;
}

/* Hero banner: break out of .case-hero's .wrap max-width/padding so
   the image spans the full viewport edge-to-edge, matching Lenovo's
   treatment. Opportuna-only. */
.page-opportuna .case-hero-cover{
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.page-opportuna .case-hero-cover img{ width:100%; height:auto; display:block; }

/* Give every static figure image on Opportuna a thin border + soft shadow,
   matching the treatment already used on Lenovo, so screenshots read as
   deliberately framed rather than floating loose on the page. Scoped to
   .figure img so the full-bleed hero banner above (a separate selector)
   and the prototype device/video are both left untouched. */
.page-opportuna .figure img{
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

/* Two-up ideation comparison (Option A vs. Option B). Equal-width flex
   columns, but the two source wireframes turned out to have quite
   different aspect ratios (Option A ~1.49:1, Option B ~1.72:1), so
   width:100%/height:auto — equal width, independent height — left
   them reading as two different-sized images sitting next to each
   other rather than one matched "vs" pair. Each figure shares a fixed
   height so both wireframes render at the same size; only the width
   varies with each image's own aspect ratio. The figure box itself is
   no longer stretched to the full column width — that left pillarbox
   gaps between the border and the image whenever a wireframe came in
   narrower than the column at the shared height. .idea-option is now
   the flex container: with no flex-grow set, .figure sizes to its own
   content (the image) on the main axis and justify-content centers
   it, so the border sits flush against the image every time. Option A
   carries the "this is what we chose" treatment: an accent border
   (overriding the default .figure img border above) plus a small
   checkmark badge. */
.page-opportuna .idea-compare{
  display:flex;
  align-items:flex-start;
  gap: clamp(1rem, 2.5vw, 1.6rem);
  margin-top: clamp(1.4rem, 2.5vw, 2.1rem);
}
.page-opportuna .idea-option{
  flex: 1 1 0;
  min-width:0;
  display:flex;
  justify-content:center;
}
/* position:relative lives on .figure itself (not .idea-option) so the
   checkmark badge — a child of .figure now — anchors to the image's own
   corner. It previously anchored to .idea-option, which spans the full
   flex column; since the figure shrinks to hug the image and sits
   centered within that wider column, the gap between the column's edge
   and the image's actual edge changes with viewport width, so the badge
   drifted away from the corner at different screen sizes. */
.page-opportuna .idea-option .figure{
  position:relative;
  margin-top:0;
  width:auto;
  max-width:100%;
  min-width:0;
  height: clamp(11rem, 20vw, 15rem);
  display:block;
  /* width:auto overrides the base .figure rule's width:100% (used
     everywhere else on the site to fill a text column). Left as 100%
     here, .figure's flex-basis (auto, which resolves to the width
     property's value) would compute to 100% of the .idea-option
     column — stretching the whole box to the column's full width
     instead of shrink-wrapping the image — which is exactly why the
     checkmark badge, anchored to .figure's real corner, was landing
     out at the column's edge instead of the image's own edge. */
  /* min-width:0 overrides the default min-width:auto that flex items get.
     Without it, .figure's intrinsic min-size is based on the image's own
     natural rendered width, so at intermediate viewport widths — once the
     .idea-option column got narrower than that — .figure refused to
     shrink with it and instead overflowed the column. Since the checkmark
     is positioned relative to .figure's corner, an overflowing .figure
     reads as the badge floating away from the visible image edge, even
     though it was technically still anchored to .figure the whole time.
     min-width:0 lets .figure shrink in step with the column at every
     width, same fix as .idea-option's own min-width:0 above.
     No overflow:hidden here (unlike the fixed-height boxes elsewhere on
     the site) — the checkmark badge is a child of .figure now, and it's
     deliberately positioned half outside the figure's own box (top:-.7rem
     right:-.7rem) to sit on the corner. The figure already shrinks to
     exactly the image's own rendered size, so there's nothing left to
     clip; overflow:hidden here would just cut the badge off. */
}
.page-opportuna .idea-option .figure img{
  display:block;
  width:auto;
  height:100%;
  max-width:100%;
}
.page-opportuna .idea-option.is-chosen .figure img{
  border-color: var(--accent);
  border-width: 2px;
}
.page-opportuna .idea-check{
  position:absolute;
  top:-.7rem;
  right:-.7rem;
  width:1.8rem;
  height:1.8rem;
  border-radius:50%;
  background: var(--accent);
  color: var(--paper);
  display:flex;
  align-items:center;
  justify-content:center;
  font-size: .85rem;
  font-weight:700;
  border: 2px solid var(--paper);
  box-shadow: var(--shadow-sm);
}
@media (max-width: 640px){
  .page-opportuna .idea-compare{ flex-direction:column; align-items:center; }
}

/* Pin markers on Decision 01's dashboard screenshot, showing exactly
   what each numbered callout below refers to. Percentage-based
   left/top so the markers track the image's own scaling responsively
   instead of drifting at narrower widths. Numbers match the
   .callout-num badges in the callout list beneath the figure. */
.page-opportuna .pin-figure{ position:relative; }
.page-opportuna .pin-marker{
  position:absolute;
  transform: translate(-50%, -50%);
  width: 1.4rem; height:1.4rem;
  border-radius:50%;
  background: #CE4996;
  color: var(--paper);
  border: 2px solid var(--paper);
  box-shadow: var(--shadow-sm);
  display:flex; align-items:center; justify-content:center;
  font-size: var(--fs-micro); font-weight:700;
}

/* CClubs-only: the shared .compare-item .figure rule fills 100% of its
   grid column, which is fine for wide landscape screenshots but reads
   as oversized for CClubs' tall portrait phone screenshots (nearly a
   2:1 height:width ratio) — they were rendering at ~750px tall inside
   the Key Decisions section. Capping the width and centering within
   the column brings them down to a more proportionate size, matching
   what the standalone Decision 01 figure was changed to just below.
   Doesn't touch the Final Prototype section's device--phone mockup,
   or Lenovo/Opportuna's own .compare-item figures. */
.page-cclubs .decision .compare-item .figure{
  max-width: 15rem;
  width:100%;
  margin-inline:auto;
}
/* The label above needs to line up with the now-centered image's left
   edge specifically, not float centered over the whole image — so it
   gets the same max-width and centered box as the figure above, with
   its text left-aligned inside that matching box. Same box, same left
   edge, text just starts at that edge instead of floating mid-image. */
.page-cclubs .decision .compare-item .compare-label{
  max-width: 15rem;
  margin-inline:auto;
  justify-content:flex-start;
}

/* CClubs-only: same fix as Opportuna's tile-grid — the shared version
   draws a single bordered container with 1px divider lines (and an
   empty gray cell where the grid's 6th auto-fit slot has no content).
   5 independent cards instead: a wrapping flex row centered with
   justify-content, so 3 sit on top and the remaining 2 wrap to a
   second row, centered as their own group. Every card gets the same
   fixed width so sizing stays consistent between rows; content
   (number, heading, paragraph) keeps its existing top-aligned, left-
   aligned styling. Scoped to .page-cclubs so Lenovo's own tile-grid is
   untouched. */
.page-cclubs .tile-grid{
  display:flex;
  flex-wrap:wrap;
  justify-content:center;
  gap: clamp(.85rem, 1.75vw, 1.25rem);
  background:none;
  border:none;
  border-radius:0;
  overflow:visible;
}
.page-cclubs .tile{
  flex: 0 1 auto;
  width: clamp(9rem, 23vw, 14rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* Same fix as Opportuna: the shared .decision-head is flex +
   flex-wrap:wrap, so once "01" and the heading can't both fit on one
   line at a given viewport width, the whole heading drops to its own
   line below the number instead of staying side by side. Turning off
   item-level wrapping keeps them on the same row always; the
   heading's own text still wraps normally within its remaining space
   right next to the number, matching Lenovo's side-by-side look
   without changing the existing gap. */
.page-cclubs .decision-head{
  flex-wrap:nowrap;
}
.page-cclubs .decision-head h3{
  min-width:0;
}

/* Same divider-width fix as Lenovo/Opportuna. The hero banner is now
   full-bleed (see the .page-cclubs .case-hero-cover rule below), so
   the divider width is sized to 100vw instead of the content-column
   width, to keep matching the banner exactly. Since these .wrap
   sections are themselves centered on the page, a 100vw background
   centered on the element's own box lands exactly on the viewport
   edges. */
.page-cclubs .case-block,
.page-cclubs .decision,
.page-cclubs .case-nav{
  border-top: none;
  background-image: linear-gradient(var(--line), var(--line));
  background-repeat: no-repeat;
  background-size: 100vw 1px;
  background-position: center top;
}

/* Hero banner: break out of .case-hero's .wrap max-width/padding so
   the image spans the full viewport edge-to-edge, matching Lenovo's
   and Opportuna's treatment. CClubs-only. */
.page-cclubs .case-hero-cover{
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.page-cclubs .case-hero-cover img{ width:100%; height:auto; display:block; }

/* Give every static figure image on CClubs a thin border + soft shadow,
   matching Lenovo/Opportuna. Scoped to .figure img so the full-bleed
   hero banner above (a separate selector) and the prototype video
   (not a .figure) are both left untouched. Option-card images are
   excluded right below since those already sit inside a bordered,
   shadowed card — an image-level border there would double up. */
.page-cclubs .figure img{
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.page-cclubs .option-card .figure img{
  border:none;
  box-shadow:none;
}

/* The three "Final Design" images (one per Key Decision's before/after
   compare) get a pink-bordered treatment instead of the neutral line
   above, so the resolved direction visually stands apart from its
   "Early Iterations" counterpart at a glance. */
.page-cclubs .figure-final img{
  border: 1px solid #CE4996;
}
/* On mobile, the shared .compare component collapses Early
   Iterations/Final Design to one full-width column each — fine for
   normal screenshots, but these are unusually tall portrait mockups,
   so full width made each one enormous. Kept side by side at mobile
   widths instead, at half the width each, with a tighter gap between
   them. Scoped to .page-cclubs so Lenovo/Opportuna's own .compare
   sections (shorter, wider source images) keep the shared stacked
   mobile behavior. */
@media (max-width: 640px){
  .page-cclubs .compare{
    grid-template-columns: 1fr 1fr;
    gap: .6rem;
  }
}

/* Role/Team/Timeline: same as Lenovo's fix — left-align the text
   within each column so multi-line values share the column's left
   edge instead of each line centering independently, while leaving
   .case-meta's own justify-content:center untouched so the group of
   three columns stays centered together in the middle of the page. */
.page-opportuna .case-meta > div{ text-align:left; }
.page-cclubs .case-meta > div{ text-align:left; }
.page-rovin .case-meta > div{ text-align:left; }

/* Pin markers, same component as Opportuna/Lenovo: numbered circle
   badges positioned with percentage-based left/top so they track the
   image's own responsive scaling. */
.page-cclubs .pin-figure{ position:relative; }
.page-cclubs .pin-marker{
  position:absolute;
  transform: translate(-50%, -50%);
  width: 1.4rem; height:1.4rem;
  border-radius:50%;
  background: #CE4996;
  color: var(--paper);
  border: 2px solid var(--paper);
  box-shadow: var(--shadow-sm);
  display:flex; align-items:center; justify-content:center;
  font-size: var(--fs-micro); font-weight:700;
}

/* Shared across all three pages' pin-markers: a small grow-on-hover so
   the annotation dots feel like they respond to attention rather than
   sitting inert on the image. Gated to fine-pointer devices; on touch
   the dot is still perfectly legible without a hover state to reach. */
@media (hover:hover) and (pointer:fine){
  .pin-marker{ transition: transform .3s var(--ease), box-shadow .3s var(--ease); }
  .pin-marker:hover{
    transform: translate(-50%, -50%) scale(1.18);
    box-shadow: var(--shadow);
  }
}

/* Texting-style Problem intro: two chat bubbles standing in for the
   "conversation every Cornell student had," matching the reference
   composition. Bubbles size to their own text (fit-content) rather than
   a fixed width, mirroring how real message bubbles behave; each keeps
   one squared-off corner on the side it "points" toward, like the
   standard iMessage-style incoming/outgoing shape. */
.page-cclubs .chat-thread{
  display:flex;
  flex-direction:column;
  gap: 1.8rem;
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}
.page-cclubs .chat-bubble{
  width: fit-content;
  max-width: 100%;
  padding: .85rem 1.3rem;
  font-size: var(--fs-body);
  line-height:1.45;
  border-radius: 1.3rem;
}
.page-cclubs .chat-bubble.is-them{
  align-self: flex-start;
  background: var(--paper-dim);
  color: var(--ink);
  border: 1px solid var(--line);
  border-bottom-left-radius: .35rem;
}
.page-cclubs .chat-bubble.is-me{
  align-self: flex-end;
  background: var(--ink);
  color: var(--paper);
  border-bottom-right-radius: .35rem;
  box-shadow: var(--shadow-sm);
}
.page-cclubs .chat-caption{
  margin-top: 1.1rem;
  font-family: var(--font-display);
  font-style: italic;
  font-weight:400;
  font-size: var(--fs-lede);
  color: var(--accent);
  text-align:center;
}
@media (max-width: 640px){
  .page-cclubs .chat-bubble{ max-width: 88%; }
}

/* Final Prototype video, no phone frame: previously wrapped in the
   .device--phone bezel/notch/home-indicator shell; now just the raw
   recording, sized down to a reasonable width and centered, matching
   the rest of the site's plain (no card, no frame) image treatment.
   The source recording is itself a phone mockup with its own rounded
   silhouette sitting on a flat grey canvas — its corner radius is much
   larger than our standard --radius (14px), so clipping the rectangular
   video at that small radius left slivers of that grey canvas visible
   in each corner, outside the phone's own curve. Rounding the video
   itself well past the phone's radius hides that leak completely. */
.proto-video-plain{
  display:block;
  width:100%;
  max-width: 18rem;
  margin-inline:auto;
  border-radius: 48px;
  overflow:hidden;
}
.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;
}
.center{ text-align:center; }
.mt-lg{ margin-top: var(--space-block); }
.mt-sm{ margin-top: clamp(.9rem, 1.8vw, 1.3rem); }

/* ==========================================================================
   Rovin (NDA case study)
   ========================================================================== */
.page-rovin .case-hero-cover{
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
.page-rovin .case-hero-cover img{ width:100%; height:auto; display:block; }
