*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-font-smoothing:antialiased;scroll-behavior:smooth}
body{
  font-family:var(--f);font-size:var(--t3);line-height:1.55;color:var(--ink);
  background:radial-gradient(120% 80% at 50% 0%, var(--bg-lift) 0%, var(--bg) 62%), var(--bg);
  background-attachment:fixed;
}
img{max-width:100%;display:block}
button{font:inherit}

/* ── the scene, fixed behind everything ─────────────────── */
#scene{position:fixed;inset:0;width:100%;height:100%;z-index:0;pointer-events:none}
#scene.live{pointer-events:auto}

/* ── fixed chrome ───────────────────────────────────────── */
/* One bar, one line, one height. Everything that was floating separately in
   the top left is now a single row, so it reads as a bar and not as three
   things that happen to be near each other. */
/* The bar has to occlude what scrolls under it. A ground-coloured wash that
   fades out at its lower edge does that without drawing a hard line, which the
   v2 rules disallow. */
/* The wash is a SOLID colour faded by the mask. It used to be a gradient
   running to `transparent`, and `transparent` is rgba(0,0,0,0), so the browser
   interpolates through black and paints a dark band across the top of the
   page. Fading a solid fill with a mask has no colour interpolation in it at
   all, so there is nothing to go grey. */
.topbar{isolation:isolate}
.topbar::before{content:'';position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:var(--bg);
  -webkit-mask-image:linear-gradient(#000 62%, rgba(0,0,0,0));
  mask-image:linear-gradient(#000 62%, rgba(0,0,0,0))}
.topbar{position:fixed;top:0;left:0;right:0;height:calc(var(--bar-h) + var(--s5));z-index:20;
  display:flex;align-items:center;gap:var(--s5);padding:0 var(--s6) var(--s5);
  pointer-events:none}
.topbar > *{pointer-events:auto}
/* the name is the way home, so it has to look and behave like a control */
.mark{font-size:var(--t2);letter-spacing:-.01em;line-height:1.15;
  border:0;background:none;padding:6px 10px;margin-left:-10px;text-align:left;
  color:var(--ink);font-family:inherit;cursor:pointer;border-radius:var(--r-ctl);
  transition:background .2s var(--ease-ui)}
.mark:hover{background:var(--panel-2)}
.mark:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.mark span{display:block;font-size:var(--t1);color:var(--ink-3);margin-top:1px}

.social{display:flex;align-items:center;gap:var(--s2)}
.social a{display:grid;place-items:center;width:var(--ctl-h);height:var(--ctl-h);
  border-radius:var(--r-pill);color:var(--ink-3);
  transition:color .2s var(--ease-ui),background .2s var(--ease-ui)}
.social a:hover{color:var(--ink);background:var(--panel-2)}
.social a:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.social svg{width:var(--icon);height:var(--icon);fill:currentColor;display:block}

/* DELETED 2026-08-20 — two blocks of dead CSS, `.index` and `.rail`.
   Neither had an element in the markup or a reference in the JS: verified live,
   `document.querySelectorAll('.index').length` is 0. They were a section index
   rail that was designed and never enabled.

   Worth recording rather than just deleting, for two reasons.

   The design audit reported `transition: all` on 48 elements. There is one
   `transition: all` in this stylesheet and it was on `.index a i`, a 14x1px
   hairline that does not exist on the page. Counting matched elements rather
   than rules turned one line of dead code into a finding.

   And bare `.rail{display:none}` was a live trap. `.chat.rail` uses `rail` as
   a MODIFIER, so that rule matched the chat. It lost only on source order —
   `.rail` at line 63 against `.chat` at line 136, identical specificity. Move
   either block and the chat disappears. This is the same fault as the `sheet`
   class collision that hid the content of every section on 2026-08-18: a
   generic class name used both as a component and as a modifier. */

/* z-index 5, under the station at 8. These describe the hero, and they were
   sitting ABOVE the full-screen overlay — so a section opened over an
   unscrolled hero showed the scroll cue and a 0% counter on top of itself.
   Fading them from script is belt and braces; being behind the overlay is
   what makes it structurally impossible. */
.hint{position:fixed;left:var(--s6);bottom:var(--s5);z-index:5;font-size:var(--t1);color:var(--ink-3);
  transition:opacity .4s var(--ease-ui)}
.count{position:fixed;right:var(--s6);bottom:var(--s5);z-index:5;font-family:var(--m);
  font-size:var(--t1);color:var(--ink-3);font-variant-numeric:tabular-nums}

/* ── hero ───────────────────────────────────────────────── */
#hero{height:289vh;position:relative;z-index:1;pointer-events:none}
.hero-copy{position:fixed;left:0;top:0;width:100%;height:100dvh;display:flex;align-items:center;
  pointer-events:none;z-index:3}
.hero-copy .inner{max-width:var(--max);margin:0 auto;padding:0 var(--s6);width:100%;
  display:flex;justify-content:flex-end}
.hero-copy .inner > div{width:min(30ch,44%)}
.hero-copy h1{font-size:clamp(28px,3.4vw,var(--t7));font-weight:400;letter-spacing:-.03em;line-height:1.08}
.hero-copy p{font-size:var(--t3);color:var(--ink-2);margin-top:var(--s4)}

/* THE OPENING LINE TYPES ITSELF. The height is reserved in script from the
   longest phrase measured in the real column, because everything below a line
   that grows from nothing moves while it grows.

   The caret is a border rather than a character: a `|` in the text node gets
   selected, copied and read out, and it also changes the string being measured.
   It only blinks while the line is at rest, which is where a caret blinks on a
   real terminal, and it is a token from the ramp rather than an accent, this
   palette having no accent. */
.hero-h1{min-width:0}
/* The caret is sized in `em` so it stays proportional to the heading, which is
   a clamp between 28px and the display size: a fixed 1.5px rule that reads
   right at body size is a hairline against 49px type. */
.hero-h1 .type{border-right:.055em solid var(--ink-3);padding-right:.04em}
.hero-h1 .type.on{animation:caret 1.05s step-end infinite}
@keyframes caret{0%,100%{border-right-color:var(--ink-3)}50%{border-right-color:transparent}}
@media (prefers-reduced-motion: reduce){
  .hero-h1 .type{border-right:0;padding-right:0;animation:none}
}

/* Visible to a screen reader, and to nothing else. The animated span is
   `aria-hidden`; this carries the same sentence once, as text. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.hero-copy .meta{margin-top:var(--s6);font-family:var(--m);font-size:11px;color:var(--ink-3)}

/* ── the theme toggle ───────────────────────────────────── */
.theme{display:flex;align-items:center;gap:var(--s3);cursor:pointer;border:0;
  background:none;color:var(--ink-3);font-size:var(--t1);padding:0;
  height:var(--ctl-h);margin-left:var(--s3)}
.theme-label{min-width:32px;text-align:left}   /* Dark and Light differ in width;
                                                  reserving it stops the icons
                                                  shifting when the theme flips */
.theme:hover{color:var(--ink-2)}
.theme .track{position:relative;width:38px;height:21px;border-radius:var(--r-pill);
  background:var(--panel-2);box-shadow:inset 0 0 0 1px var(--hair);
  transition:background .24s var(--ease-ui)}
.theme .knob{position:absolute;top:3px;left:3px;width:15px;height:15px;border-radius:50%;
  background:var(--ink-3);transition:transform .26s var(--ease), background .24s var(--ease-ui)}
:root[data-theme="dark"] .theme .knob{transform:translateX(17px);background:var(--ink)}
.theme:focus-visible{outline:2px solid var(--ink);outline-offset:3px;border-radius:var(--r-pill)}

/* ── the hub: the core is the navigation ────────────────── */
.hub{position:fixed;inset:0;z-index:6;pointer-events:none;opacity:0;
  transition:opacity .5s var(--ease), transform .6s var(--ease);transform:scale(1)}
.hub.on{opacity:1;pointer-events:none}
.hub.on .leaders,.hub.on .askbar{pointer-events:auto}
.hub.out{opacity:0;transform:scale(.82)}

.leaders{position:absolute;inset:0}
.leaders button{position:absolute;border:0;background:none;padding:0;cursor:pointer;
  color:var(--ink);font-weight:500;font-size:var(--t2);white-space:nowrap;display:flex;align-items:flex-end;justify-content:center;
  border-radius:50%;transition:color .2s var(--ease-ui);will-change:transform}
.leaders button span{transform:translateY(calc(var(--drop,120%) + var(--nudge,0px)));
  transition:color .2s var(--ease-ui),transform .25s var(--ease-ui)}
.leaders button:hover,.leaders button:focus-visible{color:var(--ink);outline:none}
.leaders button:hover span{font-weight:500}
.leaders button:focus-visible{outline:2px solid var(--ink);outline-offset:4px}

.askbar{position:absolute;left:50%;transform:translateX(-50%);display:flex;align-items:center;
  width:min(560px,80vw);z-index:600}
.askbar .grow{box-shadow:var(--lift)}
.askbar textarea::placeholder{color:var(--ink-3)}

/* ── the chat, when it owns the bottom of the page ──────── */
/* Above the station (8) and the parked core (9). It used to sit at 7, so in a
   section every click aimed at the bar landed on the station underneath it and
   the chat simply did not respond. */
/* A centred panel over the hub rather than a sheet across the bottom. The
   sheet cut the page in two and wasted the whole width either side of a 760px
   column; this floats on the layout instead of rearranging it. Above the
   station (8) and the parked core (9) so its controls are actually clickable. */
/* The hub keeps the top 40 percent and the chat takes the bottom 60. A centred
   panel covered the orb and its clusters, which are the navigation, so it hid
   the thing the page is for. */
.chat{position:fixed;left:50%;bottom:var(--s5);z-index:12;
  width:min(760px,92vw);height:calc(40dvh - var(--s5));
  background:var(--panel);border-radius:var(--r-panel);
  box-shadow:var(--lift-2);display:flex;flex-direction:column;
  padding:var(--s4) 0 var(--s4);
  transform:translate(-50%,calc(100% + var(--s6)));
  transition:transform .5s var(--ease)}
.chat.on{transform:translate(-50%,0)}
.chat-head,.chat-form,.chat-log{width:min(760px,calc(100% - var(--s7)));margin:0 auto}
.chat-head{display:flex;align-items:center;justify-content:space-between;
  font-size:var(--t1);color:var(--ink-3);padding-bottom:var(--s3)}
.chat-head button{border:0;background:none;color:var(--ink-3);font-size:var(--t1);cursor:pointer}
.chat-head button:hover{color:var(--ink)}
.chat-log{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  scrollbar-width:none;padding-right:2px}
.chat-log::-webkit-scrollbar{display:none}
/* the visitor's own words sit in a tinted block pushed to the right; his
   answer runs plain on the sheet. Two different things should not read as one
   column of text. */
.chat-q{font-size:var(--t2);margin:var(--s5) 0 0 auto;padding:10px var(--s4);
  width:fit-content;max-width:80%;color:var(--ink);
  border-radius:var(--r-panel) var(--r-panel) 4px var(--r-panel);
  background:var(--panel-2);box-shadow:inset 0 0 0 1px var(--hair)}
.chat-q:first-child{margin-top:0}
.chat-a{font-size:var(--t2);color:var(--ink-2);margin-top:var(--s3);max-width:64ch;
  line-height:var(--lh-body)}
.chat-a ul{margin:var(--s2) 0 0;padding-left:18px}
.chat-a li{margin-top:5px}
.chat-cite{display:flex;gap:var(--s2);flex-wrap:wrap;margin-top:var(--s3)}
.thinking{display:flex;align-items:center;gap:var(--s3);color:var(--ink-2);
  font-size:var(--t2);margin-top:var(--s3)}
.thinking i{display:flex;gap:4px}
.thinking i b{width:6px;height:6px;border-radius:50%;background:currentColor;
  animation:blink 1.15s var(--ease-ui) infinite}
.thinking i b:nth-child(2){animation-delay:.16s}
.thinking i b:nth-child(3){animation-delay:.32s}
@keyframes blink{0%,80%,100%{opacity:.3}40%{opacity:1}}
@media (prefers-reduced-motion: reduce){.thinking i b{animation:none;opacity:.55}}

/* what the answer cost. Only ever rendered on localhost. */
.meter{font-family:var(--m);font-size:11px;color:var(--ink-3);margin-top:var(--s2);
  display:flex;gap:var(--s3);flex-wrap:wrap}
.chat-form{position:relative;display:flex;padding-top:var(--s4)}
/* One composer, used by the hub bar and the rail alike. The bar grows with
   what is typed, and it does that WITHOUT measuring anything in script: the
   wrapper carries an invisible copy of the same text in the same grid cell,
   so the box is sized by layout. Reading scrollHeight to set a height is the
   usual trick and it is unreliable — it holds a stale value once the element
   has been sized once, and it needs a live frame, which a hidden tab has not
   got. The two rules below MUST stay in one block: the mirror only measures
   correctly while its box is identical to the field's. */
.grow{position:relative;flex:1;min-width:0;
  max-height:40vh;overflow:hidden;border-radius:calc(var(--ctl-h)/2);
  background:var(--panel);box-shadow:inset 0 0 0 1px var(--hair)}
/* the mirror alone sizes the box; the field is taken out of flow so its own
   intrinsic height cannot fight it */
.grow::after{content:attr(data-v) " ";display:block;visibility:hidden;
  white-space:pre-wrap;word-break:break-word}
.grow > textarea{position:absolute;inset:0}
.grow > textarea,.grow::after{
  /* box-sizing must be stated here: the universal selector does not match a
     pseudo-element, so the mirror would otherwise add its padding on top of
     the minimum height and sit a line taller than the field */
  box-sizing:border-box;
  font:inherit;font-size:var(--t2);line-height:22px;
  padding:11px calc(var(--ctl-h) + 6px) 11px var(--ctl-pad);
  min-height:var(--ctl-h);border:0;background:none;color:var(--ink);
  resize:none;overflow-y:auto;scrollbar-width:none}
.grow > textarea::-webkit-scrollbar{display:none}
.grow > textarea:focus{outline:none}
.grow:focus-within{box-shadow:inset 0 0 0 2px var(--ink)}

/* The pill's height comes from `.grow::after` mirroring what has been TYPED,
   so it knows nothing about the placeholder -- and "Where would you like to
   navigate next?" is wider than a 328px rail field, so it wrapped to a second
   line the pill had no height for and spilled out the bottom of it.
   Constrained only while the field is empty, so a long question a visitor
   actually types still wraps and still grows the pill. */
.grow.empty > textarea{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.grow.empty > textarea::placeholder{white-space:nowrap;text-overflow:ellipsis}

/* one send control, used by the hub bar and the rail alike */
.send{position:absolute;right:6px;bottom:6px;
  width:32px;height:32px;border:0;cursor:pointer;border-radius:var(--r-pill);
  background:none;color:var(--ink-3);display:grid;place-items:center;
  transition:color .2s var(--ease-ui),background .2s var(--ease-ui)}
.send svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}
.send:not(:disabled){background:var(--ink);color:var(--bg);box-shadow:var(--lift)}
.send:not(:disabled):hover{box-shadow:var(--lift-2)}
.send:not(:disabled):active{transform:translateY(1px)}
.send:disabled{background:var(--panel-2);color:var(--ink-3);
  box-shadow:inset 0 0 0 1px var(--hair)}
/* The disc stays 32px because that is what fits inside a 44px-tall field and
   a bigger one would read as a blob rather than a control. What has to be 44
   is the area a thumb can hit, so it is extended past the disc rather than
   the disc being grown into it. 32 + 6 either side is exactly 44, which is
   why the inset is the bar's own padding. */
.send::before{content:'';position:absolute;top:-6px;right:-6px;bottom:-6px;left:-6px}


/* the rail stays a single line until there is something to show */
/* As a rail the container is invisible and, now that it runs most of the
   height, it was an empty box lying over the right of the section and eating
   clicks meant for the card underneath. The box stops taking pointer events;
   the things actually drawn inside it take them back. */
.chat.rail{background:none;box-shadow:none;padding:var(--s4) 0 var(--s5);
  display:flex;flex-direction:column;justify-content:flex-start;
  pointer-events:none}
.chat.rail > *{pointer-events:auto}
.chat.rail .chat-head{display:none}
.chat.rail .chat-form{order:1;padding-top:0}
.chat.rail .chat-log{order:2;flex:0 1 auto;max-height:0;overflow:hidden;
  transition:max-height .5s var(--ease)}
/* while the opening offer stands, the log has to have somewhere to be */
.chat.rail.sugg .chat-log{max-height:60vh;overflow:visible}
/* the thread is its own scroller, contained, so a flick through the
   conversation never reaches the page behind it */
.chat.rail.open .chat-log{max-height:calc(100dvh - var(--dock) - 200px);overflow-y:auto;
  overscroll-behavior:contain;padding:var(--s5) var(--s5);
  margin-top:var(--s3);background:var(--panel);border-radius:var(--r-panel);
  box-shadow:var(--lift)}
.chat.rail .chat-form input{background:var(--panel);box-shadow:var(--lift);border-color:transparent}


/* ── shared furniture ───────────────────────────────────── */
.card{background:var(--panel);border-radius:var(--r-panel);box-shadow:var(--lift);padding:var(--s6)}
.lbl{font-size:var(--t1);color:var(--ink-3)}
.num{font-family:var(--m);font-variant-numeric:tabular-nums}
.grid{display:grid;gap:var(--s5)}
/* grid and flex children default to min-content width, so one wide scroller
   inside a card drags the whole page wider than the viewport */
.grid > *,.appcard > *,.card > *{min-width:0}
.g2{grid-template-columns:repeat(2,1fr)}
.g3{grid-template-columns:repeat(3,1fr)}
.rowline{display:flex;gap:var(--s5);flex-wrap:wrap}
.btn{border:0;height:var(--ctl-h);padding:0 var(--ctl-pad);border-radius:var(--r-pill);
  display:inline-flex;align-items:center;justify-content:center;text-decoration:none;
  background:var(--ink);color:var(--bg);box-shadow:var(--lift);cursor:pointer;font-size:var(--t2);
  transition:transform .18s var(--ease-ui)}
.btn:active{transform:translateY(1px)}
/* The one control on this site that had no focus rule, so it painted Chrome's
   default blue on a page with no accent hue anywhere. `.btn` is the solid
   button, so the ring goes OUTSIDE it and takes the ink; `.btn.ghost` is a
   panel and takes the same ring, which reads against both. */
.btn:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.btn.ghost{background:var(--panel);color:var(--ink);box-shadow:var(--lift), inset 0 0 0 1px var(--hair)}
.chip{display:inline-block;font-size:var(--t1);color:var(--ink-2);background:var(--panel-2);
  padding:7px 14px;border-radius:var(--r-pill);box-shadow:inset 0 0 0 1px var(--hair);border:0}
.ln{height:7px;border-radius:2px;background:var(--hair);opacity:.6;margin-top:7px}
.note{background:var(--panel-2);border-radius:var(--r-ctl);padding:var(--s4) var(--s5);
  font-size:var(--t2);color:var(--ink-2);max-width:70ch;margin-bottom:var(--s6);
  box-shadow:inset 0 0 0 1px var(--hair)}
.rise{opacity:0;transform:translateY(18px);transition:opacity .7s var(--ease), transform .7s var(--ease)}
.rise.in{opacity:1;transform:none}
.appicon{width:112px;height:112px;border-radius:26px;background:linear-gradient(160deg,var(--panel),var(--stage));
  box-shadow:var(--lift), inset 0 0 0 1px var(--hair);position:relative;overflow:hidden}
.appicon canvas{width:100%;height:100%;display:block}
/* a real logo, where the product has one. A line mark is inset and takes the
   page's ink so it themes itself; a finished app icon fills the tile. */
/* `.appicon.mark` — the 56% inline-SVG glyph on a gradient plate — came off on
   2026-08-22. Every product with a real icon now uses `.shot`, which fills the
   tile the way CASEY's always did. */
.appicon.shot{background:none}
.appicon.shot img{width:100%;height:100%;object-fit:cover;display:block}
.appmeta h3{display:flex;align-items:baseline;gap:var(--s2);flex-wrap:wrap;
  font-size:var(--t5);font-weight:500;letter-spacing:-.02em;line-height:var(--lh-tight)}
/* none of these names says what the thing is, so the descriptor is part of
   the heading rather than a line under it */
.pname{font-size:var(--t5)}
/* The product name and its description were joined by an em-dash in the
   markup. He asked for em-dashes off the site because they read as machine
   written [@maaz · 2026-08-22], so the separator is space now, not punctuation. */
.pdesc{font-size:var(--t3);font-weight:400;letter-spacing:0;color:var(--ink-2);
  margin-left:.5ch}
.appstats{display:flex;gap:var(--s6);margin-top:var(--s4);padding-top:var(--s3);border-top:1px solid var(--hair)}
.pcard .appstats{margin-top:var(--s3)}
.appstats div .v{font-size:var(--t4)}
.shots{display:flex;gap:var(--s3);max-width:100%;overflow-x:auto;padding:var(--s4) 0 var(--s2);scroll-snap-type:x mandatory}
.shots .shot{flex:0 0 264px;height:186px;border-radius:var(--r-ctl);background:var(--panel-2);
  box-shadow:inset 0 0 0 1px var(--hair);scroll-snap-align:start;overflow:hidden}
.shots .shot img{width:100%;height:100%;object-fit:cover;display:block}

/* ── the chat as a rail, alongside an open section ──────── */
.chat.rail{left:auto;right:var(--s5);top:calc(var(--dock) + var(--s4));bottom:auto;
  height:auto;max-height:calc(100dvh - var(--dock) - var(--s7));
  width:var(--rail-w);transform:translateX(calc(100% + var(--s6)));opacity:1}
.chat.rail.on{transform:none;opacity:1}
.chat.rail .chat-head,.chat.rail .chat-form,.chat.rail .chat-log{width:calc(100% - var(--s6));margin:0 auto}
.station.railed .station-inner{padding-right:calc(var(--rail-w) + var(--s5))}
@media(max-width:900px){
  .chat.rail{left:0;right:0;top:auto;bottom:0;height:auto;max-height:56dvh;width:auto;
    border-radius:var(--r-panel) var(--r-panel) 0 0;transform:translateY(101%);padding-top:var(--s5)}
  .chat.rail.on{transform:none}
  .station.railed .station-inner{padding-right:var(--s4);padding-bottom:56dvh}
}

/* ── a station ──────────────────────────────────────────── */
/* A full-screen overlay paints its own ground. This was transparent, and so
   depended on the hero being faded out underneath it — an invariant that does
   not always hold. When it broke, a section opened with his face, the hero
   headline, the scroll cue and a 0% counter all showing through it. An overlay
   must not rely on what is behind it being invisible. */
.station{position:fixed;inset:0;z-index:8;overflow-y:auto;overflow-x:hidden;
  background:var(--bg);
  opacity:0;pointer-events:none;transform:scale(1.08);
  transition:opacity .52s var(--ease), transform .62s var(--ease);
  scrollbar-width:none;overscroll-behavior:contain}
.station::-webkit-scrollbar{display:none}
.station.on{opacity:1;pointer-events:auto;transform:none}
/* the scroll cue, the percentage and the progress hairline belong to the hero.
   While a section is open there is nothing for them to describe. */
body.in-section .hint,
body.in-section .count,
body.in-section .prog{opacity:0;pointer-events:none}
.station-inner{max-width:var(--max);margin:0 auto;
  padding:calc(var(--bar-h) + var(--s7)) var(--s5) 10vh}

section.mod > .head{display:flex;align-items:baseline;gap:var(--s4);margin-bottom:var(--s5)}
section.mod > .head h2{font-size:var(--t6);font-weight:400;letter-spacing:-.03em;line-height:var(--lh-tight)}
section.mod > .head .n{margin-left:auto;font-family:var(--m);font-size:var(--t1);color:var(--ink-3)}
/* The heading is focused when a section opens so a screen reader's cursor
   moves with the page. It is a DESTINATION, not a control: tabindex is -1, so
   no keyboard user can arrive here by tabbing and none of them needs a ring to
   find it. Chrome paints its default ring anyway — `rgb(153,200,255) auto 1px`
   — so every section opened with a blue box drawn around its title, on a site
   whose design language has no accent hue anywhere. [@maaz · 2026-08-18]
   The focus move stays; only the ring goes. */
section.mod > .head h2:focus,
section.mod > .head h2:focus-visible{outline:none}
section.mod > .lede{max-width:62ch;color:var(--ink-2);font-size:var(--t4);line-height:var(--lh-mid);margin-bottom:var(--s6)}

/* the core, parked and clickable */
/* Centred on the rail beneath it, not jammed into the corner: the core and the
   ask bar read as one object stacked, which is what he drew. */
.dock{position:fixed;top:0;right:calc(var(--s5) + (var(--rail-w) - var(--dock)) / 2);
  width:var(--dock);height:var(--dock);
  z-index:9;border:0;background:none;cursor:pointer;opacity:0;pointer-events:none;
  font-size:0;color:transparent;transition:opacity .4s var(--ease-ui)}
.dock.on{opacity:1;pointer-events:auto}
.dock::after{content:'Back';position:absolute;left:50%;bottom:2px;transform:translateX(-50%);
  font-size:11px;color:var(--ink-3);opacity:0;transition:opacity .2s var(--ease-ui)}
.dock:hover::after,.dock:focus-visible::after{opacity:1}
.dock:focus-visible{outline:2px solid var(--ink);outline-offset:-6px;border-radius:var(--r-panel)}

/* The parked core has to be SEEN, not merely clickable. `.station` paints its
   own ground at z-index 8 — the fix that stopped the hero bleeding through —
   and the canvas sits at z-index 0, so the station covered it completely and
   the core went missing from all six sections. `.dock` is a hit target with no
   pixels of its own, so the way back was clickable but invisible, which is
   worse than either state alone.
   Raising the canvas is safe here and only here: while a section is open the
   hero is HELD at 1, so every point has arrived on the orb, the photograph is
   at zero alpha, the face mesh is multiplied by (1 - dock) and the filaments
   are skipped for any point that has finished travelling. The canvas paints
   exactly one object — the core — and nothing else.
   Pointer events stay off: `.tappable` is only added while no section is open,
   so the station underneath keeps every one of its controls. */
body.in-section #scene{z-index:9}



@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  .rise{opacity:1;transform:none;transition:none}
  main{transition:none}
}
@media(max-width:900px){
  #hero{height:204vh}
  .station-inner{padding:12vh var(--s4) 14vh}
  .leaders button{font-size:12px}
  .rail{display:none}
  .g2,.g3{grid-template-columns:1fr}
  .sheet{padding:0 var(--s4)}
  #hero{height:320vh}
  .appcard{grid-template-columns:64px 1fr;gap:var(--s4)}
  .appicon{width:64px;height:64px;border-radius:15px}
  .appcard > .rowline{grid-column:1 / -1}
}


/* ── the timeline, used by Startups ─────────────────────── */
.tl{list-style:none;margin:0;padding:0;position:relative}
.tl::before{content:'';position:absolute;left:5px;top:10px;bottom:10px;width:1px;
  background:var(--hair)}
.tl-item{position:relative;padding:0 0 var(--s7) var(--s7);display:block}
.tl-item:last-child{padding-bottom:0}
.tl-dot{position:absolute;left:0;top:6px;width:11px;height:11px;border-radius:50%;
  background:var(--panel);box-shadow:inset 0 0 0 1px var(--hair), var(--lift)}
.tl-item:first-child .tl-dot{background:var(--ink);box-shadow:var(--lift)}
.tl-head{display:flex;align-items:baseline;gap:var(--s3);flex-wrap:wrap}
.tl-head h3{font-size:var(--t5);font-weight:500;letter-spacing:-.02em}
.tl-copy{margin-top:var(--s2);color:var(--ink-2);font-size:var(--t3);max-width:58ch}
.tl-facts{margin-top:var(--s4)}
.tl-close{margin-top:var(--s6);color:var(--ink-2);font-size:var(--t3);max-width:64ch}

/* ── business cases ─────────────────────────────────────────
   A large dedicated card, not one of three small equal ones. Two shapes share
   it: a written piece, which has nothing to look at and so leads with its
   argument, and a deck, which is nothing but things to look at. The parts are
   in the order somebody actually reads them — what it was, the claim, the
   evidence, the detail, the numbers, and what he did. */
.ccard{padding:var(--s6)}
.chead{display:flex;flex-direction:column;gap:var(--s1)}
.chead h3{font-size:var(--t5);font-weight:500;letter-spacing:-.02em;line-height:var(--lh-tight)}
.csub{font-size:var(--t3);color:var(--ink-2);line-height:var(--lh-mid)}
/* the claim, set larger than the body because it is the thing to remember */
.cthesis{font-size:var(--t4);line-height:var(--lh-mid);max-width:58ch;
  margin:var(--s5) 0;padding-left:var(--s4);box-shadow:inset 2px 0 0 var(--ink)}
.ccard .pstage{height:clamp(200px,42dvh,460px)}
.cbody{font-size:var(--t3);max-width:72ch;margin-top:var(--s5)}
.cmine{margin-top:var(--s5);padding-top:var(--s4);border-top:1px solid var(--hair)}
.cmine p{font-size:var(--t2);color:var(--ink-2);max-width:72ch;margin-top:var(--s2);
  line-height:var(--lh-body)}
.cactions{margin-top:var(--s5)}
.cnote{font-size:var(--t1);color:var(--ink-3);max-width:72ch;margin-top:var(--s5);
  line-height:var(--lh-mid)}

/* THE DISCLAIMER. Sits above the argument, not in the grey note at the bottom,
   because a disclaimer a reader never reaches has not been given. Deliberately
   quiet rather than alarming — it is a statement of authorship, not a warning,
   so it takes the hairline box and the muted label the rest of the card uses
   rather than a colour that would read as an error. Full ink on the text so it
   is never dimmer than the body it qualifies. */
.cdisc{margin:var(--s5) 0 0;padding:var(--s4);max-width:72ch;
  border:1px solid var(--hair);border-radius:var(--r-panel);
  background:var(--bg-lift);
  font-size:var(--t2);line-height:var(--lh-body);color:var(--ink)}
.cdisc .lbl{display:block;margin-bottom:var(--s2)}

/* ── shared bits the sections were setting inline ───────── */
.rowline.tight{gap:var(--s2);margin-top:var(--s3)}
ul.plain{list-style:none;margin:var(--s4) 0 0;padding:0;display:grid;gap:var(--s3)}
ul.plain li{color:var(--ink-2);font-size:var(--t2);line-height:var(--lh-body);
  padding-left:var(--s4);position:relative}
ul.plain li::before{content:'';position:absolute;left:0;top:9px;width:6px;height:1px;
  background:var(--ink-3)}

/* ── section furniture. Every value here comes from tokens.css, which is the
      whole point: a section that sets its own 13.5px and 6px is how the type
      scale and the spacing rhythm drifted apart in the first place. ──────── */
.card-h{font-size:var(--t4);font-weight:500;letter-spacing:-.015em;margin-top:var(--s2)}
.card-p{font-size:var(--t2);color:var(--ink-2);margin-top:var(--s3);line-height:var(--lh-body)}
.card-v{font-size:var(--t3);margin-top:var(--s1)}
.row-between{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s3)}
.row-media{display:flex;gap:var(--s4);align-items:center}
.row-media .grow-cell{flex:1;min-width:0}
.thumb{width:72px;height:72px;border-radius:var(--r-ctl);background:var(--panel-2);
  box-shadow:inset 0 0 0 1px var(--hair);flex:none;overflow:hidden}
.thumb img{width:100%;height:100%;object-fit:cover;display:block}
.divide{margin-top:var(--s4);padding-top:var(--s3);border-top:1px solid var(--hair)}
.field{display:flex;gap:var(--s3);margin-top:var(--s2)}
.field input{flex:1;min-width:0;font:inherit;font-size:var(--t2);height:var(--ctl-h);
  padding:0 var(--ctl-pad);border-radius:calc(var(--ctl-h)/2);
  border:1px solid var(--hair);background:var(--panel);color:var(--ink)}
.field input::placeholder{color:var(--ink-3)}
.field input:focus{outline:2px solid var(--ink);outline-offset:1px}
.fine{font-size:var(--t1);color:var(--ink-2);margin-top:var(--s3)}
.narrow{max-width:520px}


/* ── the chat's own controls ────────────────────────────────
   A closed thread has to be reachable again. The chevron on the bar opens the
   conversation that already exists, and the one in the panel head folds it
   away without ending it. Both are the same control in two places. */
/* The chevron sits INSIDE the field, at its left end.
   It used to hang outside to the left, at -52px, and that broke wherever the
   field reaches the edge of the viewport: on a phone the bar starts at x=16,
   so the control landed at x=-36 and all a person saw was a 4px sliver of its
   own edge; in the desktop rail the labelled version overlapped the very input
   it belongs to. Both were the same bug, and neither is reachable by tuning
   the offset -- any fixed negative offset fails at some width.
   Inside the pill it cannot clip at any width. The constraint that put it
   outside still holds: the FIELD does not move, only its text indents, so the
   bar stays on the same vertical line as the core above it. */
/* z-index 2, and it is load-bearing. `.grow` is a positioned sibling that
   comes AFTER this in the markup and paints an opaque pill, so without it the
   chevron sat correctly placed and correctly sized UNDERNEATH the field --
   invisible, and un-clickable with it, since every tap on it landed on the
   textarea. It was reported as missing twice. */
/* It lives INSIDE the pill now, and is positioned against the pill, mirroring
   the send button on the other end. Every earlier attempt anchored it to the
   FORM — first centred on it, then to its bottom edge — and both were wrong in
   the same way: the form is the bar's whole row, the pill is only part of it,
   so the control kept landing beside the bar rather than in it. Positioning
   against the thing it must sit inside is the fix; an offset was never going
   to be. */
.grow > .chev{position:absolute;left:6px;bottom:6px;z-index:2;width:32px;height:32px}
.grow > .chev.on ~ textarea,
.grow > .chev.on + textarea{padding-left:44px}
/* 44, not 34: the control spans 5..39, so a 34px indent started the text
   underneath its trailing edge. This is the gap that has to clear it. */
/* the textarea is the FIRST child, so the sibling combinator cannot reach it
   from the button; the pill carries the indent instead */
.grow.has-chev > textarea{padding-left:44px}
/* Same fault, same fix: this sat inside a field whose own ground is `--panel`
   and was painted `--panel`, so it was there, correctly placed, at the right
   size, and invisible -- which is indistinguishable from missing, and was
   reported as missing. A control has to be able to be SEEN against the surface
   it is on, and that is measured now rather than eyeballed. */
.chev{width:var(--ctl-h);height:var(--ctl-h);flex:none;border:0;cursor:pointer;
  border-radius:var(--r-pill);background:var(--ink);color:var(--bg);
  box-shadow:var(--lift);display:none;place-items:center;
  transition:box-shadow .2s var(--ease-ui),opacity .2s var(--ease-ui)}
.chev.on{display:grid}
.chev:hover{box-shadow:var(--lift-2)}
.chev:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
.chev svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.8;
  stroke-linecap:round;stroke-linejoin:round;
  transition:transform .28s var(--ease)}
/* The icon points the way the thread will travel. Closed, the conversation is
   below the fold and the control pulls it UP; open, the control puts it back
   DOWN. It was the other way round in both states -- the shipped SVG is a down
   chevron and the rules rotated the wrong one -- so the only two states a
   person sees were each showing the opposite of what pressing them does. */
.chev svg{transform:rotate(180deg)}
.chev[aria-expanded="true"] svg{transform:rotate(0deg)}
.chat-head .chev{width:32px;height:32px}
.chat-head .chev svg{transform:rotate(0deg)}   /* it only ever collapses */

/* ── pictures ───────────────────────────────────────────────
   Placeholder photography, but real photography: a page laid out around grey
   rectangles does not tell you what the finished page looks like. */
/* A supporting picture, not the entry itself. At full card width a 16:10 band
   is 984x615 and buries the name, the copy and the facts above it. */
.tl .fig{max-width:520px}
.fig{margin:var(--s4) 0 0;border-radius:var(--r-ctl);overflow:hidden;
  box-shadow:var(--lift);background:var(--panel-2);aspect-ratio:16/10}
.fig.wide{aspect-ratio:21/9;margin:0 0 var(--s6)}
.fig img{width:100%;height:100%;object-fit:cover;display:block}
.card .fig{margin:0 0 var(--s4)}


/* ── the product card, image first ──────────────────────────
   The picture is the product. It runs the full width of the card at 16:10,
   and the strip under it switches which picture is up rather than being the
   only place the images appear. */
.pcard{padding:var(--s5) var(--s6)}
.phead{display:grid;grid-template-columns:auto 1fr auto;gap:var(--s5);
  align-items:center;margin-bottom:var(--s4)}
.phead .appicon{width:72px;height:72px;border-radius:17px}
.pactions{margin-top:0}
/* sized to the viewport, not to a ratio, so the card and its picture fit on
   one screen instead of running off the bottom */
/* Sized for a SCREENSHOT, not for a stock photograph. The old box was 968x234,
   a 4:1 letterbox that cropped the only part of a product shot worth seeing —
   Handrail's arrow sits at the bottom of its frame and was being cut off. The
   picture is now shown whole on the panel behind it: a product shot that has
   been cropped to fit a box is no longer evidence of anything. */
.pstage{margin:0;border-radius:var(--r-panel);overflow:hidden;
  height:clamp(200px,38dvh,420px);
  background:var(--panel-2);box-shadow:inset 0 0 0 1px var(--hair)}
.pstage img{width:100%;height:100%;object-fit:contain;display:block;
  transition:opacity .28s var(--ease-ui)}
/* `display:block` here beats the browser's own `[hidden]{display:none}`, which
   is a plain attribute selector — so the hidden picture kept its 605px and the
   architecture slide sat under it in a stage twice the height it needed. */
.pstage img[hidden]{display:none}
.pstage.swapping img{opacity:.35}
.pstrip{display:flex;gap:var(--s3);margin-top:var(--s3)}
.pthumb{flex:1 1 0;min-width:0;height:clamp(40px,6dvh,64px);padding:0;border:0;cursor:pointer;
  border-radius:var(--r-ctl);overflow:hidden;background:var(--panel-2);
  box-shadow:inset 0 0 0 1px var(--hair);opacity:.55;
  transition:opacity .2s var(--ease-ui),box-shadow .2s var(--ease-ui)}
.pthumb img{width:100%;height:100%;object-fit:contain;display:block}
.pthumb:hover{opacity:.85}
.pthumb.on{opacity:1;box-shadow:0 0 0 2px var(--ink)}
.pthumb:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
/* the card is a summary, not the whole story: four lines, then it stops */
/* The clamp is gone. It was there when the card was a picture and a blurb and
   four lines was the whole of the writing; the card now carries a diagram and
   three lines of technical note, so cutting the description mid-sentence with
   an ellipsis reads as a fault rather than as restraint. */
.pcard .card-p{margin-top:var(--s4);max-width:78ch}
@media(max-width:900px){
  .phead{grid-template-columns:56px 1fr;gap:var(--s4)}
  .pactions{grid-column:1 / -1}
  .pthumb{height:56px}
}


/* ── responsive ─────────────────────────────────────────────
   Two targets that matter: a laptop, and a phone. The hub is a canvas scene
   so it scales on its own; everything around it has to be told. */

/* laptop, roughly 1280 and under: the rail is the first thing to give */
@media(max-width:1280px){
  :root{--rail-w:min(300px,24vw); --dock:min(150px,18vw)}
  .station-inner{padding-left:var(--s4);padding-right:var(--s4)}
  .pcard{padding:var(--s4) var(--s5)}
  .station.railed .station-inner{padding-right:calc(var(--rail-w) + var(--s4))}
  .pstage{height:clamp(180px,32dvh,340px)}
}

/* phone: one column, no rail, the chat becomes a sheet, and every control
   clears the 44px touch target */
@media(max-width:760px){
  :root{--bar-h:56px; --dock:96px; --ctl-h:46px}
  .topbar{gap:var(--s3);padding:0 var(--s4) var(--s4)}
  .mark{font-size:var(--t1)}
  .theme .theme-label{display:none}          /* the switch says it already */
  .social a{width:40px;height:40px}
  .hint,.count{display:none}

  .hero-copy .inner{padding:0 var(--s4);justify-content:flex-start;align-items:flex-end}
  .hero-copy .inner > div{width:100%}
  .hero-copy{align-items:flex-end;padding-bottom:12dvh}

  /* pinned to the foot with real padding under it, so the thumb has somewhere
     to rest and the nodes above are not crowded */
  .askbar{width:calc(100vw - var(--s4) * 2);top:auto;bottom:calc(var(--s5) + env(safe-area-inset-bottom,0px))}
  /* DELETED 2026-08-20 — `.askbar .chev,.chat-form .chev{position:static}`.
     This was the cause under every chevron symptom on a phone, and it outlived
     three attempts to fix them by moving the control. `static` puts it back in
     the FLOW of the bar's row, so it takes its own 34px beside the pill, the
     pill starts where it ends, and every left/bottom offset written anywhere
     else is silently ignored. That is why it kept landing next to the bar
     instead of in it, and why "slightly high" then became "slightly low": both
     were flow, not placement. It lives inside the pill now and is positioned
     against the pill, so there is nothing left for this to modify. */
  .askbar,.chat-form{display:flex;align-items:flex-end}

  /* On a phone the rail is a sheet, and it has to read as a layer floating
     OVER the section rather than as one more card in the same stack: a
     different surface, a deeper shadow and a hairline along its top edge,
     which is what stopped it blending into the product cards behind it. */
  .chat.rail{left:0;right:0;top:auto;bottom:0;width:auto;
    max-height:76dvh;transform:translateY(101%);
    padding:var(--s3) 0 var(--s4);
    background:var(--bg-lift);
    border-radius:var(--r-panel) var(--r-panel) 0 0;
    box-shadow:var(--lift-2), inset 0 1px 0 var(--hair);
    pointer-events:auto}
  .chat.rail.on{transform:none}
  /* the field sits at the foot of the sheet, the same way it does on the hub */
  .chat.rail .chat-head{order:1}
  .chat.rail .chat-log{order:2}
  .chat.rail .chat-form{order:3;padding-top:var(--s3)}
  .chat.rail.open .chat-log{max-height:52dvh;background:none;box-shadow:none;
    border-radius:0;margin-top:0;padding:var(--s2) var(--s4) var(--s3)}
  /* the chat sheet sits over the foot of the section, so the content has to
     end above it rather than under it */
  .station.railed .station-inner{padding-right:var(--s4);padding-bottom:30dvh}

  /* A band of its own above the heading. The core used to be written over the
     words because it was floated on top of them; now the content simply starts
     below it and nothing has to dodge anything. */
  .station-inner{padding-top:calc(var(--bar-h) + var(--dock) + var(--s5))}
  section.mod > .head{padding-right:0}
  section.mod > .head h2{font-size:var(--t5)}
  section.mod > .lede{font-size:var(--t3)}
  .card{padding:var(--s5) var(--s4)}
  .grid{gap:var(--s4)}
  .g2,.g3{grid-template-columns:1fr}
  /* the heading wraps to two or three lines on a phone, and an icon centred
     against a wrapped heading floats in the middle of it */
  .phead{grid-template-columns:52px 1fr;gap:var(--s3);align-items:start}
  .phead .appicon{width:52px;height:52px;border-radius:13px}
  .pactions{grid-column:1 / -1}
  .pactions .btn{flex:1}
  .pstage{height:clamp(140px,18dvh,200px)}
  /* a 16:9 slide at phone width is only about 170px tall, so the desktop
     case stage leaves half the box empty here */
  .ccard .pstage{height:clamp(150px,24dvh,200px)}
  .ccard{padding:var(--s5) var(--s4)}
  .pstrip{gap:var(--s2);margin-top:var(--s2)}
  /* 34px was under the 44px a thumb can reliably hit, and these are the only
     control in a card. The picture inside still reads at 34 — object-fit
     contains it — so the extra ten pixels are hit area, not scale. */
  .pthumb{height:44px;border-radius:8px}
  .pcard{padding:var(--s4)}
  .pcard .card-p{margin-top:var(--s3)}
  .appstats{gap:var(--s4) var(--s5);flex-wrap:wrap;margin-top:var(--s3)}
  .appstats div{flex:1 0 40%}
  .row-media{flex-direction:row}
  .thumb{width:56px;height:56px}
  .tl-item{padding-left:var(--s5)}
  .fig.wide{aspect-ratio:16/10}
  .field{flex-direction:column;align-items:stretch}
  .field .btn{width:100%}
}


/* The labelled chevron is gone with the control's open role. [@maaz - 2026-08-20]
   The bar opens the thread now, so there is nothing to advertise: the only
   chevron left is the one that puts an OPEN panel away, and what it does is
   visible from the fact that the panel is open. */

/* ── the phone's navigation ─────────────────────────────────
   No overlay at all. The clusters ARE the buttons: tapping the core parks them
   in a readable arrangement and their names appear. An overlay was floated
   over the scene before this, and the sphere read as switched off behind it. */
.tapme{position:fixed;left:50%;transform:translateX(-50%);z-index:12;
  font-size:var(--t1);color:var(--ink-3);letter-spacing:.02em;
  pointer-events:none;opacity:0;transition:opacity .4s var(--ease-ui);display:none}
.tapme.on{opacity:.72}
.tapme::after{content:'';display:block;width:20px;height:1px;margin:7px auto 0;
  background:currentColor;opacity:.5}
@media(max-width:760px){ .tapme{display:block} }

/* parked: the names come back and the clusters take taps */
@media(max-width:760px){
  .leaders.parked button{pointer-events:auto}
  /* plain text, not a pill: the dots are the button and the name is its label,
     so dressing the name as a control says the wrong thing about what to press */
  .leaders.parked button span{display:block;color:var(--ink);font-size:var(--t2);
    white-space:nowrap;background:none;padding:0;box-shadow:none}
  /* the CONTAINER stays transparent to taps: a child with pointer-events auto
     still receives them, and switching the container on put the whole layer
     back over the canvas so the core stopped being tappable. This is the
     second time that rule has been written; it is wrong both times. */
}

/* the core is the button on a phone, so it says so when touched */
#scene.tappable{pointer-events:auto;cursor:pointer}

@media(min-width:761px){ .navsheet{display:none !important} }
@media(max-width:760px){
  /* The names come off the clusters and the whole navigation layer stops
     taking taps, container included. Switching off only the buttons left the
     .leaders box itself, which spans the viewport, sitting over the canvas and
     swallowing every tap aimed at the core. */
  .leaders button span{display:none}
  .leaders button{pointer-events:none}
  .hub.on .leaders{pointer-events:none}
}


/* ── how far through the hero ───────────────────────────────
   Switching a browser to a mobile viewport mid-scroll lands the page halfway
   through the hero, which looks like a broken layout rather than an unfinished
   scroll. One hairline says which it is. It sits above the bar's wash, takes no
   pointer events, and goes once the hub is there, so it interferes with
   nothing. */
.prog{position:fixed;top:0;left:0;right:0;height:2px;z-index:5;
  pointer-events:none;opacity:0;transition:opacity .45s var(--ease-ui)}
.prog.on{opacity:1}
.prog i{display:block;height:100%;width:0%;background:var(--ink);
  opacity:.55;transform-origin:left;transition:width .12s linear}
@media (prefers-reduced-motion: reduce){ .prog i{transition:none} }


/* ── a picture you can actually look at ─────────────────────
   The slides are dense analyst pages -- a 6-point table, a map, a financial
   model. At the size a card gives them they are an illustration of a document
   rather than the document, so the picture opens to the whole screen and can
   be walked through from there. [@maaz - 2026-08-20]
   One lightbox serves every gallery on the site. Both business cases and
   products render the same `.pstage`, and a second implementation is a second
   thing to keep in step. */
/* position:relative, and it is the whole reason the arrows were nowhere near
   the picture. `.pnav` is absolutely positioned, and this was the element it
   was meant to resolve against -- but `.pstage` set no `position`, so they
   resolved against the initial containing block instead and pinned themselves
   to the left and right edges of the WINDOW, centred on the window. That is
   why they turned up beside unrelated cards, why they "appeared briefly while
   scrolling", and why they were never next to the slide they belong to.
   The contrast fix before this was real but it was not this. */
.pstage{cursor:zoom-in;position:relative}
/* Painted like the send button -- ink ground, page-coloured mark -- because
   `--panel` on a `--panel` surface is not a control, it is a rumour.
   MEASURED 2026-08-20: the arrow's ground against the stage it sits on came to
   **1.05:1** in the light theme and the same story in the dark one, white on
   white and dark on dark. That is why they "appeared briefly while scrolling"
   and then vanished -- they were only ever visible in the moment they crossed
   a picture with different tones under them. Opacity was never the problem and
   raising it from 0 to .82 could not have fixed it. */
.pnav{position:absolute;top:50%;transform:translateY(-50%);z-index:2;
  width:var(--ctl-h);height:var(--ctl-h);border:0;cursor:pointer;
  border-radius:var(--r-pill);background:var(--ink);color:var(--bg);
  box-shadow:var(--lift-2);display:grid;place-items:center;
  transition:opacity .18s var(--ease-ui),box-shadow .2s var(--ease-ui)}
.pnav svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:1.8;
  stroke-linecap:round;stroke-linejoin:round}
.pnav.prev{left:var(--s3)} .pnav.next{right:var(--s3)}
/* Always on the picture, not revealed by hovering it. [@maaz - 2026-08-20]
   A control that only exists once the pointer is already over the thing is a
   control most people never learn is there -- and it was invisible in the very
   screenshot asking where the arrows were. They sit at rest and firm up under
   the pointer instead. */
.pnav{opacity:.82}
.pstage:hover .pnav,.pnav:hover,.pnav:focus-visible{opacity:1}
.pnav:hover{box-shadow:var(--lift-2)}
.pnav:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
@media(hover:none){ .pnav{opacity:1} }

.lightbox{position:fixed;inset:0;z-index:2000;display:grid;place-items:center;
  background:rgba(9,12,14,.94);padding:var(--s5);
  opacity:0;transition:opacity .22s var(--ease-ui)}
/* `display` on the base rule beats what the `hidden` attribute does, which is
   set `display:none` in the UA sheet at the lowest possible priority. Without
   this line the closed lightbox is a full-screen pane at z-index 2000 over the
   entire site: nothing is visibly wrong, and nothing is clickable either.
   Caught by the 44px reach check, which measures what a tap actually lands on
   rather than how big a control's box is -- the send button read 32x32 with a
   reach of 0x0, meaning covered. */
.lightbox[hidden]{display:none}
.lightbox.on{opacity:1}
.lb-img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;
  border-radius:var(--r-ctl);display:block}
.lb-btn{position:absolute;width:var(--ctl-h);height:var(--ctl-h);border:0;cursor:pointer;
  border-radius:var(--r-pill);background:var(--panel);color:var(--ink);
  box-shadow:var(--lift);display:grid;place-items:center;
  transition:box-shadow .2s var(--ease-ui)}
.lb-btn svg{width:20px;height:20px;stroke:currentColor;fill:none;stroke-width:1.8;
  stroke-linecap:round;stroke-linejoin:round}
.lb-btn:hover{box-shadow:var(--lift-2)}
.lb-btn:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.lb-prev{left:var(--s5);top:50%;transform:translateY(-50%)}
.lb-next{right:var(--s5);top:50%;transform:translateY(-50%)}
.lb-close{right:var(--s5);top:var(--s5)}
.lb-count{position:absolute;left:50%;bottom:var(--s5);transform:translateX(-50%);
  margin:0;font-family:var(--m);font-size:var(--t1);color:#E9ECEE;
  background:rgba(9,12,14,.6);padding:6px 12px;border-radius:var(--r-pill)}
/* The scrim is the same near-black in both themes on purpose: a lightbox is a
   dark room, and a light one flares against the picture it is showing. The
   count is therefore given a fixed light ink rather than a theme token, since
   --ink is black in the light theme and would vanish into the scrim. */
@media(max-width:760px){
  .lightbox{padding:var(--s4)}
  .lb-prev{left:var(--s3)} .lb-next{right:var(--s3)}
  .lb-close{right:var(--s3);top:var(--s3)}
  /* On a phone the arrows sit low, clear of the picture's middle, so a thumb
     reaches them and they do not cover what they are moving through. */
  .lb-prev,.lb-next{top:auto;bottom:var(--s4);transform:none}
  .lb-count{bottom:calc(var(--s4) + var(--ctl-h) + var(--s3))}
}

/* An icon inside a button is not decoration here — it is what a person reads
   first. Sized to the type, given a real gap, and never allowed to squash. */
.pactions .btn{display:inline-flex;align-items:center;gap:7px}
.pactions .btn svg{width:15px;height:15px;flex:none;display:block}
.pactions button.btn{font:inherit;cursor:pointer;border:0}
.pactions button.btn:disabled{opacity:.6;cursor:progress}

/* A repository button that exists before the repository does. It must read as
   "not yet", never as "broken" — so it is quieter than a real control, it does
   not respond to a hover or a press, and it carries the word rather than
   leaving a person to work out why nothing happened. */
.pactions .repo-soon{opacity:.55;cursor:default;user-select:none}
.pactions .repo-soon em{font-style:normal;font-family:var(--m);font-size:10px;
  letter-spacing:.06em;text-transform:uppercase;opacity:.8;margin-left:1px}


/* ── Media ──────────────────────────────────────────────────
   The one section that is not a card grid. A grid says "six things of equal
   weight"; this page has a shape — one thing he made, then a run of things he
   appeared in — and rows carry a view count and a rank without straining.
   Everything below is the v2 language: one neutral ramp, no accent hue, weight
   and elevation doing the work colour is not allowed to do. */

/* the show, given the width it earns by being his */
.mshow{display:grid;grid-template-columns:200px 1fr;gap:var(--s6);align-items:start;
  background:var(--panel);border-radius:var(--r-panel);padding:var(--s6);
  box-shadow:inset 0 0 0 1px var(--hair)}
.mshow-art{width:200px;height:200px;border-radius:var(--r-ctl);object-fit:cover;
  box-shadow:var(--lift)}
.mshow-name{font-size:var(--t5);font-weight:500;letter-spacing:-.02em;
  margin:2px 0 var(--s3)}
.mshow-on{margin-top:var(--s5)}

/* the episodes: a list, numbered in mono, so it reads as a run rather than
   three separate facts */
.mep{list-style:none;margin:var(--s5) 0 0;padding:0;
  border-top:1px solid var(--hair)}
.mep li{display:grid;grid-template-columns:auto 1fr auto;gap:var(--s4);
  align-items:baseline;padding:9px 0;border-bottom:1px solid var(--hair)}
.mep-n{font-family:var(--m);font-size:var(--t1);color:var(--ink-3)}
.mep-t{font-size:var(--t2);color:var(--ink-2);min-width:0}
.mep-d{font-family:var(--m);font-size:var(--t1);color:var(--ink-3);white-space:nowrap}

/* the index heading, and the only place a total is stated */
.mhead{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s4);
  margin:var(--s8) 0 0;padding-bottom:var(--s3);border-bottom:1px solid var(--ink)}
.mhead-t{font-size:var(--t3);font-weight:500;letter-spacing:-.01em;margin:0}
.mhead-n{font-family:var(--m);font-size:var(--t1);color:var(--ink-2);
  font-variant-numeric:tabular-nums}

/* the appearances themselves */
.mlist{list-style:none;margin:0;padding:0}
.mrow{border-bottom:1px solid var(--hair)}
.mrow-a{display:grid;grid-template-columns:34px 1fr 92px 128px 68px;gap:var(--s4);
  align-items:baseline;padding:var(--s4) var(--s3);text-decoration:none;color:inherit;
  border-radius:var(--r-ctl);
  transition:background .18s var(--ease-ui)}
a.mrow-a:hover{background:var(--panel-2)}
a.mrow-a:focus-visible{outline:2px solid var(--ink);outline-offset:-2px}
.mrow-i{font-family:var(--m);font-size:var(--t1);color:var(--ink-3);
  font-variant-numeric:tabular-nums}
.mrow-main{min-width:0}
.mrow-t{display:block;font-size:var(--t3);letter-spacing:-.01em;color:var(--ink)}
/* the arrow appears only where there is somewhere to go, and it is part of the
   title's line so it reads as "this one opens" rather than as an icon column */
.mrow-go{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;color:var(--ink-3);
  margin-left:7px;vertical-align:-1px;
  transition:color .18s var(--ease-ui),transform .18s var(--ease-ui)}
a.mrow-a:hover .mrow-go{color:var(--ink);transform:translate(1px,-1px)}
.mrow-note{display:block;font-size:var(--t1);color:var(--ink-3);margin-top:3px;
  max-width:60ch}
.mrow-kind,.mrow-where{font-size:var(--t1);color:var(--ink-3)}
.mrow-v{font-family:var(--m);font-size:var(--t2);color:var(--ink-2);
  text-align:right;font-variant-numeric:tabular-nums}
/* a row with no count gets a dash, not a zero — nobody measured it, and a zero
   would be a claim that somebody did */
.mrow-v i{font-style:normal;color:var(--ink-3)}
/* a row that goes nowhere must not pretend it might */
.mrow-flat .mrow-a{cursor:default}

@media(max-width:760px){
  /* the artwork stops being a column and becomes a header, because 200px of
     cover beside a 150px column of words is neither */
  .mshow{grid-template-columns:1fr;gap:var(--s5);padding:var(--s5)}
  .mshow-art{width:112px;height:112px}
  .mep li{grid-template-columns:auto 1fr;row-gap:2px}
  .mep-d{grid-column:2;font-size:10px}
  /* the index keeps the title and the count, and drops the two columns a phone
     has no room for — they are context, not the point */
  .mrow-a{grid-template-columns:26px 1fr 58px;gap:var(--s3);padding:var(--s4) 2px}
  .mrow-kind,.mrow-where{display:none}
  .mrow-t{font-size:var(--t2)}
  .mhead{margin-top:var(--s7)}
}


/* ── About: a collage board ─────────────────────────────────
   Tiles of different sizes on one grid. The differing sizes are what make it
   read as a pinned board rather than a gallery of equal things — a grid where
   everything is the same size is a table with pictures in it. */

/* his half, held open and visibly empty rather than filled on his behalf */

.ab-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s4);
  margin:var(--s8) 0 var(--s5);padding-bottom:var(--s3);border-bottom:1px solid var(--ink)}
.ab-head-t{font-size:var(--t3);font-weight:500;letter-spacing:-.01em;margin:0}
.ab-head-n{font-family:var(--m);font-size:var(--t1);color:var(--ink-2)}

.ab-ep{list-style:none;margin:var(--s4) 0 var(--s4);padding:0;border-top:1px solid var(--hair)}
.ab-ep li{display:grid;grid-template-columns:auto 1fr auto;gap:var(--s3);
  align-items:baseline;padding:7px 0;border-bottom:1px solid var(--hair)}
.ab-ep-n{font-family:var(--m);font-size:var(--t1);color:var(--ink-3)}
.ab-ep-t{font-size:var(--t1);color:var(--ink-2);min-width:0}
.ab-ep-d{font-family:var(--m);font-size:10px;color:var(--ink-3);white-space:nowrap}

/* ── About: the showpiece ───────────────────────────────────
   REBUILT 2026-08-22. The board used to be twelve Instagram tiles and a set of
   quote cards; he cut both and asked for photographs of the work instead. So
   the section leads with ONE picture at a size that lets it be a picture, and
   the rest are frames rather than tiles — no play glyph, no link out, a
   caption that says what it is. */
.ab-lead{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1fr);align-items:center;
  gap:var(--s7);align-items:center;margin-bottom:var(--s7)}
.ab-hero{margin:0;border-radius:var(--r-panel);overflow:hidden;background:var(--stage)}
/* The hero is a portrait frame now — the hoodie photograph he chose is 9:16
   [@maaz · 2026-08-22] — so it is capped and cropped rather than left to run
   to its natural height, which pushed the opening paragraph a screen down and
   left the top of the section empty. */
.ab-hero img{width:100%;height:auto;max-height:520px;object-fit:cover;
  object-position:50% 38%;display:block}
.ab-lead-p{font-size:var(--t4);line-height:1.45;letter-spacing:-.01em;
  color:var(--ink);margin:0;max-width:34ch}
/* A definition list, because these ARE definitions — a label and its value.
   Rows rather than a table so the values can run to two lines on a phone. */
.ab-facts{margin:var(--s6) 0 0;display:flex;flex-direction:column;gap:var(--s4)}
.ab-facts > div{display:grid;grid-template-columns:88px 1fr;gap:var(--s4);
  align-items:baseline;padding-top:var(--s3);border-top:1px solid var(--hair)}
.ab-facts dt{font-family:var(--m);font-size:var(--t1);color:var(--ink-3);
  letter-spacing:.02em}
.ab-facts dd{margin:0;font-size:var(--t2);color:var(--ink-2);max-width:44ch}

/* Frames, not tiles. Columns rather than a grid so mixed heights pack instead
   of leaving holes, which is what a board of photographs actually looks like. */
/* ── the rail's opening offer ──────────────────────────────────────────
   Four things a visitor can press when they have not thought of a question.
   Only inside a section and only on a desktop-width screen; the JS decides
   both, so there is no media query here doing it a second time. */
.chat-sugg{display:flex;flex-direction:column;align-items:flex-start;gap:var(--s2);
  padding:var(--s2) 0 var(--s3)}
.chat-sugg-t{margin:0 0 var(--s2);font-size:var(--t1);color:var(--ink-3);
  font-family:var(--m);letter-spacing:.02em}
.chat-chip{display:block;max-width:100%;text-align:left;
  font:inherit;font-size:var(--t2);line-height:1.35;color:var(--ink);
  background:var(--panel-2,var(--panel));border:1px solid var(--hair);
  border-radius:var(--r-pill);padding:9px 14px;cursor:pointer;
  transition:background .18s var(--ease-ui),border-color .18s var(--ease-ui),
             transform .18s var(--ease-ui)}
.chat-chip:hover{background:var(--panel);border-color:var(--ink-3);transform:translateX(2px)}
.chat-chip:focus-visible{outline:2px solid var(--ink);outline-offset:2px}

/* ── THE MOODBOARD ────────────────────────────────────────────────────
   Laid out by `js/collage.js` as justified rows: every row is filled to the
   container's exact width, so the board has no gaps, and every tile keeps its
   own aspect ratio, so no photograph is cropped. Both were faults of the span
   grid this replaces — a grid cell has a shape and the thing inside it does
   not, which left holes where cells did not pack and cut the top of a head off
   where a picture did not match its cell. [@maaz · 2026-08-22]

   The engine writes width and height onto each child in pixels. Everything
   here is appearance only; nothing in this block decides where a tile goes. */
.bd{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start}
.bd > *{flex:none}

.bd-i{margin:0;position:relative;overflow:hidden;border-radius:var(--r-panel);
  background:var(--stage)}
.bd-i img{width:100%;height:100%;object-fit:cover;display:block;
  filter:saturate(.92);transition:filter .3s var(--ease-ui),transform .5s var(--ease-ui)}
.bd-i:hover img{filter:saturate(1);transform:scale(1.02)}

/* A NOTE IS PINNED, NOT TILTED. [@maaz · 2026-08-22] "The tilt framing that you
   are giving the description cards, I don't think it is working. If you want,
   add a small design, like a pin design, on the corner so that it feels like it
   is pinned in." So the rotation is gone and a pin head sits in the corner: a
   small disc with a ring and a short cast shadow, which reads as a tack without
   needing a colour the palette does not have. */
.bd-n{position:relative;
  background:var(--panel-2,var(--panel));border:1px solid var(--hair);
  border-radius:var(--r-ctl);
  padding:var(--s5) var(--s5) var(--s5);
  /* A note's box is sized by its row, so it usually has slack. Centring the
     text in it reads as deliberate; leaving it pinned to the top reads as a
     card that failed to fill. The pin is positioned absolutely, so it stays in
     the corner either way. */
  display:flex;flex-direction:column;justify-content:center;gap:var(--s2);
  overflow:hidden;
  box-shadow:0 10px 26px -14px rgba(0,0,0,.55), 0 2px 5px -3px rgba(0,0,0,.35);
  transition:box-shadow .25s var(--ease-ui),transform .25s var(--ease-ui)}
.bd-n:hover{transform:translateY(-2px);
  box-shadow:0 16px 34px -16px rgba(0,0,0,.6), 0 2px 5px -3px rgba(0,0,0,.35)}
.bd-pin{position:absolute;top:12px;left:14px;width:9px;height:9px;border-radius:50%;
  background:var(--ink-3);
  box-shadow:0 0 0 2.5px var(--panel-2,var(--panel)),
             0 0 0 3.5px var(--hair),
             2px 3px 4px -1px rgba(0,0,0,.5)}
/* THE CARDS CARRY THE WRITING, so they are set like writing rather than like a
   caption. [@maaz · 2026-08-22] "There's a lot of empty space inside the cards.
   Use a font style that is professional but creative. Fill in the space and
   increase the font size." The heading is set in the display face at a real
   size, the body a step up from the old caption size, and the measure is left
   wide so a card fills rather than runs in a narrow gutter down the middle. */
/* No new typeface is introduced here. Which face this site is set in is still
   his open decision (Q-006 on the Open Board) and it is a matter of taste, so
   the character comes from size, weight and tracking within the stack that is
   already here rather than from a font nobody picked. */
.bd-nt{font-size:var(--t4);font-weight:500;
  letter-spacing:-.022em;line-height:1.15;margin:0 0 var(--s2)}
.bd-np{margin:0;font-size:var(--t3);line-height:1.5;color:var(--ink-2);
  letter-spacing:-.005em;text-wrap:pretty}
.bd-cv .bd-np{color:var(--ink-2)}
.bd-c .bd-np{font-size:var(--t3);line-height:1.55;max-width:64ch}
.bd-c .bd-nt{font-size:var(--t5,var(--t4))}

/* the panel and the show take a full row at their own height */
.bd-c{display:grid;grid-template-columns:minmax(0,.62fr) minmax(0,1fr);gap:var(--s5);
  background:var(--panel);border:1px solid var(--hair);border-radius:var(--r-panel);
  padding:var(--s4)}
/* The picture stretches to whatever the writing needs rather than setting the
   card's height itself. It had a 340px cap and the text column was shorter than
   that, so the card carried a band of nothing under its own paragraph.
   [@maaz · 2026-08-22] "Too much empty space. Maximize for readability." */
/* The picture is capped so the WRITING sets the card's height. Uncapped, a
   portrait at this column width solved to ~500px and the paragraph sat in the
   top half with a screen of nothing under it. The crop is taken from the top of
   the frame, where the faces are, so capping it never takes a head off. */
.bd-c-fig{margin:0;border-radius:var(--r-ctl);overflow:hidden;background:var(--stage);
  align-self:stretch;min-height:0;max-height:340px}
.bd-c-fig img{width:100%;height:100%;object-fit:cover;display:block;
  object-position:50% 22%}
.bd-c-b{min-width:0;display:flex;flex-direction:column;align-items:flex-start;
  gap:var(--s3);justify-content:center;padding:var(--s2) 0}
.bd-c-m{margin:0;font-family:var(--m);font-size:var(--t1);color:var(--ink-3)}
.bd-c-b .btn{margin-top:var(--s2);white-space:nowrap;align-self:flex-start}
.bd-c-b .ab-ep{margin:var(--s3) 0 0;width:100%}
.bd-c-b .rowline{margin-top:var(--s3)}

@media(max-width:760px){
  .bd-c{grid-template-columns:1fr}
  .bd-c-fig{max-height:240px}
}

.ab-mason{columns:2;column-gap:var(--s4);margin-top:var(--s4)}
.ab-fr{break-inside:avoid;margin:0 0 var(--s4);width:100%;
  border-radius:var(--r-ctl);overflow:hidden;background:var(--stage);position:relative}
/* EVERY FRAME KEEPS ITS OWN PROPORTIONS. Forcing a common height and cropping
   to fill cost the thing each photograph was chosen for: the nameplates came
   off the co-hosting frame, and the podcast table lost the person on the right
   of it. The pictures here are landscape stills, phone portraits and a video
   frame, and a grid that pretends they are one shape has to cut something. */
.ab-beat-figs{align-items:start}
.ab-beat-figs .ab-fr{margin:0}
.ab-beat-figs .ab-fr img{height:auto;object-fit:contain;aspect-ratio:auto}
.ab-fr img{width:100%;height:auto;display:block;
  filter:saturate(.62);transition:filter .35s var(--ease-ui)}
.ab-fr:hover img{filter:saturate(1)}
.ab-fr figcaption{padding:var(--s3) var(--s4) var(--s4);
  font-size:var(--t1);color:var(--ink-3);background:var(--panel)}

/* An appearance is a picture and a reason to press the button beside it. The
   embeds came out because LinkedIn refuses to play inside a frame. */
.ab-apps{display:flex;flex-direction:column;gap:var(--s6);margin-top:var(--s4)}
/* The picture takes the larger half, not the smaller one. At 340px the figure
   left a third of the card empty to the right of four lines of text, which is
   the same hole the filmstrip had. He asked for pictures; give them the room. */
.ab-app{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);
  gap:var(--s6);align-items:center;background:var(--panel);
  border-radius:var(--r-panel);box-shadow:var(--lift);padding:var(--s5);margin:0}
.ab-app-fig{margin:0;border-radius:var(--r-ctl);overflow:hidden;background:var(--stage)}
/* One band for all of them. The photographs are a mix of portrait and
   landscape, and a 768x1024 portrait at half a card's width made a 690px-tall
   card with four lines of text floating in the middle of the other half.
   Cropped a little high, because in every one of these the people are above
   the centre and the table is below it. */
.ab-app-fig img{width:100%;aspect-ratio:4/3;object-fit:cover;
  object-position:center 32%;display:block}
.ab-app-fig figcaption{padding:var(--s2) var(--s3) var(--s3);
  font-size:var(--t1);color:var(--ink-3)}
.ab-app-art img{aspect-ratio:1;object-fit:cover}
/* the show's cover is a square and does not want a photograph's width */
.ab-app-show{grid-template-columns:minmax(0,.8fr) minmax(0,1fr)}
.ab-app-body{min-width:0;display:flex;flex-direction:column;align-items:flex-start;gap:var(--s2);max-width:52ch}
.ab-app-t{font-size:var(--t4);font-weight:500;letter-spacing:-.02em;margin:0}
.ab-app-m{margin:0;font-family:var(--m);font-size:var(--t1);color:var(--ink-3)}
.ab-app-body .card-p{margin-top:var(--s2);max-width:58ch}
.ab-app-body .btn{margin-top:var(--s3)}
.ab-app-body .btn svg{width:12px;height:12px;stroke:currentColor;fill:none;
  stroke-width:2;stroke-linecap:round;stroke-linejoin:round;margin-left:6px}
.ab-app .ab-ep{margin-top:var(--s4)}
.ab-app .rowline{margin-top:var(--s4)}

/* ── The architecture diagram ─────────────────────────────────────────
   Drawn in the document rather than shipped as a picture, so the labels are
   real text: selectable, searchable, reflowable, and readable by a screen
   reader. One neutral ramp, elevation and contrast doing the work of colour,
   same as everything else here. */
/* The diagram lives on a gallery slide now, so it fills the stage rather than
   sitting in a panel of its own under the card. [@maaz · 2026-08-22] */
/* The stage is a fixed height for photographs, which is right: a gallery whose
   frame resizes per picture jumps as you walk it. A diagram is not a
   photograph — it is text with rules around it — so on the architecture slide
   the stage takes the diagram's own height instead of scrolling it. A scroller
   inside a page that scrolls is the defect I had already fixed once on the
   merged cards; it is not better here. */
.parch-slide{display:none;background:var(--stage);border-radius:inherit;
  padding:var(--s4)}
.pstage.on-arch .parch-slide{display:block}
.pstage.on-arch{height:auto;background:var(--stage);cursor:default}
.parch-slide .dgm{background:none;padding:0}

/* the thumbnail for it is a label, because a diagram at thumbnail size is a
   grey smudge and a word is not */
.pthumb-arch{display:grid;place-items:center;background:var(--panel-2,var(--panel))}
.pthumb-lbl{font-family:var(--m);font-size:10px;line-height:1.3;letter-spacing:.04em;
  text-transform:uppercase;color:var(--ink-3);text-align:center}
.pthumb-arch.on .pthumb-lbl{color:var(--ink)}

/* ── the nudge in the reserved band ────────────────────────────────
   Its top is set from script, the way `tapme` sets its own, because the space
   it centres in is whatever is left under a chat panel whose height depends on
   how many chips are offered. A fixed offset would be right at one viewport.

   Quiet by construction: the monospace label size, `--ink-3`, and no ground of
   its own. It is the same register as "Scroll trains it" and "Tap the core",
   which are the site's other two hints, and it should read as a caption rather
   than as something competing with the chips above it. */
.railhint{
  position:fixed; right:0; width:var(--rail-w); padding:0 var(--s5);
  margin:0; text-align:center; z-index:6; pointer-events:none;
  font-family:var(--m); font-size:11px; line-height:1.5; letter-spacing:.02em;
  color:var(--ink-3); opacity:0; transition:opacity .3s var(--ease-ui);
}
.railhint.on{opacity:1}
/* A phone has no reserved band and no chips; it has the bar and the sections
   themselves, so there is nothing here to explain and no room to explain it. */
@media (max-width:759px){ .railhint{display:none} }
@media (prefers-reduced-motion: reduce){ .railhint{transition:none} }

/* ── phone captures ────────────────────────────────────────────────
   CASEY is a phone product and its voice screen has no desktop equivalent, so
   its captures are 390x844. Contained in the same stage the landscape shots
   use, one of them painted 158px wide inside 968px — a sliver on a dead panel,
   and the thumbnails were hairlines. MEASURED, not guessed: 968x342 stage,
   158px of picture.

   So a phone gallery is a different shape rather than the same shape used
   badly. The stage becomes a frame and a sentence: the capture at its own
   proportion on the left, and beside it the description that already travels
   with the slide for anyone who cannot see it. The frame does NOT resize per
   picture — every phone shot is the same 390x844 — which is the rule the
   landscape stage follows too. */
/* MEASURED, and the reason for the side padding: the arrows are 44px pinned
   12px inside the stage, and a left-aligned phone started at 24px -- the
   Previous arrow sat on the capture. The group is centred and the sides are
   kept clear of both. */
.pstage.phones{height:clamp(300px,54dvh,520px);display:flex;align-items:center;
  justify-content:center;gap:var(--s5);padding:var(--s5) 68px}
.pstage.phones img.pmain{width:auto;height:100%;flex:0 0 auto;
  border-radius:14px;box-shadow:0 8px 30px rgba(0,0,0,.28),0 0 0 1px var(--hair)}
.pcap{flex:1 1 0;min-width:0;font-size:var(--t2);line-height:1.5;color:var(--ink-2);
  max-width:44ch}
/* on the diagram slide the stage is the diagram and nothing else: the caption
   describes a picture that is not on screen, and the flex row would put the
   lanes beside it */
.pstage.phones.on-arch{display:block;padding:0}
.pstage.phones.on-arch .pcap{display:none}

/* a thumbnail keeps the shape of the thing it stands for, or it is a hairline
   in a wide box. The architecture thumb is a word, so it keeps its own width. */
.pstrip.phones{flex-wrap:wrap}
.pstrip.phones .pthumb{flex:0 0 auto;width:auto;height:clamp(72px,11dvh,104px);
  aspect-ratio:390/844}
.pstrip.phones .pthumb-arch{aspect-ratio:auto;width:104px}

/* ON A PHONE THE SENTENCE COMES OFF AND THE FRAME GOES BACK TO A FIXED HEIGHT.
   The caption exists to fill the band beside a portrait capture, and at this
   width there is no band -- a phone shot in a phone viewport already fits.
   Keeping it made the stage 592px tall, and that is not a taste point:
   MEASURED at 390x844, the gallery arrow centred in a stage that tall landed
   at y=814 while the chat bar occupies 782 to 828, so the arrow sat behind it
   and a tap went to the chat instead. The site's own gallery check caught it.
   A phone gallery is now the same shape as every other gallery here, which is
   also why its arrows land where those arrows land. */
@media (max-width:760px){
  .pstage.phones{height:clamp(260px,40dvh,380px);padding:var(--s4);gap:0}
  .pstage.phones img.pmain{height:100%;width:auto;max-width:100%}
  .pstage.phones .pcap{display:none}
  .pstrip.phones .pthumb{height:72px}
}

.dgm{display:flex;flex-direction:column;background:var(--stage);
  border-radius:var(--r-panel);padding:var(--s5);overflow-x:auto}
.dgm-lane{display:flex;flex-direction:column;gap:var(--s3);min-width:0}
.dgm-lt{font-family:var(--m);font-size:10px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-3)}
.dgm-row{display:flex;flex-wrap:wrap;gap:var(--s3)}
.dgm-n{flex:1 1 auto;min-width:132px;background:var(--panel);
  border:1px solid var(--hair);border-radius:var(--r-ctl);
  padding:var(--s3) var(--s4);display:flex;flex-direction:column;gap:2px}
.dgm-nn{font-size:var(--t1);color:var(--ink);line-height:1.35}
.dgm-ns{font-family:var(--m);font-size:10px;line-height:1.45;color:var(--ink-3)}

/* the connector between lanes: a hairline with a chevron, not an arrowhead
   glyph — a text arrow inherits the font and drifts with it */
.dgm-link{height:var(--s5);display:flex;justify-content:center;align-items:center}
.dgm-link span{width:1px;height:100%;background:var(--hair);position:relative}
.dgm-link span::after{content:'';position:absolute;left:50%;bottom:0;
  width:5px;height:5px;border-right:1px solid var(--hair);
  border-bottom:1px solid var(--hair);
  transform:translate(-50%,1px) rotate(45deg)}

/* the three lines under the diagram: problem, call, trade */
.ptech{margin:var(--s5) 0 0;display:flex;flex-direction:column;gap:var(--s4)}
.ptech > div{display:grid;grid-template-columns:104px minmax(0,1fr);gap:var(--s4);
  align-items:baseline}
.ptech dt{font-family:var(--m);font-size:10px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-3)}
.ptech dd{margin:0;font-size:var(--t1);line-height:var(--lh-body);color:var(--ink-2);
  max-width:70ch}

@media(max-width:760px){
  .dgm{padding:var(--s4)}
  .dgm-n{min-width:100%}
  .ptech > div{grid-template-columns:1fr;gap:var(--s2)}
}

/* The qualifications line. Deliberately the quietest thing in the column:
   he asked for training and credentials to "take a slight backseat" on a
   section that is now about the person. [@maaz · 2026-08-22] */
.ab-cv{margin:var(--s6) 0 0;padding-top:var(--s4);border-top:1px solid var(--hair);
  font-size:var(--t1);line-height:1.6;color:var(--ink-3);max-width:46ch}

/* Podcast platform buttons. The glyph only, drawn inline — three brand
   wordmarks hotlinked from three CDNs is three ways for this row to go blank,
   and hosting the wordmarks is a licence question nobody asked. */
.btn.pod{display:inline-flex;align-items:center;gap:8px}
.pod-g{display:inline-flex;width:16px;height:16px;color:var(--ink-2)}
.pod-g svg{width:100%;height:100%;display:block}
.btn.pod:hover .pod-g{color:var(--ink)}

/* THE TILE OVERLAYS — restored 2026-08-22. Both of these were lost when a
   range replace swallowed a neighbouring block, and the loss was invisible in
   the stylesheet: an unstyled <svg> still renders, so every Instagram tile
   showed a full-size black triangle in a grey box UNDER its picture instead of
   a small glyph over it. Reported as an Instagram embed defect; it was ours,
   and there is no embed. `.ab-cap` went with it, so the date chip lost its
   plate too. Taken back from commit 3957225. */
.ab-cap{position:absolute;left:6px;bottom:6px;z-index:2;
  font-family:var(--m);font-size:10px;letter-spacing:.02em;color:#E9ECEE;
  background:rgba(9,12,14,.62);padding:3px 7px;border-radius:var(--r-pill)}
.ab-play{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:2;
  width:34px;height:34px;fill:#E9ECEE;filter:drop-shadow(0 1px 6px rgba(0,0,0,.55));
  pointer-events:none;opacity:.92}

/* talks */
.ab-embed{position:relative;aspect-ratio:16/10;background:var(--stage)}
.ab-embed iframe{width:100%;height:100%;border:0;display:block}
/* a still that becomes the embed on press — see the note in about.js on why
   the iframe is not there from the start */
.ab-facade{position:absolute;inset:0;width:100%;height:100%;border:0;cursor:pointer;
  background:var(--stage);color:var(--ink);display:grid;place-items:center;
  gap:10px;grid-auto-flow:row;font:inherit;font-size:var(--t2);
  transition:background .2s var(--ease-ui)}
.ab-facade:hover{background:var(--panel-2)}
.ab-facade:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}
/* the facade with a real still behind it: the frame fills the box, the play
   glyph and its label sit on a scrim over it */
.ab-facade.has-still{padding:0;display:block;position:absolute;inset:0}
.ab-still{width:100%;height:100%;object-fit:cover;display:block}
.ab-facade-c{position:absolute;inset:0;display:grid;place-items:center;
  grid-auto-flow:row;gap:10px;color:#E9ECEE;
  background:linear-gradient(rgba(9,12,14,.30),rgba(9,12,14,.55));
  transition:background .2s var(--ease-ui)}
.ab-facade.has-still:hover .ab-facade-c{background:linear-gradient(rgba(9,12,14,.18),rgba(9,12,14,.44))}
/* THE LABEL SITS ON A GROUND OF ITS OWN, not on the scrim over the photograph.
   Reported 2026-08-23 by the site's own text check at 1.09:1, and BOTH halves
   of that were worth acting on. The number is wrong -- the checker reads
   `backgroundColor`, the scrim is a `background-image` gradient, so it walked
   past the scrim and past the photograph and measured white words against the
   light panel behind everything. But the true figure is not comfortable
   either: over the bright frame of the pitch video the scrim leaves roughly
   4:1, under the 4.5 this site holds every word to.

   A solid pill answers both. It is what a person reads against, and it is what
   any checker measures against, so the two agree from here on. */
.ab-facade-c > span{background:rgba(9,12,14,.72);padding:6px 12px;
  border-radius:999px;line-height:1.2}
.ab-facade.has-still .ab-play{position:static;transform:none;width:44px;height:44px;
  fill:#E9ECEE;opacity:1}
.ab-facade .ab-play{position:static;transform:none;width:38px;height:38px;
  fill:var(--ink);filter:none;opacity:1}

@media(max-width:900px){
  .ab-lead{grid-template-columns:1fr;gap:var(--s5)}
  .ab-beat{grid-template-columns:1fr;gap:var(--s4)}
  .ab-beat.flip .ab-beat-figs{order:0}
  .ab-beat-figs[data-n="3"] .ab-fr:first-child{grid-column:span 2}
  .ab-app{grid-template-columns:1fr;gap:var(--s5)}
  .ab-app-fig{max-width:420px}
}
@media(max-width:760px){
  /* one column of frames on a phone: two 160px-wide photographs of a room
     full of people are two photographs of nothing */
  .ab-mason{columns:1}
  .ab-fr{margin-bottom:var(--s3)}
  .ab-facts > div{grid-template-columns:1fr;gap:2px}
  .ab-app{padding:var(--s4)}
  .ab-play{width:26px;height:26px}
  .ab-head{margin-top:var(--s7)}
}


/* ── Certifications ─────────────────────────────────────────
   Ten credentials in three groups. A card carries the issuer, the credential,
   when it was awarded, and one link — to the issuer's own verification page
   where one exists, and to the certificate where it does not. */
.ccert-grid{margin-bottom:var(--s6)}
.ccert{display:flex;flex-direction:column;align-items:flex-start;gap:var(--s2)}
.ccert-top{display:flex;align-items:center;gap:var(--s3);width:100%}
.ccert-top .lbl{margin-right:auto;min-width:0}
/* A white plate under every mark, in both themes. Ten brands, ten palettes,
   and several are dark-on-white with no transparency — a navy London Business
   School block and a black Cornell wordmark cannot both sit on a dark panel
   unaided. One shape for all of them is also what keeps a row of logos from
   reading as a sponsor banner. */
/* 148x42 and not 112x34, which was the first try: these lockups run from 1.4:1
   (the Salesforce cloud) to 9.8:1 (Atlassian University), and at 112 wide the
   wide ones were 13px tall and unreadable. Width is what a wide wordmark needs,
   not height. */
/* The plate fits the mark rather than the mark fitting the plate. A fixed
   148px slot letterboxed the near-square ones — the Salesforce cloud and the
   London Business School block sat marooned in a wide white field. Constant
   HEIGHT is what makes a row of logos look deliberate; constant width is what
   makes it look like a sponsor banner. */
.ccert-mark{flex:none;height:42px;width:auto;max-width:158px;
  border-radius:var(--r-ctl);background:#FFFFFF;
  box-shadow:inset 0 0 0 1px var(--hair);
  display:inline-flex;align-items:center;justify-content:center;
  padding:0 9px;overflow:hidden}
/* ABSOLUTE, and that is the whole point of it. Two earlier versions cropped
   London Business School mid-word. `max-height:100%` on an auto-sized image
   did nothing; `height:100%` on a grid item did nothing either, because the
   grid row was sized BY the image — computed height came back 98px inside a
   42px plate, and `overflow:hidden` turned that into a crop. Positioned
   against a fixed-height block there is no circularity left to resolve. */
/* An ABSOLUTE height and no percentage anywhere, which is the version that
   finally held. `max-height:100%` on an auto-sized image did nothing, and
   `height:100%` on a grid item resolved against a row the image itself was
   sizing — computed height came back 98px inside a 42px plate, and
   `overflow:hidden` turned that into London Business School cropped mid-word. */
.ccert-mark img{height:32px;width:auto;max-width:140px;
  object-fit:contain;display:block}
/* The designation's own short name, which is what it is known by on a CV. Set
   in the mono face so it reads as a code rather than as a word. */
.ccert-abbr{font-family:var(--m);font-size:var(--t1);letter-spacing:.06em;
  color:var(--ink);background:var(--panel-2);border:1px solid var(--hair);
  padding:3px 9px;border-radius:var(--r-pill);flex:none}
.ccert-t{font-size:var(--t3);font-weight:500;letter-spacing:-.01em;margin:2px 0 0}
.ccert-note{margin:0;font-size:var(--t1);color:var(--ink-3)}
.ccert-meta{margin:var(--s4) 0 0;display:flex;flex-wrap:wrap;gap:var(--s3) var(--s6);
  width:100%;padding-top:var(--s3);border-top:1px solid var(--hair)}
.ccert-meta > div{display:flex;flex-direction:column;gap:2px}
.ccert-meta dt{font-family:var(--m);font-size:10px;letter-spacing:.08em;
  text-transform:uppercase;color:var(--ink-3)}
.ccert-meta dd{margin:0;font-size:var(--t1);color:var(--ink-2)}
.ccert-id{font-family:var(--m)}
/* An expired credential gets said, not styled into a warning. The page carries
   no accent hue, so this is a rule and a weight rather than a colour — and it
   is a sentence, because a chip reading "expired" is easy to skim past. */
.ccert-lapsed{margin:var(--s4) 0 0;padding-left:var(--s4);
  border-left:2px solid var(--ink);font-size:var(--t1);color:var(--ink-2);max-width:46ch}
.ccert-lapsed b{color:var(--ink);font-weight:500}
.ccert-go{margin-top:auto;align-self:flex-start}
.ccert-go svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;margin-left:6px}

/* ── Startups: the dial ─────────────────────────────────────
   One mechanic, two axes. The dial indexes POSITION IN THE RECORD — time for
   CASEY, the user flow for TKaen — which is what lets a company with a year
   behind it and a prototype with 142 screens sit on the same page without
   either being padded to match the other. */
/* Each startup is a card. [@maaz · 2026-08-20] They were floating in the
   section with the empty half of every alternating row reading as a hole; a
   panel gives the whole record an edge and turns that space into margin
   instead of absence. */
.su{margin:0 0 var(--s6);padding:var(--s6)}
.su + .su{margin-top:var(--s6)}
.su-id{display:flex;align-items:center;gap:var(--s4);min-width:0}
/* the mark, at the size a logo is read at rather than the size a favicon is */
.su-mark{width:auto;height:30px;max-width:150px;object-fit:contain;display:block;flex:none}
.su-stats{display:flex;flex-wrap:wrap;gap:var(--s7);margin-top:var(--s5);
  padding-top:var(--s4);border-top:1px solid var(--hair)}
.su-stats .v{font-size:var(--t4);letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.su-head{display:flex;align-items:flex-start;justify-content:space-between;
  gap:var(--s5);flex-wrap:wrap}
.su-n{font-size:var(--t5);font-weight:500;letter-spacing:-.02em;margin:0}
.su-line{margin:4px 0 0;font-size:var(--t3);color:var(--ink-2)}
/* the one line the company describes itself by, given the weight of a pull
   quote rather than being buried in the paragraph under it */
.su-thesis{margin:var(--s3) 0 0;font-size:var(--t3);color:var(--ink);
  letter-spacing:-.01em;max-width:44ch;padding-left:var(--s4);
  border-left:2px solid var(--ink)}
.su-meta{display:flex;gap:var(--s2);flex-wrap:wrap}
.su-blurb{margin-top:var(--s4);max-width:68ch}



}

/* ── CASEY: the spine ───────────────────────────────────────
   Five moments on a dated line. Alternating sides only where two columns are
   wide enough to read; below that the line moves left and everything hangs off
   one side, because two 180px columns either side of a rule is a squeeze, not
   a timeline. */
.sp-axis{margin:var(--s7) 0 var(--s5)}
.sp{list-style:none;margin:0;padding:0;position:relative}
/* the line itself, drawn once behind the steps */
.sp::before{content:'';position:absolute;left:9px;top:6px;bottom:6px;width:1px;
  background:var(--hair)}
.sp-step{position:relative;padding:0 0 var(--s7) var(--s7);min-height:0}
.sp-step:last-child{padding-bottom:0}
.sp-node{position:absolute;left:4px;top:7px;width:11px;height:11px;border-radius:50%;
  background:var(--bg);box-shadow:inset 0 0 0 2px var(--ink-3);z-index:1}
.sp-step:first-child .sp-node,.sp-step:last-child .sp-node{box-shadow:inset 0 0 0 5px var(--ink)}
.sp-card{display:flex;flex-direction:column;gap:var(--s2);max-width:70ch;align-items:flex-start}
.sp-t{font-size:var(--t4);font-weight:500;letter-spacing:-.02em;margin:2px 0 0}
.sp-media{margin:var(--s4) 0 0;border-radius:var(--r-ctl);overflow:hidden;
  background:var(--stage);box-shadow:inset 0 0 0 1px var(--hair);width:fit-content}
/* capped by HEIGHT, not width. The MVP demo was shot portrait, so a width cap
   made it a thousand pixels tall and it swallowed the viewport — the moment
   became the whole page. Height decides, and width follows the aspect, so a
   portrait clip and a landscape one both sit at the same size on the line. */
.sp-media video{display:block;max-width:100%;max-height:460px;width:auto;height:auto}
/* The two stranded comments here described `.sp-fig`, the giant-numeral tile,
   which came off on 2026-08-22 — a moment with no footage now renders no frame
   at all rather than a frame containing a number. See the note in
   startups.js. */
.sp-loom{width:100%}
/* the ratio belongs here, not on the element — a section that sets its own
   values is how the scale drifts, and the linter says so */
.sp-loom .ab-embed{border-radius:var(--r-ctl);overflow:hidden;aspect-ratio:16/10}

/* THE ALTERNATING SPINE IS GONE. [@maaz · 2026-08-22] "The spine format for
   CASEY is not working." Above 1180px the moments used to sit either side of a
   centred line, which meant every single moment left the opposite half of the
   section empty — about half the section was white, the reading order jumped
   left-right-left, and a 460px video with nothing beside it was the worst of
   it. One rail down the left, which is what every viewport under 1180px
   already got, has no holes in it by construction and reads straight down.
   Kept as one rule rather than two layouts, so there is one thing to look at.
   Whether a rail is the right instrument at all is a separate question and an
   open one. */

/* ── CASEY's Instagram strip ────────────────────────────────
   One row you push sideways, not a masonry board — see the note in
   startups.js on why 32 tiles cannot be a board. Native overflow scroll and
   real links, so it drags on touch, scrolls on a trackpad, and tabs through
   for a keyboard without a line of JavaScript deciding any of it.

   The tiles keep the post's own ratio at a common height: 9:16 for a reel,
   4:5 for a carousel of written cards. Desaturated at rest and true on
   attention, the same rule the About board follows, because the page carries
   no accent hue and 32 full-colour tiles would be the loudest thing on it. */
.ig-head{display:flex;align-items:baseline;gap:var(--s3);flex-wrap:wrap;
  margin:var(--s7) 0 var(--s4)}
.ig-at{font-family:var(--m);font-size:var(--t1);color:var(--ink-2)}
a.ig-at:hover{color:var(--ink)}
.ig-n{font-size:var(--t1);color:var(--ink-3)}
/* The strip is inside a card that already has padding, and a row that stops
   short of the card edge reads as a row that has ended. It bleeds to the card
   edges and pads the ends back in, so the last tile runs under the edge
   instead of stopping short of it. */
.ig-strip{display:flex;gap:var(--s3);overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x proximity;padding-block:2px var(--s2);
  margin-inline:calc(var(--s6) * -1);
  padding-inline:var(--s6);scroll-padding-inline:var(--s6);
  scrollbar-width:thin;scrollbar-color:var(--hair) transparent}
.ig-strip::-webkit-scrollbar{height:6px}
.ig-strip::-webkit-scrollbar-thumb{background:var(--hair);border-radius:var(--r-pill)}
.ig-strip::-webkit-scrollbar-track{background:transparent}
.ig-t{position:relative;flex:none;display:block;scroll-snap-align:start;
  border-radius:var(--r-ctl);overflow:hidden;background:var(--stage)}
.ig-t img{display:block;height:320px;width:auto;
  filter:saturate(.55);transition:filter .3s var(--ease-ui),transform .4s var(--ease)}
.ig-t:hover img,.ig-t:focus-visible img{filter:saturate(1);transform:scale(1.03)}
.ig-t:focus-visible{outline:2px solid var(--ink);outline-offset:2px}
@media(max-width:760px){
  .ig-t img{height:230px}
  .ig-strip{margin-inline:calc(var(--s4) * -1);
    padding-inline:var(--s4);scroll-padding-inline:var(--s4)}
  .ig-head{margin-top:var(--s6)}
}

/* ── TKaen: the prototype stage ─────────────────────────────
   The note that produced this was "too much negative space". A phone is
   375px wide and the card is about a thousand, so the answer is not a wider
   phone — it is giving the other half something to do. Two columns: the
   thing, and what to do in it. */
.tk-wrap{display:grid;grid-template-columns:auto 1fr;gap:var(--s7);
  align-items:stretch;margin-top:var(--s6)}
.tk-stage{border-radius:var(--r-panel);background:var(--stage);padding:var(--s4);
  display:grid;place-items:center}
/* Height-capped, not width-capped. At 344px wide this phone was 749px tall
   against a 430px column of words beside it, which is the same hole the
   filmstrip had, moved to the other side. Fixing the height fixes the pair. */
.tk-phone{position:relative;height:min(600px,68vh);aspect-ratio:376/819;
  border-radius:var(--r-ctl);overflow:hidden;background:var(--panel);
  box-shadow:var(--lift-2)}
.tk-poster{width:100%;height:100%;object-fit:cover;display:block}
/* The frame replaces the poster rather than covering it — see the note in
   app.js on why a still left underneath is a lie about a failed load. */
.tk-frame{width:100%;height:100%;border:0;display:block;background:#fff}
/* `place-items` centres each row inside its own track; the tracks themselves
   still stretch, which put the glyph a quarter down and the label three
   quarters down. `place-content` is the one that centres the pair. */
.tk-open{position:absolute;inset:0;width:100%;height:100%;border:0;cursor:pointer;
  display:grid;place-content:center;justify-items:center;grid-auto-flow:row;gap:12px;
  font:inherit;font-size:var(--t2);color:#E9ECEE;
  background:rgba(9,12,14,.60);transition:background .2s var(--ease-ui)}
.tk-open:hover{background:rgba(9,12,14,.72)}
.tk-open:focus-visible{outline:2px solid var(--ink);outline-offset:-3px}
.tk-open svg{width:40px;height:40px;fill:#E9ECEE;
  filter:drop-shadow(0 1px 8px rgba(0,0,0,.5))}
/* A solid chip rather than white text on a scrim. The poster is a product
   screenshot and its brightness is not ours to rely on — the same reason the
   date chips on the Instagram tiles carry their own ground. */
.tk-open span{background:rgba(9,12,14,.82);padding:6px 12px;
  border-radius:var(--r-pill);letter-spacing:.01em}
/* The note is NOT pinned to the bottom. Pinning it pushed 170px of white
   between the flow list and one grey line, which reads as a hole; a text
   column that simply ends earlier than a phone beside it does not. */
.tk-side{min-width:0}
.tk-lede{margin-top:var(--s2);max-width:52ch}
.tk-try{list-style:none;margin:var(--s5) 0 0;padding:0;display:flex;
  flex-direction:column;gap:var(--s5)}
.tk-try li{padding-left:var(--s4);border-left:1px solid var(--hair)}
.tk-try-t{font-size:var(--t3);font-weight:500;letter-spacing:-.01em;margin:0 0 4px}
.tk-try .card-p{max-width:52ch}
.tk-flow-h{margin:var(--s6) 0 var(--s3)}
/* Labels, not tiles. A pill each would read as sixteen controls you can press,
   and none of them do anything — the prototype is the control. */
.tk-flow{list-style:none;display:flex;flex-wrap:wrap;gap:6px var(--s3);
  margin:0;padding:0;max-width:52ch}
.tk-flow li{font-size:var(--t1);color:var(--ink-2);display:flex;align-items:center}
.tk-flow li + li::before{content:'';width:3px;height:3px;border-radius:50%;
  background:var(--hair);margin-right:var(--s3)}
.tk-note{margin:var(--s5) 0 0;font-size:var(--t1);color:var(--ink-3);max-width:52ch}
.tk-note a{color:var(--ink-2)}
.tk-note a:hover{color:var(--ink)}

@media(max-width:760px){
  /* the plate stops being a fixed slot and the issuer name drops under it */
  .ccert-top{flex-wrap:wrap}
  .ccert-mark{height:38px;max-width:140px}
  .sp-media video{width:100%}
  /* the phone stops being a column of its own and the instructions fall under it */
  .tk-wrap{grid-template-columns:1fr;gap:var(--s5)}
  .tk-stage{padding:var(--s3)}
  .tk-phone{height:min(520px,62vh)}
}
