/* ============================================================================
   askchris — shared shell.
   One stylesheet for all three pages so the nav, type scale and colour ramp
   can't drift apart between them.
   ========================================================================= */

/* The text ramp is pinned to contrast, not to taste. --faint used to be
   #9a9aa3, which is 2.8:1 on white — it reads as "quiet" to anyone who can
   see it well and as nothing at all to anyone who can't, and it was carrying
   every date on the site. All three levels now clear WCAG AA (4.5:1) against
   their own background, and keep enough distance from each other to still
   read as a hierarchy. --faint is set against --chip rather than the page,
   because that is the tighter of the two and it is where the dates sit. Below
   --faint must be decoration, never text — that is what --rule is for. */
:root{
  --bg:#fff; --fg:#16161a; --muted:#55555e; --faint:#70707a;
  --rule:#e6e6ea; --chip:#f6f6f8; --wash:#fafafb;
  --ok:#1a7f4b; --bad:#b3261e; --warn:#9a6b00;
  --shadow:0 1px 2px rgba(16,16,20,.04), 0 8px 24px rgba(16,16,20,.06);
  --shadow-lg:0 4px 12px rgba(16,16,20,.08), 0 24px 64px rgba(16,16,20,.14);
}

/* ============================================================================
   The dimensions, on scales rather than by eye.

   The colour above has always been rigorous — every level pinned to a measured
   contrast ratio against the surface it actually sits on. Everything else was
   not: thirty distinct font sizes, twenty-six spacing values, thirteen radii
   and nineteen transition durations on a six-page site, with a --radius token
   defined and then bypassed twelve times.

   That is what inconsistency looks like before anyone can name it. Values now
   come from these or they are wrong, and /architecture publishes them by
   reading them back off the running stylesheet — the same rule the rest of
   that page follows, so what is documented cannot drift from what ships.
   ========================================================================= */
:root{
  /* Type. Seven steps. --t-lg is the document's own size, so anything with no
     size of its own — prose, list items — lands on the scale rather than beside
     it, which is where the loose 16.5px used to put it. */
  --t-xs:.72rem; --t-sm:.78rem; --t-md:.85rem; --t-base:.95rem;
  --t-lg:1.05rem; --t-xl:1.5rem; --t-2xl:2.1rem;

  /* Space, on a 4px base. The last two are page furniture — the gap under the
     header and the room a fixed launcher needs at the foot of a document. */
  --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px; --s-5:24px;
  --s-6:32px; --s-7:48px; --s-8:72px; --s-9:120px;

  /* Radius. --radius stays as the name the cards already use. */
  --r-xs:4px; --r-sm:8px; --radius:12px; --r-lg:16px; --r-full:999px;

  /* Motion. One curve, three speeds: a control answering, something being
     revealed, and a fold changing the shape of the page. */
  --ease:cubic-bezier(.2,.8,.3,1);
  --m-fast:.16s; --m-mid:.35s; --m-slow:.5s;

  /* Not taste. The smallest target most people can hit reliably on a phone. */
  --tap:44px;
}
html[data-theme="dark"]{
  --bg:#0f0f11; --fg:#ededf0; --muted:#9d9da7; --faint:#85858f;
  --rule:#26262b; --chip:#1b1b1f; --wash:#141417;
  --ok:#57c98a; --bad:#f2837a; --warn:#e0b055;
  --shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);
  --shadow-lg:0 4px 12px rgba(0,0,0,.5), 0 24px 64px rgba(0,0,0,.55);
}
@media (prefers-color-scheme:dark){
  html[data-theme="auto"]{
    --bg:#0f0f11; --fg:#ededf0; --muted:#9d9da7; --faint:#85858f;
    --rule:#26262b; --chip:#1b1b1f; --wash:#141417;
    --ok:#57c98a; --bad:#f2837a; --warn:#e0b055;
    --shadow:0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35);
    --shadow-lg:0 4px 12px rgba(0,0,0,.5), 0 24px 64px rgba(0,0,0,.55);
  }
}

/* Reserve the scrollbar on every page, whether or not it needs one. Without
   this, moving from a page that scrolls to one that doesn't hands back ~15px
   and the whole centred layout — header included — jumps sideways. */
html{
  scrollbar-gutter:stable;
  /* iOS inflates text in a narrow column when the phone turns landscape, which
     picks the type scale apart one page at a time. 100% keeps what's specified.
     overscroll-behavior-x stops a sideways swipe being read as a back gesture,
     which is the other way a horizontally-scrolling region misbehaves. */
  -webkit-text-size-adjust:100%;text-size-adjust:100%;
  overscroll-behavior-x:none;
}
/* The grey flash a phone paints over anything tappable, on a site whose own
   states are already drawn. Removed for the look, not to hide the feedback —
   :active and :focus-visible both still say so. */
*{-webkit-tap-highlight-color:transparent}

*{box-sizing:border-box}
body{
  margin:0;background:var(--bg);color:var(--fg);
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif;
  font-size:var(--t-lg);line-height:1.6;-webkit-font-smoothing:antialiased;
  transition:background var(--m-mid) ease,color var(--m-mid) ease;
}
a{color:inherit}
h1,h2,h3{letter-spacing:-0.025em}

/* One focus ring for everything. :focus-visible rather than :focus so it
   appears for keyboard and assistive tech without drawing a box around every
   button a mouse happens to press. Two-tone so it stays visible whether the
   thing behind it is the page, a chip or a filled button. */
:where(a,button,input,textarea,select,summary,[tabindex]):focus-visible{
  outline:2px solid var(--fg);
  outline-offset:2px;
  border-radius:var(--r-xs);
  box-shadow:0 0 0 4px var(--bg);
}

/* Lets a keyboard user reach the content without tabbing the nav on every
   page. Off-screen until focused, which is the only time it is useful. */
.skip{
  position:absolute;left:-9999px;top:0;z-index:200;
  background:var(--fg);color:var(--bg);padding:var(--s-3) var(--s-4);border-radius:0 0 8px 0;
  font-size:var(--t-md);font-weight:560;text-decoration:none;
}
.skip:focus{left:0}

/* The page scrolling behind an open dialog is disorienting and, on iOS, it is
   the page that moves instead of the dialog's own list. */
body.no-scroll{overflow:hidden}

/* Present to a screen reader, absent to everyone else. For labels the design
   deliberately leaves undrawn, and for hints that would be clutter on screen
   but are the only way to know a control's shortcut without one. */
.sr-only{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}

/* One column for the page frame, so the header lands in the same place on
   every page, and a narrower one inside it for reading.

   These used to be the same width, which meant three of them: 720 on the
   homepage, 680 on the content pages, 940 on the two "wide" ones. The nav
   inherited all three and moved 440px between pages as you clicked around,
   and so did the sub-nav under it — 106px between About and Architecture.
   One frame, one reading column, on every page. Nothing here has ever
   genuinely needed to be wider; the two pages that claimed it were only
   filling the room they were given. */
/* The page's own inset, named because the sticky header has to pull itself
   back out of it to reach the edge of the screen. Two copies of these numbers
   would drift the first time one of them changed. */
.wrap{
  --pad-t:var(--s-5); --pad-x:var(--s-5);
  max-width:940px;margin:0 auto;padding:var(--pad-t) var(--pad-x) var(--s-9);
}
main,footer{width:100%;max-width:680px;margin-left:auto;margin-right:auto}
@media (max-width:600px){ .wrap{--pad-t:var(--s-4)} }

/* ---- top bar: who this is, where else to go, and the theme ----
   Identical on every page. The homepage used to override the alignment and
   the margin, which is half of why it looked like a different site.

   Stuck to the top at every width. This used to pin only on narrow screens,
   on the theory that a desktop header is close enough to reach by scrolling
   up. Long-form articles broke that theory — several screens of reading is
   exactly when losing the nav costs the most, on any size screen — so now it
   stays put everywhere rather than reading differently page to page.

   The negative margin and matching padding are what stop content showing in
   the gap above the bar as it scrolls under: the sticky box has to be flush
   with the top of the viewport, and the page's own top padding sits inside it.
   The safe-area inset is for a notched phone in landscape.

   The room left under it stays width-dependent even though the stickiness no
   longer is: --s-5 is tight enough for a phone's limited height, --s-7 is the
   breathing room a wide page can afford. That split predates this rule going
   sitewide and is still the right call — losing it in the merge would have
   quietly regrown the gap under the bar on every phone. */
.topbar{
  display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);
  position:sticky;top:0;z-index:50;background:var(--bg);
  margin:calc(var(--pad-t) * -1) calc(var(--pad-x) * -1) var(--s-5);
  padding:var(--s-3) max(var(--pad-x),env(safe-area-inset-right))
          var(--s-3) max(var(--pad-x),env(safe-area-inset-left));
  border-bottom:1px solid transparent;
  transition:border-color var(--m-fast) ease;
}
@media (min-width:861px){ .topbar{margin-bottom:var(--s-7)} }
/* The rule appears only once there is something scrolled underneath it, so
   the header reads as part of the page until it has to read as a bar. */
.topbar.stuck{border-bottom-color:var(--rule)}

/* The name, so a page that isn't the front one still says whose it is. Bold
   given name, quiet surname — enough to read as a mark rather than a heading,
   without being a logo nobody asked for. */
/* margin-right:auto rather than space-between on the bar: with three items,
   space-between centres the middle one in whatever room is left, so the tabs
   shifted a few pixels between pages as the bold current label changed width.
   Pushed right as a group, their right edge is pinned by the toggle. */
.wordmark{
  flex:0 0 auto;margin-right:auto;text-decoration:none;color:var(--muted);
  font-size:var(--t-base);font-weight:500;letter-spacing:-.01em;padding:var(--s-3) 2px;
}
.wordmark b{color:var(--fg);font-weight:660}
.wordmark:hover{color:var(--fg)}
/* Four links, and nothing drawn around them.

   This was a pill: a --chip track with the current tab as a white capsule
   inside it. Two more shapes on a page whose whole argument is restraint, to
   say something weight and colour already say. Where you are is the one in
   full contrast; the rest are quiet until you point at them.

   The row used to scroll sideways, on the theory that four words don't fit a
   320px phone. They do — the padding was the problem, not the labels. Tightened
   below 600px the four tabs come to 166px against 272px of room, and the row
   fits without scrolling, wrapping, shrinking the text or hiding behind a
   hamburger. The overflow rules stay because a fifth destination would need
   them, and because the fade is the honest thing to show if one ever doesn't
   fit; they simply don't fire at any width the site is built for. */
.tabs{
  display:flex;gap:var(--s-1);
  /* min-width:0 is what lets it shrink and scroll instead of pushing the
     toggle off a narrow phone: a flex item defaults to min-width:auto, which
     refuses to go below its content. */
  overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;min-width:0;
}
.tabs::-webkit-scrollbar{display:none}
/* Only when it genuinely scrolls, or the fade would clip a tab that fits. */
.tabs.overflowing{
  -webkit-mask-image:linear-gradient(to right,#000 86%,transparent 100%);
  mask-image:linear-gradient(to right,#000 86%,transparent 100%);
}
/* The long form of a label, dropped when the header runs out of room. Used by
   "About this page" → "About", the wordmark, and the Minimize button. */
@media (max-width:560px){ .long{display:none} }
/* The header is one line and four tabs, so under 560px the labels give up
   their long forms rather than the row giving up its last tab to a scroll
   nobody discovers. The gap tightens for the same reason. */
@media (max-width:560px){ .topbar{gap:var(--s-2)} }
/* 11px above and below is what makes the target 44px without a box to see. */
.tabs a{
  flex:0 0 auto;white-space:nowrap;
  text-decoration:none;color:var(--muted);font-size:var(--t-md);font-weight:520;
  padding:var(--s-3) var(--s-2);border-radius:var(--r-sm);transition:color var(--m-fast) ease;
}
.tabs a:hover{color:var(--fg)}
/* What buys the room to stop the row scrolling: 8px of side padding per tab is
   32px across four of them, and 32px is the entire shortfall at 320px — the
   labels themselves always fitted. The vertical padding is untouched, so the
   target stays 44px tall, and the tabs keep 8px between them.

   After .tabs a rather than beside the other narrow-screen rules, because a
   media query adds no specificity and these two weigh the same. */
@media (max-width:600px){ .tabs a{padding-left:var(--s-1);padding-right:var(--s-1)} }
.tabs a[aria-current="page"]{color:var(--fg);font-weight:650}
/* Scrolling the row means the container clips anything drawn outside a tab,
   and the standard ring sits 8px outside one. Drawn inside the tab instead,
   so making the bar scrollable doesn't quietly cost a keyboard user their
   focus indicator. */
.tabs a:focus-visible{outline-offset:-3px;box-shadow:none}
/* 44px is the smallest target most people can hit reliably on a phone. The
   toggle is drawn at 34px because a bigger circle would shout; the padding
   makes the tappable area the size it needs to be without the pixels. */
.toggle{
  width:34px;height:34px;border-radius:50%;flex:0 0 auto;position:relative;
  border:1px solid var(--rule);background:var(--bg);color:var(--muted);cursor:pointer;
  display:flex;align-items:center;justify-content:center;font-size:var(--t-xs);padding:0;
}
.toggle::after{content:'';position:absolute;inset:-6px}
.toggle:hover{border-color:var(--fg);color:var(--fg)}

/* ---- second level, under "About this page" ---- */
.subnav{
  display:flex;gap:var(--s-1);flex-wrap:wrap;margin:0 0 var(--s-6);
  border-bottom:1px solid var(--rule);padding-bottom:var(--s-3);
}
.subnav a{
  text-decoration:none;color:var(--muted);font-size:var(--t-md);font-weight:520;
  padding:var(--s-2) var(--s-3);border-radius:var(--r-sm);transition:color var(--m-fast) ease,background var(--m-fast) ease;
}
.subnav a:hover{color:var(--fg);background:var(--chip)}
.subnav a[aria-current="page"]{color:var(--fg);background:var(--chip);font-weight:600}

/* ---- section furniture ---- */
section{margin-top:var(--s-8)}
main > section:first-of-type{margin-top:0}
.page-title{font-size:var(--t-xl);font-weight:640;margin:0 0 var(--s-2)}
.page-intro{max-width:66ch;margin:0 0 var(--s-6);color:var(--muted)}
.prose{max-width:68ch}
.prose:first-of-type{margin-top:0}
/* A real <h2>, not a styled <p>. It looks like a small caps label either way,
   but only one of them lets someone on a screen reader jump between sections
   instead of arrowing through the whole page. */
.label{
  font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.15em;font-weight:700;
  color:var(--muted);margin:0 0 var(--s-4);padding-bottom:var(--s-2);border-bottom:1px solid var(--rule);
  line-height:1.5;
}
.sub{color:var(--muted);margin:0 0 var(--s-4)}
.live{
  display:inline-flex;align-items:center;gap:var(--s-2);font-size:var(--t-xs);color:var(--muted);
  background:var(--chip);border-radius:var(--r-full);padding:var(--s-1) var(--s-3);
}
.live i{width:6px;height:6px;border-radius:50%;background:var(--ok);display:inline-block}
.live i.stale{background:var(--warn)}
.live i.down{background:var(--bad)}

footer{
  margin-top:var(--s-8);padding-top:var(--s-2);border-top:1px solid var(--rule);
  color:var(--muted);font-size:var(--t-sm);display:flex;gap:var(--s-4);flex-wrap:wrap;
}
/* Text links are as tall as their line box — 18px. --tap is what anyone
   actually has to hit, so every link that isn't already a chip claims it. */
footer a,.subnav a,.chat-meta a{display:inline-flex;align-items:center;min-height:var(--tap)}

/* ---- cards: projects, articles, and the links out of "About this page" ----
   One card, three uses. The whole card is the click target rather than the
   title alone, which is both a bigger target and the behaviour people expect
   from something that looks like a card. */
.cards{display:flex;flex-direction:column;gap:var(--s-3)}
.card{
  display:block;text-decoration:none;position:relative;
  border:1px solid var(--rule);border-radius:var(--radius);padding:var(--s-4) var(--s-5);
  transition:border-color var(--m-fast) ease,transform var(--m-fast) ease,box-shadow var(--m-fast) ease;
}
/* Only the ones that actually go somewhere lift on hover. A card that reacts
   to the pointer and then does nothing when clicked is a small lie. */
a.card:hover{border-color:var(--fg);transform:translateY(-1px);box-shadow:var(--shadow)}
@media (prefers-reduced-motion:reduce){ a.card:hover{transform:none} }
.card h3{margin:0;font-size:var(--t-lg);font-weight:620;display:flex;align-items:baseline;gap:var(--s-2);flex-wrap:wrap}
.card .when{
  font-size:var(--t-xs);color:var(--faint);font-weight:500;font-variant-numeric:tabular-nums;
  letter-spacing:0;
}
.card p{margin:var(--s-1) 0 0;color:var(--muted);font-size:var(--t-base)}
.card .host{
  display:inline-block;margin-top:var(--s-2);font-size:var(--t-xs);color:var(--faint);
  font-variant-numeric:tabular-nums;
}
.card .tag{
  display:inline-block;font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.12em;
  font-weight:700;color:var(--muted);background:var(--chip);
  border-radius:var(--r-full);padding:3px var(--s-2);
}

/* A button that has to read as a link, because it opens the chat rather than
   navigating. Kept a button so it announces as one and works from a keyboard. */
.linkish{
  font:inherit;color:inherit;background:none;border:none;padding:0;
  text-decoration:underline;cursor:pointer;
}

/* Said plainly rather than dressed up as a broken card. */
.empty-note{
  border:1px dashed var(--rule);border-radius:var(--radius);
  padding:var(--s-5);color:var(--muted);font-size:var(--t-md);margin:0;
}

/* ---- the way into the chat, on pages that don't offer one inline ----
   Mounted by chat.js rather than written into each page, because the inline
   version is exactly what went missing when a page was copied to make the
   next one. Sits where the modal will open from, so opening reads as the pill
   growing rather than as a second thing arriving. */
.ask-fab{
  /* max() against the inset, so the launcher clears the home indicator on a
     phone without moving on anything that doesn't have one. */
  position:fixed;z-index:99;
  right:max(24px,env(safe-area-inset-right));
  bottom:max(24px,env(safe-area-inset-bottom));
  display:flex;align-items:center;gap:var(--s-2);
  min-height:44px;padding:var(--s-1) var(--s-4) var(--s-1) var(--s-1);
  font:inherit;font-size:var(--t-md);font-weight:560;color:var(--fg);
  background:var(--bg);border:1px solid var(--rule);border-radius:var(--r-full);
  box-shadow:var(--shadow);cursor:pointer;
  transition:transform var(--m-fast) var(--ease),box-shadow var(--m-fast) ease,opacity var(--m-fast) ease;
}
/* Same rule as the homepage portrait: colour in the dark, black and white in
   the light. Two photos of the same person in two treatments on one site is
   the kind of small inconsistency that reads as carelessness. */
.ask-fab img{
  width:32px;height:32px;border-radius:50%;object-fit:cover;object-position:50% 18%;
  background:var(--chip);display:block;filter:grayscale(1);
}
html[data-theme="dark"] .ask-fab img{filter:none}
@media (prefers-color-scheme:dark){ html[data-theme="auto"] .ask-fab img{filter:none} }
.ask-fab:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg)}
/* Out of the way while the modal it opens is on screen. dialog() has already
   made it inert; this is only so it isn't a ghost behind the scrim. */
body.no-scroll .ask-fab{opacity:0;pointer-events:none}
@media (max-width:600px){ .ask-fab{right:16px;bottom:16px} }
@media (prefers-reduced-motion:reduce){ .ask-fab{transition:none} .ask-fab:hover{transform:none} }

/* ============================================================================
   Chat — a modal on top of whatever page you were reading.
   ========================================================================= */
.chat-backdrop{
  position:fixed;inset:0;z-index:100;background:rgba(16,16,20,.28);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  opacity:0;pointer-events:none;transition:opacity var(--m-mid) ease;
}
html[data-theme="dark"] .chat-backdrop{background:rgba(0,0,0,.55)}
.chat-backdrop.open{opacity:1;pointer-events:auto}

.chat{
  position:fixed;z-index:101;right:24px;bottom:24px;width:min(440px,calc(100vw - 48px));
  height:min(620px,calc(100vh - 48px));display:flex;flex-direction:column;
  background:var(--bg);border:1px solid var(--rule);border-radius:var(--r-lg);
  box-shadow:var(--shadow-lg);overflow:hidden;
  opacity:0;transform:translateY(14px) scale(.97);pointer-events:none;
  transition:opacity var(--m-mid) var(--ease),transform var(--m-mid) var(--ease);
}
.chat.open{opacity:1;transform:none;pointer-events:auto}
@media (max-width:600px){
  .chat{right:0;bottom:0;width:100vw;height:100dvh;border-radius:16px 16px 0 0;border-bottom:none}
}
@media (prefers-reduced-motion:reduce){
  .chat,.chat-backdrop{transition:none}
}

.chat-head{
  display:flex;align-items:center;gap:var(--s-3);padding:var(--s-3) var(--s-3);
  border-bottom:1px solid var(--rule);flex:0 0 auto;
}
.chat-head .face{
  width:34px;height:34px;border-radius:50%;overflow:hidden;flex:0 0 auto;
  background:var(--chip);border:1px solid var(--rule);
}
/* Same cutout, same crop and same colour treatment as the launcher it opened
   from, so the head doesn't change between the two. */
.chat-head .face img{
  width:100%;height:100%;object-fit:cover;object-position:50% 18%;display:block;
  filter:grayscale(1);
}
html[data-theme="dark"] .chat-head .face img{filter:none}
@media (prefers-color-scheme:dark){ html[data-theme="auto"] .chat-head .face img{filter:none} }
.chat-head .who{flex:1;min-width:0}
.chat-head .who b{display:block;font-size:var(--t-base);font-weight:620;line-height:1.3}
.chat-head .who span{display:block;font-size:var(--t-xs);color:var(--muted);line-height:1.3}
.chat-x{
  border:none;background:transparent;color:var(--muted);cursor:pointer;
  width:44px;height:44px;border-radius:var(--r-sm);font-size:var(--t-lg);line-height:1;flex:0 0 auto;
}
.chat-x:hover{background:var(--chip);color:var(--fg)}

.chat-body{flex:1;overflow-y:auto;padding:var(--s-4) var(--s-4);scroll-behavior:smooth}
.chat-empty{color:var(--muted);font-size:var(--t-base);margin:2px 0 var(--s-4)}

.msg{margin-bottom:var(--s-4);animation:rise var(--m-mid) var(--ease)}
@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){ .msg{animation:none} }
.msg.you{display:flex;justify-content:flex-end}
.msg.you p{
  margin:0;background:var(--fg);color:var(--bg);border-radius:14px 14px 3px 14px;
  padding:var(--s-2) var(--s-3);max-width:82%;font-size:var(--t-base);
}
.msg.bot p{margin:0;font-size:var(--t-base);white-space:pre-wrap}
.msg.bot.refused p{color:var(--muted)}
.msg.bot.error p{color:var(--bad)}
.cites{margin-top:var(--s-2);display:flex;flex-wrap:wrap;gap:var(--s-1)}
.cites span{
  background:var(--chip);border-radius:var(--r-xs);padding:2px var(--s-2);
  font-size:var(--t-xs);color:var(--muted);font-variant-numeric:tabular-nums;
}
.caret{
  display:inline-block;width:7px;height:1.02em;background:var(--fg);
  vertical-align:text-bottom;margin-left:1px;animation:blink 1s steps(2) infinite;
}
@keyframes blink{0%,50%{opacity:1}51%,100%{opacity:0}}
.dots i{
  display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--muted);
  margin-right:var(--s-1);animation:pulse 1.2s infinite;
}
.dots i:nth-child(2){animation-delay:.15s}
.dots i:nth-child(3){animation-delay:.3s}
@keyframes pulse{0%,60%,100%{opacity:.25}30%{opacity:1}}
.tool-note{color:var(--faint);font-size:var(--t-sm);font-style:italic}

.chat-foot{flex:0 0 auto;border-top:1px solid var(--rule);padding:var(--s-2) var(--s-3) var(--s-3)}
.chat-form{
  display:flex;gap:var(--s-2);align-items:flex-end;
  border:1px solid var(--rule);border-radius:var(--radius);padding:var(--s-2) var(--s-2) var(--s-2) var(--s-3);
  transition:border-color var(--m-fast) ease;
}
.chat-form:focus-within{border-color:var(--fg)}
.chat-form textarea{
  flex:1;border:none;outline:none;resize:none;background:transparent;color:var(--fg);
  font:inherit;font-size:var(--t-base);line-height:1.5;padding:var(--s-2) 0;min-height:44px;max-height:104px;
}
.chat-form textarea::placeholder{color:var(--muted)}
.chat-send{
  border:none;background:var(--fg);color:var(--bg);border-radius:var(--r-sm);
  width:36px;height:36px;flex:0 0 auto;cursor:pointer;font-size:var(--t-sm);
  position:relative;transition:opacity var(--m-fast) ease;
}
.chat-send::after{content:'';position:absolute;inset:-5px}
.chat-send:disabled{opacity:.3;cursor:default}
.chat-meta{
  display:flex;justify-content:space-between;gap:var(--s-2);
  margin-top:var(--s-2);font-size:var(--t-xs);color:var(--faint);
}
.chat-meta a{color:var(--muted)}

.starters{display:flex;flex-wrap:wrap;gap:var(--s-1);margin-top:2px}
.starter{
  border:1px solid var(--rule);background:transparent;color:var(--muted);
  border-radius:var(--r-full);padding:var(--s-1) var(--s-3);font:inherit;font-size:var(--t-sm);cursor:pointer;
  transition:border-color var(--m-fast) ease,color var(--m-fast) ease;text-align:left;
}
.starter:hover{border-color:var(--fg);color:var(--fg)}

/* ---- message form: shown after a refusal, or from the header ---- */
.msg-form{
  border:1px solid var(--rule);border-radius:var(--radius);padding:var(--s-3);margin-top:var(--s-1);
  animation:rise var(--m-mid) var(--ease);
}
.msg-form p.lead{margin:0 0 var(--s-2);font-size:var(--t-base);color:var(--muted)}
.msg-form input,.msg-form textarea{
  width:100%;border:1px solid var(--rule);border-radius:var(--r-sm);background:transparent;
  color:var(--fg);font:inherit;font-size:var(--t-md);padding:var(--s-2) var(--s-2);margin-bottom:var(--s-2);outline:none;
}
.msg-form input:focus,.msg-form textarea:focus{border-color:var(--fg)}
.msg-form textarea{resize:vertical;min-height:74px}
.msg-form .row{display:flex;gap:var(--s-2);align-items:center;justify-content:flex-end;margin-top:2px}
.msg-form button{
  border:none;background:var(--fg);color:var(--bg);border-radius:var(--r-sm);
  padding:var(--s-2) var(--s-4);font:inherit;font-size:var(--t-md);font-weight:530;cursor:pointer;
}
.msg-form button:disabled{opacity:.35;cursor:default}
.msg-form button.ghost{background:transparent;color:var(--muted);border:1px solid var(--rule)}
.msg-form .err{color:var(--bad);font-size:var(--t-sm);margin:2px 0 0}

/* iOS Safari zooms the page in when you focus a field whose text is under
   16px, and it does not zoom back out when you leave — so one tap on the ask
   box leaves a visitor stranded on a magnified page they have to pinch their
   way out of. Every field on this site was under it: 15.2px in the two chat
   composers, 13.6px in the message form.

   Raised only where the pointer is coarse, so the type scale the rest of the
   site is built on is untouched on a desktop. 16px is not a preference here,
   it is the threshold.

   Placed after the rules it overrides rather than beside them. A media query
   adds no specificity, so this and `.msg-form input` weigh the same and the
   later one simply wins — written higher up the file it lost silently, which
   is how the message form kept zooming after the fix was supposedly in. */
@media (pointer:coarse){
  .chat-form textarea,
  .msg-form input,
  .msg-form textarea{font-size:16px}
}
.msg-sent{
  border:1px solid var(--rule);border-left:2px solid var(--ok);border-radius:var(--r-sm);
  padding:var(--s-3) var(--s-3);margin-top:var(--s-1);font-size:var(--t-md);
}
.offer{margin-top:var(--s-2)}
.offer button{
  border:1px solid var(--rule);background:transparent;color:var(--muted);
  border-radius:var(--r-full);padding:var(--s-1) var(--s-3);font:inherit;font-size:var(--t-md);cursor:pointer;
}
.offer button:hover{border-color:var(--fg);color:var(--fg)}

/* ============================================================================
   Experience — one continuous path (design option 1b).
   Roles alternate either side of a dotted spine; logo medallions sit on it.
   ========================================================================= */
.xp-head{
  display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-3);
  border-bottom:1px solid var(--rule);padding-bottom:var(--s-2);margin-bottom:var(--s-1);
}
.xp-head .label{margin:0;padding:0;border:none}
.xp-head span{font-size:var(--t-xs);color:var(--faint);font-variant-numeric:tabular-nums}

/* One gap, set once, rather than a margin on each kind of child. Every entry
   used to sit 15–21px from its neighbour while the lines inside an entry sat
   1–4px apart — the separation between two roles was barely larger than the
   separation within one, so seven blocks read as one wall of text. The rule
   being applied is proximity: what belongs together stays tight, and the space
   between groups has to be big enough to actually group them. */
.xp{position:relative;margin-top:var(--s-5);display:flex;flex-direction:column;gap:var(--s-7)}
/* the spine. A slow march downward suggests time passing without demanding
   attention; it stops entirely for anyone who asked for less motion. */
.xp-spine{
  position:absolute;left:calc(50% - 1px);top:10px;bottom:10px;width:2px;
  background:repeating-linear-gradient(180deg,var(--faint) 0 5px,transparent 5px 11px);
  opacity:.55;animation:march 2.4s linear infinite;
}
@keyframes march{to{background-position:0 22px}}

/* Each side is placed by column rather than by document order, so a row that
   only uses one side needs no empty filler element to hold the other. */
.xp-row{
  display:grid;grid-template-columns:1fr 96px 1fr;align-items:center;
  position:relative;
}
.xp-row .left{grid-column:1;text-align:right;padding-right:var(--s-2)}
.xp-row .medal-cell{grid-column:2}
.xp-row .right{grid-column:3;padding-left:var(--s-2)}

/* every medallion is the same circle, whatever shape the logo inside is */
.medal{
  width:64px;height:64px;border-radius:50%;margin:0 auto;
  background:var(--bg);border:1px solid var(--rule);box-shadow:var(--shadow);
  display:flex;align-items:center;justify-content:center;
  transition:transform var(--m-fast) var(--ease),box-shadow var(--m-fast) ease,border-color var(--m-fast) ease;
}
.xp-row:hover .medal{transform:scale(1.07);box-shadow:var(--shadow-lg);border-color:var(--fg)}

/* The logo is the mask, not the image: source files disagree on ink colour
   (two are white, one is black), so we paint the glyph in the theme's own
   colour and every logo reads at one weight. */
.medal i{
  display:block;width:40px;height:40px;background:var(--fg);opacity:.82;
  -webkit-mask:var(--logo) center/contain no-repeat;
  mask:var(--logo) center/contain no-repeat;
  transition:opacity var(--m-fast) ease;
}
.xp-row:hover .medal i{opacity:1}

.xp-role{margin:0;font-weight:620;font-size:var(--t-base);letter-spacing:-0.01em;line-height:1.35}
.xp-where{color:var(--muted);font-size:var(--t-sm);margin-top:1px}
.xp-dur{
  display:inline-block;margin-top:var(--s-1);font-size:var(--t-xs);color:var(--faint);
  background:var(--chip);border-radius:var(--r-full);padding:2px var(--s-2);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}

/* waypoints: today, acquisitions, the far end */
.xp-mark{display:flex;justify-content:center;position:relative}
.xp-mark span{
  display:inline-flex;align-items:center;gap:var(--s-2);font-size:var(--t-xs);color:var(--muted);
  background:var(--chip);border-radius:var(--r-full);padding:var(--s-1) var(--s-3);
  font-variant-numeric:tabular-nums; /* every waypoint carries a year */
}
.xp-mark.now span{color:var(--fg);font-weight:560;font-size:var(--t-sm)}
.xp-mark i{width:6px;height:6px;border-radius:50%;background:var(--ok);display:inline-block}
.xp-mark.now i{animation:breathe 2.4s ease-in-out infinite}
@keyframes breathe{0%,100%{opacity:.4}50%{opacity:1}}

/* The promotions inside one company: a rail of their own, branching off the
   main one. Dates sit under their title rather than beside it — side by side
   there is not enough column left to keep a title like "Business Technology
   Analyst" on one line, and a wrapped job title reads as a squeeze. */
.xp-climb{position:relative;display:flex;flex-direction:column;gap:var(--s-2);margin-top:var(--s-2)}
.xp-climb::before{content:'';position:absolute;top:6px;bottom:7px;width:1px;background:var(--rule)}
.xp-climb div{position:relative}
.xp-climb div::before{
  content:'';position:absolute;top:6px;width:5px;height:5px;
  border-radius:50%;background:var(--faint);
}
/* The rail mirrors, so it always runs along the side facing the spine. */
.right .xp-climb::before{left:3px}
.right .xp-climb div{padding-left:var(--s-4)}
.right .xp-climb div::before{left:1px}
.left .xp-climb::before{right:3px}
.left .xp-climb div{padding-right:var(--s-4)}
.left .xp-climb div::before{right:1px}

.xp-climb b{display:block;font-weight:620;font-size:var(--t-sm)}
.xp-climb span:first-child{display:block;font-size:var(--t-sm);font-weight:500}
/* "7" on one line and "mo" on the next reads as unfinished. */
.xp-climb span:last-child{
  display:block;margin-top:1px;white-space:nowrap;
  color:var(--faint);font-size:var(--t-xs);font-variant-numeric:tabular-nums;
}

/* the side venture, drawn as an aside rather than a step on the line */
.xp-aside{
  border:1px dashed var(--faint);border-radius:var(--radius);padding:var(--s-2) var(--s-3);
  display:inline-block;max-width:250px;text-align:left;
}
.xp-aside .k{
  font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.13em;
  font-weight:700;color:var(--faint);margin-bottom:2px;
}
/* A step smaller than a role on the line itself: this ran alongside one of
   them rather than being one of them, and the type should say so. */
.xp-aside .xp-role{font-size:var(--t-md)}
.xp-aside .xp-where{font-size:var(--t-sm)}

/* Reveal on scroll. Everything starts hidden only if we can observe it —
   without IntersectionObserver the section renders plainly rather than
   staying invisible forever. */
.xp.observed .reveal{opacity:0;transform:translateY(12px)}
.xp.observed .reveal.in{
  opacity:1;transform:none;
  transition:opacity var(--m-slow) var(--ease),transform var(--m-slow) var(--ease);
}
.xp.observed .medal{opacity:0;transform:scale(.6)}
.xp.observed .reveal.in .medal,
.xp.observed .medal.in{opacity:1;transform:none;transition:opacity var(--m-slow) ease,transform var(--m-slow) var(--ease)}

/* The promotions arrive after the company they happened at, one after the
   next, so the climb reads in the order it was climbed. Their delays are set
   in experience.js, because they have to be measured from when the row above
   them appears rather than from when the class lands — see the note there. */
.xp.observed .xp-climb div{opacity:0;transform:translateY(6px)}
.xp.observed .xp-climb::before{transform:scaleY(0);transform-origin:top}
.xp.observed .reveal.in .xp-climb div{
  opacity:1;transform:none;
  transition:opacity var(--m-mid) ease,transform var(--m-mid) var(--ease);
}
.xp.observed .reveal.in .xp-climb::before{
  transform:none;transition:transform var(--m-slow) ease var(--rail-delay,var(--m-fast));
}

@media (prefers-reduced-motion:reduce){
  .xp-spine,.xp-mark.now i{animation:none}
  .xp.observed .reveal,.xp.observed .medal,
  .xp.observed .xp-climb div,.xp.observed .xp-climb::before{
    opacity:1;transform:none;transition:none;
  }
}

/* Narrow: the spine moves left and everything stacks beside it. */
@media (max-width:620px){
  .xp-spine{left:31px}
  .xp{gap:var(--s-6)}
  .xp-row{grid-template-columns:64px 1fr;gap:var(--s-3)}
  .xp-row .left,.xp-row .right{text-align:left;padding:0;grid-column:2}
  .xp-row .medal-cell{grid-column:1;grid-row:1}
  /* .left pins to the medal's row; .right takes whatever row is left, so it
     sits beside the medal when it's alone and underneath when it isn't. */
  .xp-row .left{grid-row:1}
  /* Everything is on the right of the spine now, so the climb rail is too. */
  .left .xp-climb::before{left:3px;right:auto}
  .left .xp-climb div{padding-left:var(--s-4);padding-right:0}
  .left .xp-climb div::before{left:1px;right:auto}
  .xp-mark{justify-content:flex-start;padding-left:var(--s-1)}
}

/* ---- the design system, published on /architecture ---- */
.ds{display:flex;flex-direction:column;gap:var(--s-4)}
.ds-row{
  border:1px solid var(--rule);border-radius:var(--radius);
  padding:var(--s-4) var(--s-5);background:var(--wash);
}
.ds-row h3{margin:0 0 var(--s-1);font-size:var(--t-base);font-weight:620}
.ds-row p{margin:0 0 var(--s-3);color:var(--muted);font-size:var(--t-md)}
.ds-row code{font-size:var(--t-sm);background:var(--chip);border-radius:var(--r-xs);padding:1px 5px}
.ds-row pre{
  margin:0;overflow-x:auto;font-size:var(--t-sm);color:var(--fg);
  font-variant-numeric:tabular-nums;line-height:1.9;
}
.ds-ink{border-collapse:collapse;width:100%;font-size:var(--t-sm)}
.ds-ink td{padding:var(--s-1) 0;border-top:1px solid var(--rule);vertical-align:middle}
.ds-ink tr:first-child td{border-top:none}
.ds-ink i{
  display:inline-block;width:13px;height:13px;border-radius:var(--r-xs);
  border:1px solid var(--rule);margin-right:var(--s-2);vertical-align:-2px;
}
.ds-ink .on{color:var(--muted)}
.ds-ink .num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.ds-ink .grade{text-align:right;color:var(--ok);font-weight:620;white-space:nowrap;padding-left:var(--s-3)}

/* A number the page would rather not print. Marked so it can't be mistaken
   for a feature — this is the section admitting what isn't done yet. */
.note-honest{
  max-width:68ch;margin:var(--s-4) 0 0;padding:var(--s-3) var(--s-4);
  border-left:2px solid var(--warn);background:var(--wash);
  color:var(--muted);font-size:var(--t-md);
}
.note-honest b{color:var(--fg)}

/* ---- the interaction decisions, and the risk register, on /about ---- */
.why{display:flex;flex-direction:column;gap:var(--s-3)}
.why-row{border-left:2px solid var(--rule);padding:var(--s-1) 0 var(--s-1) var(--s-4)}
.why-row h3{margin:0 0 var(--s-1);font-size:var(--t-base);font-weight:620}
.why-row p{margin:0;color:var(--muted);font-size:var(--t-md);max-width:66ch}
/* The measurement that settled it, set apart from the argument for it. */
.why-n{
  margin-top:var(--s-2)!important;color:var(--faint)!important;font-size:var(--t-sm)!important;
  font-variant-numeric:tabular-nums;
}

.risks table{border-collapse:collapse;width:100%;font-size:var(--t-md)}
.risks th{
  text-align:left;font-size:var(--t-xs);font-weight:620;color:var(--muted);
  text-transform:uppercase;letter-spacing:.08em;padding:0 var(--s-4) var(--s-2) 0;
}
.risks td{
  border-top:1px solid var(--rule);padding:var(--s-3) var(--s-4) var(--s-3) 0;
  vertical-align:top;color:var(--muted);
}
.risks td:first-child{width:34%;color:var(--fg)}
.risks td:last-child{padding-right:0}
.risks td span{display:block;margin-top:2px;color:var(--muted);font-size:var(--t-sm);font-weight:400}
@media (max-width:600px){
  .risks td:first-child{width:auto}
  .risks tr{display:block;border-top:1px solid var(--rule)}
  .risks td{display:block;border-top:none;padding:var(--s-2) 0}
  .risks th{display:none}
}

/* ---- the cost model, on /architecture ---- */
.cost{display:flex;flex-direction:column;gap:var(--s-4)}
.cost-note{margin:var(--s-3) 0 0;color:var(--faint);font-size:var(--t-sm);max-width:64ch}
.cost-proj{border-collapse:collapse;width:100%;font-size:var(--t-sm);font-variant-numeric:tabular-nums}
.cost-proj th{
  text-align:left;font-weight:600;color:var(--muted);font-size:var(--t-xs);
  text-transform:uppercase;letter-spacing:.07em;padding-bottom:var(--s-2);
}
.cost-proj td{border-top:1px solid var(--rule);padding:var(--s-2) 0}
.cost-proj .num{text-align:right;padding-left:var(--s-4)}
.cost-proj .x{color:var(--fg);font-weight:620}
.cost-proj .now td{color:var(--fg)}
.cost-proj em{font-style:normal;color:var(--faint);font-size:var(--t-xs)}

/* ============================================================================
   Long-form articles — /articles/*.

   These arrived as a standalone export with their own palette (warm gold on
   cream), their own fonts and their own nav. None of that survives the move.
   Every rule below draws from the same tokens as the rest of the site, so a
   post reads as a page of this site that happens to be long, not as a
   pasted-in brochure. Emphasis is weight and contrast, the way the rest of
   the site does it, not a second colour — the palette above stays exactly
   two hues (ink and the semantic ok/bad/warn) on every page including these.
   ========================================================================= */
.post{max-width:68ch}
.post-kicker{
  display:inline-block;font-size:var(--t-xs);font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted);border:1px solid var(--rule);
  background:var(--chip);padding:var(--s-1) var(--s-3);border-radius:var(--r-full);
  margin-bottom:var(--s-4);
}
.post-title{
  font-size:clamp(1.7rem,4.4vw,2.5rem);font-weight:660;letter-spacing:-.02em;
  line-height:1.12;margin:0 0 var(--s-4);
}
.post-dek{font-size:var(--t-lg);color:var(--muted);line-height:1.55;margin:0 0 var(--s-4);max-width:62ch}
.post-byline{font-size:var(--t-sm);color:var(--faint);margin:0}
.post-hero{margin-bottom:var(--s-7)}

.post section{margin-top:var(--s-8)}
.post section:first-of-type{margin-top:0}
.post .secnum{
  display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:var(--t-xs);letter-spacing:.12em;color:var(--faint);margin-bottom:var(--s-2);
}
.post h2{font-size:var(--t-xl);font-weight:640;letter-spacing:-.01em;line-height:1.2;margin:0 0 var(--s-4)}
.post h3{font-size:var(--t-lg);font-weight:640;margin:var(--s-6) 0 var(--s-2)}
.post p{margin:0 0 var(--s-4);color:var(--fg);max-width:66ch}
.post p.lead{font-size:var(--t-lg);color:var(--fg)}
.post ul,.post ol{margin:0 0 var(--s-4) var(--s-5);color:var(--fg)}
.post li{margin:var(--s-1) 0}
.post strong{font-weight:640}
.post a{text-decoration:underline;text-underline-offset:2px}
.post code{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.86em;
  background:var(--chip);border:1px solid var(--rule);border-radius:var(--r-xs);
  padding:1px 6px;white-space:nowrap;
}
.post pre{
  background:var(--fg);color:var(--bg);border-radius:var(--radius);
  padding:var(--s-4) var(--s-5);overflow-x:auto;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  font-size:var(--t-sm);line-height:1.7;margin:var(--s-4) 0;
}
.post pre code{background:none;border:none;padding:0;color:inherit;white-space:pre}
.post .term{border-bottom:1.5px dotted var(--muted);cursor:help}

/* A quoted, larger claim pulled out of the paragraph around it — the one
   callout that gets to look different from a card, because it's doing a
   different job (emphasis, not containment). */
.post .pull{margin:var(--s-6) 0;padding-left:var(--s-5);border-left:3px solid var(--fg)}
.post .pull p{font-size:var(--t-xl);font-weight:560;line-height:1.35;color:var(--fg);margin:0;max-width:none}

.post .stats{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  gap:var(--s-3);margin:var(--s-6) 0;
}
.post .stat{border:1px solid var(--rule);border-radius:var(--radius);padding:var(--s-4);text-align:center}
.post .stat .n{font-size:var(--t-2xl);font-weight:700;color:var(--fg);line-height:1.1}
.post .stat .l{font-size:var(--t-sm);color:var(--muted);margin-top:var(--s-1);line-height:1.4}

/* A boxed aside — context worth setting apart, not a warning. --note-honest
   is reserved for the site's own gap-admissions, so this gets its own class
   even though the shape is the same. */
.post .aside{
  background:var(--wash);border:1px solid var(--rule);border-radius:var(--radius);
  padding:var(--s-4) var(--s-5);margin:var(--s-5) 0;
}
.post .aside p{margin:0;font-size:var(--t-base);color:var(--muted);max-width:none}
.post .aside b{color:var(--fg)}
.post .defbox{
  background:var(--wash);border:1px solid var(--rule);border-radius:var(--r-sm);
  padding:var(--s-3) var(--s-4);margin:var(--s-4) 0;font-size:var(--t-base);color:var(--muted);
}
.post .defbox b{color:var(--fg)}
.post .fwd{
  display:inline-block;font-size:var(--t-xs);font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;background:var(--chip);color:var(--faint);
  border-radius:var(--r-xs);padding:1px var(--s-2);vertical-align:2px;
}

/* ---- figures: the frame stays a flat light card in both themes, because
   the diagrams inside are authored as dark ink on a light surface and an
   inverted frame would erase their strokes. Same call the export made, kept
   for the same reason. ---- */
.post figure{margin:var(--s-6) 0}
.post figure .frame{
  background:#f6f6f8;border:1px solid #e6e6ea;border-radius:var(--r-lg);
  padding:var(--s-5);overflow-x:auto;
}
.post figcaption{font-size:var(--t-sm);color:var(--faint);margin-top:var(--s-3);text-align:center;line-height:1.5}

/* ---- then/now interactive demo ---- */
.post .duo{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-4);margin:var(--s-6) 0}
@media (max-width:640px){ .post .duo{grid-template-columns:1fr} }
.post .panel{border:1px solid var(--rule);border-radius:var(--r-lg);overflow:hidden;display:flex;flex-direction:column}
.post .panel .ph{
  padding:var(--s-3) var(--s-4);border-bottom:1px solid var(--rule);
  font-size:var(--t-xs);font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  display:flex;align-items:center;gap:var(--s-2);
}
.post .panel .dot{width:7px;height:7px;border-radius:50%;background:var(--faint)}
.post .panel.then .ph{color:var(--faint)}
.post .panel.now .ph{color:var(--fg)}
.post .panel.now .dot{background:var(--fg)}
.post .panel .pb{padding:var(--s-4);flex:1}
.post .bubble-q{
  background:var(--chip);border-radius:14px 14px 14px 4px;padding:var(--s-2) var(--s-3);
  font-size:var(--t-sm);color:var(--fg);margin-bottom:var(--s-3);max-width:90%;
}
.post .mono-result{
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:var(--t-xs);
  line-height:1.75;color:var(--muted);background:var(--wash);border:1px dashed var(--rule);
  border-radius:var(--r-sm);padding:var(--s-3);white-space:pre-wrap;
}
.post .chips{display:flex;flex-wrap:wrap;gap:var(--s-1);margin-bottom:var(--s-3)}
.post .chip{
  font:inherit;font-size:var(--t-xs);font-weight:600;border:1px solid var(--rule);
  background:var(--bg);color:var(--muted);border-radius:var(--r-full);
  padding:var(--s-1) var(--s-3);cursor:pointer;transition:border-color var(--m-fast) ease,color var(--m-fast) ease;
}
.post .chip:hover{border-color:var(--fg);color:var(--fg)}
.post .chip.on{background:var(--fg);border-color:var(--fg);color:var(--bg)}
.post .demo-cards{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-2)}
@media (max-width:400px){ .post .demo-cards{grid-template-columns:1fr} }
.post .demo-card{
  border:1px solid var(--rule);border-radius:var(--r-sm);padding:var(--s-2) var(--s-3);
  background:var(--bg);transition:opacity var(--m-mid) ease;
}
.post .demo-card.hide{display:none}
.post .demo-card .t{font-weight:620;font-size:var(--t-sm);line-height:1.3}
.post .demo-card .m{font-size:var(--t-xs);color:var(--faint);margin-top:2px;line-height:1.5}
.post .demo-card .tag{
  display:inline-block;font-size:var(--t-xs);font-weight:700;color:var(--muted);
  background:var(--chip);border-radius:var(--r-xs);padding:1px 6px;margin-top:var(--s-1);
}
.post .demo-hint{
  font-size:var(--t-xs);color:var(--muted);font-weight:600;margin-top:var(--s-3);
  display:flex;align-items:center;gap:var(--s-2);
}

/* ---- decision table ---- */
.post .scrollx{overflow-x:auto;margin:var(--s-5) 0}
.post .decide{
  display:grid;grid-template-columns:1.3fr 1fr 1fr 1fr;
  border:1px solid var(--rule);border-radius:var(--radius);overflow:hidden;
  background:var(--bg);font-size:var(--t-sm);min-width:560px;
}
.post .decide>div{padding:var(--s-3) var(--s-4);border-bottom:1px solid var(--rule)}
.post .decide>div:nth-child(4n+2),.post .decide>div:nth-child(4n+3),.post .decide>div:nth-child(4n+4){
  border-left:1px solid var(--rule);text-align:center;
}
.post .decide .hd{background:var(--chip);font-weight:700;font-size:var(--t-xs)}
.post .decide .hd.pick{background:var(--wash);color:var(--fg)}
.post .decide .req{font-weight:620;color:var(--fg)}
.post .decide .y{color:var(--ok);font-weight:700}
.post .decide .n{color:var(--bad);font-weight:700}
.post .decide .p{color:var(--warn);font-weight:700}
.post .decide .pickcol{background:var(--wash)}

/* ---- spec / claim audit ---- */
.post .audit{border:1px solid var(--rule);border-radius:var(--r-lg);padding:var(--s-5) var(--s-6);margin:var(--s-6) 0}
.post .audit h4{font-size:var(--t-lg);font-weight:640;margin-bottom:var(--s-1)}
.post .audit .sub{font-size:var(--t-sm);color:var(--muted);margin-bottom:var(--s-4)}
.post .audit ul{list-style:none;margin:0}
.post .audit li{
  display:flex;gap:var(--s-3);align-items:flex-start;padding:var(--s-3) 0;
  border-top:1px solid var(--rule);font-size:var(--t-base);margin:0;
}
.post .audit li:first-child{border-top:none}
.post .audit .mark{
  flex:none;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;
  justify-content:center;font-size:var(--t-xs);font-weight:800;margin-top:2px;
}
.post .audit .mark.ok{background:var(--chip);color:var(--ok)}
.post .audit .mark.ex{background:var(--chip);color:var(--warn)}
.post .audit li code{font-size:.82em}

/* ---- reading progress ---- */
.post-progress{position:fixed;top:0;left:0;right:0;height:2.5px;z-index:60;pointer-events:none}
.post-progress span{display:block;height:100%;width:0;background:var(--fg)}

/* ---- sources / further reading ---- */
.post-refs{border-top:1px solid var(--rule);padding-top:var(--s-6);margin-top:var(--s-8)}
.post-refs h2{margin-bottom:var(--s-3)}
.post-refs ol{font-size:var(--t-sm);color:var(--muted);margin:0 0 0 var(--s-5)}
.post-refs li{margin:var(--s-2) 0}
.post-refs .impl-note{font-size:var(--t-xs);color:var(--faint);margin-top:var(--s-4);max-width:66ch}
