/* Bike San Antonio — site styles.
   A light "blueprint" layout: the content column is framed by hairlines that run the height of the
   page, horizontal rules cross them at small square nodes, and the side margins are rails
   that the bike chains (chain.js) hang in. One neutral palette, one accent (the brand light blue
   #79ADFA, the logo's skyline), flat surfaces, system fonts with a monospace for small labels, and a
   deep navy footer. The front page opens with a photo banner, inner pages with a tinted title band.
   Hand-written CSS on purpose: no framework, no build step and no network fetch, so the
   compiler stays deterministic and the page ships nothing third-party. */

:root {
  /* Type scale: every font-size below is multiplied by this, so all the text is 25% bigger
     (two steps up the browser's size list: 16px → 20px). Lengths — column widths, padding,
     gaps — are deliberately left alone, so only the text grows, not the layout. */
  --font-scale: 1.25;
  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

  --background: #ffffff;
  --foreground: #0b0d12;
  --ink-700: #374151;
  --muted: #f6f7f9;
  --muted-foreground: #6b7280;
  --border: #e5e7eb;
  /* The frame: hairlines (--border); --line-strong is the dashed line closing off the rails and the
     dashed frames around the video and signup blocks. */
  --line-strong: #d1d6dd;

  /* Theme palette: one scale built around the logo's light blue, #79ADFA (500, the mid tone),
     in the logo's hue (OKLCH hue 258), lighter above it and darker below.
       50–100  tints: the page title band, current-page highlight
       200–400 soft accents, button text and button hover text, the wheel in the title band
       500     the theme colour: bars, rings, focus outlines
       600     stronger accent
       700–800 links and text in the theme colour (5.6:1 and 8.1:1 on white; 500 is only 2.3:1)
       900–950 dark surfaces: buttons (400 text on 950 is 8.6:1) and the footer */
  --brand-50: #f3f7fe;
  --brand-100: #e2eeff;
  --brand-200: #c8defe;
  --brand-300: #a5c9ff;
  --brand-400: #8cb8f9;
  --brand-500: #79adfa;
  --brand-600: #4885df;
  --brand-700: #3067b8;
  --brand-800: #234f8e;
  --brand-900: #173560;
  --brand-950: #091a34;
  --brand: var(--brand-500);

  /* Bike motifs, drawn in the palette above and inlined so nothing is fetched (the hanging chains
     in the rails are drawn by chain.js, in --border and --muted):
       lane    a tinted underline with slightly deeper dashes on it, two close steps of the palette
               so it reads as a rule first and a road marking second; the element's background-size
               sets the period (dash 5/8). Headings, card tops; --lane-dark on the footer's navy.
       wheel   a 24-spoke wheel, crossed lacing, in 200 on the 50 tint: the page title band.
       bike    a line bicycle on a white patch, the centre of a section break (<hr>).
       node    the small square where two lines of the frame cross (11px, centred on the line). */
  --lane: linear-gradient(90deg, var(--brand-300) 0 62.5%, var(--brand-100) 0);
  --lane-dark: linear-gradient(90deg, var(--brand-800) 0 62.5%, var(--brand-900) 0);
  --wheel: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-50 -50 100 100' fill='none' stroke='%23c8defe'><circle r='45' stroke-width='6'/><circle r='40' stroke-width='1.5'/><circle r='5' stroke-width='2'/><path stroke-width='.8' d='M1.6 -4.2L40 0M0.5 4.5L38.6 10.4M3.5 -2.8L34.6 20M-1.8 4.1L28.3 28.3M4.4 -0.7L20 34.6M-3.6 2.7L10.4 38.6M4.2 1.6L0 40M-4.5 0.5L-10.4 38.6M2.8 3.5L-20 34.6M-4.1 -1.8L-28.3 28.3M0.7 4.4L-34.6 20M-2.7 -3.6L-38.6 10.4M-1.6 4.2L-40 0M-0.5 -4.5L-38.6 -10.4M-3.5 2.8L-34.6 -20M1.8 -4.1L-28.3 -28.3M-4.4 0.7L-20 -34.6M3.6 -2.7L-10.4 -38.6M-4.2 -1.6L0 -40M4.5 -0.5L10.4 -38.6M-2.8 -3.5L20 -34.6M4.1 1.8L28.3 -28.3M-0.7 -4.4L34.6 -20M2.7 3.6L38.6 -10.4'/></svg>");
  --bike: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-5 0 42 20'><rect x='-5' width='42' height='20' fill='%23fff'/><g fill='none' stroke='%2379adfa' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><circle cx='7' cy='13' r='6'/><circle cx='25' cy='13' r='6'/><path d='M7 13L14 13L11.5 6ZM11.5 6L22.3 6.5M14 13L22.6 8M21.8 4.5L25 13M21.8 4.5L21.5 3L24 3M11.5 6L11 4M9.5 4L13 4'/></g></svg>");
  --node: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 11'><rect x='.5' y='.5' width='10' height='10' rx='2.5' fill='%23fff' stroke='%23c3cad4'/><rect x='4' y='4' width='3' height='3' rx='.5' fill='%2379adfa'/></svg>");

  --radius: .5rem;
  --radius-sm: .375rem;
  --shadow-sm: 0 1px 3px rgb(15 23 42 / .08), 0 1px 2px rgb(15 23 42 / .05);
  --shadow-md: 0 12px 28px -14px rgb(15 23 42 / .25);
  /* The column, and the padding inside its frame. */
  --wrap: 72rem;
  --pad: clamp(1.25rem, 3.5vw, 2.5rem);
  --main-pad-top: clamp(1.75rem, 4vw, 3rem);
  /* The rails: bands in the side margins, up to --rail-max wide, 1.5rem short of the window's edge,
     so the dashed line that closes them off is always in view. chain.js hangs a chain down the
     middle of each. --dot-gap is the footer's dot grid. */
  --dot-gap: 16px;
  --rail-max: 10rem;

  font-family: var(--font-sans);
  line-height: 1.65;
  color: var(--foreground);
  background: var(--background);
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

* { box-sizing: border-box; }
/* Always reserve the scrollbar gutter, so pages don't shift sideways when one appears. */
html { scrollbar-gutter: stable; scroll-behavior: smooth; }
/* The footer stays at the bottom of the window on short pages. */
body { margin: 0; font-size: calc(1rem * var(--font-scale)); display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1 0 auto; display: flex; flex-direction: column; }
::selection { background: var(--brand-200); color: var(--brand-950); }
html { accent-color: var(--brand-600); }

img { max-width: 100%; height: auto; }
a { color: var(--brand-700); text-decoration: none; }
a:hover { color: var(--brand-800); text-decoration: underline; text-underline-offset: .15em; }
.prose a:not(.button) { text-decoration: underline; text-decoration-thickness: 1px; text-decoration-color: rgb(48 103 184 / .4); text-underline-offset: .18em; }
.prose a:not(.button):hover { text-decoration-color: currentColor; }
:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 3px; }

/* Visible only to screen readers (e.g. the mobile menu button's label). */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* Keyboard users get to the content without tabbing through the menu. */
.skip-link {
  position: absolute; left: -100vw; top: 0; z-index: 100;
  background: var(--background); color: var(--brand-700);
  padding: .6rem 1rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md); font-weight: 600;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* ----------------------------------------------------------------- column + rails */

/* One content column, shared by the header, the main text and the footer. Once the window is wider
   than the column, its edges are drawn as hairlines, so the three stack into one frame that runs
   from the top of the page to the bottom. */
.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--pad); border-inline: 1px solid transparent; }
@media (min-width: 74rem) {
  .wrap { border-inline-color: var(--border); }
  .site-footer .wrap { border-inline-color: rgb(255 255 255 / .07); }
}
main > .wrap { flex: 1 0 auto; padding-top: var(--main-pad-top); }

/* The rails: an open band on each side of the column, fixed to the window (the page scrolls over
   them, like a cutting mat), closed off on the outside by a dashed line. */
body::before, body::after {
  content: ""; position: fixed; top: 0; bottom: 0; z-index: -2; pointer-events: none;
  width: clamp(0px, calc((100% - var(--wrap)) / 2 - 1.5rem), var(--rail-max));
  --rail-dash: linear-gradient(var(--line-strong) 50%, transparent 0);
}
body::before { right: calc((100% + var(--wrap)) / 2); background: var(--rail-dash) left top / 1px 12px repeat-y; }
body::after { left: calc((100% + var(--wrap)) / 2); background: var(--rail-dash) right top / 1px 12px repeat-y; }

/* The hanging chains (chain.js): a canvas fixed behind everything but the rails, so the header,
   cards and the footer all pass in front of them. */
.bg-chain { position: fixed; inset: 0; z-index: -1; width: 100%; height: 100%; pointer-events: none; }
/* The subscribers' easter egg (chain.js): while the chains are tripping, this brings them back. */
.chain-normal { position: fixed; left: 50%; bottom: 1.25rem; z-index: 60; translate: -50% 0; font: inherit; font-weight: 700; cursor: pointer; }
.chain-normal[hidden] { display: none; }
@media print { .bg-chain, .chain-normal, body::before, body::after { display: none; } }

/* Nodes: where a horizontal rule meets the frame, a small square sits on the crossing. Each rule
   gets them from one pseudo-element laid along it, 6px past both ends, with a node at each end. */
.header-inner::after, .page-head::after, .hero::after, :is(.wrap > section + section:not(.prose), .back-to-top)::before {
  content: none; position: absolute; left: -6px; right: -6px; height: 11px; pointer-events: none;
  background: var(--node) left center / 11px 11px no-repeat, var(--node) right center / 11px 11px no-repeat;
}
@media (min-width: 74rem) {
  .header-inner::after, .page-head::after, .hero::after, :is(.wrap > section + section:not(.prose), .back-to-top)::before { content: ""; }
}

/* A rule across the whole column: the breaks between sections, and the "back to top" row. */
:is(.wrap > section + section:not(.prose), .back-to-top) { position: relative; margin-inline: calc(-1 * var(--pad)); padding-inline: var(--pad); border-top: 1px solid var(--border); }
:is(.wrap > section + section:not(.prose), .back-to-top)::before { top: -6px; }

/* ---------------------------------------------------------------- header + menu */

/* White bar that stays put while the page scrolls, with a hairline rule under it. */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: rgb(255 255 255 / .9);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
}
.header-inner { position: relative; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem 1rem; min-height: 4.5rem; }
.header-inner::after { bottom: -6px; }

.brand { display: inline-flex; align-items: center; gap: .7rem; color: var(--foreground); font-weight: 750; letter-spacing: -.025em; font-size: clamp(calc(1rem * var(--font-scale)), 4.4vw, calc(1.1rem * var(--font-scale))); line-height: 1.1; }
.brand:hover { color: var(--brand-700); text-decoration: none; }
.brand-mark { display: block; width: 2.25rem; height: 2.25rem; }

.main-nav { margin-inline-start: auto; }
.nav-list { display: flex; flex-wrap: wrap; align-items: center; gap: .1rem; list-style: none; margin: 0; padding: 0; }
.nav-list a { position: relative; display: block; padding: .5rem .75rem; border-radius: var(--radius-sm); color: var(--ink-700); font-weight: 550; font-size: calc(.8rem * var(--font-scale)); transition: background-color 150ms ease, color 150ms ease; }
.nav-list a:hover { color: var(--foreground); background: var(--muted); text-decoration: none; }
.nav-list a[aria-current="page"] { color: var(--brand-700); }
/* Under each item a short lane marking (theme-colour dashes on a pale rule): faint on hover, full on
   the current page. */
.nav-list a::after { content: ""; position: absolute; left: .75rem; right: .75rem; bottom: .1rem; height: 3px; background: linear-gradient(90deg, var(--brand-500) 0 62.5%, var(--brand-200) 0) 0 0 / .75rem 100% repeat-x; opacity: 0; transition: opacity 150ms ease; }
.nav-list a:hover::after { opacity: .45; }
.nav-list a[aria-current="page"]::after { opacity: 1; }
/* Seven menu items is a lot: below the widest screens they tuck in a little so the
   whole bar still fits on one row next to the wordmark. */
@media (max-width: 68rem) {
  .nav-list a { padding: .45rem .6rem; font-size: calc(.78rem * var(--font-scale)); }
  .nav-list a::after { left: .6rem; right: .6rem; }
}

/* The menu button only exists on narrow screens, and only when JavaScript can drive it
   (templates.mjs adds `has-js` to <html>); without it the list wraps instead of hiding. */
.nav-toggle {
  display: none; align-items: center; gap: .4rem;
  margin-inline-start: auto;
  padding: .45rem .7rem; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--background); color: var(--foreground); font: inherit; font-weight: 600;
  font-size: calc(.9rem * var(--font-scale)); cursor: pointer;
}
.nav-toggle:hover { background: var(--muted); }
.nav-toggle-icon { display: block; width: 1.35rem; height: 1.35rem; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }

/* Small phones: shrink the wordmark and the mark so the brand and the menu button
   share one row instead of wrapping. */
@media (max-width: 26rem) {
  .brand { gap: .5rem; font-size: calc(1rem * var(--font-scale)); }
  .brand-mark { width: 2rem; height: 2rem; }
}

@media (max-width: 62rem) {
  .has-js .nav-toggle { display: inline-flex; }
  .has-js .main-nav {
    position: absolute; left: 0; right: 0; top: 100%;
    margin: 0; background: var(--background);
    border-bottom: 1px solid var(--border); box-shadow: var(--shadow-md);
    display: none;
  }
  .has-js .main-nav.is-open { display: block; }
  .has-js .nav-list { flex-direction: column; align-items: stretch; gap: 0; padding: .5rem .75rem .9rem; max-width: var(--wrap); margin-inline: auto; }
  .has-js .nav-list a { padding: .7rem .85rem; border-radius: var(--radius-sm); font-size: calc(.95rem * var(--font-scale)); }
  .has-js .nav-list a:hover { background: var(--muted); }
  .has-js .nav-list a[aria-current="page"] { background: var(--brand-50); }
  .has-js .nav-list a::after { content: none; }
}

/* ------------------------------------------------------------------ main + text */

h1, h2, h3 { line-height: 1.15; letter-spacing: -.02em; font-weight: 700; text-wrap: balance; }
h1 { font-size: calc(1.9rem * var(--font-scale)); margin: 0 0 .75rem; letter-spacing: -.035em; }
section { margin-block: 3rem; }
/* A page heading is followed by its first section: keep the gap tight. */
h1 + section { margin-top: 2.25rem; }
.wrap > :first-child { margin-top: 0; }

/* Small labels (dates, bylines, the "All …" links) are set in monospace capitals, like the
   annotations on a drawing. */
time, .meta, .section-head a, .back-to-top {
  font-family: var(--font-mono); font-size: calc(.7rem * var(--font-scale)); font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted-foreground);
}

/* Section headings in the listing pages; on the home page the "All …" link sits on the same row. */
section:not(.prose) > h2, .section-head h2 { font-size: calc(1.4rem * var(--font-scale)); margin: 0 0 1.5rem; }
.section-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: .5rem 1rem; margin-bottom: 1.5rem; }
.section-head h2 { margin: 0; }
/* Section headings in the listing pages sit on a short lane marking. */
section:not(.prose) > h2::after, .section-head h2::after, .hero h1::after {
  content: ""; display: block; width: 3.3rem; height: 3px; margin-top: .6rem;
  background: var(--lane) 0 0 / 1.1rem 100% repeat-x;
}
/* The "All …" link is a small outlined tag. */
.section-head a { display: inline-flex; align-items: center; gap: .4rem; padding: .4rem .7rem; border: 1px solid var(--border); border-radius: var(--radius-sm); color: var(--ink-700); background: var(--background); transition: border-color 150ms ease, color 150ms ease, background-color 150ms ease; }
.section-head a:hover { border-color: var(--brand-300); color: var(--brand-700); background: var(--brand-50); text-decoration: none; }

/* Sections after the first are divided by a rule across the column, with nodes at the frame. */
.wrap > section + section:not(.prose) { margin-top: 3.5rem; padding-top: 3rem; }

.lead-text { color: var(--muted-foreground); font-size: calc(1.08rem * var(--font-scale)); line-height: 1.55; margin: .5rem 0 0; max-width: 44rem; }

/* Home hero: it fills the column from frame to frame and meets the header. With a photo, the photo
   is a long banner across the top of the column and the title, the description and the buttons sit
   under it. */
.hero {
  position: relative; isolation: isolate;
  margin-inline: calc(-1 * var(--pad)); padding: clamp(2.5rem, 6vw, 4.5rem) var(--pad);
  border-bottom: 1px solid var(--border);
}
.wrap > .hero:first-child { margin-top: calc(-1 * var(--main-pad-top)); }
.hero::after { bottom: -6px; }
.hero + section { margin-top: clamp(2.25rem, 5vw, 3.5rem); }
.hero h1 { font-size: clamp(calc(2rem * var(--font-scale)), calc(5vw * var(--font-scale)), calc(3rem * var(--font-scale))); letter-spacing: -.045em; line-height: 1; margin: 0; }
.hero h1::after { width: 6.6rem; height: 4px; margin-top: 1.25rem; background-size: 1.65rem 100%; }
.hero .lead-text { margin-top: 1.25rem; }
.hero-actions { display: flex; flex-wrap: wrap; gap: .9rem; margin: 2rem 0 0; }
/* The photo: a long banner running frame to frame, meeting the header's rule, with a hairline under
   it. It shows the whole width of the shot (the home page's picture is a wide one). */
.hero-media { position: relative; aspect-ratio: 2.6 / 1; border-bottom: 1px solid var(--border); }
.hero-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 55%; }
.hero-image { padding: 0; }
.hero-image .hero-content { padding: clamp(2.5rem, 5vw, 4rem) var(--pad) clamp(2.75rem, 5.5vw, 4.5rem); }
/* Narrow screens: a taller crop, so the banner does not become a slit. */
@media (max-width: 52rem) {
  .hero-media { aspect-ratio: 16 / 9; }
}

/* Page and post titles sit in a tinted band that fills the column and meets the header, with a rule
   under it. Its backdrop (.page-head-bg, an empty element so it can be clipped to the band while the
   nodes hang over the band's edges) carries the wheel. */
.page-head {
  position: relative; isolation: isolate;
  margin-inline: calc(-1 * var(--pad)); padding: clamp(2.25rem, 5vw, 3.5rem) var(--pad);
  margin-bottom: clamp(2rem, 4vw, 3rem);
  background: var(--brand-50);
  border-bottom: 1px solid var(--border);
}
.wrap > .page-head:first-child { margin-top: calc(-1 * var(--main-pad-top)); }
.page-head::after { bottom: -6px; }
.page-head-bg { position: absolute; inset: 0; z-index: -1; overflow: hidden; }
.page-head h1 { margin: 0; max-width: 46rem; font-size: clamp(calc(1.75rem * var(--font-scale)), calc(4.5vw * var(--font-scale)), calc(2.4rem * var(--font-scale))); }
.page-head .meta { margin: 0 0 .9rem; }
.page-head .meta a { color: var(--brand-700); }
.page-head .lead-text { margin-top: .75rem; }
.page-head + section { margin-top: 0; }
/* A big spoked wheel, cropped by the band, rolls in to the right-hand end of it as the page opens.
   (translate centres it; transform is left to the animation, which rolls it: 10rem of travel on a
   19rem wheel is 60° of turn, anticlockwise as it moves left.) */
.page-head-bg::after {
  content: ""; position: absolute;
  top: 50%; right: -2rem; width: 19rem; aspect-ratio: 1;
  translate: 0 -50%;
  background: var(--wheel) center / contain no-repeat;
  animation: wheel-roll-in 1.1s cubic-bezier(.2, .7, .25, 1) both;
}
@keyframes wheel-roll-in { from { transform: translateX(10rem) rotate(60deg); opacity: 0; } }
@media (max-width: 40rem) {
  .page-head-bg::after { width: 14rem; right: -4.5rem; opacity: .75; }
}

/* Lead images (a page's `image:`, and the front page banner) read as one card. */
img.lead { display: block; width: 100%; margin-block: 1.5rem; border-radius: var(--radius); border: 1px solid var(--border); }

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

.events, .posts { list-style: none; margin: 0; padding: 0; }
/* Events: one table-like block, a row per event, the date in the theme colour. */
.events { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.events:not(:has(> li:not([hidden]))) { display: none; }
.event {
  position: relative;
  padding: 1.1rem 1.35rem 1.2rem 1.6rem;
  background: var(--background);
  transition: background-color 150ms ease;
}
.event:not([hidden]) ~ .event { border-top: 1px solid var(--border); }
/* Down the left edge of each row, a lane marking on end. */
.event::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: linear-gradient(var(--brand-500) 0 62.5%, var(--brand-100) 0) 0 0 / 100% .9rem repeat-y; }
.event:hover { background: var(--brand-50); }
.event h3, .post-card h3 { margin: .35rem 0 .35rem; font-size: calc(1.1rem * var(--font-scale)); }
.event h3 a, .post-card h3 a { color: var(--foreground); }
.event h3 a:hover, .post-card h3 a:hover { color: var(--brand-700); text-decoration: none; }
.event p, .post-card p { margin: .35rem 0 0; }
.location { color: var(--muted-foreground); font-size: calc(.92rem * var(--font-scale)); }
.time-badge { display: inline-block; color: var(--brand-700); font-weight: 600; }
.empty { color: var(--muted-foreground); font-style: italic; }

/* The whole news item is the link: the title's link is stretched over it. */
.post-card { position: relative; }
.post-card h3 a::after { content: ""; position: absolute; inset: 0; }
.post-card p { color: var(--ink-700); }
.post-card time { font-variant-numeric: tabular-nums; }

/* News page: one item per row, the date in its own column, drawn as a ride route: a dashed line
   down the left with a stop on it for every post, from the newest at the top to the oldest.
   --stop is the stop's centre, level with the date's first line (padding + .2rem + half a line). */
.posts-list .post-card { --stop: 2.4rem; display: grid; grid-template-columns: 12rem 1fr; gap: .25rem 2rem; padding-block: 1.5rem; padding-inline-start: 2.75rem; }
.posts-list .post-card time { padding-top: .35rem; }
.posts-list .post-card h3 { margin-top: 0; }
.posts-list .post-card:hover h3 a { color: var(--brand-700); }
.posts-list .post-card::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: calc(.55rem - 1.5px); width: 3px;
  background: linear-gradient(var(--brand-300) 0 60%, transparent 0) 0 0 / 100% .85rem repeat-y;
}
.posts-list .post-card:first-child::before { top: var(--stop); }
.posts-list .post-card:last-child::before { bottom: calc(100% - var(--stop)); }
.posts-list .post-card::after {
  content: ""; position: absolute; left: 0; top: calc(var(--stop) - .55rem); width: 1.1rem; height: 1.1rem;
  box-sizing: border-box; border: 3px solid var(--brand-500); border-radius: 50%; background: var(--background);
  transition: background-color 150ms ease, box-shadow 150ms ease;
}
.posts-list .post-card:hover::after { background: var(--brand-500); box-shadow: 0 0 0 4px var(--brand-100); }
@media (max-width: 40rem) {
  .posts-list .post-card { --stop: 2.2rem; grid-template-columns: 1fr; }
  .posts-list .post-card time { padding-top: 0; }
}

/* Home page: the latest few as one block of cells that share their hairlines (the 1px gap shows
   the list's border colour through), with a node on each outer corner. */
.posts-grid {
  position: relative;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: 1px;
  background: var(--border); border: 1px solid var(--border);
}
.posts-grid::after {
  content: ""; position: absolute; inset: -6px; pointer-events: none;
  background: var(--node) 0 0 / 11px 11px no-repeat, var(--node) 100% 0 / 11px 11px no-repeat, var(--node) 0 100% / 11px 11px no-repeat, var(--node) 100% 100% / 11px 11px no-repeat;
}
.posts-grid .post-card {
  display: flex; flex-direction: column;
  padding: 1.6rem 1.6rem 1.75rem;
  background: var(--lane) 0 0 / 2rem 3px repeat-x, var(--background);
  transition: background-color 200ms ease, background-position 600ms ease;
}
/* On hover the cell tints, its lane marking rolls forward by one dash and the arrow moves on. */
.posts-grid .post-card:hover { background-color: var(--brand-50); background-position: 2rem 0, 0 0; }
.posts-grid .post-card:hover h3 a { color: var(--brand-700); }
.posts-grid .post-card h3 { margin-top: .75rem; line-height: 1.3; }
.posts-grid .post-card::after {
  content: "→"; margin-top: auto; padding-top: 1.25rem; color: var(--brand-600); font-weight: 600;
  transition: translate 200ms ease;
}
.posts-grid .post-card:hover::after { translate: .35rem 0; }

/* The human-readable sitemap: every page and post as one plain list. */
.sitemap { list-style: none; margin: 0; padding: 0; }
.sitemap li { padding-block: .45rem; border-bottom: 1px solid var(--border); }
.sitemap li:last-child { border-bottom: 0; }
.sitemap li time { display: inline-block; min-width: 11.5rem; margin-right: .5rem; }

/* ------------------------------------------------------------------------ prose */

.prose { max-width: 46rem; }
.prose h2 { margin-top: 2.5rem; font-size: calc(1.3rem * var(--font-scale)); }
.prose h3 { margin-top: 1.75rem; font-size: calc(1.1rem * var(--font-scale)); }
.prose p { margin-block: 1rem; }
.prose ul, .prose ol { padding-inline-start: 1.4rem; }
.prose li { margin-block: .35rem; }
.prose li::marker { color: var(--brand); }
/* Bullets are little wheels: a ring in the theme colour, centred on the first line. */
.prose ul { list-style: none; padding-inline-start: 1.6rem; }
.prose ul > li { position: relative; }
.prose ul > li::before {
  content: ""; position: absolute; left: -1.35rem; top: calc(.825em - .3em);
  width: .6em; height: .6em; box-sizing: border-box;
  border: 2px solid var(--brand-500); border-radius: 50%;
}
/* A section break is a dashed lane with a bicycle riding in the middle of it. */
.prose hr {
  height: 1.5rem; border: 0; margin-block: 2.25rem;
  background:
    var(--bike) center / 3.15rem 1.5rem no-repeat,
    linear-gradient(90deg, var(--brand-200) 0 60%, var(--brand-100) 0) 0 50% / 1.25rem 2px repeat-x;
}
.prose blockquote { margin: 1.5rem 0; padding: .25rem 0 .25rem 1.25rem; border-inline-start: 3px solid var(--brand); color: var(--muted-foreground); }
.prose img:not(.lead) { display: block; max-height: 28rem; width: auto; margin-block: 1.5rem; border-radius: var(--radius); border: 1px solid var(--border); }
.prose code { background: var(--muted); border: 1px solid var(--border); border-radius: .3rem; padding: .1rem .35rem; font-family: var(--font-mono); font-size: .88em; }
.prose pre { background: var(--muted); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 1rem; overflow-x: auto; }
.prose pre code { background: none; border: 0; padding: 0; }
.prose table { width: 100%; border-collapse: collapse; margin-block: 1.5rem; }
.prose th, .prose td { border: 1px solid var(--border); padding: .5rem .75rem; text-align: start; }

/* ---------------------------------------------------------------------- buttons */

/* A paragraph that is only a bold link (see build.mjs) renders as a button.
   Buttons are the darkest theme shade with light theme blue as text, in a fat ring of the theme
   colour at low opacity: no border, so the ring blends into whatever is behind it (pale blue on
   white, a blue haze on graph paper). */
.button-row { margin-block: 1.75rem; }
.button {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .75rem 1.5rem;
  background: var(--brand-950); color: var(--brand-400);
  border: 0; border-radius: var(--radius-sm);
  font-weight: 650; letter-spacing: .005em; line-height: 1.2;
  box-shadow: 0 0 0 5px rgb(121 173 250 / .25);
  transition: background-color 150ms ease, color 150ms ease, box-shadow 150ms ease;
}
.button:hover {
  background: var(--brand-900); color: var(--brand-300); text-decoration: none;
  box-shadow: 0 0 0 5px rgb(121 173 250 / .45);
}
.button:active { box-shadow: 0 0 0 3px rgb(121 173 250 / .45); }

/* ------------------------------------------------------------------- media blocks */

/* Embedded videos (see videoEmbedSrc in build.mjs): a dashed frame, like a slot on a drawing. */
.embed { background: var(--background); border: 1px dashed var(--line-strong); border-radius: var(--radius); padding: .5rem; margin-block: 1.25rem; }
.embed iframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; border-radius: var(--radius-sm); }

/* Home: a video at the top of the body sits beside the text, like the old site. */
.home-body { max-width: none; }
@media (min-width: 52rem) {
  .home-body:has(> .embed:first-child) { display: grid; grid-template-columns: 1.05fr 1fr; column-gap: 3rem; align-items: start; }
  .home-body:has(> .embed:first-child) > .embed { grid-row: 1 / span 20; margin-top: 0; }
  .home-body:has(> .embed:first-child) > :not(.embed) { grid-column: 2; }
  .home-body:has(> .embed:first-child) > :nth-child(2) { margin-top: 0; }
}

/* Mailing-list signup form (see newsletterForm in templates.mjs). */
.newsletter-embed {
  display: flex; flex-direction: column; align-items: center; gap: .75rem;
  width: 100%; margin-block: 1.75rem;
  padding: 1.25rem; background: var(--brand-50);
  border: 1px dashed var(--line-strong); border-radius: var(--radius);
}
.newsletter-embed iframe { width: 540px; max-width: 100%; height: 405px; border: 0; background: var(--background); border-radius: var(--radius-sm); }
.newsletter-fallback { margin: 0; font-size: calc(.9rem * var(--font-scale)); color: var(--muted-foreground); }

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

/* "Back to top": the last row of the column, under a rule with nodes at the frame. */
.back-to-top { margin-top: 4.5rem; margin-bottom: 0; padding-block: 1.1rem; }
.back-to-top a { color: var(--muted-foreground); }
.back-to-top a::before { content: "↑ "; }
.back-to-top a:hover { color: var(--brand-700); text-decoration: none; }

/* Dark footer: the brand and social buttons, the menu again, then the copyright line, on a faint dot
   grid. The footer is the road: a dashed lane marking runs along its top edge. */
.site-footer {
  background:
    var(--lane-dark) 0 0 / 3.5rem 4px repeat-x,
    radial-gradient(circle, rgb(121 173 250 / .14) 1px, transparent 1.5px) 0 0 / var(--dot-gap) var(--dot-gap),
    var(--brand-950);
  color: rgb(255 255 255 / .7); font-size: calc(.9rem * var(--font-scale));
}
.site-footer a { color: var(--brand-100); }
.site-footer a:hover { color: #fff; }
.footer-main { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2rem 3rem; padding-block: 3.5rem 3rem; }
.footer-brand .brand { color: #fff; }
.footer-brand .brand:hover { color: #fff; text-decoration: none; }
/* The logo's gear is dark grey, so on the navy it sits on a white tile. */
.footer-brand .brand-mark { width: 2.75rem; height: 2.75rem; padding: .2rem; background: #fff; border-radius: var(--radius-sm); }
.footer-brand .social { margin-top: 1.25rem; }
.footer-nav ul { list-style: none; margin: 0; padding: 0; columns: 2; column-gap: 3.5rem; }
.footer-nav li { padding-block: .3rem; break-inside: avoid; }
.footer-bottom { font-family: var(--font-mono); font-size: calc(.7rem * var(--font-scale)); letter-spacing: .06em; text-transform: uppercase; color: rgb(255 255 255 / .5); }
.footer-bottom p { margin: 0; padding-block: 1.25rem; border-top: 1px solid rgb(255 255 255 / .1); }

/* Social icon buttons: inline SVG brand marks, each filled with its network's own colour
   (the colour is set inline on the link, so `currentColor` is the brand). */
.social { display: flex; list-style: none; margin: 0; padding: 0; gap: .6rem; }
.social a { display: block; padding: .45rem; border-radius: var(--radius-sm); background: #fff; transition: box-shadow 150ms ease, transform 150ms ease; }
.social a:hover { text-decoration: none; transform: translateY(-1px); box-shadow: 0 6px 16px -6px rgb(0 0 0 / .5); }
.social svg { display: block; width: 1.25rem; height: 1.25rem; fill: currentColor; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
  .social a:hover, .posts-grid .post-card:hover::after { transform: none; translate: none; }
  .page-head-bg::after { animation: none; }
}
