/* ==========================================================================
   heyssp, portfolio
   --------------------------------------------------------------------------
   Two themes, light by default, remembered per visitor. Colour is only ever
   read through the semantic tokens below, which is what lets one block of
   overrides flip a theme, and another flip the connect section to the opposite
   of whatever the page is.

   Light   F7FFF9 ground . FFFFFF panel . DDD8CA rule . 1B1A17 ink . 2F9C73 accent
   Dark    0B0C0F ground . 101217 panel . 212429 rule . ECE9E2 ink . 49D49D accent
   Both palettes are heyssp.in's own.

   Type    Inter, one family end to end -- tracking tightens with size (about
           -2% at body, toward -4.5%/-6% on the big display numbers) rather
           than a flat value everywhere, so weight still does most of the
           hierarchy work but kerning stays optically correct at scale.
           Caveat for redlines only.
   Space   4 8 12 16 24 32 48 64 96
   Radius  3px, matching heyssp.in.
   Ground  One gradient sized to the whole document, so it actually travels as
           you scroll rather than sitting still behind you.
   Motion  Hero fades up on load and reads out your cursor. Content lights as it
           arrives and dims once it is behind you. The built work shuffles
           itself, idle, to a random tool every few seconds. Off entirely
           under prefers-reduced-motion.
   ========================================================================== */

:root {
  /* light, the default */
  --bg:        #f7fff9;
  --bg-alt:    #ffffff;
  --bg-lift:   #ece9e0;
  --rule:      #ddd8ca;
  --ink:       #1b1a17;
  --ink-dim:   #5b584f;
  --ink-faint: #948f81;
  --accent:    #2f9c73;
  --accent-deep: #a7e2ca;
  --accent-glow: rgba(47, 156, 115, .22);
  --on-accent: #f7f6f1;

  /* read by the canvas that paints the ground. Retint towards the
     Luminescence flow blend (Spring Celadon -> Young Willow -> Pine Leaf ->
     Black Moss): the two lightest stops carry the full-page gradient, since
     ink text sits directly on it top to bottom, while Pine Leaf drives the
     glow blobs, where it only ever shows through at partial alpha. */
  --bg-top:  #f0fbef;
  --bg-bot:  #cef3d9;
  --bloom:   34, 199, 154;
  --bloom-a: .34;

  /* the hero sits on the page's own surface now, not a solid panel of its
     own, so its text needs to read on white just as much as on black */
  --hero-ink:      var(--ink);
  --hero-dim:      var(--ink-dim);
  --hero-mute:     #3f8f6b;
  --hero-hl:       #1f7a58;
  --hero-chip-bg:      #ffffff;
  --hero-chip-border:  var(--rule);
  --hero-chip-bg-h:    var(--bg-lift);
  --hero-chip-border-h: var(--ink-faint);
  --hero-fx: 34, 199, 154;

  --track: 1240px;
  --pad: 32px;
  --r: 3px;

  --fs-display: clamp(40px, 6.4vw, 94px);
  --fs-h1:      clamp(32px, 4.4vw, 62px);
  --fs-h2:      clamp(25px, 3vw, 40px);
  --fs-h3:      21px;
  --fs-lead:    clamp(16.5px, 1.3vw, 19px);
  --fs-body:    16px;
  --fs-sm:      14px;
  --fs-cap:     12.5px;
  --fs-eyebrow: 11.5px;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px; --s5: 24px;
  --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;

  --ease: cubic-bezier(.22, 1, .36, 1);
  color-scheme: light;
}

:root[data-theme="dark"] {
  --bg:        #0b0c0f;
  --bg-alt:    #101217;
  --bg-lift:   #15181e;
  --rule:      #212429;
  --ink:       #ece9e2;
  --ink-dim:   #8d9096;
  --ink-faint: #5b5e64;
  --accent:    #49d49d;
  --accent-deep: #2b6b52;
  --accent-glow: rgba(73, 212, 157, .18);
  --on-accent: #06120d;

  --bg-top:  #090a0d;
  --bg-bot:  #1a2227;
  --bloom:   73, 212, 157;
  --bloom-a: .26;
  color-scheme: dark;

  --hero-ink:      #ece9e2;
  --hero-dim:      #b9c2bd;
  --hero-mute:     #8fb3a0;
  --hero-hl:       #b9f5dd;
  --hero-chip-bg:      rgba(236, 233, 226, .06);
  --hero-chip-border:  rgba(236, 233, 226, .18);
  --hero-chip-bg-h:    rgba(236, 233, 226, .12);
  --hero-chip-border-h: rgba(236, 233, 226, .34);
  --hero-fx: 73, 212, 157;
}

/* The connect section is always the opposite of the page it sits on. */
.flip {
  --bg:        #0b0c0f;
  --bg-alt:    #101217;
  --bg-lift:   #15181e;
  --rule:      #262a30;
  --ink:       #ece9e2;
  --ink-dim:   #9a9da3;
  --ink-faint: #6b6f76;
  --accent:    #49d49d;
  --accent-deep: #2b6b52;
  --accent-glow: rgba(73, 212, 157, .2);
  --on-accent: #06120d;
  color-scheme: dark;
}
:root[data-theme="dark"] .flip {
  --bg:        #f7fff9;
  --bg-alt:    #ffffff;
  --bg-lift:   #ece9e0;
  --rule:      #ddd8ca;
  --ink:       #1b1a17;
  --ink-dim:   #5b584f;
  --ink-faint: #948f81;
  --accent:    #2f9c73;
  --accent-deep: #a7e2ca;
  --accent-glow: rgba(47, 156, 115, .22);
  --on-accent: #f7f6f1;
  color-scheme: light;
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; background: var(--bg); }
/* folding is compensated by hand below, so the browser must not also try */
html, body { overflow-anchor: none; }

body {
  margin: 0;
  color: var(--ink);
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: var(--fs-body);
  line-height: 1.6;
  letter-spacing: -0.02em;
  font-feature-settings: "cv11", "ss01";
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;

  background-color: var(--bg);
  transition: background-color .35s var(--ease), color .35s var(--ease);
}

/* The ground is painted, not layered.
   CSS gradients this large band on 8-bit displays, and a tiled noise overlay
   seams. One canvas draws the whole field at quarter resolution with a per
   pixel dither, which kills both. It shifts with scroll, so the ground
   travels down the page instead of sitting still behind it. */
#bg {
  position: fixed; inset: 0; z-index: -1;
  width: 100%; height: 100%; display: block;
  pointer-events: none;
}

a { color: inherit; text-decoration: none; }
img { max-width: 100%; display: block; }
button { font: inherit; letter-spacing: inherit; color: inherit; }

/* ---------------------------------------------------------------- tracker */
/* The same rulers-and-a-point language as the selection handles, but reading
   the cursor across the whole page rather than just the hero. Fixed to the
   viewport, so the coordinates are the pointer's, not any one section's.
   Off entirely on touch devices and under reduced motion. */
.tracker {
  position: fixed; inset: 0; z-index: 45; pointer-events: none;
  opacity: 0; transition: opacity .5s var(--ease);
}
.tracker.is-live { opacity: 1; }
.tracker .gx, .tracker .gy { position: absolute; background: var(--accent); opacity: .14; }
.tracker .gx { top: 0; bottom: 0; width: 1px; left: var(--gx, -100px); }
.tracker .gy { left: 0; right: 0; height: 1px; top: var(--gy, -100px); }
.tracker .gdot {
  position: absolute; width: 5px; height: 5px; border-radius: 50%;
  left: var(--gx, -100px); top: var(--gy, -100px); transform: translate(-50%, -50%);
  background: var(--accent); box-shadow: 0 0 0 4px var(--accent-glow);
}
.tracker .gread {
  position: absolute; left: var(--gx, -100px); top: var(--gy, -100px);
  transform: translate(12px, -28px);
  font-size: 10px; font-weight: 700; letter-spacing: .03em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
  color: var(--on-accent); background: var(--accent);
  padding: 3px 6px; border-radius: 2px;
}
/* Belt and braces: some mobile browsers misreport hover capability, so a
   touch-primary or narrow viewport hides it outright rather than trusting
   the hover query alone. This is what was showing as a stray "0 x 0" pinned
   to the top of the page on phones. */
@media (hover: none), (pointer: coarse), (max-width: 640px) {
  .tracker { display: none !important; }
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }
/* A solid, opaque fill plus a forced text-colour swap is what read as
   blocky across a big display headline: every line's own selection
   rectangle is a hard box to begin with (that part is the browser's, not
   ours), and an abrupt colour change at each one's edge is what turned
   that into visibly disjointed chunks. Translucent instead, with the
   text kept at its own colour underneath — closer to how the OS's own
   selection looks, and the box shape reads as a soft tint rather than a
   stamped rectangle. */
::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); color: inherit; }

/* ------------------------------------------------------------ primitives */

.track { width: 100%; max-width: var(--track); margin: 0 auto; padding: 0 var(--pad); }

.eyebrow {
  font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-faint); line-height: 1;
}

.note {
  font-family: "Caveat", "Segoe Script", cursive;
  font-size: 21px; font-weight: 600; letter-spacing: 0; line-height: 1.15;
  color: var(--accent);
}

/* No margin reset existed anywhere above this for a plain <p>, so every
   .lead was quietly carrying the browser's own default 1em top margin on
   top of whatever spacing was actually intended around it -- most visible
   in .split, where it stacked with the deliberate .35em optical-alignment
   padding below and pushed the paragraph well past top-aligned with its
   heading. Margin was never the mechanism doing that alignment on purpose;
   it was an accident sitting on top of the one that was. */
.lead { margin: 0; font-size: var(--fs-lead); line-height: 1.6; color: var(--ink-dim); max-width: 54ch; }
.num { font-variant-numeric: tabular-nums; }

.ico { width: 18px; height: 18px; flex: none; stroke: currentColor; fill: none;
       stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ico-sm { width: 15px; height: 15px; }
.ico-brand { fill: currentColor; stroke: none; width: 17px; height: 17px; }

/* buttons ---------------------------------------------------------------- */
/* Standing rule: every button on the site is square or rectangle, corner
   radius var(--r) (3px) like everything else that isn't text -- never a
   pill or a circle, no matter how small the control gets (an icon-only
   button is still a small rectangle, not a circle). Applies to .btn,
   .rail-arrow, .icon-cta, .cc-copy, .theme-btn, .link-pill and anything
   added after them. A perfectly round control was tried for the rail's
   prev/next arrows and for ps-plugins.html's Download/Creative Cloud
   buttons and both got corrected back to this; nothing on the site
   should reach for border-radius: 999px / 50% on a clickable control
   again. (Small non-interactive marks -- status dots, the numbered
   circles on a screenshot walkthrough, tag-style chips -- aren't
   buttons and are exempt; this is about anything a person clicks.) */

.btn {
  display: inline-flex; align-items: center; gap: 10px;
  height: 50px; padding: 0 22px;
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--bg-alt); color: var(--ink);
  font-size: var(--fs-sm); font-weight: 600; cursor: pointer; white-space: nowrap;
  transition: background .18s var(--ease), border-color .18s var(--ease),
              color .18s var(--ease), transform .18s var(--ease);
}
.btn:hover { background: var(--bg-lift); border-color: var(--ink-faint); transform: translateY(-1px); }
.btn .ico { transition: transform .18s var(--ease); }
.btn:hover .ico { transform: translate(2px, -2px); }

.btn-accent { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.btn-accent:hover { background: var(--accent); border-color: var(--accent); color: var(--on-accent);
  box-shadow: 0 10px 34px var(--accent-glow); filter: brightness(1.06); }
.btn-sm { height: 38px; padding: 0 14px; font-size: var(--fs-cap); }

/* --------------------------------------------------- transform handles */
/* Hover or tab onto anything selectable and it gets picked up the way a layer
   does on canvas. Exactly one thing is ever selected, and it is raised above
   its neighbours while it is. */

.sel { position: relative; }
.sel.is-sel { z-index: 8; }
.selbox {
  position: absolute; inset: -1px; z-index: 4;
  pointer-events: none; opacity: 0;
  transition: opacity .14s var(--ease);
}
.sel.is-sel > .selbox { opacity: 1; }
.selbox::before { content: ""; position: absolute; inset: 0; border: 1px solid var(--accent); }
.selbox i {
  position: absolute; width: 7px; height: 7px;
  background: var(--bg); border: 1px solid var(--accent); border-radius: 1px;
}
.selbox i.nw { top: -4px; left: -4px; }
.selbox i.n  { top: -4px; left: 50%; margin-left: -4px; }
.selbox i.ne { top: -4px; right: -4px; }
.selbox i.e  { top: 50%; right: -4px; margin-top: -4px; }
.selbox i.se { bottom: -4px; right: -4px; }
.selbox i.s  { bottom: -4px; left: 50%; margin-left: -4px; }
.selbox i.sw { bottom: -4px; left: -4px; }
.selbox i.w  { top: 50%; left: -4px; margin-top: -4px; }
.selbox em {
  position: absolute; top: -21px; left: -1px;
  background: var(--accent); color: var(--on-accent);
  font-size: 10.5px; font-weight: 700; font-style: normal;
  letter-spacing: .01em; line-height: 1;
  padding: 4px 7px; border-radius: 2px; white-space: nowrap;
}

/* ------------------------------------------------------------------- nav */
/* Unstuck it floats over the hero photograph, so it is light regardless of
   theme. Stuck it belongs to the page and takes the page's colours. */

/* Unstuck, the nav used to float over a hero photograph that was always
   dark, so it could get away with one hardcoded dark-glass look regardless
   of theme. The hero is the page's own colour now, so in light theme that
   same dark bar read as a low-contrast smudge before you'd scrolled past
   it. Light and dark now each get glass in their own direction. */
.nav {
  position: sticky; top: 0; z-index: 40;
  background: rgba(11, 12, 15, .34);
  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color .3s var(--ease), background .3s var(--ease);
  --nav-fg: #ece9e2; --nav-dim: #b7bac0; --nav-rule: rgba(236, 233, 226, .22);
  --nav-accent: #49d49d; --nav-on-accent: #06120d;
}
/* :where() keeps this tied in specificity with plain .nav rather than
   beating .nav.stuck below, which needs to keep winning once you've
   scrolled regardless of theme */
:root:where(:not([data-theme="dark"])) .nav {
  background: rgba(255, 255, 255, .72);
  --nav-fg: var(--ink); --nav-dim: var(--ink-dim); --nav-rule: rgba(27, 26, 23, .12);
  --nav-accent: var(--accent); --nav-on-accent: var(--on-accent);
}
.nav.stuck {
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  border-bottom-color: var(--rule);
  --nav-fg: var(--ink); --nav-dim: var(--ink-dim); --nav-rule: var(--rule);
  --nav-accent: var(--accent); --nav-on-accent: var(--on-accent);
}
.nav-in { display: flex; align-items: center; gap: var(--s5); height: 72px; }

.brand {
  font-size: clamp(17px, 2.2vw, 21px); font-weight: 800; letter-spacing: -.045em;
  color: var(--nav-fg); white-space: nowrap; transition: color .3s var(--ease), font-weight .18s var(--ease);
}
.brand:hover { font-weight: 900; }
.brand .dot { color: var(--nav-accent); }

.nav-links { display: none; gap: var(--s5); margin-left: var(--s6); }
.nav-links a {
  font-size: var(--fs-cap); font-weight: 500; color: var(--nav-dim);
  position: relative; padding: 4px 0; transition: color .18s var(--ease), font-weight .18s var(--ease);
}
.nav-links a::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 0;
  background: var(--nav-accent); transition: width .22s var(--ease);
}
.nav-links a:hover { color: var(--nav-fg); font-weight: 700; }
.nav-links a:hover::after { width: 100%; }
@media (min-width: 900px) { .nav-links { display: flex; } }

.nav-right { margin-left: auto; display: flex; align-items: center; gap: var(--s3); }
.status {
  display: none; align-items: center; gap: 8px;
  font-size: var(--fs-cap); font-weight: 500; color: var(--nav-dim);
  padding: 6px 12px 6px 10px; border: 1px solid var(--nav-rule); border-radius: 999px;
}
.status .pip { width: 7px; height: 7px; border-radius: 50%; background: var(--nav-accent);
  animation: pulse 2.6s var(--ease) infinite; }
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--nav-accent) 45%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (min-width: 680px) { .status { display: flex; } }

.theme-btn {
  width: 38px; height: 38px; flex: none; display: grid; place-items: center;
  border: 1px solid var(--nav-rule); border-radius: var(--r);
  background: transparent; color: var(--nav-dim); cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.theme-btn:hover { color: var(--nav-accent); border-color: var(--nav-accent); }
.theme-btn .ico-moon { display: none; }
:root[data-theme="dark"] .theme-btn .ico-sun { display: none; }
:root[data-theme="dark"] .theme-btn .ico-moon { display: block; }

.nav .btn-accent {
  background: var(--nav-accent); border-color: var(--nav-accent); color: var(--nav-on-accent);
}

/* ----------------------------------------------------------------- hero */

.hero {
  position: relative; overflow: hidden;
  min-height: min(820px, calc(100svh - 72px));
  display: flex; align-items: center;
  padding: clamp(56px, 8vw, 104px) 0 clamp(64px, 8vw, 100px);
  margin-top: -72px; padding-top: calc(clamp(56px, 8vw, 104px) + 72px);
  color: var(--hero-ink);
  background: var(--bg);
  transition: background-color .35s var(--ease), color .35s var(--ease);
}
/* No more dissolving a solid panel into the page: the hero sits directly on
   the page's own colour (it is the same --bg the rest of the page paints),
   so there is nothing to seam against in the first place. The canvas only
   ever paints where the effect actually is — soft green blobs adrift on the
   light theme, a faint drifting field of glyphs on the dark one — and is
   transparent everywhere else, so it can end anywhere without a visible
   edge. See drawFx in site.js. */
.hero-bg { position: absolute; inset: 0; z-index: 0; }
.hero-bg canvas {
  width: 100%; height: 100%; display: block;
  opacity: 0; animation: bgIn 1.6s var(--ease) .1s forwards;
}
@keyframes bgIn { to { opacity: 1; } }

.hero > .track { position: relative; z-index: 1; }

.hero-kicker { display: flex; align-items: center; gap: 12px; margin-bottom: var(--s5); }
.hero-kicker .bar { width: 28px; height: 1px; flex: none; background: var(--hero-mute); }
/* one line at every width, so it reads as a title and not a sentence */
.hero-kicker .eyebrow { color: var(--hero-mute); white-space: nowrap;
  font-size: clamp(9.4px, 2.55vw, 11.5px); letter-spacing: .12em; }
@media (max-width: 420px) { .hero-kicker .bar { width: 16px; } }

h1.display {
  margin: 0; font-size: var(--fs-display); font-weight: 300;
  line-height: .96; letter-spacing: -.045em; text-wrap: balance;
  max-width: 13ch; /* ch here is measured at the display size, not body size */
}
h1.display b { font-weight: 800; }
h1.display .hl { color: var(--hero-hl); font-weight: 800; }

.hero-sub { margin: var(--s6) 0 0; max-width: 40ch; color: var(--hero-dim); }
.hero-cta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3); margin-top: var(--s6); }
@media (max-width: 639px) {
  .hero-cta .btn { height: 46px; padding: 0 16px; }
}
.hero-cta .btn { background: var(--hero-chip-bg); border-color: var(--hero-chip-border); color: var(--hero-ink); }
.hero-cta .btn:hover { background: var(--hero-chip-bg-h); border-color: var(--hero-chip-border-h); }
.hero-cta .btn-accent { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.hero-cta .btn-accent:hover { background: var(--accent); border-color: var(--accent); }

/* the head reaches up into the primary button, the tail runs out to the words */
.hero-hint { display: flex; align-items: flex-start; gap: 10px; margin: 4px 0 0 54px; }
.hero-hint svg { width: 42px; height: 32px; flex: none; stroke: var(--hero-mute);
  fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.hero-hint .note { padding-top: 16px; color: var(--hero-mute); font-size: 22px; }

/* stat band -------------------------------------------------------------- */

.stats { border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  display: grid; grid-template-columns: repeat(2, 1fr); position: relative; z-index: 1; }
@media (min-width: 780px) { .stats { grid-template-columns: repeat(4, 1fr); } }
.stat { padding: var(--s5) var(--s5) var(--s5) 0; }
.stat + .stat { border-left: 1px solid var(--rule); padding-left: var(--s5); }
@media (max-width: 779px) {
  .stat:nth-child(3) { border-left: 0; padding-left: 0; border-top: 1px solid var(--rule); }
  .stat:nth-child(4) { border-top: 1px solid var(--rule); }
}
.stat .v { font-size: clamp(30px, 3.4vw, 46px); font-weight: 800;
  letter-spacing: -.045em; line-height: 1; }
.stat .k { margin-top: 10px; font-size: var(--fs-cap); color: var(--ink-faint); }

/* --------------------------------------------------------------- section */

.sec { position: relative; z-index: 1; padding: clamp(72px, 9vw, 120px) 0 0; }
.sec-head { display: flex; align-items: center; gap: var(--s4); padding-bottom: var(--s6); }
.sec-head .rule { flex: 1; height: 1px; background: var(--rule); }
.sec-head .meta { font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-faint); }
@media (max-width: 599px) { .sec-head .meta { display: none; } }

h2.h1 { margin: 0; font-size: var(--fs-h1); font-weight: 800;
  line-height: 1.02; letter-spacing: -.04em; text-wrap: balance; max-width: 16ch; }
h2.h1 .dim { color: var(--ink-faint); font-weight: 300; }
.h1-note { display: block; margin-top: var(--s3); }

.split { display: grid; gap: var(--s5); grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .split { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); gap: var(--s8); }
  /* both columns start on the same line. Bottom aligning a four line paragraph
     against a two line heading only ever looks like a mistake. */
  .split > * { align-self: start; }
  .split .lead, .split .hire-lead { padding-top: .35em; }
}

/* --------------------------------------------------------- hairline grids */

.grid {
  display: grid; gap: 1px; background: var(--rule);
  border: 1px solid var(--rule); border-radius: var(--r);
  grid-template-columns: 1fr;
}
.grid > * { background: var(--bg-alt); }
@media (min-width: 700px) { .grid-3, .grid-4 { grid-template-columns: 1fr 1fr; } }
@media (min-width: 900px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1000px) { .grid-4 { grid-template-columns: repeat(4, 1fr); } }

.pane { padding: var(--s6) var(--s5); display: flex; flex-direction: column; gap: var(--s4); }
.pane h3 { margin: 0; font-size: var(--fs-eyebrow); font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase; color: var(--ink-faint); }
.pane p { margin: 0; font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.62; }
.pane p b { color: var(--ink); font-weight: 600; }
.pane ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 9px; }
.pane li { font-size: var(--fs-sm); font-weight: 500; }
.pane li .sub { display: block; font-size: var(--fs-cap); font-weight: 400;
  color: var(--ink-faint); margin-top: 1px; }
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip { padding: 5px 11px; border: 1px solid var(--rule); border-radius: 999px;
  font-size: var(--fs-cap); font-weight: 500; color: var(--ink-dim);
  transition: color .18s var(--ease), border-color .18s var(--ease); }
.chip:hover { color: var(--accent); border-color: var(--accent); }
/* Skills/Tools/Certifications/Education, tightened: four panes of a plain
   list each read as a lot of scroll on a phone. Same content, less padding
   between panes and less gap above each chip row; the chips themselves
   cascading in on entry (`.rv`, staggered per chip in the markup) is the
   "subtlety" standing in for actually cutting anything. */
#toolbox .pane { padding: var(--s5) var(--s5); gap: var(--s3); }

/* --------------------------------------------------------- built, numbered */
/* The rail carousel worked but hid four tools behind arrows. This reads as
   one row of steps instead: a bar of big numbers, one lit at a time, the
   detail for whichever is lit sitting underneath. Click or arrow-key a
   number, the panel beneath swaps -- a plain tab pattern wearing the
   numbers as its tabs. One green, not five brand colours: --c is set once,
   on .process itself, to the brighter of the two greens already mixed into
   the ground gradient (see --bloom up top) and every tool inherits the same
   value, so .card-tag / .card-mark / the active number all read as one
   accent instead of a different hue per tool. */
.process { --c: rgb(var(--bloom)); }
.process-nums {
  display: flex; flex-wrap: wrap; list-style: none; margin: 0; padding: 0;
}
.process-num {
  flex: 1 1 72px; min-width: 64px;
  padding: var(--s4) var(--s3) 0; margin: 0;
  border: 0; border-top: 2px solid var(--rule); border-radius: 0;
  background: none; text-align: left; cursor: pointer;
  font-size: clamp(44px, 7vw, 104px); font-weight: 300; line-height: 1;
  letter-spacing: -.05em;
  /* resting numbers still read as green, just a quieter, desaturated shade --
     the ladder goes muted -> accent (hover, previewing) -> --c (active,
     brightest), so committing to a tool is the one moment that gets the
     boldest colour. */
  color: color-mix(in srgb, var(--accent) 42%, var(--ink-faint));
  transition: color .28s var(--ease), font-weight .18s var(--ease),
    border-color .28s var(--ease);
}
.process-num:hover { color: var(--accent); font-weight: 500; }
.process-num.active { color: var(--c); font-weight: 800; border-top-color: var(--c); }
@media (max-width: 599px) {
  .process-num { font-size: clamp(30px, 11vw, 46px); flex-basis: 32px; min-width: 0;
    padding-left: 6px; padding-right: 6px; }
}

/* A plain hairline card, same surface as every other panel on the site
   (.pane, .mini-item's grid, the plugin rows) -- floating bare on the page
   gradient was a big part of why this read as unfinished. Every panel is
   stacked into the same grid cell (grid-area: 1/1) instead of being
   measured by hand: a CSS grid row sizes itself to the tallest thing
   placed in it regardless of which one is visible, so the card is already
   as tall as Sticky Checks needs even while Sift is showing, and swapping
   tools -- by click or the idle shuffle -- never shifts the card's own
   height by so much as a pixel. Hidden panels use visibility, not
   display:none, specifically so they still count toward that sizing. */
.process-panels {
  display: grid; align-items: start;
  margin-top: var(--s6); padding: var(--s7) var(--s6);
  border: 1px solid var(--rule); border-radius: var(--r); background: var(--bg-alt);
}
.process-panel {
  grid-area: 1 / 1; display: grid;
  grid-template-columns: minmax(240px, 360px) 1fr; gap: var(--s8); align-items: start;
  visibility: hidden; opacity: 0; pointer-events: none;
}
.process-panel.active {
  visibility: visible; opacity: 1; pointer-events: auto;
  animation: panelIn .32s var(--ease);
}
@keyframes panelIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .process-panel.active { animation: none; } }
@media (max-width: 779px) {
  .process-panels { padding: var(--s6) var(--s5); }
  .process-panel { grid-template-columns: 1fr; gap: var(--s5); }
}

.process-panel-body { display: flex; flex-direction: column; gap: var(--s5); }
.process-panel .card-desc { margin: 0; max-width: 58ch; }
.process-panel .card-meta { border-top: 0; padding-top: 0; margin-top: 0; max-width: 480px; }

.card-top { display: flex; align-items: flex-start; gap: var(--s4); }
.card-mark {
  width: 42px; height: 42px; flex: none; border-radius: var(--r);
  background: var(--c); color: #0b0c0f; display: grid; place-items: center;
}
.card-mark .ico { width: 21px; height: 21px; stroke-width: 2.1; }
.process-panel .card-mark { width: 52px; height: 52px; }
.process-panel .card-mark .ico { width: 25px; height: 25px; }
.process-panel .card-name h3 { font-size: clamp(28px, 3.4vw, 44px); }
.process-panel .card-tag { font-size: var(--fs-body); margin-top: 7px; }

.card-name { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.card-name h3 { margin: 0; font-size: 25px; font-weight: 800; letter-spacing: -.045em; line-height: 1; }
.card-name .yr { font-size: var(--fs-cap); color: var(--ink-faint); }
.card-tag { margin-top: 5px; font-size: var(--fs-sm); font-weight: 600; color: var(--c); }
.card-desc { margin: 0; font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.62; max-width: 44ch; }

.card-meta {
  margin-top: auto;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s3);
  padding-top: var(--s4); border-top: 1px solid var(--rule);
}
.card-meta dt { font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint); }
.card-meta dd { margin: 5px 0 0; font-size: var(--fs-cap); font-weight: 500; }
/* a column here can end up narrower than a single word in it — "Discipline",
   "Production" — and a grid item does not clip that overflow on its own; it
   was spilling sideways into the next column and reading as two labels
   mashed together. Letting the word itself break is what actually stops it,
   at any width the row lands on. */
.card-meta dt, .card-meta dd { overflow-wrap: break-word; word-break: break-word; }
/* Three columns each barely wider than a label was what forced "Discipline"
   and "Platform" to break mid-word. At the width a rail tile actually gets
   on a phone, label-beside-value reads better anyway: one line per fact,
   three lines total, instead of two short, cramped columns of wrapped
   fragments. Desktop's grid tile is wide enough that three columns never
   had this problem, so it keeps them. */
@media (max-width: 599px) {
  .card-meta {
    grid-template-columns: auto 1fr;
    column-gap: var(--s3); row-gap: 4px;
  }
  /* dt/dd need to be the grid's direct children for auto/1fr to pair them
     one fact per row -- each one still sits inside its own <div> in the
     markup, so that wrapper has to get out of the way here. Without this,
     the three .card-meta divs become the grid items instead: two facts
     side by side, the third alone on its own row underneath. */
  .card-meta > div { display: contents; }
  .card-meta dt { white-space: nowrap; align-self: start; padding-top: 1px; }
  .card-meta dd { margin: 0; }
}

.card-links { display: flex; flex-wrap: wrap; gap: var(--s2); }
/* The icon-only collapse this used to do below 599px was tuned for the old
   380px rail tile; these pills now live in a full-width card and the three
   labels fit (or wrap to a second line) with room to spare, so a phone
   just gets the same pill everyone else does -- no more button that shows
   nothing but an icon until you press and hold it. */
.link-pill {
  position: relative;
  display: inline-flex; align-items: center; gap: 7px;
  height: 34px; padding: 0 12px;
  border: 1px solid var(--rule); border-radius: var(--r); background: transparent;
  font-size: var(--fs-cap); font-weight: 600; color: var(--ink-dim); cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.link-pill .ico { transition: transform .3s var(--ease); }
/* Same formula as .icon-cta on the product pages: an enabled pill picks up
   the tool's own --c on hover instead of a neutral ink tone, so Download/
   GitHub/Case study read as that tool's buttons, not generic site chrome.
   The hand-drawn ring this used to grow on hover didn't read as intentional
   at this size -- .sel/.selbox below is the same corner-handle-and-label
   box the rest of the site already uses for "this is the thing you're
   pointing at" (the contact rows, the work banner), so these pills use
   that instead of a one-off effect of their own. */
.link-pill:not([aria-disabled="true"]):hover,
.link-pill:not([aria-disabled="true"]):focus-visible {
  color: var(--c); border-color: var(--c);
}
.link-pill:not([aria-disabled="true"]):hover .ico,
.link-pill:not([aria-disabled="true"]):focus-visible .ico { transform: scale(1.15); }
@media (prefers-reduced-motion: reduce) {
  .link-pill .ico { transition: none; }
}
.link-pill[aria-disabled="true"] { cursor: default; opacity: .6; }

/* --------------------------------------------------------------- rail nav */
/* Arrows either side of the dots, so moving to the next tile is always a
   deliberate tap — never dependent on a drag landing cleanly, which is what
   kept going wrong here. Tapping a spine directly still opens it too; the
   arrows just mean you are never required to hit one precisely. Loops
   both directions, real index modulo, no edge to fall off. */
.rail-nav { display: none; align-items: center; justify-content: center; gap: var(--s4);
  padding-top: var(--s4); }
.is-rail-scrollable + .rail-nav { display: flex; }
.rail-arrow {
  width: 36px; height: 36px; flex: none; display: grid; place-items: center;
  border: 1px solid var(--rule); border-radius: var(--r); background: var(--bg-alt);
  color: var(--ink-dim); cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease), background .18s var(--ease);
}
.rail-arrow:hover { color: var(--accent); border-color: var(--accent); }
.rail-arrow .ico { width: 17px; height: 17px; }
.rail-nav .rail-dots { display: flex; padding-top: 0; }

/* ------------------------------------------------------------------ rails */
/* A row of tiles where one is open and the rest are spines. The open width is
   measured in JS and written to the container, so the content inside never
   reflows while a tile is moving: the tile is only ever clipping a panel that
   is already the right size. Closing runs slower than opening, so a tile
   leaves the middle unhurriedly and the next one arrives without a lurch. */

.rail-inner { display: flex; flex-direction: column; gap: var(--s4); height: 100%; }

.is-rail {
  display: flex; flex-wrap: nowrap;
  overflow-x: auto; overflow-y: hidden;
  /* deliberately no scroll snapping: the tiles change width as they open, and
     mandatory snap re-snaps on every one of those changes, which puts the
     scroller and the layout in a loop that never settles */
  scrollbar-width: none; -ms-overflow-style: none;
  overscroll-behavior-x: contain;
  /* A horizontal drag was disabled outright for a while (pan-y, handing it
     back to the page) after several rounds of it settling off-centre —
     but centring is now worked out analytically (targetLeft), corrected
     the moment a drag actually stops (checkSettle), and the loop's own
     clone-swap (rebase) all landed since then. Restoring the gesture lets
     it ride on math that has been solid for a while, with the arrows
     staying as the always-reliable fallback rather than the only way to
     move the row. */
  touch-action: pan-x pan-y;
}
.is-rail::-webkit-scrollbar { display: none; }
/* contain keeps a width change from re-laying out everything inside the tile.
   The panel in there is already a fixed width, so there is nothing to reflow,
   and saying so is what makes this smooth on a phone. */
/* Height is set from JS (Rail.syncHeight), as a plain inline style on every
   tile, not left to stretch and not routed through a CSS custom property.
   It was left to stretch across three straight rounds and broke a
   different way each time: every tile matching whichever one of all four
   happened to have the longest copy (a wall of dead space under the short
   ones), then each tile sizing to only its own content (a jagged row).
   Routing the fix through a custom property read via var() looked right
   and quietly wasn't: `contain: layout` below is what keeps a tile's own
   width transition from disturbing the rest of the page, and the same
   containment was suppressing the relayout an inherited custom-property
   change would otherwise trigger, so every tile kept showing whatever
   height happened to be current before containment took over. A plain
   property set straight on the element has no such ambiguity. */
.is-rail > .rail-item {
  flex: 0 0 auto; position: relative; overflow: hidden;
  /* column flex, single child (.rail-inner), centred: when a shorter card's
     natural content sits inside the row's uniform (tallest-card) height, the
     leftover room split evenly above and below reads as a considered
     amount of breathing room around the card; left at the top the way a
     plain block box would, the exact same leftover room reads as a mistake
     sitting under the last button. .rail-inner itself is untouched by this —
     still auto height, still what syncHeight actually measures — centring
     only ever repositions that already-sized box within the taller one
     around it, so it can't feed back into the height math the way actually
     stretching .rail-inner already proved it would (see the comment below). */
  display: flex; flex-direction: column; justify-content: center;
  /* `contain` (layout and even paint alone) was confirmed, directly, to be
     the reason a tile's rendered box could disagree with its own just-set
     inline height: the inline value and computed value read back
     differently for the same property on the same element in the same
     frame, which is not something CSS itself allows for — a containment
     bug in this rendering path, not a cascade problem. Gone entirely
     rather than narrowed further; `overflow: hidden` above already clips
     the tile the way this was additionally trying to help with. */
  width: var(--shut-w, 84px);
  transition: width .62s var(--ease), height .4s var(--ease);
}
.is-rail > .rail-item.is-open { width: var(--open-w, 380px); transition-duration: .4s; }

/* room at both ends so the first and last tiles can reach the middle too.
   The start side gives back exactly the .rail-cap's own width — the cap
   is real layout, not padding, so without this the two would stack and
   double the gap in front of tile 0 instead of one replacing the other. */
.is-rail-scrollable {
  padding-inline-end: max(0px, calc((100% - var(--open-w, 380px)) / 2));
  padding-inline-start: max(0px, calc((100% - var(--open-w, 380px)) / 2 - var(--shut-w, 84px) / 2));
}
.is-rail .rail-inner { width: var(--open-w, 380px);
  /* height:100% (set above for the desktop grid, where a stretched cell is
     what .card-meta's margin-top:auto pins the footer against) is exactly
     what must NOT apply here: JS reads this element's own scrollHeight to
     size the row, and a panel already told it is 100% of a tall parent can
     only ever measure as at least that same height back — a loop that
     ratchets the row up to the tallest tile it has ever shown and leaves it
     stuck there. Auto keeps the measurement honest to the tile's own
     content, independent of whatever height the row currently has. */
  height: auto;
}

.rail-spine { display: none; }
.is-rail .rail-spine {
  /* full width of the tile, not the closed width: it then covers the panel it
     is hiding at every point of the transition, not just at the ends */
  display: flex; position: absolute; inset: 0;
  flex-direction: column; align-items: center; justify-content: center; gap: var(--s4);
  padding: var(--s5) 0; background: var(--bg-alt);
  /* Closing runs on the tile's own .62s width transition, which is slower
     than this fade ever was, so for a stretch in the middle the spine was
     already opaque over a tile that had not finished narrowing yet — its
     real content, reflowing at a half-open width, showing straight through
     a translucent spine mid-crossfade. That's what read as "everything
     overlapping". Held back with a delay so it only appears once the tile
     is nearly back to its closed width, there is nothing underneath left to
     show through it by the time it does. */
  opacity: 1; transition: opacity .2s var(--ease) .3s;
}
.is-rail .rail-item.is-open .rail-spine {
  opacity: 0; pointer-events: none;
  /* opening runs the other way: the spine should clear out immediately, not
     linger over the panel that is about to need the space */
  transition: opacity .14s var(--ease);
}

/* A rail tile clips its own contents, which is how the collapse works, so the
   selection box has to sit inside the tile rather than straddle its edge. */
.is-rail > .rail-item > .selbox { inset: 4px; }
.is-rail > .rail-item > .selbox em { top: auto; bottom: 0; left: 0; }
.rail-spine .card-mark, .rail-spine .ico { flex: none; }
.rail-spine .ico { color: var(--accent); }
/* A closed tile is wide enough to read across on a desktop, so the name stays
   horizontal there. Only the narrow phone spine turns it on its side, and it is
   set larger there because it is the only thing on the tile.
   The label settles in on its own small delay/beat rather than arriving in
   lockstep with the spine's own crossfade (opposite page) — a flat, single
   fade read as the vertical pill just appearing rather than the row easing
   into it, which is what "more organic" meant here. Nothing about this
   actually rotates over time (writing-mode has no transition of its own to
   animate; the 180deg stays constant throughout) — the settle is a small
   translate lagging a beat behind the spine's opacity, which is what gives
   the sense of motion instead. */
.rail-spine-name {
  font-size: 13.5px; font-weight: 700; letter-spacing: -.03em;
  color: var(--ink); white-space: nowrap; overflow: hidden;
  text-align: center; padding: 0 8px;
  transform: translateY(0);
  transition: transform .38s var(--ease) .1s;
}
.is-rail .rail-item.is-open .rail-spine-name {
  transform: translateY(5px);
  transition: transform .16s var(--ease);
}
@media (max-width: 859px) {
  .rail-spine-name {
    writing-mode: vertical-rl; transform: rotate(180deg) translateY(0);
    font-size: 18px; padding: 0;
  }
  .is-rail .rail-item.is-open .rail-spine-name { transform: rotate(180deg) translateY(5px); }
  /* Every closed spine trying to carry a vertical label at once is what went
     illegible on a narrow row. Only the tile immediately after the open one
     keeps its name; the rest are icon only, so there is always exactly one
     name reading out next to whatever is currently open, never a wall of
     rotated text. Opacity rather than display here too, so stepping between
     tiles fades the label rather than cutting it. */
  #railCards .rail-spine-name, #railTools .rail-spine-name {
    opacity: 0; pointer-events: none; transition: opacity .2s var(--ease), transform .38s var(--ease) .1s;
  }
  /* both neighbours of the open tile now keep a sliver on screen at rest
     (see Rail.measure's peek margin), so both get to keep their name —
     previously only the next one did, back when the open tile filled the
     whole row and there was only ever one edge with anything to read */
  #railCards .rail-item.is-peek .rail-spine-name,
  #railCards .rail-item.is-peek-prev .rail-spine-name,
  #railTools .rail-item.is-peek .rail-spine-name,
  #railTools .rail-item.is-peek-prev .rail-spine-name {
    opacity: 1; pointer-events: auto; transition-delay: .1s;
  }
}

/* The clone that lets the built-tools row loop instead of dead-ending. A
   plain duplicate of the first tile, sat after the last one, so a swipe past
   the end has somewhere to go; once it is the one centred, the row snaps back
   behind it onto the real first tile, unseen. Hidden outright on the desktop
   grid, where there is no row to loop. */
.rail-clone { display: none; }
/* block, matching a real .rail-item's own default (it is never given a
   display of its own — only `flex: 0 0 auto` as a flex-ITEM property of the
   row around it) — not flex. A clone turned into its own flex container was
   letting its .rail-inner child shrink-to-fit under the default flex-shrink,
   even though .rail-inner has an explicit width — a real .rail-item never
   does this to it, block children don't shrink that way, only flex ones do.
   Every clone was quietly narrower than the tile it duplicates, which fed a
   wrong (smaller) number into scrollHeight, and syncHeight takes the worst
   of every real+clone reading — one shrunk clone was enough to undersize the
   whole row's height on any layout that actually uses the clone loop
   (mobile, where the cards row becomes a real rail instead of a grid). */
.is-rail > .rail-clone { display: block; }

/* Sits before tile 0, the one position with no real or cloned neighbour
   ahead of it to peek at (clones only ever trail the last real tile) —
   without it, centring tile 0 left a strip of bare page background
   showing through the padding that exists to let it reach the middle at
   all. `.grid > *` already gives it the same surface and hairline a real
   tile gets; it only needs a width, half a spine, matching the peek every
   other tile shows. Rail.targetLeft/scrollToIndex account for it. */
.rail-cap { display: none; }
/* Gated on scrollable, not just .is-rail: the cap exists purely to fill the
   room .is-rail-scrollable reserves so tile 0 can reach the middle, and that
   room only exists when the row actually scrolls. A row wide enough to show
   every tile at once never adds that padding, so the cap has nothing to sit
   beside — before this, it rendered anyway, a real flex item eating a real
   half-spine of space nothing was reserving it for, which read as an empty
   box before the first tile even though the row never scrolled a pixel. */
.is-rail-scrollable > .rail-cap { display: block; flex: 0 0 auto; width: calc(var(--shut-w, 84px) / 2); }

/* .rail-cap only fills its own half-spine width; the room reserved beside it
   so tile 0 can still reach the middle (.is-rail-scrollable's own
   padding-inline-start, computed the same way) is real space with nothing
   drawn in it, so together the two read as a gap of two different widths
   instead of one continuous edge — the empty-looking box before the first
   tile. This paints that leftover strip the same white a spine already is,
   purely decorative and behind everything real, so it never touches the
   scroll math that already gets the row's resting position right. */
.is-rail-scrollable { position: relative; }
.is-rail-scrollable::before {
  content: ""; position: absolute; inset: 0 auto 0 0; z-index: 0; pointer-events: none;
  width: max(0px, calc((100% - var(--open-w, 380px)) / 2 - var(--shut-w, 84px) / 2));
  background: var(--bg-alt);
}

/* the little progress read-out under a scrolling rail */
.rail-dots { display: none; gap: 6px; justify-content: center; padding-top: var(--s4); }
.rail-dots i {
  width: 5px; height: 5px; border-radius: 50%; background: var(--rule);
  transition: background .3s var(--ease), transform .3s var(--ease);
}
.rail-dots i.on { background: var(--accent); transform: scale(1.5); }

/* small tools row -------------------------------------------------------- */

.tools-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s4); flex-wrap: wrap; margin: var(--s8) 0 var(--s5); }
.tools-head .note { transform: rotate(-2deg); margin-right: auto; }
.tools-all {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-dim);
  transition: color .18s var(--ease), gap .18s var(--ease);
}
.tools-all:hover { color: var(--accent); gap: var(--s3); }

/* A plain equal grid, not a rail -- every tile the same fixed size, all
   five always showing their full name/description/status, nothing opening
   or collapsing to make room for another. Below 640px there just isn't a
   sane number of equal columns, so five full-height tiles stacked instead
   ran very long down the page; a horizontal swipe -- still every card the
   same fixed size, just laid out sideways with scroll-snap instead of JS --
   covers the same five tiles in a fraction of the scroll. */
.mini { grid-template-columns: 1fr; }
@media (min-width: 640px) { .mini { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .mini { grid-template-columns: repeat(5, 1fr); } }
@media (max-width: 639px) {
  .mini {
    display: flex; gap: var(--s3); background: none; border: 0;
    overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-left: var(--pad);
    margin-inline: calc(var(--pad) * -1); padding: 2px var(--pad) var(--s2);
    scrollbar-width: none;
  }
  .mini::-webkit-scrollbar { display: none; }
  .mini-item {
    flex: 0 0 auto; width: min(78vw, 320px); scroll-snap-align: start;
    border: 1px solid var(--rule); border-radius: var(--r); background: var(--bg-alt);
  }
}

.mini-item {
  padding: var(--s5); display: flex; flex-direction: column; gap: 8px;
  transition: background .22s var(--ease);
}
@media (max-width: 859px) {
  .mini-item h4 { font-size: 20px; }
  .mini-item p { font-size: var(--fs-sm); line-height: 1.6; }
  .card-name h3 { font-size: 27px; }
  .card-desc { font-size: 15px; }
}
.mini-item:hover { background: var(--bg-lift); }
.mini-item .ico { color: var(--accent); margin-bottom: 4px; }
.mini-item h4 { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.04em;
  transition: font-weight .18s var(--ease); }
.mini-item:hover h4 { font-weight: 900; }
.mini-item p { margin: 0; font-size: var(--fs-cap); color: var(--ink-dim); line-height: 1.55; }
.mini-item .st { display: block; margin-top: auto; padding-top: var(--s3);
  font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint); }

/* work: one case study, and the door through to everything else ----------- */

.case {
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--bg-alt); padding: var(--s5);
  display: grid; gap: var(--s4); grid-template-columns: 1fr; align-items: center;
  transition: background .22s var(--ease);
}
.case:hover { background: var(--bg-lift); }
@media (min-width: 760px) {
  .case { grid-template-columns: 150px minmax(0, 1fr) auto; gap: var(--s5); padding: var(--s5) var(--s6); }
}
.case h3 { margin: 0; font-size: clamp(20px, 2vw, 26px);
  font-weight: 800; letter-spacing: -.04em; line-height: 1.1; }
.case p { margin: 3px 0 0; font-size: var(--fs-sm); color: var(--ink-dim); }

.work-banner {
  position: relative;
  margin-top: var(--s4);
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--bg-alt); padding: clamp(28px, 4vw, 48px) var(--s6);
  display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap;
  transition: background .22s var(--ease);
}
.work-banner:hover { background: var(--bg-lift); }
.work-word {
  font-size: clamp(56px, 12vw, 150px); font-weight: 800;
  letter-spacing: -.06em; line-height: .82;
}
.work-word .dot { color: var(--accent); }
.work-say {
  flex: 1 1 240px; min-width: 200px;
  font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.55;
  display: flex; flex-direction: column; gap: 8px;
  /* room held for the arrow sitting in the card's own corner below, so the
     copy never runs under it */
  padding: 0 34px 22px 0;
}
/* Pinned to the card's own corner rather than travelling in the flex row —
   a flex sibling with nothing tying it to a line of its own was what left
   it stranded on an empty wrapped row once the wordmark forced a wrap at
   narrow widths. Anchored like this it is always bottom-right, at any width,
   with nothing above it to leave a gap. */
.work-go {
  position: absolute; right: var(--s6); bottom: clamp(28px, 4vw, 48px);
  color: var(--ink-faint);
  transition: transform .2s var(--ease), color .2s var(--ease);
}
.work-banner:hover .work-go { transform: translate(3px, -3px); color: var(--accent); }

/* experience ------------------------------------------------------------- */

/* No border-top here at all now: the section header's own rule sits right
   above this at every width, and a second line directly under it always
   read as one divider too many rather than a deliberate one. */
.xp-row { border-bottom: 1px solid var(--rule); }
.xp-btn {
  width: 100%; background: none; border: 0; text-align: left; cursor: pointer;
  padding: var(--s5) 0;
  display: grid; align-items: center; gap: 10px var(--s5);
  grid-template-columns: minmax(0, 1fr) 24px;
  grid-template-areas: "when sign" "what what";
}
.xp-when { grid-area: when; font-size: var(--fs-cap); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.xp-what { grid-area: what; font-size: clamp(19px, 1.9vw, 26px); font-weight: 700;
  letter-spacing: -.04em; line-height: 1.15; transition: color .18s var(--ease), font-weight .18s var(--ease); }
.xp-what .at { color: var(--ink-faint); font-weight: 400; }
.xp-where { grid-area: where; display: none; font-size: var(--fs-cap); color: var(--ink-dim); }
.xp-sign { grid-area: sign; width: 24px; height: 24px; display: grid; place-items: center;
  justify-self: end; color: var(--ink-faint);
  transition: color .18s var(--ease), transform .28s var(--ease); }
@media (min-width: 860px) {
  .xp-btn { grid-template-columns: 178px minmax(0, 1fr) 180px 24px;
            grid-template-areas: "when what where sign"; }
  .xp-where { display: block; }
}
.xp-btn:hover .xp-what { color: var(--accent); font-weight: 800; }
.xp-btn:hover .xp-sign { color: var(--accent); }
.xp-row.open .xp-sign { transform: rotate(45deg); color: var(--accent); }

.xp-panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .38s var(--ease); }
.xp-row.open .xp-panel { grid-template-rows: 1fr; }
.xp-panel > div { overflow: hidden; }
.xp-panel ul { margin: 0; padding: 0 0 var(--s6); list-style: none; display: grid; gap: 12px; }
@media (min-width: 860px) { .xp-panel ul { padding-left: 202px; } }
.xp-panel li { position: relative; padding-left: 22px;
  font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.62; max-width: 68ch; }
.xp-panel li::before { content: ""; position: absolute; left: 0; top: 10px;
  width: 8px; height: 1px; background: var(--accent); }

/* ------------------------------------------------------------- connect */

.flip {
  position: relative; z-index: 1;
  margin-top: clamp(84px, 10vw, 148px);
  padding: clamp(64px, 8vw, 108px) 0 0;
  background: var(--bg); color: var(--ink);
}
.flip ::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); color: inherit; }

h2.hire { margin: 0; font-size: clamp(38px, 6.6vw, 92px); font-weight: 800;
  line-height: .96; letter-spacing: -.05em; text-wrap: balance; }
h2.hire .dim { color: var(--ink-faint); font-weight: 300; }
.hire-lead { margin: 0; font-size: var(--fs-lead); color: var(--ink-dim); max-width: 44ch; }
.hire-lead b { color: var(--ink); font-weight: 600; }

.connect { grid-template-columns: 1fr; margin-top: var(--s6); }
@media (min-width: 620px) { .connect { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1000px) { .connect { grid-template-columns: repeat(3, 1fr); } }

.cc {
  display: flex; align-items: center; gap: var(--s4);
  padding: var(--s4) var(--s5); min-height: 82px;
  transition: background .2s var(--ease);
}
.cc:hover { background: var(--bg-lift); }
.cc > .ico { color: var(--accent); }
.cc-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.cc-label { font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-faint); }
.cc-value { font-size: var(--fs-sm); font-weight: 600; transition: font-weight .18s var(--ease);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cc:hover .cc-value { font-weight: 800; }
.cc-go { margin-left: auto; color: var(--ink-faint);
  transition: transform .18s var(--ease), color .18s var(--ease); }
.cc:hover .cc-go { transform: translate(2px, -2px); color: var(--accent); }

/* one email row, two addresses, taking turns */
.cc-cycle { display: grid; overflow: hidden; }
.cc-cycle > span {
  grid-area: 1 / 1; white-space: nowrap;
  opacity: 0; transform: translateY(-55%);
  transition: opacity .22s var(--ease), transform .32s var(--ease);
}
/* the outgoing address clears out and drops below the line before the
   incoming one rises up into it from the same spot, so it reads as one
   ticker rolling over rather than a flat cross-dissolve */
.cc-cycle > span.on { opacity: 1; transform: none; transition-delay: .28s; }

.cc-copy {
  margin-left: auto; position: relative; z-index: 7;
  width: 38px; height: 38px; flex: none;
  display: grid; place-items: center;
  border: 1px solid var(--rule); border-radius: var(--r);
  background: var(--bg-alt); color: var(--ink-faint); cursor: pointer;
  transition: color .18s var(--ease), border-color .18s var(--ease);
}
.cc-copy:hover { color: var(--accent); border-color: var(--accent); }
.cc-copy .ico-check { display: none; color: var(--accent); }
.cc-copy.done .ico-copy { display: none; }
.cc-copy.done .ico-check { display: block; }
.cc-copy.done { border-color: var(--accent); }
.cc-tip {
  position: absolute; z-index: 9; bottom: calc(100% + 8px); left: 50%;
  transform: translate(-50%, 4px);
  background: var(--accent); color: var(--on-accent);
  font-size: 10.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: 4px 8px; border-radius: 2px; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity .18s var(--ease), transform .18s var(--ease);
}
.cc-copy.done .cc-tip { opacity: 1; transform: translate(-50%, 0); }

.colophon {
  margin-top: clamp(56px, 7vw, 92px);
  border-top: 1px solid var(--rule);
  padding-block: var(--s5) var(--s7);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s4);
  font-size: var(--fs-cap); color: var(--ink-faint);
}
.colophon .spacer { flex: 1; }
.colophon a { border-bottom: 1px solid transparent;
  transition: border-color .18s var(--ease), color .18s var(--ease); }
.colophon a:hover { color: var(--accent); border-bottom-color: var(--accent); }

/* --------------------------------------------------------------- motion */

.rv { opacity: 0; transform: translateY(12px); }
.rv.in { opacity: 1; transform: none;
  transition: opacity .6s var(--ease), transform .6s var(--ease); transition-delay: var(--d, 0s); }


@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .rv { opacity: 1; transform: none; }
  .hero-bg canvas { opacity: 1; }
  [data-fade] { opacity: 1 !important; transform: none !important; }
  /* no auto advance, and every tile stays open */
  .is-rail > .rail-item, .is-rail > .rail-item.is-open { width: var(--open-w, 380px); }
  .is-rail .rail-spine { display: none; }
}

/* ------------------------------------------------------------------- reel */
/* The gallery page. Columns drift on their own and the whole thing is masked
   top and bottom, so nothing is ever cut off at an edge: tiles fade out before
   they get there. Each column holds its set twice and travels exactly half its
   own height, which is why the loop has no seam. The spacing is a margin on the
   tiles rather than a grid gap, so that half is exact. */

/* padding-block, not the padding shorthand: this sits on the same element as
   .track, which sets left/right padding of its own, and a shorthand here
   was quietly zeroing that back out again — the page margin the mobile work
   page was missing. */
.reel-head { padding-block: clamp(56px, 8vw, 104px) var(--s6); }
.reel-title {
  margin: var(--s4) 0 0; font-size: clamp(64px, 15vw, 200px); font-weight: 800;
  letter-spacing: -.06em; line-height: .84;
}
.reel-title .dot { color: var(--accent); }
.reel-say { margin: var(--s5) 0 0; max-width: 46ch; }

.reel {
  position: relative; overflow: hidden;
  height: clamp(460px, 74vh, 860px);
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(2, 1fr);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 84%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 16%, #000 84%, transparent 100%);
}
@media (min-width: 700px)  { .reel { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1000px) { .reel { grid-template-columns: repeat(4, 1fr); } }
/* the reel packed with tiles reads as edge to edge next to the rest of the
   page's usual margin; a little extra on top of it just here */
@media (max-width: 640px) { .reel, .reel-note { margin-inline: 10px; } }

.reel-col { animation: reel var(--dur, 60s) linear infinite; will-change: transform; }
.reel-col:nth-child(even) { animation-direction: reverse; }
.reel-col span {
  position: relative;
  display: block; margin-bottom: var(--s3); border-radius: var(--r);
  background: var(--bg-alt); box-shadow: inset 0 0 0 1px var(--rule);
  overflow: hidden; cursor: pointer;
  transition: transform .3s var(--ease), box-shadow .3s var(--ease);
}
@keyframes reel { from { transform: translateY(0); } to { transform: translateY(-50%); } }

/* Pointer devices only: point at a tile and the drift stops, the tile lifts
   a little, and the one line it can offer right now — that it is not
   uploaded yet — fades in over it. Nothing to hover at with a touch, so it
   never fires there and the reel just keeps drifting as normal. */
@media (hover: hover) and (pointer: fine) {
  .reel-col:has(span:hover) { animation-play-state: paused; }
  .reel-col span:hover {
    transform: scale(1.055);
    box-shadow: inset 0 0 0 1px var(--accent), 0 14px 30px rgba(0, 0, 0, .2);
    z-index: 2;
  }
}

/* A tap does on touch what hover already does with a pointer: picks the tile
   out, stops its column drifting, and marks it with the same transform-
   handle language as [data-sel] elsewhere on the page (see .selbox) — a
   hairline box plus small corner squares, not a heavy stroke/drop shadow,
   which read as a "selected card" UI rather than the page's own selection
   marks. One tile picked at a time. */
.reel-col.is-paused { animation-play-state: paused; }
/* The tile's own overflow: hidden (needed to crop whatever image eventually
   fills it) was also clipping the anchor's corner squares, which sit right
   at its edge by design — the same way Figma's own handles straddle the
   selection boundary rather than staying safely inside it. Letting overflow
   go visible only while a tile is picked is what lets them actually show. */
.reel-col span.is-picked { transform: scale(1.06); z-index: 3; overflow: visible; }
/* `.reel-col span` (a class plus a type selector) outranks a bare
   `.pick-box` class on specificity alone, regardless of source order, and
   was quietly winning the `position` property — the box was rendering
   `position: static/relative` from that base rule instead of `absolute`,
   collapsing to zero height since nothing else was there to size it.
   Scoped under `.reel-col` here so it wins on the same terms. */
.reel-col .pick-box {
  position: absolute; inset: 4px; z-index: 4; pointer-events: none;
  opacity: 0; transition: opacity .16s var(--ease);
}
.reel-col span.is-picked .pick-box { opacity: 1; }
.pick-box::before { content: ""; position: absolute; inset: 0; border: 1px solid var(--accent); }
.pick-box i {
  position: absolute; width: 7px; height: 7px;
  background: var(--bg-alt); border: 1px solid var(--accent); border-radius: 1px;
}
.pick-box i.nw { top: -4px; left: -4px; }
.pick-box i.ne { top: -4px; right: -4px; }
.pick-box i.se { bottom: -4px; right: -4px; }
.pick-box i.sw { bottom: -4px; left: -4px; }
.reel-cap {
  position: absolute; inset: auto 0 0 0; z-index: 1;
  padding: 10px 12px;
  font-style: normal; font-size: var(--fs-cap); font-weight: 600; color: #f3f1ea;
  background: linear-gradient(to top, rgba(8, 8, 6, .58), transparent);
  opacity: 0; transform: translateY(6px);
  transition: opacity .22s var(--ease), transform .22s var(--ease);
}
.reel-col span:hover .reel-cap { opacity: 1; transform: translateY(0); }

.reel-note { display: flex; justify-content: center; padding: var(--s6) 0 0; }
.reel-note span {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 10px 18px; border-radius: 999px;
  background: var(--bg-alt); border: 1px solid var(--rule);
  font-size: var(--fs-cap); font-weight: 600; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-dim);
}
.reel-note span i { width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
  animation: pulse 2.6s var(--ease) infinite; }

@media (prefers-reduced-motion: reduce) { .reel-col { animation: none; } }

/* -------------------------------------------------------- product page -- */
/* sift.html: one small, standalone landing page, screenshot-led rather
   than prose-led. Reuses the same tokens/primitives as the rest of the
   site (.sec, .split, .btn, .card-mark …) so it never reads as a
   separately-branded microsite; the only new pieces are the hero block
   and the screenshot grid below. */

/* padding-block, not the padding shorthand: this sits on the same element
   as .track (which sets left/right padding of its own), and a shorthand
   here would zero that back out — the exact bug already hit once on the
   work page's header (see .reel-head above). */
.prod-head { padding-block: clamp(56px, 8vw, 104px) var(--s7); }
.prod-display {
  margin: var(--s4) 0 0; font-size: clamp(34px, 5.2vw, 66px); font-weight: 800;
  line-height: 1.02; letter-spacing: -.04em; text-wrap: balance; max-width: 16ch;
}
.prod-display .dim { color: var(--ink-faint); font-weight: 300; }
.prod-lead { margin-top: var(--s5); max-width: 58ch; }

.prod-tool:first-of-type { padding-top: clamp(48px, 6vw, 80px); }
.prod-tool .sec-head .card-mark { width: 34px; height: 34px; background: var(--c); }
.prod-tool .sec-head .card-mark .ico { width: 18px; height: 18px; color: #0b0c0f; }

/* the problem/time-saved callout: a tinted rectangle rather than another
   plain paragraph, so the "why this exists" reads as a distinct claim
   you can weigh, not just more lead copy. The number is always worked
   math (stated plainly), never a collected statistic — neither app has
   telemetry, so nothing here pretends to be measured. */
.impact {
  margin-top: var(--s6);
  display: grid; gap: var(--s5); grid-template-columns: 1fr;
  padding: var(--s6); border-radius: var(--r);
  border: 1px solid var(--rule);
  background: color-mix(in srgb, var(--c) 7%, var(--bg-alt));
}
@media (min-width: 760px) {
  .impact { grid-template-columns: 1fr 1px 1fr; }
}
.impact-rule { display: none; }
@media (min-width: 760px) { .impact-rule { display: block; align-self: stretch; background: var(--rule); } }
.impact-k {
  display: block; font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .12em;
  text-transform: uppercase; color: var(--c); margin-bottom: var(--s3);
}
.impact-problem p { margin: 0; font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.6; }
.impact-n {
  display: block; font-size: clamp(30px, 4vw, 44px); font-weight: 800; letter-spacing: -0.03em;
  line-height: 1;
}
.impact-l { display: block; font-size: var(--fs-cap); color: var(--ink-faint); margin-top: 6px; }
.impact-math { margin: var(--s3) 0 0; font-size: var(--fs-cap); color: var(--ink-dim); line-height: 1.55; }

/* three screenshots read as one continuous walkthrough — a numbered mark,
   an arrow between each pair on desktop, a plain top-to-bottom stack on a
   phone (the arrow is a "next" cue that only makes sense left-to-right). */
.steps {
  display: grid; gap: var(--s5); grid-template-columns: 1fr;
  margin-top: var(--s6); align-items: start;
}
@media (min-width: 860px) {
  .steps { grid-template-columns: 1fr 28px 1fr 28px 1fr; gap: var(--s4); }
  /* a walkthrough with only two real states (add something, see the
     result) instead of three — same component, one fewer column pair */
  .steps.steps-2 { grid-template-columns: 1fr 28px 1fr; }
}
.step { margin: 0; }
.step-n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; margin-bottom: var(--s3);
  border-radius: 50%; background: var(--c); color: #0b0c0f;
  font-size: 11.5px; font-weight: 800; font-variant-numeric: tabular-nums;
}
/* the screenshots themselves are the one place on the site that gets a
   real frame — a white mat and a soft shadow, like a photo set down on
   the page, rather than the hairline hosts everything else uses */
.step img {
  width: 100%; display: block; border-radius: 14px;
  border: 3px solid #fff; background: var(--bg-lift);
  box-shadow: 0 18px 40px rgba(20, 20, 15, .16), 0 3px 10px rgba(20, 20, 15, .08);
}
:root[data-theme="dark"] .step img {
  box-shadow: 0 18px 40px rgba(0, 0, 0, .55), 0 3px 10px rgba(0, 0, 0, .35);
}
.step figcaption {
  margin-top: var(--s3); font-size: var(--fs-cap); color: var(--ink-dim); line-height: 1.5;
}
.step figcaption b { color: var(--ink); font-weight: 700; }

.step-arrow { display: none; }
@media (min-width: 860px) {
  .step-arrow {
    display: flex; align-items: center; justify-content: center;
    height: 100%; padding-top: 60px; color: var(--ink-faint);
  }
  .step-arrow .ico { width: 18px; height: 18px; }
}

.prod-facts {
  margin-top: var(--s6); padding-top: var(--s5); border-top: 1px solid var(--rule);
  display: grid; gap: var(--s4); grid-template-columns: 1fr;
}
@media (min-width: 760px) { .prod-facts { grid-template-columns: repeat(3, 1fr); } }
.prod-fact {
  display: flex; align-items: flex-start; gap: var(--s3);
  font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.5;
}
.prod-fact .ico { flex: none; width: 18px; height: 18px; margin-top: 1px; color: var(--accent); }

.prod-cta {
  margin-top: var(--s6); display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
}
.prod-cta [aria-disabled="true"] { cursor: default; opacity: .55; }
.prod-cta .btn-accent[aria-disabled="true"] { background: var(--c); border-color: var(--c); color: #0b0c0f; }
.prod-cta-note { font-size: var(--fs-cap); color: var(--ink-faint); }

.prod-foot { padding-block: var(--s8) clamp(64px, 8vw, 96px); }
.colophon-link {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: var(--s3) var(--s4); margin-inline-start: calc(var(--s4) * -1);
  border-radius: var(--r);
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-dim);
  transition: color .18s var(--ease), gap .18s var(--ease), background .18s var(--ease);
}
/* The arrow icon it shares with every other button on the site points up-right,
   rotated here to point back the way this link actually goes. Rotating the
   *use* element directly, as this originally did, spins it around the SVG's
   own coordinate-space origin (0,0 of its 0-24 viewBox) rather than the
   glyph's own centre — since the arrow's path only occupies the middle of
   that box, the rotation swings the whole shape out past the SVG's clipped
   edge, and it simply vanishes. transform-box: fill-box makes the glyph's
   own bounding box the frame the rotation happens in, so it turns in place
   instead of swinging out of frame. */
.colophon-link .ico {
  width: 16px; height: 16px;
  transform: rotate(-135deg); transform-box: fill-box; transform-origin: center;
}
.colophon-link:hover { color: var(--accent); gap: var(--s3); background: var(--bg-lift); }

/* -------------------------------------------------------- ps-plugins.html */

.plugin-list { display: flex; flex-direction: column; gap: var(--s5); margin-top: var(--s6); }
.plugin {
  border: 1px solid var(--rule); border-radius: var(--r); background: var(--bg-alt);
  padding: var(--s6); display: grid; gap: var(--s5); grid-template-columns: 1fr;
  transition: background .2s var(--ease);
}
@media (min-width: 860px) {
  /* the CTA column is a fixed width, not auto -- auto would size itself to
     whichever button happens to be expanded at that instant, which is what
     let the middle (description) column's own width, and the text
     reflowing inside it, get dragged along by a hover two columns over. */
  .plugin { grid-template-columns: 200px minmax(0, 1fr) 168px; align-items: start; gap: var(--s6); }
}
.plugin-head { display: flex; align-items: flex-start; gap: var(--s4); }
.plugin-mark {
  width: 42px; height: 42px; flex: none; border-radius: var(--r);
  background: var(--c); color: #0b0c0f; display: grid; place-items: center;
}
.plugin-mark .ico { width: 21px; height: 21px; stroke-width: 2.1; }
.plugin-name { display: flex; flex-direction: column; gap: 4px; padding-top: 2px; }
.plugin-name h3 { margin: 0; font-size: 21px; font-weight: 800; letter-spacing: -0.03em; }
.plugin-status {
  font-size: var(--fs-eyebrow); font-weight: 600; letter-spacing: .1em;
  text-transform: uppercase; color: var(--c);
}
.plugin-body { display: flex; flex-direction: column; gap: var(--s4); min-width: 0; }
.plugin-body p { margin: 0; font-size: var(--fs-sm); color: var(--ink-dim); line-height: 1.62; max-width: 56ch; }
.plugin-uses { margin: 0; padding: 0; list-style: none; display: grid; gap: 7px; }
.plugin-uses li {
  position: relative; padding-left: 17px;
  font-size: var(--fs-cap); color: var(--ink-dim); line-height: 1.5; max-width: 52ch;
}
.plugin-uses li::before {
  content: ""; position: absolute; left: 0; top: 8px; width: 7px; height: 1px; background: var(--c);
}
.plugin-ctas { display: flex; flex-direction: row; align-items: center; gap: var(--s3); }
@media (min-width: 860px) { .plugin-ctas { flex-direction: column; align-items: flex-end; } }

/* Icon only at rest; a pointer that can actually hover gets the label on
   hover or focus, a touch device (no hover to reveal it with) just stays
   expanded. The resize itself is deliberate -- it's what tells you the
   button IS a button with a label, not decoration -- the earlier problem
   was never the resizing button, it was the *column* next to it also
   being auto-width, so the description text two columns over reflowed
   along with it. Fixed at the layout level instead: .plugin's third grid
   column is a fixed 168px (see above), sized to fit this button's full
   220px max-width plus room either side, so the button has somewhere
   fixed to grow into and nothing beside it ever has to move. Square
   corners throughout, per the shape rule on .btn -- a resizing button is
   still not a pill. */
.icon-cta {
  display: inline-flex; align-items: center; height: 40px; max-width: 40px;
  border-radius: var(--r); border: 1px solid var(--rule); background: var(--bg);
  color: var(--ink-dim); overflow: hidden; white-space: nowrap; cursor: pointer;
  transition: max-width .32s var(--ease), background .18s var(--ease),
              border-color .18s var(--ease), color .18s var(--ease);
}
.icon-cta .ico { flex: none; width: 17px; height: 17px; margin-inline: 11px; }
.icon-cta .lbl { font-size: var(--fs-cap); font-weight: 600; padding-right: 16px; }
.icon-cta:hover, .icon-cta:focus-visible { color: var(--c); border-color: var(--c); }
@media (hover: hover) and (pointer: fine) {
  .icon-cta:hover, .icon-cta:focus-visible { max-width: 220px; }
}
@media (hover: none), (pointer: coarse) {
  .icon-cta { max-width: 220px; }
}
.icon-cta[aria-disabled="true"] { cursor: default; opacity: .6; }
.icon-cta[aria-disabled="true"]:hover, .icon-cta[aria-disabled="true"]:focus-visible {
  color: var(--ink-dim); border-color: var(--rule);
}
