/* doghut.org — quiet, editorial, unhurried. v3 */
/* Font/shape/width knobs live in plain :root so a per-site theme override
   (an inline <style> the server emits after this file) can restyle them. */
:root {
  --font-head: "Newsreader", Georgia, "Times New Roman", serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: "Cascadia Code", "SF Mono", Consolas, Menlo, monospace;
  --r-lg: 14px; --r-md: 10px; --r-pill: 20px; --r-avatar: 22px;
  --width: 760px;
  --shellw: 1120px;
  /* the statement band is near-black on the light palette; dark palettes lift
     it to a warm tone (+ hairline) so it stays a visible container */
  --band-bg: #161511; --band-ink: #f4f2ec; --band-line: transparent;
}
/* :root carries the light palette so the page is styled even if the inline
   theme script never runs (JS disabled); a no-JS dark-system visitor gets the
   dark palette via prefers-color-scheme. With JS, [data-theme] wins. */
:root, :root[data-theme="light"] {
  --bg: #faf9f7; --card: #f3f0e9; --ink: #26241f; --soft: #6e6c64;
  --accent: #5878a3; --accent-ink: #ffffff; --line: #e7e2d6; --shadow: 0 1px 3px rgba(35,30,20,.05);
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --bg: #16171a; --card: #1e2024; --ink: #e9e7e2; --soft: #9a978f;
    --accent: #8fb0d8; --accent-ink: #14151a; --line: #2c2e33; --shadow: 0 1px 3px rgba(0,0,0,.35);
    --band-bg: #2c2a23; --band-line: rgba(255, 255, 255, .09);
    color-scheme: dark;
  }
}
:root[data-theme="dark"] {
  --bg: #16171a; --card: #1e2024; --ink: #e9e7e2; --soft: #9a978f;
  --accent: #8fb0d8; --accent-ink: #14151a; --line: #2c2e33; --shadow: 0 1px 3px rgba(0,0,0,.35);
  --band-bg: #2c2a23; --band-line: rgba(255, 255, 255, .09);
  color-scheme: dark;
}

* { box-sizing: border-box; margin: 0; }
html { scroll-behavior: smooth; }
body {
  background: var(--bg); color: var(--ink);
  font: 16px/1.72 sans-serif; font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
  transition: background .25s, color .25s;
  min-height: 100vh; display: flex; flex-direction: column;
}
h1, h2, h3 { font-family: var(--font-head); font-optical-sizing: auto;
             font-weight: 500; line-height: 1.16; letter-spacing: -.012em; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }
::selection { background: var(--accent); color: var(--accent-ink); }

/* ---------------- header (full-width hairline; inner aligned to the content
   column like the footer; the theme toggle floats in the top-right corner) ---- */
header { border-bottom: 1px solid var(--line); position: relative; }
.hin {
  max-width: var(--width); margin: 0 auto; padding: 1.15rem 1.4rem;
  display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap;
}
.brand {
  font-family: var(--font-head); font-optical-sizing: auto; font-size: 1.32rem;
  font-weight: 500; color: var(--ink); margin-right: auto;
  display: flex; align-items: baseline; gap: .5rem;
}
.brand:hover { text-decoration: none; }
.brand span { color: var(--soft); font-size: .8rem; font-family: var(--font-body);
              letter-spacing: .04em; }
nav { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; }
nav a {
  color: var(--soft); font-size: .9rem; padding: .1rem 0;
  background: linear-gradient(currentColor, currentColor) 0 100% / 0 1.5px no-repeat;
  transition: color .18s, background-size .22s;
}
nav a:hover { color: var(--ink); text-decoration: none; background-size: 100% 1.5px; }
nav a.active { color: var(--ink); background: linear-gradient(var(--accent), var(--accent)) 0 100% / 100% 1.5px no-repeat; }
#themeToggle {
  position: absolute; top: 50%; right: 1.6rem; margin-top: -1.075rem;  /* half its height → vertically centered */
  background: none; border: 1px solid var(--line); border-radius: 50%;
  width: 2.15rem; height: 2.15rem; cursor: pointer; color: var(--soft); padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .18s, border-color .18s, transform .25s;
}
#themeToggle svg { display: block; }
#themeToggle:hover { color: var(--ink); border-color: var(--soft); transform: rotate(24deg); }

/* ---------------- layout ---------------- */
main { max-width: var(--width); width: 100%; margin: 0 auto; padding: 3rem 1.4rem 4.5rem; flex: 1; }
footer { border-top: 1px solid var(--line); }
.fin {
  /* aligned to the content column, not the header shell — the wide split read as a bug */
  max-width: var(--width); margin: 0 auto; padding: 1.6rem 1.4rem 2.2rem;
  color: var(--soft); font-size: .84rem;
  display: flex; gap: 1rem; align-items: baseline; flex-wrap: wrap;
}
.fin .totop { margin-left: auto; color: var(--soft); font-size: .82rem; }
.fin .totop:hover { color: var(--ink); text-decoration: none; }

/* ---------------- type & building blocks ---------------- */
.eyebrow {
  font-size: .73rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--soft); font-weight: 600; margin-bottom: 1rem;
}
.display {
  font-family: var(--font-head); font-optical-sizing: auto; font-weight: 500;
  font-size: clamp(2.4rem, 6.5vw, 3.4rem); line-height: 1.08; letter-spacing: -.018em;
}
.section-title {
  font-family: var(--font-body); font-size: .74rem; color: var(--soft);
  text-transform: uppercase; letter-spacing: .15em; font-weight: 600;
  margin: 2.8rem 0 1.1rem;
}
.band {
  background: var(--band-bg); color: var(--band-ink);
  border: 1px solid var(--band-line);
  border-radius: calc(var(--r-lg) + 8px);
  padding: clamp(2.4rem, 7vw, 4.2rem) clamp(1.5rem, 5vw, 3.2rem);
  margin: 2.6rem 0; text-align: center;
}
.band h2, .band p { font-family: var(--font-head); font-optical-sizing: auto;
                    font-weight: 500; font-size: clamp(1.5rem, 4vw, 2.2rem); line-height: 1.28; }
.band a { color: inherit; text-decoration: underline; text-underline-offset: 4px; }

/* ---------------- home hero ---------------- */
.hero {
  display: flex; align-items: center; justify-content: space-between; gap: 2rem;
  margin: 1.4rem 0 3rem;
}
.hero-main { min-width: 0; }
.hero h1 { font-size: clamp(2.6rem, 8vw, 3.9rem); letter-spacing: -.02em; line-height: 1.05; }
.hero .tag {
  color: var(--soft); margin-top: .9rem; font-size: 1.12rem; max-width: 30em;
  font-family: var(--font-head); font-optical-sizing: auto; font-style: italic;
}
.hero-avatar { width: 118px; height: 118px; border-radius: var(--r-avatar);
               box-shadow: var(--shadow); flex: none; }

/* ---------------- prose (rich text) ---------------- */
/* break-word: an unbroken run (a pasted URL, a long identifier) wraps instead of
   dragging the whole page sideways — surfaced by the 2026-07-18 markdown audit */
.prose { font-size: 1.04rem; overflow-wrap: break-word; }
.prose p { margin: 0 0 1.1rem; }
.prose h2 { font-size: 1.55rem; margin: 2.1rem 0 .8rem; }
.prose h3 { font-size: 1.18rem; margin: 1.6rem 0 .55rem; }
.prose img { max-width: 100%; border-radius: var(--r-md); }
.prose blockquote {
  border-left: 2px solid var(--accent); margin: 1.3rem 0;
  padding: .15rem 0 .15rem 1.15rem; color: var(--soft);
  font-family: var(--font-head); font-optical-sizing: auto; font-size: 1.1rem;
}
.prose pre {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: .9rem 1.05rem; overflow-x: auto; font-size: .87rem;
  /* wrap long code lines instead of growing a scroll rail — on a website a
     sideways-scrolling box reads worse than a wrapped line */
  white-space: pre-wrap; overflow-wrap: break-word;
}

/* ---------------- markdown output (inside .prose) ---------------- */
/* Server-rendered markdown: project pages, sub-pages and markdown blocks. Uses
   only theme variables, so every preset + dark mode come along for free. */
.prose > :first-child { margin-top: 0; }
.prose h1 { font-size: 1.95rem; margin: 2.4rem 0 .85rem; }
.prose h4 { font-family: var(--font-head); font-optical-sizing: auto; font-weight: 600;
            font-size: 1.02rem; margin: 1.4rem 0 .45rem; }
.prose hr { border: 0; border-top: 1px solid var(--line); margin: 2.2rem 0; }
.prose strong { font-weight: 600; color: var(--ink); }
.prose code {
  font-family: var(--font-mono); font-size: .87em;
  background: var(--card); border: 1px solid var(--line); border-radius: 5px;
  padding: .1em .36em; word-break: break-word;
}
.prose pre { font-family: var(--font-mono); line-height: 1.6; margin: 1.4rem 0; }
.prose pre code { background: none; border: 0; padding: 0; font-size: .87rem; word-break: normal; }
.prose .footnote { border-top: 1px solid var(--line); margin-top: 2.4rem; padding-top: .4rem;
                   color: var(--soft); font-size: .88rem; }
.prose .footnote ol { margin-left: 1.1rem; }
.prose sup a { text-decoration: none; }

/* ---------------- lists (rich text + markdown, Google-Docs depth markers) ------
   Markers change with nesting depth, exactly like Google Docs:
     bullets  disc -> circle -> square -> (repeat)
     numbers  1. -> a. -> i. -> (repeat)
   Everything below is variable-driven, so presets + dark mode come along. */
.prose ul, .prose ol { margin: 0 0 1.1rem; padding-left: 1.6rem; }
.prose li { margin: .3rem 0; }
.prose li > ul, .prose li > ol { margin: .35rem 0 .35rem; padding-left: 1.5rem; }
.prose li::marker { color: var(--soft); }

.prose ul                      { list-style-type: disc; }
.prose ul ul                   { list-style-type: circle; }
.prose ul ul ul                { list-style-type: square; }
.prose ul ul ul ul             { list-style-type: disc; }
.prose ul ul ul ul ul          { list-style-type: circle; }
.prose ul ul ul ul ul ul       { list-style-type: square; }

.prose ol                      { list-style-type: decimal; }
.prose ol ol                   { list-style-type: lower-alpha; }
.prose ol ol ol                { list-style-type: lower-roman; }
.prose ol ol ol ol             { list-style-type: decimal; }
.prose ol ol ol ol ol          { list-style-type: lower-alpha; }
.prose ol ol ol ol ol ol       { list-style-type: lower-roman; }

/* LEGACY FALLBACK — content saved before the Quill list normalizer existed.
   Quill 2 writes every list as a flat <ol> with data-list="bullet|ordered" on
   the <li> and class="ql-indent-N" for depth, and draws the markers from its
   own editor stylesheet (which this site never loads) — so bullets rendered as
   numbers. Saving that content again through /opensesame rewrites it into the
   real <ul>/<ol> nesting above and these rules stop applying; they exist so old
   content looks right without a re-save. Safe to delete once nothing uses it. */
/* `list-item 0` (not `none`) is what actually stops a bullet item from bumping
   the number counter of a legacy MIXED list */
.prose li[data-list="bullet"]  { list-style-type: disc; counter-increment: list-item 0; }
.prose li[data-list="ordered"] { list-style-type: decimal; }
.prose li[data-list="bullet"].ql-indent-1,
.prose li[data-list="bullet"].ql-indent-4,
.prose li[data-list="bullet"].ql-indent-7 { list-style-type: circle; }
.prose li[data-list="bullet"].ql-indent-2,
.prose li[data-list="bullet"].ql-indent-5,
.prose li[data-list="bullet"].ql-indent-8 { list-style-type: square; }
.prose li[data-list="ordered"].ql-indent-1,
.prose li[data-list="ordered"].ql-indent-4,
.prose li[data-list="ordered"].ql-indent-7 { list-style-type: lower-alpha; }
.prose li[data-list="ordered"].ql-indent-2,
.prose li[data-list="ordered"].ql-indent-5,
.prose li[data-list="ordered"].ql-indent-8 { list-style-type: lower-roman; }
.prose li.ql-indent-1 { margin-left: 1.5rem; }
.prose li.ql-indent-2 { margin-left: 3rem; }
.prose li.ql-indent-3 { margin-left: 4.5rem; }
.prose li.ql-indent-4 { margin-left: 6rem; }
.prose li.ql-indent-5 { margin-left: 7.5rem; }
.prose li.ql-indent-6 { margin-left: 9rem; }
.prose li.ql-indent-7 { margin-left: 10.5rem; }
.prose li.ql-indent-8 { margin-left: 12rem; }
.prose li.ql-indent-9 { margin-left: 13.5rem; }
.prose li .ql-ui { display: none; }   /* Quill's editor-only marker span */

/* ---------------- rich-text spacing (.rt = written in the Quill editor) --------
   Quill's own editor draws paragraphs with NO margin, so pressing Enter there
   looks like a plain new line. The page used to add 1.1rem under every <p>, which
   silently turned each Enter into a blank line — you could not write two lines
   that sit together. Rich text now matches the editor: a paragraph is a line, and
   a blank line is a blank line (press Enter twice — Quill stores that as an empty
   paragraph, which occupies exactly one line here).

   Markdown is NOT .rt and keeps its spaced-paragraph rhythm: in markdown a blank
   line is already the only way to start a new paragraph, so nothing was ever
   forced there. Headings, lists, quotes and images keep their margins in both. */
.prose.rt p, .band.rt p { margin: 0; }
.prose.rt p:empty, .band.rt p:empty { min-height: 1.7em; }   /* a stripped-out empty line */
/* Headings, quotes and <pre> carry their own top margins, so a paragraph before
   them still gets a gap. Lists set only a BOTTOM margin (they relied on the
   paragraph's now-removed bottom margin), so restore the gap when a list follows
   a rich-text paragraph — otherwise the two touch. */
.prose.rt p + ul, .prose.rt p + ol { margin-top: 1.1rem; }

/* .spa = a rich-text block whose spacing mode is "auto": classic spaced
   paragraphs, but a touch tighter than the standard 1.1rem prose/markdown gap
   (per Ray). Bands get an explicit matching rhythm instead of the browser's
   default 1em top+bottom. */
.prose.spa p { margin: 0 0 .9rem; }
.band.spa p { margin: 0 0 .8em; }

/* tables: hairline rows, quiet zebra, and their own scroll rail on narrow screens */
.tablewrap { margin: 1.6rem 0; overflow-x: auto; -webkit-overflow-scrolling: touch;
             border: 1px solid var(--line); border-radius: var(--r-md); background: var(--card); }
.tablewrap table { border-collapse: collapse; width: 100%; min-width: 480px; font-size: .9rem; }
.tablewrap th, .tablewrap td {
  text-align: left; vertical-align: top; padding: .68rem .95rem;
  border-top: 1px solid var(--line); line-height: 1.55;
}
.tablewrap thead th {
  border-top: 0; border-bottom: 1px solid var(--soft);
  font-family: var(--font-body); font-weight: 600; font-size: .69rem;
  text-transform: uppercase; letter-spacing: .11em; color: var(--soft); white-space: nowrap;
}
.tablewrap tbody tr:nth-child(even) { background: color-mix(in srgb, var(--ink) 3.5%, transparent); }
.tablewrap td p { margin: 0 0 .5rem; }
.tablewrap td p:last-child { margin-bottom: 0; }
.tablewrap code { background: var(--bg); }

/* ---------------- project pages ---------------- */
.phead { display: flex; align-items: center; justify-content: space-between; gap: 1.6rem; }
.phead-main { min-width: 0; }
.part {
  width: 108px; height: 108px; flex: none; border-radius: var(--r-lg);
  object-fit: cover; background-color: var(--card);
  border: 1px solid var(--line); box-shadow: var(--shadow);
  user-select: none; -webkit-user-select: none;
}
.ptags { margin-top: 1rem; display: flex; flex-wrap: wrap; gap: .35rem; }
.pmeta { margin: 1.8rem 0 2.2rem; }
.pmeta .v a { word-break: break-word; }
.pbody { margin-top: 1.4rem; }
.crumb { font-size: .88rem; margin-bottom: 1.4rem; }
.crumb a { color: var(--soft); }
.crumb a:hover { color: var(--accent); text-decoration: none; }
.pmore { margin-top: .9rem; font-size: .9rem; font-weight: 500; }
.pmore .arr { display: inline-block; transition: transform .18s; }
.pmore a:hover { text-decoration: none; }
.pmore a:hover .arr { transform: translateX(3px); }
a.ptitle { color: inherit; }
a.ptitle:hover { color: var(--accent); text-decoration: none; }
@media (max-width: 520px) {
  .phead { gap: 1rem; }
  .part { width: 76px; height: 76px; }
}

/* ---------------- cards ---------------- */
.card {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 1.4rem 1.5rem; margin-bottom: 1rem; box-shadow: var(--shadow);
  transition: border-color .18s, transform .18s;
}
.card h3 { font-size: 1.3rem; }
.card .meta { color: var(--soft); font-size: .82rem; margin-top: .3rem; }
.card p.desc { color: var(--ink); font-size: .95rem; margin-top: .6rem; }
.searchbar {
  display: block; width: 100%; max-width: 330px;
  margin: .2rem 0 1.1rem; padding: .48rem 1rem;
  font: inherit; font-size: 1rem; color: var(--ink);   /* under 16px, iOS zooms on focus */
  background: transparent; border: 1px solid var(--line); border-radius: var(--r-pill);
}
.searchbar:focus { outline: none; border-color: var(--accent); }
.searchbar::placeholder { color: var(--soft); }
a.cardlink { display: block; color: inherit; }
a.cardlink:hover { text-decoration: none; }
a.cardlink:hover .card, .card.hoverable:hover { border-color: var(--soft); transform: translateY(-2px); }
a.cardlink h3 { transition: color .18s; }
a.cardlink:hover h3 { color: var(--accent); }
.rowline { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.goarr { color: var(--soft); font-size: 1.15rem; transition: transform .18s, color .18s; flex: none; }
a.cardlink:hover .goarr { transform: translateX(4px); color: var(--accent); }

/* Anthropic-style hairline meta rows inside cards */
.mrows { margin-top: 1.1rem; font-size: .8rem; }
.mrow {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: .48rem 0; border-top: 1px solid var(--line);
}
.mrow .k { color: var(--soft); text-transform: uppercase; letter-spacing: .11em; font-size: .67rem;
           font-weight: 600; align-self: center; }
.mrow .v { color: var(--ink); }

/* ---------------- media cards ---------------- */
.mediacard { display: flex; gap: 1.3rem; align-items: flex-start; }
.mart {
  width: 92px; height: 92px; border-radius: var(--r-md); flex: none;
  object-fit: cover; background-color: var(--bg);
  border: 1px solid var(--line);
  user-select: none; -webkit-user-select: none;
}
.mart.ph { display: flex; align-items: center; justify-content: center; background: var(--bg); }
.mart.ph span { font-family: var(--font-head); font-optical-sizing: auto; font-size: 2.1rem;
                color: var(--soft); line-height: 1; }
.mbody { min-width: 0; flex: 1; }
.mbody .eyebrow { margin-bottom: .35rem; font-size: .66rem; }

/* ---------------- buttons & chips ---------------- */
.btn {
  display: inline-flex; align-items: center; gap: .45rem;
  background: var(--accent); color: var(--accent-ink);
  border: 0; border-radius: var(--r-md); padding: .58rem 1.1rem; font-size: .9rem;
  cursor: pointer; font-family: inherit; font-weight: 500;
  transition: opacity .15s, transform .15s;
}
.btn:hover { opacity: .9; text-decoration: none; transform: translateY(-1px); }
.btn.ghost { background: transparent; color: var(--ink); border: 1px solid var(--line); }
.btn.ghost:hover { border-color: var(--soft); }
.btn .arr { transition: transform .18s; }
.btn:hover .arr { transform: translateX(3px); }
.chip {
  /* tags are uncapped now, so a very long one WRAPS inside the pill. line-height
     1.4 spaces the wrapped lines (at the old 1, they nearly touched); the padding
     drop of .144rem per side (the added half-leading, (1.4-1)/2 × .72rem) keeps a
     single-line chip's box and text position EXACTLY as before:
     1 × .72 + .3 + .26 == 1.4 × .72 + .156 + .116. */
  display: inline-flex; align-items: center; font-size: .72rem; line-height: 1.4;
  border: 1px solid var(--line); color: var(--soft);
  border-radius: var(--r-pill); padding: .156rem .62rem .116rem;
  vertical-align: .18em; letter-spacing: .02em; font-family: var(--font-body); font-weight: 500;
}
/* a title with chips lays out as a wrapping flex row: gap spaces the chips on
   every line, so a chip that wraps starts flush with the title's left edge
   (inline margins used to indent it after the wrap) */
h3:has(> .chip) { display: flex; flex-wrap: wrap; align-items: baseline; gap: .71rem .45rem; }
/* optically center the pill on the title's capitals: baseline alignment alone leaves
   its box hanging ~.4rem below the baseline (level with the g/y/p descenders), and a
   full seat-on-the-baseline lift reads too high. -.26rem puts the pill's center on the
   cap-height midpoint — it overhangs the cap top and the baseline by the same few px.
   The nudge is visual only, so the enlarged row-gap keeps ~.45rem under wrapped rows. */
h3:has(> .chip) > .chip { position: relative; top: -.26rem; }

/* ---------------- tabs (projects) ---------------- */
.tabs { display: flex; gap: .5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.tabs button {
  background: transparent; color: var(--soft); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: .4rem 1.05rem; cursor: pointer;
  font-size: .88rem; font-family: inherit; transition: all .15s;
}
.tabs button:hover { color: var(--ink); border-color: var(--soft); }
.tabs button.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.attachments { margin-top: .9rem; display: flex; flex-wrap: wrap; gap: .5rem; }
.attachments a {
  border: 1px solid var(--line); border-radius: var(--r-md); padding: .34rem .78rem;
  font-size: .84rem; background: var(--bg); color: var(--ink); transition: border-color .15s;
}
.attachments a:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
.attachments a::before { content: "↓ "; color: var(--soft); }

/* ---------------- gallery ---------------- */
.gallery-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
  gap: .9rem; margin-top: 1.6rem;
}
.gallery-grid figure {
  margin: 0; border-radius: var(--r-md); overflow: hidden; position: relative;
  border: 1px solid var(--line); background: var(--card); cursor: zoom-in;
  transition: transform .18s, border-color .18s;
  line-height: 0;  /* no phantom strip under uncaptioned photos */
}
.gallery-grid figure:hover { transform: translateY(-2px); border-color: var(--soft); }
.gallery-grid .gimg {
  /* real <img> (lazy-loaded, srcset-sized), not a CSS background. No crop and
     NO author aspect-ratio since the justified layout (phase 25): the old
     1/1 square rule would force .jgal to re-declare 'auto', which wipes the
     width/height-attr ratio hint that reserves each box pre-load (P24 trap). */
  display: block; width: 100%; height: auto;
  background: var(--card);   /* quiet placeholder while a tile loads */
  user-select: none; -webkit-user-select: none;
}
/* Captions sit ON the photo rather than in a strip beneath it. That one move fixes
   all three things wrong with the old treatment: a long caption can no longer make
   its tile taller than the rest of its row, a photo with NO caption leaves no empty
   band behind (so an uncaptioned gallery looks deliberate instead of broken), and
   the photograph stays the thing being looked at. Two lines is the budget — the full
   caption, the description and the tags are all in the lightbox a click away. */
.gallery-grid figcaption {
  position: absolute; left: 0; right: 0; bottom: 0;
  /* Kept deliberately shallow (~43px). A justified row's tile height is
     (column − gaps) / Σaspect, and Σaspect is unbounded above — three 16:9 photos
     already give a ~129px tile, a panorama in the row can give ~74px. At the original
     two lines and 1.7rem of lead-in the caption ate 57% of the first and CLIPPED the
     second (overflow:hidden, no ellipsis, because the clamp never got to run). One
     line that always fits beats two lines that sometimes don't — the full caption,
     the description and the tags are all in the lightbox one click away. */
  padding: 1.05rem .7rem .5rem;
  font-size: .78rem; line-height: 1.45; color: #f4f2ee;
  background: linear-gradient(to top, rgba(10, 9, 8, .84), rgba(10, 9, 8, .5) 46%, transparent);
  text-shadow: 0 1px 3px rgba(0, 0, 0, .5);
  opacity: .92; transition: opacity .18s;
  /* the whole tile is the lightbox trigger; the caption must not eat the click */
  pointer-events: none;
}
/* The clamp lives on an INNER span, not on the figcaption. An absolutely positioned
   box has its display blockified, which turns -webkit-box into flow-root and drops
   -webkit-line-clamp on the floor — the caption then just gets sliced mid-line by
   overflow:hidden, with no ellipsis. Clamping a static child sidesteps that. */
.gallery-grid figcaption span {
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden;
}
.gallery-grid figure:hover figcaption { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .gallery-grid figcaption { transition: none; }
}
/* search bar above the grid, same control as blog/projects */
.gsearch { margin-top: 1.6rem; }
.gsearch + .gallery-grid { margin-top: .9rem; }
.gnone { margin-top: 1.4rem; }
/* JUSTIFIED gallery (phase 25): same math as content image rows — flex-grow =
   aspect ratio -> every photo in a row lands at the SAME height with its whole
   shape visible (no square crop). Rows are packed server-side to a target
   ratio sum so row heights stay consistent; a sparse tail row is size-capped
   via --gcap instead of stretching its photos to fill the line. */
.gallery-grid.jgal { display: block; }
.jgal .gjrow { display: flex; gap: .9rem; margin-bottom: .9rem; }
.jgal .gjrow:last-child { margin-bottom: 0; }
.jgal .gjrow figure { flex: 1 1 0%; min-width: 0; max-width: var(--gcap, none); }
/* no .jgal img override needed: the base .gimg rule is crop-free with
   height:auto, so the width/height attrs' UA aspect-ratio hint reserves each
   box before load (review catch — an author 'aspect-ratio: auto' would wipe it) */
@media (max-width: 620px) {
  /* phones: stack full-width — whole photos, no shrunken row slivers.
     max-width (not --gcap): an inline custom property would win over a
     media-query declaration of the same property. */
  .jgal .gjrow { flex-wrap: wrap; }
  .jgal .gjrow figure { flex-basis: 100%; max-width: none; }
}
.lightbox {
  position: fixed; inset: 0; background: rgba(12, 11, 9, .93); z-index: 40;
  display: none; align-items: center; justify-content: center; flex-direction: column;
  /* Tight on purpose. The caption block sits BELOW the photo in this column, so for a
     WIDE photo every pixel of vertical chrome costs its aspect ratio in width — a 2:1
     panorama loses two pixels across for each one spent here. At 1920x1080 maximized the
     old 2.2rem/.9rem left the photo at 84% of what a full-window photo viewer shows, and
     it was HEIGHT, not width, that bound. Don't restore the old values without redoing
     that measurement. (Kept clear of the ✕ at top:1.1rem: the widest a photo goes is
     .92vw, so its top-right corner stays left of the button.) */
  gap: .55rem; padding: 1.4rem; cursor: zoom-out;
}
.lightbox.open { display: flex; }
/* while the lightbox is open the page behind must not scroll (mobile swipes
   were dragging the whole site around under the overlay) */
html.lbopen, html.lbopen body { overflow: hidden; }
.lightbox .limg {
  /* Explicit centered width. The old rule used align-self:stretch + max-width, but a
     stretched flex item clamped by max-width gets pinned to flex-start (left) by the
     browser, which shoved the photo off-center. An explicit width lets the parent's
     align-items:center do its job. flex:0 1 82vh keeps the old shrink-to-fit height so
     a long caption/description can't push the image past the viewport.
     calc(100vw - 148px) reserves a real gutter for the side arrows (16px inset +
     46px button + breathing room per side) so they never sit on the photo. */
  /* --lbmax is set per photo by site.js: the width this photo is actually painted,
     already limited by the 82vh height below, by its own pixels / dpr (so the box
     never asks the browser to upscale) and by MIN_BOX. There is deliberately NO flat
     px cap here any more. One used to sit at 1600px and it only ever bit WIDE photos
     — a 4:3 photo is bounded by the 82vh height regardless, but a panorama is bounded
     by width, so it was pinned to 1600 CSS px however large the monitor was, and
     looked soft beside its original for that reason alone. Absent --lbmax (no JS, or
     a source the server couldn't probe) falls back to the old cap. */
  width: min(92vw, calc(100vw - 148px), var(--lbmax, 1600px)); max-width: 100%;
  /* 94vh is a CEILING the flex-shrink then trims to whatever the caption block leaves —
     it is not the height the photo gets. It was 82vh, chosen to approximate the leftover
     space; raising it lets an UNcaptioned photo actually use the room it has, while a
     captioned one still shrinks to fit exactly as before. BOX_VH in site.js mirrors this
     number and p30_shape asserts they agree. */
  flex: 0 1 94vh; min-height: 0;
  background-size: contain; background-repeat: no-repeat; background-position: center;
  user-select: none; -webkit-user-select: none;
  position: relative; overflow: hidden;   /* clips the two sliding image layers */
}
/* stacked layers inside .limg — site.js slides the old photo out and the new one
   in from the direction of travel (arrows, arrow keys, swipes) */
.lightbox .limg .lslide {
  position: absolute; inset: 0;
  background-size: contain; background-repeat: no-repeat; background-position: center;
  will-change: transform;
}
.lightbox .lcap { color: #d9d6cf; font-size: .9rem; text-align: center; }
.lightbox .ldesc { color: #a8a49b; font-size: .82rem; text-align: center; max-width: 640px;
                   line-height: 1.55; white-space: pre-line; }
.lightbox .ltags { display: flex; gap: .4rem; flex-wrap: wrap; justify-content: center; }
/* Same vertical recipe as the site's other tag chips (see the .ptags rule): explicit
   line-height plus padding that is .04rem heavier on TOP. With symmetric padding and
   `line-height: normal` — which is 1.72 for this font, not 1.2 — the glyphs' optical
   center landed ~0.9px above the pill's center, which reads as the text riding high.
   The chips elsewhere already carry this compensation; this pill was written separately
   and never got it. Keep the two in step. */
.lightbox .ltags span { border: 1px solid #4a463f; color: #b5b1a8; border-radius: var(--r-pill);
                        display: inline-flex; align-items: center; line-height: 1.4;
                        padding: .156rem .62rem .116rem; font-size: .72rem; }
.lightbox .lcap:empty, .lightbox .ldesc:empty, .lightbox .ltags:empty { display: none; }
.lightbox .lbtn {
  position: absolute; z-index: 2; width: 46px; height: 46px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(30, 28, 24, .6); color: #e8e5de;
  border: 1px solid #4a463f; border-radius: 50%;
  font: 26px/1 Georgia, serif; cursor: pointer;
  touch-action: manipulation;   /* rapid arrow taps must not double-tap-zoom on iOS */
}
/* hover only where hover exists — touch browsers keep the emulated :hover lit after a tap */
@media (hover: hover) {
  .lightbox .lbtn:hover { background: rgba(58, 54, 47, .85); color: #fff; }
}
.lightbox .lbtn:active { background: rgba(58, 54, 47, .85); }
.lightbox .lclose { top: 1.1rem; right: 1.1rem; font-size: 17px; }
.lightbox .lprev { left: 1rem; top: 50%; transform: translateY(-50%); padding-bottom: 4px; }
.lightbox .lnext { right: 1rem; top: 50%; transform: translateY(-50%); padding-bottom: 4px; }
@media (max-width: 620px) {
  /* phones: give the photo its full 92vw back and move the arrows to the
     bottom corners — mid-screen arrows collided with the image, and the
     corners are where thumbs already are. Still capped by --lbmax, because
     the box width IS the derivative's natural width, and overriding it would
     resample every photo on every phone. boxCss's mobile branch already keeps
     --lbmax at or below 92vw, so this never grows the box.
     KEEP THIS COMMENT'S OPENING MARKER INTACT: it was once lost in an edit, and
     a comment with only a closing marker silently ate the whole rule below it —
     CSS error recovery swallows the next rule with no warning anywhere. */
  .lightbox .limg { width: min(92vw, var(--lbmax, 92vw)); }
  .lightbox .lbtn { width: 42px; height: 42px; }
  .lightbox .lclose { top: .7rem; right: .7rem; }
  .lightbox .lprev, .lightbox .lnext {
    top: auto; transform: none;
    bottom: calc(.8rem + env(safe-area-inset-bottom, 0px));
  }
  .lightbox .lprev { left: .7rem; }
  .lightbox .lnext { right: .7rem; }
  /* keep the caption block off the corner arrows: inset the text and let a
     very long description scroll instead of filling the whole overlay */
  .lightbox .lcap, .lightbox .ldesc, .lightbox .ltags { padding: 0 3.6rem; }
  .lightbox .ldesc { max-height: 38vh; overflow-y: auto; overscroll-behavior: contain; }
}
.noselect { user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }

/* ---------------- page-builder blocks ---------------- */
.bh2 { font-size: 1.6rem; margin: 2.3rem 0 .85rem; }
.bh3 { font-size: 1.2rem; margin: 1.8rem 0 .55rem; }
.bblock { margin: 1.1rem 0; }
.bimg { margin: 1.8rem 0; }
/* height:auto is LOAD-BEARING with the width/height attrs (phase 24): without
   it the height attribute sticks while max-width shrinks the image — vertical
   distortion on anything wider than the column. The attrs still feed the UA's
   aspect-ratio hint, so the box is reserved before load. */
.bimg img { max-width: 100%; height: auto; border-radius: var(--r-lg); display: block; }
.bimg figcaption { color: var(--soft); font-size: .82rem; margin-top: .55rem; }
.bimgs { display: grid; gap: .9rem; margin: 1.8rem 0; }
.bimgs.n2 { grid-template-columns: repeat(2, 1fr); }
.bimgs.n3 { grid-template-columns: repeat(3, 1fr); }
.bimgs.n4 { grid-template-columns: repeat(2, 1fr); }
.bimgs figure { margin: 0; min-width: 0; }
/* no forced crop since the justified rows (phase 24) — height:auto keeps the
   height ATTR from sticking (distortion) while its aspect-ratio hint still
   reserves the box pre-load. Framed cells: the later .fitframe img rules win. */
.bimgs img { width: 100%; height: auto; border-radius: var(--r-md); display: block; }
.bimgs figcaption { color: var(--soft); font-size: .78rem; margin-top: .4rem; }
.bnote {
  color: var(--soft); font-size: .82rem; line-height: 1.6; white-space: pre-line;
  border-top: 1px solid var(--line); padding-top: .7rem; margin: 1.6rem 0 1.1rem;
}
.bbtns { display: flex; gap: .7rem; flex-wrap: wrap; margin: 1.5rem 0; }

/* ---------------- block alignment / sizing / framing (phase 22) ---------------- */
/* content caps are generous now — no free-text element may drag the page
   sideways. overflow-wrap inherits and only bites when a word can't fit its
   line, so this is a pure safety net; grid/flex cells additionally need
   min-width:0 or they refuse to shrink below their content. */
main { overflow-wrap: break-word; }
/* surfaces the <main> net can't reach (header brand) or where flex intrinsic
   sizing ignores break-word (hero, lightbox text): 'anywhere' shrinks the
   min-content size so even one giant unbroken word wraps instead of overflowing */
.brand { min-width: 0; overflow-wrap: anywhere; }
.hero-main { max-width: 100%; overflow-wrap: anywhere; }
.lightbox .lcap, .lightbox .ldesc { max-width: min(640px, 92vw); overflow-wrap: anywhere; }
/* lightbox tag pills too — gallery tags are uncapped now (phase 23), and a flex
   pill without these can neither shrink nor wrap: one long tag would run past
   both screen edges */
.lightbox .ltags { max-width: min(640px, 92vw); }
.lightbox .ltags span { max-width: 100%; overflow-wrap: anywhere; }
.brow .bname, .brow .bnote2, .brow .bpos { min-width: 0; }
/* overflow-wrap:anywhere (not break-word) so the flex min-content size can
   shrink — a one-word label longer than the column wraps inside the button.
   Same for card titles that became flex rows to wrap chips (phase 17), and
   for the chips themselves (handles/tags can be long now). */
.bbtns .btn { max-width: 100%; min-width: 0; overflow-wrap: anywhere; }
h3:has(> .chip) { min-width: 0; overflow-wrap: anywhere; }
.chip { max-width: 100%; overflow-wrap: anywhere; }
/* text-align variants for heading + note blocks */
.bh2.bal-c, .bh3.bal-c, .bnote.bal-c { text-align: center; }
.bh2.bal-r, .bh3.bal-r, .bnote.bal-r { text-align: right; }
/* button rows */
.bbtns.bal-c { justify-content: center; }
.bbtns.bal-r { justify-content: flex-end; }
/* the figure HUGS its image (so alignment moves the image and the caption
   follows it); framed blocks set an explicit inline width instead, since a
   fixed-aspect frame has no intrinsic width to hug */
.bimg { width: fit-content; max-width: 100%; }
.bimg.bal-c { margin-left: auto; margin-right: auto; }
.bimg.bal-r { margin-left: auto; margin-right: 0; }
/* the caption must FOLLOW the image's width, not drive the figure's fit-content
   sizing (width:0 + min-width:100% = zero intrinsic contribution, full resolved width) */
.bimg figcaption { width: 0; min-width: 100%; }
/* and the caption TEXT follows the block's alignment too */
.bimg.bal-c figcaption { text-align: center; }
.bimg.bal-r figcaption { text-align: right; }
/* capw set (.bcapw): caption unhooked from the image — figure spans the column,
   the image/frame and the caption are each placed by the bal classes, and the
   caption's inline width:N% is column-relative */
.bimg.bcapw { width: 100%; }
.bimg.bcapw figcaption { min-width: 0; }   /* release the width:0/min-width:100% hug hack */
.bimg.bcapw.bal-c img, .bimg.bcapw.bal-c .fitframe,
.bimg.bcapw.bal-c figcaption { margin-left: auto; margin-right: auto; }
.bimg.bcapw.bal-r img, .bimg.bcapw.bal-r .fitframe,
.bimg.bcapw.bal-r figcaption { margin-left: auto; }
.bimg.bcapw .fitframe img { margin: 0; }   /* the framed img is absolutely placed; no margins */
/* Quill per-paragraph alignment (the editor toolbar writes these classes) */
.ql-align-center { text-align: center; }
.ql-align-right { text-align: right; }
.ql-align-justify { text-align: justify; }
/* block images: same save/copy deterrents as the gallery */
.bimg img, .bimgs img { user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }
/* captions may hold deliberate line breaks now */
.bimg figcaption, .bimgs figcaption, .gallery-grid figcaption { white-space: pre-line; }
/* JUSTIFIED image rows (phase 24): flex row, each cell's flex-grow = its
   (relative-size x) aspect ratio -> widths distribute until every image in the
   row is the SAME height with its whole shape visible (no auto-crop). A framed
   cell participates with its framed shape instead. 4-image blocks are two
   exact pairs (.bimgs2r) — mixing flex-basis with grow would break the math. */
.jrow { display: flex; gap: .9rem; }
.jrow figure { flex: 1 1 0%; min-width: 0; margin: 0; }
.jrow .fitframe { border-radius: var(--r-md); }
/* block-level alignment: places a scaled-down (max-width < 100%) row in the
   column; horizontal margins only — the shorthand's 1.8rem stays vertical */
.bimgs.bal-c { margin-left: auto; margin-right: auto; }
.bimgs.bal-r { margin-left: auto; margin-right: 0; }
/* per-image caption alignment inside a row (relative to its own image). The
   auto margins also re-anchor a capw-NARROWED caption box to match (a no-op at
   full width) — without them a 50%-wide right-aligned caption would end its
   text at the image's middle, not its right edge (review catch). */
.bimgs figcaption.cal-c { text-align: center; margin-left: auto; margin-right: auto; }
.bimgs figcaption.cal-r { text-align: right; margin-left: auto; }
/* the two pair-rows of a 4-image block are grid children of .bimgs — its own
   .9rem gap already separates them (an extra margin here would double it) */
@media (max-width: 620px) {
  /* three-across gets too small on phones: stack them full-width instead */
  .jrow.n3 { flex-wrap: wrap; }
  .jrow.n3 figure { flex-basis: 100%; }
}

/* pre-load state (set by site.js): hide the alt-text flash inside the image's
   reserved box and show a quiet note instead. Fixed small type that WRAPS in
   narrow boxes (overflow-wrap) rather than shrinking away; clipped as the last
   resort in truly tiny slots. */
.bimg, .bimgs figure { position: relative; }
img.imgloading { color: transparent; }
/* gallery tiles flash their caption (the alt text) the same way while loading —
   the beige placeholder tile alone reads better */
.gimg { color: transparent; }
.imgload {
  position: absolute; display: flex; align-items: center; justify-content: center;
  text-align: center; font-size: .72rem; line-height: 1.35; color: var(--soft);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  border-radius: var(--r-md); padding: .25rem; overflow: hidden;
  overflow-wrap: anywhere; pointer-events: none;
}

/* framed image block: fixed-aspect box, image fitted inside (z=1 shows the
   whole image with letterbox bars in the chosen color; z>1 crops inward,
   z<1 shrinks further; pan via translate). The editor preview mirrors this. */
.fitframe { position: relative; overflow: hidden; border-radius: var(--r-lg); width: 100%; }
.fitframe img { position: absolute; inset: 0; width: 100%; height: 100%;
                object-fit: contain; transform-origin: center; display: block;
                max-width: none; border-radius: 0; }
/* framed square cover (blog thumb / project art / media art): the outer span
   keeps the slot's size/border classes; the inner image pans/zooms within */
.fitbox { position: relative; overflow: hidden; display: block; }
.fitbox img { width: 100%; height: 100%; object-fit: contain;
              transform-origin: center; display: block; user-select: none; -webkit-user-select: none; }
.bcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
          gap: .9rem; margin: 1.8rem 0; }
.bcard {
  display: flex; flex-direction: column; gap: .5rem;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 1.15rem 1.2rem; color: inherit;
  transition: border-color .18s, transform .18s;
}
a.bcard:hover { text-decoration: none; border-color: var(--soft); transform: translateY(-2px); }
.bcard h3 { font-size: 1.12rem; }
.bcard p { color: var(--soft); font-size: .87rem; flex: 1; }
.bcard-label { font-size: .84rem; font-weight: 500; color: var(--ink); }
a.bcard:hover .bcard-label { color: var(--accent); }
a.bcard:hover .arr { transform: translateX(3px); }
.bcard .arr { display: inline-block; transition: transform .18s; }
/* optional square artwork on blog rows / project cards (media-card style) */
.card.withimg { display: flex; gap: 1.1rem; align-items: flex-start; }
.card.withimg .cbody { flex: 1; min-width: 0; }
.sqthumb { width: 64px; height: 64px; flex: none; border-radius: var(--r-md);
           object-fit: cover; background-color: var(--line);
           user-select: none; -webkit-user-select: none; }
.sqthumb.lg { width: 92px; height: 92px; }
@media (max-width: 520px) { .sqthumb.lg { width: 72px; height: 72px; } }

.bcols { display: grid; gap: 2rem; margin: 1.1rem 0; }
.bcols.n2 { grid-template-columns: repeat(2, 1fr); }
.bcols.n3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 620px) {  /* columns stack on phones, as the editor promises */
  .bcols.n2, .bcols.n3 { grid-template-columns: 1fr; gap: 1.2rem; }
}
.bcols .bcol { min-width: 0; }
/* scoreboard / ranking block */
.board {
  margin: 1.5rem 0; border: 1px solid var(--line); border-radius: var(--r-lg);
  background: var(--card); overflow: hidden;
}
.brow {
  display: grid; grid-template-columns: auto 1fr auto; align-items: baseline;
  gap: .9rem; padding: .85rem 1.15rem; border-top: 1px solid var(--line);
}
.brow:first-child { border-top: 0; }
.bpos {
  font-family: var(--font-head); font-size: .95rem; font-weight: 600; color: var(--soft);
  white-space: nowrap; letter-spacing: .01em;
}
.bname { font-size: 1rem; min-width: 0; }
.bnote2 { color: var(--soft); font-size: .82rem; text-align: right; }  /* notes may be long now — no nowrap */
/* first place: gold rail, bigger type, the whole point of a scoreboard */
.brow.lead {
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 9%, transparent), transparent 70%);
  box-shadow: inset 3px 0 0 var(--accent);
  padding-top: 1rem; padding-bottom: 1rem;
}
.brow.lead .bpos { color: var(--accent); font-size: 1.15rem; }
.brow.lead .bname { font-family: var(--font-head); font-size: 1.3rem; }
@media (max-width: 520px) {
  .brow { grid-template-columns: auto 1fr; gap: .6rem; padding: .75rem .9rem; }
  .bnote2 { grid-column: 2; text-align: left; white-space: normal; }
}

.bhtml { margin: 1.1rem 0; max-width: 100%; overflow-x: auto; }
.bhr { border: 0; border-top: 1px solid var(--line); margin: 2.2rem 0; }
.bspace.s { height: 1rem; } .bspace.m { height: 2.4rem; } .bspace.l { height: 4.5rem; }

/* ---------------- arcade ---------------- */
.gamecard { margin-bottom: 1.6rem; }
.gamecard .ghead { display: flex; align-items: center; gap: .6rem; }
.gport { margin-top: 1.1rem; display: flex; justify-content: center; }
/* pan-y so a resting canvas never traps page scrolling on phones; each game
   sets touch-action:none on itself only while a run is actually in progress */
.gcanvas { display: block; width: 100%; max-width: 448px; border-radius: var(--r-md);
           touch-action: pan-y pinch-zoom; outline: none; }
.gcanvas.dash { max-width: 800px; }
.gcanvas:focus-visible { box-shadow: 0 0 0 2px var(--accent); }
.ghint { color: var(--soft); font-size: .78rem; margin-top: .7rem; text-align: center; }
@media (max-width: 520px) {
  /* let the game bleed through the card padding — every pixel counts at 360px */
  .gamecard .gport { margin-inline: -1.5rem; }
  .gamecard .gcanvas { border-radius: 0; }
}

/* ---------------- contact ---------------- */
.contact-row { display: flex; gap: .8rem; flex-wrap: wrap; margin-top: 1.5rem; }
#captchaBox { margin-top: 1.3rem; }
#captchaBox svg { border: 1px solid var(--line); border-radius: 9px; }  /* matches the SVG's own rx */
#captchaBox input {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-md);
  padding: .48rem .7rem; color: var(--ink); font-size: 1rem; width: 7rem; font-family: inherit;
}
#captchaBox input:focus { outline: none; border-color: var(--accent); }

.muted { color: var(--soft); }
.empty { color: var(--soft); font-style: italic; }

@media (max-width: 880px) {
  /* As the viewport nears the 760px content column, the centered corner toggle would
     collide with the right-aligned single-line nav (nav is pinned to the column edge,
     the toggle to the viewport edge). So drop the nav to its own line below the brand
     and pin the toggle top-right beside the brand — the same shape as the phone view. */
  nav { flex-basis: 100%; }
  #themeToggle { top: 1.15rem; margin-top: 0; right: 1.4rem; }
}
@media (max-width: 620px) {
  .hin { padding: 1rem 1.2rem; }
  #themeToggle { top: 1rem; right: 1.2rem; }
  nav { gap: .95rem; }
  main { padding-top: 2.2rem; }
  .hero { flex-direction: column-reverse; align-items: flex-start; gap: 1.4rem; margin-bottom: 2.2rem; }
  .hero-avatar { width: 84px; height: 84px; }
  .bimgs.n3 { grid-template-columns: repeat(2, 1fr); }
  .mediacard { gap: 1rem; }
  .mart { width: 68px; height: 68px; }
}
@media (max-width: 520px) {
  /* phones: pin the toggle to the top-right beside the brand row so it never sits
     between wrapped lines, and tighten the links so all seven fit one line to 360px */
  .hin { padding-inline: .8rem; }
  #themeToggle { top: .8rem; margin-top: 0; right: .8rem; }
  nav { gap: .5rem; }
  nav a { font-size: .8rem; }
}
