/* ---------------------------------------------------------------
   raspberryc.at — one stylesheet, no build step, no framework.

   Two grounds, both warm and both deliberately neutral, so accents
   can be pushed in any direction later without fighting a tint:

     light — parchment #f4efe1
     dark  — soot      #221f19

   They're each other's inverse: soot is the ink on parchment, and
   parchment is very nearly the ink on soot. One design, two values.

   The accent is inverted between them, on purpose. Gold is the one
   thing dark mode can do that light mode can't: at the lightness AA
   allows on parchment, yellow has almost no chroma left and reads as
   ink rather than as a colour. Cyan is the opposite — naturally dark
   AND saturated — and it opposes both the warm ground and the warm
   ink instead of sitting on the same axis as them.

     light — dark cyan  #00699e
     dark  — gold       #d8a72c

   Every colour below clears WCAG AA on its own ground, against the
   darkest surface it actually sits on — ratios noted, and they're
   checked, not guessed.

   Sections, in order:

     Fira Sans Condensed
     links
     header / nav
     theme toggle
     content icons ({% icon %} shortcode)
     headings & prose
     lists
     blockquotes
     rules and figures
     dungeon maps (```map)
     code
     tables: rulebook, not spreadsheet
     citations and bibliography
     footnotes (markdown-it-footnote)
     post index (archive)
     post lists
     single post
     tag index
     tag browser (archive)
     rolls (blogroll / podroll / vidroll)
     the sources under a roll
     collapsible roll table
     blogroll as a random table
     roll button (added by /js/roll.js)
     recent posts under a blogroll entry
     webrings
     footer
     small screens
     print

   Colours are all custom properties defined in the :root blocks below;
   nothing further down should hardcode one.
   --------------------------------------------------------------- */

/* --- Fira Sans Condensed -----------------------------------------
   Self-hosted, SIL OFL 1.1 (see /fonts/OFL.txt). Mozilla/Telefónica.

   Five real cuts — no synthesised weights or slants. The 500 weight is
   deliberately not shipped because nothing uses it.

   Declaring a face costs nothing until something on the page actually
   uses that weight: browsers only fetch the files they need. So a plain
   post pulls 400 (+400 italic if it has emphasis), and the bold cuts
   stay unfetched.
   ------------------------------------------------------------------ */

/* The tagline, and only the tagline. Fontshare ships no italic for Chillax,
   which is fine here — a tagline never needs one. */
@font-face {
  font-family: "Chillax";
  src: url("/fonts/chillax-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Fira Sans Condensed";
  src: url("/fonts/fira-sans-condensed-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Fira Sans Condensed";
  src: url("/fonts/fira-sans-condensed-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Fira Sans Condensed";
  src: url("/fonts/fira-sans-condensed-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Fira Sans Condensed";
  src: url("/fonts/fira-sans-condensed-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Fira Sans Condensed";
  src: url("/fonts/fira-sans-condensed-700-italic.woff2") format("woff2");
  font-weight: 700;
  font-style: italic;
  font-display: swap;
}
/* THE MAP FACE — two fonts wearing one name, split by which characters each is
   good at.

   Square, by Wouter van Oortmerssen, is drawn for roguelikes: every glyph fills
   a cell as wide as it is tall, which is what lets the grid be square. But it
   has no real lowercase. Its a-z are the capitals at 88%, with a descender of
   exactly zero, so `r` and `R` are the same mark and `pqgy` never drop below
   the baseline — fatal when lowercase is how a roguelike names its creatures.

   Orbitron is the closest thing to Square's idiom that has proper lowercase:
   capitals 0.99 as wide as they are tall against Square's 1.00, monoline, flat
   terminals — and an x-height of 0.81 with a 0.23 descender.

   So: Square for everything, Orbitron for a-z. `unicode-range` is what makes
   that a per-character decision rather than a fallback, which would never fire
   — Square HAS lowercase glyphs, they're just the wrong shape.

   The two happen to sit together well. Orbitron's lowercase is 0.583 em against
   Square's 0.813 cap, so the pair reads at an x-height ratio of 0.72.

   Neither is monospaced and neither needs to be: every character sits in its own
   fixed grid track and is centred there, so the cell does the spacing.

   `block`, not `swap`: the fallback is a monospace stack of quite different
   proportions, so a flash of it would visibly reshape the map before settling.

   THE OVERRIDES ARE LEAD, NOT DECORATION. A cell centres its line box, and the
   baseline within it comes from the font's own ascent and descent — for a flex
   item that works out to `cell/2 + (ascent - descent) x font-size / 2`, with
   line-height cancelling. Square declares 0.875/0.125, because it HAS no
   descenders and never needed to allow for any. That puts the baseline at 82.3%
   of the cell, and Orbitron's g then hangs to 101.9% — out the bottom of its
   cell and into the row below.

   So both faces are given the same overridden metrics, chosen as the ink that
   actually has to fit: Square's 0.813 cap above the baseline, Orbitron's 0.229
   descender below it. That lands the baseline at 75.1%, with the cap top at
   5.2% and the g bottom at 94.8% — the whole alphabet inside its cell, and both
   fonts sharing one baseline. */
@font-face {
  font-family: "Map";
  src: url("/fonts/square.ttf") format("truetype");
  unicode-range: U+0020-0060, U+007B-007E;   /* all of printable ASCII but a-z */
  ascent-override: 81.3%;
  descent-override: 22.9%;
  line-gap-override: 0%;
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
/* Orbitron at 500, not 400: next to Square's 0.125 em monoline it reads thin at
   map size, and the two have to look like one alphabet. The weight is declared
   HERE, on the face, rather than as a font-weight on the cell — the two fonts
   share a family, so bolding the element would ask the browser to synthesise a
   bold for Square as well, which it does by smearing. */
@font-face {
  font-family: "Map";
  src: url("/fonts/orbitron-500.woff2") format("woff2");
  unicode-range: U+0061-007A;                /* a-z */
  ascent-override: 81.3%;
  descent-override: 22.9%;
  line-gap-override: 0%;
  font-weight: 400;
  font-style: normal;
  font-display: block;
}

:root {
  color-scheme: light dark;

  --bg:        #f4efe1;  /* parchment */
  --bg-sunken: #ece5d6;  /* code blocks, panels, the webring box */
  --fg:        #221f19;  /* body — soot     14.6:1 */
  --fg-muted:  #5f574a;  /* dates, captions  6.6:1 */
  /* Held down to clear AA on the tag chip (#e8e0cf), the darkest light
     surface it ever sits on — parchment alone would allow it lighter. */
  --accent:    #00699e;  /* links            5.2:1 on parchment, 4.6:1 on a chip */
  --accent-hi: #005580;  /* hover            7.0:1 */
  --rule:      #ddd4c2;  /* hairlines — not text, so AA doesn't apply */
  --tag-bg:    #e8e0cf;

  /* No plate needed on parchment: favicons are designed for light grounds. */
  --favicon-plate: transparent;
  --favicon-pad:   0;

  /* Highlighted rows: a jumped-to reference, a rolled table row.
     Strong enough to find at a glance (1.27:1 against the ground), while
     every colour that sits ON it still clears AA — which is why the accent
     deepens here rather than reusing the link colour. */
  /* The site name, picked off the raspberry in the logo. ONE value, not a
     per-theme pair: it's a brand colour, so it stays put and the outline
     around it does the adapting instead. Defined here rather than in each
     theme block for exactly that reason. */
  --raspberry:        #b81d47;
  --raspberry-hi:     #d42456;


  --zebra:            #ece5d6;   /* alternating table rows */
  /* Cool, so it belongs to the cyan accent rather than fighting it. Held at
     1.22:1 against parchment — any lighter and a rolled row stops being
     findable at a glance. */
  --highlight:        #c9dde6;
  --highlight-muted:  #5f574a;   /* 5.1:1 on the highlight */
  --highlight-accent: #006191;   /* 4.6:1 — the link cyan drops to 4.0 here */

  /* One family for prose and furniture alike. The fallbacks are the
     narrowest system faces available, so a failed font load shifts the
     layout as little as possible. */
  --font-body: "Fira Sans Condensed", "Segoe UI", Roboto, system-ui, sans-serif;
  --font-ui:   "Fira Sans Condensed", "Segoe UI", Roboto, system-ui, sans-serif;
  /* The site name is a traced path, not text — see partials/wordmark.svg — so
     there's no display face to load. This is the tagline under it. */
  --font-caption: "Chillax", "Fira Sans Condensed", system-ui, sans-serif;
  /* Code stays monospaced — a condensed sans can't hold a code block. */
  --font-mono: ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace;
  /* Maps only — see the two @font-face rules above, which put Square and
     Orbitron under this one name. Kept apart from --font-mono because between
     them they carry the 95 printable ASCII characters and nothing else: enough
     for a dungeon, nowhere near enough for a code block. */
  --font-map: "Map", var(--font-mono);

  /* Narrower than the old 40rem: condensed glyphs fit noticeably more
     characters per line, and 40rem of Fira Condensed overshoots a
     comfortable line length. */
  --measure: 36rem;

  --gutter: 1.5rem;
}

/* Dark when the OS asks for it — unless the reader has explicitly
   chosen light with the toggle. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:        #221f19;  /* soot */
    --bg-sunken: #2b2720;
    --fg:        #eae3d4;  /* body            12.9:1 */
    --fg-muted:  #9a8f7e;  /* dates, captions  5.2:1 */
    --accent:    #d8a72c;  /* links            7.4:1 */
    --accent-hi: #ecc054;  /* hover           10.1:1 */
    --rule:      #443e33;
    --tag-bg:    #2b2720;  /* tag text on this  4.7:1 */

    /* Light plate so dark-glyph favicons stay visible. */
    --favicon-plate: #f4efe1;
    --favicon-pad:   1px;


    --zebra:            #2a2620;   /* alternating table rows */

    /* A tint of this theme's own accent, exactly as the light theme's is a
       tint of its cyan. The old #39332a was neutral brown: it had a HIGHER
       contrast ratio than the light highlight and still read weaker, because
       it shifted only in lightness — colour step 2.2 against light mode's
       14.6. Same overall strength here, spent on colour instead. */
    --highlight:        #3b2f13;   /* 1.25:1 against the ground, colour step 15.2 */
    --highlight-muted:  #aca08a;   /* 5.1:1 on it; the usual muted fails */
    --highlight-accent: #d8a72c;   /* 5.9:1 — gold on gold still clears AA */
  }
}

/* An explicit choice always wins, whatever the OS says. The attribute
   selector outranks the bare :root above, so order isn't load-bearing.
   The values are repeated rather than shared because custom properties
   can't be inherited across a media-query boundary — annoying, but the
   alternative (light-dark()) fails hard on older browsers instead of
   degrading. */
:root[data-theme="dark"] {
  --bg:        #221f19;
  --bg-sunken: #2b2720;
  --fg:        #eae3d4;
  --fg-muted:  #9a8f7e;
  --accent:    #d8a72c;
  --accent-hi: #ecc054;
  --rule:      #443e33;
  --tag-bg:    #2b2720;  /* tag text on this  4.7:1 */

  --favicon-plate: #f4efe1;
  --favicon-pad:   1px;


  --zebra:            #2a2620;
  --highlight:        #3b2f13;
  --highlight-muted:  #aca08a;
  --highlight-accent: #d8a72c;
}

/* Keep native form controls and scrollbars in step with the choice. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

*, *::before, *::after { box-sizing: border-box; }

html {
  font-size: 100%;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;

  /* Reserve the scrollbar's width always. Without this, moving from a long
     page to a short one removes the scrollbar and the whole layout jumps
     sideways by ~15px — the single most noticeable jitter when clicking
     between pages. */
  scrollbar-gutter: stable;
}

/* Smooth cross-document navigation where the browser supports it. This is
   what makes a multi-page site feel less like a hard reload: the old page
   fades to the new one instead of blanking. Ignored everywhere else, so it
   costs nothing. */
@view-transition { navigation: auto; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

body {
  margin: 0;
  padding: 0 var(--gutter) 4rem;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  text-rendering: optimizeLegibility;
}

/* --- links ------------------------------------------------------ */

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}
a:hover, a:focus { color: var(--accent-hi); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: var(--gutter);
  top: 0.5rem;
  z-index: 10;
  padding: 0.5rem 0.75rem;
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* --- header / nav ----------------------------------------------- */

.site-header,
main,
.site-footer {
  max-width: var(--measure);
  margin-inline: auto;
}

.site-header {
  padding: 2.5rem 0 1.25rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 2.5rem;
}

/* Logo, then the title and tagline stacked beside it. The tagline stays
   outside the link so the link's accessible name is just the site name. */
.site-id {
  display: flex;
  align-items: center;
  /* Tight, because the ripple spreads to the very edge of the logo's box:
     the drawing brings its own optical margin and 0.9rem on top of that read
     as a gap. */
  gap: 0.45rem;
}

.site-logo { flex-shrink: 0; line-height: 0; }

/* Two hand-drawn variants rather than one recoloured by CSS. Both are in the
   markup; only the one matching the active theme is shown.

   NOTE the specificity: these hide rules are (0,1,0), so anything that sets
   `display` on .site-logo's children with a heavier selector will beat them
   and BOTH logos appear at once. That exact bug bit the sun/moon toggle. */
.site-logo__light,
.site-logo__dark { display: none; }

.site-logo__light { display: flex; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .site-logo__light { display: none; }
  :root:not([data-theme="light"]) .site-logo__dark { display: flex; }
}
:root[data-theme="dark"] .site-logo__light { display: none; }
:root[data-theme="dark"] .site-logo__dark { display: flex; }
:root[data-theme="light"] .site-logo__light { display: flex; }
:root[data-theme="light"] .site-logo__dark { display: none; }

/* Sized by HEIGHT, not width: this mark is wider than it is tall and the
   ripple runs to both edges, so pinning the width would let the drawing set
   the height of the whole header row. Pinning the height keeps that row
   stable and lets the crop change only how wide the mark sits. */
.site-logo img { height: 96px; width: auto; }
/* Below the measure the header has to share its width with the theme toggle,
   so the mark comes down rather than pushing the tagline into a wrap.

   56px is the ceiling, and it is a WIDTH limit wearing a height's clothes:
   at 320px the row is logo + 0.45rem + the wordmark's 13.5rem floor, with
   nothing to spare. 64px here clips the wordmark's last letters. */
@media (max-width: 26rem) {
  .site-logo img { height: 56px; }
}

/* The tagline carries the space below the whole header block; with the logo
   beside it, that margin has to sit on the group instead or the logo and the
   text stop being vertically centred against each other. */
.site-id__text > .site-tagline { margin-bottom: 0; }
.site-id { margin-bottom: 1.1rem; }

/* Both halves of the lockup carry a stroke in the PAGE colour, not a contrast
   colour — that's what lets the tagline sit over the title without the two
   sets of letterforms tangling. `paint-order: stroke fill` puts the stroke
   behind the glyph, so the shapes stay intact instead of being eaten from the
   inside; without it a stroke this heavy would close up the counters. */
/* Thick enough to read as a deliberate outline rather than an artefact. In em,
   so it tracks the fluid size instead of going spindly on a big screen.
   `paint-order` draws the stroke behind the glyph — without it a stroke this
   heavy eats the letterforms from the inside and closes up the counters. */
.site-title {
  display: block;
  /* The link is only a wrapper now; the wordmark carries the size. */
  line-height: 0;
  text-decoration: none;
}

/* The traced wordmark. Geometry comes from the SVG (viewBox, stroke width,
   rounded joins, paint-order); only colour and size live here.

   `paint-order="stroke"` in the file is what makes the outline grow outward
   from the letterforms instead of eating into them — the same reason the CSS
   version needed `paint-order: stroke fill`, but here `stroke-linejoin` is
   honoured too, so the corners are genuinely round rather than mitred or
   approximated. */
.wordmark {
  display: block;
  width: clamp(13.5rem, 58vw, 24rem);
  height: auto;
  fill: var(--raspberry);
  stroke: var(--fg);
}
.site-title:hover .wordmark { fill: var(--raspberry-hi); }

/* Emboldened variant: the outline is its own path, so the two colours have to
   be assigned separately — see tools/build-wordmark.mjs. */
.wordmark--bold .wordmark__outline { fill: none; stroke: var(--fg); }
.wordmark--bold .wordmark__fill { fill: var(--raspberry); stroke: var(--raspberry); }
.site-title:hover .wordmark--bold .wordmark__fill {
  fill: var(--raspberry-hi);
  stroke: var(--raspberry-hi);
}


/* Sits clear below the title rather than crossing it, so it needs no outline
   of its own — the gap does the separating. Plain ink, so the raspberry above
   is the only colour in the masthead. */
.site-tagline {
  /* Negative, because the wordmark's viewBox carries padding for the outline —
     there's a band of empty SVG under the letters that reads as a gap the
     tagline has to climb back over. */
  margin: -0.15rem 0 0 0;
  font-family: var(--font-caption);
  font-weight: 500;
  font-size: 0.82rem;
  color: var(--fg);
  font-style: normal;
  letter-spacing: 0.01em;
  line-height: 1.3;
  text-wrap: balance;
}

/* --- theme toggle ------------------------------------------------ */

.site-id-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
}

/* Hidden unless JavaScript ran — theme.js adds .js to <html>. A toggle
   that can't toggle is worse than no toggle. */
.theme-toggle { display: none; }

/* Styled to match .nav-icons exactly: bare glyph, colour shift on hover,
   no border and no plate. The only extras are the <button> resets that an
   <a> doesn't need. Focus ring still comes from the global :focus-visible. */
.js .theme-toggle {
  display: flex;
  flex-shrink: 0;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  color: var(--fg-muted);
  cursor: pointer;
}
.js .theme-toggle:hover { color: var(--accent); }

/* NB: do not add a `.theme-toggle span { display: flex }` rule here. Its
   specificity (0,1,1) beats the plain `.theme-toggle__sun { display: none }`
   below (0,1,0), so before a theme is explicitly chosen BOTH icons render.
   The show/hide rules set display themselves. */

/* Show the icon for what the button will DO, not what the page is now:
   a moon on the light theme, a sun on the dark one. */
.theme-toggle__sun  { display: none; }
.theme-toggle__moon { display: flex; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle__sun  { display: flex; }
  :root:not([data-theme="light"]) .theme-toggle__moon { display: none; }
}

:root[data-theme="dark"]  .theme-toggle__sun  { display: flex; }
:root[data-theme="dark"]  .theme-toggle__moon { display: none; }
:root[data-theme="light"] .theme-toggle__sun  { display: none; }
:root[data-theme="light"] .theme-toggle__moon { display: flex; }

.site-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1.25rem;
}

.nav-links,
.nav-icons {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-links,
.nav-icons { flex-wrap: wrap; }

.nav-links a {
  font-family: var(--font-ui);
  font-size: 0.9rem;
  text-transform: lowercase;
  letter-spacing: 0.02em;
  color: var(--fg-muted);
  text-decoration: none;
}
.nav-links a:hover { color: var(--accent); }

/* Set by helpers.getLinkActiveState: aria-current for the exact page,
   data-state for a section the current page sits inside. */
.nav-links a[aria-current="page"],
.nav-links a[data-state="active"] {
  color: var(--fg);
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 0.3em;
}

.nav-icons { gap: 0.85rem; }

.nav-icons a {
  display: flex;
  color: var(--fg-muted);
}
.nav-icons a:hover { color: var(--accent); }

.icon-glyph {
  display: inline-block;
  vertical-align: -0.13em;   /* optical baseline sit for a 1em glyph */
  fill: currentColor;
  flex-shrink: 0;
}

/* An icon opening a paragraph or list item wants a little breathing room. */
.icon-glyph + * { margin-left: 0.1em; }

/* Sized so every nav glyph has the SAME INK HEIGHT, not the same box.
   Simple Icons share a 24x24 viewBox but draw to different extents inside
   it, so equal pixel sizes give visibly unequal glyphs:

     rss        ink fills 100% of its box   (reference, 20px -> 20px of ink)
     rootring   ink fills 100%
     itchdotio  ink fills  90%   (y 1.2-22.8)
     bluesky    ink fills  88%   (y 1.4-22.6)

   Measured by tools/icon-bounds.mjs, which parses the path data properly.
   Regenerate with:  node tools/icon-bounds.mjs --ref rss --size 20 */
.icon--bluesky   { width: 22.75px; height: 22.75px; }
.icon--itchdotio { width: 22.25px; height: 22.25px; }

/* The rootr.ing mark is a full-bleed disc — its outer edge sits exactly on the
   viewBox boundary, so it can't grow within the glyph. Rendering it a little
   larger is what pushes that outer edge out. Tune this one value; the hole
   scales with it, so for a thicker *band* change the inner radius in
   src/_icons/ui/rootring.svg instead. */
.icon--rootring { width: 20px;    height: 20px; }

/* Image marks (e.g. the rootr.ing badge) are solid discs, which read heavier
   than the line glyphs beside them — a couple of pixels smaller optically
   matches. They don't inherit currentColor, so no hover recolour either. */
.icon--img {
  width: 18px;
  height: 18px;
  border-radius: 50%;
}

/* --- headings & prose -------------------------------------------
   Tuned for Fira Sans Condensed. A condensed face has a large x-height
   and narrow glyphs, which means two things: it can carry a slightly
   smaller size than a serif at the same legibility, and it needs more
   leading, because tight glyphs on loose lines is what makes condensed
   type feel cramped.
   ----------------------------------------------------------------- */

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-ui);
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.01em;
  text-wrap: balance;          /* stops a lone word dangling on line two */
}

/* Space above is deliberately much larger than below: a heading should
   bind to the text it introduces, not float between two blocks. */
h2 { font-size: 1.5rem;  margin: 2.75rem 0 0.7rem; }
h3 { font-size: 1.2rem;  margin: 2.1rem 0 0.55rem; }
h4 { font-size: 1.02rem; margin: 1.8rem 0 0.45rem; }

/* Past h4 there's no size left to give, so the distinction switches from
   scale to treatment. */
h5, h6 {
  font-size: 0.85rem;
  margin: 1.6rem 0 0.4rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-muted);
}
h6 { font-weight: 400; font-style: italic; text-transform: none; letter-spacing: 0.02em; }

h1 {
  font-size: 2rem;
  margin: 0 0 0.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}

/* A heading straight after another heading shouldn't get the full gap. */
h2 + h3, h3 + h4, h4 + h5, h5 + h6 { margin-top: 1.1rem; }

main > h1 + .lede { margin-top: -0.35rem; }

.lede {
  color: var(--fg-muted);
  font-size: 1.05rem;
}

p { margin: 0 0 1.1rem; }

/* --- lists ------------------------------------------------------- */

/* Scoped to prose, deliberately. These were once bare `li` selectors, which
   also matched .nav-links li and .nav-icons li — and because a flex row
   centres the MARGIN box, the bottom margin lifted every item except the
   last, leaving the final nav item sitting ~2.4px low. Navigation, rolls and
   feed lists all style their own items; only Markdown output needs these. */
.post-body ul,
.post-body ol { margin: 0 0 1.1rem; padding-left: 1.35rem; }

.post-body li { margin-bottom: 0.3rem; }
.post-body li:last-child { margin-bottom: 0; }

/* Nested lists tighten up and lose the bottom margin, so a nested block
   doesn't open a gap inside its parent item. */
.post-body li > ul,
.post-body li > ol { margin: 0.3rem 0 0.4rem; }

.post-body ul { list-style: square; }
.post-body ul ul { list-style: disc; }
.post-body ul ul ul { list-style: circle; }

/* The marker is furniture, not content — mute it so the text leads. */
.post-body li::marker { color: var(--fg-muted); }
.post-body ol > li::marker { font-family: var(--font-ui); font-variant-numeric: tabular-nums; }

/* A list item containing paragraphs needs its own rhythm back. */
.post-body li > p { margin-bottom: 0.6rem; }
.post-body li > p:last-child { margin-bottom: 0; }

/* --- blockquotes --------------------------------------------------- */

blockquote {
  margin: 1.6rem 0;
  padding: 0.1rem 0 0.1rem 1.15rem;
  border-left: 2px solid var(--accent);
  color: var(--fg-muted);
}
blockquote p { margin-bottom: 0.7rem; }
blockquote p:last-child { margin-bottom: 0; }

/* Nested quotes step down in weight rather than repeating the accent. */
blockquote blockquote {
  margin: 0.8rem 0 0.2rem;
  border-left-color: var(--rule);
  font-size: 0.97em;
}

/* --- rules and figures --------------------------------------------- */

hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 2.5rem auto;
  width: 100%;
}

img, video, svg.figure { max-width: 100%; height: auto; }

/* Images in prose get room and centring, and fill the measure by default —
   an image in a post is an illustration, not an inline ornament. Set a
   different width with a #fragment on the URL:  ![alt](/images/x.png#320)
   which becomes an .img-w-* class in /css/generated.css. */
.post-body img {
  display: block;
  width: 100%;
  height: auto;
  margin: 1.6rem auto;
  color: var(--fg-muted);
}

/* A manual width (an .img-w-* class from /css/generated.css) overrides the
   100% above. Those rules live in the generated sheet and double their own
   class to outrank this one — see lib/markdown.js. */

/* Lone images become <figure> at render time — see eleventy.config.js.
   Use  ![alt](/images/x.svg "Caption")  to get a real caption. */
figure {
  margin: 1.8rem 0;
  text-align: center;
}
/* In a figure, the FIGURE owns the spacing. Written as `.post-body figure img`
   because `.post-body img` above scores (0,1,1) and would otherwise win: it
   gives a prose image 1.6rem of margin, and that bottom margin was pushing a
   caption a whole 1.6rem further from its picture than a map's caption sits
   from its panel. */
figure img,
.post-body figure img { display: block; margin: 0 auto; }

figcaption {
  margin-top: 0.55rem;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--fg-muted);
  text-align: center;
  text-wrap: pretty;
}

/* Raster images carry their own background, which on the dark theme means a
   white screenshot glares out of the page. A hairline and matching plate let
   it sit as an object on the page rather than a hole in it. SVGs are exempt:
   they use currentColor and are meant to blend. */
.post-body img:not([src$=".svg"]) {
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-radius: 4px;
}

/* --- dungeon maps --------------------------------------------------- */
/* ```map "The Sunken Vault" #=8a5700 ~=2f6f7a
   A terminal inside the page: fixed grid, no ligatures, no wrapping. Colour
   comes from generated .ink-* classes (see /css/generated.css), because the CSP
   forbids inline styles. */

.map { margin: 1.9rem 0; }

.map__frame {
  padding: 1.2rem 1.2rem 1.1rem;
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-radius: 4px;
}

/* A wide level shouldn't drag the whole page sideways — and the scroller is
   the grid alone, so the key underneath stays put while the map moves. */
.map__scroll {
  overflow-x: auto;
  text-align: center;
}

/* THE CELL.
   Every character and every icon occupies one square of exactly this size —
   not by arithmetic that has to come out right, but because each is a grid
   item in a fixed track. A monospace font alone can't do this: a letter
   advances 1ch, an inline SVG advances by its own width, and the two don't
   agree. Change --cell and the whole map scales with it. */
.map__grid {
  /* SQUARE, and deliberately so. A terminal cell is about half as wide as it is
     tall, inherited from the IBM VGA text mode, and every map drawn on one is
     geometrically stretched — a round room comes out as a tall ellipse unless
     it's drawn wide to compensate. Square is drawn for exactly this: its glyphs
     fill a square cell, so a map is read as the shape it was typed as.

     It also settles the icons. They're 512x512 from game-icons.net, so a square
     cell is the only one they fit without letterboxing — on a terminal cell an
     icon capped to the column ends up half the size of the letters beside it. */
  --cell: 1.2rem;
  --col:  var(--cell);
  display: inline-block;
  font-family: var(--font-map);
  /* Whatever a code block gets. `pre` sets no colour of its own, so that's the
     body ink — and the map is a code block by another name. Muting it made the
     structure recede behind the coloured glyphs it's meant to frame. */
  color: var(--fg);
  text-align: left;
}

.map__row {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--col);
  height: var(--cell);
}

.map__cell {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(var(--cell) * 0.86);
  line-height: 1;

  /* Anchors the copiable character below and the background square, and keeps a
     lone space from collapsing — the map is meant to paste back as the text it
     was drawn as.

     z-index establishes a stacking context, which is what keeps that square
     from sliding behind the panel: a negative z-index child paints above its
     own parent's background but below the parent's content, and without a
     context here it would escape and end up under .map__frame instead. */
  position: relative;
  z-index: 0;
  white-space: pre;

  /* "->" and "--" must stay two separate cells, never one ligature. */
  font-variant-ligatures: none;
  font-feature-settings: "liga" 0, "calt" 0;
}

/* An icon standing in for a character — a bed, a door. Not a tile: those are
   backgrounds, painted by the cell.

   Capped on BOTH axes and never larger than the cell, which is possible because
   the cell is square and game-icons artwork is square. 0.84 rather than 1 to
   match the ink rather than the box: Square's capitals paint 0.81 of their cell,
   so an icon at full width would read as heavier than the letters beside it. */
.map__cell .map__icon {
  width: 84%;
  height: 84%;
  max-width: 100%;
  max-height: 100%;
  margin: 0;
  vertical-align: baseline;
  color: inherit;
}

/* Quarter turns, set per legend entry — see lib/roguelike.js. One drawing then
   serves several directions: stairs down and stairs up are the same icon at
   0 and 180.

   Only quarter turns, and only on a square cell: any other angle would put the
   artwork's corners outside the tile, and on a non-square cell even 90 degrees
   would. Rotation about the centre of a square keeps a square icon exactly
   where it was. */
.map__icon--rot90  { transform: rotate(90deg); }
.map__icon--rot180 { transform: rotate(180deg); }
.map__icon--rot270 { transform: rotate(270deg); }

/* A tile — a wall, a floor, anything covering a whole cell — is painted by the
   CELL, as a background image, and never as an <svg> inside it. Which means
   there is no rule for it here: lib/roguelike.js generates one class per tile
   per colour into /css/generated.css.

   Worth knowing why, because it is not obvious and three fixes failed before
   it. An <svg> is geometry rasterised inside a box, so on a fractional cell —
   1.2rem is 19.2px — its edges antialias against transparency, and two abutting
   cells each fade over the same half pixel. That reads as a seam. Sizing the
   tile a pixel over and pulling it back scaled the artwork, which cost every
   joint that landed on a seam; spilling shapes past the viewBox kept the pitch
   but still painted over the seam; shape-rendering="crispEdges" changed how the
   geometry rounded but not what painted it. A background is CLIPPED to the box
   instead, and neighbouring boxes resolve against the same rounded edge — which
   is why the squares below have always tiled cleanly. */

/* A square behind one glyph — the `bg` colour in a legend entry:

     L = pillow  bg #2d7348  "on the bed"

   Drawn, not styled: a piece of the map rather than a property of the box. It
   fills the cell, sits BEHIND the character or icon and IN FRONT of the panel,
   and works the same whether the cell shows a letter or artwork.

   z-index: -1 is what puts it in that sandwich; the stacking context on the
   cell keeps it there. */
.map__bg {
  position: absolute;
  inset: 0;
  z-index: -1;
}
/* Same square under a legend key, so the key samples the cell it stands for. */
.map__key .map__bg { inset: 0 -0.15rem; }

/* The character a cell was drawn with, kept under its icon so the map can be
   selected and pasted back as the grid it came from. Invisible, but real text:
   an <svg> copies as nothing, so without this a map of icons pastes as gaps.

   Out of flow, or it would sit beside the icon and halve the cell. Not
   `display:none` or `visibility:hidden` — either would take it out of the
   selection along with the layout.

   Unscoped, because the key under the map needs the same treatment: a legend
   entry whose key is an icon is otherwise a label with nothing to label it. */
.map__char {
  position: absolute;
  inset: 0;
  color: transparent;
  pointer-events: none;
}

/* The unpainted characters are structure — walls, floor. Anything given a
   colour is what you're meant to look at, so it carries a little more weight.

   Applied to the key as well as the cell, and to cells carrying a background
   (`cell-`) as well as a plain ink, so one character never renders two ways.
   Square ships a single weight, so this is the browser's synthetic bold — which
   is the more reason for it to be consistent in both places. */
.map__cell[class*="ink-"],
.map__cell[class*="cell-"],
.map__key [class*="ink-"],
.map__key [class*="cell-"] { font-weight: 600; }

/* An icon standing in for one cell.
   The advance has to stay exactly 1ch or the whole grid shears, so the glyph
   is drawn larger than its cell and the overhang is taken back in margin:
   1.4ch wide minus 2 x 0.2ch = 1ch of actual space. Icons are drawn at cell
   size elsewhere; here legibility wins over tidiness, and the neighbours are
   floor dots. */
/* `.icon-glyph + *` gives icons in prose 0.1em of breathing room, which inside
   a grid would push a cell out of its track. Same shape as the nav droop: an
   unscoped rule leaking into a context it wasn't written for. */
.map__grid .icon-glyph + * { margin-left: 0; }

/* The key, inside the panel with the grid — it belongs to the map, whereas
   the caption belongs to the page. The rule separates them without needing
   a second box. */
/* Written as `.post-body .map__legend` because `.post-body ul` is (0,1,1)
   and would otherwise win on margin and padding — it was adding 1.1rem of
   dead space under the key. */
.post-body .map__legend,
.map__legend {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.3rem 1.2rem;

  /* Room to breathe under the grid, then the rule, then the key close under
     it — the key belongs to the map above it, not to the panel edge below. */
  margin: 1.6rem 0 0;
  padding: 1rem 0 0;
  border-top: 1px solid var(--rule);
  list-style: none;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  color: var(--fg-muted);
  text-align: left;
}
.post-body .map__legend li,
.map__legend li {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0;
}

/* Fixed width so the meanings line up whatever the glyph. Positioned, to anchor
   the copiable character the same way a cell does. */
/* Square, not the monospace stack: the key is a sample OF the grid, so the same
   character has to be drawn by the same font in both places. It was set in
   --font-mono, which made a legend key and its cell visibly different letters.

   No blanket weight either — that's decided by whether the character is
   coloured, below, exactly as it is in the grid. */
.map__key {
  position: relative;
  flex: 0 0 1.5rem;
  font-family: var(--font-map);
  text-align: center;
}
.map__key .map__icon { width: 1.05em; height: 1.05em; margin: 0; vertical-align: -0.15em; }

/* Selection has to be set for both themes or one of them gets an unreadable
   default pairing. */
::selection {
  background: var(--accent);
  color: var(--bg);
}

/* --- code ----------------------------------------------------------- */

code, kbd, pre {
  font-family: var(--font-mono);
  /* Monospace runs visually larger than Fira Condensed at the same size,
     so it needs pulling back or code shouts over the prose. */
  font-size: 0.86em;
}

code {
  background: var(--bg-sunken);
  padding: 0.12em 0.35em;
  border-radius: 3px;
  overflow-wrap: break-word;
}

pre {
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0.9rem 1rem;
  margin: 1.5rem 0;
  overflow-x: auto;          /* long lines scroll, never wrap or overflow */
  line-height: 1.55;
  tab-size: 2;
}
pre code {
  background: none;
  padding: 0;
  overflow-wrap: normal;
  white-space: pre;
}

kbd {
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-bottom-width: 2px;
  border-radius: 3px;
  padding: 0.05em 0.4em;
}

/* --- tables: rulebook, not spreadsheet ------------------------------- */
/* Modelled on a random table in a module — a firm rule under the header,
   banded rows, and a narrow first column for
   the die result. */

/* Added around every Markdown table by eleventy.config.js. */
.table-scroll {
  margin: 1.6rem 0;
  overflow-x: auto;
  /* Hints that the content scrolls, on touch devices that hide scrollbars. */
  overscroll-behavior-x: contain;
}
.table-scroll table { margin: 0; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}

thead th {
  font-family: var(--font-ui);
  font-size: 0.74rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  text-align: left;
  color: var(--fg);
  padding: 0 0.7rem 0.4rem 0;
  border-bottom: 2px solid var(--fg);
  white-space: nowrap;
}

tbody td {
  padding: 0.42rem 0.7rem;
  vertical-align: baseline;
}

/* Same banding as the roll tables — a random table in a post should look
   like a random table in a book. */
tbody tr:nth-child(odd) td { background: var(--zebra); }

/* The die column: narrow, tabular, accented — it's an index, not data. */
tbody td:first-child,
thead th:first-child {
  padding-right: 0.9rem;
  white-space: nowrap;
}
tbody td:first-child {
  font-family: var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* Markdown's alignment syntax should still work. */
th[align="right"], td[align="right"] { text-align: right; }
th[align="center"], td[align="center"] { text-align: center; }
td[align="right"] { font-variant-numeric: tabular-nums; }

/* Wide content scrolls itself, never the page. */
.post-body > pre,
.post-body > table { max-width: 100%; }

/* --- citations and bibliography ------------------------------------ */

/* CITATIONS use a bracketed number in the accent colour: [1]
   FOOTNOTES use a plain superscript number, muted: 1
   They must never look alike — they're numbered independently, so a post can
   legitimately contain both a [1] and a 1 meaning different things. */
a.citation {
  text-decoration: none;
  padding: 0 0.1em;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
a.citation sup {
  font-size: 0.72em;
  line-height: 0;
  font-weight: 700;
}
a.citation:hover sup { text-decoration: underline; }

/* The target of a jump gets a moment of highlight, so landing in a long
   list doesn't leave you hunting for which line you were sent to. */
/* The row you were sent to. A plain tinted block, with the shadow only
   used to pad the tint out past the text — no accent bar down the side. */
.references li:target,
.footnotes li:target {
  background: var(--highlight);
  box-shadow: 0 0 0 0.45rem var(--highlight);
  border-radius: 2px;
}
/* The marker is inside the highlight now, so it needs the highlight's own
   accent — plain gold drops to 4.0:1 on the light tint. */
.references li:target::before,
.footnotes li:target::before { color: var(--highlight-accent); }

/* Notes and References are the same kind of thing — an apparatus at the foot
   of the post — so they share their chrome, layout and rhythm exactly. Only
   the marker differs: [1] for a citation, a. for a footnote. */
.bibliography,
.footnotes {
  margin-top: 2.75rem;
  padding-top: 1.3rem;
  border-top: 1px solid var(--rule);
  color: var(--fg-muted);
}
/* One heading style for both sections. margin-top is explicitly 0: the
   footnotes block renders INSIDE .post-body while the bibliography is placed
   outside it, so any margin inherited from the prose heading scale would
   push one down and not the other. */
.bibliography h2,
.footnotes h2 {
  margin: 0 0 0.9rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-muted);
}

/* The marker lives INSIDE the <li> — padding plus an absolutely placed
   ::before — rather than outside it as a list marker, so the :target
   highlight covers the number too. */
.references,
.footnotes ol {
  list-style: none;
  margin: 0;
  padding-left: 0;
  font-size: 0.94rem;
  line-height: 1.55;
}

.references li,
.footnotes li {
  position: relative;
  padding-left: 2.4rem;
  margin-bottom: 0.6rem;
}

.references li::before,
.footnotes li::before {
  position: absolute;
  left: 0;
  width: 2rem;
  text-align: right;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}

/* The only difference between the two: what the marker says. */
.references      { counter-reset: ref; }
.references li   { counter-increment: ref; }
.references li::before { content: "[" counter(ref) "]"; }

.footnotes ol    { counter-reset: fn; }
.footnotes li    { counter-increment: fn; }
.footnotes li::before { content: counter(fn, lower-alpha) "."; }

/* The site is the "publication" in the reference — italic, as a journal
   title would be. */
.ref-site { font-style: italic; }

.ref-back {
  text-decoration: none;
  margin-left: 0.15rem;
  font-size: 0.85em;
}
.ref-back:hover { text-decoration: underline; }

/* --- footnotes (markdown-it-footnote) ------------------------------- */

.footnotes hr { display: none; }   /* the border above already divides it */


.footnotes p { margin: 0 0 0.4rem; }

/* Superscript letters (see footnote_caption in the config). Same accent and
   weight as a citation marker: a marker you can't find is a marker that
   doesn't work, and muted was too easy to skim past. The distinction is
   carried entirely by the form — letters vs [numbers] — not by colour. */
.footnote-ref a {
  text-decoration: none;
  font-weight: 700;
  color: var(--accent);
  padding: 0 0.05em;
}
.footnote-ref a:hover { text-decoration: underline; }

.footnote-backref {
  text-decoration: none;
  margin-left: 0.15rem;
}
.footnote-backref:hover { text-decoration: underline; }

/* --- post lists -------------------------------------------------- */


/* --- the archive index ------------------------------------------------
   A denser list than the home page's: date and title on one line, nothing
   else. You come here knowing roughly what you want, so the useful thing is
   fitting as many titles on screen as possible. */

.post-index {
  list-style: none;
  margin: 2.2rem 0 0;
  padding: 0;
}

.post-index li {
  display: grid;
  /* Wide enough for "04 Aug 2026" in tabular figures, so the titles start on
     a straight edge no matter what month it is. */
  grid-template-columns: 6rem 1fr;
  gap: 0 1rem;
  align-items: baseline;
  padding: 0.4rem 0;
  border-bottom: 1px solid var(--rule);
}
.post-index li:last-child { border-bottom: 0; }

/* Same treatment as the dates in the recent-posts list — small caps-ish UI
   type, muted — so the two pages read as one site. Tabular figures and the
   zero-padded day are what keep this column straight. */
.post-index time {
  font-family: var(--font-ui);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  color: var(--fg-muted);
  white-space: nowrap;
}

/* Undecorated until hovered, matching the recent-posts titles. A list this
   dense would be a thicket of rules otherwise. */
.post-index a {
  font-size: 1.02rem;
  text-decoration: none;
  text-wrap: pretty;
}
.post-index a:hover { text-decoration: underline; }

.post-index__empty {
  display: block;
  color: var(--fg-muted);
}

/* On a narrow phone the date column eats too much of the title, so it goes
   above instead. */
@media (max-width: 26rem) {
  .post-index li {
    grid-template-columns: 1fr;
    gap: 0.1rem;
    padding: 0.55rem 0;
  }
}

.post-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.post-list-item {
  padding: 1.1rem 0;
  border-bottom: 1px solid var(--rule);
}
.post-list-item:last-child { border-bottom: 0; }

.post-list-item time {
  display: block;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
}

.post-list-item h3 {
  margin: 0.2rem 0 0.3rem;
  font-size: 1.15rem;
}
.post-list-item h3 a { text-decoration: none; }
.post-list-item h3 a:hover { text-decoration: underline; }

.post-summary {
  margin: 0;
  color: var(--fg-muted);
  font-size: 0.98rem;
}

.more-link { font-family: var(--font-ui); font-size: 0.9rem; }

/* --- single post -------------------------------------------------- */

.post-header { margin-bottom: 2rem; }

.post-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin: 0.4rem 0 0;
  font-family: var(--font-ui);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--fg-muted);
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
}

.tag {
  display: inline-block;
  background: var(--tag-bg);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  font-family: var(--font-ui);
  font-size: 0.75rem;
  letter-spacing: 0.03em;
  color: var(--fg-muted);
  text-decoration: none;
}
a.tag:hover, a.tag:focus {
  color: var(--accent);
  background: var(--bg-sunken);
}

/* --- tag index ------------------------------------------------------- */

.tag-cloud {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  list-style: none;
  margin: 1.5rem 0;
  padding: 0;
}
.tag-cloud li {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}
.tag-count {
  font-family: var(--font-ui);
  font-size: 0.72rem;
  color: var(--fg-muted);
}

.post-list-headline {
  font-size: 1.35rem;
  margin-top: 0;
}

/* --- tag browser (archive) -------------------------------------------- */

.tag-browser {
  margin: 2rem 0 3rem;
  padding: 1.1rem 1.25rem;
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-radius: 6px;
}

.tag-browser__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
}

.tag-browser__heading {
  margin: 0;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-muted);
}

.tag-browser__index-link {
  font-family: var(--font-ui);
  font-size: 0.8rem;
  text-decoration: none;
}
.tag-browser__index-link:hover { text-decoration: underline; }

.tag-browser .tag-cloud { margin: 0.85rem 0 0; }

.tag-browser__more { margin-top: 0.9rem; }

.tag-browser__more > summary {
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  color: var(--accent);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.tag-browser__more > summary::-webkit-details-marker { display: none; }

/* Disclosure triangle, rotated when open. */
.tag-browser__more > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 150ms ease;
}
.tag-browser__more[open] > summary::before { transform: rotate(90deg); }

.tag-browser__more > summary:hover { color: var(--accent-hi); }

.tag-browser__panel { padding-top: 0.85rem; }

.tag-browser__panel .tag-cloud {
  margin: 0;
  max-height: 15rem;
  overflow-y: auto;
}

.tag-browser__empty {
  margin: 0.6rem 0 0;
  font-family: var(--font-ui);
  font-size: 0.85rem;
  color: var(--fg-muted);
}

/* The search field is injected by /js/tag-filter.js, so these styles only
   ever apply when the enhancement is actually running. */
.tag-search { margin-bottom: 0.85rem; }

.tag-search__input {
  width: 100%;
  padding: 0.45rem 0.65rem;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--rule);
  border-radius: 4px;
  font-family: var(--font-ui);
  font-size: 0.9rem;
}
.tag-search__input::placeholder { color: var(--fg-muted); }
.tag-search__input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .tag-browser__more > summary::before { transition: none; }
}


.post-nav {
  max-width: var(--measure);
  margin: 3rem auto 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui);
  font-size: 0.9rem;
}
.post-nav a { text-decoration: none; }
.post-nav a:hover { text-decoration: underline; }

/* --- rolls (blogroll / podroll / vidroll) ------------------------- */

.link-section { margin-bottom: 2.75rem; }

.section-note {
  margin-top: -0.4rem;
  color: var(--fg-muted);
  font-size: 0.92rem;
  font-style: italic;
}

.link-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* A tabbed list: labels align in their own column, the entry runs on in the
   rest of the width. A table was the obvious reach for this and the wrong one —
   four columns at this measure wrapped every credit onto two or three lines.
   Only the LABEL wants aligning; the rest is a sentence. */
.credit-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.55rem 1.4rem;
  margin: 0;
}
.credit-list dt {
  font-family: var(--font-ui);
  font-size: 0.74rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  /* Optically level with the first line of its entry, which is larger. */
  padding-top: 0.22em;
  color: var(--fg-muted);
  white-space: nowrap;
}
/* Pinned to the second column so a label can carry MORE THAN ONE entry — two
   faces under Maps, each on its own line. Left to auto-placement the second
   would fall into the label column instead. */
.credit-list dd {
  grid-column: 2;
  margin: 0;
}

/* Below the measure the label column would leave the entry no room, so the
   two stack and the label sits above what it names. */
@media (max-width: 30rem) {
  .credit-list { grid-template-columns: 1fr; gap: 0.15rem; }
  .credit-list dd { grid-column: 1; margin-bottom: 0.7rem; }
}

.link-entry {
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--rule);
}
.link-entry:last-child { border-bottom: 0; }

.link-entry h3 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.25rem;
  font-size: 1.05rem;
}
.link-entry h3 a { text-decoration: none; }
.link-entry h3 a:hover { text-decoration: underline; }

.link-entry p {
  margin: 0;
  color: var(--fg-muted);
  font-size: 0.96rem;
}

.entry-icon { display: flex; color: var(--fg-muted); }

.handle {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--fg-muted);
}

.entry-note {
  margin-top: 0.3rem !important;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* --- collapsible roll table --------------------------------------- */

.roll-details { margin-bottom: 0.5rem; }

/* No gap: the header row and the rows below it are one table. */
.roll-details > .section-note { margin-top: 0.8rem; }

/* The summary is drawn as the table's header row — same columns, same rule
   underneath — so collapsed, the roll table is exactly one row tall, and
   clicking that row opens it. The table's own <thead> is hidden for sighted
   readers (it stays for screen readers), or there'd be two header rows. */
.roll-summary {
  display: grid;
  grid-template-columns: 2.6rem 1fr auto;
  align-items: center;
  gap: 0 0.6rem;
  padding: 0 0.7rem 0.4rem 0;
  border-bottom: 2px solid var(--fg);
  cursor: pointer;
  list-style: none; /* Firefox */
}
.roll-summary::-webkit-details-marker { display: none; }

/* Still a real <h2>, so collapsing doesn't punch a hole in the outline. */
.roll-summary h2 {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg);
}
.roll-summary:hover h2 { color: var(--accent); }

/* Sits in the die column, matching .roll-table__die so the grid lines up with
   the rows below it. */
.roll-summary__die {
  display: flex;
  justify-content: center;
}
.roll-summary__die .icon-glyph {
  width: 1.35em;
  height: 1.35em;
  color: var(--accent);
}

.roll-summary__count {
  font-family: var(--font-ui);
  font-size: 0.78rem;
  color: var(--fg-muted);
}

/* Open state: a caret after the count, so the row still reads as a control
   once it's expanded. Drawn rather than using the native marker, which can't
   be placed inside a grid cell. */
.roll-summary__count::after {
  content: "";
  display: inline-block;
  margin-left: 0.5rem;
  border-left: 5px solid var(--accent);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transform: translateY(-0.05em);
  transition: transform 150ms ease;
}
.roll-details[open] > .roll-summary .roll-summary__count::after {
  transform: translateY(-0.05em) rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
  .roll-summary__count::after { transition: none; }
}

/* --- blogroll as a random table ----------------------------------- */

.roll-table-wrap { overflow-x: auto; }

.roll-table {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 1rem;
}

.roll-table thead th {
  font-family: var(--font-ui);
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  text-align: left;
  padding: 0 0.7rem 0.4rem 0;
  border-bottom: 2px solid var(--fg);
  color: var(--fg);
}

/* The die sits in the header where the column label would be — it *is* the
   label: this column holds what you rolled. */
.roll-table__die {
  width: 2.6rem;
  text-align: center !important;
  padding-right: 0.6rem !important;
}
.roll-table__die .icon-glyph {
  width: 1.35em;
  height: 1.35em;
  vertical-align: -0.3em;
  color: var(--accent);
}

.roll-table tbody td {
  padding: 0.65rem 0.7rem;
  vertical-align: top;
}

/* Alternating rows, like a table in a book. The banding does the work that
   hairlines used to, so the row rules come out — both together is fussy. */
.roll-table tbody tr:nth-child(odd) td { background: var(--zebra); }

.roll-table__n {
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--accent);
  padding-right: 0.6rem !important;
}

.roll-table__name {
  display: inline-block;
  margin-top: 0.05rem;
  font-weight: 600;
  font-size: 1.05rem;
  text-decoration: none;
  color: var(--fg);
}
.roll-table__name:hover { color: var(--accent); text-decoration: underline; }

.roll-table td p {
  margin: 0.15rem 0 0;
  color: var(--fg-muted);
  font-size: 0.94rem;
}

/* Source line, above the title: favicon + blog name in the accent, so the
   eye can group rows by publication without them being grouped in the DOM. */
.roll-table__source {
  display: flex;
  align-items: center;
  gap: 0.42rem;
  margin: 0 0 0.1rem !important;
  font-family: var(--font-ui);
  font-size: 0.82rem !important;
  font-weight: 600;
}
.roll-table__source a {
  color: var(--accent);
  text-decoration: none;
}
.roll-table__source a:hover { text-decoration: underline; }

/* Favicons are other people's artwork at 16px — a fixed box and a slight
   rounding keeps a row of them tidy regardless of what they actually are. */
.favicon {
  width: 16px;
  height: 16px;
  border-radius: 2px;
  flex-shrink: 0;
  object-fit: contain;
}

/* A light plate, for the few marks that are a dark glyph on transparency and
   so disappear into the dark theme — the two forums, and nothing else.

   It used to go behind EVERY favicon, on the grounds that a plate everywhere
   was safer than detecting which needed one. It isn't: most icons carry their
   own background, and a plate behind those reads as a box someone forgot to
   remove. Marked per source in the data instead, because it's a judgement
   about the artwork and there are two of them.

   Transparent in light mode, so nothing changes there. */
.favicon--plate {
  background: var(--favicon-plate, transparent);
  box-sizing: content-box;
  padding: var(--favicon-pad, 0);
}

/* Reading time and date — quietest line in the row. */
.roll-table__meta {
  font-family: var(--font-ui);
  font-size: 0.76rem !important;
  font-style: italic;
  display: flex;
  flex-wrap: wrap;
  gap: 0 0.45rem;
  align-items: baseline;
  margin-top: 0.15rem !important;
}
.roll-table__meta .sep { opacity: 0.45; font-style: normal; }

/* The blog list below the roll is a reference, not the main event. */
.link-list--compact .link-entry { padding: 0.6rem 0; }
.link-list--compact .link-entry h3 { font-size: 0.98rem; }

.roll-table__empty .roll-table__n { opacity: 0.45; }
.roll-again {
  font-family: var(--font-ui);
  font-size: 0.85rem;
  font-style: italic;
  color: var(--fg-muted);
  opacity: 0.75;
}

/* The rolled row: a tint across the whole row. */
/* Beats the banding: declared after it, same specificity. */
.roll-table tbody tr.is-rolled td { background: var(--highlight); }

/* Text sitting on the highlight needs its own values — the ordinary muted
   grey drops to 3.9:1 on the dark tint, and the gold to 4.0:1 on the light
   one. Both would fail AA exactly where a reader is looking hardest. */
.roll-table tbody tr.is-rolled .roll-table__meta,
.roll-table tbody tr.is-rolled .roll-table__source { color: var(--highlight-muted); }
.roll-table tbody tr.is-rolled .roll-table__meta a,
.roll-table tbody tr.is-rolled .roll-table__source a { color: var(--highlight-muted); }
.roll-table tbody tr.is-rolled .roll-table__n,
.roll-table tbody tr.is-rolled .roll-again { color: var(--highlight-accent); }
/* The rolled row is marked by its tint alone. A coloured bar down the edge
   read as a scrollbar or a quote rule rather than a result. */

/* --- the die, as a roll button (upgraded by /js/roll.js) ---------- */

/* Nothing but the die. A <button> arrives with a background, a border and the
   system font, all of which have to go — the affordance is the glyph lighting
   up on hover, not a box around it. Scoped to no particular parent so it keeps
   working wherever the die sits. */
button.roll-die {
  display: flex;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  line-height: 0;
  -webkit-appearance: none;
  appearance: none;
}
button.roll-die:hover { color: var(--accent-hi); }
button.roll-die:active { transform: translateY(1px); }

/* Keyboard focus still needs to be visible — the hover colour alone isn't
   enough when there's no pointer to follow. */
button.roll-die:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 3px;
}

/* --- the sources under a roll ------------------------------------ */
/* Everything below the roll table answers a different question — not "what
   should I read now" but "whose writing is in there". The rule and the space
   are what stop the second half reading as another roll. */

/* No rule above it. The roll table already ends in one — the summary's own
   header rule — and a second line a few centimetres below read as a divider
   between two unrelated things rather than as a change of subject. The space
   does that job on its own. */
.source-list {
  margin-top: 3.2rem;
}

.source-list > h2 {
  margin: 0 0 1.6rem;
  font-family: var(--font-ui);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-muted);
}

/* Groups within it — "Tabletop", "Everything else". Quieter than the section
   heading above them, because they're a subdivision of it, not a new topic. */
.source-group + .source-group { margin-top: 2rem; }

.source-group > h3 {
  margin: 0 0 0.3rem;
  font-size: 1.05rem;
}

/* The entry titles drop to <h4> now they sit two levels down; keep them
   looking exactly as they did. */
.source-group .link-entry h4 {
  margin: 0;
  font-size: 0.98rem;
  font-weight: 600;
  display: flex;
  align-items: baseline;
  gap: 0.45rem;
}
.source-group .link-entry h4 a { text-decoration: none; }
.source-group .link-entry h4 a:hover { text-decoration: underline; }

/* --- recent posts under a blogroll entry -------------------------- */
/* Pulled from each feed at build time. Indented under the entry with a
   rule, so it reads as "what this blog has been writing" rather than as
   part of your own post list. */

.feed-posts {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0 0 0 0.9rem;
  border-left: 1px solid var(--rule);
}

.feed-posts li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 0.6rem;
  margin-bottom: 0.28rem;
}
.feed-posts li:last-child { margin-bottom: 0; }

.feed-posts a {
  font-size: 0.94rem;
  text-decoration: none;
}
.feed-posts a:hover { text-decoration: underline; }

.feed-posts time {
  font-family: var(--font-ui);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--fg-muted);
  white-space: nowrap;
}

/* --- webrings ------------------------------------------------------ */

/* Shared box for "a place to go" blocks — the webring and each forum. One
   rule so they can't drift apart. */
.panel,
.webring {
  padding: 1.1rem 1.25rem;
  background: var(--bg-sunken);
  border: 1px solid var(--rule);
  border-radius: 6px;
  margin-bottom: 1rem;
}
.panel h3,
.webring h3 {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.35rem;
  font-size: 1.05rem;
}
.panel h3 a,
.webring h3 a { text-decoration: none; }
.panel h3 a:hover,
.webring h3 a:hover { text-decoration: underline; }

.panel > p,
.webring p { margin: 0; color: var(--fg-muted); font-size: 0.96rem; }

/* Threads sit inside the panel, so the rule that marks them off should be
   lighter than the panel's own border. */
.forum .feed-posts {
  margin-top: 0.75rem;
  border-left-color: var(--rule-soft, var(--rule));
}

.webring-pending { opacity: 0.7; }
.webring-note { margin-top: 0.5rem !important; font-style: italic; }

/* Every panel heading carries the mark that site serves — forums and webrings
   alike — so they're all sized together. Scoped to .panel so the 16px favicons
   in the roll tables are untouched. */
.panel h3 .favicon {
  width: 20px;
  height: 20px;
}

/* Prev / Random / Directory / Next. Lives on the community page only. */
.webring-nav { margin-top: 0.85rem; }

.webring-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.35rem;
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-ui);
  font-size: 0.85rem;
}
.webring-nav a { text-decoration: none; }
.webring-nav a:hover { text-decoration: underline; }

/* --- footer --------------------------------------------------------- */

/* Just a credits link now, but the rule stays — it's what closes the page
   off, and without it the link floats loose under the content. */
.site-footer {
  margin-top: 3.5rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--rule);
  font-family: var(--font-ui);
  font-size: 0.78rem;
  color: var(--fg-muted);
}
.site-footer p { margin: 0; }
.site-footer a { color: var(--fg-muted); text-decoration: none; }
.site-footer a:hover { color: var(--accent); text-decoration: underline; }

/* --- small screens --------------------------------------------------- */

@media (max-width: 34rem) {
  :root { --gutter: 1.1rem; }
  body { font-size: 1rem; }
  h1 { font-size: 1.6rem; }
  .site-header { padding-top: 1.75rem; }
  .site-nav { justify-content: flex-start; }
  .nav-links { gap: 0.9rem; }
}

/* --- print ----------------------------------------------------------- */

@media print {
  .site-nav, .post-nav, .skip-link { display: none; }
  body { background: #fff; color: #000; font-size: 11pt; }
  a { color: #000; }
}
