/* =========================================================
   ALT 2 — variation on styles-alt.css
   - project nav lives inside the topbar row (right side)
   - portrait sits beside the intro copy, not in the topbar
   Loaded after styles.css and styles-alt.css. Overrides only.
   ========================================================= */

/* ---- neutral: same lightness, hue swung from violet to blue ----
   The old ramp sat at ~252° which read as lilac. These are the same HSL
   lightness and saturation rotated to ~213°, so the page stays exactly as
   light and only the temperature changes. --accent stays violet: the
   headings are meant to be the warm note against a cool ground. */
.alt2{
  --bg:#f3f5f8;
  --plate:#ffffff;
  --ink:#1f242b;
  --ink-soft:#666e78;
  --line:#dfe4eb;
  --chip:#e7ecf2;
}
body.alt2{background:var(--bg)}
.alt2 .nav:hover{border-color:#c2bdd2}
/* The viewer arrows carry the same geometric chevron as the lightbox (see
   render.js) instead of ‹ ›. font-size:0 kills the inherited line box, which
   otherwise adds descender space below the svg and pushes the mark up. Left at
   full opacity: these are dark-on-white, where .8 reads as washed out, unlike
   the lightbox pair sitting on a photo. */
.alt2 .nav{font-size:0;line-height:0;padding:0}
.alt2 .nav .lb-chev{width:12px;height:12px;display:block}

/* ---- shared display step ----
   The lede and the section headings sit at the same level of the ramp, so
   they use one size and one weight. Previously the lede was 24.9px/500 while
   h2 was 28px/700 — smaller AND lighter than the headings it introduces — and
   the two grew at different rates (2.9vw vs 3.2vw), so the mismatch changed
   with viewport width instead of resolving. One coefficient keeps the
   relationship constant; role is carried by position and the date eyebrow,
   not by size. */
.alt2 .header-intro-inner p:first-of-type,
.alt2 .section-head h2{
  font-size:clamp(26px,3.2vw,36px);
  font-weight:600;
  letter-spacing:-.018em;
  color:var(--accent);
}
.alt2 .header-intro-inner p:first-of-type{line-height:1.18}
.alt2 .section-head h2{line-height:1.08}
/* The date already uses --ink-soft, the same colour as the Projects chip label,
   but at weight 200 with .13em tracking it lays down so little ink that it read
   noticeably lighter. Matching the chip's 400 evens out the optical weight —
   the fix is stroke, not colour. */
.alt2 .section-head p.section-date{font-weight:400}

/* ---- sticky: only the topbar pins, not the whole header ----
   styles.css sticks .site-header, which in alt2 also contains the
   portrait + bio. Release the header and pin just the topbar row. */
.alt2 .site-header{position:static}
.alt2 .topbar{position:sticky;top:0;z-index:50}

/* ---- topbar: name left, project nav right, one row ---- */
/* margin:0 rather than the base layer's `0 auto`, for the same reason
   .intro-row and .section drop their centring: a centred box pushes its left
   edge in by (100vw - 1180) / 2 on wide screens, so the name drifted right of
   the 134px content column while the content stayed pinned. Left-aligning keeps
   the 1180px cap, which is what puts the nav's right rail on 1156 — the same
   line the section headings end on. Both rails now match at every width. */
.alt2 .topbar-inner{
  max-width:var(--maxw);margin:0;
  display:flex;align-items:center;justify-content:space-between;
  gap:28px;padding:9px 24px;
}
.alt2 .topbar-text{margin-left:0;flex:0 0 auto;gap:1px}
.alt2 .topbar-brand{font-size:25px;line-height:1.05}
.alt2 .topbar-tagline{font-size:10.5px;letter-spacing:.22em}

/* name + portrait; portrait breaks out past the bar's bottom edge */
.alt2 .topbar-inner{position:relative}
/* the name block tracks the portrait: as it shrinks, the gutter closes so the
   gap between portrait and name stays deliberate instead of opening up.
   x runs 134px (the content column) → 96px (= 24 margin + 52 portrait + 20). */
.alt2 .topbar-id{
  display:flex;align-items:center;flex:0 0 auto;
  padding-left:calc(110px - 38px * var(--shrink));
}
.alt2 .topbar-avatar{
  position:absolute;left:24px;top:8px;bottom:auto;z-index:2;
  width:76px;height:76px;border-radius:50%;object-fit:cover;
  border:3px solid var(--plate);
  box-shadow:0 2px 9px rgba(20,26,40,.10);
}

/* ---- scrolled state ----
   .topbar carries --shrink (0→1) set from scroll position, so the portrait
   tracks the scroll continuously rather than snapping at a threshold.

   Anchored from the top, not the bottom, so the grid-aligned number is the
   one being controlled: top runs 8px → 4px, both on the 4px grid. x stays at
   the page's 24px margin (same edge as the footer icons) — it used to drift
   right to centre in the gutter, but once the name slides left that just
   crushed the gap between them. Size 76px → 52px, leaving the portrait's
   right edge at 76px and a 20px gap to the name at 96px.
   In the 61px bar that leaves 5px below vs 4px above — a half-pixel off
   dead-centre, traded for staying on the grid.

   No transition here: the value is already driven by scroll, and easing it
   would just add lag.

   The shadow is set against the artwork plate rather than judged on its own.
   The plate is 720px wide under a 34px blur at 8% — a blur of 5% of its width.
   The portrait was carrying 14px under 76px at 16%: nearly four times the
   relative blur at twice the alpha, so the small element was lifted harder off
   the page than the large one, which is backwards. 9px is 12%: still more than
   the plate, because a small shape needs proportionally more blur before a
   shadow    reads at all, but no longer the heaviest thing in the bar.

   Only box-shadow carries a var() fallback. On phones render.js moves the
   portrait out of .topbar and into the intro copy, so --shrink has nothing to
   resolve against down there — and an unresolvable var() invalidates the whole
   declaration, which is why the mobile portrait used to draw no shadow at all.
   The four dimensions above want exactly that behaviour: they are meant to
   fall through to the phone block's own width, height and border. Only the
   shadow is the same on both. */
.alt2 .topbar{--shrink:0}
.alt2 .topbar-avatar{
  width:calc(76px - 24px * var(--shrink));
  height:calc(76px - 24px * var(--shrink));
  top:calc(8px - 4px * var(--shrink));
  border-width:calc(3px - 1px * var(--shrink));
  box-shadow:0 calc(2px - 1px * var(--shrink,0)) calc(9px - 5px * var(--shrink,0))
             rgba(20,26,40,.10);
}
/* One edge at a time: the static hairline fades out exactly as the shadow
   fades in. Stacking a 1px border on top of a 1px shadow read as a 2px grey
   band — subtle in isolation, dull together. Shadow is tinted toward the
   palette's 258° hue rather than neutral grey so it reads as depth, not dirt. */
.alt2 .topbar{
  border-bottom-color:rgba(225,223,235,calc(1 - var(--shrink)));
  box-shadow:0 1px 2px rgba(70,60,110,calc(.06 * var(--shrink)));
}

/* nav is a peer of the name now, not a full-width band */
.alt2 .page-nav{
  background:transparent;border:none;
  flex:0 1 auto;min-width:0;
}
.alt2 .page-nav-inner{
  max-width:none;margin:0;padding:0;
  display:flex;flex-wrap:wrap;justify-content:flex-end;gap:2px;
  background:transparent;
}
.alt2 .page-nav a{
  padding:6px 11px;border-radius:999px;
  font-size:14px;color:var(--ink-soft);white-space:nowrap;
}
.alt2 .page-nav a:hover{color:var(--ink);background:var(--chip)}
.alt2 .page-nav a.active{color:var(--accent);background:var(--chip)}

/* "More work…" overflow group */
.alt2 .nav-more{position:relative}
.alt2 .nav-more-toggle{
  appearance:none;border:0;background:transparent;cursor:pointer;
  font-family:'mr-eaves-sans','Poppins',sans-serif;
  /* The chip sat a pixel low. Padding alone can't fix it: the button is an
     inline-block aligned on its baseline, so adding padding at the top pushes
     the glyph down and drags the box up by half as much, and box and text
     never land together. Measured against a sibling link, this pairing is the
     one that matches both - 5/8 puts the glyph on 20.5 and the relative nudge
     puts the box back on 15.5. */
  padding:5px 11px 8px;border-radius:999px;
  position:relative;top:-1px;
  font-size:14px;letter-spacing:.3px;color:var(--ink-soft);white-space:nowrap;
}
.alt2 .nav-more-toggle:hover{color:var(--ink);background:var(--chip)}
.alt2 .nav-more.open .nav-more-toggle{color:var(--ink);background:var(--chip)}
.alt2 .nav-more:has(a.active) .nav-more-toggle{color:var(--accent);background:var(--chip)}
.alt2 .nav-more-menu{
  display:none;position:absolute;top:calc(100% + 6px);right:0;z-index:60;
  min-width:170px;padding:6px;
  background:var(--plate);border:1px solid var(--line);border-radius:12px;
  box-shadow:0 8px 28px rgba(20,26,40,.14);
}
.alt2 .nav-more.open .nav-more-menu{display:block}
.alt2 .nav-more-menu a{display:block;border-radius:8px}

/* ---- intro row ---- */
/* Mirrors .section exactly: same max-width, same margin:0 (NOT auto — .section
   is left-aligned, so centring this drifted them apart above 1180px), and the
   same 134px left gutter. That keeps the intro copy on the identical axis as
   every section heading and writeup below it, at every width. */
.alt2 .intro-row{
  max-width:var(--maxw);margin:0;
  padding:0 24px 0 134px;
}
.alt2 .header-intro{background:transparent}
.alt2 .header-intro-inner{padding:40px 0;max-width:none}
/* One measure for both intro paragraphs. They were on separate ch counts (48ch
   of the 26px display face vs 70ch of the 18px body face), so their right edges
   landed ~100px apart and moved independently as the viewport changed. A shared
   value in absolute units means they line up at every width regardless of which
   typeface has loaded. The clamp mirrors the body paragraph's own scaling:
   70ch of mr-eaves-modern is ~38.6em, against font-size clamp(18px,1.25vw,21px). */
.alt2 .header-intro-inner{--intro-measure:clamp(696px,48.3vw,811px)}
.alt2 .header-intro-inner p:first-of-type{max-width:var(--intro-measure);text-wrap:balance}
.alt2 .header-intro-inner p + p{
  margin-top:18px;
  /* same declaration as .section-write p so intro and body prose stay locked
     to one size at every viewport width */
  font-size:clamp(18px,1.25vw,21px);line-height:1.5;max-width:var(--intro-measure);
  text-wrap:pretty;
}

/* No narrower measure for the lede here any more: it shares --intro-measure
   with the paragraph below it, and below ~900px the column is already narrower
   than that measure, so both run to the same edge on their own. */

/* ---- media: the artwork fills the plate ----
   .media is a fixed 16/9 plate and the base layer already stretches img.free
   to fill it with object-fit:contain. Contain letterboxes whichever axis
   isn't constraining, so near-16/9 screenshots sat inside thin slivers of
   plate. Switch the default to cover: the image fills the box and the small
   difference is trimmed instead of shown.

   Cover only stays honest while the source is close to 16/9. Anything far
   from it loses real content — 27% off the sides of a web page, 60% off a
   wordmark — so those keep contain and letterbox until they're recropped. */
.alt2 .viewer .media > img.free{object-fit:cover}

/* A cover crop would cost these 16-60%. Percentages are of the long axis. */
.alt2 .viewer .media > img.free[src*="ms-image-library"],      /* 1.46 - 18% */
.alt2 .viewer .media > img.free[src*="upside-slide16-01-ooh"], /* 1.50 - 16% */
.alt2 .viewer .media > img.free[src*="asana-41"],              /* 1.07 - 39% */
.alt2 .viewer .media > img.free[src*="lens-goooogle"],         /* 4.40 - 60% */
.alt2 .viewer .media > img.free[src*="rebrand-brand-system"],         /* 2.13 - 17% */
.alt2 .viewer .media > img.free[src*="no-shadow-"]{             /* the six Def */
  object-fit:contain;                                          /* Jam artist  */
}                                                              /* sites, 1.33 */

/* Re-exported at 1024x768 with real alpha and no baked-in shadow, which is
   what the old translateY nudge was compensating for. drop-shadow follows the
   painted pixels, so it traces the browser-window silhouette rather than the
   plate. Inset slightly because contain is height-limited at 1.33 — at full
   height the shadow would be clipped by the plate's overflow. */
.alt2 .viewer .media > img.free[src*="no-shadow-"]{
  transform:scale(.68);
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.03)) drop-shadow(0 4px 12px rgba(0,0,0,.04));
}

/* ms-copilot is a full app window, not artwork — at 690px the ribbon and the
   Copilot pane are already at the edge of legibility, and bleeding it to the
   plate edge made it read as the page rather than as a screenshot of one.
   Contain first so nothing is trimmed, then inset it inside the plate. */
/* Two app-window captures, both 1.865. Bleeding them to the plate edge made
   them read as the page itself rather than as a screenshot of one, so they're
   contained, pulled in slightly, and lifted off the plate. */
.alt2 .viewer .media > img.free[src*="ms-copilot"],
.alt2 .viewer .media > img.free[src*="ms-create"]{
  object-fit:contain;
  transform:scale(.95);
  /* drop-shadow, not box-shadow: the element box is the whole plate, so
     box-shadow would outline the plate. A filter follows the painted pixels,
     which is the app window itself. Kept faint — the plate has its own. */
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.03)) drop-shadow(0 4px 12px rgba(0,0,0,.04));
}

/* ms-library is 5488x3802 (1.44) — cover was trimming 19% off the top and
   bottom, which took the "Library" title with it. Contain to get the whole
   frame back, then scale from the bottom edge so the cut-off grid row stays
   flush with the plate and reads as content continuing below. */
/* Height of the plate, exactly: contain is height-limited here (1.44 < 1.778),
   so no transform at all is the natural full-height fit. Nothing is cropped;
   the #fafafa bars either side are the plate showing through, matched to the
   image canvas so the seam is invisible. */
.alt2 .viewer .media > img.free[src*="ms-library"]{
  object-fit:contain;
}

/* The bug-list screenshot is 1.96 and mostly small type — filling the plate
   made a wall of unreadable rows. Pulled well in so it reads as an artifact
   you glance at rather than something you're meant to parse.

   This one drops the base absolute-fill model. A border and a radius are drawn
   on the ELEMENT box, and under inset:0/width:100% that box is the whole
   plate — the stroke would frame the plate, not the screenshot. Sizing the
   element to the image instead makes the two boxes the same, so the stroke
   lands on the artwork. box-sizing:border-box means the 1px eats inward, which
   is the inner stroke; border-radius clips the image content to match. */
.alt2 .viewer .media > img.free[src*="ms-quality-team-v2"],
.alt2 .viewer .media > img.free[src*="ms-spreadsheet"],
.alt2 .viewer .media > img.free[src*="upside-slide06-01-funnel"]{
  object-fit:contain;
  inset:auto; left:50%; top:50%;
  width:auto; height:auto;
  transform:translate(-50%,-50%);
  border:1px solid var(--line);
  border-radius:6px;
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.03)) drop-shadow(0 4px 12px rgba(0,0,0,.04));
}
/* True hairline. 1px CSS is 2 device pixels on a retina panel, which is why it
   reads heavy; .5px gets back to a single device pixel. Guarded by the media
   query because a sub-pixel border can round away to nothing at 1dppx. */
@media (min-resolution:2dppx){
  .alt2 .viewer .media > img.free[src*="ms-quality-team-v2"],
  .alt2 .viewer .media > img.free[src*="ms-spreadsheet"],
  .alt2 .viewer .media > img.free[src*="upside-slide06-01-funnel"]{
    border-width:.5px;
  }
}
/* Sizes differ because the source ratios do; the treatment above is shared. */
.alt2 .viewer .media > img.free[src*="ms-quality-team-v2"]{max-width:72%;max-height:72%}
.alt2 .viewer .media > img.free[src*="ms-spreadsheet"]{max-width:68%;max-height:68%}
.alt2 .viewer .media > img.free[src*="upside-slide06-01-funnel"]{max-width:88%;max-height:88%}

/* The design.google GIF is 2.13 — much wider than the plate, so contain
   already leaves bands top and bottom. Pulling it in slightly makes those
   bands read as intentional margin rather than as letterboxing. */
.alt2 .viewer .media > img.free[src*="rebrand-brand-system"]{
  transform:scale(.9);
}

/* Three phone frames in a row, 1.44 — cover was cutting the tops and bottoms
   off the handsets. Contain keeps all three whole and equally sized so the
   row still reads as a grid, then pull it in so they sit as objects on the
   plate rather than pressing against it. */
.alt2 .viewer .media > img.free[src*="ms-mobile"]{
  object-fit:contain;
  transform:scale(.95);
}

/* The screenshot's own canvas is #fafafa, so against a #fff plate it read as
   a grey rectangle floating on white. Matching the plate to it makes the
   inset disappear and the slide read as one surface. */
.alt2 .viewer .media:has(> img.free[src*="ms-library"]){background:#fafafa}
/* Contained, so the letterbox bars show; black lets the night OOH shot sit on
   its own ground instead of floating on the page white. */
.alt2 .viewer .media:has(> img.free[src*="upside-slide16-01-ooh"]){background:#000}
/* Both rebrand motion studies are rendered on their own light grey ground;
   matching the plate to it hides the seam where the video ends. */
.alt2 .viewer .media:has(> video[src*="rebrand-g-"]){background:#f6f6f6}

/* ---- footer: date left, icons right, tighter ---- */
/* The band was top-heavy: 56 margin + 40 padding + 8 inner put 104px above the
   content against 62px below, which read as the footer sagging away from the
   page. Trimming the margin lifts the whole band, and matching the band's own
   padding top-to-bottom stops it from tipping the other way. */
.alt2 .site-footer{margin-top:40px;padding:24px 24px 32px}
/* top padding is small because the 34px icon buttons already carry ~8px of
   internal space above the 17px glyph — the optical gap is padding + that. */
/* max-width:none + margin:0 instead of the base layer's centred 1180px box:
   centring made the left edge jump — 134px on wide screens (where it happened
   to meet the section rail) but 48px as soon as the box stopped being capped.
   The footer sits flush to the page edge at every width instead. */
/* horizontal padding is 0 because .site-footer already carries 24px of its
   own — keeping both stacked them into a 48px gutter. */
.alt2 .site-footer-inner{max-width:none;margin:0;padding:8px 0 22px}
.alt2 .footer-meta{order:-1}
/* the -8px optical offset follows the icons to the right edge */
.alt2 .footer-links{margin-left:0;margin-right:-8px}

/* ---- sections share the header's left edge ---- */
.alt2 .section{padding-left:134px}

/* ---- scroll offset ----
   styles.css already sets scroll-margin-top:var(--headh), and render.js now
   measures the element that actually pins in alt2 (.topbar, since .site-header
   is released above) and subtracts the section's own top padding. The old
   hardcoded calc(78px - 9vh) guessed at that number and drifted whenever the
   bar's real height changed — which it just did on mobile, 115px to 59px. */
/* The plate used to inherit its width from the 1180px column, so it topped out
   at 936px and every slide filled whatever space was left over. 800px is a
   deliberate ceiling instead of a leftover: it keeps the artwork nearer the
   caption rail and stops wide sources dominating the scroll on large displays.
   margin:0 (not auto) holds it against the gutter rather than centring it in
   the column, so the left edge stays aligned with the copy above.
   The base layer insets .item by 2px, which left the plate 2px inside the
   viewer while the arrows stayed pinned to the viewer edge - so the plate,
   the caption and the body copy above all sat on slightly different left
   edges. Dropping that padding puts every one of them on the same line. */
.alt2 .viewer{margin:0;max-width:720px}

/* ---- carousel ----
   The slides used to be stacked absolutely and crossfaded. They sit in a row
   now, which is what makes a swipe possible at all: dragging has to move
   something continuously, and there is nothing to drag on a crossfade.

   --slide-gap is the blank between slides while the track is in motion. It has
   to be at least twice the window's side padding, or the neighbouring slide
   shows in that padding while the carousel is at rest. It is a flex `gap`
   rather than Embla's documented negative-margin trick: that trick offsets the
   whole row by one gap, which pulled the plate off the left edge it shares
   with the copy above. Embla derives its step from the slides' own positions,
   so a real gap measures the same.

   The padding/negative-margin pair is what keeps the plate's drop shadow: the
   window clips at its padding edge, so the padding is the room the shadow
   needs, and the matching negative margin puts the box back exactly where it
   was — the plate's left edge stays on the same line as the copy above it. */
.alt2 .viewer{--slide-gap:48px}
.alt2 .viewer-window{
  overflow:hidden;
  padding:10px 24px 46px;margin:-10px -24px -46px;
}
.alt2 .viewer-track{
  display:flex;align-items:flex-start;gap:var(--slide-gap);
  /* vertical scrolling still belongs to the page — only the horizontal axis is
     the carousel's to claim */
  touch-action:pan-y;
}
.alt2 .viewer-track .item{
  position:relative;flex:0 0 100%;min-width:0;
  padding:0;pointer-events:auto;
}
/* the base layer hides everything but .active for the crossfade; here every
   slide has to be painted, because the ones on either side come into view
   under a finger long before they are selected */
.alt2 .viewer-track .item .media,
.alt2 .viewer-track .item figcaption{opacity:1;transition:none}
/* the 48px gutters were there to clear the arrows, which now sit in their own
   row below rather than on the caption's line */
.alt2 .viewer figcaption{padding:0 8px}

/* fallback when the carousel script is unavailable: a native snap strip, which
   is swipeable on its own — the controls just drive scrollLeft instead */
.alt2 .viewer.no-embla .viewer-window{overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none}
.alt2 .viewer.no-embla .viewer-window::-webkit-scrollbar{display:none}
.alt2 .viewer.no-embla .viewer-track .item{scroll-snap-align:start}

/* ---- controls: prev · dots · next, one centred cluster under the slide ----
   Same arrangement as .lightbox-bar, which already centres prev/caption/next
   as a group. The arrows started out on the viewer's outer rails, because that
   is where they sat when they flanked the caption — but ~700px apart is a long
   trip for a control whose whole job is to step one slide, and it read as
   decoration rather than something to click. Pulled in they sit where the eye
   already is: on the caption's centre line. */
.alt2 .viewer-bar{
  display:flex;align-items:center;justify-content:center;
  /* 9, not the lightbox's 14: the end dots carry 5px of their own padding, so
     this is the value that draws the same 14px gap the lightbox does */
  gap:9px;margin-top:16px;
}
/* the arrows were absolutely positioned against .viewer (bottom:4px, pinned to
   its left and right edges) so they could sit either side of the caption */
.alt2 .viewer-bar .nav{position:static;inset:auto;transform:none;flex:0 0 auto}
.alt2 .viewer-dots{
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:2px;flex:0 1 auto;min-width:0;
}
/* the mark is 7px; the padding is what makes the target big enough to hit with
   a thumb without drawing anything larger */
.alt2 .viewer-dot{
  appearance:none;border:0;background:none;cursor:pointer;
  padding:11px 5px;line-height:0;position:relative;
}
/* same trick as .page-nav-toggle: an invisible ::after takes the target to the
   44px minimum without drawing anything bigger than the 7px mark */
.alt2 .viewer-dot::after{
  content:'';position:absolute;left:0;right:0;top:50%;
  height:44px;transform:translateY(-50%);
}
.alt2 .viewer-dot::before{
  content:'';display:block;width:7px;height:7px;border-radius:999px;
  background:var(--line);transition:background .2s,transform .2s;
}
.alt2 .viewer-dot:hover::before{background:#c2c9d3}
.alt2 .viewer-dot.on::before{background:var(--accent);transform:scale(1.15)}
@media (prefers-reduced-motion:reduce){
  .alt2 .viewer-dot::before{transition:none}
}

@media (max-width:720px){
  /* Name left, "Projects" chip right — the same one-row relationship as
     desktop, just with the link row collapsed behind a chip. Stacking it put
     the chip on its own 43px band and pushed the bar to 115px tall. */
  .alt2 .topbar-inner{flex-direction:row;align-items:center;gap:12px}
  /* static so the dropdown below resolves against .topbar-inner (position:
     relative) and spans the full bar, not the width of the chip */
  .alt2 .page-nav{width:auto;position:static;flex:0 0 auto;margin-left:auto}
  /* styles.css collapses .page-nav-inner here, but the alt2 rule that makes it
     a flex row is a class deeper, so it won and re-opened the list permanently:
     the full project list rendered under the toggle and overlapped the intro.
     Restate the collapse at alt2's specificity. */
  .alt2 .page-nav-inner{display:none}
  .alt2 .page-nav.open .page-nav-inner{
    display:flex;
    position:absolute;top:calc(100% + 6px);left:20px;right:20px;z-index:60;
    flex-direction:column;justify-content:flex-start;gap:2px;
    background:var(--plate);
    border:1px solid var(--line);border-radius:12px;padding:6px;
    box-shadow:0 8px 28px rgba(20,26,40,.14);
  }
  /* full-width rows: the 14px/6px desktop pill is a ~28px target, under the
     44px minimum, and a centred pill reads as a button not a list row */
  .alt2 .page-nav a{display:block;padding:12px 14px;font-size:16px;border-radius:8px}
  /* the toggle IS the desktop pill — same padding, radius, size and colour */
  .alt2 .page-nav-toggle{
    width:auto;padding:6px 11px;border-radius:999px;
    font-size:14px;letter-spacing:.3px;color:var(--ink-soft);
    background:var(--chip);white-space:nowrap;
  }
  .alt2 .page-nav.open .page-nav-toggle{color:var(--ink)}
  /* caret: was a rotated square built from two borders, which is why it never
     sat right — it has no optical centre, its 45° edges antialias unevenly, and
     the rotate/translate order made it drift as it flipped. This is a real
     stroked chevron with round caps, the same icon language as the footer
     glyphs, and it flips by rotating a symmetric shape 180° — no nudging. */
  .alt2 .page-nav-toggle{gap:5px}
  .alt2 .page-nav-toggle::after{content:none}
  .alt2 .page-nav-caret{
    width:11px;height:11px;display:block;flex:0 0 auto;
    /* stroke is currentColor, so it already matches the label — but a 2.25
       stroke at 13px reads heavier than 14px text and looked darker than it
       measured. Smaller and thinner evens out the optical weight. */
    transition:transform .2s ease;
  }
  .alt2 .page-nav-caret path{stroke-width:2}
  .alt2 .page-nav.open .page-nav-caret{transform:rotate(180deg)}
  /* the chip stays 26px to match desktop; an invisible ::before grows the
     touch target to 44px without changing what's drawn */
  .alt2 .page-nav-toggle{position:relative}
  .alt2 .page-nav-toggle::before{
    content:'';position:absolute;left:0;right:0;top:50%;
    height:44px;transform:translateY(-50%);
  }
  .alt2 .section{padding-left:20px}
  /* The portrait moves OUT of the bar on phones (render.js reparents the same
     node into .header-intro-inner) so the bar is just name + chip. It's
     absolutely positioned against .topbar-inner on desktop, which CSS alone
     can't escape — hence the reparent. */
  .alt2 .topbar-id{padding-left:0}
  .alt2 .intro-row{padding:0 20px}
  /* A wrapping flex row, not a float: a float let the lede's last line slide
     back under the portrait, which broke the left edge of the paragraph. As a
     flex item the lede keeps one straight left edge beside the image. Only the
     lede shares the row — every later paragraph takes a full 100% basis and
     wraps to its own line, so there's no tall empty gutter beside the copy. */
  .alt2 .header-intro-inner{
    padding:28px 0;
    display:flex;flex-wrap:wrap;align-items:flex-start;
  }
  .alt2 .topbar-avatar{
    position:static;float:none;flex:0 0 auto;
    width:56px;height:56px;border-width:2px;
    /* the 4px drops the top edge from the lede's line box onto its cap height —
       half-leading plus the font's ascender-to-cap gap */
    margin:4px 14px 0 0;
  }
  /* Three lines or more (class set in render.js) and the copy is the taller of
     the pair, so the portrait centres against it instead of hanging off the
     first line. The 4px cap-height nudge goes with it — it only makes sense
     when the two are aligned along their top edges. */
  .alt2 .header-intro-inner.lede-tall .topbar-avatar{align-self:center;margin-top:0}
  .alt2 .header-intro-inner > p:first-of-type{flex:1 1 0;min-width:0}
  .alt2 .header-intro-inner > p + p{flex:0 0 100%}
  .alt2 .header-intro-inner p:first-of-type,
  .alt2 .header-intro-inner p + p{max-width:none}
  /* Captions inherit the body's 1.6, which is set for long-form reading. A
     caption is two lines at most, and at 17px that leaves a 27px stride — a
     visible gap between line one and two that reads as two separate remarks
     rather than one sentence. 1.3 closes it to 22px, so the pair holds
     together while the caption still sits clear of the artwork above. */
  .alt2 figcaption{line-height:1.3}
  /* On a phone the swipe is the way you page, so the arrows are redundant
     chrome on a 32px target each; the dots carry position on their own and get
     the full width to centre in. */
  .alt2 .viewer-bar .nav{display:none}
  /* the window's shadow gutters outdent past the 20px section padding, which
     would otherwise give the page a few pixels of sideways scroll */
  .alt2 .viewer-window{padding-left:16px;padding-right:16px;margin-left:-16px;margin-right:-16px}
  .alt2 .viewer{--slide-gap:32px}
}

/* narrow phones: with the portrait out of the bar the row fits easily, so this
   is just margin trimming */
@media (max-width:400px){
  .alt2 .topbar-inner{padding-left:16px;padding-right:16px;gap:8px}
  .alt2 .topbar-brand{font-size:22px}
  .alt2 .topbar-avatar{width:48px;height:48px}
  .alt2 .page-nav.open .page-nav-inner{left:16px;right:16px}
}

/* ---- lightbox ---- */
/* No plate behind the artwork. styles.css paints .lightbox-frame #faf9f7 and
   styles-alt.css repaints it var(--plate); both drew a white 16:9 card that the
   image floated on. Transparent lets the artwork sit directly on the scrim, and
   the shadow has to go with it — it was drawing on the frame box, not the
   image, so it outlined the card that is no longer there. */
.alt2 .lightbox-frame{background:transparent;box-shadow:none;border-radius:0}
/* .media carries its own white plate and drop shadow for the in-page viewer —
   clearing only .lightbox-frame left that card still painted underneath. */
.alt2 .lightbox-frame .media,
.alt2 .lightbox-frame .media.vid{background:transparent;box-shadow:none;border-radius:0}
/* prev / caption / next on one line (see render.js — they share .lightbox-bar).
   The nav buttons were position:fixed at top:50%, pinned to the viewport's
   middle and unrelated to anything on screen. */
.alt2 .lightbox-bar{
  display:flex;align-items:center;justify-content:center;gap:14px;
  width:min(96vw,calc(92vh * 16 / 9));
}
.alt2 .lightbox-bar .lightbox-nav{
  position:static;transform:none;flex:0 0 auto;
  width:32px;height:32px;
  /* the old ‹ › glyphs could never centre: styles.css compensates with
     padding-bottom:3px, and system-ui's guillemets sit off-centre in their own
     em box anyway. A stroked SVG is centred by geometry, not by nudging. */
  padding:0;font-size:0;
}
/* .8 on the mark only, not the button: dimming the whole control would take the
   dark disc and its hairline border down with it, and those are what hold the
   chevron off the artwork. */
.alt2 .lightbox-nav .lb-chev{width:14px;height:14px;display:block;opacity:.8}
/* padding:0 matters: the UA's default button padding is asymmetric top-to-bottom,
   which left the mark half a pixel high even once it was a centred SVG. */
.alt2 .lightbox-close{
  width:32px;height:32px;font-size:0;line-height:0;padding:0;
  color:rgba(255,255,255,.8);
}
.alt2 .lightbox-close .lb-chev{width:13px;height:13px;opacity:1}
/* margin:0 is load-bearing: styles.css gives every figcaption margin-top:16px,
   and in a centre-aligned flex row that offsets the text by half the margin —
   which is exactly why the caption sat 7px below the buttons. */
.alt2 .lightbox-bar .lightbox-caption{flex:1 1 auto;max-width:none;margin:0;font-size:16px}

/* ---- fun projects: Zelda / Mario ---- */
/* The Zelda frames are nearly square (1.20 / 1.31) and the Mario trio is a
   transparent PNG carrying its own border and shadow — cover would crop the
   first two and clip the shadow off the third. Contain keeps them whole. */
.alt2 .viewer .media > img.free[src*="g-mario-trio"],
.alt2 .viewer .media > img.free[src*="g-zelda"]{
  object-fit:contain;
}
/* Both Zelda frames are small originals (472x394 and 628x481). Contain alone
   would blow them up past 100% on a 936px plate and soften the pixel art, so
   cap them near their native size instead. */
.alt2 .viewer .media > img.free[src*="g-zelda.png"]{max-width:62%;max-height:62%}
.alt2 .viewer .media > img.free[src*="g-zelda-dc"]{max-width:90%;max-height:90%}
/* Both frames are flat rectangular screenshots. drop-shadow traces the painted
   pixels, so it hugs the artwork instead of the letterboxed box a box-shadow
   would outline. */
.alt2 .viewer .media > img.free[src*="g-zelda"]{
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.04)) drop-shadow(0 4px 14px rgba(0,0,0,.06));
}

/* The Google Go frame is a small 400x400 original — contain alone would blow it
   up ~2.3x on a 936px plate and soften it, so cap it near native size. */
.alt2 .viewer .media > img.free[src*="g-go-lens"]{
  object-fit:contain;
  max-width:76%;max-height:76%;
}

/* The "41 designers" chart is 1.15 — nearly square, so contain makes it
   height-limited and it fills the plate top to bottom with no breathing room.
   Pull it in so the artwork sits inside the plate rather than against it. */
.alt2 .viewer .media > img.free[src*="asana-41"]{max-width:72%;max-height:72%}

/* Every capped image above is position:absolute with inset:0 and width/height
   100%. Once max-width/max-height shrink the box it no longer fills those
   insets, and with no auto margin it pins to the top-left corner instead of
   sitting in the middle of the plate. margin:auto resolves the leftover space
   evenly on both axes. */
.alt2 .viewer .media > img.free[src*="g-zelda"],
.alt2 .viewer .media > img.free[src*="g-go-lens"],
.alt2 .viewer .media > img.free[src*="asana-41"]{margin:auto}

/* The base rule sizes the lightbox purely against the window — min(96vw, 92vh
   * 16/9) — so a 1920 screen opens it at ~1766px and a 2560 one at ~2458px.
   Eleven of the section images are under 1400px natively (the Zelda frames are
   472 and 628), so that was upscaling artwork 1.5-3.7x to fill a frame nothing
   had the pixels for. Cap the width so the viewport only ever makes it smaller.
   The caption tracks the frame so its wrap stays tied to the artwork's edge. */
.alt2 .lightbox-frame,
.alt2 .lightbox-caption{max-width:1280px}
.alt2 .lightbox-frame{width:min(96vw, calc(92vh * 16 / 9), 1280px)}

/* ---- Lens product shots ---- */
/* 1.22, 0.52 and 2.06 against a 16/9 plate — the default cover crop was taking
   two thirds of the height off the portrait one. Contain keeps them whole, the
   cap stops contain from pushing them to the plate edge, and margin:auto
   re-centres the shrunken box (inset:0 + width:100% pins it top-left once a
   max-width applies). All three carry alpha, so drop-shadow follows the device
   silhouette instead of outlining the letterboxed rectangle a box-shadow would. */
.alt2 .viewer .media > img.free[src*="lens-screenshot"],
.alt2 .viewer .media > img.free[src*="lens-searchbox"],
.alt2 .viewer .media > img.free[src*="lens-shopping"]{
  object-fit:contain;
  max-width:90%;max-height:90%;
  margin:auto;
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.04)) drop-shadow(0 4px 14px rgba(0,0,0,.06));
}
/* The shopping shot is near 16/9 itself, so the shared 90% cap left it floating
   in more margin than the others — it reads small where they read framed. */
.alt2 .viewer .media > img.free[src*="lens-shopping"]{max-width:96%;max-height:96%}
/* At 0.52 the searchbox is height-limited, so the 82% cap was costing it height
   twice over — the box shrank AND contain letterboxed inside it, leaving the
   artwork ~157px wide. It has no width to spare on a 16/9 plate, so let it fill
   the plate outright. 100% is the ceiling: the image is width:100%/height:100%
   and max-* can only shrink a box, never grow one, so anything above 100% here
   is inert. Its shadow still shows because the artwork is inset in transparency,
   well clear of the plate's overflow:hidden edge.
   translateY rather than a top offset: the box is absolutely positioned with
   inset:0 and centred by margin:auto, so touching top would fight that. */
.alt2 .viewer .media > img.free[src*="lens-searchbox"]{
  max-width:100%;max-height:100%;
  transform:translateY(2px);
}
/* The base layer scales this wordmark to .5; at 4.40 it's width-limited, so
   even halved it still ran 328px across the plate and read as a banner rather
   than a detail. .38 settles it back into the frame. */
.alt2 .viewer .media > img.free[src*="lens-goooogle"]{transform:scale(.38)}

/* The source carried a 4px #d8dae5 band on its left, right and bottom edges
   and nothing on top - an asymmetric frame from the original slide crop that
   scaled down to a 0.68px smudge on three sides. That crop had missed: the
   2768x2400 canvas still held a hairline of the band at all four edges while
   the artwork sat inset 104px either side. Two things went wrong from it -
   contain sized against the canvas (1.15) instead of the artwork (1.07), so it
   drew 8% narrower than it should, and drop-shadow follows painted pixels, so
   it traced that hairline and hung a ghost rectangle ~19px off both sides
   rather than hugging the image. Recropped to 2560x2400, the artwork's true
   bounds, so this now restores edge definition evenly on all four. */
.alt2 .viewer .media > img.free[src*="asana-41"]{
  filter:drop-shadow(0 1px 1px rgba(0,0,0,.04)) drop-shadow(0 4px 14px rgba(0,0,0,.06));
}
