/* 13 — Quarto.
   Keeps the principles of the reference tradition (large display against tiny labels,
   strict grid, hairlines, generous whitespace, restrained motion) and replaces every
   measured signature of the reference: no left rail, no centred 672px column, no pixel
   display face, no "01 —" labels, no dot fields, no rotated project fan, no pure white,
   no bottom-of-rail control cluster. See doc/monograph-safe-plan.md for the swap table. */

.qt{
  --paper:#f1f1ee;
  --paper2:#e9e9e4;
  --ink:#12140f;
  --soft:#3c4038;
  --faint:#6a6c63;   /* 4.71:1 on the paper — the old #8b8d84 was 2.97:1 and failed AA */
  --rule:#d5d5cd;
  --rule-soft:#e2e2da;
  --moss:#4c5f3d;
  --grain:.035;
  margin:0;
  background:var(--paper);
  color:var(--ink);
  font-family:Geist,system-ui,sans-serif;
  font-size:17.5px;
  line-height:1.72;
  /* Keep the ink from thinning out on light backgrounds, and let the browser avoid
     leaving a single word alone on the last line of a paragraph or heading. */
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
  text-wrap:pretty;
}
/* Reading measure. Prose was capped between 46ch and 56ch in a dozen places, which is
   narrower than it needs to be and made the same text feel denser than it is. One
   variable now sets it, at the top of the comfortable range rather than the bottom. */
.qt{--measure:64ch;--measure-tight:58ch}

/* Shared components (writing index, exposure panel, community room) are authored against
   the --dlg-* names because they were born inside the dialog. They also render on the
   page routes, where those names did not exist, so every rule that used one silently did
   nothing. Aliasing the page's own palette onto the same names lets one set of component
   rules serve both, and .mj-dialog still overrides them with its own values inside the
   modal. See doc/plan.md: .tag-list had exactly this bug before. */
.qt{
  --dlg-bg:var(--paper);
  --dlg-raise:var(--paper2);
  --dlg-hover:var(--paper2);
  --dlg-ink:var(--ink);
  --dlg-muted:var(--soft);
  --dlg-faint:var(--faint);
  --dlg-line:var(--rule);
  --dlg-accent:var(--moss);
  --dlg-accent-ink:var(--paper);
  --dlg-ok:var(--moss);
  --dlg-ok-bg:var(--paper2);
  --dlg-bad:#b4442c;
  --dlg-bad-bg:var(--paper2);
  --dlg-warn:#8a6a1f;
  --dlg-code-bg:var(--paper2);
  --dlg-code-ink:var(--ink);
  --dlg-focus:var(--moss);
  --dlg-range:var(--moss);
}
:root[data-theme="dark"] .qt{
  --paper:#101210;
  --paper2:#171a16;
  --ink:#e9eae2;
  --soft:#c0c3b8;
  --faint:#7d8177;
  --rule:#282c26;
  --rule-soft:#1f231e;
  --moss:#9dbb84;
  --grain:.05;
}

/* The shared dialog is a child of <body class="qt">, so it inherits the page but not the
   page's palette: its tokens live on .mj-dialog itself and defaulted to the light set.
   That left every project, credential, game and the palette opening as a white sheet
   behind a near-white scrim on a #101210 page — the harshest moment on the site, and one
   the body-level theme check could not see because it never opened a dialog.
   The values below are Quarto's own dark palette, re-expressed in the dialog's tokens. */
:root[data-theme="dark"] .qt .mj-dialog{
  --dlg-bg:#171a16;
  --dlg-raise:#1f231e;
  --dlg-hover:#232722;
  --dlg-ink:#e9eae2;
  --dlg-muted:#c0c3b8;   /* 9.8:1 on the dialog */
  --dlg-faint:#8d9186;   /* 5.5:1 — still AA at 11.5px */
  --dlg-line:#2c3129;
  --dlg-accent:#9dbb84;
  --dlg-accent-ink:#12140f;
  --dlg-ok:#8fc98a;
  --dlg-ok-bg:#1c2a1c;
  --dlg-bad:#e08d7a;
  --dlg-bad-bg:#2e1c18;
  --dlg-warn:#e0be6a;
  --dlg-code-bg:#0d100c;
  --dlg-code-ink:#d7e3cc;
  --dlg-scrim:#07080799;
  --dlg-shadow:0 24px 90px #00000073;
  --dlg-focus:var(--moss);
  --dlg-range:var(--moss);
}
.qt *{box-sizing:border-box}
.qt a{color:inherit;text-decoration:none}
/* Navigation and controls are named by their position; a link inside a sentence is
   not, so it gets an underline set clear of the descenders rather than a colour
   change alone, which is invisible to anyone who cannot separate the two. */
.qt-measure p a,.qt-colophon p a,.qt-page .card-summary a,.qt-page li a{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;
  text-decoration-color:var(--rule)}
.qt-measure p a:hover,.qt-colophon p a:hover,.qt-page .card-summary a:hover,.qt-page li a:hover{
  text-decoration-color:var(--moss)}
.qt button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
.qt :focus-visible{outline:2px solid var(--moss);outline-offset:3px}
.qt h1,.qt h2,.qt h3,.qt h4{margin:0;font-weight:400;text-wrap:balance}
.qt h1,.qt .qt-page-title{letter-spacing:-.022em}
.qt h2{letter-spacing:-.018em}
.qt p{margin:0 0 1.15em}
.qt-measure p,.qt-page .about-para p{max-width:var(--measure)}
.qt .icon{width:15px;height:15px}
.qt .skip-link{position:fixed;left:16px;top:-90px;background:var(--ink);color:var(--paper);padding:10px 16px;z-index:120}
.qt .skip-link:focus{top:12px;transform:none}

/* Grain replaces the reference's radial dot fields. */
.qt-grain{position:fixed;inset:0;z-index:1;pointer-events:none;opacity:var(--grain);mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)'/%3E%3C/svg%3E")}
:root[data-theme="dark"] .qt-grain{mix-blend-mode:screen}

/* A very slow wash under the grain. Two soft pools drift against each other; nothing reads as movement
   unless you look for it, which is the point. */
.qt-wash{position:fixed;inset:-20%;z-index:0;pointer-events:none;opacity:.5;
  background:radial-gradient(38% 34% at 22% 18%,color-mix(in srgb,var(--moss) 13%,transparent),transparent 70%),
             radial-gradient(42% 38% at 82% 74%,color-mix(in srgb,var(--ink) 7%,transparent),transparent 72%);
  animation:qt-wash 54s ease-in-out infinite alternate}
@keyframes qt-wash{from{transform:translate3d(-2%,-1.5%,0) scale(1)}to{transform:translate3d(2.5%,2%,0) scale(1.08)}}
:root[data-theme="dark"] .qt-wash{opacity:.7}

/* Reading progress: one hairline, drawn under the bar, scaled from the scroll position. */
.qt-progress{position:fixed;top:var(--bar-h,60px);left:0;right:0;height:1px;z-index:41;background:var(--moss);
  transform:scaleX(0);transform-origin:left;pointer-events:none;opacity:.85}
@media(prefers-reduced-motion:reduce){.qt-wash{animation:none}}
/* Three full-viewport composited layers — a multiply-blended grain, an infinitely
   animating wash and the bar's backdrop blur — is a lot to carry up a scroll on a phone,
   and the two atmospheric ones are the least of what the page is for. Below 760px the
   grain and the wash come off and the bar falls back to an opaque ground. */
@media(max-width:760px){
  .qt-grain,.qt-wash{display:none}
  .qt-bar{backdrop-filter:none;background:var(--paper)}
}

/* ---------- top bar: the rail's replacement ---------- */
.qt-bar{position:sticky;top:0;z-index:40;background:color-mix(in srgb,var(--paper) 88%,transparent);
  backdrop-filter:blur(10px);border-bottom:1px solid var(--rule)}
.qt-bar-inner{max-width:1180px;margin:0 auto;padding:0 32px;display:flex;align-items:center;gap:28px;min-height:60px}
.qt-mark{display:flex;flex-direction:column;line-height:1.25;padding:8px 0}
.qt-mark span{font:600 15.5px/1.2 Geist;letter-spacing:-.012em}
.qt-mark small{font:500 11.5px/1.45 'Geist Mono',monospace;color:var(--faint);letter-spacing:.06em;text-transform:lowercase}
.qt .qt-menu{display:none}
.qt-links{display:flex;flex-wrap:wrap;gap:2px 4px;flex:1;justify-content:flex-end}
.qt-links a{font:500 12.5px/1.2 'Geist Mono',monospace;letter-spacing:.03em;color:var(--faint);padding:9px 8px;border-radius:3px;position:relative}
.qt-links a:hover{color:var(--ink)}
.qt-links a[aria-current=page]{color:var(--ink)}
.qt-links a[aria-current=page]::after{content:"";position:absolute;left:8px;right:8px;bottom:3px;height:1px;background:var(--moss)}
.qt-tools{display:flex;gap:4px;flex-shrink:0}
.qt-tool{width:34px;height:34px;display:grid;place-items:center;border:1px solid var(--rule);border-radius:3px;color:var(--faint)}
.qt-tool:hover{color:var(--ink);border-color:var(--faint)}
.qt-tool[aria-pressed=true]{color:var(--moss);border-color:var(--moss)}

/* ---------- the hanging-margin grid ---------- */
.qt-sheet{max-width:1180px;margin:0 auto;padding:0 32px 96px}
.qt-block{display:grid;grid-template-columns:200px minmax(0,660px);gap:0 44px;padding:52px 0;border-top:1px solid var(--rule-soft)}
.qt-block:first-child{border-top:0}
.qt-margin{font:500 11.5px/1.7 'Geist Mono',monospace;color:var(--faint);letter-spacing:.09em;text-transform:uppercase}
.qt-margin-note{font:500 11.5px/1.7 'Geist Mono',monospace;color:var(--faint);letter-spacing:.09em;text-transform:uppercase;margin:0}
.qt-mark-num{display:block;color:var(--moss);margin-bottom:8px}
/* Measured 127x16 — the worst target on the page. Same rule, same type, 28px of height
   under the pointer. inline-flex rather than padding so the underline stays on the text. */
.qt-margin a{display:inline-flex;align-items:center;min-height:28px;color:var(--faint);position:relative}
.qt-margin a::after{content:"";position:absolute;left:0;right:0;bottom:5px;height:1px;
  background:var(--rule);transition:background-color .2s}
.qt-margin a:hover{color:var(--ink)}
.qt-margin a:hover::after{background:var(--moss)}
.qt-measure > :last-child{margin-bottom:0}
.qt-margin,.qt-measure{min-width:0;overflow-wrap:anywhere}

/* ---------- display type: mono promoted to the display role ---------- */
.qt-display{font-family:'Geist Mono',ui-monospace,monospace;font-weight:400;letter-spacing:-.045em;
  text-transform:lowercase;line-height:.98}
.qt-hero h1{font-size:clamp(38px,7.2vw,72px)}
.qt-page-title{font-size:clamp(30px,4.6vw,46px);margin-bottom:30px}
.qt-section-title{font-size:clamp(22px,3vw,30px);margin-bottom:20px}

/* ---------- hero ---------- */
.qt-hero{padding-top:64px}
.qt-portrait{position:relative;width:100%;max-width:200px;border:1px solid var(--rule);padding:7px;background:var(--paper2)}
.qt-portrait img{filter:grayscale(1) contrast(1.06);width:100%;height:auto}
/* The source photograph has a white studio background, which reads as a lit panel on a
   dark page. Pull it down and warm the frame so it sits in the paper rather than on it. */
:root[data-theme="dark"] .qt-portrait img{filter:grayscale(1) contrast(.92) brightness(.62)}
:root[data-theme="dark"] .qt-portrait{background:var(--paper)}
:root[data-theme="dark"] .qt-portrait::after{border-color:color-mix(in srgb,var(--ink) 22%,transparent)}
.qt-portrait::after{content:"";position:absolute;inset:7px;border:1px solid color-mix(in srgb,var(--ink) 12%,transparent);pointer-events:none}
.qt-portrait-caption{margin-top:12px}
.qt-lede{font-family:Newsreader,Georgia,serif;font-size:22px;line-height:1.58;color:var(--soft);max-width:var(--measure-tight);letter-spacing:.005em}
.qt-lede strong{font-weight:500;color:var(--ink)}
/* github / résumé / email — 49x19 as measured, the three links most likely to be the
   reason someone is on the page at all. Padded to 32px tall without moving the rule. */
.qt-inline-links{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:22px}
.qt-inline-links a{display:inline-flex;align-items:center;min-height:32px;
  font:500 12.5px/1.2 'Geist Mono',monospace;letter-spacing:.05em;position:relative}
.qt-inline-links a::after{content:"";position:absolute;left:0;bottom:8px;width:100%;height:1px;background:var(--rule);
  transform-origin:left;transition:background-color .28s}
.qt-inline-links a:hover::after{background:var(--moss)}

/* ---------- facts strip: rows, not tiles ---------- */
.qt-facts{border-top:1px solid var(--rule-soft)}
.qt-fact{display:flex;align-items:baseline;justify-content:space-between;gap:20px;padding:13px 0;border-bottom:1px solid var(--rule-soft)}
.qt-fact strong{font:500 22px/1 'Geist Mono',monospace;letter-spacing:-.03em}
.qt-fact span{font:500 11.5px/1.55 'Geist Mono',monospace;color:var(--faint);letter-spacing:.09em;text-transform:uppercase;text-align:right}

/* ---------- project index: rows that open in place ---------- */
.qt-index{border-top:1px solid var(--rule)}
.qt-entry{display:block;width:100%;text-align:left;border-bottom:1px solid var(--rule-soft);padding:18px 0;color:inherit}
.qt-entry-head{display:flex;align-items:baseline;gap:16px}
.qt-entry-num{font:500 11.5px 'Geist Mono',monospace;color:var(--faint);width:28px;flex-shrink:0}
.qt-entry-name{font-size:20px;font-weight:500;letter-spacing:-.015em;flex:1}
.qt-entry-cat{font:500 11.5px 'Geist Mono',monospace;color:var(--faint);letter-spacing:.08em;text-transform:uppercase}
/* The index does not move. The inline summary expanded on :hover, which pushed every
   row below it down 116px — pointing at a row shoved the next one out from under the
   cursor — and on touch it never opened at all. Above 760px the summary is set in the
   margin instead (.qt-preview); here it stays out of the flow entirely, and the row
   marks itself with a rule and a colour, neither of which costs any height. */
.qt-entry-body{display:none}
.qt-entry{transition:border-color .2s,padding-left .2s}
.qt-entry:hover,.qt-entry:focus-visible{border-color:var(--moss);padding-left:6px}
.qt-entry:hover .qt-entry-name,.qt-entry:focus-visible .qt-entry-name{color:var(--moss)}
.qt-entry-summary{display:block;font-size:16.5px;line-height:1.7;color:var(--soft);margin:12px 0 0;max-width:var(--measure)}
.qt-entry-meta{display:block;font:500 11.5px 'Geist Mono',monospace;color:var(--faint);letter-spacing:.06em;margin-top:10px;padding-bottom:4px}

/* The margin preview. Fixed height so the column beside the index never reflows either,
   and it holds its last entry rather than emptying on the way between rows. */
.qt-preview{margin-top:26px;padding-top:16px;border-top:1px solid var(--rule-soft);
  min-height:150px;opacity:0;transition:opacity .24s ease}
.qt-preview[data-on]{opacity:1}
.qt-preview-name{display:block;font:500 13px/1.4 'Geist Mono',monospace;letter-spacing:.02em;
  color:var(--ink);text-transform:none;margin-bottom:8px}
.qt-preview-summary{display:block;font-family:Newsreader,Georgia,serif;font-size:15px;line-height:1.56;
  color:var(--soft);letter-spacing:0;text-transform:none;margin-bottom:10px}
.qt-preview-stack{display:block;font:500 10.5px/1.6 'Geist Mono',monospace;color:var(--faint);letter-spacing:.07em}
@media(prefers-reduced-motion:reduce){.qt-preview,.qt-entry{transition:none}}

/* ---------- rows shared by experience, credentials, affiliations ---------- */
.qt-row{display:grid;grid-template-columns:84px 1fr;gap:16px;padding:15px 0;border-bottom:1px solid var(--rule-soft)}
.qt-row:first-child{border-top:1px solid var(--rule)}
.qt-row time,.qt-row .qt-row-key{font:500 11.5px/1.9 'Geist Mono',monospace;color:var(--faint);letter-spacing:.06em}
.qt-row strong{font-weight:550;font-size:17px}
.qt-row span{display:block;font-size:16px;line-height:1.6;color:var(--soft)}
/* Only the rows that genuinely lead somewhere read as interactive. */
.qt-row-link{cursor:pointer;transition:border-color .2s}
.qt-row-link:hover{border-bottom-color:var(--moss)}
.qt-row-link:hover strong{color:var(--moss)}
.qt-row-verify{display:inline;font:500 10.5px 'Geist Mono',monospace;letter-spacing:.08em;
  text-transform:uppercase;color:var(--faint);white-space:nowrap;margin-left:6px}
/* A writing row opens the piece, so unlike the résumé rows it is genuinely a control. */
.qt-row-post{width:100%;text-align:left;background:none;border-left:0;border-right:0;border-top:0;
  color:inherit;font:inherit;cursor:pointer;transition:border-color .2s,padding-left .2s}
.qt-row-post:hover,.qt-row-post:focus-visible{border-bottom-color:var(--moss);padding-left:5px}
.qt-row-post:hover strong{color:var(--moss)}
.qt-row-link:hover .qt-row-verify{color:var(--moss)}
.qt-quiet{font-family:Newsreader,Georgia,serif;font-size:20px;line-height:1.64;color:var(--soft);border-left:2px solid var(--moss);padding-left:22px;max-width:var(--measure-tight)}

/* ---------- tools in the margin ---------- */
/* These measured 160x23 and were the page's primary way in to everything interactive,
   under the 24px WCAG 2.5.8 floor. The type is unchanged — the hairline still sits on
   the text — but the hit area is padded out to 32px and the gap tightened to keep the
   list the same overall height. */
.qt-tool-list{display:flex;flex-direction:column;align-items:flex-start;gap:2px;margin-top:10px}
.qt-tool-list button{display:inline-flex;align-items:center;min-height:32px;
  font:500 12px/1.6 'Geist Mono',monospace;letter-spacing:.085em;text-transform:uppercase;
  color:var(--faint);text-align:left;position:relative}
.qt-tool-list button::after{content:"";position:absolute;left:0;right:0;bottom:6px;height:1px;
  background:var(--rule);transition:background-color .2s}
.qt-tool-list button:hover{color:var(--ink)}
.qt-tool-list button:hover::after{background:var(--moss)}
.qt-tool-note{margin:10px 0 0;font:500 11.5px/1.6 'Geist Mono',monospace;color:var(--faint);letter-spacing:.08em;text-transform:uppercase}
/* The dock sits in the margin, under the tools, in the page's own register. */
.qt-dock{margin-top:22px;padding-top:16px;border-top:1px solid var(--rule-soft);
  color:var(--faint);--cm-ground:var(--paper);flex-direction:column;align-items:flex-start;gap:9px}
.qt-dock .cm-dock-live{color:var(--moss)}
.qt-dock .cm-dock-now,.qt-dock .cm-dock-total{color:var(--faint);text-transform:uppercase;letter-spacing:.08em}
.qt-dock .cm-dock-now b{color:var(--ink)}
.qt-dock .cm-dock-chat{border-color:var(--rule);color:var(--faint);text-transform:uppercase}
.qt-dock .cm-dock-chat:hover{border-color:var(--moss);color:var(--ink)}
/* The colophon copy sits outside <main>, so it is on every route rather than only the
   front page. Laid out along the line rather than stacked, because down here it is a
   footer readout and not a panel. */
.qt-dock-foot{flex-direction:row;align-items:center;gap:18px;flex-wrap:wrap;
  margin:0 0 20px;padding-top:0;border-top:0}
@media(max-width:600px){.qt-dock-foot{gap:12px}}
/* The cover's ground, so the reveal reads as part of the press rather than over it. */
.qt-press .rv{color:var(--ink)}
.qt-press .rv-value{color:var(--ink)}
.qt-press .rv-tag{color:var(--moss)}
.qt-presence{margin-top:22px;font:500 11.5px/1.7 'Geist Mono',monospace;color:var(--faint)}
.qt-presence b{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--moss);margin-right:7px;vertical-align:middle}

/* ---------- interior routes reuse the shared section renderer ---------- */
.qt-page{padding:64px 0 0}
.qt-page .section-intro{font-family:Newsreader,Georgia,serif;font-size:21px;line-height:1.62;color:var(--soft);max-width:var(--measure-tight);margin:0 0 34px}
.qt-page .tag{font:500 11.5px/1.55 'Geist Mono',monospace;padding:3px 7px;border:1px solid var(--rule);color:var(--soft)}
.qt-page ul:not(.tag-list){padding-left:20px;color:var(--soft);font-size:16.5px;line-height:1.78;margin:0;max-width:var(--measure)}
.qt-page ul:not(.tag-list) li+li{margin-top:8px}
.qt-page .source-note{font:500 13px/1.72 'Geist Mono',monospace;color:var(--faint);border-top:1px solid var(--rule-soft);padding-top:13px;margin-top:18px;max-width:var(--measure)}
.qt-page .notice{font:500 13px/1.78 'Geist Mono',monospace;color:var(--soft);background:var(--paper2);padding:14px 16px;margin-bottom:24px;max-width:var(--measure)}
.qt-page .skill-group{margin-bottom:26px}
.qt-page .btn{min-height:42px;padding:10px 15px;border:1px solid var(--rule);display:inline-flex;align-items:center;gap:8px;font:500 12.5px 'Geist Mono',monospace;letter-spacing:.05em;text-transform:uppercase}
.qt-page .btn:hover{border-color:var(--moss);color:var(--moss)}
.qt-page .btn.primary{background:var(--ink);color:var(--paper);border-color:var(--ink)}
.qt-page .btn.primary:hover{color:var(--paper);opacity:.86}
.qt-page .actions{display:flex;flex-wrap:wrap;gap:9px;margin:20px 0}
.qt-page details{padding:16px 0;border-bottom:1px solid var(--rule-soft)}
.qt-page summary{font-size:16.5px;cursor:pointer;min-height:44px;line-height:1.7}
.qt-page .resource-row{padding:16px 0;border-top:1px solid var(--rule-soft);display:flex;justify-content:space-between;gap:20px}
.qt-page .resource-row:hover{color:var(--moss)}
.qt-page .resource-row small{display:block;font:500 11.5px 'Geist Mono',monospace;color:var(--faint);margin-top:4px}
.qt-page .empty-mark{font:44px/1 'Geist Mono',monospace;color:var(--rule);margin-bottom:18px}
.qt-page .text-link{font:500 12.5px 'Geist Mono',monospace;letter-spacing:.05em}

/* ---------- the moth: an engraving, and the light it wants ----------
   Flat ink on paper, hatched in the negative with the paper colour. No glow on the
   moth itself — the page has none to give. The lamp is the single exception, and
   that is the point: it is the only lit thing on the sheet. */
.qt-pet{color:var(--ink)}
.qt-pet .qt-moth-wing path{fill:var(--ink)}
.qt-pet .mj-pet-bubble{background:var(--paper);color:var(--ink);border:1px solid var(--ink);border-radius:0;
  font-family:Newsreader,Georgia,serif;font-size:14px;line-height:1.45}
.qt-ink-dot{position:fixed;width:5px;height:5px;border-radius:50%;background:var(--ink);z-index:19;
  pointer-events:none;animation:qt-ink 4.5s ease-out forwards}
@keyframes qt-ink{0%{opacity:.5;transform:scale(.4)}20%{opacity:.34;transform:scale(1)}100%{opacity:0;transform:scale(1.1)}}

.qt-lamp{position:fixed;z-index:18;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;
  background:#ffc23d;box-shadow:0 0 14px 5px #ffb00099,0 0 40px 14px #ffb00040;pointer-events:none;
  animation:qt-lamp-in 1.1s cubic-bezier(.2,.9,.2,1) both,qt-lamp-pulse 1.9s ease-in-out 1.1s infinite}
/* The bloom, and a ring that opens out of it — the page has no other light, so this
   is the one thing on the sheet that moves and glows. */
.qt-lamp::before{content:"";position:absolute;inset:-46px;border-radius:50%;
  background:radial-gradient(circle,#ffb00055 0%,#ffb00022 38%,transparent 68%);
  animation:qt-lamp-breathe 1.9s ease-in-out infinite}
.qt-lamp::after{content:"";position:absolute;inset:0;border-radius:50%;border:1.5px solid #ffb000;
  animation:qt-lamp-ring 1.9s cubic-bezier(.2,.6,.3,1) infinite}
.qt-lamp-out{animation:qt-lamp-out .6s ease-in forwards}
@keyframes qt-lamp-in{0%{opacity:0;transform:scale(0)}60%{opacity:1;transform:scale(1.5)}100%{opacity:1;transform:scale(1)}}
@keyframes qt-lamp-pulse{0%,100%{transform:scale(1);box-shadow:0 0 14px 5px #ffb00099,0 0 40px 14px #ffb00040}
  50%{transform:scale(1.16);box-shadow:0 0 20px 8px #ffb000cc,0 0 60px 22px #ffb00055}}
@keyframes qt-lamp-breathe{0%,100%{opacity:.75}50%{opacity:1}}
@keyframes qt-lamp-ring{0%{opacity:.85;transform:scale(1)}100%{opacity:0;transform:scale(4.6)}}
@keyframes qt-lamp-out{to{opacity:0;transform:scale(.3)}}

.qt-pet-toggle{font:500 12px/1.6 'Geist Mono',monospace;letter-spacing:.085em;text-transform:uppercase;
  color:var(--faint);border-bottom:1px solid var(--rule);padding-bottom:2px}
.qt-pet-toggle:hover{color:var(--ink);border-color:var(--moss)}

/* ---------- colophon ---------- */
.qt-colophon{border-top:1px solid var(--rule);padding:44px 0 60px}
.qt-colophon-grid{max-width:1180px;margin:0 auto;padding:0 32px;display:grid;grid-template-columns:200px minmax(0,660px);gap:0 44px}
.qt-more{display:flex;flex-wrap:wrap;gap:10px 22px;padding-bottom:26px;margin-bottom:22px;border-bottom:1px solid var(--rule-soft)}
.qt-more a{font:500 12.5px/1.2 'Geist Mono',monospace;letter-spacing:.03em;color:var(--faint);padding:4px 0}
.qt-more a:hover{color:var(--ink)}
.qt-more a[aria-current=page]{color:var(--moss)}
.qt-colophon p{font-family:Newsreader,Georgia,serif;font-size:17.5px;line-height:1.66;color:var(--soft);max-width:var(--measure)}
.qt-colophon-links{display:flex;flex-wrap:wrap;gap:20px;margin:0}
.qt-colophon-links a{font:500 11.5px 'Geist Mono',monospace;letter-spacing:.06em;color:var(--faint);border-bottom:1px solid var(--rule);padding-bottom:3px}
.qt-colophon-links a:hover{color:var(--ink);border-color:var(--moss)}

/* ---------- motion: rules draw, headings unmask. No content-shift entry. ---------- */
@keyframes qt-rule{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@keyframes qt-unmask{from{clip-path:inset(0 0 105% 0)}to{clip-path:inset(0 0 -8% 0)}}
/* Reveal is opt-in from the script: without JavaScript every block is simply visible. */
.qt[data-reveal] .qt-block{opacity:0;transform:translateY(8px);
  transition:opacity .38s cubic-bezier(.2,0,0,1),transform .38s cubic-bezier(.2,0,0,1)}
.qt[data-reveal] .qt-block::before{content:"";position:absolute;left:0;right:0;top:-1px;height:1px;background:var(--rule);
  transform:scaleX(0);transform-origin:left;transition:transform .55s cubic-bezier(.2,0,0,1) .05s}
.qt-block{position:relative;scroll-margin-top:88px}
.qt[data-reveal] .qt-block.is-in{opacity:1;transform:none}
.qt[data-reveal] .qt-block.is-in::before{transform:scaleX(1)}
.qt-display{animation:qt-unmask .58s cubic-bezier(.2,0,0,1) both}
@media(prefers-reduced-motion:reduce){
  .qt-display{animation:none}
  .qt[data-reveal] .qt-block{opacity:1;transform:none;transition:none}
  .qt[data-reveal] .qt-block::before{transform:scaleX(1);transition:none}
}

/* ---------- responsive ---------- */
/* ---------- contents: a sticky index in the right gutter ---------- */
/* The index sat in the leftover space to the right of the 1180px sheet, which only
   exists above 1280px — so it vanished on exactly the laptop widths most people read
   this on (a maximised 1366 window is 1351 after the scrollbar; 1280 is 1265). Narrowing
   it to 168px and letting it sit closer to the sheet buys the whole 1100–1280 band. */
.qt-contents{position:fixed;top:96px;right:max(14px,calc(50vw - 590px + 14px));width:168px;z-index:30;
  max-height:calc(100vh - 160px);overflow:auto;overscroll-behavior:contain}
.qt-contents[hidden]{display:none}
.qt-contents .toc-label{font:500 11.5px/1.7 'Geist Mono',monospace;color:var(--faint);letter-spacing:.09em;
  text-transform:uppercase;padding-bottom:8px;border-bottom:1px solid var(--rule-soft)}
.qt-contents .toc-list{padding-top:6px}
.qt-contents .toc-list a{display:flex;align-items:flex-start;gap:8px;min-height:26px;
  font:500 11.5px/1.6 'Geist Mono',monospace;letter-spacing:.05em;text-transform:uppercase;
  color:var(--faint);padding:6px 0 6px 10px;margin-left:-10px;border-left:1px solid transparent;
  transition:color .2s,border-color .2s}
.qt-contents .toc-list a:hover{color:var(--ink)}
.qt-contents .toc-list a.is-current{color:var(--moss);border-left-color:var(--moss)}
.qt-contents .toc-index{font-size:11px}

/* ---------- 'go to' opens the palette ---------- */
.qt-go{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 10px;border:1px solid var(--rule);
  border-radius:3px;font:500 12.5px 'Geist Mono',monospace;letter-spacing:.04em;color:var(--faint)}
.qt-go:hover{color:var(--ink);border-color:var(--faint)}
.qt-go kbd{font:500 11px 'Geist Mono',monospace;padding:3px 5px;border:1px solid var(--rule);border-radius:3px;color:var(--faint)}

/* Below 1280 there is genuinely no room for a labelled rail beside a 1180px sheet — the
   grid needs 904px of its own, so reserving 190px more would push the sheet off centre,
   and a centred document that stops being centred to make room for its own furniture has
   its priorities backwards. The index does not need the room: it collapses to its section
   numbers, floats over the gutter, and opens to the full list on hover or focus. Nothing
   in the layout moves, and the reader keeps the index down to 1024. */
@media(max-width:1279px) and (min-width:1025px){
  .qt-contents{right:12px;width:34px;background:color-mix(in srgb,var(--paper) 92%,transparent);
    border:1px solid transparent;padding:8px 6px;transition:width .22s cubic-bezier(.2,0,0,1),
    background-color .22s,border-color .22s,box-shadow .22s;overflow:hidden}
  .qt-contents .toc-label{opacity:0;white-space:nowrap;transition:opacity .18s}
  .qt-contents .toc-text{display:none}
  .qt-contents .toc-list a{padding-left:4px;margin-left:0;justify-content:center}
  .qt-contents:hover,.qt-contents:focus-within{width:184px;padding:10px 12px;
    background:var(--paper);border-color:var(--rule-soft);
    box-shadow:-8px 0 28px color-mix(in srgb,var(--ink) 7%,transparent)}
  .qt-contents:hover .toc-label,.qt-contents:focus-within .toc-label{opacity:1}
  .qt-contents:hover .toc-text,.qt-contents:focus-within .toc-text{display:inline}
  .qt-contents:hover .toc-list a,.qt-contents:focus-within .toc-list a{padding-left:10px;margin-left:-10px;justify-content:flex-start}
  /* Collapsed, the current section is the only thing that has to read at a glance. */
  .qt-contents .toc-list a.is-current .toc-index{color:var(--moss);font-weight:600}
}
@media(prefers-reduced-motion:reduce){.qt-contents,.qt-contents .toc-label{transition:none}}
/* At 1024 and below the measure becomes 1fr and fills the block, so there is no
   gutter left to float over and the bar's Contents menu is the way in. */
@media(max-width:1024px){.qt-contents{display:none}}
@media(max-width:1024px){
  .qt-block,.qt-colophon-grid{grid-template-columns:150px minmax(0,1fr);gap:0 28px}
}
@media(max-width:760px){
  .qt-bar-inner{flex-wrap:wrap;gap:10px;padding:10px 20px}
  .qt .qt-menu{display:block;font-size:11.5px;font-weight:500;font-family:'Geist Mono',monospace;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);border:1px solid var(--rule);padding:10px 12px;min-height:44px;margin-left:auto}
  .qt-links{display:none;order:3;width:100%;flex-direction:column;justify-content:flex-start;border-top:1px solid var(--rule-soft);padding-top:6px}
  .qt-links.open{display:flex}
  .qt-links a{padding:12px 4px;min-height:44px;display:flex;align-items:center;font-size:12px}
  .qt-links a[aria-current=page]::after{left:4px;right:auto;width:18px}
  .qt-tools{order:2}
  .qt-sheet{padding:0 20px 70px}
  .qt-block,.qt-colophon-grid{grid-template-columns:minmax(0,1fr);gap:14px;padding:36px 0}
  /* No margin column and no hover down here, so the summary is set under the row —
     the only place a phone can put it, and previously the one place it never appeared. */
  .qt-preview{display:none}
  .qt-entry-body{display:block}
  .qt-entry:hover,.qt-entry:focus-visible{padding-left:0}
  /* The number kept its default order while the name and category were reordered around
     it, so it wrapped onto a line of its own above the title. It belongs beside it. */
  .qt-entry-head{flex-wrap:wrap;gap:2px 10px;align-items:baseline}
  .qt-entry-num{order:1}
  .qt-entry-cat{order:2;flex:1}
  .qt-entry-name{flex:1 1 100%;order:3;font-size:19px;margin-top:2px}
  .qt-entry-summary{font-size:15.5px;margin-top:8px}
  .qt-colophon-grid{padding:0 20px}
  .qt-hero{padding-top:34px}
  .qt-portrait{max-width:168px}
    .qt-row{grid-template-columns:70px 1fr;gap:12px}
}


/* ==========================================================================
   The press — Quarto's entrance, route transitions and re-typeset refresh.
   The page underneath is fully rendered and readable from the first frame; this
   only covers it. See prototypes/shared/stage.js and doc/motion-plan.md.
   ========================================================================== */

.qt-press{position:fixed;inset:0;z-index:90;background:var(--paper2);
  display:grid;grid-auto-flow:row;place-content:center;justify-items:center;gap:34px;
  transition:opacity .5s ease}
.qt-press[data-state="done"]{opacity:0;pointer-events:none}

/* ---------- the fold ----------------------------------------------------------
   A quarto is a sheet folded twice: four leaves, eight pages. The entrance performs
   exactly that, and every step of it is one continuous transform. The version before
   this printed rows of text one at a time, which is a slideshow rather than a motion,
   and that is what made it feel inert however long it ran.

   Two leaves fold over a face. The face carries the creases, and the creases outlive
   the folds: what is left when the leaves lift is the grid of the page underneath. */
.qt-fold-stage{position:relative;height:min(62vh,640px);aspect-ratio:1/1.294;
  perspective:1600px;perspective-origin:50% 45%;
  transform:scale(.965);opacity:0;
  transition:transform 1.1s cubic-bezier(.22,1,.36,1),opacity .7s ease}
.qt-press[data-step="in"] .qt-fold-stage,
.qt-press[data-step="fold-1"] .qt-fold-stage,
.qt-press[data-step="fold-2"] .qt-fold-stage{transform:scale(1);opacity:1}

/* The folded block sits smaller and centred, the way a gathered quarto would. */
.qt-press[data-step="gathered"] .qt-fold-stage{transform:scale(.88);opacity:1;
  transition:transform 1s cubic-bezier(.32,.9,.3,1)}
/* Then it opens outward into the page and dissolves into it. */
.qt-press[data-step="read"] .qt-fold-stage{transform:scale(1.32);opacity:0;
  transition:transform .8s cubic-bezier(.4,0,.2,1),opacity .55s ease}
.qt-press[data-step="open"] .qt-fold-stage,
.qt-press[data-step="out"] .qt-fold-stage{transform:scale(1.9);opacity:0;
  transition:transform 1.15s cubic-bezier(.5,0,.2,1),opacity .85s ease .2s}

.qt-fold-sheet{position:absolute;inset:0;transform-style:preserve-3d}
.qt-fold-face{position:absolute;inset:0;background:var(--paper);
  border:1px solid var(--rule-soft);
  box-shadow:0 26px 60px -34px #12140f4d,0 4px 14px -8px #12140f26;overflow:hidden}

/* The creases, drawn before the folds and still there afterwards. */
.qt-crease{position:absolute;background:var(--rule);opacity:0}
.qt-crease-h{left:0;right:0;top:50%;height:1px;transform-origin:left;transform:scaleX(0);
  transition:transform .75s cubic-bezier(.2,0,0,1),opacity .4s ease}
.qt-crease-v{top:0;bottom:0;left:50%;width:1px;transform-origin:top;transform:scaleY(0);
  transition:transform .75s cubic-bezier(.2,0,0,1),opacity .4s ease}
.qt-press[data-step="fold-1"] .qt-crease-h,.qt-press[data-step="fold-2"] .qt-crease-h,
.qt-press[data-step="gathered"] .qt-crease-h,
.qt-press[data-step="read"] .qt-crease-h,
.qt-press[data-step="open"] .qt-crease-h{opacity:.95;transform:scaleX(1)}
.qt-press[data-step="fold-2"] .qt-crease-v,.qt-press[data-step="gathered"] .qt-crease-v,
.qt-press[data-step="read"] .qt-crease-v,.qt-press[data-step="open"] .qt-crease-v{opacity:.95;transform:scaleY(1)}

/* Faint text-block rules, so the sheet reads as a page rather than a blank card. */
.qt-fold-rule{position:absolute;left:12%;height:1px;background:var(--rule-soft);
  transform:scaleX(0);transform-origin:left;transition:transform .9s cubic-bezier(.2,0,0,1)}
.qt-fold-rule-1{top:22%;right:20%}
.qt-fold-rule-2{top:30%;right:34%}
.qt-fold-rule-3{top:70%;right:26%}
.qt-press[data-step="in"] .qt-fold-rule,.qt-press[data-step="fold-1"] .qt-fold-rule,
.qt-press[data-step="fold-2"] .qt-fold-rule,.qt-press[data-step="gathered"] .qt-fold-rule,
.qt-press[data-step="open"] .qt-fold-rule{transform:scaleX(1)}
.qt-press[data-step="in"] .qt-fold-rule-2{transition-delay:.12s}
.qt-press[data-step="in"] .qt-fold-rule-3{transition-delay:.24s}

/* The leaves: each is a half of the sheet rotating over the face on its crease. */
/* A leaf is the same paper seen from the front; its back is the reverse of the sheet,
   so it sits a shade darker and carries the shadow it casts on what is underneath. */
.qt-leaf{position:absolute;background:var(--paper);border:1px solid var(--rule-soft);
  backface-visibility:hidden;box-shadow:0 16px 40px -20px #12140f5c}
.qt-leaf-bottom{left:0;right:0;top:50%;bottom:0;transform-origin:50% 0;
  transform:rotateX(0deg);transition:transform 1.15s cubic-bezier(.62,.02,.24,1)}
.qt-leaf-right{top:0;bottom:0;left:50%;right:0;transform-origin:0 50%;
  transform:rotateY(0deg);transition:transform 1.15s cubic-bezier(.62,.02,.24,1)}
.qt-press[data-step="fold-1"] .qt-leaf-bottom,.qt-press[data-step="fold-2"] .qt-leaf-bottom,
.qt-press[data-step="gathered"] .qt-leaf-bottom{transform:rotateX(-180deg)}
.qt-press[data-step="fold-2"] .qt-leaf-right,
.qt-press[data-step="gathered"] .qt-leaf-right{transform:rotateY(180deg)}
/* Opening lifts the leaves away rather than unfolding them back, so the motion keeps
   going in one direction instead of rewinding itself. */
.qt-press[data-step="open"] .qt-leaf-bottom,.qt-press[data-step="out"] .qt-leaf-bottom{
  transform:rotateX(-92deg);opacity:0;
  transition:transform .95s cubic-bezier(.5,0,.2,1),opacity .6s ease .25s}
.qt-press[data-step="open"] .qt-leaf-right,.qt-press[data-step="out"] .qt-leaf-right{
  transform:rotateY(92deg);opacity:0;
  transition:transform .95s cubic-bezier(.5,0,.2,1),opacity .6s ease .25s}

/* The caption names each state, crossfading in place so nothing jumps. */
.qt-fold-caption{position:relative;margin:0;width:min(90vw,420px);height:1.6em;
  text-align:center;pointer-events:none}
.qt-fold-caption span{position:absolute;left:0;right:0;
  font:500 12px/1.6 'Geist Mono',monospace;letter-spacing:.2em;text-transform:uppercase;
  color:var(--faint);opacity:0;transform:translateY(4px);
  transition:opacity .5s ease,transform .5s cubic-bezier(.2,0,0,1)}
.qt-press[data-step="in"] [data-fold="0"],
.qt-press[data-step="fold-1"] [data-fold="1"],
.qt-press[data-step="fold-2"] [data-fold="2"],
.qt-press[data-step="gathered"] [data-fold="3"]{opacity:1;transform:none}

/* The rule that becomes the page's own top bar, drawn at the real bar height. */
.qt-press-bar{position:absolute;left:0;right:0;top:var(--bar-h,60px);height:1px;background:var(--rule);
  transform:scaleX(0);transform-origin:left;
  transition:transform 1.05s cubic-bezier(.2,0,0,1)}
.qt-press[data-step="gathered"] .qt-press-bar,.qt-press[data-step="read"] .qt-press-bar,.qt-press[data-step="open"] .qt-press-bar,
.qt-press[data-step="out"] .qt-press-bar{transform:scaleX(1)}

/* The name, already in register with the hero, so the cover lifting does not move it. */
/* Placed on the hero's own box — left edge, width and all — so the name does not move,
   rewrap or realign when the cover goes. */
.qt-press-name{position:absolute;left:var(--press-left,0px);top:var(--press-top,120px);
  width:var(--press-width,auto);overflow:hidden}
.qt-press-name span{display:block;text-align:left;color:var(--ink);
  /* Fallback only: publishRegister copies the hero's computed type over these as soon
     as there is a hero to measure. */
  font-family:'Geist Mono',monospace;font-weight:500;
  font-size:clamp(38px,7.2vw,72px);line-height:.98;text-transform:lowercase;
  letter-spacing:-.03em;
  opacity:0;transform:translateY(.42em);
  transition:opacity .7s ease .35s,transform .9s cubic-bezier(.2,1,.3,1) .35s}
.qt-press[data-step="open"] .qt-press-name span,
.qt-press[data-step="out"] .qt-press-name span{opacity:1;transform:none}

@media(max-width:760px){
  .qt-fold-stage{height:auto;width:min(72vw,300px);max-height:56vh}
  .qt-fold-caption{top:calc(50% + min(48vw,220px) + 34px)}
  .qt-press{align-content:center}
  .qt-press-name{position:static;width:auto;padding-top:0;margin-top:34px}
  .qt-press-name span{text-align:center}
  .qt-press-name span{font-size:clamp(28px,9vw,44px)}
}
@media(prefers-reduced-motion:reduce){.qt-press{display:none}}

/* The entrance meter, in the cover's own vocabulary. */
.qt-press .mj-stage-track{background:var(--rule-soft);height:1px}
.qt-press .mj-stage-track i{background:var(--moss)}
.qt-press .mj-stage-skip{font:500 11.5px/1 'Geist Mono',monospace;letter-spacing:.14em;
  text-transform:uppercase;color:var(--faint)}

/* The caption belongs to the fold, so it leaves with it. */
.qt-press[data-step="read"] .qt-fold-caption span{opacity:0}

/* The reveal takes the stage the sheet just left. It is absolutely placed rather than
   laid out in the press's grid, because the grid is sized around the fold and would
   shove the name off centre for the cut that follows. */
.qt-press .rv{position:absolute;inset:0;display:grid;place-items:center;z-index:6}
.qt-press .rv-inner{width:min(560px,86vw)}
