/* CodeAtlas project page — the drill-in.

   Reading, not scanning. Wider measure than the portfolio's cards, generous
   line height, and every panel omitted entirely when it has no content: an
   empty "Data models" heading reads as a project that has none rather than as
   a page with nothing to say. */

:root {
  --bg: #0e1116;
  --panel: #161b22;
  --line: #262d36;
  --ink: #e6edf3;
  --dim: #9aa4b2;
  --dim2: #6e7681;
  --accent: #4a9eff;
  /* `.dfix` carries the command that clears a failing check and was
     referencing --ok, which this file never defined -- so the colour
     declaration was invalid and the text inherited body ink. Same
     value as the console uses, and it clears AA on both --bg and
     --panel. */
  --ok: #3fb950;
  --mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.65 -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
}

.pwrap { max-width: 900px; margin: 0 auto; padding: 0 24px; }

a { color: var(--accent); }

/* ---------- back bar ---------- */
.pback {
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  font-size: 13px;
}
.pback .pwrap {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding-top: 11px; padding-bottom: 11px;
}
.pback a { text-decoration: none; font-weight: 600; }
.pback a:hover { text-decoration: underline; }
.dim { color: var(--dim2); }

/* ---------- hero ---------- */
.phero { border-bottom: 1px solid var(--line); padding: 34px 0 28px; }
.phero h1 {
  margin: 0 0 10px; font-size: 30px; line-height: 1.2;
  letter-spacing: -0.4px;
}
.plede { margin: 0 0 12px; font-size: 17.5px; color: var(--ink); max-width: 66ch; }
.phead {
  margin: 0 0 14px; color: var(--accent); font-size: 15px;
  font-weight: 600; max-width: 66ch;
}
.phead:empty { display: none; }

/* Links out: the write-up, the game, the live site (overrides `links`). */
.ext-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
.ext {
  display: inline-block; padding: 5px 12px; border: 1px solid currentColor;
  border-radius: 999px; font-size: 13.5px; font-weight: 600; line-height: 1.4;
  color: var(--accent); text-decoration: none; overflow-wrap: anywhere;
}
.ext:hover, .ext:focus { text-decoration: underline; }
@media print { .ext::after { content: " " attr(href); font-weight: 400; } }

.pstats {
  display: flex; flex-wrap: wrap; gap: 26px; margin-top: 20px;
}
.pstat b {
  display: block; font-size: 21px; font-weight: 650;
  font-variant-numeric: tabular-nums; letter-spacing: -0.3px;
}
.pstat span {
  font-size: 11px; color: var(--dim2); text-transform: uppercase;
  letter-spacing: .6px;
}

/* ---------- screenshot ---------- */
.pshot { margin: 26px 0 0; }
.pshot img {
  width: 100%; height: auto; display: block; border-radius: 10px;
  border: 1px solid var(--line);
}
.pshot figcaption {
  margin-top: 8px; font-size: 12.5px; color: var(--dim2);
}

/* ---------- blocks ---------- */
.pblock { margin: 34px 0; }
.pblock h2 {
  margin: 0 0 10px; font-size: 13px; text-transform: uppercase;
  letter-spacing: 1px; color: var(--dim2); font-weight: 650;
}
.pblock h3 {
  margin: 18px 0 7px; font-size: 13.5px; color: var(--dim);
  font-weight: 600;
}
.pblock p { margin: 0 0 12px; max-width: 68ch; }
.pnote { color: var(--dim2); font-size: 13.5px; margin-bottom: 12px !important; }
.lead { font-size: 16.5px; }
.proof {
  color: var(--dim); font-size: 14px; font-variant-numeric: tabular-nums;
  border-left: 2px solid var(--accent); padding-left: 13px;
}

.bullets { margin: 0; padding-left: 20px; max-width: 68ch; }
.bullets li { margin-bottom: 7px; }

.chips { list-style: none; margin: 0 0 10px; padding: 0;
  display: flex; flex-wrap: wrap; gap: 6px; }
.chips li {
  font-size: 12.5px; padding: 4px 10px; border-radius: 5px;
  background: var(--panel); border: 1px solid var(--line); color: var(--dim);
}

/* The same defect the portfolio's connections list had. One `auto` grid
   column cannot shrink below its widest item, and a route such as
   `PATCH /stories/{story_id}/characters/{character_id}/health` has no break
   point after the verb -- so Kitten TTS's page measured 433px at 390px.
   Both halves are needed: `minmax(0,1fr)` alone left the route spilling out
   of its row (419px); wrapping the token is what brings it inside. */
.arch, .endpoints { list-style: none; margin: 0; padding: 0; display: grid;
                    grid-template-columns: minmax(0, 1fr); gap: 5px; }
.arch li, .endpoints li {
  display: flex; align-items: baseline; gap: 12px; font-size: 13.5px;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 6px; padding: 8px 13px;
}
.arch code, .endpoints code {
  font-family: var(--mono); font-size: 12.5px; color: var(--ink);
  overflow-wrap: anywhere;
}
.arch span { margin-left: auto; color: var(--dim2); font-size: 12px; }

.evidence {
  margin: 0; padding: 13px 17px; border-left: 3px solid var(--line);
  background: var(--panel); border-radius: 0 8px 8px 0;
  color: var(--dim); font-size: 14px; max-width: 72ch;
}

/* ---------- languages ---------- */
.langs { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; }
.langs li { display: flex; align-items: center; gap: 12px; font-size: 13.5px; }
.lname { flex: 0 0 128px; }
.lbar {
  flex: 1; height: 7px; background: var(--panel); border-radius: 4px;
  overflow: hidden; border: 1px solid var(--line);
}
.lbar i { display: block; height: 100%; background: var(--accent); }
.lnum {
  flex: 0 0 90px; text-align: right; color: var(--dim2);
  font-variant-numeric: tabular-nums; font-size: 12.5px;
}

/* ---------- key/value ---------- */
.kv {
  display: grid; grid-template-columns: max-content 1fr; gap: 7px 20px;
  margin: 0; font-size: 14px;
}
.kv dt { color: var(--dim2); }
.kv dd { margin: 0; }

/* ---------- footer ---------- */
.pfoot {
  margin: 46px 0 60px; padding-top: 22px; border-top: 1px solid var(--line);
  color: var(--dim2); font-size: 13.5px;
}
.pfoot p { max-width: 68ch; }

@media (max-width: 620px) {
  .phero h1 { font-size: 24px; }
  .pstats { gap: 18px; }
  .lname { flex-basis: 92px; }
  .kv { grid-template-columns: 1fr; gap: 2px 0; }
  .kv dd { margin-bottom: 10px; }
}

@media print {
  body { background: #fff; color: #000; }
  .pback { display: none; }
  .pblock { break-inside: avoid; }
}

/* ---------- vocabulary ----------
   89,966 callables were measured and reached no screen. Counts are exact;
   the names are a sample, and the note above says so. */
.vh {
  margin: 18px 0 8px; font-size: 11px; text-transform: uppercase;
  letter-spacing: .8px; color: var(--dim2); font-weight: 600;
}
.vocab { list-style: none; margin: 0; padding: 0;
         display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
         gap: 7px 18px; }
.vocab li { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.vocab code {
  font-family: var(--mono); font-size: 12.5px; color: var(--ink);
  background: none; padding: 0; overflow-wrap: anywhere;
}
.vwhere { font-family: var(--mono); font-size: 10.5px; color: var(--dim2);
          overflow-wrap: anywhere; }
.vmeth { font-size: 11px; color: var(--dim); overflow-wrap: anywhere; }

/* ---------- what the measurements mean ----------
   Prose as an entry point, not a summary that replaces the detail. Every
   sentence names its fields and links to the section holding them. */
.narrate { border-left: 2px solid var(--accent); padding-left: 18px; }
.narr { list-style: none; margin: 0; padding: 0;
        display: flex; flex-direction: column; gap: 14px; }
.narr li { font-size: 15px; line-height: 1.6; color: var(--ink); }
.narr .nterm { white-space: nowrap; }
.narr .nfrom {
  display: block; margin-top: 3px; font-family: var(--mono); font-size: 10.5px;
  color: var(--dim2); overflow-wrap: anywhere;
}
.ndrill { margin-left: 8px; font-size: 12.5px; white-space: nowrap; }
/* Severity reads before the sentence does. */
.narr li.n-credential { border-left: 2px solid #f85149; padding-left: 12px; }
.narr li.n-unversioned,
.narr li.n-truncated { border-left: 2px solid #d29922; padding-left: 12px; }
.pblock:target { outline: 2px solid var(--accent); outline-offset: 8px; }

/* ---------- vulnerable dependencies ----------
   Severity is encoded three ways: a text marker, a border, and colour. WCAG
   1.4.1 -- colour alone cannot carry meaning, and a red dot says nothing on a
   monochrome print or to a reader who cannot see red. */
.deps { list-style: none; margin: 0; padding: 0;
        display: flex; flex-direction: column; gap: 8px; }
.dep {
  display: grid; grid-template-columns: 22px minmax(0,1fr) auto auto;
  gap: 3px 12px; align-items: baseline; font-size: 13px;
  padding-left: 10px; border-left: 3px solid var(--line);
}
.dep.u-act-now    { border-left-color: #f85149; }
.dep.u-should-fix { border-left-color: #d29922; }
.dep.u-decide     { border-left-color: var(--dim2); }
.dsev {
  font-family: var(--mono); font-weight: 700; font-size: 12px;
  letter-spacing: -1px;
}
.dep.u-act-now .dsev    { color: #f85149; }
.dep.u-should-fix .dsev { color: #d29922; }
.dep.u-decide .dsev     { color: var(--dim2); }
.dpkg { font-family: var(--mono); font-size: 12.5px; color: var(--ink);
        background: none; padding: 0; overflow-wrap: anywhere; }
.dver { font-family: var(--mono); font-size: 11.5px; color: var(--dim);
        white-space: nowrap; }
.dfix { font-family: var(--mono); font-size: 11.5px; color: var(--ok);
        white-space: nowrap; font-weight: 600; }
.dadv { grid-column: 2 / -1; font-family: var(--mono); font-size: 10.5px;
        color: var(--dim2); overflow-wrap: anywhere; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}


/* ---------- diagrams ----------

   Every shape is a measured number and the number is printed beside it. The
   palette comes in through custom properties so one SVG renders correctly on
   the light portfolio and the dark dashboard without being generated twice.

   `width:100%; height:auto` on a viewBox'd SVG makes it fluid down to a phone
   without a media query; `xMidYMin` keeps the top edge anchored so a tall
   diagram crops from the bottom rather than floating. */

:root {
  --dg-ink: #1a1d21;
  --dg-dim: #5b6672;
  --dg-line: #dfe3e8;
  --dg-accent: #2563c7;
  --dg-panel: #f5f7fa;
  --dg-ok: #1a7f37;
  --dg-warn: #9a6700;
  --dg-bad: #cf222e;
}
@media (prefers-color-scheme: dark) {
  :root {
    --dg-ink: #e6edf3;
    --dg-dim: #8b95a3;
    --dg-line: #2a3038;
    --dg-accent: #4a9eff;
    --dg-panel: #161b22;
    --dg-ok: #3fb950;
    --dg-warn: #d29922;
    --dg-bad: #f85149;
  }
}
.dg { display: block; width: 100%; height: auto; margin: 10px 0 4px; }
.dgwrap { margin: 22px 0; }

/* ---------- picture cycle ----------
   A project walked through has several pictures; choosing one throws the
   rest away. These cross-fade, in CSS, because the project pages are static
   HTML with no script and the portfolio is one file you can send someone.

   Each picture's fade-out occupies the same window as the next
   one's fade-in, so the two cross rather than both passing through
   zero -- ending the fade-out where the next fade-in starts left a
   0.3s flash of bare background at every handoff.

   This CSS cycle is the fallback, and what a reader without script sees.
   site/template-shared/cycler.js takes a set over when it runs: previous and
   next, a counter, a pause control, the arrow keys and a swipe. That is what
   keeps any picture reachable without waiting for it -- the names of the
   pictures are deliberately not listed underneath, which read as scaffolding
   on a portfolio. */
/* Defaults for the two properties the markup sets inline. A var() with no
   definition makes the whole declaration invalid and it is dropped in
   silence -- so markup that ever omits --n would leave every frame with no
   animation at all, and nothing on the page would say why. */
.cyc{--n:1}
.cyc-img{--i:0}
.cyc-frame{position:relative;overflow:hidden;aspect-ratio:16/10;
  background:var(--panel)}
.cyc-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  object-position:top center;opacity:0;
  animation-duration:calc(var(--n) * 3.5s);animation-iteration-count:infinite;
  animation-timing-function:linear;animation-delay:calc(var(--i) * 3.5s)}
.cyc-n2 .cyc-img{animation-name:cyc2}
.cyc-n3 .cyc-img{animation-name:cyc3}
.cyc-n4 .cyc-img{animation-name:cyc4}
.cyc-n5 .cyc-img{animation-name:cyc5}
.cyc-n6 .cyc-img{animation-name:cyc6}
.cyc-n7 .cyc-img{animation-name:cyc7}
.cyc-n8 .cyc-img{animation-name:cyc8}
.cyc-n9 .cyc-img{animation-name:cyc9}
.cyc-n10 .cyc-img{animation-name:cyc10}
.cyc-n11 .cyc-img{animation-name:cyc11}
.cyc-n12 .cyc-img{animation-name:cyc12}
.cyc:hover .cyc-img,.cyc:focus-within .cyc-img{animation-play-state:paused}
/* Controls, added by cycler.js. Everything here sits ON a screenshot whose
   colours are unknown, so it carries its own dark translucent ground and a
   white glyph instead of the page's tokens: a token tuned for the page
   background is exactly wrong over a picture of somebody's interface. */
.cyc-sr{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
.cyc.cyc-live .cyc-img{animation:none;opacity:0;transition:opacity .63s linear}
.cyc.cyc-live .cyc-img.is-on{opacity:1}
.cyc-btn{position:absolute;z-index:2;display:flex;align-items:center;
  justify-content:center;width:40px;height:40px;padding:0;margin:0;border:0;
  border-radius:50%;cursor:pointer;color:#fff;background:rgba(10,12,16,.46);
  box-shadow:0 1px 3px rgba(0,0,0,.35);transition:background-color .15s}
.cyc-btn:hover{background:rgba(10,12,16,.74)}
.cyc-btn:focus-visible{outline:2px solid #fff;outline-offset:2px;
  box-shadow:0 0 0 5px rgba(10,12,16,.82)}
.cyc-prev,.cyc-next{top:50%;transform:translateY(-50%)}
.cyc-prev{left:10px}
.cyc-next{right:10px}
.cyc-bar{position:absolute;z-index:2;right:10px;bottom:10px;display:flex;
  align-items:center;gap:2px;padding:2px 11px 2px 2px;border-radius:999px;
  color:#fff;background:rgba(10,12,16,.6);
  font:600 12px/1 system-ui,-apple-system,"Segoe UI",sans-serif;
  font-variant-numeric:tabular-nums;letter-spacing:.02em}
.cyc-bar .cyc-btn{position:static;width:28px;height:28px;background:transparent;
  box-shadow:none;transform:none}
.cyc-bar .cyc-btn:hover{background:rgba(255,255,255,.16)}
@media (prefers-reduced-motion:reduce){
  .cyc-img{animation:none}
  .cyc-hero,.cyc-img:first-child{opacity:1}
  .cyc.cyc-live .cyc-img,.cyc-btn{transition:none}
}
@media print{.cyc-img{animation:none}.cyc-hero,.cyc-img:first-child{opacity:1}
  .cyc-btn,.cyc-bar{display:none}}
@keyframes cyc2{0%{opacity:0}9.00%{opacity:1}50.00%{opacity:1}59.00%{opacity:0}100%{opacity:0}}
@keyframes cyc3{0%{opacity:0}6.00%{opacity:1}33.33%{opacity:1}39.33%{opacity:0}100%{opacity:0}}
@keyframes cyc4{0%{opacity:0}4.50%{opacity:1}25.00%{opacity:1}29.50%{opacity:0}100%{opacity:0}}
@keyframes cyc5{0%{opacity:0}3.60%{opacity:1}20.00%{opacity:1}23.60%{opacity:0}100%{opacity:0}}
@keyframes cyc6{0%{opacity:0}3.00%{opacity:1}16.67%{opacity:1}19.67%{opacity:0}100%{opacity:0}}
@keyframes cyc7{0%{opacity:0}2.57%{opacity:1}14.29%{opacity:1}16.86%{opacity:0}100%{opacity:0}}
@keyframes cyc8{0%{opacity:0}2.25%{opacity:1}12.50%{opacity:1}14.75%{opacity:0}100%{opacity:0}}
@keyframes cyc9{0%{opacity:0}2.00%{opacity:1}11.11%{opacity:1}13.11%{opacity:0}100%{opacity:0}}
@keyframes cyc10{0%{opacity:0}1.80%{opacity:1}10.00%{opacity:1}11.80%{opacity:0}100%{opacity:0}}
@keyframes cyc11{0%{opacity:0}1.64%{opacity:1}9.09%{opacity:1}10.73%{opacity:0}100%{opacity:0}}
@keyframes cyc12{0%{opacity:0}1.50%{opacity:1}8.33%{opacity:1}9.83%{opacity:0}100%{opacity:0}}
.pshot.cyc{margin:26px 0 0}
.pshot.cyc .cyc-frame{border-radius:10px;border:1px solid var(--line)}

/* =========================================================================
   COLOUR: the console's grammar, one meaning per colour (2026-11-10).
     cyan titles · yellow numbers · blue labels · teal code and tags ·
     white the sentence that matters · each section its own colour down the
     left edge. Lowest pairing 6.4:1 on --panel (floor 4.5:1), checked by
     TestEveryStylesheetIsSelfConsistent; every coloured thing keeps its words.
   ========================================================================= */
:root {
  --c-cyan: #5fd7e8;
  --c-yellow: #f5d36b;
  --c-blue: #8fc4ff;
  --c-teal: #56d4bc;
  --c-white: #f6f8fa;
  --c-green: #7ee787;
  --c-magenta: #e29cff;
  --c-orange: #ffab70;
}
.phero h1 { color: var(--c-cyan); }
.plede { color: var(--c-white); }
.pstat b { color: var(--c-yellow); }
.pstat span { color: var(--c-blue); }
.pblock { border-left: 3px solid var(--line); padding-left: 16px; }
.pblock h2 { color: var(--c-cyan); }
.pblock:nth-of-type(6n+1) { border-left-color: var(--c-cyan); }
.pblock:nth-of-type(6n+2) { border-left-color: var(--c-green); }
.pblock:nth-of-type(6n+2) h2 { color: var(--c-green); }
.pblock:nth-of-type(6n+3) { border-left-color: var(--c-blue); }
.pblock:nth-of-type(6n+3) h2 { color: var(--c-blue); }
.pblock:nth-of-type(6n+4) { border-left-color: var(--c-magenta); }
.pblock:nth-of-type(6n+4) h2 { color: var(--c-magenta); }
.pblock:nth-of-type(6n+5) { border-left-color: var(--c-orange); }
.pblock:nth-of-type(6n+5) h2 { color: var(--c-orange); }
.pblock:nth-of-type(6n+6) { border-left-color: var(--c-teal); }
.pblock:nth-of-type(6n+6) h2 { color: var(--c-teal); }
.pblock h3 { color: var(--c-blue); }
/* On a phone the indent cost the content its width: one page went 9px past
   a 390px screen. There the colour moves onto the title instead. */
@media (max-width: 640px) {
  .pblock { border-left: 0; padding-left: 0; }
  .pblock h2 { border-left: 3px solid currentColor; padding-left: 8px; }
}
.bullets li::marker { color: var(--c-yellow); }
.chips li { color: var(--c-teal); }
.arch code, .endpoints code, .vocab code { color: var(--c-teal); }
:not(pre) > code { color: var(--c-teal); }
.lname, .kv dt { color: var(--c-blue); }
.lnum, .dver { color: var(--c-yellow); }
.kv dd, .dpkg { color: var(--c-white); }