/* ==========================================================================
   Luis Xavier García Pimentel Ascencio — portfolio
   Design system: TEST-BENCH INSTRUMENT PANEL

   The project board is a bank of instrument channels. Each channel module
   reads its color from the --ch custom property the HTML sets inline, so a
   single rule set themes all six (address, reading, trace, border, dot,
   focus ring). Everything outside the board stays quiet.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Surfaces — blue-charcoal instrument background, never pure black */
  --bg:      #0a0e14;
  --panel:   #111721;
  --panel-2: #161d29;
  --rule:    #1f2937;

  /* Text.
     --dim (4.25:1 on --bg) FAILS AA for normal-size text, so it is used only
     for non-text decoration: the panel grid texture and the scope graticule.
     --dim-strong (5.37–6.14:1 on all three surfaces) carries every piece of
     small secondary text. */
  --text:       #d7dee8;
  --dim:        #6b7787;
  --dim-strong: #8792a3;

  /* Channel colors — one per project, meaningful assignment.
     All nine pass AA on --bg / --panel / --panel-2. */
  --ch-amber:  #ffb000; /* JTCS           — count / demand      */
  --ch-green:  #46e88a; /* NeuroBeat      — bio-signal          */
  --ch-cyan:   #38bdf8; /* TelemetryStack — live telemetry      */
  --ch-violet: #b48ead; /* Robot ROS2     — navigation          */
  --ch-coral:  #ff8f6b; /* Mapa TEC       — routing             */
  --ch-lime:   #c3e88d; /* MadRams        — teaching/onboarding */
  --ch-emerald: #14b8a6; /* AgroBot       — greenhouse harvesting */
  --ch-magenta: #e879f9; /* Loopzels      — cognitive puzzles     */
  --ch-red:     #ff3d6e; /* Malaria       — field / physics       */
  /* Each sub-page holds one bank of channels, so distinctness only has to
     hold *within* a page. The picks below still clear every existing hue by
     eye on --bg, and no two colors ever share a board. */
  --ch-coche:   #f97316; /* Coche MadRams — Baja rust-orange (vs. lime)  */
  --ch-quantum: #a855f7; /* Quantum       — high-voltage electric purple */
  --ch-elyos:   #3b82f6; /* Silca Elyos   — award steel-blue             */
  --ch-teal:    #5eead4; /* LaTeX Studio  — typeset paper-mint           */
  --ch-indigo:  #818cf8; /* Frenado Magn. — field-line periwinkle        */
  --ch-orchid:  #c084fc; /* LLM / Orquest.— agent-orchestration violet   */

  /* Default channel for anything outside a channel module (nav, hero,
     footer). Amber is the master channel. Every accent, focus ring and
     hover state in the sheet resolves through var(--ch). */
  --ch: var(--ch-amber);

  /* Type */
  --font-mono: "Space Mono", ui-monospace, SFMono-Regular, "JetBrains Mono", monospace;
  --font-sans: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;

  --t-2xs:  0.625rem;   /* 10px — reading kicker            */
  --t-xs:   0.6875rem;  /* 11px — badges, pills             */
  --t-sm:   0.75rem;    /* 12px — eyebrows, units, captions */
  --t-md:   0.9375rem;  /* 15px — blurbs                    */
  --t-base: 1rem;       /* 16px — body                      */
  --t-lg:   1.0625rem;  /* 17px — bio, hero subline         */
  --t-xl:   1.125rem;   /* 18px — channel names             */
  --t-2xl:  1.5rem;     /* 24px — section headings          */
  --t-read: 2.5rem;     /* 40px — the reading value         */
  --t-hero: clamp(1.85rem, 6.2vw, 3.35rem);

  --track-wide:  0.18em;  /* tracked-out uppercase mono */
  --track-tight: -0.02em;

  /* Space */
  --s1: 0.25rem;   /*  4 */
  --s2: 0.5rem;    /*  8 */
  --s3: 0.75rem;   /* 12 */
  --s4: 1rem;      /* 16 */
  --s5: 1.5rem;    /* 24 */
  --s6: 2rem;      /* 32 */
  --s7: 3rem;      /* 48 */
  --s8: 4rem;      /* 64 */

  /* Radius — squared-off, instrument hardware, not soft cards */
  --r-sm: 3px;
  --r-md: 6px;
  --r-lg: 10px;

  /* Layout */
  --measure: 1200px;
  --pad-x: clamp(16px, 4vw, 40px); /* 16px at 360 keeps the 320px-min
                                      Calendly widget inside the viewport */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: 7rem;
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--t-base);
  line-height: 1.6;
  letter-spacing: 0.005em;
  overflow-wrap: break-word;
}

/* Panel substrate — a dot lattice, deliberately NOT a grid: the trace screens
   own the crosshatch, so repeating it page-wide spent that cue everywhere.
   Fixed, so it reads as a surface the page sits on rather than something that
   scrolls with it. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* 22% put the lattice at 1.24:1 against the page — technically present,
     practically invisible, and the landing's bank rows have enough open field
     that the texture has to actually carry it. 42% lands near 1.8:1: readable
     as a surface, still quiet enough to sit under type. */
  background-image: radial-gradient(
    color-mix(in srgb, var(--dim) 42%, transparent) 1.15px,
    transparent 1.25px);
  background-size: 28px 28px;
  background-position: center top;
}

/* The single ambient wash, in the hero channel's own amber. It lives on body,
   not on .hero, because .hero is capped at --measure: a layer inside that box
   gets clipped at the content column and the light ends on a vertical edge
   mid-screen. Here it spans the viewport, its origin sits off-canvas to the
   left so there is no visible start, and it reaches full transparency at 72%
   of a box far taller than it needs, so it never ends on an edge either.
   One wash, not scattered glow — the board is where colour is spent. */
body::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1200px;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(115% 78% at -8% 16%,
    color-mix(in srgb, var(--ch-amber) 7%, transparent) 0,
    color-mix(in srgb, var(--ch-amber) 3%, transparent) 34%,
    transparent 72%);
}

h1, h2, h3, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }

a { color: inherit; }

svg { display: block; }

::selection {
  background: color-mix(in srgb, var(--ch) 30%, transparent);
  color: var(--text);
}

/* One focus treatment, channel-aware: inside a module it takes that
   module's color, elsewhere it falls back to the master channel. */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--ch);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

/* --------------------------------------------------------------------------
   3. Skip link
   -------------------------------------------------------------------------- */

.skip-link {
  position: absolute;
  left: var(--s4);
  top: var(--s2);
  z-index: 100;
  transform: translateY(-160%);
  padding: var(--s2) var(--s4);
  background: var(--ch);
  color: var(--bg);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--r-sm);
  transition: transform 0.18s var(--ease);
}

.skip-link:focus-visible { transform: translateY(0); }

/* --------------------------------------------------------------------------
   4. Header / nav — quiet
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--bg); /* opaque fallback */
  border-bottom: 1px solid var(--rule);
}

@supports ((backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px))) {
  .site-header {
    background: color-mix(in srgb, var(--bg) 76%, transparent);
    -webkit-backdrop-filter: blur(12px) saturate(1.3);
    backdrop-filter: blur(12px) saturate(1.3);
  }
}

.nav {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--s3) var(--pad-x);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s4);
}

.brand {
  margin-right: auto;
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  text-decoration: none;
  min-width: 0;
}

.brand__mark {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 1.0625rem;
  letter-spacing: 0.02em;
  color: var(--text);
}

.brand:hover .brand__mark { color: var(--ch); }

/* Name and discipline read as one subtitle line, joined by the separator below.
   They are two elements only because the subtitle's textContent belongs to the
   i18n pass — a name folded into that span gets overwritten on load. */
.brand__name,
.brand__subtitle {
  display: none;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.06em;
  color: var(--dim-strong);
  white-space: nowrap;
}

.brand__subtitle::before {
  content: "·";
  margin-right: var(--s3);
  color: var(--dim);
}

.nav__links {
  order: 3;
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
  padding-top: var(--s3);
  border-top: 1px solid var(--rule);
}

.nav__link {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--dim-strong);
  padding-block: 2px;
  border-bottom: 1px solid transparent;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.nav__link:hover {
  color: var(--text);
  border-bottom-color: var(--ch);
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.lang-toggle {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--dim-strong);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 0.45rem 0.6rem;
  min-height: 34px;
  cursor: pointer;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.lang-toggle:hover {
  color: var(--text);
  border-color: color-mix(in srgb, var(--ch) 45%, var(--rule));
}

@media (min-width: 760px) {
  .nav__links {
    order: 0;
    flex-basis: auto;
    gap: var(--s5);
    padding-top: 0;
    border-top: 0;
    margin-inline-end: var(--s2);
  }
}

@media (min-width: 700px) {
  .brand__name { display: inline; }
}

/* The subtitle only joins once the name, the nav and the CTA all fit without
   crowding — it is the least load-bearing thing in the bar. */
@media (min-width: 1200px) {
  .brand__subtitle { display: inline; }
}

/* --------------------------------------------------------------------------
   5. Buttons — instrument switch labels
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 40px;
  padding: 0.55rem 1.05rem;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: background-color 0.18s var(--ease), border-color 0.18s var(--ease),
              color 0.18s var(--ease);
}

.btn--primary {
  background: var(--ch);
  color: var(--bg);
  border-color: var(--ch);
}

.btn--primary:hover {
  background: color-mix(in srgb, var(--ch) 82%, #ffffff);
  border-color: color-mix(in srgb, var(--ch) 82%, #ffffff);
}

.btn--secondary {
  background: transparent;
  color: var(--text);
  border-color: var(--rule);
}

.btn--secondary:hover {
  color: var(--ch);
  border-color: color-mix(in srgb, var(--ch) 50%, var(--rule));
}

.btn--small {
  min-height: 34px;
  padding: 0.35rem 0.75rem;
  letter-spacing: 0.08em;
}

/* Wayfinding, not decoration: a primary control whose whole job is to jump to
   the booking panel carries that panel's channel color (§13 themes .section--book
   with --ch-cyan), so the color names the destination. This is why the first
   viewport is not single-accent — the second color is doing a job. */
.btn--primary[href="#book"] { --ch: var(--ch-cyan); }

/* In-module link buttons: Demo carries the channel color, Code stays quiet. */
.btn--demo {
  color: var(--ch);
  background: color-mix(in srgb, var(--ch) 12%, transparent);
  border-color: color-mix(in srgb, var(--ch) 42%, var(--rule));
}

.btn--demo:hover {
  background: color-mix(in srgb, var(--ch) 20%, transparent);
  border-color: var(--ch);
}

.btn--code {
  color: var(--dim-strong);
  background: transparent;
  border-color: var(--rule);
}

.btn--code:hover {
  color: var(--text);
  border-color: color-mix(in srgb, var(--ch) 40%, var(--rule));
}

/* --------------------------------------------------------------------------
   6. Hero — the thesis: raw signal resolving into a decision
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  max-width: var(--measure);
  margin-inline: auto;
  padding: clamp(32px, 6vw, 56px) var(--pad-x) clamp(40px, 6vw, 72px);
}

.hero__content { position: relative; }

/* The master scope, with its axis captions. The captions name the two halves
   of the trace — raw input on the left, resolved decision on the right — so
   the hero encodes the thesis instead of illustrating it. */
.hero__scope {
  position: relative;
  width: 100%;
  max-width: 560px;
}

.hero__axis {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  margin-top: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--dim-strong);
}

.hero__axis-out { text-align: right; }

.hero__heading {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-hero);
  line-height: 1.08;
  letter-spacing: -0.03em;
  color: var(--text);
  max-width: 17ch;
  margin-top: clamp(24px, 5vw, 40px);
}

.hero__subline {
  margin-top: var(--s5);
  max-width: 58ch;
  font-size: var(--t-lg);
  line-height: 1.65;
  color: var(--dim-strong);
}

.hero__actions {
  margin-top: var(--s6);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
}

.social {
  margin-top: var(--s7);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
}

.social__link {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--dim-strong);
  border-bottom: 1px solid transparent;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.social__link:hover {
  color: var(--ch);
  border-bottom-color: color-mix(in srgb, var(--ch) 60%, transparent);
}

/* Desktop: the master scope sits alongside the thesis, not above it. */
@media (min-width: 940px) {
  .hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.92fr);
    align-items: center;
    column-gap: var(--s8);
    padding-block: clamp(56px, 7vw, 96px);
  }

  .hero__scope {
    grid-column: 2;
    grid-row: 1;
    max-width: none;
  }

  .hero__content {
    grid-column: 1;
    grid-row: 1;
  }

  .hero__heading { margin-top: 0; }
}

/* --------------------------------------------------------------------------
   7. Section chrome
   -------------------------------------------------------------------------- */

.section {
  max-width: var(--measure);
  margin-inline: auto;
  padding: clamp(48px, 8vw, 88px) var(--pad-x);
  border-top: 1px solid var(--rule);
}

/* Panel header bar: label, then a hairline running to the panel edge. */
.section__eyebrow,
.section__heading {
  display: flex;
  align-items: center;
  gap: var(--s4);
  font-family: var(--font-mono);
  font-weight: 700;
}

.section__eyebrow::after,
.section__heading::after {
  content: "";
  flex: 1 1 0;
  height: 1px;
  background: var(--rule);
}

.section__eyebrow {
  gap: var(--s3);
  font-size: var(--t-sm);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim-strong);
}

/* the board's panel header carries a live channel marker */
.section__eyebrow::before {
  content: "";
  flex: 0 0 auto;
  width: 3px;
  height: 14px;
  background: var(--ch);
  border-radius: 1px;
}

.section__heading {
  font-size: var(--t-2xl);
  letter-spacing: -0.01em;
  color: var(--text);
}

/* --------------------------------------------------------------------------
   7b. Sub-page masthead — the landing is the whole instrument board, each
       sub-page is one channel bank zoomed in. The masthead says which bank
       you are looking at and how to get back, and takes the bank's own --ch
       (set on the <section>) so the route is colour-coded end to end.
   -------------------------------------------------------------------------- */

.section--sub { border-top: 0; }

.backlink {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 40px;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--dim-strong);
  transition: color 0.18s var(--ease);
}

.backlink::before {
  content: "←";
  display: inline-block;
  transition: transform 0.18s var(--ease);
}

.backlink:hover { color: var(--ch); }
.backlink:hover::before { transform: translateX(-4px); }

.subhead__title {
  margin-top: var(--s4);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(1.6rem, 5vw, 2.4rem);
  letter-spacing: var(--track-tight);
  color: var(--text);
}

/* the channel key bar again, this time keying a whole page */
.subhead__title::before {
  content: "";
  display: block;
  width: 72px;
  height: 3px;
  margin-bottom: var(--s4);
  background: var(--ch);
  border-radius: 2px;
}

.subhead__line {
  margin-top: var(--s4);
  max-width: 62ch;
  font-size: var(--t-lg);
  line-height: 1.65;
  color: var(--dim-strong);
}

.subhead__count {
  margin-top: var(--s5);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ch);
}

/* Foot of every sub-page: the other four banks, so a visitor can cross to a
   sibling category without going back through the landing. Each link carries
   its destination's own --ch, so the hover colour is the colour you land in. */
.siblings {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3) var(--s5);
}

.siblings__label {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim);
}

/* --------------------------------------------------------------------------
   8. The reading board — the loud part
   -------------------------------------------------------------------------- */

.board {
  margin-top: var(--s6);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s5);
  /* Each module is only as tall as its own content. The default `stretch`
     looks tidier at rest, but the moment one module's panel expands its
     row-mates stretch with it and grow a dead gap the height of the panel. */
  align-items: start;
}

@media (min-width: 700px) {
  .board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
  .board { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Group headings split a bank's board into named sub-groups (LLM/Orquestación
   runs "Orquestación propia" then "Capacidades / skills"). Numbering runs
   continuously across groups, so the addresses read in one sequence even as the
   heading breaks the grid. Only pages that carry sub-groups emit these; a
   single-group bank draws one .board with no heading, exactly as before. */
.board__group {
  margin-top: var(--s7);
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim-strong);
}

.board__group::before {
  content: "";
  width: 24px;
  height: 3px;
  border-radius: 2px;
  background: var(--ch);
}

.section__eyebrow + .board__group { margin-top: var(--s6); }
.board__group + .board { margin-top: var(--s4); }

/* The Percibir/Procesar/Comunicar row eyebrows went away with the single-grid
   layout: the landing now shows one module per category (each tagged on its
   own address line) and every sub-page holds exactly one category, so a label
   spanning a row no longer has a row to speak for. */

/* One rule set, six channels: everything below resolves through var(--ch),
   which each <article> sets inline. */
.channel {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--s5);
  background-color: var(--panel);
  /* a whisper of the channel color down from the key bar */
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--ch) 5%, transparent), transparent 150px);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}

/* channel key: a short bar of the channel color painted on the top edge */
.channel::before {
  content: "";
  position: absolute;
  top: 0;
  left: 18px;
  width: 72px;
  height: 3px;
  background: var(--ch);
  border-radius: 0 0 2px 2px;
}

.channel:hover {
  border-color: color-mix(in srgb, var(--ch) 34%, var(--rule));
  box-shadow: 0 20px 48px -34px color-mix(in srgb, var(--ch) 85%, transparent);
}

/* Module rhythm — every child is spaced explicitly at the same specificity
   (0,2,0) so nothing later in the sheet silently loses. */
.channel > * { margin-top: var(--s3); }
.channel > :first-child { margin-top: 0; }

.channel__addr {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-sm);
  letter-spacing: var(--track-wide);
  color: var(--ch);
}

.channel__addr::after {
  content: "";
  flex: 1 1 0;
  height: 1px;
  background: color-mix(in srgb, var(--ch) 22%, var(--rule));
}

/* Award ribbon — one module carries it (Silca Elyos), so it is a modifier on
   the module rather than a channel of its own. */
.channel__award {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--ch);
  background: color-mix(in srgb, var(--ch) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--ch) 34%, var(--rule));
  border-radius: var(--r-sm);
}

.channel__award::before {
  content: "🏆";
  font-size: var(--t-sm);
  line-height: 1;
}

.channel > .channel__name {
  margin-top: var(--s3);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-xl);
  line-height: 1.3;
  /* two lines reserved: the long names wrap to two at every breakpoint, so
     holding the space keeps every readout well — and therefore every trace
     screen — on the same line across a row */
  min-height: 2.6em;
  letter-spacing: -0.015em;
  color: var(--text);
}

/* The readout well: reading + trace form one continuous inset screen.
   One row each for kicker / value / unit, and each row is exactly one line
   tall, so all six wells are the same height at every breakpoint and in both
   languages — which is what makes the trace screens line up across a row.
   (Under flex-wrap the unit used to share the value's row and drop to a third
   line only on the longer strings, so CH.03 sat ~25px lower than CH.01.) */
.channel > .channel__reading {
  margin-top: var(--s4);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
  padding: var(--s3) var(--s4) var(--s2);
  background: var(--panel-2);
  border: 1px solid var(--rule);
  border-bottom: 0;
  border-radius: var(--r-md) var(--r-md) 0 0;
}

/* Values and units are short by design; nowrap keeps the well's height
   deterministic. The unit also ellipsises (it is the string that changes
   length between ES and EN); the value must not clip, since Space Mono's
   ascent+descent at 40px overflows its own 1.15 line box. */
.reading__value,
.reading__unit {
  min-width: 0;
  white-space: nowrap;
}

.reading__unit {
  overflow: hidden;
  text-overflow: ellipsis;
}

.reading__label {
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--dim-strong);
}

.reading__value {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-read);
  line-height: 1.15;
  letter-spacing: var(--track-tight);
  font-variant-numeric: tabular-nums;
  color: var(--ch);
}

.reading__unit {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.04em;
  color: var(--dim-strong);
}

/* --------------------------------------------------------------------------
   9. Traces — scope screens + the draw
   -------------------------------------------------------------------------- */

.trace {
  width: 100%;
  height: auto;
  overflow: visible;
  background-color: var(--panel-2);
  /* graticule: --dim used as non-text decoration only */
  background-image:
    repeating-linear-gradient(to right,
      color-mix(in srgb, var(--dim) 16%, transparent) 0 1px, transparent 1px 24px),
    repeating-linear-gradient(to bottom,
      color-mix(in srgb, var(--dim) 16%, transparent) 0 1px, transparent 1px 12px);
  border: 1px solid var(--rule);
}

.channel > .trace {
  margin-top: 0;
  border-top: 0;
  border-radius: 0 0 var(--r-md) var(--r-md);
}

.hero__trace {
  width: 100%;
  /* width is capped by .hero__scope so the axis captions match the screen */
  border-radius: var(--r-lg);
  background-image:
    repeating-linear-gradient(to right,
      color-mix(in srgb, var(--dim) 14%, transparent) 0 1px, transparent 1px 40px),
    repeating-linear-gradient(to bottom,
      color-mix(in srgb, var(--dim) 14%, transparent) 0 1px, transparent 1px 20px);
}

.trace path {
  fill: none;
  stroke: var(--ch);
  /* User-unit stroke width, not screen px: these paths carry pathLength="1"
     and draw via stroke-dasharray/-dashoffset, which Chromium resolves in
     user-space. vector-effect:non-scaling-stroke would resolve the dash in
     screen units instead and desync it from pathLength, clipping the trace.
     Sized so the rendered stroke lands near 2px at each SVG's viewBox scale:
     card traces (viewBox 0 0 120 36, render ~250-340px wide, scale ~2.1-2.8x). */
  stroke-width: 0.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Marker dots/squares on waypoint and route traces (Nav2 waypoints, the
   mapatec origin/destination) — filled in the channel color, no stroke. */
.trace circle,
.trace rect {
  fill: var(--ch);
  stroke: none;
}

.hero__trace path {
  /* hero viewBox is 0 0 400 120, rendered ~328-560px wide across breakpoints
     (scale ~0.8-1.4x) — a much larger user-space unit than the card traces,
     so it needs a proportionally larger stroke-width to still read as ~2px. */
  stroke-width: 1.8;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--ch) 32%, transparent));
}

/* Draw once on load, then hold. Paths carry pathLength="1"; the CSS
   animation's forwards fill holds the fully-drawn state afterward — no
   JS involved. */
.trace path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: trace-draw 1.1s var(--ease) 0.15s forwards;
}

.hero__trace path { animation-duration: 1.6s; }

/* Same idea on the landing, where each module is alone inside its own bank row:
   the stagger has to count banks, not sibling modules. */
.banks .bank:nth-of-type(2) .trace path { animation-delay: 0.25s; }
.banks .bank:nth-of-type(3) .trace path { animation-delay: 0.35s; }
.banks .bank:nth-of-type(4) .trace path { animation-delay: 0.45s; }
.banks .bank:nth-of-type(5) .trace path { animation-delay: 0.55s; }

/* stagger the board so it reads as channels coming online in order.
   nth-of-type (not nth-child) counts only sibling <article> channels, so the
   row-label <p> elements interleaved between rows don't shift the count. */
.board .channel:nth-of-type(2) .trace path { animation-delay: 0.25s; }
.board .channel:nth-of-type(3) .trace path { animation-delay: 0.35s; }
.board .channel:nth-of-type(4) .trace path { animation-delay: 0.45s; }
.board .channel:nth-of-type(5) .trace path { animation-delay: 0.55s; }
.board .channel:nth-of-type(6) .trace path { animation-delay: 0.65s; }
.board .channel:nth-of-type(7) .trace path { animation-delay: 0.75s; }
.board .channel:nth-of-type(8) .trace path { animation-delay: 0.85s; }
.board .channel:nth-of-type(9) .trace path { animation-delay: 0.95s; }

@keyframes trace-draw {
  to { stroke-dashoffset: 0; }
}

/* Decision markers fade/scale in once their own trace finishes drawing,
   rather than sitting on screen from frame 0 waiting for the line to catch
   up to them. Delay = that trace's path delay + duration, so the marker
   lands right as the line resolves into it. */
.trace circle,
.trace rect {
  transform-box: fill-box;
  transform-origin: center;
  opacity: 0;
  animation: marker-in 0.35s var(--ease) 1.25s forwards;
}

@keyframes marker-in {
  from { opacity: 0; transform: scale(0.4); }
  to { opacity: 1; transform: scale(1); }
}

.hero__trace circle { animation-delay: 1.75s; }

.banks .bank:nth-of-type(2) .trace circle,
.banks .bank:nth-of-type(2) .trace rect { animation-delay: 1.35s; }
.banks .bank:nth-of-type(3) .trace circle,
.banks .bank:nth-of-type(3) .trace rect { animation-delay: 1.45s; }
.banks .bank:nth-of-type(4) .trace circle,
.banks .bank:nth-of-type(4) .trace rect { animation-delay: 1.55s; }
.banks .bank:nth-of-type(5) .trace circle,
.banks .bank:nth-of-type(5) .trace rect { animation-delay: 1.65s; }

.board .channel:nth-of-type(2) .trace circle,
.board .channel:nth-of-type(2) .trace rect { animation-delay: 1.35s; }
.board .channel:nth-of-type(3) .trace circle,
.board .channel:nth-of-type(3) .trace rect { animation-delay: 1.45s; }
.board .channel:nth-of-type(4) .trace circle,
.board .channel:nth-of-type(4) .trace rect { animation-delay: 1.55s; }
.board .channel:nth-of-type(5) .trace circle,
.board .channel:nth-of-type(5) .trace rect { animation-delay: 1.65s; }
.board .channel:nth-of-type(6) .trace circle,
.board .channel:nth-of-type(6) .trace rect { animation-delay: 1.75s; }
.board .channel:nth-of-type(7) .trace circle,
.board .channel:nth-of-type(7) .trace rect { animation-delay: 1.85s; }
.board .channel:nth-of-type(8) .trace circle,
.board .channel:nth-of-type(8) .trace rect { animation-delay: 1.95s; }
.board .channel:nth-of-type(9) .trace circle,
.board .channel:nth-of-type(9) .trace rect { animation-delay: 2.05s; }

/* the only hover motion on the board: a 1–2px drift */
.channel > .trace {
  transition: transform 0.3s var(--ease);
}

.channel:hover > .trace { transform: translateY(-2px); }

/* --------------------------------------------------------------------------
   10. Board footer link
   -------------------------------------------------------------------------- */

.board__more { margin-top: var(--s6); }

.link {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--dim-strong);
  border-bottom: 1px solid var(--rule);
  padding-bottom: 2px;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.link:hover {
  color: var(--ch);
  border-bottom-color: var(--ch);
}

/* --------------------------------------------------------------------------
   11. About — the calm counterweight
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   11a. The lap — Nordschleife behind the narrative

   The bio is set to a 68ch measure inside a 1200px section, so on wide screens
   there is a column of open field to its right doing nothing. A slow lap goes
   there: the track draws itself once when the section arrives, then a single
   car runs it for as long as you stay. It is the same idea as the course site's
   Monaco background, rebuilt in vanilla — the portfolio ships no libraries and
   its CSP admits no third-party script, so the technique is reused, not the code.

   Below 1100px it is not shown at all: there is no open field left to fill, and
   a 5.5KB path animating behind body copy on a phone is all cost, no benefit.
   -------------------------------------------------------------------------- */

.about__circuit { display: none; }

@media (min-width: 1100px) {
  /* Two columns for the length of the narrative: the story on the left, the lap
     on the right. The heading and the skills legend still span the full width —
     only the reading run is paired with the track. */
  /* The lap column is a fixed 400px rather than a percentage: the svg's height
     then follows from its own aspect ratio (272.7 / 220.1 → 323px), which is
     what makes the exact centring in `top` below possible. */
  .section--about {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 400px;
    column-gap: var(--s7);
  }

  .section--about > .section__heading { grid-column: 1 / -1; grid-row: 1; }
  .about__narrative { grid-column: 1; grid-row: 2; }
  .section--about > .teams { grid-column: 1 / -1; grid-row: 3; }
  .section--about > .skills { grid-column: 1 / -1; grid-row: 4; }

  /* The wrapper is a stretched grid item spanning the whole narrative, and the
     svg sticks inside it — so the lap rides down beside the text the entire
     read and only leaves when the story does. Offset clears the sticky header. */
  .about__circuit {
    --ch: var(--ch-coche);
    display: block;
    grid-column: 2;
    grid-row: 2;
    pointer-events: none;
  }

  /* Centred in the viewport, not pinned near the top: pinned, the track sat
     high on the way down and low on the way back up, so the two directions did
     not read the same. 50vh less half the svg's own height puts it on the
     centre line whichever way you scroll.

     Centring via a 100vh flex box would be height-agnostic and tempting, but a
     100vh child sets a 100vh floor on this grid row — on a tall viewport that
     pushed the teams and skills blocks below the fold. This costs one magic
     number and touches nothing else's layout. */
  .about__circuit svg {
    position: sticky;
    top: calc(50vh - 161px); /* half of 400 / (272.7 / 220.1) */
    display: block;
    width: 100%;
  }
}

.circuit__track {
  fill: none;
  stroke: var(--ch);
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
  opacity: 0.62;
  filter: drop-shadow(0 0 6px color-mix(in srgb, var(--ch) 45%, transparent));
}

/* Same progressive-enhancement contract as the bank strips: undrawn only once
   script is there to draw it. */
.js .circuit__track {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}

/* A long, deliberate draw — 20.8 km of track should not snap into place. */
.about__circuit.is-onscreen .circuit__track {
  animation: trace-draw 4.5s var(--ease) forwards;
}

.circuit__car {
  fill: var(--ch);
  filter: drop-shadow(0 0 7px var(--ch));
}

/* Names the shape, inside the loop. Sized in user units, so it scales with the
   svg instead of drifting off the track at other widths. */
.circuit__label,
.circuit__sublabel {
  font-family: var(--font-mono);
  text-transform: uppercase;
  fill: var(--ch);
}

.circuit__label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  opacity: 0.8;
}

.circuit__sublabel {
  font-size: 7.5px;
  letter-spacing: 0.24em;
  opacity: 0.5;
}

.about__bio {
  margin-top: var(--s5);
  max-width: 68ch;
  font-size: var(--t-lg);
  line-height: 1.72;
  color: var(--text);
}

/* The bio is a story now, not a paragraph. One measure for the whole run so
   the eye keeps its line, a lede that picks the hero's sentence back up, and
   a closer that lands it. */
.about__narrative {
  margin-top: var(--s5);
  max-width: 68ch;
}

.about__narrative p {
  font-size: var(--t-lg);
  line-height: 1.72;
  color: var(--text);
}

.about__narrative p + p { margin-top: var(--s5); }

.about__narrative p:first-child {
  font-size: clamp(1.15rem, 3.6vw, 1.4rem);
  line-height: 1.5;
  letter-spacing: var(--track-tight);
}

.about__narrative p:last-child {
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
  font-weight: 500;
}

.skills {
  margin-top: var(--s7);
  display: grid;
  gap: var(--s4);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
}

/* Teams and skills share the same row grammar — dot, label, tags — which made
   them read as one thirteen-row list. Teams gets a panel of its own so the eye
   sees two objects: where the work happens, then what it is done with. */
.teams {
  margin-top: var(--s7);
  display: grid;
  gap: var(--s4);
  padding: var(--s5);
  background: color-mix(in srgb, var(--panel) 55%, transparent);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
}

.teams + .skills {
  margin-top: var(--s8);
}

.skills__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim-strong);
}

/* Says what the glow means, next to a tag that is actually glowing — the
   sentence alone left people to guess, and a legend nobody reads first is
   just decoration. Sits above the groups for that reason. */
.tier-note {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s2);
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--dim-strong);
}

.skills .tier-note {
  margin-top: var(--s2);
  padding-bottom: var(--s3);
}

.teams .tier-note { margin-top: var(--s2); }

.skills__group {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s2) var(--s3);
}

.skills__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ch);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ch) 16%, transparent);
}

.skills__group-name {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim-strong);
}

.pills,
.badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

.pills { grid-column: 1 / -1; }

.pill,
.badge {
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.04em;
  color: var(--dim-strong);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  padding: 0.25rem 0.5rem;
}

.badge { background: var(--panel-2); }

/* --------------------------------------------------------------------------
   Expert tier — a tag that runs hot

   Two tiers only: normal, and this. The glow is the group's own channel colour
   emitted softly, not a neon outline, so it reads as a hot channel on the bench
   rather than a highlight pen. Kept to a small set on purpose: glow everything
   and it stops meaning anything.
   -------------------------------------------------------------------------- */
.pill--expert {
  color: var(--text);
  font-weight: 600;
  border-color: color-mix(in srgb, var(--ch) 60%, var(--rule));
  background: color-mix(in srgb, var(--ch) 8%, var(--panel));
  box-shadow: 0 0 6px color-mix(in srgb, var(--ch) 45%, transparent),
              0 0 1px color-mix(in srgb, var(--ch) 80%, transparent);
}

/* Very slow breathing so the set feels live without pulling the eye off the
   text. The global reduced-motion block kills this and leaves the static glow. */
@media (prefers-reduced-motion: no-preference) {
  .pill--expert { animation: skill-pulse 4s ease-in-out infinite; }
}

@keyframes skill-pulse {
  0%, 100% { box-shadow: 0 0 5px color-mix(in srgb, var(--ch) 38%, transparent),
                         0 0 1px color-mix(in srgb, var(--ch) 70%, transparent); }
  50%      { box-shadow: 0 0 9px color-mix(in srgb, var(--ch) 62%, transparent),
                         0 0 1px color-mix(in srgb, var(--ch) 85%, transparent); }
}

/* Without this the glow is decoration nobody can read. */
.skills__caption {
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--rule);
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--dim-strong);
}

/* A group that is a direction, not a delivery. Dashed everywhere and tagged in
   words as well as in style, so nothing here can be read as shipped work. */
.skills__status {
  margin-left: var(--s2);
  padding: 0.1rem 0.4rem;
  font-size: var(--t-2xs);
  letter-spacing: 0.1em;
  color: var(--ch);
  border: 1px dashed color-mix(in srgb, var(--ch) 45%, var(--rule));
  border-radius: var(--r-sm);
  white-space: nowrap;
}

.skills__group--planned .skills__dot {
  background: transparent;
  border: 1px dashed var(--ch);
  box-shadow: none;
}

.skills__group--planned .pill {
  border-style: dashed;
  color: var(--dim);
}

@media (min-width: 768px) {
  .skills__group {
    grid-template-columns: auto 13rem minmax(0, 1fr);
    gap: var(--s3);
  }
  .pills { grid-column: auto; }
}

/* --------------------------------------------------------------------------
   Skill legend cross-links — a pill that maps to shipped work is clickable,
   opening a popover of the project(s) that use it. The affordance is quiet:
   the group's channel colour on hover plus a small chevron, so a linked pill
   reads as "there's more here" without shouting over the inert ones.
   -------------------------------------------------------------------------- */
.pill--linked {
  cursor: pointer;
  position: relative;
  padding-right: 1.2rem;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.pill--linked::after {
  content: "\203A"; /* › */
  position: absolute;
  right: 0.45rem;
  top: 50%;
  transform: translateY(-50%);
  font-weight: 700;
  color: color-mix(in srgb, var(--ch) 70%, var(--dim-strong));
  opacity: 0.75;
}

.pill--linked:hover,
.pill--linked:focus-visible,
.pill--linked[aria-expanded="true"] {
  color: var(--text);
  border-color: color-mix(in srgb, var(--ch) 55%, var(--rule));
  background: color-mix(in srgb, var(--ch) 12%, var(--panel));
}

.pill--linked:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--ch) 60%, transparent);
  outline-offset: 2px;
}

/* The popover reads as a patch-panel readout, not a Material menu: sharp-ish
   instrument panel, a channel-coloured top rail, a header bar identical to the
   board's own (.section__eyebrow — tick, mono label, hairline to the edge), and
   a probe caret pointing back up at the pill it belongs to. Separation comes
   from a hairline plus a faint channel halo — no soft grey drop shadow. */
.skill-pop {
  position: fixed;
  z-index: 60;
  min-width: 13rem;
  max-width: min(20rem, calc(100vw - 2rem));
  padding: var(--s3);
  background: var(--panel);
  border: 1px solid color-mix(in srgb, var(--ch) 30%, var(--rule));
  border-top: 2px solid var(--ch);
  border-radius: var(--r-sm);
  box-shadow: 0 8px 22px -14px rgba(0, 0, 0, 0.85),
              0 0 18px -8px color-mix(in srgb, var(--ch) 40%, transparent);
}

/* Probe caret: a channel-coloured tick at the top edge, aligned by JS to the
   centre of the pill (--caret-x), so the readout visibly patches to its tag. */
.skill-pop::before {
  content: "";
  position: absolute;
  top: -7px;
  left: var(--caret-x, 18px);
  transform: translateX(-50%);
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-bottom: 6px solid var(--ch);
}

.skill-pop__label {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0 0 var(--s2);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-2xs);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim-strong);
}

.skill-pop__label::before { /* channel tick, as on .section__eyebrow */
  content: "";
  flex: 0 0 auto;
  width: 3px;
  height: 11px;
  background: var(--ch);
  border-radius: 1px;
}

.skill-pop__label::after { /* hairline running to the panel edge */
  content: "";
  flex: 1 1 0;
  height: 1px;
  background: color-mix(in srgb, var(--ch) 22%, var(--rule));
}

.skill-pop__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.skill-pop__link {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.4rem 0.5rem;
  font-size: var(--t-sm);
  color: var(--text);
  text-decoration: none;
  box-shadow: inset 2px 0 0 transparent; /* the rail the hover fills in */
  transition: color 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
}

.skill-pop__link::before { /* a mono probe marker, not a decorative arrow */
  content: "\203A"; /* › */
  font-family: var(--font-mono);
  font-weight: 700;
  color: color-mix(in srgb, var(--ch) 65%, var(--dim-strong));
}

.skill-pop__link:hover,
.skill-pop__link:focus-visible {
  outline: none;
  color: var(--text);
  background: color-mix(in srgb, var(--ch) 12%, transparent);
  box-shadow: inset 2px 0 0 var(--ch); /* patched: the channel rail lights up */
}

.skill-pop__link:hover::before,
.skill-pop__link:focus-visible::before {
  color: var(--ch);
}

@media (prefers-reduced-motion: no-preference) {
  .skill-pop { animation: pop-in 130ms ease-out both; }
}

@keyframes pop-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Deep-link landing: the card arrived at by #id flashes its channel colour,
   then settles to a faint ring so it stays identifiable. scroll-margin clears
   the sticky header. */
.channel { scroll-margin-top: 96px; }

.channel:target {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ch) 45%, transparent);
}

@media (prefers-reduced-motion: no-preference) {
  .channel:target { animation: card-flash 2.4s ease-out 1; }
}

@keyframes card-flash {
  0% {
    box-shadow: 0 0 0 2px var(--ch),
                0 0 24px 3px color-mix(in srgb, var(--ch) 35%, transparent);
  }
  100% {
    box-shadow: 0 0 0 1px color-mix(in srgb, var(--ch) 45%, transparent);
  }
}

/* --------------------------------------------------------------------------
   12. Channel module prose + links (kept after .pills/.badges so the
       flex gap above applies, and module spacing wins on .channel__links)
   -------------------------------------------------------------------------- */

.channel > .channel__tagline {
  margin-top: var(--s4);
  font-size: var(--t-base);
  font-weight: 500;
  line-height: 1.5;
  color: var(--text);
}

.channel__blurb {
  font-size: var(--t-md);
  line-height: 1.66;
  color: var(--dim-strong);
}

/* A scope caveat the module states about itself ("this part wasn't mine",
   "this stayed a prototype"). Quieter than the blurb and rule-separated, so it
   reads as a footnote to the claim above rather than more of the claim. */
.channel__note {
  padding-left: var(--s3);
  border-left: 2px solid color-mix(in srgb, var(--ch) 26%, var(--rule));
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--dim);
}

.channel .badges { margin-top: var(--s4); }

.channel > .channel__links {
  margin-top: auto;
  padding-top: var(--s5);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

/* --------------------------------------------------------------------------
   12a. Landing banks — one row per category

   The landing is not a grid of five modules any more: it is five rows, each
   pairing the bank's own framing (what this category is, how many channels it
   holds, where the rest of them live) with its flagship module. The rows
   alternate side so the eye zig-zags down the page instead of tracking one
   column, and so neither the words nor the instrument owns the layout.

   The module inside a row is the ordinary .channel, byte for byte the same one
   the sub-pages use — the row supplies the context, not a new card format.
   -------------------------------------------------------------------------- */

.banks {
  margin-top: var(--s6);
  display: grid;
  gap: var(--s8);
}

.bank {
  display: grid;
  gap: var(--s5);
  align-items: start;
}

@media (min-width: 900px) {
  /* Words take the flexible column, the module a fixed one: the card keeps the
     proportions it has everywhere else instead of stretching to fill a half. */
  /* Top-aligned, not centred: the bank's key bar and the module's key bar then
     start on the same line, which is what makes the row read as one label and
     one instrument rather than two things floating near each other. Modules
     vary a lot in height (a disclosure panel adds ~200px), so centring left the
     heading stranded mid-row. */
  .bank {
    grid-template-columns: minmax(0, 1fr) minmax(0, 400px);
    gap: var(--s7);
    align-items: start;
  }

  /* Every other row swaps sides. nth-of-type keeps the alternation correct on
     its own if a bank is ever added or removed; DOM order stays intro-then-
     module on every row, so focus order never zig-zags with the layout. */
  .bank:nth-of-type(even) {
    grid-template-columns: minmax(0, 400px) minmax(0, 1fr);
  }

  .bank:nth-of-type(even) .bank__intro {
    grid-column: 2;
    grid-row: 1;
  }

  .bank:nth-of-type(even) > .channel {
    grid-column: 1;
    grid-row: 1;
  }
}

.bank__addr {
  display: flex;
  align-items: center;
  gap: var(--s3);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-sm);
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ch);
}

/* the bank's key bar, same language as the one on each module's top edge */
.bank__addr::before {
  content: "";
  flex: none;
  width: 40px;
  height: 3px;
  background: var(--ch);
  border-radius: 2px;
}

.bank__name {
  margin-top: var(--s4);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(1.5rem, 4vw, 2rem);
  line-height: 1.2;
  letter-spacing: var(--track-tight);
  color: var(--text);
}

.bank__line {
  margin-top: var(--s4);
  max-width: 46ch;
  font-size: var(--t-lg);
  line-height: 1.6;
  color: var(--dim-strong);
}

.bank__count {
  margin-top: var(--s4);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dim);
}

/* The handoff to the sub-page. It leaves the page, so it is not a .btn and not
   the dashed .disclosure — it gets an underline that fills in and an arrow that
   travels, and it is the only interactive thing in this column. */
.bank__link {
  margin-top: var(--s5);
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 40px;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text);
  border-bottom: 1px solid color-mix(in srgb, var(--ch) 40%, var(--rule));
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.bank__link::after {
  content: "→";
  display: inline-block;
  transition: transform 0.18s var(--ease);
}

.bank__link:hover {
  color: var(--ch);
  border-bottom-color: var(--ch);
}

.bank__link:hover::after { transform: translateX(4px); }

/* The rest of the bank. A wide row leaves a lot of open field under the intro,
   and the honest thing to put there is the inventory the row is not showing:
   you can see what else is in the bank before deciding to click through. Each
   line carries that channel's own colour and a shorthand of its trace, so the
   landing and the sub-page are visibly the same instrument. */
.bank__rest {
  margin-top: var(--s7);
  /* same measure as .bank__line, so the list sits under the framing paragraph
     instead of sprawling across the whole flexible column */
  max-width: 46ch;
  border-top: 1px solid var(--rule);
}

.bank__rest-label {
  margin-top: var(--s4);
  margin-bottom: var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dim-strong);
}

.rest {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) 132px;
  align-items: center;
  gap: var(--s4);
  padding: var(--s4) 0;
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent);
}

.rest__addr {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--ch);
}

.rest__name {
  min-width: 0;
  font-size: var(--t-lg);
  line-height: 1.35;
  color: var(--text);
}

.rest__status {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: right;
  white-space: nowrap;
  color: var(--dim);
}

.rest__status--live { color: color-mix(in srgb, var(--ch) 72%, var(--dim-strong)); }

/* Sub-group teaser row: when a bank has enough channels to graduate to
   sub-groups (LLM/Orquestación), the landing teaser names the groups instead of
   listing every channel loose. Two stacked lines — the group name and its
   members — in place of the addr/name/trace grid. */
.rest--group {
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s1);
  align-items: start;
}

.rest__group-name {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ch);
}

.rest__members {
  min-width: 0;
  font-size: var(--t-lg);
  line-height: 1.35;
  color: var(--dim-strong);
}

/* Bare strokes, no readout well: at 84×22 the graticule and border of a full
   .trace would be all you could see. */
/* 132×35 keeps the 84×22 viewBox ratio, so the glyphs authored for that box
   scale up without letterboxing. */
.rest__trace {
  width: 132px;
  height: 35px;
  overflow: visible;
}

.rest__trace path {
  fill: none;
  stroke: var(--ch);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.8;
}

/* Undrawn only once script is present to draw them: main.js stamps .js on
   <html> before anything else, so with JS off these render finished instead of
   leaving nine blank strips on the page. They sit far below the fold, so the
   deferred class arrives long before anyone scrolls this far. */
.js .rest__trace path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}

/* They draw when their bank arrives rather than on load, where the animation
   would be over before anyone had scrolled to see it. */
.bank.is-onscreen .rest__trace path {
  animation: trace-draw 0.9s var(--ease) forwards;
}

.bank.is-onscreen .rest:nth-child(2) .rest__trace path { animation-delay: 0.10s; }
.bank.is-onscreen .rest:nth-child(3) .rest__trace path { animation-delay: 0.20s; }
.bank.is-onscreen .rest:nth-child(4) .rest__trace path { animation-delay: 0.30s; }
.bank.is-onscreen .rest:nth-child(5) .rest__trace path { animation-delay: 0.40s; }


/* --------------------------------------------------------------------------
   12b. Expanding demo modules — disclosure button + inline panel

   Only the three modules with a live demo carry these. Everything resolves
   through var(--ch), which the <article> already sets inline, so there is not
   a single per-channel selector here.
   -------------------------------------------------------------------------- */

/* The control is one <button> — not a clickable header — spanning the full
   card width so the target is as large as a clickable header would have been,
   while staying a single tab stop with a real role and state. */
.channel > .disclosure {
  margin-top: var(--s3);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  width: 100%;
  min-height: 40px;
  padding: var(--s2) var(--s3);
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--ch) 30%, var(--rule));
  border-radius: var(--r-sm);
  color: var(--dim-strong);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  cursor: pointer;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease),
              background-color 0.18s var(--ease);
}

.disclosure:hover {
  color: var(--ch);
  border-color: color-mix(in srgb, var(--ch) 60%, var(--rule));
  background: color-mix(in srgb, var(--ch) 8%, transparent);
}

.disclosure[aria-expanded="true"] {
  color: var(--ch);
  border-style: solid;
  border-color: color-mix(in srgb, var(--ch) 46%, var(--rule));
}

.disclosure__chevron {
  width: 12px;
  height: 12px;
  flex: none;
  transition: transform 0.25s var(--ease);
}

.disclosure[aria-expanded="true"] .disclosure__chevron { transform: rotate(180deg); }

/* Both labels are in the DOM so the i18n pass has a node for each key; CSS
   picks the one the current state calls for. display:none keeps the other out
   of the accessible name computation, so the button is named "Ver más" or
   "Ver menos" and never both. */
.disclosure[aria-expanded="false"] .disclosure__label--less,
.disclosure[aria-expanded="true"] .disclosure__label--more { display: none; }

/* The height transition: a grid whose single row goes 0fr -> 1fr. No measured
   pixel heights, so the panel is free to reflow (wrapping text, mobile width)
   without JS re-measuring. `hidden` is what actually removes it from the a11y
   tree and the tab order — the 0fr row is only the animation. */
.panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.32s var(--ease);
}

/* Beats the UA `[hidden] { display: none }`, which `display: grid` above
   would otherwise override. Without this the panel would be zero-height but
   still focusable — the exact defect this pattern has to avoid. */
.panel[hidden] { display: none; }

.channel > .panel { margin-top: 0; }

.panel.is-open { grid-template-rows: 1fr; }

.panel__inner {
  min-height: 0;
  overflow: hidden;
}

.panel__body {
  margin-top: var(--s4);
  padding: var(--s4);
  border: 1px solid color-mix(in srgb, var(--ch) 24%, var(--rule));
  border-left: 3px solid var(--ch);
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--ch) 6%, transparent);
}

.panel__body > * { margin-top: var(--s3); }
.panel__body > :first-child { margin-top: 0; }

.panel__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ch);
}

.panel__text {
  font-size: var(--t-md);
  line-height: 1.66;
  color: var(--text);
}

.panel__list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--dim-strong);
}

.panel__list li {
  padding-left: var(--s4);
  position: relative;
}

.panel__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.5em;
  width: 6px;
  height: 2px;
  background: var(--ch);
}

.panel__note {
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--dim-strong);
  border-top: 1px solid color-mix(in srgb, var(--ch) 18%, var(--rule));
  padding-top: var(--s3);
}

.panel__cta {
  margin-top: var(--s4);
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

/* --------------------------------------------------------------------------
   13. Booking — quiet, tinted by the live-telemetry channel
   -------------------------------------------------------------------------- */

.section--book { --ch: var(--ch-cyan); }

.book__intro {
  margin-top: var(--s5);
  max-width: 60ch;
  color: var(--dim-strong);
}

.tabs {
  margin-top: var(--s6);
  display: inline-flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

.tab {
  min-height: 38px;
  padding: 0.5rem 1.1rem;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--dim-strong);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  cursor: pointer;
  transition: color 0.18s var(--ease), background-color 0.18s var(--ease),
              border-color 0.18s var(--ease);
}

.tab:hover { color: var(--text); }

/* aria-selected is the source of truth; .is-active is supported so nothing
   depends on script load order */
.tab[aria-selected="true"],
.tab.is-active {
  color: var(--ch);
  background: color-mix(in srgb, var(--ch) 14%, transparent);
  border-color: color-mix(in srgb, var(--ch) 40%, var(--rule));
}

/* The booking well. Styled off our own class, not Calendly's: the element only
   gains .calendly-inline-widget when main.js initialises the widget, so the
   well's box (and therefore the reserved space) is identical before and after. */
.book__panel {
  /* Calendly's loading spinner is position:absolute; without a positioned
     ancestor it escapes the well and paints over the hero. Containing it here
     also makes the loading state read as part of the panel. */
  position: relative;
  margin-top: var(--s5);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* Calendly's embed colour params (background_color/text_color/primary_color on
   the tab data-url) are a paid-plan feature; on the free plan Calendly ignores
   them and renders the booking iframe on its default white, which clashes with
   the dark panel. We can't reach inside a cross-origin iframe to theme it, so we
   invert it from the outside to fake a dark theme. hue-rotate(180deg) flips the
   inverted hues back so the blue accent (selected dates) stays blue. Drop this
   rule if the account moves back to a paid plan and the params apply again. */
#calendly-inline iframe {
  filter: invert(0.92) hue-rotate(180deg);
}

/* Placeholder shown until the Calendly widget replaces it (also covers a
   blocked third-party frame, where it never arrives). Absolutely positioned
   within the already-reserved .book__panel box, so it adds no height and
   CLS stays 0. */
.book__loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dim-strong);
}

.book__footnote {
  margin-top: var(--s4);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.03em;
  color: var(--dim-strong);
}

/* --------------------------------------------------------------------------
   14. Footer — quiet
   -------------------------------------------------------------------------- */

.site-footer {
  max-width: var(--measure);
  margin-inline: auto;
  padding: var(--s6) var(--pad-x) var(--s7);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
}

.footer__link {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--dim-strong);
  border-bottom: 1px solid transparent;
  transition: color 0.18s var(--ease), border-color 0.18s var(--ease);
}

.footer__link:hover {
  color: var(--text);
  border-bottom-color: var(--ch);
}

.footer__meta {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.06em;
  color: var(--dim-strong);
}

/* --------------------------------------------------------------------------
   14b. Course program (/formacion) — a curriculum, not a ranking. Three sets
        of equal width and equal weight; the only thing that separates them is
        which levels are live, and that is stated per level, never by making
        one set louder than another.
   -------------------------------------------------------------------------- */

/* Unlike .board, these stretch to a common height on purpose: three sets of
   equal width AND equal height is what makes the curriculum read as one bar
   rather than a ranking. Nothing here expands, so stretching costs nothing. */
.program {
  margin-top: var(--s6);
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s5);
}

@media (min-width: 760px) {
  .program { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
  .program { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.course {
  display: flex;
  flex-direction: column;
  padding: var(--s5);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
}

.course > * { margin-top: var(--s4); }
.course > :first-child { margin-top: 0; }

.course__name {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-xl);
  letter-spacing: -0.015em;
  color: var(--text);
}

.course__levels {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}

.level {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s2) 0;
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.04em;
}

.level__state {
  flex: none;
  width: 1.25em;
  text-align: center;
}

.level__name { color: var(--text); }

/* A level that exists is a link; one that doesn't is plainly dimmed and says
   so in words. No level is ever styled as available when it isn't. */
.level--live .level__name {
  color: var(--ch);
  text-decoration: none;
  border-bottom: 1px solid color-mix(in srgb, var(--ch) 35%, transparent);
}

.level--live .level__name:hover {
  border-bottom-color: var(--ch);
}

.level--soon,
.level--soon .level__name { color: var(--dim); }

/* Availability in words, not only in the glyph — the ✅/🔜 are decorative. */
.level__status {
  margin-left: auto;
  padding-left: var(--s2);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--dim);
}

.level--live .level__status { color: color-mix(in srgb, var(--ch) 72%, var(--dim-strong)); }

/* One same-length line per set, and all three land on the same baseline —
   the symmetry is the point, so it is enforced in layout, not just in copy. */
.course__extra {
  margin-top: auto;
  padding-top: var(--s4);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--dim-strong);
}

/* Bridge from the classroom to the actual car — the last thing on the page,
   and the only element here that points off it. */
.bridge {
  margin-top: var(--s6);
  padding: var(--s5);
  border: 1px dashed color-mix(in srgb, var(--ch) 30%, var(--rule));
  border-radius: var(--r-lg);
  background: color-mix(in srgb, var(--ch) 5%, transparent);
}

.bridge__eyebrow {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--ch);
}

.bridge__text {
  margin-top: var(--s3);
  max-width: 62ch;
  font-size: var(--t-md);
  line-height: 1.66;
  color: var(--text);
}

.bridge__cta { margin-top: var(--s4); }

/* --------------------------------------------------------------------------
   15. Reduced motion — no motion anywhere, traces stay fully drawn
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }

  .trace path,
  .js .rest__trace path,
  .js .circuit__track {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }

  .trace circle,
  .trace rect {
    opacity: 1;
    transform: none;
  }

  .channel:hover > .trace { transform: none; }
  .skip-link:focus-visible { transform: translateY(0); }
}

/* --------------------------------------------------------------------------
   14. Vehicle pages — one level below a bank

   A bank card grew into a whole build, so it earns its own page: a prose hero
   that states the one idea the vehicle is organised around, then a board of
   engineering *modules*. A module is the same instrument card as a channel,
   but its loud line is the decision, not a reading — so it reuses .board and
   the panel language, and adds only what a channel doesn't have: a maturity
   status and a compressed "what it solves" line.

   Every string on these pages is rendered from the data table in main.js, so
   adding a module is a data edit, never an HTML one.
   -------------------------------------------------------------------------- */

/* Hero — the thesis of the build. Same key-bar grammar as .subhead, but the
   lede is a long technical paragraph, so it gets reading measure and air. */
.vhero {
  border-top: 0;
  padding-top: var(--s6);
}

.vhero__title {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(1.35rem, 3.6vw, 1.9rem);
  line-height: 1.25;
  letter-spacing: var(--track-tight);
  color: var(--text);
}

/* the chain string (pack → BMS → controller → motor) reads as wiring, so it
   is mono and channel-coloured, sitting under the title like a subtitle */
.vhero__chain {
  margin-top: var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  letter-spacing: 0.04em;
  color: var(--ch);
}

.vhero__lede {
  margin-top: var(--s5);
  max-width: 68ch;
  font-size: var(--t-lg);
  line-height: 1.7;
  color: var(--text);
  padding-left: var(--s4);
  border-left: 3px solid var(--ch);
}

/* the citation / basis line under the lede — a source, kept quiet */
.vhero__cite {
  margin-top: var(--s3);
  max-width: 68ch;
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.02em;
  color: var(--dim-strong);
  padding-left: var(--s4);
}

/* the "worth keeping" detail — a footnote-grade callout below the lede */
.vhero__detail {
  margin-top: var(--s5);
  max-width: 68ch;
  padding: var(--s4);
  font-size: var(--t-md);
  line-height: 1.6;
  color: var(--dim-strong);
  background: color-mix(in srgb, var(--ch) 5%, var(--panel));
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
}

.vhero__actions {
  margin-top: var(--s5);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}

/* Result ribbon — a citable placing (4th, 6th). Kin to .channel__award but no
   trophy: a placing is a fact to state flat, not a prize to dress up. */
.vresult {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--ch);
  background: color-mix(in srgb, var(--ch) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--ch) 34%, var(--rule));
  border-radius: var(--r-sm);
}

.vresult::before {
  content: "▸";
  color: var(--ch);
}

/* Module card — the channel panel, restated for a decision instead of a
   reading. Left key-bar, hover lift, explicit child rhythm: all the same. */
.module {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--s5);
  background-color: var(--panel);
  background-image: linear-gradient(180deg,
    color-mix(in srgb, var(--ch) 5%, transparent), transparent 150px);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  transition: border-color 0.25s var(--ease), box-shadow 0.25s var(--ease);
}

.module::before {
  content: "";
  position: absolute;
  top: 0;
  left: 18px;
  width: 72px;
  height: 3px;
  background: var(--ch);
  border-radius: 0 0 2px 2px;
}

.module:hover {
  border-color: color-mix(in srgb, var(--ch) 34%, var(--rule));
  box-shadow: 0 20px 48px -34px color-mix(in srgb, var(--ch) 85%, transparent);
}

.module > * { margin-top: var(--s3); }
.module > :first-child { margin-top: 0; }

/* head: the name, and the status pushed to the far edge so a column of cards
   reads as a status column down the right */
.module__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s3);
}

.module__name {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--t-lg);
  line-height: 1.3;
  letter-spacing: -0.015em;
  color: var(--text);
}

/* the compressed "what it solves" line — quiet, above the loud decision */
.module__solves {
  font-size: var(--t-sm);
  line-height: 1.5;
  color: var(--dim-strong);
}

/* the decision — the valuable part, so it carries body weight and measure */
.module__decision {
  font-size: var(--t-md);
  line-height: 1.66;
  color: var(--text);
}

.module .badges { margin-top: var(--s4); }

/* Status chip — a five-rung maturity ladder, coloured by rung. Each state
   resolves through a local --st so the dot, text and border stay one hue.
   The label is words, never colour alone (AA), and the dot is a square to
   match the squared-off hardware language of the sheet. */
.status {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: 0.15rem 0.45rem;
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--st, var(--dim-strong));
  border: 1px solid color-mix(in srgb, var(--st, var(--rule)) 45%, var(--rule));
  border-radius: var(--r-sm);
}

.status__dot {
  width: 7px;
  height: 7px;
  flex: none;
  background: var(--st, var(--dim));
  box-shadow: 0 0 5px color-mix(in srgb, var(--st, transparent) 60%, transparent);
}

/* Maturity ladder, dim → hot. Exploratory carries no fill and a dashed edge,
   so nothing at that rung can be misread as built. */
.status--explore { --st: var(--dim-strong); border-style: dashed; }
.status--explore .status__dot { background: transparent; border: 1px dashed var(--dim-strong); box-shadow: none; }
.status--design      { --st: var(--ch-elyos);   }
.status--integration { --st: var(--ch-amber);   }
.status--bench       { --st: var(--ch-cyan);    }
.status--track       { --st: var(--ch-green);   }

/* Honest-status callout — the caveat the whole page is accountable to. Louder
   than a module note (it governs the page) but still a note, not a claim: a
   left rule in the channel colour and a labelled heading. */
.vstate {
  margin-top: var(--s7);
  max-width: 72ch;
  padding: var(--s4) var(--s5);
  background: var(--panel);
  border: 1px solid var(--rule);
  border-left: 3px solid color-mix(in srgb, var(--ch) 55%, var(--rule));
  border-radius: var(--r-md);
}

.vstate__label {
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  color: var(--dim-strong);
}

.vstate__body {
  margin-top: var(--s3);
  font-size: var(--t-md);
  line-height: 1.66;
  color: var(--text);
}

/* the no-JS fallback: these pages are data-rendered, so with scripting off we
   still hand the reader the two doors that matter — the parent bank and the
   published documentation. */
.vehicle-noscript {
  margin-top: var(--s5);
  padding: var(--s4);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  color: var(--dim-strong);
}
.vehicle-noscript a { color: var(--ch); }
