/* jeremy-osborne.com — one stylesheet. Split into tokens/base/components if it grows. */

:root{
  --bg:#03060c;
  --bg-2:#070d17;
  --edge:#132132;
  --edge-2:#1c2c40;
  --fg:#e6f0f7;
  --fg-dim:#7f94a8;

  --c0:#1b2c8a;
  --c1:#0d9bd6;
  --c2:#22c98a;
  --c3:#e8d24a;
  --c4:#f2622e;

  --pad:clamp(20px,5vw,84px);
  --wrapw:1600px;               /* .wrap max-width */
  --herow:720px;                /* .hero column max-width on desktop */
  /* Distance from the hero's left edge back to the viewport's left edge —
     what full-bleed children inside the hero must undo. */
  --bleed:calc(max((100vw - var(--wrapw))/2, 0px) + var(--pad));
}

*{box-sizing:border-box;margin:0;padding:0}
/* A class that sets `display` beats the UA stylesheet's [hidden]{display:none},
   which silently made the draw panel permanently visible. */
[hidden]{display:none!important}
html{scroll-behavior:smooth}
body{
  background:var(--bg);color:var(--fg);
  font-family:Inter,system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing:antialiased;
  /* clip, not hidden: hidden makes body a scroll container, which breaks the
     sticky top bar. clip contains the full-bleed sheets without doing that. */
  overflow-x:clip;
}
.mono{font-family:'JetBrains Mono',ui-monospace,SFMono-Regular,monospace}
::selection{background:var(--c2);color:#04120c}
:focus-visible{outline:2px solid var(--c2);outline-offset:3px}

.skip{position:absolute;left:-9999px}
.skip:focus{left:var(--pad);top:12px;z-index:99;background:var(--c2);color:#04120c;
  padding:10px 16px;border-radius:2px;font-size:13px}

/* ---------- flow canvas ----------
   A window bounded by the hero, full-bleed across the viewport width. It scrolls
   away with the hero rather than staying pinned behind the rest of the document,
   so the tunnel belongs to the top of the page instead of following you down it. */
/* left:calc(50% - 50vw) was wrong here: it centres the 100vw window on the
   HERO's centre, and the hero is a left-hand column, so the whole simulation
   sat shifted left with a dead band down the right of wide screens. --bleed
   walks back to the actual viewport edge instead. */
.stage{position:absolute;top:0;bottom:0;left:calc(0px - var(--bleed));width:100vw;
  z-index:-1;isolation:isolate;overflow:hidden}
#flow{position:absolute;inset:0;width:100%;height:100%;z-index:0;display:block}
.veil{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(100deg,var(--bg) 4%,rgba(3,6,12,.72) 34%,rgba(3,6,12,0) 62%),
    radial-gradient(ellipse 90% 80% at 50% 45%,transparent 40%,var(--bg) 100%)}

.wrap{position:relative;z-index:3;padding:0 var(--pad);max-width:1600px;margin-inline:auto}

/* Opaque full-bleed backing, so content below the hero covers the canvas rather
   than letting it show through the gaps between cards. */
.sheet{position:relative;isolation:isolate}
.sheet::before{content:'';position:absolute;top:0;bottom:0;
  left:calc(50% - 50vw);right:calc(50% - 50vw);background:var(--bg);z-index:-1}

/* ---------- top bar ---------- */
/* A visibly lighter band than the page, so the bar reads as its own surface
   instead of black on black. */
.topbar{position:sticky;top:0;z-index:20;
  background:linear-gradient(180deg,rgba(216,228,240,.13),rgba(216,228,240,.07)),rgba(9,15,25,.9);
  backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
  border-bottom:1px solid rgba(216,228,240,.18);
  transition:background .25s linear,border-color .25s linear,backdrop-filter .25s linear}
/* Sits clear over the page at the top and pops in once content scrolls under it.
   Gated on .js so the no-script fallback keeps the solid band. */
.js .topbar:not(.scrolled){background:transparent;border-bottom-color:transparent;
  backdrop-filter:none;-webkit-backdrop-filter:none}
.hero{margin-top:-59px;padding-top:calc(clamp(30px,6vh,80px) + 59px)}
.topbar-inner{max-width:1600px;margin-inline:auto;padding:0 var(--pad);
  display:flex;justify-content:space-between;align-items:center;gap:16px;
  min-height:58px;font-size:11px;letter-spacing:.16em;text-transform:uppercase}
.topbar .id{color:var(--fg);font-weight:500;text-decoration:none}
.topbar .id em{color:var(--c2);font-style:normal}
.topbar ul{display:flex;gap:clamp(14px,2.4vw,30px);list-style:none}
.topbar a{color:var(--fg-dim);text-decoration:none;transition:color .2s}
.topbar a:hover,.topbar a:focus-visible{color:var(--c2)}
.topbar a[aria-current=page]{color:var(--fg)}

.burger{display:none;background:none;border:1px solid var(--edge-2);border-radius:2px;
  width:40px;height:34px;padding:0;cursor:pointer}
.burger span{display:block;width:16px;height:1.5px;background:var(--fg);margin:4px auto;
  transition:transform .22s,opacity .22s}
.topbar.open .burger span:nth-child(1){transform:translateY(5.5px) rotate(45deg)}
.topbar.open .burger span:nth-child(2){opacity:0}
.topbar.open .burger span:nth-child(3){transform:translateY(-5.5px) rotate(-45deg)}

@media (max-width:760px){
  .burger{display:block}
  .topbar nav{position:absolute;top:100%;left:0;right:0;display:none;
    background:rgba(3,6,12,.97);border-bottom:1px solid var(--edge)}
  .topbar.open nav{display:block}
  .topbar nav ul{flex-direction:column;gap:0}
  .topbar nav li+li{border-top:1px solid var(--edge)}
  .topbar nav a{display:block;padding:15px var(--pad)}
}

/* ---------- hero ---------- */
.hero{position:relative;min-height:clamp(500px,80vh,860px);display:flex;flex-direction:column;
  justify-content:center;padding:clamp(30px,6vh,80px) 0;max-width:min(100%,var(--herow))}
.eyebrow{font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:var(--c1);
  margin-bottom:20px;opacity:0}
h1{font-size:clamp(48px,9.5vw,138px);line-height:.87;font-weight:800;letter-spacing:-.045em}
h1 .w{display:inline-block;opacity:0}
h1 .grad{background:linear-gradient(96deg,var(--c1),var(--c2) 38%,var(--c3) 70%,var(--c4));
  -webkit-background-clip:text;background-clip:text;color:transparent}
.about{margin-top:26px;font-size:clamp(14px,1.4vw,17.5px);color:var(--fg-dim);
  max-width:54ch;line-height:1.7;opacity:0}
.about b{color:var(--fg);font-weight:500}

.legend{margin-top:36px;max-width:340px;opacity:0}
.legend .cap{font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;
  color:var(--fg-dim);margin-bottom:9px}
.legend .bar{height:8px;border-radius:2px;transform-origin:left center;
  background:linear-gradient(90deg,var(--c0),var(--c1) 28%,var(--c2) 52%,var(--c3) 76%,var(--c4))}
.legend .ticks{display:flex;justify-content:space-between;margin-top:8px;position:relative;
  font-size:9.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--fg-dim)}
/* Pressure: Cp -2..+1, so free stream (Cp 0, transparent as on the canvas) sits 2/3 along. */
.legend.pressure .bar{background:linear-gradient(90deg,var(--c1),var(--c0) 45%,transparent 66.667%,var(--c4) 72%,var(--c3))}
.legend .fs{position:absolute;left:66.667%;transform:translateX(-50%);white-space:nowrap}
.legend-note{margin-top:11px;font-size:10.5px;letter-spacing:.08em;color:var(--fg-dim)}

.cta{display:flex;gap:12px;margin-top:34px;flex-wrap:wrap;opacity:0}
.btn{display:inline-flex;align-items:center;gap:9px;padding:13px 22px;border-radius:2px;
  border:1px solid var(--edge-2);color:var(--fg);text-decoration:none;font-size:11.5px;
  letter-spacing:.14em;text-transform:uppercase;background:transparent;cursor:pointer;
  font-family:inherit;transition:border-color .22s,color .22s,background .22s}
.btn:hover,.btn:focus-visible{border-color:var(--c2);color:var(--c2)}
.btn.primary{background:var(--c2);border-color:var(--c2);color:#04120c;font-weight:600}
.btn.primary:hover,.btn.primary:focus-visible{filter:brightness(1.14);color:#04120c}

/* ---------- specimen plate (what the flow is running over) ---------- */
/* Absolute inside the hero and offset out to the simulation window's real
   bottom-right corner (the stage is full-bleed, the hero is a left column),
   so the readout is fixed within the window and scrolls away with it. */
.specimen{position:absolute;
  right:calc(100% + var(--bleed) + var(--pad) - 100vw);
  bottom:clamp(14px,2.5vh,30px);z-index:4;
  text-align:right;font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--fg-dim);opacity:0;transition:opacity .45s ease}
/* The plate describes the hero simulation — below the fold it is just clutter
   sitting on top of the footer. */
.specimen.is-hidden{opacity:0!important;pointer-events:none}
.specimen .name{color:var(--fg);display:block;margin-bottom:4px}
.specimen .note{color:var(--c1);display:block;margin-bottom:12px;text-transform:none;
  letter-spacing:.06em;font-size:11px}
.coeff{display:flex;justify-content:flex-end;gap:16px;margin-bottom:10px}
.coeff div{text-align:right}
.coeff dt{font-size:9px;letter-spacing:.16em;color:var(--fg-dim);margin-bottom:3px}
.coeff dd{font-size:15px;color:var(--fg);letter-spacing:.02em;
  font-variant-numeric:tabular-nums}
.traces{display:flex;flex-direction:column;gap:6px;margin:0 0 12px}
.trace-row{display:flex;align-items:center;justify-content:flex-end;gap:9px}
.trace-key{font-size:11px;letter-spacing:.06em;color:var(--fg);width:2.2em;text-align:right}
.trace-row canvas{display:block;width:200px;height:30px;
  border:1px solid var(--edge);background:rgba(3,6,12,.5)}
.specimen button{border:1px solid var(--edge-2);background:rgba(3,6,12,.6);color:var(--fg-dim);
  font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;padding:8px 13px;cursor:pointer;border-radius:2px;
  transition:border-color .2s,color .2s}
.specimen button:hover,.specimen button:focus-visible{border-color:var(--c2);color:var(--c2)}
.specimen-actions{display:flex;gap:8px;justify-content:flex-end}

/* ---------- draw your own ---------- */
/* width/height are load-bearing: a canvas is a replaced element, so inset:0 alone
   leaves it at its intrinsic 0x0 and every drawn point collapses to the origin. */
#draw-overlay{position:fixed;inset:0;width:100%;height:100%;z-index:6;cursor:crosshair;
  background:rgba(3,6,12,.55);touch-action:none}
body.drawing{cursor:crosshair}

/* ---------- exhibits ---------- */
.section-head{display:flex;align-items:baseline;gap:16px;
  margin:clamp(50px,9vh,110px) 0 24px;font-size:11px;letter-spacing:.2em;
  text-transform:uppercase;color:var(--fg-dim)}
.section-head::after{content:'';flex:1;height:1px;background:var(--edge)}

.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,330px),1fr));gap:1px;
  background:var(--edge);border:1px solid var(--edge)}
.card{background:rgba(7,13,23,.86);padding:clamp(22px,3vw,32px);position:relative;
  transition:background .3s}
/* Hidden only where motion.js is loaded to reveal them — on every other page a
   card that starts at zero opacity simply never appears. */
body:not(.page) .card{opacity:0}
.card:hover{background:rgba(10,20,32,.94)}
.card::before{content:'';position:absolute;left:0;top:0;width:100%;height:2px;
  transform:scaleX(0);transform-origin:left;transition:transform .4s cubic-bezier(.2,.7,.3,1)}
.card:hover::before{transform:scaleX(1)}
.card:nth-child(4n+1)::before{background:var(--c1)}
.card:nth-child(4n+2)::before{background:var(--c2)}
.card:nth-child(4n+3)::before{background:var(--c3)}
.card:nth-child(4n+4)::before{background:var(--c4)}
/* A card is the whole click target, so the entire cell is the link. */
a.card{display:block;color:inherit;text-decoration:none}
a.card:focus-visible{outline:2px solid var(--c2);outline-offset:-2px}

/* Low-opacity project image behind the card's text. Scaled to fit inside the
   cell, never stretched or cropped. */
.card{overflow:hidden;isolation:isolate}
.card .card-art{position:absolute;inset:0;z-index:-1;opacity:.16;pointer-events:none;
  background-position:center;background-repeat:no-repeat;background-size:cover}
.card:hover .card-art{opacity:.24}
.card h3{text-shadow:0 1px 10px rgba(3,6,12,.85)}

/* The abstract lives in a hover popup, not on the card face. Devices without
   hover just go through to the page, where the same abstract opens it. */
.card .card-abs{display:none}
@media (hover:hover){
  .card:hover .card-abs,a.card:focus-visible .card-abs{display:block;position:absolute;
    left:-1px;right:-1px;bottom:calc(100% + 6px);z-index:12;margin:0;
    background:rgba(5,10,18,.97);border:1px solid var(--edge-2);border-radius:2px;
    padding:14px 16px;font-size:12.5px;line-height:1.65;color:var(--fg-dim);
    box-shadow:0 14px 34px rgba(0,0,0,.5)}
  /* Top-row cards have no room above; flip the popup below. */
  .grid .card:nth-child(-n+3):hover .card-abs,
  .grid a.card:nth-child(-n+3):focus-visible .card-abs{bottom:auto;top:calc(100% + 6px)}
}

.card-go{display:block;margin-top:16px;font-size:10px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--fg-dim);transition:color .2s}
a.card:hover .card-go,a.card:focus-visible .card-go{color:var(--c2)}
/* Cherry-picked, not ranked — the marker says so without a second heading. */
.card.star h3::after{content:'★';margin-left:9px;font-size:.62em;
  vertical-align:.28em;color:var(--c3)}
.card h3{font-size:clamp(17px,2vw,22px);font-weight:600;letter-spacing:-.02em;line-height:1.25}
/* Museum plaque: what it is, what it was made with, when. */
.card .plaque{margin-top:10px;font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fg-dim)}
.card p{margin-top:14px;font-size:13.5px;color:var(--fg-dim);line-height:1.7}

/* ---------- inner pages ---------- */
/* No flow canvas on reading pages — it competes with the text. */
body.page{background:
  radial-gradient(ellipse 120% 70% at 78% -10%,rgba(13,155,214,.10),transparent 60%),
  radial-gradient(ellipse 90% 60% at 10% 110%,rgba(34,201,138,.07),transparent 60%),
  var(--bg)}
body.page .wrap{max-width:1000px}
.page-title{font-size:clamp(38px,6vw,74px);font-weight:800;letter-spacing:-.04em;
  margin:clamp(24px,5vh,52px) 0 clamp(18px,3vh,30px)}
.lede{font-size:clamp(16px,1.7vw,21px);line-height:1.6;color:var(--fg);
  max-width:60ch;margin-bottom:22px}
main p{font-size:clamp(14px,1.35vw,16.5px);line-height:1.78;color:var(--fg-dim);
  max-width:68ch;margin-bottom:20px}
main p b{color:var(--fg);font-weight:600}
main h2{font-size:clamp(20px,2.6vw,30px);font-weight:700;letter-spacing:-.025em;
  margin:clamp(38px,6vh,64px) 0 16px;color:var(--fg)}
main h3{font-size:clamp(17px,2vw,22px);font-weight:600;letter-spacing:-.02em;
  margin:clamp(26px,4vh,40px) 0 8px;color:var(--fg)}

.spec-sheet{border:1px solid var(--edge);margin:clamp(28px,5vh,52px) 0;max-width:760px}
.spec-sheet div{display:grid;grid-template-columns:minmax(150px,28%) 1fr;gap:16px;
  padding:13px 18px;border-bottom:1px solid var(--edge)}
.spec-sheet div:last-child{border-bottom:0}
.spec-sheet dt{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--fg-dim)}
.spec-sheet dd{font-size:13.5px;color:var(--fg)}

.contact-block{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
  gap:clamp(16px,3vw,36px);margin:clamp(34px,6vh,60px) 0;padding-top:26px;
  border-top:1px solid var(--edge)}
.contact-block dt{font-size:9.5px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--fg-dim);margin-bottom:8px}
.contact-block dd,.contact-block dd a{font-size:14.5px;color:var(--fg);text-decoration:none}
.contact-block dd a{border-bottom:1px solid var(--edge-2);padding-bottom:2px}
.contact-block dd a:hover,.contact-block dd a:focus-visible{color:var(--c2);border-color:var(--c2)}

/* Deliberately visible so a missing image can never read as a finished page. */
.shot{margin:clamp(20px,3.5vh,34px) 0}
.shot-placeholder{display:flex;align-items:center;justify-content:center;text-align:center;
  min-height:clamp(150px,22vh,240px);border:1px dashed var(--edge-2);border-radius:2px;
  padding:22px;font-size:11px;letter-spacing:.1em;color:var(--fg-dim);
  background:rgba(7,13,23,.5)}
.shot img{display:block;width:100%;height:auto;border:1px solid var(--edge-2);border-radius:2px}
/* Captions used to sit at hairline colour and vanished into the background. */
.shot figcaption{margin-top:9px;font-size:11px;letter-spacing:.06em;color:var(--fg-dim)}
.shot figcaption code{color:var(--c1)}

/* ---------- project hero banner ---------- */
/* The project's image at the very top of its page: fills the banner, scaled
   and cropped with the original proportions kept, never stretched. */
.proj-hero{margin:4px 0 clamp(18px,3vh,28px)}
.proj-hero img{display:block;width:100%;height:clamp(210px,36vh,380px);
  object-fit:cover;object-position:center;
  border:1px solid var(--edge);border-radius:2px;background:rgba(3,6,12,.5)}
.proj-hero figcaption{margin-top:8px;font-size:11px;letter-spacing:.06em;color:var(--fg-dim)}

/* ---------- persistent mobile contact bar ---------- */
.mobile-cta{display:none}
@media (max-width:880px){
  .mobile-cta{display:flex;position:fixed;left:0;right:0;bottom:0;z-index:25;
    background:linear-gradient(180deg,rgba(216,228,240,.12),rgba(216,228,240,.06)),rgba(7,12,20,.96);
    border-top:1px solid rgba(216,228,240,.18);
    padding-bottom:env(safe-area-inset-bottom)}
  .mobile-cta a{flex:1;text-align:center;padding:15px 8px;min-height:44px;
    color:var(--fg);text-decoration:none;font-size:11px;letter-spacing:.18em;
    text-transform:uppercase}
  .mobile-cta a+a{border-left:1px solid rgba(216,228,240,.14)}
  .mobile-cta a:active{background:rgba(216,228,240,.08)}
  /* Keep the page's own footer reachable above the fixed bar. */
  body{padding-bottom:calc(52px + env(safe-area-inset-bottom))}
}

/* ---------- image gallery / carousel ---------- */
/* Without JS this is a swipeable scroll row; carousel.js upgrades it to one
   slide at a time with buttons, dots and a timed auto-advance. */
.gallery{display:flex;gap:10px;overflow-x:auto;scroll-snap-type:x mandatory;
  padding-bottom:8px;margin:clamp(20px,3.5vh,34px) 0}
.gallery figure{flex:0 0 min(88%,560px);scroll-snap-align:start;margin:0}
.gallery img{display:block;width:100%;height:auto;border:1px solid var(--edge-2);
  border-radius:2px}
.gallery figcaption{margin-top:8px;font-size:11px;letter-spacing:.06em;color:var(--fg-dim)}

.gallery.carousel{display:block;overflow:visible;position:relative;padding-bottom:0}
.gal-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;
  scrollbar-width:none;border-radius:2px}
.gal-track::-webkit-scrollbar{display:none}
.gallery.carousel .gal-track figure{flex:0 0 100%;scroll-snap-align:start;margin:0;
  min-width:0}
.gallery.carousel img{width:100%;height:clamp(240px,44vh,460px);object-fit:cover;
  object-position:center}
.gallery.carousel figcaption{margin-top:10px;font-size:12px;line-height:1.6;
  letter-spacing:.05em;color:var(--fg-dim);min-height:2.6em}

.gal-btn{position:absolute;top:calc(clamp(240px,44vh,460px)/2 - 22px);z-index:5;
  width:44px;height:44px;border-radius:2px;cursor:pointer;
  border:1px solid rgba(216,228,240,.28);color:var(--fg);
  background:rgba(5,10,18,.72);backdrop-filter:blur(4px);
  font-size:26px;line-height:1;display:flex;align-items:center;justify-content:center;
  transition:background .15s,border-color .15s}
.gal-btn:hover,.gal-btn:focus-visible{background:rgba(216,228,240,.16);
  border-color:var(--c2);color:var(--c2)}
.gal-btn:focus-visible{outline:2px solid var(--c2);outline-offset:2px}
.gal-prev{left:10px}
.gal-next{right:10px}

.gal-dots{display:flex;justify-content:center;gap:8px;margin-top:12px}
.gal-dot{width:8px;height:8px;border-radius:50%;border:1px solid var(--edge-2);
  background:transparent;cursor:pointer;padding:0}
.gal-dot[aria-current="true"]{background:var(--c2);border-color:var(--c2)}
.gal-dot:focus-visible{outline:2px solid var(--c2);outline-offset:2px}

.proj{border-top:1px solid var(--edge);padding-top:clamp(22px,4vh,38px);
  margin-top:clamp(34px,6vh,60px)}
.proj .plaque{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fg-dim);margin:6px 0 16px}

/* ---------- contact form ---------- */
.contact-form{max-width:620px;margin:clamp(26px,5vh,44px) 0}
.field{margin-bottom:22px}
.field label{display:block;font-size:10.5px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--fg-dim);margin-bottom:9px;font-family:'JetBrains Mono',monospace}
.field .opt{color:var(--edge-2);letter-spacing:.1em}
.field input,.field textarea{width:100%;background:rgba(7,13,23,.8);color:var(--fg);
  border:1px solid var(--edge-2);border-radius:2px;padding:13px 15px;font:inherit;
  font-size:15px;line-height:1.55;transition:border-color .2s}
.field textarea{resize:vertical;min-height:150px}
.field input:focus,.field textarea:focus{outline:none;border-color:var(--c2)}
.field input[aria-invalid],.field textarea[aria-invalid]{border-color:var(--c4)}
.err{margin-top:8px;font-size:12.5px;color:var(--c4)}

/* Offscreen rather than display:none — bots skip hidden inputs but fill this. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

.form-actions{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.form-status{font-size:13px;color:var(--fg-dim)}
.form-status.good{color:var(--c2)}
.form-status.bad{color:var(--c4)}
button.btn{cursor:pointer;font-family:'JetBrains Mono',monospace}
button.btn:disabled{opacity:.55;cursor:progress}

.readout{border:1px solid var(--edge);background:rgba(7,13,23,.7);border-radius:2px;
  padding:clamp(14px,2.4vw,22px);font-size:clamp(10px,1.15vw,12.5px);line-height:1.65;
  color:var(--fg-dim);overflow-x:auto;margin:clamp(22px,4vh,38px) 0;max-width:760px}
.plainlist{list-style:none;display:flex;flex-direction:column;gap:9px;margin:6px 0 0}
.plainlist a{color:var(--fg);text-decoration:none;font-size:14.5px;
  border-bottom:1px solid var(--edge-2);padding-bottom:2px}
.plainlist a:hover,.plainlist a:focus-visible{color:var(--c2);border-color:var(--c2)}

@media (max-width:640px){ .spec-sheet div{grid-template-columns:1fr;gap:5px} }

/* ---------- footer ---------- */
footer{margin-top:clamp(50px,8vh,90px);border-top:1px solid var(--edge);
  padding:clamp(26px,4vh,44px) 0 clamp(30px,5vw,54px);
  display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;
  font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--fg-dim)}
footer a{color:var(--fg-dim);text-decoration:none}
footer a:hover,footer a:focus-visible{color:var(--c2)}

/* Below this the hero copy is wide enough that a body beside it would land on the
   text. The body must stay centred in its channel or it makes spurious lift, so the
   domain moves instead of the body. The band's mid-line is where the body sits, so
   it has to clear the last line of hero content, not merely start below the heading. */
@media (max-width:1150px){
  .veil{background:
    linear-gradient(180deg,rgba(3,6,12,.9) 0%,rgba(3,6,12,.7) 40%,rgba(3,6,12,.2) 72%,rgba(3,6,12,.75) 100%)}
}
@media (max-width:880px){
  .hero{max-width:100%}
  /* In flow rather than pinned, so it reads as the panel belonging to the tunnel
     above it instead of a floating overlay. order keeps it directly under the
     stage now that both live inside the hero's flex column. */
  .specimen{position:static;text-align:left;opacity:1;order:100;
    margin-top:-1px;border:1px solid var(--edge);border-top:0;
    background:rgba(7,13,23,.72);padding:14px 16px 16px}
  .coeff{justify-content:flex-start;gap:22px}
  .coeff div{text-align:left}
  .coeff dd{font-size:17px}
  .traces{margin-bottom:14px}
  .trace-row{justify-content:flex-start}
  /* Let the graphs use the width they have rather than sitting at a fixed 200px. */
  .trace-row canvas{width:100%;max-width:none;flex:1;min-width:0}
  .specimen-actions{justify-content:flex-start;flex-wrap:wrap}
  /* 44px is the accepted minimum tap target; these were 38. */
  .specimen button{padding:14px 16px;font-size:10.5px;min-height:44px}
}

/* Motion is an enhancement — never a precondition for reading the page. */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .eyebrow,.about,.legend,.cta,.card,.specimen,h1 .w{opacity:1!important}
}
html:not(.js) :is(.eyebrow,.about,.legend,.cta,.card,.specimen,h1 .w){opacity:1!important}

/* ---------- project pages ---------- */
.back{margin-bottom:14px;font-size:11px;letter-spacing:.14em;text-transform:uppercase}
.back a{color:var(--fg-dim);text-decoration:none}
.back a:hover,.back a:focus-visible{color:var(--c2)}
.proj-title{font-size:clamp(28px,4.4vw,52px);font-weight:800;letter-spacing:-.03em;
  line-height:1.05;margin-bottom:12px}
.proj-plaque{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--c1);margin-bottom:clamp(20px,3vh,32px)}


/* ---------- the tunnel as its own block, portrait only ---------- */
@media (max-width:880px) and (orientation:portrait){
  /* position:relative is load-bearing — without it the absolutely positioned
     canvas sizes against .wrap and becomes as tall as the whole document. */
  .stage{position:relative;top:auto;bottom:auto;left:auto;width:auto;z-index:auto;
    order:99;display:block;height:52svh;margin:clamp(18px,3vh,26px) 0 0;
    border:1px solid var(--edge);background:rgba(3,6,12,.5)}
  .veil{display:none}
  .hero{min-height:0;padding-bottom:0;margin-top:0;padding-top:clamp(30px,6vh,80px)}
}

/* ---------- exhibit grids ---------- */
/* Three across on desktop. The prose pages cap .wrap at a reading measure, which
   is right for paragraphs and wrong for a grid of cards, so the grid breaks out. */
@media (min-width:1000px){
  .grid{grid-template-columns:repeat(3,1fr)}
}

/* ---------- page backgrounds ---------- */
/* Fixed behind everything, dim on purpose: this sits under body text. The veil
   keeps contrast where the copy is, so the field is texture rather than noise. */
.pagebg{position:fixed;inset:0;width:100%;height:100%;z-index:0;display:block;opacity:.5}
.pagebg-veil{position:fixed;inset:0;z-index:1;pointer-events:none;
  background:
    linear-gradient(180deg,var(--bg) 0,rgba(3,6,12,.6) 16%,rgba(3,6,12,.6) 84%,var(--bg) 100%),
    linear-gradient(90deg,var(--bg) 0,rgba(3,6,12,.42) 26%,rgba(3,6,12,.42) 74%,var(--bg) 100%)}
@media (prefers-reduced-motion:reduce){ .pagebg{opacity:.2} }

/* ---------- mobile input and touch sizing ---------- */
@media (max-width:880px){
  /* iOS Safari zooms the viewport when a focused control is under 16px. On a
     contact form that throws the layout every time someone taps a field. */
  .contact-form input,.contact-form textarea,.contact-form select{font-size:16px}
  /* 44px minimum touch target. */
  .burger{width:44px;height:44px}
  .btn{min-height:44px}
  .topbar nav a{min-height:44px;display:flex;align-items:center}
}

/* ---------- ASCII city (software page) ---------- */


@media (max-width:620px){
}

/* ---------- landing category buttons ---------- */
/* Two big doors above the selected exhibits: taller than a card, art a touch
   brighter, an arrow so they read as navigation rather than exhibits. */
.cat-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));margin-bottom:clamp(26px,4vh,44px)}
.cat-card{min-height:170px;display:flex;flex-direction:column;justify-content:flex-end}
.cat-card .card-art{opacity:.22}
.cat-card:hover .card-art{opacity:.3}
.cat-card h3{font-size:clamp(24px,3vw,34px);letter-spacing:-.03em}
.cat-card h3::after{content:' →';color:var(--c2);font-weight:400}
/* Group hero on the category pages sits after the lede, a little shorter. */
.group-hero{margin:clamp(22px,4vh,40px) 0}
.group-hero img{height:clamp(180px,30vh,320px)}

/* Charts are figures on paper: never cropped, on a light ground so the axes
   and gridlines stay legible against the dark page. */
.shot img.chart,.gallery img.chart{object-fit:contain;background:#eef2f6;padding:8px}
.formula{font-size:13.5px;line-height:2;color:var(--fg);background:var(--bg-2);
  border:1px solid var(--edge);border-left:2px solid var(--c2);border-radius:2px;
  padding:14px 18px;margin:18px 0;overflow-x:auto}
.formula sub,.formula i{color:var(--c1)}

/* Heavier mono for the small uppercase labels, they read too thin on dark. */
.topbar-inner,.eyebrow,.section-head,.back,.plaque,.legend .cap,.legend .ticks,.spec-sheet dt,.contact-block dt,.legend-note{font-weight:600}
.topbar .id{font-weight:700}
.topbar-inner{font-size:11.5px}
.eyebrow{font-size:11.5px}
.back span{color:var(--edge-2);margin:0 8px}
.back b{color:var(--fg-dim);font-weight:600}

/* ---------- project explorer (home) ---------- */
.explorer .section-head{margin-top:clamp(40px,7vh,90px)}
.xp{display:grid;grid-template-columns:minmax(280px,380px) minmax(0,1fr);border:1px solid var(--edge);background:var(--bg-2)}
.xp-list{border-right:1px solid var(--edge);display:flex;flex-direction:column;min-width:0;min-height:0}
.xp-tools{padding:12px 14px;border-bottom:1px solid var(--edge);display:flex;flex-direction:column;gap:10px}
.xp-search{width:100%;background:transparent;border:1px solid var(--edge-2);border-radius:2px;color:var(--fg);padding:8px 10px;
  font-size:11px;letter-spacing:.12em;text-transform:uppercase}
.xp-search::placeholder{color:var(--fg-dim)}
.xp-search:focus{outline:none;border-color:var(--c2)}
.xp-filters{display:flex;gap:8px}
.xp-filters button{font:inherit;font-size:10.5px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;padding:7px 12px;
  border:1px solid var(--edge-2);border-radius:2px;background:transparent;color:var(--fg-dim);cursor:pointer}
.xp-filters button:hover{color:var(--fg)}
.xp-filters button.on{background:var(--c2);border-color:var(--c2);color:#04120c}
.xp-rows{display:flex;flex-direction:column;overflow:auto;flex:1 1 0;min-height:0;scrollbar-width:thin}
.xp-rows:focus-visible{outline:2px solid var(--c2);outline-offset:-2px}
.xp-row{display:grid;grid-template-columns:24px 1fr auto;gap:12px;align-items:center;padding:11px 16px;border:0;
  border-bottom:1px solid var(--edge);border-left:2px solid transparent;background:transparent;color:var(--fg-dim);
  text-align:left;font:inherit;font-size:14px;line-height:1.3;cursor:pointer;transition:background .15s,color .15s}
.xp-row .n{font-size:10px;letter-spacing:.06em}
.xp-row .tag{font-size:9.5px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--edge-2)}
.xp-row:hover{color:var(--fg);background:rgba(216,228,240,.03)}
.xp-row[aria-selected=true]{color:var(--fg);font-weight:600;background:#0a1420;border-left-color:var(--c2)}
.xp-row[aria-selected=true] .tag{color:var(--c1)}
.xp-row[hidden]{display:none}
.xp-detail{display:flex;flex-direction:column;text-decoration:none;color:inherit;min-width:0}
.xp-scene{position:relative;aspect-ratio:16/9;max-height:380px;border-bottom:1px solid var(--edge);overflow:hidden;
  background:linear-gradient(var(--edge) 1px,transparent 1px) center/44px 44px,linear-gradient(90deg,var(--edge) 1px,transparent 1px) center/44px 44px,var(--bg)}
.xp-scene img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;transition:opacity .3s}
.xp-scene.is-swapping img{opacity:0}
.xp-bar{height:2px;background:var(--edge)}
.xp-bar i{display:block;height:2px;width:0;background:var(--c2)}
.xp-body{padding:26px 36px 32px;display:flex;flex-direction:column;gap:12px}
.xp-cat{font-size:10.5px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--c1)}
.xp-body h3{font-size:clamp(20px,2.4vw,28px);font-weight:600;letter-spacing:-.02em;line-height:1.2;text-wrap:balance}
.xp-body .plaque{font-size:10.5px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--fg-dim)}
.xp-abs{font-size:14.5px;line-height:1.7;color:var(--fg-dim);max-width:62ch}
.xp-open{align-self:flex-start;margin-top:4px}
.xp-detail:hover .xp-open{filter:brightness(1.14)}
.xp-foot{display:flex;gap:26px;margin-top:14px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase}
.xp-foot a{color:var(--fg-dim);text-decoration:none}
.xp-foot a:hover{color:var(--c2)}
@media (max-width:880px){
  .xp{grid-template-columns:1fr}
  .xp-list{border-right:0;border-bottom:1px solid var(--edge)}
  .xp-tools{flex-direction:row;align-items:center}
  .xp-search{width:auto;flex:1}
  .xp-rows{flex-direction:row;overflow-x:auto;max-height:none}
  .xp-row{grid-template-columns:auto;padding:10px 14px;border-bottom:0;border-left:0;border-right:1px solid var(--edge);white-space:nowrap}
  .xp-row .n,.xp-row .tag{display:none}
  .xp-row[aria-selected=true]{box-shadow:inset 0 -2px 0 var(--c2)}
  .xp-body{padding:20px 18px 24px}
}

.specimen .solver{margin:10px 0 12px;font-size:9.5px;line-height:1.6;letter-spacing:.06em;text-transform:none;color:var(--fg-dim)}
.specimen .solver b{color:var(--fg);font-weight:500}

.also{font-size:14.5px;line-height:1.75;color:var(--fg-dim);max-width:70ch}
.also b{color:var(--fg);font-weight:500}

/* ---------- formatting pass, September 2026 ---------- */
/* One column per page type. Everything on a page aligns to it: text, figures,
   tables, captions, contact block. Hub pages (they contain a .grid) run wider. */
:root{--prose:#b9c6d3}
body.page .wrap{max-width:860px;padding-inline:clamp(18px,4vw,40px)}
body.page .wrap:has(.grid){max-width:1120px}
main p,main li{font-size:clamp(15.5px,1.15vw,16.5px);line-height:1.75;color:var(--prose);
  max-width:none;text-wrap:pretty}
.lede{font-size:clamp(17.5px,1.6vw,20.5px);line-height:1.6;max-width:none;margin-bottom:26px}
body.page .wrap:has(.grid) .lede{max-width:74ch}
main h2{margin:clamp(44px,7vh,68px) 0 14px;text-wrap:balance}
.proj-title{text-wrap:balance}
.proj-plaque{line-height:1.9;text-wrap:balance;margin-bottom:6px}
.page-title{margin-top:clamp(28px,6vh,60px)}
.spec-sheet{max-width:none}
.spec-sheet dd{font-size:14.5px;line-height:1.6}
.spec-sheet dt{padding-top:3px}
.also{font-size:clamp(15px,1.1vw,16px)!important;max-width:none}
.formula{font-size:14.5px}
[id]{scroll-margin-top:80px}

/* Captions: one size everywhere, readable, never wider than the figure. */
.shot figcaption,.proj-hero figcaption,.gallery figcaption,.gallery.carousel figcaption{
  font-size:12.5px;line-height:1.55;letter-spacing:.02em;color:var(--fg-dim);margin-top:10px}
.gallery.carousel figcaption{min-height:2.4em}
.shot,.gallery{margin:clamp(24px,4vh,38px) 0}

/* Carousels show the whole image. Screenshots, plots and drawings lose their
   meaning when cropped to fill, so they sit whole on a dark panel instead. */
.gallery.carousel img{object-fit:contain;background:#070d17;border:1px solid var(--edge-2);
  height:clamp(260px,52vh,520px)}
.gallery.carousel img.chart{background:#eef2f6}
.gal-btn{top:calc(clamp(260px,52vh,520px)/2 - 22px)}
.shot img{background:#070d17}

/* Cards: real gaps instead of hairlines on a painted ground, so an unfilled row
   is simply empty. Two across; a lone last card takes the whole row. */
.grid{gap:14px;background:none;border:0;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr))}
.card{border:1px solid var(--edge);border-radius:3px;min-height:168px;display:flex;flex-direction:column;
  justify-content:flex-end;padding:clamp(20px,2.4vw,28px)}
a.card{display:flex}
.card:hover{border-color:var(--edge-2)}
.card .card-art{opacity:.22}
.card:hover .card-art{opacity:.32}
.card .plaque{color:#8fa3b6;line-height:1.7}
@media (min-width:760px){
  body.page .grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  body.page .grid>.card:last-child:nth-child(odd){grid-column:1/-1}
}
@media (hover:hover){
  .card:hover .card-abs,a.card:focus-visible .card-abs{left:0;right:0;font-size:13.5px;color:var(--prose)}
}
.section-head{margin:clamp(44px,7vh,80px) 0 20px}

/* Explorer keeps its own width on the home page; tidy its type to match. */
.xp-abs{font-size:15.5px;color:var(--prose)}
.xp-row{font-size:14.5px}

/* Mobile: the fixed contact bar must not cover the footer. */
@media (max-width:880px){
  body{padding-bottom:64px}
  .gallery.carousel img{height:clamp(220px,40vh,360px)}
  .gal-btn{top:calc(clamp(220px,40vh,360px)/2 - 22px);width:40px;height:40px}
  .spec-sheet div{grid-template-columns:1fr;gap:4px}
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
/* Explorer tools stack on small screens; side by side they overflow the viewport. */
@media (max-width:880px){
  .xp-tools{flex-direction:column;align-items:stretch}
  .xp-search{width:100%;flex:none}
  .xp-filters{flex-wrap:wrap}
  .xp-body h3{font-size:21px}
}
/* On small screens the list is a horizontal strip with its own height; the
   flex-basis:0 that lets it fill the desktop column would collapse it here. */
@media (max-width:880px){
  .xp-rows{flex:none;min-height:auto}
  .xp-row{font-size:14px}
}

/* ---------- project page foot links and home feature bands ---------- */
.next{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:clamp(40px,7vh,64px) 0 0}
.next a{border:1px solid var(--edge);border-radius:3px;padding:16px;display:flex;flex-direction:column;gap:6px;text-decoration:none;transition:border-color .22s}
.next a:hover{border-color:var(--edge-2)}
.next a:last-child{text-align:right}
.next span{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--fg-dim)}
.next b{color:var(--fg);font-size:15px;font-weight:600}

.gal-thumbs{gap:6px;flex-wrap:wrap}
.gal-thumbs .gal-dot{width:74px;height:50px;border-radius:2px;padding:0;overflow:hidden;opacity:.5;background:#070d17;transition:opacity .2s,border-color .2s}
.gal-thumbs .gal-dot img{width:100%;height:100%;object-fit:cover;display:block}
.gal-thumbs .gal-dot img.chart{background:#eef2f6}
.gal-thumbs .gal-dot:hover{opacity:.85}
.gal-thumbs .gal-dot[aria-current="true"]{opacity:1;background:#070d17;border-color:var(--c1)}


.features .intro{font-size:clamp(17.5px,1.6vw,20.5px);line-height:1.6;color:var(--fg);max-width:62ch;margin:clamp(44px,8vh,90px) 0 28px}
.band{display:grid;grid-template-columns:1.1fr 1fr;border:1px solid var(--edge);background:var(--bg-2);text-decoration:none;color:inherit;margin-bottom:14px;transition:border-color .22s}
.band:hover{border-color:var(--edge-2)}
.band:nth-of-type(even) .band-pic{order:2}
.band-pic{position:relative;min-height:clamp(240px,34vh,360px);overflow:hidden;background:#070d17}
.band-pic img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.band-txt{display:flex;flex-direction:column;justify-content:center;gap:12px;padding:clamp(22px,3.4vw,52px)}
.band-txt strong{font-size:clamp(21px,2.3vw,31px);line-height:1.18;font-weight:800;letter-spacing:-.015em;color:var(--fg);text-wrap:balance}
/* links inside prose look like the contact links, not browser-default blue */
main p:not(.back) a:not(.btn){color:var(--fg);text-decoration:none;border-bottom:1px solid var(--edge-2);padding-bottom:1px;transition:color .2s,border-color .2s}
main p:not(.back) a:not(.btn):hover,main p:not(.back) a:not(.btn):focus-visible{color:var(--c2);border-color:var(--c2)}
/* the plaque line leads straight into the lede now the strips are gone */
.proj-plaque{margin-bottom:26px}
.band-abs{font-size:15px;line-height:1.65;color:var(--prose);max-width:52ch}
.band-go{font-size:12px;color:var(--c1)}
.band:hover .band-go{color:var(--fg)}
@media (min-width:760px){body.page .grid>.card.wide{grid-column:1/-1;min-height:220px}}
@media (max-width:880px){
  .band{grid-template-columns:1fr}
  .band:nth-of-type(even) .band-pic{order:0}
  .band-pic{min-height:200px;max-height:240px}
  .gal-thumbs .gal-dot{width:56px;height:40px}
}

/* ---------- header and labels, September 2026 ---------- */
/* The name in the corner is a signature, not a label. */
.topbar .id{font-family:'Ms Madi',cursive;font-size:30px;font-weight:400;letter-spacing:0;
  text-transform:none;line-height:1;color:var(--fg);padding-top:4px}
.topbar .id:hover,.topbar .id:focus-visible{color:var(--fg)}
/* Small uppercase mono labels read faint at 400 on the dark ground: heavier and a touch brighter, same size. */
:root{--label:#a7b8c9}
.topbar-inner,.eyebrow,.back,.section-head,.plaque,.proj-plaque,.band-go,footer{font-weight:600}
.topbar a,footer,footer a,.back a{color:var(--label)}
.topbar a[aria-current=page]{color:var(--fg)}
/* The home intro runs wider on desktop; it's two sentences. */
.features .intro{max-width:1000px}

/* ---------- simulation view and settings ---------- */
/* Pressed and open states use the hover colour, so the current view reads at a glance. */
.specimen button[aria-pressed=true],.specimen button[aria-expanded=true]{border-color:var(--c2);color:var(--c2)}
.specimen-actions+.specimen-actions{margin-top:8px}
.sim-settings{width:min(100%,300px);margin:0 0 12px auto;padding:12px 14px 14px;text-align:left;
  border:1px solid var(--edge-2);background:rgba(3,6,12,.82);border-radius:2px}
.set-row{display:flex;justify-content:space-between;gap:12px;font-size:9.5px;letter-spacing:.16em}
.set-row label{color:var(--fg-dim)}
.set-row output{color:var(--fg);font-variant-numeric:tabular-nums}
.sim-settings input[type=range]{display:block;width:100%;margin:6px 0 12px;accent-color:var(--c2)}
@media (max-width:880px){
  .sim-settings{margin-left:0}
  .sim-settings input[type=range]{min-height:44px}
}

/* ---------- carousel full-screen viewer (carousel.js) ---------- */
@media (pointer:fine){.gallery.carousel .gal-track img{cursor:zoom-in}}
/* Top-right corner of the slide, clear of prev/next which sit at mid-height. */
.gal-btn.gal-fs{top:10px;right:10px;width:44px;height:44px}
.lb{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;gap:12px;
  padding:64px 16px 16px;background:#03060c;color:var(--fg)}
.lb:focus{outline:none}
.lb-stage{position:relative;flex:1;min-height:0;overflow:hidden}
/* Absolute + inset:0 + margin:auto fits and centres the image without upscaling it. */
.lb-img{position:absolute;inset:0;margin:auto;max-width:100%;max-height:100%;
  transform-origin:0 0;transition:transform .25s ease-out;cursor:zoom-in;user-select:none}
.lb-img.chart{background:#eef2f6}
.lb[data-zoom] .lb-img{cursor:zoom-out;touch-action:none}
.lb-cap{margin:0 auto;max-width:72ch;text-align:center;font-size:12.5px;line-height:1.55;
  letter-spacing:.02em;color:var(--fg-dim)}
.lb-count{position:absolute;top:22px;left:16px;font-size:12px;letter-spacing:.1em;
  color:var(--label);font-variant-numeric:tabular-nums}
.lb .gal-btn{top:calc(50% - 22px);width:44px;height:44px}
.lb .lb-close{top:10px;right:10px}
@media (prefers-reduced-motion:reduce){.lb-img{transition:none}}

/* ---------- About: about me beside the tools, September 2026 ---------- */
.spec-sheet dd a{color:var(--fg);text-decoration:none;border-bottom:1px solid var(--edge-2)}
.spec-sheet dd a:hover,.spec-sheet dd a:focus-visible{color:var(--c2);border-color:var(--c2)}
body.page .wrap:has(.about-page){max-width:1240px}
.about-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr);gap:clamp(24px,4vw,64px);align-items:start}
.about-me .lede{margin-top:0}
.about-side > h2:first-child{margin-top:0}
.about-side .spec-sheet{margin-top:0;max-width:none}
@media (max-width:900px){ .about-grid{grid-template-columns:1fr} }
