/* =============================================================
   style.css — Eldho Kuriakose Portfolio
   Base styles: tokens, reset, sidebar nav, and all components
   shared across pages. Media queries live in responsive.css.
   ============================================================= */

/* ============ TOKENS ============ */
:root{
  --bg: #FFFFFF;
  --bg-2: #F5F5F5;
  --surface: #FFFFFF;
  --surface-2: #FAFAFA;
  --text: #0A0A0A;
  --text-soft: #444444;
  --text-faint: #8C8C8C;
  --accent: #E4222A;
  --accent-strong: #B4171D;
  --accent-soft: rgba(228,34,42,0.08);
  --accent-text: #FFFFFF;
  /* Gradient variant of --accent, for backgrounds specifically (buttons,
     badges, the cursor bubble) — CSS custom properties can hold a
     gradient fine, but only properties that accept an image value
     (background/background-image) can consume it; anything needing a
     flat color (text, borders, SVG stroke) still reads solid --accent.
     #E4222A sits as this gradient's own midpoint, so a component using
     the gradient and one using the flat --accent still read as the same
     red. Defined once here at :root (not per-theme) since index.html's
     dark theme uses this exact same red too, just applied as a flat
     fill in some spots and this same gradient in others — no separate
     dark-theme value needed. */
  --accent-gradient: linear-gradient(135deg, #FF5A4E 0%, #E4222A 55%, #A8121A 100%);
  --gold: #E4222A;
  --red: #E4222A;
  --line: rgba(10,10,10,0.10);
  --line-strong: rgba(10,10,10,0.20);
  --shadow: rgba(10,10,10,0.12);
  --cursor-mix: rgba(10,10,10,0.9);

  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 28px; --sp-7: 40px; --sp-8: 56px;
  --sp-9: 80px; --sp-10: 120px; --sp-11: 160px;

  --radius-s: 4px; --radius-m: 10px; --radius-l: 20px;
  --sidebar-w: 260px;
  --ease: cubic-bezier(.22,.68,0,1);
  --ease-soft: cubic-bezier(.16,1,.3,1);
  /* Single timing pair for every sidebar expand/collapse property, used
     identically in both the base rule and every .sidebar--collapsed
     override — no per-direction delays, no per-property duration
     tuning. That symmetry is what makes collapse read as the same
     motion as expand played backward, instead of two separately-tuned
     animations. Opacity/label-fade uses the faster pair so text finishes
     fading before the space around it finishes closing (see
     .sidebar__brand-text), but that ratio is identical in both
     directions since there is no direction-specific override anymore. */
  --sidebar-dur: .35s;
  --sidebar-dur-fast: .2s;
  --sidebar-ease: cubic-bezier(.4,0,.2,1);
  color-scheme: light;
}

*{ box-sizing: border-box; }
html{ scroll-behavior: smooth; }
body{
  margin:0; background: var(--bg); color: var(--text);
  font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased;
  font-size: 16px; line-height: 1.5;
}
::selection{ background: var(--accent); color: var(--accent-text); }
h1,h2,h3,h4{ font-family: 'Inter', sans-serif; font-weight: 600; margin: 0; letter-spacing: -0.01em; }
p{ margin: 0; }
a{ color: inherit; text-decoration: none; }
img{ max-width: 100%; display:block; }
button{ font-family: inherit; background: none; border: none; color: inherit; cursor: pointer; }
ul{ margin:0; padding:0; list-style:none; }
.eyebrow{ font-family: 'IBM Plex Mono', monospace; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); font-weight: 500; }
a:focus-visible, button:focus-visible{ outline: 2px solid var(--accent); outline-offset: 3px; }

/* progress + cursor */
.progress{ position: fixed; top:0; left:0; height: 2px; width: 0%; background: var(--accent); z-index: 9999; transition: width .1s linear; }
.cursor-dot, .cursor-ring{ position: fixed; top:0; left:0; pointer-events: none; z-index: 9999; border-radius: 50%; transform: translate(-50%,-50%); will-change: transform; }
.cursor-dot{ width: 6px; height: 6px; background: var(--cursor-mix); transition: opacity .2s; }
.cursor-ring{ width: 34px; height: 34px; border: 1px solid var(--cursor-mix); opacity: .5; transition: width .25s var(--ease), height .25s var(--ease), opacity .25s, background .25s; }
.cursor-ring.is-hover{ width: 60px; height: 60px; background: var(--accent-soft); opacity: .85; }

/* Liquid bubble cursor — replaces the crosshair above (main.js hides
   #cursorDot/#cursorRing and injects this instead). A soft, blurred
   radial-gradient blob in the site's own brand red, trailing the
   pointer with rAF-lerp lag and stretching/squishing along its
   direction of travel in proportion to how fast it's moving (a cheap
   speed-driven scale, not a real physics engine). Literal red rgb, not
   var(--accent) — this is the cursor's own consistent signature across
   every page, same reasoning as the sidebar's glow border, robust even
   if the accent token changes again later. Position is driven by the
   `translate` property (set every frame in JS) while `transform`
   handles centering + the squish/stretch + the hover-grow — the same
   translate/transform split used elsewhere in this file so position
   updates never fight a transform-driven state change. Only transform/
   translate/opacity change per frame or on hover — nothing here reads
   or writes layout, so it's compositor-only. */
.cursor-bubble{
  position: fixed; top:0; left:0; z-index: 9999; pointer-events: none;
  width: 34px; height: 34px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,90,78,.65), rgba(168,18,26,.35) 70%);
  filter: blur(8px); opacity: .85; will-change: transform;
  transition: opacity .3s;
}
.cursor-bubble.is-hover{ background: radial-gradient(circle, rgba(255,90,78,.85), rgba(168,18,26,.5) 70%); opacity: 1; }
@media (hover: none), (pointer: coarse){ .cursor-dot, .cursor-ring, .cursor-bubble{ display: none; } }
@media (prefers-reduced-motion: reduce){ *{ animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } }

/* ============ SIDEBAR — floating glass panel ============ */
/* --sidebar-gap is the margin the panel floats with off the top/left/
   bottom edges. .mainarea's margin-left has to account for both this
   gap AND the panel's own width, plus a second gap so content doesn't
   sit flush against the panel's right edge either. */
:root{ --sidebar-gap: 28px; }
.sidebar{
  position: fixed; top: var(--sidebar-gap); left: var(--sidebar-gap); bottom: var(--sidebar-gap);
  width: var(--sidebar-w); border-radius: 32px;
  background: rgba(255,255,255,.58);
  backdrop-filter: blur(24px) saturate(1.5);
  -webkit-backdrop-filter: blur(24px) saturate(1.5);
  border: 1px solid rgba(228,34,42,.14);
  /* third shadow layer is a soft, restrained red glow — a literal rgb
     value (not var(--accent)) so it stays robust if the accent token
     ever changes again; it happens to match the brand red on both
     themes right now, same as --accent does. Kept deliberately faint
     per "clean and minimal" — this is a hint of glow, not a neon
     border. */
  box-shadow: 0 24px 60px -24px rgba(10,10,10,.28), 0 6px 20px -10px rgba(10,10,10,.14), 0 0 24px -8px rgba(228,34,42,.1);
  display: flex; flex-direction: column; padding: var(--sp-6) var(--sp-5);
  z-index: 100; overflow-y: auto; overflow-x: hidden;
  /* border-radius/padding-left/padding-right added here — collapsing
     used to only transition width, so these three snapped instantly
     while the shell was still mid-resize, reading as a jump even
     though the width itself was animating smoothly. Everything below
     that used to rely on display:none (brand text, nav labels, the
     visual panel, the footer) has the same problem for the same
     reason — display can't be transitioned at all — and is switched to
     opacity+max-height fades further down instead. */
  transition: width var(--sidebar-dur) var(--sidebar-ease), border-radius var(--sidebar-dur) var(--sidebar-ease), padding-left var(--sidebar-dur) var(--sidebar-ease), padding-right var(--sidebar-dur) var(--sidebar-ease), background .3s, border-color .3s;
}
/* on index.html (body.page-dark) the panel floats over a near-black
   page instead of white, so it needs a dark tint + light-colored edge
   to read as glass — same shape/blur/shadow/red-glow concept, inverted
   base tint */
body.page-dark .sidebar{
  background: rgba(255,255,255,.07);
  border-color: rgba(228,34,42,.2);
  box-shadow: 0 24px 60px -24px rgba(0,0,0,.6), 0 6px 20px -10px rgba(0,0,0,.4), 0 0 28px -6px rgba(228,34,42,.15);
}
.sidebar__brand{
  /* flex-direction used to flip row<->column on collapse — a discrete
     property the browser can't tween, so it snapped mid-transition no
     matter how the delay was tuned. Row stays constant in both states
     instead: the mark is pinned at a fixed x (padding below is a
     constant, not a toggled value — the same left inset whether
     expanded or collapsed), and the brand text just fades/shrinks away
     beside it exactly like a nav label does. Padding is calibrated so
     the mark also lands visually centered in the 72px collapsed rail:
     collapsed interior is 72px - 2*var(--sp-3) = 48px, so var(--sp-1)
     (4px) each side leaves 40px for the 38px mark — 1px slack per
     side, imperceptible, and unnoticeable next to the mark at full
     width in the expanded state too. */
  display:flex; align-items:center; flex-wrap:wrap; gap: var(--sp-3); margin-bottom: var(--sp-8); padding: 0 var(--sp-1);
  transition: gap var(--sidebar-dur) var(--sidebar-ease), margin-bottom var(--sidebar-dur) var(--sidebar-ease), padding var(--sidebar-dur) var(--sidebar-ease);
}
.sidebar__mark{ width: 38px; height:38px; border-radius: 50%; background: var(--accent-gradient); color: var(--accent-text); display:flex; align-items:center; justify-content:center; font-family: 'Inter', sans-serif; font-weight:700; font-size:14px; flex-shrink:0; }
/* max-width+overflow:hidden (not display:none) is what lets this
   smoothly shrink-and-fade shut on collapse instead of vanishing
   instantly — same reasoning as .sidebar__nav-label/.sidebar__visual/
   .sidebar__foot below. Opacity (--sidebar-dur-fast) is deliberately
   faster than the max-width shrink (--sidebar-dur) so the text has
   actually faded out before the space around it finishes closing,
   rather than looking clipped mid-fade — same ratio, same tokens, in
   both directions, so it never needs a separate collapse-only timing. */
.sidebar__brand-text{ max-width: 180px; overflow:hidden; white-space:nowrap; opacity:1; transition: max-width var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease); }
.sidebar__brand-text b{ display:block; font-size: 14.5px; font-weight: 600; }
.sidebar__brand-text span{ display:block; font-family:'IBM Plex Mono', monospace; font-size: 10px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .05em; margin-top: 2px; }

.sidebar__nav{ display: flex; flex-direction: column; gap: var(--sp-4); margin-bottom: var(--sp-8); }
.sidebar__nav a{
  /* justify-content used to flip flex-start<->center on collapse — like
     .sidebar__brand's flex-direction, a discrete property that can only
     snap, never tween, so the icon visibly hopped sideways mid-
     transition no matter how the flip was delayed. flex-start stays
     constant in both states instead; padding is a fixed calibrated
     value (not toggled between an expanded and collapsed number) so
     the icon never moves horizontally at all. Collapsed interior is
     72px - 2*var(--sp-3) = 48px; var(--sp-4) (16px) each side leaves
     16px for the 17px icon — 0.5px overflow per side, invisible — while
     reading as ordinary left-aligned padding at full width. */
  font-size: 14px; color: var(--text-soft); padding: 13px var(--sp-4); border-radius: 999px; position:relative;
  display:flex; align-items:center; justify-content:flex-start; gap: var(--sp-3);
  transition: background .2s, color .2s, box-shadow .2s, padding var(--sidebar-dur) var(--sidebar-ease), gap var(--sidebar-dur) var(--sidebar-ease);
}
.sidebar__nav a:hover{ background: rgba(10,10,10,.06); color: var(--text); }
.sidebar__nav a.active{
  background: #fff; color: var(--text);
  box-shadow: 0 6px 16px -6px rgba(10,10,10,.25);
}
body.page-dark .sidebar__nav a:hover{ background: rgba(255,255,255,.08); }
body.page-dark .sidebar__nav a.active{ background: rgba(255,255,255,.95); color:#0d0d0d; box-shadow: 0 6px 16px -6px rgba(0,0,0,.5); }
.sidebar__nav a svg{ width: 17px; height: 17px; opacity: .75; flex-shrink:0; }
/* same max-width+opacity fade as .sidebar__brand-text, same reasoning */
.sidebar__nav-label{ max-width: 160px; overflow:hidden; white-space:nowrap; opacity:1; transition: max-width var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease); }

.sidebar__visual{
  border-radius: 24px; overflow: hidden; height: 150px; margin-bottom: auto;
  background: linear-gradient(150deg, var(--accent) 0%, #7C3AED 55%, #14B8A6 100%);
  /* background-size + a slow position drift is the "gradient itself
     subtly shifts color over time" ask — a real repaint of stops would
     be jarring; sliding the same three stops across an oversized layer
     just breathes gently between red/violet/teal instead. */
  background-size: 220% 220%;
  animation: sidebarFactsDrift 16s ease-in-out infinite;
  position: relative;
  display:flex; align-items:center; justify-content:center; padding: var(--sp-4);
  /* max-height (generous, well above the real 150px) + opacity, not
     display:none — transitioning max-height is what lets the sidebar's
     flex column reflow smoothly frame-by-frame as this shrinks shut,
     instead of the footer/nav instantly snapping into the freed space
     the moment collapse starts. */
  max-height: 200px; opacity: 1;
  transition: max-height var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease), margin var(--sidebar-dur) var(--sidebar-ease), padding var(--sidebar-dur) var(--sidebar-ease);
}
.sidebar__visual::after{ content:''; position:absolute; inset:0; background: radial-gradient(circle at 30% 20%, rgba(255,255,255,.25), transparent 60%); pointer-events:none; }
@keyframes sidebarFactsDrift{
  0%, 100%{ background-position: 0% 30%; }
  50%{ background-position: 100% 70%; }
}

/* ---- fun-facts card, injected into .sidebar__visual by
   initSidebarFacts() in main.js (same "shared markup stays untouched,
   behavior comes from main.js" pattern as the nav/contact icons above)
   ---- */
.sidebar-facts{ position:relative; z-index:1; width:100%; height:100%; display:flex; flex-direction:column; align-items:center; justify-content:center; gap: var(--sp-4); }
.sidebar-facts__stage{ position:relative; width:100%; flex:1; min-height:0; }
.sidebar-facts__fact{
  position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap: var(--sp-2);
  text-align:center; padding: 0 var(--sp-2);
  opacity:0; transform: translateY(10px);
  transition: opacity .5s var(--ease-soft), transform .5s var(--ease-soft);
}
.sidebar-facts__fact.is-active{ opacity:1; transform: translateY(0); }
.sidebar-facts__fact.is-prev{ opacity:0; transform: translateY(-10px); }
.sidebar-facts__icon{ width:32px; height:32px; border-radius:50%; background: rgba(255,255,255,.22); display:flex; align-items:center; justify-content:center; color:#fff; flex-shrink:0; }
.sidebar-facts__icon svg{ width:16px; height:16px; }
.sidebar-facts__label{ font-size:12px; line-height:1.35; font-weight:600; color:#fff; }
.sidebar-facts__dots{ position:relative; z-index:1; display:flex; align-items:center; gap:6px; flex-shrink:0; }
.sidebar-facts__dot{ width:5px; height:5px; border-radius:50%; background: rgba(255,255,255,.35); transition: background .2s, transform .2s var(--ease); }
.sidebar-facts__dot.is-active{ background:#fff; transform: scale(1.4); }
.sidebar__foot{
  padding-top: var(--sp-7); padding-left: var(--sp-2); padding-right: var(--sp-2);
  max-height: 240px; opacity: 1; overflow:hidden;
  transition: max-height var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease), padding-top var(--sidebar-dur) var(--sidebar-ease);
}
/* max-height+opacity fade, not display:none — same reasoning as
   .sidebar__brand-text/.sidebar__nav-label above; this is the only
   part of the foot that still hides when collapsed. */
.sidebar__foot-label{ font-family: 'IBM Plex Mono', monospace; font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-faint); margin-bottom: var(--sp-4); max-height: 24px; opacity: 1; overflow:hidden; transition: max-height var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease), margin-bottom var(--sidebar-dur) var(--sidebar-ease); }
.sidebar__links{ display: flex; flex-direction: column; gap: var(--sp-3); }
/* .sidebar__links-main groups the icon+label so justify-content:
   space-between on the row only ever distributes space between that
   group and the trailing arrow, not between the icon and its own
   label (see js/main.js's contact-link injection). space-between
   itself is a continuous (interpolable-enough) property here and
   stays constant in both states — it never needs to flip to "center",
   because once the label and trailing arrow both shrink to zero width
   on collapse, space-between naturally leaves the icon flush against
   the row's start edge on its own, which is exactly the fixed x-
   position .sidebar__nav a achieves by staying at flex-start. Padding
   is likewise a constant, calibrated value rather than a toggled one:
   collapsed interior here is 72px - 2*var(--sp-3) (sidebar) -
   2*var(--sp-2) (.sidebar__foot's own inset) = 32px, so var(--sp-2)
   (8px) each side leaves 16px for the 17px icon — same ~0.5px/side
   rounding as the nav icons above, invisible in practice. */
.sidebar__links a{ font-size: 12.5px; color: var(--text-soft); display:flex; align-items:center; justify-content:space-between; padding: 13px var(--sp-2); border-radius: 999px; position:relative; transition: color .2s, background .2s, padding var(--sidebar-dur) var(--sidebar-ease); }
.sidebar__links a:hover{ color: var(--accent); }
/* flex-shrink:0 everywhere here: overflow:hidden on .sidebar__links-label
   makes its auto min-width resolve to 0 per the flexbox spec, so
   without this the shrink algorithm (nested two flex levels deep, with
   the arrow span competing for space) was squeezing "Email" down to
   "Ema" instead of leaving it full-width until the max-width transition
   actually collapses it on sidebar collapse. */
.sidebar__links-main{ display:flex; align-items:center; gap: var(--sp-3); flex-shrink:0; transition: gap var(--sidebar-dur) var(--sidebar-ease); }
.sidebar__links a svg{ width:17px; height:17px; opacity:.75; flex-shrink:0; }
.sidebar__links-label{ max-width: 140px; overflow:hidden; white-space:nowrap; opacity:1; flex-shrink:0; transition: max-width var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease); }
.sidebar__links a > span:last-child{ color: var(--text-faint); font-family:'IBM Plex Mono', monospace; font-size:10.5px; max-width: 20px; opacity:1; overflow:hidden; flex-shrink:0; transition: max-width var(--sidebar-dur) var(--sidebar-ease), opacity var(--sidebar-dur-fast) var(--sidebar-ease); }

.sidebar--collapsed .sidebar__links a:hover{ background: rgba(10,10,10,.06); }
body.page-dark .sidebar--collapsed .sidebar__links a:hover{ background: rgba(255,255,255,.08); }
.sidebar--collapsed .sidebar__links-main{ gap:0; }
.sidebar--collapsed .sidebar__links-label{ max-width:0; opacity:0; }
.sidebar--collapsed .sidebar__links a > span:last-child{ max-width:0; opacity:0; }

.sidebar-toggle{ display:none; }

/* ============ SIDEBAR — collapsible rail ============ */
/* .sidebar__toggle, the icon <svg> inside each nav link, and the
   .sidebar__nav-label wrapper are all injected by initSidebarCollapse()
   in main.js — no page's sidebar markup needed to change for this to
   apply site-wide. The glass/floating/pill treatment above applies
   identically whether the panel is expanded or collapsed — only width
   and label visibility change below. */
.sidebar__toggle{
  display:flex; align-items:center; justify-content:center; width:26px; height:26px; margin-left:auto;
  border: 1px solid rgba(10,10,10,.12); border-radius: 50%; color: var(--text-soft); flex-shrink:0;
  transition: border-color .2s, color .2s, background .2s;
}
.sidebar__toggle:hover{ border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.sidebar__toggle svg{ width:13px; height:13px; transition: transform var(--sidebar-dur) var(--sidebar-ease); }
.sidebar--collapsed .sidebar__toggle svg{ transform: rotate(180deg); }
body.page-dark .sidebar__toggle{ border-color: rgba(255,255,255,.18); }

.sidebar--collapsed{ width: 72px; border-radius: 36px; padding-left: var(--sp-3); padding-right: var(--sp-3); }
/* .sidebar__brand no longer flips flex-direction on collapse (see its
   base rule above) — row stays constant, so this override only needs
   to shrink the gap/margin, both continuous properties that tween
   cleanly on their own. The touch-only .sidebar__toggle button (see
   main.js) can end up too wide to sit beside the mark in the 48px
   collapsed interior; .sidebar__brand's flex-wrap:wrap lets it drop to
   its own line there instead of overlapping, rather than adding a
   second discrete-property special case just for that edge. */
.sidebar--collapsed .sidebar__brand{ margin-bottom: var(--sp-6); }
.sidebar--collapsed .sidebar__brand-text{ max-width:0; opacity:0; }
/* gap:0 removes the invisible-label's leftover gap that was pulling
   the icon off-center — padding and justify-content are both constant
   now (see .sidebar__nav a's base rule above), so this is the only
   property left to override here. */
.sidebar--collapsed .sidebar__nav a{ gap:0; }
.sidebar--collapsed .sidebar__nav-label{ max-width:0; opacity:0; }
.sidebar--collapsed .sidebar__visual{ max-height:0; opacity:0; margin-bottom:0; padding:0; }
/* the foot no longer collapses away entirely — Email/Phone/LinkedIn
   stay visible as icons even when collapsed, so only the "Get in
   touch" heading (no room for it at 72px) fades out; padding-top
   shrinks rather than zeroing so the icon row keeps some breathing
   room under the nav. */
.sidebar--collapsed .sidebar__foot{ padding-top: var(--sp-4); }
.sidebar--collapsed .sidebar__foot-label{ max-height:0; opacity:0; margin-bottom:0; }

/* collapsed-state tooltip — shown on hover since the label text is hidden */
.sidebar--collapsed .sidebar__nav a::after, .sidebar--collapsed .sidebar__links a::after{
  content: attr(data-label); position:absolute; left: calc(100% + 16px); top:50%; transform: translateY(-50%) translateX(-4px);
  background: var(--text); color:#fff; font-family:'IBM Plex Mono', monospace; font-size: 11px; white-space:nowrap;
  padding: 6px 10px; border-radius: var(--radius-s); pointer-events:none; opacity:0; z-index: 200;
  transition: opacity .2s, transform .2s;
}
.sidebar--collapsed .sidebar__nav a:hover::after, .sidebar--collapsed .sidebar__links a:hover::after{ opacity:1; transform: translateY(-50%) translateX(0); }

/* ============ MAIN WRAP — true overlay, not a reserved column ============ */
/* .mainarea itself no longer reserves any width for the sidebar — it's
   full viewport width, so section backgrounds/borders genuinely span
   edge to edge, with the glass sidebar floating on top of that
   (z-index), not sitting in its own separate gutter beside it.
   --content-clear is the one place readable TEXT still needs to stay
   clear of the sidebar's footprint, applied only to the few direct
   .mainarea children that actually hold text (.wrap, .topbar, .footer)
   — NOT to .mainarea as a whole, which is what would make this a
   column-reservation layout again in disguise.
   It's sized to the EXPANDED sidebar width always, even while
   collapsed — content position must not shift when the sidebar
   toggles; only the sidebar's own width changes. Reset to 0 inside
   the existing 980px breakpoint, where the sidebar already becomes a
   static, non-overlapping top bar. */
:root{ --content-clear: calc(var(--sidebar-w) + var(--sidebar-gap) * 2); }
.mainarea{ margin-left: 0; }
/* Collapsing the sidebar frees up real width (260px panel down to a
   72px rail) — content should reclaim that space and re-center into it,
   not hold a clearance sized for the expanded sidebar forever (which
   read as "stuck shifted, cramped" — the bug this fixes). So
   --content-clear itself becomes collapse-aware here, recalculated to
   the collapsed rail's footprint, and .wrap/.topbar/.footer each get a
   `padding-left` transition below so the recalculation animates at the
   same .3s timing as the sidebar's own width transition instead of
   snapping. */
.sidebar--collapsed ~ .mainarea{ margin-left: 0; --content-clear: calc(72px + var(--sidebar-gap) * 2); }
.wrap{ max-width: 1080px; margin: 0 auto; padding: 0 var(--sp-8) 0 calc(var(--content-clear) + var(--sp-8)); transition: padding-left var(--sidebar-dur) var(--sidebar-ease); }

/* topbar inside main area (status row) — same --content-clear as .wrap,
   since it's a direct .mainarea child that isn't itself wrapped in .wrap */
.topbar{ display:flex; justify-content: space-between; align-items:center; padding: var(--sp-6) var(--sp-8) 0 calc(var(--content-clear) + var(--sp-8)); transition: padding-left var(--sidebar-dur) var(--sidebar-ease); }
.topbar__badge{ display:inline-flex; align-items:center; gap:7px; font-family:'IBM Plex Mono', monospace; font-size: 11.5px; color: var(--text-soft); }
.topbar__badge::before{ content:''; width:6px; height:6px; border-radius:50%; background:#3FBF6F; box-shadow: 0 0 0 3px rgba(63,191,111,0.18); }
.topbar__loc{ font-family:'IBM Plex Mono', monospace; font-size: 11.5px; color: var(--text-faint); display:flex; align-items:center; gap:6px; }

/* ============ HERO ============ */
/* min-height + flex centering vertically centers .wrap's content (the
   intro block through the collage) as one unit within the viewport
   below the topbar — 120px is a rough estimate of the topbar's own
   height plus breathing room, not a pixel-exact measurement; min-height
   (not height) means short viewports/lots of content still just
   overflow normally rather than clipping. */
.hero{
  position:relative; overflow:hidden; text-align:center;
  min-height: calc(100vh - 120px);
  display:flex; flex-direction:column; justify-content:center;
  padding: var(--sp-9) 0;
}
/* width:100% is load-bearing, not decorative: .wrap is a flex item now
   that .hero is a flex column (for the vertical-centering above), and
   .wrap's own margin:0 auto counts as an auto cross-axis margin to the
   flex algorithm — which disables the default stretch-to-fill behavior
   and left .wrap sizing to its own max-content width instead (driven by
   .hero__title wanting up to 19em). Harmless on wide viewports where
   1080px + auto margins already centers it either way, but on mobile
   it let the title's max-width genuinely exceed the viewport instead of
   wrapping inside it. Explicit width:100% forces normal block-style
   sizing regardless of the flex stretch quirk. */
.hero .wrap{ position:relative; z-index:1; width:100%; }
/* No display:flex/align-items here — this is just a semantic grouping
   wrapper. Its children already center themselves individually
   (margin:0 auto on .hero__title/.hero__sub, justify-content:center on
   .hero__cta-row), and align-items:center on a flex parent would have
   switched them from "stretch to full available width, then wrap" to
   fit-content sizing on the cross axis — which let .hero__title's
   max-width (646px at its 34px min font-size) win out over an actual
   ~320px mobile viewport instead of the text wrapping inside it,
   causing real horizontal overflow at 375px. Plain block flow avoids
   that entirely. */
.hero__eyebrow{ font-family:'IBM Plex Mono', monospace; font-size: 12px; color: var(--text-faint); margin-bottom: var(--sp-5); }
/* max-width raised from 15em/34em — "give the hero more width" so a
   2-line headline/subtext doesn't need uncomfortably small text to get
   there; both are still capped well short of .wrap's own 1080px so
   line length stays comfortable, just less aggressively narrowed. */
.hero__title{ font-size: clamp(34px, 4.4vw, 54px); font-weight: 800; letter-spacing: -0.02em; line-height: 1.15; max-width: 19em; margin: 0 auto; }
.hero__title .accent-word{ color: var(--accent); }
.hero__sub{ font-size: 16px; color: var(--text-soft); max-width: 40em; margin: var(--sp-6) auto 0; line-height: 1.6; }
.hero__cta-row{ display:flex; align-items:center; justify-content:center; gap: var(--sp-7); margin-top: var(--sp-7); flex-wrap:wrap; }
.btn-primary{ font-family:'IBM Plex Mono', monospace; font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; background: var(--text); color: var(--bg); padding: 13px 26px; border-radius: 30px; display:inline-block; transition: background .25s, transform .2s var(--ease); }
.btn-primary:hover{ background: var(--accent); }
/* Replaces the old single-line .hero__tag mono caption — same info
   (years + teams shipped to), presented as a compact labeled-number
   pairing beside the CTA button instead of a plain text string. */
.hero__stats{ display:flex; align-items:center; gap: var(--sp-5); }
.hero__stat{ display:flex; flex-direction:column; align-items:flex-start; line-height:1.2; }
.hero__stat b{ font-family:'Inter', sans-serif; font-size: 20px; font-weight: 700; color: var(--text); }
.hero__stat span{ font-family:'IBM Plex Mono', monospace; font-size: 9.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); margin-top: 2px; }
.hero__stat-divider{ width: 1px; height: 28px; background: var(--line-strong); }

/* collage */
.collage{ display:grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-5); margin-top: var(--sp-9); }
.collage__panel{ aspect-ratio: 4/3; border-radius: var(--radius-m); overflow:hidden; border: 1px solid var(--line); background: var(--bg-2); transition: transform .5s var(--ease-soft); }
.collage__panel:hover{ transform: translateY(-6px); }
.collage__panel .browser-frame, .collage__panel .mono-panel{ height:100%; }

.browser-frame{ background: var(--surface); height:100%; display:flex; flex-direction:column; }
.browser-frame__bar{ display: flex; align-items: center; gap: var(--sp-3); padding: 10px var(--sp-4); background: var(--bg-2); border-bottom: 1px solid var(--line); }
.bf-dot{ width: 8px; height: 8px; border-radius: 50%; }
.bf-dot--r{ background: #E5675A; } .bf-dot--y{ background: #E8B84B; } .bf-dot--g{ background: #63B76C; }
.browser-frame__body{ padding: var(--sp-4); flex:1; display:flex; flex-direction:column; justify-content:center; }
/* auto-advancing crossfade carousel, swapped in for one hero collage
   card in place of the old static mock-dash content (see
   initHeroCarousel() in main.js) — position:relative/overflow:hidden
   plus padding:0 turn the existing .browser-frame__body into a full-
   bleed image window instead of the padded, vertically-centered text
   layout the base rule was built for; flex:1 still comes from that
   base rule since both classes land on the same element. */
.hero-carousel{ padding:0; position:relative; overflow:hidden; }
/* the inactive state sits at a hair over 100% scale so the incoming
   image settles inward as it fades in (rather than a flat opacity
   swap) — the outgoing image runs the same transform in reverse,
   nudging outward as it fades out, so the pair reads as one image
   dissolving/blending into the next instead of a hard cut. */
.hero-carousel img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; opacity:0; transform: scale(1.04); transition: opacity .7s ease-in-out, transform .7s ease-in-out; }
.hero-carousel img.is-active{ opacity:1; transform: scale(1); }
.mono-panel{ position:relative; display:flex; align-items:center; justify-content:center; flex-direction:column; gap: var(--sp-3); color: var(--accent-text); background: linear-gradient(150deg, var(--accent), #7C3AED); padding: var(--sp-4); text-align:center; }
/* .mono-panel's direct <b>/<span> children (the old large centered "EK
   / Agentic UX" content) are no longer in the markup — replaced by the
   corner badge + rotating quote below — but left here undeleted since
   nothing else uses .mono-panel. */
.mono-panel span{ font-family:'IBM Plex Mono', monospace; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; opacity:.9; }
.mono-panel b{ font-family: 'Inter', sans-serif; font-size: 26px; }
/* small corner monogram — a watermark, not the card's focal point
   anymore, so it sits low-contrast/translucent rather than solid like
   .sidebar__mark elsewhere on the site. position:absolute takes it out
   of the panel's own centering flex flow entirely, so the quote below
   can be perfectly centered in the full card regardless of the badge. */
.mono-panel__badge{
  position:absolute; top: var(--sp-3); left: var(--sp-3);
  width:24px; height:24px; border-radius:50%;
  background: rgba(255,255,255,.16); border: 1px solid rgba(255,255,255,.3);
  display:flex; align-items:center; justify-content:center;
  font-family:'Inter', sans-serif; font-weight:700; font-size:10px; color: rgba(255,255,255,.92);
}
.mono-panel__quote{ display:flex; flex-direction:column; align-items:center; gap: var(--sp-2); width:100%; transition: opacity .4s var(--ease); }
.mono-panel__quote.is-hidden{ opacity:0; }
.mono-panel__quote-label{ font-family:'IBM Plex Mono', monospace; font-size: 9.5px; text-transform: uppercase; letter-spacing: .1em; color: rgba(255,255,255,.65); }
/* width (not max-width) is deliberate: .mono-panel__quote's
   align-items:center means a flex-column child sizes to its own
   content by default, so a percentage max-width here was being ignored
   and long lines were overflowing the card instead of wrapping — an
   explicit width forces it to actually fill (and wrap within) the
   already-constrained 100%-wide parent. */
.mono-panel__quote-line{
  margin:0; width: 92%; font-family:'IBM Plex Mono', monospace; font-size: 11px; line-height:1.5; color:#fff;
  word-wrap: break-word;
}
.mono-panel__quote-cursor{ display:inline-block; margin-left:1px; animation: monoCursorBlink 1s step-end infinite; }
@keyframes monoCursorBlink{ 0%, 49%{ opacity:1; } 50%, 100%{ opacity:0; } }

.demo__msg{ font-size: 12.5px; line-height: 1.5; }
.demo__msg--ai .demo__ai-tag{ font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; display: block; margin-bottom: 4px; }
.mock-dash__tabs{ display:flex; gap: var(--sp-4); margin-bottom: var(--sp-3); }
.mock-dash__tab{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; color: var(--text-faint); }
.mock-dash__tab.active{ color: var(--text); border-bottom: 2px solid var(--accent); padding-bottom: 6px; }
.mock-dash__row-title{ font-size: 12.5px; color: var(--text); font-weight: 500; }

/* ============ SECTION HEAD ============ */
.section-head{ text-align:center; margin-bottom: var(--sp-8); }
.section-head .eyebrow-pill{ display:inline-block; font-family:'IBM Plex Mono', monospace; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-soft); border: 1px solid var(--line-strong); padding: 6px 14px; border-radius: 20px; margin-bottom: var(--sp-5); }
.section-head h2{ font-size: clamp(28px, 3.6vw, 40px); line-height: 1.2; max-width: 14em; margin: 0 auto; }

/* ============ CLIENTS & PRODUCTS — infinite marquee ============ */
.logowall{ padding: var(--sp-8) 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow:hidden; }
.logowall__label{ text-align:center; font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-faint); margin-bottom: var(--sp-6); }
.logowall__viewport{
  overflow:hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
/* .logowall__track holds two identical .logowall__row sets side by side;
   animating it by exactly -50% of its own (doubled) width loops seamlessly
   back to a frame identical to the start — the standard trick for a
   no-visible-jump marquee. */
.logowall__track{ display:flex; width: max-content; animation: logowallScroll 26s linear infinite; }
.logowall__row{ display:flex; gap: var(--sp-7); padding-right: var(--sp-7); }
.logowall__viewport:hover .logowall__track{ animation-play-state: paused; }
@keyframes logowallScroll{ from{ transform: translateX(0); } to{ transform: translateX(-50%); } }
/* Logos sit directly on the page background — no plate, real colors by
   default (not grayscale). Two escape hatches, applied per-logo via
   modifier class where the source file genuinely needs it:
     --invert  : for a solid black mark on an opaque (non-transparent)
                 white background — filter:invert(1) flips it to a white
                 mark on a black field, which then blends almost
                 seamlessly with this page's near-black background.
                 (Only correct for solid black-on-white; inverting a
                 colorful logo would wreck its real hue instead.)
     --plate   : for logos that are BOTH opaque (no alpha — the source
                 file's white background is baked in, not transparent)
                 AND genuinely full-color, where invert() would distort
                 the real brand color and no blend-mode can cleanly
                 strip an opaque white background over a dark backdrop
                 without also crushing the color (multiply darkens the
                 logo toward black same as the page; screen keeps white
                 white instead of removing it). A small tight white
                 plate is the only clean option left for that specific
                 combination. */
.logowall__chip{
  flex-shrink:0; display:flex; align-items:center; justify-content:center; height: 76px; padding: 0 var(--sp-5);
  transition: transform .3s var(--ease), opacity .3s;
  opacity: .82;
}
.logowall__chip:hover{ opacity: 1; transform: scale(1.06); }
.logowall__chip span{ font-family:'Inter', sans-serif; font-weight: 700; font-size: 15px; white-space:nowrap; color: var(--text); }
/* source logo files vary wildly in size/aspect ratio (from ~130px wide
   wordmarks up to 3840px raster exports) — normalizing height with
   width:auto + object-fit:contain is what keeps the row looking even
   regardless of each file's native dimensions. max-height raised ~36%
   (28px -> 38px) with the chip's own height scaled by the same ratio
   (56px -> 76px) so the breathing room around each logo stays
   proportional instead of just cropping tighter. */
.logowall__chip img{ max-height: 38px; max-width: 100%; width:auto; height:auto; object-fit: contain; }
body.page-dark .logowall__chip--invert img{ filter: invert(1); }
body.page-dark .logowall__chip--plate{
  background:#fff; border-radius: var(--radius-s); padding: 8px var(--sp-4); height:auto;
}
body.page-dark .logowall__chip--plate img{ max-height: 30px; }

/* ============ WHAT I DO — card grid ============ */
.whatido{ padding: var(--sp-10) 0; }
.whatido__grid{ display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-5); }
.whatido__card{
  border: 1px solid var(--line); border-radius: var(--radius-m); padding: var(--sp-6); min-height: 190px;
  display:flex; flex-direction:column; justify-content:flex-start;
  opacity: 0; translate: 0 24px;
  transition: opacity .6s var(--ease-soft), translate .6s var(--ease-soft), border-color .3s, transform .3s var(--ease), background .3s var(--ease);
}
.whatido__grid.is-visible .whatido__card{ opacity: 1; translate: 0 0; }
/* staggered entrance — reuses the existing .reveal/.is-visible trigger already
   on .whatido__grid (see initScrollReveal in main.js); only the opacity/translate
   pair is delayed per card so hover response (border-color, transform, background)
   stays instant. .whatido__card--cta deliberately does NOT redeclare `transition`
   itself, so it inherits this exact 5-property list — a separate shorthand there
   would silently drop the entrance transition and desync these delay lists. */
.whatido__grid .whatido__card:nth-child(1){ transition-delay: 0s, 0s, 0s, 0s, 0s; }
.whatido__grid .whatido__card:nth-child(2){ transition-delay: .08s, .08s, 0s, 0s, 0s; }
.whatido__grid .whatido__card:nth-child(3){ transition-delay: .16s, .16s, 0s, 0s, 0s; }
.whatido__grid .whatido__card:nth-child(4){ transition-delay: .24s, .24s, 0s, 0s, 0s; }
.whatido__grid .whatido__card:nth-child(5){ transition-delay: .32s, .32s, 0s, 0s, 0s; }
.whatido__grid .whatido__card:nth-child(6){ transition-delay: .4s, .4s, 0s, 0s, 0s; }
.whatido__card:hover{ border-color: var(--accent); transform: translateY(-4px); }
.whatido__card-top{ display:flex; justify-content:space-between; align-items:flex-start; }
.whatido__card h4{ font-size: 17px; font-weight: 600; }
.whatido__card p{ font-size: 13.5px; color: var(--text-soft); line-height: 1.6; margin-top: var(--sp-4); }

.whatido__icon{ width: 44px; height: 44px; border-radius: 50%; background: var(--bg-2); display:flex; align-items:center; justify-content:center; margin-bottom: var(--sp-4); flex-shrink:0; transition: background .3s var(--ease); }
.whatido__icon svg{ width: 22px; height: 22px; stroke: var(--text-soft); transition: stroke .3s var(--ease), transform .3s var(--ease); }
.whatido__card:hover .whatido__icon{ background: var(--accent-soft); }
.whatido__card:hover .whatido__icon svg{ stroke: var(--accent); transform: scale(1.08); }

.whatido__card--cta{ background: var(--text); color: var(--bg); border-color: var(--text); display:flex; flex-direction:column; justify-content:space-between; }
.whatido__card--cta h4{ color: var(--bg); font-size: 16px; }
.whatido__card--cta p{ color: rgba(255,255,255,.65); font-size: 13px; }
.whatido__card--cta .btn-primary{ background: var(--accent); color: #fff; align-self:flex-start; }
.whatido__card--cta .whatido__icon{ background: rgba(255,255,255,.08); }
.whatido__card--cta .whatido__icon svg{ stroke: rgba(255,255,255,.85); }
.whatido__card--cta:hover{ background: color-mix(in srgb, var(--text) 88%, white); border-color: color-mix(in srgb, var(--text) 88%, white); }
.whatido__card--cta:hover .whatido__icon{ background: rgba(255,255,255,.14); }
.whatido__card--cta:hover .whatido__icon svg{ stroke: #fff; transform: translate(2px, -2px); }

/* ============ ABOUT ============ */
.about-section{ padding: var(--sp-10) 0; border-top: 1px solid var(--line); }
.about-section__grid{ display: grid; grid-template-columns: 1.3fr 1fr; gap: var(--sp-9); align-items: start; }
.about-section__eyebrow{ font-family:'IBM Plex Mono', monospace; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); margin-bottom: var(--sp-5); }
.about-section__bio{ font-size: 17px; line-height: 1.75; color: var(--text-soft); }
.about-section__bio strong{ color: var(--text); }
.about-section__logos{ margin-top: var(--sp-8); }
.about-section__logos-label{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-faint); margin-bottom: var(--sp-5); }
.about-section__logos-track{ display:flex; flex-wrap:wrap; gap: var(--sp-6); }
/* Small supporting photo near the bio's bike mention — deliberately
   modest (200px), not hero-scale, matching the rounded-corner/border/
   shadow treatment already used on cards elsewhere on the site
   (.collage__panel, .portfolio__card) rather than inventing a new one. */
.about-section__ride{ margin: var(--sp-7) 0 0; }
.about-section__ride-img{
  width: 200px; max-width: 100%; aspect-ratio: 3/4; object-fit: cover;
  border-radius: var(--radius-m); border: 1px solid var(--line);
  box-shadow: 0 14px 30px -18px var(--shadow);
}
.about-section__ride-cap{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); margin-top: var(--sp-3); }
/* Same no-plate, original-color, size-normalized treatment as
   .logowall__chip above — this is the "worked on products for" row's
   own variant of the identical logo set, just laid out as a wrapping
   row (.about-section__logos-track) instead of a scrolling marquee. */
/* height/img max-height both raised ~34-35% (32px->43px, 24px->32px),
   same ratio as .logowall__chip above, so the two "worked with" logo
   treatments stay visually consistent at their new size. */
.logo-chip{ display:flex; align-items:center; height: 43px; opacity: .7; transition: opacity .3s, transform .3s var(--ease); }
.logo-chip:hover{ opacity: 1; transform: scale(1.05); }
.logo-chip span{ font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 600; color: var(--text-faint); }
.logo-chip:hover span{ color: var(--text); }
.logo-chip img{ max-height: 32px; max-width: 100%; width:auto; height:auto; object-fit: contain; }
body.page-dark .logo-chip--invert img{ filter: invert(1); }
body.page-dark .logo-chip--plate{ background:#fff; border-radius: var(--radius-s); padding: 6px 12px; height:auto; }
body.page-dark .logo-chip--plate img{ max-height: 24px; }
/* Mirror image of --plate above: a small DARK plate for logos with a
   solid-white element that only goes invisible on the LIGHT theme (see
   initLogoTrack()'s comment in main.js for which logos and why) —
   removed again once body.page-dark is present, since neither logo
   needs it there. */
.logo-chip--dark-plate{ background:#0d0d0d; border-radius: var(--radius-s); padding: 6px 12px; height:auto; }
.logo-chip--dark-plate img{ max-height: 24px; }
body.page-dark .logo-chip--dark-plate{ background:none; padding:0; height:43px; border-radius:0; }
body.page-dark .logo-chip--dark-plate img{ max-height: 32px; }

.portrait-panel{ border-radius: var(--radius-l); overflow: hidden; aspect-ratio: 3/4; background: linear-gradient(160deg, #1a1a1c 0%, #2F4BDB 60%, #7C3AED 100%); position: relative; display:flex; align-items:flex-end; padding: var(--sp-6); }
.portrait-panel::before{ content:''; position:absolute; inset:0; background: radial-gradient(circle at 70% 20%, rgba(255,255,255,.18), transparent 55%); }
.portrait-panel__photo{ position:absolute; inset:0; z-index:0; width:100%; height:100%; object-fit: cover; object-position: center top; }
/* Carousel: same container/frame as .portrait-panel itself (unchanged) —
   this is a content swap inside that existing frame, not a new
   component. Both images sit stacked via position:absolute at the same
   inset, and a plain opacity crossfade (no slide, matching this site's
   restrained motion language) switches which one is visible. */
.portrait-carousel{ position:absolute; inset:0; z-index:0; }
.portrait-carousel__img{
  position:absolute; inset:0; width:100%; height:100%; object-fit: cover; object-position: center top;
  opacity:0; transition: opacity 1s var(--ease-soft);
}
.portrait-carousel__img.is-active{ opacity:1; }
.portrait-panel__mono{ position:relative; z-index:1; }
.portrait-panel__mono b{ font-family: 'Inter', sans-serif; font-size: 44px; color: #fff; display:block; }
.portrait-panel__mono span{ font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: rgba(255,255,255,.75); text-transform: uppercase; letter-spacing: .08em; }

/* ============ PORTFOLIO — image card grid ============ */
.portfolio{ padding: var(--sp-10) 0; border-top: 1px solid var(--line); }
.portfolio__grid{ display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-5); }
.portfolio__card{ position: relative; border-radius: var(--radius-m); overflow: hidden; aspect-ratio: 4/3; border: 1px solid var(--line); transition: transform .4s var(--ease-soft); }
.portfolio__card:hover{ transform: translateY(-5px); }
.portfolio__card-visual{ position:absolute; inset:0; }
.portfolio__card-img{ width:100%; height:100%; object-fit: cover; object-position: center; }
.portfolio__card-overlay{ position:absolute; left:0; right:0; bottom:0; padding: var(--sp-5); display:flex; justify-content:space-between; align-items:flex-end; background: linear-gradient(to top, rgba(0,0,0,.55), transparent 70%); }
.portfolio__card-title{ color:#fff; font-size: 19px; font-weight: 600; }
.portfolio__card-meta{ color: rgba(255,255,255,.75); font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; text-align:right; }
.portfolio__card-badge{ position:absolute; top: var(--sp-4); right: var(--sp-4); font-family:'IBM Plex Mono', monospace; font-size: 9.5px; text-transform:uppercase; color:#fff; background: rgba(0,0,0,.35); padding: 5px 10px; border-radius: 20px; letter-spacing:.04em; }
/* Hover-only "View Project" CTA — a separate layer from
   .portfolio__card-overlay (which stays always-visible for title/meta).
   pointer-events:none on the scrim so it never blocks the card's own
   href; the pill itself doesn't need pointer-events restored since the
   whole card is the link. Entrance is opacity + translateY + a slight
   scale so the pill reads as "arriving", not just fading in — the
   scrim darkens at the same time so the pill stays legible over any
   thumbnail regardless of that image's own colors. */
.portfolio__card-hover{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  background: rgba(0,0,0,0); opacity:0; pointer-events:none;
  transition: opacity .35s var(--ease-soft), background .35s var(--ease-soft);
}
.portfolio__card:hover .portfolio__card-hover{ opacity:1; background: rgba(0,0,0,.42); }
.portfolio__card-cta{
  display:inline-flex; align-items:center; gap: 8px;
  font-family:'IBM Plex Mono', monospace; font-size: 12px; text-transform:uppercase; letter-spacing:.05em;
  color:#fff; background: var(--accent-gradient); padding: 13px 24px; border-radius:999px;
  box-shadow: 0 14px 28px -10px rgba(0,0,0,.55);
  transform: translateY(16px) scale(.88); opacity:0;
  transition: transform .45s var(--ease-soft) .05s, opacity .4s var(--ease-soft) .05s;
}
.portfolio__card:hover .portfolio__card-cta{ transform: translateY(0) scale(1); opacity:1; }
.portfolio__card-cta svg{ width:14px; height:14px; transition: transform .3s var(--ease); }
.portfolio__card:hover .portfolio__card-cta svg{ transform: translate(3px,-3px); }
.portfolio__explore{ text-align:center; margin-top: var(--sp-8); }

/* ============ PROCESS — scroll-driven timeline ============ */
.process{ padding: var(--sp-10) 0; border-top: 1px solid var(--line); }
.process__grid{ display: grid; grid-template-columns: 0.8fr 1.2fr; gap: var(--sp-9); }
.process__eyebrow{ font-family:'IBM Plex Mono', monospace; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); margin-bottom: var(--sp-5); }
.process__heading{ font-size: clamp(26px, 3vw, 36px); line-height: 1.25; }

.process__list{ position:relative; display:flex; flex-direction:column; gap: var(--sp-5); padding-left: 34px; }
.process__list::before{ content:''; position:absolute; left: 4px; top: 6px; bottom: 6px; width: 2px; background: var(--line-strong); }
.process__row{
  position:relative; padding: var(--sp-6) var(--sp-5); margin-left: -18px; border-radius: var(--radius-m);
  border-left: 2px solid transparent;
  transition: border-color .3s var(--ease), background .3s var(--ease), transform .3s var(--ease);
}
/* No first-child padding override (there used to be one) — every row now
   uses the exact same top padding, which is what the marker dot's `top`
   below is computed from. That was the root cause of the misalignment:
   the marker was a hardcoded 34px for every row, but the first row had
   8px less top padding than the rest, so only the first dot was ever
   actually off — invisible at a glance, but real at every viewport
   width since it wasn't a responsive issue, it was a fixed-vs-token
   mismatch. Deriving `top` from the same --sp-6 token the icon's own
   position depends on keeps them in sync automatically. */
.process__row::before{
  content:''; position:absolute; left: -32px; top: calc(var(--sp-6) + 18px); width: 11px; height: 11px; border-radius: 50%;
  background: var(--bg); border: 2px solid var(--line-strong);
  transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease);
}
.process__row:hover, .process__row.is-active{ border-left-color: var(--accent); background: var(--bg-2); transform: translateX(4px); }
.process__row:hover::before, .process__row.is-active::before{ background: var(--accent); border-color: var(--accent); transform: scale(1.2); }

.process__row-icon.whatido__icon{ width: 36px; height: 36px; margin-bottom: var(--sp-4); }
.process__row-icon svg{ width: 18px; height: 18px; }
.process__row:hover .process__row-icon, .process__row.is-active .process__row-icon{ background: var(--accent-soft); }
.process__row:hover .process__row-icon svg, .process__row.is-active .process__row-icon svg{ stroke: var(--accent); }

.process__row-top{ display:flex; align-items:baseline; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.process__row-top h4{ font-size: 18px; font-weight: 600; }
.process__row-top span{ font-family:'IBM Plex Mono', monospace; font-size: 11px; color: var(--accent); }
.process__row p{ font-size: 14px; color: var(--text-soft); line-height: 1.6; }

/* ============ PROCESS — horizontal card layout (index.html only) ============
   New class names (.process__cards/.process__card etc.), not a
   restructure of .process__grid/.process__list/.process__row above —
   those stay exactly as they were for about.html, which still uses the
   old vertical-line-with-dot markup and still needs them. This is a
   parallel, independent set of rules for index.html's new markup. */
.process__head{ text-align:center; margin-bottom: var(--sp-8); }
.process__head .process__eyebrow{ margin-bottom: var(--sp-4); }
.process__head .process__heading{ font-size: clamp(28px, 3.6vw, 40px); max-width: 14em; margin: 0 auto; }

.process__cards{ display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-5); }
.process__card{
  border: 1px solid var(--line); border-radius: var(--radius-m); padding: var(--sp-6);
  opacity: 0; translate: 0 24px;
  transition: opacity .6s var(--ease-soft), translate .6s var(--ease-soft), border-color .3s, transform .3s var(--ease), box-shadow .3s var(--ease);
}
.process__cards.is-visible .process__card{ opacity: 1; translate: 0 0; }
.process__cards .process__card:nth-child(1){ transition-delay: 0s, 0s, 0s, 0s, 0s; }
.process__cards .process__card:nth-child(2){ transition-delay: .12s, .12s, 0s, 0s, 0s; }
.process__cards .process__card:nth-child(3){ transition-delay: .24s, .24s, 0s, 0s, 0s; }
.process__card:hover{ transform: translateY(-6px); border-color: var(--accent); box-shadow: 0 20px 40px -22px var(--shadow); }

.process__card-icon.whatido__icon{ width: 44px; height: 44px; margin-bottom: var(--sp-5); }
.process__card:hover .process__card-icon{ background: var(--accent-soft); }
.process__card:hover .process__card-icon svg{ stroke: var(--accent); }

.process__card-top{ display:flex; align-items:baseline; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.process__card-top h4{ font-size: 18px; font-weight: 600; }
.process__card-top span{ font-family:'IBM Plex Mono', monospace; font-size: 11px; color: var(--accent); }
.process__card p{ font-size: 14px; color: var(--text-soft); line-height: 1.6; }

/* ============ FEEDBACK — bento grid, scoped dark theme ============ */
/* --feedback-accent is a custom property defined only on .feedback, so
   it's only ever visible to elements inside this section — CSS custom
   properties don't leak to siblings or the rest of the page. It's red
   now too (matching the rest of the site's accent), kept as its own
   scoped variable rather than switched to var(--accent) directly so this
   section can still diverge again later without touching the rest of
   the site. The star rating specifically does NOT use this variable —
   see .feedback__stars below for its own gold gradient. */
.feedback{
  --feedback-accent: #E4222A;
  position:relative; overflow:hidden; padding: var(--sp-10) 0; background: #0d0d0d;
}
/* pure-CSS grain: a small tiled fractal-noise SVG, blended over the
   near-black background at very low opacity. No image file. */
.feedback::before{
  content:''; position:absolute; inset:0; pointer-events:none; opacity:.05; mix-blend-mode:overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.feedback .wrap{ position:relative; z-index:1; }
.feedback .section-head .eyebrow-pill{ color: rgba(255,255,255,.7); border-color: rgba(255,255,255,.2); }
.feedback .section-head h2{ color:#fff; }

/* Quote columns (1-2) are wider than the stat column (3) — stat cards
   read as compact accent cards next to full-size quote cards instead of
   matching them. grid-auto-rows:auto (not a fixed/minmax row height)
   means every row sizes off its own tallest cell — safe now that the
   4 quotes are all trimmed to a similar length (see the quotes
   themselves below), so rows land close to even without forcing it,
   and nothing gets clipped if one quote/role happens to wrap an extra
   line. Stat cards sit one-per-row now (not spanning both rows), so
   their height also just follows whatever their row's quote cards need
   — no separate sizing logic to keep in sync. */
.feedback__grid{ display:grid; grid-template-columns: 1.3fr 1.3fr 0.9fr; grid-auto-rows: auto; gap: var(--sp-5); }
.feedback__grid .feedback__card:nth-of-type(1){ grid-column: 1; grid-row: 1; }
.feedback__grid .feedback__card:nth-of-type(2){ grid-column: 2; grid-row: 1; }
.feedback__grid .feedback__card:nth-of-type(3){ grid-column: 1; grid-row: 2; }
.feedback__grid .feedback__card:nth-of-type(4){ grid-column: 2; grid-row: 2; }
.feedback__stat--white{ grid-column: 3; grid-row: 1; }
.feedback__stat--accent{ grid-column: 3; grid-row: 2; }

.feedback__card{
  display:flex; flex-direction:column; background: #1a1a1a; color:#fff;
  border: 1px solid rgba(255,255,255,.1); border-radius: var(--radius-m); padding: var(--sp-6);
  opacity:0; translate: 0 20px;
  transition: opacity .6s var(--ease-soft), translate .6s var(--ease-soft), transform .3s var(--ease), border-color .3s;
}
.feedback__grid.is-visible .feedback__card{ opacity:1; translate: 0 0; }
.feedback__grid .feedback__card:nth-of-type(1){ transition-delay: 0s, 0s, 0s, 0s; }
.feedback__grid .feedback__card:nth-of-type(2){ transition-delay: .08s, .08s, 0s, 0s; }
.feedback__grid .feedback__card:nth-of-type(3){ transition-delay: .16s, .16s, 0s, 0s; }
.feedback__grid .feedback__card:nth-of-type(4){ transition-delay: .24s, .24s, 0s, 0s; }
.feedback__card:hover{ transform: translateY(-4px); border-color: var(--feedback-accent); }

/* Stars are gold, deliberately independent of --feedback-accent (red) —
   a 5-star rating reads as gold/amber in real life, and that shouldn't
   change just because the site's own brand accent does. Each star's
   fill is url(#starGold), a gradient defined once (see the hidden <svg
   width="0" height="0"> right before the first feedback card) and
   referenced by every star icon — SVG gradient defs are resolved
   against the whole document, so one definition can be shared by every
   <svg> on the page, not just the one it's declared inside. */
.feedback__stars{ display:flex; gap: 3px; margin-bottom: var(--sp-4); }
.feedback__stars svg{ width: 13px; height: 13px; }
.feedback__quote{ font-size: 14px; line-height: 1.6; color: rgba(255,255,255,.85); flex:1; margin-bottom: var(--sp-5); }
.feedback__meta{ display:flex; align-items:center; gap: var(--sp-3); }
.feedback__avatar{
  width: 36px; height: 36px; border-radius: 50%; background: var(--accent-gradient);
  flex-shrink:0; display:flex; align-items:center; justify-content:center;
  font-family:'IBM Plex Mono', monospace; font-size: 11px; font-weight:600; color:#fff;
}
.feedback__name{ display:block; font-size: 14px; font-weight: 600; }
.feedback__role{ display:block; font-family:'IBM Plex Mono', monospace; font-size: 9.5px; text-transform: uppercase; letter-spacing: .05em; color: rgba(255,255,255,.5); margin-top: 2px; }

.feedback__stat{
  display:flex; flex-direction:column; justify-content:center; border-radius: var(--radius-m); padding: var(--sp-5);
  opacity:0; translate: 0 20px;
  transition: opacity .6s var(--ease-soft), translate .6s var(--ease-soft), transform .3s var(--ease), border-color .3s;
}
.feedback__grid.is-visible .feedback__stat{ opacity:1; translate: 0 0; }
/* THE BLANK-CARD BUG: this card's background is a hardcoded, always-
   white #fff, but its text color used to read var(--text) — fine on
   every light-themed page (var(--text) is near-black there), but on
   index.html's body.page-dark, var(--text) is redefined to white,
   which made this white-on-white and fully invisible. Hardcoded to
   match the card's own always-white fill instead, same pattern
   .feedback__stat--accent already uses for its own hardcoded text. */
.feedback__stat--white{ transition-delay: .32s, .32s, 0s, 0s; background:#fff; color: #0d0d0d; border: 1px solid rgba(255,255,255,.1); }
.feedback__stat--accent{ transition-delay: .4s, .4s, 0s, 0s; background: var(--accent-gradient); color: #fff; border: 1px solid var(--feedback-accent); }
.feedback__stat:hover{ transform: translateY(-4px); }
.feedback__stat--white:hover{ border-color: var(--feedback-accent); }
.feedback__stat p{ font-size: 11.5px; margin-bottom: var(--sp-3); opacity:.7; }
.feedback__stat b{ font-family:'Inter', sans-serif; font-size: 34px; font-weight: 800; line-height:1; letter-spacing:-0.02em; }
.feedback__stat span{ display:block; font-family:'IBM Plex Mono', monospace; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; margin-top: var(--sp-2); opacity:.65; }

/* ============ TESTIMONIAL — unused sitewide now (about.html's old
   "Reviews" section was replaced by .experience below), left in place
   rather than deleted per this site's usual "harmless unused CSS stays"
   convention. ============ */
.testimonial{ padding: var(--sp-10) 0; border-top: 1px solid var(--line); text-align:center; }
.testimonial__card{ max-width: 46em; margin: 0 auto; border: 1px solid var(--line); border-radius: var(--radius-m); padding: var(--sp-8); }
.testimonial__quote{ font-size: 20px; line-height: 1.6; color: var(--text); font-weight: 500; }
.testimonial__quote cite{ display:block; font-family:'IBM Plex Mono', monospace; font-style:normal; font-size: 12px; color: var(--text-faint); margin-top: var(--sp-6); text-transform: uppercase; letter-spacing: .04em; }

/* ============ EXPERIENCE — minimal animated work-history list ============ */
/* A plain bordered row list rather than a card grid, deliberately —
   "minimal" per the brief, and it reads more like a real resume/CV
   section this way. Logos sit directly on the page with no
   background plate, same "real colors, no box" rule already used for
   every other logo on this site (these are the same transparent PNGs
   from the "Companies I've worked with" row, just at a smaller fixed
   size here). */
.experience{ padding: var(--sp-10) 0; border-top: 1px solid var(--line); }
.experience__list{ max-width: 46em; margin: 0 auto; display:flex; flex-direction:column; }
.experience__row{
  display:flex; align-items:flex-start; gap: var(--sp-6); padding: var(--sp-6) var(--sp-4);
  border-bottom: 1px solid var(--line); border-left: 2px solid transparent;
  opacity:0; translate: 0 20px;
  transition: opacity .6s var(--ease-soft), translate .6s var(--ease-soft), border-color .3s var(--ease), background .3s var(--ease), padding-left .3s var(--ease);
}
.experience__list.is-visible .experience__row{ opacity:1; translate:0 0; }
.experience__list .experience__row:nth-child(1){ transition-delay: 0s, 0s, 0s, 0s, 0s; }
.experience__list .experience__row:nth-child(2){ transition-delay: .12s, .12s, 0s, 0s, 0s; }
.experience__list .experience__row:nth-child(3){ transition-delay: .24s, .24s, 0s, 0s, 0s; }
.experience__row:last-child{ border-bottom:none; }
.experience__row:hover{ border-left-color: var(--accent); background: var(--bg-2); padding-left: var(--sp-5); }

.experience__logo{ width:56px; height:56px; flex-shrink:0; display:flex; align-items:center; justify-content:center; transition: transform .3s var(--ease); }
.experience__row:hover .experience__logo{ transform: scale(1.08); }
.experience__logo img{ max-width:100%; max-height:100%; width:auto; height:auto; object-fit:contain; }

.experience__body{ flex:1; min-width:0; }
.experience__top{ display:flex; align-items:baseline; justify-content:space-between; gap: var(--sp-4); flex-wrap:wrap; margin-bottom: 2px; }
.experience__top h4{ font-size: 17px; font-weight: 600; }
.experience__period{ font-family:'IBM Plex Mono', monospace; font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: .05em; white-space:nowrap; }
.experience__company{ font-family:'IBM Plex Mono', monospace; font-size: 11.5px; color: var(--accent); text-transform: uppercase; letter-spacing: .04em; margin-bottom: var(--sp-3); }
.experience__row p{ font-size: 14px; color: var(--text-soft); line-height: 1.6; }

/* ============ CERTIFICATIONS — paginated card carousel ============ */
/* .cert-carousel__page is absolutely positioned when inactive (removed
   from flow, so it can't affect .cert-carousel__track's height) and
   becomes position:relative only while .is-active — that's what lets
   .cert-carousel__track's height (set explicitly in initCertCarousel(),
   main.js) track just the current page's real content height, in case
   a page's cards ever wrap to a different number of lines than another's.
   Badge colors are the one deliberate brand-color nod (Google blue,
   Udemy violet, IBM blue, LinkedIn blue) on otherwise plain initials —
   "recognizable without being an exact logo", same reasoning as
   .recognition__item's color-coded icons above. */
.certifications{ padding: var(--sp-9) 0; border-top: 1px solid var(--line); }
.cert-carousel__track{ position:relative; overflow:hidden; transition: height .4s var(--ease-soft); }
.cert-carousel__page{
  display:grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-5);
  position:absolute; inset:0; opacity:0; visibility:hidden; pointer-events:none;
  transform: translateX(16px);
  transition: opacity .4s var(--ease), transform .4s var(--ease), visibility 0s .4s;
}
.cert-carousel__page.is-active{
  position:relative; opacity:1; visibility:visible; pointer-events:auto; transform:translateX(0);
  transition: opacity .4s var(--ease), transform .4s var(--ease);
}
.cert-card{
  border: 1px solid var(--line); border-radius: var(--radius-m); padding: var(--sp-6);
  transition: border-color .3s, transform .3s var(--ease), box-shadow .3s;
}
.cert-card:hover{ border-color: var(--accent); transform: translateY(-4px); box-shadow: 0 14px 28px -18px var(--shadow); }
.cert-card__badge{
  width: 40px; height: 40px; border-radius: 50%; color:#fff;
  display:flex; align-items:center; justify-content:center; margin-bottom: var(--sp-4);
  font-family:'Inter', sans-serif; font-weight:700; font-size: 13px;
}
.cert-card__badge--pg{ background: var(--accent); }
.cert-card__badge--google{ background: #4285F4; }
.cert-card__badge--udemy{ background: #A435F0; }
.cert-card__badge--ibm{ background: #0F62FE; font-size: 10.5px; }
.cert-card__badge--linkedin{ background: #0A66C2; font-size: 15px; }
.cert-card__title{ font-size: 14.5px; font-weight: 600; line-height: 1.4; margin-bottom: var(--sp-3); min-height: 2.8em; }
.cert-card__issuer{ font-size: 12.5px; color: var(--text-soft); margin-bottom: var(--sp-1); }
.cert-card__date{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); }

.cert-carousel__controls{ display:flex; align-items:center; justify-content:center; gap: var(--sp-5); margin-top: var(--sp-7); }
/* same circular-icon-button treatment as .sidebar__toggle elsewhere on
   the site, reused here rather than inventing a new control style. */
.cert-carousel__arrow{
  display:flex; align-items:center; justify-content:center; width:36px; height:36px; flex-shrink:0;
  border: 1px solid var(--line-strong); border-radius:50%; color: var(--text-soft); background:none; cursor:pointer;
  transition: border-color .2s, color .2s, background .2s;
}
.cert-carousel__arrow svg{ width:16px; height:16px; }
.cert-carousel__arrow:hover{ border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.cert-carousel__dots{ display:flex; align-items:center; gap: var(--sp-3); }
.cert-carousel__dot{ width:8px; height:8px; border-radius:50%; background: var(--line-strong); border:none; padding:0; cursor:pointer; transition: background .2s, transform .2s var(--ease); }
.cert-carousel__dot.is-active{ background: var(--accent); transform: scale(1.3); }
.cert-carousel__dot:hover{ background: var(--text-soft); }

/* ============ RECOGNITION — illustrated award cards ============ */
/* Deliberately steps outside the strict black/white/red system, per the
   brief — colors reuse hues already present elsewhere in this codebase
   (gold from .bf-dot--y, blue from the portrait-panel/case gradients,
   teal + purple from .sidebar__visual's gradient) rather than inventing
   new ones, so it still feels like the same site having a celebratory
   moment, not a different site pasted in. */
.recognition{ padding: var(--sp-9) 0; border-top: 1px solid var(--line); background: var(--bg-2); }
.recognition__grid{ display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-6); }
.recognition__item{
  position:relative; border-radius: var(--radius-m); padding: var(--sp-6); border: 1px solid var(--line);
  background: linear-gradient(150deg, color-mix(in srgb, var(--rec-color) 10%, white), color-mix(in srgb, var(--rec-color) 22%, white));
  transition: transform .3s var(--ease), border-color .3s, box-shadow .3s, background .3s;
}
.recognition__item:hover{
  transform: translateY(-5px); border-color: var(--rec-color);
  box-shadow: 0 14px 28px -16px color-mix(in srgb, var(--rec-color) 50%, transparent);
  background: linear-gradient(150deg, color-mix(in srgb, var(--rec-color) 16%, white), color-mix(in srgb, var(--rec-color) 32%, white));
}
.recognition__item b{ display:block; font-size: 15px; font-weight: 600; margin-bottom: var(--sp-1); }
.recognition__item p{ font-size: 12.5px; color: var(--text-soft); line-height: 1.5; }
.recognition__item--gold{ --rec-color: #E8B84B; }
.recognition__item--blue{ --rec-color: #2F4BDB; }
.recognition__item--teal{ --rec-color: #14B8A6; }
.recognition__item--purple{ --rec-color: #7C3AED; }

.recognition__icon{
  width: 52px; height: 52px; border-radius: 50%; background: var(--rec-color); color: #fff;
  display:flex; align-items:center; justify-content:center; margin-bottom: var(--sp-4);
  box-shadow: 0 6px 16px -6px color-mix(in srgb, var(--rec-color) 60%, transparent);
  transition: transform .4s var(--ease);
}
.recognition__icon svg{ width: 26px; height: 26px; }
.recognition__item--gold:hover .recognition__icon{ transform: scale(1.12) rotate(-8deg); }
.recognition__item--blue:hover .recognition__icon{ transform: rotate(12deg) scale(1.06); }
.recognition__item--teal:hover .recognition__icon{ transform: scale(1.15) translateY(-3px); }
.recognition__item--purple:hover .recognition__icon{ transform: translateY(-4px) rotate(14deg) scale(1.12); }

.writing{ padding: var(--sp-9) 0; border-top: 1px solid var(--line); }
.article-card{ border: 1px solid var(--line); border-radius: var(--radius-m); display:grid; grid-template-columns: 1fr 1.6fr; gap: var(--sp-7); align-items:center; transition: border-color .3s, transform .3s var(--ease); }
.article-card:hover{ border-color: var(--accent); transform: translateY(-3px); }
.article-card__visual{ border-radius: var(--radius-s); display:flex; align-items:center; justify-content:center; overflow:hidden; }
.article-card__visual span{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform:uppercase; letter-spacing:.08em; color:#fff; text-align:center; padding: var(--sp-4); background: linear-gradient(150deg, var(--accent), #14B8A6); width:100%; aspect-ratio: 4/3; display:flex; align-items:center; justify-content:center; }
.article-card__img{ width:100%; height:auto; display:block; }
.article-card__tag{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; color: var(--accent); letter-spacing: .05em; margin-bottom: var(--sp-3); display:block; }
.article-card h3{ font-size: 22px; font-weight: 600; margin-bottom: var(--sp-3); }
.article-card p{ font-size: 14px; color: var(--text-soft); line-height: 1.6; margin-bottom: var(--sp-4); }
.article-card__link{ font-family:'IBM Plex Mono', monospace; font-size: 12px; text-transform: uppercase; border-bottom: 1px solid var(--text); padding-bottom:2px; transition: color .2s, border-color .2s; }
.article-card__link:hover{ color: var(--accent); border-color: var(--accent); }
.article-card__link .nudge-arrow{ display:inline-block; transition: transform .2s var(--ease); }
.article-card__link:hover .nudge-arrow{ transform: translate(2px, -2px); }

/* ============ CONTACT / BIG CTA ============ */
.contact-strip{ padding: var(--sp-8) 0; border-top: 1px solid var(--line); display:flex; justify-content: space-between; flex-wrap:wrap; gap: var(--sp-5); font-family:'IBM Plex Mono', monospace; font-size: 12.5px; }
.contact-strip a{ color: var(--text-soft); transition: color .2s; }
.contact-strip a:hover{ color: var(--accent); }

/* .wrap gets the min-height + flex centering here (not #contact itself)
   specifically so .bigcta — a sibling of .wrap, sitting right after it
   in normal flow — is completely unaffected: it keeps its own margin/
   position exactly as before, only the eyebrow+heading+cards block
   above it gets centered within its own box. 60vh is a deliberately
   more modest figure than the hero's own ~100vh centering elsewhere on
   the site — this is a secondary page section sharing the viewport
   with .bigcta below it, not a first-screen hero. */
.contact-hero{ min-height: 60vh; display:flex; flex-direction:column; justify-content:center; padding: var(--sp-9) 0; }

.contact-cards{ display:flex; flex-wrap:wrap; justify-content:center; gap: var(--sp-5); margin-top: var(--sp-8); }
.contact-card{
  display:flex; align-items:center; gap: var(--sp-4); min-width: 250px;
  border: 1px solid var(--line); border-radius: var(--radius-m); padding: var(--sp-5) var(--sp-6);
  transition: border-color .3s, transform .3s var(--ease), background .3s var(--ease);
}
/* same hover language as .whatido__card: lift + border-color to accent
   + icon circle background to accent-soft + icon color to accent —
   reused rather than invented, per the brief. */
.contact-card:hover{ border-color: var(--accent); transform: translateY(-4px); }
.contact-card__icon.whatido__icon{ margin-bottom: 0; }
.contact-card:hover .contact-card__icon{ background: var(--accent-soft); }
.contact-card__icon svg{ stroke: var(--text-soft); transition: stroke .3s var(--ease), fill .3s var(--ease), transform .3s var(--ease); }
.contact-card:hover .contact-card__icon svg{ stroke: var(--accent); transform: scale(1.08); }
/* the LinkedIn mark is the one filled (not stroked) icon here — using
   the real brand glyph is worth the one-off inconsistency, since a
   generic stroke icon wouldn't read as "LinkedIn" the way this does.
   Its currentColor fill still follows the same hover-to-accent
   transition as the other three via this modifier. */
.contact-card__icon svg.icon-fill{ fill: var(--text-soft); stroke: none; }
.contact-card:hover .contact-card__icon svg.icon-fill{ fill: var(--accent); }
.contact-card__body{ display:flex; flex-direction:column; align-items:flex-start; text-align:left; }
.contact-card__label{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); margin-bottom: 3px; }
.contact-card__value{ font-size: 14.5px; font-weight: 600; color: var(--text); }

.bigcta{ background: var(--text); color: var(--bg); padding: var(--sp-10) var(--sp-8); text-align:center; border-radius: var(--radius-l) var(--radius-l) 0 0; margin-top: var(--sp-8); }
.bigcta h2{ font-size: clamp(48px, 9vw, 110px); line-height: 1; font-weight: 700; letter-spacing: -0.02em; }
.bigcta__sub{ font-family:'IBM Plex Mono', monospace; font-size: 12.5px; color: rgba(255,255,255,.55); margin-top: var(--sp-6); text-transform: uppercase; letter-spacing:.05em; }

/* same --content-clear as .wrap/.topbar — the sidebar is fixed to the
   viewport, so it's still floating over the left edge of the screen
   even once the user has scrolled all the way down to the footer */
.footer{ padding: var(--sp-6) var(--sp-8) var(--sp-6) calc(var(--content-clear) + var(--sp-8)); display: flex; justify-content: space-between; font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--text-faint); background: var(--text); border-radius: 0; flex-wrap: wrap; gap: var(--sp-3); transition: padding-left .3s var(--ease); }
.footer a{ color: rgba(255,255,255,.6); }

/* ============ REVEAL ============ */
.reveal{ opacity: 0; transform: translateY(24px); transition: opacity .7s var(--ease-soft), transform .7s var(--ease-soft); }
.reveal.is-visible{ opacity: 1; transform: translateY(0); }


/* ============ CASE STUDY PAGE COMPONENTS ============ */
/* (device mockups, case hero, diagrams, stats — used on project-*.html) */
.demo__label{ display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--sp-4); }
.demo__label .eyebrow{ color: var(--text-faint); font-size: 10.5px; }
.demo__dot{ width: 6px; height: 6px; border-radius: 50%; background: #5FBF7E; box-shadow: 0 0 0 3px rgba(95,191,126,0.18); display: inline-block; }
.demo__thread{ min-height: 178px; display: flex; flex-direction: column; gap: var(--sp-3); justify-content: flex-end; margin-bottom: var(--sp-4); }
.demo__msg{ font-size: 13.5px; line-height: 1.5; opacity: 0; transform: translateY(6px); animation: msgIn .45s var(--ease) forwards; }
.demo__msg--user{ align-self: flex-end; background: var(--accent-soft); border: 1px solid var(--line); padding: 9px 13px; border-radius: var(--radius-s); max-width: 80%; color: var(--text); }
.demo__msg--ai{ color: var(--text-soft); max-width: 92%; }
.demo__msg--ai .demo__ai-tag{ font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; display: block; margin-bottom: 4px; }
@keyframes msgIn{ to{ opacity:1; transform: translateY(0); } }
.demo__cards{ display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-2); margin-top: var(--sp-1); }
.demo__card{ background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--radius-s); padding: 9px 10px; font-size: 11px; opacity: 0; transform: translateY(8px); animation: msgIn .4s var(--ease) forwards; }
.demo__card b{ display:block; font-size: 11.5px; margin-bottom: 3px; font-weight: 500; color: var(--text);}
.demo__card span{ color: var(--text-faint); font-family:'IBM Plex Mono',monospace; font-size: 9.5px; text-transform: uppercase; }
.demo__thinking{ font-family: 'IBM Plex Mono', monospace; font-size: 11.5px; color: var(--accent); display: flex; align-items: center; gap: 6px; }
.demo__thinking::before{ content:''; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); animation: pulse 1s ease-in-out infinite; }
@keyframes pulse{ 0%,100%{opacity:1;} 50%{opacity:.25;} }
.demo__prompts{ display: flex; gap: var(--sp-2); flex-wrap: wrap; border-top: 1px solid var(--line); padding-top: var(--sp-4); }
.demo__prompt{ font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: var(--text-soft); border: 1px solid var(--line-strong); padding: 7px 12px; border-radius: 20px; transition: background .2s, border-color .2s; }
.demo__prompt:hover, .demo__prompt:focus-visible{ background: var(--accent-soft); border-color: var(--accent); color: var(--text); }
.demo__prompt:disabled{ opacity: .35; }
.demo__caption{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--text-faint); margin-top: var(--sp-3); text-align: right; }
.case-hero{ padding: 150px 0 var(--sp-8); border-bottom: 1px solid var(--line); }
.case-hero__back{ display:inline-flex; align-items:center; gap: var(--sp-2); font-family:'IBM Plex Mono', monospace; font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint); margin-bottom: var(--sp-7); transition: color .2s, gap .2s; }
.case-hero__back:hover{ color: var(--accent); gap: var(--sp-3); }
.case-hero__back svg{ width: 13px; height: 13px; }
.case-hero__top{ display:flex; justify-content: space-between; align-items:flex-start; gap: var(--sp-7); flex-wrap:wrap; margin-bottom: var(--sp-6); }
.case-hero__title{ font-family: 'Inter', sans-serif; font-weight: 400; font-size: clamp(42px, 6vw, 80px); line-height: 1.05; }
.case-hero__nav{ display:flex; gap: var(--sp-3); font-family: 'IBM Plex Mono', monospace; font-size: 11px; }
.case-hero__nav a{ padding: 8px 14px; border: 1px solid var(--line-strong); border-radius: 20px; color: var(--text-faint); transition: color .2s, border-color .2s; }
.case-hero__nav a.on{ color: var(--accent); border-color: var(--accent); }
.case-hero__meta-row{ display: flex; gap: var(--sp-9); flex-wrap: wrap; margin-top: var(--sp-7); padding-top: var(--sp-7); border-top: 1px solid var(--line); }
.case-hero__meta dt{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-faint); margin-bottom: var(--sp-2); }
.case-hero__meta dd{ margin:0; font-size: 15px; color: var(--text-soft); max-width: 20em; }
.case-content{ padding: var(--sp-9) 0; }
.case-content p{ font-size: 17.5px; line-height: 1.75; color: var(--text-soft); margin-bottom: var(--sp-6); max-width: 46em; }
.case-content strong{ color: var(--text); font-weight: 600; }
.case-content h3{ font-size: 26px; font-weight: 500; margin: var(--sp-9) 0 var(--sp-5); max-width: 30em; }
.case__quote{ font-family: 'Inter', sans-serif; font-style: normal; font-weight: 500; font-size: 24px; line-height: 1.5; color: var(--text); border-left: 2px solid var(--accent); padding-left: var(--sp-6); margin: var(--sp-9) 0; max-width: 38em; }
.case__quote cite{ display: block; font-family: 'IBM Plex Mono', monospace; font-style: normal; font-size: 11px; color: var(--text-faint); margin-top: var(--sp-4); text-transform: uppercase; letter-spacing: 0.04em; }
.case__visual{ margin: var(--sp-9) 0; perspective: 1200px; }
.case__visual .browser-frame, .case__visual .phone-frame{ margin: 0 auto; }
.case__visual-cap{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; color: var(--text-faint); letter-spacing: 0.05em; margin-top: var(--sp-4); text-align: center; }
.mock-chat__sidebar{ display:flex; gap:6px; margin-bottom: var(--sp-4); }
.mock-chat__pill{ font-family:'IBM Plex Mono', monospace; font-size: 10px; padding: 5px 10px; border-radius: 20px; border: 1px solid var(--line-strong); color: var(--text-faint); }
.mock-chat__pill.active{ background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.mock-dash__tabs{ display:flex; gap: var(--sp-5); border-bottom: 1px solid var(--line); margin-bottom: var(--sp-4); }
.mock-dash__tab{ font-family:'IBM Plex Mono', monospace; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-faint); padding-bottom: 10px; border-bottom: 2px solid transparent; }
.mock-dash__tab.active{ color: var(--text); border-color: var(--accent); }
.mock-dash__row{ display:flex; justify-content:space-between; align-items:center; padding: 12px 0; border-bottom: 1px solid var(--line); }
.mock-dash__row:last-child{ border-bottom:none; }
.mock-dash__row-title{ font-size: 13.5px; color: var(--text); font-weight: 500; }
.mock-dash__row-sub{ font-size: 11.5px; color: var(--text-faint); font-family: 'IBM Plex Mono', monospace; }
.mock-dash__status{ font-family:'IBM Plex Mono', monospace; font-size: 10px; text-transform: uppercase; padding: 4px 10px; border-radius: 20px; background: var(--accent-soft); color: var(--accent); }
.phone-frame{ width: 270px; background: var(--surface); border: 8px solid #1A1A1C; border-radius: 34px; padding: 10px; box-shadow: 0 40px 80px -30px var(--shadow); transition: transform .5s var(--ease-soft); }
.phone-frame__notch{ width: 90px; height: 18px; background: #1A1A1C; border-radius: 0 0 12px 12px; margin: 0 auto 6px; }
.phone-frame__screen{ background: var(--bg-2); border-radius: 22px; overflow:hidden; position: relative; height: 410px; }
.mock-map{ position: relative; height: 235px; background: radial-gradient(circle at 30% 30%, var(--accent-soft), transparent 60%), linear-gradient(135deg, var(--bg-2), var(--bg)); overflow:hidden; }
.mock-map::before{ content:''; position:absolute; inset:0; background-image: linear-gradient(var(--line) 1px, transparent 1px), linear-gradient(90deg, var(--line) 1px, transparent 1px); background-size: 22px 22px; opacity: .5; }
.mock-pin{ position:absolute; width: 10px; height:10px; border-radius: 50% 50% 50% 0; background: var(--accent); transform: rotate(-45deg); box-shadow: 0 2px 6px var(--shadow); }
.mock-pin--main{ width:14px; height:14px; background: var(--red); }
.mock-sheet{ background: var(--surface); padding: 14px 16px; position:relative; }
.mock-sheet__handle{ width: 34px; height:4px; background: var(--line-strong); border-radius: 4px; margin: 0 auto 12px; }
.mock-sheet__title{ font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.mock-sheet__sub{ font-family:'IBM Plex Mono', monospace; font-size: 10.5px; color: var(--text-faint); margin-bottom: 12px; }
.mock-sheet__btn{ background: var(--accent); color: var(--accent-text); text-align:center; padding: 10px; border-radius: 8px; font-size: 12.5px; font-weight: 600; }
.case__diagram{ margin: var(--sp-8) 0; border: 1px solid var(--line); background: var(--bg-2); padding: var(--sp-6); border-radius: var(--radius-m); }
.case__diagram-label{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; color: var(--text-faint); letter-spacing: 0.05em; margin-bottom: var(--sp-5); }
.flow{ display: flex; align-items: center; gap: 0; flex-wrap: wrap; }
.flow__step{ font-size: 12.5px; background: var(--surface); border: 1px solid var(--line-strong); padding: 9px 13px; white-space: nowrap; color: var(--text-soft); border-radius: var(--radius-s); }
.flow__arrow{ font-family: 'IBM Plex Mono', monospace; color: var(--text-faint); padding: 0 var(--sp-2); font-size: 13px; }
/* for a cluster of parallel (not sequential) options inside one flow —
   e.g. the branch of app sections after "Home dashboard" on
   po-cruises.html. A single wide .flow__step can't wrap internally
   (white-space:nowrap) and would overflow its card at narrow widths;
   grouping the options as separate .flow__step chips with their own
   gap, wrapped as a unit, fixes that while reusing the exact same chip
   styling — no new visual language, just a wrapper. */
.flow__step-group{ display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.flow--before .flow__step{ color: var(--red); border-color: color-mix(in srgb, var(--red) 35%, transparent); }
.flow--after .flow__step{ color: var(--text); border-color: var(--line-strong); background: var(--bg-2); font-weight: 600; }
.case__stats{ display: flex; gap: var(--sp-8); margin: var(--sp-8) 0 var(--sp-2); flex-wrap: wrap; }
.case__stats div b{ display: block; font-family: 'Inter', sans-serif; font-size: 32px; font-weight: 500; color: var(--accent); font-variant-numeric: tabular-nums; }
.case__stats div span{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; color: var(--text-faint); }
.case-nextprev{ display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--line); }
.case-nextprev a{ padding: var(--sp-8); transition: background .3s; }
.case-nextprev a:first-child{ border-right: 1px solid var(--line); }
.case-nextprev a:hover{ background: var(--bg-2); }
.case-nextprev__label{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; text-transform: uppercase; color: var(--text-faint); letter-spacing: .05em; margin-bottom: var(--sp-3); display:block; }
.case-nextprev__title{ font-family: 'Inter', sans-serif; font-size: 28px; font-weight: 500; }

/* =============================================================
   DARK THEME — site-wide, via body.page-dark
   Every page now carries this class (index.html, about.html,
   projects.html, contact.html, and every case-study page), so the
   whole site shares one black/red theme rather than index.html being
   the one dark page among light ones. Every component above already
   reads color through var(--bg)/var(--text)/var(--accent)/var(--line)/
   etc. rather than hardcoded hex, so redefining just the token layer
   re-themes most of every page automatically — including all the
   case-study-only components (.chapter, .decision, .media--placeholder,
   .impact-card, .learnings__item, .star-summary, etc. from
   case-study.css), verified by reading that file fully and confirming
   it has no hardcoded colors either. What follows the token block are
   the handful of targeted fixes for spots that had theme-specific logic
   baked in and would otherwise break or look wrong under a pure token
   swap — each one explains why inline. .feedback is NOT touched here:
   it already hardcodes its own #0d0d0d/--feedback-accent directly
   rather than reading the shared tokens, so it already matches this
   theme exactly and stays correct with zero changes.
   ============================================================= */
/* Neutral grey graph-paper grid, dark-themed pages only. Two 1px-line
   repeating-linear-gradients (horizontal + vertical) as a fixed,
   full-viewport pseudo-element behind everything (z-index:-1) — plain
   white at very low alpha, not tinted toward either accent, so it
   reads as neutral grey rather than picking up violet or any other
   brand color. A radial-gradient mask fades it out toward the edges so
   it reads as ambient texture in the middle of the page instead of a
   hard-edged layer with a visible boundary. Sections with their own
   opaque fill (.feedback, .recognition, .footer) sit on top and hide
   it in those spots — expected, this is meant to be barely-there
   texture in the open space between sections, not a uniform overlay. */
body.page-dark::before{
  content:''; position:fixed; inset:0; z-index:-1; pointer-events:none;
  background-image:
    repeating-linear-gradient(to right, rgba(255,255,255,.06), rgba(255,255,255,.06) 1px, transparent 1px, transparent 40px),
    repeating-linear-gradient(to bottom, rgba(255,255,255,.06), rgba(255,255,255,.06) 1px, transparent 1px, transparent 40px);
  -webkit-mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, #000 0%, transparent 80%);
  mask-image: radial-gradient(ellipse 70% 60% at 50% 40%, #000 0%, transparent 80%);
}
body.page-dark{
  --bg: #0d0d0d;
  --bg-2: #1a1a1a;
  --surface: #1a1a1a;
  --surface-2: #242424;
  --text: #ffffff;
  --text-soft: #c9c9c9;
  --accent: #E4222A;
  --accent-strong: #FF5A4E;
  --accent-soft: rgba(228,34,42,0.14);
  --accent-text: #ffffff;
  /* --accent-gradient is NOT redefined here — this page's red is
     literally the same red as the light theme's (unlike the old lime/
     violet accents, which needed their own dark-theme value), so the
     :root definition already applies correctly on this page too. */
  --line: rgba(255,255,255,0.12);
  --line-strong: rgba(255,255,255,0.22);
  --shadow: rgba(0,0,0,0.45);
  --cursor-mix: rgba(255,255,255,0.6);
}

/* .whatido__card and .process__row had no fill of their own in the
   light theme (just a border, page-white showing through) — the brief
   explicitly wants these as filled dark surfaces, so that needs adding
   rather than falling out of the token swap. */
body.page-dark .whatido__card,
body.page-dark .process__row{ background: var(--bg-2); }
/* .whatido__icon's default background is --bg-2 — identical to the
   card fill above, so without this the icon circle would be invisible
   (same color as what's behind it). */
body.page-dark .whatido__icon{ background: rgba(255,255,255,.07); }

/* .btn-primary's un-hovered fill was var(--text) (black-on-white
   theme) with var(--accent) only on hover — "invert on hover" made
   sense when var(--text) was black. On this page the brief wants
   buttons red by default, not just on hover — the gradient variant
   specifically, so the primary CTA reads as the richest expression of
   the brand red on the page. White text (not the near-black used for
   the old brighter lime/violet accents) since red is a darker color
   overall and needs the higher-contrast option here. */
body.page-dark .btn-primary{ background: var(--accent-gradient); color: #fff; }
body.page-dark .btn-primary:hover{ background: var(--accent-strong); }

/* the "What I Do" CTA card is the WHITE standout card among the dark
   ones now (an explicit, deliberate choice, not the base rule's
   background:var(--text) falling through) — text/icon/button below all
   flip to dark-on-white to match. .btn-primary keeps the red gradient
   since it already reads clearly on white without needing its own
   inversion. */
body.page-dark .whatido__card--cta{ background: #fff; border-color: #fff; color: #0d0d0d; }
body.page-dark .whatido__card--cta p{ color: rgba(13,13,13,.65); }
body.page-dark .whatido__card--cta .btn-primary{ background: var(--accent-gradient); color: #fff; }
body.page-dark .whatido__card--cta:hover{ background: color-mix(in srgb, var(--accent) 10%, white); border-color: var(--accent); }
body.page-dark .whatido__card--cta .whatido__icon{ background: rgba(13,13,13,.06); }
body.page-dark .whatido__card--cta .whatido__icon svg{ stroke:#0d0d0d; }
body.page-dark .whatido__card--cta:hover .whatido__icon{ background: var(--accent-soft); }
body.page-dark .whatido__card--cta:hover .whatido__icon svg{ stroke: var(--accent); transform: translate(2px,-2px); }

/* index.html-only: drop the thin border-top/bottom hairlines that
   separate major sections everywhere else on the site. Scoped to
   body.page-dark (not a blanket edit to the shared rules themselves)
   so about.html and any other page using these same section classes
   keep their dividers untouched — this is specifically an index.html
   request, not a site-wide one. */
body.page-dark .logowall,
body.page-dark .about-section,
body.page-dark .portfolio,
body.page-dark .process,
body.page-dark .recognition,
body.page-dark .writing{ border-top: none; border-bottom: none; }

/* Recognition cards keep their own light celebratory gradients on
   purpose (color-mix(...,white) is hardcoded to literal "white", not
   a token, so it's unaffected by this theme) — but their text was
   about to inherit the page's new white default, which is unreadable
   on a light card. Force it back to dark text specifically here. */
body.page-dark .recognition__item{ color:#0d0d0d; }
body.page-dark .recognition__item p{ color:#3a3a3a; }


/* footer was already dark via background:var(--text) (which pointed
   at near-black in the light theme) — now that --text is white on
   this page, it needs to be pinned to an explicit dark value instead
   of following the token. */
body.page-dark .footer{ background:#000; }
body.page-dark .footer a{ color: rgba(255,255,255,.6); }

/* .placeholder-icon (case-study.css) is an opaque light-grey PNG — a
   CMS-style "choose your image" stand-in used ~20+ times each across
   several still-in-progress case studies (adi-green, backlog-ai,
   po-cruises, relay). It's a real raster image, not decorative CSS, so
   the token swap can't touch its color — without this it'd sit as a
   glaring white box in an otherwise dark page. invert(.92) rather than
   invert(1): a plain full invert pushed the warm grey into a muddy
   brown; backing off slightly plus a touch of brightness keeps it a
   clean neutral dark grey that still reads clearly as "placeholder",
   just dark-page-appropriate. */
body.page-dark .placeholder-icon{ filter: invert(.92) brightness(.9); }

/* THE SAME BLANK-TEXT BUG AS THE FEEDBACK STAT CARD, different
   component: .bigcta{background:var(--text)} flips to a bold WHITE
   card on this theme (var(--text) is white here) — correct and
   intentional, matches the site's established "occasional white
   accent block" motif (.whatido__card--cta, .feedback__stat--white).
   But .bigcta__sub hardcodes color:rgba(255,255,255,.55) — designed
   for the ORIGINAL light-theme bigcta, which had a BLACK fill
   (background:var(--text) resolved dark there). On this now-white
   card that hardcoded white text is invisible. Hardcoded to dark here
   instead, same fix pattern as the feedback card. */
body.page-dark .bigcta__sub{ color: rgba(13,13,13,.55); }

.case-nextprev a:last-child{ text-align: right; }