/* ===========================================================================
   Penjester — site
   Every value here is a token. No hex, no raw oklch, no font-family string.
   =========================================================================== */

/* tokens.css is linked from every page rather than imported here: an @import
   cannot start downloading until this file has arrived and been parsed, so the
   custom properties and the @font-face rules always landed a round-trip late.
   That window is what rendered the opening mark as a full-screen black square.
   Kept as a comment so nobody helpfully adds it back. */

*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  /* clip, never hidden: `hidden` on the root kills position:sticky in the
     docs table of contents, and the bug looks like a CSS typo for an hour. */
  overflow-x: clip;
  background: var(--color-paper);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  font-feature-settings: "kern", "liga";
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

img, svg { display: block; max-width: 100%; }

::selection { background: var(--color-accent); color: var(--color-paper); }

:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}

/* ---- type ---------------------------------------------------------------
   Headings are roman, always. An italicised word inside an upright heading is
   the single most reliable tell that nobody chose it.
   ------------------------------------------------------------------------- */

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 700;
  letter-spacing: -0.022em;
  line-height: var(--leading-tight);
  margin: 0 0 0.5em;
  overflow-wrap: anywhere;
  min-width: 0;
}

h1 { font-size: var(--text-4xl); line-height: var(--leading-display); font-weight: 800; }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-xl); font-weight: 600; letter-spacing: -0.015em; }
h4 { font-size: var(--text-base); font-weight: 600; letter-spacing: -0.01em; }

p { margin: 0 0 1.1em; max-width: var(--measure); }

ul, ol { max-width: var(--measure); }
section li { margin: 0 0 0.45em; color: var(--color-ink-2); }
p:last-child { margin-bottom: 0; }

a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--color-rule);
  transition: border-color var(--dur-fast) var(--ease-out);
}
a:hover { border-bottom-color: var(--color-accent); }

code, kbd, samp { font-family: var(--font-mono); font-size: 0.88em; }
code { color: var(--color-ink-2); }

em { font-style: italic; }
strong, b { font-weight: 650; }

.lead {
  font-size: var(--text-lg);
  color: var(--color-ink-2);
  max-width: 50rem;
}

.muted { color: var(--color-ink-mute); }
.small { font-size: var(--text-sm); }
.measure { max-width: var(--measure); }

/* The one place a label is allowed to shout. Mono, tracked, tiny. */
.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
  margin: 0 0 var(--space-md);
}

/* ---- shell --------------------------------------------------------------- */

.shell {
  width: 100%;
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

section { padding-block: var(--space-3xl); border-top: var(--rule-hair); }
section[id], [id] { scroll-margin-top: 84px; }
section:first-of-type { border-top: 0; }

/* ---- the board's legend -------------------------------------------------
   This used to be a marquee across the top of the page. A marquee is motion
   asking to be watched, and what it was asking on behalf of is a legend — the
   same five verdicts the board underneath already prints. So it sits with the
   board now, static, where a reader meets the words in the place they mean
   something. One fewer thing moving, one fewer thing to explain.
   ------------------------------------------------------------------------- */

.board-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
  align-items: center;
  padding: var(--space-md) var(--space-lg);
  border-top: var(--rule-mid);
  background: var(--color-paper-3);
}
.legend-head {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
  width: 100%;
  margin-bottom: var(--space-2xs);
}
.legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--color-ink-mute);
}
.legend-item .verdict { flex: none; }
.legend-aside {
  width: 100%;
  margin-top: var(--space-2xs);
  padding-top: var(--space-sm);
  border-top: var(--rule-hair);
  color: var(--color-ink-faint);
}

.dot { width: 5px; height: 5px; border-radius: var(--radius-pill); background: currentColor; }
.v-leak    { color: var(--color-accent); }
.v-held    { color: var(--color-held); }
.v-review  { color: var(--color-review); }
.v-skipped { color: var(--color-skipped); }

/* ---- masthead ------------------------------------------------------------ */

/* Opaque, not translucent. At 88% with a blur, an 800-weight display heading
   scrolling underneath read straight through the bar — the blur softened it
   into a smear rather than hiding it. A sticky header has one job and doing
   it 88% of the way is worse than not being sticky. */
.top { position: sticky; top: 0; z-index: 40;
       background: var(--color-paper);
       border-bottom: var(--rule-hair);
       transition: box-shadow var(--dur-base) var(--ease-out); }
.top.is-stuck { box-shadow: 0 1px 0 var(--color-hairline),
                            0 10px 28px -18px rgb(0 0 0 / 0.9); }
/* min-width: 0 on both the row and the nav is the whole fix for mobile.
   A flex item refuses to shrink below its min-content width by default, and four
   nowrap nav links plus the wordmark are wider than a phone. The row then grew
   past the viewport, every .shell inherited that width from body, and
   `overflow-x: clip` quietly sliced the right edge off every section on the site
   — headings, body copy and all. It reads as a dozen unrelated bugs. */
.top .shell { display: flex; align-items: center; gap: var(--space-xl);
              min-height: 58px; min-width: 0; }

.brand {
  display: inline-flex; align-items: center; gap: var(--space-sm);
  flex: none;
  border: 0;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: -0.03em;
  font-size: var(--text-lg);
}
.brand img { width: 22px; height: 22px; }
.tag-beta {
  font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: 0.08em; text-transform: uppercase; font-weight: 500;
  color: var(--color-ink-mute);
  border: 1px solid var(--color-rule); border-radius: var(--radius-xs);
  padding: 0.08rem 0.34rem; margin-left: 0.45rem; vertical-align: 0.18em;
}

.top nav { margin-left: auto; display: flex; gap: var(--space-lg);
           align-items: center; min-width: 0;
           /* Scrolls rather than pushes, on the narrowest phones only. Nothing is
              hidden behind a menu — a control nobody can find is not a control. */
           overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.top nav::-webkit-scrollbar { display: none; }
.top nav a {
  border: 0;
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-ink-mute);
  white-space: nowrap;
  padding-block: var(--space-2xs);
}
.top nav a:hover, .top nav a[aria-current="page"] { color: var(--color-ink); }
.top nav a[aria-current="page"]:not(.nav-cta) { box-shadow: 0 1px 0 var(--color-accent); }

/* The bar had four plain links and no action in it. Three of them explain the
   tool; the fourth is the thing a reader came to do, so it stops looking like
   prose. The hairline before it is what makes the group read as "here are the
   pages" and then "here is the button", rather than as five words in a row. */
.nav-sep {
  width: 1px;
  align-self: stretch;
  margin-inline: var(--space-2xs);
  background: var(--color-hairline);
}

.top nav a.nav-cta {
  color: var(--color-ink);
  padding: 0.34rem 0.85rem;
  font-size: var(--text-xs);
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: var(--color-paper-2);
}
.top nav a.nav-cta:hover {
  border-color: var(--color-accent);
  background: var(--color-accent-wash);
  color: var(--color-ink);
}
.top nav a.nav-cta[aria-current="page"] {
  border-color: var(--color-accent);
  background: var(--color-accent-wash);
}

/* ---- buttons ------------------------------------------------------------- */

.btn {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
  padding: 0.6rem 1.15rem;
  border: 1px solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: var(--color-paper-2);
  color: var(--color-ink);
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
.btn:hover { background: var(--color-paper-3); border-color: var(--color-ink-faint); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--color-accent); border-color: var(--color-accent);
               color: var(--color-paper); }
.btn.primary:hover { background: var(--color-accent-hot); border-color: var(--color-accent-hot); }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--space-sm);
           margin-top: var(--space-xl); }

/* ---- hero ----------------------------------------------------------------
   Marquee: the headline is the largest thing on the page by a wide margin and
   sits on its own, with the argument underneath at a fifth the size. The
   contrast is the layout.
   ------------------------------------------------------------------------- */

.hero { padding-block: var(--space-3xl) var(--space-2xl); border-top: 0; }
/* Short enough for the full display scale, which is the point of having one.
   Two lines, broken in the markup so the pairing holds at any width. */
.hero h1 {
  font-size: clamp(2.7rem, 7.6vw, 6rem);
  max-width: 22ch;
  margin-bottom: var(--space-lg);
}
.hero .lead { font-size: var(--text-xl); max-width: 46rem; }

.tagline {
  position: relative;
  font-family: var(--font-ui);
  font-size: var(--text-lg);
  font-weight: 600;
  font-style: normal;
  letter-spacing: -0.015em;
  color: var(--color-ink);
  margin: 0 0 var(--space-lg);
  padding-left: var(--space-md);
  border-left: 2px solid var(--color-rule);
  max-width: 40rem;
}
.hero-meta {
  display: flex; flex-wrap: wrap; gap: var(--space-lg);
  margin-top: var(--space-xl); padding-top: var(--space-lg);
  border-top: var(--rule-hair);
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--color-ink-faint);
}
.hero-meta b { color: var(--color-ink-2); font-weight: 500; }

/* ---- the board ----------------------------------------------------------
   This is the page's one enrichment and the reason the layout works: the
   product's actual output is a board of rows with verdicts, so the airport
   departures device is not a costume borrowed from a travel site — it is the
   literal shape of what Penjester prints. Pure CSS grid, no table, so it can
   restack on a phone without a horizontal scrollbar.
   ------------------------------------------------------------------------- */

.board { border: var(--rule-mid); border-radius: var(--radius-md); overflow: hidden;
         background: var(--color-paper-2); }

.board-head {
  display: flex; align-items: baseline; gap: var(--space-md); flex-wrap: wrap;
  padding: var(--space-md) var(--space-lg);
  border-bottom: var(--rule-mid);
  background: var(--color-paper-3);
  font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-ink-mute);
}
.board-head .ts { margin-left: auto; color: var(--color-ink-faint); }

.board-row {
  display: grid;
  grid-template-columns: minmax(0, 2.4fr) minmax(0, 1fr) minmax(0, 1fr) auto;
  gap: var(--space-md);
  align-items: center;
  padding: var(--space-md) var(--space-lg);
  border-bottom: var(--rule-hair);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}
.board-row:last-child { border-bottom: 0; }
.board-row .ep { color: var(--color-ink); overflow-wrap: anywhere; min-width: 0; }
.board-row .ep small { display: block; font-size: var(--text-2xs);
                       color: var(--color-ink-faint); letter-spacing: 0.04em;
                       margin-top: 2px; }
.board-row .who { color: var(--color-ink-mute); font-size: var(--text-xs); }
.board-row .who b { display: block; color: var(--color-ink-2); font-weight: 500;
                    font-size: var(--text-sm); }

.verdict {
  display: inline-flex; align-items: center; gap: var(--space-xs);
  font-size: var(--text-2xs); letter-spacing: 0.1em; text-transform: uppercase;
  font-weight: 500; white-space: nowrap;
  padding: 0.25rem 0.55rem;
  border: 1px solid currentColor; border-radius: var(--radius-xs);
}
/* The words are the extension's own — McpTools.verdictWord prints LEAK,
   DENIED, SCOPED, REVIEW — so a reader who has seen the Burp tab sees the same
   five things on this page. The site had invented "Held", which is the gloss
   rather than the label, and a product page using different nouns from the
   product is the first thing an evaluator stops trusting. */
.verdict.leak    { color: var(--color-accent); background: var(--color-accent-wash); }
.verdict.denied  { color: var(--color-held); }
.verdict.scoped  { color: var(--color-held); }
.verdict.review  { color: var(--color-review); }
.verdict.skipped { color: var(--color-skipped); }

.board-foot {
  padding: var(--space-md) var(--space-lg);
  border-top: var(--rule-mid);
  background: var(--color-paper);
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--color-ink-faint);
}
.board-foot b { color: var(--color-ink-2); font-weight: 500; }

@media (max-width: 760px) {
  /* Shortened for a phone: the endpoint on its own line with its reason sub-text
     dropped, then alice, bob and the verdict all on one line — two lines per row
     instead of four. */
  .board-row { grid-template-columns: max-content max-content 1fr;
               column-gap: var(--space-md); row-gap: var(--space-2xs);
               align-items: center; padding-block: var(--space-sm); }
  .board-row .ep { grid-column: 1 / -1; margin-bottom: var(--space-2xs); }
  .board-row .ep small { display: none; }
  .board-row .verdict { justify-self: end; }
}

/* ---- numbered sections --------------------------------------------------
   Hyperlane's rhythm: a mono index on the left, the claim on the right, and a
   rule between every entry. Used on the features index, where the page is a
   list and pretending otherwise would be a worse page.
   ------------------------------------------------------------------------- */

.entries { border-top: var(--rule-mid); }

.entry {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) minmax(0, 13rem);
  gap: var(--space-lg);
  padding-block: var(--space-xl);
  border-bottom: var(--rule-hair);
  align-items: start;
}
.entry .idx {
  font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 500;
  letter-spacing: 0.1em; color: var(--color-ink-faint);
  padding-top: 0.35rem;
}
.entry h3 { margin-bottom: 0.35em; }
.entry p { color: var(--color-ink-mute); font-size: var(--text-base); }
.entry .where {
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--color-ink-faint); text-align: right; padding-top: 0.4rem;
  line-height: 1.8;
}
.entry .where span { display: block; }
.entry .where b { color: var(--color-ink-2); font-weight: 500; }

@media (max-width: 860px) {
  .entry { grid-template-columns: 2.5rem minmax(0, 1fr); }
  .entry .where { grid-column: 2; text-align: left; padding-top: var(--space-sm); }
}

/* ---- cards --------------------------------------------------------------- */

.grid { display: grid; gap: var(--space-lg); }
.grid.two   { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.four  { grid-template-columns: repeat(4, minmax(0, 1fr)); }

.card {
  border: var(--rule-hair); border-radius: var(--radius-md);
  background: var(--color-paper-2);
  padding: var(--space-lg);
}
.card .n {
  display: block; font-family: var(--font-mono); font-size: var(--text-xs);
  letter-spacing: 0.12em; color: var(--color-ink-faint);
  margin-bottom: var(--space-sm);
}
.card h3 { margin-bottom: 0.4em; }
.card p { color: var(--color-ink-mute); font-size: var(--text-sm); }

/* ---- evidence ------------------------------------------------------------ */

.evidence {
  border: var(--rule-mid);
  border-left: 2px solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-paper-2);
  padding: var(--space-lg);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.85;
  color: var(--color-ink-mute);
  overflow-x: auto;
}
.evidence b { color: var(--color-ink); font-weight: 500; }
.evidence .hit { color: var(--color-accent); }
.evidence .ok  { color: var(--color-held); }

/* ---- refusals list ------------------------------------------------------- */

.vs { list-style: none; margin: 0; padding: 0; }
.vs li {
  display: grid; grid-template-columns: 1.6rem minmax(0, 1fr); gap: var(--space-md);
  padding-block: var(--space-lg); border-top: var(--rule-hair);
}
.vs li:first-child { border-top: 0; padding-top: 0; }
.vs .mk { font-family: var(--font-mono); color: var(--color-accent); }
.vs b { display: block; font-family: var(--font-ui); font-weight: 600;
        letter-spacing: -0.01em; margin-bottom: 0.25em; }
.vs .t { color: var(--color-ink-mute); font-size: var(--text-sm); }

/* ---- notes --------------------------------------------------------------- */

.note {
  border: var(--rule-hair); border-left: 2px solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: var(--color-paper-2);
  padding: var(--space-md) var(--space-lg);
  font-size: var(--text-sm);
  color: var(--color-ink-mute);
}
.note.warn { border-left-color: var(--color-rule); }
.note.hard { border-left-color: var(--color-rule); }
.note .h { font-family: var(--font-ui); font-weight: 600;
           color: var(--color-ink); margin-bottom: 0.3em; }

/* ---- pills --------------------------------------------------------------- */

.pill {
  display: inline-block;
  font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: 0.1em; text-transform: uppercase;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--color-rule); border-radius: var(--radius-xs);
  color: var(--color-ink-mute);
}
.pill.live { border-color: var(--color-held); color: var(--color-held); }
.pill.soon { border-color: var(--color-review); color: var(--color-review); }
.pill.dig  { border-color: var(--color-accent); color: var(--color-accent); }
.pill.no   { border-color: var(--color-rule); color: var(--color-ink-faint); }

/* ---- tables -------------------------------------------------------------- */

/* Styled as elements, not only as a class. The pages written before this
   stylesheet use bare <table> and <pre>, and the port carried the markup across
   without carrying the rules — so every requirements table and every command
   block rendered as raw browser default while the rest of the page looked
   designed. A visitor reads that as a half-finished site, correctly. */

table { width: 100%; border-collapse: collapse; margin: 0 0 1.6em;
        font-size: var(--text-sm); }
th, td { text-align: left; padding: var(--space-sm) var(--space-md);
         border-bottom: var(--rule-hair); vertical-align: top; }
th { font-family: var(--font-mono); font-size: var(--text-2xs);
     letter-spacing: 0.12em; text-transform: uppercase;
     color: var(--color-ink-faint); font-weight: 500;
     border-bottom-color: var(--color-rule); white-space: nowrap; }
td { color: var(--color-ink-mute); }
td:first-child { color: var(--color-ink-2); }
tr:last-child td { border-bottom: 0; }

.tbl { }                       /* kept so existing markup still reads as intent */
.tbl td:first-child { font-family: var(--font-mono); white-space: nowrap; }
.tbl-scroll { overflow-x: auto; }

/* A command block is something you copy, so it is set apart and it scrolls
   rather than wrapping a path across two lines. */
pre {
  margin: 0 0 1.5em;
  padding: var(--space-md) var(--space-lg);
  border: var(--rule-hair);
  border-left: 2px solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-paper-2);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  line-height: 1.75;
  color: var(--color-ink-2);
  overflow-x: auto;
  max-width: 54rem;
}
pre code { background: none; border: 0; padding: 0; color: inherit; font-size: inherit; }

/* Inline code is marked, not boxed — a border on every third word is noise. */
code:not(pre code) {
  padding: 0.1em 0.3em;
  border-radius: var(--radius-xs);
  background: var(--color-paper-3);
  color: var(--color-ink-2);
}

hr { border: 0; border-top: var(--rule-hair); margin: var(--space-xl) 0; }

/* ---- docs layout --------------------------------------------------------- */

.doc { display: grid; grid-template-columns: 220px minmax(0, 1fr);
       gap: var(--space-2xl); padding-block: var(--space-2xl); }
.toc { position: sticky; top: 80px; align-self: start; font-size: var(--text-sm); }
.toc ol { list-style: none; margin: 0; padding: 0; }
.toc li { margin: 0 0 1px; }
.toc a { border: 0; display: block; padding: 4px 10px;
         color: var(--color-ink-faint);
         border-left: 2px solid transparent;
         font-family: var(--font-ui); font-size: var(--text-sm); }
.toc a:hover { color: var(--color-ink); background: var(--color-paper-2); }
.toc a.active { color: var(--color-ink); border-left-color: var(--color-ink-mute); }
.toc .grp { font-family: var(--font-mono); font-size: var(--text-2xs);
            letter-spacing: 0.12em; text-transform: uppercase;
            color: var(--color-ink-faint); margin: var(--space-lg) 0 var(--space-xs);
            padding-left: 10px; }
.toc .grp:first-child { margin-top: 0; }

.doc article h2 { margin-top: 2.2em; scroll-margin-top: 88px; }
.doc article h2:first-of-type { margin-top: 0; }
.doc article h3 { margin-top: 1.9em; scroll-margin-top: 88px; }
.doc article ul, .doc article ol { padding-left: 1.25em; margin: 0 0 1.2em;
                                   max-width: var(--measure); }
.doc article li { margin: 0 0 0.45em; color: var(--color-ink-2); }
.doc article > h1 { font-size: var(--text-4xl); margin-bottom: 0.35em; }

/* ---- screenshots --------------------------------------------------------
   A capture that does not exist yet shows what to capture, so an unfinished
   page looks unfinished on purpose rather than broken by accident.
   ------------------------------------------------------------------------- */

.shot { margin: 0; position: relative; }
/* A window frame, so a screenshot reads as a deliberate product shot rather than a raw
   paste: a chrome bar with three muted dots, a border that seams into the image, and a
   soft elevation under it. The dots are grey on purpose, no traffic-light colour. */
.shot::before {
  content: attr(data-tab);
  display: block; height: 30px; line-height: 30px;
  padding-left: 68px;
  font-family: var(--font-mono); font-size: var(--text-2xs);
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-ink-faint);
  white-space: nowrap; overflow: hidden;
  border: var(--rule-mid); border-bottom: 0;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  background:
    radial-gradient(circle 3.5px at 19px 15px, var(--color-ink-faint) 97%, transparent 100%),
    radial-gradient(circle 3.5px at 35px 15px, var(--color-ink-faint) 97%, transparent 100%),
    radial-gradient(circle 3.5px at 51px 15px, var(--color-ink-faint) 97%, transparent 100%),
    var(--color-paper-3);
  transition: transform var(--dur-base) var(--ease-out);
}
.shot img { display: block; width: 100%;
            border: var(--rule-mid); border-top: 0;
            border-radius: 0 0 var(--radius-md) var(--radius-md);
            background: var(--color-paper-2);
            box-shadow: 0 28px 64px -28px rgba(0, 0, 0, 0.75);
            transition: transform var(--dur-base) var(--ease-out),
                        box-shadow var(--dur-base) var(--ease-out); }
.shot:hover::before, .shot:hover img { transform: translateY(-5px); }
.shot:hover img { box-shadow: 0 40px 92px -30px rgba(0, 0, 0, 0.9); }
.shot .pending { display: none; }
.shot.is-pending img { display: none; }
.shot.is-pending::before { display: none; }
.shot.is-pending .pending {
  display: grid; place-content: center; gap: var(--space-xs);
  min-height: 220px; text-align: center;
  border: 1px dashed var(--color-rule); border-radius: var(--radius-md);
  background: var(--color-paper-2);
  font-family: var(--font-mono); font-size: var(--text-xs);
  padding: var(--space-xl) var(--space-md);
}
.shot .what  { color: var(--color-ink-2); }
.shot .where { color: var(--color-ink-faint); }
.shot figcaption { margin-top: var(--space-md); font-size: var(--text-sm);
                   line-height: var(--leading-snug); color: var(--color-ink-mute);
                   max-width: var(--measure); }
.shot figcaption b { color: var(--color-ink); font-weight: 600; }

/* ---- footer -------------------------------------------------------------- */

.bottom { border-top: var(--rule-mid); padding-block: var(--space-2xl); }
.bottom .cols { display: grid; grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
                gap: var(--space-xl); }
.bottom h4 { font-family: var(--font-mono); font-size: var(--text-2xs);
             letter-spacing: 0.14em; text-transform: uppercase;
             color: var(--color-ink-faint); font-weight: 500;
             margin-bottom: var(--space-md); }
.bottom ul { list-style: none; margin: 0; padding: 0; }
.bottom li { margin-bottom: var(--space-xs); }
.bottom a { border: 0; font-family: var(--font-ui); font-size: var(--text-sm);
            color: var(--color-ink-mute); }
.bottom a:hover { color: var(--color-ink); }
.bottom .colophon { margin-top: var(--space-2xl); padding-top: var(--space-lg);
                    border-top: var(--rule-hair);
                    display: flex; flex-wrap: wrap; gap: var(--space-md);
                    font-family: var(--font-mono); font-size: var(--text-xs);
                    color: var(--color-ink-faint); }
.bottom .colophon .sp { margin-left: auto; }

/* ---- spacing utilities --------------------------------------------------
   Named because `style-src 'self'` has no 'unsafe-inline': an inline
   style= attribute is dropped by the browser, and only in production.
   ------------------------------------------------------------------------- */

.mt-s { margin-top: var(--space-md); }
.mt-m { margin-top: var(--space-lg); }
.mt-l { margin-top: var(--space-xl); }
.mt-xl { margin-top: var(--space-2xl); }
.mb-s { margin-top: 0; margin-bottom: var(--space-md); }
.mb-l { margin-bottom: var(--space-xl); }
.pt-0 { padding-top: 0; }
.pb-m { padding-bottom: var(--space-xl); }
.pb-l { padding-bottom: var(--space-2xl); }

/* Install steps: h2 carries no top margin, so numbered steps run together. Give each
   step real air above it; the first sits flush under the hero divider. */
.steps h2 { margin-top: var(--space-2xl); }
.steps h2:first-child { margin-top: 0; }
@media (max-width: 760px) { .steps h2 { margin-top: var(--space-xl); } }

/* ---- responsive ----------------------------------------------------------
   320 / 375 / 414 / 768 all verified. The rules that matter: nothing
   clickable wraps to two lines, every image-bearing track is minmax(0, 1fr),
   and the gutter never drops below 18px.
   ------------------------------------------------------------------------- */

@media (max-width: 980px) {
  .doc { grid-template-columns: minmax(0, 1fr); gap: var(--space-xl); }
  .toc { position: static; border-bottom: var(--rule-hair);
         padding-bottom: var(--space-md); }
  .toc ol { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
  .toc a { border-left: 0; border-bottom: 2px solid transparent; padding: 4px 8px; }
  .toc a.active { border-left: 0; border-bottom-color: var(--color-ink-mute); }
  .toc .grp { display: none; }
  .grid.four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .bottom .cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 760px) {
  section { padding-block: var(--space-xl); }
  .grid.two, .grid.three, .grid.four { grid-template-columns: minmax(0, 1fr); }
  .top .shell { gap: var(--space-md); min-height: 54px; }
  .top nav { gap: var(--space-md); }
  .top nav a { font-size: var(--text-xs); }
  .bottom .cols { grid-template-columns: minmax(0, 1fr); gap: var(--space-lg); }
  .bottom .colophon .sp { margin-left: 0; width: 100%; }

  /* Type on a phone. The desktop display scale floors far too high here — the
     hero h1 alone filled the screen — so the whole scale steps down a full tier
     and the hero's vertical rhythm tightens, because a headline that reads well
     at arm's length on a laptop is a billboard held 30cm from your face. Body
     copy drops to 16px with tighter leading; headings re-clamp so the viewport
     does the work instead of a fixed floor. */
  body { font-size: 1rem; line-height: 1.55; }
  p { margin-bottom: 0.9em; }
  .eyebrow { margin-bottom: var(--space-sm); }
  .hero { padding-block: var(--space-lg) var(--space-xl); }
  .hero h1 { font-size: clamp(1.6rem, 6.8vw, 2.4rem); line-height: 1.1;
             margin-bottom: var(--space-sm); text-wrap: balance; }
  /* The desktop line break ("…AI era" / "needs an upgrade.") orphans "era" once
     the first half already wraps on a phone. Drop it and let balance set the rag. */
  .hero h1 br { display: none; }
  h1, .doc article > h1 { font-size: clamp(1.6rem, 6.6vw, 2.5rem); }
  h2 { font-size: clamp(1.4rem, 5.4vw, 1.9rem); }
  h3 { font-size: var(--text-base); }
  .lead, .hero .lead { font-size: var(--text-base); line-height: 1.5; }
  .tagline { font-size: var(--text-sm); padding-left: var(--space-sm);
             margin-bottom: var(--space-md); }
  /* The hero already says it twice (bold tagline, then the serif lead); the muted
     "a personal tool…" note is a third pass that only pushes the board further down
     a phone. Drop it on small screens — desktop still shows it. */
  .hero .small.muted { display: none; }
  .btn-row { margin-top: var(--space-md); }
  .hero-meta { font-size: var(--text-xs); gap: var(--space-xs) var(--space-md);
               margin-top: var(--space-md); padding-top: var(--space-md); }

  /* A 64px-blur shadow under a full-width image is a heavy composite to repaint
     on every scroll frame, and the hover lift never fires on a touch screen. Cut
     both back so scrolling stays smooth on a phone. */
  .shot img { box-shadow: 0 14px 30px -22px rgba(0, 0, 0, 0.7); }
  .shot:hover::before, .shot:hover img { transform: none; }
  .shot:hover img { box-shadow: 0 14px 30px -22px rgba(0, 0, 0, 0.7); }

  /* Compact the dense blocks. Cards, finding boxes, the board legend and code
     all carry desktop padding and type that crowd a narrow screen — this is the
     "make it compact" pass, not a content change. */
  .grid { gap: var(--space-md); }
  .card { padding: var(--space-md); }
  .card .n { margin-bottom: var(--space-xs); }
  .vs li { padding-block: var(--space-md); }
  .note { padding: var(--space-md); }

  /* The mono evidence boxes were 14px, which wrapped a long path mid-line and
     read as a sloppy paste. 12px fits "GET /api/teams/…/details" on one line and
     tightens the whole block. */
  .evidence { font-size: var(--text-xs); line-height: 1.7; padding: var(--space-md); }
  pre { font-size: var(--text-xs); line-height: 1.65; padding: var(--space-md); }

  /* On a phone the legend is just the verdict words — no boxes, no sentences. The
     bordered pill grid read as clutter; this is a plain inline key, "got through"
     in the one red and the rest neutral, middot-separated. The board rows above
     show each verdict in use, so the words alone are enough here. (font-size:0 on
     the item collapses the bare explanation text node; the word restores its own
     rem size.) */
  .board-legend { display: block; padding: var(--space-md); line-height: 2; }
  .legend-head { display: block; margin-bottom: var(--space-2xs); }
  .legend-item { display: inline; font-size: 0; margin: 0; padding: 0; border: 0; }
  .legend-item .verdict { display: inline; font-size: var(--text-2xs);
                          border: 0; background: none; padding: 0; white-space: normal; }
  .legend-item:not(:last-child) .verdict::after {
    content: "·"; margin: 0 0.3ch 0 0.7ch; color: var(--color-ink-faint); }
  .legend-aside { display: inline; margin: 0; padding: 0; border: 0; }

  /* Secondary copy dropped on a phone to keep a section short. */
  .sm-hide { display: none; }
  .board-head, .board-row, .board-foot { padding-inline: var(--space-md); }
}

/* ---- mobile header: two rows, nothing hidden -----------------------------
   The single scrolling nav row pushed the Install button off the right edge on
   a phone, and a bug compounded it: the `.btn` full-width rule below was also
   catching the Install nav button (it carries class `btn`) and blowing it up to
   the nav's whole width, forcing the row to scroll. The brand now sits on its
   own row; the links spread full width beneath it on a hairline, so every link
   including Install is visible at once with no horizontal scroll. */
@media (max-width: 560px) {
  .top .shell { flex-wrap: wrap; align-items: center; gap: 0;
                min-height: 0; padding-block: var(--space-sm); }
  .brand { flex: 1 1 auto; }
  .top nav { width: 100%; margin-left: 0; margin-top: var(--space-sm);
             padding-top: var(--space-sm); border-top: var(--rule-hair);
             overflow: visible; gap: var(--space-md);
             justify-content: space-between; }
  .top nav a { font-size: var(--text-sm); letter-spacing: 0;
               padding-block: var(--space-2xs); }
  .top nav a.nav-cta { padding: 0.34rem 0.9rem; }
  .nav-sep { display: none; }
}

@media (max-width: 460px) {
  .brand { font-size: var(--text-base); gap: var(--space-xs); }
  .brand img { width: 19px; height: 19px; }
  .top .shell { padding-inline: 18px; }
  .top nav { gap: var(--space-sm); }
}

@media (max-width: 400px) {
  /* Only hero/section action rows stack full width — never the nav button. */
  .btn-row .btn { width: 100%; justify-content: center; }
  .btn-row { gap: var(--space-xs); }
}

/* ---- print ---------------------------------------------------------------
   Docs get printed. Light paper, no sticky chrome, links resolved.
   ------------------------------------------------------------------------- */

@media print {
  :root {
    --color-paper: #fff; --color-paper-2: #fff; --color-paper-3: #f4f4f5;
    --color-ink: #000; --color-ink-2: #222; --color-ink-mute: #444;
    --color-ink-faint: #666; --color-rule: #bbb; --color-hairline: #ddd;
  }
  .top, .ticker, .btn-row, .bottom nav { display: none; }
  .toc { display: none; }
  .doc { grid-template-columns: minmax(0, 1fr); }
  a { border: 0; }
}

/* ===========================================================================
   The opening
   ---------------------------------------------------------------------------
   PJ becomes PenJester. The monogram is not a flourish: P and J are the two
   letters the product's name is built from and they are also the author's
   initials, so the expansion is the one piece of motion on this site that
   explains something rather than decorating it.

   Two squares slide into their overlap first, and the red appears at the
   moment they meet. That is the whole product in one second — two accounts,
   and the bytes one of them should not have.

   Rules it obeys:
   · CSS drives everything. If the script never runs, the overlay still plays
     and still leaves, because `animation-fill-mode: forwards` ends it at
     opacity 0 / pointer-events none. A broken script must not leave a sheet
     of near-black over the site.
   · Once per browser session, not once per page view.
   · Any click, key or scroll ends it immediately.
   · prefers-reduced-motion skips straight to the end.
   =========================================================================== */

.intro {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--space-lg);
  padding: var(--gutter);
  background: var(--color-paper);
  text-align: center;
  animation: intro-leave var(--dur-slow) var(--ease-out) 2850ms forwards;
}

/* The page underneath does not scroll while the sheet is up. */
body.intro-up { overflow: hidden; }

@keyframes intro-leave {
  to { opacity: 0; visibility: hidden; pointer-events: none; }
}

/* ---- the mark ---- */

.intro-mark { width: clamp(64px, 14vw, 104px); height: auto; overflow: visible; }
.intro-mark .sq  { fill: var(--color-ink); }
.intro-mark .lap { fill: var(--color-accent); }

.intro-mark .sq {
  opacity: 0;
  animation: sq-in 760ms var(--ease-out) forwards;
}
.intro-mark .sq-a { animation-delay: 60ms;  transform: translate(-10px, -10px); }
.intro-mark .sq-b { animation-delay: 160ms; transform: translate(10px, 10px); }

@keyframes sq-in {
  from { opacity: 0; }
  to   { opacity: 1; transform: translate(0, 0); }
}

/* The overlap arrives after both squares have landed, because it is a
   consequence of them meeting rather than a third shape. */
.intro-mark .lap {
  opacity: 0;
  transform-origin: 36px 36px;
  animation: lap-in 420ms var(--ease-out) 780ms forwards;
}
@keyframes lap-in {
  from { opacity: 0; transform: scale(0.4); }
  60%  { opacity: 1; transform: scale(1.08); }
  to   { opacity: 1; transform: scale(1); }
}

/* ---- PJ becoming PenJester ----------------------------------------------
   The monogram does not just get wider. P and J start tightly kerned, the way
   a two-letter mark is set, and then the whole word tracks open while "en" and
   "ester" rise into the space that opens up. Letters arriving into room that
   was made for them reads as a word assembling; letters appearing in a gap
   that was always there reads as a loading bar.

   A hairline in the brand red draws under it at the end — the same red that
   means "finding" everywhere else, used once, as a signature.
   ------------------------------------------------------------------------- */

.intro-word {
  position: relative;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.4rem, 9vw, 5.5rem);
  line-height: 1;
  color: var(--color-ink);
  display: flex;
  align-items: baseline;
  justify-content: center;
  white-space: nowrap;
  max-width: 100%;

  /* Tracks open from a monogram's kerning to the wordmark's. */
  letter-spacing: -0.07em;
  animation: track-out 1100ms var(--ease-out) 1250ms forwards;
}

@keyframes track-out { to { letter-spacing: -0.022em; } }

.intro-word .l {
  opacity: 0;
  animation: letter-in 520ms var(--ease-out) forwards;
}
.intro-word .l-p { animation-delay: 880ms; }
.intro-word .l-j { animation-delay: 990ms; }

@keyframes letter-in {
  from { opacity: 0; transform: translateY(0.16em) scale(0.94); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* A 0fr→1fr grid track is the one way to open a word out of nothing without
   hard-coding its width, which would break the moment the typeface fell back. */
.intro-word .g {
  display: inline-grid;
  grid-template-columns: 0fr;
  animation: grow 900ms var(--ease-out) 1250ms forwards;
}
.intro-word .g > span {
  overflow: hidden;
  min-width: 0;
  /* Slack on the clip, cancelled in layout. The word tracks to -0.022em, which
     pulls each glyph's advance in without shrinking the glyph — so the final r
     paints past the box it is measured at, and overflow:hidden slices it down
     the middle. scrollWidth cannot see that, because painted ink is not layout
     overflow, which is why measuring the span reported it as clean.
     The padding widens the clip rect; the negative margin takes the width back
     out of the grid track, so nothing moves. No sliver shows at the start
     because the text is still at opacity 0 while the track opens. */
  padding-right: 0.14em;
  margin-right: -0.14em;
  opacity: 0;
  transform: translateY(0.3em);
  animation: rise-letters 760ms var(--ease-out) 1380ms forwards;
}

/* "en" opens first and "ester" follows, so the name assembles left to right
   the way it is read rather than both halves arriving at once. */
.intro-word .g-en    { animation-delay: 1250ms; }
.intro-word .g-en > span { animation-delay: 1380ms; }
.intro-word .g-ester { animation-delay: 1400ms; }
.intro-word .g-ester > span { animation-delay: 1530ms; }

@keyframes grow { to { grid-template-columns: 1fr; } }
@keyframes rise-letters {
  to { opacity: 1; transform: translateY(0); }
}

/* The signature. Drawn, not faded — it should look like a stroke. */
.intro-rule {
  display: block;
  height: 2px;
  width: 0;
  margin: 0.45em auto 0;
  background: var(--color-accent);
  border-radius: var(--radius-pill);
  animation: draw-rule 620ms var(--ease-out) 2150ms forwards;
}
@keyframes draw-rule { to { width: min(62%, 15rem); } }

.intro-skip {
  position: absolute;
  bottom: var(--space-xl);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-ink-faint);
  border: 0;
  opacity: 0;
  animation: fade-in 500ms var(--ease-out) 1700ms forwards;
}
.intro-skip:hover { color: var(--color-ink-2); }

/* Dismissed by hand: leave now rather than finishing the schedule. */
.intro.is-done { animation: intro-leave 320ms var(--ease-out) forwards; }

/* Nothing moves. The end state is shown, and it is gone in a fifth of a
   second — a reader who asked for no motion still should not have to look at
   a title card. */
@media (prefers-reduced-motion: reduce) {
  .intro { animation: intro-leave 1ms linear 200ms forwards; }
  .intro-mark .sq, .intro-mark .lap, .intro-word .l,
  .intro-word .g > span, .intro-skip {
    opacity: 1; transform: none; animation: none;
  }
  .intro-word { letter-spacing: -0.022em; animation: none; }
  .intro-word .g { grid-template-columns: 1fr; animation: none; }
  .intro-rule { width: min(62%, 15rem); animation: none; }
}

/* ---- the page arriving under it -----------------------------------------
   The hero does not sit there waiting; it rises as the sheet lifts. Only the
   first screen is animated, because an element that animates on load below
   the fold animates where nobody is looking.
   ------------------------------------------------------------------------- */

.rise > * {
  opacity: 0;
  animation: rise-in 700ms var(--ease-out) forwards;
}
.rise > *:nth-child(1) { animation-delay: 2950ms; }
.rise > *:nth-child(2) { animation-delay: 3030ms; }
.rise > *:nth-child(3) { animation-delay: 3110ms; }
.rise > *:nth-child(4) { animation-delay: 3190ms; }
.rise > *:nth-child(5) { animation-delay: 3270ms; }

/* A repeat visit in the same session has no sheet to wait for. */
body.no-intro .rise > * { animation-delay: 80ms; }
body.no-intro .intro { display: none; }

@keyframes rise-in {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .rise > * { opacity: 1; transform: none; animation: none; }
}

/* On a phone the opening is a ~3-second near-blank splash before any content, which
   reads as the site being slow — the first thing flagged about mobile. Skip it there:
   hide the sheet, release the one-time scroll lock, and show the hero immediately
   instead of waiting out the rise delay. Desktop keeps the full PJ → Penjester mark. */
@media (max-width: 560px) {
  .intro { display: none; }
  body.intro-up { overflow: visible; }
  .rise > * { opacity: 1; transform: none; animation: none; }
}

/* ---- scroll reveal -------------------------------------------------------
   Everything below the fold fades up the first time it is reached, once.
   `.reveal` starts hidden only when the script is there to un-hide it —
   otherwise a reader with no JavaScript gets a blank page, which is a far
   worse outcome than no animation.
   ------------------------------------------------------------------------- */

.js .reveal { opacity: 0; transform: translateY(18px); }
.js .reveal.seen {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 620ms var(--ease-out), transform 620ms var(--ease-out);
}
.js .reveal.seen.d1 { transition-delay: 70ms; }
.js .reveal.seen.d2 { transition-delay: 140ms; }
.js .reveal.seen.d3 { transition-delay: 210ms; }

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

/* ---- comparison rows -----------------------------------------------------
   Four things you already own, and the sentence each cannot say. The hinge is
   the label — "cannot", "stops at" — so the row is built to make that the
   thing you read second, after the name and before the explanation.

   A vertical rule separates the two columns rather than a gap alone, because
   when this collapsed to one column on a narrow window the label became a
   stranded word floating between two paragraphs and the whole section read as
   a wall of prose. The rule becomes the left edge of the claim at that width,
   so the shape survives the breakpoint instead of dissolving at it.
   ------------------------------------------------------------------------- */

.compare { counter-reset: cmp; }

.compare-row {
  display: grid;
  grid-template-columns: 2.5rem minmax(0, 14rem) minmax(0, 1fr);
  gap: 0 var(--space-xl);
  padding-block: var(--space-xl);
  border-top: var(--rule-hair);
  align-items: start;
}
.compare-row:first-child { border-top: var(--rule-mid); }
.compare-row:last-child { border-bottom: var(--rule-mid); }

/* Numbered by the stylesheet, not by hand, so inserting a row cannot leave
   the page claiming there are four when there are five. */
.compare-row::before {
  counter-increment: cmp;
  content: counter(cmp, decimal-leading-zero);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  color: var(--color-ink-faint);
  padding-top: 0.4rem;
}

.compare-what h3 { margin-bottom: 0.3em; }
.compare-what p { margin: 0; }

.compare-gap {
  padding-left: var(--space-xl);
  border-left: var(--rule-mid);
}
.compare-gap p { margin: 0; color: var(--color-ink-2); }

.gap-label {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: var(--space-xs);
}

@media (max-width: 900px) {
  /* One column, and the rule moves to the left edge of the whole entry so the
     label still has something to belong to. */
  .compare-row { grid-template-columns: 2.5rem minmax(0, 1fr); gap: 0 var(--space-md); }
  .compare-what { margin-bottom: var(--space-md); }
  .compare-gap {
    grid-column: 2;
    padding-left: var(--space-md);
    border-left: 2px solid var(--color-rule);
  }
}

@media (max-width: 520px) {
  .compare-row { grid-template-columns: minmax(0, 1fr); }
  .compare-row::before { padding-top: 0; margin-bottom: var(--space-xs); }
  .compare-gap { grid-column: 1; }
}

/* ---- byline --------------------------------------------------------------
   One person built this. A site that hides that behind a plural "we" is doing
   the same thing as a findings list that hides its skips.
   ------------------------------------------------------------------------- */

.byline {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding-top: var(--space-xl);
  margin-top: var(--space-xl);
  border-top: var(--rule-hair);
}
.byline .pj {
  flex: none;
  width: 42px; height: 42px;
}
.byline .who { font-family: var(--font-ui); font-weight: 600; color: var(--color-ink); }
.byline .what { font-size: var(--text-sm); color: var(--color-ink-mute); margin: 0; }
.byline .what b { color: var(--color-ink-2); font-weight: 500; }

/* The monogram the opening is built from, at rest. P and J are the initials
   and the first letters of the two halves of the name; the red is where the
   two squares overlap, which is the same red and the same meaning as
   everywhere else. */
.byline .pj .sq  { fill: var(--color-ink-mute); }
.byline .pj .lap { fill: var(--color-accent); }

/* ---- the OWASP map -------------------------------------------------------
   Ten cells, and the two dark ones are the reason it is here. A coverage
   graphic that showed eight lit cells and two empty ones would be making
   exactly the claim this project refuses to make — an empty cell reads as
   "we did not get to it", and A04 and A09 are not that. They are questions a
   reply cannot answer. So those cells carry the sentence instead of a bar.

   The tally marks are the real distribution of the eighteen rules, counted
   from Rules.java. No invented numbers anywhere on this page.
   ------------------------------------------------------------------------- */

.owasp {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--space-sm);
}

.owasp .cell {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-height: 9.5rem;
  padding: var(--space-md);
  border: var(--rule-hair);
  border-radius: var(--radius-md);
  background: var(--color-paper-2);
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
.owasp .cell:hover { border-color: var(--color-rule); background: var(--color-paper-3); }

.owasp .code {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.14em;
  color: var(--color-ink-faint);
}
.owasp .name {
  font-family: var(--font-ui);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--color-ink);
  line-height: 1.25;
}
.owasp .part {
  font-size: var(--text-2xs);
  color: var(--color-ink-faint);
  line-height: 1.4;
}

/* One mark per rule. Counting beats a percentage here, because eighteen rules
   across ten categories is a small number and a bar chart of it would be
   inventing precision. */
.owasp .bar { display: flex; gap: 3px; margin-top: auto; padding-top: var(--space-xs); }
.owasp .bar i {
  display: block;
  width: 10px; height: 4px;
  border-radius: var(--radius-pill);
  background: var(--color-accent);
}

.owasp .is-dark { background: transparent; border-style: dashed; }
.owasp .is-dark .name { color: var(--color-ink-mute); }
.owasp .is-dark .why {
  margin-top: auto;
  font-size: var(--text-2xs);
  line-height: 1.45;
  color: var(--color-ink-faint);
  border-top: var(--rule-hair);
  padding-top: var(--space-xs);
}

@media (max-width: 1040px) { .owasp { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 680px)  { .owasp { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 380px)  { .owasp { grid-template-columns: minmax(0, 1fr); }
                             .owasp .cell { min-height: 0; } }

/* ---- coverage meter ------------------------------------------------------
   The grey rows are the feature. A class nothing has credited yet gets a
   sentence where its bar would be, never a bar at zero — a zero-width bar and
   an untested class look identical, and only one of them is a result.
   ------------------------------------------------------------------------- */

.meter { border-top: var(--rule-mid); }

.meter-row {
  display: grid;
  grid-template-columns: minmax(0, 10rem) minmax(0, 1fr) minmax(0, 5rem);
  gap: var(--space-md);
  align-items: center;
  padding-block: var(--space-md);
  border-bottom: var(--rule-hair);
}
.meter-row .k { font-size: var(--text-sm); color: var(--color-ink-2); }
.meter-row .t { display: block; }
.meter-row .v {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-ink-mute);
  text-align: right;
}

.meter-row .t > .fill,
.meter-row .t { position: relative; }
.meter-row:not(.is-none) .t {
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--color-paper-3);
  overflow: hidden;
}
.meter-row .fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--color-held);
  transform-origin: left center;
}
.w94 { width: 94%; }
.w61 { width: 61%; }
.w22 { width: 22%; }

.meter-row.is-none .k { color: var(--color-ink-mute); }
.meter-row.is-none .none {
  font-size: var(--text-xs);
  color: var(--color-ink-faint);
  font-style: normal;
}
.meter-row.is-none .v { color: var(--color-ink-faint); }

@media (max-width: 560px) {
  .meter-row { grid-template-columns: minmax(0, 1fr) auto; row-gap: var(--space-xs); }
  .meter-row .t { grid-column: 1 / -1; }
}
