/* Beò base styles — the prry.uk design, rebuilt.
   ============================================================================

   Ported from Pure Blog's assets/css/style.css plus the hand-written
   content/css/custom.css that sat on top of it. Two sheets became one,
   because the reason for the split was that one of them belonged to someone
   else's software.

   Every value that was hard-coded there and is a matter of taste is a custom
   property here, emitted into the head from Settings by app/core/theme.php:
   the palette, the four typefaces, the size scale and the measure. The
   defaults below are prry.uk's own, so an install that changes nothing looks
   like the site it replaces.

   Fonts are served from this origin (assets/fonts). custom.css pulled them
   from cdn.prry.uk, which told that host about every reader of every page. */

/* --- typefaces -------------------------------------------------------------
   Atkinson Hyperlegible was drawn by the Braille Institute to be legible at
   low vision: the letterforms are differentiated rather than harmonised, so
   1/l/I and 0/O cannot be confused. font-display:swap because a font is
   never worth a blank page. */
@font-face {
  font-family: "Atkinson Hyperlegible";
  src: url("/assets/fonts/atkinsonhyperlegiblenext-regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible Bold";
  src: url("/assets/fonts/atkinsonhyperlegiblenext-extrabold.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible Mono";
  src: url("/assets/fonts/atkinsonhyperlegiblemono-regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Gloria Hallelujah";
  src: url("/assets/fonts/gloria-hallelujah-latin-400-normal.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}

/* --- tokens ---------------------------------------------------------------
   Defaults only. The head overrides any of these that Settings has a value
   for, so this block is what a fresh install looks like — not a fallback
   nobody sees. */
:root {
  --bg: #fafafa; --text: #14141e; --accent: #630f33;
  --accent-bg: #f5f7ff; --border: #ccc; --muted: #666;

  --tok-com: #6e7781; --tok-str: #0a3069; --tok-kw: #a01b28; --tok-num: #0550ae;
  --tok-fn: #7c3aad; --tok-tag: #116329; --tok-att: #0550ae; --tok-var: #8a3a00;

  --font-main:   "Atkinson Hyperlegible", system-ui, -apple-system, sans-serif;
  --font-title:  "Atkinson Hyperlegible Bold", "Atkinson Hyperlegible", system-ui, sans-serif;
  --font-code:   "Atkinson Hyperlegible Mono", ui-monospace, SFMono-Regular, monospace;
  --font-scrawl: "Gloria Hallelujah", cursive;

  --font-size: 1.15rem;
  --size-h1: 3rem; --size-h2: 2.4rem; --size-h3: 2rem;
  --size-h4: 1.44rem; --size-h5: 1.15rem; --size-h6: 0.96rem;
  --measure: 55rem;
  --size-title: 3.5rem;

  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #212121; --text: #dcdcdc; --accent: #ffb300;
    --accent-bg: #2b2b2b; --border: #555; --muted: #999;

    --tok-com: #9aa4ae; --tok-str: #a5d6ff; --tok-kw: #ff7b72; --tok-num: #79c0ff;
    --tok-fn: #d2a8ff; --tok-tag: #7ee787; --tok-att: #79c0ff; --tok-var: #ffa657;

    color-scheme: dark;
  }
}
@media (max-width: 720px) {
  :root { --size-h1: 2.5rem; --size-h2: 2.1rem; --size-h3: 1.75rem; --size-h4: 1.25rem;
          --size-title: 2.45rem; }
}

/* --- page ----------------------------------------------------------------- */
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  min-height: 100vh; display: flex; flex-direction: column; align-items: stretch;
  background: var(--bg); color: var(--text);
  font-family: var(--font-main); font-size: var(--font-size);
  margin: 0; padding: 0 1rem; line-height: 1.6; text-wrap: pretty;
}
header.site, main, footer { width: 100%; max-width: var(--measure); margin: 0 auto; }
main { flex: 1; }
header.site { border-bottom: 1px solid var(--border); margin-bottom: 2rem; }
footer { text-align: center; font-size: 1rem; margin-top: 4rem;
         border-top: 1px solid var(--border); padding-top: 1rem; }

/* --- type ----------------------------------------------------------------- */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-title); color: var(--accent);
  margin-bottom: 1rem; overflow-wrap: break-word; text-wrap: balance;
}
h1, h2, h3 { line-height: 1.1; }
h1 { font-size: var(--size-h1); margin-top: 0; }
h2 { font-size: var(--size-h2); margin-top: 3rem; }
h3 { font-size: var(--size-h3); margin-top: 3rem; }
h4 { font-size: var(--size-h4); }
h5 { font-size: var(--size-h5); }
h6 { font-size: var(--size-h6); }
main h1 { margin-bottom: 2rem; }
p { margin: 1.5rem 0; }
/* The hand-scrawl face on emphasis is the site's one flourish. Inside a
   blockquote it would fight the italic, so it stands down there. */
em { font-family: var(--font-scrawl); font-style: normal; }
blockquote em, cite em { font-family: inherit; font-style: italic; }
a { color: var(--accent); }
a:hover { text-decoration: none; }
abbr { cursor: help; }
hr { border: 0; border-top: 1px solid var(--border); }
cite { font-size: 0.9rem; }
.muted { color: var(--muted); font-size: 0.9rem; }
.hidden { display: none; }

/* --- masthead -------------------------------------------------------------
   Title, then menu, then one rule under the lot — prry.uk's shape. The h-card
   is a block within it when Settings puts it here, and absent when Settings
   puts it in the footer.

   In the header the h-card IS the masthead — the title link is its p-name —
   so it stacks: avatar and title on the first line, then the note, then the
   address. A block, not the two-column flex the footer card uses, because
   there is no separate text column here to line up against. */
header.site .h-card { display: block; margin: 1rem 0; }
header.site .h-card::after { content: ""; display: block; clear: both; }
header.site .site-title {
  font-family: var(--font-title); font-size: var(--size-title); line-height: 1;
  color: var(--accent); text-decoration: none; max-width: 100%;
  margin: 0.5rem 0 0.25rem;
  /* Logo and name on one line, name after the logo. inline-flex rather than
     flex so the box hugs the words: a full-width block would sit under the
     floated avatar instead of beside it. */
  display: inline-flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
}
header.site .site-title:hover span { text-decoration: underline; }
/* Sized in em, not rem, so the logo comes down with the title on a phone
   instead of towering over the name it sits beside. */
header.site .site-logo { max-height: 1em; width: auto; border-radius: 5px; margin: 0; flex: none; }
/* The note starts below the avatar rather than wrapping around it: on a phone
   the title line is shorter than the picture is tall, and the first line of
   the bio was indenting itself past a photo nothing else lined up with. */
header.site .p-note { clear: left; }
header.site .p-adr, header.site .u-email { display: block; margin: 0.3rem 0; }
header.site .p-note p { margin: 0.2rem 0; }
.site-nav { display: flex; flex-wrap: wrap; gap: 0.75rem 1rem; margin: 1rem 0; }
.site-nav a { color: inherit; text-decoration: none; font-size: 0.9em;
  display: inline-flex; align-items: center; gap: 0.3rem; }
.site-nav a:hover, .site-nav a.current { color: var(--accent); text-decoration: underline; }
.site-nav .icon { padding-right: 0; width: 1.15em; height: 1.15em; }
/* The admin's own menu. Wider spacing than the reader's, because there are six
   entries rather than fifteen and they are places to work rather than places
   to browse. */
.site-nav.admin-menu { gap: 0.75rem 1.4rem; }
.site-nav.admin-menu a { font-size: 1rem; }
/* The tools, on the Settings screen: cards rather than a row of links,
   because each is a place you go for a reason and the reason is worth saying
   before you click. */
/* What the outbox is doing, said where it cannot be missed. The one
   unforgivable outcome here is a writer believing a post is published when it
   is sitting in a queue on their own phone, so this is fixed to the bottom of
   the window rather than tucked into the page. */
.outbox-status {
  position: fixed; left: 1rem; right: 1rem; bottom: 1rem; z-index: 60;
  max-width: var(--measure); margin: 0 auto;
  padding: 0.6rem 0.9rem; border-radius: 6px; font-size: 0.9rem;
  border: 1px solid var(--border); background: var(--accent-bg); color: var(--text);
  box-shadow: 0 2px 12px rgba(0,0,0,0.12);
}
.outbox-waiting { border-color: var(--accent); }
.outbox-sending { border-color: var(--accent); opacity: 0.9; }
.outbox-done    { border-color: #1a7f37; }
.outbox-error   { border-color: #c1272d; }
@media (prefers-color-scheme: dark) { .outbox-done { border-color: #7ee787; } }

.admin-tools { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0.6rem; margin: 1rem 0 2rem; }
.admin-tool { display: flex; gap: 0.6rem; align-items: flex-start; text-decoration: none;
  border: 1px solid var(--border); border-radius: 6px; padding: 0.7rem 0.9rem;
  background: var(--accent-bg); color: var(--text); }
.admin-tool:hover { border-color: var(--accent); }
.admin-tool .icon { flex: none; width: 1.4em; height: 1.4em; padding-right: 0;
  color: var(--accent); position: static; }
.admin-tool span { display: block; }
.admin-tool small { display: block; font-size: 0.82rem; line-height: 1.35; }
/* The settings pages themselves, on the index. Same card, a little more room
   than the tools below them, because these are where the work is. */
.admin-pages { grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.admin-pages .admin-tool { padding: 0.85rem 1rem; }
/* Moving between settings pages without going back to the index each time.
   The page you are on is a marked word rather than a link to itself. */
.settings-subnav { gap: 0.4rem 1rem; margin: 0.4rem 0 1.2rem; font-size: 0.9rem; }
.settings-subnav a, .settings-subnav .current {
  display: inline-flex; align-items: center; gap: 0.3rem;
}
.settings-subnav .current { color: var(--accent); font-weight: 600; }
.settings-subnav .icon { width: 1.05em; height: 1.05em; padding-right: 0; }

/* The web archive screen. The coverage line is the headline number — how much
   of this site exists somewhere that is not this site — so it is set larger
   than the detail underneath it. */
.wayback-coverage { font-size: 1.15rem; margin-bottom: 0.2rem; }
.wayback-actions { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: center;
  margin: 1.2rem 0; padding: 0.9rem 1rem; border: 1px solid var(--border);
  border-radius: 6px; background: var(--accent-bg); }
.wayback-actions label { display: inline-flex; align-items: center; gap: 0.4rem; margin: 0; }
.wayback-actions select { width: auto; margin: 0; }
.wayback-filter { gap: 0.5rem 1.2rem; margin: 1.2rem 0 0.4rem; }
.wayback-table { width: 100%; border-collapse: collapse; }
.wayback-table th, .wayback-table td { text-align: left; padding: 0.4rem 0.6rem 0.4rem 0;
  border-bottom: 1px solid var(--border); vertical-align: top; font-size: 0.92rem; }
.wayback-why { font-size: 0.82rem; }
.wayback-why, .wayback-table tr.wayback-error td:nth-child(2) { color: #c1272d; }
@media (prefers-color-scheme: dark) {
  .wayback-why, .wayback-table tr.wayback-error td:nth-child(2) { color: #ff9a92; }
}
/* Where the copy of a post lives, under the post. Quiet: it is a fact about
   the page rather than part of it. */
.archived { font-size: 0.85rem; }
.archived span { opacity: 0.8; }
.admin-add a { margin-right: 0.9rem; white-space: nowrap; }
.admin-nav .icon { width: 1.05em; height: 1.05em; padding-right: 0.25rem; }
.nav-count {
  display: inline-block; min-width: 1.3em; padding: 0 0.3em; margin-left: 0.15rem;
  background: var(--accent); color: var(--bg); border-radius: 999px;
  font-size: 0.72em; line-height: 1.5; text-align: center; font-family: var(--font-code);
}
header.site .tagline { margin: 0 0 0.5rem; }

/* --- the h-card, header or footer ------------------------------------------
   Photo beside the note, which is how prry.uk lays it out. Two columns rather
   than a float, because everything under the note — the location, the rel=me
   icons — belongs to the same column as the words: a float lets them slide
   under the photo the moment the note is shorter than the picture is tall,
   and on a phone it wraps the note itself mid-sentence. Wrapping puts the
   photo on its own line instead. */
.h-card { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 0 1.25rem; }
.profile-img { max-width: 8rem; margin: 0 0 0.5rem; flex: none;
  border: 2px solid var(--accent); border-radius: 6px; }
.card-text { text-align: left; flex: 1 1 16rem; min-width: 0; }
.card-text .p-name { font-family: var(--font-title); font-size: 1.4em; text-decoration: none; }
.card-text .p-note p { margin: 0.4rem 0; }
/* Name and the ways to reach that name on one row. baseline rather than
   center so the icons sit on the name's own line, and the row wraps whole
   when there is not width for both. */
.card-name-row { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 0.4rem 1rem; margin-bottom: 0.4rem; }
.card-name-row .social { margin: 0; }
.social { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0.6rem 0 0; }
.social a { display: inline-flex; }
.social .icon { width: 1.5em; height: 1.5em; padding-right: 0; color: var(--accent); }

/* --- footer ----------------------------------------------------------------
   In the footer the h-card is a card: a bordered panel, so the photo and the
   note read as one block about a person rather than as the first two lines of
   the site's small print. It keeps the header version plain — there the card
   sits under the masthead rule and a second box around it would be a box in a
   box. Everything below it is spaced apart rather than stacked tight, because
   the links, the badges and the licence are four unrelated things. */
footer { padding-bottom: 2.5rem; }
footer .h-card {
  text-align: left; margin: 2.5rem auto; max-width: 40rem;
  background: var(--accent-bg); border: 1px solid var(--border);
  border-radius: 8px; padding: 1.5rem;
}
footer .h-card .profile-img { max-width: 6.5rem; }
.footer-links { margin: 2rem 0 1.5rem; }
.footer-extra { margin: 1.5rem 0; }
.footer-extra img { display: inline-block; margin: 0.2rem; vertical-align: middle;
  border-radius: 0; max-width: 88px; }
.footer-colophon, .footer-licence { margin: 1.2rem 0; }
.footer-colophon .icon, .footer-licence .icon { vertical-align: -0.2em; padding-right: 0; }

/* --- front page ------------------------------------------------------------
   The intro is prose and gets prose's measure; the sections below it are
   lists. The jump-link row is the one piece of navigation on the page, so it
   sits under the intro and above the first heading, as prry.uk has it. */
.home-intro { margin-bottom: 2.5rem; }
.home-intro > :first-child { margin-top: 0; }
.home-intro img { border-radius: 5px; }
/* The intro's own portrait, floated beside the prose — prry.uk's `.header-img`,
   kept under its own name so the markup an owner already has keeps working.
   `margin`, not `margin-left`: `main img` sets `margin: 1.5rem auto`, and
   `auto` on a floated element centres nothing but does drop the picture a line
   below the paragraph it is meant to sit beside. */
.header-img {
  float: right; width: 300px; max-width: 45%;
  margin: 0.3rem 0 1rem 1.25rem;
  border: 3px solid var(--accent); border-radius: 5px;
}
/* Clear it, so a short intro cannot leave the picture hanging over the first
   section heading. */
.home-intro::after { content: ""; display: block; clear: both; }
@media (max-width: 620px) {
  /* Below this the text column beside a 45% image is four words wide. */
  .header-img { float: none; width: 100%; max-width: 100%; margin: 1rem 0; }
}
.home-jump { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem 0.5rem;
  margin: 2rem 0; font-size: 0.95rem; }
.home-jump a { display: inline-flex; align-items: center; gap: 0.3rem; text-decoration: none; }
.home-jump a:hover { text-decoration: underline; }
.home-jump .icon { width: 1.15em; height: 1.15em; padding-right: 0; }
/* A section heading is a divider between two kinds of thing, so it needs the
   space an h2 gets mid-article — but not the 3rem that would put the first
   one a screen away from the jump-links that point at it. */
.home-section { margin: 0 0 3rem; }
.home-section > h2 { margin-top: 2.5rem; }
.home-section:first-of-type > h2 { margin-top: 1.5rem; }
.home-viewall { margin: 1rem 0 0; font-size: 0.95rem; }

/* --- media ---------------------------------------------------------------- */
img { max-width: 100%; height: auto; }
figure { margin: 0; }
main img, main video { max-width: 100%; border-radius: 5px; margin: 1.5rem auto; display: block; }
figcaption, img + em {
  display: block; font-family: var(--font-main); font-style: normal;
  text-align: center; font-size: 0.7em; margin-top: -1rem;
}
.video-embed { position: relative; width: 100%; padding-top: 56.25%; margin: 1.5rem 0; }
.video-embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
  border-radius: 5px; }
/* Click-to-play placeholder. No poster image: fetching one from the provider
   would make the request this mode exists to avoid. */
.video-play { position: absolute; inset: 0; width: 100%; height: 100%;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.5rem; background: var(--accent-bg); color: var(--text);
  border: 1px solid var(--border); border-radius: 5px; cursor: pointer;
  font-family: inherit; font-size: 1rem; padding: 1rem; }
.video-play:hover { background: var(--accent-bg); border-color: var(--accent); color: var(--accent); }
.video-play-icon { font-size: 2.5rem; line-height: 1; color: var(--accent); }
.video-play-label { display: flex; flex-direction: column; gap: 0.2rem; text-align: center; }
.video-play-label small { opacity: 0.7; font-size: 0.8em; }
.video-fallback { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; border: 1px solid var(--border); border-radius: 5px; }

/* --- code ----------------------------------------------------------------- */
pre {
  background: var(--accent-bg); border: 1px solid var(--border); border-radius: 5px;
  margin: 2rem 0; padding: 1rem; overflow-x: auto;
  font-family: var(--font-code); white-space: pre-wrap; word-break: break-word;
}
pre code { border: none; padding: 0; background: none; }

/* Syntax highlighting. Nine colours, no more: every extra class is a decision
   about what a reader is meant to notice, and a block where everything is
   coloured says nothing. Defaults live here; beo_theme_css() re-emits them
   when the site is pinned light or dark, so a forced-light page does not get
   the dark palette out of the media query below. */
.tok-com { color: var(--tok-com); font-style: italic; }
.tok-str { color: var(--tok-str); }
.tok-kw  { color: var(--tok-kw); }
.tok-num { color: var(--tok-num); }
.tok-fn  { color: var(--tok-fn); }
.tok-tag { color: var(--tok-tag); }
.tok-att { color: var(--tok-att); }
.tok-var { color: var(--tok-var); }
.tok-op  { color: inherit; }
/* The label a fence carried, in the corner, so a reader can tell Python from
   pseudocode without reading it — and so a mislabelled block is visible. */
pre.hl { position: relative; }
pre.hl::after {
  content: attr(data-lang); position: absolute; top: 0.35rem; right: 0.6rem;
  font-size: 0.7rem; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--muted); opacity: 0.65; pointer-events: none; user-select: none;
}
@media (max-width: 480px) { pre.hl::after { display: none; } }
code {
  border: 1px solid var(--border); border-radius: 5px; background: var(--accent-bg);
  padding: 0.1rem 0.25rem; font-size: 1rem; font-family: var(--font-code);
}
h2 code, h3 code { font-size: 90%; }
kbd {
  font-size: 1rem; background: var(--accent-bg); border: 1px solid var(--text);
  border-bottom: 3px solid var(--text); border-radius: 5px; padding: 0.25rem;
}

/* --- quotes, tables, details ---------------------------------------------- */
blockquote {
  border-left: 3px solid var(--accent); padding-left: 1rem;
  margin: 2.5rem 0 2.5rem 2.5rem; font-style: italic;
}
blockquote p { opacity: 0.8; }
table { border-collapse: collapse; margin: 1.5rem 0; }
td, th { border: 1px solid var(--border); text-align: start; padding: 0.5rem; }
th { background: var(--accent-bg); font-weight: bold; }
tbody tr:nth-child(even) { background: var(--accent-bg); }
details {
  padding: 0.7rem 1rem; margin: 2rem 0; background: var(--accent-bg);
  border: 1px solid var(--border); border-radius: 5px;
}
summary { cursor: pointer; font-weight: bold; padding: 0.7rem 1rem; margin: -0.7rem -1rem; }
details[open] > summary { margin-bottom: 0.5rem; }
details[open] > summary + * { margin-top: 0; }
details[open] > :last-child { margin-bottom: 0; }

/* --- content furniture ---------------------------------------------------- */
article { margin-bottom: 2rem; }
article + article { border-top: 1px solid var(--border); margin-top: 1.5rem; padding-top: 1.5rem; }
p.post-date { margin: -1.5rem 0 2rem; font-size: 1rem; }
.wikilink-missing { border-bottom: 1px dashed var(--muted); color: var(--muted); }
.backlinks { border-top: 1px solid var(--border); margin-top: 2rem; padding-top: 1rem; }
.tags a { margin-right: 0.5rem; }
.footnotes {
  font-size: 1rem; font-style: italic; margin-top: 2.5rem;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.footnotes hr { display: none; }
/* Prev/next, the shape Pure Blog used: two columns, one rule above and below. */
.post-nav {
  display: flex; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  margin: 3rem auto; padding: 0;
}
.post-nav div { flex: 1 1 0; }
.post-nav p { padding: 1rem 0; }
.post-nav a { font-weight: bold; }
.post-nav-next { text-align: right; }
@media (max-width: 800px) { .post-nav { display: block; } }

/* Owner-authored HTML below a post, page or entry (app/content/inserts.php).
   Deliberately near-unstyled: whatever is in there brought its own. */
.insert { margin: 2.5rem 0; }

/* --- the training log ------------------------------------------------------
   A dense column of days. The title already carries the whole summary as
   emoji, so the row is: day, title, and a marker when there is a track behind
   it worth opening. Class names are prry.uk's, because this is the same
   furniture doing the same job. */
.log-cards { list-style: none; padding: 0; margin: 0 0 1.5em; }
.log-card { margin: 0; padding: 0; border-bottom: 1px solid var(--border); }
.log-card:last-child { border-bottom: none; }
.log-card-link { display: flex; align-items: baseline; gap: 0.75em;
  padding: 0.5em 0.25em; text-decoration: none; color: inherit;
  transition: background 0.12s; }
.log-card-link:hover { background: var(--accent-bg); }
.log-card time { flex: 0 0 auto; font-variant-numeric: tabular-nums;
  color: var(--muted); font-size: 0.85em; min-width: 2.5em; text-align: right; }
.log-card-title { flex: 1 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.log-card-icons { flex: 0 0 auto; font-size: 0.8em; letter-spacing: 0.05em;
  user-select: none; opacity: 0.6; }
/* Nothing parsed behind it: still readable, visibly quieter. */
.log-card-plain .log-card-link { opacity: 0.6; }
@media (max-width: 600px) {
  .log-card-title { white-space: normal; }
}

/* --- listings -------------------------------------------------------------
   Two shapes, as prry.uk has: a dense date+title grid for articles and the
   log, and a card feed for notes, snaps and clips. */
/* A fixed date column, not max-content. A year page renders one grid per
   month, and max-content is resolved per grid — so December, holding only
   "9 Dec 2018", got a narrower column than April's "27 Apr 2018", and the
   titles started at a different place in every month down the page. The
   column is wide enough for the longest date any listing writes ("27 Apr
   2018" in tabular figures at 0.9rem) with room to spare. */
:root { --archive-date-col: 7rem; }
.archive-list { display: grid; grid-template-columns: var(--archive-date-col) 1fr;
  gap: 0.5rem 1.5rem; align-items: baseline; margin: 1.5rem 0; }
.archive-list .archive-row { margin: 0; border: 0; padding: 0; }
.archive-list time { font-variant-numeric: tabular-nums; white-space: nowrap; }
.archive-row-title { min-width: 0; overflow-wrap: break-word; }
@media (max-width: 600px) {
  .archive-list { grid-template-columns: 1fr; gap: 0; }
  .archive-list .archive-row { margin-top: 1rem; }
  .archive-row-title { margin-bottom: 0.25rem; display: block; }
}
.archive-month { font-size: var(--size-h5); text-transform: uppercase; letter-spacing: 0.05em;
  border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; margin: 2.5rem 0 0.5rem; }
.note-card-footer .tags a { margin-left: 0.4rem; }
.notes-feed { display: flex; flex-direction: column; gap: 1.25rem; max-width: 100%; }
.note-card {
  background: var(--accent-bg); border: 1px solid var(--border);
  border-radius: 6px; padding: 1rem 1.25rem 0.75rem;
}
.note-card + .note-card { margin-top: 0; border-top: 1px solid var(--border); padding-top: 1rem; }
.note-card-body p:first-child { margin-top: 0; }
.note-card-body p:last-child { margin-bottom: 0.5rem; }
.note-card-body img { max-width: 75%; }
.note-card-footer {
  border-top: 1px solid var(--border); padding-top: 0.5rem; margin-top: 0.75rem;
  font-size: 0.85em; opacity: 0.7;
}
.excerpt-view, .tag-item { margin-bottom: 4rem; }
.excerpt-view h2, .tag-item h2 { font-size: var(--size-h4); margin: 0 0 -1rem; }
.excerpt-view p, .tag-item p { font-size: 1rem; }
.full-post-view { margin-bottom: 4rem; }
.full-post-view hr { margin-top: 4rem; max-width: 70%; }

/* --- forms and buttons ---------------------------------------------------- */
label { display: block; }
input, textarea, select {
  font: inherit; color: var(--text); background: var(--bg);
  border: 1px solid var(--border); border-radius: 5px;
  padding: 0.5em; margin-bottom: 0.5rem; max-width: 100%; display: inline-block;
}
textarea:not([cols]) { width: 100%; }
input[type="checkbox"], input[type="radio"] { accent-color: var(--accent); width: min-content; }
input[type="color"] { height: 2.5rem; padding: 0.2rem; }
input[type="file"] { border: 0; }
@media (max-width: 720px) { textarea, select, input { width: 100%; } }
form.stack label { display: block; margin-top: 1rem; }
form.stack input, form.stack textarea, form.stack select { width: 100%; box-sizing: border-box; }
button, a.button {
  cursor: pointer; background: var(--accent); color: var(--bg); border: none;
  width: auto; border-radius: 5px; padding: 0.5rem 0.75rem;
  font-family: inherit; font-size: inherit; text-decoration: none;
}
button:hover, a.button:hover { background: var(--text); }
/* A button that does something small and reversible — a connection test, a
   retry — beside one that does the actual work. Outlined rather than filled,
   so the row has one obvious primary action. */
button.secondary { background: none; color: var(--accent); border: 1px solid currentColor; }
button.secondary:hover { background: var(--accent); color: var(--bg); }
button.danger { background: none; color: #c0392b; border: 1px solid currentColor; }
button.danger:hover { background: #c0392b; color: var(--bg); }
button[disabled], input:disabled, textarea:disabled, select:disabled {
  cursor: not-allowed; opacity: 0.5;
}
button[disabled]:hover { background: var(--accent); }
.notice { background: rgba(200, 60, 60, 0.12); border: 1px solid #c66; padding: 0.5rem 1rem; border-radius: 4px; }
.icon {
  position: relative; top: 0.1em; display: inline-block; width: 1em; height: 1em;
  padding-right: 0.25rem; stroke-width: 0; stroke: currentColor; fill: currentColor;
}
.saved-notice { background: rgba(60, 160, 60, 0.12); border: 1px solid #6a6; padding: 0.5rem 1rem; border-radius: 4px; }

/* admin */
.admin-nav { border-bottom: 1px solid var(--border); padding-bottom: 0.6rem; }
.admin-nav .current, p .current { font-weight: 700; }
.admin-table { width: 100%; border-collapse: collapse; }
.admin-table th, .admin-table td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border); }
.admin-table tr.current > * { background: var(--accent-bg); }
.inline-form { display: inline; margin: 0; }
/* A destructive action that is a button (so it can POST with a CSRF token)
   but reads as the quiet link it should be. */
.linklike { background: none; border: 0; padding: 0; font: inherit; color: inherit;
  text-decoration: underline; cursor: pointer; }
/* The weigh-in form is seven short fields; a single column would be a lot of
   scrolling for a number and five percentages. */
.weighin-form { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.6rem 1rem; }
.weighin-form label { margin: 0; }
.weighin-form > p { grid-column: 1 / -1; margin: 0.2rem 0 0; }
.admin-search { display: flex; flex-wrap: wrap; align-items: end; gap: 0.6rem; margin: 0.8rem 0; }
.admin-search label { flex: 1 1 16rem; margin: 0; }
.admin-count { margin: 0.5rem 0; }

/* The filter row sits under the column headings, inside the same <thead>, so
   it scrolls and aligns with the columns it narrows rather than floating
   above the table as a separate bar. Smaller and quieter than the headings:
   these are controls, not labels, and the heading above already says what
   each one is for. */
.admin-filter-row > td { padding: 0.3rem 0.5rem 0.6rem; vertical-align: top;
  border-bottom: 2px solid var(--border); }
.admin-filter-row select {
  width: 100%; max-width: 100%; min-width: 0; font-size: 0.85rem;
  padding: 0.25rem 0.3rem; margin: 0;
}
/* Year and month share one cell and one line until there is no room. */
.admin-filter-row td:nth-child(4) { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.admin-filter-row td:nth-child(4) select { flex: 1 1 5.5rem; }
.admin-filter-row select:disabled { opacity: 0.5; }
/* The slug beside an untitled post's borrowed first line — it is the thing
   that identifies the row, so it needs to be readable, just not loud. */
.admin-slug { display: block; font-family: var(--font-code); font-size: 0.8rem; }

/* editor: transparent textarea over the highlight backdrop (Cairn pattern).
   Monospace both, IDENTICAL metrics — the backdrop must line up glyph-for-glyph,
   so every box property is stated on both and nothing is left to inherit.
   Selectors are qualified with form.stack because the generic
   `form.stack textarea` rule above is more specific than a bare
   `.editor-wrap textarea` and would otherwise win on padding and colour. */
.editor-wrap { position: relative; display: block; }
.editor-wrap .editor-backdrop,
form.stack .editor-wrap textarea {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.95rem; line-height: 1.5; tab-size: 4; letter-spacing: normal;
  margin: 0; border: 1px solid var(--border); border-radius: 4px;
  padding: 0.6rem; box-sizing: border-box; width: 100%;
  white-space: pre-wrap; word-wrap: break-word; overflow-wrap: anywhere;
}
.editor-wrap .editor-backdrop {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  background: var(--bg); color: var(--text); border-color: transparent;
  margin: 0;
}
form.stack .editor-wrap textarea {
  position: relative; background: transparent; color: transparent;
  caret-color: var(--text); resize: vertical; min-height: 22rem; display: block;
}
/* Selection needs a visible highlight of its own — the text itself is
   transparent, so the browser default would show nothing. */
form.stack .editor-wrap textarea::selection { background: var(--accent); color: var(--bg); }
/* highlight tokens: colour and weight only, never size */
.hl-mark { color: var(--muted); }
.hl-h { font-weight: 700; }
.hl-bold { font-weight: 700; }
.hl-em { font-style: italic; }
.hl-del { text-decoration: line-through; }
.hl-code { color: var(--accent); }
.hl-link { color: var(--accent); }
.hl-wikilink { color: var(--accent); font-weight: 600; }
.hl-quote { color: var(--muted); }

/* autocomplete popup */
.ac-popup { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
            box-shadow: 0 4px 14px rgba(0,0,0,0.18); z-index: 50; min-width: 12rem; max-width: 20rem; }
.ac-popup .ac-item { display: flex; justify-content: space-between; gap: 0.8rem; width: 100%;
                     border: 0; background: none; text-align: left; padding: 0.35rem 0.6rem;
                     color: var(--text); font: inherit; }
.ac-popup .ac-item.sel { background: var(--accent-bg); }
.ac-popup .ac-hint { color: var(--muted); font-size: 0.8em; }

/* catalog */
.catalog-head { display: flex; gap: 1.2rem; align-items: flex-start; }
.catalog-cover { max-width: 9rem; border-radius: 4px; }
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr)); gap: 1.2rem; }
.catalog-card { border: 0 !important; margin: 0 !important; padding: 0 !important; }
/* margin, not margin-bottom. `main img` sets `margin: 1.5rem auto` for images
   in prose, and overriding only the bottom left a 24px top margin on every
   cover — which put each picture one line below the placeholder in the cell
   beside it and made every row of the grid ragged. */
.catalog-card img { width: 100%; border-radius: 4px; display: block; margin: 0 0 0.3rem; }
.catalog-card a { text-decoration: none; }
/* Covers are not one shape — a book is tall, an album is square, a place's
   satellite view is wide. Left alone they make a grid of ragged rows where
   the titles never line up. A fixed box with object-fit keeps the columns
   honest and crops rather than distorts; the aspect ratio is the compromise
   between a paperback and an LP sleeve. */
.catalog-grid-bare .catalog-card img,
.catalog-grid-bare .catalog-nocover {
  aspect-ratio: 3 / 4; width: 100%; object-fit: cover; border-radius: 4px;
  margin: 0 0 0.4rem;
}
/* An item with no cover at all: its kind's own drawing on the card's ground,
   so the cell holds its place in the grid instead of collapsing. */
.catalog-nocover {
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-bg); border: 1px solid var(--border);
}
.catalog-nocover .icon { width: 2.5rem; height: 2.5rem; padding-right: 0; color: var(--muted); }
/* The anchor wraps the whole card, so it has to be a block container. Left
   inline it opens an inline formatting context, and the whitespace between the
   tags becomes a line box that pushes the picture down by one line — but only
   in the branch that renders an <img>, so the covers sat 24px lower than the
   placeholders beside them and every row was ragged. */
.catalog-card a { display: flex; flex-direction: column; }
.catalog-grid-bare .p-name {
  display: block; font-size: 0.9rem; line-height: 1.3;
  /* Two lines, always: a one-line title and a two-line title next to each
     other otherwise leave the row below them stepped. Longer titles are cut
     with an ellipsis, and the full text is still the link's own text for
     anything reading the page rather than looking at it. */
  min-height: 2.6em; overflow: hidden;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}
.catalog-card a:hover .p-name { text-decoration: underline; }
/* Seven kinds on one page, so the rhythm between them is tighter than the
   front page's — there the sections are long and need the air; here each is
   one row of covers, and the standard spacing turns seven shelves into seven
   screens of mostly nothing. */
.stream-page .home-section { margin-bottom: 2rem; }
.stream-page .home-section > h2 { margin-top: 2rem; margin-bottom: 0.8rem; }
.stream-page .home-section:first-of-type > h2 { margin-top: 1rem; }
.stream-page .home-viewall { margin-top: 0.8rem; }
.engagement-history .engagement { border-top: 1px solid var(--border); padding-top: 0.6rem; margin-top: 0.6rem; }
.engagement-panel { border: 1px solid var(--border); border-radius: 6px; padding: 0.6rem 1rem; margin-top: 1.5rem; }
.episode-list { list-style: none; padding-left: 0.4rem; columns: 2; }
.episode-list li { break-inside: avoid; }
.inline-forms input[type=number] { padding: 0.2rem; }
.lookup-row { display: block; width: 100%; text-align: left; border: 0; background: var(--accent-bg);
              padding: 0.4rem 0.6rem; margin-top: 2px; border-radius: 4px; color: var(--text); font: inherit; cursor: pointer; }
#lookup-results { margin-top: 0.3rem; }
.lookup-line { display: flex; gap: 0.5rem; align-items: stretch; }
.lookup-line input { flex: 1; min-width: 0; }
.lookup-note { font-size: 0.85rem; margin: 0.3rem 0 0; }
/* A row that opens a match rather than filling the form. Marked, because a
   click that appears to do nothing is the worst thing a list can do. */
.lookup-more::after { content: " →"; color: var(--muted); }

/* --- the match scorecard --------------------------------------------------
   Drawn rather than fetched: two club crests are somebody's trademark, so a
   match's cover is made from its own record. Inline SVG, so it takes the
   page's colours and is right in both themes without a second copy of
   anything. See app/content/scorecard.php. */
.scorecard { display: block; width: 100%; aspect-ratio: 3 / 4; }
.scorecard text { text-anchor: middle; fill: currentColor; font-family: var(--font-title); }
.scorecard-panel { fill: var(--accent-bg); stroke: var(--border); stroke-width: 2; }
.scorecard-comp { fill: var(--muted); font-family: var(--font-main);
  letter-spacing: 0.04em; text-transform: uppercase; }
.scorecard-score { fill: var(--accent); font-family: var(--font-code); }
/* The card already says "Atherton Collieries 3-0 Irlam" in letters an inch
   high; printing the same sentence again underneath is the caption on a
   photograph of itself. The text stays in the document — it is the link's
   accessible name and what a screen reader announces — it simply is not drawn
   twice. */
.catalog-card-drawn .p-name {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* On its own page the card sits in the cover slot, which is sized for a book
   jacket — the span is a flex item, so the width has to be pinned or the SVG
   claims the whole row. */
.catalog-cover .scorecard { width: 9rem; }
.catalog-grid-bare .scorecard { margin: 0 0 0.4rem; }
.match-line { margin: 0.2rem 0 0.6rem; }

/* --- who was on the pitch -------------------------------------------------
   Two squads side by side where there is room, stacked where there is not.
   A line-up is a list of names and nothing else, so it is set tight: the
   interesting thing is scanning it for one you recognise. */
.match-detail { margin-top: 2rem; }
.match-teams { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.5rem; }
.match-team h3 { margin-bottom: 0.2rem; }
.match-team h4 { margin: 0.8rem 0 0.2rem; font-size: 1rem; color: var(--muted); }
ol.squad { list-style: none; padding-left: 0; margin: 0; }
ol.squad li { padding: 0.12rem 0; line-height: 1.35; }
.shirt { display: inline-block; min-width: 2rem; color: var(--muted);
  font-family: var(--font-code); font-size: 0.85em; }
/* Everything that happened to a man, beside his name, with the minute on it.
   Drawn rather than named, because that is how a card is read — but each
   carries its words too, for anyone the shape means nothing to. */
.ev { display: inline-block; vertical-align: baseline; margin-left: 0.3rem;
  white-space: nowrap; }
.ev-min { font-family: var(--font-code); font-size: 0.72em; color: var(--muted);
  margin-left: 0.12rem; vertical-align: 0.08em; }
.ev-yellow::before, .ev-red::before {
  content: ""; display: inline-block; width: 0.5em; height: 0.72em;
  border-radius: 1px; vertical-align: -0.05em;
}
.ev-yellow::before { background: #e3b505; }
.ev-red::before { background: #c1272d; }
/* A ball, drawn rather than typed: the football emoji is a different size and
   a different colour in every font it lands in, and an own goal has to be the
   same shape as a goal in a different colour to read as one. */
.ball { display: inline-block; width: 0.62em; height: 0.62em; border-radius: 50%;
  vertical-align: -0.02em; background: var(--text);
  box-shadow: inset -0.12em -0.12em 0 rgba(255,255,255,0.35); }
/* An own goal counts for the other side — the one event that belongs to one
   man and scores for his opponents. Drawn the same as a goal in the same
   colour, it makes a scoreline that does not add up. */
.ball-og { background: #c1272d; }
.ev-try, .ev-kick { font-size: 0.85em; }
.ev-try { color: var(--accent); }
.ev-on { color: #1a7f37; font-size: 0.85em; }
.ev-off { color: var(--muted); font-size: 0.85em; }
@media (prefers-color-scheme: dark) { .ev-on { color: #7ee787; } }
.match-orphans { margin-top: 1rem; }
/* The sources, offered on the add screen. A list of links, not a paragraph:
   they are things to click, and one of them is where you were going anyway. */
.lookup-sources { display: flex; flex-wrap: wrap; gap: 0.4rem 0.9rem;
  margin: 0.4rem 0 0; font-size: 0.85rem; }
.lookup-sources a { white-space: nowrap; }
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* The editor's read-only view of a structured field. */
.json-field { display: flex; gap: 0.6rem; align-items: center; margin: 0.2rem 0; }
.json-clear { font-size: 0.8rem; padding: 0.1rem 0.6rem; border-radius: 999px; }
/* Just filled by an import, and not yet saved. Marked because the field is
   hidden: without it the only evidence a squad arrived is to save and look at
   the page — which is exactly long enough to conclude the import is broken. */
.json-filled .muted { color: var(--accent); }
.json-filled::after { content: " — save to keep"; color: var(--muted); font-size: 0.8rem; }

/* settings colour fields */
.color-field { display: flex; gap: 0.5rem; align-items: center; }
.color-field input { flex: 1; }
.color-field .swatch { width: 1.6rem; height: 1.6rem; border: 1px solid var(--border); border-radius: 4px; flex: none; }

/* h-card header */
.site-avatar { width: 3rem; height: 3rem; border-radius: 50%; float: left; margin: 0.6rem 0.8rem 0.3rem 0; object-fit: cover; }

/* interactions */
.facepile { display: flex; flex-wrap: wrap; gap: 0.4rem; margin: 1rem 0; }
.facepile .face { position: relative; display: block; width: 2.4rem; height: 2.4rem; }
.facepile .face img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.facepile .face-initial { display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; border-radius: 50%; background: var(--accent-bg); color: var(--text); }
.facepile .face-badge { position: absolute; right: -2px; bottom: -2px; font-size: 0.7rem;
  background: var(--bg); border-radius: 50%; padding: 0 2px; }
.interactions { border-top: 1px solid var(--border); margin-top: 2rem; padding-top: 1rem; }
.comment { border-top: 1px solid var(--border); padding-top: 0.8rem; margin-top: 0.8rem; }
.comment-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.comment-avatar { width: 1.8rem; height: 1.8rem; border-radius: 50%; }
.comment-form { margin-top: 1.4rem; }
.comment-form summary { cursor: pointer; }
/* Threads, front and back, one rule.
   --depth is set inline by the template and capped at 3 by
   beo_interaction_thread(), so the indent can never run a conversation off
   the side of a phone however deep the real reply chain goes.

   The public side used to indent by 2.2ch against a grey hairline, which at
   one level of nesting is about four characters and reads as a slightly
   ragged flat list rather than a conversation. The admin's treatment is the
   one that works — a real step and a rail you can follow with your eye — so
   both now share it. A reply drops its top border: the rail is what says
   "this belongs to the thing above", and a line across the top as well cuts
   the two apart at the same moment the indent joins them. */
.comment-reply, .mod-reply {
  margin-left: calc(var(--depth, 0) * 1.6rem);
  border-left: 3px solid var(--accent); border-top: 0;
  padding-left: 1rem; padding-top: 0.5rem;
}
/* The quoted body, which is what makes the admin view scan: the words someone
   wrote are visually separated from the furniture around them. */
.comment blockquote, .guestbook-entry blockquote, .mod-row blockquote {
  border-left: 3px solid var(--accent); margin: 0.4rem 0;
  padding-left: 0.9rem; font-style: italic;
}
@media (max-width: 600px) {
  /* 1.6rem three deep is a third of a phone's width spent on indent. */
  .comment-reply, .mod-reply { margin-left: calc(var(--depth, 0) * 0.7rem); padding-left: 0.7rem; }
}
.comment-inreplyto { margin: 0 0 0.2rem; font-size: 0.85em; }
.comment-actions { margin: 0.3rem 0 0; }
.comment-actions .reply-link { font-size: 0.85em; }
/* Replying puts ?reply=N in the URL, so :target cannot mark the form open —
   highlight the parent instead, which is the thing the reader wants to see. */
.comment:target, .guestbook-entry:target { background: var(--accent-bg); }
.guestbook-entry.comment-reply { padding-top: 0.6rem; padding-bottom: 0.6rem; }
/* The name leads the line, as it does in the admin. It was a plain link, which
   underlined it and made the strongest thing on the row a URL rather than a
   person. */
.guestbook-who .p-name, .comment-head .p-name { font-weight: 600; text-decoration: none; }
.guestbook-who .p-name:hover, .comment-head .p-name:hover { text-decoration: underline; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
/* Two honeypots hidden two different ways, because bots differ in what they
   skip: some ignore display:none, some ignore off-screen positioning. Both
   must come back empty. Never give either an autofill-friendly name or a
   real label — a password manager filling one would fail a real visitor. */
.hp-none { display: none !important; }

.inline-check { display: flex; gap: 0.5rem; align-items: baseline; margin: 0; }
.inline-check input { width: auto; }

/* blogroll */
.blogroll { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 1rem; }
.blogroll-entry { border: 1px solid var(--border); border-radius: 6px; padding: 0.8rem 1rem; margin: 0 !important; }
.blogroll-entry h3 { margin: 0 0 0.3rem; font-size: 1.05rem; }
.blogroll-entry p { margin: 0.3rem 0 0; }
.blogroll-host { word-break: break-all; }

/* guestbook */
/* Longhands, not `padding: 0.9rem 0` and `margin: 0`.
   A shorthand sets all four sides, so this rule — later in the file and of
   equal specificity — was silently resetting the margin-left and padding-left
   that .comment-reply uses to indent a thread. Every guestbook reply sat at
   the same depth however deep the conversation went, while the admin, whose
   row rule happens to use longhands, nested correctly. The two looked like
   different features; they were one rule losing a specificity tie. */
.guestbook-entry { border-top: 1px solid var(--border);
  padding-top: 0.9rem; padding-bottom: 0.9rem; margin-top: 0; margin-bottom: 0; }
.guestbook-who { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: baseline; margin: 0 0 0.3rem; }
.guestbook-who .p-name { font-weight: 600; }
.guestbook-intro { margin-bottom: 1.2rem; }
.mod-row { border-top: 1px solid var(--border); padding-top: 0.8rem; margin-top: 0.8rem; }
/* Same reason as .guestbook-entry above: never a shorthand on anything a
   reply rule has to indent. */
.comment { padding-bottom: 0; }
.mod-row blockquote { margin: 0.4rem 0; }
/* Indented only in the single-post thread view, where the rows really are a
   conversation. A chronological list is not one, and indenting it would
   imply an order the rows are not in. */
/* .mod-reply is styled with .comment-reply above — one treatment, so the
   public thread and the moderation view cannot drift apart again. */
.mod-parent { margin: 0 0 0.3rem; font-size: 0.9em; }
.mod-subject { padding: 0.5rem 0.7rem; background: var(--accent-bg);
  border-radius: 4px; margin-bottom: 0.8rem; }
.mod-status { border: 1px solid var(--border); border-radius: 3px;
  padding: 0 0.3rem; font-size: 0.85em; }
.mod-tabs .current { font-weight: 600; }
.mod-search { display: flex; gap: 0.5rem; align-items: center; margin: 0.6rem 0; }
.mod-search input[type="search"] { flex: 1 1 22rem; }
.mod-threads { width: 100%; border-collapse: collapse; }
.mod-threads th, .mod-threads td { text-align: left; padding: 0.35rem 0.5rem 0.35rem 0;
  border-bottom: 1px solid var(--border); vertical-align: top; }

/* media + uploads */
.image-field { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.image-field input[type=text] { flex: 1 1 14rem; }
.image-field .image-preview { max-height: 3rem; border-radius: 4px; }
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 1rem; }
.media-tile { margin: 0; }
.media-tile img { width: 100%; border-radius: 4px; display: block; }
.media-tile figcaption input { width: 100%; font-size: 0.75rem; padding: 0.2rem; }
.editor-tools { display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap; margin-bottom: 0; }
.editor-tools input[type=file] { max-width: 14rem; }
/* mf2 lint */
.lint-report { border-top: 1px solid var(--border); margin-top: 1rem; padding-top: 0.6rem; }
.lint-report h2 { font-size: 1rem; }
.lint-ok { color: #2e7d32; margin: 0.2rem 0; }
.lint-warn { color: #c0392b; margin: 0.2rem 0; }
@media (prefers-color-scheme: dark) { .lint-ok { color: #7fd18a; } .lint-warn { color: #ff8a75; } }
.scope { display: block; margin-top: 0.3rem; }
fieldset { border: 1px solid var(--border); border-radius: 4px; }

/* training log */
.route-map { width: 100%; height: auto; border-radius: 6px; display: block; }
.map-note { margin-top: 0.3rem; }
/* Satellite imagery for a place. Same shape as a route map — it is the same
   size of picture doing the same job — with the margins a <figure> needs. */
figure.place-map { margin: 1.2rem 0; }
figure.place-map img, img.place-map { width: 100%; height: auto; border-radius: 6px; display: block; }
figure.place-map figcaption { margin-top: 0.4rem; font-size: 0.9em; }
.setting-note { margin: -0.4rem 0 0.8rem; }
label.toggle { display: flex; align-items: center; gap: 0.5rem; }
label.toggle input[type="checkbox"] { width: auto; }
.activity-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: 0.8rem; margin: 1.2rem 0; }
.activity-stats div { margin: 0; }
.activity-stats dt { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; }
.activity-stats dd { margin: 0; font-size: 1.15rem; font-weight: 600; }
.log-row { border-top: 1px solid var(--border); padding: 0.5rem 0; margin: 0; display: flex;
  justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.log-nav .current { font-weight: 700; }

/* search results */
.search-result { border-top: 1px solid var(--border); padding: 0.8rem 0; margin: 0; }
.search-result h2 { margin: 0 0 0.3rem; font-size: 1.1rem; }
.search-excerpt { margin: 0 0 0.3rem; }
.search-excerpt mark { background: var(--accent-bg); color: inherit; padding: 0 0.1em; }

/* site menu — the layout rules live in the masthead block above. This is the
   remainder of an older, separate .site-nav block; its border-top drew a
   second rule across the banner that prry.uk does not have. */
.site-nav a { padding: 0.15rem 0; }
.site-nav .current { font-weight: 700; }

/* archive navigation — year strip and newer/older.
   The year strip is one row per decade of writing, so it wraps rather than
   scrolling sideways, and each year keeps a wide enough tap target to be
   usable on a phone. */
.pagination { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.7rem;
  margin: 0.9rem 0; font-size: 0.95rem; }
.pagination a, .pagination strong, .pagination span { padding: 0.25rem 0.1rem; }
.pagination strong { font-weight: 700; }
.year-nav { color: var(--muted); }
.pagination [rel="next"] { margin-left: auto; }
.archive-month { font-size: 1.05rem; color: var(--muted); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; margin: 1.6rem 0 0.4rem;
  border-bottom: 1px solid var(--border); padding-bottom: 0.25rem; }
.sparkline { width: 100%; height: auto; }
.splits summary { cursor: pointer; }

/* markdown helper popup */
.md-help { position: relative; border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg); padding: 0.6rem 0.8rem; margin: 0.4rem 0 0;
  box-shadow: 0 6px 18px rgba(0,0,0,0.14); }
/* grid, not flex-wrap: on a phone the hint wraps within its own cell and the
   close button stays on the first line instead of dropping below it. */
.md-help-head { display: grid; grid-template-columns: auto 1fr auto; align-items: baseline;
  gap: 0.6rem; border-bottom: 1px solid var(--border);
  padding-bottom: 0.4rem; margin-bottom: 0.4rem; }
.md-help-head button { border: 0; background: none; color: var(--muted);
  font-size: 1.2rem; line-height: 1; padding: 0 0.3rem; cursor: pointer; }
.md-help ul { list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 2px; }
.md-help li { margin: 0; }
/* wrap, not overflow: `~~strikethrough~~` has no spaces to break at, so
   without this it paints over the next grid column. */
.md-help .md-insert { display: flex; gap: 0.5rem; align-items: baseline; flex-wrap: wrap;
  width: 100%; min-width: 0; text-align: left; border: 0; background: none; color: var(--text);
  padding: 0.25rem 0.4rem; border-radius: 4px; font: inherit; cursor: pointer; }
.md-help .md-insert:hover, .md-help .md-insert:focus-visible { background: var(--accent-bg); }
.md-help .md-insert code { background: none; padding: 0; color: var(--accent);
  min-width: 0; overflow-wrap: anywhere; }
#btn-md-help { padding: 0.25rem 0.6rem; font-size: 0.9rem; }

/* Connected apps (§9). A wide table on a narrow screen scrolls in its own
   box rather than pushing the page sideways. */
.apps-table { width: 100%; border-collapse: collapse; margin: 1rem 0; display: block; overflow-x: auto; }
.apps-table th, .apps-table td { text-align: left; padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border); vertical-align: top; white-space: nowrap; }
.apps-table th { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.apps-table code { font-size: 0.85rem; white-space: normal; }
.apps-table .muted { font-size: 0.85rem; }
.apps-table form.inline { margin: 0; }
tr.token-revoked td, tr.token-expired td { opacity: 0.55; }
tr.token-no.expiry td { background: var(--accent-bg); }

/* --- garden beds -----------------------------------------------------------
   A tree of sections. Indented by nesting, capped by the partial rather than
   by the stylesheet, so a deep bed stops indenting instead of disappearing
   off the side of a phone. */
.bed-list { list-style: none; padding: 0; margin: 0; }
.bed-list .bed-list { margin-left: 1.4rem; padding-left: 0.9rem;
  border-left: 2px solid var(--border); }
.bed-item { margin: 0.6rem 0; }
.bed-name { font-family: var(--font-title); font-size: 1.1em; text-decoration: none; }
.bed-name:hover { text-decoration: underline; }
.bed-count { margin-left: 0.4rem; font-size: 0.8em; }
.bed-description { margin: 0.1rem 0 0; font-size: 0.9em; }
.bed-items { list-style: none; padding: 0; margin: 1rem 0; }
.bed-items li { padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
.bed-items li:last-child { border-bottom: none; }
.bed-intro { margin-bottom: 1.5rem; }
.bed-unfiled { margin-top: 2.5rem; }
.bed-also { margin-top: 1rem; }
.breadcrumb { margin: 0 0 0.5rem; font-size: 0.9em; }
/* Maturity, the garden's own vocabulary: a seed is not a claim, an evergreen
   page is. Shown as a quiet pill rather than a badge fighting the title. */
.maturity { border: 1px solid var(--border); border-radius: 999px;
  padding: 0 0.5em; font-size: 0.75em; margin-left: 0.5rem; }
.bed-admin-row { border-top: 1px solid var(--border); padding: 0.8rem 0;
  margin-left: calc(var(--depth, 0) * 1.2rem); }
.bed-admin-path { margin: 0 0 0.4rem; font-size: 0.85em; }
.bed-admin-fields { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; }
.bed-admin-fields label { flex: 1 1 10rem; }
.bed-picker { border: 1px solid var(--border); border-radius: 5px; padding: 0.8rem 1rem; margin: 1rem 0; }
.bed-picker legend { font-weight: 600; padding: 0 0.4rem; }
.bed-pick { margin: 0.2rem 0; }

/* --- garden maturity, revisions, the owner's shortcut ---------------------- */

/* The stage badge. Coloured by stage, because the whole point is that a reader
   can tell a seed from an evergreen without reading the word — and the word is
   there too, because colour alone is not a distinction everyone can see. */
.maturity, .garden-stages a {
  display: inline-flex; align-items: center; gap: 0.35rem;
  font-size: 0.85rem; text-decoration: none;
  border: 1px solid var(--border); border-radius: 999px;
  padding: 0.15rem 0.7rem 0.15rem 0.5rem;
}
.maturity:hover, .garden-stages a:hover { border-color: var(--accent); }
.maturity .icon, .garden-stages .icon { width: 1.1em; height: 1.1em; padding-right: 0; }
.garden-maturity { margin: 0 0 1.5rem; }
.garden-stages { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 2rem; }
.garden-maturity-blurb { margin: -0.5rem 0 1.5rem; }
.garden-stage-list li { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: baseline; }

/* The history. A trail, not a table: the reader's question is "is this being
   tended", which a column of dates and short notes answers. */
.revisions { border-top: 1px solid var(--border); margin-top: 2.5rem; padding-top: 1rem; }
.revisions h2 { font-size: var(--size-h5); text-transform: uppercase;
  letter-spacing: 0.05em; margin: 0 0 0.8rem; }
.revision-trail { list-style: none; padding: 0; margin: 0;
  border-left: 2px solid var(--border); }
.revision { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; align-items: baseline;
  padding: 0.35rem 0 0.35rem 1rem; position: relative; font-size: 0.95rem; }
/* The dot on the rail. A trail without them is two columns of text. */
.revision::before {
  content: ""; position: absolute; left: -5px; top: 0.9em;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); border: 2px solid var(--bg);
}
.revision time { color: var(--muted); font-variant-numeric: tabular-nums;
  flex: 0 0 6rem; font-size: 0.9rem; }
/* A worked-out note is quieter than one someone wrote. Both are true; only one
   is somebody telling you something. */
.revision-auto .revision-note { color: var(--muted); }

/* The owner's shortcut, bottom right. Fixed, small, and out of the way — it is
   on every page and must never be the thing a reader notices first. */
/* The dock: back to the top for everyone, the owner's shortcut beside it. */
.admin-dock { position: fixed; right: 1rem; bottom: 1rem; z-index: 50;
  display: flex; align-items: center; gap: 0.5rem; }

/* .admin-bar is a <details>, and the generic `details` rule two hundred lines
   up gives every one of them a bordered, padded, accent-backed box with a
   2rem margin. That box was the grey rectangle sitting behind the button.
   Reset here rather than made more specific there, because that rule is right
   for every other <details> on the site. */
.admin-bar { position: relative; padding: 0; margin: 0;
  background: none; border: 0; border-radius: 0; }

/* Both dock buttons, drawn once. The summary also has to undo the generic
   `summary` rule's padding and negative margins, which were pulling the icon
   off-centre inside the circle. */
.dock-btn {
  list-style: none; cursor: pointer; text-decoration: none;
  display: flex; align-items: center; justify-content: center;
  width: 2.9rem; height: 2.9rem; border-radius: 50%;
  padding: 0; margin: 0; border: 0;
  background: var(--accent); color: var(--bg);
}
.dock-btn:hover { background: var(--text); }
/* top: 0 — the base .icon rule nudges every icon down 0.1em so it sits on a
   text baseline. There is no baseline in a circular button; the nudge just
   put the drawing two pixels below centre. */
.dock-btn .icon { width: 1.3rem; height: 1.3rem; padding-right: 0; top: 0; }
.admin-bar > summary::-webkit-details-marker { display: none; }
.admin-bar[open] > summary { margin-bottom: 0; }
/* No drop shadow. It read as a grey box hanging off the page; a plain border
   on the page's own background is enough to separate a menu from what is
   behind it, and it is one less thing that looks different in dark mode. */
.admin-bar-menu {
  position: absolute; right: 0; bottom: 3.4rem; min-width: 14rem;
  background: var(--bg); border: 1px solid var(--accent); border-radius: 8px;
  padding: 0.4rem; display: flex; flex-direction: column;
}
.admin-bar-menu a {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.6rem; border-radius: 5px;
  text-decoration: none; color: var(--text); font-size: 0.95rem;
}
.admin-bar-menu a:hover { background: var(--accent-bg); color: var(--accent); }
.admin-bar-menu .icon { width: 1.1em; height: 1.1em; padding-right: 0; flex: none; }
.admin-bar-menu hr { margin: 0.3rem 0; }
.admin-bar-count {
  margin-left: auto; background: var(--accent); color: var(--bg);
  border-radius: 999px; padding: 0 0.45rem; font-size: 0.8rem;
}
/* Printing a page should not print a menu that only one person can use. */
@media print { .admin-dock { display: none; } }
/* The Admin entry is an action, not a section — the same treatment the site's
   other action buttons get. Overrides .site-nav a, which is styled to look
   like plain text. */
.site-nav a.nav-action { color: var(--bg); padding: 0.25rem 0.7rem; border-radius: 5px; }
.site-nav a.nav-action:hover { color: var(--bg); background: var(--text); text-decoration: none; }

/* Only shown when it is not a plain album — see beo_release_type_label(). */
.release-type { white-space: nowrap; }

/* Wikilinks in the editor that resolve to nothing yet, with an offer to plant
   each as a seed. Quiet by design: it is a prompt, not an error. */
.seed-panel { border: 1px dashed var(--border); border-radius: 6px;
  padding: 0.6rem 0.8rem; margin: 0.6rem 0 1rem; }
.seed-panel > p { margin: 0 0 0.5rem; }
.seed-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.seed-row { display: inline-flex; align-items: center; gap: 0.4rem;
  background: var(--accent-bg); border: 1px solid var(--border);
  border-radius: 999px; padding: 0.2rem 0.3rem 0.2rem 0.7rem; font-size: 0.9rem; }
.seed-row code { font-size: 0.95em; }
.seed-plant { font-size: 0.8rem; padding: 0.1rem 0.6rem; border-radius: 999px; }
.seed-done { color: var(--muted); font-size: 0.85rem; padding-left: 0.4rem; }
.seed-error { color: var(--accent); font-size: 0.85rem; }
/* Same chip, but the action is a choice rather than a button, and the preview
   of the block has to be allowed to be long without pushing the select off. */
.fence-row { max-width: 100%; }
.fence-row code { max-width: 22rem; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; opacity: 0.8; }
.fence-row select { font-size: 0.8rem; padding: 0.1rem 0.3rem; border-radius: 999px;
  max-width: 9rem; }

/* --- activity charts and best efforts ------------------------------------ */
/* Explicit single column. A grid with no template sizes its track to its
   content, so an SVG carrying an intrinsic width drags the column down to it
   and "width: 100%" faithfully fills a box that is already too narrow. */
.activity-charts { margin: 1.25rem 0; display: grid; grid-template-columns: 1fr;
  gap: 0.4rem; width: 100%; }
.activity-chart { margin: 0; width: 100%; position: relative; }
/* preserveAspectRatio="none" lets the chart stretch to the column, so the
   viewBox is a coordinate space rather than a size. The stroke would stretch
   with it, which is what vector-effect on the path prevents. */
.activity-chart svg { display: block; width: 100%; height: 120px;
  background: var(--accent-bg); border-radius: 5px; }
/* Overlaid rather than beneath: saves a line per chart and names the thing
   before you look at it. Pointer-events off so it can never sit between the
   reader and the chart. */
.activity-chart figcaption { position: absolute; top: 0.35rem; left: 0.6rem;
  font-size: 0.75rem; color: var(--muted); pointer-events: none;
  letter-spacing: 0.01em; }
.activity-chart figcaption b { color: var(--text); font-weight: 600; }

/* Best efforts. The generic <details> is a 2rem-margined tinted card, which is
   right for an aside and too heavy for the numbers most readers came for. */
.splits { margin: 1.25rem 0; padding: 0.6rem 0.9rem; background: transparent;
  border: 1px solid var(--border); }
.splits summary { cursor: pointer; padding: 0.4rem 0.9rem; margin: -0.6rem -0.9rem; }
.splits[open] > summary { margin-bottom: 0.6rem; }
.effort-tables { display: grid; gap: 0.9rem 1.75rem; width: 100%;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
.effort-table { border-collapse: collapse; width: 100%; font-size: 0.9rem; }
.effort-table caption { text-align: left; font-weight: 600; padding-bottom: 0.3rem;
  color: var(--muted); font-size: 0.8rem; text-transform: uppercase;
  letter-spacing: 0.05em; }
/* No row rules: five small tables side by side become a grid of lines, and the
   alignment does the separating on its own. */
.effort-table th, .effort-table td { text-align: left; font-weight: normal;
  padding: 0.15rem 0; border: 0; }
.effort-table td { text-align: right; font-variant-numeric: tabular-nums; }
.effort-table tr + tr th, .effort-table tr + tr td { border-top: 1px solid var(--border); }

/* --- year on year --------------------------------------------------------- */
.yoy { margin: 1.5rem 0 2.5rem; }
.yoy h2 { margin-bottom: 0.2rem; }
.yoy-heads { display: grid; gap: 0.75rem 1.5rem; margin: 1rem 0;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
.yoy-heads .label { display: block; font-size: 0.75rem; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.05em; }
.yoy-heads strong { font-size: 1.6rem; font-variant-numeric: tabular-nums; }
.yoy-heads .yoy-unit { font-size: 0.85rem; }
.yoy-controls { display: flex; flex-wrap: wrap; gap: 0.5rem 1rem; align-items: center;
  margin: 0.75rem 0; font-size: 0.9rem; }
.yoy-sports { display: flex; flex-wrap: wrap; gap: 0.3rem 0.8rem; }
.yoy-sports label { display: inline-flex; align-items: center; gap: 0.25rem;
  color: var(--muted); }
/* The chart is the one thing here that must not be cramped, so it keeps its
   aspect ratio rather than stretching: a squashed year reads as a flat year. */
.yoy-chart { width: 100%; height: auto; display: block; background: var(--accent-bg);
  border-radius: 5px; padding: 0.25rem 0; }
.yoy-note { font-size: 0.8rem; margin-top: 0.4rem; }

/* Full-screen the year-on-year panel. A fixed overlay rather than the
   Fullscreen API: that asks for a permission-shaped confirmation, overrides
   the reader's own zoom, and on iOS applies to video and nothing else. */
/* Right-aligned on the heading row and labelled. Sitting inline beside the
   title as a lone glyph it was invisible — an unlabelled symbol in running
   text reads as punctuation, not as a control. */
.yoy-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.yoy-fs { display: inline-flex; align-items: center; gap: 0.35rem; cursor: pointer;
  border: 1px solid var(--border); border-radius: 5px; background: transparent;
  color: var(--muted); font: inherit; font-size: 0.7rem; font-weight: normal;
  text-transform: uppercase; letter-spacing: 0.06em; line-height: 1;
  padding: 0.35rem 0.6rem; white-space: nowrap; flex: none; }
.yoy-fs:hover { color: var(--accent); border-color: var(--accent); }
.yoy-fs svg { width: 13px; height: 13px; display: block; }
/* One button, two faces: the state lives on the section, so the icon and the
   word follow it without any script rewriting the button's contents. */
.yoy-fs .icon-out, .yoy-fs .lbl-out { display: none; }
.yoy-fullscreen .yoy-fs .icon-in, .yoy-fullscreen .yoy-fs .lbl-in { display: none; }
.yoy-fullscreen .yoy-fs .icon-out { display: block; }
.yoy-fullscreen .yoy-fs .lbl-out { display: inline; }
/* The projection is an estimate, so it is stated more quietly than the
   figures beside it that actually happened. */
.yoy-heads .yoy-projected strong { color: var(--muted); }
.yoy-fullscreen { position: fixed; inset: 0; z-index: 900; margin: 0;
  background: var(--bg); overflow: auto; padding: 1.5rem clamp(1rem, 4vw, 3rem); }
body.yoy-fs-open { overflow: hidden; }
/* Given the whole viewport, the chart should use the height rather than
   stretch to a letterbox — capped so the headline figures stay in view. */
.yoy-fullscreen .yoy-chart { max-height: 70vh; }
