/* ==========================================================================
   agladze.org — Mikhail Agladze
   One design system: 4 surfaces, 2 accents, a single 1.25 fluid type scale.
   Hand-authored. No frameworks, no webfonts, no jQuery.
   ========================================================================== */

:root{
  --bg:        #070A0F;
  --bg-2:      #0B1018;
  --surface:   #101825;
  --surface-2: #16202F;
  --line:      rgba(255,255,255,.09);
  --line-2:    rgba(255,255,255,.16);

  --text:  #E9EFF8;
  --muted: #93A2B8;   /* 6.9:1 on --surface */
  --dim:   #8593A8;   /* 6.1:1 — the darker #64748B failed AA at body size */

  --accent:   #22D3EE;
  --accent-2: #F0B429;
  --glow:     rgba(34,211,238,.20);

  --t--1: clamp(.78rem, .76rem + .12vw, .84rem);
  --t-0:  clamp(1rem, .97rem + .16vw, 1.075rem);
  --t-1:  clamp(1.15rem, 1.09rem + .3vw, 1.35rem);
  --t-2:  clamp(1.4rem, 1.28rem + .6vw, 1.85rem);
  --t-3:  clamp(1.75rem, 1.5rem + 1.2vw, 2.6rem);
  --t-4:  clamp(2.4rem, 1.8rem + 3vw, 4.6rem);

  --sp:   1rem;
  --sp-2: 1.5rem;
  --sp-3: 2.5rem;
  --sp-4: 4rem;
  --sp-5: clamp(4rem, 3rem + 5vw, 7.5rem);
  --wrap: 1140px;
  --r:    14px;

  --sans: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

*,*::before,*::after{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; scroll-behavior:smooth; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto; } }

body{
  margin:0; background:var(--bg); color:var(--text);
  font-family:var(--sans); font-size:var(--t-0); line-height:1.65;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
}

img{ max-width:100%; height:auto; display:block; }
a{ color:var(--accent); text-underline-offset:.2em; text-decoration-thickness:1px; }
a:hover{ color:#7DE9F7; }
:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; border-radius:4px; }

h1,h2,h3,h4{ line-height:1.12; margin:0; letter-spacing:-.022em; font-weight:650; text-wrap:balance; }
h2{ font-size:var(--t-3); }
h3{ font-size:var(--t-1); letter-spacing:-.015em; }
p{ margin:0 0 var(--sp); text-wrap:pretty; }

.wrap{ width:min(100% - 2.5rem, var(--wrap)); margin-inline:auto; }
@media (max-width:560px){ .wrap{ width:min(100% - 2rem, var(--wrap)); } }

.skip{
  position:absolute; top:0; left:0; z-index:100;
  background:var(--accent); color:#04222A; padding:.7rem 1.1rem;
  border-radius:0 0 var(--r) 0; font-weight:650; text-decoration:none;
  clip-path:inset(50%); white-space:nowrap;
}
.skip:focus{ clip-path:none; }

/* ---------- shared ---------- */
.kicker{
  font-size:var(--t--1); letter-spacing:.16em; text-transform:uppercase;
  color:var(--accent); font-weight:650; margin:0 0 .75rem;
}
.eyebrow{
  display:inline-flex; align-items:center; gap:.6rem;
  font-size:var(--t--1); letter-spacing:.13em; text-transform:uppercase;
  color:var(--muted); font-weight:600; margin:0 0 var(--sp-2);
}
.dot{ width:7px; height:7px; border-radius:50%; background:var(--accent); box-shadow:0 0 0 4px var(--glow); }

.btn{
  display:inline-flex; align-items:center; justify-content:center;
  padding:.85rem 1.5rem; border-radius:999px;
  border:1px solid var(--line-2); background:rgba(255,255,255,.04);
  color:var(--text); font-weight:600; font-size:var(--t-0); text-decoration:none;
  transition:background .18s ease, border-color .18s ease, transform .18s ease;
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
}
.btn:hover{ background:rgba(255,255,255,.09); border-color:var(--accent); color:var(--text); transform:translateY(-1px); }
.btn--primary{ background:var(--accent); border-color:var(--accent); color:#04222A; box-shadow:0 8px 34px -8px var(--glow); }
.btn--primary:hover{ background:#67E8F9; border-color:#67E8F9; color:#04222A; }
.btn--lg{ padding:1.05rem 2rem; font-size:var(--t-1); font-weight:650; }

/* ---------- nav ---------- */
.nav{
  position:sticky; top:0; z-index:50;
  background:rgba(7,10,15,.7);
  backdrop-filter:saturate(160%) blur(14px); -webkit-backdrop-filter:saturate(160%) blur(14px);
  border-bottom:1px solid var(--line);
}
.nav__in{ display:flex; align-items:center; justify-content:space-between; gap:var(--sp-2); min-height:64px; }
.brand{ display:inline-flex; align-items:center; gap:.6rem; color:var(--text); text-decoration:none; font-weight:650; letter-spacing:-.01em; }

/* ---- the mark: a neon brain, slow-pulsing ---------------------------------------
   Fixed 30px box so the nav can never reflow. The halo is the same geometry blurred
   behind the crisp core; only opacity + scale animate, both compositor-only, on a
   5.6s ease-in-out cycle — a heartbeat, not a strobe.                               */
.brand__id{ position:relative; width:30px; height:30px; flex:none; }
.brand__brain{
  position:absolute; inset:0; width:30px; height:30px; overflow:visible;
  color:var(--accent);
}
.brain__core{ opacity:.94; }
.brain__halo{
  color:var(--accent); opacity:.42;
  transform-box:view-box; transform-origin:50% 50%;
  animation:brainPulse 5.6s cubic-bezier(.45,0,.55,1) infinite;
  will-change:opacity, transform;
}
@keyframes brainPulse{
  0%,100%{ opacity:.30; transform:scale(.97); }
  50%    { opacity:.92; transform:scale(1.07); }
}
.brand:hover .brain__core,
.brand:focus-visible .brain__core{ opacity:1; }

.nav__links{ display:flex; gap:1.6rem; list-style:none; margin:0; padding:0; min-width:0; }
.nav__links li{ flex:none; }
.nav__links a{
  color:var(--muted); text-decoration:none; font-size:var(--t--1);
  font-weight:600; letter-spacing:.04em; text-transform:uppercase;
  padding:.3rem 0; border-bottom:1px solid transparent; transition:color .16s, border-color .16s;
}
.nav__links a:hover{ color:var(--text); border-bottom-color:var(--accent); }
@media (max-width:860px){
  .brand__txt{ display:none; }
  .nav__links{ gap:1.05rem; }
  .nav__links a{ font-size:.72rem; letter-spacing:.06em; }
}
/* Narrow phones: five anchors can't share a row with the brand. Two centred rows, and
   drop sticky — on a one-pager a sticky bar costs more viewport than it earns. */
@media (max-width:640px){
  .nav{ position:static; }
  .nav__in{ flex-direction:column; gap:.55rem; min-height:0; padding:.75rem 0; }
  .brand__txt{ display:inline; }
  .nav__links{ flex-wrap:wrap; justify-content:center; gap:0 .8rem; width:100%; }
  .nav__links a{ font-size:.64rem; letter-spacing:.04em; white-space:nowrap; padding:.62rem 0; }
  .brand{ padding:.3rem 0; }
  .social a{ padding:.45rem .3rem; }
  .foot a{ display:inline-block; padding:.45rem 0; }
}

/* ---------- hero — full bleed ---------- */
.hero{
  position:relative; isolation:isolate; overflow:hidden;
  min-height:clamp(560px, 86vh, 900px);
  display:flex; align-items:center;
  padding:clamp(3.5rem,3rem+6vw,7rem) 0 clamp(3rem,2.5rem+4vw,5.5rem);
}
.hero__art{ position:absolute; inset:0; z-index:-2; }
.hero__art img{ width:100%; height:100%; object-fit:cover; object-position:62% 50%; }
/* Scrim, not a panel edge: the image runs the full width and is graded down under the copy. */
.hero__scrim{
  position:absolute; inset:0; z-index:-1;
  background:
    linear-gradient(100deg, rgba(7,10,15,.97) 0%, rgba(7,10,15,.9) 26%, rgba(7,10,15,.62) 48%, rgba(7,10,15,.22) 72%, rgba(7,10,15,.42) 100%),
    linear-gradient(180deg, rgba(7,10,15,.8) 0%, rgba(7,10,15,0) 22%, rgba(7,10,15,0) 62%, rgba(7,10,15,.95) 100%);
}
.hero__in{ position:relative; }
.hero__in > *{ max-width:42rem; }
.hero__h1{ font-size:var(--t-4); font-weight:700; letter-spacing:-.035em; margin:0 0 var(--sp); line-height:1.02; }
.hero__tag{
  font-size:var(--t-2); font-weight:600; letter-spacing:-.02em; line-height:1.22;
  color:var(--text); margin:0 0 var(--sp-2); max-width:34rem;
}
.hero__lede{ font-size:var(--t-1); color:#C6D3E4; max-width:36rem; margin-bottom:var(--sp-3); }
.hero__cta{ display:flex; flex-wrap:wrap; gap:.85rem; }
@media (max-width:720px){ .br-lg{ display:none; } }

/* ---------- proof bar ---------- */
.stats{ border-block:1px solid var(--line); background:var(--bg-2); }
.stats__grid{ display:grid; grid-template-columns:repeat(4,1fr); gap:var(--sp-2); padding:var(--sp-3) 0; }
.stat{ display:flex; flex-direction:column; gap:.4rem; position:relative; padding-left:var(--sp); }
.stat::before{ content:""; position:absolute; left:0; top:.25rem; bottom:.25rem; width:2px; background:linear-gradient(180deg,var(--accent),transparent); border-radius:2px; }
.stat__n{ font-size:var(--t-3); font-weight:700; letter-spacing:-.03em; line-height:1; color:var(--text); font-variant-numeric:tabular-nums; }
.plus{ color:var(--accent); }
.stat__l{ font-size:var(--t--1); color:var(--muted); line-height:1.45; }
.stat__l em{ color:var(--text); font-style:normal; font-weight:650; }
@media (max-width:820px){ .stats__grid{ grid-template-columns:repeat(2,1fr); gap:var(--sp-2) var(--sp); } }
@media (max-width:420px){ .stats__grid{ grid-template-columns:1fr; } }

/* ---------- sections ---------- */
.sec{ padding:var(--sp-5) 0; position:relative; }
.sec--alt{ background:var(--bg-2); border-block:1px solid var(--line); }
.sec__head{ max-width:46rem; margin-bottom:var(--sp-4); }
.sec__sub{ color:var(--muted); font-size:var(--t-1); margin-top:var(--sp); margin-bottom:0; }

/* section band imagery — a motif behind the content so the eye never hits a flat wall */
.sec--band{ isolation:isolate; overflow:hidden; }
.band__art{ position:absolute; inset:0; z-index:-2; }
.band__art img{ width:100%; height:100%; object-fit:cover; opacity:.4; }
.band__art--soft img{ opacity:.40; }
.sec--band::after{
  content:""; position:absolute; inset:0; z-index:-1;
  background:linear-gradient(180deg, var(--bg) 0%, rgba(7,10,15,.72) 30%, rgba(7,10,15,.78) 70%, var(--bg) 100%);
}
.sec--band.sec--alt::after{
  background:linear-gradient(180deg, var(--bg-2) 0%, rgba(11,16,24,.76) 28%, rgba(11,16,24,.8) 72%, var(--bg-2) 100%);
}
.band__in{ position:relative; }

/* ---------- capability cards ---------- */
.cards{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--sp-2); }
@media (max-width:940px){ .cards{ grid-template-columns:1fr; max-width:34rem; } }
.card{
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r);
  overflow:hidden; display:flex; flex-direction:column;
  transition:border-color .22s ease, transform .22s ease, box-shadow .22s ease;
}
.card:hover{ border-color:var(--line-2); transform:translateY(-3px); box-shadow:0 18px 50px -30px var(--glow); }
.card__art{ aspect-ratio:16/10; overflow:hidden; border-bottom:1px solid var(--line); background:#0A0F17; }
.card__art img{ width:100%; height:100%; object-fit:cover; opacity:.92; }
.card__body{ padding:var(--sp-2); }
.card__body h3{ margin-bottom:.6rem; }
.card__body p{ color:var(--muted); font-size:var(--t--1); line-height:1.6; }
.ticks{ list-style:none; margin:var(--sp) 0 0; padding:0; display:flex; flex-direction:column; gap:.45rem; }
.ticks li{ position:relative; padding-left:1.35rem; font-size:var(--t--1); color:#B6C4D6; }
.ticks li::before{ content:""; position:absolute; left:0; top:.55em; width:6px; height:6px; border-radius:50%; background:var(--accent); opacity:.85; }

/* Each bullet opens its own modal. A real <button> — it does something, and it has to be
   reachable and operable by keyboard without any of it being reinvented. */
.cap__b{
  appearance:none; -webkit-appearance:none; background:none; border:0;
  font:inherit; color:inherit; text-align:left; cursor:pointer;
  /* Padding gives the control a ≥24px target (WCAG 2.5.8 — a standalone button, unlike a
     link inside a sentence, gets no exemption); the matching negative margin gives the
     space straight back to the layout, so the list rhythm and the bullet dots don't move.
     Measured 21px before this. */
  display:block; padding:.34rem 0; margin:-.34rem 0;
  transition:color .2s ease, text-shadow .2s ease;
}
.cap__b:hover,
.cap__b:focus-visible{ color:var(--accent); text-shadow:0 0 18px var(--glow); }
.ticks li:hover::before,
.ticks li:has(.cap__b:focus-visible)::before{ opacity:1; box-shadow:0 0 0 4px var(--glow); }
.cap__b:focus-visible{ outline:2px solid var(--accent); outline-offset:6px; border-radius:4px; }

/* ---------- work ---------- */
.work{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--sp-4); }
.work__item{
  display:grid; grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr); gap:var(--sp-3);
  align-items:start;
  background:var(--surface); border:1px solid var(--line); border-radius:var(--r);
  overflow:hidden;
}
.work__item--flip .work__media{ order:2; }
.work__media{ margin:0; height:100%; min-height:240px; background:#0A0F17; border-right:1px solid var(--line); }
.work__item--flip .work__media{ border-right:0; border-left:1px solid var(--line); }
.work__media img{ width:100%; height:100%; object-fit:cover; opacity:.95; }
.work__body{ padding:var(--sp-3) var(--sp-3) var(--sp-3) 0; }
.work__item--flip .work__body{ padding:var(--sp-3) 0 var(--sp-3) var(--sp-3); }
.work__meta{ display:flex; flex-wrap:wrap; align-items:baseline; gap:.5rem 1rem; margin-bottom:.75rem; }
.work__yr{ font-family:var(--mono); font-size:var(--t--1); color:var(--accent); letter-spacing:.04em; }
.work__role{ font-size:var(--t--1); color:var(--dim); }
.work__body h3{ margin-bottom:var(--sp); }
.work__body p{ color:var(--muted); }
/* The lead engagement carries the architecture diagram, which needs the full card width —
   a 980-unit viewBox in a ~560px column would force a horizontal scroll. Stack it instead,
   which also gives the flagship case study a deliberately more prominent treatment. */
.work__item--lead{ grid-template-columns:1fr; }
.work__item--lead .work__media{ aspect-ratio:24/7; min-height:0; border-right:0; border-bottom:1px solid var(--line); }
.work__item--lead .work__body{ padding:var(--sp-3); }
@media (max-width:900px){
  .work__item--lead .work__media{ aspect-ratio:16/9; }
  /* minmax(0,1fr), not 1fr: a 1fr track takes its min-content floor, and the diagram's
     560px min-width would blow the whole card (prose included) past the viewport where
     .work__item{overflow:hidden} then clips it mid-word. minmax(0,…) lets the track shrink
     so the diagram's own overflow-x:auto finally engages and the SVG scrolls in place. */
  .work__item, .work__item--flip{ grid-template-columns:minmax(0,1fr); gap:0; }
  .work__media, .work__item--flip .work__media{ order:0; min-height:0; aspect-ratio:16/9; border:0; border-bottom:1px solid var(--line); }
  .work__body, .work__item--flip .work__body{ padding:var(--sp-2); }
}

.chips{ list-style:none; display:flex; flex-wrap:wrap; gap:.45rem; margin:var(--sp) 0 0; padding:0; }
.chips li{
  font-family:var(--mono); font-size:.76rem;
  padding:.32rem .7rem; border-radius:999px;
  border:1px solid var(--line-2); background:rgba(255,255,255,.03); color:#AFC0D4;
}

/* ---------- architecture diagram ---------- */
.diagram{
  margin:var(--sp-2) 0; padding:var(--sp) var(--sp) .5rem;
  border:1px solid var(--line); border-radius:var(--r);
  background:linear-gradient(180deg, rgba(34,211,238,.05), rgba(255,255,255,.015));
  overflow-x:auto;
}
.diagram__svg{ width:100%; min-width:560px; height:auto; display:block; }
.diagram figcaption{ margin-top:.6rem; font-size:var(--t--1); color:var(--dim); line-height:1.5; }

/* The mobile diagram has two modes, and JS picks between them (progressive enhancement):

   BASELINE (no JS, or no real <dialog>) — html:not(.js-diag). The 980-unit cascade cannot shrink
   to width without its labels going unreadable, so it scrolls sideways in its card. Make that
   obvious: an auto-hiding edge shadow (Verou's background-attachment trick — the local covers mask
   each shadow once you reach that end) + a caption hint. No dead end without JS.

   ENHANCED (JS + real <dialog>) — html.js-diag. The preview instead FITS the card (nothing cut,
   which was the real defect: a mid-cut diagram reads as "broken"), and a "Tap to expand" button
   opens the full-size view. The scroll hint is dropped so we never say scroll AND tap.

   Everything here is inside @media (max-width:900px); desktop sees none of it. */
@media (max-width:900px){
  html:not(.js-diag) .diagram{
    background:
      linear-gradient(to right, var(--surface), rgba(16,24,37,0)) 0 0 / 30px 100% no-repeat,
      linear-gradient(to left,  var(--surface), rgba(16,24,37,0)) 100% 0 / 30px 100% no-repeat,
      radial-gradient(farthest-side at 0 50%,   rgba(0,0,0,.55), rgba(0,0,0,0)) 0 0 / 16px 100% no-repeat,
      radial-gradient(farthest-side at 100% 50%, rgba(0,0,0,.55), rgba(0,0,0,0)) 100% 0 / 16px 100% no-repeat,
      linear-gradient(180deg, rgba(34,211,238,.05), rgba(255,255,255,.015));
    background-attachment: local, local, scroll, scroll, scroll;
    background-color: var(--surface);
    overscroll-behavior-x: contain;
  }
  html:not(.js-diag) .diagram figcaption::before{
    content:"\2194  scroll the diagram  \00B7  ";
    color:var(--accent); font-family:var(--mono); letter-spacing:.02em;
  }

  html.js-diag .diagram .diagram__svg{ min-width:0; }   /* fit the card — nothing cut */
  html.js-diag .diagram{ overflow-x:hidden; }
  html.js-diag .diagram__expand{ display:inline-flex; }
}

/* the "Tap to expand" trigger — hidden by default (desktop, and mobile-baseline); the rule above
   reveals it only when enhanced. Words lead; the corner-arrows glyph is decorative. */
.diagram__expand{
  display:none; align-items:center; justify-content:center; gap:.5rem;
  width:100%; margin-top:.7rem; padding:.72rem 1rem;
  font-family:var(--mono); font-size:var(--t--1); letter-spacing:.03em;
  color:var(--accent); background:rgba(34,211,238,.06);
  border:1px solid rgba(34,211,238,.3); border-radius:10px; cursor:pointer;
  transition:background .18s ease, border-color .18s ease;
}
.diagram__expand:hover, .diagram__expand:focus-visible{ background:rgba(34,211,238,.13); border-color:var(--accent); }
.diagram__expand-i{ width:16px; height:16px; flex:0 0 auto; }

/* ---- expanded diagram dialog. .modal.modal--diagram beats the generic .modal mobile rule
   regardless of source order, so it's a true fullscreen surface, not the bottom-sheet card. ---- */
.modal.modal--diagram{
  width:100vw; max-width:100vw; height:100vh; height:100dvh; max-height:100dvh;
  margin:0; padding:0; border:0; border-radius:0; overflow:hidden;
  background:var(--bg); transform:none;
}
@starting-style{ .modal.modal--diagram[open]{ transform:none; opacity:0; } }
.modal--diagram .modal__in--diagram{ position:relative; height:100%; padding:0; }
.modal--diagram .modal__x{ z-index:3; }
.diagram-stage{ position:absolute; inset:0; overflow:hidden; }
/* Absolute centering (top/left 50% + translate), NOT grid place-items: the svg's pre-rotation
   layout box is ~90dvh wide — wider than the viewport — and grid can't center an overflowing
   item reliably. Out-of-flow + translate centers it whatever its box size, then we rotate. */
.diagram-stage .diagram__svg{
  position:absolute; top:50%; left:50%;
  min-width:0; height:auto; display:block; transform-origin:center;
  transform:translate(-50%,-50%);
  filter:drop-shadow(0 10px 44px rgba(0,0,0,.55));
}
.diagram-turn{
  position:absolute; top:max(1rem, env(safe-area-inset-top)); left:0; right:0; z-index:2; margin:0;
  text-align:center; font-family:var(--mono); font-size:var(--t--1); letter-spacing:.08em;
  text-transform:uppercase; color:var(--dim); pointer-events:none;
}
/* portrait: rotate onto the phone's long axis — large, but text is sideways, which cues the turn */
@media (orientation:portrait){
  .diagram-stage .diagram__svg{ width:88vh; width:88dvh; transform:translate(-50%,-50%) rotate(90deg); }
}
/* landscape: natural orientation, fills the width, labels legible */
@media (orientation:landscape){
  .diagram-stage .diagram__svg{ width:96vw; transform:translate(-50%,-50%); }
  .diagram-turn{ display:none; }
}
/* the token cascade replays in the expanded view too (reduced-motion still hides .toks globally) */
.modal--diagram.run .tok--1{ animation:tok-t1 2.5s cubic-bezier(.42,0,.58,1) both; }
.modal--diagram.run .tok--2{ animation:tok-t2 2.5s cubic-bezier(.42,0,.58,1) both; }
.modal--diagram.run .tok--3{ animation:tok-t3 2.5s cubic-bezier(.42,0,.58,1) both; }
.d-tier{ fill:var(--accent); font:650 12px var(--mono); letter-spacing:.12em; }
.d-tier--a{ fill:var(--accent-2); }
.d-hd{ fill:#E9EFF8; font:600 15px var(--sans); }
.d-sub{ fill:#8593A8; font:400 12.5px var(--sans); }
.d-esc{ fill:#8593A8; font:500 12px var(--mono); letter-spacing:.04em; }
.d-esc--a{ fill:var(--accent-2); }

/* ---- the cascade runs -------------------------------------------------------------
   Documents enter, most resolve at Tier 1 and peel onto the rail, fewer reach Tier 2,
   fewest reach the model. The thesis performed instead of asserted. Once, on view,
   then it settles back to the static diagram — which stays the resting state.        */
.tok{
  fill:var(--accent); opacity:0;
  transform-box:view-box; transform-origin:0 0;
  transform:translate(67px,112px);
}
.tok--3{ fill:var(--accent-2); }
.diagram.run .tok--1{ animation:tok-t1 2.5s cubic-bezier(.42,0,.58,1) both; }
.diagram.run .tok--2{ animation:tok-t2 2.5s cubic-bezier(.42,0,.58,1) both; }
.diagram.run .tok--3{ animation:tok-t3 2.5s cubic-bezier(.42,0,.58,1) both; }

@keyframes tok-t1{
  0%   { transform:translate(67px,112px);  opacity:0; }
  7%   { opacity:.85; }
  28%  { transform:translate(280px,112px); opacity:.85; }
  40%  { transform:translate(280px,214px); opacity:.85; }
  90%  { transform:translate(930px,214px); opacity:.85; }
  100% { transform:translate(930px,214px); opacity:0; }
}
@keyframes tok-t2{
  0%   { transform:translate(67px,112px);  opacity:0; }
  7%   { opacity:.85; }
  20%  { transform:translate(280px,112px); }
  36%  { transform:translate(532px,112px); opacity:.85; }
  48%  { transform:translate(532px,214px); opacity:.85; }
  90%  { transform:translate(930px,214px); opacity:.85; }
  100% { transform:translate(930px,214px); opacity:0; }
}
@keyframes tok-t3{
  0%   { transform:translate(67px,112px);  opacity:0; }
  7%   { opacity:.9; }
  18%  { transform:translate(280px,112px); }
  32%  { transform:translate(532px,112px); }
  46%  { transform:translate(784px,112px); opacity:.9; }
  58%  { transform:translate(784px,214px); opacity:.9; }
  90%  { transform:translate(930px,214px); opacity:.9; }
  100% { transform:translate(930px,214px); opacity:0; }
}
/* Reduced motion: the tokens simply never exist. The static diagram is already the
   final state, so nothing is lost and nothing needs re-describing. */
@media (prefers-reduced-motion: reduce){ .toks{ display:none; } }

/* ---------- writing / shelf ---------- */
.shelf{ display:grid; grid-template-columns:repeat(4,1fr); gap:var(--sp-2); }
@media (max-width:980px){ .shelf{ grid-template-columns:repeat(2,1fr); } }
@media (max-width:560px){ .shelf{ grid-template-columns:1fr; } }
.vol{
  border:1px solid var(--line); border-radius:var(--r); overflow:hidden;
  background:rgba(16,24,37,.82); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
  display:flex; flex-direction:column;
  transition:transform .22s ease, border-color .22s ease, box-shadow .22s ease;
}
.vol:hover{ transform:translateY(-4px); border-color:var(--line-2); box-shadow:0 20px 50px -32px var(--glow); }
.vol__art{ aspect-ratio:1/1; background:#0A0F17; border-bottom:1px solid var(--line); }
.vol__art img{ width:100%; height:100%; object-fit:cover; opacity:.9; }
.vol__body{ padding:var(--sp-2) var(--sp); }
.vol__cat{
  font-size:var(--t--1); letter-spacing:.14em; text-transform:uppercase;
  color:var(--accent); font-weight:650; margin:0 0 .85rem;
}
.vol__list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:.7rem; }
.vol__list li{
  font-size:var(--t--1); font-weight:600; color:var(--text); line-height:1.3;
  padding-left:.8rem; border-left:2px solid rgba(34,211,238,.45);
}
.vol__list span{ display:block; font-weight:400; color:var(--dim); margin-top:.15rem; }
.books__note{ margin-top:var(--sp-3); color:var(--muted); font-size:var(--t--1); }

/* Book name-lines open the per-book modal. Same affordance as the capability bullets
   (.cap__b): a real button, a persistent accent hairline under the title that brightens on
   hover/focus (so it reads as interactive without a pointer), and a ≥24px target via padding
   with the space given straight back by a matching negative margin. Only the title is
   underlined — the subtitle sits on its own dim line below. */
.bookline{
  appearance:none; -webkit-appearance:none; background:none; border:0;
  font:inherit; color:inherit; text-align:left; cursor:pointer;
  display:block; width:100%; padding:.32rem 0; margin:-.32rem 0;
}
.bookline__t{
  text-decoration:underline; text-decoration-color:rgba(34,211,238,.34);
  text-decoration-thickness:1px; text-underline-offset:.22em;
  transition:color .2s ease, text-decoration-color .2s ease, text-shadow .2s ease;
}
.bookline:hover .bookline__t,
.bookline:focus-visible .bookline__t{ color:var(--accent); text-decoration-color:var(--accent); text-shadow:0 0 18px var(--glow); }
.bookline:focus-visible{ outline:2px solid var(--accent); outline-offset:5px; border-radius:5px; }

/* ---------- about ---------- */
.about{ display:grid; grid-template-columns:1.6fr 1fr; gap:var(--sp-4); align-items:start; }
@media (max-width:900px){ .about{ grid-template-columns:1fr; gap:var(--sp-3); } }
.about__text h2{ margin-bottom:var(--sp-2); }
.about__text p{ color:var(--muted); }
.about__side{ display:flex; flex-direction:column; gap:var(--sp); }
.panel{
  padding:var(--sp-2); border:1px solid var(--line); border-radius:var(--r);
  background:rgba(16,24,37,.82); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
}
/* ---- About portrait: the real, warm candid -----------------------------------------
   Deliberately NOT graded to the dark palette — the sunlit candor is the point; it's
   what reads as a real person against the section's mystique. The frame and a soft inner
   edge seat it in the dark section without cooling it. Fixed aspect box = zero CLS.
   (The cinematic graded portrait lives in the nav reveal instead — two photos, two homes.) */
.portrait{
  position:relative; margin:0;
  border:1px solid var(--line-2); border-radius:var(--r); overflow:hidden;
  background:#0A0F17; aspect-ratio:1/1; isolation:isolate;
}
.portrait img{
  width:100%; height:100%; object-fit:cover; object-position:50% 40%;
  transform:translateY(0) scale(1.06); will-change:transform;
}
/* A whisper of inner shade at the edges only — seats the bright frame against the dark
   band. It does not touch the centre, so his face and the warmth are untouched. */
.portrait--candid::after{
  content:""; position:absolute; inset:0; pointer-events:none; border-radius:inherit;
  box-shadow:inset 0 0 40px 6px rgba(7,10,15,.5);
}

/* Parallax only where scroll-driven animation exists; elsewhere it is simply absent. */
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion: no-preference) and (min-width:901px){
    @keyframes portrait-drift{ from{ transform:translateY(-14px) scale(1.06); } to{ transform:translateY(14px) scale(1.06); } }
    .portrait img{ animation:portrait-drift linear both; animation-timeline:view(); animation-range:entry 10% exit 90%; }
  }
}
.panel__h{ font-size:var(--t--1); letter-spacing:.14em; text-transform:uppercase; color:var(--accent); margin-bottom:var(--sp); }
.plain{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:.75rem; }
.plain li{ font-size:var(--t--1); color:var(--muted); line-height:1.55; }
.plain strong{ color:var(--text); font-weight:650; display:block; }

.stack__h{ margin:var(--sp-5) 0 var(--sp-2); font-size:var(--t-2); }
.stack{ display:grid; grid-template-columns:repeat(4,1fr); gap:var(--sp-2); }
@media (max-width:900px){ .stack{ grid-template-columns:repeat(2,1fr); } }
@media (max-width:560px){ .stack{ grid-template-columns:1fr; } }
.stack__grp h4{ font-size:var(--t--1); letter-spacing:.13em; text-transform:uppercase; color:var(--dim); margin-bottom:.85rem; font-weight:650; }
.stack__grp .chips{ margin-top:0; }
@media (min-width:901px){ .stack{ grid-template-columns:repeat(3,1fr); } }   /* Cloud moved out below */

/* ---- Cloud: a card per cloud, each service its own tile ----
   Reads as command of an arsenal rather than a résumé run-on. Monochrome + cyan; the
   LLM-era leads carry a whisper of accent so the eye lands on them first. */
/* Three clouds in one row — narrower cards, so the service tiles wrap into more rows
   INSIDE each card rather than the cards wrapping. Grid stretches them to equal height. */
.clouds{ display:grid; grid-template-columns:repeat(3,1fr); gap:var(--sp-2); margin-top:var(--sp-3); align-items:stretch; }
@media (max-width:760px){ .clouds{ grid-template-columns:1fr; } }
.cloud{
  background:var(--bg-2); border:1px solid var(--line); border-radius:var(--r);
  padding:var(--sp-2);
}
.cloud__name{
  margin:0 0 var(--sp); font-size:var(--t-1); font-weight:700; letter-spacing:-.01em; color:var(--text);
}
.cloud__name::before{
  content:""; display:inline-block; width:8px; height:8px; margin-right:.6rem;
  border-radius:2px; background:var(--accent); vertical-align:middle;
  box-shadow:0 0 0 4px var(--glow);
}
.tiles{ list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:.5rem; }
.tile{
  font-size:var(--t--1); color:#B6C4D6; line-height:1.2;
  padding:.5rem .7rem; border-radius:9px;
  border:1px solid var(--line-2); background:rgba(255,255,255,.03);
}
.tile--hot{
  color:var(--text); border-color:rgba(34,211,238,.42); background:var(--glow);
}

/* ---------- contact ---------- */
.contact{ position:relative; isolation:isolate; overflow:hidden; padding:var(--sp-5) 0; text-align:center; }
.contact__art{ position:absolute; inset:0; z-index:-2; }
.contact__art img{ width:100%; height:100%; object-fit:cover; opacity:.55; }
.contact::after{
  content:""; position:absolute; inset:0; z-index:-1;
  background:radial-gradient(95% 130% at 50% 100%, rgba(7,10,15,.45) 0%, rgba(7,10,15,.85) 55%, var(--bg) 100%);
}
.contact__in{ position:relative; max-width:44rem; margin-inline:auto; }
.contact__lede{ color:#C6D3E4; font-size:var(--t-1); margin-block:var(--sp-2) var(--sp-3); }
.contact .btn--lg{ word-break:break-word; }
.social{ list-style:none; display:flex; flex-wrap:wrap; justify-content:center; gap:var(--sp-2); margin:var(--sp-3) 0 0; padding:0; }
.social a{ display:inline-flex; align-items:center; gap:.5rem; color:var(--muted); text-decoration:none; font-size:var(--t--1); font-weight:600; transition:color .16s ease; }
.social a:hover{ color:var(--accent); }
.social svg{ width:18px; height:18px; flex:none; }

/* ---------- footer ---------- */
.foot{ border-top:1px solid var(--line); background:var(--bg); padding:var(--sp-2) 0; }
.foot__in{ display:flex; flex-wrap:wrap; gap:var(--sp); justify-content:space-between; align-items:center; }
.foot p{ margin:0; font-size:var(--t--1); color:var(--dim); }
.foot a{ color:var(--muted); text-decoration:none; }
.foot a:hover{ color:var(--accent); }

/* Visually hidden, still in the accessibility tree — and out of flow, so injecting the full
   quote set into it cannot shift the page. */
.sr-only{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}

/* ---------- the technology bar --------------------------------------------------
   Two identical sets in one track; the track travels exactly -50%, so the second set
   lands where the first began and the loop has no seam. Transform-only, and the height
   is fixed by the mark size, so it contributes nothing to CLS. Decorative: every mark
   here is also a word in the chip lists below, which is what a screen reader gets.     */
.lg-defs{ position:absolute; width:0; height:0; overflow:hidden; }
.marq{ margin:0 0 var(--sp-3); }   /* unlabeled by design — the section heading is 'Stack' */
.marq__win{
  overflow:hidden;
  /* fade the marks into the background at both ends so the loop point is never the
     thing you notice */
  -webkit-mask-image:linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
          mask-image:linear-gradient(to right, transparent, #000 7%, #000 93%, transparent);
}
.marq__track{ display:flex; width:max-content; animation:marq 44s linear infinite; }
.marq__set{ display:flex; align-items:center; gap:clamp(2.2rem, 5vw, 4rem); padding-right:clamp(2.2rem, 5vw, 4rem); }
@keyframes marq{ from{ transform:translate3d(0,0,0); } to{ transform:translate3d(-50%,0,0); } }
.marq:hover .marq__track,
.marq:has(:focus-visible) .marq__track{ animation-play-state:paused; }
.lg{
  /* aspect-ratio, not width:auto. An <svg> with a viewBox but no width/height attribute has
     NO intrinsic width, so `width:auto` falls back to the CSS replaced-element default of
     300px — every mark was sitting in a 300px slot with the glyph centred inside it. All
     eleven viewBoxes are square, so 1/1 is exact. */
  height:30px; aspect-ratio:1 / 1; width:auto;
  flex:none; color:#5C6B80; fill:currentColor;
  transition:color .28s ease, transform .28s cubic-bezier(.16,1,.3,1);
}
.lg path{ fill:currentColor; }        /* the two devicon marks ship with a hard fill */
.lg:hover{ color:var(--text); transform:translateY(-3px); }

/* Per-mark optical sizing, set from measurement rather than eye. Rendered at a common
   30px the nine glyph marks fill their viewBox height completely, but the AWS wordmark's
   ink occupies only 0.59 of its box — its letterforms came out ~18px tall and it read
   thin and pale next to the rest. Measured ink area at 30px ran 826px² (AWS) to 2403px²
   (Python) against a 1460px² mean; these three overrides pull that band in. */
.lg--aws{ height:38px; }              /* 826px² -> ~1325px²; letterforms now ~22px */
.lg--python{ height:28px; }           /* the densest glyph, trimmed */
.lg--azure{ height:28px; }            /* solid mark, same reason */
@media (max-width:640px){
  .lg{ height:26px; }
  .lg--aws{ height:33px; }
  .lg--python,.lg--azure{ height:24px; }
}

@media (prefers-reduced-motion: reduce){
  /* No roll at all — a static wrapped row of the nine, one copy only. */
  .marq__track{ animation:none; width:auto; flex-wrap:wrap; }
  .marq__set{ flex-wrap:wrap; justify-content:flex-start; row-gap:1.5rem; padding-right:0; }
  .marq__set + .marq__set{ display:none; }
  .marq__win{ -webkit-mask-image:none; mask-image:none; }
  .lg{ transition:none; }
  .lg:hover{ transform:none; }
}

/* ---- each mark is now a real <button> trigger ---------------------------------------
   The <button> is a bare wrapper: the mark looks unchanged, but it's keyboard-reachable and
   works on touch. Colour stays monochrome here — brand colour lives in the modal only, so the
   bar reads as a stack rather than a sponsor wall. */
.lg-btn{ display:inline-flex; align-items:center; padding:.45rem; border:0; background:none;
         color:inherit; cursor:pointer; border-radius:10px; -webkit-tap-highlight-color:transparent; }
.lg-btn:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
.lg-btn:hover .lg,
.lg-btn:focus-visible .lg{ color:var(--text); transform:translateY(-3px); }
/* The whole button box is the tap target — never the SVG ink. Without this, marks with a
   transparent centre (the AWS wordmark) have a dead spot where a tap falls straight through. */
.lg-btn .lg{ pointer-events:none; }
/* Touch has no hover: a press pauses the belt (:active fires on touchstart) so the tap lands on a
   stationary mark. The belt is slow (~one set-width per 44s), so drift between finger-down and the
   pause is sub-pixel — measured tap-accuracy on the moving row is in review-marq-mobile.md. */
.marq:active .marq__track{ animation-play-state:paused; }
/* The row freezes to a static grid only when someone has SIGNALLED they want it still — a motion
   preference (below) or keyboard navigation (:has(:focus-visible)) — never merely because the
   device is a phone. So the marquee rolls on mobile exactly as on desktop; input type is not the
   switch, motion preference is.
   :has(:focus-visible), NOT :focus-within — the static grid is a KEYBOARD affordance, so it must
   fire only when the browser shows a focus ring (keyboard traversal), never on a mouse click.
   :focus-within also matched a mouse click, and since <dialog> restores focus to the trigger on
   close, the row stayed frozen/wrapped after a click-opened modal shut, until focus left. */
.marq:has(:focus-visible) .marq__track{ animation:none; width:auto; flex-wrap:wrap; }
.marq:has(:focus-visible) .marq__set{ flex-wrap:wrap; justify-content:flex-start; row-gap:1.4rem; padding-right:0; }
.marq:has(:focus-visible) .marq__set + .marq__set{ display:none; }
.marq:has(:focus-visible) .marq__win{ -webkit-mask-image:none; mask-image:none; }

/* ---- stack explainer modal: a large brand-coloured mark, the name, Mike's copy (no CTA) ---- */
.modal--stack .stack-mark{ margin:.1rem 0 1.15rem; }
.modal--stack .stack-mark .lg{ height:54px; transition:none; }
.modal--stack .stack-mark .lg:hover{ transform:none; }   /* the modal mark isn't interactive */
.modal--stack .stack-mark--aws .lg,
.modal--stack .stack-mark--azure .lg{ height:60px; }      /* wordmark/solid marks read smaller — bump */
/* Brand colours — MODAL ONLY. The bar stays monochrome. */
.stack-mark--python .lg{ color:#3776AB; }
.stack-mark--pytorch .lg{ color:#EE4C2C; }
.stack-mark--tensorflow .lg{ color:#FF6F00; }
.stack-mark--langchain .lg{ color:#1C9C8B; }   /* official mark is near-black #1C3C3C — lifted to a legible teal of the same hue for the dark surface (flagged in review) */
.stack-mark--spark .lg{ color:#E25A1C; }
.stack-mark--databricks .lg{ color:#FF3621; }
.stack-mark--snowflake .lg{ color:#29B5E8; }
.stack-mark--mongodb .lg{ color:#00ED64; }
.stack-mark--terraform .lg{ color:#7B42BC; }
.stack-mark--aws .lg{ color:#FF9900; }
.stack-mark--azure .lg{ color:#0078D4; }
.stack-mark--googlecloud .lg{ color:#4285F4; }

/* ---------- capability modals -----------------------------------------------------
   Native <dialog> + showModal(): the focus trap, the ESC handling, the inert background
   and focus restoration are the platform's, correct, and not worth reimplementing.
   Backdrop-click and scroll-lock are the two the platform doesn't give you.          */
.modal{
  padding:0; border:1px solid var(--line-2); border-radius:var(--r);
  background:var(--surface); color:var(--text);
  width:min(46rem, calc(100vw - 2.5rem)); max-height:min(88vh, 56rem);
  overflow:auto; overscroll-behavior:contain;
  box-shadow:0 40px 120px -40px #000;
  opacity:0; transform:translate3d(0,14px,0) scale(.985);
  transition:opacity .34s cubic-bezier(.16,1,.3,1), transform .44s cubic-bezier(.16,1,.3,1),
             overlay .44s allow-discrete, display .44s allow-discrete;
}
.modal[open]{ opacity:1; transform:none; }
@starting-style{ .modal[open]{ opacity:0; transform:translate3d(0,14px,0) scale(.985); } }
.modal::backdrop{
  background:rgba(4,6,10,.66);
  -webkit-backdrop-filter:blur(7px) saturate(.8); backdrop-filter:blur(7px) saturate(.8);
}
.modal__in{ position:relative; padding:clamp(1.5rem, 3.4vw, 2.15rem); }
.modal__x{
  position:absolute; top:.9rem; right:.9rem; width:44px; height:44px;
  display:grid; place-items:center; cursor:pointer;
  border:1px solid var(--line-2); border-radius:50%;
  background:rgba(7,10,15,.6); color:var(--muted);
  transition:color .2s ease, border-color .2s ease;
}
.modal__x:hover,.modal__x:focus-visible{ color:var(--text); border-color:var(--accent); }
.modal__x svg{ width:20px; height:20px; }
/* Header wrapper around .modal__x. On desktop it's display:contents — no box, so the button keeps
   its absolute top-right position against .modal__in and desktop is byte-identical. It becomes a
   real sticky bar only in the mobile-sheet media query below. (The diagram modal has no .modal__hd.) */
.modal__hd{ display:contents; }
.modal__kicker{
  margin:0 0 .5rem; font-size:var(--t--1); font-weight:650;
  letter-spacing:.14em; text-transform:uppercase; color:var(--accent);
}
.modal__h{ margin:0 0 1rem; font-size:var(--t-2); line-height:1.2; padding-right:3rem; }
/* Reading measure, generous leading — this is the one place on the site that is genuinely
   long-form, and it should be a pleasure rather than a cramped tooltip. */
.modal__body{ max-width:38rem; }
.modal__body p{ margin:0 0 .9rem; font-size:var(--t-0); line-height:1.62; color:#C6D3E4; }
.modal__body p:first-child{ font-size:var(--t-1); line-height:1.45; color:var(--text); }
.modal__body em{ color:var(--text); font-style:italic; }
.modal__body code{
  font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:.88em;
  background:rgba(255,255,255,.06); padding:.1em .35em; border-radius:4px;
}
.modal__scope{
  margin-top:1.25rem !important; padding-top:1rem; border-top:1px solid var(--line);
  color:var(--muted) !important; font-size:var(--t--1) !important;
}
.modal__scope b{ color:var(--accent); font-weight:650; }
@media (max-width:640px){
  /* Mobile modal = a centred, floating, internally-scrollable CARD (Mike, tested on Samsung), NOT a
     bottom sheet. This DELETES the sheet special-case: it's the same idiom as desktop — all-round
     radius + border (inherited from base .modal) and centred (the dialog UA default margin:auto) —
     just narrower and shorter, with real gaps top and bottom so it reads as a framed, obviously
     scrollable panel and clears the phone navigation bar by geometry (no viewport-fit/env needed).
     dvh (not vh) so the height stays correct as browser chrome shows and hides. */
  .modal{ width:calc(100vw - 2rem); max-width:calc(100vw - 2rem); max-height:80dvh; }
  /* ---- pinned sticky header, shared by all 21 sheet modals (diagram has no .modal__hd) ----
     The sheet (.modal) is the scroll container; this bar is the first child in that scroll flow,
     so position:sticky pins it to the top however far the content scrolls. It (a) keeps the close
     button always reachable and (b) is opaque and full-bleed, so content slides UNDER a deliberate
     straight edge instead of being clipped by the rounded top corner (Mike's "amputated cover"). */
  .modal__hd{
    display:flex; align-items:center; justify-content:flex-end;
    position:sticky; top:0; z-index:4; min-height:2.9rem;
    background:var(--surface); border-radius:var(--r) var(--r) 0 0;
    border-bottom:1px solid var(--line);
  }
  /* Capability sheets: .modal__in pads 1.5rem at <=640px (clamp floor) — negative-margin the bar
     out to the sheet edges so it masks the full width and covers the rounded corners. */
  .modal__in > .modal__hd{ margin:-1.5rem -1.5rem 0; padding:0 .55rem 0 1.5rem; }
  /* Book sheets: .modal__in--book has padding:0, so the bar is already full-bleed. */
  .modal__in--book > .modal__hd{ margin:0; padding:0 .55rem; }
  /* The close button drops its absolute positioning and sits inside the flex bar. */
  .modal__hd .modal__x{ position:static; top:auto; right:auto; width:40px; height:40px; }
  /* Book cover no longer needs to clear a floating close button; the bar owns the top strip. */
  .modal--book .book{ padding-top:1.15rem; }
}
@media (prefers-reduced-motion: reduce){
  .modal{ transition:opacity .14s linear, overlay .14s allow-discrete, display .14s allow-discrete;
          transform:none !important; }
  @starting-style{ .modal[open]{ transform:none; } }
}
html.modal-open{ overflow:hidden; }

/* ---- per-book modal: cover (revealed here for the first time) + meta + one Amazon link ---- */
.modal--book .modal__in--book{ padding:0; }
.book{
  display:grid; grid-template-columns:auto 1fr; gap:clamp(1.1rem,3vw,1.9rem);
  align-items:start; padding:clamp(1.5rem, 3.4vw, 2.15rem);
}
.book__cover{ width:clamp(140px, 32vw, 210px); }
.book__cover img{
  width:100%; height:auto; display:block; border-radius:8px;
  border:1px solid var(--line-2); box-shadow:0 24px 60px -22px #000;
}
.book__meta{ min-width:0; }
.book__meta .modal__kicker{ margin-top:.15rem; }
.book__meta .modal__h{ font-size:var(--t-1); }
.book__sub{ display:block; font-size:var(--t-0); font-weight:400; color:var(--muted); margin-top:.25rem; }
.book__meta .modal__body{ max-width:none; }
.book__meta .modal__body p{ font-size:var(--t-0); line-height:1.6; color:#C6D3E4; }
.book__buy{
  display:inline-flex; align-items:center; gap:.5rem; margin-top:1.35rem;
  padding:.72rem 1.15rem; border-radius:9px;
  background:var(--accent); color:#04121a; font-weight:650; font-size:var(--t--1);
  letter-spacing:.01em; text-decoration:none;
  transition:filter .18s ease, transform .18s ease;
}
.book__buy:hover{ filter:brightness(1.08); transform:translateY(-1px); }
.book__buy:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
.book__buy-i{ width:15px; height:15px; flex:0 0 auto; }
/* narrow phones: stack the cover above the copy so neither gets cramped */
@media (max-width:520px){
  .book{ grid-template-columns:1fr; gap:1.1rem; }
  .book__cover{ width:min(168px, 44%); }
}

/* ---------- the reveal ------------------------------------------------------------
   position:fixed throughout, so the overlay is outside layout and contributes zero CLS.
   Two open modes: "hover" (pointer/keyboard — scrim stays click-through so the mouse can
   keep moving) and "tap" (touch — scrim becomes the dismiss target and the ✕ appears).  */
.reveal{ position:fixed; inset:0; z-index:80; pointer-events:none; }
.reveal__scrim{
  position:absolute; inset:0; opacity:0;
  background:rgba(7,10,15,.46);
  backdrop-filter:blur(6px) saturate(.78); -webkit-backdrop-filter:blur(6px) saturate(.78);
  transition:opacity .46s cubic-bezier(.16,1,.3,1);
}
/* The stage lays out plate + quote. Flex rather than absolute positioning, so the quote is
   placed relative to the plate's real box instead of a magic number that drifts per viewport. */
.reveal__stage{
  position:absolute; inset:0; display:flex; align-items:center;
  gap:clamp(1.25rem, 3.5vw, 3.25rem);
  padding-right:clamp(1.25rem, 5vw, 4rem);
}
.reveal__plate{
  --x-in:-96%; --x-out:-7%;
  position:relative; flex:none; margin:0;
  width:min(52vw, 700px);
  opacity:0; filter:blur(16px);
  transform:translate3d(var(--x-in), 0, 0);
  /* Feather EVERY edge, weighted to his face. A rectangle with four hard cuts reads as a
     banner dropped on the page; dissolving all four edges is what makes it read as a
     projection. One radial gradient, so no mask-composite support question. */
  /* Radii are ~the box half-dimensions on purpose: the ellipse must actually REACH the edges,
     or the stops all land inside the box and nothing feathers at all. 72%/62% puts the top and
     bottom edges at .81 of the ray and the right edge at .97 — both inside the fade. */
  -webkit-mask-image:radial-gradient(72% 62% at 30% 50%, #000 40%, rgba(0,0,0,.9) 62%, transparent 97%);
          mask-image:radial-gradient(72% 62% at 30% 50%, #000 40%, rgba(0,0,0,.9) 62%, transparent 97%);
  transition:transform .74s cubic-bezier(.16,1,.3,1),
             filter    .62s cubic-bezier(.16,1,.3,1),
             opacity   .40s cubic-bezier(.16,1,.3,1);
  will-change:transform, filter, opacity;
}
.reveal__plate img{ display:block; width:100%; height:auto; }
.reveal__close{
  position:absolute; top:1.1rem; right:1.1rem; width:44px; height:44px;
  display:none; place-items:center;
  border:1px solid var(--line-2); border-radius:50%;
  background:rgba(7,10,15,.72); color:var(--muted); cursor:pointer;
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
}
.reveal__close svg{ width:20px; height:20px; }

/* ---- the thought stream ---- */
.reveal__quote{
  position:relative; flex:1 1 auto; min-width:0; max-width:34rem;
  /* Purely a centring anchor. The layers inside are absolutely positioned and centred on this
     box, so a 4-word aphorism and a 56-word painpoint both balance on the same axis and
     NEITHER can resize anything. The set is meant to grow, so nothing here is sized to the
     current content. */
  height:15rem;
  opacity:0; transform:translate3d(10px, 0, 0);
  transition:opacity .42s cubic-bezier(.16,1,.3,1) .16s,
             transform .56s cubic-bezier(.16,1,.3,1) .16s;
}
/* A soft local darkening, because the blurred page behind is not uniformly dark — the hero's
   amber starburst sits right where this text lands. Guarantees legibility without a hard card. */
/* Sized and weighted from measurement, not taste: against the brightest backdrop pixel (the
   hero's amber starburst bleeding through the blur) the gloss and byline were falling to
   3.2:1 and 2.7:1. This panel is what carries them back over AA. */
/* A blurred solid slab rather than a radial gradient. The gradient's dense core covered the
   quote but had already fallen off by the byline's row, which measured 3.6:1 (desktop) and
   2.3:1 (mobile, over the CTA's cyan glow). A slab covers every row equally and its blur
   keeps the edges soft. It lives on the LAYER, not the container, so it always matches the
   height of the quote actually being shown — a 56-word painpoint gets a taller backdrop. */
.q__layer::before{
  content:""; position:absolute; inset:-1.75rem -2.25rem; z-index:-1;
  border-radius:1.75rem; background:rgba(7,10,15,.88); filter:blur(20px);
}
/* The off layer is display:none, not merely transparent or visibility:hidden. Both of those
   still keep a layout box, and the shift API counts the TEXT reflowing inside it — reserving
   the blockquote's height was not enough, because the line boxes themselves move. Only
   display:none removes the layout entirely, which makes re-showing an appearance rather than
   a shift. Measured across three ticker beats: 0.00163 -> 0.00000.
   The fade is therefore sequenced in JS (display, then a frame, then opacity). */
.q__layer{
  position:absolute; left:0; right:0; top:50%; transform:translateY(-50%);
  display:none; opacity:0;
  /* durations come from the JS constants so there is one source of truth for the feel */
  transition:opacity var(--q-fade-out, 800ms) ease-in;
}
.q__layer.is-on{ display:block; }
.q__layer.is-lit{ opacity:1; transition:opacity var(--q-fade-in, 1200ms) ease-out; }
/* No reserved heights: with the off layer at display:none, a re-shown layer has no previous
   rect to be compared against, so content of any length is shift-free on its own. Sizing is
   scaled to length instead — a four-word aphorism deserves display size, a fifty-word
   painpoint is body copy. Tier is set on the hidden layer, so it costs nothing. */
.q__text{
  margin:0; font-size:var(--t-1); line-height:1.45; font-weight:500;
  color:var(--text); text-wrap:balance;
}
.q__layer[data-len="s"] .q__text{ font-size:var(--t-2); }
.q__layer[data-len="l"] .q__text{ font-size:var(--t-0); line-height:1.55; text-wrap:pretty; }
/* Kept deliberately though no entry currently uses this type: the painpoint→solution lines
   moved to the capability modals, and if any come back to the stream they are prose, not
   sayings — curly quotes around "Painpoint: … We solve it by …" read as a misquotation. */
.q__layer[data-type="painpoint-solution"] .q__text::before,
.q__layer[data-type="painpoint-solution"] .q__text::after{ content:none; }
.q__text::before{ content:"\201C"; color:var(--accent); margin-right:.1em; }
.q__text::after { content:"\201D"; color:var(--accent); margin-left:.06em; }
/* The byline belongs to the QUOTE and sits tight under it, as one unit. The gloss is
   Mikhail's reading of someone else's words and is pushed clearly below and inset behind its
   own label — nobody should be able to read it as something Clausewitz or Breiman said. */
.q__by{
  margin:.7rem 0 0; font-size:var(--t--1); font-weight:600; letter-spacing:.04em;
  text-transform:uppercase; color:#C6D3E4;   /* --dim and --muted both measured under AA here */
}
.q__by::before{ content:"— "; }
.q__by:empty{ visibility:hidden; }

.q__gloss{
  margin:1.6rem 0 0; border-left:2px solid var(--accent); padding-left:.85rem;
}
.q__gloss[hidden]{ display:none; }
.q__gloss-eyebrow{
  margin:0 0 .3rem; font-size:.7rem; font-weight:700;
  letter-spacing:.13em; text-transform:uppercase; color:var(--accent);
}
.q__gloss-text{
  margin:0; font-size:var(--t--1); line-height:1.55; color:var(--muted);
}

:root[data-reveal] .reveal__scrim{ opacity:1; }
:root[data-reveal] .reveal__plate{
  opacity:1; filter:blur(0); transform:translate3d(var(--x-out), 0, 0);
}
:root[data-reveal] .reveal__quote{ opacity:1; transform:none; }
/* Touch only: the overlay becomes interactive so tap-away dismisses. */
:root[data-reveal="tap"] .reveal{ pointer-events:auto; }
:root[data-reveal="tap"] .reveal__close{ display:grid; }

@media (max-width:900px){
  /* Phone/tablet: the quote goes BELOW the plate, and the plate centres instead of bleeding
     off the left — a half-off-screen photo on a 390px viewport is just a cropped photo. */
  .reveal__stage{
    flex-direction:column; justify-content:flex-start;
    gap:clamp(1rem, 3vh, 1.75rem);
    padding:max(3.5rem, 7vh) clamp(1rem, 5vw, 2rem) clamp(1rem, 4vh, 2rem);
  }
  .reveal__quote{ flex:none; max-width:34rem; height:19rem; width:100%; }
  /* Top-aligned here, not centred: a phone has no room to spare, and a centred layer that is
     taller than its box overflows UPWARD into the photo. Measured — the longest quote+gloss
     overlapped the plate by 16px before this. Downward overflow is the only safe direction. */
  .q__layer{ top:0; transform:none; }
  .q__layer::before{ inset:-1.25rem -1.5rem; }
}
@media (max-width:640px){
  .reveal__plate{
    --x-in:-108%; --x-out:0%; width:min(84vw, 460px);   /* smaller plate buys the text room */
    /* Vertical radius deliberately BELOW 50% of the box so the top and bottom edges land late
       on the ray — at 66% they sat at .76 and still read as a hard cut on a phone. */
    -webkit-mask-image:radial-gradient(80% 56% at 44% 50%, #000 40%, rgba(0,0,0,.88) 64%, transparent 99%);
            mask-image:radial-gradient(80% 56% at 44% 50%, #000 40%, rgba(0,0,0,.88) 64%, transparent 99%);
  }
}

/* ---------- motion ---------- */
.rise{ opacity:0; transform:translateY(16px); transition:opacity .6s ease, transform .6s ease; }
.rise.in{ opacity:1; transform:none; }
@media (prefers-reduced-motion: reduce){
  .rise{ opacity:1; transform:none; transition:none; }
  .portrait img{ transform:none !important; animation:none !important; }
  .brain__halo{ animation:none !important; opacity:.55 !important; transform:none !important; }
  /* No slide, no focus-pull: the plate is already at its final position and simply fades.
     These survive the blanket `*` duration clamp below on specificity, not source order. */
  .reveal__plate{
    transform:translate3d(var(--x-out), 0, 0) !important;
    filter:none !important;
    transition:opacity .18s linear !important;
  }
  .reveal__scrim{ transition:opacity .18s linear !important; }
  /* The stream still advances; it just doesn't churn. No slide-in, no per-quote crossfade. */
  .reveal__quote{ transform:none !important; transition:opacity .18s linear !important; }
  .q__layer{ transition:none !important; }
  *,*::before,*::after{ animation-duration:.001ms !important; transition-duration:.001ms !important; }
}
/* Below 900px: avatar stays, parallax is dropped (small viewports scroll fast enough
   that parallax reads as jitter). The portrait itself remains — it's the honest one. */
@media (max-width:900px){ .portrait img{ animation:none; transform:none; } }

/* ---------- print ---------- */
@media print{
  .nav,.hero__art,.hero__scrim,.contact__art,.band__art,.skip{ display:none !important; }
  body{ background:#fff; color:#000; }
  .sec,.contact,.hero{ padding:1rem 0; min-height:0; }
}

/* ---------- capability bullets: the affordance ------------------------------------
   The bullets are the doors to the best content on the site, and until now nothing said
   so until you hovered — which on touch is never. A persistent hairline under each one
   reads as "interactive" without hovering, and brightens when it is. Cheap, quiet, and
   it survives having no pointer at all. */
.cap__b{
  text-decoration:underline;
  text-decoration-color:rgba(34,211,238,.34);
  text-decoration-thickness:1px;
  text-underline-offset:.28em;
  transition:color .2s ease, text-shadow .2s ease, text-decoration-color .2s ease;
}
.cap__b:hover,
.cap__b:focus-visible{ text-decoration-color:var(--accent); }

.sec__hint{
  display:flex; align-items:center; gap:.55rem;
  margin:var(--sp) 0 0; max-width:44rem;
  font-size:var(--t--1); color:var(--dim);
}
.sec__hint-i{
  flex:none; width:15px; height:15px; border-radius:50%;
  border:1px solid var(--accent); position:relative; opacity:.85;
}
.sec__hint-i::after{
  content:""; position:absolute; left:50%; top:50%; width:4px; height:4px;
  margin:-2px 0 0 -2px; border-radius:50%; background:var(--accent);
}
