/* Restraint is the brief. One accent, plain type, hairline rules, a lot of air.
   The directory is always visible on the left; content sits in a card on the right.

   LIGHT AND DARK ARE SITE-WIDE, not a gallery setting. The switch sets data-theme on <html>; every colour below
   comes from a variable, so nothing else has to know which mode it is in. The eyes listen for the same switch. */

:root, :root[data-theme="light"]{
  --ink:#16161a;
  --ink-2:#55555f;
  --ink-3:#8a8a94;
  --line:#e6e6ea;
  --bg:#fff;
  --card:#fff;
  --prose:#35313f;
  --tile:#fff;
  --code:#f5f5f7;
  --accent:#d0206e;
  /* The M8 trio, same faces and weights M8 Magic City loads.
     Inter = body · JetBrains Mono = metadata and titles (the "OS/console" look) · Silkscreen = pixel accent. */
  --sans:'Inter',system-ui,-apple-system,"Segoe UI",Helvetica,Arial,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  --pixel:'Silkscreen','JetBrains Mono',monospace;
  --side:300px;
}

:root[data-theme="dark"]{
  --ink:#eceaf2;
  --ink-2:#a9a6b6;
  --ink-3:#76727f;
  --line:#26242e;
  --bg:#0b0913;
  --card:#100e19;
  --prose:#c3c0d0;
  --tile:#0b0913;
  --code:#171522;
  --accent:#ff92c2;
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;background:var(--bg);color:var(--ink);
  /* Same size as the dictionary entry in the sidebar, so the whole page reads at one scale. */
  font:13.5px/1.75 var(--sans);
  /* Light text on a dark ground renders HEAVIER under macOS subpixel antialiasing, which is what makes the
     body read as bold when it is only regular weight. Grayscale smoothing removes that thickening. */
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  text-rendering:optimizeLegibility;
}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}

.skip{position:absolute;left:-9999px}
.skip:focus{left:8px;top:8px;background:var(--card);padding:8px;z-index:10}

.page{display:grid;grid-template-columns:var(--side) 1fr;gap:0;min-height:100vh;max-width:1500px}

/* ---------- the directory, always visible ---------- */
.side{padding:52px 34px 60px;border-right:1px solid var(--line);position:sticky;top:0;align-self:start;max-height:100vh;overflow-y:auto}
.brand{display:block;font-family:var(--pixel);font-size:23px;font-weight:400;letter-spacing:.02em;line-height:1.25;color:var(--accent)}
.brand:hover{text-decoration:none}
/* A real dictionary entry. The WORDMARK above is the headword, so this starts at the grammar line.
   Numbers hang in the left margin the way a printed entry sets them. */
.entry{margin-top:8px}
.gram{margin:0;font-size:12.5px;color:var(--ink-3)}
.gram em{font-style:italic}
.gram .pron{font-family:var(--mono);font-size:12px;letter-spacing:-.02em;margin-left:6px}
ol.senses{list-style:none;counter-reset:sense;margin:9px 0 0;padding:0}
ol.senses li{
  counter-increment:sense;position:relative;padding-left:19px;
  font-size:13.5px;line-height:1.45;color:var(--ink-2);margin-bottom:6px;
}
ol.senses li::before{
  content:counter(sense);position:absolute;left:0;top:1px;
  font-family:var(--mono);font-size:11px;color:var(--ink-3);
}
.side nav{margin-top:44px}
.side ul{list-style:none;margin:0;padding:0}
.side nav > ul > li{border-top:1px solid var(--line)}
.side nav > ul > li:last-child{border-bottom:1px solid var(--line)}
.side nav a,.side nav summary{
  display:block;padding:11px 0;color:var(--ink);font-size:16px;cursor:pointer;
}
.side nav a:hover,.side nav summary:hover{color:var(--accent);text-decoration:none}
.side nav a.on{color:var(--accent)}
.side nav summary{list-style:none;display:flex;align-items:center;justify-content:space-between}
.side nav summary::-webkit-details-marker{display:none}
.side nav summary::after{content:"⌄";color:var(--ink-3);font-size:14px;line-height:1;transform:translateY(-2px)}
.side nav details[open] summary::after{content:"⌃";transform:translateY(2px)}
.side nav ul.sub{padding:0 0 8px 0}
.side nav ul.sub a{padding:7px 0 7px 14px;font-size:15px;color:var(--ink-2)}

/* ---------- content ---------- */
main{padding:52px 46px 120px;min-width:0}
/* No box. The content sits straight on the page, held only by a reading width. */
.card{max-width:760px}
.card > :first-child{margin-top:0}
h1{font-family:var(--mono);font-size:24px;font-weight:600;letter-spacing:-.01em;margin:0 0 20px}
h2{font-family:var(--mono);font-size:17px;font-weight:600;margin:36px 0 11px;color:var(--ink)}
h3{font-family:var(--mono);font-size:15px;font-weight:600;margin:28px 0 8px;color:var(--ink)}
p{margin:0 0 17px;color:var(--prose)}
/* Emphasis carries by WEIGHT and colour lift, not by a different hue. Bold sits at the heading colour so the
   eye picks it out when skimming; italic stays in the prose colour and only changes shape. */
strong{font-weight:600;color:var(--ink)}
em{font-style:italic}
ul li,ol li{color:var(--prose)}
hr{border:0;border-top:1px solid var(--line);margin:30px 0}
blockquote{margin:0 0 16px;padding-left:16px;border-left:2px solid var(--line);color:var(--ink-2)}
code{font:13.5px var(--mono);background:var(--code);padding:1px 5px;border-radius:4px}
pre{background:var(--code);padding:14px 16px;border-radius:10px;overflow-x:auto}
pre code{background:none;padding:0}
img{max-width:100%;height:auto;border-radius:8px}
/* Video embeds. Responsive by aspect-ratio rather than the old padding hack. */
.video{position:relative;aspect-ratio:16/9;margin:20px 0;border-radius:10px;overflow:hidden;background:var(--code)}
.video.short{aspect-ratio:9/16;max-width:340px}

/* Small thumbnails, not embedded players. One image each instead of one video player each, and nothing is
   requested from YouTube until a thumbnail is clicked. */
.vids{display:grid;grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:10px;margin:16px 0 26px}
.vid{
  position:relative;padding:0;border:1px solid var(--line);border-radius:8px;overflow:hidden;
  background:var(--code);cursor:pointer;aspect-ratio:16/9;display:block;
}
.vid img{width:100%;height:100%;object-fit:cover;display:block;border-radius:0;
  transition:transform .25s ease,opacity .2s}
.vid:hover img{transform:scale(1.05);opacity:.85}
.vid .play{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:0;height:0;border-style:solid;border-width:7px 0 7px 12px;
  border-color:transparent transparent transparent #fff;
  filter:drop-shadow(0 1px 3px rgba(0,0,0,.6));pointer-events:none;
}
.vid:hover{border-color:var(--accent)}

/* The player, opened over the page and torn down on close so nothing keeps playing behind you. */
.vplayer{
  position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.82);
  display:flex;align-items:center;justify-content:center;padding:24px;
}
.vframe{position:relative;width:min(960px,100%);aspect-ratio:16/9}
.vframe iframe{width:100%;height:100%;border:0;border-radius:10px}
.vclose{
  position:absolute;top:-38px;right:0;width:30px;height:30px;border-radius:999px;cursor:pointer;
  border:1px solid rgba(255,255,255,.35);background:transparent;color:#fff;font-size:17px;line-height:1;
}
.vclose:hover{border-color:#fff}
.video iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
.meta{color:var(--ink-3);font-size:14px}

/* The funding panel. Hairlines and mono labels rather than cards and shadows: the point is legibility of the
   numbers, not decoration. */
.funding{border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:16px 0 6px;margin:22px 0}
.fhead{display:flex;align-items:baseline;justify-content:space-between;gap:14px;flex-wrap:wrap}
.flabel{font:11px var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--accent)}
/* The figure AND its label carry the accent; only the funded-until line stays quiet beneath them. */
.ftreasury{font:11px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent);text-align:right}
.ftreasury i{display:block;font-style:normal;text-transform:none;letter-spacing:0;font-size:11.5px;margin-top:4px;color:var(--accent)}
.ftreasury b{font-family:var(--mono);font-size:20px;letter-spacing:-.01em;color:var(--accent);margin-right:6px}
.buckets{display:grid;grid-template-columns:1fr 1fr;gap:26px;margin-top:16px}
.bucket h3{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--ink);margin:0 0 5px}
.bucket p{margin:0 0 9px;font-size:12.5px;line-height:1.5;color:var(--ink-3)}
ul.costs{list-style:none;margin:0;padding:0}
ul.costs li{display:flex;align-items:center;gap:9px;padding:6px 0;border-top:1px solid var(--line);font-size:12.5px;color:var(--prose)}
ul.costs label{flex:1;cursor:pointer}
ul.costs input{width:13px;height:13px;flex:none;accent-color:var(--accent);cursor:pointer;margin:0}
ul.costs b{font-family:var(--mono);font-weight:400;color:var(--ink);white-space:nowrap}
ul.costs li:has(input:checked){color:var(--ink)}

/* The running total. Quiet until something is ticked, then it carries the accent. */
.ftotal{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-top:18px;padding-top:11px;border-top:1px solid var(--line);
  font:11px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent);
}
/* The number stays quiet until something is ticked; the label beside it is always accent. */
.ftotal b{font-size:20px;letter-spacing:-.01em;color:var(--ink-3);transition:color .15s}
.ftotal b.on{color:var(--accent)}
.fnote{margin:14px 0 8px;font-size:12px;color:var(--ink-3)}
@media(max-width:640px){.buckets{grid-template-columns:1fr;gap:20px}}

/* Tables. Addresses are long, unbreakable strings, so they get mono and are allowed to wrap mid-string
   rather than pushing the page sideways. */
table{width:100%;border-collapse:collapse;margin:18px 0;font-size:13px}
th,td{text-align:left;padding:9px 10px 9px 0;border-bottom:1px solid var(--line);vertical-align:top}
th{font:11px var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);font-weight:400}
td{color:var(--prose)}
td:last-child{font-family:var(--mono);font-size:12px;word-break:break-all;color:var(--ink)}
/* The address takes the space it needs and the button is pushed to the right edge, so the buttons line up
   down the column instead of following the end of each address. */
/* The address column takes the slack, so the buttons line up. The network column must then be told NOT to
   wrap, or it gets squeezed to its narrowest possible width and a name like "Ethereum, Base, Polygon" breaks
   across three lines. */
td:last-child{width:100%}
th:first-child,td:first-child{white-space:nowrap;padding-right:26px}
.addrrow{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}
.addrrow .addr{min-width:0;word-break:break-all}
button.copy{
  flex:none;margin-left:auto;vertical-align:1px;
  font:10px var(--mono);letter-spacing:.08em;text-transform:uppercase;
  border:1px solid var(--line);background:transparent;color:var(--ink-3);
  border-radius:999px;padding:2px 8px;cursor:pointer;white-space:nowrap;
}
button.copy:hover{border-color:var(--accent);color:var(--accent)}
button.copy.ok{border-color:var(--accent);color:var(--accent)}

/* post lists on topic pages */
ul.posts{list-style:none;margin:26px 0 0;padding:0}
ul.posts li{border-top:1px solid var(--line);padding:14px 0}
ul.posts a{font-size:17px}
ul.posts time{display:block;color:var(--ink-3);font-size:13px;font-family:var(--mono);margin-top:2px}
ul.posts .sum{display:block;color:var(--ink-2);font-size:14.5px;margin-top:4px}

/* the light/dark switch — one glyph, top right, above everything */
/* Top LEFT, in the sidebar, sitting on the same line as the page title on the right. The wordmark and its
   definition start below it. */
.themetoggle{
  display:flex;margin:0 0 30px;
  width:34px;height:34px;padding:0;cursor:pointer;
  /* SOLID. This is the only control on the page, so it is filled rather than outlined: the accent carries the
     button and the bulb is knocked out of it in the page colour. */
  border:1px solid var(--accent);border-radius:999px;
  background:var(--accent);color:var(--bg);
  align-items:center;justify-content:center;
  transition:color .15s,border-color .15s,transform .15s;
}
.themetoggle:hover{transform:rotate(-12deg);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 18%,transparent)}
.themetoggle svg{width:19px;height:19px;overflow:visible}
.themetoggle svg{stroke:currentColor;stroke-width:1.4;stroke-linecap:round;fill:none}
/* Under LIGHT the bulb is lit: filled glass, rays showing. Under DARK it is off: outline only, no rays. */
:root[data-theme="light"] .themetoggle .glass{fill:currentColor}   /* currentColor is --bg here, so the bulb reads as lit */
:root[data-theme="dark"] .themetoggle .rays{display:none}

/* In the DEFAULT (dark) mode the switch carries the accent colour, so the one control on the page reads as
   something to press rather than as another muted glyph. Under light the bulb is already lit and filled, so it
   holds its own and stays in the quiet ink. */


/* The same bulb inline in copy. Decorative: it mirrors the switch so the reader recognises what to press,
   but it is not a control and takes no pointer events. */
.bulbicon{
  width:15px;height:15px;vertical-align:-2px;margin-right:5px;pointer-events:none;
  fill:none;stroke:currentColor;stroke-width:1.4;stroke-linecap:round;
}
/* The icon in COPY is always the lit bulb, in both themes. It illustrates the sentence rather than reporting
   state: the switch shows what mode you are in, this shows what the sentence is talking about. */
.bulbicon .glass{fill:currentColor}
.bulbicon .rays{display:inline}

/* ---------- the eyes ---------- */
.gallery{margin:26px 0 0}
.gallery .more{height:1px}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:22px}
.eye{display:block;color:inherit}
.eyelink{display:block;color:inherit}
.eyelink:hover{text-decoration:none}
/* Scoped to .tile, NOT to .eye .tile: the share page draws a tile that is not inside a gallery card, and
   scoping it to .eye left that canvas with no aspect ratio and therefore no height. */
.tile{
  aspect-ratio:1/1;border-radius:12px;overflow:hidden;display:block;width:100%;
  background:var(--tile);border:1px solid var(--line);
}
.tile canvas{display:block;width:100%;height:100%}
.eye .cap{margin-top:9px}
.eye .txt{
  font-size:13.5px;line-height:1.4;color:var(--ink);
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  /* Always reserve BOTH lines, even when the post only fills one. Without this the stats row rides up on short
     posts and the numbers and share button sit at a different height in every card. */
  min-height:2.8em;
}
/* The stat line is used by the gallery cards AND the share page, so it must NOT be scoped to .eye. When it was,
   the share page got no sizing at all and the icons rendered at their natural size. */
.num{
  display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  font:11px var(--mono);color:var(--ink-3);margin-top:5px;letter-spacing:.02em;
}
.num .stat{display:inline-flex;align-items:center;gap:3px}
.num svg{width:11px;height:11px;fill:currentColor;opacity:.75;flex:none}
.eye:hover .txt{color:var(--accent)}
/* Pushed to the right of the stats line, on both the grid card and the share page. */
button.share{
  margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:4px;
  border:0;background:transparent;padding:2px;cursor:pointer;color:var(--ink-3);
  font:10px var(--mono);letter-spacing:.08em;text-transform:uppercase;
}
button.share svg{width:13px;height:13px;fill:currentColor;opacity:.75}
button.share .slabel{min-width:44px;text-align:left}
button.share:hover{color:var(--accent)}
button.share.ok{color:var(--accent)}

/* The share page is one gallery card, enlarged. Same order, same parts, one column, capped so the eye never
   outgrows the reading width. */
/* Same height as the switch in the sidebar, so the two sit on one line across the page. */
.back{
  display:inline-flex;align-items:center;gap:7px;height:34px;
  font-size:13px;color:var(--ink-3);
}
.back:hover{color:var(--accent);text-decoration:none}
/* The reading page's own header row. The second bulb only exists on a phone, where the sidebar one has
   scrolled out of reach by the time you are looking at the eye. */
.backrow{display:flex;align-items:center;justify-content:space-between;gap:14px}
.themetoggle.mini{display:none;margin:0;flex:0 0 auto}
/* One line of instruction, directly above the picture it is about. */
.illum{max-width:420px;margin:18px auto 0;text-align:center}
.illum .bulbicon{vertical-align:-3px;margin-right:4px}
#single{max-width:420px;margin:34px auto 0}
#solometa{max-width:420px;margin:14px auto 0}
.author{
  max-width:420px;margin:13px auto 0;
  font:12px var(--mono);color:var(--ink-3);letter-spacing:.02em;
}
.author:empty{display:none}
h1.rtitle{
  max-width:420px;margin:3px auto 0;
  font-family:var(--sans);font-size:14.5px;font-weight:400;line-height:1.5;color:var(--ink);
}
.solinks{max-width:420px;margin:22px auto 0;font-size:12.5px}

/* The menu button. Desktop has room for the whole directory, so it only exists on a phone. */
.burger{display:none}

/* ---------- small screens: the entry stays, the directory folds away ---------- */
/* A phone opens on the word and its definition, then the gallery. The directory is one tap away rather than a
   screen of links to scroll past before anything is seen. */
@media(max-width:860px){
  .page{grid-template-columns:1fr}
  /* a reading page shows exactly one bulb, and it is the one beside the way back */
  .page.single .side .themetoggle{display:none}
  .themetoggle.mini{display:flex}
  .side{position:relative;max-height:none;border-right:0;border-bottom:1px solid var(--line);padding:26px 22px 22px}
  main{padding:26px 22px 90px}
  .grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px}

  /* the switch and the menu button share the top line, one at each end */
  .themetoggle{margin-bottom:18px}
  .burger{
    display:flex;flex-direction:column;justify-content:center;gap:5px;
    position:absolute;top:26px;right:18px;width:34px;height:34px;
    padding:0;border:0;background:transparent;cursor:pointer;
  }
  .burger span{display:block;width:22px;height:1.5px;background:var(--ink);border-radius:2px;
    transition:transform .18s ease,opacity .12s ease}
  .side nav{display:none;margin-top:24px}
  .side.navopen nav{display:block}
  /* open state: the three bars become a cross, so the button says how to close itself */
  .side.navopen .burger span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
  .side.navopen .burger span:nth-child(2){opacity:0}
  .side.navopen .burger span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}
}


/* The CV header above the chart. */
.profile{border-bottom:1px solid var(--line);padding-bottom:20px;margin-bottom:24px}
/* One line above the chart, set in the body face: it is a sentence, not a label. */
.lede{border-bottom:1px solid var(--line);padding-bottom:20px;margin-bottom:24px}
/* Each line of the haiku is its own paragraph, so it breaks where it is meant to. */
.lede p{margin:0;line-height:1.55}
/* Verse: one sentence per line, so the copy reads as lines rather than a block. */
.verse{margin-bottom:16px}
.verse p{margin:0;line-height:1.75}
.profile p:first-child{font-size:15px;color:var(--ink);margin-bottom:12px}
.profile p:last-child{margin-bottom:0;font:11px var(--mono);letter-spacing:.06em;color:var(--ink-3)}

/* ---------- the timeline chart ---------- */
/* Tabs, then a cartesian chart: categories down y, time along x. Drawn as SVG so the labels stay real text and
   everything stays crisp at any zoom. */
.timeline{margin:22px 0 30px}
/* Sits UNDER the chart as a legend. No active underline: the dot already says what is showing. */
.tltabs{display:flex;flex-wrap:wrap;gap:8px 26px;border-top:1px solid var(--line);padding-top:13px;margin-top:14px}
.tltabs button{
  border:0;background:transparent;padding:0;cursor:pointer;
  font:11px var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3);
}
.tltabs button{display:inline-flex;align-items:center;gap:7px}
.tlbreak{flex-basis:100%;height:0}
/* A filled dot when the group is showing, a hollow ring when it is hidden. */
.tltabs button i{width:7px;height:7px;border-radius:999px;border:1px solid currentColor;background:transparent}
.tltabs button:hover{color:var(--ink-2)}
.tltabs button.on{color:var(--ink)}
.tltabs button.on i{background:var(--accent);border-color:var(--accent)}

.tlstage{overflow-x:auto}
.tlsvg{width:100%;min-width:640px;height:auto;display:block}

.tlgrid{stroke:var(--line);stroke-width:1}
.tlrow{stroke:var(--line);stroke-width:1}
.tlaxis{stroke:var(--ink-3);stroke-width:1;fill:var(--ink-3)}
.tlyear,.tlrowlab{font:10.5px var(--mono);fill:var(--ink);letter-spacing:.06em}
.tlaxlab{font-style:italic;font-size:13px;fill:var(--ink-3)}
.tlaxdesc{font:9.5px var(--mono);fill:var(--ink-3);letter-spacing:.08em;opacity:.8}
.tlcaption{margin:10px 0 0;font:10.5px var(--mono);color:var(--ink-3);letter-spacing:.04em}
.tlnow{stroke:var(--ink-3);stroke-width:1;stroke-dasharray:3 4;opacity:.7}
.tlnowlab{font:9.5px var(--mono);fill:var(--ink-3);letter-spacing:.14em}
.tlmarker{stroke:var(--ink-3);stroke-width:1;stroke-dasharray:3 4;opacity:.7}
.tlmarker.pink{stroke:#ff5fa2;opacity:.4}
.tlmarkerlab{font:9.5px var(--mono);fill:var(--ink-3);letter-spacing:.14em}
.tlmarkerlab.pink{fill:#ff5fa2;opacity:.75}

/* THE LENS. Holding a skill lights the work it came out of and lets everything else fall back, so the
   answer is read off the career itself. Nothing moves: only weight changes. */
.tlbar,.tllabel{transition:opacity .18s}
.tlbar.dim,.tllabel.dim{opacity:.16}
.tlbar.lit .tlspan{stroke:var(--accent);stroke-width:3.4}
.tlbar.lit .tlend{stroke:var(--accent)}
.tlbar.lit .tlarrow{fill:var(--accent)}
.tllabel.lit .tlname{fill:var(--accent)}

.tlspan{stroke:var(--ink);stroke-width:2.5;stroke-linecap:round}
.tlend{fill:var(--bg);stroke:var(--ink);stroke-width:2}
.tlarrow{fill:var(--ink)}
.tlmark{fill:var(--accent)}
.tllabel{fill:var(--ink)}
.tlname{font-size:12.5px}
.tlrole{font:9.5px var(--mono);fill:var(--ink);letter-spacing:.1em}

/* Hover or focus an item and it takes the accent, with a dotted line back to the axis. */
.tlcross{stroke:var(--accent);stroke-width:1;stroke-dasharray:2 4;opacity:0;transition:opacity .15s}
.tlitem{cursor:default;outline:none}
.tlitem:hover .tlcross,.tlitem:focus .tlcross,.tlitem.hot .tlcross{opacity:.75}
.tlitem:hover .tlspan,.tlitem:focus .tlspan,.tlitem.hot .tlspan{stroke:var(--accent)}
.tlitem:hover .tlend,.tlitem:focus .tlend,.tlitem.hot .tlend{stroke:var(--accent)}
.tlitem:hover .tlarrow,.tlitem:focus .tlarrow,.tlitem.hot .tlarrow{fill:var(--accent)}
.tlitem:hover .tlname,.tlitem:focus .tlname,.tlitem.hot .tlname{fill:var(--accent)}

/* ---------------------------------------------------------------------------- the contact form ----- */
/* One form, used by every page that used to carry a Telegram handle. Fields are full width because the
   message is the point; the honeypot is off-screen rather than display:none, which some bots skip. */
.cform{margin:18px 0 0;display:flex;flex-direction:column;gap:6px}
/* display:flex would otherwise beat the browser's own [hidden] rule, so the form stayed on screen after
   sending, with the confirmation jammed under a frozen button. */
.cform[hidden]{display:none}
.cform label{font:10.5px var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ink-3);margin-top:10px}
.cform .req{color:var(--accent);letter-spacing:.08em}
.cform input,.cform textarea{
  width:100%;box-sizing:border-box;background:transparent;color:var(--ink);
  border:1px solid var(--line);border-radius:2px;padding:9px 10px;font:14px/1.5 inherit;
}
.cform textarea{resize:vertical;min-height:120px}
.cform input:focus,.cform textarea:focus{outline:none;border-color:var(--accent)}
.cform button{
  align-self:flex-start;margin-top:16px;cursor:pointer;
  border:1px solid var(--ink);background:var(--ink);color:var(--bg);
  font:11px var(--mono);letter-spacing:.16em;text-transform:uppercase;padding:9px 18px;border-radius:2px;
}
.cform button[disabled]{opacity:.5;cursor:default}
.cerr{margin:12px 0 0;font-size:13px;color:var(--accent)}
/* The confirmation stands where the form was, so it needs the breathing room the form had. */
.cdone{margin-top:44px}
.cdone p{margin:0;line-height:1.6}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}

/* REFERENCE MARKS ---------------------------------------------------------
   Raised numerals in running copy, the way a paper sets them. `line-height:0`
   is the important part: without it a <sup> stretches the line it sits on and
   the paragraph's rhythm changes wherever a citation happens to fall. */
.ref{
  font-family:var(--mono);
  font-size:.62em;
  line-height:0;
  vertical-align:super;
  color:var(--accent);
  padding-left:.1em;
  letter-spacing:.02em;
}
/* Sources list: quieter than body copy, and the long titles wrap without
   colliding with the numeral. */
h2 + ol{ font-size:.92em; }

/* The one-line argument above the research chart. Monospaced and quiet, so it
   reads as a diagram rather than as another paragraph, and wraps on the arrows
   instead of running off a phone. */
.chain{margin:18px 0 22px;padding:14px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.chain p{margin:0;font:11.5px/2 var(--mono);letter-spacing:.06em;color:var(--ink-3);text-transform:uppercase}

/* THE RESEARCH LINE ---------------------------------------------------------
   One rule down the middle: steps to its left, the argument bracketed on its
   right, topics underneath as a legend. Vertical because fifteen labels laid
   out horizontally need ~1180px before they stop colliding.

   Shared with /making-of/ rather than restated: .tlstage, .tltabs, .tlsvg,
   .tlyear and .tlrole all come from there, so both charts keep the same size
   and weight without being kept in sync by hand. */
.phdwrap{margin:26px 0 34px}
/* Overrides .tlsvg's min-width, which exists to stop the career chart squashing.
   This one has nothing to squash, so it must never produce a scrollbar. */
.phdstage{overflow:visible}
/* Centred in the column, so the rule runs down the middle of the page. */
.phdsvg{min-width:0;max-width:1020px;margin-inline:auto}

.phdline-rule{stroke:var(--ink);stroke-width:1.5}
.phddot{fill:var(--bg);stroke:var(--ink);stroke-width:2;transition:fill .18s,stroke .18s}
.phdname{font-size:13px;fill:var(--ink)}
.phdnum{opacity:.7}

/* The argument, bracketed on the right. Quiet enough to read as annotation
   rather than as a second column of content. */
.phdsecbracket{fill:none;stroke:var(--line);stroke-width:1}
.phdsec{font:11px var(--mono);letter-spacing:.09em;text-transform:uppercase;fill:var(--ink-3)}

/* The coarser cut: a hairline where the second part begins, taking no height. */
.phdpartrule{stroke:var(--line);stroke-width:1}
.phdpartlab{fill:var(--ink-3)}

/* THE LENS. Holding a topic lights the whole step it belongs to: dot, numeral
   and name together. Only colour changes, never weight or position, so nothing
   reflows and the column stays exactly where it was. */
.phdname,.phdnum{transition:fill .18s,opacity .18s}
.phdstep.lit .phddot{fill:var(--accent);stroke:var(--accent)}
.phdstep.lit .phdname{fill:var(--accent)}
.phdstep.lit .phdnum{fill:var(--accent);opacity:1}

/* ---- The three layers of reality, drawn ----------------------------------------------------
   A hairline pyramid with the three tiers named over it. The SVG is stretched with
   preserveAspectRatio=none and its divider lines sit at exact thirds, so the bands and the three
   text rows line up at every width without any measuring. Apex row first. */
.pyramid{position:relative;margin:1.8rem auto;max-width:34rem;padding:0}
.pyramid svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
.pyramid polygon{fill:none;stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke}
.pyramid line{stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke}
.pyramid .tiers{position:relative;display:grid;grid-template-rows:repeat(3,1fr);
  min-height:19.5rem;text-align:center}
.pyramid .tier{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.15rem;
  padding:0 1rem}
.pyramid .tier i{font:400 .6rem/1 var(--pixel);letter-spacing:.14em;color:var(--ink-3);font-style:normal}
.pyramid .tier b{font:500 .95rem/1.2 var(--mono);letter-spacing:.06em;color:var(--ink)}
/* The examples. Small enough that the apex row still fits, and if it grazes the hairline at the very
   top that reads as intentional rather than broken. */
.pyramid .tier em{font:400 .68rem/1.35 var(--mono);letter-spacing:.02em;color:var(--ink-2);font-style:normal}
.pyramid .tier span{font:400 .74rem/1.35 var(--sans);color:var(--ink-3);max-width:15rem}
/* The base band is the widest, so it can carry the longest line without crowding the edges. */
.pyramid .tier:last-child span{max-width:19rem}

/* ---------- the follow readout, bottom right ---------- */
:root{ --rainbow:linear-gradient(90deg,#f2b3d0,#f6c99a,#ffe9a8,#a6e6cf,#a6c8f0,#b8a6e6); }
.hud{
  position:fixed;right:24px;bottom:24px;width:252px;z-index:40;text-align:left;
  background:color-mix(in srgb,var(--card) 86%,transparent);
  border:1px solid var(--line);border-radius:10px;
  box-shadow:0 24px 60px -28px rgba(0,0,0,.45);
  backdrop-filter:blur(10px);
  opacity:0;transform:translateY(10px);transition:opacity .26s ease,transform .26s ease;
}
.hud.in{opacity:1;transform:translateY(0);animation:hudfloat 7s ease-in-out 1.2s infinite}
@keyframes hudfloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.hud__bar{
  display:flex;align-items:center;justify-content:space-between;
  padding:7px 11px;border-bottom:1px solid var(--line);
  font-family:var(--pixel);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);
}
.hud__dots{display:flex;gap:6px;align-items:center}
.hud__dots i{width:9px;height:9px;border-radius:50%;background:var(--accent);opacity:.45}
.hud__close{
  width:12px;height:12px;padding:0;border:0;border-radius:50%;background:var(--accent);color:var(--card);
  display:grid;place-items:center;line-height:1;cursor:pointer;
  font-family:var(--mono);font-size:8px;font-weight:700;transition:transform .12s;
}
.hud__close:hover{transform:scale(1.18)}
.hud__body{padding:11px 14px;font-family:var(--mono);font-size:11.5px;line-height:1.9}
.hud__lvl{display:flex;justify-content:space-between;align-items:baseline;gap:8px;color:var(--ink-2);white-space:nowrap}
.hud__lvl a{color:var(--accent);white-space:nowrap}
.hud__stat{margin:6px 0}
.hud__k{
  display:flex;justify-content:space-between;font-family:var(--pixel);font-size:9px;
  letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);
}
.hud__track{height:6px;border-radius:999px;overflow:hidden;display:flex;margin-top:4px}
.hud__track b{background:var(--rainbow)}
.hud__track u{flex:1;background:repeating-conic-gradient(var(--ink) 0 25%,var(--card) 0 50%) 0 0/6px 6px;opacity:.5}
@media (max-width:820px){ .hud{display:none} }
@media (prefers-reduced-motion:reduce){ .hud.in{animation:none} }
/* collapsed state: the readout folds into a pill in the same corner */
.hud{transform-origin:calc(100% - 22px) calc(100% - 22px)}
.hud:not(.in){transform:translateY(0) scale(.24);opacity:0}
.hudpill{
  position:fixed;right:24px;bottom:24px;z-index:40;
  /* matches .themetoggle: same 34px circle, same accent ring */
  width:34px;height:34px;padding:0;border-radius:999px;cursor:pointer;
  border:1px solid var(--accent);background:color-mix(in srgb,var(--card) 86%,transparent);
  backdrop-filter:blur(10px);box-shadow:0 18px 44px -22px rgba(0,0,0,.45);
  color:var(--accent);font-size:14px;line-height:1;display:grid;place-items:center;
  opacity:0;transform:scale(.4);transition:opacity .24s ease,transform .24s cubic-bezier(.2,1.5,.4,1);
}
.hudpill.in{opacity:1;transform:scale(1);animation:hudfloat 7s ease-in-out 1.2s infinite}
.hudpill::after{
  content:"";position:absolute;inset:-7px;border-radius:50%;
  background:var(--rainbow);opacity:.5;z-index:-1;
  /* the fade is a radial mask, not filter:blur. blur inside a backdrop-filter
     ancestor re-rasterises on transform and the gradient's square edges show. */
  -webkit-mask:radial-gradient(circle,#000 46%,transparent 72%);
          mask:radial-gradient(circle,#000 46%,transparent 72%);
  transition:opacity .15s;
}
.hudpill:hover{transform:scale(1.09)}
.hudpill:hover::after{opacity:.62}
@media (max-width:820px){ .hudpill{display:none} }
@media (prefers-reduced-motion:reduce){ .hudpill.in{animation:none} }

/* ---------- the phone feed ---------- */
/* Under 860px the gallery grid is replaced by a full-screen snap feed. The whole page becomes the
   scroller: the sidebar is card 0 (the word and its definition), then the CTA, then one reading per
   screen, looping. docs/mobile-feed-spec.md */
@media(max-width:860px){
  html.feedmode, html.feedmode body{height:100%;overflow:hidden}
  html.feedmode .page{
    height:100dvh;overflow-y:auto;overflow-x:hidden;
    scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;
    scrollbar-width:none;
  }
  html.feedmode .page::-webkit-scrollbar{display:none}

  /* card 0 — the word and the definition, centred on its own screen */
  html.feedmode .side{
    overflow:visible;min-height:100dvh;scroll-snap-align:start;scroll-snap-stop:always;
    display:flex;flex-direction:column;justify-content:center;
    border-bottom:0;padding:26px 22px 60px;
  }
  /* the two controls leave the flow and sit over the feed */
  html.feedmode > body > .themetoggle,
  html.feedmode .side .themetoggle{position:fixed;top:18px;left:18px;z-index:46;margin:0}
  html.feedmode .burger{position:fixed;top:18px;right:18px;z-index:46;display:flex}
  /* the directory opens as a full sheet over the feed rather than pushing card 0 taller */
  html.feedmode .side.navopen nav{
    position:fixed;inset:0;z-index:44;margin:0;padding:70px 22px 40px;
    background:var(--bg);overflow-y:auto;
  }

  html.feedmode main{padding:0}
  html.feedmode main > .card{padding:0;border:0;background:transparent}

  .feed{display:block}
  /* ONE reserved zone at the foot of every card. The buttons live in it, nothing else ever enters it,
     and every card keeps the same clear band whether the post is one line or forty. Change --bar-zone
     and the padding, the bar's position and the text's ceiling all move together. */
  .feed,.factbar{
    --bar-gap:26px;      /* how far the bar sits off the bottom edge */
    --bar-h:40px;        /* the pills' own height */
    --bar-air:40px;      /* the guaranteed breathing room above them */
    --bar-zone:calc(var(--bar-gap) + var(--bar-h) + var(--bar-air));
  }
  .fcard{
    position:relative;z-index:0;
    min-height:100dvh;scroll-snap-align:start;scroll-snap-stop:always;
    display:flex;flex-direction:column;justify-content:center;
    padding:64px 22px var(--bar-zone);box-sizing:border-box;
  }
  /* the eye is the card: it takes the width it can and stays square */
  .fcard .tile{width:min(100%,46dvh);margin:0 auto}
  /* A long post scrolls inside its own box. No fade: the title being swallowed by a gradient reads as
     a bug rather than as a hint. */
  /* A FIXED height, not a maximum: every card then has identical geometry, so the band under the text
     is the same on a one-line post and a forty-line one. overscroll-behavior is auto on purpose. With
     contain, reaching the end of a long post stopped the gesture dead instead of carrying on into the
     next card, which felt like the feed had jammed. */
  /* NO nested scrolling. Four lines and then "read more", which opens the aura's own page. A scroll
     box inside a snap feed is a fight between two gestures and the feed always wins.
     The height is fixed at four lines plus the link, so every card has the same geometry whether the
     post is one line or forty. */
  /* the same bounds as the aura and the meta line above it, so the card is one column */
  .fcard .cap{width:min(100%,46dvh);margin:18px auto 0;height:calc(4 * 1.5em + 1.9em)}
  .fcard .txt{
    display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;
  }
  /* bottom right of the text block, where a reader's eye already is when the fourth line runs out */
  .fmore{
    display:block;width:100%;text-align:right;margin-top:.5em;
    border:0;background:transparent;padding:0;cursor:pointer;
    font:11px var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--accent);
  }
  /* Expanded in place: the card grows and the feed carries on scrolling through it. No box inside a
     box, so there is still only one gesture. */
  .fcard .cap.open{height:auto}
  .fcard .cap.open .txt{display:block;-webkit-line-clamp:unset;overflow:visible}
  .fcard:has(.cap.open){padding-bottom:calc(var(--bar-zone) + 30px)}
  /* No spacer and no special case: the reserved zone already guarantees the gap, so the last line of
     a long post stops in the same place a short post's does. */
  .fcard .cap::-webkit-scrollbar{display:none}
  /* justified to the column, last line left: the block reads as one shape with the aura above it
     rather than as a floating caption */
  .fcard .txt{
    font-size:15px;line-height:1.5;min-height:0;
    text-align:justify;text-align-last:left;
    /* Justification has no maximum gap of its own: it takes whatever space the line has left. The only
       real cure in a narrow column is letting long words break, which gives the engine more places to
       end a line. html carries lang="en", which hyphenation needs. */
    -webkit-hyphens:auto;hyphens:auto;
    hyphenate-limit-chars:6 3 3;      /* break shorter words too: more break points, smaller gaps */
    word-spacing:-.012em;             /* the base gap the justifier then stretches, nudged tighter */
  }
  /* flush with the eye's left edge, above it */
  .fcard .num{
    width:min(100%,46dvh);margin:0 auto 10px;justify-content:flex-start;font-size:11.5px;
  }
  /* FIXED to the screen, like the bulb and the menu. One bar for the whole feed: it retargets itself
     to whichever reading is in the middle of the screen, so it never scrolls away from the thumb. */
  .factbar{
    position:fixed;left:0;right:0;bottom:var(--bar-gap);z-index:38;
    display:flex;justify-content:center;align-items:stretch;gap:9px;
    opacity:0;pointer-events:none;transition:opacity .2s ease;
  }
  .factbar.in{opacity:1;pointer-events:auto}
  /* SOLID, not transparent: a reading scrolling behind a see-through pill made both unreadable. */
  .fopen,.flike,.fshare{
    position:relative;overflow:hidden;
    display:flex;align-items:center;justify-content:center;
    font:11px var(--mono);letter-spacing:.14em;text-transform:uppercase;
    color:var(--accent);background:var(--bg);border:1px solid var(--accent);
    border-radius:999px;padding:9px 18px;cursor:pointer;
  }
  /* A sheen travelling across the three, half a second apart, so it reads as one wave. The arrows on
     the right work in opacity, so this uses a different channel and the two do not compete. */
  /* The same idea as the arrows on the right, laid on its side: each pill lights in turn and the wave
     runs left to right. Same cycle and same stagger, so the two read as one language rather than two
     competing animations. */
  .fopen::after,.fshare::after,.flike::after{
    content:"";position:absolute;inset:0;pointer-events:none;border-radius:inherit;
    background:color-mix(in srgb,var(--accent) 26%,transparent);
    opacity:0;
  }
  /* a beat of stillness first: the bar arrives, settles, and only then introduces itself */
  .factbar.in .fopen::after,.factbar.in .fshare::after,.factbar.in .flike::after{
    animation:pillchase 11s ease-in-out 2.4s infinite;
  }
  .factbar.in .fshare::after{animation-delay:2.68s}
  .factbar.in .flike::after{animation-delay:2.96s}
  /* the wave itself is still quick; the rest of the cycle is deliberately empty */
  @keyframes pillchase{0%,13%,100%{opacity:0}4.5%{opacity:1}}
  .fopen:hover{text-decoration:none}
  /* icon only, the same pill as the heart: two actions, two glyphs, one shape */
  .fshare{padding:9px 13px}
  .fshare svg{width:14px;height:14px;display:block;fill:currentColor}
  .fshare.ok{background:var(--accent);color:var(--bg)}
  .flike{padding:9px 13px}
  .flike svg{width:14px;height:14px;display:block;fill:none;stroke:currentColor;stroke-width:1.7}
  /* liked: the heart fills, and the button carries the accent so it reads at a glance */
  .flike.on{background:var(--accent);color:var(--bg)}
  .flike.on::after{background:rgba(255,255,255,.34)}
  .flike.on svg{fill:currentColor;stroke:currentColor}
  .flike.pop{animation:likepop .32s cubic-bezier(.2,1.6,.4,1)}
  @keyframes likepop{0%{transform:scale(1)}45%{transform:scale(1.22)}100%{transform:scale(1)}}
  @media (prefers-reduced-motion:reduce){
    .flike.pop{animation:none}
    .factbar.in .fopen::after,.factbar.in .fshare::after,.factbar.in .flike::after{animation:none;display:none}
  }

  /* the two interstitials share a face */
  .fcta__in{max-width:340px;margin:0 auto;text-align:center}
  .fk{font:10px var(--pixel);letter-spacing:.2em;text-transform:uppercase;color:var(--accent);margin:0 0 18px}
  .fh{font-family:var(--mono);font-size:19px;line-height:1.35;color:var(--ink);margin:0 0 12px}
  .fb{font-size:14px;line-height:1.6;color:var(--ink-2);margin:0 0 26px}
  .fbtn{
    display:inline-block;font:11px var(--mono);letter-spacing:.14em;text-transform:uppercase;
    color:var(--bg);background:var(--accent);border:1px solid var(--accent);
    border-radius:999px;padding:11px 20px;
  }
  .fbtn:hover{text-decoration:none}
  /* the mint card carries the rainbow, so it reads as the other kind of invitation */
  /* the rainbow is a pastel in both themes, so the label is dark in both. --ink flips and would vanish. */
  .fmint .fbtn{color:#16161a;background:var(--rainbow);border-color:transparent}
  .fmint .fk{color:var(--ink-3)}

  .ftail{height:1px}

  /* A SHARED LINK on a phone. The same card as in the feed, with the page's own reading furniture
     out of the way. There is nothing to snap to, so the card is simply the screen. */
  html.solofeed .backrow,html.solofeed .illum,html.solofeed #single,
  html.solofeed #solometa,html.solofeed #soloauthor,
  html.solofeed .rtitle,html.solofeed .solinks{display:none}
  html.solofeed .side{display:none}
}

/* ---------------------------------------------------------------------------------------------------------
 * SCROLL CUE. Three magic arrows down the right edge, chasing one after another.
 *
 * An IDLE cue rather than a first-run one: it leaves the moment anything moves and returns after three
 * seconds of stillness. It also only appears when there is something below, so it never lies.
 * ------------------------------------------------------------------------------------------------------ */
.scrollcue{
  position:fixed;right:20px;top:50%;z-index:39;transform:translateY(-50%);
  display:flex;flex-direction:column;align-items:center;gap:0;
  color:var(--accent);line-height:.56;opacity:0;pointer-events:none;
  transition:opacity .45s ease;
}
/* the chase keeps its own light-to-dark ratio; the whole cue just sits 20% quieter */
.scrollcue.in{opacity:.64}
/* The glyph's ink sits well inside its em box and off to one side, so rotating it left the arrows
   looking clipped against the edge. Give each one a square box and centre the ink in it, then the
   position is the box's position and nothing depends on the font's side bearing. */
.scrollcue i{
  display:grid;place-items:center;font-style:normal;font-size:68px;
  width:1em;height:.44em;line-height:1;
  transform:rotate(-90deg);                      /* the same magic arrow, pointed up: swipe up for more */
  opacity:0;
}
.scrollcue.in i{animation:cuechase 5.5s ease-in-out infinite}
.scrollcue.in i:nth-child(1){animation-delay:.56s}
.scrollcue.in i:nth-child(2){animation-delay:.28s}
.scrollcue.in i:nth-child(3){animation-delay:0s}
/* Quick wave, long pause. The percentages are doubled against the 5.5s cycle so each pulse keeps the
   same real duration while coming round twice as often. It goes to ZERO between passes: a permanent
   faint arrow is furniture, and the cue is meant to arrive and leave. */
@keyframes cuechase{0%,26%,100%{opacity:0}9%{opacity:1}}
@media (prefers-reduced-motion:reduce){ .scrollcue.in i{animation:none;opacity:.55} }
/* Desktop scrolls with a wheel and a trackpad, so the swipe cue is phone-only. */
@media (min-width:861px){ .scrollcue{display:none} }

/* A page held back while its text is still being worked on. See `coming_soon` in build.mjs. */
.soon{
  font-family:var(--mono);font-size:13px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--ink-3);margin:34px 0 0;
}
