/* ============================================================
   Project detail pages.
   Layers on top of styles.css - tokens, nav, footer and buttons
   all come from there. This file adds the article chrome and the
   five diagrams, one per project, deliberately none alike.
   ============================================================ */

/* ---------- Page chrome ---------- */
/* Sub-pages carry no navigation bar - the site nav belongs to the main page.
   All that floats here is the theme switch, and the hero's own "All work" link
   is the way back. */
.p-toggle {
  position: fixed; z-index: 60;
  top: clamp(12px, 2.4vw, 20px); right: clamp(12px, 3vw, 26px);
  border-radius: 999px; padding: 3px;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
  border: 1px solid var(--line);
}
.p-toggle .theme-switch { height: 30px; }
.p-hero, .p-section, .footer { position: relative; }

.p-hero { padding: clamp(58px, 9vw, 92px) 0 clamp(30px, 5vw, 44px); position: relative; }
.p-back {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: .85rem; color: var(--tx-2); font-weight: 500;
  transition: gap var(--t-fast), color var(--t-fast);
}
.p-back:hover { color: var(--tx); gap: 12px; }
.p-back svg { width: 15px; height: 15px; }

.p-eyebrow {
  display: flex; align-items: center; gap: 10px; width: fit-content; margin-top: 26px;
  font-family: var(--mono); font-size: .76rem; color: var(--acc);
}
.p-eyebrow i { width: 4px; height: 4px; border-radius: 50%; background: currentColor; }
.p-title {
  margin-top: 14px; font-family: var(--display); font-optical-sizing: auto;
  font-size: clamp(2rem, 5.4vw, 3.2rem);
  line-height: 1.06; letter-spacing: -.018em; font-weight: 600;
}
.p-lede {
  margin-top: 18px; max-width: 62ch; color: var(--tx-2);
  font-size: clamp(1rem, 2.4vw, 1.1rem); line-height: 1.72;
}
.p-facts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
  gap: 1px; margin-top: 34px;
  background: var(--line); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden;
}
.p-fact { background: var(--bg); padding: 15px 18px; }
.p-fact dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .07em; color: var(--tx-3); }
.p-fact dd { margin: 6px 0 0; font-size: .9rem; color: var(--tx); font-weight: 550; }
.p-fact dd.mono { font-family: var(--mono); font-size: .84rem; }

.p-section { padding: clamp(34px, 6vw, 56px) 0; }
.p-section + .p-section { border-top: 1px solid var(--line); }
.p-h2 {
  font-size: clamp(1.25rem, 3vw, 1.6rem); letter-spacing: -.028em;
  display: flex; align-items: baseline; gap: 12px;
}
.p-h2 span { font-family: var(--mono); font-size: .8rem; color: var(--acc); font-weight: 400; }
.p-body { margin-top: 18px; max-width: 72ch; color: var(--tx-2); line-height: 1.8; }
.p-body p + p { margin-top: 15px; }
.p-body strong { color: var(--tx); font-weight: 600; }
.p-body code {
  font-family: var(--mono); font-size: .86em;
  background: var(--surface-2); border: 1px solid var(--line);
  padding: 1px 6px; border-radius: 5px; color: var(--tx);
}
.p-body a { color: var(--acc); }

.p-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 18px; margin-top: 26px; }
.p-card {
  border: 1px solid var(--line); border-radius: var(--r); background: var(--surface);
  padding: 20px 22px; transition: border-color var(--t), transform var(--t);
}
.p-card:hover { border-color: var(--acc-line); transform: translateY(-2px); }
.p-card h3 { font-size: .99rem; letter-spacing: -.02em; }
.p-card p { margin-top: 9px; color: var(--tx-2); font-size: .9rem; line-height: 1.7; }
.p-card .k { font-family: var(--mono); font-size: .74rem; color: var(--acc); }

.p-list { margin-top: 20px; display: grid; gap: 13px; max-width: 72ch; }
.p-list li { position: relative; padding-left: 26px; color: var(--tx-2); line-height: 1.75; }
.p-list li::before {
  content: ""; position: absolute; left: 4px; top: .62em;
  width: 7px; height: 7px; border-radius: 2px;
  border: 1px solid var(--acc-line); background: var(--acc-soft);
}
.p-list li strong { color: var(--tx); font-weight: 600; }

.p-note {
  margin-top: 24px; padding: 16px 20px; border-radius: var(--r);
  border: 1px solid var(--line); border-left: 2px solid var(--acc);
  background: var(--surface); color: var(--tx-2); font-size: .92rem; line-height: 1.72;
}

.p-figcap { margin-top: 14px; font-size: .82rem; color: var(--tx-3); line-height: 1.65; text-align: center; }

.p-stage {
  margin-top: 26px; border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface); padding: clamp(18px, 4vw, 30px);
}
.p-stage-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 22px; }
.p-stage-head h3 { font-size: 1.02rem; letter-spacing: -.02em; }
.p-stage-head p { font-size: .84rem; color: var(--tx-3); }
/* Status, not a control - the animations restart themselves. */
.p-looping {
  display: inline-flex; align-items: center; gap: 8px; flex: none;
  font-family: var(--mono); font-size: .66rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--tx-3);
  border: 1px solid var(--line); background: var(--bg);
  padding: 5px 11px; border-radius: 999px;
}
.p-looping i { width: 5px; height: 5px; flex: none; border-radius: 50%; background: var(--acc); }
.js .p-looping i { animation: loopdot 2.4s var(--ease) infinite; }
@keyframes loopdot { 0%, 100% { opacity: .3; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .js .p-looping i { animation: none; opacity: .8; } }

.p-tablewrap { margin-top: 22px; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r); }
.p-table { width: 100%; border-collapse: collapse; font-size: .88rem; min-width: 460px; }
.p-table th, .p-table td { text-align: left; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.p-table th { font-size: .74rem; text-transform: uppercase; letter-spacing: .06em; color: var(--tx-3); background: var(--surface); font-weight: 600; }
.p-table td { color: var(--tx-2); }
.p-table tr:last-child td { border-bottom: 0; }
.p-table code { font-family: var(--mono); font-size: .84em; color: var(--tx); }

.p-next { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 30px; }

/* ============================================================
   1. CAN bus arbitration - Body control module
   A shared bus where three ECUs talk at once and the lowest
   identifier wins, bit by bit. Nothing else on the site is a
   contention race, so this page gets one.
   ============================================================ */
.can { display: grid; gap: 0; }
.can-lane { display: grid; grid-template-columns: 128px 1fr; align-items: center; gap: 16px; padding: 9px 0; }
.can-name { display: flex; flex-direction: column; gap: 2px; }
.can-name b { font-size: .84rem; font-weight: 600; letter-spacing: -.01em; }
.can-name span { font-family: var(--mono); font-size: .68rem; color: var(--tx-3); }
.can-bits { display: flex; gap: 5px; flex-wrap: wrap; }
.can-bit {
  width: 26px; height: 30px; flex: none;
  display: grid; place-items: center; border-radius: 5px;
  font-family: var(--mono); font-size: .8rem; font-weight: 600;
  border: 1px solid var(--line-strong); background: var(--bg); color: var(--tx-3);
  opacity: .25; transform: translateY(3px);
  transition: opacity .22s var(--ease), transform .22s var(--ease),
              background .22s var(--ease), color .22s var(--ease), border-color .22s var(--ease);
}
.can-bit.on { opacity: 1; transform: none; color: var(--tx); }
/* A dominant 0 that survives arbitration. */
.can-bit.dom { background: var(--acc-soft); border-color: var(--acc-line); color: var(--acc); }
/* The bit where a node discovered it lost and backed off. */
.can-bit.lost { background: color-mix(in srgb, #E0685A 16%, transparent); border-color: color-mix(in srgb, #E0685A 45%, transparent); color: #E0685A; }
.can-lane.out .can-name b { color: var(--tx-3); text-decoration: line-through; }
.can-lane.out .can-bit:not(.lost) { opacity: .12; }
.can-lane.win .can-name b { color: var(--good); }

.can-bus { position: relative; margin: 18px 0 4px; height: 3px; border-radius: 2px; background: var(--line-strong); }
.can-bus i {
  position: absolute; top: -3px; left: 0; width: 46px; height: 9px; border-radius: 5px;
  background: linear-gradient(90deg, transparent, var(--acc), transparent);
  opacity: 0;
}
.can.is-sending .can-bus i { animation: canframe 1.15s var(--ease) forwards; }
@keyframes canframe {
  0%   { opacity: 0; left: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { opacity: 0; left: calc(100% - 46px); }
}
.can-verdict {
  margin-top: 16px; font-family: var(--mono); font-size: .78rem;
  color: var(--tx-3); min-height: 1.5em;
}
.can-verdict b { color: var(--good); }

/* ============================================================
   2. Finite state machine - Battleship
   A ring of states with a token walking the transitions, plus a
   live board. This page is about control flow, so it animates as
   a graph traversal rather than a race or a pipeline.
   ============================================================ */
.fsm-wrap { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 26px; align-items: start; }
.fsm svg { display: block; width: 100%; max-width: 420px; height: auto; margin-inline: auto; }
.fsm-node rect { fill: var(--bg); stroke: var(--line-strong); stroke-width: 1; transition: fill .3s var(--ease), stroke .3s var(--ease); }
.fsm-node text { font-family: var(--sans); font-size: 11.5px; font-weight: 600; fill: var(--tx-2); transition: fill .3s var(--ease); }
.fsm-node.is-active rect { fill: var(--acc-soft); stroke: var(--acc); }
.fsm-node.is-active text { fill: var(--acc); }
.fsm-edge { stroke: var(--line-strong); stroke-width: 1.2; fill: none; opacity: .55; }
.fsm-edge.is-hot { stroke: var(--acc); opacity: 1; }
.fsm-arrow { fill: var(--tx-3); }
.fsm-cap { font-family: var(--mono); font-size: 8.5px; fill: var(--tx-3); }

.board { display: grid; grid-template-columns: repeat(10, 1fr); gap: 3px; max-width: 300px; margin-inline: auto; }
.board i {
  aspect-ratio: 1; border-radius: 3px; background: var(--surface-2);
  border: 1px solid var(--line); transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease);
}
.board i.miss { background: var(--line-strong); border-color: var(--line-strong); opacity: .85; }
.board i.hit { background: color-mix(in srgb, #E0685A 62%, transparent); border-color: #E0685A; transform: scale(1.14); }
.board i.sunk { background: var(--good); border-color: var(--good); }
.board i.aim { border-color: var(--acc); background: var(--acc-soft); }
.fsm-log {
  margin-top: 16px; font-family: var(--mono); font-size: .74rem;
  color: var(--tx-3); line-height: 1.75; min-height: 5.2em;
}
.fsm-log b { color: var(--acc); font-weight: 500; }
.fsm-log .hitline { color: #E0685A; }

/* ============================================================
   3. Clock tree - RCC/PLL bring-up
   A tree that divides one oscillator into many rates. It animates
   by locking stages in sequence and running square waves at the
   real relative frequencies, which suits a page about clocks.
   ============================================================ */
.clk { display: grid; gap: 12px; }
.clk-row { display: grid; grid-template-columns: 150px 1fr 108px; align-items: center; gap: 14px; }
.clk-cell {
  border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--bg); padding: 11px 14px;
  opacity: .35; transition: opacity .4s var(--ease), border-color .4s var(--ease), background .4s var(--ease);
}
.clk-cell b { display: block; font-size: .84rem; letter-spacing: -.01em; }
.clk-cell span { display: block; margin-top: 2px; font-family: var(--mono); font-size: .68rem; color: var(--tx-3); }
.clk-row.locked .clk-cell { opacity: 1; border-color: var(--acc-line); background: var(--acc-soft); }
.clk-row.locked.term .clk-cell { border-color: color-mix(in srgb, var(--good) 45%, transparent); background: transparent; }

.clk-wave { height: 34px; opacity: .2; transition: opacity .4s var(--ease); }
.clk-row.locked .clk-wave { opacity: 1; }
.clk-wave svg { width: 100%; height: 100%; display: block; }
.clk-wave path { fill: none; stroke: var(--acc); stroke-width: 1.6; stroke-linejoin: round; }
.clk-row.locked.term .clk-wave path { stroke: var(--good); }
.clk-wave .w-run { stroke-dasharray: 5 7; }
.clk-row.locked .clk-wave .w-run { animation: clkrun 1s linear infinite; }
.clk-row.locked .clk-wave.f2 .w-run { animation-duration: 2s; }
.clk-row.locked .clk-wave.f4 .w-run { animation-duration: 4s; }
@keyframes clkrun { to { stroke-dashoffset: -48; } }

.clk-hz {
  font-family: var(--mono); font-size: .92rem; font-weight: 600;
  color: var(--tx-3); text-align: right;
  transition: color .4s var(--ease);
}
.clk-row.locked .clk-hz { color: var(--acc); }
.clk-row.locked.term .clk-hz { color: var(--good); }
.clk-hz small { display: block; font-size: .64rem; font-weight: 400; color: var(--tx-3); margin-top: 1px; }

/* ============================================================
   4. Inference pipeline - assistive doorbell
   A frame moving left to right through fixed stages, with a
   latency budget filling underneath. Linear and timed, unlike
   anything else here.
   ============================================================ */
.pipe { display: grid; gap: 0; }
.pipe-track { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; }
.pipe-stage {
  position: relative; border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  background: var(--bg); padding: 14px 12px; text-align: center;
  transition: border-color .3s var(--ease), background .3s var(--ease), transform .3s var(--ease);
}
.pipe-stage .ico { display: block; width: 20px; height: 20px; margin: 0 auto 9px; color: var(--tx-3); transition: color .3s var(--ease); }
.pipe-stage b { display: block; font-size: .78rem; letter-spacing: -.01em; }
.pipe-stage span { display: block; margin-top: 3px; font-family: var(--mono); font-size: .64rem; color: var(--tx-3); }
.pipe-stage.hot { border-color: var(--acc); background: var(--acc-soft); transform: translateY(-3px); }
.pipe-stage.hot .ico { color: var(--acc); }
.pipe-stage.done { border-color: color-mix(in srgb, var(--good) 40%, transparent); }
.pipe-stage.done .ico { color: var(--good); }

.pipe-budget { margin-top: 22px; }
.pipe-bar { height: 30px; border: 1px solid var(--line); border-radius: 7px; background: var(--bg); overflow: hidden; display: flex; }
.pipe-seg { height: 100%; width: 0; transition: width .3s linear; display: grid; place-items: center; overflow: hidden; }
.pipe-seg span { font-family: var(--mono); font-size: .64rem; color: var(--on-acc); white-space: nowrap; opacity: 0; transition: opacity .3s; }
.pipe-seg.show span { opacity: 1; }
.pipe-seg.s1 { background: color-mix(in srgb, var(--acc) 40%, transparent); }
.pipe-seg.s2 { background: color-mix(in srgb, var(--acc) 62%, transparent); }
.pipe-seg.s3 { background: var(--acc); }
.pipe-seg.s4 { background: color-mix(in srgb, var(--good) 70%, transparent); }
.pipe-seg.s5 { background: var(--good); }
.pipe-legend { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 12px; font-family: var(--mono); font-size: .7rem; color: var(--tx-3); }
.pipe-legend span { display: inline-flex; align-items: center; }
.pipe-legend i { display: inline-block; width: 9px; height: 9px; flex: none; border-radius: 2px; margin-right: 7px; }
.pipe-legend i.s1 { background: color-mix(in srgb, var(--acc) 40%, transparent); }
.pipe-legend i.s2 { background: color-mix(in srgb, var(--acc) 62%, transparent); }
.pipe-legend i.s3 { background: var(--acc); }
.pipe-legend i.s4 { background: color-mix(in srgb, var(--good) 70%, transparent); }
.pipe-legend i.s5 { background: var(--good); }

.phone {
  margin-top: 26px; margin-inline: auto; max-width: 260px;
  border: 1px solid var(--line-strong); border-radius: 20px; background: var(--bg);
  padding: 14px; min-height: 108px; display: grid; align-content: start; gap: 9px;
}
.phone-note {
  display: flex; gap: 11px; align-items: flex-start;
  border: 1px solid var(--line); border-radius: 11px; background: var(--surface);
  padding: 11px 12px;
  opacity: 0; transform: translateY(-10px) scale(.97);
  transition: opacity .4s var(--ease), transform .4s var(--ease);
}
.phone-note.in { opacity: 1; transform: none; }
.phone-note .pn-ico { width: 26px; height: 26px; flex: none; border-radius: 7px; background: var(--acc-soft); color: var(--acc); display: grid; place-items: center; }
.phone-note .pn-ico svg { width: 14px; height: 14px; }
.phone-note b { display: block; font-size: .76rem; }
.phone-note span { display: block; margin-top: 2px; font-size: .68rem; color: var(--tx-3); line-height: 1.5; }
.phone-idle { font-family: var(--mono); font-size: .68rem; color: var(--tx-3); text-align: center; padding: 22px 0; }

/* ============================================================
   5. Thread pool vs thread-per-client - TCP server
   Live queue and workers, with a toggle that runs the naive
   strategy beside it. The only simulation on the site that the
   reader can steer.
   ============================================================ */
.tp { display: grid; gap: 20px; }
.tp-controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.tp-seg { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; background: var(--bg); padding: 3px; }
.tp-seg button {
  font-family: var(--mono); font-size: .72rem; color: var(--tx-2);
  padding: 7px 14px; border-radius: 999px; border: 0; background: none;
  transition: background var(--t-fast), color var(--t-fast);
}
.tp-seg button[aria-pressed="true"] { background: var(--acc); color: var(--on-acc); }

.tp-stats { display: flex; flex-wrap: wrap; gap: 18px; margin-left: auto; font-family: var(--mono); font-size: .72rem; color: var(--tx-3); }
.tp-stats b { color: var(--tx); font-weight: 600; }
.tp-stats b.warn { color: #E0685A; }

.tp-flow { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr)); gap: 16px; align-items: start; }
.tp-col { border: 1px solid var(--line); border-radius: var(--r); background: var(--bg); padding: 15px; min-height: 178px; }
.tp-col h4 { font-family: var(--mono); font-size: .7rem; color: var(--tx-3); font-weight: 500; letter-spacing: .04em; text-transform: uppercase; }
.tp-col .sub { display: block; margin-top: 3px; font-family: var(--mono); font-size: .64rem; color: var(--tx-3); opacity: .8; }

.tp-clients, .tp-queue { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 13px; }
.tp-conn {
  width: 22px; height: 22px; border-radius: 5px;
  background: var(--acc-soft); border: 1px solid var(--acc-line);
  display: grid; place-items: center;
  font-family: var(--mono); font-size: .56rem; color: var(--acc);
  animation: tpin .3s var(--ease);
}
@keyframes tpin { from { opacity: 0; transform: scale(.5); } to { opacity: 1; transform: none; } }
.tp-queue .tp-conn { background: var(--surface-2); border-color: var(--line-strong); color: var(--tx-3); }
.tp-empty { margin-top: 13px; font-family: var(--mono); font-size: .66rem; color: var(--tx-3); opacity: .6; }

.tp-workers { display: grid; gap: 7px; margin-top: 13px; }
.tp-worker {
  display: grid; grid-template-columns: 54px 1fr 34px; align-items: center; gap: 9px;
  font-family: var(--mono); font-size: .66rem; color: var(--tx-3);
}
.tp-worker .wbar { height: 9px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; }
.tp-worker .wfill { display: block; height: 100%; width: 0; background: var(--acc); transition: width .1s linear; }
.tp-worker.busy { color: var(--tx); }
.tp-worker.busy .wfill { background: var(--good); }

.tp-threads { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 13px; max-height: 128px; overflow: hidden; }
.tp-thread { width: 13px; height: 13px; border-radius: 3px; background: color-mix(in srgb, #E0685A 30%, transparent); border: 1px solid color-mix(in srgb, #E0685A 55%, transparent); animation: tpin .25s var(--ease); }
.tp-warn { margin-top: 12px; font-family: var(--mono); font-size: .68rem; color: #E0685A; line-height: 1.6; }

@media (max-width: 640px) {
  .can-lane { grid-template-columns: 1fr; gap: 7px; }
  .clk-row { grid-template-columns: 1fr; gap: 8px; }
  .clk-hz { text-align: left; }
  .pipe-track { grid-template-columns: repeat(2, 1fr); }
  .tp-stats { margin-left: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .can-bus i, .clk-wave .w-run, .tp-conn, .tp-thread { animation: none !important; }
  .can-bit, .pipe-stage, .phone-note, .board i { transition: none !important; }
}

/* ============================================================
   Project cover - one wide figure at the head of each page.

   Drawn rather than photographed: every cover is a picture of the
   actual system, so it carries information the lede cannot. All of
   it is token-driven, which is why it is inline SVG and not an
   <img> - an external file could not follow the theme toggle.
   ============================================================ */
.p-cover {
  margin: clamp(28px, 4.6vw, 44px) 0 0;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--surface) 90%, transparent);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
.p-cover svg { display: block; width: 100%; height: auto; }
.p-cover figcaption {
  padding: 12px 18px 14px;
  border-top: 1px solid var(--line);
  font-family: var(--mono); font-size: .68rem; letter-spacing: .03em;
  color: var(--tx-3);
}

/* Shared cover vocabulary. Fills stay flat so the shapes read at a glance. */
.cv-panel  { fill: var(--bg-2); }
.cv-node   { fill: var(--raised); stroke: var(--line-strong); stroke-width: 1; }
.cv-node-a { fill: var(--acc-soft); stroke: var(--acc-line); stroke-width: 1.2; }
.cv-line   { stroke: var(--line-strong); stroke-width: 1.4; fill: none; }
.cv-wire   { stroke: var(--acc-line); stroke-width: 1.6; fill: none; }
.cv-acc    { fill: var(--acc); }
.cv-accs   { stroke: var(--acc); stroke-width: 2; fill: none; }
.cv-good   { fill: var(--good); }
.cv-warn   { fill: #E0685A; }
.cv-warns  { stroke: #E0685A; stroke-width: 1.6; fill: none; }
.cv-dim    { fill: var(--tx-3); opacity: .5; }
.cv-t      { fill: var(--tx);   font-family: var(--sans); font-size: 17px; font-weight: 600; }
.cv-m      { fill: var(--tx-2); font-family: var(--mono); font-size: 14px; }
.cv-s      { fill: var(--tx-3); font-family: var(--mono); font-size: 12.5px; }
.cv-ac     { fill: var(--acc);  font-family: var(--mono); font-size: 12.5px; }

/* A travelling highlight, so the cover is alive without being busy. */
.cv-flow { stroke-dasharray: 16 210; animation: cvflow 5.2s linear infinite; }
@keyframes cvflow { to { stroke-dashoffset: -226; } }

/* Fine annotation is unreadable once the figure is phone-width, and squeezing
   a 1000-unit figure into 390px makes the rest illegible too. Drop the
   annotation layer and let the figure scroll, the way the code tables do. */
@media (max-width: 620px) {
  .cv-s { display: none; }
  .p-cover { overflow-x: auto; }
  .p-cover svg { min-width: 580px; }
  .p-cover figcaption { font-size: .64rem; }
}
@media (prefers-reduced-motion: reduce) {
  .cv-flow { animation: none; }
}

/* ============================================================
   Credential pages - degree and certification detail.

   Institution marks are supplied as flat artwork on white, so they
   sit on an explicit white chip rather than inheriting the surface.
   Recolouring somebody else's mark to suit a dark theme would
   misrepresent it; a chip keeps it correct in both themes.
   ============================================================ */
.p-logo {
  /* Block, not inline-flex: it follows the inline .p-back link and would
     otherwise share its line and overlap it. */
  display: flex; width: fit-content;
  align-items: center; justify-content: center;
  margin-top: clamp(20px, 3vw, 28px);
  background: #FFFFFF;
  border: 1px solid var(--line-strong);
  border-radius: var(--r);
  padding: 14px 20px;
  box-shadow: var(--shadow-md);
}
.p-logo img { display: block; width: auto; height: clamp(34px, 5vw, 46px); }
.p-logo + .p-eyebrow { margin-top: 24px; }

/* Where a claim on this page came from. */
.p-source {
  margin-top: 26px; font-family: var(--mono); font-size: .72rem;
  letter-spacing: .02em; color: var(--tx-3);
}
.p-source a { color: var(--tx-2); border-bottom: 1px solid var(--line-strong); }
.p-source a:hover { color: var(--acc); border-bottom-color: var(--acc-line); }

/* A scanned credential, shown as evidence. Captions sit on the same left
   gutter as body copy - .p-figcap centres, which reads as misaligned next
   to a left-set article. */
.p-cert {
  margin: 26px 0 0; padding: 14px;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--surface); box-shadow: var(--shadow-md);
}
.p-cert img { display: block; width: 100%; height: auto; border-radius: var(--r-sm); }
.p-cert figcaption {
  margin-top: 14px; padding: 0 4px 2px; text-align: left;
  font-family: var(--mono); font-size: .7rem; letter-spacing: .02em;
  color: var(--tx-3); line-height: 1.75;
}
/* A portrait-format scan is centred; a wide one fills the column. */
.p-cert-narrow { max-width: 520px; margin-inline: auto; }

/* Numeric columns line up on their digits, not their left edge. */
.p-table th.num, .p-table td.num { text-align: right; font-family: var(--mono); }
.p-table td.g { font-family: var(--mono); color: var(--acc); font-weight: 500; }
.p-table td.t { color: var(--tx); }

/* ============================================================
   Platform blocks - hardware specialist page.

   The marks are drawn here rather than being the vendors' own
   logos. Two reasons: three foreign brand marks in three different
   visual languages would read as a sponsor wall on a warm page,
   and a repair page carrying vendor logos implies an authorisation
   that never existed. These say what the silicon looks like.
   ============================================================ */
.plat { display: grid; gap: 20px; margin-top: 26px; }
.plat-row {
  display: grid; grid-template-columns: 196px minmax(0, 1fr); gap: 26px;
  align-items: start; padding: 22px 24px;
  border: 1px solid var(--line); border-radius: var(--r);
  background: color-mix(in srgb, var(--surface) 92%, transparent);
  transition: border-color var(--t);
}
.plat-row:hover { border-color: var(--acc-line); }
.plat-mark svg { display: block; width: 100%; height: auto; border-radius: var(--r-sm); }
.plat-mark figcaption {
  margin-top: 9px; font-family: var(--mono); font-size: .66rem;
  color: var(--tx-3); line-height: 1.6;
}
.plat-body h3 { font-size: 1.06rem; letter-spacing: -.02em; }
.plat-body > p { margin-top: 10px; color: var(--tx-2); font-size: .93rem; line-height: 1.74; }
.plat-body .p-list { margin-top: 14px; }
.plat-body .p-list li { font-size: .9rem; }

@media (max-width: 700px) {
  .plat-row { grid-template-columns: 1fr; gap: 18px; }
  .plat-mark { max-width: 196px; }
}

/* Personal projects carry my own mark rather than an organisation's. Drawn,
   not photographic, so it takes the accent from the theme like the nav does. */
.p-logo.is-mark {
  background: var(--surface); border-color: var(--line-strong);
  padding: 0; width: clamp(52px, 6vw, 62px); height: clamp(52px, 6vw, 62px);
  color: var(--acc);
}
.p-logo.is-mark svg { width: 46%; height: 46%; }

/* Some marks ship with their own ground (a dark tile, a gradient card).
   Those get no chip - a white box behind them would frame a frame. */
.p-logo.is-bleed {
  background: none; border: 0; padding: 0; box-shadow: none;
}
.p-logo.is-bleed img {
  height: clamp(54px, 7vw, 74px);
  border-radius: var(--r);
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-md);
}

/* Vendor brand marks beside each platform heading. Monochrome and inheriting
   currentColor, so they read as labels rather than as endorsements - the
   no-affiliation line under this section still applies. */
.plat-h { display: flex; align-items: center; gap: 10px; }
.plat-brand { width: 20px; height: 20px; flex: none; color: var(--acc); }

/* Cursor wash - matches the main page. The card must be a positioning context
   and clip the radial to its own corners. */
.p-card, .plat-row, .p-cover, .p-cert, .p-note, .p-facts { position: relative; }
@media (hover: hover) and (pointer: fine) {
  .p-card::after, .plat-row::after, .p-cover::after,
  .p-cert::after, .p-note::after, .p-facts::after {
    content: ""; position: absolute; inset: 0; border-radius: inherit;
    pointer-events: none; z-index: 2;
    background: radial-gradient(380px circle at var(--mx, 50%) var(--my, 0%), var(--acc-soft), transparent 60%);
    opacity: 0; transition: opacity .4s var(--ease);
  }
  .p-card:hover::after, .plat-row:hover::after, .p-cover:hover::after,
  .p-cert:hover::after, .p-note:hover::after, .p-facts:hover::after { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .p-card::after, .plat-row::after, .p-cover::after,
  .p-cert::after, .p-note::after, .p-facts::after { display: none; }
}

/* Marks a project as mine rather than an organisation's. */
.p-own {
  margin-top: 10px;
  font-family: var(--mono); font-size: .68rem; letter-spacing: .06em;
  text-transform: uppercase; color: var(--acc);
}

/* Real board photography replaces the drawn package marks. These are stock
   product shots illustrating the platform, not photographs of boards I
   worked on - the captions say what each one is, and the no-affiliation
   line under the section still applies. */
.plat-shot { margin: 0; }
.plat-shot img {
  display: block; width: 100%; height: auto;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-strong);
}
.plat-shot figcaption {
  margin-top: 9px;
  font-family: var(--mono); font-size: .66rem; line-height: 1.6;
  color: var(--tx-3);
}
