/* ==========================================================================
   0xF5 Write-ups — prototype theme
   Dark, professional cybersecurity look. No external fonts/CDNs.
   ========================================================================== */

:root {
  --bg: #050806;
  --bg-secondary: #0a100c;
  --bg-elevated: #0d1410;
  --border: #17231b;
  --border-strong: #223126;

  --green: #00ff66;
  --green-dim: #17c458;
  --green-muted: #72b889;
  --text: #d7e5db;
  --text-dim: #8fa599;
  /* Was #5a6b60 (2026-08-25 fix): computed WCAG contrast against --bg was
     3.55:1, failing the 4.5:1 AA minimum for normal text -- used sitewide
     (dates, tool/technique lists, footer, stats labels) at normal font
     sizes, not just large/decorative text where 3:1 would have been
     enough. #708379 computes to 4.99:1, same muted green-gray hue family,
     just lighter. */
  --text-faint: #708379;

  --amber: #e6b450;
  --red: #ef5a5a;
  --insane: #c34ae0;
  --blue: #4fa8e8;
  /* Tool tags (2026-08-27, owner-requested): was --green, same as the page
     title/Easy badge/Windows OS mark, so a Tool tag didn't read as its own
     category at a glance. Deliberately a different purple than --insane
     above (lighter, less saturated) so a Tool tag next to an Insane
     difficulty badge on the same card still reads as two different things,
     not the same color used twice. */
  --purple: #b490e0;
  /* Category badges on a technique/tool detail page (2026-08-28,
     owner-reported: the plain, uncolored .badge those used inherited
     --text -- same off-white as normal body copy, so a category pill
     didn't read as its own thing at a glance). Every other hue on the
     wheel already means something specific (green: brand/Easy, amber:
     Medium/Technique tags, red: Hard, insane: Insane, blue: MITRE/
     Windows, purple: Tool tags) -- teal is the one genuinely unclaimed
     gap between green and blue, and reads as a calm, neutral label
     rather than a severity/status/action color, matching what Category
     actually is (browsing/classification metadata, not a signal). */
  --teal: #2dd4bf;

  --radius: 6px;
  --mono: "Cascadia Code", "JetBrains Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  --content-max: 1200px;
}

* {
  box-sizing: border-box;
}

html {
  color-scheme: dark;
}

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

/* Visually hidden but still in the DOM/accessibility tree -- for an <h1>
   that search engines and screen readers should see but the page doesn't
   need to show on screen (2026-08-27, SEO TODO item #2: the Techniques/
   Tools index pages already show "you are here" via the nav's
   .nav-link.current, so a second on-screen heading saying the same thing
   would just be visual noise -- unlike the homepage's own real, visible
   <h1>). Standard clip-based pattern, not display:none/visibility:hidden
   (those remove it from the accessibility tree too, defeating the point). */
.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;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  background: var(--bg-elevated);
  color: var(--green);
  padding: 0.6rem 1rem;
  border: 1px solid var(--green-dim);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 0.85rem;
}
.skip-link:focus {
  left: 1rem;
  top: 1rem;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  line-height: 1.6;
  position: relative;
  min-height: 100vh;
}

a {
  color: var(--green-muted);
  text-decoration: none;
}
a:hover {
  color: var(--green);
  text-decoration: underline;
}

code, pre, kbd {
  font-family: var(--mono);
}

/* --------------------------------------------------------------------
   Fog / mist background — CSS only, no JS, no WebGL
   -------------------------------------------------------------------- */
.fog {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
}
.fog::before,
.fog::after {
  content: "";
  position: absolute;
  width: 70vw;
  height: 70vw;
  border-radius: 50%;
  filter: blur(120px);
  opacity: 0.06;
}
.fog::before {
  background: var(--green);
  top: -20%;
  left: -15%;
  animation: drift-a 90s ease-in-out infinite alternate;
}
.fog::after {
  background: var(--green-muted);
  bottom: -25%;
  right: -10%;
  animation: drift-b 110s ease-in-out infinite alternate;
}
@keyframes drift-a {
  from { transform: translate(0, 0); }
  to   { transform: translate(6vw, 8vh); }
}
@keyframes drift-b {
  from { transform: translate(0, 0); }
  to   { transform: translate(-5vw, -6vh); }
}
@media (prefers-reduced-motion: reduce) {
  .fog::before, .fog::after {
    animation: none;
  }
}

.page {
  position: relative;
  z-index: 1;
}

/* --------------------------------------------------------------------
   Header
   -------------------------------------------------------------------- */
header.site-header {
  border-bottom: 1px solid var(--border);
  background: rgba(10, 16, 12, 0.7);
  backdrop-filter: blur(6px);
  position: sticky;
  top: 0;
  z-index: 10;
}
.site-header-inner {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 1rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--mono);
  font-size: 1.15rem;
  color: var(--text);
  flex-shrink: 0;
}
.brand .key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.85em;
  color: var(--green);
  background: var(--bg-secondary);
  border: 1.5px solid var(--green-dim);
  border-radius: var(--radius);
  padding: 0.15em 0.45em 0.2em;
  line-height: 1;
}
.site-nav {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-left: auto;
}
.site-nav a {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.6rem;
  border-radius: var(--radius);
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: 0.85rem;
}
.site-nav a svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  display: block;
}
.site-nav a:hover,
.site-nav a.current {
  color: var(--green);
  background: var(--bg-elevated);
  text-decoration: none;
}
.search-wrap {
  flex: 1 1 260px;
  max-width: 360px;
  position: relative;
}
/* Search overlay (2026-08-27, owner-requested) -- only ever built by
   writeup.js when the search box has data-search-target set
   (content_detail.html's technique/tool-scoped search), never on the
   homepage/write-up pages' own plain Enter-only box. Positioned below
   .search-wrap, same visual language as .detail-pop's popover elsewhere
   on the site (bg-elevated, green-dim border) but opening downward, the
   conventional direction for a search-results dropdown rather than a
   hover-triggered info popover. */
.search-overlay {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--bg-elevated);
  border: 1px solid var(--green-dim);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.search-overlay-item {
  display: block;
  padding: 0.55rem 0.8rem;
  color: var(--text);
  text-decoration: none;
  font-family: var(--mono);
  font-size: 0.82rem;
  border-bottom: 1px solid var(--border);
}
.search-overlay-item:last-child {
  border-bottom: none;
}
.search-overlay-item:hover,
.search-overlay-item.active {
  background: var(--bg-secondary);
  color: var(--green);
}
.search-overlay-more {
  display: block;
  padding: 0.5rem 0.8rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--green-dim);
  border-top: 1px solid var(--border);
  text-decoration: none;
}
.search-overlay-more:hover {
  color: var(--green);
  background: var(--bg-secondary);
}
.search-input {
  width: 100%;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.9rem;
  padding: 0.55rem 0.8rem;
  border-radius: var(--radius);
  outline: none;
}
.search-input::placeholder {
  color: var(--text-faint);
}
.search-input:focus {
  border-color: var(--green-dim);
  box-shadow: 0 0 0 3px rgba(0, 255, 102, 0.08);
}

/* --------------------------------------------------------------------
   Filters
   -------------------------------------------------------------------- */
/* Real, visible page heading (2026-08-27 SEO review finding) -- the
   homepage previously had no <h1> (or any heading) at all, jumping
   straight from the header into the filters bar. Same horizontal
   alignment as .filters-bar right below it (same max-width/margin/
   padding values), small and understated to match the site's aesthetic
   rather than a big hero banner. */
.page-title {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 1.2rem 1.5rem 0;
  font-family: var(--mono);
  font-size: 1.15rem;
  color: var(--green);
}
.filters-bar {
  /* History: originally a single row all filters plus results-count/reset
     competed for, which broke (2026-08-24, an item wrapped onto its own
     orphaned line once a 7th field stopped fitting) -- fixed at the time
     by giving results-count/reset a row of their own, always, regardless
     of how many filters fit above. That in turn broke again once the Tool
     filter (2026-08-27) made the filter *fields'* own row wrap
     unpredictably, since a native <select> sizes to its widest option and
     a long real tool name (e.g. impacket-secretsdump) could push the row
     over on its own, independent of filter count. Fixed the same day by
     replacing the single flex-wrap fields row with two deliberate ones --
     see .filter-row/.view-row below for the current shape and reasoning. */
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 1rem 1.5rem 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
/* Two deliberate rows (2026-08-27, owner-proposed after the accidental
   flex-wrap version reported as looking wrong): .filter-row = every
   control that actually narrows the result set (Platform through Tool);
   .view-row = Sort plus Reset/results-count -- everything about *how
   you're viewing* whatever's left, not narrowing anything. Matches the
   distinction getFiltered()/render() already draw in app.js itself (Sort
   is deliberately excluded from the "any filter active" check there,
   since it's "an ordering preference, not something narrowing the result
   set") -- this just makes that same distinction visible in the layout.
   Both rows fit within --content-max on their own regardless of which
   option is selected in any field (verified against the single longest
   real tool name on the site, impacket-secretsdump, at exactly 1200px --
   the narrowest this ever needs to hold before the mobile toggle takes
   over), unlike the single flex-wrap list before it, whose wrap point
   depended on unpredictable option-text width.

   .view-row is a sibling of #filter-fields in the markup, not nested
   inside it -- deliberately, so it keeps working with the mobile filter
   collapse (below): #filter-fields is what hides behind "Show filters",
   and Sort/Reset/results-count need to stay visible through that exactly
   like Reset/results-count already did before this change (a `display:
   none` ancestor can't be overridden back on by anything the descendant
   itself declares, so this has to be a sibling, not a CSS trick on a
   child). Also a nice side effect: it's the same narrows-vs-views split
   doing double duty for both the desktop layout and which controls the
   mobile toggle collapses -- the collapsible group is exactly "the
   narrowing filters" either way. */
.filter-fields {
  display: flex;
}
.filter-row {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: flex-end;
}
.view-row {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
}
.filter-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.filter-field label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}
.filter-field select {
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.45rem 0.6rem;
  font-family: var(--sans);
  font-size: 0.85rem;
  min-width: 150px;
}
.filter-results {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.85rem;
}
.filter-reset {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: none;
  border: none;
  padding: 0;
  color: var(--green-dim);
  font-family: var(--mono);
  font-size: 0.78rem;
  cursor: pointer;
}
.filter-reset:hover {
  color: var(--green);
  text-decoration: underline;
}
/* 15px (2026-08-27, owner-requested -- tried 12px first, "hard to see") --
   a rotating-arrow signals "reset" on its own, same reasoning as the print
   button's icon elsewhere on the page; stroke-based SVG matching the
   site's existing icon style, not an emoji. */
.filter-reset svg {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  display: block;
}
.filter-results-count {
  font-size: 0.8rem;
  color: var(--text-faint);
  font-family: var(--mono);
}

/* Mobile: filters collapsed behind a toggle, closed by default (2026-08-27,
   UX review finding) -- confirmed real: on a 375px viewport the seven
   stacked filter fields pushed the first write-up card down to 689px of an
   812px-tall screen, so a first-time mobile visitor saw almost nothing but
   controls. Same toggle-button/JS-driven-.open-class pattern the TOC's own
   mobile collapse already uses (#toc-toggle/.toc.open in writeup.js) for
   consistency, not a new interaction pattern. Results-count/reset (and,
   since the .filter-row/.view-row split below, Sort too) stays visible
   even collapsed -- only the narrowing filters hide -- so a visitor still
   sees the count and can still reset/reorder without opening the panel. */
.filters-mobile-toggle {
  display: none;
}
@media (max-width: 767px) {
  .filter-fields {
    display: none;
  }
  .filters-bar.open .filter-fields {
    display: flex;
  }
  .filters-mobile-toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    align-self: flex-start;
    background: none;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--green-muted);
    font-family: var(--mono);
    font-size: 0.8rem;
    padding: 0.45rem 0.7rem;
    cursor: pointer;
  }
}

/* --------------------------------------------------------------------
   Card grid
   -------------------------------------------------------------------- */
main {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 1.5rem;
}
.card-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
}
@media (max-width: 1024px) {
  .card-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 620px) {
  .card-grid { grid-template-columns: 1fr; }
}

.card {
  display: block;
  min-width: 0; /* grid items default to min-width:auto -- Safari-specific bug where
    that lets -webkit-line-clamp collapse/fail to render on .card-excerpt below */
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  color: var(--text);
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.card:hover {
  border-color: var(--green-dim);
  color: var(--text);
  text-decoration: none;
  transform: translateY(-2px);
}
.card-cover {
  position: relative;
  height: 110px;
  background-color: var(--bg-elevated);
  overflow: hidden;
}
.card-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.card-cover::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(10, 16, 12, 0) 40%, var(--bg-secondary) 100%);
}
.range-ribbon {
  position: absolute;
  top: 0.5rem;
  left: 0.5rem;
  z-index: 1;
  font-family: var(--mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--green);
  background: rgba(5, 8, 6, 0.75);
  border: 1px solid var(--green-dim);
  border-radius: var(--radius);
  padding: 0.15rem 0.5rem;
}
.card-body {
  padding: 1rem;
}
.badge-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-bottom: 0.5rem;
}
.badge-row .badge {
  margin-bottom: 0;
}
.card-title {
  font-family: var(--mono);
  font-size: 1.02rem;
  color: var(--green);
  margin: 0 0 0.35rem;
}
.card-platform {
  font-size: 0.82rem;
  color: var(--text-dim);
  margin-bottom: 0.6rem;
}
.badge {
  display: inline-block;
  font-size: 0.7rem;
  font-family: var(--mono);
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid currentColor;
  margin-bottom: 0.5rem;
}
.badge.diff-easy { color: var(--green-muted); }
.badge.diff-medium { color: var(--amber); }
.badge.diff-hard { color: var(--red); }
.badge.diff-insane { color: var(--insane); }
/* A technique/tool's Category badge (content_detail.html) -- see --teal's
   own comment above for why this hue specifically. */
.badge.badge-category { color: var(--teal); }

/* Target-OS mark (2026-08-24): a single letter in a circle, same
   `border: 1px solid currentColor` treatment as .badge above just round
   instead of pill-shaped -- an illustrated Tux/Windows-flag icon was
   tried first but never read cleanly at badge size against the dark
   theme; a monospace letter sidesteps that entirely and matches the
   badge/kbd-key visual language already used elsewhere on the site.
   color falls back to --text-dim for any OS name that isn't Linux or
   Windows, rather than being unstyled. */
.os-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid currentColor;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-dim);
  flex-shrink: 0;
}
.os-linux { color: var(--amber); }
.os-windows { color: var(--blue); }

.card-techniques,
.card-tools,
.card-mitre {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  min-width: 0;
  position: relative;
  font-size: 0.78rem;
  color: var(--text-faint);
  font-family: var(--mono);
  margin-bottom: 0.5rem;
  min-height: 1.1em;
}
.detail-text {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* "+N" chip for the items MAX_CARD_LIST_ITEMS truncated off card-techniques/
   card-tools (see buildTruncatedLine() in app.js) -- opens .detail-pop on
   hover, keyboard focus, or tap. Replaced a plain `title` tooltip (2026-08-25)
   which gave no visual hint a row had more content and did nothing on touch. */
.detail-chip {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  font-size: 0.72rem;
  font-family: var(--mono);
  color: var(--text-dim);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  padding: 0.05rem 0.4rem;
  cursor: pointer;
}
.detail-chip:hover,
.detail-chip:focus-visible {
  color: var(--green);
  border-color: var(--green-dim);
  outline: none;
}
.detail-pop {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  z-index: 5;
  width: max-content;
  max-width: 230px;
  background: var(--bg-elevated);
  border: 1px solid var(--green-dim);
  border-radius: var(--radius);
  padding: 0.5rem 0.65rem;
  font-size: 0.75rem;
  line-height: 1.6;
  color: var(--text);
  white-space: normal;
}
.card-date {
  font-size: 0.75rem;
  color: var(--text-faint);
}
.card-excerpt {
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
  font-size: 0.72rem;
  line-height: 1.5;
  color: var(--text-faint);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3; /* unprefixed version Safari 16.4+ also honors natively */
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card-excerpt mark {
  background: none;
  color: var(--green);
  font-weight: 600;
}

.empty-state {
  grid-column: 1 / -1;
  text-align: center;
  padding: 3rem 1rem;
  color: var(--text-faint);
  font-family: var(--mono);
}

/* --------------------------------------------------------------------
   Pagination
   -------------------------------------------------------------------- */
.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.4rem;
  margin: 2rem 0 1rem;
  font-family: var(--mono);
  font-size: 0.85rem;
}
.pagination button {
  background: var(--bg-secondary);
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  border-radius: var(--radius);
  padding: 0.4rem 0.7rem;
  cursor: pointer;
}
.pagination button:hover:not(:disabled) {
  border-color: var(--green-dim);
  color: var(--green);
}
.pagination button.active {
  color: var(--green);
  border-color: var(--green-dim);
}
.pagination button:disabled {
  opacity: 0.35;
  cursor: default;
}

/* --------------------------------------------------------------------
   Footer
   -------------------------------------------------------------------- */
footer.site-footer {
  max-width: var(--content-max);
  margin: 0 auto;
  padding: 2rem 1.5rem 3rem;
  color: var(--text-faint);
  font-size: 0.8rem;
  border-top: 1px solid var(--border);
  margin-top: 2rem;
}

footer.site-footer p {
  margin: 0.35rem 0 0;
}

footer.site-footer p:first-child {
  margin-top: 0;
}

.footer-sep {
  margin: 0 0.5em;
}

/* About link (2026-08-29) -- lives beside the copyright/content-update
   text, not the header nav (docs/ARCHITECTURE.md's About/contact entry:
   this is about the site's author, the same category as the social-link
   icons already in this footer, not another way to browse write-up
   content). Same text-faint-at-rest/green-on-hover treatment as every
   other quiet footer link here, plus an icon (matching the header nav's
   icon-plus-text pattern) and a "current" state -- green, no underline,
   same as .site-nav a.current -- when the About page is the one showing
   it (2026-08-29 follow-up, owner-requested). The About page's own <h1>
   went visually-hidden the same day this shipped, same reasoning
   .sr-only's own comment already gives for Techniques/Tools: the "you are
   here" signal now lives in this active nav state, a second on-screen
   heading saying the same thing would be redundant. */
.footer-about-link {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  color: var(--text-faint);
  text-decoration: none;
}
.footer-about-link svg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  display: block;
}
.footer-about-link:hover,
.footer-about-link:focus-visible {
  color: var(--green);
  text-decoration: underline;
}
.footer-about-link.current {
  color: var(--green);
}

.footer-row-1 {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.footer-icon-links {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.footer-icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.35rem;
  border-radius: var(--radius);
  color: var(--text-dim);
}

.footer-icon-link svg {
  width: 16px;
  height: 16px;
  display: block;
}

/* Text fallback for a social link platform with no drawn icon (see
   render.py's build_social_link_items()) -- sized/weighted to read as the
   same visual weight as the 16px SVG icons next to it, not a separate,
   heavier component. */
.footer-icon-badge {
  font-family: var(--mono);
  font-size: 0.62rem;
  font-weight: bold;
  line-height: 1;
  letter-spacing: 0.02em;
}

.footer-icon-link:hover,
.footer-icon-link:focus-visible {
  color: var(--green);
  background: var(--bg-elevated);
}

/* --------------------------------------------------------------------
   Page hero / cover image
   -------------------------------------------------------------------- */
.page-hero {
  position: relative;
  width: 100%;
  height: 260px;
  overflow: hidden;
  background-color: var(--bg-elevated);
}
.page-hero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.page-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(5, 8, 6, 0) 35%, var(--bg) 100%);
}
@media (max-width: 620px) {
  .page-hero {
    height: 160px;
  }
}

/* --------------------------------------------------------------------
   Reading progress bar
   -------------------------------------------------------------------- */
.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 0%;
  background: var(--green);
  z-index: 50;
  box-shadow: 0 0 6px rgba(0, 255, 102, 0.5);
}
@media (prefers-reduced-motion: reduce) {
  .reading-progress {
    box-shadow: none;
  }
}

/* --------------------------------------------------------------------
   Back link
   -------------------------------------------------------------------- */
.back-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.2rem;
  margin-bottom: 1.2rem;
}
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--text-dim);
  margin-bottom: 1.2rem;
}
.back-links .back-link {
  margin-bottom: 0;
}
/* Technique/tool detail page only (2026-08-27): holds the back-link and,
   only when there's no TOC to nest a print button in instead, the
   standalone print button next to it -- same margin-bottom-reset pattern
   as .back-links above, the row itself carries the spacing instead. */
.content-top-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1.2rem;
}
.content-top-row .back-link {
  margin-bottom: 0;
}
.back-link:hover {
  color: var(--green);
  text-decoration: none;
}

/* --------------------------------------------------------------------
   Range navigation
   -------------------------------------------------------------------- */
.range-header {
  border-bottom: 1px solid var(--border);
  padding-bottom: 1.2rem;
  margin-bottom: 1.5rem;
}
.range-header h1 {
  font-family: var(--mono);
  color: var(--green);
  font-size: 1.9rem;
  margin: 0 0 0.4rem;
}
.range-header p {
  color: var(--text-dim);
  max-width: 60ch;
}
.range-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  font-size: 0.85rem;
  color: var(--text-dim);
  margin-top: 0.6rem;
}

.range-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.8rem 1rem;
  margin-bottom: 1.5rem;
  font-family: var(--mono);
  font-size: 0.82rem;
}
.range-nav .range-nav-label {
  color: var(--text-faint);
  text-transform: uppercase;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  margin-right: 0.3rem;
}
.range-nav a {
  color: var(--text-dim);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.25rem 0.6rem;
}
.range-nav a:hover {
  color: var(--green);
  border-color: var(--green-dim);
  text-decoration: none;
}
.range-nav a.current {
  color: var(--green);
  border-color: var(--green-dim);
  background: var(--bg-elevated);
}
.range-nav .arrow {
  color: var(--text-faint);
}

/* --------------------------------------------------------------------
   Write-up article layout
   -------------------------------------------------------------------- */
.writeup-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 240px;
  gap: 2.5rem;
  align-items: start;
}
@media (max-width: 900px) {
  .writeup-layout {
    grid-template-columns: 1fr;
  }
}

.writeup-header {
  border-bottom: 1px solid var(--border);
  padding-bottom: 1.2rem;
  margin-bottom: 1.5rem;
}
.writeup-header h1 {
  font-family: var(--mono);
  color: var(--green);
  font-size: 1.9rem;
  margin: 0 0 0.4rem;
}
.writeup-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  font-size: 0.85rem;
  color: var(--text-dim);
  align-items: center;
  margin-bottom: 0.6rem;
}
.writeup-meta .sep {
  color: var(--text-faint);
}
.writeup-meta .badge {
  /* .badge's own margin-bottom: 0.5rem exists for when badges wrap onto
     their own row (e.g. .badge-row on cards); it has no reset here like
     .badge-row .badge does, so it was pulling the difficulty pill's
     visual center ~4px above every other item on this single-line flex
     row -- most noticeable once the OS mark shipped right next to it at
     the correct height. Same fix as .badge-row .badge. */
  margin-bottom: 0;
}
.meta-os {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.5rem;
}
.tag-list .label {
  /* Shared min-width (2026-08-27, owner-requested) so every tag-list's
     first tag starts at the same column, whether the row's own label is
     short ("Category") or long ("MITRE ATT&CK") -- measured against the
     widest label actually used sitewide (both writeup.html's Tools/
     Techniques/MITRE ATT&CK rows and content_detail.html's Category/MITRE
     ATT&CK rows share this one class): "MITRE ATT&CK" itself, ~84px
     rendered, so 88px leaves a little headroom without being so wide it
     creates an obviously oversized gap after "Tools". */
  display: inline-block;
  min-width: 88px;
  font-size: 0.72rem;
  text-transform: uppercase;
  color: var(--text-faint);
  margin-right: 0.4rem;
  letter-spacing: 0.05em;
}
.tag {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--green-muted);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.15rem 0.5rem;
  background: var(--bg-elevated);
}

button.tag {
  cursor: pointer;
}
button.tag:hover,
button.tag:focus-visible {
  border-color: var(--green-dim);
  color: var(--green);
}

/* Amber/green instead of the default green-muted so a clickable technique
   or tool tag is visually distinct from a plain (non-interactive) tag at
   rest, not just on hover -- and from each other, so which explain-overlay
   kind a tag opens is visible before clicking it. */
.tag-technique {
  color: var(--amber);
  border-color: var(--amber);
}
.tag-technique:hover,
.tag-technique:focus-visible {
  color: var(--bg);
  background: var(--amber);
  border-color: var(--amber);
}
/* Info icon marks the tag as clickable (opens the explain overlay in
   place) -- deliberately not MITRE's ↗ below, since that arrow means
   "leaves the page" and this doesn't (2026-08-26, picked from a few
   candidates the owner reviewed first). Only on the real button state --
   .tag-technique-plain below never gets it, nothing to open. */
.tag-technique::after {
  content: " \24d8";
}

/* A technique whose page isn't Published yet (2026-08-26): still shown, in
   the same amber as a real .tag-technique so it's still clearly a technique,
   but deliberately not the boxed/bordered .tag treatment -- no border, no
   background, no cursor pointer -- so it reads as inert at a glance rather
   than looking like a broken button. Plain text, not a button element. */
.tag-technique-plain {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--amber);
  padding: 0.15rem 0.15rem;
}

.tag-tool {
  color: var(--purple);
  border-color: var(--purple);
}
.tag-tool:hover,
.tag-tool:focus-visible {
  color: var(--bg);
  background: var(--purple);
  border-color: var(--purple);
}
/* See .tag-technique::after above -- same info icon, same reasoning. */
.tag-tool::after {
  content: " \24d8";
}

/* A tool whose page isn't Published yet (2026-08-26, mirrors
   .tag-technique-plain): still shown, same green as a real .tag-tool, but
   no border, no background, no cursor pointer -- plain text, not a button. */
.tag-tool-plain {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--purple);
  padding: 0.15rem 0.15rem;
}

/* Blue instead of amber -- same boxed-tag treatment as .tag-technique,
   just its own color so a MITRE badge (external link) still reads as
   visually distinct from a technique tag (opens the overlay). */
.tag-mitre {
  text-decoration: none;
  color: var(--blue);
  border-color: var(--blue);
}
.tag-mitre::after {
  content: " \2197";
}
.tag-mitre:hover,
.tag-mitre:focus-visible {
  color: var(--bg);
  background: var(--blue);
  border-color: var(--blue);
}

/* --------------------------------------------------------------------
   Techniques/Tools index + detail pages (2026-08-26, docs/ARCHITECTURE.md #10)
   -------------------------------------------------------------------- */
.content-category-section {
  margin-bottom: 2.5rem;
}
.content-category-title {
  font-family: var(--mono);
  color: var(--green);
  font-size: 1.1rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.5rem;
  margin: 0 0 1rem;
  scroll-margin-top: 5rem;
}
/* Deliberately denser than the homepage's write-up cards -- these have no
   cover image and are meant to read as a compact glossary ("small cards"),
   not a scaled-down version of the same card. */
.content-card-grid {
  grid-template-columns: repeat(5, 1fr);
}
@media (max-width: 1024px) {
  .content-card-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 620px) {
  .content-card-grid { grid-template-columns: repeat(2, 1fr); }
}
.content-card .card-body {
  padding: 0.85rem 1rem;
}
/* Amber/purple title matching .tag-technique/.tag-tool's own colors on the
   write-up page, so which kind of card this is stays visible at a glance,
   same reasoning as the tag colors these mirror. (Tool cards used to just
   fall through to .card-title's shared default green -- that stopped being
   correct once .tag-tool itself moved off green to purple, 2026-08-27, so
   this needs its own explicit rule too now, not just the technique one.) */
.content-card-technique .card-title {
  color: var(--amber);
}
.content-card-tool .card-title {
  color: var(--purple);
}
.card-mitre-label {
  display: block;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-faint);
  margin-bottom: 0.15rem;
}
/* Shares .card-techniques/.card-tools's layout (flex row, anchor for
   .detail-pop, etc.) via that shared selector below -- blue instead of
   --text-faint, matching .tag-mitre's own color, is the only thing this
   overrides. */
.card-mitre {
  color: var(--blue);
}
/* content-index.js toggles [hidden] while filtering by search -- .card's own
   `display: block` (an author rule) otherwise wins over the browser's
   default `[hidden] { display: none }` (a UA-stylesheet rule, always lowest
   priority regardless of selector specificity), so this needs to be
   explicit rather than relying on the attribute alone. */
.content-card[hidden],
.content-category-section[hidden] {
  display: none !important;
}
/* Deliberately its own class, not a reuse of .related-writeups/.related-title
   -- writeup.js's Related-cards hydration pass (2026-08-26) selects
   .related-writeups specifically and expects detail-chips.js to be loaded
   alongside it, neither of which applies to this plain machine-name-only
   list (confirmed via a real ReferenceError the one time this borrowed the
   class name instead). */
.content-usage {
  border-top: 1px solid var(--border);
  margin-top: 2.5rem;
  padding-top: 2rem;
}
.content-usage-title {
  font-family: var(--mono);
  font-size: 1.05rem;
  color: var(--green);
  margin-bottom: 1rem;
}
/* A real grid, not flex with a hand-picked min-width (2026-08-27,
   owner-requested): badge/name/platform/date each get their own column,
   shared between the header row and every data row via `subgrid` -- a
   longer machine name or a wider badge (e.g. "Medium" vs "Easy") just
   grows its own column and every row (including the header) re-aligns
   with it automatically, no manual width tuning needed later. */
.content-usage-list {
  display: grid;
  grid-template-columns: 64px minmax(160px, 1fr) minmax(110px, 1fr) auto;
  column-gap: 0.9rem;
  font-family: var(--mono);
  font-size: 0.88rem;
}
.content-usage-header {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  padding: 0 0.2rem 0.5rem;
  border-bottom: 1px solid var(--border-strong);
}
.content-usage-header span {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-faint);
}
.content-usage-row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  padding: 0.65rem 0.2rem;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.content-usage-row:hover {
  color: var(--text);
  text-decoration: none;
  background: var(--bg-secondary);
}
.content-usage-row .badge {
  justify-self: start;
  margin-bottom: 0;
}
.content-usage-name {
  color: var(--green-muted);
  font-size: 0.92rem;
}
.content-usage-row:hover .content-usage-name {
  color: var(--green);
}
.content-usage-platform {
  color: var(--text-dim);
  font-size: 0.85rem;
}
.content-usage-date {
  color: var(--text-faint);
  font-size: 0.8rem;
}
.content-usage-more {
  display: inline-block;
  margin-top: 1rem;
  color: var(--green-dim);
  font-family: var(--mono);
  font-size: 0.85rem;
}
.content-usage-more:hover {
  color: var(--green);
}
@media (max-width: 620px) {
  .content-usage-platform,
  .content-usage-header-platform {
    display: none;
  }
}

/* --------------------------------------------------------------------
   Related write-ups + Previous/Next navigation (2026-08-25)
   -------------------------------------------------------------------- */
.related-writeups {
  border-top: 1px solid var(--border);
  margin-top: 2.5rem;
  padding-top: 2rem;
}
.related-title {
  font-family: var(--mono);
  font-size: 1.05rem;
  color: var(--green);
  margin-bottom: 1rem;
}

.writeup-prevnext {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-top: 2.5rem;
  padding-top: 2rem;
  border-top: 1px solid var(--border);
}
@media (max-width: 620px) {
  .writeup-prevnext {
    grid-template-columns: 1fr;
  }
}
.prevnext-link {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  /* Was transparent (just the page's own near-black --bg showing through)
     -- --bg-secondary is the same surface .card/.stat-tile already use, so
     Previous/Next reads as a real boxed control rather than floating
     directly on the page background (2026-08-26, owner feedback). */
  background: var(--bg-secondary);
  color: var(--text);
  text-decoration: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.8rem 1rem;
  transition: border-color 0.15s ease;
}
.prevnext-link:hover {
  border-color: var(--green-dim);
  color: var(--text);
  text-decoration: none;
}
.prevnext-next {
  grid-column: 2;
  justify-content: flex-end;
  text-align: right;
}
@media (max-width: 620px) {
  .prevnext-next {
    grid-column: 1;
  }
}
.prevnext-arrow {
  font-size: 1.1rem;
  color: var(--text-faint);
  flex-shrink: 0;
}
.prevnext-label {
  display: block;
  font-size: 0.65rem;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.2rem;
}
.prevnext-title {
  display: block;
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--green);
}
/* Difficulty badge + OS mark under the title (2026-08-26) -- same order as
   the homepage/Related write-ups cards' own .badge-row (difficulty first,
   then OS), so a reader gets a quick preview of what they're about to open
   without needing a full card. */
.prevnext-meta {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.35rem;
}
.prevnext-next .prevnext-meta {
  justify-content: flex-end;
}
/* .badge's own margin-bottom (meant for contexts where it sits above other
   content, e.g. the writeup-meta line) throws off align-items: center here
   against the OS mark next to it -- same fix .badge-row already needed for
   the same reason (2026-08-26). */
.prevnext-meta .badge {
  margin-bottom: 0;
}

/* --------------------------------------------------------------------
   Technique/tool explain overlay
   -------------------------------------------------------------------- */
.overlay-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(2, 5, 3, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  z-index: 100;
}
.overlay-backdrop[hidden] {
  display: none;
}
.overlay-panel {
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  max-width: min(90vw, 52rem);
  width: 100%;
  max-height: 80vh;
  overflow-y: auto;
  padding: 1.25rem 1.5rem;
}
.overlay-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.75rem;
}
.overlay-title {
  font-family: var(--mono);
  color: var(--green);
  font-size: 1rem;
  margin: 0;
}
.overlay-close {
  background: none;
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  border-radius: var(--radius);
  width: 1.8rem;
  height: 1.8rem;
  line-height: 1;
  cursor: pointer;
  flex: none;
}
.overlay-close:hover,
.overlay-close:focus-visible {
  color: var(--green);
  border-color: var(--green-dim);
}
.overlay-body {
  color: var(--text-dim);
  font-size: 0.9rem;
  line-height: 1.6;
}
.overlay-body p {
  margin: 0 0 0.75rem;
}
.overlay-body p:last-child {
  margin-bottom: 0;
}
.overlay-body img {
  display: block;
  width: 100%;
  border-radius: 4px;
}
#screenshot-lightbox .overlay-panel {
  max-width: min(94vw, 80rem);
}

/* TOC */
.toc {
  position: sticky;
  top: 5.5rem;
  font-size: 0.85rem;
  border-left: 1px solid var(--border);
  padding-left: 1rem;
}
.toc-title {
  font-family: var(--mono);
  color: var(--text-dim);
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  margin-bottom: 0.6rem;
}
.toc-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.toc-title-row .toc-title {
  margin-bottom: 0;
}
/* Bordered pill with a visible "Print" label (2026-08-27, owner-requested)
   -- was a bare icon-only button with no border, easy to miss/mistake for
   decoration; same printer icon throughout, just given a consistent,
   more discoverable treatment everywhere it appears (a write-up page's
   TOC, a technique/tool page's TOC, and the standalone version below for
   when there's no TOC to nest it in at all). */
.toc-print-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.25rem 0.55rem;
  border-radius: var(--radius);
  color: var(--text-dim);
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  font-family: var(--mono);
  font-size: 0.72rem;
  cursor: pointer;
  flex-shrink: 0;
}
.toc-print-btn svg {
  width: 13px;
  height: 13px;
  display: block;
}
.toc-print-btn:hover,
.toc-print-btn:focus-visible {
  color: var(--green);
  border-color: var(--green-dim);
}
.toc ul {
  list-style: none;
  margin: 0;
  padding: 0;
}
.toc li {
  margin-bottom: 0.4rem;
}
.toc a {
  color: var(--text-dim);
}
.toc a:hover {
  color: var(--green);
  text-decoration: none;
}
.toc a.active {
  color: var(--green);
  font-weight: 600;
}
.toc .toc-h3 {
  padding-left: 0.9rem;
  font-size: 0.8rem;
}
.toc .toc-h4 {
  padding-left: 1.7rem;
  font-size: 0.78rem;
}
.toc .toc-h5,
.toc .toc-h6 {
  padding-left: 2.5rem;
  font-size: 0.76rem;
}
.toc-mobile-toggle {
  display: none;
}
@media (max-width: 900px) {
  .toc {
    position: static;
    border-left: none;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 0.8rem 1rem;
    margin-bottom: 1.5rem;
  }
  .toc ul { display: none; }
  .toc.open ul { display: block; margin-top: 0.6rem; }
  .toc-mobile-toggle {
    display: inline-block;
    background: none;
    border: none;
    color: var(--green-muted);
    font-family: var(--mono);
    font-size: 0.78rem;
    cursor: pointer;
    padding: 0;
  }
}

/* article content */
.article-content h2 {
  font-family: var(--mono);
  color: var(--text);
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.4rem;
  margin-top: 2.2rem;
  scroll-margin-top: 5rem;
}
.article-content h3 {
  font-family: var(--mono);
  color: var(--green-muted);
  margin-top: 1.8rem;
  scroll-margin-top: 5rem;
}
.article-content p {
  color: var(--text);
}
.article-content ul, .article-content ol {
  color: var(--text);
}
.article-content li.checkbox {
  list-style: none;
  margin-left: -1.4rem;
}
.article-content li.checkbox input {
  margin-right: 0.5rem;
}

/* --------------------------------------------------------------------
   Contact form (About page, 2026-08-29)
   -------------------------------------------------------------------- */
.contact-section {
  max-width: 640px;
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid var(--border-strong);
}
.contact-section h2 {
  font-family: var(--mono);
  color: var(--text);
  margin: 0 0 0.5rem;
}
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 1.25rem;
}
.form-field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.form-field label {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--text-dim);
}
.form-field input,
.form-field textarea {
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  color: var(--text);
  font-family: var(--sans);
  font-size: 0.9rem;
  padding: 0.6rem 0.75rem;
  border-radius: var(--radius);
  outline: none;
  resize: vertical;
}
.form-field input:focus,
.form-field textarea:focus {
  border-color: var(--green-dim);
  box-shadow: 0 0 0 3px rgba(0, 255, 102, 0.08);
}
/* Web3Forms' own honeypot convention: a checkbox named "botcheck", hidden
   visually (not [hidden]/display:none on an ancestor -- some bots skip
   fields their own script can tell are display:none) but still present
   in the tab order removed via tabindex="-1" in the markup itself. */
.botcheck-field {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.btn-primary {
  align-self: flex-start;
  background: var(--green);
  color: var(--bg);
  border: none;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 0.85rem;
  padding: 0.65rem 1.4rem;
  border-radius: var(--radius);
  cursor: pointer;
}
.btn-primary:hover,
.btn-primary:focus-visible {
  background: var(--green-dim);
}
.btn-primary:disabled {
  opacity: 0.6;
  cursor: default;
}
.contact-success {
  color: var(--green);
  font-family: var(--mono);
  font-size: 0.85rem;
}
.contact-error {
  color: var(--red);
  font-family: var(--mono);
  font-size: 0.85rem;
}

blockquote {
  margin: 1rem 0;
  padding: 0.7rem 1rem;
  border-left: 3px solid var(--green-dim);
  background: var(--bg-secondary);
  color: var(--text-dim);
}

.callout {
  display: flex;
  gap: 0.7rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  margin: 1rem 0;
}
.callout .icon {
  color: var(--amber);
}

.column-list {
  display: flex;
  gap: 1.5rem;
  margin: 1.1rem 0;
  align-items: flex-start;
}
.column {
  flex: 1 1 0;
  min-width: 0; /* a wide code block/table/image inside a column must shrink
                   with it, not force the column past its share of the row */
}
.column > :first-child {
  margin-top: 0;
}
.column > :last-child {
  margin-bottom: 0;
}
/* A column whose only content is image(s) -- one or more, nothing else
   (an avatar/logo next to descriptive text, e.g. the About page, or two
   screenshots placed alone side by side; 2026-08-30 code-review finding
   widened this from exactly-one-image to any count, since the CSS below
   already applies per-image and needed no change for it) -- Notion's own
   manual image resize isn't exposed via the API (see blocks.py's
   _render_column()), so this is a fixed default rather than the real size
   the owner picked in Notion. Applies only to this specific shape; an
   image sharing its column with anything else still fills the column's
   own width, unchanged.
   flex: 0 0 auto (not the shared .column rule above) shrinks the column
   itself to fit the capped image -- without this override, the column
   kept its full Notion-drag width_ratio share of the row even after the
   image inside it shrank, leaving a dead gap before the text column and
   making the centered image look stranded in the middle of it (2026-08-30,
   owner-reported, found immediately after the cap first shipped). */
.column.column-single-image {
  flex: 0 0 auto;
}
.column-single-image figure.screenshot {
  max-width: 220px;
}
@media (max-width: 620px) {
  .column-list {
    flex-direction: column;
  }
}

.bookmark-link,
.file-attachment {
  margin: 1rem 0;
  padding: 0.6rem 1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-elevated);
}
.bookmark-link a {
  font-family: var(--mono);
  font-size: 0.9rem;
  color: var(--blue);
  word-break: break-all;
}
.file-attachment {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  color: var(--text-faint);
}
.file-attachment a {
  font-family: var(--mono);
  font-weight: 600;
  color: var(--green-muted);
}
.file-attachment-caption {
  color: var(--text-dim);
  font-size: 0.85rem;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0;
  font-size: 0.9rem;
}
th, td {
  border: 1px solid var(--border-strong);
  padding: 0.5rem 0.7rem;
  text-align: left;
}
th {
  background: var(--bg-secondary);
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: 0.8rem;
  text-transform: uppercase;
}

code.inline {
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: 0.1rem 0.35rem;
  font-size: 0.88em;
  color: var(--green-muted);
}

.code-block {
  position: relative;
  margin: 1.1rem 0;
  background: #08100b;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  overflow: hidden;
}
.code-block-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.4rem 0.8rem;
  border-bottom: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.header-actions {
  display: flex;
  gap: 0.4rem;
}
.copy-btn,
.wrap-btn {
  background: none;
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  font-family: var(--mono);
  font-size: 0.7rem;
  padding: 0.2rem 0.55rem;
  border-radius: 4px;
  cursor: pointer;
  text-transform: none;
  letter-spacing: normal;
}
.copy-btn:hover,
.wrap-btn:hover {
  color: var(--green);
  border-color: var(--green-dim);
}
.copy-btn.copied,
.wrap-btn.active {
  color: var(--green);
  border-color: var(--green-dim);
}
.wrap-btn.active {
  background: rgba(0, 255, 102, 0.08);
}
pre {
  margin: 0;
  padding: 0.9rem 1rem;
  overflow-x: auto;
}
pre code {
  font-size: 0.85rem;
  white-space: pre;
  color: #c9e6d3;
}
pre code.wrapped {
  white-space: pre-wrap;
  word-break: break-all;
}

figure.screenshot {
  margin: 1.2rem 0;
  position: relative;
}
figure.screenshot img {
  display: block;
  max-width: 100%;
  height: auto;
}
/* Only added by screenshot-lightbox.js, and only on a screenshot whose
   real resolution is wider than the article column -- a screenshot that
   already fits has nothing to zoom into. */
.screenshot-zoom-btn {
  position: absolute;
  right: 0.6rem;
  bottom: 0.6rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius);
  background: rgba(10, 16, 12, 0.85);
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  cursor: pointer;
}
.screenshot-zoom-btn svg {
  width: 16px;
  height: 16px;
  display: block;
}
.screenshot-zoom-btn:hover,
.screenshot-zoom-btn:focus-visible {
  color: var(--green);
  border-color: var(--green-dim);
}
.image-placeholder {
  background: var(--bg-secondary);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-faint);
  font-family: var(--mono);
  font-size: 0.8rem;
  height: 220px;
}
figcaption {
  font-size: 0.78rem;
  color: var(--text-faint);
  margin-top: 0.4rem;
  text-align: center;
}

/* Mini MITRE block */
.mitre-block {
  margin: 1.5rem 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg-secondary);
  padding: 1rem;
}
.mitre-block .mitre-title {
  font-family: var(--mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin-bottom: 0.6rem;
}
.mitre-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}
.mitre-item {
  font-family: var(--mono);
  font-size: 0.8rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.4rem 0.6rem;
  color: var(--green-muted);
  background: var(--bg-elevated);
}
.mitre-item .id {
  color: var(--green);
  margin-right: 0.4rem;
}

/* --------------------------------------------------------------------
   Error page (404)
   -------------------------------------------------------------------- */
.error-page {
  text-align: center;
  padding: 5rem 1rem;
}
.error-code {
  font-family: var(--mono);
  font-size: 5rem;
  color: var(--green);
  text-shadow: 0 0 24px rgba(0, 255, 102, 0.25);
  line-height: 1;
  margin-bottom: 0.5rem;
}
.error-page h1 {
  font-family: var(--mono);
  color: var(--text);
  margin: 0 0 0.6rem;
}
.error-page p {
  color: var(--text-dim);
  max-width: 46ch;
  margin: 0 auto 1.5rem;
}
.error-page .back-link {
  display: inline-flex;
  margin: 0 auto;
}

/* --------------------------------------------------------------------
   Stats page
   -------------------------------------------------------------------- */
.page-title {
  font-family: var(--mono);
  color: var(--green);
  font-size: 1.9rem;
  margin: 0 0 1.5rem;
}
.stats-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
  margin-bottom: 2.5rem;
}
@media (max-width: 1024px) {
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 620px) {
  .stats-grid { grid-template-columns: 1fr; }
}
.stat-tile {
  background: var(--bg-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.2rem;
  text-align: center;
}
.stat-tile .value {
  font-family: var(--mono);
  font-size: 2rem;
  color: var(--green);
}
.stat-tile .label {
  font-size: 0.78rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 0.3rem;
}
.stats-section {
  margin-bottom: 2.2rem;
}
.stats-section h2 {
  font-family: var(--mono);
  color: var(--text);
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.4rem;
  font-size: 1.1rem;
}
.stats-section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.6rem;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0.4rem;
  margin-bottom: 0.8rem;
}
.stats-section-header h2 {
  border-bottom: none;
  padding-bottom: 0;
  margin: 0;
}
.year-select-inline {
  background: var(--bg-elevated);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 0.35rem 0.6rem;
  font-family: var(--sans);
  font-size: 0.85rem;
}
.stat-bar-row {
  display: grid;
  /* 150px -> 230px (2026-08-26): technique/tool names run longer than
     platform/difficulty ones, especially now the clickable ones carry a
     trailing info icon too -- was truncating with an ellipsis too eagerly.
     230px comfortably fits the longest real name seen so far ("Insecure
     Deserialization" + icon, ~213px). Bar (1fr) shrinks to make room;
     still plenty wide to compare relative counts at a glance. */
  grid-template-columns: 230px 1fr 36px;
  align-items: center;
  gap: 0.8rem;
  margin-bottom: 0.55rem;
  font-size: 0.85rem;
}
.stat-bar-label {
  color: var(--text-dim);
  font-family: var(--mono);
  /* Wraps to a second line instead of truncating with an ellipsis
     (2026-08-26) -- a fixed column width plus truncation meant any name
     longer than whatever the widest one happened to be at the time would
     get cut off again, needing another manual width bump. Wrapping fits
     any length without ever needing to revisit this: that row just grows
     taller, the column width stays fixed so bars stay aligned. The 230px/
     150px (mobile) column widths above are still tuned to fit most real
     names on one line -- wrapping is the fallback for outliers, not the
     normal case. */
  overflow-wrap: break-word;
  line-height: 1.3;
}
/* Technique/tool bars are clickable buttons (open the explain overlay),
   reset to look like the plain text label they replace, then colored
   amber/green (matching .tag-technique/.tag-tool, see below) so they read
   as clickable the same way the write-up page's tags do. */
button.stat-bar-label {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font-size: inherit;
  text-align: left;
  cursor: pointer;
}
button.stat-bar-label.tag-technique {
  color: var(--amber);
}
button.stat-bar-label.tag-technique:hover,
button.stat-bar-label.tag-technique:focus-visible {
  color: var(--bg);
  background: var(--amber);
}
button.stat-bar-label.tag-tool {
  color: var(--purple);
}
button.stat-bar-label.tag-tool:hover,
button.stat-bar-label.tag-tool:focus-visible {
  color: var(--bg);
  background: var(--purple);
}
/* Platform/difficulty bars link to the homepage, pre-filtered (2026-08-28,
   stats.js's buildBarList()). Each label gets a real color below (teal for
   platform, matching the Category badge fix; the site's own existing
   diff-easy/medium/hard/insane colors for difficulty, same class names
   .badge already uses for the identical meaning, just applied to this
   different base component) -- hover/focus underlines instead of
   flattening to one neutral color, so the color that's doing real
   semantic work stays visible through the interaction, not just at rest. */
a.stat-bar-label:hover,
a.stat-bar-label:focus-visible {
  text-decoration: underline;
}
.stat-bar-label.stat-bar-label-platform { color: var(--teal); }
.stat-bar-label.diff-easy { color: var(--green-muted); }
.stat-bar-label.diff-medium { color: var(--amber); }
.stat-bar-label.diff-hard { color: var(--red); }
.stat-bar-label.diff-insane { color: var(--insane); }
/* Same outbound arrow MITRE badges already use (.tag-mitre::after) for the
   same reason: this is a real navigation, leaves the page, unlike the
   info-icon technique/tool tags use for "opens an overlay in place".
   Every real <a class="stat-bar-label"> on the page is one of these two
   filter links -- no separate marker class needed to scope this. */
a.stat-bar-label::after {
  content: " \2197";
}
.stat-bar-track {
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  height: 10px;
  overflow: hidden;
}
.stat-bar-fill {
  background: linear-gradient(90deg, var(--green-dim), var(--green));
  height: 100%;
}
.stat-bar-value {
  text-align: right;
  color: var(--text-faint);
  font-family: var(--mono);
}
.stat-bar-empty {
  color: var(--text-faint);
  font-family: var(--mono);
  font-size: 0.85rem;
}
@media (max-width: 500px) {
  .stat-bar-row {
    grid-template-columns: 150px 1fr 30px;
  }
}

/* --------------------------------------------------------------------
   Activity matrix (year x day-of-month)
   -------------------------------------------------------------------- */
.activity-matrix-wrap {
  overflow-x: auto;
  padding-bottom: 0.3rem;
}
.activity-matrix {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 700px;
}
.activity-row {
  display: grid;
  grid-template-columns: 42px repeat(31, 16px);
  gap: 3px;
  align-items: center;
}
.activity-month-label {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--text-dim);
}
.activity-day-label {
  font-family: var(--mono);
  font-size: 0.6rem;
  color: var(--text-faint);
  text-align: center;
}
.activity-cell {
  width: 16px;
  height: 16px;
  border-radius: 3px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-strong);
}
.activity-cell-empty {
  background: transparent;
  border: none;
}
.activity-cell-active {
  background: var(--green);
  border-color: var(--green);
  box-shadow: 0 0 4px rgba(0, 255, 102, 0.5);
}
/* Clickable day cells (2026-08-27) -- every active cell, in any view,
   is a real <a> element (see stats.js); the base .activity-cell rule
   above already gives them the right size/shape, this just adds the
   affordances a plain glowing square doesn't need. */
.activity-cell-link {
  display: block;
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}
.activity-cell-link:hover,
.activity-cell-link:focus-visible {
  box-shadow: 0 0 8px rgba(0, 255, 102, 0.9);
}

/* --------------------------------------------------------------------
   Print stylesheet (2026-08-24) -- a technical write-up (commands, code
   blocks, procedure steps) is a plausible print/"save as PDF" candidate.
   Strips chrome that only makes sense on screen (nav, hero, TOC, reading
   progress, interactive tag/copy/wrap buttons), switches to an
   ink-friendly light theme instead of printing the full dark theme, and
   collapses the two-column layout to one. External links print their
   URL since a paper copy can't be clicked.
   -------------------------------------------------------------------- */
@media print {
  .site-header,
  .reading-progress,
  .page-hero,
  .back-links,
  /* Real gap found while adding the standalone print button below
     (2026-08-27): the plain (non-range) .back-link on a write-up page
     was never in this list at all -- only its .back-links wrapper was,
     which the range case uses but the plain case doesn't. Fixed alongside
     .content-top-row (also covers the technique/tool page's own
     .back-link, plus the new standalone print button itself -- printing
     a button that prints the page would be silly). */
  .back-link,
  .content-top-row,
  .toc,
  .site-footer,
  .header-actions,
  .fog,
  .range-nav,
  #detail-overlay,
  #screenshot-lightbox,
  .screenshot-zoom-btn {
    display: none !important;
  }

  body {
    background: #ffffff;
    color: #1a1a1a;
  }

  .writeup-layout {
    display: block;
    max-width: 100%;
  }

  .writeup-header {
    border-bottom-color: #ccc;
  }
  .writeup-header h1 {
    color: #111111;
  }
  .writeup-meta {
    color: #444444;
  }
  .badge {
    color: #333333;
  }
  .tag,
  .tag-technique,
  .tag-tool,
  .tag-mitre {
    color: #333333;
    border-color: #999999;
    background: #f2f2f2;
  }
  .tag-technique-plain,
  .tag-tool-plain {
    color: #333333;
  }
  .tag-mitre::after,
  .tag-technique::after,
  .tag-tool::after,
  .stat-bar-label::after {
    content: "";
  }

  .article-content h2,
  .article-content h3,
  .content-category-title {
    color: #111111;
    border-bottom-color: #ccc;
  }
  .article-content p,
  .article-content li {
    color: #1a1a1a;
  }
  .article-content a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 0.75em;
    color: #555555;
  }

  .code-block {
    background: #f7f7f7;
    border-color: #cccccc;
    break-inside: avoid;
  }
  .code-block-header {
    color: #666666;
    border-bottom-color: #dddddd;
  }
  pre {
    overflow-x: visible;
  }
  pre code {
    color: #111111;
    /* On screen, an unwrapped long line just scrolls horizontally inside
       `pre` (overflow-x: auto) -- the Wrap button opts a block into
       white-space: pre-wrap instead. Print has no scrollbar, so a code
       block left in its default unwrapped state was silently clipped at
       the page edge instead. Force-wrap every code block for print,
       regardless of its on-screen .wrapped toggle state -- there's no
       print equivalent of "scroll to see the rest." */
    white-space: pre-wrap;
    word-break: break-all;
  }

  figure.screenshot {
    break-inside: avoid;
  }
  .image-placeholder {
    border-color: #999999;
    color: #666666;
  }
}
