:root{
  /* Monochrome. Two contexts, one ramp: --ink is whatever the surface is,
     --bone is whatever sits on it. :root is the dark context; .paper inverts
     the whole set, so a component dropped into either reads correctly without
     knowing where it landed. */
  --ink:#0b0b0c; --ink-2:#141416; --ink-3:#202023;
  --bone:#e8e8ea; --bone-2:#fbfbfc; --muted:#8d8d95;
  /* Surfaces and body copy stay neutral -- that is what reads as black and
     white. The colour lives in the accents: section labels, the italic in a
     headline, card edges, the small print that points somewhere. Hues are the
     intro's, so the page and the gate belong to each other: gold against the
     deep blue, then aqua, coral and violet off the nebula. */
  --tone-1:#f0bd6b; --tone-2:#5fd3c4; --tone-3:#f08a72; --tone-4:#a992f5;
  /* Card edges + CTAs, a step brighter than the tones so dark text sitting on
     top of them stays readable. --on-accent flips with the context. */
  --a1:#f5c87e; --a2:#72dfd0; --a3:#f79a83; --a4:#b8a4ff;
  --on-accent:#0c0c0d;
  /* The chat is the one thing on the page that is not the page: it answers as a
     person. It gets its own colour, pulled from the blue-violet the intro's
     nebula runs on, so it reads as belonging to this site without competing
     with the black and white everywhere else. */
  --chat:#8ea2ff; --chat-deep:#6b80ea; --chat-ink:#080a16;
  --serif:'Fraunces',Georgia,serif; --sans:'Hanken Grotesk',system-ui,sans-serif; --mono:'JetBrains Mono',monospace;
  --maxw:1120px; --r:14px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
html{scroll-behavior:smooth;}
body{background:var(--ink);color:var(--bone);font-family:var(--sans);font-size:15px;line-height:1.65;-webkit-font-smoothing:antialiased;overflow-x:hidden;}
.grain{position:fixed;inset:0;z-index:1;pointer-events:none;opacity:.05;mix-blend-mode:overlay;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}
a{color:inherit;} img{display:block;max-width:100%;}

/* ===== INTERACTIVE PARTICLE GATE (WebGL) ===== */
/* Deep space. The stars themselves are WebGL points in #scene above this, but
   the depth behind them is cheaper to paint as gradients than to render: a
   faint galactic band on the diagonal, dust clouds either side of it, and a
   vignette pulling the corners down so the field reads as far rather than flat.
   All of it is static, so it costs one paint and never touches a frame budget
   the particle gate needs. */
.gate{position:fixed;inset:0;z-index:900;overflow:hidden;
  background:
    /* the band, off-axis so it does not read as a stripe through the middle */
    linear-gradient(107deg, transparent 30%, rgba(226,232,255,.07) 47%,
                    rgba(240,236,255,.095) 52%, rgba(210,220,255,.05) 58%, transparent 74%),
    /* dust clouds either side of it, cool one way and warm the other -- a
       single tint everywhere reads as a colour cast rather than as depth */
    radial-gradient(58% 42% at 24% 30%, rgba(84,104,214,.16), transparent 70%),
    radial-gradient(52% 40% at 78% 68%, rgba(150,70,160,.13), transparent 72%),
    radial-gradient(34% 30% at 62% 20%, rgba(48,150,170,.11), transparent 70%),
    radial-gradient(40% 34% at 14% 78%, rgba(190,110,70,.075), transparent 72%),
    /* corners down, centre barely lifted */
    radial-gradient(125% 95% at 50% 45%, #0f1020 0%, #08080f 54%, #030307 100%);}
.gate.leaving{background-position:center;}
.gate.done{opacity:0;pointer-events:none;transition:opacity .8s ease;}
#scene{position:absolute;inset:0;width:100%;height:100%;display:block;cursor:grab;}
/* With the palette monochrome, the particles and the type are the same white,
   and the clusters drift straight through the nameplate. A soft well of dark
   behind the copy buys the contrast the colour used to provide -- sized in
   percentages of the copy so it tracks the text at any viewport. */
.gate-copy::before{content:"";position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);width:170%;height:270%;z-index:-1;pointer-events:none;
  background:radial-gradient(closest-side,rgba(5,5,7,.88),rgba(5,5,7,.66) 45%,transparent 78%);}
.gate-copy{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:3;text-align:center;
  width:min(90vw,760px);pointer-events:none;opacity:0;transition:opacity .8s ease,transform .8s ease;}
.gate-copy.show{opacity:1;}
.gate.leaving .gate-copy{opacity:0;transform:translate(-50%,-58%);}
.gate-rule{height:2px;background:linear-gradient(90deg,transparent,rgba(227,227,227,.55) 12%,rgba(227,227,227,.55) 88%,transparent);
  margin:0 auto 16px;max-width:88%;}
.gate-rule.thin{height:1px;margin:22px auto 0;
  background:linear-gradient(90deg,transparent,rgba(227,227,227,.3) 12%,rgba(227,227,227,.3) 88%,transparent);}
.gate-copy .gk{font-family:var(--mono);font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:var(--tone-1);margin-bottom:18px;}
.gate-copy .gt{font-family:var(--serif);font-weight:900;letter-spacing:-.03em;line-height:.94;
  font-size:clamp(44px,8.5vw,108px);color:var(--bone-2);text-shadow:0 4px 60px rgba(11,11,12,.9);}
.gate-copy .gt em{font-style:italic;font-weight:500;color:var(--tone-1);}
.ek-touch{display:none;}
.gate-copy .gt{text-shadow:0 2px 34px rgba(0,0,0,.9),0 0 12px rgba(0,0,0,.6);}
.gate-copy .gs{margin-top:18px;font-size:clamp(14px,1.6vw,18px);color:var(--bone);opacity:.82;text-shadow:0 2px 30px rgba(11,11,12,.9);}
/* No longer a lozenge to click: the way in is Enter or a scroll, and this is
   the instruction saying so. It stays a real <button> so keyboard and screen
   reader users get a focusable, announced control rather than a hidden gesture. */
.enter{pointer-events:auto;margin-top:26px;display:inline-flex;align-items:center;gap:12px;
  font-family:var(--mono);font-size:11.5px;letter-spacing:.18em;text-transform:uppercase;
  color:rgba(227,227,227,.72);background:none;border:none;padding:8px 4px;cursor:pointer;
  transition:color .25s ease;}
.enter:hover,.enter:focus-visible{color:var(--bone-2);}
.enter:focus-visible{outline:1px solid var(--tone-1);outline-offset:8px;border-radius:4px;}
.enter b{font-weight:500;color:var(--tone-1);}
.edot{width:5px;height:5px;border-radius:50%;background:var(--tone-1);flex:none;
  animation:enterPulse 2.4s ease-in-out infinite;}
@keyframes enterPulse{0%,100%{opacity:.25;transform:scale(.8);}50%{opacity:1;transform:scale(1);}}
@media(prefers-reduced-motion:reduce){.edot{animation:none;opacity:.8;}}
.hint{position:absolute;left:50%;bottom:30px;transform:translateX(-50%);z-index:3;font-family:var(--mono);font-size:11px;
  letter-spacing:.16em;color:var(--muted);opacity:0;transition:opacity .8s ease;pointer-events:none;}
.hint.show{opacity:.7;}
/* The gate is the first thing between a recruiter and the work, so the way past
   it has to be obvious. Was muted text on a transparent chip in the corner; now
   it reads at a glance and states what it does. */
.skip{position:absolute;bottom:26px;right:26px;z-index:5;font-family:var(--mono);
  font-size:12px;letter-spacing:.08em;color:var(--bone-2);
  background:rgba(227,227,227,.10);border:1px solid rgba(227,227,227,.42);
  border-radius:100px;padding:10px 19px;cursor:pointer;backdrop-filter:blur(6px);
  transition:background .2s ease,border-color .2s ease,transform .2s ease;}
.skip:hover{background:var(--bone-2);border-color:var(--bone-2);color:var(--on-accent);transform:translateY(-1px);}
.skip:focus-visible{outline:2px solid var(--a1);outline-offset:3px;}
@media(max-width:520px){.skip{bottom:18px;right:50%;transform:translateX(50%);}
  .skip:hover{transform:translateX(50%) translateY(-1px);}}
.replay{position:fixed;bottom:92px;right:16px;z-index:40;font-family:var(--mono);font-size:11px;color:var(--muted);background:rgba(37,37,37,.85);border:1px solid var(--ink-3);border-radius:100px;padding:8px 14px;cursor:pointer;transition:all .2s;}
.replay:hover{color:var(--tone-1);border-color:var(--tone-1);}

/* ===== NAV ===== */
nav.topbar{position:fixed;top:0;left:0;right:0;z-index:50;display:flex;align-items:center;justify-content:space-between;
  padding:16px clamp(20px,5vw,52px);background:linear-gradient(var(--ink) 0%,rgba(11,11,12,.88) 58%,rgba(11,11,12,.42) 82%,transparent 100%);}
.brand{font-family:var(--serif);font-weight:700;font-size:20px;letter-spacing:-.01em;color:var(--bone-2);}
.brand span{color:var(--tone-1);}
.nav-links{display:flex;gap:22px;list-style:none;}
.nav-links a{font-size:13px;text-decoration:none;color:rgba(250,250,252,.72);transition:color .2s;}
.nav-links a:hover{color:var(--bone-2);}
.avail{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;color:var(--ink);background:var(--tone-1);padding:6px 13px;border-radius:100px;font-weight:500;}
.avail::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ink);animation:beat 2.4s ease-out infinite;}
@keyframes beat{0%{box-shadow:0 0 0 0 rgba(11,11,12,.55)}70%,100%{box-shadow:0 0 0 7px rgba(11,11,12,0)}}

section{position:relative;z-index:2;max-width:var(--maxw);margin:0 auto;padding:88px clamp(20px,5vw,52px);}
.eyebrow{font-family:var(--mono);font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--tone-1);display:flex;align-items:center;gap:14px;margin-bottom:26px;}
.eyebrow::after{content:"";flex:1;height:1px;background:var(--ink-3);}
h2.title{font-family:var(--serif);font-weight:900;font-size:clamp(30px,4.4vw,50px);line-height:1;letter-spacing:-.02em;color:var(--bone-2);margin-bottom:42px;}

/* ===== HERO ===== */
#hero:not([data-sheet]){min-height:100vh;display:flex;align-items:center;padding-top:104px;padding-bottom:56px;}
.tile{position:relative;border-radius:var(--r);padding:22px;overflow:hidden;display:flex;flex-direction:column;justify-content:flex-end;box-shadow:0 14px 30px -18px rgba(0,0,0,.7);}
body.cinema .tile{opacity:0;transform:scale(.84);}
body.cinema.reveal .tile{opacity:1;transform:none;transition:opacity .5s ease,transform .55s cubic-bezier(.18,.8,.25,1.05);transition-delay:var(--d,0s);}
.tile:hover{transform:translateY(-4px);transition:transform .28s ease;}

.t-links{background:var(--ink-2);justify-content:center;}
.t-links{flex-direction:row;flex-wrap:wrap;gap:9px;align-items:center;}
.t-links a{font-family:var(--mono);font-size:12px;text-decoration:none;color:var(--bone);border:1px solid var(--ink-3);border-radius:100px;padding:8px 15px;transition:all .2s;}
.t-links a:hover{border-color:var(--tone-1);color:var(--tone-1);}
.t-links a.primary{background:var(--bone);color:var(--ink);border-color:var(--bone);font-weight:500;}
.t-links a.primary:hover{background:var(--tone-1);border-color:var(--tone-1);color:var(--ink);}

/* Cards emerge one at a time rather than the whole grid landing at once: --i is
   stamped per card by site.js and staggers the delay down the group. */
.rv{opacity:0;transform:translateY(26px) scale(.985);
  transition:opacity .62s cubic-bezier(.2,.75,.3,1),transform .62s cubic-bezier(.2,.75,.3,1);
  transition-delay:calc(var(--i,0) * .11s);}
.rv.in{opacity:1;transform:none;}

/* Accent edge that draws itself in just behind the card, so the reveal carries
   colour instead of being a plain fade. */
.edge{position:relative;}
.edge::after{content:"";position:absolute;left:0;top:0;height:3px;width:100%;
  background:var(--accent,var(--a1));border-radius:var(--r) var(--r) 0 0;
  transform:scaleX(0);transform-origin:left;
  transition:transform .75s cubic-bezier(.2,.75,.3,1);
  transition-delay:calc(var(--i,0) * .11s + .2s);}
.edge.in::after{transform:scaleX(1);}

/* ===== WORK ===== */
.job{display:grid;grid-template-columns:210px 1fr;gap:34px;padding:34px 0;border-top:1px solid var(--ink-3);}
.job:last-child{border-bottom:1px solid var(--ink-3);}
.job .when{font-family:var(--mono);font-size:12px;color:var(--accent,var(--tone-1));}
.job .org{font-size:15px;color:var(--bone-2);font-weight:600;}
.job .place{font-family:var(--mono);font-size:12px;color:var(--muted);margin-top:2px;}
.orgrow{display:flex;align-items:center;gap:12px;margin-top:12px;}

/* Real brand logos sit on a light chip: most are dark-on-transparent artwork and
   would disappear against the ink background. Same 44px footprint as the
   monogram marks so the two styles line up in a row. */
.mark.logo{background:#fff;border-color:rgba(255,255,255,.16);padding:7px;
  box-shadow:0 4px 14px -6px rgba(0,0,0,.55);}
.mark.logo img{width:100%;height:100%;object-fit:contain;}
/* SKCET publish their logo only as a wide lockup (crest + name). Cover + left
   origin crops it down to just the crest, so no separate image file is needed. */
.mark.logo.crest{overflow:hidden;}
.mark.logo.crest img{object-fit:cover;object-position:left center;}

/* Wordmark logos have no square symbol to crop down to, so instead of shrinking
   one into a 44px box until it is unreadable, the badge keeps a constant 44px
   height and lets its width follow the artwork. Every mark in the column still
   lines up on the same baseline. */
.mark.logo.wide{width:auto;min-width:44px;max-width:118px;padding:7px 11px;}
.mark.logo.wide img{width:auto;height:100%;max-width:100%;}

/* The AWS credential badge is already a finished coloured badge, so it needs no
   chip behind it. */
.mark.badge{background:none;border:none;box-shadow:none;padding:0;}
.mark.badge img{width:100%;height:100%;object-fit:contain;}

/* Monogram marks stand in where no official logo is publicly available. */
.mark{flex:none;display:grid;place-items:center;width:44px;height:44px;border-radius:12px;
  font-family:var(--serif);font-weight:900;letter-spacing:-.02em;line-height:1;
  color:var(--accent,var(--a1));
  background:color-mix(in srgb,var(--accent,var(--a1)) 13%,var(--ink-2));
  border:1px solid color-mix(in srgb,var(--accent,var(--a1)) 34%,transparent);
  box-shadow:inset 0 1px 0 color-mix(in srgb,var(--accent,var(--a1)) 18%,transparent);}
.mark.s3{font-size:12.5px;} .mark.s4{font-size:11px;} .mark.s2{font-size:17px;}
.job h3{font-family:var(--serif);font-weight:700;font-size:23px;color:var(--bone-2);margin-bottom:14px;letter-spacing:-.01em;}
.job ul{list-style:none;display:flex;flex-direction:column;gap:11px;}
.job li{position:relative;padding-left:22px;font-size:14.5px;color:var(--bone);line-height:1.6;}
.job li::before{content:"";position:absolute;left:0;top:9px;width:8px;height:8px;border-radius:2px;background:var(--tone-2);}
.job li b{color:var(--tone-1);font-weight:600;}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:16px;}
.chip{font-family:var(--mono);font-size:11px;color:var(--muted);border:1px solid var(--ink-3);border-radius:100px;padding:4px 11px;}

/* ===== SKILLS ===== */
.stackgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;}
.stackcard{background:var(--ink-2);border-radius:var(--r);padding:24px;}
.stackcard h4{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--tone-1);margin-bottom:16px;display:flex;align-items:center;gap:9px;}
.stackcard h4::before{content:"";width:9px;height:9px;border-radius:3px;background:var(--tone-1);}
.stackcard.teal h4{color:var(--tone-2);} .stackcard.teal h4::before{background:var(--tone-2);}
.stackcard.clay h4{color:var(--tone-3);} .stackcard.clay h4::before{background:var(--tone-3);}
.stackcard.bone h4{color:var(--bone-2);} .stackcard.bone h4::before{background:var(--bone-2);}
.tags{display:flex;flex-wrap:wrap;gap:7px;}
.tags span{font-size:13px;color:var(--bone);background:var(--ink);border:1px solid var(--ink-3);border-radius:8px;padding:6px 11px;transition:all .18s;}
.tags span:hover{border-color:var(--tone-1);color:var(--bone-2);transform:translateY(-2px);}

/* ===== PROJECTS ===== */
.projgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;}
.proj{background:var(--ink-2);border-radius:var(--r);padding:26px;position:relative;border:1px solid transparent;transition:border-color .22s,transform .22s;display:flex;flex-direction:column;}
.proj:hover{border-color:var(--ink-3);transform:translateY(-3px);}
.proj .ptop{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;}
.proj .cat{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--tone-2);}
.proj .yr{font-family:var(--mono);font-size:11px;color:var(--muted);}
.proj h3{font-family:var(--serif);font-weight:700;font-size:21px;color:var(--bone-2);line-height:1.15;margin-bottom:10px;letter-spacing:-.01em;}
.proj p{font-size:14px;color:var(--bone);line-height:1.65;margin-bottom:16px;}
.proj .badge{font-family:var(--mono);font-size:11.5px;color:var(--tone-1);margin-bottom:14px;line-height:1.5;}
.proj .ptags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px;}
.proj .ptags span{font-family:var(--mono);font-size:11px;color:var(--muted);border:1px solid var(--ink-3);border-radius:100px;padding:3px 10px;}
/* Was a faint underlined link that didn't read as clickable at all. Now a solid
   pill in the card's own accent, with an arrow that slides on hover. */
.proj .go{margin-top:auto;align-self:flex-start;display:inline-flex;align-items:center;gap:9px;
  font-family:var(--mono);font-size:12.5px;font-weight:500;letter-spacing:.02em;text-decoration:none;
  color:var(--on-accent);background:var(--accent,var(--a1));border-radius:100px;padding:11px 20px;
  box-shadow:0 8px 24px -12px var(--accent,var(--a1));
  transition:transform .2s ease,box-shadow .2s ease,filter .2s ease;}
.proj .go .arw{transition:transform .22s ease;}
.proj .go:hover{transform:translateY(-2px);filter:brightness(1.08);
  box-shadow:0 16px 34px -12px var(--accent,var(--a1));}
.proj .go:hover .arw{transform:translateX(4px);}
.proj .go:focus-visible{outline:2px solid var(--bone-2);outline-offset:3px;}

/* Each project owns an accent, reused by its edge, category label and button. */
.proj .cat{color:var(--accent,var(--a1));}
.proj:hover{border-color:color-mix(in srgb,var(--accent,var(--a1)) 45%,transparent);}

/* ===== EDUCATION ===== */
.edugrid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;}
.edu{background:var(--ink-2);border-radius:var(--r);padding:28px;position:relative;overflow:hidden;}
.edu{--accent:var(--a1);}
.edu.t2{--accent:var(--a2);}
.edu::before{content:"";position:absolute;top:0;left:0;width:100%;height:4px;background:var(--accent);}
.edu h3{font-family:var(--serif);font-weight:700;font-size:20px;color:var(--bone-2);line-height:1.2;margin-bottom:8px;}
.edu .sch{color:var(--accent);font-size:14px;font-weight:500;}
.edu .dt{font-family:var(--mono);font-size:12px;color:var(--muted);margin:4px 0 14px;}
.edu .grade{display:inline-block;font-family:var(--mono);font-size:12px;color:var(--bone-2);border:1px solid var(--ink-3);border-radius:100px;padding:4px 12px;margin-bottom:14px;}
.edu .crs{font-size:13px;color:var(--muted);line-height:1.7;}

/* ===== CERTIFICATIONS ===== */
.certgrid{display:grid;grid-template-columns:repeat(2,1fr);gap:16px;}
.cert{display:flex;gap:16px;align-items:flex-start;text-decoration:none;
  background:var(--ink-2);border-radius:var(--r);padding:24px;overflow:hidden;
  border:1px solid transparent;transition:border-color .22s ease,transform .22s ease;}
.cert:hover{transform:translateY(-3px);
  border-color:color-mix(in srgb,var(--accent,var(--a1)) 45%,transparent);}
.cert .mark{width:52px;height:52px;border-radius:14px;}
.cert h3{font-family:var(--serif);font-weight:700;font-size:17px;color:var(--bone-2);
  line-height:1.25;letter-spacing:-.01em;margin-bottom:6px;}
.cert .issuer{font-family:var(--mono);font-size:11.5px;color:var(--muted);line-height:1.5;}
.cert .verify{display:inline-flex;align-items:center;gap:6px;margin-top:12px;
  font-family:var(--mono);font-size:12px;font-weight:500;color:var(--accent,var(--a1));
  border-bottom:1px solid color-mix(in srgb,var(--accent,var(--a1)) 40%,transparent);
  padding-bottom:2px;transition:border-color .2s ease;}
.cert:hover .verify{border-color:var(--accent,var(--a1));}
.cert .verify .arw{transition:transform .22s ease;}
.cert:hover .verify .arw{transform:translate(3px,-3px);}
.cert:focus-visible{outline:2px solid var(--bone-2);outline-offset:3px;}

/* ===== ASK WIDGET (floating launcher + popup chat) ===== */
.askw{position:fixed;right:18px;bottom:18px;z-index:80;}

/* Flight canvas covers the full viewport only while the rocket is en route
   (top-left in, wavy path, lands bottom-left where the launcher sits) --
   removed from the DOM entirely once landed, see js/ask.js. */
.askw-flight{position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:85;
  opacity:1;transition:opacity .4s ease;}
.askw-flight.gone{opacity:0;}

.askw-launcher{position:absolute;right:12px;bottom:12px;width:60px;height:60px;border-radius:50%;
  background:var(--chat);border:none;color:var(--chat-ink);display:grid;place-items:center;cursor:pointer;
  box-shadow:0 10px 30px -8px rgba(0,0,0,.55);
  opacity:0;transform:scale(.4);
  transition:opacity .4s cubic-bezier(.2,.8,.3,1.2),transform .4s cubic-bezier(.2,.8,.3,1.2),filter .2s ease;}
.askw-launcher.show{opacity:1;transform:scale(1);}
.askw-launcher:hover{filter:brightness(1.08);}
.askw-launcher svg{width:26px;height:26px;}
/* Attention pulse -- stops permanently the first time the panel is opened. */
.askw-launcher.blink{animation:askglow 2s ease-in-out infinite;}
@keyframes askglow{
  0%,100%{box-shadow:0 10px 30px -8px rgba(0,0,0,.55),0 0 0 0 rgba(142,162,255,.75),0 0 0 0 rgba(142,162,255,.35);transform:scale(1);}
  50%{box-shadow:0 10px 30px -8px rgba(0,0,0,.55),0 0 0 8px rgba(142,162,255,0),0 0 0 22px rgba(142,162,255,0);transform:scale(1.07);}
}

.askw-panel{position:absolute;right:0;bottom:84px;width:min(360px,calc(100vw - 32px));
  background:var(--ink-2);border:1px solid var(--ink-3);border-radius:var(--r);
  padding:20px;box-shadow:0 24px 60px -20px rgba(0,0,0,.65);
  transform-origin:bottom right;opacity:0;transform:scale(.92) translateY(8px);pointer-events:none;
  transition:opacity .22s ease,transform .22s ease;}
.askw-panel.show{opacity:1;transform:none;pointer-events:auto;}
.askw-panel[hidden]{display:block;} /* let the transition run before unmounting */
.askw-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;}
.askw-head span{font-family:var(--serif);font-weight:700;font-size:16px;color:var(--bone-2);}
.askw-close{background:none;border:none;color:var(--muted);font-size:20px;line-height:1;cursor:pointer;
  padding:2px 6px;transition:color .18s ease;}
.askw-close:hover{color:var(--chat);}

.ask-log{margin:4px 0 2px;max-height:min(46vh,320px);overflow-y:auto;overscroll-behavior:contain;
  display:flex;flex-direction:column;gap:9px;padding-right:4px;scrollbar-width:thin;
  scrollbar-color:var(--ink-3) transparent;}
.ask-log::-webkit-scrollbar{width:5px;}
.ask-log::-webkit-scrollbar-thumb{background:var(--ink-3);border-radius:3px;}
.ask-msg{font-size:13.5px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;
  padding:9px 13px;border-radius:14px;max-width:88%;
  animation:askrise .26s cubic-bezier(.2,.7,.3,1) both;}
/* Ajay's side: flush left, quiet background. */
.ask-msg.them{align-self:flex-start;background:var(--ink);color:var(--bone);
  border:1px solid var(--ink-3);border-bottom-left-radius:5px;}
/* The visitor's side: accent, mirrored corner. */
.ask-msg.me{align-self:flex-end;background:var(--chat);color:var(--chat-ink);
  border-bottom-right-radius:5px;}
.ask-msg.error{align-self:flex-start;background:transparent;border:1px solid var(--ink-3);
  color:var(--a3);}
@keyframes askrise{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}

.ask-form{display:flex;gap:8px;margin-top:12px;}
.ask-input{flex:1;min-width:0;background:var(--ink);border:1px solid var(--ink-3);border-radius:100px;
  padding:11px 16px;color:var(--bone-2);font-family:var(--sans);font-size:13.5px;outline:none;
  transition:border-color .2s ease;}
.ask-input::placeholder{color:var(--muted);}
.ask-input:focus{border-color:var(--chat);}
.ask-submit{flex:none;font-family:var(--mono);font-size:12.5px;font-weight:500;color:var(--chat-ink);
  background:var(--chat);border:none;border-radius:100px;padding:11px 18px;cursor:pointer;
  transition:transform .2s ease,filter .2s ease,opacity .2s ease;}
.ask-submit:hover:not(:disabled){transform:translateY(-2px);filter:brightness(1.08);}
.ask-submit:disabled{opacity:.55;cursor:default;}

.ask-note{margin-top:9px;font-size:10.5px;line-height:1.5;color:var(--muted);opacity:.75;}

/* Typing indicator, styled as one of Ajay's own bubbles so the reply appears to
   land in the same place the dots were. */
.ask-typing{align-self:flex-start;display:inline-flex;gap:5px;padding:12px 14px;
  background:var(--ink);border:1px solid var(--ink-3);border-radius:14px;
  border-bottom-left-radius:5px;animation:askrise .26s cubic-bezier(.2,.7,.3,1) both;}
.ask-typing span{width:6px;height:6px;border-radius:50%;background:var(--muted);
  animation:askpulse 1.1s ease-in-out infinite;}
.ask-typing span:nth-child(2){animation-delay:.15s;}
.ask-typing span:nth-child(3){animation-delay:.3s;}
@keyframes askpulse{0%,60%,100%{opacity:.3;transform:scale(.85);}30%{opacity:1;transform:scale(1);}}

@media(max-width:480px){
  .askw{right:14px;bottom:14px;}
  .askw-panel{right:-6px;}
}
@media(prefers-reduced-motion:reduce){
  .askw-launcher.blink{animation:none;}
}

/* ===== CONTACT ===== */
#contact{text-align:center;padding-bottom:70px;}
#contact .big{font-family:var(--serif);font-weight:900;font-size:clamp(38px,6vw,72px);line-height:.98;letter-spacing:-.03em;color:var(--bone-2);margin-bottom:18px;}
#contact .big em{font-style:italic;font-weight:500;color:var(--tone-1);}
#contact .lede{max-width:440px;margin:0 auto 34px;color:var(--muted);font-size:15px;}
.clinks{display:flex;justify-content:center;flex-wrap:wrap;gap:11px;}
.clinks a{font-family:var(--mono);font-size:13px;text-decoration:none;color:var(--bone);border:1px solid var(--ink-3);border-radius:100px;padding:12px 22px;transition:all .2s;}
.clinks a:hover{border-color:var(--tone-1);color:var(--tone-1);transform:translateY(-2px);}
.clinks a.solid{background:var(--tone-1);color:var(--ink);border-color:var(--tone-1);font-weight:500;}
.clinks a.solid:hover{background:var(--bone-2);border-color:var(--bone-2);}
footer{position:relative;z-index:2;text-align:center;padding:26px;border-top:1px solid var(--ink-3);font-family:var(--mono);font-size:11px;color:var(--muted);letter-spacing:.04em;}

::selection{background:var(--tone-1);color:var(--ink);}
::-webkit-scrollbar{width:9px;}::-webkit-scrollbar-track{background:var(--ink);}::-webkit-scrollbar-thumb{background:var(--ink-3);border-radius:10px;}

@media(max-width:860px){
  .nav-links{display:none;}
  .job{grid-template-columns:1fr;gap:14px;}
  .stackgrid,.projgrid,.edugrid,.certgrid{grid-template-columns:1fr;}
}
@media(prefers-reduced-motion:reduce){
  .tile,body.cinema .tile{opacity:1!important;transform:none!important;}
  .rv{opacity:1;transform:none;transition:none;transition-delay:0s;}
  .edge::after{transform:scaleX(1);transition:none;transition-delay:0s;}
  .avail::before{animation:none;}
}

/* ═══════════════════════════════════════════════════════════════════════
   EDITORIAL LAYER
   Alternating paper/ink sections, magazine framing, oversized display type.

   The whole inversion is a variable swap rather than a parallel stylesheet:
   every component already draws from --ink (surface) and --bone (text), so
   redefining those four variables inside .paper flips a section wholesale and
   any component dropped into it inherits the right theme for free.
   ═══════════════════════════════════════════════════════════════════════ */

.paper{
  --ink:#f1f1f0; --ink-2:#e7e7e6; --ink-3:#d6d6d5;
  --bone:#191919; --bone-2:#0a0a0a; --muted:#6b6b71;
  /* Same four hues, taken down until they hold against paper. They are text on
     the light sections and fills on the dark ones, so the light-context values
     above would either glare or vanish here. */
  --tone-1:#8a5a10; --tone-2:#0e6b60; --tone-3:#a8412a; --tone-4:#5b3fb0;
  --a1:#8a5a10; --a2:#0e6b60; --a3:#a8412a; --a4:#5b3fb0;
  --on-accent:#f4f4f5;
  color:var(--bone);
  position:relative;
}
/* `section` is a centred max-width box, so painting its background would leave
   dark gutters either side and the inversion would read as a broken card rather
   than a change of world. This bleeds the surface to the full viewport while the
   content stays in the measure. */
.paper::before{
  content:"";position:absolute;top:0;bottom:0;left:50%;
  width:100vw;transform:translateX(-50%);
  background:var(--ink);z-index:-1;
}
/* Hairline between worlds, so the flip reads as deliberate rather than broken. */
.paper + section:not(.paper), section:not(.paper) + .paper{border-top:1px solid rgba(0,0,0,.16);}
.paper .chip,.paper .tags span,.paper .ptags span{border-color:rgba(0,0,0,.14);}
.paper .mark.logo{background:#fff;border-color:rgba(0,0,0,.10);}
.paper .grain{opacity:.28;}

/* ── masthead ─────────────────────────────────────────────────────────── */
.masthead{display:flex;align-items:baseline;justify-content:space-between;gap:20px;
  padding-bottom:12px;border-bottom:2px solid currentColor;margin-bottom:34px;}
.masthead .mh-l,.masthead .mh-r{font-family:var(--mono);font-size:10.5px;letter-spacing:.24em;
  text-transform:uppercase;color:var(--tone-1);opacity:.85;white-space:nowrap;}
.masthead .mh-c{font-family:var(--serif);font-weight:900;font-size:15px;letter-spacing:.22em;
  text-transform:uppercase;color:var(--bone-2);}
@media(max-width:760px){.masthead .mh-l,.masthead .mh-r{display:none;}
  .masthead{justify-content:center;}}

/* ── the oversized display headline ───────────────────────────────────── */
#hero.paper:not([data-sheet]){min-height:0;display:block;padding-top:112px;padding-bottom:72px;}
.display{font-family:var(--serif);font-weight:900;letter-spacing:-.035em;line-height:.87;
  /* Capped so "BUILDING WHAT" clears the measure at the widest breakpoint
     instead of running off the page. */
  font-size:clamp(40px,9.2vw,124px);color:var(--bone-2);margin:0;
  overflow-wrap:break-word;}
.display em{font-style:italic;font-weight:900;color:var(--tone-1);}
.display-sub{font-family:var(--serif);font-style:italic;font-weight:500;
  font-size:clamp(15px,2.1vw,24px);color:var(--bone);opacity:.82;margin-top:14px;
  padding-bottom:22px;border-bottom:1px solid currentColor;}

/* ── two-column: prose with drop cap | contents index ─────────────────── */
.spread{display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(28px,5vw,72px);
  margin-top:30px;align-items:start;}
@media(max-width:900px){.spread{grid-template-columns:1fr;}}

.colhead{font-family:var(--mono);font-size:10px;letter-spacing:.24em;text-transform:uppercase;
  color:var(--tone-1);padding-bottom:7px;border-bottom:1px solid currentColor;margin-bottom:16px;}

.prose p{font-size:15.5px;line-height:1.62;margin-bottom:13px;text-align:justify;
  hyphens:auto;color:var(--bone);}
.prose p:last-child{margin-bottom:0;}
.prose .lead::first-letter{font-family:var(--serif);font-weight:900;float:left;
  font-size:56px;line-height:.82;padding:5px 9px 0 0;color:var(--bone-2);}
.prose .close{font-weight:700;color:var(--bone-2);}

/* Contents index. Page numbers are the one magazine affectation kept: they give
   the eye a right-hand rail and make the list feel authored. */
.contents a{display:flex;align-items:baseline;justify-content:space-between;gap:14px;
  padding:11px 0;border-bottom:1px solid rgba(112,112,112,.28);text-decoration:none;
  transition:padding-left .22s ease,background .22s ease;}
.contents a:hover{padding-left:9px;background:linear-gradient(90deg,rgba(235,235,238,.13),transparent);}
.contents .ct{font-family:var(--serif);font-weight:700;font-size:17.5px;color:var(--bone-2);}
.contents .cd{display:block;font-family:var(--sans);font-style:italic;font-weight:400;
  font-size:12.5px;color:var(--muted);margin-top:2px;}
.contents .cp{font-family:var(--mono);font-size:10.5px;letter-spacing:.18em;color:var(--tone-1);
  white-space:nowrap;}

/* ── figure captions ──────────────────────────────────────────────────── */
.fig{font-family:var(--serif);font-style:italic;font-size:12px;color:var(--muted);
  margin-top:9px;}
.fig b{font-family:var(--mono);font-style:normal;font-weight:500;font-size:10px;color:var(--tone-1);
  letter-spacing:.16em;text-transform:uppercase;}

/* ── numbered project list (replaces the card grid) ───────────────────── */
/* ── project cards ───────────────────────────────────────────────────────
   Was a ruled list: rows separated by hairlines, which read as an index rather
   than as work. Cards give each project its own surface, so the four of them
   scan as four things instead of one table.                                */
.projlist{display:grid;grid-template-columns:repeat(2,1fr);gap:clamp(14px,1.6vw,20px);}

.projrow{position:relative;display:flex;flex-direction:column;overflow:hidden;
  background:var(--ink-2);border:1px solid var(--ink-3);border-radius:var(--r);
  padding:clamp(22px,2.4vw,30px);
  transition:transform .25s ease,border-color .25s ease,box-shadow .25s ease;}
/* The accent edge draws across the top of each card, giving the four a tonal
   order without four different colours. */
.projrow::before{content:"";position:absolute;top:0;left:0;right:0;height:3px;
  background:var(--accent,var(--a1));opacity:.9;}
.projrow:nth-child(1){--accent:var(--a1);}
.projrow:nth-child(2){--accent:var(--a2);}
.projrow:nth-child(3){--accent:var(--a3);}
.projrow:nth-child(4){--accent:var(--a4);}
.projrow:hover{transform:translateY(-4px);
  border-color:color-mix(in srgb,var(--accent,var(--a1)) 42%,transparent);
  box-shadow:0 20px 44px -22px rgba(0,0,0,.75);}
.paper .projrow{box-shadow:none;}
.paper .projrow:hover{box-shadow:0 18px 40px -24px rgba(0,0,0,.3);}

/* The index number sits behind the card's own content rather than beside it. */
.projnum{position:absolute;top:clamp(10px,1.4vw,16px);right:clamp(14px,1.8vw,22px);
  font-family:var(--sans);font-weight:700;font-size:clamp(44px,4.6vw,68px);
  line-height:.9;color:var(--bone-2);opacity:.09;letter-spacing:-.04em;
  pointer-events:none;transition:opacity .25s ease;}
.projrow:hover .projnum{opacity:.2;}

.projbody{flex:1;min-width:0;}
/* Only the heading has to clear the index number; reserving that space on the
   whole body left every paragraph wrapping short against nothing. */
.projbody h3{font-family:var(--sans);font-weight:700;letter-spacing:-.02em;
  font-size:clamp(19px,1.9vw,25px);line-height:1.12;color:var(--bone-2);margin-bottom:10px;
  padding-right:clamp(46px,4.6vw,72px);}
.projbody p{font-size:14px;line-height:1.62;color:var(--bone);opacity:.86;}
.projmeta{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--muted);margin-top:14px;}

/* Pinned to the bottom so the buttons line up across cards of unequal length. */
.projside{margin-top:18px;padding-top:16px;border-top:1px solid var(--ink-3);}
.projrow .go{display:inline-flex;align-items:center;gap:8px;text-decoration:none;
  font-family:var(--mono);font-size:11.5px;font-weight:500;letter-spacing:.04em;
  color:var(--accent,var(--a1));transition:gap .2s ease;}
.projrow .go:hover{gap:13px;}
.projrow .go .arw{transition:transform .22s ease;}

@media(max-width:880px){
  .projlist{grid-template-columns:1fr;}
}

/* ── floating pill nav ────────────────────────────────────────────────── */
.pillnav{position:fixed;left:50%;bottom:22px;transform:translateX(-50%) translateY(0);
  z-index:70;display:flex;gap:2px;padding:5px;border-radius:100px;
  background:rgba(11,11,12,.86);backdrop-filter:blur(14px);
  border:1px solid rgba(255,255,255,.10);box-shadow:0 12px 40px -12px rgba(0,0,0,.6);
  opacity:0;pointer-events:none;transition:opacity .3s ease,transform .3s ease;}
.pillnav.show{opacity:1;pointer-events:auto;}
.pillnav a{font-family:var(--mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  color:rgba(227,227,227,.62);text-decoration:none;padding:8px 15px;border-radius:100px;
  transition:color .2s ease,background .2s ease;white-space:nowrap;}
.pillnav a:hover{color:var(--bone-2);}
.pillnav a.on{background:var(--bone-2);color:var(--on-accent);}
@media(max-width:520px){
  .pillnav{bottom:14px;}
  .pillnav a{padding:7px 11px;font-size:9.5px;letter-spacing:.1em;}
}

/* The pill nav is fixed bottom-centre, so the last thing on the page needs
   clearance or it sits under the pill. */
footer{padding-bottom:86px;}

/* ── the plate: a photograph that behaves like newsprint ──────────────────
   The reference runs a looping video here, labelled "live motion portrait".
   Same intent reached from a still: high-contrast duotone ink, a halftone dot
   screen, drifting grain and a slow Ken Burns push. It reads as a printed
   photograph that happens to be breathing, and needs no video file.        */
.spread{grid-template-columns:1.05fr .8fr .85fr;}
@media(max-width:1080px){.spread{grid-template-columns:1fr 1fr;}
  .plate{grid-column:1 / -1;order:3;max-width:420px;}}
@media(max-width:900px){.spread{grid-template-columns:1fr;}}

.plate{margin:0;}
.plate-img{position:relative;overflow:hidden;contain:paint;border:1px solid rgba(0,0,0,.18);
  background:var(--ink-2);aspect-ratio:1/1;}
.plate-img img,.plate-img .plate-video{width:100%;height:100%;object-fit:cover;
  object-position:center 32%;display:block;
  /* The intro arrives already graded, so the plate no longer drains it. What
     stays is the press behaviour rather than the press palette: contrast lifted
     the way ink lifts it, and a touch of saturation so the halftone below does
     not read as a grey film over the colour. */
  filter:contrast(1.12) saturate(1.06);
  transform:none;
  animation:plateDrift 26s ease-in-out infinite alternate;}

/* Halftone dot screen. Multiply keeps the dots reading as ink on paper. */
.plate-img::after{content:"";position:absolute;inset:0;pointer-events:none;
  background-image:radial-gradient(circle at center,rgba(12,12,13,.62) 0.9px,transparent 1.15px);
  background-size:3.4px 3.4px;mix-blend-mode:multiply;opacity:.3;}

/* Press grain, drifting so the print never sits perfectly still. */
.plate-img::before{content:"";position:absolute;inset:-12%;z-index:2;pointer-events:none;
  background-image:url("data:image/svg+xml;utf8,\
<svg xmlns='http://www.w3.org/2000/svg' width='90' height='90'>\
<filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='1'/>\
</filter><rect width='90' height='90' filter='url(%23n)' opacity='.5'/></svg>");
  mix-blend-mode:multiply;opacity:.16;
  will-change:transform;
  animation:plateGrain 1.6s steps(4) infinite;}

/* Drift without zoom: scaling up re-crops the frame, which is the opposite of
   what is wanted here. A sub-pixel float is enough to keep it from feeling
   frozen. */
@keyframes plateDrift{
  from{transform:translate3d(0,0,0);}
  to{transform:translate3d(-0.6%,-0.8%,0);}
}
@keyframes plateGrain{
  0%{transform:translate3d(0,0,0);}
  33%{transform:translate3d(-1.6%,1.1%,0);}
  66%{transform:translate3d(1.1%,-1.6%,0);}
  100%{transform:translate3d(0,0,0);}
}
@media(prefers-reduced-motion:reduce){
  .plate-img img,.plate-img .plate-video{animation:none;transform:scale(1.02);}
  .plate-img::before{animation:none;}
}


/* ═══════════════════════════════════════════════════════════════════════
   THE TEAR
   A pinned stage: the front page fills one screen, then rips horizontally
   across the middle and the two halves slide apart, revealing what is behind.

   Geometry copied in spirit from the reference, which clips two copies of its
   newspaper with an 85-point polygon whose y wanders between 46.6% and 51.2%
   — a torn fibre edge, not a cut. The polygon here is generated in JS so the
   ragged edge is regenerated per viewport instead of being frozen at one width.
   ═══════════════════════════════════════════════════════════════════════ */

/* This is a <section>, so it picks up the global max-width, auto margins and
   88px padding meant for content sections. A runway needs none of that: it has
   to be full-bleed and exactly as tall as the scroll it provides. */
.tearzone{position:relative;height:230vh;background:#0c0c0d;
  max-width:none;width:100%;margin:0;padding:0;}
.tearstage{position:sticky;top:0;height:100vh;overflow:hidden;}

/* The front page has to fit one screen for the tear to read: you cannot rip a
   page you can only half see. */
.paper[data-sheet]{
  position:absolute;inset:0;min-height:0;height:100vh;overflow:hidden;
  padding:76px clamp(20px,5vw,52px) 28px;
  max-width:none;margin:0;display:flex;flex-direction:column;
}
.paper[data-sheet] .spread{margin-top:20px;flex:1;min-height:0;}
.paper[data-sheet]::before{width:100%;left:0;transform:none;}

/* Tighter measure inside a fixed-height sheet. */
[data-sheet] .display{font-size:clamp(34px,6.6vw,86px);}
[data-sheet] .display-sub{font-size:clamp(13px,1.5vw,17px);margin-top:9px;padding-bottom:14px;}
[data-sheet] .masthead{margin-bottom:20px;padding-bottom:9px;}
[data-sheet] .prose p{font-size:13px;line-height:1.5;margin-bottom:8px;}
[data-sheet] .prose .lead::first-letter{font-size:40px;padding:3px 7px 0 0;}
[data-sheet] .contents a{padding:7px 0;}
[data-sheet] .contents .ct{font-size:14.5px;}
[data-sheet] .contents .cd{font-size:11px;}
[data-sheet] .plate-img{aspect-ratio:1/1;}
/* The plate is square, so its width sets its height, and the sheet cannot
   scroll. Capping it against viewport height keeps it inside the box on a short
   window as well as a narrow one; min() means a tall screen still cannot make it
   wider than its column. */
[data-sheet] .plate{max-width:min(100%,42vh);}
[data-sheet] .fig{font-size:10.5px;margin-top:6px;}
[data-sheet] .tile.t-links{margin-top:12px;}

/* The sheet is a fixed 100vh box with overflow:hidden -- it has to be, or the
   tear has nothing to rip across. On a wide screen the three-column spread fits
   that box. Below 1080px the columns stack, the sheet runs about 1600px against
   844px of room, and everything past the lead paragraph is clipped away: the
   portrait, the contents index, the mosaic. The video was playing the whole
   time, 790px below the bottom edge of a box that cannot scroll.

   So the narrow sheet becomes what a front page actually is -- masthead,
   headline, standfirst, picture. The detail those hidden columns carried is
   the rest of the page's job, and the contents index only duplicates the nav
   that is already on screen. */
@media(max-width:1080px){
  [data-sheet] .spread{grid-template-columns:1fr;gap:12px;}
  [data-sheet] .spread > div:last-child{display:none;}   /* contents + mosaic */
  [data-sheet] .prose p:not(.lead){display:none;}
  [data-sheet] .plate{order:2;margin:2px auto 0;}
  [data-sheet] .fig{display:none;}
}

/* The two torn halves. Clones, built and clipped in JS. */
.sheethalf{position:absolute;inset:0;will-change:transform;pointer-events:none;}
.sheethalf.live{pointer-events:auto;}

/* What is behind the paper. */
.tearback{position:absolute;inset:0;display:grid;place-items:center;
  background:#0c0c0d;overflow:hidden;}
.tearback-sky{position:absolute;inset:-20%;
  background:
    radial-gradient(1.6px 1.6px at 18% 24%,rgba(214,228,255,.9),transparent 60%),
    radial-gradient(1.4px 1.4px at 62% 14%,rgba(255,236,205,.7),transparent 60%),
    radial-gradient(1.8px 1.8px at 82% 62%,rgba(255,214,170,.8),transparent 60%),
    radial-gradient(1.3px 1.3px at 34% 78%,rgba(186,214,255,.7),transparent 60%),
    radial-gradient(1.5px 1.5px at 8% 58%,rgba(240,240,248,.55),transparent 60%),
    radial-gradient(1.2px 1.2px at 92% 30%,rgba(255,225,225,.6),transparent 60%),
    radial-gradient(120% 90% at 50% 120%,rgba(120,90,190,.20) 0%,transparent 58%),
    radial-gradient(90% 70% at 50% -10%,rgba(60,130,190,.16) 0%,transparent 60%);
  background-size:44% 44%,52% 52%,60% 60%,48% 48%,56% 56%,64% 64%,100% 100%,100% 100%;
  animation:skyDrift 90s linear infinite;}
@keyframes skyDrift{to{transform:translate3d(-6%,-4%,0);}}

.tearback-copy{position:relative;z-index:2;text-align:center;padding:0 24px;}

/* Each word watches the same --reveal the tear drives, and switches on once the
   rip passes the point it was given. Doing it per word rather than fading the
   whole block means the sentence is written by the tearing, not merely exposed
   by it. */
.tw{display:inline-block;margin:0 .18em;
  opacity:clamp(0, (var(--reveal, 0) - var(--at)) * 14, 1);
  transform:translateY(calc((1 - clamp(0, (var(--reveal, 0) - var(--at)) * 14, 1)) * 18px));
  filter:blur(calc((1 - clamp(0, (var(--reveal, 0) - var(--at)) * 14, 1)) * 5px));
  will-change:opacity,transform;}
.tw.br{display:block;margin-top:.06em;}
.tb-t{font-family:var(--serif);font-weight:900;letter-spacing:-.03em;line-height:.94;
  font-size:clamp(30px,6.2vw,84px);color:var(--bone-2);margin:0 auto;max-width:16ch;}
/* Gold rather than the aqua the other accents use: this line sits directly over
   the earth, and a blue-green clause on a blue planet is the one pairing here
   that would not separate. */
.tb-t .tw:nth-child(n+5){font-size:.62em;color:var(--tone-1);}

@media(prefers-reduced-motion:reduce){
  .tearzone{height:auto;}
  .tearstage{position:static;height:auto;overflow:visible;}
  .paper[data-sheet]{position:static;height:auto;min-height:0;padding-bottom:72px;}
  .tearback{display:none;}
  .tearback-sky{animation:none;}
}

/* ── the earth ────────────────────────────────────────────────────────────
   Sits behind the torn paper. Built from gradients rather than a texture so
   there is no image to load behind a fold most visitors see for two seconds.
   The landmasses are a wide repeating gradient panned horizontally, which
   reads as rotation without a sphere-mapping trick.                        */
.earth{position:absolute;left:50%;bottom:-42vmin;transform:translateX(-50%);
  width:96vmin;height:96vmin;pointer-events:none;
  opacity:calc(var(--reveal,0) * .95);transition:opacity .18s linear;}
.earth-globe{position:absolute;inset:0;border-radius:50%;overflow:hidden;
  background:#06111d;
  box-shadow:inset -30px -20px 70px rgba(0,0,0,.78),
             inset 16px 12px 44px rgba(140,200,255,.14);}
/* NASA Blue Marble, equirectangular. Repeated on x and panned, so the map wraps
   seamlessly and reads as rotation without any 3D. */
.entered .earth-land{background-image:url("../assets/earth-map.jpg");}
.earth-land{position:absolute;inset:0;
  background-repeat:repeat-x;background-size:200% 100%;
  /* Painted only once body.entered lands, so the decode does not compete with
     the intro's animation frames. */
  animation:earthSpin 140s linear infinite;}
/* The limb falling into night, plus a lit crescent, which is what actually
   sells a flat map as a sphere. */
.earth-night{position:absolute;inset:0;border-radius:50%;
  background:
    radial-gradient(circle at 32% 28%,transparent 0 52%,rgba(2,7,14,.22) 74%,rgba(1,4,9,.72) 100%),
    linear-gradient(104deg,transparent 44%,rgba(3,8,16,.26) 74%,rgba(1,4,9,.6) 96%);}
.earth-atmo{position:absolute;inset:-4%;border-radius:50%;pointer-events:none;
  background:radial-gradient(circle at 32% 28%,rgba(150,205,255,.26) 0 50%,transparent 66%);
  box-shadow:0 0 100px 22px rgba(120,185,255,.20);}
@keyframes earthSpin{to{background-position:-100% 0;}}

/* The copy has to clear the globe. */
.tearback-copy{padding-bottom:16vmin;}

@media(prefers-reduced-motion:reduce){.earth-land{animation:none;}}
@media(max-width:600px){.earth{width:130vmin;height:130vmin;bottom:-56vmin;}}

/* Sound toggle. Deliberately quiet in the corner: it is an offer, not a
   feature to advertise, and it sits above the pill nav rather than beside the
   chat launcher. */
.soundtoggle{position:fixed;left:18px;bottom:22px;z-index:70;
  font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;
  color:rgba(227,227,227,.55);background:rgba(11,11,12,.82);backdrop-filter:blur(10px);
  border:1px solid rgba(255,255,255,.10);border-radius:100px;padding:8px 13px;
  cursor:pointer;transition:color .2s ease,border-color .2s ease;}
.soundtoggle:hover{color:var(--bone-2);border-color:rgba(255,255,255,.24);}
.soundtoggle[aria-pressed="true"]{color:var(--a1);border-color:rgba(235,235,238,.4);}

/* Grain on the clone would double the cost of the page's heaviest filter for a
   layer that is mostly hidden behind the tear. */
.nograin .plate-img::before{display:none;}

/* ── the callout ─────────────────────────────────────────────────────────
   Appears after the rocket lands, then retires. A bubble that never leaves is
   clutter beside a button that is already pulsing for attention.            */
/* Stacked above the replay pill rather than beside the launcher. That corner is
   already two deep -- launcher at the bottom, replay pill directly on top of it
   -- so anything placed level with either one lands on top of it. */
.askw-callout{position:absolute;right:6px;bottom:134px;z-index:79;width:max-content;max-width:250px;
  text-align:right;pointer-events:none;
  /* It is fixed to the corner while the page scrolls underneath, so it lands on
     dark sections and light ones alike. Bare text with a shadow held up on the
     dark and vanished on the paper; it needs its own ground, like every other
     floating control on this page has. */
  background:rgba(11,11,12,.84);backdrop-filter:blur(12px);
  border:1px solid rgba(255,255,255,.10);border-radius:14px;
  padding:12px 15px;box-shadow:0 16px 40px -18px rgba(0,0,0,.7);
  opacity:0;transform:translateY(10px) scale(.94);transform-origin:100% 100%;
  transition:opacity .5s cubic-bezier(.2,.8,.3,1),transform .5s cubic-bezier(.2,.8,.3,1);}
.askw-callout.show{opacity:1;transform:none;}
.askw-callout[hidden]{display:none;}
.ac-k{display:block;font-family:var(--mono);font-size:9.5px;letter-spacing:.26em;
  text-transform:uppercase;color:var(--chat);margin-bottom:5px;}
.ac-t{display:block;font-family:var(--serif);font-weight:900;font-size:19px;line-height:1.12;
  letter-spacing:-.01em;color:var(--bone-2);}
.ac-t em{font-style:italic;color:var(--chat);}
.ac-s{display:block;margin-top:5px;font-size:11.5px;color:var(--muted);}
/* Each line arrives just behind the one above it, so the thought lands in the
   order it is read rather than all at once. */
.askw-callout.show .ac-k{animation:acIn .45s cubic-bezier(.2,.8,.3,1) both;}
.askw-callout.show .ac-t{animation:acIn .5s cubic-bezier(.2,.8,.3,1) .07s both;}
.askw-callout.show .ac-s{animation:acIn .5s cubic-bezier(.2,.8,.3,1) .15s both;}
@keyframes acIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
@media(max-width:520px){
  .askw-callout{right:4px;bottom:120px;max-width:190px;}
  .ac-t{font-size:16px;} .ac-s{display:none;}
}
@media(prefers-reduced-motion:reduce){
  .askw-callout,.askw-callout.show .ac-k,.askw-callout.show .ac-t,.askw-callout.show .ac-s{
    animation:none;transition:opacity .3s ease;transform:none;}
}

/* A stronger radar now that it has to carry the callout's invitation: wider
   rings, warmer core, and a second ring offset so the pulse never fully rests. */
.askw-launcher.blink{animation:askglowStrong 2.4s ease-in-out infinite;}
@keyframes askglowStrong{
  0%,100%{box-shadow:0 10px 30px -8px rgba(0,0,0,.55),
                     0 0 0 0 rgba(142,162,255,.85),
                     0 0 0 0 rgba(142,162,255,.45),
                     0 0 22px 2px rgba(142,162,255,.38);
          transform:scale(1);}
  50%    {box-shadow:0 10px 30px -8px rgba(0,0,0,.55),
                     0 0 0 14px rgba(142,162,255,0),
                     0 0 0 34px rgba(142,162,255,0),
                     0 0 34px 6px rgba(142,162,255,.6);
          transform:scale(1.09);}
}
@media(prefers-reduced-motion:reduce){.askw-launcher.blink{animation:none;}}


/* ═══════════════════════════════════════════════════════════════════════
   PHONE
   Five separate fixed elements share the bottom of the screen: pill nav,
   sound toggle, chat launcher, replay pill and the callout. Each was
   positioned against the viewport on its own, which is fine at 1440px where
   there is room to be sloppy and collides at 390px where there is not.

   So they are laid out here as one ladder, bottom upward, and the numbers
   are chained deliberately -- each row clears the height of the one below
   it rather than being picked to look right:

     10px   pill nav, full width
     62px   sound toggle (left) and chat launcher (right)
     133px  replay pill, right      (clears the 61px launcher)
     176px  callout                 (clears the 33px replay pill)
   ═══════════════════════════════════════════════════════════════════════ */
@media(max-width:640px){
  .pillnav{left:10px;right:10px;bottom:10px;transform:none;justify-content:center;}

  .soundtoggle{left:10px;bottom:62px;font-size:9.5px;padding:7px 10px;}

  /* The launcher sits 12px above its container, so the container carries the
     row height: 50 + 12 = 62, level with the sound toggle. */
  .askw{right:12px;bottom:50px;}
  .replay{bottom:133px;right:12px;}
  /* Measured from .askw, which is already 50px up. */
  .askw-callout{bottom:126px;right:2px;}

  /* A 19px text link is not a tap target. The card link gets a real one, and
     the row it sits in keeps its baseline across cards. */
  .projrow .go{padding:11px 0;min-height:44px;align-items:center;}

  /* Cursor copy on a device with no cursor, sitting under the skip button. */
  .hint{display:none;}

  /* Swap the entry instruction for one a phone can actually act on. */
  .ek-key,.enter .edot{display:none;}
  .ek-touch{display:inline;}
}


/* A short viewport -- a phone held sideways, or a small laptop window -- has no
   vertical room for a full front page, and the sheet cannot scroll to make any.
   Everything below the headline gives way, and the type is measured against
   height rather than width because height is the scarce axis here. */
@media(max-height:560px){
  .paper[data-sheet]{padding-top:56px;padding-bottom:14px;}
  [data-sheet] .masthead{margin-bottom:10px;padding-bottom:6px;}
  [data-sheet] .display{font-size:clamp(24px,5vh,44px);}
  [data-sheet] .display-sub,[data-sheet] .prose,[data-sheet] .colhead{display:none;}
  [data-sheet] .plate{max-width:min(100%,34vh);}
}

/* While the gate is up the document behind it must not move. Without this the
   wheel gesture that enters the page also scrolls it, and since the tear is
   driven by scroll position, the hero is already torn open by the time anyone
   sees it. touch-action keeps the same true on a phone, where overflow alone
   does not reliably stop a pan; the entry handler reads touchmove, which still
   fires. Applied from JS, so a failed script leaves the page scrollable. */
html.gate-locked, html.gate-locked body{overflow:hidden;}
html.gate-locked{touch-action:none;overscroll-behavior:none;}
