/*
 * This is a manifest file that'll be compiled into application.css.
 *
 * With Propshaft, assets are served efficiently without preprocessing steps. You can still include
 * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard
 * cascading order, meaning styles declared later in the document or manifest will override earlier ones,
 * depending on specificity.
 *
 * Consider organizing styles into separate files for maintainability.
 */

/* single dimension now: each theme is a complete bundled palette
   (bg/fg/border/accent together), not a mode + a separate accent
   layer - "Dark" and "Light" are just two entries in this same list,
   not a separate toggle. See CLAUDE.md's "Header controls". */
:root {
  color-scheme: dark;
  --col-left: 24%;
  --col-right: 24%;
  --header-h: 3.5rem; /* fallback - JS overwrites with the real measured height */
  --bg: #14161a;
  --bg-raised: #1c1f25;
  --fg: #e6e6e6;
  --fg-dim: #8a8f98;
  --border: #2c313a;
  --accent: #f2f2f2;
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f7f5f2; --bg-raised: #ffffff; --fg: #1c1c1c; --fg-dim: #6b6b6b; --border: #ddd8d0;
  --accent: #1c1c1c;
}
:root[data-theme="cream"] {
  color-scheme: light;
  --bg: #f4ecd8; --bg-raised: #fbf6ea; --fg: #4a3728; --fg-dim: #8a7660; --border: #ddd0b3;
  --accent: #b5651d;
}
:root[data-theme="brown"] {
  color-scheme: dark;
  --bg: #120d0a; --bg-raised: #1c1611; --fg: #e8dcc8; --fg-dim: #a8967e; --border: #4a3c2e;
  --accent: #d9a066;
}
:root[data-theme="blue"] {
  color-scheme: dark;
  --bg: #0a0f1c; --bg-raised: #101a2e; --fg: #dbe4f5; --fg-dim: #8496b8; --border: #28324a;
  --accent: #6ea8fe;
}
:root[data-theme="ruby"] {
  color-scheme: dark;
  --bg: #1a1215; --bg-raised: #241a1d; --fg: #f0e0e2; --fg-dim: #b08d92; --border: #3a2529;
  --accent: #e0546b;
}
:root[data-theme="purple"] {
  color-scheme: dark;
  --bg: #150f1e; --bg-raised: #1f1729; --fg: #e6dbf5; --fg-dim: #a190b8; --border: #332440;
  --accent: #a679ff;
}
:root[data-theme="pink"] {
  color-scheme: dark;
  --bg: #1f1217; --bg-raised: #2a1a20; --fg: #f5dbe6; --fg-dim: #b88fa0; --border: #402a35;
  --accent: #ec5fa3;
}
/* "Contrast" - nested one level inside the Theme submenu (see
   layouts/_theme_picker.html.erb), not part of the main flat list.
   Same idea as the rest of the theme list (one data-theme attribute,
   one bundled palette) but a different pattern entirely: true black
   (or true white) background, and body text *itself* is the hue - no
   separate neutral fg with a colored accent - with a hotter/brighter
   version of the same hue as the accent, for an old-CRT/phosphor-
   terminal glow. Green was the original one-off version of this; the
   rest generalize the same recipe to other hues, plus a true
   black/white pair for max-contrast with no hue at all. */
:root[data-theme="contrast-green"] {
  color-scheme: dark;
  --bg: #000000; --bg-raised: #0a0f0a; --fg: #29cc29; --fg-dim: #1f8f1f; --border: #1a331a;
  --accent: #39ff14;
}
/* Amber is Green's classic phosphor-CRT counterpart - P1 (green) and P3
   (amber) were the two dominant monochrome monitor phosphors, so this
   completes the pair the "old-CRT" reasoning above was always actually
   pointing at, not a color added just to add one. */
:root[data-theme="contrast-amber"] {
  color-scheme: dark;
  --bg: #000000; --bg-raised: #0f0c07; --fg: #cc8a1f; --fg-dim: #8f611f; --border: #332410;
  --accent: #ffb020;
}
:root[data-theme="contrast-ruby"] {
  color-scheme: dark;
  --bg: #000000; --bg-raised: #0f0a0a; --fg: #cc2929; --fg-dim: #8f1f1f; --border: #331a1a;
  --accent: #ff3b3b;
}
:root[data-theme="contrast-purple"] {
  color-scheme: dark;
  --bg: #000000; --bg-raised: #0d0817; --fg: #a64dff; --fg-dim: #6b2fa3; --border: #241333;
  --accent: #cc99ff;
}
/* was missing entirely despite "Blue" existing in the main Theme list -
   the one hue in that list with no contrast counterpart */
:root[data-theme="contrast-blue"] {
  color-scheme: dark;
  --bg: #000000; --bg-raised: #070a0f; --fg: #2d7fe0; --fg-dim: #1f5799; --border: #0f1f33;
  --accent: #4da6ff;
}
:root[data-theme="contrast-black"] {
  /* the hueless pair of the family - pure black/white, maximum
     contrast, for when a color glow isn't the point */
  color-scheme: dark;
  --bg: #000000; --bg-raised: #0d0d0d; --fg: #e5e5e5; --fg-dim: #999999; --border: #3d3d3d;
  --accent: #ffffff;
}
:root[data-theme="contrast-white"] {
  color-scheme: light;
  --bg: #ffffff; --bg-raised: #f0f0f0; --fg: #1a1a1a; --fg-dim: #666666; --border: #cccccc;
  --accent: #000000;
}

/* "Editor" - a second family nested inside Themes, alongside Contrast
   (see layouts/_theme_picker.html.erb). Not the black-bg/hued-text
   Contrast recipe - these are real, complete palettes lifted directly
   from well-known editor/terminal color schemes, each with its own
   genuine bg/fg/border/accent relationship rather than one shared
   formula. Colors match each scheme's own published values, not
   approximated from memory. */
:root[data-theme="dracula"] {
  color-scheme: dark;
  --bg: #282a36; --bg-raised: #343746; --fg: #f8f8f2; --fg-dim: #6272a4; --border: #44475a;
  --accent: #bd93f9;
}
:root[data-theme="nord"] {
  color-scheme: dark;
  --bg: #2e3440; --bg-raised: #3b4252; --fg: #e5e9f0; --fg-dim: #7b88a1; --border: #434c5e;
  --accent: #88c0d0;
}
:root[data-theme="gruvbox"] {
  color-scheme: dark;
  --bg: #282828; --bg-raised: #32302f; --fg: #ebdbb2; --fg-dim: #a89984; --border: #3c3836;
  --accent: #fe8019;
}
:root[data-theme="monokai"] {
  color-scheme: dark;
  --bg: #272822; --bg-raised: #33342c; --fg: #f8f8f2; --fg-dim: #90876f; --border: #3e3d32;
  --accent: #f92672;
}
:root[data-theme="solarized"] {
  color-scheme: dark;
  --bg: #002b36; --bg-raised: #073642; --fg: #93a1a1; --fg-dim: #586e75; --border: #0a4552;
  --accent: #268bd2;
}
* { box-sizing: border-box; }
/* reset - without this, h1/h2/h3/p/ul/time all keep the browser's
   own default margins *underneath* whatever margin rules are written
   below, stacking invisibly on top of them. That's what was actually
   making the post header (h1 + time + tags) balloon compared to the
   homepage's title-only header: default UA heading/list margins
   compounding with the intended ones, not any single rule being
   "wrong." Zero everything, then every margin below is the only
   margin that exists - no hidden defaults left to fight. */
h1, h2, h3, h4, h5, h6, p, ul, ol, time, figure { margin: 0; }
/* Real semibold (600) file exists (fonts.css) for exactly this - the
   browser's own default heading weight is a plain "bold" (~700), which
   has no matching font file and would otherwise render as synthetic/
   faux bold (the regular weight, algorithmically thickened). Setting
   this explicitly avoids that everywhere a heading appears, not just
   in post content. */
h1, h2, h3, h4, h5, h6 { font-weight: 600; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--font-size-base);
  line-height: 1.7;
  /* guaranteed backstop: nothing on the page can EVER force horizontal
     page scroll, no matter the cause - a code block, a long URL, a
     future content type nobody's thought of yet. Position:fixed
     elements (the mobile drawer) anchor to the wrong viewport when the
     page itself gains horizontal overflow - that's what a single
     unbroken code line did here, the actual root cause of the
     marginalia bug, not the marginalia code itself. This makes that
     entire bug class structurally impossible going forward, rather
     than patching each individual content type that could trigger it. */
  overflow-x: hidden;
}
code, pre, .code-block {
  font-family: var(--font-code);
}
/* baseline safety net for EVERY <pre>, wrapped or not: MarkdownRenderer
   doesn't wrap fenced code in <div class="code-block"> yet (that's the
   code-block-highlighting branch, not built here) - which means real
   posts render a completely bare <pre><code>, so the more specific
   ".code-block pre" overflow rule below never actually applies to real
   content, only to the hand-written example in tmp/layout-schema.html.
   A long code line scrolls horizontally *within* the block - the
   normal, standard way every code block on the web handles this - it
   never wraps/breaks the code's own formatting. */
pre {
  display: block;
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  white-space: pre;
}

/* inline code - a subtle pill, not a full code block. margin (not just
   the internal padding) so the pill's own border doesn't sit flush
   against the surrounding prose on either side */
:not(pre) > code {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1em 0.4em;
  margin: 0 0.15em;
  font-size: 0.88em;
}

/* fenced code block: header (language + copy button) above the
   highlighted code itself */
.code-block {
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.code-block-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 1rem;
  background: var(--bg-raised);
  border-bottom: 1px solid var(--border);
}
.code-lang {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--fg-dim);
}
/* Icon-only (GitHub/VS Code style), not the "Copy" text label this
   shipped with originally. The icon can't be a real <svg> tag here
   the way the header icons are - this button's markup comes from
   MarkdownRenderer#block_code, which is part of sanitized post
   content, and an inline SVG's camelCase attributes (viewBox) aren't
   guaranteed to survive Rails' sanitizer (see CLAUDE.md's "Code
   blocks" section on why a text label was chosen the first time
   around). A CSS mask-image sidesteps that entirely: the icon lives in
   the static stylesheet, never in the sanitized HTML, and takes its
   color from `currentColor` like the real inline SVGs do. The visible
   "Copy"/"Copied!" text moves to a visually-hidden .sr-only span for
   screen readers instead of disappearing outright. */
.code-copy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 0.3rem;
}
.code-copy-btn:hover { color: var(--fg); }
.code-copy-btn::before {
  content: "";
  display: inline-block;
  width: 14px;
  height: 14px;
  background-color: currentColor;
  mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNiAxNicgZmlsbD0nbm9uZScgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxLjMnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCc+PHJlY3QgeD0nNS41JyB5PSc1LjUnIHdpZHRoPSc5JyBoZWlnaHQ9JzknIHJ4PScxLjMnLz48cGF0aCBkPSdNMy41IDEwLjJoLTFhMSAxIDAgMCAxLTEtMXYtNi43YTEgMSAwIDAgMSAxLTFoNi43YTEgMSAwIDAgMSAxIDF2MScvPjwvc3ZnPgo=") no-repeat center / contain;
  -webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNiAxNicgZmlsbD0nbm9uZScgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxLjMnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCc+PHJlY3QgeD0nNS41JyB5PSc1LjUnIHdpZHRoPSc5JyBoZWlnaHQ9JzknIHJ4PScxLjMnLz48cGF0aCBkPSdNMy41IDEwLjJoLTFhMSAxIDAgMCAxLTEtMXYtNi43YTEgMSAwIDAgMSAxLTFoNi43YTEgMSAwIDAgMSAxIDF2MScvPjwvc3ZnPgo=") no-repeat center / contain;
}
.code-copy-btn.copied { color: var(--accent); }
.code-copy-btn.copied::before {
  mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNiAxNicgZmlsbD0nbm9uZScgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxLjYnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCc+PHBhdGggZD0nTTMgOC41bDMuMiAzLjJMMTMgNC41Jy8+PC9zdmc+Cg==");
  -webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNiAxNicgZmlsbD0nbm9uZScgc3Ryb2tlPSdibGFjaycgc3Ryb2tlLXdpZHRoPScxLjYnIHN0cm9rZS1saW5lY2FwPSdyb3VuZCcgc3Ryb2tlLWxpbmVqb2luPSdyb3VuZCc+PHBhdGggZD0nTTMgOC41bDMuMiAzLjJMMTMgNC41Jy8+PC9zdmc+Cg==");
}

/* standard visually-hidden pattern: present for screen readers, removed
   from the visual layout entirely (unlike display:none, which hides it
   from screen readers too) */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.code-block pre {
  margin: 0;
  padding: 1rem 1.2rem;
  overflow-x: auto;
  font-size: 0.92rem;
  line-height: 1.6;
}
.code-block code { background: none; border: none; padding: 0; font-size: inherit; }

/* Rouge's real token class names (github.com/rouge-ruby/rouge) - kept
   to a light, theme-adaptive palette rather than a fixed per-theme
   set: --fg/--fg-dim/--accent already exist for every theme, plus
   one extra hardcoded warm tone for strings/literals that reads
   reasonably against every background this project ships. Real
   implementation needs an actual light-vs-dark Rouge theme pair, see
   CLAUDE.md's Code blocks section - this is a mocked approximation. */
.k, .kp, .kd { color: var(--accent); }               /* keyword */
.s, .s1, .s2, .sr { color: #d9a066; }                 /* string */
.n, .nb, .vi { color: var(--fg); }                    /* identifier/name */
.nf { color: var(--fg); font-weight: 600; }            /* method/function name */
.o { color: var(--fg-dim); }                          /* operator */
.c, .c1, .cm { color: var(--fg-dim); font-style: italic; } /* comment */
.mi, .mf { color: #d9a066; }                          /* number */

/* ---- real layout skeleton ---- */
.layout {
  display: grid;
  grid-template-columns: var(--col-left) 1fr var(--col-right);
  /* without this, both rows default to "auto" sizing, and CSS Grid's
     default stretch behavior distributes any leftover space (from
     min-height: 100vh) across every auto-sized row - on a short post
     in a tall browser window, that silently inflated the header row
     along with the content row, since neither had a size that opted
     out of stretching. Pinning the header row to "auto" explicitly
     here isn't the fix by itself (auto is already the default) -
     the fix is giving the *content* row "1fr" so it - and only it -
     absorbs 100% of the leftover space, leaving the header locked to
     exactly its own content's height regardless of viewport height
     or how short the post is. */
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "header header header"
    "left   main   right";
  min-height: 100vh;
}

.site-header {
  grid-area: header;
  padding: 0.5rem 2.5rem;
  border-bottom: 1px solid var(--border);
  /* sticky, not fixed: stays a normal grid item (so it doesn't need
     its own width math like `fixed` would), but pins to the top of
     the viewport once scrolling would carry it past `top`. */
  position: sticky;
  top: 0;
  z-index: 100;
  background: var(--bg);
  /* body's line-height:1.7 is tuned for reading paragraphs - left
     inherited here it inflates every line box in the header (brand
     text, icons' alignment) well past what the padding numbers alone
     suggest, which is why the header kept measuring bigger than
     expected even after padding was trimmed */
  line-height: 1.2;
}
/* the bar itself (background/border) still spans the full viewport
   width above - only the actual brand/nav content is capped and
   centered within it. Without this, brand and the gear button sat
   flush against the literal browser edges on a wide monitor, which
   also pushed the gear's submenu dropdown (see .config-dropdown) past
   the right edge of the viewport with nowhere to go. `width: 90%` (not
   just `max-width`) is what actually makes the inset show up at
   ordinary desktop widths, not just past 1440px - a `max-width` alone
   only ever caps extreme wide screens and does nothing below it. */
.site-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 90%;
  max-width: 80rem;
  margin: 0 auto;
}
.site-header .brand {
  font-weight: bold;
  letter-spacing: 0.02em;
  color: var(--fg);
  text-decoration: none;
  font-size: 1.3rem;
}
.site-header-right { display: flex; align-items: center; gap: 1.1rem; }

.header-link {
  color: var(--fg-dim);
  text-decoration: none;
  font-size: 0.95rem;
}
.header-link:hover { color: var(--fg); }

.icon-btn {
  background: none;
  border: none;
  color: var(--fg-dim);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  padding: 0.2rem;
  line-height: 0;
}
.icon-btn:hover { color: var(--fg); }
.icon-btn svg { width: 20px; height: 20px; }

/* GitHub icon specifically inverts .icon-btn's usual dim-then-brighten
   hover: bright white by default, darkening on hover instead - the
   opposite of every other icon-btn, by explicit request. Fixed colors,
   not theme variables, to get a literal "white" regardless of theme. */
.icon-btn.github-icon { color: #ffffff; }
.icon-btn.github-icon:hover { color: #4a4a4a; }

.search-form {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.25rem 0.6rem;
}
.search-form input {
  background: none;
  border: none;
  color: var(--fg);
  font: inherit;
  font-size: 0.9rem;
  width: 9rem;
  outline: none;
}
.search-form svg { width: 16px; height: 16px; color: var(--fg-dim); flex-shrink: 0; }

.lang-switch { display: flex; align-items: center; gap: 0.3rem; font-size: 0.9rem; }
.lang-option {
  background: none;
  border: none;
  color: var(--fg-dim);
  font: inherit;
  cursor: pointer;
  padding: 0.1rem 0.15rem;
}
.lang-option.active { color: var(--accent); font-weight: bold; }

/* desktop only: a slightly taller, roomier header with everything in it
   (brand, links, icons, lang switch) a bit bigger - modest bumps all
   around, not a redesign. --header-h isn't touched directly here: it's
   measured live off the real header height by theme.js, so it follows
   this automatically. */
@media (min-width: 881px) {
  .site-header { padding: 0.85rem 5.5rem; }
  .site-header .brand { font-size: 1.5rem; }
  .header-link { font-size: 1.05rem; }
  .icon-btn svg { width: 23px; height: 23px; }
  .search-form input { font-size: 1rem; width: 10rem; }
  .search-form svg { width: 18px; height: 18px; }
  .lang-switch { font-size: 1rem; }
}
.lang-sep { color: var(--border); }

.header-config { position: relative; }
/* one interaction pattern at every width, desktop included: the box is
   a fixed-size clipped viewport, and a horizontal track inside it
   slides between three same-size panels (root/theme/fonts) - "click a
   row, the panel updates" rather than a second box flying out beside
   it. A flyout was tried first and rejected: it either overflowed a
   normal-width desktop viewport or needed constant width-dependent
   tuning to avoid it, whereas one sliding track behaves identically
   regardless of where the dropdown sits on screen. */
.config-dropdown {
  position: absolute;
  top: calc(100% + 0.75rem);
  right: 0;
  width: 15rem;
  max-width: calc(100vw - 2rem);
  overflow: hidden;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgb(0 0 0 / 30%);
  z-index: 250;
  transform-origin: top right;
  transform: scale(0.95);
  opacity: 0;
  pointer-events: none;
  /* height is set inline by theme.js's syncDropdownHeight(), to match
     whichever panel is actually showing - transitioning it here is
     what makes that height change slide smoothly instead of jumping */
  transition: transform 0.15s ease, opacity 0.15s ease, height 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.config-dropdown.open { transform: scale(1); opacity: 1; pointer-events: auto; }
.config-link { display: block; color: var(--fg); text-decoration: none; padding: 0.3rem 0; font-size: 0.9rem; }
/* the GitHub row specifically - icon only (an aria-label carries the
   name for screen readers), not a text link like the others here */
.config-link-icon { display: flex; align-items: center; }
.config-link-icon svg { width: 16px; height: 16px; flex-shrink: 0; }
.config-search { margin: 0.4rem 0; }

/* four panels now: root, theme, fonts, contrast (Contrast nests one
   level inside Theme - see the partial's DOM order, which this math
   depends on: panel N sits at -N * (100/4)% along the track). */
/* five panels now: root, theme, fonts, contrast, editor - both
   Contrast and Editor nest inside Theme (see the partial's DOM order,
   which this math depends on: panel N sits at -N * (100/5)% along
   the track). */
.config-track {
  display: flex;
  /* flex-start, not the stretch default - each panel keeps its own
     natural content height instead of being forced to match the
     tallest one, which is what theme.js's offsetHeight measurement
     needs to read the *real* per-panel height. */
  align-items: flex-start;
  width: 500%;
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.config-dropdown[data-view="theme"] .config-track { transform: translateX(-20%); }
.config-dropdown[data-view="fonts"] .config-track { transform: translateX(-40%); }
.config-dropdown[data-view="contrast"] .config-track { transform: translateX(-60%); }
.config-dropdown[data-view="editor"] .config-track { transform: translateX(-80%); }
/* each panel gets its own padding (not the dropdown box itself) since
   the track needs to be edge-to-edge for the width math above to work */
.config-panel { width: 20%; flex-shrink: 0; box-sizing: border-box; padding: 0.75rem; }

/* root-level entries that push into a submenu ("Theme", "Fonts") */
.config-menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: none;
  border: none;
  color: var(--fg);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  padding: 0.45rem 0.2rem;
  border-radius: 4px;
  cursor: pointer;
}
.config-menu-item:hover { background: var(--bg); }
.config-menu-item .chevron { width: 16px; height: 16px; color: var(--fg-dim); flex-shrink: 0; }

.config-back {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  width: 100%;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  color: var(--fg-dim);
  font: inherit;
  font-size: 0.85rem;
  padding: 0.2rem 0.2rem 0.6rem;
  margin-bottom: 0.4rem;
  cursor: pointer;
}
.config-back:hover { color: var(--fg); }
.config-back svg { width: 14px; height: 14px; flex-shrink: 0; }

.config-accents { display: flex; flex-direction: column; gap: 0.2rem; }
.accent-swatch {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: none;
  border: none;
  color: var(--fg);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  padding: 0.3rem 0.2rem;
  border-radius: 4px;
  cursor: pointer;
}
.accent-swatch:hover { background: var(--bg); }
.accent-swatch.active { color: var(--accent); font-weight: bold; }
.accent-swatch .dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
/* one class per theme instead of an inline style="" attribute per dot -
   CSP's style-src blocks inline style attributes the same way
   script-src blocks inline scripts, and this also matches the
   project's own "styling lives only in CSS, never in markup"
   principle more directly than the inline version ever did. Colors
   match each theme's actual --bg (previously drifted stale after
   brown/blue got darkened later - fixed here too, not just moved). */
.dot-dark { background: #14161a; border: 1px solid #444; }
.dot-light { background: #f7f5f2; border: 1px solid #ccc; }
.dot-cream { background: #f4ecd8; border: 1px solid #ccc; }
.dot-brown { background: #120d0a; }
.dot-blue { background: #0a0f1c; }
.dot-ruby { background: #1a1215; }
.dot-purple { background: #150f1e; }
.dot-pink { background: #1f1217; }
/* contrast dots: bg is always the theme's own true black/white, with a
   border in the theme's hue - a plain black dot would otherwise be
   indistinguishable from another dark theme's dot at a glance */
.dot-contrast-green { background: #000000; border: 1px solid #39ff14; }
.dot-contrast-amber { background: #000000; border: 1px solid #ffb020; }
.dot-contrast-ruby { background: #000000; border: 1px solid #ff3b3b; }
.dot-contrast-purple { background: #000000; border: 1px solid #cc99ff; }
.dot-contrast-blue { background: #000000; border: 1px solid #4da6ff; }
.dot-contrast-black { background: #000000; border: 1px solid #ffffff; }
.dot-contrast-white { background: #ffffff; border: 1px solid #000000; }
/* editor dots: bg is the scheme's own real background (not a shared
   black), border in its own accent hue for the same at-a-glance
   distinguishability reasoning as the contrast dots above */
.dot-dracula { background: #282a36; border: 1px solid #bd93f9; }
.dot-nord { background: #2e3440; border: 1px solid #88c0d0; }
.dot-gruvbox { background: #282828; border: 1px solid #fe8019; }
.dot-monokai { background: #272822; border: 1px solid #f92672; }
.dot-solarized { background: #002b36; border: 1px solid #268bd2; }

/* Fonts submenu - typeface list previews each option in its own actual
   font (cheap: all four are already self-hosted for the reading body
   text itself, see fonts.css, nothing extra loaded just to preview
   them), size list is plain text since there's no analogous "self" way
   to preview a relative size choice as a label. */
.config-fonts, .config-font-sizes { display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.65rem; }
.config-fonts:last-of-type, .config-font-sizes:last-of-type { margin-bottom: 0; }
.font-option, .font-size-option {
  display: block;
  width: 100%;
  background: none;
  border: none;
  color: var(--fg);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  padding: 0.3rem 0.2rem;
  border-radius: 4px;
  cursor: pointer;
}
.font-option:hover, .font-size-option:hover { background: var(--bg); }
.font-option.active, .font-size-option.active { color: var(--accent); font-weight: bold; }
.font-option[data-font="inter"] { font-family: "Inter", sans-serif; }
.font-option[data-font="plex"] { font-family: "IBM Plex Sans", sans-serif; }
.font-option[data-font="source"] { font-family: "Source Sans 3", sans-serif; }
.font-option[data-font="serif"] { font-family: "Source Serif 4", serif; }

.mobile-only { display: none; }

.menu-button { display: none; }

.col-left, .col-right { font-size: 0.95rem; }
.col-left { grid-area: left; }
.col-main { grid-area: main; }
.col-right { grid-area: right; }
/* grid items default to min-width: auto - meaning they refuse to
   shrink below their CONTENT's own intrinsic minimum width (a long
   paragraph, the tag row, a code block) unless told otherwise. That
   silently doesn't matter on desktop (plenty of room either way),
   but on mobile it's exactly what was causing the page to render
   "as if it were still desktop": .col-main refused to shrink to the
   phone's width and just overflowed/clipped instead of reflowing.
   min-width: 0 opts every column out of that default. */
.col-left, .col-main, .col-right { min-width: 0; }

/* .col-left/.col-right stay tall, ordinary grid cells (stretched to
   match the row's full height, i.e. however long the post is) - that
   height is exactly what gives the sticky wrapper below "room to
   travel" as the page scrolls. The wrapper itself is the compact,
   visually-sized, actually-sticky piece: pinned at the same offset
   the title sits at, capped to half the viewport's height, and
   scrollable if its content doesn't fit. It only releases and
   scrolls away once the tall outer column's own bottom edge (i.e.
   the end of the post) comes into view - "sticks" for the entire
   read, not just briefly. */
.col-left-sticky, .col-right-sticky {
  position: sticky;
  top: calc(var(--header-h) + 1.5rem);
  /* fills essentially the whole viewport below the header, rather than
     a flat 50vh cap - the 50vh version left both panels feeling cramped
     regardless of how tall the browser window actually was */
  height: calc(100vh - var(--header-h) - 3rem);
  margin: 0 1rem;
}
.col-right-sticky { overflow-y: auto; }
/* clips the marginalia card's off-screen slide (translateX(-115%))
   to this box - without this, the slid-out state would poke past
   the viewport's left edge and cause page-wide horizontal overflow */
.col-left-sticky { overflow: hidden; }

/* the content block is centered WITHIN the middle track, so it doesn't
   sit flush left with dead space on the right when the track is wider
   than the reading measure (max-width) - text itself stays left-aligned */
.col-main-inner {
  max-width: 68ch;
  margin: 0 auto;
  padding: 1.5rem 2rem;
}
/* prose rhythm for the post body - explicit now that the reset above
   removed the browser's own default paragraph/heading margins this
   was silently leaning on before.

   Scoped to .post-content, not .col-main-inner: the actual heading/
   paragraph elements Redcarpet outputs live inside #post-content, one
   level deeper than .col-main-inner - a ">" direct-child selector on
   .col-main-inner never matched them at all, which is why headings
   rendered with zero spacing regardless of the rule below. */
.post-content > p { margin: 0 0 1.15em; }
.post-content > h2 { margin: 2rem 0 0.75rem; font-size: 1.55rem; }
.post-content > h3 { margin: 1.5rem 0 0.5rem; font-size: 1.3rem; }
.post-content > h4 { margin: 1.25rem 0 0.4rem; font-size: 1.15rem; }
.post-content > h5 { margin: 1.1rem 0 0.4rem; font-size: 1.05rem; }
.post-content > h6 { margin: 1rem 0 0.4rem; font-size: 1rem; }
.post-content > h2:first-child, .post-content > h3:first-child { margin-top: 0; }

/* an image should never render wider than the post text itself (see
   CLAUDE.md's "Images" section) - not a > direct-child selector, since
   Redcarpet wraps a standalone ![]() in a <p>, so the <img> is a
   grandchild of .post-content, not a direct one. max-width: 100% is
   relative to that <p>'s own width (already capped by .col-main-inner),
   so this only ever shrinks an oversized image, never grows a small
   one - height: auto keeps it proportional once width is constrained. */
.post-content img {
  max-width: 100%;
  height: auto;
  display: block;
  margin: 1.5rem auto;
  border-radius: 8px;
}

/* every ## heading gets an <hr> auto-inserted right after it
   (MarkdownRenderer#header) - sits close under the heading on purpose,
   then a fuller gap before the paragraph that follows */
.post-content hr { border: none; border-top: 1px solid var(--border); margin: 1rem 0; }
.post-content h2 + hr { margin-top: 0.5rem; margin-bottom: 1.5rem; }
/* every `* `/`- ` Markdown bullet renders through this - same "›" in
   the code font used for the featured-posts list and the sidebar
   back/top actions, so a bullet point reads as the same visual idea
   everywhere it shows up on the site, not a plain browser disc here
   and a stylized glyph elsewhere. Ordered lists (`1. `) keep their
   real numbers - only unordered ones get the glyph treatment, since a
   number carries real information a bullet doesn't. */
.post-content ul { list-style: none; padding-left: 1.4rem; margin: 1rem 0; }
.post-content ul li { position: relative; margin: 0.35rem 0; }
.post-content ul li::before {
  content: "\203a";
  position: absolute;
  left: -1.1rem;
  color: var(--accent);
  font-family: var(--font-code);
  font-weight: 700;
}
.post-content ol { padding-left: 1.6rem; margin: 1rem 0; }
.post-content ol li { margin: 0.35rem 0; }

/* Tables - terminal/CLI-table styled, matching the same visual language
   as .code-block: a bordered, rounded box, a raised header treated like
   a code fence's language tag (code font, small, uppercase), thin grid
   lines standing in for a real terminal table's row/column rules. The
   scroll container (see MarkdownRenderer#table) is what actually
   contains a wide table on mobile - same "contain overflow, don't let
   the page itself scroll sideways" instinct as fenced code blocks. */
.table-wrap {
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow-x: auto;
}
.post-content table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.95rem;
}
.post-content thead th {
  background: var(--bg-raised);
  color: var(--fg);
  font-family: var(--font-code);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 700;
  text-align: left;
  white-space: nowrap;
  padding: 0.65rem 1rem;
  border-bottom: 2px solid var(--accent);
}
.post-content thead th:not(:last-child) { border-right: 1px solid var(--border); }
.post-content tbody td {
  padding: 0.6rem 1rem;
  border-bottom: 1px solid var(--border);
}
.post-content tbody td:not(:last-child) { border-right: 1px solid var(--border); }
.post-content tbody tr:last-child td { border-bottom: none; }
.post-content tbody tr:hover { background: var(--bg-raised); }
/* alignment classes, not Redcarpet's default inline style="" - see
   MarkdownRenderer#table_cell for why (this site's CSP blocks inline
   styles outright, so they'd never have taken effect anyway) */
.post-content .align-center { text-align: center; }
.post-content .align-right { text-align: right; }

/* every prose link (post body, About page - both use .post-content)
   stays the accent color always, :visited included - underline kept on
   purpose (unlike most other links on the site) so a link is still
   visually distinguishable as clickable regardless of how close the
   accent color happens to sit to the surrounding text color on a given
   theme. Same rule as marginalia description links, applied blog-wide. */
.post-content a,
.post-content a:visited {
  color: var(--accent);
  text-decoration: underline;
}

/* title, date, tags, excerpt all centered as a unit - only the body
   content below (headings, paragraphs) stays left-aligned */
.post-header { line-height: 1.3; text-align: center; }
h1.post-title { font-size: 2rem; margin-bottom: 0.4rem; }
.post-header time { display: block; margin-top: 0.4rem; color: var(--fg-dim); font-size: 1rem; }
.post-excerpt {
  /* width: auto (the default) always fills 100% of the available
     width, only getting capped once that 100% exceeds max-width - on a
     narrow viewport (.col-main-inner drops its own max-width below
     880px) a short excerpt's 100% can end up narrower than 48ch,
     meaning the cap never actually engages and the block stretches
     edge-to-edge instead of hugging its own (short) text. fit-content
     shrinks the box to the text itself first, with max-width still
     capping a genuinely long excerpt from stretching too wide. */
  width: fit-content;
  max-width: 48ch;
  margin: 0.9rem auto 1rem;
  padding: 0 1rem;
  border-left: 3px solid var(--border);
  font-style: italic;
  color: var(--fg-dim);
  font-size: 1.15rem;
  /* a long stretch of text with no spaces to break at (a pasted URL, a
     typo missing spaces) has nowhere to wrap by default and just
     overflows straight past its own box, past the reading column, into
     whatever sits to the right of it - same overflow bug class as the
     unwrapped code block that broke the mobile marginalia drawer
     earlier in this project, fixed the same way: force a break rather
     than let content dictate the box's real width. */
  overflow-wrap: break-word;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
}
.tag {
  display: inline-block;
  font-size: 0.85rem;
  color: var(--fg-dim);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  padding: 0.25rem 0.8rem;
  border-radius: 999px;
  text-decoration: none;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.tag:hover { color: var(--accent); border-color: var(--accent); }

/* reserved-but-empty marginalia slot. Placeholder and filled card
   stack in the same spot (absolute, filling .col-left-sticky
   completely) so the filled card can slide/fade over the placeholder
   without affecting layout. The sticky wrapper (see .col-left-sticky
   above) is what actually handles position/size/scroll now - these
   two just fill whatever box it defines. */
.marginalia-placeholder, .marginalia-filled {
  position: absolute;
  inset: 0;
  border-radius: 10px;
}
/* genuinely empty - no border, no text. Confirmed correction: the
   dashed border + label were mockup-only wireframe scaffolding, never
   meant to ship as real UI. "Reserved but empty" means invisible. */
/* genuinely inert - this box only ever exists to fade in/out, nothing
   in it is ever clickable, so it must never intercept clicks aimed at
   whatever's actually stacked underneath it (e.g. the homepage's
   featured-posts pill, a sibling in the same .col-left-sticky) */
.marginalia-placeholder { transition: opacity 0.25s ease; pointer-events: none; }
.marginalia-placeholder.faded { opacity: 0; }

.marginalia-filled {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  box-shadow: 0 12px 32px rgb(0 0 0 / 28%);
  padding: 1.4rem 1.3rem;
  overflow-y: auto;
  transform: translateX(-115%);
  opacity: 0;
  pointer-events: none;
  transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
  z-index: 50;
}
.marginalia-filled.active { transform: translateX(0); opacity: 1; pointer-events: auto; }
.marginalia-filled .marginalia-close {
  position: absolute;
  top: 0.6rem;
  right: 0.75rem;
  background: none;
  border: none;
  color: var(--fg-dim);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}
.marginalia-filled .marginalia-close:hover { color: var(--fg); }
.marginalia-filled .term {
  font-weight: 700;
  font-size: 1.4rem;
  line-height: 1.25;
  color: var(--accent);
  margin: 0 1.5rem 0.75rem 0;
}
.marginalia-filled hr, .post-header hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 0.85rem 0;
}
.marginalia-filled .desc { line-height: 1.65; }
.marginalia-filled .desc p { margin: 0 0 0.75em; }
.marginalia-filled .desc p:last-child { margin-bottom: 0; }
/* a link inside a description stays the accent color always - :visited
   is overridden too, since browsers apply their own default (usually
   purple) there otherwise, which would otherwise be the one thing
   breaking "always the same color". Underline kept on purpose - color
   alone doesn't reliably read as "clickable" against every theme. */
.marginalia-filled .desc a,
.marginalia-filled .desc a:visited {
  color: var(--accent);
  text-decoration: underline;
}

.right-col-heading {
  color: var(--fg-dim);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 0.75rem;
}

.toc { list-style: none; margin: 0 0 1rem; padding: 0; }
.toc li { margin: 0.35rem 0; }
.toc a { color: var(--fg-dim); text-decoration: none; }
.toc a:hover { color: var(--fg); }
.toc .level-3 { padding-left: 1rem; }
.toc .level-4 { padding-left: 2rem; }
.toc .level-5 { padding-left: 3rem; }
.toc .current a { color: var(--accent); font-weight: bold; }

.side-action {
  position: relative;
  display: block;
  width: 100%;
  text-align: left;
  text-decoration: none;
  font: inherit;
  font-size: 0.85rem;
  background: none;
  color: var(--accent);
  border: none;
  padding: 0.4rem 0 0.4rem 1.15rem;
  cursor: pointer;
}
.side-action:hover { text-decoration: underline; }
/* same stylized-glyph-in-the-code-font treatment as the featured-posts
   list's "›" bullets, just pointed the direction each action actually
   goes - up to jump to the top, left/back to leave the post entirely */
.side-action::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.4rem;
  font-family: var(--font-code);
  font-weight: 700;
}
.side-action[data-action="back-to-top"]::before { content: "^"; }
.side-action-back::before { content: "\2039"; }
.side-divider { border: none; border-top: 1px solid var(--border); margin: 0.75rem 0; }

/* Featured posts - styled as a fenced code block (see .code-block
   below), on request: "Destaques" plays the role of the language tag
   in a ```ruby fence, the list plays the role of the highlighted code
   underneath it. Same border/radius/overflow-hidden square shape, same
   raised-header-over-plain-body split - not a pill anymore. Rendered
   twice (see layouts/_marginalia.html.erb and posts/index.html.erb):
   once in the desktop left column, once inline on mobile where that
   column is hidden. Both copies share this CSS and
   featured_posts.js's event-delegated toggle - only the surrounding
   layout differs. */
/* top margin doubles as the mobile copy's breathing room (sits right
   after the <h1>) - desktop overrides it (see below) */
.featured-posts {
  margin: 1.75rem 0 1.5rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
/* desktop-only static fallback before featured_posts.js measures the
   real offset to the first month heading and overrides this inline -
   see that file for why a fixed value can't be correct on its own
   (reader-controlled font size changes heading height) */
.col-left .featured-posts { margin-top: 5.5rem; margin-bottom: 1.25rem; }

.featured-posts-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: var(--bg-raised);
  border: none;
  border-bottom: 1px solid var(--border);
  padding: 0.6rem 1rem;
  color: var(--fg);
  font: inherit;
  cursor: pointer;
}
.featured-posts-toggle:hover .featured-posts-heading,
.featured-posts-toggle:hover .chevron { color: var(--fg); }
/* same treatment as .code-lang (the "ruby" tag in a real fenced code
   block) - small, dim, uppercase, wide letter-spacing - left-aligned,
   not centered */
.featured-posts-heading {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--fg-dim);
}
.featured-posts-toggle .chevron {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--fg-dim);
  transition: transform 0.25s ease, color 0.15s ease;
}
/* base chevron points down ("opens below"); open state flips it to
   point up ("click to close back up") - not the sideways/rotating
   chevron the gear dropdown's nested menus use, since those slide
   sideways into a submenu while this expands straight down in place */
.featured-posts.open .featured-posts-toggle .chevron { transform: rotate(180deg); }

/* grid-template-rows 0fr -> 1fr is what makes this animate smoothly
   without JS measuring scrollHeight (unlike the gear dropdown's
   multi-panel track, this is just one open/closed state, so the
   simpler CSS-only trick is enough here) - the inner wrap needs its
   own overflow:hidden since a collapsing grid *row* alone doesn't clip
   its content during the transition, only once it's fully collapsed. */
.featured-posts-collapse {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.featured-posts.open .featured-posts-collapse { grid-template-rows: 1fr; }
.featured-posts-list-wrap { overflow: hidden; min-height: 0; }

.featured-posts-list {
  list-style: none;
  margin: 0;
  /* extra left padding reads as "nested under the pill above", not
     flush with its left edge; bottom padding keeps the last item off
     the box's own bottom edge */
  padding: 0.6rem 0.3rem 0.9rem 1.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
/* the actual overflow-to-the-right fix: a long title wraps within the
   narrow column instead of pushing past its right edge - min-width: 0
   is required on the flex/grid ancestors above for this to take effect
   at all (without it, a flex item refuses to shrink below its content's
   natural width, and overflow-wrap never gets the chance to apply) */
.featured-posts-list li { position: relative; min-width: 0; padding-left: 1.15rem; }
/* a stylized ">" (real glyph, not the raw ASCII character) in the code
   font - a terminal-log-line feel for the bullet, not a plain browser
   dot */
.featured-posts-list li::before {
  content: "\203a";
  position: absolute;
  left: 0.1rem;
  top: -0.2rem;
  color: var(--accent);
  font-family: var(--font-code);
  font-weight: 700;
}
.featured-posts-list a {
  display: block;
  color: var(--fg);
  text-decoration: none;
  font-size: 1.1rem;
  line-height: 1.4;
  overflow-wrap: break-word;
}
.featured-posts-list a:hover { color: var(--accent); }

.month-heading {
  font-size: 1.8rem;
  font-weight: 600;
  color: var(--fg-dim);
  line-height: 1.2;
  margin: 4.5rem 0 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}
.month-heading:first-child { margin-top: 1.25rem; }

.post-card-full { padding: 1.1rem 0; border-bottom: 1px solid var(--border); }
.post-card-full a { color: var(--fg); text-decoration: none; font-size: 1.35rem; font-weight: 600; }
.post-card-full time { display: block; color: var(--fg-dim); font-size: 0.95rem; margin-top: 0.3rem; }
.post-card-full .tag-list { justify-content: flex-start; margin-top: 0.6rem; }
.post-card-full .tag { font-size: 0.85rem; padding: 0.2rem 0.7rem; }
.draft-badge {
  display: inline-block;
  margin-left: 0.5rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  border: 1px solid #d98c3f;
  color: #d98c3f;
  border-radius: 1rem;
  padding: 0.05rem 0.5rem;
  vertical-align: middle;
}

/* admin-only, shown on a post's own page (see posts/show.html.erb) -
   tag-shaped like .tag, but a fixed yellow rather than a theme color,
   so it always reads as "editorial action", not page content */
.edit-pill {
  display: inline-block;
  margin-top: 1rem;
  font-size: 0.85rem;
  border: 1px solid #e6b800;
  color: #e6b800;
  border-radius: 999px;
  padding: 0.25rem 0.8rem;
  text-decoration: none;
  transition: background 0.15s ease, color 0.15s ease;
}
.edit-pill:hover { background: #e6b800; color: var(--bg); }

/* the about page has no header block (no title/date/tags) to nest its
   edit-pill inside like a post does - this is that same centered
   "header area" treatment, just for a single standalone action */
.about-actions { text-align: center; margin-bottom: 1.5rem; }
.about-actions .edit-pill { margin-top: 0; }

.load-more-row { text-align: center; margin: 1.5rem 0; }
.load-more-btn {
  font: inherit;
  background: var(--bg-raised);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.6rem 1.4rem;
  cursor: pointer;
}
.load-more-btn:hover { border-color: var(--accent); }

/* Error pages (app/controllers/errors_controller.rb) - rendered inside
   the normal layout with the homepage's own header shape, this is just
   the empty-state content that replaces the post list. Terminal
   illustration matches the site's established "stylized terminal"
   motif (the featured-posts list's "›" bullets, the code-block
   headers) rather than a generic broken-robot/lost-astronaut image. */
.error-panel { text-align: center; padding: 2rem 0 3rem; }
.error-illustration { display: block; width: 100%; max-width: 30rem; margin: 0 auto 1.5rem; }
.error-illustration-frame { fill: none; stroke: var(--border); stroke-width: 2; }
.error-illustration-dot { fill: var(--fg-dim); }
.error-illustration-line {
  font-family: var(--font-code);
  font-size: 0.85rem;
  fill: var(--fg);
}
.error-illustration-error { fill: var(--accent); }
.error-illustration-cursor { fill: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
  .error-illustration-cursor { animation: error-cursor-blink 1.1s steps(1) infinite; }
}
@keyframes error-cursor-blink { 50% { opacity: 0; } }

.error-message { color: var(--fg-dim); font-size: 1.05rem; margin: 0 0 1.5rem; }
.error-back-link {
  display: inline-block;
  font: inherit;
  text-decoration: none;
  background: var(--bg-raised);
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.6rem 1.4rem;
}
.error-back-link:hover { border-color: var(--accent); }

.marginalia-trigger { border-bottom: 2px dotted var(--accent); cursor: pointer; color: inherit; }
.marginalia-trigger.triggered { color: var(--accent); }

/* clicking a TOC entry jumps here via a plain #anchor link - without
   this, the sticky header would cover the top of whatever heading was
   just scrolled to, since the header stays pinned at the same y
   position the browser is trying to scroll the heading up to. */
h2, h3, h4, h5 { scroll-margin-top: calc(var(--header-h) + 1rem); }

/* smooth-scrolls both real #anchor navigation (a TOC link, "back to
   posts") and any JS-driven scrollIntoView/scrollTo call ("back to
   top") through one shared browser mechanism, rather than needing a
   JS smooth-scroll implementation for the plain anchor links too. */
html { scroll-behavior: smooth; }

/* ---- mobile: side columns collapse behind a menu button + shared drawer ---- */
@media (max-width: 880px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-areas:
      "header"
      "main";
  }
  .col-left, .col-right { display: none; }
  .col-main-inner { padding: 1.25rem; max-width: none; }
  .menu-button { display: inline-flex; }

  /* header: only the mandated-always-visible controls (theme, lang,
     gear) stay directly in the header - About/GitHub/search fold into
     the gear dropdown instead, there's not enough width otherwise */
  .site-header-right .header-desktop-only { display: none; }
  .mobile-only { display: block; }

  /* 48ch can land close to (or past) a phone's actual viewport width,
     leaving little to no visible margin even once width: fit-content
     and overflow-wrap: break-word (see .post-excerpt) are doing their
     job - a percentage guarantees real breathing room on both sides
     regardless of how many px a "ch" happens to work out to. */
  .post-excerpt { max-width: 90%; }
  .post-content thead th, .post-content tbody td { padding: 0.5rem 0.75rem; }
}

/* shared drawer component - used for BOTH mobile TOC/post-list menu
   AND mobile marginalia (same bottom sheet, two different triggers).
   Kept always in the DOM (not [hidden]) so transform/opacity can
   transition smoothly on open/close. */
.drawer-overlay {
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 60%);
  z-index: 199;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.drawer-overlay.open { opacity: 1; pointer-events: auto; }
.drawer {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 70vh;
  overflow-y: auto;
  background: var(--bg-raised);
  border-top: 1px solid var(--border);
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -12px 32px rgb(0 0 0 / 25%);
  padding: 1rem 1.25rem;
  z-index: 200;
  transform: translateY(100%);
  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
.drawer.open { transform: translateY(0); }
.drawer-handle { width: 36px; height: 4px; background: var(--border); border-radius: 2px; margin: 0 auto 1rem; }
.drawer-close {
  position: absolute;
  top: 0.75rem;
  right: 1rem;
  background: none;
  border: none;
  color: var(--fg-dim);
  font-size: 1.3rem;
  cursor: pointer;
}

/* ==========================================================================
   Misc - not from the prototype, existing site chrome
   ========================================================================== */
.flash-notice {
  color: green;
}

.flash-alert,
.flash-error,
.form-errors {
  color: red;
}

.preview-pane {
  border: 1px solid;
  padding: 1rem;
  margin-top: 0.5rem;
}

.language-switch a.active {
  font-weight: bold;
  text-decoration: none;
}

.translation-notice {
  font-style: italic;
}
