/* N5HQ edge draft — piece 4 "Subpage chrome"
   Self-contained: does NOT rely on instruments.css. Ports the exact corner
   chrome (wordmark lockup, role label, link pills, social row) from
   src/p3/home.html plus the atoms it borrowed from instruments.css
   (.n5i-cluster/.n5i-label/.n5i-dim/.n5i-mark/.n5i-lockup family), then adds
   the subpage content column per design-system.md "Layout (subpage)".
   Tokens, faces, spacing: carbon / bone / vein / glow / violet. Weight 400
   everywhere. No shadows, no cards, no gradients. */

:root{
  --carbon:#12130f;
  --bone:#e4dfda;
  --bone-55: rgba(228,223,218,.55);
  --vein:#3c3c38;
  --glow:#a493ff;
  --violet:#5e18eb; /* brand violet, sampled from the logo; hover fill + rim only */
  --n5i-mono:'JetBrains Mono','IBM Plex Mono',ui-monospace,Menlo,monospace;
  --n5i-sans:'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
  --n5i-display:'Doto','Jersey 10',system-ui,monospace;

  /* ---------- fluid edge-draft scaling (2026-09-08/09, insets re-anchored 2026-09-09) ----------
     Byte-identical copy of this block lives in p3/home.html's inline :root
     — see design-system.md "Fluid scaling" for the full derivation and the
     drift-risk note.
     --n5i-lockup-w / --n5i-u stay anchored at 1440x900 (today, must stay
     byte-identical — the clamp's ramp term is `100vw - 1440px`, exactly 0
     at that width regardless of slope rounding) up to 2504px wide (Eric's
     spec: lockup ~300px, everything else 1.35x); below 1440 both floor at
     their 1440 value (147px / 1px).
     --n5i-inset-x / --n5i-inset-y are anchored differently (Eric,
     2026-09-09: "keep the border parameters in place at all sizes, the
     threshold for the no borders is when width < 768"): the ramp term is
     `100vw - 768px`, floored at 768 (16px / 12px, matching the phone
     breakpoint below) and still ceilinged at 2504 (100px / 60px) — so the
     inset never collapses back to a flat 12px between 768 and 1440 the way
     it used to. Slopes are rounded so the raw calc() value at 2504
     slightly OVERSHOOTS 100/60, which is what makes clamp()'s upper bound
     (not the arithmetic) land the value bit-exact at 2504.
     --n5i-u is a LENGTH (1px..1.35px) so `calc(N * var(--n5i-u))` type-
     checks as number*length=length; hairlines (1px borders/rules) are
     deliberately NOT multiplied by it. */
  --n5i-inset-x: clamp(16px, calc(16px + (100vw - 768px) * 0.048388), 100px);
  --n5i-inset-y: clamp(12px, calc(12px + (100vw - 768px) * 0.027650), 60px);
  --n5i-lockup-w: clamp(147px, calc(147px + (100vw - 1440px) * 0.143797), 300px);
  --n5i-u: clamp(1px, calc(1px + (100vw - 1440px) * 0.000329), 1.35px);
}

*{ box-sizing:border-box; margin:0; padding:0; }

html,body{
  width:100%;
  background:var(--carbon);
  color:var(--bone);
  font:400 calc(16 * var(--n5i-u))/1.25 var(--n5i-sans);
  -webkit-font-smoothing:antialiased;
  /* Freeze-pane layout (2026-09-11): main.page becomes the only scrolling
     region at desktop widths (see "content column" below) — the document
     itself must never scroll, so the corners it's pinned beside never move.
     Reopened to a normal scrolling document below 768px (see the phone
     media query near the bottom of this file) — a viewport-pinned,
     internally-scrolling pane is the wrong call on a phone; the corners
     already flatten back into normal flow there. */
  overflow:hidden;
  height:100vh;
}
body{ position:relative; height:100vh; }

/* ---------- skip link ---------- */
.skip-link{
  position:absolute;
  left:-9999px;
  top:0;
  z-index:10;
  background:var(--carbon);
  color:var(--bone);
  padding:calc(8 * var(--n5i-u)) calc(16 * var(--n5i-u));
  font:400 calc(14 * var(--n5i-u))/1.25 var(--n5i-sans);
  text-decoration:underline;
}
.skip-link:focus{ left:var(--n5i-inset-x); top:var(--n5i-inset-y); }

/* ---------- corners (ported from home.html, unchanged) ---------- */
.corner{
  position:fixed;
  z-index:2;
  padding:var(--n5i-inset-y) var(--n5i-inset-x);
  /* load choreography: readable at rest (opacity .15, not 0) as a static
     fallback before JS runs; subpage.js settles this to full via WAAPI. */
  opacity:.15;
  transform:translateY(10px);
}
.tl{ top:0; left:0; }
.tr{ top:0; right:0; }

.role{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
  margin-top:calc(4 * var(--n5i-u));
  white-space:nowrap;
}

/* ---------- instruments.css atoms the corner needs (ported, not linked) ---------- */
.n5i-cluster{ display:flex; flex-direction:column; gap:calc(4 * var(--n5i-u)); }
.n5i-label{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  white-space:nowrap;
}
.n5i-dim{ opacity:.55; }

.n5i-mark{
  font:400 calc(40 * var(--n5i-u))/0.70 var(--n5i-display);
  text-decoration:none;
  color:inherit;
  display:inline-block;
  padding-top:calc(6 * var(--n5i-u));
}
/* Logo lockup: still SVG at rest; the mark itself carries both colour states now (no more
   canvas keying or clip video decode). Width tracks the role label under it at
   rest (min-width sits below the label's own max-content width at 1440, so
   nothing changes there); above ~1441px min-width becomes the corner's
   shrink-to-fit driver and carries the lockup to --n5i-lockup-w (~300px at
   2504) — width stays 100% (never set height). */
.n5i-lockup{ position:relative; display:block; width:100%; min-width:var(--n5i-lockup-w); aspect-ratio:1698/804; height:auto; padding-top:0; line-height:0; }
.n5i-lockup .lk-rest,.n5i-lockup .lk-hover{position:absolute;inset:0;width:100%;height:100%;display:block;object-fit:contain}
.n5i-lockup .lk-hover{opacity:0}
/* neon strike (Eric, 2026-09-24): mouseenter/focus adds .lk-neon, which runs the ~.8s tube-flicker
   keyframes below on the violet hover mark (rest hides underneath); JS swaps .lk-neon -> .lk-hov
   on animationend, which just holds the violet mark lit, glow settled. mouseleave/blur strip both
   classes synchronously — there is no exit animation, so a re-enter always restarts the strike clean. */
.n5i-lockup.lk-armed.lk-neon .lk-rest,.n5i-lockup.lk-armed.lk-hov .lk-rest{opacity:0}
.n5i-lockup.lk-armed.lk-hov .lk-hover{opacity:1}
.n5i-lockup.lk-armed.lk-neon .lk-hover{animation:n5i-neon-strike .8s steps(1,end) forwards}
@keyframes n5i-neon-strike{
  0%{opacity:0;filter:drop-shadow(0 0 0 var(--violet,#5e18eb))}
  8%{opacity:.9;filter:drop-shadow(0 0 6px var(--violet,#5e18eb))}
  18%{opacity:.2;filter:drop-shadow(0 0 1px var(--violet,#5e18eb))}
  32%{opacity:1;filter:drop-shadow(0 0 10px var(--violet,#5e18eb))}
  46%{opacity:.5;filter:drop-shadow(0 0 2px var(--violet,#5e18eb))}
  60%{opacity:1;filter:drop-shadow(0 0 14px var(--violet,#5e18eb))}
  100%{opacity:1;filter:drop-shadow(0 0 4px var(--violet,#5e18eb))}
}
@media (prefers-reduced-motion:reduce){.n5i-lockup .lk-hover{animation:none}.n5i-lockup .lk-rest{opacity:1!important}.n5i-lockup:hover .lk-hover,.n5i-lockup:focus-visible .lk-hover{opacity:1}.n5i-lockup:hover .lk-rest,.n5i-lockup:focus-visible .lk-rest{opacity:0!important}}

/* ---------- pill (ported from home.html, unchanged mechanics) ---------- */
.pill{
  position:relative;
  overflow:hidden;
  display:inline-block;
  padding:calc(3 * var(--n5i-u)) calc(11 * var(--n5i-u));
  border-radius:9999px;
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  text-decoration:none;
  color:var(--bone);
  background:rgba(18,19,15,.4);
  -webkit-backdrop-filter:blur(8px);
  backdrop-filter:blur(8px);
  border:1px solid transparent;
  cursor:pointer;
  transition:border-color 0s;
}
.pill-l{ position:relative; display:inline-block; }
.pill-c{
  position:absolute;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  background:var(--violet);
  color:#fff;
  border-radius:9999px;
  transform:translateY(120%);
  transition:transform .25s cubic-bezier(0,.55,.45,1);
}
.pill:focus-visible{ outline:2px solid var(--bone); outline-offset:2px; }
.pill:focus-visible{ border-color:var(--violet); transition:border-color .5s cubic-bezier(.4,0,.2,1); }
.pill:focus-visible .pill-c{ transform:none; }
@media (hover:hover){
  .pill:hover{ border-color:var(--violet); transition:border-color .5s cubic-bezier(.4,0,.2,1); }
  .pill:hover .pill-c{ transform:none; }
}
@media (prefers-reduced-motion: reduce){
  .pill, .pill-c{ transition:none; }
}
/* current-page pill: the hover state, permanently */
.pill[aria-current="page"]{ border-color:var(--violet); }
.pill[aria-current="page"] .pill-c{ transform:none; }

/* Header nav (Eric, 2026-09-08, reverted to vertical the same day: "the nav bar being
   flat was also a mistake, let's get the original vertical style layout like the one
   on the homepage to the current top left region" — disambiguated to top-RIGHT, since
   that's where the header nav already lives; only the internal arrangement changes).
   Ported byte-for-byte from p3/home.html's .br nav (About/Projects/Blog/Contact pills,
   then the social icon row, then the .n5i-nav-rule divider, then Login — same 4px nav
   gap, same rule geometry, same socials margin) — top-right instead of bottom-right, so
   it grows downward from the top edge instead of upward from the bottom. Markup is now
   flat children of <nav> (no .n5i-pills wrapper), matching home exactly. */
/* Eric, 2026-09-09: the nav links do not scale — they were the right size at the
   1440 reference and grow out of proportion on a large screen. Resetting the scale
   unit for the nav's subtree reverts every value inside it (pill padding and type,
   the 23px icon discs, the divider, the gaps) to its reference size in one place,
   while the corner's own inset still holds it 100px off the edge. Pills used
   elsewhere — accordion triggers, the contact mailto, About's closing link — are
   content rather than chrome and keep scaling. */
.tr nav{ --n5i-u:1px;
  display:flex;
  flex-direction:column;
  align-items:flex-end;
  gap:calc(4 * var(--n5i-u));
}
/* separates the four page pills from the Login pill below it — ported from home's
   .n5i-nav-rule, same Vein divider token and geometry. Height is a 1px hairline,
   deliberately not scaled. */
.n5i-nav-rule{
  width:calc(28 * var(--n5i-u));
  height:1px;
  background:var(--vein);
  margin:calc(12 * var(--n5i-u)) calc(25 * var(--n5i-u)) calc(12 * var(--n5i-u)) 0;
}
.n5i-socials{
  display:flex;
  align-items:center;
  justify-content:flex-end;
  gap:calc(4 * var(--n5i-u));
  margin:calc(-2 * var(--n5i-u)) calc(6 * var(--n5i-u)) 0 0;
}
.pill.ico{
  padding:0;
  width:calc(23 * var(--n5i-u));
  height:calc(23 * var(--n5i-u));
  display:inline-flex;
  align-items:center;
  justify-content:center;
  border-radius:50%;
}
.pill.ico .pill-l{ display:inline-flex; opacity:.7; }
.pill.ico .pill-c{ border-radius:50%; }
.pill.ico:hover .pill-l, .pill.ico:focus-visible .pill-l{ opacity:1; }

/* pill-lg — the one text pill on /contact/: 16px Inter, not uppercase */
.pill-lg{
  font:400 calc(16 * var(--n5i-u))/1.25 var(--n5i-sans);
  letter-spacing:normal;
  text-transform:none;
  padding:calc(8 * var(--n5i-u)) calc(18 * var(--n5i-u));
}

@media (prefers-reduced-motion: reduce){
  .corner{ opacity:1; transform:none; transition:none; }
}

/* ---------- content column ---------- */
/* margin-left/-right are DERIVED, not magic numbers (2026-09-09) — they must
   never be left fixed once the corner insets/lockup scale, or the column
   collides with a 300px logo or runs under the nav (see design-system.md
   "Fluid scaling"). Left = 2x the corner's own horizontal inset + the
   lockup's width (i.e. the .tl corner's own shrink-to-fit width, so the
   column can never sit under it by construction) + a scaled breathing gap
   (53px @1440 -> 224px total, matching today exactly; 71.55px @2504 ->
   571.55px total).
   Right = ALSO 2x the corner inset (matching the .tr corner's own
   shrink-to-fit box, which is 2*inset-x + its intrinsic nav content —
   both sides use --n5i-inset-x, same as .tl) + a scaled gap standing in for
   that intrinsic nav content — 176px @1440 -> 200px total (today's value,
   unchanged), 437.6px @2504 -> 437.6px total. First cut of this formula
   used only 1x inset-x and shrank the clearance at wide viewports (88px at
   1440 down to ~30px at 2504) because the .tr corner's own box gains a
   SECOND inset-x on its far side that a single-inset-x margin never
   accounted for — caught by Playwright measurement (measure3.mjs) at
   2504/3440, not by inspection. With 2x inset-x on both sides of this
   comparison, clearance is monotonic: ~88px (1440) -> ~103px (1920) ->
   ~114px (2504 and beyond, both tokens maxed) — verified against the
   nav's actual rendered width, not assumed. */
main.page{
  position:relative;
  z-index:1;
  margin-left:calc(2 * var(--n5i-inset-x) + var(--n5i-lockup-w) + 53 * var(--n5i-u));
  margin-right:calc(2 * var(--n5i-inset-x) + 176 * var(--n5i-u));
  padding:calc(96 * var(--n5i-u)) 0 calc(128 * var(--n5i-u));
  opacity:.15;
  transform:translateY(10px);
}
@media (prefers-reduced-motion: reduce){
  main.page{ opacity:1; transform:none; }
}

/* ---------- freeze-pane geometry (2026-09-11) ----------
   main.page becomes a bounded, internally-scrolling viewing pane instead of
   part of the scrolling document — https://dimension.dev/'s reference: a
   pinned chrome column beside a bounded pane, nothing else ever scrolls.
   `inset:0` pins the box to all four viewport edges; the SAME derived
   margin-left/margin-right above still apply on a fixed box with opposite
   offsets set (standard CSS box-model behaviour), so the pane's visible
   edges land exactly where the content column always sat — only the
   scrolling mechanism changed, not the geometry. Desktop-only: below 768px
   the corners flatten into normal flow (existing max-width:767px block)
   and the document itself is the right thing to scroll, so this block is
   gated the same way. */
@media (min-width:768px){
  main.page{
    position:fixed;
    inset:0;
    overflow-y:auto;
    overscroll-behavior-y:contain;
    -webkit-overflow-scrolling:touch;
    /* NO border-left. A 1px Vein leading edge was added here 2026-09-11 so the
       pane would read as a distinct region; Eric removed it the same day — the
       body text sat directly on the rule and looked propped against it. The
       pane is defined by its own scroll behaviour and the whitespace around it,
       not by a drawn edge. Do not reinstate a border here without asking. */
    /* Subtle, Vein-toned scrollbar — never the violet accent. */
    scrollbar-width:thin;
    scrollbar-color:var(--vein) transparent;
  }
  main.page::-webkit-scrollbar{ width:8px; }
  main.page::-webkit-scrollbar-track{ background:transparent; }
  main.page::-webkit-scrollbar-thumb{ background:var(--vein); border:2px solid transparent; background-clip:content-box; }
}

.page .label{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
}
/* .post-title-h carries the page-heading type at whatever level the
   context needs: a post embedded under the blog landing's own h1 renders as
   an h2, a post opened on its own stays an h1. Type follows the class, not
   the tag, so the outline can be correct without the design changing. */
.page h1, .page .post-title-h{
  font:400 calc(30 * var(--n5i-u))/1.1 var(--n5i-sans);
  letter-spacing:-0.02em;
  color:var(--bone);
  margin-top:calc(4 * var(--n5i-u));
}
/* Page-identity headers (Eric, 2026-09-09: "the header of every page should be
   using the Bigger font, same one as the clock on home page") -- About/Projects/
   Blog's own <h1> (Eric L. / XP Farm / Tech Journey Journal), via .page-title.
   Extended the same day to every blog POST title too (.post-title-h, added by
   setHeadingLevels() in subpage.js to whichever post is currently shown, h1 or
   demoted h2, plus post.html's own static <h1> -- see the class on that markup):
   Eric's reasoning is that a selected post becomes the page's own first segment
   at that point, the same role .page-title plays elsewhere, so it should carry
   the same type. Bigger Display is capitals-and-digits-only (see the @font-face
   below and its use on the home clock), so text-transform:uppercase is required,
   not decorative -- without it any lowercase glyph the face lacks falls back to
   the next font in the stack mid-word.

   Size: Bigger Display's actual glyph ink sits smaller within its em-box than
   Inter's (same as the difference between typefaces generally, just more visible
   on a display face) -- at the base rule's 30u it read visibly SMALLER than the
   plain Inter heading did before this change, most noticeably on mobile where
   --n5i-u floors at 1px and there's no larger desktop size masking it. Bumped to
   38u so the rendered cap-height reads at least as large as the old Inter 30u
   did, at every width, not just desktop. Still read too small on desktop (Eric,
   2026-09-10: "1.5x bigger") -- 38u * 1.5 = 57u.

   Specificity: `.page h1.page-title` (element+two-classes) and `main.page
   .post-title-h` (element+two-classes) are both one point above the base rule's
   two branches (`.page h1` is class+element; `.page .post-title-h` is two
   classes), so the override always wins regardless of source order -- plain
   `.page .post-title-h` alone would only TIE the base rule's own `.page
   .post-title-h` branch and depend on file order instead. */
@font-face{
  font-family:'Bigger Display';
  src:url(bigger-display.otf) format('opentype');
  font-weight:400;
  font-style:normal;
  font-display:swap;
}
.page h1.page-title,
main.page .post-title-h{
  font-family:'Bigger Display', var(--n5i-sans);
  font-size:calc(57 * var(--n5i-u));
  text-transform:uppercase;
}
.page .lede{
  margin-top:calc(12 * var(--n5i-u));
  max-width:40ch;
}
/* The .page section + .lede reorder rule from 2026-09-09 (Goal repositioned
   between h1 and .lede) is gone: the lede's content was folded into
   Aspirations the same day, so /about/ no longer has a standalone .lede
   element at all. Removed rather than left dead. */
.page p{ max-width:40ch; }

.page section{ margin-top:calc(64 * var(--n5i-u)); }
/* /about/ (Eric, 2026-09-09): "I don't think there needs to be such big gaps between the
   collapsible pill boxes." Scoped to consecutive accordion sections only (a section whose
   first child is an .acc-trigger, immediately following another one) so the gap ABOVE the
   first accordion section (Skills, following the always-visible Goal/HomeLab sections)
   keeps the normal 64px rhythm — only the Skills->Certs and Certs->Background gaps tighten. */
.page section:has(> .acc-trigger) + section:has(> .acc-trigger){ margin-top:calc(24 * var(--n5i-u)); }
.page section > .label{ display:block; }
.page section > h2{
  font:400 calc(16 * var(--n5i-u))/1.25 var(--n5i-sans);
  color:var(--bone);
  margin-top:calc(4 * var(--n5i-u));
}

/* Blog landing (Eric, 2026-09-08): a 24px gap under the "Latest blog" section label,
   matching the .post-list gap under "All posts" — scoped to this one section so the
   breadcrumb's own spacing on a standalone/tree-selected post view (where it's the
   first thing in main.page, flush with the page's own top padding) is untouched. */
#blog-landing-view section{ margin-bottom:calc(24 * var(--n5i-u)); }

.page .list{
  list-style:none;
  max-width:72ch;
  margin-top:calc(24 * var(--n5i-u));
}
.page .list li{
  border-top:1px solid var(--vein);
  padding:calc(12 * var(--n5i-u)) 0;
}
.page .list .row{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:calc(12 * var(--n5i-u));
}
.page .list .row .title{
  font:400 calc(16 * var(--n5i-u))/1.25 var(--n5i-sans);
  color:var(--bone);
}
.page .list .row .status{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
  white-space:nowrap;
}
.page .list .sentence{ margin-top:calc(4 * var(--n5i-u)); max-width:72ch; }
.page .list .tags{
  display:block;
  margin-top:calc(4 * var(--n5i-u));
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  color:var(--bone-55);
}

.page .social-row{
  display:flex;
  gap:calc(8 * var(--n5i-u));
  margin-top:calc(24 * var(--n5i-u));
  flex-wrap:wrap;
}

/* underlined links inside content — pills roll instead. .crumb-link (breadcrumb
   segments — BLOG/year/month, Change 1, 2026-09-08) shares this treatment exactly:
   the site's one in-content link style, underlined at rest, brightening on
   hover/focus — so a breadcrumb segment reads as interactive the same way any other
   in-content link does. It's a <button>, not an <a> (there is no real listing URL to
   navigate to without JS), so the button-chrome reset lives alongside the shared rule. */
.page a:not(.pill),
.page .crumb-link{
  color:var(--bone);
  text-decoration:underline;
  text-decoration-color:var(--bone-55);
  transition:text-decoration-color .2s ease-out;
}
.page a:not(.pill):hover,
.page a:not(.pill):focus-visible,
.page .crumb-link:hover,
.page .crumb-link:focus-visible{
  text-decoration-color:var(--bone);
}
.page .crumb-link{
  display:inline;
  font:inherit;
  letter-spacing:inherit;
  text-transform:inherit;
  vertical-align:baseline;
  background:none;
  border:none;
  padding:0;
  margin:0;
  cursor:pointer;
}
/* the trailing crumb segment (day on a post, the viewed listing's own label) is
   exactly where the page already is — deliberately NOT a link, no underline, no
   pointer, so it reads as clearly non-interactive next to the linked segments
   beside it. */
.crumb-current{ cursor:default; }

:focus-visible{ outline:2px solid var(--bone); outline-offset:2px; }

/* ---------- below 768px: corners back in flow ---------- */
@media (max-width: 767px){
  html,body{ overflow:auto; height:auto; }
  body{
    display:flex;
    flex-direction:column;
    min-height:100vh;
    min-height:100dvh;
  }
  .corner{ position:relative; }
  .tl{ order:1; align-self:flex-start; }
  /* header nav relocates from footer to top, directly under the wordmark
     (Eric, 2026-09-08: "the nav block sits directly under the wordmark at
     the TOP of the page") — same phone treatment as before, just moved:
     pills on one centred row, icons centred on the row beneath. */
  .tr{ order:2; align-self:center; }
  main.page{
    order:3;
    margin:0;
    padding:24px 12px 32px;
    max-width:100%;
  }

  /* Below 768px the vertical stack would waste height (five pills tall), so it
     flattens exactly like home's own .br nav does at this width: all five pills
     (About/Projects/Blog/Contact/Login, direct children of nav, no wrapper) on one
     centred row, the divider dropped, the icon row centred on its own line beneath —
     ported byte-for-byte from home's `@media (max-width:767px) .br nav` block. */
  .tr nav{
    flex-direction:row;
    flex-wrap:wrap;
    align-items:center;
    justify-content:center;
    gap:2px;
  }
  .tr nav > .pill:not(.ico){ padding:3px 6px; flex:0 1 auto; min-width:0; }
  .tr nav > .pill .pill-l{ white-space:nowrap; }
  .n5i-nav-rule{ display:none; }
  .n5i-socials{
    order:10;
    flex-basis:100%;
    justify-content:center;
    gap:4px;
    margin:8px 0 0;
  }
}

/* ---------- Post rows (/blog/) ---------- */
.post-list{ max-width:72ch; margin-top:calc(24 * var(--n5i-u)); }
.post-row{
  display:grid;
  grid-template-columns:14ch 1fr;
  gap:calc(16 * var(--n5i-u));
  padding:calc(12 * var(--n5i-u)) 0;
  border-top:1px solid var(--vein);
  text-decoration:none;
  color:var(--bone);
  max-width:72ch;
}
.post-meta{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
  white-space:nowrap;
}
.post-title{
  font:400 calc(16 * var(--n5i-u))/1.25 var(--n5i-sans);
  color:var(--bone);
  text-decoration:underline;
  text-decoration-color:var(--bone-55);
  text-underline-offset:calc(3 * var(--n5i-u));
  transition:text-decoration-color .15s ease-out;
}
.post-row:hover .post-title,
.post-row:focus-visible .post-title{
  text-decoration-color:var(--bone);
}
@media (max-width:767px){
  .post-row{ grid-template-columns:1fr; gap:4px; }
}

/* ---------- Post prose (/blog/posts/*) ---------- */
/* long-form measure is wider than the 40ch lede/paragraph default above */
.page .post-chips{ margin-top:calc(12 * var(--n5i-u)); }
.prose{ max-width:60ch; margin-top:calc(24 * var(--n5i-u)); }
/* /about/ Goal (Eric, 2026-09-09): went 60ch -> 40ch (matching the lede)
   -> reverted to 60ch -> now 72ch, matching the .stat-row/.list/.acc-trigger/
   .marq measure elsewhere on this page. .prose-wide overrides just Goal's
   instance; the Skills/Certifications/Background accordion panels keep the
   standard 60ch .prose measure, unaffected. */
.prose.prose-wide,
.prose.prose-wide p{ max-width:72ch; }
.prose h2, .prose h3{
  font:400 calc(20 * var(--n5i-u))/1.25 var(--n5i-sans);
  color:var(--bone);
  margin:calc(40 * var(--n5i-u)) 0 calc(12 * var(--n5i-u));
}
.prose p{
  max-width:60ch;
  margin:0 0 calc(16 * var(--n5i-u));
  line-height:1.5;
}
.prose ul,
.prose ol{
  max-width:60ch;
  padding-left:1.2em;
  margin:0 0 calc(16 * var(--n5i-u));
  line-height:1.5;
}
.prose code{
  font:400 calc(14 * var(--n5i-u))/1.25 var(--n5i-mono);
}
.prose pre{
  font:400 calc(13 * var(--n5i-u))/1.5 var(--n5i-mono);
  overflow-x:auto;
  border-top:1px solid var(--vein);
  border-bottom:1px solid var(--vein);
  padding:calc(12 * var(--n5i-u)) 0;
  background:none;
}
.prose blockquote{
  border-left:1px solid var(--vein);
  padding-left:calc(16 * var(--n5i-u));
  color:var(--bone-55);
  max-width:60ch;
  margin:0 0 calc(16 * var(--n5i-u));
}

/* ---------- accordion (projects page; reuses .pill mechanics) ---------- */
.acc-trigger{
  display:block;
  width:100%;
  max-width:72ch;
  text-align:left;
  padding:0;
  margin-top:0;
}
.acc-trigger .pill-l,
.acc-trigger .pill-c{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:calc(8 * var(--n5i-u));
  width:100%;
  padding:calc(10 * var(--n5i-u)) calc(16 * var(--n5i-u));
  box-sizing:border-box;
}
/* Disclosure indicator (Eric, 2026-09-08): collapsed points right, expanded points down —
   one shared down-pointing chevron glyph, rotated in CSS only (no markup/JS change, so this
   single rule covers every current and future .acc-trigger: the /projects/ accordion AND the
   blog tree's root/year/month triggers, the latter generated at runtime by the tree builder
   but carrying the same class). */
.acc-chevron{
  flex:none;
  transform:rotate(-90deg); /* collapsed: '>' */
  transition:transform .25s cubic-bezier(.4,0,.2,1);
}
.acc-trigger[aria-expanded="true"] .acc-chevron{ transform:none; } /* expanded: down arrow */
/* open state: the same fill Eric's current-page pill parks on, keyed off aria-expanded */
.acc-trigger[aria-expanded="true"]{ border-color:var(--violet); }
.acc-trigger[aria-expanded="true"] .pill-c{ transform:none; }

.acc-panel{
  display:grid;
  grid-template-rows:0fr;
  transition:grid-template-rows .3s cubic-bezier(.4,0,.2,1);
}
.acc-panel.is-open{ grid-template-rows:1fr; }
.acc-panel > div{
  overflow:hidden;
  min-height:0;
}
@media (prefers-reduced-motion: reduce){
  .acc-panel, .acc-chevron{ transition:none; }
}

/* ---------- blog archive tree (/blog/ only; corner c-tl, flat/non-indented) ----------
   Three acc-trigger levels (root "Blog" -> years -> months) reusing the accordion
   mechanics above byte-for-byte, plus plain leaf links. Eric, 2026-09-08: "instead of
   the indentation normally seen with topology systems, just have them all in line" —
   every row (root, year, month, leaf) starts at the same left x; a mono tree-branch
   glyph (├── / └──) is the only nod to hierarchy. Eric, 2026-09-08 (second pass): "Add
   Blog as the root of the blog topology" — a single "BLOG" row wraps the whole tree, an
   .acc-trigger like any other, alone in its own group (data-acc-group="tree-root") so it
   always renders └── and the tree's rest state is exactly one visible pill.
   🔴 The entire DOM below <nav data-blog-tree> (root/years/months/leaves) is BUILT AT
   RUNTIME by subpage.js from the "All posts" list (#blog-list-view .post-row), not
   hand-authored — see the JS section below for why. This stylesheet only needs to keep
   describing the shape (classes/ids the builder emits), never the post content itself.
   Width hazard (found by Playwright, not by inspection): .corner is position:fixed
   with width:auto, so its actual width is a "shrink-to-fit" computed from the
   MAX-CONTENT width of its children — the role label's nowrap text is what pins it
   to ≈147.2px today (measured), same as the lockup's width:100%. A leaf's post
   title is long, unbounded prose; text-overflow:ellipsis only clips at RENDER time
   and does nothing to stop that title's full unwrapped width from being counted
   during the shrink-to-fit pass, so without a hard cap here a long title silently
   stretched the corner to 632px in testing and slid it under main.page (which
   started at x:224) — the exact failure mode the task called out. --tree-w gives
   every row (trigger AND leaf) a fixed (non-percentage) max-width — tied directly to
   --n5i-lockup-w (2026-09-09), not a separate literal, since that token IS the
   lockup's own min-width and therefore the corner's real shrink-to-fit driver once
   scaling pushes the lockup past the role label's max-content width; the corner can
   never grow past what the wordmark already pins it to, at any viewport width. */
.blog-tree{
  --tree-w:var(--n5i-lockup-w);
  width:100%;
  max-width:var(--tree-w);
  margin-top:calc(8 * var(--n5i-u)); /* + the .n5i-cluster gap above = the cluster gap */
  display:flex;
  flex-direction:column;
  gap:calc(4 * var(--n5i-u));
}
/* .blog-tree stays pinned inside .corner.tl (chrome, outside the pane — see
   the freeze-pane geometry block above), but 46 leaves is taller than a
   900px viewport at 1440. .tl itself is capped to the viewport height and
   the tree gets its own independent scroll region within it, so the tree
   is fully reachable without the corner ever growing past the viewport or
   the wordmark/role label above it scrolling away. Desktop-only, gated the
   same as the pane geometry above; .blog-tree is display:none below 768px
   in favour of the mobile Year/Month/Day selector, so nothing to reset there. */
@media (min-width:768px){
  .tl{
    max-height:100vh;
    overflow:hidden;
  }
  .blog-tree{
    flex:1 1 auto;
    min-height:0;
    overflow-y:auto;
    scrollbar-width:thin;
    scrollbar-color:var(--vein) transparent;
  }
  .blog-tree::-webkit-scrollbar{ width:6px; }
  .blog-tree::-webkit-scrollbar-track{ background:transparent; }
  .blog-tree::-webkit-scrollbar-thumb{ background:var(--vein); }
}
.tree-root,
.tree-year,
.tree-month{ display:flex; flex-direction:column; gap:calc(4 * var(--n5i-u)); }

/* compact padding: the shared .acc-trigger padding (10px 16px, scaled) is sized for
   the 72ch content-column buttons on /projects/; the corner is much narrower, so this
   pair of classes (specificity 0,0,3,0, beats the shared 0,0,2,0 rule by source
   order+count) tightens it without touching the shared rule. */
.acc-trigger.tree-trigger .pill-l,
.acc-trigger.tree-trigger .pill-c{
  padding:calc(6 * var(--n5i-u)) calc(10 * var(--n5i-u));
  gap:calc(6 * var(--n5i-u));
}
.tree-label{
  display:flex;
  align-items:center;
  gap:calc(6 * var(--n5i-u));
  overflow:hidden;
  white-space:nowrap;
}
.tree-glyph{
  font:400 calc(12 * var(--n5i-u))/1 var(--n5i-mono);
  flex:none;
  opacity:.55; /* opacity, not a bone-55 color, so it dims correctly on both the
                  normal (bone-on-carbon) AND the open/violet-fill (white) pill states */
}

.tree-leaves{ list-style:none; }
.tree-leaves li + li{ margin-top:calc(2 * var(--n5i-u)); }
.tree-leaf{
  display:flex;
  align-items:baseline;
  gap:calc(6 * var(--n5i-u));
  padding:calc(4 * var(--n5i-u)) calc(10 * var(--n5i-u));
  text-decoration:none;
  color:var(--bone-55);
  font:400 calc(12 * var(--n5i-u))/1.4 var(--n5i-mono);
  letter-spacing:-0.05em;
  min-width:0;
}
.tree-leaf .tree-glyph{ opacity:.4; }
.tree-day{
  flex:none;
  font-variant-numeric:tabular-nums;
  opacity:.7;
}
.tree-title{
  flex:1 1 auto;
  min-width:0; /* the flexbox default (auto) refuses to shrink below content size,
                  which would defeat the ellipsis below and let a long title push
                  the corner wider than its 147px content box */
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
.tree-leaf:hover,
.tree-leaf:focus-visible{ color:var(--bone); }
.tree-leaf:hover .tree-title,
.tree-leaf:focus-visible .tree-title{
  text-decoration:underline;
  text-decoration-color:var(--bone-55);
}
/* "current" leaf (Eric, 2026-09-08): not the pill/violet tile treatment
   (that's reserved for the accordion triggers above) — just the day and
   title recolored to the violet feature colour. The glyph is deliberately
   excluded, only bumped slightly brighter so the row still reads as active. */
.tree-leaf[aria-current="page"] .tree-glyph{ opacity:.55; }
.tree-leaf[aria-current="page"] .tree-day,
.tree-leaf[aria-current="page"] .tree-title{ color:var(--violet); }

@media (prefers-reduced-motion: reduce){
  .tree-leaf:hover .tree-title,
  .tree-leaf:focus-visible .tree-title{ transition:none; }
}

/* ---------- Mobile Year/Month/Day archive selector (/blog/ only; <768px) ----------
   Eric, 2026-09-09: "the tree does not work for a mobile page. Is it possible to do
   a nav tree. The nav tree will have to be replaced by a drop down menu to select
   year, month and day." The three-level accordion above reads fine in a 147px-capped
   desktop column, but the same ├──/└── glyph rows are a poor fit once main.page's
   own width becomes the constraint — a phone has no 147px side column to put a tree
   in, and shrinking the tree's type further would make it untappable. Rather than
   reflow .blog-tree's own markup at small widths, subpage.js builds a second,
   independent selector (native <select>s — see [data-blog-mobile-nav] in blog.html
   and the "Mobile cascading Year/Month/Day selector" comment in subpage.js) from the
   exact same grouped post data the tree itself is built from, so the two can never
   disagree about what years/months/days exist. This stylesheet is the ONLY thing
   that decides which of the two paints — no JS rebuild on resize, exactly like
   `.tr nav`'s own flip between the vertical-desktop and flattened-mobile layouts a
   few rules up. `.blog-mobile-nav{display:none}` is the unconditional (outside any
   media query) desktop default: a fully display:none subtree is invisible AND out of
   the Tab order per the HTML spec, so nothing extra (inert, aria-hidden) is needed to
   keep three <select>s a keyboard user would otherwise stumble over out of a desktop
   Tab sequence.
   [data-blog-mobile-nav] is not nested next to .blog-tree in #c-tl the way the desktop
   reader might expect — that cluster is a shrink-to-fit flex column pinned to the
   wordmark's own width (--n5i-lockup-w, 147px below 1440px — see the .blog-tree
   comment above for the full derivation), and three <select>s full of real post
   titles need the phone's actual content width, not a 147px sliver. Giving the
   cluster itself width:100% to fit them would blow the wordmark logo up to full
   viewport width too, which nothing asked for and nothing else in this design wants.

   Placement went through two passes (Eric, 2026-09-09, both same day). First: a
   top-level sibling of #c-tl/#c-tr in <body>, positioned via a flex `order` on
   <body>'s direct children — order:1 (tied with .tl, landing it under the wordmark,
   above the header nav pills) then order:4 (after main.page, so content led and the
   dropdown trailed everything). Both were wrong for the same reason: a single fixed
   position can't answer "between the header and the latest post" on the landing view
   AND "right before whatever's selected" once the header's gone. Second pass moved
   the markup inside #main instead, between #blog-landing-view and #blog-post-view
   (see blog.html) — real DOM position now does both jobs for free with no `order`
   and no JS view-switching hook: see the placement comment on the element itself in
   blog.html for the mechanism. Nothing in this rule needs an `order` value any more. */
.blog-mobile-nav{ display:none; }

@media (max-width: 767px){
  .blog-tree{ display:none; }
  .blog-mobile-nav{
    display:flex;
    flex-direction:column;
    gap:calc(8 * var(--n5i-u));
    width:100%;
    max-width:420px; /* readable select rows without stretching edge-to-edge on wide phones */
    margin:calc(24 * var(--n5i-u)) 0;
  }
  .mnav-field{ display:flex; flex-direction:column; gap:calc(4 * var(--n5i-u)); }
  .mnav-select{
    width:100%;
    background:var(--carbon);
    color:var(--bone);
    border:1px solid var(--vein);
    border-radius:2px;
    padding:calc(8 * var(--n5i-u)) calc(10 * var(--n5i-u));
    font:400 calc(14 * var(--n5i-u))/1.3 var(--n5i-sans);
  }
  .mnav-select:hover,
  .mnav-select:focus-visible{ border-color:var(--violet); }
}

/* Visually-hidden but still-announced label text (the three <select>s above need a
   real <label> each for a screen reader / form-mode assistive tech, per the task's
   "this is a form control, not a decorative element" — but a visible "Year"/"Month"/
   "Day" caption above each select would just repeat what the select's own selected
   option already says). Standard 1px-clip technique, not display:none (which VoiceOver
   and friends *do* skip) and not the .sr-only-on-focus "skip link" treatment above
   (that one is meant to reappear on :focus; a <label> is never itself focusable — its
   `for` target is what receives focus — so it has no reason to ever un-hide). */
.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;
}

/* ---------- Post prev/next (Eric, 2026-09-08: "< Previous and Next >... so it
   can cycle between posts") ---------- */
.post-prevnext{
  display:flex;
  justify-content:space-between;
  align-items:center;
  gap:calc(12 * var(--n5i-u));
  max-width:72ch;
  margin-top:calc(16 * var(--n5i-u));
}
/* both slots always render (even empty) so the surviving control keeps its side
   at either end of the archive — an empty slot has zero intrinsic width, so
   justify-content:space-between still anchors the other one correctly. */
.pn-prev, .pn-next{ display:block; }

/* ---------- Breadcrumb-triggered listing (/blog/ root, year, month; Change 1,
   2026-09-08 — reached from the breadcrumb now, not from a tree-node click; see
   subpage.js) ---------- */
#blog-listing-view [data-listing-count]{ margin-top:8px; }
#blog-listing-view .post-list .label{ margin-top:20px; }
#blog-listing-view .post-list .label:first-child{ margin-top:0; }

/* ---------- /blog/ in-place list <-> post <-> listing swap ---------- */
#blog-post-view[hidden],
#blog-list-view[hidden],
#blog-listing-view[hidden]{ display:none; }

/* ---------- /about/ only: telemetry stat tiles ----------
   Reuses the exact data-stat markup convention the pre-commit hook's sed
   depends on (data-stat="KEY"><b>N</b>, pulled from home/index.html's
   .mtile tiles) but re-themed to the edge-draft telemetry-cluster look
   described in ds-tab.html § Instruments (mono uppercase labels, tabular
   nums) instead of the old card/shadow treatment — no cards, no shadows,
   no gradients, consistent with the rest of this stylesheet. */
/* Sub-labels identifying each of the three stat-rows (Eric, 2026-09-10: "the
   first row is the pve stack", "middle one atm is the unifi camera network",
   "this is the network stack") -- same type as .page .label, just with its own
   top margin since it sits BETWEEN rows rather than opening a section; the
   following .stat-row's own 24u top margin supplies the gap underneath it. */
.stat-row-label{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
  margin-top:calc(32 * var(--n5i-u));
}
.stat-row{
  display:flex;
  flex-wrap:wrap;
  justify-content:center;
  gap:calc(28 * var(--n5i-u)) calc(36 * var(--n5i-u));
  margin-top:calc(24 * var(--n5i-u));
  max-width:72ch;
}
/* Eric, 2026-09-09: centered -- both stat-tile groups (the cluster row AND
   the live Docker row) as a block within the column, and each tile's own
   number-over-label internally. .stat-row is /about/-only (not shared with
   any other page), so this is the base rule, not a scoped modifier. */
.stat-tile{ display:flex; flex-direction:column; align-items:center; text-align:center; gap:calc(2 * var(--n5i-u)); }
.stat-tile b{
  font:400 calc(24 * var(--n5i-u))/1.1 var(--n5i-sans);
  font-variant-numeric:tabular-nums;
  color:var(--bone);
}
.stat-tile span{
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
  white-space:nowrap;
}

/* /about/ "HomeLab" section (Eric, 2026-09-09): static homelab-cluster facts —
   node/core/memory counts and the Proxmox VE version — lead the section,
   ahead of the live Docker stat-row (Eric asked for cluster first, then
   stacks, then tooling, under one "HomeLab" label rather than three). These
   four, plus the VLAN-count tile appended after Networks in the second row,
   use the same .stat-tile visual family but deliberately carry NO data-stat
   attribute: the pre-commit hook's sed only ever targets
   data-stat="stacks|containers|images|volumes|networks", and giving a static
   value a data-stat key (even a new one) would misleadingly imply it's
   hook-managed when it isn't. The VLAN names list that used to run as a
   caption line under the cluster row was dropped (Eric, 2026-09-09) in favor
   of a plain "5 VLANs" tile sitting next to "10 Networks" — no separate
   .cluster-vlans component needed any more. No sub-label separates the two
   .stat-row groups — .stat-row's own 24u top margin is what gives the
   live-stat row its gap under the cluster row. */

/* ---------- /about/ only: tool marquee ----------
   Ports the convention already live on home/index.html's "Technology stack"
   band (.marq/.marq-track: doubled track, edge-masked, infinite translateX
   loop, decorative aria-hidden container) — re-themed to this stylesheet's
   mono/Bone-55 telemetry-line type instead of home's sans/grey-dim, since
   the logo SVGs already ship a fixed grey fill (no invert filter needed
   here). A deliberate, Eric-approved exception to the "no icons anywhere
   except the bottom-right social row" rule (see ds-tab.html) — the marquee
   stays decorative/aria-hidden and is paired with a .sr-only text list so
   the tool names are still announced to assistive tech. */
.marq{
  overflow:hidden;
  /* Eric, 2026-09-09: 72ch -> 40ch (matching the lede/text column) -> back to
     72ch, matching .list/.acc-trigger/.stat-row -- the page moved toward the
     wider measure everywhere (Goal's prose, the HomeLab stat tiles), so the
     marquee now extends to cover that same width again instead of the
     narrower text-column measure. */
  max-width:72ch;
  margin-top:calc(24 * var(--n5i-u));
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
  mask-image:linear-gradient(90deg,transparent,#000 6%,#000 94%,transparent);
}
.marq-track{
  display:flex;
  gap:calc(32 * var(--n5i-u));
  width:max-content;
  animation:n5i-marq 32s linear infinite;
}
@keyframes n5i-marq{
  from{ transform:translateX(0); }
  to{ transform:translateX(-50%); }
}
@media (prefers-reduced-motion: reduce){
  .marq-track{ animation:none; }
}
.marq-track span{
  display:inline-flex;
  align-items:center;
  gap:calc(6 * var(--n5i-u));
  font:400 calc(12 * var(--n5i-u))/1.25 var(--n5i-mono);
  letter-spacing:-0.05em;
  text-transform:uppercase;
  color:var(--bone-55);
  white-space:nowrap;
}
.marq-track img{
  width:calc(16 * var(--n5i-u));
  height:calc(16 * var(--n5i-u));
  display:block;
}
