/* site.css — teerthsharma.github.io
   Built from SPEC.md sections 3 (tokens), 4 (type), 5 (gloss),
   11 (responsive), 12 (accessibility), 15 (class name contract). */

/* ---------- 3. Colour tokens (verbatim) ---------- */
:root{
  --ground:#fbfaf7; --ground2:#f3f1eb; --raised:#ffffff;
  --hair:#e5e2da; --hair2:#cfcbc1;
  --ink:#1c1b19; --muted:#5f5b53;

  --blue-700:#163d9a;  --blue-500:#2456dc;  --blue-100:#e4ecff;   /* MEMORY */
  --violet-700:#6b35c4;--violet-500:#a66cf0;--violet-100:#efe6ff; /* PARALLELISM */
  --amber-700:#9a4906; --amber-500:#d96a06; --amber-100:#ffefdf;  /* TIME */
  --mint-700:#0a6b7c;  --mint-500:#0b93ab;  --mint-100:#dcf5f9;   /* SCALE */
  --coral-700:#a01c3f; --coral-500:#d9376e; --coral-100:#ffe6ec;  /* THE OLD PATH */
  --green-700:#0e4a23; --green-500:#146a32; --green-100:#dff5e6;  /* CORRECTNESS */

  /* ---------- 4. Type ---------- */
  --font-sans:"Instrument Sans","Segoe UI",system-ui,-apple-system,sans-serif;
  --font-mono:"JetBrains Mono",ui-monospace,"Cascadia Mono",monospace;

  /* scale, ratio 1.333 */
  --fs-0:0.750rem; --fs-1:1.000rem; --fs-2:1.333rem; --fs-3:1.777rem;
  --fs-4:2.369rem; --fs-5:3.157rem; --fs-6:4.209rem; --fs-7:5.610rem;
  --fs-giant:7.478rem;

  /* ---------- 5. Gloss ---------- */
  --r:10px;
  --e0: none;
  --e1: 0 1px 0 #ffffff inset, 0 1px 2px rgba(28,27,25,.06), 0 1px 1px rgba(28,27,25,.04);
  --e2: 0 1px 0 #ffffff inset, 0 2px 4px rgba(28,27,25,.07), 0 8px 20px rgba(28,27,25,.06);
  --e3: 0 1px 0 #ffffff inset, 0 4px 8px rgba(28,27,25,.08), 0 18px 40px rgba(28,27,25,.09);
  --hover-ease: cubic-bezier(.2,.7,.3,1);
}

/* ---------- base / reset ---------- */
*,*::before,*::after{ box-sizing:border-box; }
html{ -webkit-text-size-adjust:100%; }
/* clip, not hidden: overflow-x:hidden makes the page itself a scroll
   container, and then the sticky site nav never sticks. clip cuts the
   same sideways overflow without that, so the nav follows the reader on
   every page. hidden stays as the fallback for browsers without clip. */
html,body{ max-width:100%; overflow-x:hidden; }
@supports (overflow-x: clip){ html, body{ overflow-x:clip; } }
body{
  margin:0;
  background:var(--ground);
  color:var(--ink);
  font-family:var(--font-sans);
  font-size:var(--fs-1);
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
}
h1,h2,h3,p,figure{ margin:0; }
ul{ list-style:none; margin:0; padding:0; }
a{ color:inherit; text-decoration:none; }
button{ font:inherit; color:inherit; background:none; border:0; }
img,svg{ max-width:100%; display:block; }

/* ---------- 12. Accessibility ---------- */
:focus-visible{ outline:2px solid var(--blue-500); outline-offset:2px; }
.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;
}

/* ---------- Utility ---------- */
.muted{ color:var(--muted); }
.mono{ font-family:var(--font-mono); font-variant-numeric:tabular-nums; }
.is-hidden{ display:none !important; }

[data-reveal]{
  opacity:0;
  transform:translateY(8px);
  transition:opacity 200ms ease-out, transform 200ms ease-out;
}
[data-reveal].is-in{ opacity:1; transform:none; }

@media (prefers-reduced-motion: reduce){
  [data-reveal]{ opacity:1; transform:none; transition:none; }
  .card, .proj, .btn, .filter, .proofcell{ transition:none; }
  .card:hover, .proj:hover, .proofcell:hover{ transform:none; }
  .mosaic__read{ transition:none; }
}

/* ---------- SVG animation system (draw / pop / wipe / pulse) ----------
   Base state for .draw/.pop/.wipe is the plain, fully-visible SVG markup —
   nothing here hides them. site.js (initArt) sets the "hidden" starting
   inline style only at animation time, then transitions it away, so the
   page is correct with JS disabled and does nothing extra when
   prefers-reduced-motion is set (initArt bails out before touching the DOM). */
.pop, .pulse{ transform-box:fill-box; transform-origin:center; }

@keyframes art-pulse{
  0%, 100%{ transform:scale(1); }
  50%{ transform:scale(1.15); }
}
.is-pulsing{
  animation:art-pulse 900ms var(--hover-ease) 2;
  animation-delay:var(--pulse-delay, 0ms);
}

/* ---------- Layout ---------- */
.wrap{ max-width:1120px; margin:0 auto; padding:0 24px; }

.band{ padding:88px 0; background:var(--ground); }
.band--tint{ background:var(--ground2); }

.grid{ display:grid; gap:24px; align-items:stretch; }
.grid--3{ grid-template-columns:repeat(3,1fr); }
.grid--2{ grid-template-columns:repeat(2,1fr); }
@media (max-width:1080px){
  .grid--3{ grid-template-columns:repeat(2,1fr); }
  /* same orphan guard as .proofgrid: only fires when the child count is odd. */
  .grid--3 > *:nth-child(2n+1):last-child{ grid-column:span 2; }
}
@media (max-width:760px){
  .grid--3{ grid-template-columns:1fr; }
  .grid--2{ grid-template-columns:1fr; }
}

.nav{
  position:sticky; top:0; z-index:50;
  height:56px;
  background:color-mix(in srgb, var(--ground) 92%, transparent);
  background-color:var(--ground); /* fallback, no color-mix */
  border-bottom:1px solid var(--hair);
}
@supports (background: color-mix(in srgb, red 50%, transparent)){
  .nav{ background:color-mix(in srgb, var(--ground) 92%, transparent); }
}
.nav__inner{
  max-width:1120px; margin:0 auto; padding:0 24px;
  height:56px;
  display:flex; align-items:center; justify-content:space-between;
  gap:24px;
}
.nav__brand{ font-weight:700; font-size:var(--fs-1); color:var(--ink); }
.nav__links{ display:flex; align-items:center; gap:24px; }
.nav__link{ color:var(--muted); font-weight:500; }
.nav__link:hover{ color:var(--ink); }
@media (max-width:480px){
  .nav__links{ gap:16px; }
  .nav__link:not(:last-child){ display:none; }
}

/* ---------- Hero ---------- */
.hero{ padding:88px 0 64px; }
.hero__h1{
  font-size:var(--fs-6); line-height:1.05; letter-spacing:-0.02em; font-weight:700;
}
.hero__sub{
  margin-top:20px;
  color:var(--muted);
  font-size:var(--fs-2);
  line-height:1.6;
  max-width:66ch;
}

.cta-row{ display:flex; flex-wrap:wrap; align-items:center; gap:16px; margin-top:32px; }

/* ---------- Hero (two column, with proof panel) ---------- */
.hero--split{
  display:grid; grid-template-columns:1.1fr 1fr; gap:48px; align-items:center;
}
.hero--split .hero__copy{ min-width:0; }
.hero__proof{ min-width:0; }

.proofgrid{
  display:grid; grid-template-columns:repeat(4,1fr); gap:12px;
}
.proofcell{
  display:flex; flex-direction:column; gap:6px;
  padding:14px;
  background:var(--raised);
  border:1px solid var(--hair);
  border-radius:8px;
  box-shadow:var(--e1);
  min-width:0;
  transition:box-shadow 160ms var(--hover-ease), transform 160ms var(--hover-ease), border-color 160ms var(--hover-ease);
}
.proofcell:hover{
  box-shadow:var(--e2);
  transform:translateY(-2px);
  border-color:var(--hair2);
}
.proofcell:focus-visible{
  outline:2px solid var(--blue-500);
  outline-offset:2px;
  box-shadow:var(--e2);
}
.proofcell__mark{ width:20px; height:20px; object-fit:contain; flex:none; }
.proofcell__art{ width:100%; }
.proofcell__art svg{ width:100%; height:auto; display:block; }
.proofcell__num{
  font-family:var(--font-mono); font-variant-numeric:tabular-nums;
  font-weight:800; font-size:var(--fs-1);
  color:var(--ink);
  overflow-wrap:anywhere;
  min-width:0;
}
.proofcell__org{
  font-family:var(--font-mono); font-size:var(--fs-0);
  text-transform:uppercase; letter-spacing:0.08em;
  color:var(--muted);
}
@media (max-width:900px){
  .hero--split{ grid-template-columns:1fr; }
  .proofgrid{ grid-template-columns:repeat(2,1fr); }
  /* eleven cells at 2 columns leaves a lone 11th on its own row; give it
     the full row instead of stranding it. No-op whenever the child count
     is even. */
  .proofcell:nth-child(2n+1):last-child{ grid-column:span 2; }
}

.btn{
  display:inline-flex; align-items:center; justify-content:center;
  padding:12px 22px;
  border-radius:var(--r);
  font-weight:600;
  font-size:var(--fs-1);
  border:1px solid transparent;
  transition:box-shadow 160ms var(--hover-ease), transform 160ms var(--hover-ease), border-color 160ms var(--hover-ease), background-color 160ms var(--hover-ease);
}
.btn--primary{
  background:var(--blue-700); color:#fff; box-shadow:var(--e1);
}
.btn--primary:hover{ box-shadow:var(--e2); transform:translateY(-2px); }
.btn--ghost{
  background:var(--raised); color:var(--ink); border-color:var(--hair);
}
.btn--ghost:hover{ border-color:var(--hair2); box-shadow:var(--e1); transform:translateY(-2px); }
.btn--text{ padding:12px 4px; color:var(--blue-700); font-weight:600; }
.btn--text:hover{ text-decoration:underline; }

/* Number strip: superseded by the assembling mosaic (#mosaic, initMosaic
   in site.js), which shows the same per-PR numbers with attribution
   instead of four unattributed aggregates. */

/* ---------- Cards ---------- */
.card{
  display:flex; flex-direction:column;
  background:var(--raised);
  border:1px solid var(--hair);
  border-radius:var(--r);
  box-shadow:var(--e1);
  padding:24px;
  transition:box-shadow 160ms var(--hover-ease), transform 160ms var(--hover-ease), border-color 160ms var(--hover-ease);
}
.card:hover{
  box-shadow:var(--e2);
  transform:translateY(-2px);
  border-color:var(--hair2);
}
.card__head{ display:flex; align-items:center; gap:10px; }
.card__mark{ width:28px; height:28px; object-fit:contain; flex:none; }
.card__repo{
  font-family:var(--font-mono); font-variant-numeric:tabular-nums;
  font-size:var(--fs-0); color:var(--muted);
}
.card__title{
  margin-top:16px;
  font-size:var(--fs-2); line-height:1.3; font-weight:600; color:var(--ink);
}
.card__body{ margin-top:8px; font-size:var(--fs-1); line-height:1.6; color:var(--ink); max-width:66ch; }
.card__art{ width:100%; margin-top:20px; }
.card__art svg{ width:100%; height:auto; }
.card__cap{
  margin-top:12px;
  font-size:var(--fs-0); line-height:1.4; font-weight:500;
  text-transform:uppercase; letter-spacing:0.08em;
  color:var(--muted);
}
.card__link{
  margin-top:auto;
  padding-top:16px;
  font-weight:600; font-size:var(--fs-0);
  color:var(--blue-700);
  align-self:flex-start;
}
.card__link:hover{ text-decoration:underline; }

.card--panel{
  padding:64px 48px;
  box-shadow:var(--e3);
  text-align:center;
  align-items:center;
}
.card--panel:hover{ transform:none; box-shadow:var(--e3); border-color:var(--hair); }

/* ---------- Projects (work.html) ---------- */
.filters{ display:flex; flex-wrap:wrap; gap:12px; }
.filter{
  padding:8px 18px;
  border:1px solid var(--hair2);
  border-radius:999px;
  background:var(--raised);
  font-size:var(--fs-0);
  font-weight:600;
  color:var(--ink);
  cursor:pointer;
  transition:background-color 160ms var(--hover-ease), border-color 160ms var(--hover-ease), color 160ms var(--hover-ease);
}
.filter:hover{ border-color:var(--hair2); box-shadow:var(--e1); }
.filter[aria-pressed="true"]{
  background:var(--blue-100); border-color:var(--blue-500); color:var(--blue-700);
}

.proj{
  display:flex; flex-direction:column;
  background:var(--raised);
  border:1px solid var(--hair);
  border-radius:var(--r);
  box-shadow:var(--e1);
  padding:24px;
  transition:box-shadow 160ms var(--hover-ease), transform 160ms var(--hover-ease), border-color 160ms var(--hover-ease);
}
.proj:hover{ box-shadow:var(--e2); transform:translateY(-2px); border-color:var(--hair2); }
.proj__name{ font-size:var(--fs-2); line-height:1.3; font-weight:600; color:var(--ink); }
.proj__claim{ margin-top:8px; font-size:var(--fs-1); line-height:1.6; color:var(--muted); max-width:66ch; }
.proj__art{ width:100%; margin-top:20px; }
.proj__art svg{ width:100%; height:auto; }
.proj__nums{
  margin-top:16px;
  display:flex; flex-wrap:wrap; gap:6px 16px;
  font-family:var(--font-mono); font-variant-numeric:tabular-nums;
  font-size:var(--fs-0); color:var(--ink);
}
.proj__tags{ margin-top:16px; display:flex; flex-wrap:wrap; gap:8px; }
.tags{ display:flex; flex-wrap:wrap; gap:8px; }
.tag{
  display:inline-flex;
  align-items:center;
  max-width:100%;
  padding:4px 10px;
  border-radius:999px;
  background:var(--ground2);
  color:var(--muted);
  font-family:var(--font-mono);
  font-size:var(--fs-0);
  font-weight:600;
  text-transform:uppercase;
  letter-spacing:0.06em;
  line-height:1.3;
  overflow-wrap:anywhere;
}
.tag--lang{ background:var(--blue-100); color:var(--blue-700); }
.tag--class{ background:var(--mint-100); color:var(--mint-700); }
.tag--doi{ background:var(--green-100); color:var(--green-700); }

/* ---------- Mosaic (assembling pictogram, replaces the number strip) ---------- */
.mosaic{ background:var(--ground2); }
.mosaic__inner{
  display:grid; grid-template-columns:1fr 1fr; gap:48px; align-items:center;
}
/* One canvas, 40 x 10 cells plus a swarm margin, fitted to this width by
   initMosaic, which also sets its height. */
.mosaic__grid{ max-width:640px; min-width:0; }
.mosaic__grid canvas{ display:block; width:100%; }

.mosaic__read{ transition:opacity 260ms ease; }
.mosaic__read.is-fading{ opacity:0; }
.mosaic__org{
  font-family:var(--font-mono); font-size:var(--fs-0); color:var(--muted);
  text-transform:uppercase; letter-spacing:0.08em;
}
.mosaic__num{
  margin-top:8px;
  font-family:var(--font-mono); font-variant-numeric:tabular-nums;
  font-weight:800; font-size:5.6rem; line-height:1; letter-spacing:-0.02em;
  color:var(--ink);
  overflow-wrap:anywhere;
}
.mosaic__label{ margin-top:8px; font-size:var(--fs-2); font-weight:600; color:var(--ink); }
.mosaic__sub{ margin-top:4px; font-size:var(--fs-0); color:var(--muted); }

.mosaic__pause{
  margin-top:20px;
  display:inline-flex; align-items:center; gap:6px;
  padding:8px 14px;
  border:1px solid var(--hair2);
  border-radius:999px;
  background:var(--raised);
  font-size:var(--fs-0); font-weight:600; color:var(--ink);
  cursor:pointer;
  transition:border-color 160ms var(--hover-ease), box-shadow 160ms var(--hover-ease);
}
.mosaic__pause:hover{ box-shadow:var(--e1); }

@media (max-width:900px){
  .mosaic__inner{ grid-template-columns:1fr; }
}
@media (max-width:480px){
  .mosaic__num{ font-size:2.8rem; }
}

/* ---------- Figures ---------- */
.fig{ width:100%; }
.fig svg{ width:100%; height:auto; }
.fig__cap{
  margin-top:12px;
  font-size:var(--fs-0); line-height:1.4; font-weight:500;
  text-transform:uppercase; letter-spacing:0.08em;
  color:var(--muted);
}

/* ---------- Slider ---------- */
.slider{ display:flex; flex-direction:column; gap:10px; margin-top:16px; }
.slider__input{
  width:100%;
  accent-color:var(--blue-500);
}
.slider__out{
  font-family:var(--font-mono); font-variant-numeric:tabular-nums;
  font-size:var(--fs-1); font-weight:600; color:var(--ink);
}

/* ---------- Triton schedule scan (pr-triton-kernels-22 card) ----------
   This replaces a random per-block scatter. 136 rects each drifting in its
   own direction did not read as a design; it read as a rendering fault,
   and the unscheduled grey blocks ghosting around made it worse.

   The diagram is a causal block SCHEDULE, so the honest animation is the
   schedule being walked. A brightness scan sweeps through the blocks in
   document order, which for a row-major lower triangle is a diagonal wave
   from the first row to the last. No transform is involved at all, so no
   block can ever appear out of place: the triangle is always exactly as
   drawn, and only lightness moves across it.

   Delay buckets are modulo-based, so the wave holds for any block count.
   ------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference){
  .tri-block{
    transform-box: fill-box;
    transform-origin: center;
    animation: tri-scan 3.2s ease-in-out infinite;
  }
  @keyframes tri-scan{
    0%   { opacity: 1;   transform: scale(1); }
    50%  { opacity: .52; transform: scale(.88); }
    100% { opacity: 1;   transform: scale(1); }
  }
  .tri-block:nth-of-type(12n+1){ animation-delay:0ms; }
  .tri-block:nth-of-type(12n+2){ animation-delay:70ms; }
  .tri-block:nth-of-type(12n+3){ animation-delay:140ms; }
  .tri-block:nth-of-type(12n+4){ animation-delay:210ms; }
  .tri-block:nth-of-type(12n+5){ animation-delay:280ms; }
  .tri-block:nth-of-type(12n+6){ animation-delay:350ms; }
  .tri-block:nth-of-type(12n+7){ animation-delay:420ms; }
  .tri-block:nth-of-type(12n+8){ animation-delay:490ms; }
  .tri-block:nth-of-type(12n+9){ animation-delay:560ms; }
  .tri-block:nth-of-type(12n+10){ animation-delay:630ms; }
  .tri-block:nth-of-type(12n+11){ animation-delay:700ms; }
  .tri-block:nth-of-type(12n){ animation-delay:770ms; }
}



@media (max-width:760px){
  .band{ padding:64px 0; }
  .hero{ padding:64px 0 48px; }
}
@media (max-width:480px){
  .wrap{ padding:0 16px; }
  .nav__inner{ padding:0 16px; }
  .band{ padding:48px 0; }
  .hero__h1{ font-size:var(--fs-6); }
  .card--panel{ padding:40px 24px; }
}

/* ------------------------------------------------------------------
   Diagram motion, pure CSS.
   Driven by CSS animation rather than JavaScript on purpose: an
   IntersectionObserver plus getTotalLength was silently doing nothing in
   testing, and a diagram that animates by script is a diagram that can be
   left invisible when the script does not run. pathLength="1" on every
   .draw element normalises each path to a length of 1, so one keyframe
   draws any path without measuring it.
   Everything here sits inside prefers-reduced-motion: no-preference, so
   the reduced-motion state is simply the finished state.
   ------------------------------------------------------------------ */
@media (prefers-reduced-motion: no-preference){
  @keyframes art-draw { from { stroke-dashoffset: 1; } to { stroke-dashoffset: 0; } }
  @keyframes art-pop  { from { opacity: 0; transform: scale(.55); } to { opacity: 1; transform: scale(1); } }
  @keyframes art-wipe { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
  @keyframes art-pulse{ 0%,100% { r: var(--r0); } 50% { r: calc(var(--r0) * 1.6); } }

  /* The draw-on entrance is deliberately not applied to strokes.
     stroke-dasharray:1 with dashoffset:1 means the line is entirely
     hidden until the animation runs, and this site drives animation from
     a scroll timeline where supported. A timeline that does not advance
     leaves a chart with no line in it, and an invisible measurement is a
     worse outcome than a static one. Strokes are solid from first paint.
     The motion that stays cannot fail closed: point fields brighten, the
     Triton schedule scans, the mosaic assembles, the marks pulse. */
  .pop {
    transform-box: fill-box;
    transform-origin: center;
    animation: art-pop 520ms cubic-bezier(.2,.8,.3,1) both;
  }
  .wipe {
    animation: art-wipe 1250ms cubic-bezier(.4,0,.2,1) both;
  }
  .pop:nth-of-type(1){ animation-delay: 26ms; }
  .pop:nth-of-type(2){ animation-delay: 52ms; }
  .pop:nth-of-type(3){ animation-delay: 78ms; }
  .pop:nth-of-type(4){ animation-delay: 104ms; }
  .pop:nth-of-type(5){ animation-delay: 130ms; }
  .pop:nth-of-type(6){ animation-delay: 156ms; }
  .pop:nth-of-type(7){ animation-delay: 182ms; }
  .pop:nth-of-type(8){ animation-delay: 208ms; }
  .pop:nth-of-type(9){ animation-delay: 234ms; }
  .pop:nth-of-type(10){ animation-delay: 260ms; }
  .pop:nth-of-type(11){ animation-delay: 286ms; }
  .pop:nth-of-type(12){ animation-delay: 312ms; }
  .pop:nth-of-type(13){ animation-delay: 338ms; }
  .pop:nth-of-type(14){ animation-delay: 364ms; }
  .pop:nth-of-type(15){ animation-delay: 390ms; }
  .pop:nth-of-type(16){ animation-delay: 416ms; }
  .pop:nth-of-type(17){ animation-delay: 442ms; }
  .pop:nth-of-type(18){ animation-delay: 468ms; }
  .pop:nth-of-type(19){ animation-delay: 494ms; }
  .pop:nth-of-type(20){ animation-delay: 520ms; }
  .pop:nth-of-type(21){ animation-delay: 546ms; }
  .pop:nth-of-type(22){ animation-delay: 572ms; }
  .pop:nth-of-type(23){ animation-delay: 598ms; }
  .pop:nth-of-type(24){ animation-delay: 624ms; }
  .pop:nth-of-type(25){ animation-delay: 650ms; }
  .pop:nth-of-type(26){ animation-delay: 676ms; }
  .pop:nth-of-type(27){ animation-delay: 700ms; }
  .pop:nth-of-type(28){ animation-delay: 700ms; }
  .pop:nth-of-type(29){ animation-delay: 700ms; }
  .pop:nth-of-type(30){ animation-delay: 700ms; }

  /* Scroll-driven where the browser supports it, so a diagram draws as it
     arrives rather than all at once on load. Falls back to the time-based
     animation above everywhere else. */
  @supports (animation-timeline: view()) {
    .draw, .pop, .wipe {
      animation-timeline: view();
      animation-range: entry 15% cover 45%;
    }
  }
}
/* ===================================================================
   EDITORIAL LAYOUT

   Text leads on every entry and the figure follows on the right. An
   earlier version alternated the sides, which meant every second entry
   showed the reader a picture before telling them what it was of. Symmetry
   is not worth that.

   Both columns start at the top, so a title and its figure begin on the
   same line rather than drifting apart. The figure is capped: a three line
   paragraph beside a 460px drawing opened a canyon that centring only
   moved around.
   =================================================================== */
@media (min-width:900px){
  .grid, .grid--3, .grid--2{ display:block; }

  /* Two columns, so two columns in the markup and a flex row here. The
     previous version was a grid in which the figure spanned
     grid-row 1 / span 99 and every text child sat one per row beside it,
     which is where the layout faults came from: the rows stretched to the
     figure's height, the figure stretched out of its own aspect ratio, and
     the whole thing was held together by align-content, min-content rows and
     an align-self patch. Nothing spans anything now. */
  .card, .proj{
    display:flex;
    flex-direction:row;
    align-items:flex-start;
    gap:56px;
    padding:52px 48px;
    margin:0;
    border:0;
    border-radius:0;
    box-shadow:none;
    border-top:1px solid var(--hair);
    transform:none;
  }
  .card:first-of-type, .proj:first-of-type{ border-top:0; }
  .card:hover, .proj:hover{ transform:none; box-shadow:none; border-color:var(--hair); }

  .card__text, .proj__text{
    flex:1 1 0;
    min-width:320px;
    display:flex;
    flex-direction:column;
    align-items:flex-start;
  }
  .card__figure, .proj__figure{
    flex:1 1 0;
    min-width:0;
    align-self:flex-start;
  }
  .card__art, .proj__art{ margin-top:0; }
}


@media (max-width:899px){
  .card, .proj{
    display:block;
    padding:28px 20px;
    border-radius:var(--r);
  }
  .card__art svg, .proj__art svg{ width:100%; height:auto; max-height:260px; }
}

/* ---------- Organisation marks are labels, not billboards -------------
   The marks had no size ceiling, so on a narrow screen they rendered at
   their natural 200px and one logo filled its entire cell. A mark that
   size stops being attribution and starts looking like a fan poster,
   which is the opposite of what it is there to do: it is a label saying
   which repository took the change, and the number beside it is the
   thing worth reading.

   Capped hard, at every breakpoint, with the intrinsic size of the file
   no longer able to decide the layout.
   -------------------------------------------------------------------- */
.proofcell__org,
.proofcell img,
.proofcell__art img{
  width:auto;
  height:56px;
  max-height:56px;
  max-width:150px;
  object-fit:contain;
  object-position:left center;
  display:block;
}
.card__mark{
  width:22px; height:22px;
  max-width:22px; max-height:22px;
  object-fit:contain;
  flex:0 0 auto;
}
@media (max-width:899px){
  .proofcell__org,
  .proofcell img,
  .proofcell__art img{ height:40px; max-height:40px; max-width:112px; }
}

/* ---------- Evidence: effort made visible ----------------------------
   The page described what each change did and never what it took to be
   sure of it, so a five line fix and a change verified across 10,000
   generated hypergraphs read as equal work. Each entry now carries the
   checks that stand behind it, and the strips are deliberately uneven:
   the length of the list is the size of the job.

   Collapsed by default, because eleven expanded lists would bury the
   figures. The summary states the count, so the weight is legible even
   closed, and it is a real <details> so it works with no JavaScript.
   -------------------------------------------------------------------- */
.ev{ margin:14px 0 0; }
.ev__sum{
  font-family:var(--font-mono);
  font-size:var(--fs-0);
  letter-spacing:.02em;
  color:var(--green-700);
  cursor:pointer;
  list-style:none;
  padding:6px 12px;
  border:1px solid var(--green-100);
  background:var(--green-100);
  border-radius:999px;
  display:inline-block;
}
.ev__sum::-webkit-details-marker{ display:none; }
.ev__sum:hover{ border-color:var(--green-500); }
.ev[open] .ev__sum{ border-color:var(--green-500); }
.ev__list{
  margin:12px 0 0;
  padding:0 0 0 18px;
  list-style:none;
}
.ev__list li{
  position:relative;
  font-family:var(--font-mono);
  font-size:var(--fs-0);
  line-height:1.65;
  color:var(--ink);
}
.ev__list li::before{
  content:"";
  position:absolute; left:-18px; top:.62em;
  width:7px; height:7px; border-radius:50%;
  background:var(--green-500);
}

/* ---------- The proof wall: marks the size they were, cells wide enough
   for their own labels ------------------------------------------------
   Four columns left each cell too narrow for its caption, so words broke
   across lines mid-word. Three columns give each cell roughly a third
   more width, and breaking is turned off outright so no figure or label
   can ever be split again.
   -------------------------------------------------------------------- */
@media (min-width:900px){
  .proofgrid{ grid-template-columns:repeat(3, 1fr); gap:14px; }
  .proofcell{ padding:16px 16px 18px; }
}
.proofcell__num,
.proofcell__org,
.proofcell{
  hyphens:none;
  overflow-wrap:normal;
  word-break:normal;
}
.proofcell__num{ font-size:var(--fs-1); line-height:1.3; }

/* ---------- Figures stop looking like terminal output ------------------
   Every label in every figure was monospace, because the site's captions
   are and the figures followed. Mono is a signal, and the signal it sends
   is console. Twenty two figures set entirely in it read as log output
   with a picture in them, whatever they depict, and that is the single
   largest reason they kept being called ugly while getting steadily more
   correct.

   Figure text now uses the text face. Mono is kept only where it carries
   meaning and would be wrong in anything else: identifiers, hashes, code,
   and the numerals in a measured figure, which want tabular alignment.
   Those opt in with class="id".
   -------------------------------------------------------------------- */
.card__art svg text,
.proj__art svg text,
.hero__art svg text{
  font-family: var(--font-sans, "Instrument Sans", system-ui, sans-serif);
  letter-spacing: -0.005em;
}
.card__art svg text.id,
.proj__art svg text.id,
.card__art svg text.mono,
.proj__art svg text.mono{
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

/* The caption under a figure is prose, not a readout. */
.fig__cap{
  font-family: var(--font-sans, "Instrument Sans", system-ui, sans-serif);
  text-transform: none;
  letter-spacing: 0;
  font-size: var(--fs-1);
  color: var(--muted);
  max-width: 46ch;
}

/* ---------- Words do not move ------------------------------------------
   .pop carries a translateY, and text elements had been given that class, so
   every label in every figure was sliding under the reader. The class is off
   them now and this makes it permanent: inside a figure, anything that is a
   word is fixed in place, whatever class a later redraw hands it. Shapes are
   what move. A label that drifts is unreadable and reads as a fault, which is
   exactly how it read. */
.card__art svg text, .proj__art svg text, .hero__art svg text,
.card__art svg tspan, .proj__art svg tspan, .hero__art svg tspan{
  transform: none !important;
  translate: none !important;
  rotate: none !important;
  scale: none !important;
}

/* ---------- Canvas figures ----------------------------------------------
   Three figures were too dense for the DOM: 1,316 elements in the island
   discovery field, 484 in the GPU field, and a caustic that wanted hundreds
   of rays and was given forty four. They are now a canvas with an SVG laid
   over it in the SAME coordinate space, so a label at x=235 in the overlay
   lands exactly where the canvas puts x=235 and the two cannot drift apart
   when the card resizes.

   The canvas draws no words. Words stay in the overlay, where they are crisp
   at any pixel ratio, readable by a screen reader, and incapable of moving. */
.figc{
  position: relative;
  width: 100%;
  aspect-ratio: var(--figc-ar, 470/540);
}
.figc > canvas,
.figc > .figc__lab{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.figc > .figc__lab{ pointer-events: none; max-height: none; }

/* ---------- The figure keeps its own shape --------------------------------
   .card__art is a grid item spanning every row, so the grid stretched it to
   whatever height the text column happened to be. That made the figure box a
   different shape from the viewBox, which left the drawing and its labels
   depending on both layers letterboxing identically to stay lined up. They do
   now, but the whole failure mode is avoidable: the item sits at the top at
   its natural height instead of being stretched, so the box is exactly the
   viewBox shape, the letterbox is a no-op, and the two layers cannot drift
   apart however either of them is later changed. */

/* ---------- Narrow layout, stated once and last ---------------------------
   Measured at a 390px viewport: the card grid was still resolving to two
   unequal columns of 225px and 196px even though the max-width:760px rule
   asking for one column was matching, and .card was computing to display
   block while its own flex-direction was column. Two rules from earlier in
   the cascade were winning, which is how a figure ended up 154px wide beside
   another at 183px on a phone.

   Below the point where the two column card layout takes over, the entries
   are a single stack and each entry is a flex column: text, then figure,
   both full width. Stated last so nothing earlier can win. */
@media (max-width:899px){
  .grid, .grid--3, .grid--2{ display:block; }
  .grid > * + *{ margin-top:20px; }

  .card, .proj{
    display:flex;
    flex-direction:column;
    align-items:stretch;
  }
  .card__text, .proj__text,
  .card__figure, .proj__figure{
    width:100%;
    min-width:0;
    display:flex;
    flex-direction:column;
    align-items:flex-start;
  }
  .card__figure, .proj__figure{ margin-top:20px; }
  .card__art, .proj__art{ width:100%; margin-top:0; }
}

/* ---------- Figures arrive rather than being there already ---------------
   Uncovered top to bottom as each one reaches the reading position. Nothing
   is moved or scaled, because a transform would carry the labels with it and
   words on this site do not move: clip-path only decides how much is shown.
   The hidden state exists only on a page fig.js has marked, so a failed
   script leaves every figure visible instead of blank. The small negative
   inset keeps the glass figure's soft shadow from being cut off. */
.js-reveal .card__figure,
.js-reveal .proj__figure{
  clip-path: inset(-24px -24px 100% -24px);
  transition: clip-path 1100ms cubic-bezier(.22,.9,.24,1);
}
.js-reveal .card__figure.is-in,
.js-reveal .proj__figure.is-in{
  clip-path: inset(-24px -24px -24px -24px);
}
@media (prefers-reduced-motion: reduce){
  .js-reveal .card__figure, .js-reveal .proj__figure{ clip-path: none; transition: none; }
}
/* The backstop. Whatever the transition did or did not do, a revealed figure
   ends with no clip at all. */
.js-reveal .card__figure.is-done,
.js-reveal .proj__figure.is-done{
  clip-path: none;
  transition: none;
}

/* ===================================================================
   apple.py — work.html as product tiles
   Appended once by apple.py. Everything here is keyed to classes that
   only work.html carries after the transform (.psub, .pshow, .pt, .pgal,
   .hero--product), so index.html is untouched by it.

   The page is built the way apple.com builds a product row: every project
   is a large rounded tile, name huge, a one line tagline, a blue pill and
   a text link, and the figure standing in the middle as the product shot.
   The four biggest projects get a full width tile each; the other seven
   share a snap gallery with the next tile peeking at the edge.

   Rules held here, because each one has been broken before:
     - Colour is the site tokens and nothing else. Tiles alternate
       --ground2 and --raised. The only literal is white pill text.
     - Words do not move. No text here is transformed or animated, the
       work.html hero keeps its words still, and the tile never lifts on
       hover. The only animated thing is the progress fill inside the
       active gallery dot, which is a shape.
     - Nothing inside .proj__art is sized or clipped on its own. The tile
       sets the width of .proj__figure and the figure keeps its own
       aspect ratio below that, so canvas and label layer stay locked.
   =================================================================== */

/* Sticky needs a page that is not itself a scroll container. overflow-x:
   hidden on html and body turns body into one, which is why the site nav
   never actually stuck. clip clips the same overflow without that. */
html:has(.psub), html:has(.psub) body{ overflow-x:clip; }

/* ---------- Hero: centred, product-page scale ---------- */
.hero--product{ padding:clamp(56px, 9vw, 120px) 0 clamp(40px, 6vw, 72px); text-align:center; }
.hero--product .hero__h1{
  font-size:clamp(40px, 6.6vw, 84px); line-height:1.04; letter-spacing:-0.035em;
  max-width:13em; margin:0 auto; text-wrap:balance;
  animation:none; /* motion.css raises this line on load; here words stay put */
}
.hero--product .hero__sub{
  margin:24px auto 0; max-width:36em;
  font-size:clamp(18px, 1.9vw, 23px); line-height:1.5; text-wrap:pretty;
  animation:none;
}

/* ---------- Product strip: sticky under the site nav ---------- */
.psub{
  position:sticky; top:56px; z-index:49;
  background-color:var(--ground);
  border-bottom:1px solid var(--hair);
}
@supports (background: color-mix(in srgb, red 50%, transparent)){
  .psub{
    background:color-mix(in srgb, var(--ground) 78%, transparent);
    -webkit-backdrop-filter:saturate(1.8) blur(20px);
    backdrop-filter:saturate(1.8) blur(20px);
  }
}
.psub__scroll{ overflow-x:auto; scrollbar-width:none; overscroll-behavior-x:contain; }
.psub__scroll::-webkit-scrollbar{ display:none; }
.psub__list{
  display:flex; align-items:center;
  width:max-content; margin:0 auto; padding:0 12px;
}
.psub__link{
  display:block; padding:14px 12px;
  font-size:14px; font-weight:500; line-height:20px; white-space:nowrap;
  color:var(--ink);
}
.psub__link:hover{ color:var(--blue-700); }
.psub__sep{ width:1px; height:18px; margin:0 10px; background:var(--hair2); }

/* ---------- The showroom ---------- */
.pshow{ padding:0 0 clamp(64px, 9vw, 120px); background:var(--ground); }
.pshow__heroes{
  max-width:1280px; margin:0 auto; padding:0 clamp(16px, 2.5vw, 32px);
  display:grid; gap:24px;
}

/* ---------- A tile ----------------------------------------------------
   .proj.pt outranks every earlier .proj rule (the editorial two column
   row at 900px and up, the stacked card below it, the hover lift). */
.proj.pt{
  display:flex; flex-direction:column; align-items:stretch; gap:0;
  min-width:0; margin:0;
  padding:clamp(28px, 4.6vw, 60px) clamp(16px, 4.6vw, 64px) clamp(28px, 4.2vw, 52px);
  border:1px solid transparent; border-radius:30px;
  background:var(--ground2);
  box-shadow:none; transform:none; transition:none;
  scroll-margin-top:124px;
}
.proj.pt:hover{ transform:none; box-shadow:none; }
.pshow__heroes > .pt:nth-child(even),
.pgal__track > .pt:nth-child(even){
  background:var(--raised); border-color:var(--hair); box-shadow:var(--e1);
}
.pshow__heroes > .pt:nth-child(even):hover,
.pgal__track > .pt:nth-child(even):hover{ box-shadow:var(--e1); }

/* head: name and tagline left, the two calls to action right; they wrap
   under the tagline as soon as the row runs out of room. */
.pt__head{
  display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:space-between;
  gap:20px 40px;
}
.pt__titles{ flex:1 1 440px; min-width:0; }
.pt .proj__name{
  font-size:clamp(44px, 6vw, 80px); line-height:1.02; letter-spacing:-0.035em;
  font-weight:700; color:var(--ink);
}
/* a name too long for the line at full size is set smaller, not broken */
.pt--long .proj__name{ font-size:clamp(34px, 4.6vw, 62px); }
.pt__tagline{
  margin-top:10px;
  font-size:clamp(21px, 2.3vw, 28px); line-height:1.25; letter-spacing:-0.01em;
  color:var(--ink);
}
.pt__ctas{
  display:flex; flex-wrap:wrap; align-items:center; gap:12px 24px;
  padding-top:clamp(0px, 1.2vw, 14px);
}
.pt__cta{
  display:inline-flex; align-items:center; min-height:44px;
  padding:10px 22px; border-radius:999px;
  background:var(--blue-500); color:#fff;
  font-size:17px; font-weight:500; line-height:1.2; white-space:nowrap;
  transition:background-color 160ms var(--hover-ease);
}
.pt__cta:hover{ background:var(--blue-700); }
.pt__more{
  display:inline-flex; align-items:center; gap:4px; min-height:44px;
  font-size:17px; font-weight:500; line-height:1.2; white-space:nowrap;
  color:var(--blue-700);
}
.pt__more:hover{ text-decoration:underline; }
.pt__chev{ font-size:1.2em; line-height:1; }

/* the product shot: width only, so the figure keeps its own shape */
.pt .proj__figure{
  display:block; width:min(100%, 560px); min-width:0;
  margin:clamp(28px, 4vw, 52px) auto 0;
}
.pt .proj__art{ width:100%; margin:0; }
/* An inline SVG figure was capped at 260px tall below 900px, which shrank
   the whole drawing inside a tile built to show it big. Canvas figures
   already had this cap lifted on their label layer. */
.pt .fig > svg{ max-height:none; }

/* body: the claim, the spec strip, the tags */
.pt__body{
  margin-top:clamp(28px, 4vw, 48px);
  padding-top:clamp(24px, 3vw, 36px);
  border-top:1px solid var(--hair);
  display:grid; gap:24px 56px; align-content:start;
}
.pt .proj__claim{
  margin:0; max-width:52ch;
  font-size:clamp(18px, 1.6vw, 21px); line-height:1.55; color:var(--muted);
}
.pt .proj__tags{ margin:0; }
.pt .proj__nums{
  margin:0; align-self:start; align-content:start;
  display:grid; grid-template-columns:repeat(auto-fit, minmax(128px, 1fr)); gap:22px 20px;
  font-family:var(--font-sans); font-size:15px; color:var(--muted);
}
.pt .spec{
  display:flex; flex-direction:column; gap:6px; min-width:0;
  padding-left:14px; border-left:1px solid var(--hair2);
}
.pt .spec__n{
  font-size:clamp(34px, 3.4vw, 46px); line-height:1; font-weight:700;
  letter-spacing:-0.03em; font-variant-numeric:tabular-nums; color:var(--ink);
}
/* four specs read as a 2 x 2 block rather than three and an orphan */
.pt .proj__nums:has(> .spec:nth-child(4)){ grid-template-columns:repeat(2, minmax(0, 1fr)); }
.pt .spec__l{ font-size:15px; line-height:1.35; color:var(--muted); }
.pt .spec--t{ font-size:clamp(18px, 1.7vw, 22px); font-weight:600; line-height:1.25; letter-spacing:-0.01em; color:var(--ink); overflow-wrap:anywhere; }
@media (min-width:900px){
  .pt__body{ grid-template-columns:minmax(0, 1.15fr) minmax(0, 1fr); }
  .pt .proj__claim{ grid-column:1; grid-row:1; }
  .pt .proj__tags{ grid-column:1; grid-row:2; }
  .pt .proj__nums{ grid-column:2; grid-row:1 / span 2; }
}

/* ---------- Gallery: the seven smaller projects -----------------------
   A scroll-snap row, full bleed. Each tile is 88% of the row, centred,
   so the neighbour shows at the edge. cqw is the row's own width, which
   keeps the padding and the tile width in one unit. */
.pgal{ container-type:inline-size; margin-top:clamp(56px, 8vw, 104px); }
.pgal__track{
  --pgal-w:min(88cqw, 1120px);
  position:relative;
  display:flex; gap:clamp(12px, 1.6cqw, 22px);
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  scrollbar-width:none;
  padding:6px calc((100cqw - var(--pgal-w)) / 2) 10px;
}
.pgal__track::-webkit-scrollbar{ display:none; }
.pgal__track > .pt{
  flex:0 0 var(--pgal-w);
  scroll-snap-align:center; scroll-snap-stop:always;
}
.pgal .pt .proj__name{ font-size:clamp(40px, 4.8vw, 64px); }
.pgal .pt__tagline{ font-size:clamp(20px, 2vw, 26px); }
/* Slides share the tallest one's height; the spare room goes around the
   figure, so every slide ends on its spec strip at the same line. */
.pgal .pt__head{ margin-bottom:clamp(28px, 4vw, 52px); }
.pgal .pt .proj__figure{ width:min(100%, 460px); margin:auto; }

/* A slide whose figure is outside the visible row is parked: its art is
   taken to zero size so fig.js stops painting it and restarts its clock,
   and the figure builds in front of the reader when the slide arrives
   instead of having played out of sight. Only ever set on art that is off
   screen, never transitioned, and never on the slide in view. */
.pgal .pt.is-parked .proj__art{ transform:scale(0); }

.pgal__ctrl{
  display:flex; justify-content:center; align-items:center; gap:12px;
  margin-top:18px; padding:0 16px;
}
.pgal__dots{
  display:flex; align-items:center;
  height:52px; padding:0 14px; border-radius:999px;
  background:var(--hair);
}
.pgal__dot{
  position:relative; flex:none;
  display:flex; align-items:center; justify-content:center;
  width:24px; height:44px;
  transition:width 300ms var(--hover-ease);
}
.pgal__dot::before{
  content:""; width:8px; height:8px; border-radius:999px;
  background:var(--muted); opacity:.42;
  transition:width 300ms var(--hover-ease), opacity 300ms var(--hover-ease);
}
.pgal__dot:hover::before{ opacity:.7; }
.pgal__dot[aria-current="true"]{ width:60px; }
.pgal__dot[aria-current="true"]::before{ width:48px; opacity:.28; }
.pgal__fill{
  display:none; position:absolute; left:6px; top:50%;
  width:48px; height:8px; margin-top:-4px; border-radius:999px;
  background:var(--muted);
}
.pgal__dot[aria-current="true"] .pgal__fill{ display:block; }
@keyframes pgal-fill{ from{ width:8px; } to{ width:48px; } }
.pgal.is-auto .pgal__dot[aria-current="true"] .pgal__fill{
  animation:pgal-fill 7s linear both;
}
.pgal.is-auto.is-held .pgal__dot[aria-current="true"] .pgal__fill{ animation-play-state:paused; }

.pgal__btn{
  flex:none; display:flex; align-items:center; justify-content:center;
  width:52px; height:52px; border-radius:50%;
  background:var(--hair); color:var(--muted); cursor:pointer;
  transition:background-color 160ms var(--hover-ease);
}
.pgal__btn:hover{ background:var(--hair2); }
.pgal__btn:disabled{ opacity:.4; cursor:default; }
.pgal__btn:disabled:hover{ background:var(--hair); }
.pgal__btn[hidden]{ display:none; }
.pgal__btn svg{ width:20px; height:20px; }
.pgal__play .pgal__i-play{ display:none; }
.pgal__play.is-paused .pgal__i-play{ display:block; }
.pgal__play.is-paused .pgal__i-pause{ display:none; }

@media (prefers-reduced-motion: reduce){
  .pgal__fill{ animation:none !important; }
  .pgal__dot, .pgal__dot::before, .pt__cta, .pgal__btn{ transition:none; }
}

/* ---------- Narrow screens ---------- */
@media (max-width:640px){
  .pgal__prev, .pgal__next{ display:none; }
  .pshow__heroes{ gap:16px; }
  .proj.pt{ border-radius:26px; }
  .pt__ctas{ gap:8px 20px; }
}
/* phones: every pixel of width goes to the figure, whose labels scale with it */
@media (max-width:480px){
  .proj.pt{ padding-left:14px; padding-right:14px; }
  .pgal__track{ --pgal-w:90cqw; gap:8px; }
  .pt__ctas{ gap:8px 16px; }
  .pt__cta{ padding:10px 18px; }
  .pt__cta, .pt__more{ font-size:16px; }
}
/* A gallery slide stacked name, figure and claim, which made it 1,100px
   tall: taller than a laptop or an iPad in landscape, so the dots that
   drive it sat off screen. From 900px the figure stands left as the product
   shot and everything written about it stands right, centred against it,
   and the figure is capped by the viewport height so the slide and its
   dots fit on the screen it is read on. */
@media (min-width:900px){
  .pgal .proj.pt{
    display:grid; align-items:start;
    grid-template-columns:auto minmax(0, 1fr);
    grid-template-rows:1fr auto auto 1fr;
    column-gap:clamp(32px, 4cqw, 64px);
  }
  .pgal .pt .proj__figure{
    grid-column:1; grid-row:1 / 5; align-self:center; margin:0;
    width:min(440px, 42cqw, calc((100svh - 260px) * 0.85));
  }
  .pgal .pt__head{ grid-column:2; grid-row:2; margin-bottom:28px; }
  .pgal .pt__titles{ flex-basis:100%; }
  .pgal .pt__body{
    grid-column:2; grid-row:3;
    margin-top:0; padding-top:24px;
    grid-template-columns:minmax(0, 1fr);
  }
  .pgal .pt .proj__claim,
  .pgal .pt .proj__tags,
  .pgal .pt .proj__nums{ grid-column:auto; grid-row:auto; }
}
/* ---------- Smoother edges ------------------------------------------------
   Seen on an iPad beside apple.com, the page read too sharp. Hard one pixel
   rules were drawing every edge: under the site nav, under the product strip,
   round every white tile, and across each tile above its claim. Apple draws
   none of those. The two bars now read as one frosted layer that fades into
   the page through a soft shadow, the tiles lose their outlines for larger
   corners and a wide faint shadow, and the divider inside a tile fades out
   at both ends instead of stopping square. */
html:has(.psub) .nav{ border-bottom-color:transparent; }
.psub{
  border-bottom-color:transparent;
  box-shadow:0 18px 30px -26px color-mix(in srgb, var(--ink) 30%, transparent);
}
.proj.pt{ border-radius:38px; border-color:transparent; }
.pshow__heroes > .pt:nth-child(even),
.pgal__track > .pt:nth-child(even),
.pshow__heroes > .pt:nth-child(even):hover,
.pgal__track > .pt:nth-child(even):hover{
  border-color:transparent;
  box-shadow:0 1px 3px color-mix(in srgb, var(--ink) 5%, transparent),
             0 30px 70px -40px color-mix(in srgb, var(--ink) 22%, transparent);
}
.pt__body{
  border-top-color:transparent;
  background:linear-gradient(90deg, transparent, var(--hair2) 18%, var(--hair2) 82%, transparent) top / 100% 1px no-repeat;
}
.pt .spec{
  border-left:0;
  background:linear-gradient(180deg, transparent, var(--hair2) 25%, var(--hair2) 75%, transparent) left / 1px 100% no-repeat;
}
.pgal__dots, .pgal__btn{ background:color-mix(in srgb, var(--hair) 80%, transparent); }
@media (max-width:640px){ .proj.pt{ border-radius:30px; } }
@media (min-width:900px){ .pgal .pt__body{ background:none; } }
/* a long name in the gallery's narrower text column is set smaller, and
   may break at its own hyphens rather than run past the tile */
.pgal .pt--long .proj__name{ font-size:clamp(30px, 3.6vw, 46px); }
/* ===================== end apple.py ===================== */

/* ---------- Promo tiles and footer -----------------------------------------
   The page used to end on a lone sentence pointing at the 3D portfolio (its
   link text even named the wrong address) and a footer repeating the three
   buttons just above it plus a line nobody needed. Now two matching tiles
   sit side by side, the personal work and the 3D portfolio, and the footer
   is one quiet line. */
.promo{ display:grid; grid-template-columns:repeat(auto-fit, minmax(min(100%, 320px), 1fr)); gap:20px; }
.promo__tile{ display:flex; flex-direction:column; align-items:stretch; gap:12px; margin:0; text-align:left; min-width:0; }
.promo__tile .card__link, .promo__cube{ align-self:flex-start; }
.promo__tile h2{ margin:0; }
.promo__tile p{ margin:0; color:var(--muted); }
.promo__tile .card__link{ margin-top:auto; }
.promo__tile--3d{ color:inherit; text-decoration:none; }
.promo__tile--3d:hover .card__link{ text-decoration:underline; }
.promo__cube{ width:72px; height:72px; }
.foot{ padding:28px 0 36px; background:var(--ground); }
.foot__row{ display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:12px 24px; }
.foot__name{ font-weight:600; color:var(--ink); }
.foot__links{ display:flex; gap:20px; }
.foot__links a{ color:var(--blue-700); font-weight:500; }
.foot__links a:hover{ text-decoration:underline; }

