/* ═══════════════════════════════════════════════
 Niki Xiang — portfolio
 Style system per the Dennis Snellenberg reference:
 near-black canvas · single 450 weight · uniform
 +0.05em tracking · pills + one inset ring · one
 violet accent per viewport.
 ═══════════════════════════════════════════════ */

:root{
 --canvas:#1C1D20; /* obsidian — owns the page */
 --navy:#182233; /* sampled from photo edges */
 --fg:#FFFFFF;
 --fog:#999D9E; /* muted secondary text */
 --graphite:#494A4D; /* elevated surface */
 --iris:#455CE9; /* the one chromatic voice */
 --iris-deep:#334BD3;
 /* The blue that carries you between pages: the transition curtain, and the
 control that launches it. Pressing "About me" fills the circle with the
 same blue that then sweeps up as the curtain — the button previews the
 motion instead of merely triggering it.

 It is a THIRD surface — neither page. Matching the destination made the
 exit vanish (white lifting off white); matching the origin would make the
 entry vanish. To clear 3:1 against BOTH the dark canvas and the white
 About page, its luminance has to sit in a narrow band, L 0.137–0.30 —
 which is why every sober-looking candidate picked by eye failed: they
 were all too dark to be seen over the dark pages.

 Ink navy: the cover's own blue, lifted into that band. The cover gradient
 itself cannot serve here — every one of its stops measures 1.06–1.52
 against the home canvas, because a background colour is built to recede,
 not to sweep. Same hue family, raised until it can travel.

 Sitting mid-band rather than at its floor: 3.76 over the dark pages and
 4.52 over the white About page, so neither end is the weak one, and the
 white label reads at 4.52. Chroma, not hue, is what had been loud. */
 --transit:#4D76BC;
 --on-transit:#FFFFFF; /* label, dot, button text — 4.53 on the blue */

 /* The opening screen — the one that cycles her four languages — is the
 canvas itself, not the transit blue and not the reference's #141517.
 Measured against the home page it is 1.00, i.e. no edge at all, and that
 is the point here rather than a fault: nothing sweeps, the greetings
 simply fade and the site develops out of the same surface. A page
 transition has to out-contrast the page it covers; an arrival does not. */
 --greeting:#1C1D20; /* == --canvas */
 --ring:rgba(255,255,255,.2); /* only elevation device */
 --line:rgba(255,255,255,.12);

 /* Anything sitting ON the violet stays white in BOTH themes. Without
 its own token these inherit --fg and go dark-on-violet in light mode. */
 --on-iris:#FFFFFF;
 /* Body copy and hover tints were hardcoded light-on-dark, which paints
 white text onto the white About page. Tokenised so they invert. */
 --fg-muted:rgba(244,242,239,.72);
 --tint:rgba(255,255,255,.03);

 --track:.05em; /* uniform mechanical rhythm */
 --w:450;

 --ease:cubic-bezier(.76,0,.24,1);
 --ease-soft:cubic-bezier(.22,1,.36,1);

 --pad:clamp(1.25rem,4vw,3.5rem);

 /* Content stage. --pad tops out at 56px, so without this every row grew
 with the viewport — measured 2448px wide at a 2560 display, while
 .contact__head was the one thing capped (1100px). The page therefore
 sprawled and then abruptly narrowed at the contact section. A fixed
 stage with the slack in the margins is the whole basis of this kind of
 layout; it was the one part not implemented. Hero and marquee stay
 full-bleed on purpose — they are the two elements meant to escape it. */
 --stage:1440px;

 /* Sub-head step. The scale measured 136 / 73.6 / 36.8 / 18 / 15 / 13 / 11:
 clean ~1.9× descents down to 36.8, then a 2.04× drop to 18 with nothing
 in between, and four cramped steps below. Anything that was neither
 display nor UI label had no size to be. */
 --step-sub:1.5rem; /* 24px */
}

/* ═══ curtain page transition ═══════════════
 View Transitions were replaced here on purpose. They cannot run custom
 DOM during the swap, and the whole point of this transition is that it
 ANNOUNCES the destination — "About" on the way in, "Home" on the way
 back. That naming is what makes the motion read as considered rather
 than decorative, which is the brief for someone whose work is
 cross-border business.

 Choreography is a direct port of the reference's GSAP timeline
 (dennissnellenberg.com/assets/js/index-new.js, pageTransitionIn):
 cover 500ms Power4.easeIn — accelerates INTO the cover; the slow
 start then slam is where the weight
 comes from, not from long durations
 arc 400ms Power4.easeIn — leading dome grows 0→10vh while rising
 label 800ms Power4.easeOut — fades in drifting to −50px, never static
 exit 800ms Power3.easeInOut
 label 600ms linear fade, simultaneous with exit
 trail 850ms Power3.easeInOut, 200ms late — bottom dome flattens last
 ≈2.2s end to end, same as the reference. BOTH directions travel upward —
 the reference's return trip (About → Home) also rises; only the label
 changes. A reversed direction would be an invention, and read as a bounce. */

/* The opening screen wears the dark instead of the transit blue. The class is
 stamped on by an inline script in each page's, not by main.js
 — the curtain is painted before a script at the end of can run, so
 deciding this late would blink blue-then-dark on every fresh load. */
html.is-greeting .curtain,
html.is-greeting .curtain__arc{background:var(--greeting)}

/* Back/forward: the curtain never shows at all. Not merely skipped in JS —
 removed here, in the head-stamped class, because by the time main.js could
 hide it the covering curtain has already had a frame on screen. */
html.is-instant .curtain{display:none}

.curtain{
 position:fixed;
 inset:0;
 z-index:8000;
 display:grid;
 place-items:center;
 background:var(--transit);
 color:var(--on-transit);
 transform:translateY(0); /* covering, before JS runs */
 will-change:transform;
}
.curtain.is-gone{display:none}

/* Leading/trailing edges — the reference's "rounded-div", which is an
 overflow-hidden wrap clipping the cap off a huge ellipse. The clip lands so
 the dome's BASE meets the two screen edges: the whole height reads as curve.

 Getting that wrong is what made this look flat. At 150vw the dome's base sat
 far outside the viewport, so the screen only ever saw its middle — 80px at
 centre against 60px at the edges, a 20px sag across 1440px. Sizing the
 ellipse just past the viewport (104vw) drops the edges to ~40px and lifts the
 centre to 145px: 105px of visible curve, 5× the arc for the same idea.

 A half-ellipse border-radius reproduces the reference's silhouette without
 the wrapper — curved on the travel side, dead flat where it meets the
 curtain, so no page slivers leak through at the corners. */
:root{ --arc-h:18vh; --arc-w:104vw }
@media (max-width:540px){ :root{ --arc-h:11vh; --arc-w:108vw } }

.curtain__arc{
 position:absolute;
 left:50%;
 width:var(--arc-w);
 margin-left:calc(var(--arc-w) / -2);
 height:0;
 background:var(--transit);
 pointer-events:none;
 will-change:height;
}
/* Overlapped by 1px, not butted at exactly 100%. The curtain travels on a
 fractional transform, so an exact edge-to-edge join rounds to a subpixel gap
 and the page behind shows through as a hairline seam right across the dome's
 base — visible in a screenshot of the frozen mid-cover state. */
.curtain__arc--top{
 bottom:calc(100% - 1px); /* dome up — leads the cover */
 border-radius:50% 50% 0 0 / 100% 100% 0 0;
}
.curtain__arc--bottom{
 top:calc(100% - 1px); /* dome down — trails the exit */
 border-radius:0 0 50% 50% / 0 0 100% 100%;
}
/* The violet hairline that used to trace these curves is gone: it existed only
 because a dark curtain over a dark page had no visible edge. The curtain now
 carries its own contrast, so the silhouette needs no help. */

.curtain__word{
 display:flex;
 align-items:center;
 gap:.62em;
 font-family:"Inter",var(--cjk),system-ui,sans-serif;
 font-size:clamp(1.9rem,4.2vw,2.45rem); /* reference sets 39px */
 letter-spacing:var(--track);
 color:currentColor; /* white on violet, in both themes */
 opacity:0;
 transform:translateY(0);
 will-change:opacity,transform;
}
.curtain__dot{
 width:.26em;height:.26em;
 border-radius:50%;
 background:currentColor;
 flex-shrink:0;
}
/* Label never sits still — it fades in already drifting to −50px (reference:
 .8s Power4.easeOut, y:0→−50) and keeps that offset while fading out. */
.curtain.is-labelled .curtain__word{
 opacity:1;
 transform:translateY(-50px);
 transition:opacity .8s cubic-bezier(.22,1,.36,1),
 transform .8s cubic-bezier(.22,1,.36,1);
}

/* Phase easings, straight from the reference:
 Power4.easeIn = quint-in cubic-bezier(.64,0,.78,0)
 Power3.easeInOut = quart-in-out cubic-bezier(.76,0,.24,1) */

/* ── phase 1, on the page being left: the curtain accelerates up from the
 bottom (easeIn — slow start, fast cover) while the top dome grows.
 The bottom dome is already at full height, riding along. ── */
.curtain.is-covering{
 animation:curtain-cover 500ms cubic-bezier(.64,0,.78,0) both;
}
.curtain.is-covering .curtain__arc--top{
 animation:arc-grow 400ms cubic-bezier(.64,0,.78,0) both;
}
.curtain.is-covering .curtain__arc--bottom,
.curtain.is-holding .curtain__arc--bottom{
 height:var(--arc-h);
}
@keyframes curtain-cover{
 from{ transform:translateY(100%) }
 to { transform:translateY(0) }
}
@keyframes arc-grow{
 from{ height:0 }
 to { height:var(--arc-h) }
}

/* ── between documents: the arriving page starts covered (.curtain's base
 state is translateY(0)), label already placed — no re-entrance. ── */
.curtain.is-holding .curtain__word{
 opacity:1;
 transform:translateY(-50px);
 transition:none;
}

/* ── phase 2, on the arrived page: curtain lifts away (easeInOut), label
 fades simultaneously (600ms linear), bottom dome flattens 200ms late
 so the trailing curve is the last thing to clear. ── */
.curtain.is-lifting{
 animation:curtain-lift 800ms cubic-bezier(.76,0,.24,1) both;
}
.curtain.is-lifting .curtain__arc--bottom{
 animation:arc-shrink 850ms cubic-bezier(.76,0,.24,1) 200ms both;
}
.curtain.is-lifting .curtain__word{
 opacity:0;
 transform:translateY(-50px);
 transition:opacity .6s linear;
}
@keyframes curtain-lift{
 from{ transform:translateY(0) }
 to { transform:translateY(-100%) }
}
@keyframes arc-shrink{
 from{ height:var(--arc-h) }
 to { height:0 }
}

@media (prefers-reduced-motion:reduce){
 .curtain,.curtain__arc,.curtain__word{animation:none!important;transition:none!important}
}

/* ═══ light theme (About) ═══════════════════
 Pure white, matching the reference's About surface exactly (its main is
 #FFFFFF). Set on so it lands before first paint — the curtain
 lifts to reveal it, so any flash of the wrong canvas would be visible.
 Neutrals are pulled very slightly cool to agree with the violet accent
 — a warm grey would fight it. Borders flip to alpha-black, which
 recedes in light mode where alpha-white would glow. */
html.theme-light{
 --canvas:#FFFFFF;
 --bg-soft:#EFEFF2;
 --fg:#16171A;
 --fog:#6C6F76;
 --faint:rgba(22,23,26,.34);
 --line:rgba(22,23,26,.12);
 --ring:rgba(22,23,26,.20);
 --graphite:#E4E5EA;
 --fg-muted:rgba(22,23,26,.72);
 --tint:rgba(22,23,26,.035);
}
/* The header blends against the hero photo on the dark home page; over a
 flat light canvas that inversion just muddies it. */
html.theme-light .head{mix-blend-mode:normal}
html.theme-light ::selection{background:var(--iris);color:var(--on-iris)}

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}

html{
 -webkit-font-smoothing:antialiased;
 -moz-osx-font-smoothing:grayscale;
 scroll-behavior:auto;
}

body{
 background:var(--canvas);
 color:var(--fg);
 font-family:"Inter",system-ui,-apple-system,sans-serif;
 font-weight:var(--w);
 font-size:.9375rem; /* body-lg 15px */
 line-height:1.6;
 letter-spacing:var(--track);
 overflow-x:hidden;
 overscroll-behavior-y:none;
}

body:not(.is-ready) #scrollWrap{opacity:0}

img{display:block;max-width:100%;height:auto}
/* wraps the images for WebP fallback; keep it out of layout so
 the img sizes against its real container exactly as it did before */
picture{display:contents}
ul{list-style:none}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
sup{font-size:.65em}

::selection{background:var(--iris);color:var(--fg)}

/* Keyboard floor. 23 focusable elements shipped with no visible focus
 state. Never remove this. */
:focus-visible{
 outline:2px solid var(--iris);
 outline-offset:3px;
 border-radius:2px;
}
.pill:focus-visible,.circle-btn:focus-visible{outline-offset:4px}

/* The custom cursor is gone; the native one is back. The `cursor:none` that
 used to hide it went with it — leaving that behind would have given the
 page no pointer at all. */

/* ── smooth scroll rig ─────────────────────── */
html.has-smooth #scrollWrap{
 position:fixed;
 top:0;left:0;
 width:100%;
 will-change:transform;
}

/* ── primitives ────────────────────────────── */
/* mask taller than the line so descenders survive */
.mask{display:block;overflow:hidden;padding-bottom:.18em;margin-bottom:-.18em}
.mask>span{display:block;will-change:transform;transform:translateY(130%)}

.eyebrow{
 font-size:.6875rem;
 letter-spacing:.14em;
 text-transform:uppercase;
 color:var(--fog);
}

.reveal-block{
 opacity:0;
 transform:translateY(26px);
 transition:opacity .9s var(--ease-soft),transform .9s var(--ease-soft);
}
.reveal-block.is-in{opacity:1;transform:none}

/* ═══ pill — the system's interactive shape ═ */
.pill{
 display:inline-flex;
 align-items:center;
 gap:.5rem;
 padding:.9rem 1.5rem;
 border-radius:999px;
 font-size:.875rem;
 letter-spacing:var(--track);
 position:relative;
 overflow:hidden;
 isolation:isolate;
 transition:color .45s var(--ease);
}
.pill--sm{padding:.8rem 1.2rem;font-size:.8125rem} /* ≥44px tall */
.pill--inset{box-shadow:inset 0 0 0 1px var(--ring)}
.pill span{position:relative;z-index:1}
.pill sup{position:relative;z-index:1;color:var(--fog)}
/* fill wipes up on hover — state change by background swap */
.pill::before{
 content:"";
 position:absolute;inset:0;
 z-index:0;
 background:var(--iris);
 border-radius:999px;
 transform:translateY(101%);
 transition:transform .5s var(--ease);
}
.pill:hover::before{transform:translateY(0)}
.pill:hover sup{color:var(--on-iris)}
.pill:hover span{color:var(--on-iris)}

/* ═══ circle button (site signature CTA) ════ */
.circle-btn{
 position:relative;
 display:grid;
 place-items:center;
 width:clamp(120px,11vw,150px);
 aspect-ratio:1;
 border-radius:50%;
 box-shadow:inset 0 0 0 1px var(--ring);
 font-size:.875rem;
 text-align:center;
 overflow:hidden;
 isolation:isolate;
 will-change:transform;
}
.circle-btn span{position:relative;z-index:1;transition:color .5s var(--ease)}
/* The fill is the transit blue, not white: this control opens another page,
 and the circle growing under the cursor is the same blue that then sweeps
 up as the curtain. The hover is a preview of the transition, not a
 separate effect. White text rather than the canvas — the fill is now mid
 blue, and the dark canvas colour would sit on it at 1.6. */
.circle-btn::before{
 content:"";
 position:absolute;inset:0;
 z-index:0;
 background:var(--transit);
 border-radius:50%;
 transform:scale(0);
 transition:transform .6s var(--ease);
}
.circle-btn:hover::before{transform:scale(1)}
.circle-btn:hover span{color:var(--on-transit)}
/* violet variant — the accent, one per viewport */
.circle-btn--accent{
 background:var(--iris);
 box-shadow:none;
 width:clamp(150px,14vw,190px);
}
.circle-btn--accent::before{background:var(--iris-deep)}
.circle-btn--accent span{color:var(--on-iris)}
.circle-btn--accent:hover span{color:var(--on-iris)}

/* ═══ header ═══════════════════════════════ */
.head{
 position:fixed;
 top:0;left:0;right:0;
 z-index:6000;
 display:flex;
 align-items:center;
 justify-content:space-between;
 padding:clamp(1.1rem,2.2vw,1.8rem) var(--pad);
 mix-blend-mode:difference;
 opacity:0;
 transform:translateY(-12px);
 transition:opacity .8s var(--ease-soft) .3s,transform .8s var(--ease-soft) .3s;
}
body.is-ready .head{opacity:1;transform:none}

.head__logo{display:flex;align-items:baseline;gap:.45rem;font-size:.8125rem}
.head__mark{opacity:.6}

.head__nav{display:flex;gap:clamp(.15rem,.8vw,.6rem)}

/* Touch the word and it rises. That is the whole interaction.
 No pill, no oval, no proximity field, no rolling duplicate — each of those
 was a second idea competing with this one, and at 13px there is only room
 for one. What makes it read as considered is not what was added but what
 the rest is doing: the items sit muted, and the one under the pointer is
 the only thing at full strength.

 Two properties move together — lift and colour — on the same curve, so it
 reads as one gesture rather than two effects that happen to coincide.
 4px, not 8: the lift has to be felt without the row visibly breaking line. */
.head__nav a{
 position:relative;
 display:inline-block;
 padding:12px 16px; /* invisible now, but it holds the 44px hit area.
 12, not 10: the inner span went display:block and
 the line box lost 3px, measuring 41. */
 font-size:.8125rem;
 color:var(--fog);
 transition:color .45s var(--ease-soft);
}
.head__nav a>span{
 display:block;
 transition:transform .45s var(--ease-soft);
}
@media (hover:hover) and (pointer:fine){
 .head__nav a:hover{color:var(--fg)}
 .head__nav a:hover>span{transform:translateY(-4px)}
}
/* Keyboard gets the same state — the nav should not read differently by tab. */
.head__nav a:focus-visible{color:var(--fg)}
.head__nav a:focus-visible>span{transform:translateY(-4px)}

@media (prefers-reduced-motion:reduce){
 .head__nav a,
 .head__nav a>span{transition:none}
 .head__nav a:hover>span,
 .head__nav a:focus-visible>span{transform:none}
}

.burger{display:none;width:34px;height:16px;position:relative}
/* the bars stay 34x16; the hit area grows to the 44px touch floor.
 This is the only nav control on mobile — it cannot be a near-miss. */
.burger::after{
 content:"";
 position:absolute;
 top:50%;left:50%;
 width:48px;height:48px;
 transform:translate(-50%,-50%);
}
.burger span{
 position:absolute;left:0;
 width:100%;height:1px;
 background:var(--fg);
 transition:transform .5s var(--ease);
}
.burger span:nth-child(1){top:3px}
.burger span:nth-child(2){bottom:3px;width:70%}
.burger.is-open span:nth-child(1){transform:translateY(4.5px) rotate(45deg)}
.burger.is-open span:nth-child(2){width:100%;transform:translateY(-4.5px) rotate(-45deg)}

/* ═══ fullscreen menu ══════════════════════ */
.menu{position:fixed;inset:0;z-index:5500;pointer-events:none;visibility:hidden}
.menu.is-open{pointer-events:auto;visibility:visible}
.menu__bg{
 position:absolute;inset:0;
 background:var(--graphite);
 transform:translateY(-100%);
 transition:transform .95s var(--ease);
}
.menu.is-open .menu__bg{transform:translateY(0)}
.menu__inner{
 position:relative;
 height:100%;
 display:grid;
 grid-template-columns:1.4fr 1fr;
 align-content:end;
 gap:3rem;
 padding:var(--pad);
 padding-bottom:clamp(3rem,7vw,5rem);
}
.menu__col--nav ul{margin-top:1.5rem}
.menu__col--nav li{overflow:hidden}
.menu__col--nav a{
 display:block;
 font-size:clamp(2.5rem,7vw,5rem);
 line-height:1.08;
 letter-spacing:var(--track);
 transition:opacity .4s;
}
.menu__col--nav a span{
 display:block;
 transform:translateY(110%);
 transition:transform .9s var(--ease);
}
.menu.is-open .menu__col--nav a span{transform:translateY(0)}
.menu.is-open li:nth-child(1) a span{transition-delay:.18s}
.menu.is-open li:nth-child(2) a span{transition-delay:.25s}
.menu.is-open li:nth-child(3) a span{transition-delay:.32s}
.menu.is-open li:nth-child(4) a span{transition-delay:.39s}
.menu__col--nav ul:hover a{opacity:.35}
.menu__col--nav ul:hover a:hover{opacity:1}
.menu__col--meta{
 display:flex;flex-direction:column;justify-content:flex-end;gap:2.5rem;
 opacity:0;transform:translateY(20px);
 transition:opacity .7s var(--ease-soft) .45s,transform .7s var(--ease-soft) .45s;
}
.menu.is-open .menu__col--meta{opacity:1;transform:none}
.menu__socials{margin-top:1rem;display:flex;flex-direction:column;gap:.4rem}
.menu__socials a{position:relative;display:inline-block;width:fit-content}
.menu__socials a::after{
 content:"";position:absolute;left:0;bottom:-2px;
 width:100%;height:1px;background:currentColor;
 transform:scaleX(0);transform-origin:right;
 transition:transform .5s var(--ease);
}
.menu__socials a:hover::after{transform:scaleX(1);transform-origin:left}
.menu__loc-label{margin-bottom:.6rem}
.menu__loc{font-size:1rem;line-height:1.4}
body.menu-open{overflow:hidden}

/* ═══ hero — full-bleed photo, giant name ══ */
/* The portrait's studio haze is real photographic atmosphere — keep it.
 A flat surround can never meet a gradient, so the page continues the
 photo's own falloff instead. Stops below are sampled from the file:
 upper-left bloom #263449 (L51) → bottom-right corner #0b101b (L16). */
/* The studio backdrop is gone — Vision lifts the subject and she stands
 directly on the site canvas. Two earlier attempts tried to reconcile the
 photo's backdrop with the page (flat colour, then a blurred extension of
 the frame itself); both left a visible join. Removing the second surface
 is what actually fixes it: there is now only one background in the hero. */
.hero{
 position:relative;
 height:100svh;
 /* No overflow:hidden here any more — the portrait clips itself in
 .hero__photo. Keeping it would make "complete the descender" and "move
 the name lower" mutually exclusive: the tail can never sit below the
 floor that clips it. */
 /* The portrait's original navy, rebuilt as a clean studio falloff.
 Stops come from the source file (bloom #263449 → corner #0b101b);
 with the backdrop gone this is free to be a graphic surface rather
 than a match, so it is lit behind her head and drops to the edges. */
 background:
 /* tight halo hugging her silhouette — lifts her off the backdrop so
 she reads as a forward plane without occluding anything */
 radial-gradient(30% 42% at 50% 40%,
 rgba(86,112,150,.30) 0%,
 rgba(64,86,120,.13) 45%,
 transparent 78%),
 radial-gradient(86% 72% at 50% 33%,
 #2C3C56 0%,
 #24324A 30%,
 #1A2437 58%,
 #131B2C 82%,
 #0F1524 100%);
}

/* Below the name (z 1 vs 2) — the name must stay fully readable.
 Occluding it was tried and rejected: she stands centre-frame, so her
 body ate the whole middle of "Niki Xiang" and the surname vanished.
 Her forwardness is carried by value instead, the same way the
 reference site does it — see .hero__name below. */
/* This is now the only thing clipping the hero. .hero used to hide its own
 overflow, which also sheared the giant name's descender — the tail of the
 `g` ends 4px past its em box and the name box already sat 2px below the
 hero's floor, so ~6px of the tail was being shaved flat. Moving the clip
 down one level lets the name overhang while the portrait stays contained. */
.hero__photo{position:absolute;inset:0;z-index:1;overflow:hidden}

.hero__photo-main{
 position:absolute;
 left:50%;
 bottom:0;
 height:104%; /* headroom for parallax drift */
 width:auto;
 max-width:none;
 transform:translateX(-50%) scale(1.06);
 transform-origin:50% 100%;
 transition:transform 1.6s var(--ease) .1s;
 /* Deliberately NOT masked at the foot. A bottom fade here was washing
 the name out through her semi-transparent pixels — "Niki" stayed white
 while "Xiang" went grey. She stays opaque; the hand-off to the page is
 the background's job, not hers. */
}
body.is-ready .hero__photo-main{transform:translateX(-50%) scale(1)}

/* Behind both the name and her (z 1): settles the navy into the page
 canvas so the hero hands off to the next section with no visible band.
 Kept off the figure and the type so neither gets tinted. */
.hero::after{
 content:"";
 position:absolute;
 left:0;right:0;bottom:0;
 height:38%;
 z-index:1;
 pointer-events:none;
 background:linear-gradient(to bottom,
 rgba(15,21,36,0) 0%,
 rgba(21,24,31,.72) 62%,
 var(--canvas) 100%);
}

/* location pill — mid-left */
/* One axis for both side elements. They used to carry unrelated magic
 percentages — pill 44%, role 38% — with nothing holding them together, and
 their boxes are different heights (74 vs 86), so they could not have lined
 up at any viewport. Measured 54px apart on centres, 46px on first text line.

 `translate` rather than `transform` for the centring: both elements already
 use transform for their entrance (translateX(-30%) / translateY(24px)), and
 the independent property composes with it instead of overwriting it. */
.hero__pill,
.hero__role{
 top:var(--hero-axis,48%);
 translate:0 -50%;
}

.hero__pill{
 position:absolute;
 left:0;
 z-index:4;
 display:flex;
 align-items:center;
 gap:0;
 background:var(--canvas);
 color:var(--fg);
 border-radius:0 999px 999px 0;
 padding:.55rem .55rem .55rem 1.6rem;
 opacity:0;
 transform:translateX(-30%);
 transition:opacity .8s var(--ease) .9s,transform .9s var(--ease) .9s;
}
body.is-ready .hero__pill{opacity:1;transform:none}
.hero__pill-text{
 font-size:.75rem;
 line-height:1.25;
 padding-right:1.1rem;
}
.hero__pill-globe{
 display:grid;place-items:center;
 width:56px;height:56px;
 border-radius:50%;
 background:var(--fg);
 color:var(--canvas);
}
.hero__pill-globe svg{
 width:24px;height:24px;
 animation:spin 12s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}

/* role — mid-right */
/* Pulled in to the stage's right edge on wide screens — the same 1440 column
 the rest of the page sits on. At 2000px that is 280px in rather than 56,
 which closes the gap toward her instead of stranding the type against the
 bezel; at 1440 and below max() returns --pad and nothing moves. The hero
 image stays full-bleed; only its furniture respects the column. */
.hero__role{
 position:absolute;
 right:max(var(--pad),calc((100% - var(--stage)) / 2));
 z-index:4;
 display:flex;
 flex-direction:column;
 gap:1rem;
 font-size:clamp(.9375rem,1.25vw,1.125rem);
 line-height:1.4;
 opacity:0;
 transform:translateY(24px);
 transition:opacity .8s var(--ease) 1.05s,transform .9s var(--ease) 1.05s;
}
body.is-ready .hero__role{opacity:1;transform:none}
/* 20, not 22: the box is now tight to the drawing. The old 24-viewBox left
 6px of dead space on every side, so a "22px" arrow was really a 10px mark
 floating in a 22px box — and the gap to the text below measured 16px but
 read as 22px. */
.hero__role-arrow{width:20px;height:20px}

/* giant name, bottom, bleeding off canvas */
/* The depth cue. Pure white made the type the brightest thing on screen,
 which pulled it in front of her; her lit face is the natural highlight
 and has to win that contest. Backing off to ~86% white keeps the name
 fully legible while letting her read as the forward layer. */
.hero__name{
 position:absolute;
 left:0;right:0;
 /* --display is declared here rather than on the line so these sums resolve
 against the DISPLAY size. Written as `em` they silently resolved against
 this element's own 30px h1 default instead of the 384px line: a .24em
 pad measured 7.2px where 92 was intended, and only cleared the descender
 by 3px, by luck.

 Measured, not guessed: at 384px the baseline sits 307px into the em box
 and the `g` puts 81px of ink under it — 388px against a 384px box, so the
 tail overhangs by 4px, and the box already sat 2px low. .27 of the display
 size is well past that; the seat drops by the same amount so the letters
 do not shift, plus .03 for the nudge down. */
 /* 14.5vw, down from 19.5. At 19.5 the ten characters spanned the viewport
 edge to edge, which is what made it dominate everything else in the hero;
 14.5 lands it at roughly three quarters of the width and gives the right
 margin back as air.

 The seat is solved, not nudged. The line box's bottom sits at
 heroBottom + .24D − .27D, and the `g` puts another ~.010D of tail past
 that, so the tip lands ~.02D ABOVE the fold — inside the first screen at
 every viewport, rather than the 13px below it that the previous
 hand-tuned offset produced. */
 --display:clamp(4rem,14.5vw,18rem);
 /* Clearance below the tail is D × (S + .2596), so S = −.18 leaves about
 .08 of the display size — 17px at 1440, 22px at 1930. −.24 was solved to
 put the tip exactly at the fold and measured 4px of clearance, which is
 arithmetically inside but reads as touching. */
 bottom:calc(var(--display) * -.18);
 padding-bottom:calc(var(--display) * .27);
 z-index:2;
 overflow:hidden;
 font-weight:var(--w);
 color:rgba(255,255,255,.86);
 pointer-events:none;
}
.hero__name-line{
 display:block;
 white-space:nowrap;
 font-size:var(--display);
 line-height:1;
 letter-spacing:var(--track);
 margin-left:-.04em;
 /* 132%, not 105%: the clip box is now 1.24em tall, so a 105% start would
 leave the tops of the letters showing above the fold before the reveal
 even begins. Anything past 124% clears it. */
 transform:translateY(132%);
 transition:transform 1.3s var(--ease) .55s;
}
body.is-ready .hero__name-line{transform:translateY(0)}

/* graceful placeholder before the photo exists */
img.is-missing{
 background:linear-gradient(150deg,#20293A,#141B29);
 min-height:340px;
 font-size:0;
 color:transparent;
}

/* ═══ intro statement ══════════════════════ */
/* Everything that is not deliberately full-bleed sits on the stage. */
.intro__grid,
.stats,
.work__label,
.work__list,
.work__more,
.contact__rule,
.contact__details,
.foot__grid{
 max-width:var(--stage);
 margin-inline:auto;
}
/* .contact__head is excluded above because it carries its own 1100px cap for
 the headline measure. Centring it would have shifted it 114px right on a
 1440 display, where 1100 already bites — a visible change to a layout that
 was not in question. It instead offsets to the stage's left edge, which is
 0 until the viewport actually exceeds the stage. */
.contact__head{
 margin-left:max(0px,calc((100% - var(--stage)) / 2));
 margin-right:0;
}

.intro{padding:clamp(7rem,18vh,14rem) var(--pad)}
.intro__grid{
 display:grid;
 grid-template-columns:1.6fr 1fr;
 gap:clamp(3rem,7vw,7rem);
 align-items:start;
}
/* Capped at what it already measured on a 1440 display, so this composition
 is unchanged there and simply stops drifting above it — it was reading at
 32ch on a laptop and 61ch on a 2560 display, two different paragraphs.
 Every other block of prose on the site was already capped in ch; the one
 sentence that carries the whole page was not. */
.intro__statement{
 font-size:clamp(1.5rem,2.9vw,2.3rem); /* heading-sm territory */
 line-height:1.35;
 letter-spacing:var(--track);
 max-width:32ch;
}
/* Standing facts. Deliberately NOT a metrics panel: no cards, no borders but
 the one hairline, no accent colour, no oversized figures. The figure sits at
 the sub-head step — big enough to be the entry point, small enough that the
 row still reads as part of the intro rather than a band bolted underneath.
 tabular-nums so 04 and 05 align on the stem. */
.stats{
 display:grid;
 grid-template-columns:repeat(3,1fr);
 gap:clamp(1.5rem,4vw,4rem);
 margin-top:clamp(4rem,9vh,7rem);
 padding-top:clamp(1.5rem,3vw,2.25rem);
 border-top:1px solid var(--line);
}
.stats__k{
 font-size:var(--step-sub);
 letter-spacing:var(--track);
 font-variant-numeric:tabular-nums;
 color:var(--fg);
}
.stats__k--text{white-space:nowrap}
.stats__k i{font-style:normal;color:var(--faint);padding:0 .15em}
.stats__v{
 margin-top:.75rem;
 font-size:.8125rem;
 letter-spacing:var(--track);
 color:var(--fg);
}
/* The explanatory half drops to fog — the label is the fact, this is context */
.stats__v span{display:block;margin-top:.3rem;color:var(--fog)}

@media (max-width:720px){
 .stats{grid-template-columns:1fr;gap:1.75rem}
}

.word{display:inline-block;overflow:hidden;vertical-align:top;padding-bottom:.1em;margin-bottom:-.1em}
.word>span{
 display:inline-block;
 transform:translateY(110%);
 transition:transform .95s var(--ease);
}
.intro__statement.is-in .word>span{transform:translateY(0)}
.intro__side{
 display:flex;
 flex-direction:column;
 gap:2.5rem;
 padding-top:.5rem;
}
.intro__note{color:var(--fog);max-width:30ch}
.intro__side .circle-btn{align-self:flex-start}

/* ═══ work ═════════════════════════════════ */
.work{padding:0 var(--pad) clamp(5rem,10vh,8rem)}
.work__label{padding-bottom:clamp(1.5rem,3vw,2.5rem)}

.work__list{border-top:1px solid var(--line)}
.work__item{position:relative;border-bottom:1px solid var(--line)}
.work__item>a{
 display:flex;
 align-items:baseline;
 justify-content:space-between;
 gap:clamp(1rem,3vw,3rem);
 padding:clamp(2rem,4.5vw,3.4rem) 0;
 transition:opacity .4s var(--ease-soft),padding .6s var(--ease);
}
.work__list:hover .work__item>a{opacity:.3}
.work__list:hover .work__item:hover>a{opacity:1;padding-left:clamp(.75rem,2vw,2rem);padding-right:clamp(.75rem,2vw,2rem)}

.work__title{
 font-size:clamp(2rem,5.2vw,4.6rem); /* heading-lg */
 font-weight:var(--w);
 line-height:1.06;
 letter-spacing:var(--track);
}
.work__title .mask>span{transition:transform 1s var(--ease)}
.work__item.is-in .work__title .mask>span{transform:translateY(0)}
.work__meta{
 font-size:.8125rem;
 color:var(--fog);
 white-space:nowrap;
}

/* cursor-trailing thumbnail */
.work__thumb{
 position:fixed;
 top:0;left:0;
 z-index:4000;
 width:clamp(200px,22vw,320px);
 aspect-ratio:4/3;
 border-radius:10px;
 background:linear-gradient(140deg,var(--a),var(--b));
 pointer-events:none;
 opacity:0;
 transform:translate(-50%,-50%) scale(.82);
 transition:opacity .4s var(--ease-soft);
 will-change:transform;
}
.work__item.is-hovered .work__thumb{opacity:1}
@media (hover:none),(pointer:coarse){.work__thumb{display:none}}

.work__more{display:flex;justify-content:center;padding-top:clamp(2.5rem,6vw,4.5rem)}

/* ═══ marquee ══════════════════════════════ */
.marquee{
 overflow:hidden;
 padding:clamp(1.75rem,4vw,3rem) 0;
 border-top:1px solid var(--line);
 white-space:nowrap;
}
.marquee__track{display:inline-flex;will-change:transform}
.marquee__track span{
 font-size:clamp(2rem,6vw,5.5rem);
 letter-spacing:var(--track);
 padding-right:.35em;
}
.marquee__track i{font-style:normal;color:var(--graphite)}

/* ═══ about ════════════════════════════════ */
.about{
 display:grid;
 grid-template-columns:1fr 1.2fr;
 gap:clamp(2rem,6vw,6rem);
 align-items:center;
 padding:clamp(6rem,15vh,11rem) var(--pad);
 border-top:1px solid var(--line);
}
/* The subject is a cutout, so the frame needs its own ground or the
 rounded box renders as nothing but page background. Same navy as the
 home hero, kept as one deliberate surface. */
.about__media-mask{
 overflow:hidden;
 border-radius:10px;
 background:radial-gradient(88% 74% at 50% 30%,#2C3C56 0%,#1A2437 55%,#131B2C 100%);
 clip-path:inset(100% 0 0 0);
 transition:clip-path 1.2s var(--ease);
}
.about__media.is-in .about__media-mask{clip-path:inset(0 0 0 0)}
.about__media img{
 width:100%;
 aspect-ratio:4/5;
 object-fit:cover;
 object-position:50% 18%;
 transform:scale(1.12);
 transition:transform 1.4s var(--ease);
}
.about__media.is-in img{transform:scale(1)}

.about__body{display:flex;flex-direction:column;gap:2rem}
.about__text{
 font-size:clamp(1.35rem,2.3vw,2.05rem);
 line-height:1.4;
 letter-spacing:var(--track);
}
.about__sub{color:var(--fog);font-size:.875rem}

/* ═══ contact ══════════════════════════════ */
.contact{
 padding:clamp(6rem,15vh,11rem) var(--pad) clamp(4rem,9vh,6rem);
 border-top:1px solid var(--line);
}
.contact__head{
 position:relative;
 max-width:1100px;
}
.contact__avatar{
 width:clamp(72px,7vw,100px);
 aspect-ratio:1;
 border-radius:50%;
 overflow:hidden;
 margin-bottom:1.5rem;
}
.contact__avatar img{
 width:100%;height:100%;
 object-fit:cover;
 object-position:50% 18%;
}
.contact__title{
 font-size:clamp(3rem,9.5vw,8.5rem);
 font-weight:var(--w);
 line-height:1.04;
 letter-spacing:var(--track);
}
.contact__title .mask>span{transition:transform 1.15s var(--ease)}
.contact.is-in .contact__title .mask>span{transform:translateY(0)}
.contact.is-in .contact__title .mask:nth-child(2)>span{transition-delay:.1s}

.contact__float{
 position:absolute;
 right:clamp(0px,6vw,80px);
 bottom:-18%;
}

.contact__rule{
 height:1px;
 background:var(--line);
 /* margin-BLOCK only. The shorthand's `0` for the inline axis silently beat
 the stage's margin-inline:auto, and this rule wins on source order — the
 divider stayed pinned at the viewport edge while everything it separates
 had moved onto the stage. Measured left:56 against everything else's 560. */
 margin-block:clamp(4rem,9vw,7rem) clamp(2rem,4vw,3rem);
}
.contact__details{display:flex;flex-wrap:wrap;gap:1rem}

/* ═══ footer ═══════════════════════════════ */
.foot{padding:clamp(2.5rem,5vw,4rem) var(--pad) clamp(2rem,4vw,3rem)}
.foot__grid{
 display:flex;
 align-items:flex-end;
 gap:clamp(2rem,6vw,5rem);
}
.foot__val{font-size:.875rem;margin-top:.6rem;font-variant-numeric:tabular-nums}
.foot__cell--right{margin-left:auto}
.foot__socials{margin-top:.8rem;display:flex;gap:.6rem}

/* ═══════════════════════════════════════════
 ABOUT PAGE
 ═══════════════════════════════════════════ */

.sr-only{
 position:absolute;width:1px;height:1px;
 padding:0;margin:-1px;overflow:hidden;
 clip-path:inset(50%);white-space:nowrap;border:0;
}

/* System CJK — PingFang/Hiragino are already on the reader's machine, so
 the Chinese sets correctly without shipping a multi-megabyte webfont. */
:root{
 --cjk:"PingFang SC","PingFang HK","Hiragino Sans GB","Hiragino Sans",
 "Noto Sans CJK SC","Microsoft YaHei",sans-serif;
}

/* Current-page marker: a dot below the label, not an underline — an
 underline reads as a link affordance, a dot reads as "you are here".
 The colour here is what the resting nav is muted AGAINST: before, every
 item computed to the same --fg and this rule distinguished nothing. */
.head__nav a.is-current{color:var(--fg)}
.head__nav a.is-current::after{
 content:"";
 position:absolute;left:50%;bottom:6px; /* inside the padding, under the word */
 width:3px;height:3px;
 margin-left:-1.5px;
 border-radius:50%;
 background:currentColor;
}

/* ── masthead ───────────────────────────── */
.ab-head{
 padding:clamp(9rem,20vh,13rem) var(--pad) clamp(4rem,9vh,7rem);
}

/* Bilingual signature. CJK glyphs are full-width and carry more ink, so
 at an identical font-size they read heavier than the Latin beside them
 — the CJK run is stepped down ~10% to sit optically level. */
.ab-sig{
 display:flex;
 align-items:baseline;
 gap:.6rem;
 margin-bottom:clamp(1.5rem,3vw,2.25rem);
 font-size:clamp(1.05rem,1.6vw,1.35rem);
 color:var(--fog);
}
/* Both runs are stepped off the same base. The Latin is uppercase at .82em;
 the CJK has to land just under that (.75em) — measured 19.4px vs 17.7px
 at .9em, i.e. the CJK was reading LARGER, the opposite of the intent. */
.ab-sig__cjk{
 font-family:var(--cjk);
 font-size:.75em;
 letter-spacing:.14em;
 color:var(--fg);
}
.ab-sig__sep{opacity:.4}
.ab-sig__latin{letter-spacing:.1em;text-transform:uppercase;font-size:.82em}

.ab-title{
 font-size:clamp(2.4rem,5.6vw,5rem);
 font-weight:var(--w);
 line-height:1.06;
 letter-spacing:var(--track);
 max-width:15ch;
}
.ab-title .mask>span{transition:transform 1.15s var(--ease)}
body.is-ready .ab-title .mask>span{transform:translateY(0)}
body.is-ready .ab-title .mask:nth-child(2)>span{transition-delay:.08s}
body.is-ready .ab-title .mask:nth-child(3)>span{transition-delay:.16s}

/* Rule with the globe sitting ON it rather than beside it — the circle
 interrupts the line, which is what makes it read as an object in space
 instead of an icon in a row. */
.ab-rule{
 position:relative;
 display:flex;
 align-items:center;
 margin:clamp(2.5rem,6vw,4.5rem) 0 clamp(1.5rem,3vw,2.25rem);
 height:clamp(96px,11vw,150px);
}
.ab-rule__line{
 flex:1;
 height:1px;
 background:var(--line);
 transform:scaleX(0);
 transform-origin:left;
 transition:transform 1.2s var(--ease) .35s;
}
body.is-ready .ab-rule__line{transform:scaleX(1)}
.ab-rule__globe{
 position:absolute;
 right:clamp(6%,12vw,22%);
 display:grid;
 place-items:center;
 width:clamp(96px,11vw,150px);
 aspect-ratio:1;
 border-radius:50%;
 background:var(--iris);
 color:var(--on-iris);
 transform:scale(0);
 transition:transform 1s var(--ease) .6s;
}
body.is-ready .ab-rule__globe{transform:scale(1)}
.ab-rule__globe svg{
 width:38%;height:38%;
 animation:spin 16s linear infinite;
}

.ab-cue{
 width:22px;height:22px;
 color:var(--fog);
 opacity:0;
 transition:opacity .8s var(--ease-soft) 1s;
}
body.is-ready .ab-cue{opacity:1}

.ab-bio{
 display:flex;
 flex-direction:column;
 gap:1.1rem;
 margin-top:clamp(1.75rem,3.5vw,2.5rem);
 max-width:62ch; /* inside the 65ch readability cap */
 color:var(--fg-muted);
}
.ab-bio p:nth-child(2){transition-delay:.1s}
.ab-caption{
 margin-top:.75rem;
 color:var(--faint);
 font-size:.875rem;
 transition-delay:.2s;
}

/* ── languages ──────────────────────────── */
.ab-langs{
 padding:clamp(4rem,9vh,7rem) var(--pad);
 border-top:1px solid var(--line);
}
.ab-sec-head{
 display:flex;
 flex-wrap:wrap;
 align-items:baseline;
 justify-content:space-between;
 gap:1.5rem;
 padding-bottom:clamp(1.75rem,3.5vw,2.75rem);
}
.ab-sec-note{color:var(--fog);max-width:44ch;font-size:.9375rem}

.ab-table{
 width:100%;
 border-collapse:collapse;
 text-align:left;
}
.ab-table thead th{
 padding:0 0 1rem;
 border-bottom:1px solid var(--line);
 font-weight:var(--w);
 font-size:.6875rem;
 letter-spacing:.14em;
 text-transform:uppercase;
 color:var(--fog);
}
.ab-table tbody th,
.ab-table tbody td{
 padding:clamp(1.1rem,2.2vw,1.6rem) 1rem clamp(1.1rem,2.2vw,1.6rem) 0;
 border-bottom:1px solid var(--line);
 vertical-align:baseline;
 font-weight:var(--w);
}
.ab-table tbody tr{transition:background-color .4s var(--ease-soft)}
.ab-table tbody tr:hover{background:var(--tint)}
.ab-table th[scope="row"]{white-space:nowrap}
.ab-lang__cjk{
 font-family:var(--cjk);
 font-size:1.2rem;
 letter-spacing:.06em;
 margin-right:.5rem;
}
.ab-lang__en{color:var(--fog);font-size:.875rem}
.ab-table td:last-child{color:var(--fog);font-size:.9375rem;padding-right:0}

/* Badge, not tag: these are attached facts about the row, not filters
 the reader can select or dismiss. */
.badge{
 display:inline-block;
 padding:.3rem .7rem;
 border-radius:999px;
 font-size:.75rem;
 letter-spacing:.04em;
 color:var(--fog);
 box-shadow:inset 0 0 0 1px var(--ring);
}
.badge--strong{
 background:var(--iris);
 color:var(--on-iris);
 box-shadow:none;
}

/* ── experience ─────────────────────────── */
.ab-exp{
 padding:clamp(4rem,9vh,7rem) var(--pad);
 border-top:1px solid var(--line);
}
.ab-exp .eyebrow{padding-bottom:clamp(1.5rem,3vw,2.5rem)}
.ab-exp__list{border-top:1px solid var(--line)}
.ab-exp__row{
 display:grid;
 grid-template-columns:minmax(0,1fr) minmax(0,1.15fr) auto;
 gap:clamp(1rem,3vw,3rem);
 align-items:baseline;
 padding:clamp(1.6rem,3.4vw,2.5rem) 0;
 border-bottom:1px solid var(--line);
}
.ab-exp__role h2{
 font-size:clamp(1.1rem,1.8vw,1.5rem);
 font-weight:var(--w);
 line-height:1.25;
 letter-spacing:var(--track);
}
.ab-exp__org{margin-top:.4rem;color:var(--fog);font-size:.875rem}
.ab-exp__desc{color:var(--fog);font-size:.9375rem;line-height:1.55}
/* Years line up only with tabular figures — proportional digits stagger
 the column and it reads as sloppy. */
.ab-exp__year{
 font-variant-numeric:tabular-nums;
 font-size:.9375rem;
 color:var(--fog);
 text-align:right;
}

/* ── what I bring ───────────────────────── */
.ab-bring{
 padding:clamp(4rem,9vh,7rem) var(--pad) clamp(5rem,11vh,8rem);
 border-top:1px solid var(--line);
}
.ab-bring .eyebrow{padding-bottom:clamp(1.75rem,3.5vw,2.75rem)}

/* Asymmetric: spans rotate 5/4/3 then 3/5/4 across a 12-column grid, so
 the eye never settles into an even beat. A symmetric 3-up reads static. */
.ab-bring__grid{
 display:grid;
 grid-template-columns:repeat(12,1fr);
 gap:clamp(1.75rem,3.5vw,3rem) clamp(1.5rem,3vw,2.75rem);
}
.ab-bring__cell{
 grid-column:span var(--span);
 padding-top:1.25rem;
 border-top:1px solid var(--line);
}
.ab-bring__num{
 display:block;
 margin-bottom:.9rem;
 font-variant-numeric:tabular-nums;
 font-size:.75rem;
 letter-spacing:.08em;
 color:var(--faint);
}
.ab-bring__cell h2{
 font-size:clamp(1.15rem,1.9vw,1.6rem);
 font-weight:var(--w);
 line-height:1.2;
 letter-spacing:var(--track);
 margin-bottom:.7rem;
}
.ab-bring__cell p{color:var(--fog);font-size:.9375rem;line-height:1.55}

.ab-industries{padding-top:clamp(3rem,6vw,4.5rem)}
.ab-tags{display:flex;flex-wrap:wrap;gap:.6rem;margin-top:1rem}
/* Tag, not badge: standalone chips describing a set, each free-standing. */
.tag{
 display:inline-block;
 padding:.55rem 1.1rem;
 border-radius:999px;
 font-size:.8125rem;
 color:var(--fog);
 box-shadow:inset 0 0 0 1px var(--ring);
}

/* ═══════════════════════════════════════════
 WORK DETAIL PAGES
 ═══════════════════════════════════════════ */

.wk-head{
 padding:clamp(9rem,20vh,13rem) var(--pad) clamp(2.5rem,5vw,4rem);
}
.wk-index{
 font-variant-numeric:tabular-nums;
 font-size:.8125rem;
 letter-spacing:.08em;
 color:var(--fog);
 margin-bottom:clamp(1.25rem,2.5vw,2rem);
}
.wk-index__of{color:var(--faint)}

.wk-title{
 font-size:clamp(2.8rem,9vw,8rem);
 font-weight:var(--w);
 line-height:1.02;
 letter-spacing:var(--track);
}
.wk-title .mask>span{transition:transform 1.15s var(--ease)}
body.is-ready .wk-title .mask>span{transform:translateY(0)}
body.is-ready .wk-title .mask:nth-child(2)>span{transition-delay:.08s}

.wk-meta{
 display:flex;
 flex-wrap:wrap;
 gap:clamp(1.75rem,5vw,4.5rem);
 margin-top:clamp(2.5rem,5vw,4rem);
 padding-top:clamp(1.5rem,3vw,2.25rem);
 border-top:1px solid var(--line);
}
.wk-meta__item dd{margin-top:.55rem;font-size:.9375rem}
.wk-meta__year{font-variant-numeric:tabular-nums}

/* A colour band instead of a fake screenshot — the work is process, not
 an interface, so inventing UI imagery would misrepresent it. */
.wk-band{
 height:clamp(160px,26vh,320px);
 margin:0 var(--pad);
 border-radius:10px;
 background:linear-gradient(140deg,var(--a),var(--b));
 clip-path:inset(100% 0 0 0);
 transition:clip-path 1.2s var(--ease) .3s;
}
body.is-ready .wk-band{clip-path:inset(0 0 0 0)}

.wk-body{
 display:grid;
 grid-template-columns:1.15fr 1fr;
 gap:clamp(2.5rem,7vw,7rem);
 padding:clamp(4rem,10vh,8rem) var(--pad);
}
.wk-lede{
 margin-top:1.25rem;
 font-size:clamp(1.35rem,2.4vw,2.1rem);
 line-height:1.35;
 letter-spacing:var(--track);
 max-width:22ch;
}
.wk-scope__list{margin-top:1.25rem;border-top:1px solid var(--line)}
.wk-scope__list li{
 display:flex;
 gap:clamp(1rem,2.5vw,2rem);
 padding:1.1rem 0;
 border-bottom:1px solid var(--line);
 color:var(--fog);
 font-size:.9375rem;
 line-height:1.55;
}
.wk-scope__num{
 font-variant-numeric:tabular-nums;
 color:var(--faint);
 font-size:.75rem;
 padding-top:.3em;
 flex-shrink:0;
}

/* ── next project ───────────────────────── */
.wk-next{border-top:1px solid var(--line)}
.wk-next a{
 display:flex;
 align-items:center;
 justify-content:space-between;
 gap:1.5rem;
 flex-wrap:wrap;
 padding:clamp(3rem,7vw,5.5rem) var(--pad);
 position:relative;
 overflow:hidden;
 isolation:isolate;
 transition:padding-left .6s var(--ease);
}
.wk-next a::before{
 content:"";
 position:absolute;inset:0;
 z-index:-1;
 background:var(--iris);
 transform:translateY(101%);
 transition:transform .6s var(--ease);
}
.wk-next a:hover::before{transform:translateY(0)}
.wk-next a:hover{padding-left:calc(var(--pad) + clamp(.5rem,2vw,1.5rem))}
.wk-next__title{
 font-size:clamp(1.8rem,5vw,4rem);
 line-height:1.05;
 letter-spacing:var(--track);
 flex:1;
 min-width:0;
}
.wk-next .eyebrow{
 width:100%;
 transition:color .5s var(--ease);
}
.wk-next a:hover .eyebrow{color:rgba(255,255,255,.72)}
.wk-next a:hover{color:var(--on-iris)}
.wk-next__arrow{
 width:clamp(28px,3vw,40px);
 flex-shrink:0;
 transition:transform .55s var(--ease);
}
.wk-next__arrow svg{width:100%;height:100%}
.wk-next a:hover .wk-next__arrow{transform:translate(6px,-6px)}

/* ═══ responsive ═══════════════════════════ */
@media (max-width:1024px){
 .wk-body{grid-template-columns:1fr;gap:2.5rem}
 .wk-lede{max-width:28ch}
 .ab-bring__cell{grid-column:span 6}
 .ab-exp__row{grid-template-columns:minmax(0,1fr) auto}
 .ab-exp__desc{grid-column:1/-1;order:3}

 .intro__grid{grid-template-columns:1fr;gap:3rem}
 .about{grid-template-columns:1fr;gap:2.5rem}
 .about__media{max-width:360px}
 .contact__float{position:static;margin-top:2.5rem;display:inline-grid}
}

@media (max-width:760px){
 .head__nav{display:none}
 .burger{display:block}
 .menu__inner{grid-template-columns:1fr;gap:2.5rem}

 .ab-head{padding-top:7rem}
 .ab-rule{height:80px}
 .ab-rule__globe{width:80px;right:0}
 .ab-bring__cell{grid-column:span 12}
 .ab-sec-head{flex-direction:column;gap:.75rem}
 /* the table collapses to stacked rows — three columns at 375px would
 force each cell to two or three words per line */
 .ab-table thead{display:none}
 .ab-table tbody tr{display:block;padding:1.25rem 0;border-bottom:1px solid var(--line)}
 .ab-table tbody th,
 .ab-table tbody td{display:block;padding:0;border:0}
 .ab-table tbody th{margin-bottom:.6rem}
 .ab-table tbody td:first-of-type{margin-bottom:.6rem}

 .hero__photo-main{
 height:88%;
 width:auto;
 max-width:150%; /* let her crop past the edges rather than shrink */
 }
 .hero__pill{top:auto;bottom:32%}
 .hero__role{
 top:auto;bottom:20%;
 right:var(--pad);
 text-align:right;
 align-items:flex-end;
 }
 .hero__name-line{font-size:clamp(3.4rem,17.5vw,7rem)}

 .work__meta{display:none}
 .work__list:hover .work__item>a{opacity:1}
 .foot__grid{flex-wrap:wrap;gap:2rem}
 .foot__cell--right{margin-left:0;width:100%}
 .foot__socials{flex-wrap:wrap}
}

/* ═══ reduced motion ═══════════════════════ */
@media (prefers-reduced-motion:reduce){
 *,*::before,*::after{
 animation-duration:.01ms!important;
 animation-iteration-count:1!important;
 transition-duration:.01ms!important;
 }
 .mask>span,.word>span,.hero__name-line{transform:none!important}
 .reveal-block{opacity:1!important;transform:none!important}
 .about__media-mask{clip-path:none!important}
 .hero__photo img,.about__media img{transform:none!important}
}
