/* ── ECHO blog — the frosted reading sheet ─────────────────────────────────
   Linked ONLY by blog.html and blog-*.html, after echo.css. Nothing else on
   the site loads this file, and nothing in it is written to leak: every
   selector is either a blog-only class (.readsheet, .bgfrost) or is qualified
   by one.

   WHY A SHARED FILE AND NOT PAGE-LOCAL <style>: there are two blog pages today
   and there will be one more every time a post is written. The reading sheet
   is the format's voice - a post that shipped with its own hand-copied blur
   would drift from the index within two posts. One file, one <link>, and a new
   post inherits the treatment by existing.

   ── THE STANDING RULE THIS OVERRIDES ─────────────────────────────────────
   echo.css's --panel comment sets the site-wide rule: every content panel
   takes var(--panel) and carries NO backdrop-filter, because --glass-2 over
   the animated rain smeared it into a uniform fog. That rule still holds for
   the whole site AND for the blog's own cards.

   The OPERATOR EXPLICITLY RE-OPENED IT for blog reading panels on 2026-08-19:
   "For the blogs, I need a soft blur applied as a panel to improve the
   readability of the text. I still want the background to remain active."
   The blur below is that override, scoped to blog pages only. DO NOT STRIP IT
   as a stray violation of the --panel rule - it is the exception the rule's
   owner asked for. The fog failure mode is avoided by pairing the blur with a
   LOWER alpha than --panel, not the same or higher one (see --sheet-fill).

   WHAT IS STILL BLUR-FREE, deliberately: .bcard (the post/write-up cards on
   the index), .pull, .cbody, figure.fig. One backdrop-filter per page, never
   nested - this machine's GPU stack is fragile and stacked backdrop passes
   over a live canvas are exactly what has cost frames here before. */

:root{
  /* 6px. Deliberately the same tier as echo.css's --glass-2 (blur(6px)
     saturate(1.1)) and deliberately NOT a reference to it: --glass-2 lives in a
     file this page does not own, and a future radius bump there would silently
     raise the per-frame cost of a blur running over an animated canvas. The
     number is pinned here so the blog's GPU budget is decided in the blog's own
     file. 6px is also enough to turn a rain glyph into a smudge and not enough
     to turn the field into a flat wash - the operator asked for the background
     to remain ACTIVE, and a legible-as-motion field is the whole point. */
  --sheet-blur:blur(6px) saturate(1.08);

  /* .58, which is LOWER than --panel's .72 - and that inversion is the fix for
     the exact failure echo.css documents. The old site-wide combination was a
     high alpha AND a blur: the blur turned sparse glyphs into an even wash and
     the alpha then buried that wash, so the panel read as flat fog. Here the
     blur does the readability work (it destroys the hard edges the eye trips
     on) and the alpha steps back to let the softened field stay visible
     through the sheet. Measured against the worst pixel - a bright matrix lead
     glyph directly under a --muted body letter - .58 + 6px still clears 4.5:1,
     because after the blur the glyph is no longer a bright pixel. */
  --sheet-fill:rgba(10,15,17,.58);

  /* The in-panel veil on blog.html's hero, which is a tint FIRST and a blur
     second - see the .bgfrost block for why it cannot be the other way round. */
  --sheet-veil:rgba(10,15,17,.24);
}

/* Light theme is DERIVED from --surf rather than written as a literal, for the
   same reason echo.css derives its own light --panel: the light palette is
   declared only in index.html's <style>, so a visitor who flipped to light on
   the homepage arrives here with data-theme=light set and no light palette
   behind it. A literal near-white here would paint a white sheet under dark
   text on exactly those visits; deriving from --surf keeps the dark surface
   until the palette itself is shared. On index, where the palette IS light,
   these compute against #f7fafa. The +6 on the fill mirrors --panel's own
   dark→light step: paper needs a few more points to read as a sheet at all. */
:root[data-theme=light]{
  --sheet-fill:color-mix(in srgb,var(--surf) 64%,transparent);
  --sheet-veil:color-mix(in srgb,var(--surf) 24%,transparent);
}

/* THE FALLBACK, and it is the whole reason the alpha above could go down.
   Without backdrop-filter the sheet is just a translucent rectangle over live
   rain, and .58 is not enough surface to read 1,800 words through. So the
   no-blur branch raises the fill back to var(--panel) - the site's own
   standard panel opacity, .72 dark / .78 light - and the page degrades into
   exactly the panel every other page already ships. Nothing else changes:
   geometry, edge, padding and corner tick are all outside this query.
   Both prefixes are tested, because a UA that supports only the -webkit- form
   still gets a real blur and must NOT take this branch. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  :root{ --sheet-fill:var(--panel); --sheet-veil:rgba(10,15,17,.34); }
  /* --sheet-fill has to be RESTATED here, not inherited from the :root above:
     the normal branch's :root[data-theme=light] rule is specificity (0,2,0) and
     would beat a (0,1,0) :root no matter which one comes later in the file, so
     a light-theme visitor on a browser without backdrop-filter would otherwise
     keep the .64 frosted alpha and never get the .78 the paragraph above
     promises. Same trap applies to anything else added to the block. */
  :root[data-theme=light]{ --sheet-fill:var(--panel);
    --sheet-veil:color-mix(in srgb,var(--surf) 30%,transparent); }
}


/* ── 1. the post page: the article IS the sheet ────────────────────────────
   blog-*.html post pages hang their body copy straight off <main>, on no
   surface at all. That was not only a contrast problem, it was a PAINT-ORDER
   bug: #pulsebg is position:fixed with z-index:0, and .post's text was
   unpositioned in-flow content, which paints BELOW positioned z-index:0
   elements. The rain was drawing on TOP of the prose - visible immediately in
   any dense mode (bg=matrix), where glyphs cut straight through the title.
   position:relative + z-index:1 here lifts the whole article over #pulsebg and
   fixes that independently of the blur; the sheet then handles contrast.

   ONE ELEMENT CARRIES EVERYTHING. The blur is on the article, not on
   .post-body, not on .part/.refs/.contact, and not on .pull or .cbody inside
   it - those keep plain var(--panel) and compose over this sheet the way
   echo.css says nested panels are meant to. That keeps the page at exactly one
   backdrop pass.

   WIDTH: 800px, not the full .epage column. .post-body's own 74ch measure is
   ~640px, so the sheet clears the text by ~35px a side and still leaves ~100px
   of live, unblurred background breathing down each margin at 1280. The
   operator asked for the background to stay active; a viewport-wide sheet
   would leave it nothing to be active in. */
.post.readsheet{
  position:relative;z-index:1;
  max-width:min(100%,800px);margin:0 auto;
  padding:28px clamp(20px,4vw,44px) 44px;
  background:var(--sheet-fill);
  -webkit-backdrop-filter:var(--sheet-blur);
  backdrop-filter:var(--sheet-blur);
  /* Neutral --line, 2px, same as .bcard and .pull. NOT the hero's teal bezel:
     the no-accent-coloured-borders rule applies to new work, and the .ehero
     ring is grandfathered, not a precedent. */
  border:2px solid var(--line);
  clip-path:polygon(0 16px,16px 0,calc(100% - 16px) 0,100% 16px,
    100% calc(100% - 16px),calc(100% - 16px) 100%,16px 100%,0 calc(100% - 16px));
}
/* the etched L-tick every panel on this site wears, so the new sheet reads as
   one of them rather than as a foreign box dropped on the page */
.post.readsheet::before{content:"";position:absolute;top:13px;left:13px;width:11px;height:11px;
  border-top:1.5px solid var(--edge);border-left:1.5px solid var(--edge);opacity:.9;pointer-events:none}

/* The post's own header sat on open page and centred itself against the full
   column; inside the sheet it centres against the sheet, which is what the
   1.7-2.7rem title wants. Nothing else in the post's layout moves - this is a
   readability layer, not a re-typeset. */
.post.readsheet .post-open{padding-top:4px}

@media (max-width:720px){
  /* On a phone the sheet is already edge-to-edge inside .epage's 16px gutter,
     so the corner tick is the only thing left to pull in. */
  .post.readsheet{padding:22px 18px 34px}
  .post.readsheet::before{top:10px;left:10px;width:9px;height:9px}
}


/* ── 2. blog.html's hero: the veil that sits ON the bounded field ──────────
   The index's hero mounts its own container-sized copy of the background
   (window.echoBg.mount → .bgfield, z-index:-1) directly under the lede. A
   backdrop-filter on .ehero itself CANNOT touch that: an element's backdrop is
   what is painted BEHIND it, and .bgfield is a child painted in front of the
   hero's surface. Blurring .ehero would blur the page's #pulsebg and leave the
   in-panel field - the one actually under the words - perfectly sharp.

   So the blur goes on a layer BETWEEN the field and the content:

     .ehero surface → .bgfield (z-index:-1) → .bgfrost (z-index:-1, later in
     tree order so it paints above) → ::before bezel (z:0) → content (z:1)

   Same z-index as .bgfield and later in the DOM, which is what puts it on top
   of the field and still under the etched bezel - the same reasoning .bgfield's
   own comment gives for choosing -1 over 0. Selector is .ehero > .bgfrost
   (specificity 0,2,0) to beat echo.css's .ehero>*{z-index:1}, deliberately by
   specificity rather than by source order.

   THE TINT IS DOING MORE WORK THAN THE BLUR HERE, and that is on purpose.
   .ehero has filter:drop-shadow(), which makes it a backdrop root: this layer
   can only ever sample what is painted inside the hero, so the blur reaches the
   .bgfield and stops. But the layer's own tint paints over the hero's
   TRANSLUCENT surface, so it also cuts the page rain bleeding through
   var(--panel) from ~28% to ~21% - the larger of the two noise sources over
   that lede. Blur softens the inside field, tint suppresses the outside one,
   one composited layer, one backdrop pass.

   .24 IS A MEASURED NUMBER, not a guess: rendered at ?bg=matrix (the densest
   mode, i.e. the worst case) at .18, .24 and .28. At .28 the hero interior went
   completely flat - the field stopped existing, which fails the half of the ask
   that says the background must stay ALIVE. At .18 the field was clearly there
   but so was more of the unblurred page rain. .24 is where a soft wash of the
   field is still visible in the panel's open corners while nothing hard-edged
   survives under the lede.

   THE LIMIT, so nobody re-derives it: the page's own #pulsebg CANNOT be blurred
   from in here, only tinted down, because .ehero's drop-shadow closes the
   backdrop root around this layer. Blurring .ehero from outside would reach it
   but is the exact combination echo.css reverted site-wide (blur behind a .72
   panel = flat fog), and it would leave .bgfield sharp on top anyway. The
   in-panel layer is the version of this that improves both without re-opening
   that argument. */
.ehero > .bgfrost{position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:var(--sheet-veil);
  -webkit-backdrop-filter:var(--sheet-blur);
  backdrop-filter:var(--sheet-blur);
  /* its own copy of .ehero's 18px octagon, same as .bgfield carries, so the
     veil is cut to the panel's corners instead of trusting the parent clip */
  clip-path:polygon(0 18px,18px 0,calc(100% - 18px) 0,100% 18px,
    100% calc(100% - 18px),calc(100% - 18px) 100%,18px 100%,0 calc(100% - 18px))}


/* ── static backgrounds: nothing to special-case, and here is why ──────────
   Two things already stop the motion: the visitor picking Plain in the settings
   panel (rain.js paints that mode once per size/theme change, never on a
   timer), and prefers-reduced-motion, which blog.html pins its mounted field to
   'plain' under. A blur over a field that is not repainting costs one composite
   and then nothing, so there is no GPU reason to drop it, and the sheet reads
   the same on a still field as on a moving one - a soft wash instead of soft
   motion. Verified at ?bg=plain, both pages.

   The one thing worth naming for whoever comes next: reduced motion does NOT
   stop the page's full-viewport #pulsebg. That is pre-existing rain.js
   behaviour, out of scope for a readability pass, and the sheet's fill is what
   covers for it in the meantime. */

/* ══════════════════════════════════════════════════════════════════════════
   PRINT / PDF
   Added 2026-08-19. The ACE-T write-ups had no download because every PDF copy
   of those reports carries the candidate's legal name on the exam cover block -
   which is the whole reason that page was re-set from the submitted text in the
   first place. The fix is NOT to redact the original PDFs (redaction leaks;
   text survives under black boxes). It is to render THE PAGE, which is scrubbed
   by construction because the identity fields were never typed into it.

   This block is what makes that render publishable. Without it, print-to-PDF
   captures the nav bar, the animated background canvas and the footer row -
   a screenshot of a website rather than a report.

   It is in blog.css deliberately, not on one page: every blog-*.html links this
   file, so all of them - CVP, boroCTF, ACE-T, the rack build log - gain a clean
   PDF from the same twenty lines. There was no @media print anywhere on this
   site before this.
   ══════════════════════════════════════════════════════════════════════════ */
@media print {
  /* site chrome has no meaning on paper */
  #pulsebg, #echonav, .srail, .backrow .erow, .topctrl, #topctrl { display: none !important; }
  /* the download row offers the very file being printed - a PDF containing a
     button that says "download this PDF" is a small absurdity, so it goes */
  .dl { display: none !important; }

  /* the reading sheet is a screen affordance - a frosted panel over an animated
     field. On paper it is just a grey box that costs ink and contrast. */
  html, body { background: #fff !important; color: #111 !important; }
  .post, .post.readsheet {
    background: none !important; backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    border: 0 !important; box-shadow: none !important;
    max-width: none !important; margin: 0 !important; padding: 0 !important;
    clip-path: none !important;
  }
  .epage { padding: 0 !important; max-width: none !important; }

  /* ink-on-paper: the site's dark palette inverts to readable defaults. Tokens
     are overridden rather than every rule rewritten. */
  :root {
    --text: #111; --muted: #333; --dim: #666; --frame-soft: #444;
    --line: #bbb; --line-soft: #ddd; --edge: #999;
    --bg: #fff; --bg2: #fff; --surf: #f4f4f4; --panel: #fff;
    --echo: #0a6a58; --echo-deep: #0a6a58; --warn: #8a5a00; --alert: #a03028;
  }

  /* keep the evidence readable: tables, code and flags must not be clipped by a
     page break, and a table that scrolls on screen must wrap on paper. */
  .tw { overflow: visible !important; }
  .tw table { min-width: 0 !important; }
  pre.code, .flag, .tw, figure, .plate { break-inside: avoid; page-break-inside: avoid; }
  .chal, .vol { break-inside: auto; }
  h1, h2, h3, .post-title { break-after: avoid; page-break-after: avoid; }

  /* a printed link that says "click here" is useless - show where it goes,
     except for in-page anchors, which mean nothing on paper. */
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .82em; color: #555; }
  a[href^="#"]::after { content: ""; }

  /* the mark prints as a solid glyph, not a glowing one */
  .post-mark { filter: none !important; }
}
