/* FAU Cybersecurity Club — structural stylesheet.
 * Auto-loaded by the Relearn theme's custom-header.html partial, AFTER
 * format-html.css, so equal-specificity rules here win.
 * Palette tokens are defined in assets/css/theme-hacker.css as --wf-*.
 * NEVER put a literal hex value in this file. Use a --wf-* token. */

/* ---------- 4.1 ground ---------- */
html, body { background-color: var(--wf-bg); }

#R-body-inner {
  background: var(--wf-bg);
}
/* The matrix rain (§17) is a fixed canvas behind body, but this fill sits
   between it and every page's content and is opaque everywhere on purpose —
   readable ground for the other two variants and for every leaf/section page
   in this one. The ONLY place the rain is meant to show through is the cyber
   home hero, so the hole is scoped exactly there: transparent only when
   BOTH the variant is cyber AND we are on the page that renders .wf-hero
   (.wf-home is the home-template body class), which every leaf and section
   page keeps solid. */
:root[data-r-theme-variant='cyber'] #R-body-inner:has(.wf-home) {
  background: transparent;
}
/* The board (§18) opens the same hole in every variant: its Game of Life
   uses the --wf-rain token, which is near-invisible in daylight already, and
   the wall's content sits in panels. */
#R-body-inner:has(.wf-board) { background: transparent; }

/* The hole above is the whole home page, but the rain is only ever meant
   behind the HERO. Every panel below it fills with --wf-fill, which is
   translucent, so on this one page the live canvas showed through the cards
   and the announcements feed. Two periodic patterns — the rain's glyph grid
   fixed to the viewport, the panel's 4px stripe scrolling with the page —
   beat against each other and the interference reads as scanlines at an
   angle neither of them has, moving as you scroll.
   --wf-fill-solid is that same fill already flattened over the ground, so
   these panels look exactly as they do on every other page and the canvas
   stops reaching them. Cyber only: it is the only variant with both a
   transparent ground and a canvas. */
:root[data-r-theme-variant='cyber'] .wf-home .wf-panel {
  background-color: var(--wf-fill-solid);
}

/* 4.2 the page ground gets one very soft blue bloom at the top so the layout
 * reads as "space with depth" rather than a flat sheet. Fixed attachment, one
 * paint, no cost. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(1200px 520px at 68% -12%, var(--wf-wash-hi), transparent 70%),
    radial-gradient(900px 400px at 8% 2%, var(--wf-wash-alt), transparent 70%),
    radial-gradient(1100px 520px at 40% 108%, var(--wf-wash), transparent 72%);
}
#R-body { position: relative; z-index: 1; }

/* ---------- 4.3 links ---------- */
/* The :not([class*="wf-"]) is load-bearing. This selector carries an id, so it
   outranks .wf-panel and its hover rule — without the exclusion the blue link
   hairline paints over the card's bottom border and a hovered card shows red
   on three sides with blue peeking along the bottom. */
body #R-body-inner a:not(.btn):not(.anchor):not([class*="wf-"]) {
  color: var(--wf-chrome);
  text-decoration: none;
  border-bottom: 1px solid var(--wf-link-line);
  transition: color .15s ease, border-color .15s ease;
}
body #R-body-inner a:not(.btn):not(.anchor):not([class*="wf-"]):hover,
body #R-body-inner a:not(.btn):not(.anchor):not([class*="wf-"]):focus-visible {
  color: var(--wf-chrome-hi);
  border-bottom-color: var(--wf-chrome-hi);
}
/* Rationale: a border-bottom hairline instead of text-decoration keeps the
 * "thin bright linework" motif consistent with panel edges, and does not
 * collide with descenders the way the old 2px underline did. */

/* Panels and quick links are links, but they are not prose links: no hairline,
   no underline, in every state. Carries the same id as the rule above so it
   does not lose the cascade. */
body #R-body-inner a[class*="wf-"],
body #R-body-inner a[class*="wf-"]:hover,
body #R-body-inner a[class*="wf-"]:focus-visible,
body #R-body-inner a[class*="wf-"] :is(span, i) {
  text-decoration: none;
}

/* ---------- 4.4 focus ---------- */
:where(a, button, input, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--wf-chrome-hi);
  outline-offset: 2px;
  border-radius: var(--wf-radius);
}

/* ---------- 4.5 scrollbar ---------- */
#R-content-wrapper {
  --ps-thumb-color: var(--wf-scroll);
  --ps-thumb-hover-color: var(--wf-scroll-hi);
}
* { scrollbar-color: var(--wf-scroll) transparent; }

/* ---------- 4.6 rules ---------- */
#R-body-inner hr {
  border: 0;
  border-top: 1px solid var(--wf-edge-soft);
  margin: 2rem 0;
}

/* ---------- 5. typography ---------- */

/* Relearn centers, uppercases and 3.25rem-sizes h1, and justifies body copy in
 * `.narrow` articles at 1.2rem. Both are overridden here. Left-aligned
 * headings give a stronger scan column on a doc site. */
#R-body-inner > .flex-block-wrapper article h1 {
  font-family: var(--wf-font-mono);
  font-size: 2.375rem;
  font-weight: 600;
  letter-spacing: .04em;
  line-height: 1.15;
  text-align: left;
  text-transform: uppercase;
  color: var(--wf-text-strong);
  border-bottom: none;
  margin: .5rem 0 1.75rem;
  position: relative;
  padding-bottom: .75rem;
}
#R-body-inner > .flex-block-wrapper article h1::after {
  content: "";
  position: absolute;
  left: 0; bottom: 0;
  width: 4.5rem; height: 1px;
  background: var(--wf-chrome);
  box-shadow: 0 0 8px var(--wf-glow);
}
@media screen and (max-width: 47.999rem) {
  #R-body-inner > .flex-block-wrapper article h1 { font-size: 1.75rem; }
}

#R-body-inner h2 {
  font-size: 1.625rem;
  line-height: 1.25;
  margin: 2.5rem 0 .875rem;
  padding-bottom: .4rem;
  border-bottom: 1px solid var(--wf-edge-soft);
}
#R-body-inner h3 { font-size: 1.25rem;    line-height: 1.3;  margin: 2rem 0 .625rem; }
#R-body-inner h4 { font-size: 1.0625rem;  line-height: 1.35; margin: 1.5rem 0 .5rem; }
#R-body-inner :is(h5, h6) {
  font-size: .9375rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  margin: 1.25rem 0 .5rem;
}
#R-body-inner h6 { font-size: .875rem; }

/* body copy — kill the justified 1.2rem mono */
#R-body-inner > .flex-block-wrapper article p,
#R-body-inner > .flex-block-wrapper article.narrow > p,
#R-body-inner > .flex-block-wrapper article li {
  font-family: var(--wf-font-sans);
  font-size: 1.0625rem;
  font-weight: 400;
  line-height: 1.65;
  text-align: left;
  color: var(--wf-text);
}
/* measure: prose only. Tables, grids, panels and code are exempt.
   The :not() is load-bearing — this selector carries an id, so a plain
   .wf-grid{max-width:none} loses the specificity fight and the card grid
   gets clamped to the prose measure. */
#R-body-inner > .flex-block-wrapper article > :is(p, ul, ol, blockquote):not([class*="wf-"]) {
  max-width: 68ch;
}

#R-body-inner strong, #R-body-inner b { color: var(--wf-text-strong); font-weight: 700; }
#R-body-inner blockquote {
  border-left: 2px solid var(--wf-chrome);
  background: linear-gradient(90deg, var(--wf-wash), transparent 60%);
  padding: .75rem 1rem;
  margin: 1.5rem 0;
}

/* code keeps mono and the wireframe edge */
#R-body-inner pre:not(.mermaid) {
  border: 1px solid var(--wf-edge-soft);
  border-radius: var(--wf-radius);
  font-size: .875rem;
  line-height: 1.6;
}
#R-body-inner :not(pre) > code {
  border-radius: var(--wf-radius);
  padding: .1em .35em;
}

/* the mono utility label used across hero / panels / schedule */
.wf-kicker {
  font-family: var(--wf-font-mono);
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--wf-text-muted);
  margin: 0 0 .5rem;
  display: flex;
  align-items: center;
  gap: .5rem;
}
.wf-note {
  font-family: var(--wf-font-sans);
  font-size: .875rem;
  color: var(--wf-text-muted);
  margin: .75rem 0 0;
}

/* ---------- 6.1 panel ---------- */
.wf-panel {
  position: relative;
  display: block;
  padding: var(--wf-pad);
  border: 1px solid var(--wf-edge);
  border-radius: var(--wf-radius);
  /* No box-shadow here. Section 9.4 re-declares .wf-panel with the same
     specificity and comes later, so it owns every panel shadow — a value
     set here is dead code that silently does nothing. */
  background: var(--wf-fill);
  /* No backdrop-filter. The ground is opaque everywhere but the cyber home,
     so a blur here did nothing there — and on the cyber home it re-sampled
     the live rain canvas every scroll frame, which flickered. */
  color: var(--wf-text);
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}

/* corner ticks: the "wireframe" tell. Two opposite L brackets, never four —
 * four reads as a targeting reticle and gets noisy in a six-card grid. */
.wf-panel::before,
.wf-panel::after {
  content: "";
  position: absolute;
  width: 10px; height: 10px;
  pointer-events: none;
  border-color: var(--wf-edge-tick);
  border-style: solid;
  transition: border-color .18s ease;
}
.wf-panel::before { top: -1px;    left: -1px;  border-width: 1px 0 0 1px; }
.wf-panel::after  { bottom: -1px; right: -1px; border-width: 0 1px 1px 0; }

/* interactive variant — hover/focus is a "here" state, so it goes RED */
.wf-panel--link { text-decoration: none; }
.wf-panel--link:hover,
.wf-panel--link:focus-visible {
  border-color: var(--wf-edge-now);
  box-shadow: 0 0 22px -6px var(--wf-glow-now);
  transform: translateY(-2px);
}
.wf-panel--link:hover::before, .wf-panel--link:focus-visible::before,
.wf-panel--link:hover::after,  .wf-panel--link:focus-visible::after {
  border-color: var(--wf-red-text);
}

/* current / next state — permanent red left bar. Budget: 1 per viewport. */
/* The red is the LEFT BAR, not the perimeter. A full red border reads as an
   error box, which is exactly what the restraint rule says red must never be.
   Perimeter stays the standard blue edge so the panel matches every other
   surface on the page; the bar and the dot carry the "now". */
.wf-panel--now {
  border-color: var(--wf-edge);
  box-shadow:
    inset 3px 0 0 var(--wf-red),
    0 0 22px -8px var(--wf-glow-now);
  padding-left: calc(var(--wf-pad) + 3px);
}
.wf-panel--now::before, .wf-panel--now::after { border-color: var(--wf-red-text); }

/* flush variant for full-width surfaces */
.wf-panel--flat { box-shadow: inset 0 1px 0 var(--wf-inset); }

#R-body-inner .box:not(.cstyle.transparent) {
  border-radius: var(--wf-radius);
}
#R-body-inner .box > .box-label {
  font-family: var(--wf-font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  font-size: .8125rem;
}

/* ---------- 6.3 tables ---------- */
#R-body-inner table {
  border-collapse: collapse;
  width: 100%;
  font-family: var(--wf-font-mono);
  font-size: .9375rem;
}
#R-body-inner :is(th, td) {
  border: 0;
  border-bottom: 1px solid var(--wf-edge-soft);
  padding: .55rem .75rem;
  text-align: left;
  vertical-align: top;
}
#R-body-inner th {
  color: var(--wf-text-muted);
  font-weight: 700;
  font-size: .75rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--wf-edge);
}
/* The theme stripes every even row with a grey literal. Our rows carry their
   own state colour, so a stripe underneath it is a second, meaningless band. */
#R-body-inner tbody > tr:nth-child(even) > td { background-color: transparent; }
#R-body-inner tbody tr:hover { background: var(--wf-wash-row); }
/* wide tables must scroll inside themselves, never the page */
#R-body-inner .table-wrapper, #R-body-inner table { overflow-x: auto; display: block; }
@media (min-width: 48rem) { #R-body-inner table { display: table; } }

/* ---------- 6.4 wf-grid — the card grid ---------- */
.wf-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--wf-gap);
  list-style: none;
  margin: 1.5rem 0 2.5rem;
  padding: 0;
  max-width: none;          /* beat the 68ch prose measure */
}
.wf-grid--2 { grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr)); }
.wf-grid > li { margin: 0; }
.wf-grid > li::marker { content: none; }

.wf-grid .wf-panel {
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: .4rem;
}
/* No border-bottom override here. .wf-panel already draws all four sides and
   --link:hover recolors all four; re-declaring the bottom edge at this
   specificity is what broke the hover outline. */

.wf-grid .wf-panel > i {
  color: var(--wf-chrome);
  font-size: 1.125rem;
  margin-bottom: .25rem;
}
.wf-card-title {
  font-family: var(--wf-font-mono);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--wf-text-strong);
}
.wf-card-desc {
  font-family: var(--wf-font-sans);
  font-size: .9375rem;
  line-height: 1.55;
  color: var(--wf-text-muted);
}
.wf-panel--link:hover .wf-card-title { color: var(--wf-text-strong); }

/* ---------- 6.4b next-page card ----------
 * Every page hands off to the one after it. The target comes from the
 * theme's own pageNext helper (layouts/_default/article.html), the same
 * logic behind the topbar arrow, so the card and the arrow cannot disagree.
 * The wf- class prefix keeps it out of the prose-link underline rule. */
.wf-next {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  margin: 3rem 0 0;
  text-decoration: none;
}
.wf-next__kicker {
  font-family: var(--wf-font-mono);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--wf-chrome);
}
.wf-next__title { color: var(--wf-text-strong); font-weight: 700; }
.wf-next__desc  { color: var(--wf-text-muted); font-size: .9375rem; }

/* ---------- 6.4c square topic tiles ----------
 * The section's contents as square cards, on every page inside /learn/.
 * Icon and title only: a description in a 1:1 tile just makes it a small
 * rectangle, and the descriptions already do their work on the hub's wider
 * cards. Rendered by layouts/partials/topic-cards.html. */
.wf-topics { display: block; margin: 3rem 0 0; }
.wf-topics__kicker {
  font-family: var(--wf-font-mono);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--wf-chrome);
}
.wf-topics .wf-grid { margin: .75rem 0 0; }
.wf-grid--tiles { grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); }

/* Two-class selectors throughout: 6.4's .wf-grid .wf-panel rules set the
   column flow, the icon size and the padding, and a bare .wf-tile would lose
   to them. */
.wf-grid .wf-tile {
  aspect-ratio: 1 / 1;
  align-items: center;
  justify-content: center;
  gap: .55rem;
  padding: .875rem;
  text-align: center;
}
.wf-grid .wf-tile > i { font-size: 1.75rem; margin: 0; }
.wf-grid .wf-tile .wf-card-title {
  font-size: .875rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/* The page you are standing on: the current-page tile now carries the card's
   "selected" red — red edge, red corner brackets walked to the opposite
   corners, and the red halo — the same end state a card reaches on hover, held
   static, since this tile is not a link. The halo is NOT set here — 9.4
   re-declares .wf-panel at equal specificity later in the file and would
   overwrite it, so it lives with the other panel shadows in 9.4. */
.wf-tile--here {
  background: var(--wf-wash-hi);
  border-color: var(--wf-edge-now);
}
.wf-tile--here::before,
.wf-tile--here::after { border-color: var(--wf-red-text); }
/* Brackets sit where the card's hover animation lands them — the far corners. */
.wf-tile--here::before { left: calc(100% - 9px); transform: rotate(90deg); }
.wf-tile--here::after  { right: calc(100% - 9px); transform: rotate(90deg); }

/* ---------- 6.4d the breadcrumb is a path ----------
 * layouts/partials/breadcrumbs.html prints the trail as
 * /home/csc/learn/linux/permissions/, so it is set in mono and tightened up.
 * The id is load-bearing: the theme colours topbar links from #R-topbar
 * selectors, which a bare class here would lose to. */
#R-topbar .topbar-breadcrumbs {
  font-family: var(--wf-font-mono);
  font-size: .8125rem;
  letter-spacing: .01em;
  color: var(--wf-text-muted);
}
#R-topbar .topbar-breadcrumbs a,
#R-topbar .topbar-breadcrumbs a span { color: var(--wf-text-muted); }
#R-topbar .topbar-breadcrumbs a:hover span,
#R-topbar .topbar-breadcrumbs a:focus-visible span { color: var(--wf-chrome-hi); }
/* the leaf — the page you are on — is the one lit segment, even though it is
   now a self-link like every other. */
#R-topbar .topbar-breadcrumbs li:last-child > a span { color: var(--wf-text-strong); }

/* The topbar path is one clipped line. When the whole path is wider than the
   slot it has — the ol is a flex child between the two fixed topbar areas, so
   it shrinks to the leftover space and hides its overflow — custom.js drops
   leading segments, the root end first, and marks the ol so a "…/" stands in
   for them. The current directory and as much of its tail as fits stay; when
   the whole path fits, it shows with no ellipsis. These two classes are what
   the script toggles, and they apply at every width because a deep path can
   overflow a narrow desktop window too, not only a phone. */
#R-topbar .topbar-breadcrumbs li.wf-crumb-cut { display: none; }
#R-topbar .topbar-breadcrumbs.wf-crumbs-truncated::before {
  content: "…/";
  color: var(--wf-text-muted);
}
@media screen and (max-width: 47.999rem) {
  /* The theme clips every linked crumb down to a 1px a11y box on narrow
     screens, which pulls them out of flow and would blank the trail. Put them
     back as real, full-width flow items so the fit script measures the true
     path width and decides what to drop. */
  #R-topbar .topbar-breadcrumbs { text-align: left; }
  #R-topbar .topbar-breadcrumbs li:has(> a) {
    position: static;
    height: auto;
    width: auto;
    overflow: visible;
    clip-path: none;
    transform: none;
  }
}

/* ---------- 6.5 quick links ---------- */
.wf-quicklinks {
  display: flex;
  flex-wrap: wrap;
  gap: .625rem;
  margin: 2rem 0 2.5rem;
}
.wf-ql {
  font-family: var(--wf-font-mono);
  font-size: .875rem;
  letter-spacing: .04em;
  padding: .5rem .9rem;
  border: 1px solid var(--wf-edge);
  border-radius: var(--wf-radius);
  background: var(--wf-fill);
  color: var(--wf-text) !important;
  text-decoration: none !important;
  border-bottom-width: 1px !important;
  transition: border-color .15s ease, color .15s ease;
}
.wf-ql:hover { border-color: var(--wf-chrome); color: var(--wf-chrome-hi) !important; }
.wf-ql i { color: var(--wf-chrome); margin-right: .35rem; }

/* ---------- 6.6 sidebar ---------- */
#R-sidebar { border-right: 1px solid var(--wf-edge); }
#R-sidebar :is(a, span, label) {
  font-family: var(--wf-font-mono);
  font-size: .875rem;
  letter-spacing: .01em;
}
/* The active row does NOT set its own ::before content any more — §15 gives
   every row a tree glyph, and a hard-coded "> " here would replace it and
   break the column alignment. This only recolours whatever glyph is there. */
#R-sidebar li.active > :is(a, span)::before {
  color: var(--wf-red-text);
  font-weight: 700;
}
#R-sidebar li.active > :is(a, span) {
  color: var(--wf-red-text);
  border-left: 2px solid var(--wf-red);
  margin-left: -2px;
  padding-left: .5rem;
}
#R-topbar { border-bottom: 1px solid var(--wf-edge-soft); }

/* screen-reader-only utility — used by the home schedule table (§8.5) to hide
 * its header row visually while keeping it in the accessibility tree. */
.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;
}

/* ---------- 7. hero ---------- */
.wf-hero {
  position: relative;
  overflow: hidden;              /* REQUIRED: contains the rotated grid */
  isolation: isolate;
  min-height: 19rem;
  display: flex;
  align-items: center;
  padding: 3rem 2rem;
  /* Top matches the existing bottom value — the hero sits centred in its
     own gap rather than flush under the topbar on one side only. */
  margin: 2.5rem 0 2.5rem;
  border: 1px solid var(--wf-stage-edge);
  border-radius: var(--wf-radius);
  /* Ground plus scanlines. 1px line every 4px, so the texture covers a
     quarter of the panel and does it EVENLY — which is the whole point. A
     gradient here made the headline sit on a lighter ground at one end than
     the other, and mono type shows that immediately. Two layers in one
     declaration, no extra element. */
  background:
    repeating-linear-gradient(to bottom,
      var(--wf-stage-scan) 0 1px,
      transparent 1px 4px),
    var(--wf-stage);
}
/* layer 1: the receding floor grid */
.wf-hero-grid {
  position: absolute;
  inset: -60% -30% -40% -30%;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right,  var(--wf-stage-grid) 1px, transparent 1px),
    linear-gradient(to bottom, var(--wf-stage-grid) 1px, transparent 1px);
  background-size: 46px 46px;
  transform: perspective(620px) rotateX(62deg);
  transform-origin: 50% 100%;
  /* a mask reads ALPHA only — the colour here is never painted, so these two
     literals are not palette values and do not change between variants. */
  -webkit-mask-image: linear-gradient(to top, #000 0%, rgba(0,0,0,.35) 42%, transparent 78%);
          mask-image: linear-gradient(to top, #000 0%, rgba(0,0,0,.35) 42%, transparent 78%);
  animation: wf-fade-in .7s ease-out both;
}

/* layer 2: volumetric fog, kills the grid's hard horizon */
.wf-hero-fog {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(70% 90% at 22% 45%, var(--wf-stage-fog-a), transparent 70%),
    radial-gradient(50% 70% at 82% 50%, var(--wf-stage-fog-b), transparent 72%),
    linear-gradient(to top, var(--wf-stage-scrim) 0%, transparent 55%);
}

/* layer 3: the owl wireframe */
.wf-hero-art {
  position: absolute; z-index: 2; pointer-events: none;
  right: 4%; top: 50%; transform: translateY(-50%);
  width: min(17rem, 34%);
  opacity: var(--wf-stage-art);
  filter: drop-shadow(0 0 6px var(--wf-stage-glow));
}
/* The shield ships at its own intrinsic size; the wireframe it replaced was
   an inline svg that filled its box for free. It is 334x326, a near-square
   mark, so width alone does not bound it — max-height does, and object-fit
   centres whatever is left over rather than anchoring it to a corner.
   .wf-hero is min-height 19rem less 3rem padding top and bottom, so 12rem
   is the tallest the mark can be beside the copy without being clipped. */
.wf-hero-art img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 12rem;
  object-fit: contain;
}
/* Narrow OR short: the owl stops being a side illustration and becomes a
   watermark. The height clause is what catches a landscape phone, where the
   hero is wide enough to keep the side placement but far too short for it —
   the mark ran past the panel and got sliced by overflow:hidden.
   Mobile: the owl stops being a side illustration and becomes a watermark
   behind the copy. .wf-hero already sets position:relative and
   overflow:hidden, so the mark can overflow the panel and be cropped — which
   reads better than shrinking a 284-unit drawing into a phone column.
   .wf-hero-copy is z-index 3 and this is 2, so the text stays on top.
   Opacity scales off the same per-variant token instead of introducing a
   second one, so a variant that retunes the owl retunes the watermark too. */
@media screen and (max-width: 47.999rem), screen and (max-height: 34rem) {
  .wf-hero-art {
    right: auto; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    /* Contained, not cropped. The line-art owl was a portrait drawing that
       could overflow and still read; the shield is a closed landscape shape
       whose outline IS the mark, so slicing its edges off just looks broken. */
    width: min(22rem, 86%);
    opacity: calc(var(--wf-stage-art) * .2);
  }
  .wf-hero-art img { max-height: 13rem; }
}

.wf-hero-copy { position: relative; z-index: 3; max-width: 40rem; }
.wf-hero-kicker {
  font-family: var(--wf-font-mono);
  font-size: .75rem; font-weight: 700; letter-spacing: .22em;
  text-transform: uppercase; color: var(--wf-stage-chrome);
  margin: 0 0 .75rem;
}
.wf-hero-title {
  font-family: var(--wf-font-mono);
  font-size: clamp(2rem, 4.5vw, 3rem);
  font-weight: 600; letter-spacing: .04em; line-height: 1.1;
  text-transform: uppercase; text-align: left;
  color: var(--wf-stage-fg);
  margin: 0;
  /* glow lives on chrome and headings only — never on paragraph text */
  text-shadow: 0 0 14px var(--wf-stage-glow), 0 0 34px var(--wf-stage-glow-wide);
}
.wf-hero-tagline {
  font-family: var(--wf-font-mono);
  font-size: 1rem; letter-spacing: .06em;
  color: var(--wf-stage-accent);
  margin: .9rem 0 0;
  padding-top: .9rem;
  border-top: 1px solid var(--wf-stage-grid);
  max-width: 26rem;
}

/* ---------- 7.2 home ---------- */
.wf-home .headline:empty { display: none; }

.wf-meeting { max-width: 26rem; margin: 0 0 2.5rem; }
.wf-meeting-when {
  font-family: var(--wf-font-mono);
  font-size: 1.5rem; line-height: 1.2; margin: 0;
  color: var(--wf-text-strong);
}
.wf-meeting-when strong { color: var(--wf-text-strong); }
.wf-meeting-where {
  font-family: var(--wf-font-mono);
  font-size: 1.0625rem; letter-spacing: .06em;
  color: var(--wf-primary);
  margin: .35rem 0 0;
}
.wf-dot {
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--wf-red-text);
  box-shadow: 0 0 8px var(--wf-red-text);
  animation: wf-pulse 2.4s ease-in-out infinite;
  flex: none;
}

.wf-schedule { margin: 0 0 2.5rem; }
.wf-schedule h2 { display: flex; align-items: baseline; gap: .75rem; flex-wrap: wrap; }
.wf-schedule h2 .wf-kicker { color: var(--wf-text-muted); margin: 0; }
.wf-table { width: 100%; }
.wf-table .wf-date  { color: var(--wf-text-muted); white-space: nowrap; width: 1%; }
.wf-table .wf-title { color: var(--wf-text); }
.wf-table .wf-track {
  color: var(--wf-chrome); font-size: .75rem; letter-spacing: .12em;
  text-transform: uppercase; white-space: nowrap;
}
.wf-table .wf-state {
  font-size: .6875rem; font-weight: 700; letter-spacing: .14em;
  text-align: right; white-space: nowrap;
}
.wf-row.is-done    { opacity: .45; }
.wf-row.is-done .wf-title { text-decoration: line-through; }
.wf-row.is-cancelled { opacity: .45; }
.wf-row.is-cancelled .wf-title { text-decoration: line-through; }
.wf-row.is-cancelled .wf-state { color: var(--wf-text-muted); }
.wf-row.is-moved { opacity: .6; }
.wf-row.is-moved .wf-state { color: var(--wf-text-muted); }

/* The single RED row — "here / now". Budget: 1 per viewport, and the template
   guarantees it: a meeting dated today takes TODAY and suppresses NEXT
   entirely, so these two can never both be on screen. */
.wf-row.is-next,
.wf-row.is-today {
  background: var(--wf-now-wash);
  box-shadow: inset 3px 0 0 var(--wf-red);
}
.wf-row.is-next .wf-title,
.wf-row.is-today .wf-title { color: var(--wf-text-strong); font-weight: 700; }
.wf-row.is-next .wf-state,
.wf-row.is-today .wf-state { color: var(--wf-red-text); }
.wf-row.is-today .wf-date  { color: var(--wf-text-strong); font-weight: 700; }

/* the leftover raw markdown at the bottom of the home page */
.wf-freeform { margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--wf-edge-soft); }

/* ---------- 7.3 motion ---------- */
@keyframes wf-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes wf-pulse   { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .wf-hero-grid { opacity: 1; }
  .wf-dot { opacity: 1; }
}

/* ---------- mermaid ---------- */
/* Mermaid sizes each node's foreignObject from an internal text metric that
   under-measures what actually paints, so labels get cut off ("Virtual
   Machin…"). The measurement is invariant to fontFamily config and it ignores
   flowchart.htmlLabels in this version, so it cannot be fixed from config.
   Let the label paint past the box edge instead — the box is decoration, the
   label is the content. */
#R-body-inner .mermaid foreignObject { overflow: visible; }
#R-body-inner .mermaid foreignObject > div,
#R-body-inner .mermaid .nodeLabel,
#R-body-inner .mermaid .edgeLabel { overflow: visible !important; }

/* ---------- sidebar logo ---------- */
/* Owl icon beside the title (layouts/partials/logo.html). The theme defaults
   .logo-image to display:none and only un-hides it for images declared via
   its per-variant logo config; ours comes from a custom partial, so un-hide
   it here (this sheet loads last). */
#R-logo.R-default .logo-image { display: initial; }
#R-logo .logo-image img {
  width: 3.5rem;
  height: auto;          /* shield is taller-than-square; let it keep its ratio */
  display: block;
}
#R-logo.direction-row { align-items: center; gap: .6rem; }

/* ---------- 7.4 weekly meeting bar ---------- */
/* Reference info, read once: one compact row, not a panel. It stays visually
   distinct via the red tag and left rule without consuming a whole block. */
.wf-meetbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .75rem;
  margin: 0 0 2.5rem;
  padding: .55rem .9rem;
  border-left: 3px solid var(--wf-red);
  border-radius: 0 var(--wf-radius) var(--wf-radius) 0;
  background:
    repeating-linear-gradient(to bottom,
      var(--wf-scan) 0 1px,
      transparent 1px 4px),
    linear-gradient(90deg, var(--wf-now-wash), transparent 70%);
  font-family: var(--wf-font-mono);
  font-size: .9375rem;
  color: var(--wf-text);
}
.wf-meetbar__tag {
  display: inline-flex;
  align-items: center;
  gap: .45rem;
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--wf-red-text);
  text-shadow: 0 0 12px var(--wf-bloom-now);
}
.wf-meetbar__item strong { color: var(--wf-text-strong); }
.wf-meetbar__sep  { color: var(--wf-edge); }
.wf-meetbar__note {
  font-family: var(--wf-font-sans);
  font-size: .8125rem;
  color: var(--wf-text-muted);
  flex-basis: 100%;
}

/* ---------- 7.5 schedule as the page lead ---------- */
.wf-schedule--lead { margin-top: 0; }
.wf-schedule--lead > h2 {
  font-size: 2rem;
  margin-top: 0;
  border-bottom-color: var(--wf-edge);
}
.wf-schedule--lead .wf-table { font-size: 1rem; }
.wf-schedule--lead .wf-row > td { padding-top: .7rem; padding-bottom: .7rem; }
@media screen and (max-width: 47.999rem) {
  .wf-schedule--lead > h2 { font-size: 1.5rem; }
}

/* The scanlines go ON the schedule, not around it: an overlay drawn after the
   rows, flush to their edges. `.wf-scanlines` is a background-image, so it
   would land under the row washes and behind the heading — a pseudo-element
   is what puts the texture above the content. No border, padding or radius:
   this is not a panel, it is a film over one.
   The cyber home fills it because the rain otherwise shows through the rows
   and beats against the stripes — the same interference §4.1 flattens the
   panels for. */
.wf-sched__pane { position: relative; }
/* The pane box has to BE the table box or the overlay aligns to the wrong
   edges: the theme gives every table a 1rem margin top and bottom and a grey
   1px border of its own, so the halo lit an empty band above and below the
   rows and drew a second frame inside the first. */
.wf-sched__pane > .wf-table { margin: 0; border: 0; }
.wf-sched__pane::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 1px solid var(--wf-edge);
  border-radius: var(--wf-radius);
  background-image:
    repeating-linear-gradient(to bottom,
      var(--wf-scan) 0 1px,
      transparent 1px 4px);
  /* Stripes only. `.wf-scanlines` also carries a 158deg wash that fades out
     by 58% of the element, which on a surface this tall reads as shading
     across the rows. */
  /* The halo rides the same layer: 9.4's four panel layers, drawn on the
     overlay rather than the pane, so the glow lands ABOVE the row washes
     instead of under them. One shape, one border, one edge to hug — and the
     glow goes around the whole surface, not around each row. Values are 9.4's
     verbatim, so the schedule is lit exactly as bright as every card. */
  box-shadow:
    inset 0 0 40px -18px var(--wf-bloom-edge),
    0 0 0 1px var(--wf-hairline),
    0 0 20px -10px var(--wf-bloom);
}
:root[data-r-theme-variant='cyber'] .wf-home .wf-sched__pane {
  background-color: var(--wf-fill-solid);
}

/* ---------- 7.6 announcements ---------- */
.wf-ann { margin: 3rem 0 0; }
.wf-ann__feed {
  margin-top: 1.25rem;
  /* Bound the feed so a long backlog scrolls inside the panel instead of
     stretching the home page. The thin themed scrollbar comes from the
     global scrollbar-color rule in 4.5 — no separate scrollbar styling. */
  max-height: 32rem;
  overflow-y: auto;
}
.wf-ann__list {
  list-style: none;
  margin: 0;
  padding: 0;
}
/* One panel, so the separation between posts is a hairline rather than a gap
   between cards. Border on the later sibling keeps the first post flush with
   the panel's own top padding. */
.wf-ann__item + .wf-ann__item {
  margin-top: 1.15rem;
  padding-top: 1.15rem;
  border-top: 1px solid var(--wf-edge);
}
.wf-ann__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem;
  margin: 0;
}
.wf-ann__text {
  font-family: var(--wf-font-sans);
  font-size: .9375rem;
  line-height: 1.6;
  margin: .35rem 0 0;
  color: var(--wf-text);
  /* Discord posts carry their own line breaks and blank lines. Without this
     they collapse into one run-on paragraph. */
  white-space: pre-line;
  /* Meeting links run long and have no spaces to break at. */
  overflow-wrap: anywhere;
}
.wf-ann__link {
  font-family: var(--wf-font-mono);
  font-size: .8125rem;
  margin-left: auto;
}
.wf-ann__empty { margin-top: 1.25rem; }
/* No corner ticks on the announcement panel. It is a feed, not a card — the
   brackets read as chrome on something that should sit quiet. */
.wf-ann .wf-panel::before,
.wf-ann .wf-panel::after { display: none; }


/* ---------- 9. bloom ----------
 * The CRT halo. One rule governs the whole section: glow goes on structure,
 * chrome and headings — never on paragraph text, where a halo costs
 * legibility and buys nothing. Everything here spreads a colour the palette
 * already has, so the identity does not change; only its light does.
 * This section is last on purpose. Several rules re-declare a selector from
 * an earlier section to add a shadow, and equal specificity means the later
 * one wins. */

/* 9.1 headings — white h1 halo, blue h2/h3 halo, brighter h1 underscore */
#R-body-inner > .flex-block-wrapper article h1 {
  text-shadow: 0 0 20px var(--wf-bloom-text), 0 0 48px var(--wf-bloom-wide);
}
#R-body-inner > .flex-block-wrapper article h1::after {
  box-shadow: 0 0 7px var(--wf-bloom), 0 0 20px var(--wf-bloom-wide);
}
/* h2 is blue text and h3 is green text (relearn maps H3 to --wf-primary), so
   they take differently-coloured halos rather than one blue bloom for both. */
#R-body-inner h2 { text-shadow: 0 0 18px var(--wf-bloom-wide); }
#R-body-inner h3 { text-shadow: 0 0 18px var(--wf-bloom-green); }
/* Type below heading level still carries light. Paragraph copy does NOT —
   that exclusion is the whole point of section 9 and a halo there costs
   legibility. In daylight --wf-bloom-wide is a near-transparent dark, so
   this rule quietly does nothing there. */
#R-body-inner :is(h4, h5, h6),
#R-body-inner strong,
#R-body-inner a:not(.btn):not(.anchor),
#R-sidebar a { text-shadow: 0 0 12px var(--wf-bloom-wide); }
/* Subheadings and labels. These are <p> and <span>, not heading elements, so
   none of the rules above reached them and card titles sat flat on a page
   where every real heading glowed. Token-driven like the rest, so daylight
   picks up its soft blue rather than a second rule. */
.wf-card-title,
.wf-kicker,
.wf-next__kicker,
.wf-topics__kicker { text-shadow: 0 0 14px var(--wf-bloom-wide); }
#R-body-inner h2 { border-bottom-color: var(--wf-edge); }

/* 9.2 hairlines: a rule is a lit filament, not a pencil line */
#R-body-inner hr {
  border-top-color: var(--wf-edge);
  box-shadow: 0 0 12px -2px var(--wf-bloom-wide);
}
#R-body-inner blockquote { box-shadow: -1px 0 14px -6px var(--wf-bloom); }

/* 9.3 code: the block edge glows, the code inside stays flat and readable */
#R-body-inner pre:not(.mermaid) {
  border-color: var(--wf-edge);
  box-shadow: 0 0 20px -10px var(--wf-bloom);
}

/* 9.4 panels — resting halo, hover halo, and the red "now" halo */
.wf-panel {
  box-shadow:
    /* The inner edge, same geometry .wf-row.is-today uses for the current
       meeting: inset with a heavily negative spread, so the light sits ON
       the border and dies before the middle. This lives HERE and not in 6.1
       because this section is last and re-declares the selector — anything
       set on .wf-panel earlier in the file loses to it.
       No `inset 0 1px 0` top lip: stacked under the border and the hairline
       ring, it made the top edge three lines thick where the other three were
       one. The hero carries the edge glow alone and that is the reference.
       .wf-panel--flat keeps the lip — there it is the only treatment. */
    inset 0 0 40px -18px var(--wf-bloom-edge),
    0 0 0 1px var(--wf-hairline),
    0 0 20px -10px var(--wf-bloom);
}
.wf-panel--link:hover,
.wf-panel--link:focus-visible {
  box-shadow:
    /* Same inner edge as the resting card, switched to the red bloom. A
       selected surface should change colour, not lose the treatment. */
    inset 0 0 40px -18px var(--wf-bloom-now),
    0 0 0 1px var(--wf-hairline-now),
    0 0 34px -8px var(--wf-bloom-now);
}
.wf-panel--now {
  box-shadow:
    inset 3px 0 0 var(--wf-red),
    inset 0 0 40px -18px var(--wf-bloom-now),
    0 0 30px -9px var(--wf-bloom-now);
}
/* The current-page tile glows like a hovered card, held static. Same four
   layers as .wf-panel--link:hover above, including the outward bloom — that
   outer layer is the spill that makes a hovered card read as lit, and without
   it the tile looked flat-red next to a hovered neighbour. Set in 9.4, not in
   6.x with the tile's border and brackets, because this section re-declares
   .wf-panel last and wins. */
.wf-tile--here {
  box-shadow:
    inset 0 0 40px -18px var(--wf-bloom-now),
    0 0 0 1px var(--wf-hairline-now),
    0 0 34px -8px var(--wf-bloom-now);
}
/* The corner ticks get NO halo. An outer box-shadow on a 10px square paints a
   lit ring around it, and the unlit square inside that ring reads as a solid
   black block sitting on the card corner. The ticks are 1px brackets — they
   are bright enough on their own. */

/* re-stated: 9.4 above would otherwise out-order the flat variant and give a
   full-width surface a halo it is not supposed to have. */
.wf-panel--flat {
  box-shadow: inset 0 1px 0 var(--wf-inset);
}

/* 9.5 quick links */
.wf-ql {
  transition: border-color .15s ease, color .15s ease, box-shadow .15s ease;
}
.wf-ql:hover { box-shadow: 0 0 20px -7px var(--wf-bloom); }

/* 9.6 sidebar: the owl lights up, the current page lights up red, and the
 * divider throws a thin wash into the content column */
#R-sidebar { box-shadow: 1px 0 26px -16px var(--wf-bloom); }
/* The shield casts light into the header block. background-IMAGE only, so
   the theme's own header background colour underneath is left alone. */
#R-header-wrapper {
  background-image:
    radial-gradient(85px 70px at 2.75rem 2.4rem,
      var(--wf-logo-halo), transparent 68%);
}
#R-logo .logo-image img { filter: drop-shadow(0 0 9px var(--wf-logo-glow)); }
#R-sidebar li.active > :is(a, span) {
  color: var(--wf-red-text);
  border-left: 2px solid var(--wf-red);
  margin-left: -2px;
  padding-left: .5rem;
  text-shadow: 0 0 12px var(--wf-bloom-now);
}

/* 9.7 hero */
.wf-hero-art {
  filter:
    drop-shadow(0 0 10px var(--wf-stage-glow))
    drop-shadow(0 0 30px var(--wf-stage-glow-wide));
}
/* Inner edge glow on the hero — the same treatment the cards carry in 9.4,
   same geometry: inset with a heavy negative spread so the light sits on the
   border and dies before the middle. It lives on .wf-hero-fog, not .wf-hero:
   a shadow on the hero paints UNDER its child layers, and the fog's near-opaque
   bottom scrim then covers the bottom edge, so only three sides glowed. On the
   fog the glow paints above that scrim (a box-shadow draws over its own
   element's background) and still sits below the owl and copy, so all four
   edges glow and the text stays on top. This works where the fog is fully
   opaque: dark and light both carry the glow here. Cyber is handled below. */
:root:not([data-r-theme-variant='cyber']) .wf-hero-fog {
  box-shadow: inset 0 0 40px -18px var(--wf-bloom-edge);
}
/* Cyber thins the whole fog layer to .35 for rain show-through (the opacity
   rule below), which would drop the fog-borne glow to a third of its strength.
   So cyber carries the glow on the un-dimmed hero container instead. The
   dark/light scrim reasoning does not apply here — cyber's fog is translucent,
   so a container shadow reads on all four edges. Spread stays shallow at -6px,
   the same value the card hover glow uses in 9.4, so the light hugs the border
   rather than washing inward — the deeper -18px bloom-edge spread in 9.6 is for
   larger flat panels. */
:root[data-r-theme-variant='cyber'] .wf-hero {
  box-shadow: inset 0 0 22px -6px var(--wf-bloom-edge);
}
.wf-hero-kicker { text-shadow: 0 0 16px var(--wf-stage-glow-wide); }
.wf-hero-title {
  text-shadow: 0 0 18px var(--wf-stage-text-glow), 0 0 44px var(--wf-stage-glow-wide);
}

/* 9.8 the one red row on the schedule, and the weekly bar's live dot */
.wf-row.is-next,
.wf-row.is-today {
  background: var(--wf-now-wash);
  box-shadow: inset 3px 0 0 var(--wf-red), inset 0 0 40px -18px var(--wf-bloom-now);
}
.wf-row.is-next .wf-state,
.wf-row.is-today .wf-state {
  color: var(--wf-red-text);
  text-shadow: 0 0 12px var(--wf-bloom-now);
}
.wf-dot {
  box-shadow: 0 0 8px var(--wf-red-text), 0 0 18px var(--wf-bloom-now);
}

/* ---------- 10. motion ----------
 * Budget rule: every animated property here is `transform`, `opacity` or an
 * offset on an absolutely-positioned 10px pseudo-element. Nothing animates
 * width, height, colour of a large surface, or a filter — those repaint the
 * page. Nothing loops forever except the one live-meeting dot. That keeps the
 * whole section on the compositor and off the main thread.
 * The reduced-motion guard in §7.3 collapses all of it to a static end state. */

/* 10.1 corner ticks travel on hover.
 * Resting they bracket top-left and bottom-right. On hover each slides to the
 * far side of its own edge and rotates 90°, which turns a top-LEFT bracket
 * into a top-RIGHT one and a bottom-RIGHT into a bottom-LEFT — so the frame
 * appears to walk around the card. `left`/`right` are percentages of the card,
 * which is the only way a 10px child can know how far the far corner is. */
.wf-panel::before,
.wf-panel::after {
  transition:
    border-color .18s ease,
    left  .3s cubic-bezier(.4, 0, .2, 1),
    right .3s cubic-bezier(.4, 0, .2, 1),
    transform .3s cubic-bezier(.4, 0, .2, 1);
}
.wf-panel--link:hover::before,
.wf-panel--link:focus-visible::before {
  left: calc(100% - 9px);
  transform: rotate(90deg);
}
.wf-panel--link:hover::after,
.wf-panel--link:focus-visible::after {
  right: calc(100% - 9px);
  transform: rotate(90deg);
}

/* 10.2 the card's icon lifts with the card */
.wf-panel--link i { transition: transform .22s cubic-bezier(.4, 0, .2, 1); }
.wf-panel--link:hover i,
.wf-panel--link:focus-visible i { transform: translateY(-3px); }

/* 10.3 cards rise in on load, staggered. Runs once, then never again. */
@keyframes wf-rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.wf-grid > li {
  animation: wf-rise .4s cubic-bezier(.2, .7, .3, 1) both;
}
.wf-grid > li:nth-child(1) { animation-delay: .02s; }
.wf-grid > li:nth-child(2) { animation-delay: .06s; }
.wf-grid > li:nth-child(3) { animation-delay: .10s; }
.wf-grid > li:nth-child(4) { animation-delay: .14s; }
.wf-grid > li:nth-child(5) { animation-delay: .18s; }
.wf-grid > li:nth-child(6) { animation-delay: .22s; }
.wf-grid > li:nth-child(n+7) { animation-delay: .26s; }

/* 10.4 the h1 underscore draws itself left-to-right */
@keyframes wf-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }
#R-body-inner > .flex-block-wrapper article h1::after {
  transform-origin: left center;
  animation: wf-draw .55s .12s cubic-bezier(.2, .7, .3, 1) both;
}

/* 10.5 sidebar links step right on hover — the terminal-prompt gesture */
#R-sidebar li > :is(a, span) { transition: transform .16s ease, color .16s ease; }
#R-sidebar li > a:hover { transform: translateX(3px); }

/* 10.6 quick links lift a hair */
.wf-ql {
  transition:
    border-color .15s ease, color .15s ease,
    box-shadow .15s ease, transform .15s ease;
}
.wf-ql:hover { transform: translateY(-2px); }

/* 10.7 schedule rows settle instead of snapping */
#R-body-inner tbody tr { transition: background-color .16s ease; }

/* ---------- 11. hero text, specificity fix ----------
 * The hero markup sits INSIDE the article, so §5's prose rules
 * (`#R-body-inner > .flex-block-wrapper article h1`, `… article p`) carry an
 * id and outrank every `.wf-hero-*` colour in §7. That went unnoticed for as
 * long as the site was dark-only: --wf-text-strong happened to be near-white,
 * so the wrong rule produced the right result. On the light variant the same
 * rule painted the headline #0A121A on a near-black stage and the hero went
 * blank.
 *
 * Two classes and one id beats one id and two elements, so these win on
 * specificity rather than on source order. The hero is a screen: its text
 * takes STAGE tokens, which do not flip between variants. */
#R-body-inner .wf-hero .wf-hero-title {
  color: var(--wf-stage-fg);
  text-shadow: 0 0 18px var(--wf-stage-text-glow), 0 0 44px var(--wf-stage-glow-wide);
}
#R-body-inner .wf-hero .wf-hero-kicker {
  color: var(--wf-stage-chrome);
  text-shadow: 0 0 16px var(--wf-stage-glow-wide);
}
#R-body-inner .wf-hero .wf-hero-tagline {
  color: var(--wf-stage-accent);
  border-top-color: var(--wf-stage-grid);
}
#R-body-inner .wf-hero .wf-hero-title::after {
  background: var(--wf-stage-chrome);
  box-shadow: 0 0 7px var(--wf-stage-glow), 0 0 20px var(--wf-stage-glow-wide);
}

/* ---------- 12. theme-paired images ----------
 * The {{< mark-preview >}} shortcode emits both the dark-ground mark and its
 * light-ground twin; exactly one is shown. The variant attribute lives on
 * <html>, so this plain stylesheet can read it directly — theme-hacker*.css
 * cannot, because Hugo nests those files inside the variant selector itself. */
/* The id in these selectors is load-bearing: the theme sets `display` on
   images inside the content column with an id-carrying rule, so a bare
   `.wf-mark__light { display: none }` loses and BOTH marks render, stacked. */
.wf-mark { display: inline-block; }
#R-body-inner .wf-mark img { max-width: 100%; height: auto; }
#R-body-inner .wf-mark .wf-mark__light { display: none; }
:root[data-r-theme-variant='hacker-light'] #R-body-inner .wf-mark .wf-mark__dark  { display: none; }
:root[data-r-theme-variant='hacker-light'] #R-body-inner .wf-mark .wf-mark__light { display: block; }
#R-body-inner .wf-mark .wf-mark__green { display: none; }
:root[data-r-theme-variant='cyber'] #R-body-inner .wf-mark .wf-mark__dark  { display: none; }
:root[data-r-theme-variant='cyber'] #R-body-inner .wf-mark .wf-mark__green { display: block; }

/* Same trick for the sidebar shield. It needs its own rules because the
   sidebar is outside #R-body-inner, and the `img` in the selector is
   load-bearing: `#R-logo .logo-image img` (id + class + element) already sets
   display, so a selector without an element loses to it. */
#R-logo .logo-image img.wf-logo__light { display: none; }
:root[data-r-theme-variant='hacker-light'] #R-logo .logo-image img.wf-logo__dark  { display: none; }
:root[data-r-theme-variant='hacker-light'] #R-logo .logo-image img.wf-logo__light { display: block; }
#R-logo .logo-image img.wf-logo__green { display: none; }
:root[data-r-theme-variant='cyber'] #R-logo .logo-image img.wf-logo__dark  { display: none; }
:root[data-r-theme-variant='cyber'] #R-logo .logo-image img.wf-logo__green { display: block; }

/* ---------- 13. schedule room column ----------
 * Most weeks are in the usual room, so the column reads as quiet metadata.
 * A session that overrides the default room is the whole reason the column
 * exists, so that one gets the chrome colour and a little weight — it is the
 * cell someone needs to NOT skim past. */
.wf-table .wf-room {
  color: var(--wf-text-muted);
  font-size: .8125rem;
  letter-spacing: .04em;
  white-space: nowrap;
}
.wf-table .wf-room.is-moved-room {
  color: var(--wf-chrome);
  font-weight: 700;
}
@media screen and (max-width: 47.999rem) {
  /* NARROW SCREENS: STOP BEING A TABLE.
   * Five columns on a 390px phone leaves the title about twelve characters
   * wide, which wraps a session name one word per line and makes a two-line
   * row eight lines tall. Below 48rem each row becomes a stacked block: the
   * date and status read as one kicker line, the title gets the full width,
   * and the room follows it. Same markup, same source order — only the
   * display changes, so the desktop table is untouched. */
  .wf-table,
  .wf-table tbody,
  .wf-table tr,
  .wf-table td { display: block; width: auto; }

  .wf-table thead { display: none; }

  .wf-row {
    padding: .75rem 0 .75rem .75rem;
    border-bottom: 1px solid var(--wf-edge-soft);
  }
  .wf-row:last-child { border-bottom: 0; }

  /* Carries the ID so it outweighs the base `#R-body-inner :is(th, td)` in
     6.3, which otherwise keeps its cell padding and hairline on every stacked
     cell — four rules across one block, and a background that bleeds into the
     line above and below. The row owns the padding at this width, not the
     cells. */
  #R-body-inner .wf-table td { padding: 0; border: 0; }
  .wf-table .wf-track { display: none; }

  /* Date and status share a line, and the row is a flex line-breaker to get
     it: source order puts title and room between them, so `order` is what
     pairs them up. Selector carries .wf-table because the `display: block`
     group above is `.wf-table tr` — a bare `.wf-row` loses to it and the row
     stays a block. */
  .wf-table tr.wf-row { display: flex; flex-wrap: wrap; align-items: baseline; }
  .wf-table .wf-date  { width: auto; order: 1; }
  .wf-table .wf-state { order: 2; margin-left: .5rem; text-align: left; }
  .wf-table .wf-title { order: 3; flex: 1 0 100%; }
  .wf-table .wf-room  { order: 4; flex: 1 0 100%; }

  .wf-table .wf-title {
    margin-top: .25rem;
    line-height: 1.45;
  }
  /* the lead is a separate thought at this width, not a trailing clause */
  .wf-table .wf-title .wf-note { display: block !important; margin-top: .125rem; }

  .wf-table .wf-room { margin-top: .25rem; white-space: normal; }
}


/* ---------- 14. terminal pass ----------
 * Restrained. Four moves, and a rule about where the texture is allowed.
 *
 * SCANLINES ARE QUARANTINED. They live on chrome surfaces only — the hero,
 * the weekly strip, and anything explicitly opted in with .wf-scanlines.
 * They are NOT on the page ground and NOT on .wf-panel generally: a card
 * grid appears on every section page and those cards carry body copy, so a
 * texture behind them is a legibility cost paid site-wide for decoration
 * that only reads on the home page. Opt a surface in, never opt out. */
.wf-scanlines {
  background-image:
    repeating-linear-gradient(to bottom,
      var(--wf-scan) 0 1px,
      transparent 1px 4px),
    linear-gradient(158deg, var(--wf-wash) 0%, transparent 58%);
}

/* 14.1 selection reads as a solid block, the way a terminal draws it */
::selection {
  background: var(--wf-select-bg);
  color: var(--wf-select-fg);
  text-shadow: none;
}

/* 14.2 rules become dashed — a divider typed out, not drawn */
#R-body-inner hr { border-top-style: dashed; }

/* 14.3 bullets become the dash you would type in a plain-text file.
 * Scoped to prose lists only: ul with a class is ours (cards, grids, quick
 * links) and must keep its own markerless styling. */
#R-body-inner > .flex-block-wrapper article ul:not([class]) > li::marker {
  content: "- ";
  color: var(--wf-chrome);
  font-family: var(--wf-font-mono);
}

/* ---------- 15. the sidebar as a directory listing ----------
 * Box-drawing characters line up because the sidebar is already monospace.
 * Nested rows get branch glyphs, the last child of each list gets the elbow,
 * and anything with visible children gets a trailing slash — so a section
 * reads as a folder and a page reads as a file. */
#R-sidebar .R-shortcutmenu-home > ul > li > a::before {
  content: "/";
  color: var(--wf-text-muted);
  margin-right: .15em;
}
#R-sidebar ul ul > li > a::before {
  content: "├─ ";
  color: var(--wf-text-muted);
}
#R-sidebar ul ul > li:last-child > a::before {
  content: "└─ ";
}
/* :has() asks "does this row actually have children rendered under it?",
 * which is the real question. The theme's .parent class means "ancestor of
 * the current page", which is a different thing and would mark the wrong
 * rows as folders. */
#R-sidebar li:has(> ul > li) > a::after {
  content: "/";
  color: var(--wf-text-muted);
}

/* ---------- 16. the caret ----------
 * A block cursor parked after the hero tagline, and ONLY there. One per
 * site — a blinking block everywhere stops reading as a terminal and starts
 * reading as a broken page. Opacity-only animation, and the reduced-motion
 * guard in §7.3 already freezes it. */
.wf-caret {
  display: inline-block;
  width: .55em;
  height: 1.05em;
  margin-left: .35em;
  vertical-align: text-bottom;
  background: var(--wf-stage-accent);
  animation: wf-blink 1.15s steps(1) infinite;
}
@keyframes wf-blink { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }

/* ---------- 17. matrix rain — CYBER VARIANT ONLY ----------
 * The canvas is built and driven by assets/js/custom.js, which only runs on
 * the home page and section landings. Everything visual about it is here.
 *
 * Placement: `inset: 0` with position fixed pins it to the viewport, and
 * z-index 0 puts it in the same layer as the page bloom in §4.2. It is a
 * real child of <body> while that bloom is a ::before, so it paints just
 * above the bloom and — because #R-body carries z-index 1 — below every
 * piece of content. Nothing here can reach the text.
 *
 * Opacity: the glyph colour already carries a low alpha in --wf-rain, and
 * this multiplies it down again. Two knobs on purpose. The token sets how
 * the rain looks per variant; this sets how far back it sits, which is the
 * same answer in all three. Read the pair as "faint, then fainter" — if the
 * body copy on a section landing ever looks washed, this is the number to
 * cut, not the token.
 *
 * The glyph colour itself never appears in this file or in the JS: the
 * script reads --wf-rain off <html> at run time, so the rain follows the
 * variant and goes near-invisible in daylight. */
.wf-rain {
  position: fixed;
  inset: 0;
  /* A canvas is a replaced element: `inset: 0` does not stretch it, its CSS
     size defaults to its backing size (viewport × devicePixelRatio). At 1x
     that coincides with the viewport; zoomed out it covers a fraction of it
     and the right/bottom went bare. Size it explicitly. */
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  opacity: .42;
}

/* No prefers-reduced-motion rule here on purpose. The script honours the
   setting by drawing the field once and never starting the animation loop,
   so the texture survives and only the motion goes. Hiding the canvas
   outright removed the effect entirely for anyone with the setting on. */

/* The rain reads through the transparent content behind the hero, but the
   hero's own fog and bottom scrim (§7, painted IN FRONT of the ground inside
   the hero) sit over it and wash the glyphs out — which is why lowering only
   --wf-stage was not enough. Thin the whole fog layer for cyber so the rain
   comes through; the copy is centred and carries its own glow, and the scrim,
   even at this opacity, still darkens the lower band. Cyber + home only. */
:root[data-r-theme-variant='cyber'] .wf-hero-fog { opacity: .35; }

/* ---------- 18. game of life — THE BOARD ----------
 * Same placement as the rain: fixed to the viewport, z-index 0, under
 * #R-body. Built and driven by assets/js/custom.js when <html> carries
 * data-wf-life, which custom-footer.html sets only for a page with
 * `life = true` in front matter. Colour comes from --wf-life at run time, so
 * it follows the variant — and it is a separate token from --wf-rain because
 * hacker runs the field in red while its rain stays green.
 *
 * Sits well behind the rain's .42: cells are solid blocks where glyphs are
 * strokes, so the same alpha reads as a much heavier field. */
.wf-life {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  pointer-events: none;
  opacity: .18;
}
