/* ---------------------------------------------------------------------------
   The two faces the header is set in, served from this site.

   Reported as: the wordmark changes font for a moment on a hard refresh, and
   the tabs slide sideways. Both are one thing -- the browser draws the text in
   a fallback face, then redraws it in the real one, and the two have different
   letter widths. Proved rather than assumed, by blocking webfonts entirely:

     portfolio, desktop hard refresh
       webfonts as normal          biggest slide 12px
       webfonts blocked entirely   biggest slide  1px

   Google's copies arrive from another origin after a DNS lookup, a TLS
   handshake, a stylesheet and then the font file. These are same-origin,
   preloaded in every head, and 60KB for both -- DM Sans is one variable file
   covering 500 and 600, Playfair one covering 600.

   font-display: optional is the point of the exercise. swap says "draw it
   twice", which is the flicker; optional says "use it if it is ready, and
   otherwise never swap" -- so the text is drawn once, whatever happens. With a
   preloaded same-origin file it is ready.

   The Google stylesheet stays for everything else on these pages: the other
   weights, the italics, DM Mono. These rules come later in the cascade and so
   win for the two faces the header uses.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: 'DM Sans';
  font-style: normal;
  font-weight: 100 1000;
  font-display: optional;
  src: url('/blog/assets/fonts/dm-sans-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
                 U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  /* The italic too. The portfolio's headline is "Jayanth Katta" upright and
     "AWS Platform Engineer" italic, so leaving the italic on Google meant the
     h1 still changed height when it arrived -- measured at 163px -> 158px,
     1.8s in, which moved everything below it. Two faces solved most of the
     flicker and this is the rest of it. */
  font-family: 'Playfair Display';
  font-style: italic;
  font-weight: 100 1000;
  font-display: optional;
  src: url('/blog/assets/fonts/playfair-italic-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
                 U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Playfair Display';
  font-style: normal;
  font-weight: 100 1000;
  font-display: optional;
  src: url('/blog/assets/fonts/playfair-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC,
                 U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* Footer layout only. Colours come from whichever page this lands on.
 *
 * This file used to hardcode `background:#161a1a` and a `body.light` override.
 * Both were wrong. `.site-footer` is added by site-footer.js and this sheet is
 * injected AFTER blog.css, so at equal specificity it won -- the token rule in
 * blog.css (`.footer { background: var(--surface) }`) never applied, and the
 * footer stayed frozen on the pre-rotation green-black across all seven days.
 * The light override never fired at all: it keyed on `body.light`, and light
 * mode on this site is `body:not(.dark)` -- there is no `light` class -- so a
 * near-black bar sat under a #F7F6F5 page in light mode.
 *
 * `background: transparent` is the fix rather than a token, because this sheet
 * loads on four pages that name their tokens differently: blog.css uses
 * --surface for the ground, index.html and now.html use --surface for a panel
 * and --bg for the ground, and resume.html uses --paper. Transparent shows the
 * page's own body background, which is correct on every page, in both themes,
 * on every day of the rotation, without this file having to know any of it.
 *
 * Text derives from the page's own body colour rather than a muted token, for
 * the same reason. A fallback chain across --text-muted / --muted / --ink-faint
 * was tried and measured 3.49:1 on the home page in light mode: the names do
 * not mean the same thing on every page, so the chain picked a value intended
 * for a dark ground and put it on a light one. currentColor cannot make that
 * mistake -- it is whatever the page already proved readable for body text --
 * and mixing it 78% toward transparent gives the muted look without inventing
 * a colour. Measured 5.6:1 or better on all four pages, both themes.
 */
/* 64px, matching the header, and stated as a height rather than tuned padding.
 *
 * It was `padding: 2rem` with no height, which came to 82px: 32 + 32 around one
 * 17.28px line, plus the 1px border. The header is 64px because it says
 * `height: 64px`. So the two bars were sized by different mechanisms and nothing
 * kept them equal -- the footer was 18px taller while carrying one short line
 * against the header's logo and four links.
 *
 * Setting the height and centring the content means a change to the footer's
 * font size cannot drift the bar again, which tuned padding would. */
.site-footer {
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  height: 64px;
  padding: 0 2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border-top: 1px solid var(--border, var(--rule, transparent));
  color: inherit;                                          /* fallback */
  color: color-mix(in srgb, currentColor 78%, transparent);
  text-align: center;
  font-size: .72rem;
  line-height: 1.5;
}

.site-footer p { margin: 0; }

/* The link was a tan accent (--accent-gold / --orange, #C4A484). It measured
 * 7.37:1 on the dark ground and 2.16:1 on the light one -- the same colour
 * cannot serve both, and no fallback chain fixes that because the chain picks
 * by page, not by theme. Inheriting the footer's own colour is readable
 * wherever the footer is, and the underline carries the affordance the accent
 * used to. */
.site-footer a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
  opacity: .92;
}

.site-footer a:hover { opacity: 1; }

/* `height: auto` is what matters here: the base rule sets 64px, and with
   box-sizing:border-box the vertical padding would otherwise sit INSIDE that
   64px and leave no room for the line.

   The padding is symmetric, and that is not cosmetic. This rule used to read
   `2rem 4.25rem 2rem 1rem` with a comment claiming the asymmetric right
   padding cleared the floating scroll-to-top button -- but that button sits
   bottom-LEFT, where the padding was 1rem, so it cleared nothing and the
   copyright line ran underneath it. Two controls float over this bar and they
   are on opposite sides; only symmetric clearance misses both. Restated at the
   end of the file too, which is the rule that actually wins, and
   check_footer_clear.py measures the result rather than trusting either. */
@media (max-width: 768px) {
  .site-footer { height: auto; padding: 1.5rem 5rem; }
}

/* ── "why does this site change colour?" ─────────────────────────────────
 *
 * Colours here follow the same rule as the rest of this file: derived from the
 * page's own currentColor, never a token, because the four surfaces that load
 * this sheet name their tokens differently. The one exception is the swatches,
 * whose backgrounds are set inline by site-footer.js -- a swatch of Tuesday's
 * bone has to be bone on a Friday, so it cannot come from a live token.
 *
 * The footer's fixed 64px height is why this sits OUTSIDE it on desktop: adding
 * a second line inside would either overflow or force the bar taller than the
 * header it was matched to. The note is a sibling block below instead, and the
 * flex centring on .site-footer is what keeps both lines centred. */
.site-footer { flex-direction: column; gap: .35rem; height: auto; padding: 1.15rem 2rem; }

.pal-note { margin: 0; }

.pal-toggle {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  opacity: .8;
  transition: opacity .15s;
}
.pal-toggle:hover,
.pal-toggle:focus-visible { opacity: 1; }
.pal-name { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 2px; }


.pal-why {
  max-width: 46rem;
  margin: .5rem auto 0;
  text-align: left;
  font-size: .74rem;
  line-height: 1.7;
  border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent);
  padding-top: .8rem;
}
.pal-why p { margin: 0 0 .6rem; }
.pal-why p:last-child { margin-bottom: 0; }
.pal-why strong { font-weight: 600; opacity: 1; }

/* Seven near-identical rectangles are decoration until you can tell which day
   is which, so each carries its initial. Sunday-first, matching the JS DAYS
   order and the aria-label. */
.pal-week { display: flex; gap: .3rem; margin: 0 0 .8rem; }
/* A button, not a label: clicking repaints the page. Sized for a thumb -- the
   swatch is 24px but the hit area is the whole column including the initial. */
.pal-day {
  flex: 1; display: flex; flex-direction: column; align-items: stretch; gap: .25rem;
  background: none; border: 0; padding: 0; cursor: pointer; font: inherit; color: inherit;
}
.pal-day:hover .pal-sw,
.pal-day:focus-visible .pal-sw {
  border-color: color-mix(in srgb, currentColor 55%, transparent);
  transform: translateY(-1px);
}
.pal-day:hover .pal-ini, .pal-day:focus-visible .pal-ini { opacity: .9; }
.pal-sw { transition: transform .12s, border-color .15s; }
.pal-sw {
  height: 24px; border-radius: 3px;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
}
.pal-ini {
  font-size: .6rem; letter-spacing: .08em; text-align: center;
  opacity: .55; font-variant-numeric: tabular-nums;
}
/* Today gets a stronger edge and its initial at full strength -- no second
   colour needed, which matters because the ground itself cannot contrast with
   the page it is currently painting. */
.pal-day.is-today .pal-sw {
  border-color: color-mix(in srgb, currentColor 70%, transparent);
  box-shadow: inset 0 0 0 2px color-mix(in srgb, currentColor 16%, transparent);
}
.pal-day.is-today .pal-ini { opacity: 1; font-weight: 600; }

/* Two floating controls overlap this bar on a phone and they are on OPPOSITE
   sides: .back-top sits bottom-left (40px at 1.5rem) and .ask-launcher
   bottom-right (52px at 1.75rem). Centred text with symmetric clearance is what
   misses both. An earlier version of this rule left-aligned the content, which
   put the copyright line and the palette toggle underneath the scroll-to-top
   button -- the toggle is a control, so that made it unpressable, not just
   ugly. */
@media (max-width: 768px) {
  .site-footer { padding: 1.5rem 4.5rem 1.5rem 4.5rem; }
  .pal-why { text-align: left; }
  .pal-legend { line-height: 1.9; }
}

/* The seven names, in week order, under the swatches. The swatches alone show
   that the days differ; the names say what they are, which is the thing a
   reader who asked "why does this keep changing?" actually wanted. */
.pal-legend {
  font-size: .66rem;
  letter-spacing: .01em;
  opacity: .6;
  margin: -.35rem 0 .75rem !important;
}
.pal-legend b { font-weight: 600; opacity: 1; }

/* An underline alone did not read as "this opens something" -- on a phone it
   looks like a link to another page. A caret is the conventional disclosure
   affordance and rotates to show state. */
.pal-caret { font-size: .85em; line-height: 1; opacity: .55; transition: transform .18s; display: inline-block; }
.pal-toggle[aria-expanded="true"] .pal-caret { transform: rotate(180deg); opacity: .8; }

/* The instruction has to come before the swatches are understood as controls,
   so it sits directly under them rather than at the end. */
.pal-hint { opacity: .72; }

/* The association line, under the colour's own description. Italic and dimmer
   because it is a different KIND of claim: "earth, warmed" is a fact about the
   pixel values, this is what the colour carries generally. */
.pal-mean { font-style: italic; opacity: .72; }

.pal-reset {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  cursor: pointer; text-decoration: underline;
  text-decoration-thickness: 1px; text-underline-offset: 2px; opacity: .85;
}
.pal-reset:hover, .pal-reset:focus-visible { opacity: 1; }

/* ── palette control in the nav ──────────────────────────────────────────
 * Sized and bordered to match .audio-toggle / .nav-icon-btn (32px circle), so
 * it reads as a peer of the other appearance controls rather than an addition.
 * The border token falls back through three names because the four navs that
 * carry this control do not agree on one. */
.pal-nav { position: relative; display: inline-flex; flex-shrink: 0; }
/* Bare glyph -- see the note on .audio-toggle in blog.css. The open state can
   no longer be shown with a border colour, so it uses opacity and scale, which
   is what hover already does here. */
.pal-nav-btn {
  width: 32px; height: 32px; border-radius: 50%;
  background: transparent;
  border: none;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; padding: 0; opacity: .85;
  transition: opacity .15s, transform .15s;
}
.pal-nav-btn:hover, .pal-nav-btn[aria-expanded="true"] { opacity: 1; transform: scale(1.1); }
/* The dot IS today's ground, so on the page it came from it has no contrast of
   its own -- the inner ring is what gives it an edge. */
/* An emoji, not a swatch. A swatch here can never work: today's ground IS the
   page, so a patch of it has nothing to contrast against. The mark also gives
   the control a reason to change daily, which a static glyph would not. */
.pal-nav-dot { font-size: 15px; line-height: 1; display: block; }

.pal-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 1200;
  min-width: 210px; padding: 5px;
  background: var(--card-bg, var(--card, var(--surface, #fff)));
  border: 1px solid var(--border, var(--rule, rgba(128,128,128,.3)));
  border-radius: 10px;
  box-shadow: 0 10px 34px rgba(0,0,0,.16);
  font-size: .78rem; text-align: left;
  /* Set the colour, never inherit it. The panel hangs inside <nav>, and the
     intelligence pages give that bar a hardcoded light ink (#EDEBE6) for their
     dark header -- so in light mode the menu drew near-white text on its own
     near-white surface. Measured 1.03:1 against 11.67 on the blog and 13.32 on
     the home page, where the nav's colour happened to suit. A panel that sets
     its own background has to set its own foreground.

     --tx comes FIRST, and that ordering is the whole fix for a second bug.
     The status page defines --ink:#1D2322 unconditionally, on purpose, so its
     nav bar stays dark in both themes -- there it means "nav ink, fixed", not
     "body text". Chaining to it painted #1D2322 on that page's #1E2422 dark
     card: the same colour, a menu that rendered but could not be seen.

     The trap is that --ink names two different contracts on two pages. So the
     chain now leads with --tx, the token that is actually theme-aware, and
     only falls through to the older names on pages that do not define it. */
  color: var(--tx, var(--text, var(--ink, #1C2120)));
}
.pal-row {
  display: flex; align-items: center; gap: .55rem; width: 100%;
  padding: .42rem .5rem; border: 0; border-radius: 6px;
  background: none; color: inherit; font: inherit; cursor: pointer; text-align: left;
}
.pal-row:hover, .pal-row:focus-visible { background: color-mix(in srgb, currentColor 8%, transparent); }
.pal-row.is-on { font-weight: 600; }
.pal-row.is-on .pal-chip { box-shadow: inset 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent); }
.pal-chip {
  width: 17px; height: 17px; border-radius: 4px; flex: 0 0 auto;
  border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
.pal-row-day  { flex: 1; }
.pal-row-name { opacity: .55; font-size: .72rem; }

.pal-menu-foot {
  border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent);
  margin-top: 4px; padding: .45rem .5rem .25rem; font-size: .72rem; opacity: .7;
}
.pal-menu-reset {
  font: inherit; color: inherit; background: none; border: 0; padding: 0;
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
/* On a phone the panel hangs off the bar, not off its button.
   right:-40px worked while the palette button sat at the right-hand end of a
   single-row nav. Once the bar wrapped and the controls moved to the left of
   their own row, right-aligning a 248px panel to a button 60px from the left
   edge put it at left:-160 -- three quarters of it off the screen, which is
   what "it goes towards left and it's being cut" was.
   Making .pal-nav static hands the panel's positioning to the nav itself,
   which is sticky and therefore a containing block, so left and right insets
   pin it inside the viewport whatever the button underneath is doing. */
@media (max-width: 720px) {
  .pal-nav { position: static; }
  .pal-menu {
    top: calc(100% + 6px); left: 12px; right: 12px;
    width: auto; min-width: 0; max-width: none;
  }
}

.pal-row-mark { font-size: 13px; line-height: 1; width: 1.2em; text-align: center; opacity: .9; }

/* The mark's own line, under the colour's. Dimmer again: colour -> what it
   means -> what today's mark is, in decreasing order of how much it matters. */
.pal-mark-line { opacity: .6; }

/* Head of the menu: what the removed footer panel used to say. */
.pal-menu-head {
  padding: .5rem .55rem .55rem;
  border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
  margin-bottom: 4px;
  font-size: .76rem; line-height: 1.5;
}
.pal-menu-head strong { font-weight: 600; }
.pal-menu-head .pal-mean,
.pal-menu-head .pal-mark-line { display: block; font-size: .72rem; }
.pal-menu-head .pal-mean { font-style: italic; opacity: .7; margin-top: .15rem; }
.pal-menu-head .pal-mark-line { opacity: .55; margin-top: .1rem; }
.pal-menu { min-width: 248px; }

/* The footer is a single line again; the palette panel that used to sit under
   it moved into the nav. */
.site-footer { flex-direction: row; gap: 0; height: 64px; padding: 0 2rem; }
/* Clearance on BOTH sides, and this is the last rule, so this is the one that
   counts. It read `2rem 4.25rem 2rem 1rem` -- 4.25rem right, 1rem left -- which
   put the start of the copyright line under the scroll-to-top button, since
   that sits bottom-left from 24px to 64px. The symmetric rule 150 lines above
   was written for precisely this and was being silently overridden by this one.
   Its comment even records the same fault happening once before; a later
   asymmetric rule brought it straight back.
   5rem, measured rather than guessed: .back-top is 40px at 1.5rem, so it ends
   64px in; .ask-launcher is 52px at 1.75rem, so it ends 80px in. 4.5rem (72px)
   cleared the first and left the second overlapping by exactly 8px, which
   check_footer_clear.py reported the moment it was written. */
@media (max-width: 768px) { .site-footer { height: auto; padding: 1.5rem 5rem; } }


/* ---------------------------------------------------------------------------
   The cairn menu. Phones only: above 720px the five links fit on one line and
   collapsing them would cost a click for nothing.
   --------------------------------------------------------------------------- */
.ck-here, .ck-btn, .ck-sheet { display: none; }

/* 1080px, not 720.
   720 was the width at which the five links stopped fitting on the pages that
   carry only those five. The portfolio also carries its own section anchors --
   About, Writing, Skills, Contact, Tools -- and with both sets in one bar its
   content ran to 948px, so between roughly 860 and 1024 the last links sat off
   the right-hand edge of the window. Worse, between 721 and 820 the portfolio's
   own rules had already hidden the site links while the cairn had not yet
   appeared, leaving a width band with no way to reach another page at all.
   Measured rather than guessed: the portfolio's full bar comes to 1071px and
   does not shrink below it, so anything narrower pushed the right-hand end off
   screen. 1080 clears that with a little air.
   One breakpoint for every page, chosen for the page that needs it earliest,
   so the mark appears at the same width everywhere. The other pages could
   carry their links down to about 1000; showing the same thing at the same
   width on all of them is worth more than those eighty pixels. */
@media (max-width: 1080px) {
  /* The site's own links go behind the mark, wherever a page keeps them: in
     the list on most pages, in the action row on the portfolio. Everything
     else -- brand, palette, theme, audio, and the portfolio's section
     anchors -- stays where it was. */
  /* Marked in JS by matching the href, so a page's own section anchors are
     never mistaken for site navigation. */
  .ck-collapsed { display: none !important; }

  /* The same five, hidden from the FIRST FRAME rather than when the script
     gets there.
     .ck-collapsed is added at DOMContentLoaded, which on the blog is four
     seconds in -- 610KB of document to parse before its scripts run. For those
     four seconds a phone rendered all five links overflowing a 390px bar and
     overlapping the icons: "What's new" across the instrument, "Live status"
     off the edge over the theme control. Overlapping glyphs read as garbled
     characters, which is exactly how it was reported.
     Written out by href rather than as a blanket rule on .nav-links, because
     the Intelligence pages keep their theme and palette controls INSIDE that
     list, and the portfolio's own section anchors are not site navigation.
     Under html.ck-js, so a reader with JavaScript off keeps all five: the
     cairn that replaces them does not exist without JS. */
  html.ck-js nav .nav-links li:has(> a[href="/"]),
  html.ck-js nav .nav-links li:has(> a[href="/blog/"]),
  html.ck-js nav .nav-links li:has(> a[href="/intelligence/"]),
  html.ck-js nav .nav-links li:has(> a[href="/intelligence/whats-new/"]),
  html.ck-js nav .nav-links li:has(> a[href="/intelligence/status/"]),
  html.ck-js .nav .nav-links li:has(> a[href="/"]),
  html.ck-js .nav .nav-links li:has(> a[href="/blog/"]),
  html.ck-js .nav .nav-links li:has(> a[href="/intelligence/"]),
  html.ck-js .nav .nav-links li:has(> a[href="/intelligence/whats-new/"]),
  html.ck-js .nav .nav-links li:has(> a[href="/intelligence/status/"]) {
    display: none;
  }
  /* The list itself, once everything in it has gone behind the mark. Inside
     this query so it cannot outlive the thing that replaced it. */
  .nav-links.ck-empty { display: none !important; }

  /* ONE order for the bar, on every page.
     Measured at 390px before this existed, the same five pages produced three
     different layouts: the Intelligence pages hugged their controls to the
     left at x=46 and 91, the blog floated them into the middle at 154-242
     behind a spacer element, and the portfolio put them at 111-153 with no
     theme control at all. Each page had grown its own bar and each was
     internally reasonable; together they were not.
     Order is set here rather than in five stylesheets, because the only way
     these stay identical is if one rule decides it. The logo takes the left
     with margin-right:auto -- a single auto margin, so the outcome is
     deterministic -- and everything else lines up on the right in a fixed
     sequence, whatever order a page's markup happens to be in. */
  /* The bar sets its own spacing.
     Each page had its own gap -- 0, 12 and 24px -- so even with one order the
     first control landed in a different place on all three. The gap belongs to
     the arrangement, so it is declared with it. */
  nav, .nav { display: flex; align-items: center; gap: .55rem; }
  .nav-logo { margin-right: 0; }
  /* The controls sit with the mark, not away from it.
     The free space used to fall immediately after the logo, which pushed the
     icons across to the right and left the mark stranded alone in the corner.
     It now falls after them instead -- .ck-here already carries margin-left:
     auto -- so the bar reads as two groups: who this is and what you can
     change on the left, where you are and how to leave on the right. */
  .nav-logo    { order: 0; }
  .nav-links   { order: 1; }
  /* margin-left:0 is the point of this line. The portfolio sets auto on its
     action row, which is a second free-space claim in the same bar: with two,
     flexbox splits the slack between them and the controls drifted away from
     the mark no matter what order they were in. One claim, on the page name,
     and everything before it stays put. */
  .nav-actions { order: 2; display: flex; align-items: center; gap: .55rem;
                 margin-left: 0; }
  /* The same gap inside the list as outside it.
     On the Intelligence pages the controls are list items, so the space
     between the mark and the first icon came from the bar (8.8px) while the
     space between the two icons came from the list (13px). Two containers,
     two spacings, one row -- which reads as the icons being nudged rather
     than placed. */
  .nav-links { gap: .55rem; }
  /* margin:0 as well as order. The blog gives its audio button its own
     margin, which put the same control nine pixels further along than on
     every other page -- ordering fixes the sequence, not the spacing. */
  .audio-toggle{ order: 3; margin: 0; }
  .pal-nav     { order: 4; margin: 0; }
  .theme-toggle{ order: 5; margin: 0; }
  .ck-here     { order: 6; }
  .ck-btn      { order: 7; }
  /* A blog-only element that pushed its controls into the middle of the bar
     while every other page kept them at one end. */
  .nav-spacer  { display: none; }
  /* The portfolio hid its theme control on phones because it used to live in
     the hamburger drawer -- which the cairn replaced, taking the only way to
     change theme on that page with it. */
  .nav-actions .theme-toggle { display: inline-flex !important; }
  .nav-mobile-menu, .nav-hamburger { display: none !important; }

  .ck-here {
    display: inline-block; margin-left: auto; padding-right: .1rem;
    font-family: var(--mono, ui-monospace, monospace);
    font-size: .66rem; letter-spacing: .09em; text-transform: uppercase;
    /* Measured, not chosen. The accent is a pale tan built for a dark bar,
       where it sits at 7.2:1; on the light theme's near-white bar the same
       colour is 2.2:1, which is why the current page read as washed out
       rather than as the one thing in the row you are meant to notice.
       The light-mode value is the same hue taken down until it clears 6:1. */
    color: var(--acc, #C4A484); white-space: nowrap;
    max-width: 45vw; overflow: hidden; text-overflow: ellipsis;
  }
  body.light .ck-here { color: #7A5C3C; }
  /* There was a blog-only override here, pinning this to the dark-mode brown
     because the blog's bar used to be cream in BOTH themes -- a body.light
     rule never fired there, so the pale accent sat at 2.2:1 against it.
     Fixing the blog's theme default removed that premise and turned the
     workaround into the bug: dark brown on a now-dark bar, 2.7:1. A fix for a
     condition that no longer exists is worse than no fix, because it looks
     deliberate. The theme rules above cover every page. */
  .ck-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; padding: 0; margin-left: .1rem;
    background: none; border: none; cursor: pointer; flex: 0 0 auto;
    touch-action: manipulation; -webkit-tap-highlight-color: transparent;
  }
  .ck-cairn { width: 24px; height: 24px; display: block; }
  .ck-cairn ellipse { fill: var(--acc, #C4A484); }
  /* Resting: three stones. The fourth is balanced on as it opens. */
  .ck-cairn .ck-s2 { opacity: .82; }
  .ck-cairn .ck-s3 { opacity: .64; }
  .ck-cairn .ck-s4 { opacity: 0; transform: translateY(3px); }
  .ck-cairn ellipse { transition: opacity .22s ease, transform .22s ease; }
  .ck-open .ck-cairn .ck-s4 { opacity: .5; transform: translateY(0); }
  .ck-open .ck-cairn .ck-s3 { opacity: .74; }
  .ck-open .ck-cairn .ck-s2 { opacity: .9; }

  .ck-sheet {
    display: block; position: absolute; top: 100%; left: 0; right: 0;
    z-index: 1100;
    background: var(--card-bg, var(--card, var(--surface, #fff)));
    border-top: 1px solid var(--border, var(--rule, rgba(128,128,128,.22)));
    border-bottom: 1px solid var(--border, var(--rule, rgba(128,128,128,.22)));
    box-shadow: 0 18px 40px rgba(0,0,0,.32);
  }
  .ck-sheet[hidden] { display: none; }
  .ck-sheet ul { list-style: none; margin: 0; padding: .35rem .2rem; }
  .ck-sheet li { margin: 0; }
  .ck-sheet a {
    display: flex; align-items: center; gap: .7rem;
    padding: .85rem 1.1rem; min-height: 48px;
    text-decoration: none;
    /* --tx FIRST, for the reason spelled out on .pal-menu above: the status
       page defines --ink:#1D2322 unconditionally so its nav stays dark in
       both themes, where it means "nav ink, fixed" rather than "body text".
       Chaining to it painted #1D2322 on that page's own dark card -- the same
       colour, a panel that rendered and could not be read. Third time this
       token has bitten in this repo, so the chain matches the one that
       already survives it. */
    color: var(--tx, var(--text, var(--ink, #1C2120)));
    font-family: Georgia, "Times New Roman", serif; font-size: 1.02rem;
    touch-action: manipulation; -webkit-tap-highlight-color: transparent;
  }
  /* One stone per destination, in the same ink as the mark that opened it. */
  .ck-dot {
    width: 9px; height: 5px; border-radius: 50%; flex: 0 0 auto;
    background: var(--acc, #C4A484); opacity: .42;
  }
  /* You are here: the stone fills, the word takes the accent and a rule, and
     it says so in words. Three signals, because colour alone is the one that
     fails for the readers most likely to need it. */
  .ck-sheet a.is-here .ck-dot { opacity: 1; width: 12px; height: 6px; }
  .ck-sheet a.is-here .ck-label {
    color: var(--acc, #C4A484); font-weight: 600;
    border-bottom: 2px solid currentColor; padding-bottom: 1px;
  }
  .ck-you {
    margin-left: auto; font-family: var(--mono, ui-monospace, monospace);
    font-size: .56rem; letter-spacing: .1em; text-transform: uppercase;
    color: var(--mut, var(--muted, var(--text-muted, #7C8783)));
  }
  .ck-sheet a:active { background: color-mix(in srgb, currentColor 7%, transparent); }
  /* The page's own sections, under the site's destinations and clearly
     subordinate to them: these move you within a page, those move you between
     pages, and a reader should not have to work out which is which. */
  .ck-sub {
    margin: 0; padding: .7rem 1.1rem .25rem;
    border-top: 1px solid var(--border, var(--rule, rgba(128,128,128,.18)));
    font-family: var(--mono, ui-monospace, monospace);
    font-size: .56rem; letter-spacing: .14em; text-transform: uppercase;
    color: var(--mut, var(--muted, #7C8783));
  }
  .ck-sections a { padding-top: .6rem; padding-bottom: .6rem; min-height: 44px; }
  .ck-sections .ck-label { font-size: .92rem; }
}

@media (max-width: 1080px) and (prefers-reduced-motion: reduce) {
  .ck-cairn ellipse { transition: none; }
}

/* One wordmark, on every page.
   The three Intelligence pages set it in Playfair Display at 600; the
   portfolio and the blog set it in DM Sans at 700, and with different letter
   spacing from each other -- three variants of the same two words, in a bar
   that is otherwise identical everywhere.
   Declared here rather than in each page's stylesheet for the same reason the
   bar's order is: five copies of a rule is five chances for one to drift. This
   file is injected after each page's own CSS, so it wins on source order at
   equal specificity without needing !important.
   Colour is deliberately left alone -- the nav is dark on some pages and
   follows the theme on others, and each already handles its own. */
.nav-logo .brand-name {
  font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
  font-weight: 600;
  letter-spacing: normal;
}

/* The floating controls, identical on every page.
   Both were painted with var(--orange, var(--acc, ...)), and --acc is
   theme-aware: on the status page it darkens to #8A6A46 in light mode, so the
   same button was tan on the blog and What's New and brown there. These are
   solid accent chips sitting on top of the page rather than part of its
   surface, so they should not follow the theme at all -- the fifth instance
   tonight of a theme token used on something that does not change with the
   theme.
   Declared here because this file is injected after each page's own CSS, so
   one rule settles it for all of them. */
.back-top {
  background: #C4A484;
  color: #1D2322;
}
.to-src {
  color: #C4A484;
  border-color: #C4A484;
}

/* ---------------------------------------------------------------------------
   The bar above 1080px, decided in one place -- the same way it already is
   below 1080px.

   Everything that makes the five bars identical was written inside
   @media (max-width: 1080px), because every report until now came from a
   phone. Above that width each page was still arranging itself, and they had
   drifted into three different bars again:

     the portfolio put five section anchors AND four site links in one row,
     with .nav-links{flex:1} spreading them across the whole bar -- so at a
     1280px viewport "What's new" and "Live status" wrapped onto a second line
     inside the header

     the blog and the Intelligence pages right-aligned their links, the
     portfolio spread its own from the logo outward, so the same five words sat
     in different places on pages a reader moves between

     the current page was underlined on the Intelligence pages, coloured but
     not underlined on the blog, and unmarked on the portfolio

   So the arrangement below is the one from the phone block, with the links
   left in the bar instead of behind the mark. One order, one gap, one claim on
   the free space.
   --------------------------------------------------------------------------- */
@media (min-width: 1081px) {
  nav, .nav { display: flex; align-items: center; gap: .75rem; }

  /* The palette button used to be created by JavaScript, and this reserved
     44px for it in the meantime. That was the wrong fix and it made things
     worse: the reservation applied when the stylesheet loaded and was dropped
     when the button arrived, so the bar jumped 44px left and then 39px back
     right on every single load. It is in the markup now, present from the
     first frame, and needs no space held for it. */

  /* One size for the header, measured rather than assumed.
     Reported as: the wordmark expands on some pages and shrinks on others,
     and the instrument is bigger on the portfolio than on the blog. It is
     not the wordmark's WIDTH -- that is identical everywhere, x88 w97 at
     16px. What differs is everything around it:

       wordmark line-height   26.4  26.4  normal  normal  25.6
       instrument font-size   16px  15.2px    --      --     --
       instrument box         h34   h44       --      --     --
       theme glyph            16px  16px    16px    16px  13.3px
       control boxes          h32   h44     h32     h32    h32

     Five pages, four line-heights, three control heights, two glyph sizes.
     Each was reasonable on its own page and none of them agreed.

     Desktop only, deliberately: the 44px boxes on the blog are the phone's
     tap targets, and the phone's bar was reported as correct. */
  /* On the logo, so the mark and the wordmark inherit one value. The image is
     a fixed 30x30 either way, so this changes no box -- but three pages
     inheriting three different line-heights is how the boxes drifted in the
     first place, and a check that accepts "close enough" stops catching it. */
  nav .nav-logo, .nav .nav-logo,
  nav .brand-name, .nav .brand-name { line-height: 1.65; }
  nav .audio-toggle, .nav .audio-toggle,
  nav .theme-toggle, .nav .theme-toggle,
  nav .pal-nav-btn,  .nav .pal-nav-btn {
    width: 32px; height: 32px; padding: 0; line-height: 1;
  }
  /* The glyph inside, not just the box that holds it. */
  nav .audio-toggle, .nav .audio-toggle,
  nav .theme-toggle, .nav .theme-toggle { font-size: 16px; }

  /* And the space before them.
     The Intelligence pages keep these controls as list items inside
     .nav-links, so the space to their left came from the list's 1.4rem gap
     while the blog and the portfolio took the bar's .75rem -- which put the
     palette at x1306 on three pages and x1316 on two. The controls are a run
     of controls wherever a page happens to put them, so they take the bar's
     spacing either way. */
  html.ck-js nav .nav-links > li:has(> .pal-nav),
  html.ck-js nav .nav-links > li:has(> .theme-toggle),
  html.ck-js nav .nav-links > li:has(> .audio-toggle),
  html.ck-js nav .nav-links > li:has(> .nav-ctl) {
    margin-left: calc(.75rem - 1.4rem);
  }
  .nav-logo    { order: 0; margin-right: 0; }
  /* The single free-space claim. Two of them -- .nav-links{flex:1} on the
     portfolio and margin-left:auto on its action row -- is what spread that
     bar out; flexbox splits the slack between every auto it finds, so the
     result depends on how many a page happens to have. One claim, here, and
     the links land against the controls on all five. */
  .nav-links   { order: 1; flex: 0 0 auto; margin: 0 0 0 auto; padding: 0;
                 display: flex; align-items: center; gap: 1.4rem;
                 list-style: none; }
  .nav-actions { order: 2; display: flex; align-items: center; gap: .75rem;
                 margin-left: 0; }
  .audio-toggle{ order: 3; margin: 0; }
  .pal-nav     { order: 4; margin: 0; }
  .theme-toggle{ order: 5; margin: 0; }
  /* A blog-only spacer that claims the slack itself, and the portfolio's
     hamburger, which the cairn replaced. */
  .nav-spacer  { display: none; }
  .nav-hamburger, .nav-mobile-menu { display: none !important; }
}

/* The link boxes themselves, at every width.
   nowrap is the line that stops "What's new" breaking in half: it was the
   longest label in the row and the first to fold when the bar ran short of
   room, which read as a layout accident rather than a full bar. */
nav .nav-links a, .nav .nav-links a {
  display: inline-flex; align-items: center; height: 44px;
  position: relative; white-space: nowrap;
  font-size: .875rem; font-weight: 500; letter-spacing: normal;
  /* The spacing between links belongs to the list's gap, in one place. The
     portfolio bought its own with .85rem of padding on each link and gap:0 on
     the list, so with a shared gap applied the same five words sat 27px apart
     there and 22px apart on the blog -- close enough to look like a rendering
     difference rather than a decision. */
  padding: 0; background: none; border-bottom: 0;
}
/* One ink for the links you are not on, from the measurement in
   site-footer.js. Three values had grown here: pure white on the portfolio and
   the blog, the warm off-white on the Intelligence pages, and a third, faintly
   green grey on the status page.

   :not(.is-current) is specificity, and it is needed now for a reason worth
   recording: this stylesheet used to be injected by JS, so it always came
   last and a tie went to it. Linking it in the head to stop the bar being
   painted twice also moved it BEFORE each page's own rules -- so every tie it
   had been silently winning, it started losing. Light mode was the tell: dark
   mode unified and light mode came back three values again. */
nav .nav-links a:not([aria-current="page"]),
.nav .nav-links a:not([aria-current="page"]) {
  color: var(--nav-idle, rgb(156, 154, 148));
}
/* One APPEARANCE for the three icon controls, at every width.
   The instrument button was added to the Intelligence pages and came out as a
   raw browser button there -- rgb(240,240,240) with a 2px outset border and
   square corners -- because those pages style .theme-toggle by name and had
   never had an .audio-toggle to style. Measured:

     portfolio   transparent | 0px none | 50%
     blog        transparent | 0px none | 50%
     hub         rgb(240,240,240) | 2px outset | 0px
     whats-new   rgb(240,240,240) | 2px outset | 0px
     status      rgb(240,240,240) | 2px outset | 0px

   The glyph's colour was a third value on those pages as well: rgb(245,245,243)
   on the portfolio, rgba(255,255,255,.72) on the blog, rgba(237,235,230,.72) on
   the Intelligence pages. It takes the same ink as the links now, from the same
   measurement of the bar.

   Not inside the desktop query: a control that renders as a grey OS button is
   wrong at every width, and this sets appearance, not layout -- the phone's
   44px tap targets are untouched. */
nav .audio-toggle, .nav .audio-toggle,
nav .theme-toggle, .nav .theme-toggle,
nav .pal-nav-btn,  .nav .pal-nav-btn {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: 0;
  border-radius: 50%;
  box-shadow: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
nav .audio-toggle, .nav .audio-toggle,
nav .theme-toggle, .nav .theme-toggle {
  color: var(--nav-idle, rgb(156, 154, 148));
}

/* One size for the icon controls too. The portfolio's instrument button was
   34px wide against 32 everywhere else, which is not visible on its own page
   and is exactly the kind of two-pixel difference that adds up to five bars
   that never quite line up. */
nav .audio-toggle, .nav .audio-toggle,
nav .theme-toggle, .nav .theme-toggle { width: 32px; flex-shrink: 0; }

/* Colour on hover, not a filled chip. With the padding gone the portfolio's
   background highlight has nothing to sit in, and no other page had one.

   One hover colour as well as one shape. Measured in light mode across the
   four: the portfolio darkened to near-black ink, the Intelligence pages did
   not visibly change at all, and the blog went to the dark-mode tan -- so
   hovering the same word did three different things depending on which page
   you happened to be reading. It is now the colour the current page is marked
   in, which also says what "selected" will look like before you click. */
/* html and [href] are specificity, not decoration, and this is the third rule
   in this file to need it. The portfolio carries

     body.light .nav-links a:hover { background: rgba(0,0,0,.04); ... }

   which scores higher than a plain `nav .nav-links a:hover`, so it survived in
   light mode only: hovering any link on the portfolio drew a grey rectangle
   that no other page drew, and only for readers in light mode. Dark mode had
   no equivalent rule, which is exactly why it looked fixed.

   The lesson is the file's, not the page's: since this stylesheet moved from
   being injected last to being linked in the head, it no longer wins ties, and
   every shared rule has to out-specify the page rule it replaces rather than
   merely follow it. */
html nav .nav-links a[href]:hover,
html .nav .nav-links a[href]:hover {
  background: none;
  color: var(--nav-here, #C4A484);
}

/* Keyed on aria-current, which is in the HTML, rather than on the class
   site-footer.js adds. Same rule, but it applies on the first frame
   instead of when the script runs -- and the current link is 600 where
   the others are 500, so bolding it late made the whole right-aligned row
   slide left. The class is still added; nothing depends on it now. */
/* Which page you are on, said the same way on all five.
   The blog already had this rule and it had been dead for weeks: the tap-target
   work made .nav-links a an inline-flex box, which turns a display:block
   ::after into a flex ITEM sitting beside the word at zero width rather than a
   rule underneath it. Absolute positioning is immune to what the parent's
   display happens to be, which is the point -- this has to survive the next
   change to the bar, not just this one.
   The extra [aria-current] is specificity, not decoration: three pages set
   .nav-links a colour in light mode at the same weight as this rule, and a tie
   is settled by load order, which differs per page. */
html nav .nav-links a[href][aria-current="page"],
html .nav .nav-links a[href][aria-current="page"],
html nav .nav-actions a[href][aria-current="page"] {
  color: var(--nav-here, #C4A484);
  font-weight: 600;
  opacity: 1;
}
html nav .nav-links a[href][aria-current="page"]::after,
html .nav .nav-links a[href][aria-current="page"]::after,
html nav .nav-actions a[href][aria-current="page"]::after {
  content: ""; position: absolute; display: block;
  left: 0; right: 0; bottom: 8px; width: auto; height: 2px; margin: 0;
  border-radius: 2px; background: currentColor; opacity: .9;
}

/* The portfolio's own sections -- About, Writing, Skills, Contact, Tools.
   They are not site navigation and they were the reason its bar held ten items
   while every other page held five. They live in the cairn now, under "On this
   page", which is where the same five have been on a phone since the cairn
   replaced the drawer; site-footer.js reads them out of this list. */
.nav-sections { display: none !important; }

/* Full geometry, not just colour.
   The colour was pinned here already, but the button itself only existed on
   blog posts and the Intelligence pages -- the portfolio and the blog index
   are both long enough to want one and had none. site-footer.js now adds it
   wherever it is missing, so the geometry has to come from the shared file
   too, or the injected one would be a bare browser button. */
.back-top {
  position: fixed;
  bottom: 1.5rem;
  left: 1.5rem;
  right: auto;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  line-height: 1;
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transform: translateY(8px);
  transition: opacity .2s, transform .2s;
  box-shadow: 0 6px 20px rgba(0, 0, 0, .22);
}
/* pointer-events with the opacity: a fully transparent button still takes
   clicks, and this one sits over the bottom-left corner of every page. */
.back-top.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .back-top { transition: none; }
}

/* The disc behind the brand mark, from the measurement in site-footer.js.
   Outside the desktop query because a mismatched colour is mismatched at every
   width, and because this sets colour and not layout. */
nav .brand-mark, .nav .brand-mark {
  background: var(--nav-mark, rgb(41, 35, 35));
}
/* One ink handed down by the logo, so the mark and the wordmark cannot inherit
   two different values -- the check compares what it finds, and "invisible on
   an <img>" is how a difference survives until it lands somewhere visible. */
nav .nav-logo, .nav .nav-logo { color: var(--nav-word, #EDEBE6); }

/* Keep the scrollbar's width reserved, so the page cannot jump sideways when
   one appears.

   Reported as: on a hard refresh the portfolio moves left and right. It is not
   the navigation and not the fonts -- it is the scrollbar.

     at first paint the portfolio is 900px tall and fits the window, so there is
     no scrollbar; a moment later JS fills the terminal, the stats and the
     skills grid, the page becomes 2140px, a scrollbar appears, and on Windows
     that scrollbar takes ~15px of real width -- so the content area narrows and
     everything shifts left.

   Worth recording why this took so long to find: headless Chromium and headless
   Firefox both use OVERLAY scrollbars, which take no width at all. Every
   measurement I ran reported clientWidth 1440 from the first frame to the last,
   on a page that visibly jumps on a real Windows desktop. The instrument could
   not see the fault, so for several rounds I kept measuring the things it COULD
   see and calling them the cause.

   scrollbar-gutter: stable reserves the space whether or not a scrollbar is
   showing. The fallback covers browsers without it -- overflow-y: scroll always
   reserves, at the cost of a visible empty track on short pages, which is why
   it is the fallback and not the rule.

   No effect on phones: touch scrollbars are overlays and take no width. */
html {
  scrollbar-gutter: stable;
}
@supports not (scrollbar-gutter: stable) {
  html { overflow-y: scroll; }
}

/* ---------------------------------------------------------------------------
   "Live status" glows while one of the clouds is actually broken.

   It was a dot first. The dot was appended after a fetch, so the link got 12px
   wider a second into every page load and -- the row being right-aligned --
   every link slid left, on four of five pages. A signal meant to be
   unobtrusive became the biggest movement on the site.

   The words glow instead, and that is not a smaller version of the same idea:
   colour and text-shadow do not participate in layout AT ALL. There is no
   width to change, so there is nothing that can move. Whatever else this
   animation does, it cannot push the bar around, which after a day spent
   making this bar hold still is the property that matters most.

   It appears only when something is open, shows nothing if the status file
   cannot be read, and under prefers-reduced-motion it settles on the accent
   colour and stops moving -- the colour is the information, the breathing is
   only what draws the eye.
   --------------------------------------------------------------------------- */
nav .nav-links a.is-live, .nav .nav-links a.is-live,
.ck-sheet a.is-live .ck-label {
  animation: live-glow 2.8s ease-in-out infinite;
}
@keyframes live-glow {
  0%, 100% { color: var(--nav-idle, rgb(156, 154, 148)); text-shadow: none; }
  50%      { color: #E0A458; text-shadow: 0 0 10px rgba(224, 164, 88, .45); }
}
@media (prefers-reduced-motion: reduce) {
  nav .nav-links a.is-live, .nav .nav-links a.is-live,
  .ck-sheet a.is-live .ck-label {
    animation: none;
    color: #E0A458;
  }
}

/* The menu's own focus behaviour.

   The panel takes focus when it opens so a keyboard or screen-reader user
   lands inside it, and it is never drawn -- a container is not a control, and
   ringing it just puts a blue box over the first item.

   The links inside DO ring, on :focus-visible only, which is the browser
   saying "this person is navigating by keyboard". In the site's accent rather
   than the browser's blue, because a focus ring is part of the design for the
   people who rely on it. */
.ck-sheet:focus { outline: none; }
.ck-sheet a:focus-visible {
  outline: 2px solid #C4A484;
  outline-offset: 3px;
  border-radius: 8px;
}
