/*
 * 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;
}
:root[data-theme="green"] {
  /* old-CRT/phosphor-terminal look, not just "dark theme, green
     accent": true black background, body text itself is the
     terminal green (not white/gray), accent is a slightly hotter
     green for that glowing-highlight feel */
  color-scheme: dark;
  --bg: #000000; --bg-raised: #0a0f0a; --fg: #29cc29; --fg-dim: #1f8f1f; --border: #1a331a;
  --accent: #39ff14;
}
* { 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: 1.15rem;
  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;
  display: flex;
  align-items: center;
  justify-content: space-between;
  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;
}
.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; }
.config-dropdown {
  position: absolute;
  top: calc(100% + 0.75rem);
  right: 0;
  min-width: 13rem;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.75rem;
  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;
  transition: transform 0.15s ease, opacity 0.15s ease;
}
.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; }
.config-search { margin: 0.4rem 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; }
.dot-green { background: #000000; border: 1px solid #33ff33; }

.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 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. */
.marginalia-placeholder { transition: opacity 0.25s ease; }
.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 {
  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;
  cursor: pointer;
}
.side-action:hover { text-decoration: underline; }
.side-divider { border: none; border-top: 1px solid var(--border); margin: 0.75rem 0; }

.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); }

.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%; }
}

/* 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;
}
