/* WebRankPage, design system
   Palette from docs/brand/BRAND.md. Every colour value is contrast-checked;
   see that file before changing any of them. */

:root {
  /* Brand, recovered from the 2013 logo. See docs/brand/BRAND.md. */
  --brand-100:#E6F0FE; --brand-500:#2E86FF; --brand-600:#0A6CEF; --brand-700:#0852BC;

  /* Semantic scale. Every value contrast-checked; severity never relies on
     colour alone, each finding also carries a glyph and a word. */
  --green-100:#E3F6E9; --green-fill:#0FA855; --green-text:#0A7038; --green-dark:#075A2C;
  --teal-100:#E0F4F8;  --teal-fill:#0E9CB8;  --teal-text:#0A6F85;  --teal-dark:#085868;
  --coral-100:#FDEAE4; --coral-fill:#F4613A; --coral-text:#C13D18; --coral-dark:#9E3111;
  --amber-100:#FCF0DA; --amber-fill:#C9860F; --amber-text:#8E5E0B; --amber-dark:#754D08;
  --red-100:#FDE8E6;   --red-fill:#E23A2E;   --red-text:#C5221F;   --red-dark:#9B1B18;

  /* Grade chips, one pair per variation.
     
     Derived from the bands above but written out, because a filter cannot do
     this job: brightening the whole chip to mark a plus pushed the tint to
     near-white and the chip disappeared into the page. A plus carries more
     pigment and a deeper ink, a minus sits closer to the page, and every
     pair was contrast-checked against the surface it lands on. */
  --grade-a-plus-bg:#CDE9D7;  --grade-a-plus-ink:#075A2C;
  --grade-a-bg:#E3F6E9;       --grade-a-ink:#0A7038;
  --grade-a-minus-bg:#ECF7F1; --grade-a-minus-ink:#0A7038;
  --grade-b-plus-bg:#CBE7EC;  --grade-b-plus-ink:#085868;
  --grade-b-bg:#E0F4F8;       --grade-b-ink:#0A6F85;
  --grade-b-minus-bg:#EAF6F9; --grade-b-minus-ink:#0A6F85;
  --grade-c-plus-bg:#F1E1C5;  --grade-c-plus-ink:#754D08;
  --grade-c-bg:#FCF0DA;       --grade-c-ink:#8E5E0B;
  --grade-c-minus-bg:#FAF4E8; --grade-c-minus-ink:#8E5E0B;
  --grade-d-plus-bg:#F7D9D0;  --grade-d-plus-ink:#9E3111;
  --grade-d-bg:#FDEAE4;       --grade-d-ink:#C13D18;
  --grade-d-minus-bg:#FAF0EE; --grade-d-minus-ink:#C13D18;
  --grade-f-bg:#FDE8E6;       --grade-f-ink:#C5221F;
  /* Violet is a wayfinding hue only, it never encodes severity so it sits
     outside the semantic scale above. Used for the AI & answers section. */
  --violet-100:#EDE9FB; --violet-fill:#6D4FD6; --violet-text:#553BB0; --violet-dark:#422D8C;
  /* The brand scale predates the -fill/-text naming; alias it so group icons
     can address every hue uniformly. */
  --brand-fill: var(--brand-500); --brand-text: var(--brand-600);

  /* Neutrals carry a slight blue bias so they sit with the brand rather than
     reading as unconsidered grey. */
  --ink:      #141A22;
  --ink-2:    #4A5563;
  --ink-3:    #77828F;
  --line:     #E4E9EF;
  --line-2:   #EEF2F6;
  /* One step darker than --line, for a border that has to hold its own
     against a wide pale field: the docs search box reads as unedged at
     --line. Still well short of --ink-3, which is text, not a rule. */
  --line-3:   #D4DAE1;
  --surface:  #FFFFFF;
  --surface-2:#F7F9FB;
  --ground:   #FFFFFF;

  /* Code samples sit on the page's own ground rather than a dark slab.
     The convention in API documentation is a dark block, and it does say
     "this is code" before any colouring does, but on a light page full of
     light tables and callouts it is the only dark thing and it fights
     everything around it. A tinted surface with a border separates the
     sample just as clearly and belongs to the page it is on. */
  --code-bg:  #F7F9FB;
  --code-ink: #243041;
  --code-line: #E4E9EF;
  --code-head: #EEF2F6;

  /* Legacy aliases so existing report rules keep working. */
  --slate-100: var(--line-2);
  --slate-500: var(--ink-3);
  --slate-text: var(--ink-2);
  --slate-900: var(--ink);
  --border: var(--line);

  /* Text that sits ON the brand colour. Light theme: white on deep blue.
     Dark theme: the brand token becomes a light tint so this flips to the
     dark ground, white on it measures 2.1:1 and fails outright. */
  --on-brand: #FFFFFF;

  --radius: 10px;
  --shadow: 0 1px 2px rgba(20,26,34,.05), 0 8px 24px rgba(20,26,34,.05);

  /* One family throughout. Inter carries both roles; weight and size do the
     work that a second face was doing before. */
  --display: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  /* A tick, for list markers that should not be readable text. */
  --tick-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
}

/* JetBrains Mono ships programming ligatures and they are wrong for what this
   font is used for here. It sets URLs, selectors and markup, where // fuses
   into a single glyph and https:// reads as https:/, a typo the reader did
   not make. Disabled everywhere the mono face appears rather than per element,
   because every one of those places is showing a literal string.

   :where() keeps this at zero specificity so anything that genuinely wants a
   ligature can still ask for one. */
:where(.font-mono, code, pre, kbd, samp, [class*="mono"]),
:where(.hero-form, .cta-form, .analyze-form) input {
  font-variant-ligatures: none;
  font-feature-settings: 'liga' 0, 'calt' 0;
}

/* Dark theme. Tokens only, components never reference a colour directly
   inside a media block or they would render one theme's text on the other's
   ground. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --brand-100:#15243A; --brand-500:#5995E8; --brand-600:#7EAEF1; --brand-700:#A6C8F7;

    /* Lifted rather than deepened: on a dark page a darker block would sink
       into the ground and lose its edge. */
    --code-bg:  #111A2E;
    --code-ink: #E6EDF7;
    --code-line: #22304A;
    --code-head: #18243C;

    --green-100:#10261A; --green-fill:#2BBE6C; --green-text:#5FD68F; --green-dark:#8AE3AF;
    --teal-100:#0C2630;  --teal-fill:#2FB5D0;  --teal-text:#61CFE4;  --teal-dark:#8FE0EF;
    --coral-100:#301409; --coral-fill:#F4784F; --coral-text:#F79D7C; --coral-dark:#FABFA8;
    --amber-100:#2B1F08; --amber-fill:#DFA02B; --amber-text:#E9BC63; --amber-dark:#F2D396;
    --red-100:#2E1210;   --red-fill:#EF5A4E;   --red-text:#F5867C;   --red-dark:#F9AFA8;

    /* The same idea inverted: here a plus lifts the tint away from the page
       rather than deepening it, because the page is the dark thing. */
    --grade-a-plus-bg:#1D422D;  --grade-a-plus-ink:#8AE3AF;
    --grade-a-bg:#10261A;       --grade-a-ink:#5FD68F;
    --grade-a-minus-bg:#0F1E1B; --grade-a-minus-ink:#5FD68F;
    --grade-b-plus-bg:#1A414D;  --grade-b-plus-ink:#8FE0EF;
    --grade-b-bg:#0C2630;       --grade-b-ink:#61CFE4;
    --grade-b-minus-bg:#0D1E27; --grade-b-minus-ink:#61CFE4;
    --grade-c-plus-bg:#493817;  --grade-c-plus-ink:#F2D396;
    --grade-c-bg:#2B1F08;       --grade-c-ink:#E9BC63;
    --grade-c-minus-bg:#1E1A11; --grade-c-minus-ink:#E9BC63;
    --grade-d-plus-bg:#502A1B;  --grade-d-plus-ink:#FABFA8;
    --grade-d-bg:#301409;       --grade-d-ink:#F79D7C;
    --grade-d-minus-bg:#211412; --grade-d-minus-ink:#F79D7C;
    --grade-f-bg:#2E1210;       --grade-f-ink:#F5867C;
    --violet-100:#211A3D; --violet-fill:#9B85EE; --violet-text:#B4A3F3; --violet-dark:#CCC0F7;
    --brand-fill: var(--brand-500); --brand-text: var(--brand-600);

    --ink:      #E8EDF3;
    --ink-2:    #A4B0BE;
    --ink-3:    #74818F;
    --line:     #232C38;
    --line-2:   #1B222C;
    --line-3:   #313C4B;
    --surface:  #141A22;
    --surface-2:#10151C;
    --ground:   #0C1117;

    --on-brand: #0C1117;
    --shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
  }
}

:root[data-theme="dark"] {
  --brand-100:#15243A; --brand-500:#5995E8; --brand-600:#7EAEF1; --brand-700:#A6C8F7;

  /* Matching the media-query block above: a manual toggle has to reach the
     same values, or choosing dark looks different from the OS choosing it. */
  --code-bg:  #111A2E;
  --code-ink: #E6EDF7;
  --code-line: #22304A;
  --code-head: #18243C;

  --green-100:#10261A; --green-fill:#2BBE6C; --green-text:#5FD68F; --green-dark:#8AE3AF;
  --teal-100:#0C2630;  --teal-fill:#2FB5D0;  --teal-text:#61CFE4;  --teal-dark:#8FE0EF;
  --coral-100:#301409; --coral-fill:#F4784F; --coral-text:#F79D7C; --coral-dark:#FABFA8;
  --amber-100:#2B1F08; --amber-fill:#DFA02B; --amber-text:#E9BC63; --amber-dark:#F2D396;
  --red-100:#2E1210;   --red-fill:#EF5A4E;   --red-text:#F5867C;   --red-dark:#F9AFA8;

  /* Same grade set as the media query above, so the explicit toggle wins in
     both directions rather than falling back to the light chips. */
  --grade-a-plus-bg:#1D422D;  --grade-a-plus-ink:#8AE3AF;
  --grade-a-bg:#10261A;       --grade-a-ink:#5FD68F;
  --grade-a-minus-bg:#0F1E1B; --grade-a-minus-ink:#5FD68F;
  --grade-b-plus-bg:#1A414D;  --grade-b-plus-ink:#8FE0EF;
  --grade-b-bg:#0C2630;       --grade-b-ink:#61CFE4;
  --grade-b-minus-bg:#0D1E27; --grade-b-minus-ink:#61CFE4;
  --grade-c-plus-bg:#493817;  --grade-c-plus-ink:#F2D396;
  --grade-c-bg:#2B1F08;       --grade-c-ink:#E9BC63;
  --grade-c-minus-bg:#1E1A11; --grade-c-minus-ink:#E9BC63;
  --grade-d-plus-bg:#502A1B;  --grade-d-plus-ink:#FABFA8;
  --grade-d-bg:#301409;       --grade-d-ink:#F79D7C;
  --grade-d-minus-bg:#211412; --grade-d-minus-ink:#F79D7C;
  --grade-f-bg:#2E1210;       --grade-f-ink:#F5867C;
  --violet-100:#211A3D; --violet-fill:#9B85EE; --violet-text:#B4A3F3; --violet-dark:#CCC0F7;
  --brand-fill: var(--brand-500); --brand-text: var(--brand-600);
  --ink:#E8EDF3; --ink-2:#A4B0BE; --ink-3:#74818F;
  --line:#232C38; --line-2:#1B222C; --line-3:#313C4B;
  --surface:#141A22; --surface-2:#10151C; --ground:#0C1117;
  --on-brand: #0C1117;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.3);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
  background: var(--ground);
  -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums;
}

h1, h2, h3, .logo-text, .price .amount, .score-dial .num {
  font-family: var(--display);
  letter-spacing: -.02em;
  text-wrap: balance;
}

/*
 * Monospace utility. Used for anything that is read as a value rather than
 * prose, URLs, byte sizes, pixel widths, counts, headers, code. Tabular
 * figures so digits line up in a column and a slightly smaller size since
 * JetBrains Mono runs larger than Inter at the same point size.
 */
.font-mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: .92em;
}

a { color: var(--brand-600); }
a:hover { color: var(--brand-700); }

.wrap { max-width: 1280px; margin: 0 auto; padding: 0 1.25rem; }

/* ---------- header ---------- */

.site-header .wrap { display: flex; align-items: center; gap: 1rem; }
/* The mark sits close to the wordmark, they are one lockup, not two items in
   a row. The gap here is the mark-to-text distance; the tag sets its own. */
.logo { display: flex; align-items: center; gap: .45rem; text-decoration: none; color: var(--slate-900); }
.logo-text { font-weight: 700; font-size: 1.05rem; letter-spacing: -.01em; }
.logo-text span { color: var(--brand-600); }

/* ---------- search form ---------- */

.analyze-form { display: flex; gap: .5rem; width: 100%; }
.analyze-form:not(.hero-form) input[type="url"],
.analyze-form:not(.hero-form) input[type="text"] {
  flex: 1; min-width: 0;
  padding: .7rem .9rem;
  font: inherit;
  border: 1.5px solid var(--border);
  border-radius: var(--radius);
  background: #fff;
}
/* The hero field carries the border and the focus ring itself so the input
   inside it stays bare, otherwise both draw one and the well looks doubled. */
.analyze-form:not(.hero-form) input:focus {
  outline: none;
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px var(--brand-100);
}

/* The hidden attribute has to win.
 *
 * A browser's own [hidden] rule is a plain display:none on an element
 * selector, so any class here that sets display beats it and the element
 * stays on screen with hidden="" sitting uselessly in the markup. That is
 * how "Stamp them" showed before the file had been checked, offering the
 * irreversible half of a two-step confirmation as a first move.
 *
 * Set on the button classes rather than on each component that hides one,
 * because this has already been patched separately in six places. */
.btn[hidden], .btn-sm[hidden], .btn-xs[hidden] { display: none !important; }

/* A button that is working says so, rather than looking untouched while a
   request runs. The ring is drawn with ::before so no markup changes and
   any button can use it. */
.btn.is-working::before {
  content: ""; flex: none;
  width: 14px; height: 14px; border-radius: 50%;
  border: 2px solid currentColor; border-top-color: transparent;
  opacity: .75;
  animation: wait-spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .btn.is-working::before { animation-duration: 2.4s; }
}

.btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .7rem 1.2rem;
  font: inherit; font-weight: 600;
  /* Transparent rather than none, so a solid button and a ghost one are the
     same height: border-box absorbs a border into the box, but only when
     there is one to absorb, and a 1.5px outline on the ghost made it 2px
     taller than the button beside it. */
  border: 1.5px solid transparent; border-radius: var(--radius);
  background: var(--brand-600); color: var(--on-brand);
  cursor: pointer; text-decoration: none;
  white-space: nowrap;
}
.btn:hover { background: var(--brand-700); color: var(--on-brand); }
.btn.ghost { background: #fff; color: var(--brand-600); border: 1.5px solid var(--border); }
.btn.ghost:hover { border-color: var(--brand-600); background: var(--brand-100); }

/* ---------- tier 1: executive snapshot ---------- */

.snapshot {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2rem;
  align-items: center;
  padding: 1.75rem;
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow);
  margin: 1.5rem 0;
}


.snapshot-meta h1 {
  font-size: 1.15rem; margin: 0 0 .2rem;
  overflow-wrap: anywhere;
}
.snapshot-url {
  font-family: var(--mono); font-size: .8rem; color: var(--slate-text);
  overflow-wrap: anywhere; display: block; margin-bottom: .9rem;
}

.pills { display: flex; gap: .5rem; flex-wrap: wrap; }
.pill {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .8rem; font-weight: 600;
  padding: .3rem .7rem; border-radius: 999px;
}
.pill .g { font-family: var(--mono); }
.p-crit { background: var(--red-100);   color: var(--red-text); }
.p-rec  { background: var(--amber-100); color: var(--amber-text); }
.p-opt  { background: var(--slate-100); color: var(--slate-text); }
/* Brand blue, matching the tint a notice card carries below: the pill and the
   card are counting the same thing and should look like it. */
.p-notice { background: var(--brand-100); color: var(--brand-700); }
.p-pass { background: var(--green-100); color: var(--green-text); }

/* ---------- category bars ---------- */

.categories { margin: 1.25rem 0 2rem; }
.cat-row {
  display: grid; grid-template-columns: 13rem 1fr 4.5rem;
  gap: 1rem; align-items: center;
  padding: .55rem 0; border-bottom: 1px solid var(--slate-100);
}
.cat-row:last-child { border-bottom: 0; }
.cat-name { font-size: .88rem; font-weight: 600; }
.cat-name small { display: block; font-weight: 400; color: var(--slate-text); font-size: .78rem; }
.cat-bar { height: 8px; border-radius: 999px; background: var(--slate-100); overflow: hidden; }
.cat-bar > i { display: block; height: 100%; border-radius: 999px; }
.cat-score { text-align: right; font-weight: 700; font-family: var(--mono); font-size: .9rem; }
.cat-row.unassessed .cat-score,
.cat-row.unassessed .cat-name { color: var(--slate-500); }

/* ---------- tier 2: diagnostics ---------- */

.tabs { display: flex; gap: .3rem; flex-wrap: wrap; border-bottom: 1px solid var(--border); margin-bottom: 1.25rem; }
.tab {
  padding: .55rem .9rem; font-size: .87rem; font-weight: 600;
  color: var(--slate-text); background: none; border: 0;
  border-bottom: 2px solid transparent; cursor: pointer; font-family: inherit;
}
.tab[aria-selected="true"] { color: var(--brand-600); border-bottom-color: var(--brand-600); }
.tab .count {
  font-family: var(--mono); font-size: .75rem;
  margin-left: .3rem; padding: .05rem .35rem;
  border-radius: 4px; background: var(--slate-100); color: var(--slate-text);
}



/* ---------- tier 3: action matrix ---------- */

.actions { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.action-row {
  display: grid; grid-template-columns: 2rem 1fr auto;
  gap: .75rem; align-items: start;
  padding: .8rem .95rem; border-bottom: 1px solid var(--slate-100);
}
.action-row:last-child { border-bottom: 0; }
.action-rank { font-family: var(--mono); font-weight: 700; color: var(--slate-500); font-size: .85rem; }
.action-msg { font-size: .9rem; }
.action-fix {
  font-family: var(--mono); font-size: .78rem; color: var(--slate-text);
  margin-top: .3rem; overflow-wrap: anywhere;
}
.effort {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  padding: .2rem .5rem; border-radius: 5px; white-space: nowrap;
}
.effort-low  { background: var(--green-100); color: var(--green-text); }
.effort-med  { background: var(--amber-100); color: var(--amber-text); }
.effort-high { background: var(--slate-100); color: var(--slate-text); }

/* ---------- SERP simulator ---------- */

.serp {
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 1.1rem 1.2rem; background: #fff;
}
.serp + .serp { margin-top: .8rem; }
.serp.mobile { max-width: 26rem; }
.truncated { color: var(--amber-text); font-weight: 600; }

/* ---------- sections ---------- */


.site-footer {
  border-top: 1px solid var(--border); margin-top: 3rem; padding: 1.5rem 0 2.5rem;
  color: var(--slate-text); font-size: .82rem;
}

@media (max-width: 40rem) {
  .snapshot { grid-template-columns: 1fr; gap: 1.25rem; justify-items: start; }
  .cat-row { grid-template-columns: 1fr 3.5rem; }
  .cat-row .cat-bar { grid-column: 1 / -1; }
  .analyze-form { flex-direction: column; }
}

/* ==========================================================================
   Marketing layer
   ========================================================================== */

/* The gap shrinks with the viewport: a fixed 1.5rem is fine beside three nav
   links but, with the report controls in the same row, it is enough on its own
   to push the account buttons past the right edge on a phone. */
.header-inner {
  display: flex; align-items: center;
  /* Same token as the groups inside it so every gap across the row is one
     distance. The marketing pages set their own wider value below, where the
     row holds nav links instead of a field and a button. */
  gap: var(--hdr-gap, 1.5rem);
  min-width: 0;
}

/* The nav sits in the middle rather than crowding the right: the logo anchors
   one end, the action the other and the links are the space between. */
.site-nav { margin: 0 auto; }

/* The toggle sits with the account controls rather than against the nav. */
.site-header .theme-cycle { margin-left: auto; }
.page-report .site-header .theme-cycle { margin-left: 0; }
.logo-mark { width: 30px; height: 32px; color: var(--brand-600); flex: none; }
/* Nav links as hover pills rather than colour-only: the target is the whole
   pill instead of the word and the header reads as a set of controls rather
   than as a line of text. */
.site-nav { display: flex; align-items: center; gap: .25rem; }

.site-nav a {
  color: var(--ink-2); text-decoration: none;
  font-size: .88rem; font-weight: 500;
  padding: .4rem .7rem; border-radius: 8px;
  transition: background .15s, color .15s;
}

.site-nav a:hover { color: var(--ink); background: var(--surface-2); }

/* The sample report is the one nav item that is a demonstration rather than a
   destination so it is set apart: an icon, the brand colour and a rule
   before it. */
/* The sample report reads as a demonstration rather than a destination so it
   takes the brand colour while keeping the nav's own pill shape. */
.nav-sample { color: var(--brand-600) !important; font-weight: 600; }
.nav-sample:hover { background: var(--brand-100) !important; }

/* The category in one word so the name does not have to carry it. */
.logo-tag {
  font-size: .62rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase;
  padding: .16rem .38rem; border-radius: 5px;
  background: var(--brand-100); color: var(--brand-700);
  /* Its own small offset, on top of the lockup's gap: the tag qualifies the
     name rather than standing beside it as a third element. */
  margin-left: .1rem;
}

/* ---------- hero ---------- */

.hero { padding: 4.5rem 0 3.5rem; text-align: center; background: linear-gradient(180deg, var(--brand-100) 0%, #fff 100%); }

/* ---------- features ---------- */

.section > .lede { color: var(--slate-text); font-size: 1rem; margin: 0 0 1.75rem; max-width: 46rem; }

.feature h3 { font-size: 1rem; margin: 0 0 .4rem; }
.feature p { margin: 0; font-size: .89rem; color: var(--slate-text); }
.feature code { font-family: var(--mono); font-size: .85em; background: var(--slate-100); padding: .05rem .25rem; border-radius: 4px; }

/* ---------- claims band ---------- */

.band { background: var(--slate-100); padding: 3.25rem 0; margin: 3.5rem 0; }
.band h2 { font-size: 1.45rem; letter-spacing: -.02em; margin: 0 0 1.75rem; font-weight: 700; }
.claims { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1.75rem; }
.claim h3 { font-size: .98rem; margin: 0 0 .4rem; color: var(--brand-700); }
.claim p { margin: 0; font-size: .89rem; color: var(--slate-text); }

/* ---------- page heads & prose ---------- */

.page-head { padding: 3rem 0 .5rem; }
.page-head h1 { font-size: clamp(1.7rem, 4vw, 2.4rem); letter-spacing: -.025em; margin: 0 0 .5rem; font-weight: 800; }
.page-head .lede { color: var(--slate-text); font-size: 1.02rem; max-width: 46rem; margin: 0; }
.narrow { max-width: 44rem; }
.prose p { margin: 0 0 1.1rem; }
.prose h2 { font-size: 1.15rem; margin: 2rem 0 .7rem; letter-spacing: -.01em; }

/* ---------- feature checklists ---------- */

.check-group { margin-bottom: 2.5rem; }
.check-group h2 {
  font-size: 1.1rem; margin: 0 0 .8rem; padding-bottom: .5rem;
  border-bottom: 1px solid var(--border);
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
}
.weight {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--brand-700); background: var(--brand-100);
  padding: .2rem .55rem; border-radius: 5px; white-space: nowrap;
}
.check-list { list-style: none; padding: 0; margin: 0; }
.check-list li {
  position: relative; padding: .4rem 0 .4rem 1.5rem;
  font-size: .91rem; color: var(--slate-text);
}
.check-list li::before {
  content: "✓"; position: absolute; left: 0; top: .4rem;
  color: var(--green-text); font-weight: 700; font-family: var(--mono);
}
.check-list strong { color: var(--slate-900); }

/* The count beside each heading, read from the audits rather than typed.
   Quieter than .weight, which it replaced: the number is information rather
   than a badge, and seven loud pills down the page is a scoreboard. */
.check-count {
  font-size: .72rem; font-weight: 600;
  color: var(--ink-3); white-space: nowrap;
}
/* One line saying what the group is for, before the list of what is in it.
   Without it the page is 191 bullets and no argument. */
.check-blurb {
  margin: 0 0 .9rem;
  font-size: .88rem; line-height: 1.6; color: var(--ink-2);
  max-width: 46rem;
}

/* Two columns once there is room. Seven groups of up to twelve items is a
   long single column, and the groups are independent, so breaking between
   them costs the reader nothing. */
@media (min-width: 1000px) {
  .checks-columns {
    column-count: 2; column-gap: 3rem;
  }
  .checks-columns .check-group {
    break-inside: avoid; page-break-inside: avoid;
  }
}
.check-list code { font-family: var(--mono); font-size: .85em; background: var(--slate-100); padding: .05rem .25rem; border-radius: 4px; }

/* ---------- pricing ---------- */

.plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(16.5rem, 1fr)); gap: 1.4rem; align-items: start; }
.plan { padding: 1.6rem; border: 1px solid var(--border); border-radius: 14px; background: #fff; position: relative; }
.plan.featured { border-color: var(--brand-600); border-width: 2px; box-shadow: var(--shadow); }
.plan-badge {
  position: absolute; top: -.7rem; left: 1.6rem;
  background: var(--brand-600); color: var(--on-brand);
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  padding: .25rem .6rem; border-radius: 5px;
}
.plan h2 { font-size: 1.05rem; margin: 0 0 .5rem; }
.price { margin: 0 0 .15rem; }
.price .amount { font-size: 2.2rem; font-weight: 800; letter-spacing: -.03em; }
.price .period { color: var(--slate-text); font-size: .9rem; }
.plan-note { color: var(--slate-text); font-size: .84rem; margin: 0 0 1.1rem; }
.plan ul { list-style: none; padding: 0; margin: 0 0 1.4rem; }
.plan li { position: relative; padding: .32rem 0 .32rem 1.4rem; font-size: .89rem; }
.plan li::before {
  content: "✓"; position: absolute; left: 0; color: var(--green-text);
  font-weight: 700; font-family: var(--mono);
}
.plan .btn { width: 100%; justify-content: center; }

.faq { margin-top: 3rem; max-width: 44rem; }
.faq h2 { font-size: 1.2rem; margin: 0 0 1rem; }

/* ---------- CTA ---------- */

.cta h2 { margin-bottom: 1.1rem; }

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

.site-footer { border-top: 1px solid var(--border); margin-top: 4rem; padding: 2.5rem 0 0; background: #FCFDFD; }
.footer-inner { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 2rem; padding-bottom: 2rem; }
.footer-note { color: var(--slate-text); font-size: .86rem; margin: .6rem 0 0; max-width: 24rem; }
.footer-links h3 { font-size: .75rem; text-transform: uppercase; letter-spacing: .06em; color: var(--slate-text); margin: 0 0 .7rem; }
.footer-links a { display: block; font-size: .88rem; text-decoration: none; color: var(--slate-900); padding: .18rem 0; }
.footer-links a:hover { color: var(--brand-600); }
.footer-base { border-top: 1px solid var(--border); padding: 1.1rem 0; color: var(--slate-500); font-size: .82rem; }

@media (max-width: 46rem) {
  .site-nav { gap: 1rem; font-size: .85rem; }
  .footer-inner { grid-template-columns: 1fr 1fr; }
  .footer-inner > div:first-child { grid-column: 1 / -1; }
  .hero { padding: 3rem 0 2.5rem; }
}

/* ==========================================================================
   Refinements, theme-aware surfaces, measurement-instrument detailing

   Note on the score colours: these encode magnitude (how good a score is),
   not category identity so the categorical CVD rules do not apply, each bar
   is labelled with its name and its number and the colour only reinforces.
   The severity scale is separate and reserved and every severity also carries
   a glyph and a word. Verified with the dataviz validator: as a categorical
   set these hues would fail (coral↔amber ΔE 1.7 under deuteranopia), which is
   exactly why they are never used to distinguish series.
   ========================================================================== */

/* Surfaces must come from tokens or dark mode renders light-theme panels. */
.feature, .plan, .finding, .serp, .actions, .snapshot { background: var(--surface); }
.band { background: var(--surface-2); }
.site-header, .site-footer { background: var(--surface); }
.check-list code, .feature code { background: var(--surface-2); }

/* Type scale: display face earns its place at the top of the hierarchy only. */
.band h2, .page-head h1 { font-weight: 700; }
.feature h3, .claim h3, .plan h2 { font-family: var(--display); font-weight: 600; letter-spacing: -.015em; }

/* Numbers are the subject here so they get the mono face and tabular figures. */
.price .amount, .cat-score, .score-dial .num, .weight, .action-rank {
  font-variant-numeric: tabular-nums;
}
.price .amount { font-weight: 700; letter-spacing: -.04em; }

/* Not everything is a card: features become a ruled grid, which reads as
   a specification rather than six identical boxes. */
.feature {
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  padding: 1.6rem 1.6rem 1.6rem 0;
  background: transparent;
}

/* Claims sit on ruled columns rather than floating. */
.claim { border-left: 2px solid var(--brand-600); padding-left: 1rem; }

/* Hero: no gradient wash, a hairline baseline and generous space instead. */
.hero {
  background: none;
  border-bottom: 1px solid var(--line);
  padding: 5rem 0 4rem;
}
.hero-form input { color: var(--ink); }

/* Inputs must not inherit a light background in dark mode. The hero is exempt:
   its field is a bordered well so a second border on the input inside it
   reads as a box within a box. */
.analyze-form:not(.hero-form) input[type="url"],
.analyze-form:not(.hero-form) input[type="text"] {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--line);
}
.analyze-form input::placeholder { color: var(--ink-3); }

/* Pricing: lift only the featured plan, per "spend borders by role". */
.plan { border-radius: 12px; }
.plan:not(.featured) { border-color: var(--line); box-shadow: none; }

/* Buttons */
.btn.ghost { background: var(--surface); color: var(--brand-600); border-color: var(--line); }

/* Focus must stay visible for keyboard users.
   The radius shapes the OUTLINE but it lands on the element so anything
   with a shape of its own, a pill, a circle, has to restate its radius in
   its own focus rule or it squares off when focused. */
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* The SERP preview reproduces Google's own rendering so it takes Google's
   colours rather than ours, it is a screenshot, not part of our palette.
   That means Google's DARK colours on a dark page: this used to stay white in
   both themes, which is not fidelity, it is a white card on a dark ground. */
.serp { background: #fff; border-color: var(--line); }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .serp { background: #202124; }
}

:root[data-theme="dark"] .serp { background: #202124; }




/* ==========================================================================
   Homepage treatment

   Two patterns taken from the leaders in this field:
   - a live result card in the hero with circular stat counters so the first
     screen shows output rather than describing it (seositecheckup.com)
   - features as numbered, alternating panel/text blocks that read as a
     narrative, plus a grouped "everything tested" list (seranking.com)
   ========================================================================== */

:root {
  --elev-1: 0 1px 2px rgba(20,26,34,.04), 0 1px 3px rgba(20,26,34,.06);
  --elev-2: 0 4px 12px rgba(20,26,34,.06), 0 16px 40px rgba(20,26,34,.08);
  --elev-3: 0 12px 32px rgba(20,26,34,.08), 0 40px 80px rgba(20,26,34,.14);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --elev-1: 0 1px 2px rgba(0,0,0,.4);
    --elev-2: 0 4px 12px rgba(0,0,0,.45), 0 16px 40px rgba(0,0,0,.4);
    --elev-3: 0 12px 32px rgba(0,0,0,.5), 0 40px 80px rgba(0,0,0,.55);
  }
}
:root[data-theme="dark"] {
  --elev-1: 0 1px 2px rgba(0,0,0,.4);
  --elev-2: 0 4px 12px rgba(0,0,0,.45), 0 16px 40px rgba(0,0,0,.4);
  --elev-3: 0 12px 32px rgba(0,0,0,.5), 0 40px 80px rgba(0,0,0,.55);
}

.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;
}

/* ---- header ---- */

.site-header {
  /* Slightly taller and the rule softened: a hairline at full strength cuts
     the page in two under a hero that is meant to open it. */
  padding: .75rem 0;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 70%, transparent);
  position: sticky; top: env(safe-area-inset-top, 0px); z-index: 20;
  background: color-mix(in srgb, var(--surface) 86%, transparent);
  backdrop-filter: saturate(1.6) blur(14px);
}
.site-nav { margin-left: auto; gap: 1.7rem; }
.site-nav a { font-size: .91rem; font-weight: 500; }

/* One button that cycles light -> dark -> auto, showing the mode in effect.
   All three icons ship in the markup and CSS reveals one so switching theme
   never touches the DOM and the icon is correct in the first painted frame. */
/* A bare circle rather than a bordered box: it is a preference, not an action and a second framed control beside the CTA competes with it. */
.theme-cycle {
  display: grid; place-items: center; flex: none;
  width: 32px; height: 32px; padding: 0;
  background: none; border: 1px solid transparent;
  border-radius: 50%; cursor: pointer; color: var(--ink-3);
  transition: background .15s, color .15s, border-color .15s;
}

.theme-cycle:hover { color: var(--ink); background: var(--surface-2); }
.theme-cycle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.theme-cycle svg {
  /* Stacked in one cell so the button never resizes between modes. */
  grid-area: 1 / 1;
  width: 14px; height: 14px;
  fill: currentColor; stroke: currentColor; stroke-width: 1.3;
  visibility: hidden;
}

.theme-cycle svg [fill="none"] { fill: none; }

/* Selected from the root, which the head script stamps before first paint.
   The auto icon is also the default for the un-stamped document so the
   button is never blank if the script is blocked. */
.theme-cycle .theme-icon-auto,
:root[data-theme-mode="auto"] .theme-cycle .theme-icon-auto { visibility: visible; }

:root[data-theme-mode="light"] .theme-cycle .theme-icon-auto,
:root[data-theme-mode="dark"] .theme-cycle .theme-icon-auto { visibility: hidden; }

:root[data-theme-mode="light"] .theme-cycle .theme-icon-light,
:root[data-theme-mode="dark"] .theme-cycle .theme-icon-dark { visibility: visible; }

/* ---- hero ---- */

/* ---- hero ----
   A tinted field behind the whole section rather than a radial wash behind the
   heading: the search panel sits on it as a raised object, which is what makes
   the one thing a visitor has to use look like the one thing that matters. */

.hero { position: relative; padding: 0; overflow: hidden; background: var(--surface); }

/* Three blurred shapes rather than a gradient. A linear wash reads as a flat
   band; overlapping circles at different sizes and offsets give light that
   falls unevenly, which is what makes the section feel lit rather than tinted.
   Each is heavily blurred so the shapes themselves are never visible, only
   the colour they cast. */
.hero-glow { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }

.hero-glow .glow { position: absolute; display: block; border-radius: 50%; }

/* The main pool of light, behind the headline. Wider than it is tall so it
   lights the text rather than haloing it. */
.glow-centre {
  top: 22%; left: 50%;
  width: 46rem; height: 22rem;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--brand-500) 16%, transparent), transparent 70%);
  filter: blur(60px);
}

/* Two smaller sources in opposite corners so the light has a direction. */
.glow-right {
  top: -4rem; right: -4rem;
  width: 24rem; height: 24rem;
  background: color-mix(in srgb, var(--brand-600) 12%, transparent);
  filter: blur(64px);
}

.glow-left {
  bottom: -2rem; left: -4rem;
  width: 24rem; height: 24rem;
  background: color-mix(in srgb, var(--brand-500) 12%, transparent);
  filter: blur(64px);
}

/* The dark theme needs less: the same opacities on a dark ground read as haze
   rather than as light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .hero-glow { opacity: .55; }
}

:root[data-theme="dark"] .hero-glow { opacity: .55; }

/* A decorative layer is the first thing to drop when motion or contrast is at
   a premium. */
@media (prefers-reduced-transparency: reduce) {
  .hero-glow { display: none; }
}

.hero .wrap { position: relative; z-index: 1; padding-block: 5.5rem 0; text-align: center; }

/* The eyebrow names what the tool reads. Bordered and tinted rather than a
   solid chip so it introduces the headline instead of competing with it. */
.eyebrow-pill {
  display: inline-flex; align-items: center; gap: .6rem;
  font-size: .8rem; font-weight: 600;
  padding: .15rem .75rem; border-radius: 999px;
  /* Tinted rather than white: the pill is part of the lit field behind it,
     not a card sitting on top of it. */
  background: var(--brand-100);
  border: 1px solid color-mix(in srgb, var(--brand-500) 26%, transparent);
  color: var(--brand-700);
  box-shadow: 0 1px 2px rgb(0 0 0 / .04);
  margin-bottom: 1.5rem;
  white-space: nowrap;
}

/* An SVG with no width fills whatever box it is given, so any pill carrying an
   icon needs this: without it the glyph expands to the pill's height and
   shoulders the text into wrapping. */
.eyebrow-pill svg { width: 14px; height: 14px; flex: none; }

/* On a phone the hero starts close under the header, and the pill wraps
   inside the screen instead of running off its edge and widening the page. */
@media (max-width: 560px) {
  .hero .wrap { padding-block: 1.75rem 0; }
  .eyebrow-pill {
    max-width: 100%; white-space: normal; text-align: center; text-wrap: balance;
    font-size: .74rem; line-height: 1.4; padding: .3rem .75rem; border-radius: 14px;
    margin-bottom: 1.1rem;
  }
}

/* A live dot: a ring pulses out of it, because the tool measures a page as it
   is now and a static dot says nothing about that. */
.eyebrow-pill .dot {
  position: relative; flex: none;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--green-fill);
}

.eyebrow-pill .ping {
  position: absolute; inset: 0;
  border-radius: 50%;
  background: var(--green-fill);
  opacity: .75;
  animation: eyebrow-ping 2.4s cubic-bezier(0, 0, .2, 1) infinite;
}

@keyframes eyebrow-ping {
  75%, 100% { transform: scale(2.4); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .eyebrow-pill .ping { animation: none; opacity: .3; }
}

.hero h1 {
  /* Larger and tighter than before: a headline this short can carry the size and the tracking keeps a big weight from looking loose. */
  font-size: clamp(2.4rem, 6vw, 4.2rem);
  line-height: 1.1;
  letter-spacing: -.035em;
  margin: 0 auto 1.3rem; font-weight: 800;
  /* Wide enough that the emphasised phrase is not split across lines: balance
     alone still broke "what is / wrong" in half, which puts the break in the
     middle of the idea the colour is pointing at. */
  max-width: 20ch;
  text-wrap: balance;
}

/* And the phrase itself never breaks, whatever the width. */
.hero h1 .accent { white-space: nowrap; }

/* The emphasis is a gradient clipped to the letters rather than a flat fill.
   Across a phrase this size a single colour reads as highlighter; a gradient
   gives the words depth and keeps the eye moving along them. */
.hero h1 .accent {
  background: linear-gradient(100deg, var(--brand-700) 0%, var(--brand-600) 40%, var(--brand-500) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.hero-sub {
  font-size: 1.05rem; line-height: 1.6; color: var(--ink-2);
  max-width: 56ch; margin: 0 auto 2rem;
}

/* The panel holds the field and its reassurances as one object. Raised off the
   section, it is unmistakably the thing to use; the ticks inside it answer the
   hesitation that arises at the moment of typing. */
.hero-panel {
  max-width: 44rem; margin: 0 auto;
  padding: .85rem .85rem .2rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 20px;
  box-shadow: 0 24px 50px -28px color-mix(in srgb, var(--brand-700) 40%, transparent),
              0 2px 8px -3px rgb(0 0 0 / .05);
}

.hero-form { display: flex; gap: .65rem; margin: 0; }

/* The field keeps its own surface inside the panel: a tinted well reads as
   somewhere to type, where a borderless strip reads as a label. */
.hero-field {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: .6rem;
  padding: 0 .95rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 12px;
  transition: border-color .15s, box-shadow .15s, background .15s;
}

.hero-field:focus-within {
  background: var(--surface);
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 16%, transparent);
}

.hero-field .field-icon { width: 17px; height: 17px; flex: none; color: var(--ink-3); }

/* Monospaced, because the value is a URL and the eye reads a path more easily
   when its characters align. */
.hero-form input {
  flex: 1; min-width: 0;
  padding: .9rem 0; font-size: .95rem;
  font-family: var(--mono);
  border: 0; background: none;
  color: var(--ink);
}

.hero-form input:focus { outline: none; }
.hero-form input::placeholder { color: var(--ink-3); font-family: var(--mono); }

.hero-form .btn {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .9rem 1.6rem; font-size: .95rem;
  border-radius: 12px; flex: none;
}

.hero-form .btn svg { width: 16px; height: 16px; flex: none; }

/* Inside the panel, under the field: three short claims are scanned where one
   long line is read or skipped. */
.hero-note {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .3rem 1.6rem;
  font-size: .82rem; color: var(--ink-3);
  margin: .85rem 0 0; padding-bottom: .75rem;
}

.hero-note span { display: inline-flex; align-items: center; gap: .4rem; }
.hero-note b { color: var(--green-fill); font-weight: 700; }

@media (max-width: 38rem) {
  /* Stacked, because a button beside the field leaves too little room to read
     a URL in. */
  .hero-form { flex-direction: column; }
  .hero-form .btn { justify-content: center; }
  .hero-note { gap: .3rem 1rem; }
}

/* ---- hero: a screenshot of a real report ---- */

/* The capture is cropped to the report card itself, no page ground around
   it so the figure needs no frame of its own. It is deliberately cut
   mid-diagnostics and faded out at the bottom: the report continues past the
   fold and a hard edge would claim it ends there. The mask does the fade so there is no opaque gradient strip that would have to track the hero's
   background as it changes. */
/* The figure carries the shadow; the mask goes on the image inside it. A mask
   clips the element's own box-shadow along with its content so the two cannot
   live on the same element. */
.hero-shot {
  max-width: 62rem; margin: 4.5rem auto 0;
  position: relative; line-height: 0;
}

/* The shadow is a blurred shape on its own layer rather than a box-shadow.
   Two constraints rule box-shadow out: a mask clips the shadow an element
   casts outside its own box and an unmasked one would terminate on a hard
   line of its own. A blurred, faded block has neither problem, it thins out
   downward and stops before the image's dissolving lower edge, where depth
   would only read as a smudge. */
.hero-shot::before {
  content: '';
  /* Extends PAST the image on three sides: the picture is opaque so anything
     drawn within its footprint is hidden behind it. */
  position: absolute; left: -1.5rem; right: -1.5rem; top: -0.75rem; bottom: 30%;
  border-radius: 18px;
  background: color-mix(in srgb, var(--brand-700) 26%, transparent);
  filter: blur(30px);
}

/* Above the shadow layer and masked so the picture fades into the hero. */
.hero-shot img {
  position: relative;
  width: 100%; height: auto; display: block;
  border-radius: 14px 14px 0 0;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--brand-700) 10%, transparent);
  -webkit-mask-image: linear-gradient(180deg, #000 58%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 58%, transparent 100%);
}

/* ---- sections ---- */

/*
 * Vertical margin only. `.section` and `.wrap` are used together on the same
 * element and a `margin` shorthand here would reset .wrap's `margin: 0 auto`
 * to zero and shunt the whole section to the left.
 */
.section { margin-block: 5.5rem; }
.section-head { text-align: center; max-width: 45rem; margin: 0 auto 3rem; }
.section-head h2 {
  font-size: 2.25rem; line-height: 2.5rem; letter-spacing: normal;
  margin: 0 0 .75rem; font-weight: 800;
  /* Evens the lines rather than filling the first one: a heading that wraps
     with two words left over reads as an accident. */
  text-wrap: balance;
}
.section-head p { color: var(--ink-2); font-size: 1.02rem; margin: 0; }

/* For compounds that should not break across lines. */
.nowrap { white-space: nowrap; }
/* Keeping a phrase together is a nicety; on a phone it overflows the screen. */
@media (max-width: 560px) { h2 .nowrap { white-space: normal; } }
.eyebrow {
  display: inline-block; font-size: .76rem; font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--brand-600); margin-bottom: .75rem;
}

/* ---- numbered alternating feature blocks ---- */

.steps { display: flex; flex-direction: column; gap: 4.5rem; }
.step {
  display: grid; grid-template-columns: 1fr 1.15fr;
  gap: 3.5rem; align-items: center;
}
/*
 * Reversed rows swap the COLUMN WIDTHS as well as the order. Reordering alone
 * would put the panel in the narrow track on every other row so the panels
 * would not line up down the page.
 */
.step:nth-child(even) { grid-template-columns: 1.15fr 1fr; }
.step:nth-child(even) .step-text { order: 2; }
/* An eyebrow that names the step rather than numbering it: the headings are
   already in order so a numeral beside a word that spells the same numeral
   was saying it twice. */
/* A self-contained chip, matching the hero's eyebrow: the label needs an edge
   of its own or it floats against the column with nothing holding it. A
   trailing rule only drew attention to the empty space it ran into. */
.step-eyebrow {
  display: inline-flex; align-items: center;
  margin-bottom: .8rem;
  padding: .2rem .7rem; border-radius: 999px;
  font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
  background: var(--brand-100);
  border: 1px solid color-mix(in srgb, var(--brand-500) 22%, transparent);
  color: var(--brand-700);
}

.step-text h3 { font-size: 1.4rem; letter-spacing: -.03em; margin: 0 0 .65rem; font-weight: 700; }
.step-text p { color: var(--ink-2); font-size: .98rem; margin: 0 0 1rem; }
.step-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; }
.step-list li { position: relative; padding-left: 1.5rem; font-size: .91rem; color: var(--ink-2); }
.step-list li::before {
  content: "✓"; position: absolute; left: 0; top: 0;
  color: var(--green-text); font-weight: 700; font-family: var(--mono); font-size: .85rem;
}
.step-list strong { color: var(--ink); font-weight: 600; }

.step-panel {
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); box-shadow: var(--elev-2);
  overflow: hidden;
}
.panel-head {
  padding: .6rem .9rem; border-bottom: 1px solid var(--line);
  background: var(--surface-2);
  font-family: var(--mono); font-size: .73rem; color: var(--ink-3);
}
.panel-body { padding: 1.1rem; }

/* SERP preview inside a panel */
.serp-note {
  font-family: var(--mono); font-size: .72rem; color: var(--amber-text);
  margin-top: .6rem; display: flex; align-items: center; gap: .4rem;
}

/* code-ish evidence block */
.evidence {
  font-family: var(--mono); font-size: .78rem; line-height: 1.7;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 8px; padding: .75rem .85rem; overflow-x: auto;
}
.evidence .del { color: var(--red-text); }
.evidence .ins { color: var(--green-text); }
.evidence .cm { color: var(--ink-3); }

/* mini bar chart */
.bars { display: flex; flex-direction: column; }
.bar-row {
  display: grid; grid-template-columns: 12rem 1fr 2.4rem;
  gap: .75rem; align-items: center; font-size: .78rem;
  padding: .38rem 0;
}
/* A hairline between rows rather than a gap: the report shows these as a
   table of records and the rule is what makes each one a row. */
.bar-row + .bar-row { border-top: 1px solid var(--line-2); }

/* The outer rows sit flush: the panel's own padding is the space there and the rows' padding on top of it reads as a gap nobody asked for. */
.bar-row:first-child { padding-top: 0; }
.bar-row:last-child { padding-bottom: 0; }

/* The label carries its own second line: how many checks ran and how many
   found something, which is what turns a bare score into a measurement. */
.bar-label { display: flex; flex-direction: column; font-weight: 600; min-width: 0; line-height: 1.3; }
.bar-label b { font-weight: 400; font-size: .68rem; color: var(--ink-3); line-height: 1.4; }

.bar-track { height: 7px; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.bar-track i { display: block; height: 100%; border-radius: 999px; }
.bar-val { font-family: var(--mono); font-size: .8rem; text-align: right; font-weight: 700; }

/* The total is the answer the rows add up to so it sits below a heavier rule
   and carries more weight than any single category. */
.bar-total { margin-top: .2rem; padding-top: .55rem; }
.bar-row.bar-total { border-top: 1px solid var(--line); }
.bar-total .bar-val { font-family: var(--display); font-size: 1.05rem; font-weight: 800; }

/* ---- before vs after ---- */

.impact-toggle {
  display: flex; gap: .25rem; margin: 0 auto 1.75rem;
  padding: .25rem; width: max-content;
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 12px;
}

.impact-btn {
  font: inherit; font-size: .8rem; font-weight: 700;
  padding: .45rem 1.1rem; border: 0; border-radius: 9px;
  background: none; color: var(--ink-2); cursor: pointer;
  transition: background .15s, color .15s;
}
.impact-btn:hover { color: var(--ink); }

/* The selected state carries the colour of what it shows: red for the page
   with faults, green for the one without. */
.impact-btn.is-on { color: #fff; }
.impact-btn.is-on[data-impact="before"] { background: var(--red-fill); }
.impact-btn.is-on[data-impact="after"]  { background: var(--green-fill); }

/* On a phone the two labels share the width rather than overflowing it. */
@media (max-width: 560px) {
  .impact-toggle { width: auto; max-width: 100%; }
  .impact-btn { flex: 1; padding: .45rem .6rem; line-height: 1.3; }
}

.impact-card {
  display: grid; grid-template-columns: 17rem 1fr; gap: 2rem;
  align-items: center;
  padding: 1.75rem;
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 18px;
}

.impact-score { text-align: center; border-right: 1px solid var(--line); padding-right: 1.75rem; }

/* display wins over the hidden attribute unless it is put back: both states
   ship in the markup and the inactive one is hidden so the rule that gives
   the badge its box would otherwise show both at once. */
.impact-badge[hidden], .impact-item[hidden], .impact-more[hidden] { display: none; }

.impact-badge {
  display: inline-block; margin-bottom: .9rem;
  padding: .2rem .7rem; border-radius: 999px;
  font-size: .72rem; font-weight: 700;
}
.impact-badge.t-after  { background: var(--green-100); color: var(--green-text); }
.impact-badge.t-before { background: var(--red-100);   color: var(--red-text); }

.impact-n {
  font-family: var(--display); font-size: 4rem; font-weight: 800;
  line-height: 1; letter-spacing: -.04em; color: var(--ink);
}
.impact-label {
  margin: .35rem 0 0; font-size: .66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .09em; color: var(--ink-3);
}
.impact-sum { margin: .9rem auto 0; max-width: 22rem; font-size: .78rem; color: var(--ink-2); line-height: 1.55; }

.impact-items { display: flex; flex-direction: column; gap: .6rem; }

/* The count of what the list does not show so four items do not read as the
   whole report. */
.impact-more { margin: .15rem 0 0; font-size: .76rem; color: var(--ink-3); }

.impact-item {
  display: flex; align-items: flex-start; gap: .7rem;
  padding: .7rem .85rem; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface);
}
.impact-item.t-after { border-color: color-mix(in srgb, var(--green-text) 22%, transparent); background: color-mix(in srgb, var(--green-100) 45%, var(--surface)); }
.impact-item.t-before { border-color: color-mix(in srgb, var(--red-text) 20%, transparent);   background: color-mix(in srgb, var(--red-100) 45%, var(--surface)); }

.impact-item h5 { margin: 0; font-size: .82rem; font-weight: 700; color: var(--ink); }
.impact-item p  { margin: .15rem 0 0; font-size: .74rem; color: var(--ink-2); font-family: var(--mono); line-height: 1.5; overflow-wrap: anywhere; }

/* The marker states the outcome in a glyph as well as a colour so the two
   states are distinguishable without relying on hue alone. */
.impact-tick, .impact-cross {
  display: grid; place-items: center; flex: none;
  width: 18px; height: 18px; margin-top: .1rem;
  border-radius: 50%; font-size: .72rem; font-weight: 700; color: #fff;
}
/* Outlined circles rather than filled discs: the pair reads as two states of
   the same mark, and the lighter weight sits better on the tinted rows than a
   solid dot that competes with the text beside it. */
.impact-tick {
  background: transparent; color: var(--green-text);
  border: 1.5px solid currentColor;
  font-size: .62rem; line-height: 1;
}
.impact-cross {
  background: transparent; color: var(--red-text);
  border: 1.5px solid currentColor;
  font-size: .68rem; line-height: 1;
}

@media (max-width: 52rem) {
  .impact-card { grid-template-columns: 1fr; gap: 1.5rem; }
  .impact-score { border-right: 0; padding-right: 0; border-bottom: 1px solid var(--line); padding-bottom: 1.5rem; }
}

/* ---- grouped "everything tested" list ---- */

/* Three to a row. auto-fit gave four on a wide screen, which left the prose
   in each card too narrow to read as prose. */
.checks-grid {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}
@media (max-width: 62rem) { .checks-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 40rem) { .checks-grid { grid-template-columns: 1fr; } }

/* ---- CTA ---- */

/* ---------- pricing page ---------- */

.pricing-hero { text-align: center; padding-block: 3.5rem 2.5rem; }
.pricing-hero h1 {
  font-size: clamp(2rem, 5vw, 3rem); line-height: 1.12; font-weight: 800;
  margin: 1rem 0 .8rem; letter-spacing: -.03em; text-wrap: balance;
}
.pricing-hero h1 .accent {
  background: linear-gradient(100deg, var(--brand-700), var(--brand-500));
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.pricing-hero .lede { max-width: 46ch; margin: 0 auto; color: var(--ink-2); font-size: 1.02rem; }
.pricing-hero .billing { margin-top: 2rem; }

.pricing-grid {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: .85rem;
  align-items: stretch; margin-bottom: 1.5rem;
}

.ptier {
  display: flex; flex-direction: column;
  padding: 1.35rem;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 18px;
}

/* The featured plan is lifted rather than recoloured: a second colour here
   competes with the severity palette the report depends on. */
.ptier.is-featured {
  position: relative;
  border-color: var(--brand-600); border-width: 2px;
  padding: 1.6rem calc(1.6rem - 1px);
  box-shadow: 0 24px 50px -28px color-mix(in srgb, var(--brand-700) 55%, transparent);
}

.ptier-badge {
  position: absolute; top: -.72rem; left: 50%; transform: translateX(-50%);
  padding: .2rem .75rem; border-radius: 999px;
  background: var(--brand-600); color: #fff;
  font-size: .66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .07em; white-space: nowrap;
}

.ptier-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem; margin-bottom: .5rem; }
.ptier-head h2 { margin: 0; font-size: 1.15rem; letter-spacing: -.015em; }
.ptier-tag {
  font-size: .64rem; font-weight: 600; color: var(--ink-3);
  padding: .16rem .5rem; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line); white-space: nowrap;
}
.ptier-note { margin: 0 0 1.2rem; font-size: .82rem; color: var(--ink-2); line-height: 1.55; min-height: 3.4em; }

.ptier-price {
  margin: 0 0 1rem; padding-bottom: .9rem;
  border-bottom: 1px solid var(--line);
  display: flex; align-items: baseline; gap: .35rem; flex-wrap: wrap;
}
.ptier-price .amount { font-family: var(--display); font-size: 2.4rem; font-weight: 800; letter-spacing: -.035em; line-height: 1; }
.ptier-price .period { font-size: .8rem; color: var(--ink-3); }

.ptier-list { list-style: none; margin: 0 0 1.4rem; padding: 0; flex: 1; display: flex; flex-direction: column; gap: .6rem; }
/* The icon is the only flex item; the rest of the line is ordinary inline text.
   Flexing the whole row made every <b> its own item, so the gap landed between
   the bold phrase and the comma after it: "3 seats , $12 each after that". */
.ptier-list li { font-size: .84rem; color: var(--ink-2); line-height: 1.45; padding-left: 1.45rem; position: relative; }
.ptier-list li.off { color: var(--ink-3); }
.ptier-list b { color: var(--ink); font-weight: 700; }

.pt-tick, .pt-cross { position: absolute; left: 0; top: .18em; display: grid; place-items: center; width: 16px; height: 16px; }
.pt-tick svg, .pt-cross svg { width: 14px; height: 14px; display: block; }
.pt-tick { color: var(--green-text); }
.pt-cross { color: var(--ink-3); }

.ptier-cta { justify-content: center; width: 100%; box-sizing: border-box; }

/* The reassurance under the tiers.
 *
 * One tinted bar rather than three floating phrases: it reads as a single
 * statement about buying, which is what it is, and it gives the row of cards a
 * floor to sit on.
 */
.pricing-trust {
  list-style: none; margin: 0 auto 1.5rem; padding: .8rem 1.25rem;
  display: flex; flex-wrap: wrap; justify-content: center;
  align-items: center; gap: .5rem 1.4rem;
  font-size: .82rem; color: var(--ink-2);
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 12px;
  width: fit-content; max-width: 100%; box-sizing: border-box;
}

.pricing-trust li { display: inline-flex; align-items: center; gap: .45rem; }

/* A dot between items, drawn by the list rather than marked up, so it never
   appears before the first item or after a wrapped line's last. */
.pricing-trust li + li::before {
  content: ''; flex: none;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--ink-3); opacity: .55;
  margin-right: .95rem;
}

.trust-icon { display: grid; place-items: center; flex: none; width: 15px; height: 15px; color: var(--green-text); }
.trust-icon svg { width: 15px; height: 15px; display: block; }

/* Once the items stack, a separator between them reads as a bullet hanging off
   the left of each line. The rows are their own separation at that width. */
@media (max-width: 40rem) {
  .pricing-trust { align-items: flex-start; gap: .55rem; }
  .pricing-trust li + li::before { display: none; }
}

/* ---- what it saves ---- */

/* The saving calculator.
 *
 * A tinted panel holding white tiles: the panel reads as one instrument, and
 * the two figures it produces sit on the surface colour so they lift off it.
 * The inputs keep the left half because that is the half people touch.
 */
.roi-card {
  display: grid; grid-template-columns: 1.15fr 1fr; gap: 1.75rem;
  padding: 1.75rem;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 18px;
}

.roi-label {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  font-size: .82rem; font-weight: 700; color: var(--ink); margin-bottom: .55rem;
}
.roi-out {
  font-family: var(--mono); font-size: .82rem; font-weight: 700;
  color: var(--brand-700); background: var(--brand-100);
  padding: .18rem .55rem; border-radius: 999px; white-space: nowrap;
}

.roi-inputs input[type="range"] { width: 100%; accent-color: var(--brand-600); }

/* Four markers rather than two ends: the scale is not linear in what it costs
   you, and the middle values are the ones people aim at. */
.roi-scale {
  display: flex; justify-content: space-between;
  font-size: .68rem; color: var(--ink-3); font-family: var(--mono);
  margin: .3rem 0 1.4rem;
}

/* What the arithmetic assumes, next to the controls that feed it. */
.roi-notes { list-style: none; margin: .4rem 0 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.roi-notes li {
  position: relative; padding-left: 1.45rem;
  font-size: .78rem; color: var(--ink-2); line-height: 1.5;
}
.roi-notes .pt-tick { top: .15em; }

.roi-results { display: flex; flex-direction: column; gap: .8rem; }
.roi-stats { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }

.roi-stat {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 1.1rem .9rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.roi-stat-label {
  font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-3); line-height: 1.3;
}
.roi-stat-value {
  font-family: var(--display); font-size: 2.1rem; font-weight: 800;
  line-height: 1.15; letter-spacing: -.03em; color: var(--brand-700);
  margin: .3rem 0 .25rem; font-variant-numeric: tabular-nums;
}
.roi-stat-value.alt { color: var(--green-text); }
.roi-stat-note { font-size: .68rem; color: var(--ink-3); line-height: 1.4; }

/* The recommendation, which is the point of the whole panel: it turns two
   numbers into the one decision the reader came to the pricing page to make. */
.roi-plan {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
  margin: 0; padding: .9rem 1.1rem; border-radius: 14px;
  background: var(--brand-100);
  border: 1px solid color-mix(in srgb, var(--brand-500) 22%, transparent);
  font-size: .82rem; color: var(--ink-2);
}
.roi-plan-label { color: var(--ink-2); }
.roi-plan b { color: var(--brand-700); font-weight: 800; }
.roi-plan-price { font-family: var(--mono); font-weight: 700; color: var(--brand-700); }
.roi-plan-cta {
  margin-left: auto; font-weight: 700; color: var(--brand-700); text-decoration: none;
  white-space: nowrap;
}
.roi-plan-cta:hover { text-decoration: underline; }

/* The workings, shown rather than asserted: a calculator that will not say
   where its numbers came from is a sales device, not an estimate. */
.roi-basis { margin: 1rem 0 0; font-size: .78rem; color: var(--ink-3); line-height: 1.6; }

/* ---- comparison table ---- */

.matrix-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); }
/* Five columns need more room than four did. Below this the wrapper scrolls
   sideways rather than squeezing the columns until the words break. */
.matrix { width: 100%; border-collapse: collapse; font-size: .84rem; min-width: 46rem; }

/* The label column carries the longest text, so it gets the slack and the four
   plan columns divide what is left evenly. */
.matrix th[scope="row"] { width: 34%; }
.matrix thead th:not(:first-child) { width: 16.5%; }
.matrix thead th {
  text-align: left; padding: .85rem 1rem; font-size: .74rem; font-weight: 700;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
.matrix thead th:not(:first-child) { text-align: center; }
.matrix thead th.is-featured { color: var(--brand-700); }

.matrix tbody th { text-align: left; font-weight: 400; color: var(--ink-2); padding: .7rem 1rem; }

/* A cell holding a phrase rather than a tick keeps it on one line: "3 sites
   monitored" broken across two reads as two separate facts. */
.matrix tbody td {
  text-align: center; padding: .7rem .75rem; color: var(--ink-2);
  text-wrap: balance;
}
.matrix tbody tr { border-bottom: 1px solid var(--line-2); }
.matrix tbody tr:last-child { border-bottom: 0; }
.matrix td.is-featured { background: color-mix(in srgb, var(--brand-500) 4%, transparent); }

.matrix .m-group th {
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); background: var(--surface-2); padding: .55rem 1rem;
}
.m-yes { color: var(--green-text); font-weight: 700; }
.m-no { color: var(--ink-3); }

/* Four across needs the room. Below it, two by two keeps each card wider than
   its own price before the single column takes over. */
@media (max-width: 1080px) {
  .pricing-grid { grid-template-columns: repeat(2, 1fr); gap: 1rem; }
  .ptier { padding: 1.6rem; }
}

@media (max-width: 62rem) {
  .pricing-grid { grid-template-columns: 1fr; max-width: 26rem; margin-inline: auto; }
  .ptier.is-featured { order: -1; }
  .ptier-note { min-height: 0; }
  .roi-card { grid-template-columns: 1fr; gap: 1.5rem; }
}

/* The daily-limit page. Not an error: nothing broke so it reads as a fact
   with a way forward rather than an apology.

   The actions sit in a row and the offer block below states the rest: this
   rule used to stack them into a column, which read as two unrelated links
   rather than a choice between a primary action and a way back. */
.quota-note { margin: 0; font-size: .88rem; color: var(--ink-3); line-height: 1.6; }

/* ---- homepage pricing ---- */

/* The billing switch, above the cards it changes. */
/* The billing period.
 *
 * One switch with a label either side, rather than two buttons where only the
 * selected one is legible: both periods stay readable at all times, and the
 * knob says which is active. It is a real switch to assistive technology too,
 * so it announces as on or off instead of as two unrelated buttons.
 */
.billing {
  display: flex; align-items: center; gap: .7rem;
  margin: -1.25rem auto 2rem; width: max-content;
  padding: .5rem .9rem;
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 999px;
}

.billing-label {
  font-size: .84rem; font-weight: 700; color: var(--ink-3);
  transition: color .15s ease;
}
.billing-label.is-on { color: var(--ink); }

.billing-switch {
  position: relative; flex: none;
  width: 3.1rem; height: 1.7rem; padding: 0;
  border: 0; border-radius: 999px; cursor: pointer;
  background: var(--brand-600);
  transition: background .18s ease;
}
.billing-switch:hover { background: var(--brand-700); }
.billing-switch:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 3px; }

/* The knob sits right when unchecked, because "off" here means monthly and the
   label it points at is the one on the left. */
.billing-knob {
  position: absolute; top: 50%; left: .22rem;
  width: 1.26rem; height: 1.26rem; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgb(0 0 0 / .25);
  transform: translate(1.4rem, -50%);
  transition: transform .18s ease;
}
.billing-switch[aria-checked="false"] .billing-knob { transform: translate(0, -50%); }

.billing-save {
  font-size: .66rem; font-weight: 700;
  padding: .18rem .45rem; border-radius: 999px;
  background: var(--green-100); color: var(--green-text);
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .billing-knob, .billing-switch, .billing-label { transition: none; }
}

/* The narrowest phones: the same row, packed a little tighter, so the
   Save badge stays on the screen. */
@media (max-width: 380px) {
  .billing { gap: .45rem; padding: .45rem .65rem; max-width: 100%; }
  .billing-label { font-size: .8rem; }
}

.tiers {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: .85rem; align-items: stretch;
}

@media (max-width: 1080px) {
  .tiers { grid-template-columns: repeat(2, 1fr); gap: 1rem; }
}

.tier {
  display: flex; flex-direction: column;
  padding: 1.5rem;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 16px;
}

/* The featured plan is lifted rather than recoloured: a second colour here
   competes with the severity palette the report depends on. */
.tier.is-featured {
  position: relative;
  border-color: var(--brand-600); border-width: 2px;
  padding: 1.5rem calc(1.5rem - 1px);
  box-shadow: 0 20px 44px -26px color-mix(in srgb, var(--brand-700) 55%, transparent);
}

.tier-badge {
  position: absolute; top: -.72rem; left: 50%; transform: translateX(-50%);
  padding: .2rem .7rem; border-radius: 999px;
  background: var(--brand-600); color: #fff;
  font-size: .68rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  white-space: nowrap;
}

.tier-head { display: flex; align-items: center; justify-content: space-between; gap: .6rem; margin-bottom: .3rem; }
.tier-head h3 { margin: 0; font-size: 1.1rem; letter-spacing: -.01em; }
.tier-tag {
  font-size: .66rem; font-weight: 600; color: var(--ink-3);
  padding: .16rem .5rem; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
  white-space: nowrap;
}

.tier-note { margin: 0 0 1rem; font-size: .8rem; color: var(--ink-2); line-height: 1.5; min-height: 2.4em; }

.tier-price { margin: 0 0 .95rem; padding-bottom: .85rem; border-bottom: 1px solid var(--line); display: flex; align-items: baseline; gap: .35rem; flex-wrap: wrap; }
.tier-price .amount { font-family: var(--display); font-size: 2.1rem; font-weight: 800; letter-spacing: -.03em; line-height: 1; }
.tier-price .period { font-size: .78rem; color: var(--ink-3); }

/* flex:1 so the buttons line up across cards whose lists differ in length. */
.tier-list { list-style: none; margin: 0 0 1.25rem; padding: 0; flex: 1; display: flex; flex-direction: column; gap: .5rem; }
/* Same as the pricing page: only the icon is positioned, so a bold phrase
   mid-sentence does not get a gap inserted after it. */
.tier-list li { font-size: .82rem; color: var(--ink-2); line-height: 1.45; padding-left: 1.4rem; position: relative; }
.tier-list li.off { color: var(--ink-3); }
.tier-list b { color: var(--ink); font-weight: 700; }

/* A comma, not a compound: no element carries both classes, so ".a.b" matched
   nothing and neither icon was ever sized or placed. */
.tier-tick, .tier-cross { position: absolute; left: 0; top: .2em; display: grid; place-items: center; width: 15px; height: 15px; }
.tier-tick svg, .tier-cross svg { width: 13px; height: 13px; display: block; }
.tier-tick { color: var(--green-text); }
.tier-cross { color: var(--ink-3); }

.tier-cta { justify-content: center; width: 100%; box-sizing: border-box; }

/* Three reassurances under the cards, each with a tick: the row answers what
   a reader hesitates over at the moment they are choosing a plan. */
.tiers-foot {
  list-style: none; margin: 1.5rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem 1.75rem;
  font-size: .8rem; color: var(--ink-2);
}
.tiers-foot li { display: inline-flex; align-items: center; gap: .4rem; }
.tiers-foot svg { width: 14px; height: 14px; flex: none; color: var(--green-text); }

.tiers-link { margin: .9rem 0 0; text-align: center; font-size: .82rem; }
.tiers-link a { font-weight: 600; text-decoration: none; }
.tiers-link a:hover { text-decoration: underline; }

@media (max-width: 58rem) {
  .tiers { grid-template-columns: 1fr; max-width: 26rem; margin: 0 auto; }
  .tier.is-featured { order: -1; }
  .tier-note { min-height: 0; }
}

/* ---- homepage FAQ ---- */

.faq-list { display: flex; flex-direction: column; gap: .7rem; max-width: 48rem; margin: 0 auto; }

/* A question is its own card, tinted so the row reads as a control before it is
   opened. The report's disclosure styling is built for a dense list of findings
   under a heading; a page with six questions on it wants air between them. */
.faq-home .finding {
  border-radius: 14px; margin-bottom: 0;
  background: var(--surface-2); border: 1px solid var(--line);
  overflow: hidden;
}
.faq-home .finding[open] { background: var(--surface); border-color: var(--line); }

.faq-home .finding summary {
  display: flex; align-items: center; gap: .75rem;
  padding: 1rem 1.15rem; cursor: pointer;
  list-style: none;
}
.faq-home .finding summary::-webkit-details-marker { display: none; }
.faq-home .finding summary::marker { content: ''; }

.faq-home .f-body { flex: 1; min-width: 0; }
.faq-home .f-msg { font-weight: 700; font-size: .92rem; color: var(--ink); line-height: 1.45; }

/* The chevron is drawn here rather than marked up, so the six questions carry
   no repeated SVG and the rotation has one owner. */
.faq-home .finding summary::after {
  content: ''; flex: none; width: 9px; height: 9px;
  border-right: 2px solid var(--ink-3); border-bottom: 2px solid var(--ink-3);
  border-radius: 1px;
  transform: translateY(-2px) rotate(45deg);
  transition: transform .18s ease;
}
.faq-home .finding[open] summary::after { transform: translateY(2px) rotate(-135deg); }

.faq-home .finding summary:hover .f-msg { color: var(--brand-700); }
.faq-home .finding summary:focus-visible { outline: 2px solid var(--brand-600); outline-offset: -2px; }

.faq-home .f-detail { padding: 0 1.15rem 1.1rem; }
.faq-home .f-detail p { margin: 0; font-size: .88rem; line-height: 1.65; color: var(--ink-2); }

@media (prefers-reduced-motion: reduce) {
  .faq-home .finding summary::after { transition: none; }
}
.faq-home .f-detail p { margin: 0; font-size: .88rem; color: var(--ink-2); line-height: 1.6; }

/* A chevron that turns on open: the native marker is suppressed so without
   one nothing says these rows do anything. */
.faq-home .finding > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.faq-home .finding > summary::after {
  content: ''; flex: none;
  width: 8px; height: 8px; margin-right: .2rem;
  border-right: 2px solid var(--ink-3);
  border-bottom: 2px solid var(--ink-3);
  border-radius: 1px;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s;
}
.faq-home .finding[open] > summary::after { transform: rotate(225deg) translate(-2px, -2px); }
.faq-home .finding[open] { border-color: color-mix(in srgb, var(--brand-500) 35%, var(--line)); }

.faq-more-link { text-align: center; margin: 1.5rem 0 0; font-size: .88rem; }
.faq-more-link a { font-weight: 600; text-decoration: none; }
.faq-more-link a:hover { text-decoration: underline; }

/* ---- closing call to action ---- */

/* Two columns: the case on the left, the form on the right. A form sitting in
   the middle of a dark panel reads as an afterthought; on its own light card
   it reads as the thing to do next. */
.cta {
  display: grid; grid-template-columns: 1fr 23rem; gap: 2.5rem;
  align-items: center; text-align: left;
  border-radius: 20px;
  /* Fixed deep value, not a theme token: --brand-700 lightens in dark mode,
     which would leave white text on a pale panel. */
  background: #071A33;
  /* Near-black navy into a deep brand blue, lit from the top left. Fixed
     values rather than tokens: --brand-700 lightens in dark mode, which would
     leave white text on a pale panel. */
  background-image:
    radial-gradient(46rem 30rem at 6% -18%, rgba(96,165,250,.22), transparent 68%),
    linear-gradient(135deg, #071A33 0%, #0A2E5C 52%, #0C3E7D 100%);
  padding: 2.5rem; margin-block: 5.5rem; box-shadow: var(--elev-2);
}

.cta-pill {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .22rem .75rem; margin-bottom: 1.1rem;
  border-radius: 999px;
  background: rgba(255,255,255,.14);
  border: 1px solid rgba(255,255,255,.2);
  color: #fff; font-size: .74rem; font-weight: 600;
}
.cta-pill .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--green-fill); flex: none;
}

.cta h2 {
  color: #fff; font-size: 2.15rem; line-height: 1.2;
  margin: 0 0 .8rem; font-weight: 800; letter-spacing: -.02em;
}
/* The second line carries a gradient clipped to the letters. A flat tint
   across a whole line reads as highlighter; the gradient keeps the eye moving
   along it, which is what the hero's accent does too. */
.cta h2 .accent {
  background: linear-gradient(100deg, #7DC4FF 0%, #6EE7C7 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.cta .cta-sub { color: rgba(255,255,255,.8); margin: 0 0 1.4rem; max-width: 42ch; font-size: .96rem; }

/* Two columns of claims so the list reads as a set rather than a long
   descending line beside a short form. */
.cta-list {
  list-style: none; padding: 0; margin: 0;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: .55rem 1.25rem;
}
.cta-list li {
  position: relative; padding-left: 2rem;
  color: rgba(255,255,255,.88); font-size: .86rem; line-height: 1.45;
}

/* A tick in a tinted ring. Grid centring rather than two absolutely placed
   pseudo-elements: the earlier pair anchored one in em and the other in px so they drifted apart from each other and from the text. */
.cta-tick {
  position: absolute; left: 0; top: .08em;
  display: grid; place-items: center;
  width: 19px; height: 19px; border-radius: 50%;
  background: color-mix(in srgb, var(--green-fill) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--green-fill) 45%, transparent);
  color: #6EE7C7;
}

.cta-tick svg { width: 11px; height: 11px; display: block; }

.cta-panel {
  background: var(--surface);
  border-radius: 18px; padding: 1.4rem;
  box-shadow: 0 26px 55px -26px rgb(0 0 0 / .5);
}

/* Title left, mark right: the mark balances the heading against the panel's
   width without competing with it for the reader's first glance. */
.cta-panel-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; margin-bottom: 1rem;
}
.cta-panel h3 { margin: 0 0 .2rem; font-size: 1.15rem; letter-spacing: -.015em; }
.cta-panel-sub { margin: 0; font-size: .78rem; color: var(--ink-3); line-height: 1.5; }

.cta-panel-mark {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: 11px;
  background: var(--brand-100); color: var(--brand-700);
}
.cta-panel-mark svg { width: 19px; height: 19px; }

.cta-form { display: flex; flex-direction: column; }
.cta-form label {
  font-size: .73rem; font-weight: 600; color: var(--ink-2);
  margin-bottom: .25rem;
}

/* The icon sits inside the field rather than beside it so the well is one
   object and the caret starts where the text does. */
.cta-field {
  display: flex; align-items: center; gap: .55rem;
  padding: 0 .7rem; margin-bottom: .65rem;
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 10px;
  transition: border-color .15s, box-shadow .15s, background .15s;
}
.cta-field svg { width: 16px; height: 16px; flex: none; color: var(--ink-3); }

.cta-field:focus-within {
  background: var(--surface);
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 16%, transparent);
}

.cta-form input {
  flex: 1; min-width: 0;
  font: inherit; font-size: .86rem;
  padding: .55rem 0;
  border: 0; background: none; color: var(--ink);
}
.cta-form input:focus { outline: none; }
.cta-form input::placeholder { color: var(--ink-3); }

.cta-submit {
  justify-content: center; gap: .45rem;
  margin-top: .25rem; padding: .65rem 1rem;
  border-radius: 10px; font-size: .88rem; font-weight: 700;
}
.cta-submit svg { width: 15px; height: 15px; flex: none; }

.cta-panel-alt { margin: .75rem 0 0; font-size: .77rem; color: var(--ink-3); text-align: center; }
.cta-panel-alt a { font-weight: 700; text-decoration: none; }
.cta-panel-alt a:hover { text-decoration: underline; }

/* Three reassurances on one rule-separated row. Each carries an icon as well
   as a word so the row reads at a glance rather than as a sentence. */
.cta-trust {
  list-style: none; margin: .75rem 0 0; padding: .7rem 0 0;
  border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; justify-content: center; gap: .4rem .75rem;
  font-size: .68rem; color: var(--ink-3);
}
.cta-trust li { display: inline-flex; align-items: center; gap: .3rem; }
.cta-trust svg { width: 13px; height: 13px; flex: none; color: var(--green-text); }

@media (max-width: 58rem) {
  .cta { grid-template-columns: 1fr; gap: 2rem; padding: 2rem 1.5rem; }
  .cta h2 { font-size: 1.8rem; }
}
@media (max-width: 30rem) {
  .cta-list { grid-template-columns: 1fr; }
}


@media (max-width: 56rem) {
  .step { grid-template-columns: 1fr; gap: 1.5rem; }
  .step:nth-child(even) .step-text { order: 0; }
  .site-nav { display: none; }
}
/* ==========================================================================
   Report evidence
   ========================================================================== */

.evidence-block { margin-bottom: 1.1rem; }
.evidence-block:last-child { margin-bottom: 0; }
.evidence-block h4 {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-3); margin: 0 0 .5rem; font-weight: 700;
}
.evidence-note { font-size: .8rem; color: var(--ink-3); margin: .5rem 0 0; line-height: 1.5; }
/* Most notes trail their table so the base rule carries no bottom margin.
   The reconciliation line is the exception, it introduces the table and without this it sits flush against the header row. */
.evidence-note + .evidence-scroll,
.evidence-note + .evidence-table { margin-top: .6rem; }

.evidence-table {
  width: 100%; border-collapse: collapse; font-size: .84rem;
  border: 1px solid var(--line); border-radius: 8px; overflow: hidden;
}
.evidence-table th {
  text-align: left; font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-3); font-weight: 700;
  padding: .45rem .65rem; background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
.evidence-table td { padding: .45rem .65rem; border-bottom: 1px solid var(--line-2); }
.evidence-table tr:last-child td { border-bottom: 0; }
.evidence-table .k { font-family: var(--mono); font-size: .8rem; }
.evidence-table .num, .evidence-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.evidence-table .mark, .evidence-table th.mark { text-align: center; width: 2.5rem; }
.evidence-table .muted { color: var(--ink-3); }
.evidence-table .yes { color: var(--green-text); font-weight: 700; }
.evidence-table .no { color: var(--red-text); font-weight: 700; }
.evidence-table .bar-cell { width: 45%; }

.mini-bar { display: block; height: 6px; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.mini-bar i { display: block; height: 100%; background: var(--brand-500); border-radius: 999px; }
.mini-bar.over i { background: var(--amber-fill); }

/* ---- keyword cloud ---- */

.keyword-cloud {
  display: flex; flex-wrap: wrap; gap: .3rem .7rem; align-items: baseline;
  padding: 1rem; border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface-2); line-height: 1.5;
}
.cloud-word { color: var(--ink); cursor: default; }

/* ---- core web vitals scale ---- */

.vital-scale {
  position: relative; display: flex; height: 8px;
  border-radius: 999px; overflow: hidden; margin-bottom: .45rem;
}
.vital-band.good { background: var(--green-fill); }
.vital-band.ok   { background: var(--amber-fill); }
.vital-band.poor { background: var(--red-fill); }
.vital-marker {
  position: absolute; top: -3px; width: 3px; height: 14px;
  background: var(--ink); border-radius: 2px; transform: translateX(-50%);
  box-shadow: 0 0 0 2px var(--surface);
}
.vital-legend {
  display: flex; justify-content: space-between;
  font-family: var(--mono); font-size: .72rem; color: var(--ink-3);
}

@media (max-width: 44rem) {
  .evidence-table .bar-cell { display: none; }
}

/* ==========================================================================
   Print / PDF

   The primary PDF export is server-side via Dompdf (see Report\PdfRenderer),
   which produces a document that can be emailed, stored and white-labelled.
   This stylesheet covers the secondary case: a reader printing the page
   directly from the browser.
   ========================================================================== */

@media print {
  @page { margin: 14mm 12mm; }

  :root {
    /* Force the light palette: a dark report wastes ink and reads poorly. */
    --ink:#141A22; --ink-2:#3F4A57; --ink-3:#6B7684;
    --line:#D8DEE5; --line-2:#EBEFF3; --line-3:#C7CFD8;
    --surface:#FFFFFF; --surface-2:#F7F9FB; --ground:#FFFFFF;
    --brand-600:#0A6CEF; --brand-700:#0852BC; --brand-100:#E6F0FE;
    --shadow: none; --elev-1: none; --elev-2: none; --elev-3: none;
  }

  body { background: #fff; color: var(--ink); font-size: 10.5pt; }

  /* Chrome that means nothing on paper. */
  .site-header, .site-footer, .theme-cycle, .site-nav, .analyze-form, .cta, .hero::before, .hero::after { display: none !important; }

  .wrap { max-width: none; padding: 0; }
  .section { margin: 0 0 10mm; }

  /* Every finding opens so the PDF is the whole report rather than a
     collection of collapsed summaries. */
  details.finding { display: block; }
  details.finding > summary { list-style: none; }
  details.finding .f-detail { display: block !important; }
  .finding, .group, .snapshot, .actions, .serp {
    box-shadow: none !important;
    border: 1px solid var(--line) !important;
  }

  /* Keep a finding, a group header and an action row intact across a break. */
  .finding, .action-row, .cat-row, .serp, .evidence-block { break-inside: avoid; }
  .group-head { break-after: avoid; }
  .section-head-left, h2, h3 { break-after: avoid; }
  .group { break-inside: auto; }

  .snapshot { break-after: avoid; page-break-after: avoid; }

  /* Print the destination of a link, since it cannot be clicked. */
  .f-detail a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: .8em; color: var(--ink-3); word-break: break-all;
  }

  a { text-decoration: none; color: var(--ink); }

  /* Backgrounds carry meaning here, severity, score bands, bars so ask the
     browser to print them rather than dropping to white. */
  * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

.snapshot-actions { margin-top: 1rem; }

/* ==========================================================================
   Report dashboard
   ========================================================================== */

.section-head-left { max-width: 46rem; margin: 0 0 1.25rem; text-align: left; }
.section-head-left h2 { font-size: 1.35rem; letter-spacing: -.03em; margin: 0 0 .3rem; font-weight: 700; }
.section-head-left p { color: var(--ink-2); font-size: .92rem; margin: 0; }

/* ---- snapshot ---- */

.snapshot-card {
  border: 1px solid var(--line); border-radius: 16px;
  background: var(--surface); box-shadow: var(--elev-1);
  margin: 1.5rem 0 1.75rem; overflow: hidden;
}
.snapshot-score { display: flex; gap: 1.25rem; align-items: flex-start; min-width: 0; }

.dial-lg-readout { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.dial-lg-readout .n { font-family: var(--display); font-size: 2.1rem; font-weight: 800; line-height: 1; letter-spacing: -.03em; }
.dial-lg-readout .l { font-size: .62rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); margin-top: .2rem; }

.snapshot-meta { min-width: 0; }
.grade-chip {
  display: inline-block; font-size: .72rem; font-weight: 700;
  padding: .2rem .6rem; border-radius: 999px; margin-bottom: .55rem;
}
.snapshot-meta h1 { font-size: 1.1rem; margin: 0 0 .25rem; letter-spacing: -.02em; line-height: 1.3; }
.snapshot-url { font-size: .76rem; color: var(--ink-3); display: block; margin-bottom: .6rem; overflow-wrap: anywhere; }
.snapshot-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* ---- metric tiles ---- */

.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: .7rem; align-content: start; }

.tile {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .85rem .95rem; border-radius: 12px; border: 1px solid var(--line);
  background: var(--surface); text-align: left; cursor: pointer;
  font: inherit; transition: border-color .15s, background .15s;
}
.tile:hover { border-color: var(--ink-3); }
.tile[aria-pressed="true"] { border-width: 2px; padding: calc(.85rem - 1px) calc(.95rem - 1px); }
.tile-count { font-family: var(--display); font-size: 1.5rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; }

.tile-red { background: var(--red-100); border-color: color-mix(in srgb, var(--red-text) 20%, transparent); }
.tile-red .tile-label, .tile-red .tile-count { color: var(--red-text); }
.tile-amber { background: var(--amber-100); border-color: color-mix(in srgb, var(--amber-text) 20%, transparent); }
.tile-amber .tile-label, .tile-amber .tile-count { color: var(--amber-text); }
.tile-slate { background: var(--slate-100); border-color: var(--line); }
.tile-slate .tile-label, .tile-slate .tile-count { color: var(--ink-2); }
.tile-green { background: var(--green-100); border-color: color-mix(in srgb, var(--green-text) 20%, transparent); }
.tile-green .tile-label, .tile-green .tile-count { color: var(--green-text); }

/* ---- filter bar ---- */

.filter-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .85rem 1.5rem; border-top: 1px solid var(--line); background: var(--surface-2);
}
.filter-chips { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.filter-label { font-size: .74rem; font-weight: 700; color: var(--ink-3); text-transform: uppercase; letter-spacing: .06em; margin-right: .2rem; }
.chip {
  font: inherit; font-size: .8rem; font-weight: 500;
  padding: .3rem .7rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.chip b { font-variant-numeric: tabular-nums; font-weight: 700; }
.chip:hover { border-color: var(--ink-3); }
.chip[aria-pressed="true"] { background: var(--brand-600); border-color: var(--brand-600); color: var(--on-brand); }
.filter-search input {
  font: inherit; font-size: .82rem; padding: .4rem .7rem; width: 17rem; max-width: 100%;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink);
}
.filter-search input::placeholder { color: var(--ink-3); }
.filter-empty { padding: 1.5rem; text-align: center; color: var(--ink-3); font-size: .9rem; margin: 0; }

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

.report-grid { display: grid; grid-template-columns: 16.5rem minmax(0, 1fr); gap: 1.75rem; align-items: start; }
.report-main { min-width: 0; }

.report-nav { position: sticky; top: calc(4rem + env(safe-area-inset-top, 0px)); }
.nav-card { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); padding: 1rem; }
.nav-title {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); font-weight: 700; margin: 0 0 .6rem;
}
.nav-title + .specs, .nav-links + .nav-title { margin-top: 1.25rem; }
.nav-links { display: flex; flex-direction: column; gap: .15rem; margin-bottom: .25rem; }
.nav-link {
  display: flex; align-items: center; gap: .6rem;
  padding: .45rem .5rem; border-radius: 9px;
  font-size: .82rem; color: var(--ink-2); text-decoration: none;
}
.nav-link:hover { background: var(--surface-2); color: var(--ink); }
.nav-icon { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; flex: none; }
.nav-icon svg { width: 14px; height: 14px; fill: currentColor; }
.nav-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .82rem; }
.nav-badge {
  font-family: var(--mono); font-size: .68rem; font-weight: 700;
  padding: .05rem .35rem; border-radius: 5px; flex: none;
}
.nav-badge.bad { background: var(--red-100); color: var(--red-text); }
.nav-badge.warn { background: var(--amber-100); color: var(--amber-text); }
/* The dot occupies a badge's footprint so the right edge of the column is
   straight whichever marker a row carries but it is still a real circle,
   centred in that box by flex. A radial-gradient would also draw one and did
   briefly but its antialiased edge reads softer than a border-radius at this
   size. */
.nav-dot {
  flex: none;
  display: grid; place-items: center;
  /* Matches .nav-badge's min-width and its height so rows do not shift. */
  width: 1.2rem;
  height: 1.15rem;
}

.nav-dot::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--green-fill);
}


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

.group {
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); margin-bottom: 1rem; overflow: hidden;
  scroll-margin-top: 5rem;
}
.group-head {
  display: flex; align-items: center; gap: .85rem;
  padding: 1rem 1.15rem; border-bottom: 1px solid var(--line); background: var(--surface-2);
}
.group-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9px; flex: none; }
.group-icon svg { width: 18px; height: 18px; fill: currentColor; }
.group-title { flex: 1; min-width: 0; }
.group-title h2, .group-title h3 { font-size: .98rem; margin: 0 0 .1rem; letter-spacing: -.015em; }
.group-title p { font-size: .8rem; color: var(--ink-3); margin: 0; }
.group-counts { display: flex; align-items: center; gap: .4rem; flex: none; }
.group-body { padding: .7rem; }
.group-empty { padding: 1.25rem; text-align: center; color: var(--ink-3); font-size: .85rem; margin: 0; }


/* ---- actions ---- */

.actions { border-radius: 14px; border-color: var(--line); background: var(--surface); overflow: hidden; }
.action-row { padding: .9rem 1.1rem; }
.action-row:hover { background: var(--surface-2); }

@media (max-width: 62rem) {
  .report-grid { grid-template-columns: 1fr; }
  .report-nav { position: static; }
  .nav-links { flex-direction: row; flex-wrap: wrap; }
  .nav-link { flex: 1 1 12rem; }
  .snapshot-top { grid-template-columns: 1fr; }
}
@media (max-width: 40rem) {
  .snapshot-score { flex-direction: column; }
  .filter-search input { width: 100%; }
  .filter-search { width: 100%; }
}

/* ==========================================================================
   Report toolbar, checklist and section visualisations
   ========================================================================== */

/* Report controls, living in the site header rather than in a sticky bar of
   their own. The second bar cost a permanent row to display a URL the reader
   had just typed; merged in, it costs nothing. */
/* One rhythm for everything in the header's right-hand side.
   Each group used to set its own gap.35 here.3 there.5 on the account
   links so the eye read five different distances across one row. They all
   take this now and the only deliberately larger gap is the rule before the
   account links, which is separating two different kinds of thing. */
/* The report header packs a field, a button and account links into one row so it needs a tight rhythm. A marketing header holds three nav links and can
   breathe so it keeps the wider default. */
.site-header { --hdr-gap: 1.25rem; }
.page-report .site-header { --hdr-gap: .5rem; }

.rebar {
  display: flex; align-items: center; gap: var(--hdr-gap);
  /* min-width:0 lets the chip shrink rather than pushing the account links
     past the header's right edge. */
  min-width: 0; margin-left: auto;
}

/* The form is the field: the submit button is positioned inside it so the
   two read as one control rather than an input with something bolted beside. */
.rebar-form {
  position: relative;
  display: flex; align-items: center;
  min-width: 0; flex: 1 1 auto;
}

.rebar-url {
  font: inherit; font-family: var(--mono); font-size: .78rem;
  /* Right padding clears the magnifier sitting inside the field. */
  padding: .4rem 2.1rem .4rem .85rem;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface-2); color: var(--ink-2);
  text-overflow: ellipsis;
  /* Shrinks rather than pushing the account links off the row: no min-width,
     which flex cannot shrink past. */
  flex: 1 1 auto;
  min-width: 0;
  /* Compact at rest: the URL is already known so the field only needs to be
     readable. It grows on focus, where the reader is actually typing a path
     and the extra room is worth the neighbours shifting. */
  width: clamp(9rem, 20vw, 17rem);
  transition: width .18s ease, border-color .15s, background .15s, color .15s;
}

/* The browser's own clear button would sit underneath the magnifier. */
.rebar-url::-webkit-search-decoration,
.rebar-url::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

.rebar-url::placeholder { color: var(--ink-3); font-family: var(--font); }

.rebar-url:hover { border-color: var(--ink-3); color: var(--ink); }

/* Both states, because a text field is always :focus-visible, the caret needs
   an indicator and the global input rule sets an offset outline there. Left
   to :focus alone, the more specific selector wins and draws a second rounded
   box around the pill. Same reason .filter-search pairs them below. */
.rebar-url:focus,
.rebar-url:focus-visible {
  outline: none;
  background: var(--surface);
  color: var(--ink);
  /* The logo's own blue, the same token that colours "Page" in the wordmark
     a few inches to the left. No ring: the border carries the focus state on
     its own and a glow around a control this small reads as a halo rather
     than as emphasis. */
  border-color: var(--brand-600);
  width: clamp(18rem, 40vw, 32rem);
  /* Restated and not optional: the global :focus-visible rule sets
     border-radius: 4px to shape its own outline and that lands on the
     element, squaring the pill, which reads as a second box appearing
     around the field rather than as the field being focused. */
  border-radius: 999px;
}

/* Honour a reader who has asked for less motion: they still get the width,
   just without the animation between the two states. */
@media (prefers-reduced-motion: reduce) {
  .rebar-url { transition: border-color .15s, background .15s, color .15s; }
}

/* Inside the field, against its right edge. */
.rebar-go {
  position: absolute; right: .3rem; top: 50%;
  transform: translateY(-50%);
  display: grid; place-items: center;
  width: 26px; height: 26px; padding: 0;
  background: none; border: 0; border-radius: 50%;
  color: var(--ink-3); cursor: pointer;
  transition: color .15s, background .15s;
}

.rebar-go:hover { color: var(--brand); background: color-mix(in srgb, var(--brand) 10%, transparent); }
.rebar-go:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.rebar-url:focus ~ .rebar-go,
.rebar-url:focus-visible ~ .rebar-go { color: var(--brand-600); }

.rebar-go svg { width: 15px; height: 15px; fill: currentColor; }


/* Account links. Present on every page: on marketing pages they follow the
   nav, on a report they follow the controls and in both cases they are the
   last thing in the header so their position never moves. */
/* Spaced rather than ruled: the reference separates the toggle, the sample
   link and the button by distance alone, which reads as three controls where
   a divider reads as two groups. */
.site-account {
  display: flex; align-items: center; gap: 1.5rem; flex: none;
}

/* The rule between page controls and account actions, on report pages only.
   A real element in the header row rather than a border on .site-account: a
   border plus padding sits *inside* the flex gap and a ::before nested in the
   account group takes its left gap from the header and its right from that
   group, two different values, which is what made one side look wider. As a
   sibling, both gaps come from the same container.

   Marketing pages have no such divide: there the toggle, the sample link and
   the button are three controls separated by space alone. */
.header-sep {
  flex: none;
  width: 1px;
  height: 1.3rem;
  background: var(--line);
}

.account-link {
  font-size: .86rem; font-weight: 500;
  color: var(--ink-2); text-decoration: none; white-space: nowrap;
}

.account-link:hover { color: var(--ink); }

/* The header's own button scale, the page-body .btn is sized for a hero. */
/* 38px, the height .dash-form already gives its buttons, so a button in a
   card and a button under a form are the same object. Set as a height
   rather than padding because that is what the form rule does, and two ways
   of arriving at one number drift apart the first time either changes. */
.btn-sm {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  height: 38px; padding: 0 1.05rem;
  font-size: .84rem;
  border-radius: var(--radius);
}

/* A button inside a table row.
 *
 * btn-sm is a page action at 38px, which in a row of 0.84rem text reads as
 * the most important thing on the line. A row action is the least: it is
 * there for the one row somebody came to act on. */
.btn-xs {
  display: inline-flex; align-items: center; justify-content: center; gap: .3rem;
  height: 28px; padding: 0 .65rem;
  font-size: .78rem;
  border-radius: calc(var(--radius) - 2px);
}

/* The pill is for the marketing header and the topbar action, where one
   button sits alone against navigation and wants to read as the thing to
   press. Everywhere else a button sits among inputs and cards that are all
   rounded rectangles, and a pill among them is the odd shape out. */
.site-header .btn-sm,
.bare-header .btn-sm,
.topbar-actions .btn-sm {
  border-radius: 999px;
  /* A hint of lift, not a drop shadow: the button only has to sit above the
     nav beside it and a heavy one reads as floating off the header. */
  box-shadow: 0 2px 6px -2px color-mix(in srgb, var(--brand-700) 35%, transparent);
}

.btn-sm svg { width: 14px; height: 14px; flex: none; }

/* Below this the header cannot hold a logo, a URL, a labelled button and
   three icons on one line. The button keeps its icon and sheds its text,
   which is the only element here whose meaning survives the loss. */
@media (max-width: 760px) {
  /* Sign in goes before Register does: a returning user can find it from the
     Register page but a first-time visitor has no other route in. */
  .account-link { display: none; }
}

@media (max-width: 620px) {
  /* On a report the wordmark goes before the controls do: the controls are
     what the reader came to that page to use and the mark still links home.
     Other pages have fewer controls and keep the name. */
  .page-report .site-header .logo-text { display: none; }
}

@media (max-width: 560px) {
  /* At phone width the row cannot hold everything so the controls with
     equivalents elsewhere step out: the PDF link is repeated in the report
     body and the theme is a preference rather than a task. What remains is
     the URL, re-analyze and the way to sign up. */
  .site-header .theme-cycle { display: none; }
  .page-report .site-header { --hdr-gap: .35rem; }
  .rebar-url { width: clamp(8rem, 44vw, 15rem); font-size: .72rem; padding: .34rem 1.9rem .34rem .7rem; }
  /* The header only: unscoped, this shrank every small button on the site
     at phone width, leaving it smaller than the controls beside it. */
  .site-header .btn-sm { padding: .32rem .6rem; font-size: .76rem; }
}

/* Phone width, every page but a report: the full name on the left and the
   controls pushed to the right edge. The SEO tag and the name in the account
   chip step out to make the room; the avatar still says who is signed in. */
@media (max-width: 560px) {
  body:not(.page-report) .site-header .logo { margin-right: auto; }
  body:not(.page-report) .site-header .logo-tag,
  body:not(.page-report) .site-header .account-name { display: none; }
  body:not(.page-report) .site-header .wrap { gap: .6rem; }
  body:not(.page-report) .site-account { gap: .6rem; }
  body:not(.page-report) .account-chip { padding: .2rem; }
}


.icon-btn {
  display: grid; place-items: center; width: 30px; height: 30px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.icon-btn:hover { color: var(--ink); background: var(--surface-2); }
.icon-btn svg { width: 15px; height: 15px; fill: currentColor; }

/* ---- checklist ---- */

.checklist { padding: 1rem 1.5rem; border-top: 1px solid var(--line); }
.checklist-head { display: flex; justify-content: space-between; align-items: baseline; font-size: .82rem; margin-bottom: .45rem; }
.checklist-head span { font-weight: 600; }
.checklist-head b { color: var(--brand-600); font-variant-numeric: tabular-nums; }
.checklist-track { height: 7px; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.checklist-track i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--green-fill), var(--brand-500));
}
.checklist-foot {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-top: .5rem; font-size: .78rem; color: var(--ink-3);
}
.checklist-foot a { font-weight: 600; text-decoration: none; white-space: nowrap; }

/* ---- shared visual panel ---- */

/* ---- stat tiles ---- */

.stat-tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .8rem; margin-bottom: 1rem;
}
.stat-tile {
  display: flex; flex-direction: column; gap: .15rem;
  padding: .9rem 1rem; border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface-2);
}
.stat-label { font-size: .76rem; color: var(--ink-3); font-weight: 500; }
.stat-value {
  font-family: var(--display); font-size: 1.55rem; font-weight: 700;
  letter-spacing: -.025em; line-height: 1.15; color: var(--ink);
}
.stat-note { font-size: .72rem; font-weight: 600; }

/* ---- heading tree: each heading is its own card on the tinted panel ---- */

.visual-panel {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  /* Tinted ground so the white heading rows lift off it. */
  background: var(--surface-2);
}
.visual-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: .75rem .95rem .6rem;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-2);
}
.visual-meta { font-weight: 500; color: var(--ink-3); text-transform: none; letter-spacing: 0; font-size: .74rem; }

.tree { padding: 0 .85rem .85rem; display: flex; flex-direction: column; gap: .45rem; }
.tree-row {
  display: flex; align-items: center; gap: .7rem;
  padding: .5rem .7rem; min-width: 0;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  border-radius: 9px;
  /* Indented rows stop short of the right edge too so the nesting reads as
     nesting rather than as rows of different widths. */
  margin-right: .15rem;
}
.tree-tag {
  font-size: .65rem; font-weight: 700;
  padding: .14rem 0; border-radius: 5px; flex: none;
  /* Fixed width so every tag column lines up regardless of level. */
  width: 1.85rem; text-align: center;
}
.tree-h1 { background: var(--brand-100); color: var(--brand-700); }
.tree-h2 { background: var(--teal-100); color: var(--teal-text); }
.tree-h3, .tree-h4, .tree-h5, .tree-h6 { background: var(--slate-100); color: var(--ink-2); }
.tree-text {
  font-size: .77rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tree-text.empty { color: var(--red-text); font-style: italic; }

/* ---- core web vitals cards ---- */

.vital-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .7rem; }
.vital-card { border: 1px solid var(--line); border-radius: 11px; padding: .85rem .9rem; background: var(--surface-2); }
.vital-card.muted { opacity: .75; }
.vital-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: .4rem; }
.vital-abbr { font-family: var(--mono); font-size: .72rem; font-weight: 700; color: var(--ink-3); letter-spacing: .05em; }
.vital-verdict { font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; padding: .12rem .4rem; border-radius: 4px; }
.vital-verdict.none { background: var(--slate-100); color: var(--ink-3); }
.vital-value { font-family: var(--display); font-size: 1.7rem; font-weight: 800; letter-spacing: -.03em; line-height: 1.1; }
.vital-unit { font-family: var(--font); font-size: .8rem; font-weight: 400; color: var(--ink-3); margin-left: .15rem; }
.vital-track { height: 5px; border-radius: 999px; background: var(--line-2); overflow: hidden; margin: .5rem 0 .45rem; }
.vital-track i { display: block; height: 100%; border-radius: 999px; }
.vital-name { font-size: .74rem; margin: 0; color: var(--ink-2); }
.vital-threshold { font-size: .7rem; margin: .1rem 0 0; color: var(--ink-3); }

.callout {
  margin-top: .8rem; padding: .75rem .9rem;
  border: 1px solid color-mix(in srgb, var(--amber-text) 25%, transparent);
  border-left-width: 3px; border-radius: 9px;
  background: var(--amber-100); font-size: .82rem; line-height: 1.55;
}

/* ---- social card preview ---- */

.social-card { margin: .85rem; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--surface); }
.social-image {
  display: grid; place-items: center; gap: .3rem; min-height: 7.5rem; padding: 1rem;
  text-align: center; font-size: .74rem;
}
.social-image.has-image { background: var(--brand-100); color: var(--brand-700); }
.social-image.has-image code { font-family: var(--mono); font-size: .7rem; overflow-wrap: anywhere; }
.social-image-note { font-weight: 700; text-transform: uppercase; letter-spacing: .06em; font-size: .64rem; }
.social-image.missing { background: var(--slate-100); color: var(--ink-3); font-style: italic; }
.social-body { padding: .75rem .85rem; border-top: 1px solid var(--line); background: var(--surface-2); }
.social-domain { font-size: .66rem; font-weight: 700; letter-spacing: .07em; color: var(--ink-3); }
.social-title { font-size: .88rem; margin: .15rem 0 .2rem; line-height: 1.35; }
.social-desc { font-size: .78rem; color: var(--ink-2); margin: 0; line-height: 1.5; }

@media (max-width: 44rem) {
  .rebar-form input { font-size: .72rem; }
  .checklist-foot { flex-direction: column; gap: .3rem; }
}

.icon-btn.copied { background: var(--green-100); color: var(--green-text); border-color: var(--green-text); }


/* ==========================================================================
   Report, matched to the supplied reference design

   The governing rule: what a check measured is always visible. Expanding
   adds the explanation and the fix, not the evidence.
   ========================================================================== */

body.page-report { background: var(--surface-2); }

.page-report .snapshot-card,
.page-report .nav-card,
.page-report .group,
.page-report .actions,
.page-report .serp {
  border-radius: 16px;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--line) 75%, transparent);
  box-shadow: 0 1px 2px rgba(20,26,34,.04);
}

/* ---- section header: icon tile, title, subtitle, verdict badge ---- */

.page-report .group-head {
  background: var(--surface); padding: 1.15rem 1.35rem;
  border-bottom: 1px solid var(--line-2);
}
.group-icon { width: 38px; height: 38px; border-radius: 10px; }
.group-icon svg { width: 19px; height: 19px; }
.group-title h2, .group-title h3 { font-size: 1.02rem; font-weight: 700; letter-spacing: -.015em; }
.group-title p { font-size: .79rem; }
.group-grade {
  font-size: .75rem; font-weight: 600;
  padding: .3rem .7rem; border-radius: 8px; flex: none;
}
.page-report .group-body { padding: 1.35rem; display: flex; flex-direction: column; gap: .7rem; }
.group-visual { padding: 1.35rem 1.35rem 0; }

/* ---- two-column mini-card grid, for technical and security sections ---- */


/* ---- sidebar ---- */

.page-report .nav-card { padding: 1.15rem; }
.nav-title { font-size: .67rem; letter-spacing: .09em; }
.nav-link { padding: .5rem .55rem; border-radius: 9px; font-size: .83rem; }
.nav-link:hover { background: var(--surface-2); }
.nav-link.active { background: var(--brand-100); color: var(--brand-700); font-weight: 600; }
.nav-icon { width: 26px; height: 26px; border-radius: 7px; }
.nav-icon svg { width: 15px; height: 15px; }

/* ---- callouts ---- */

.callout {
  display: flex; gap: .65rem; align-items: flex-start;
  margin-top: .85rem; padding: .8rem .95rem;
  border: 1px solid color-mix(in srgb, var(--amber-text) 28%, transparent);
  border-radius: 10px; background: var(--amber-100);
  font-size: .83rem; line-height: 1.55; color: var(--ink-2);
}
.callout b { color: var(--amber-text); }

/* ---- SERP preview with favicon and breadcrumb ---- */

.serp-toggle { display: inline-flex; padding: 3px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 9px; }
.serp-toggle button {
  font: inherit; font-size: .78rem; font-weight: 500;
  padding: .3rem .8rem; border: 0; border-radius: 7px;
  background: none; color: var(--ink-3); cursor: pointer;
}
.serp-toggle button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(20,26,34,.07); }

.serp-stage { padding: 1.1rem; background: var(--surface-2); border: 1px solid var(--line-2); border-radius: 12px; }
.serp-stage-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-bottom: .75rem; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3);
}
.serp-stage-head .px { font-family: var(--mono); text-transform: none; letter-spacing: 0; font-weight: 600; }
.serp-stage-head .px b { color: var(--ink); }

/* Google's own colours, not the site's. This is a simulation of a search
   result so it has to look like one, a card themed to match the report would
   stop being a preview of what a searcher actually sees.
   Google has a dark mode of its own, though and a white card floating on a
   dark page reads as a bug rather than as fidelity. These are Google's dark
   values so the simulation stays accurate in both themes. */
.serp-result {
  --serp-bg: #fff;
  --serp-title: #1a0dab;
  --serp-text: #4d5156;
  --serp-site: #202124;
  --serp-favicon-bg: #f1f3f4;
  --serp-favicon-ink: #5f6368;

  background: var(--serp-bg); border: 1px solid var(--line); border-radius: 12px;
  padding: 1rem 1.1rem; max-width: 40rem;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .serp-result {
    --serp-bg: #202124;
    --serp-title: #8ab4f8;
    --serp-text: #bdc1c6;
    --serp-site: #e8eaed;
    --serp-favicon-bg: #303134;
    --serp-favicon-ink: #9aa0a6;
  }
}

:root[data-theme="dark"] .serp-result {
  --serp-bg: #202124;
  --serp-title: #8ab4f8;
  --serp-text: #bdc1c6;
  --serp-site: #e8eaed;
  --serp-favicon-bg: #303134;
  --serp-favicon-ink: #9aa0a6;
}

.serp-result.mobile { max-width: 24rem; }
.serp-site { display: flex; align-items: center; gap: .55rem; margin-bottom: .35rem; }
.serp-favicon {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--serp-favicon-bg); color: var(--serp-favicon-ink);
  font-size: .7rem; font-weight: 700;
  overflow: hidden;
}
.serp-favicon img { width: 16px; height: 16px; }
.serp-site-meta { min-width: 0; line-height: 1.3; }
.serp-site-name { display: block; font-size: .8rem; color: var(--serp-site); font-weight: 500; }
.serp-crumb { display: block; font-size: .74rem; color: var(--serp-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.serp-h { color: var(--serp-title); font-size: 1.22rem; line-height: 1.3; margin: .1rem 0 .25rem; font-weight: 400; font-family: Arial, sans-serif; }
.serp-result.mobile .serp-h { font-size: 1.05rem; }
.serp-d { color: var(--serp-text); font-size: .86rem; line-height: 1.58; font-family: Arial, sans-serif; margin: 0; }
.serp-cut { color: var(--amber-text); }

@media (max-width: 44rem) {
  .check-head { gap: .35rem; }
  .page-report .group-body { padding: 1rem; }
}

/* ==========================================================================
   Header card, three columns, matching the reference
   ========================================================================== */

.snapshot-top {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr) minmax(0, .72fr);
  gap: 1.5rem; padding: 1.4rem 1.5rem; align-items: stretch;
}
.snapshot-score { gap: 1.1rem; align-items: center; border-right: 1px solid var(--line-2); padding-right: 1.5rem; }

/*
 * Score dial in pure CSS. A conic-gradient paints the arc and a radial-gradient
 * punches the hole so there is no SVG, no circumference arithmetic and the
 * ring animates by transitioning a single custom property.
 */

/*
 * Rounded arc ends. conic-gradient cuts a hard edge so each end is a dot as
 * wide as the ring, centred on the track: one fixed where the arc starts at
 * twelve o'clock, the other rotated to where it stops.
 */

@media (prefers-reduced-motion: no-preference) {
  .dial-lg::after { transition: transform .8s ease-out; }
}

.dial-lg-readout { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.dial-lg-readout .n { font-family: var(--display); font-size: 2.1rem; font-weight: 800; line-height: 1; letter-spacing: -.03em; }
.dial-lg-readout .l { font-size: .62rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); margin-top: .2rem; }

.snapshot-meta { min-width: 0; }
.grade-chip {
  display: inline-block; font-size: .72rem; font-weight: 700;
  padding: .2rem .6rem; border-radius: 999px; margin-bottom: .55rem;
}
.snapshot-meta h1 { font-size: 1.1rem; margin: 0 0 .25rem; letter-spacing: -.02em; line-height: 1.3; }
.snapshot-url { font-size: .76rem; color: var(--ink-3); display: block; margin-bottom: .6rem; overflow-wrap: anywhere; }
.snapshot-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* ---- metric tiles ---- */

.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: .7rem; align-content: start; }

.tile {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .85rem .95rem; border-radius: 12px; border: 1px solid var(--line);
  background: var(--surface); text-align: left; cursor: pointer;
  font: inherit; transition: border-color .15s, background .15s;
}
.tile:hover { border-color: var(--ink-3); }
.tile[aria-pressed="true"] { border-width: 2px; padding: calc(.85rem - 1px) calc(.95rem - 1px); }
.tile-count { font-family: var(--display); font-size: 1.5rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; }

.tile-red { background: var(--red-100); border-color: color-mix(in srgb, var(--red-text) 20%, transparent); }
.tile-red .tile-label, .tile-red .tile-count { color: var(--red-text); }
.tile-amber { background: var(--amber-100); border-color: color-mix(in srgb, var(--amber-text) 20%, transparent); }
.tile-amber .tile-label, .tile-amber .tile-count { color: var(--amber-text); }
.tile-slate { background: var(--slate-100); border-color: var(--line); }
.tile-slate .tile-label, .tile-slate .tile-count { color: var(--ink-2); }
.tile-green { background: var(--green-100); border-color: color-mix(in srgb, var(--green-text) 20%, transparent); }
.tile-green .tile-label, .tile-green .tile-count { color: var(--green-text); }

/* ---- filter bar ---- */

.filter-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .85rem 1.5rem; border-top: 1px solid var(--line); background: var(--surface-2);
}
.filter-chips { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.filter-label { font-size: .74rem; font-weight: 700; color: var(--ink-3); text-transform: uppercase; letter-spacing: .06em; margin-right: .2rem; }
.chip {
  font: inherit; font-size: .8rem; font-weight: 500;
  padding: .3rem .7rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.chip b { font-variant-numeric: tabular-nums; font-weight: 700; }
.chip:hover { border-color: var(--ink-3); }
.chip[aria-pressed="true"] { background: var(--brand-600); border-color: var(--brand-600); color: var(--on-brand); }
.filter-search input {
  font: inherit; font-size: .82rem; padding: .4rem .7rem; width: 17rem; max-width: 100%;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink);
}
.filter-search input::placeholder { color: var(--ink-3); }
.filter-empty { padding: 1.5rem; text-align: center; color: var(--ink-3); font-size: .9rem; margin: 0; }

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

.report-grid { display: grid; grid-template-columns: 16.5rem minmax(0, 1fr); gap: 1.75rem; align-items: start; }
.report-main { min-width: 0; }

.report-nav { position: sticky; top: calc(4rem + env(safe-area-inset-top, 0px)); }
.nav-card { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); padding: 1rem; }
.nav-title {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); font-weight: 700; margin: 0 0 .6rem;
}
.nav-title + .specs, .nav-links + .nav-title { margin-top: 1.25rem; }
.nav-links { display: flex; flex-direction: column; gap: .15rem; margin-bottom: .25rem; }
.nav-link {
  display: flex; align-items: center; gap: .6rem;
  padding: .45rem .5rem; border-radius: 9px;
  font-size: .82rem; color: var(--ink-2); text-decoration: none;
}
.nav-link:hover { background: var(--surface-2); color: var(--ink); }
.nav-icon { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; flex: none; }
.nav-icon svg { width: 14px; height: 14px; fill: currentColor; }
.nav-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .82rem; }
.nav-badge {
  font-family: var(--mono); font-size: .68rem; font-weight: 700;
  padding: .05rem .35rem; border-radius: 5px; flex: none;
}
.nav-badge.bad { background: var(--red-100); color: var(--red-text); }
.nav-badge.warn { background: var(--amber-100); color: var(--amber-text); }
/* The dot occupies a badge's footprint so the right edge of the column is
   straight whichever marker a row carries but it is still a real circle,
   centred in that box by flex. A radial-gradient would also draw one and did
   briefly but its antialiased edge reads softer than a border-radius at this
   size. */
.nav-dot {
  flex: none;
  display: grid; place-items: center;
  /* Matches .nav-badge's min-width and its height so rows do not shift. */
  width: 1.2rem;
  height: 1.15rem;
}

.nav-dot::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--green-fill);
}


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

.group {
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); margin-bottom: 1rem; overflow: hidden;
  scroll-margin-top: 5rem;
}
.group-head {
  display: flex; align-items: center; gap: .85rem;
  padding: 1rem 1.15rem; border-bottom: 1px solid var(--line); background: var(--surface-2);
}
.group-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9px; flex: none; }
.group-icon svg { width: 18px; height: 18px; fill: currentColor; }
.group-title { flex: 1; min-width: 0; }
.group-title h2, .group-title h3 { font-size: .98rem; margin: 0 0 .1rem; letter-spacing: -.015em; }
.group-title p { font-size: .8rem; color: var(--ink-3); margin: 0; }
.group-counts { display: flex; align-items: center; gap: .4rem; flex: none; }
.group-body { padding: .7rem; }
.group-empty { padding: 1.25rem; text-align: center; color: var(--ink-3); font-size: .85rem; margin: 0; }


/* ---- actions ---- */

.actions { border-radius: 14px; border-color: var(--line); background: var(--surface); overflow: hidden; }
.action-row { padding: .9rem 1.1rem; }
.action-row:hover { background: var(--surface-2); }

@media (max-width: 62rem) {
  .report-grid { grid-template-columns: 1fr; }
  .report-nav { position: static; }
  .nav-links { flex-direction: row; flex-wrap: wrap; }
  .nav-link { flex: 1 1 12rem; }
  .snapshot-top { grid-template-columns: 1fr; }
}
@media (max-width: 40rem) {
  .snapshot-score { flex-direction: column; }
  .filter-search input { width: 100%; }
  .filter-search { width: 100%; }
}

/* ==========================================================================
   Report toolbar, checklist and section visualisations
   ========================================================================== */

/* Superseded by the header-merged .rebar rules above. */
.icon-btn {
  display: grid; place-items: center; width: 30px; height: 30px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.icon-btn:hover { color: var(--ink); background: var(--surface-2); }
.icon-btn svg { width: 15px; height: 15px; fill: currentColor; }

/* ---- checklist ---- */

.checklist { padding: 1rem 1.5rem; border-top: 1px solid var(--line); }
.checklist-head { display: flex; justify-content: space-between; align-items: baseline; font-size: .82rem; margin-bottom: .45rem; }
.checklist-head span { font-weight: 600; }
.checklist-head b { color: var(--brand-600); font-variant-numeric: tabular-nums; }
.checklist-track { height: 7px; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.checklist-track i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--green-fill), var(--brand-500));
}
.checklist-foot {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-top: .5rem; font-size: .78rem; color: var(--ink-3);
}
.checklist-foot a { font-weight: 600; text-decoration: none; white-space: nowrap; }

/* ---- shared visual panel ---- */

/* ---- stat tiles ---- */

.stat-tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .8rem; margin-bottom: 1rem;
}
.stat-tile {
  display: flex; flex-direction: column; gap: .15rem;
  padding: .9rem 1rem; border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface-2);
}
.stat-label { font-size: .76rem; color: var(--ink-3); font-weight: 500; }
.stat-value {
  font-family: var(--display); font-size: 1.55rem; font-weight: 700;
  letter-spacing: -.025em; line-height: 1.15; color: var(--ink);
}
.stat-note { font-size: .72rem; font-weight: 600; }

/* ---- heading tree: each heading is its own card on the tinted panel ---- */

.visual-panel {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  /* Tinted ground so the white heading rows lift off it. */
  background: var(--surface-2);
}
.visual-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: .75rem .95rem .6rem;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-2);
}
.visual-meta { font-weight: 500; color: var(--ink-3); text-transform: none; letter-spacing: 0; font-size: .74rem; }

.tree { padding: 0 .85rem .85rem; display: flex; flex-direction: column; gap: .45rem; }
.tree-row {
  display: flex; align-items: center; gap: .7rem;
  padding: .5rem .7rem; min-width: 0;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  border-radius: 9px;
  /* Indented rows stop short of the right edge too so the nesting reads as
     nesting rather than as rows of different widths. */
  margin-right: .15rem;
}
.tree-tag {
  font-size: .65rem; font-weight: 700;
  padding: .14rem 0; border-radius: 5px; flex: none;
  /* Fixed width so every tag column lines up regardless of level. */
  width: 1.85rem; text-align: center;
}
.tree-h1 { background: var(--brand-100); color: var(--brand-700); }
.tree-h2 { background: var(--teal-100); color: var(--teal-text); }
.tree-h3, .tree-h4, .tree-h5, .tree-h6 { background: var(--slate-100); color: var(--ink-2); }
.tree-text {
  font-size: .77rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tree-text.empty { color: var(--red-text); font-style: italic; }

/* ---- core web vitals cards ---- */

.vital-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .7rem; }
.vital-card { border: 1px solid var(--line); border-radius: 11px; padding: .85rem .9rem; background: var(--surface-2); }
.vital-card.muted { opacity: .75; }
.vital-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: .4rem; }
.vital-abbr { font-family: var(--mono); font-size: .72rem; font-weight: 700; color: var(--ink-3); letter-spacing: .05em; }
.vital-verdict { font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; padding: .12rem .4rem; border-radius: 4px; }
.vital-verdict.none { background: var(--slate-100); color: var(--ink-3); }
.vital-value { font-family: var(--display); font-size: 1.7rem; font-weight: 800; letter-spacing: -.03em; line-height: 1.1; }
.vital-unit { font-family: var(--font); font-size: .8rem; font-weight: 400; color: var(--ink-3); margin-left: .15rem; }
.vital-track { height: 5px; border-radius: 999px; background: var(--line-2); overflow: hidden; margin: .5rem 0 .45rem; }
.vital-track i { display: block; height: 100%; border-radius: 999px; }
.vital-name { font-size: .74rem; margin: 0; color: var(--ink-2); }
.vital-threshold { font-size: .7rem; margin: .1rem 0 0; color: var(--ink-3); }

.callout {
  margin-top: .8rem; padding: .75rem .9rem;
  border: 1px solid color-mix(in srgb, var(--amber-text) 25%, transparent);
  border-left-width: 3px; border-radius: 9px;
  background: var(--amber-100); font-size: .82rem; line-height: 1.55;
}

/* ---- social card preview ---- */

.social-card { margin: .85rem; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--surface); }
.social-image {
  display: grid; place-items: center; gap: .3rem; min-height: 7.5rem; padding: 1rem;
  text-align: center; font-size: .74rem;
}
.social-image.has-image { background: var(--brand-100); color: var(--brand-700); }
.social-image.has-image code { font-family: var(--mono); font-size: .7rem; overflow-wrap: anywhere; }
.social-image-note { font-weight: 700; text-transform: uppercase; letter-spacing: .06em; font-size: .64rem; }
.social-image.missing { background: var(--slate-100); color: var(--ink-3); font-style: italic; }
.social-body { padding: .75rem .85rem; border-top: 1px solid var(--line); background: var(--surface-2); }
.social-domain { font-size: .66rem; font-weight: 700; letter-spacing: .07em; color: var(--ink-3); }
.social-title { font-size: .88rem; margin: .15rem 0 .2rem; line-height: 1.35; }
.social-desc { font-size: .78rem; color: var(--ink-2); margin: 0; line-height: 1.5; }

@media (max-width: 44rem) {
  .rebar-form input { font-size: .72rem; }
  .checklist-foot { flex-direction: column; gap: .3rem; }
}

.icon-btn.copied { background: var(--green-100); color: var(--green-text); border-color: var(--green-text); }


/* ==========================================================================
   Report, matched to the supplied reference design

   The governing rule: what a check measured is always visible. Expanding
   adds the explanation and the fix, not the evidence.
   ========================================================================== */

body.page-report { background: var(--surface-2); }

.page-report .snapshot-card,
.page-report .nav-card,
.page-report .group,
.page-report .actions,
.page-report .serp {
  border-radius: 16px;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--line) 75%, transparent);
  box-shadow: 0 1px 2px rgba(20,26,34,.04);
}

/* ---- section header: icon tile, title, subtitle, verdict badge ---- */

.page-report .group-head {
  background: var(--surface); padding: 1.15rem 1.35rem;
  border-bottom: 1px solid var(--line-2);
}
.group-icon { width: 38px; height: 38px; border-radius: 10px; }
.group-icon svg { width: 19px; height: 19px; }
.group-title h2, .group-title h3 { font-size: 1.02rem; font-weight: 700; letter-spacing: -.015em; }
.group-title p { font-size: .79rem; }
.group-grade {
  font-size: .75rem; font-weight: 600;
  padding: .3rem .7rem; border-radius: 8px; flex: none;
}
.page-report .group-body { padding: 1.35rem; display: flex; flex-direction: column; gap: .7rem; }
.group-visual { padding: 1.35rem 1.35rem 0; }

/* ---- two-column mini-card grid, for technical and security sections ---- */


/* ---- sidebar ---- */

.page-report .nav-card { padding: 1.15rem; }
.nav-title { font-size: .67rem; letter-spacing: .09em; }
.nav-link { padding: .5rem .55rem; border-radius: 9px; font-size: .83rem; }
.nav-link:hover { background: var(--surface-2); }
.nav-link.active { background: var(--brand-100); color: var(--brand-700); font-weight: 600; }
.nav-icon { width: 26px; height: 26px; border-radius: 7px; }
.nav-icon svg { width: 15px; height: 15px; }

/* ---- callouts ---- */

.callout {
  display: flex; gap: .65rem; align-items: flex-start;
  margin-top: .85rem; padding: .8rem .95rem;
  border: 1px solid color-mix(in srgb, var(--amber-text) 28%, transparent);
  border-radius: 10px; background: var(--amber-100);
  font-size: .83rem; line-height: 1.55; color: var(--ink-2);
}
.callout b { color: var(--amber-text); }

/* ---- SERP preview with favicon and breadcrumb ---- */

.serp-toggle { display: inline-flex; padding: 3px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 9px; }
.serp-toggle button {
  font: inherit; font-size: .78rem; font-weight: 500;
  padding: .3rem .8rem; border: 0; border-radius: 7px;
  background: none; color: var(--ink-3); cursor: pointer;
}
.serp-toggle button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(20,26,34,.07); }

.serp-stage { padding: 1.1rem; background: var(--surface-2); border: 1px solid var(--line-2); border-radius: 12px; }
.serp-stage-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-bottom: .75rem; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3);
}
.serp-stage-head .px { font-family: var(--mono); text-transform: none; letter-spacing: 0; font-weight: 600; }
.serp-stage-head .px b { color: var(--ink); }

/* Google's own colours, not the site's. This is a simulation of a search
   result so it has to look like one, a card themed to match the report would
   stop being a preview of what a searcher actually sees.
   Google has a dark mode of its own, though and a white card floating on a
   dark page reads as a bug rather than as fidelity. These are Google's dark
   values so the simulation stays accurate in both themes. */
.serp-result {
  --serp-bg: #fff;
  --serp-title: #1a0dab;
  --serp-text: #4d5156;
  --serp-site: #202124;
  --serp-favicon-bg: #f1f3f4;
  --serp-favicon-ink: #5f6368;

  background: var(--serp-bg); border: 1px solid var(--line); border-radius: 12px;
  padding: 1rem 1.1rem; max-width: 40rem;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .serp-result {
    --serp-bg: #202124;
    --serp-title: #8ab4f8;
    --serp-text: #bdc1c6;
    --serp-site: #e8eaed;
    --serp-favicon-bg: #303134;
    --serp-favicon-ink: #9aa0a6;
  }
}

:root[data-theme="dark"] .serp-result {
  --serp-bg: #202124;
  --serp-title: #8ab4f8;
  --serp-text: #bdc1c6;
  --serp-site: #e8eaed;
  --serp-favicon-bg: #303134;
  --serp-favicon-ink: #9aa0a6;
}

.serp-result.mobile { max-width: 24rem; }
.serp-site { display: flex; align-items: center; gap: .55rem; margin-bottom: .35rem; }
.serp-favicon {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--serp-favicon-bg); color: var(--serp-favicon-ink);
  font-size: .7rem; font-weight: 700;
  overflow: hidden;
}
.serp-favicon img { width: 16px; height: 16px; }
.serp-site-meta { min-width: 0; line-height: 1.3; }
.serp-site-name { display: block; font-size: .8rem; color: var(--serp-site); font-weight: 500; }
.serp-crumb { display: block; font-size: .74rem; color: var(--serp-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.serp-h { color: var(--serp-title); font-size: 1.22rem; line-height: 1.3; margin: .1rem 0 .25rem; font-weight: 400; font-family: Arial, sans-serif; }
.serp-result.mobile .serp-h { font-size: 1.05rem; }
.serp-d { color: var(--serp-text); font-size: .86rem; line-height: 1.58; font-family: Arial, sans-serif; margin: 0; }
.serp-cut { color: var(--amber-text); }

@media (max-width: 44rem) {
  .check-head { gap: .35rem; }
  .page-report .group-body { padding: 1rem; }
}

/* ==========================================================================
   Header card, three columns, matching the reference
   ========================================================================== */

.snapshot-top {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr) minmax(0, .72fr);
  gap: 1.5rem; padding: 1.4rem 1.5rem; align-items: stretch;
}
.snapshot-score { gap: 1.1rem; align-items: center; border-right: 1px solid var(--line-2); padding-right: 1.5rem; }

/*
 * Score dial in pure CSS. A conic-gradient paints the arc and a radial-gradient
 * punches the hole so there is no SVG, no circumference arithmetic and the
 * ring animates by transitioning a single custom property.
 */

/*
 * Rounded arc ends. conic-gradient cuts a hard edge so each end is a dot the
 * width of the ring, centred on the track: one fixed at twelve o'clock where
 * the arc starts, the other rotated to where it stops.
 */

@media (prefers-reduced-motion: no-preference) {
  .dial-lg::after { transition: transform .8s ease-out; }
}

.dial-lg-readout { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.dial-lg-readout .n { font-family: var(--display); font-size: 2.1rem; font-weight: 800; line-height: 1; letter-spacing: -.03em; }
.dial-lg-readout .l { font-size: .62rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); margin-top: .2rem; }

.snapshot-meta { min-width: 0; }
.grade-chip {
  display: inline-block; font-size: .72rem; font-weight: 700;
  padding: .2rem .6rem; border-radius: 999px; margin-bottom: .55rem;
}
.snapshot-meta h1 { font-size: 1.1rem; margin: 0 0 .25rem; letter-spacing: -.02em; line-height: 1.3; }
.snapshot-url { font-size: .76rem; color: var(--ink-3); display: block; margin-bottom: .6rem; overflow-wrap: anywhere; }
.snapshot-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* ---- metric tiles ---- */

.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: .7rem; align-content: start; }

.tile {
  display: flex; flex-direction: column; gap: .1rem;
  padding: .85rem .95rem; border-radius: 12px; border: 1px solid var(--line);
  background: var(--surface); text-align: left; cursor: pointer;
  font: inherit; transition: border-color .15s, background .15s;
}
.tile:hover { border-color: var(--ink-3); }
.tile[aria-pressed="true"] { border-width: 2px; padding: calc(.85rem - 1px) calc(.95rem - 1px); }
.tile-count { font-family: var(--display); font-size: 1.5rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; }

.tile-red { background: var(--red-100); border-color: color-mix(in srgb, var(--red-text) 20%, transparent); }
.tile-red .tile-label, .tile-red .tile-count { color: var(--red-text); }
.tile-amber { background: var(--amber-100); border-color: color-mix(in srgb, var(--amber-text) 20%, transparent); }
.tile-amber .tile-label, .tile-amber .tile-count { color: var(--amber-text); }
.tile-slate { background: var(--slate-100); border-color: var(--line); }
.tile-slate .tile-label, .tile-slate .tile-count { color: var(--ink-2); }
.tile-green { background: var(--green-100); border-color: color-mix(in srgb, var(--green-text) 20%, transparent); }
.tile-green .tile-label, .tile-green .tile-count { color: var(--green-text); }

/* ---- filter bar ---- */

.filter-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .85rem 1.5rem; border-top: 1px solid var(--line); background: var(--surface-2);
}
.filter-chips { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
.filter-label { font-size: .74rem; font-weight: 700; color: var(--ink-3); text-transform: uppercase; letter-spacing: .06em; margin-right: .2rem; }
.chip {
  font: inherit; font-size: .8rem; font-weight: 500;
  padding: .3rem .7rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.chip b { font-variant-numeric: tabular-nums; font-weight: 700; }
.chip:hover { border-color: var(--ink-3); }
.chip[aria-pressed="true"] { background: var(--brand-600); border-color: var(--brand-600); color: var(--on-brand); }
.filter-search input {
  font: inherit; font-size: .82rem; padding: .4rem .7rem; width: 17rem; max-width: 100%;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink);
}
.filter-search input::placeholder { color: var(--ink-3); }
.filter-empty { padding: 1.5rem; text-align: center; color: var(--ink-3); font-size: .9rem; margin: 0; }

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

.report-grid { display: grid; grid-template-columns: 16.5rem minmax(0, 1fr); gap: 1.75rem; align-items: start; }
.report-main { min-width: 0; }

.report-nav { position: sticky; top: calc(4rem + env(safe-area-inset-top, 0px)); }
.nav-card { border: 1px solid var(--line); border-radius: 14px; background: var(--surface); padding: 1rem; }
.nav-title {
  font-size: .68rem; text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3); font-weight: 700; margin: 0 0 .6rem;
}
.nav-title + .specs, .nav-links + .nav-title { margin-top: 1.25rem; }
.nav-links { display: flex; flex-direction: column; gap: .15rem; margin-bottom: .25rem; }
.nav-link {
  display: flex; align-items: center; gap: .6rem;
  padding: .45rem .5rem; border-radius: 9px;
  font-size: .82rem; color: var(--ink-2); text-decoration: none;
}
.nav-link:hover { background: var(--surface-2); color: var(--ink); }
.nav-icon { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; flex: none; }
.nav-icon svg { width: 14px; height: 14px; fill: currentColor; }
.nav-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .82rem; }
.nav-badge {
  font-family: var(--mono); font-size: .68rem; font-weight: 700;
  padding: .05rem .35rem; border-radius: 5px; flex: none;
}
.nav-badge.bad { background: var(--red-100); color: var(--red-text); }
.nav-badge.warn { background: var(--amber-100); color: var(--amber-text); }
/* The dot occupies a badge's footprint so the right edge of the column is
   straight whichever marker a row carries but it is still a real circle,
   centred in that box by flex. A radial-gradient would also draw one and did
   briefly but its antialiased edge reads softer than a border-radius at this
   size. */
.nav-dot {
  flex: none;
  display: grid; place-items: center;
  /* Matches .nav-badge's min-width and its height so rows do not shift. */
  width: 1.2rem;
  height: 1.15rem;
}

.nav-dot::before {
  content: "";
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--green-fill);
}


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

.group {
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface); margin-bottom: 1rem; overflow: hidden;
  scroll-margin-top: 5rem;
}
.group-head {
  display: flex; align-items: center; gap: .85rem;
  padding: 1rem 1.15rem; border-bottom: 1px solid var(--line); background: var(--surface-2);
}
.group-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 9px; flex: none; }
.group-icon svg { width: 18px; height: 18px; fill: currentColor; }
.group-title { flex: 1; min-width: 0; }
.group-title h2, .group-title h3 { font-size: .98rem; margin: 0 0 .1rem; letter-spacing: -.015em; }
.group-title p { font-size: .8rem; color: var(--ink-3); margin: 0; }
.group-counts { display: flex; align-items: center; gap: .4rem; flex: none; }
.group-body { padding: .7rem; }
.group-empty { padding: 1.25rem; text-align: center; color: var(--ink-3); font-size: .85rem; margin: 0; }


/* ---- actions ---- */

.actions { border-radius: 14px; border-color: var(--line); background: var(--surface); overflow: hidden; }
.action-row { padding: .9rem 1.1rem; }
.action-row:hover { background: var(--surface-2); }

@media (max-width: 62rem) {
  .report-grid { grid-template-columns: 1fr; }
  .report-nav { position: static; }
  .nav-links { flex-direction: row; flex-wrap: wrap; }
  .nav-link { flex: 1 1 12rem; }
  .snapshot-top { grid-template-columns: 1fr; }
}
@media (max-width: 40rem) {
  .snapshot-score { flex-direction: column; }
  .filter-search input { width: 100%; }
  .filter-search { width: 100%; }
}

/* ==========================================================================
   Report toolbar, checklist and section visualisations
   ========================================================================== */

/* Superseded by the header-merged .rebar rules above. */
.icon-btn {
  display: grid; place-items: center; width: 30px; height: 30px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.icon-btn:hover { color: var(--ink); background: var(--surface-2); }
.icon-btn svg { width: 15px; height: 15px; fill: currentColor; }

/* ---- checklist ---- */

.checklist { padding: 1rem 1.5rem; border-top: 1px solid var(--line); }
.checklist-head { display: flex; justify-content: space-between; align-items: baseline; font-size: .82rem; margin-bottom: .45rem; }
.checklist-head span { font-weight: 600; }
.checklist-head b { color: var(--brand-600); font-variant-numeric: tabular-nums; }
.checklist-track { height: 7px; border-radius: 999px; background: var(--line-2); overflow: hidden; }
.checklist-track i {
  display: block; height: 100%; border-radius: 999px;
  background: linear-gradient(90deg, var(--green-fill), var(--brand-500));
}
.checklist-foot {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-top: .5rem; font-size: .78rem; color: var(--ink-3);
}
.checklist-foot a { font-weight: 600; text-decoration: none; white-space: nowrap; }

/* ---- shared visual panel ---- */

/* ---- stat tiles ---- */

.stat-tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: .8rem; margin-bottom: 1rem;
}
.stat-tile {
  display: flex; flex-direction: column; gap: .15rem;
  padding: .9rem 1rem; border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface-2);
}
.stat-label { font-size: .76rem; color: var(--ink-3); font-weight: 500; }
.stat-value {
  font-family: var(--display); font-size: 1.55rem; font-weight: 700;
  letter-spacing: -.025em; line-height: 1.15; color: var(--ink);
}
.stat-note { font-size: .72rem; font-weight: 600; }

/* ---- heading tree: each heading is its own card on the tinted panel ---- */

.visual-panel {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  /* Tinted ground so the white heading rows lift off it. */
  background: var(--surface-2);
}
.visual-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  padding: .75rem .95rem .6rem;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ink-2);
}
.visual-meta { font-weight: 500; color: var(--ink-3); text-transform: none; letter-spacing: 0; font-size: .74rem; }

.tree { padding: 0 .85rem .85rem; display: flex; flex-direction: column; gap: .45rem; }
.tree-row {
  display: flex; align-items: center; gap: .7rem;
  padding: .5rem .7rem; min-width: 0;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
  border-radius: 9px;
  /* Indented rows stop short of the right edge too so the nesting reads as
     nesting rather than as rows of different widths. */
  margin-right: .15rem;
}
.tree-tag {
  font-size: .65rem; font-weight: 700;
  padding: .14rem 0; border-radius: 5px; flex: none;
  /* Fixed width so every tag column lines up regardless of level. */
  width: 1.85rem; text-align: center;
}
.tree-h1 { background: var(--brand-100); color: var(--brand-700); }
.tree-h2 { background: var(--teal-100); color: var(--teal-text); }
.tree-h3, .tree-h4, .tree-h5, .tree-h6 { background: var(--slate-100); color: var(--ink-2); }
.tree-text {
  font-size: .77rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tree-text.empty { color: var(--red-text); font-style: italic; }

/* ---- core web vitals cards ---- */

.vital-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .7rem; }
.vital-card { border: 1px solid var(--line); border-radius: 11px; padding: .85rem .9rem; background: var(--surface-2); }
.vital-card.muted { opacity: .75; }
.vital-card-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: .4rem; }
.vital-abbr { font-family: var(--mono); font-size: .72rem; font-weight: 700; color: var(--ink-3); letter-spacing: .05em; }
.vital-verdict { font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; padding: .12rem .4rem; border-radius: 4px; }
.vital-verdict.none { background: var(--slate-100); color: var(--ink-3); }
.vital-value { font-family: var(--display); font-size: 1.7rem; font-weight: 800; letter-spacing: -.03em; line-height: 1.1; }
.vital-unit { font-family: var(--font); font-size: .8rem; font-weight: 400; color: var(--ink-3); margin-left: .15rem; }
.vital-track { height: 5px; border-radius: 999px; background: var(--line-2); overflow: hidden; margin: .5rem 0 .45rem; }
.vital-track i { display: block; height: 100%; border-radius: 999px; }
.vital-name { font-size: .74rem; margin: 0; color: var(--ink-2); }
.vital-threshold { font-size: .7rem; margin: .1rem 0 0; color: var(--ink-3); }

.callout {
  margin-top: .8rem; padding: .75rem .9rem;
  border: 1px solid color-mix(in srgb, var(--amber-text) 25%, transparent);
  border-left-width: 3px; border-radius: 9px;
  background: var(--amber-100); font-size: .82rem; line-height: 1.55;
}

/* ---- social card preview ---- */

.social-card { margin: .85rem; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--surface); }
.social-image {
  display: grid; place-items: center; gap: .3rem; min-height: 7.5rem; padding: 1rem;
  text-align: center; font-size: .74rem;
}
.social-image.has-image { background: var(--brand-100); color: var(--brand-700); }
.social-image.has-image code { font-family: var(--mono); font-size: .7rem; overflow-wrap: anywhere; }
.social-image-note { font-weight: 700; text-transform: uppercase; letter-spacing: .06em; font-size: .64rem; }
.social-image.missing { background: var(--slate-100); color: var(--ink-3); font-style: italic; }
.social-body { padding: .75rem .85rem; border-top: 1px solid var(--line); background: var(--surface-2); }
.social-domain { font-size: .66rem; font-weight: 700; letter-spacing: .07em; color: var(--ink-3); }
.social-title { font-size: .88rem; margin: .15rem 0 .2rem; line-height: 1.35; }
.social-desc { font-size: .78rem; color: var(--ink-2); margin: 0; line-height: 1.5; }

@media (max-width: 44rem) {
  .rebar-form input { font-size: .72rem; }
  .checklist-foot { flex-direction: column; gap: .3rem; }
}

.icon-btn.copied { background: var(--green-100); color: var(--green-text); border-color: var(--green-text); }


/* ==========================================================================
   Report, matched to the supplied reference design

   The governing rule: what a check measured is always visible. Expanding
   adds the explanation and the fix, not the evidence.
   ========================================================================== */

body.page-report { background: var(--surface-2); }

.page-report .snapshot-card,
.page-report .nav-card,
.page-report .group,
.page-report .actions,
.page-report .serp {
  border-radius: 16px;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--line) 75%, transparent);
  box-shadow: 0 1px 2px rgba(20,26,34,.04);
}

/* ---- section header: icon tile, title, subtitle, verdict badge ---- */

.page-report .group-head {
  background: var(--surface); padding: 1.15rem 1.35rem;
  border-bottom: 1px solid var(--line-2);
}
.group-icon { width: 38px; height: 38px; border-radius: 10px; }
.group-icon svg { width: 19px; height: 19px; }
.group-title h2, .group-title h3 { font-size: 1.02rem; font-weight: 700; letter-spacing: -.015em; }
.group-title p { font-size: .79rem; }
.group-grade {
  font-size: .75rem; font-weight: 600;
  padding: .3rem .7rem; border-radius: 8px; flex: none;
}
.page-report .group-body { padding: 1.35rem; display: flex; flex-direction: column; gap: .7rem; }
.group-visual { padding: 1.35rem 1.35rem 0; }

/* ---- two-column mini-card grid, for technical and security sections ---- */


/* ---- sidebar ---- */

.page-report .nav-card { padding: 1.15rem; }
.nav-title { font-size: .67rem; letter-spacing: .09em; }
.nav-link { padding: .5rem .55rem; border-radius: 9px; font-size: .83rem; }
.nav-link:hover { background: var(--surface-2); }
.nav-link.active { background: var(--brand-100); color: var(--brand-700); font-weight: 600; }
.nav-icon { width: 26px; height: 26px; border-radius: 7px; }
.nav-icon svg { width: 15px; height: 15px; }

/* ---- callouts ---- */

.callout {
  display: flex; gap: .65rem; align-items: flex-start;
  margin-top: .85rem; padding: .8rem .95rem;
  border: 1px solid color-mix(in srgb, var(--amber-text) 28%, transparent);
  border-radius: 10px; background: var(--amber-100);
  font-size: .83rem; line-height: 1.55; color: var(--ink-2);
}
.callout b { color: var(--amber-text); }

/* ---- SERP preview with favicon and breadcrumb ---- */

.serp-toggle { display: inline-flex; padding: 3px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 9px; }
.serp-toggle button {
  font: inherit; font-size: .78rem; font-weight: 500;
  padding: .3rem .8rem; border: 0; border-radius: 7px;
  background: none; color: var(--ink-3); cursor: pointer;
}
.serp-toggle button[aria-pressed="true"] { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(20,26,34,.07); }

.serp-stage { padding: 1.1rem; background: var(--surface-2); border: 1px solid var(--line-2); border-radius: 12px; }
.serp-stage-head {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1rem;
  margin-bottom: .75rem; font-size: .7rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3);
}
.serp-stage-head .px { font-family: var(--mono); text-transform: none; letter-spacing: 0; font-weight: 600; }
.serp-stage-head .px b { color: var(--ink); }

/* Google's own colours, not the site's. This is a simulation of a search
   result so it has to look like one, a card themed to match the report would
   stop being a preview of what a searcher actually sees.
   Google has a dark mode of its own, though and a white card floating on a
   dark page reads as a bug rather than as fidelity. These are Google's dark
   values so the simulation stays accurate in both themes. */
.serp-result {
  --serp-bg: #fff;
  --serp-title: #1a0dab;
  --serp-text: #4d5156;
  --serp-site: #202124;
  --serp-favicon-bg: #f1f3f4;
  --serp-favicon-ink: #5f6368;

  background: var(--serp-bg); border: 1px solid var(--line); border-radius: 12px;
  padding: 1rem 1.1rem; max-width: 40rem;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .serp-result {
    --serp-bg: #202124;
    --serp-title: #8ab4f8;
    --serp-text: #bdc1c6;
    --serp-site: #e8eaed;
    --serp-favicon-bg: #303134;
    --serp-favicon-ink: #9aa0a6;
  }
}

:root[data-theme="dark"] .serp-result {
  --serp-bg: #202124;
  --serp-title: #8ab4f8;
  --serp-text: #bdc1c6;
  --serp-site: #e8eaed;
  --serp-favicon-bg: #303134;
  --serp-favicon-ink: #9aa0a6;
}

.serp-result.mobile { max-width: 24rem; }
.serp-site { display: flex; align-items: center; gap: .55rem; margin-bottom: .35rem; }
.serp-favicon {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--serp-favicon-bg); color: var(--serp-favicon-ink);
  font-size: .7rem; font-weight: 700;
  overflow: hidden;
}
.serp-favicon img { width: 16px; height: 16px; }
.serp-site-meta { min-width: 0; line-height: 1.3; }
.serp-site-name { display: block; font-size: .8rem; color: var(--serp-site); font-weight: 500; }
.serp-crumb { display: block; font-size: .74rem; color: var(--serp-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.serp-h { color: var(--serp-title); font-size: 1.22rem; line-height: 1.3; margin: .1rem 0 .25rem; font-weight: 400; font-family: Arial, sans-serif; }
.serp-result.mobile .serp-h { font-size: 1.05rem; }
.serp-d { color: var(--serp-text); font-size: .86rem; line-height: 1.58; font-family: Arial, sans-serif; margin: 0; }
.serp-cut { color: var(--amber-text); }

@media (max-width: 44rem) {
  .check-head { gap: .35rem; }
  .page-report .group-body { padding: 1rem; }
}

/* ==========================================================================
   Header card, three columns, matching the reference
   ========================================================================== */

/* The score column holds the dial plus a meta block whose two action buttons
   need ~268px side by side; at 1.05fr it landed 2px short and they stacked.
   Widened to 1.25fr, taken from the tiles, four short numbers that read fine
   a little narrower. */
.snapshot-top {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, .95fr) minmax(0, .72fr);
  gap: 1.5rem; padding: 1.4rem 1.5rem; align-items: stretch;
}
.snapshot-score { gap: 1.1rem; align-items: center; border-right: 1px solid var(--line-2); padding-right: 1.5rem; }

/*
 * Score dial in pure CSS. A conic-gradient paints the arc and a radial-gradient
 * punches the hole so there is no SVG, no circumference arithmetic and the
 * ring animates by transitioning a single custom property.
 */

/*
 * Rounded arc ends. conic-gradient cuts a hard edge so each end is a dot the
 * width of the ring: one parked at twelve o'clock, one rotated to the end of
 * the arc. Sized and positioned from the same --dial-thickness as the ring so * they stay aligned if it changes.
 */
/* Inherited: --score is set on .dial-lg but read by the .dial-arc descendant. */
@property --score { syntax: '<number>'; initial-value: 0; inherits: true; }
@media (prefers-reduced-motion: no-preference) {
  .dial-lg { transition: --score .8s ease-out; }
}
.dial-lg-readout .n { font-size: 1.95rem; color: var(--ink); }
.dial-lg-readout .l { font-size: .58rem; }

.snapshot-meta h1 { font-size: 1.12rem; font-weight: 700; margin: 0 0 .2rem; }
.grade-chip { font-size: .73rem; padding: .18rem .6rem; margin-bottom: .45rem; }
.snapshot-url { font-size: .8rem; color: var(--ink-2); margin-bottom: .45rem; }
/* Two deliberate rows rather than one wrapping list: when the crawl time,
   status and duration shared a line they broke at whatever point the column
   ran out, which put the split in a different place on every report. Each row
   holds together on its own line instead. */
.snapshot-facts {
  display: flex; align-items: center; gap: .45rem; flex-wrap: nowrap;
  font-size: .7rem; color: var(--ink-3); margin-bottom: .2rem;
  white-space: nowrap;
}
.snapshot-facts:last-of-type { margin-bottom: .8rem; }
@media (max-width: 40rem) {
  .snapshot-facts { flex-wrap: wrap; white-space: normal; }
}
.snapshot-facts .dot { width: 4px; height: 4px; border-radius: 50%; background: var(--line); flex: none; }
/* The relative time restates the timestamp beside it so it sits back. */
.fact-rel { opacity: .75; }
/* Side by side: the wider dial narrowed this column enough that the two
   buttons stacked, which read as a list rather than a pair of actions. */
.snapshot-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
.snapshot-actions .btn {
  display: inline-flex; align-items: center; gap: .4rem;
  font-size: .79rem; padding: .4rem .85rem; white-space: nowrap;
}
.snapshot-actions .btn svg { width: 14px; height: 14px; flex: none; }

/* ---- tiles: thin border, tinted ground, icon top-right ---- */

.tiles { grid-template-columns: repeat(2, 1fr); gap: .65rem; align-content: center; }
.tile { gap: .3rem; padding: .8rem .9rem; border-radius: 12px; }
.tile-top { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.tile-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; opacity: .85; }
.tile-label { font-size: .75rem; font-weight: 500; }
.tile-count { font-size: 1.45rem; }

.tile-red { background: color-mix(in srgb, var(--red-100) 55%, var(--surface)); }
.tile-red .tile-top { color: var(--red-text); }
.tile-amber { background: color-mix(in srgb, var(--amber-100) 55%, var(--surface)); }
.tile-amber .tile-top { color: var(--amber-text); }
.tile-green { background: color-mix(in srgb, var(--green-100) 55%, var(--surface)); }
.tile-green .tile-top { color: var(--green-text); }
.tile-slate { background: color-mix(in srgb, var(--brand-100) 45%, var(--surface)); }
.tile-slate .tile-top { color: var(--brand-600); }
.tile-slate .tile-count { color: var(--brand-700); }

/* ---- checklist: its own panel in the third column ---- */

.checklist {
  display: flex; flex-direction: column; justify-content: center; gap: .45rem;
  padding: 1rem; border: 1px solid var(--line-2); border-radius: 12px;
  background: var(--surface-2);
}
.checklist-head { margin-bottom: 0; font-size: .79rem; }
.checklist-foot {
  margin-top: .15rem; font-size: .72rem;
  flex-direction: column; align-items: flex-start; gap: .3rem;
}
/* The panel has room so the focus line wraps in full rather than clamping. */
.checklist-foot span { line-height: 1.45; }
.checklist-foot a { font-size: .74rem; }

/* ---- filter bar ---- */

/* Compact filter bar: the chips are short labels so the row needs little
   more height than the controls themselves. Pills on a tinted ground, with
   the active one inverted to near-black rather than brand blue, the blue is
   spent on links and the score so the selected filter needs its own weight. */
.filter-bar { padding: .62rem 1rem; gap: .6rem; border-top: 1px solid var(--line-2); background: var(--surface); }
.filter-chips { gap: .3rem; }

.filter-label {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .76rem; font-weight: 500; color: var(--ink-3);
  text-transform: none; letter-spacing: 0; margin-right: .15rem;
}
.filter-funnel { width: 13px; height: 13px; flex: none; }

.chip {
  border-radius: 6px; font-size: .76rem; font-weight: 500;
  padding: .24rem .64rem; line-height: 1.5;
  /* A border a step darker than the fill so each chip reads as its own
     control against the card rather than as a floating tint. */
  border: 1px solid var(--line); background: var(--surface-2); color: var(--ink-2);
}
.chip b { font-weight: 500; color: var(--ink-3); }
.chip:hover { border-color: var(--ink-3); background: var(--line-2); }
.chip[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--surface); }
.chip[aria-pressed="true"] b { color: var(--surface); opacity: .75; }

/* The icon sits inside the field so the input is padded to clear it. */
.filter-search { position: relative; display: flex; align-items: center; }
.filter-search-icon {
  position: absolute; left: .55rem; width: 13px; height: 13px;
  color: var(--ink-3); pointer-events: none;
}
.filter-search input {
  border-radius: 6px; font-size: .76rem;
  padding: .27rem .75rem .27rem 1.75rem; width: 15rem; line-height: 1.5;
}

/* A text field is always :focus-visible (the caret needs an indicator) so the
   offset ring fired on plain mouse clicks. Focus stays visible as the border
   plus a soft inset glow, which reads as the field activating rather than as a
   detached outline around it. */
.filter-search input:focus,
.filter-search input:focus-visible {
  outline: none;
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 16%, transparent);
}

/* ---- sidebar: dot when clean, count when not ---- */



@media (max-width: 68rem) {
  .snapshot-top { grid-template-columns: 1fr 1fr; }
  .checklist { grid-column: 1 / -1; }
  .snapshot-score { border-right: 0; padding-right: 0; }
}
@media (max-width: 46rem) {
  .snapshot-top { grid-template-columns: 1fr; }
}


/* ==========================================================================
   Check cards, label, verdict, evidence

   Cards sit on a tinted ground inside a white section, which is what makes
   them read as objects rather than rows. Siblings in a row share a height so
   the grid stays even whether or not a card carries evidence.
   ========================================================================== */

.page-report .group-body.check-grid {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  /* stretch, not start: siblings match the tallest in their row. */
  align-items: stretch;
  /* A full-width card between two half-width ones would otherwise leave the
     card before it stranded alone in its row. dense lets a later half-width
     card back-fill that hole so every row stays paired. */
  grid-auto-flow: row dense;
}
/* A card carrying a table or a chart takes the whole row, see the `wide`
   class in HtmlRenderer::finding(). */
.page-report .group-body.check-grid > .check.wide { grid-column: 1 / -1; }

/* The meta tags group sits under the full-width snippet preview and holds only
   the two checks that measure it so its cards run the full width too: a lone
   half-width card beside the preview reads as a stray column. */
#group-snippet .group-body.check-grid { grid-template-columns: 1fr; }

@media (max-width: 52rem) {
  .page-report .group-body.check-grid { grid-template-columns: 1fr; }
}

.check {
  display: flex; flex-direction: column;
  border: 1px solid var(--line); border-radius: 12px;
  background: var(--surface-2);
  padding: .95rem 1.05rem;
  transition: border-color .15s, box-shadow .15s;
}
.check:hover { border-color: color-mix(in srgb, var(--ink-3) 50%, transparent); }

/* Severity tints the whole card, not just its border. */
.check.t-amber {
  border-color: color-mix(in srgb, var(--amber-fill) 45%, transparent);
  background: color-mix(in srgb, var(--amber-100) 42%, var(--surface));
}
.check.t-red {
  border-color: color-mix(in srgb, var(--red-fill) 40%, transparent);
  background: color-mix(in srgb, var(--red-100) 38%, var(--surface));
}

details.check > summary { list-style: none; cursor: pointer; display: block; }
details.check > summary::-webkit-details-marker { display: none; }

.check-top { display: flex; align-items: flex-start; gap: .55rem; margin-bottom: .18rem; }
.check-label { font-size: .85rem; font-weight: 600; color: var(--ink); flex: 1; min-width: 0; line-height: 1.4; }
.check-status { width: 17px; height: 17px; fill: none; stroke: currentColor; flex: none; margin-top: .1rem; }
.check-pill {
  font-size: .67rem; font-weight: 600;
  padding: .1rem .38rem; border-radius: 5px; flex: none;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink-3); white-space: nowrap;
}

.check-verdict { font-size: .81rem; font-weight: 400; margin: 0; line-height: 1.5; }

/* Evidence sits on white inside the tinted card so it reads as a quoted
   value rather than more prose. */
.check-mono {
  font-size: .73rem; line-height: 1.6;
  margin: .65rem 0 0; padding: .5rem .65rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 8px; color: var(--ink-2);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.check-evidence { margin-top: .65rem; }

/* A quiet third line for context that is not the verdict. */
.check-note { font-size: .74rem; color: var(--ink-3); margin: .5rem 0 0; }

.check-more {
  display: inline-flex; align-items: center; gap: .25rem;
  margin-top: auto; padding-top: .65rem;
  font-size: .73rem; font-weight: 600; color: var(--brand-600);
}
.check-more svg { width: 13px; height: 13px; fill: currentColor; transition: transform .2s; }
details.check[open] .check-more svg { transform: rotate(180deg); }

.check-expand { margin-top: .7rem; padding-top: .7rem; border-top: 1px solid var(--line); }
.check-fix-label {
  display: block; font-size: .66rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); margin-bottom: .35rem;
}
.check-expand .check-mono { margin: 0 0 .5rem; }

.copy-btn {
  display: inline-flex; align-items: center; gap: .3rem;
  font: inherit; font-size: .72rem; font-weight: 600;
  padding: .28rem .55rem; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--brand-600);
}
.copy-btn svg { width: 11px; height: 11px; fill: currentColor; }
.copy-btn:hover { border-color: var(--brand-600); }
.copy-btn.copied { background: var(--green-100); border-color: var(--green-text); color: var(--green-text); }

/* ==========================================================================
   Sidebar, compact

   The nav was built on boxed icons and roomy rows, which made the column
   taller than the checks it indexes. Bare icons in the accent hue, tighter
   rows and smaller type let the whole sidebar sit within one screen.
   ========================================================================== */

.nav-title { font-size: .65rem; letter-spacing: .08em; margin-bottom: .55rem; }
.nav-title + .specs { margin-top: 1.15rem; }

/* "Page facts" is the only .nav-title that follows the link list so it takes
   the rule separating the two halves of the sidebar. */
.nav-links + .nav-title {
  margin-top: .9rem;
  padding-top: .9rem;
  border-top: 1px solid var(--line-2);
}
.nav-links + .nav-title + .specs { margin-top: 0; }

/* The last fact has no rule under it so it needs less room beneath than the
   rows above, which are each separated by a line. */
.specs { margin-bottom: 0; }
.spec-row:last-child { padding-bottom: 0; }
.page-report .nav-card { padding: 1.15rem 1.15rem .85rem; }

/* .section carries a 5.5rem block margin for the marketing pages, where each
   section is a full-width band. The report stacks dense panels instead so it
   takes a much tighter rhythm and the first panel sits flush with the top of
   the sidebar beside it. */
/* The group icons share Group::icon() with the sidebar, which is now a set of
   stroked outlines so the header tiles need the same stroke treatment or they draw nothing. The tinted tile stays so these keep the darker -text
   hue for contrast against it. */
.group-icon svg {
  fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

.report-main > .section { margin-block: 2.5rem; }
.report-main > .section:first-child,
.report-main > :first-child { margin-top: 0; }
.report-main > .section:last-child { margin-bottom: 0; }
.nav-links { gap: .06rem; }

.nav-link { padding: .44rem .5rem; border-radius: 8px; font-size: .81rem; gap: .6rem; }

/* No tinted tile behind the glyph: the icon carries the section's hue on its
   own, which removes 26px of visual weight from every row. The icons are
   stroked outlines in the saturated -fill hue, the darker -text step was
   mixed for contrast against a tint and reads dull standing alone. */
.nav-icon { width: 18px; height: 18px; border-radius: 0; background: none !important; }
.nav-icon svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor;
  stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;
}

.nav-text { font-size: .81rem; }

.nav-badge { font-size: .65rem; padding: .07rem .36rem; border-radius: 6px; min-width: 1.2rem; }

/* Matches the badge's min-width above so a row with a dot ends at the same
   place as a row with a count. */
.nav-dot { width: 1.2rem; height: 1.15rem; }

/* The default dot is green, "everything passed". A preview has nothing to
   pass so it gets a neutral one rather than claiming a clean bill.
   Targets the drawn circle, not the box, which is transparent. */
.nav-dot.neutral::before { background: var(--line); }

/* Page facts in the sidebar: label left, value right. */
.specs { display: flex; flex-direction: column; }
.spec-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: .5rem;
  padding: .38rem 0; border-bottom: 1px solid var(--line-2); font-size: .77rem;
}
.spec-row:last-child { border-bottom: 0; }
.spec-row span { color: var(--ink-3); }
.spec-row b { font-family: var(--mono); font-weight: 600; font-size: .75rem; text-align: right; }

/* ---- restored: still referenced by markup ---- */

/* Pricing FAQ, which reuses the disclosure pattern. */
.faq .finding, .faq-home .finding { border: 1px solid var(--line); border-radius: 11px; background: var(--surface); margin-bottom: .5rem; }
.faq .finding > summary, .faq-home .finding > summary { padding: .85rem 1rem; cursor: pointer; list-style: none; font-weight: 600; font-size: .92rem; }
.faq .finding > summary::-webkit-details-marker, .faq-home .finding > summary::-webkit-details-marker { display: none; }
.faq .f-body, .faq-home .f-body { display: block; }
.faq .f-msg, .faq-home .f-msg { font-weight: 600; }
.faq .f-detail, .faq-home .f-detail { padding: 0 1rem 1rem; }
.faq .f-detail p, .faq-home .f-detail p { margin: 0; font-size: .9rem; color: var(--ink-2); }

/* Homepage feature and SERP demo blocks. */
.check-card {
  border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem;
  background: var(--surface);
  /* The prose runs to different lengths per card; a column keeps the lists
     starting level and pins each count to the bottom of its own card. */
  display: flex; flex-direction: column;
}
.check-card ul { flex: 1; }
/* Icon left, title and subtitle stacked beside it: the icon is the thing the
   eye lands on and giving it the full height of both lines makes it read as
   the card's mark rather than a bullet in front of the heading. */
.check-head { display: flex; align-items: center; gap: .7rem; margin-bottom: .85rem; }
.check-head > div { min-width: 0; }
.check-card h3 { font-size: .95rem; margin: 0 0 .1rem; }

/* The section's own icon and colour so a card is recognisable as the part of
   the report it describes rather than a separate decorative set. */
.check-icon {
  display: grid; place-items: center; flex: none;
  width: 42px; height: 42px; border-radius: 11px;
}
.check-icon svg { width: 22px; height: 22px; }
.check-icon.t-brand  { background: var(--brand-100);  color: var(--brand-700); }
.check-icon.t-teal { background: var(--teal-100,  #CCFBF1); color: var(--teal-text,  #0F766E); }
.check-icon.t-violet { background: var(--violet-100,#EDE9FE); color: var(--violet-text,#6D28D9); }
.check-icon.t-amber { background: var(--amber-100, #FEF3C7); color: var(--amber-text, #B45309); }
.check-icon.t-green { background: var(--green-100, #DCFCE7); color: var(--green-text, #15803D); }
.check-icon.t-coral { background: var(--coral-100, #FFE4E6); color: var(--coral-text, #BE123C); }
.check-card .w { font-size: .75rem; color: var(--ink-3); display: block; }

/* A sentence of why before the what: a bare list says which checks run but not
   what they are for and the reason is the part that earns the card. */
.check-card p {
  font-size: .82rem; line-height: 1.55; color: var(--ink-2);
  margin: 0 0 .85rem;
}

/* The count stands in for the rest of the list rather than the page printing
   all of it: three examples show the kind of thing, the number shows the
   depth and the full list lives on the features page. */
.check-card .more {
  display: block; margin-top: .7rem;
  font-size: .76rem; color: var(--ink-3);
}
.check-card ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .28rem; }
.check-card li { font-size: .84rem; color: var(--ink-2); position: relative; padding-left: 1.1rem; }
.check-card li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--brand-500);
}

.serp-demo { font-family: Arial, sans-serif; }
.serp-demo .u { font-size: .78rem; color: #4d5156; }
.serp-demo .t { color: #1a0dab; font-size: 19px; line-height: 1.3; margin: .15rem 0; }
.serp-demo .d { color: #4d5156; font-size: 13.5px; line-height: 1.56; }
.serp-demo .cut { border-left: 2px solid var(--amber-fill); padding-left: .5rem; margin-left: .1rem; }

/* Report tile caption and the empty-tree message. */
.tile-note { font-size: .69rem; color: var(--ink-3); }
.tree-none { padding: .85rem; margin: 0; color: var(--ink-3); font-size: .82rem; }
.f-tag { font-size: .64rem; font-weight: 700; letter-spacing: .06em; }

/* Notice tier: informational so it takes the brand blue rather than a
   severity colour, it is not a problem, just something worth knowing. */
/* Selected tile: an inset ring, not a thicker border.

   The earlier rule went to border-width: 2px with negative padding to
   compensate but that still shifted the contents, calc(.85rem - 1px) lands
   on a fractional pixel and rounds differently than the unselected value so the number and labels jumped as the filter was toggled.

   box-shadow draws inside the border box and takes up no layout space at all so selecting a tile cannot move anything. The ring is doubled: the inner
   spread reads as the thicker edge, the outer one as a soft lift. */
.tile { transition: box-shadow .15s, border-color .15s, background .15s; }

.tile[aria-pressed="true"] {
  border-width: 1px;
  padding: .8rem .9rem;
  border-color: currentColor;
  box-shadow:
    inset 0 0 0 2px currentColor,
    0 1px 3px color-mix(in srgb, var(--ink) 10%, transparent);
}

/* currentColor on the button is the tile's own accent, set by .tile-* .tile-top so each severity rings in its own hue rather than one shared colour. */
.tile-red[aria-pressed="true"]   { color: var(--red-text); }
.tile-amber[aria-pressed="true"] { color: var(--amber-text); }
.tile-green[aria-pressed="true"] { color: var(--green-text); }
.tile-brand[aria-pressed="true"],
.tile-slate[aria-pressed="true"] { color: var(--brand-600); }

.tile-brand { background: color-mix(in srgb, var(--brand-100) 45%, var(--surface)); }
.tile-brand .tile-top { color: var(--brand-600); }
.tile-brand .tile-count { color: var(--brand-700); }
.check.t-brand { border-color: color-mix(in srgb, var(--brand-500) 32%, transparent); }

/* ==========================================================================
   Evidence tables

   The table sits inside a card that is already tinted by severity so a hard
   neutral box and a grey header read as a second, competing container. Instead
   the table is open, no outer border, no fill and takes its accent from the
   card it lives in: a caps header separated by one strong rule, hairlines
   between rows and nothing around the outside.

   --ev-line / --ev-head are set per severity below; the neutral pair is the
   default so a card with no tint still renders correctly.
   ========================================================================== */

.evidence-table {
  --ev-line: var(--line);
  --ev-head: var(--ink-3);
  width: 100%; border-collapse: collapse; font-size: .84rem;
  border: 0; border-radius: 0; overflow: visible;
}

.evidence-table th {
  text-align: left; font-size: .68rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ev-head); font-weight: 700;
  padding: .3rem .65rem .45rem;
  background: none;
  /* The one strong rule in the table: it separates the header from the data
     and does the job the outer box used to do. */
  border-bottom: 1.5px solid var(--ev-line);
  white-space: nowrap;
}

.evidence-table td {
  padding: .5rem .65rem;
  border-bottom: 1px solid color-mix(in srgb, var(--ev-line) 45%, transparent);
}
.evidence-table tr:last-child td { border-bottom: 0; }

/* Each severity lends the table its own hue so the rules sit with the card's
   tint rather than cutting across it. */
.check.t-amber {
  --ev-line: color-mix(in srgb, var(--amber-fill) 42%, transparent);
  --ev-head: var(--amber-text);
}
.check.t-red {
  --ev-line: color-mix(in srgb, var(--red-fill) 38%, transparent);
  --ev-head: var(--red-text);
}
.check.t-green {
  --ev-line: color-mix(in srgb, var(--green-fill) 34%, transparent);
  --ev-head: var(--green-text);
}
.check.t-brand {
  --ev-line: color-mix(in srgb, var(--brand-500) 34%, transparent);
  --ev-head: var(--brand-600);
}

/* A long list of offending URLs scrolls in place rather than being cut to the
   first five: the count says 21 so all 21 have to be reachable. Capped at
   roughly nine rows so one bad check cannot push the rest of the report off
   the screen. */
.evidence-scroll {
  max-height: 17rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-bottom: 1px solid color-mix(in srgb, var(--ev-line, var(--line)) 45%, transparent);
}
.evidence-scroll .evidence-table thead th {
  /* The header stays put while the rows move under it. */
  position: sticky; top: 0; z-index: 1;
  background: var(--card-bg, var(--surface-2));
}

.url-table { table-layout: fixed; }
.url-table .line-no { width: 5.5rem; color: var(--ink-3); font-family: var(--mono); font-size: .76rem; white-space: nowrap; }
.url-table th.num { width: 5.5rem; }
/* The line number sits behind the occurrence index when every entry shares a
   line so it reads as a suffix rather than competing with the index. */
.url-table .line-dim { opacity: .55; font-size: .92em; }
/* How many times the same URL appeared, when duplicates were collapsed. */
/* The repeat count is what reconciles a 22-row table against a "42 found"
   pill so it has to read as data rather than as a footnote. Given a chip so
   it survives a glance down the URL column. */
.url-table .url-times {
  display: inline-block;
  font-family: var(--font); font-size: .7rem; font-weight: 700;
  color: var(--ev-head, var(--ink-2));
  background: color-mix(in srgb, var(--ev-line, var(--line)) 40%, transparent);
  border-radius: 999px;
  padding: .05rem .4rem;
  vertical-align: baseline;
  /* A flex item that must never absorb the cell's width shortage: without
     this it collapses to a few pixels and the count breaks across lines. */
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The social preview is a simulation of a real card, which renders around
   500px wide on Facebook and X. Letting it stretch the full panel width would
   show the reader a layout no network actually produces and the 1.91:1 image
   would tower over the text beneath it. */
.social-card { max-width: 32rem; margin-inline: .85rem; }

/* Five vitals as 3 + 2, ordered by what Google actually ranks on.

   The three Core Web Vitals lead in a row of three; FCP and real-user TTFB
   follow as the two supporting metrics, each taking half the width. A plain
   auto-fit packed four across and stranded the fifth alone, which read as an
   afterthought rather than a deliberate second tier.

   Six columns make both splits exact: 2 each on the first row, 3 each on the
   second. */
.page-report .vital-cards { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 1rem; }
.page-report .vital-cards > .vital-card { grid-column: span 2; }
.page-report .vital-cards > .vital-card:nth-child(n + 4) { grid-column: span 3; }

@media (max-width: 52rem) {
  .page-report .vital-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .page-report .vital-cards > .vital-card,
.page-report .vital-cards > .vital-card:nth-child(n + 4) { grid-column: span 1; }
}
@media (max-width: 34rem) {
  .page-report .vital-cards { grid-template-columns: 1fr; }
}

/* The banded scale inside a panel card sits tighter than in a check card. */
.vital-card .vital-scale { margin: .55rem 0 .5rem; }

/* The explainer follows the "How to fix" pattern: a bare link-style trigger
   with no rule above it and the border only on the content once it opens, a line under every closed card cut the panel into stripes. */
.vital-help { margin-top: auto; }
.vital-help > summary {
  list-style: none; cursor: pointer;
  padding-top: .65rem;
  font-size: .73rem; font-weight: 600; color: var(--brand-600);
  display: inline-flex; align-items: center; gap: .25rem;
}
.vital-help > summary::-webkit-details-marker { display: none; }
.vital-help > summary::after {
  content: ""; width: 5px; height: 5px; flex: none;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform .15s;
}
.vital-help[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

.vital-help-body {
  margin-top: .6rem; padding-top: .6rem;
  border-top: 1px solid var(--line);
}
.vital-help-body p { font-size: .75rem; color: var(--ink-2); margin: 0; line-height: 1.55; }
.vital-help-body p + p { margin-top: .5rem; }
.vital-help-fix b { color: var(--ink); }

/* Cards in a row share a height so the trigger sits at the bottom of each. */
.vital-card { display: flex; flex-direction: column; }

/* Scale bands: lighter than the raw -fill tokens but no less saturated.

   Mixing toward the surface lightens by washing the colour out, which made
   the bands read as disabled. These lift the lightness in OKLCH and raise the
   chroma to compensate so each band is brighter and still fully coloured, the difference between a pastel and a light, vivid tone. */
.page-report .vital-band.good { background: oklch(72% 0.17 152); }
.page-report .vital-band.ok   { background: oklch(78% 0.16 78); }
.page-report .vital-band.poor { background: oklch(66% 0.20 27); }

/* Hairline separators so the boundaries stay legible once the fills are
   softer, the band edges are the information, not the colour itself. */
.page-report .vital-scale { height: 7px; }
.page-report .vital-band + .vital-band {
  box-shadow: inset 1px 0 0 color-mix(in srgb, var(--surface) 70%, transparent);
}

/* Overall score row.

   It is the sum of the categories above, not another one of them so it is
   separated by a rule with real space above it, set in the display face at a
   larger size and given a taller bar. Distinct enough that the eye reads it
   as the total without a heading saying so. */
.cat-row.cat-total {
  margin-top: .75rem;
  padding-top: 1rem;
  border-top: 2px solid var(--line);
  border-bottom: 0;
  align-items: center;
}
/* The row before the total already ends the list so its own bottom rule would
   stack with the total's top rule and read as a double border. */
.cat-row:has(+ .cat-total) { border-bottom: 0; }

/* ==========================================================================
   Summary panels

   "What to fix first" and "Score breakdown" sat bare on the page ground while
   every other section is a white card so the report looked like it stopped
   being designed near the end. They take the same card as the groups.

   Deliberately no grade tint: these rows are already coloured per category, 92 green beside 40 red and a panel-wide wash in the overall grade's hue
   would flatten the contrast the rows exist to show.
   ========================================================================== */

/* Both summary sections are built from .group, exactly like the diagnostics
   panels so the card, the icon tile, the title block and the rule under the
   header all come from the same rules, nothing to keep in sync here. Only
   the inner lists need their page-level margins removed. */
.group-body > .categories { margin: 0; }

/* Inside a group the action list is the section's content, not a panel within
   it: its own border and radius drew a second box inside the card. The rows
   run edge to edge instead, separated only by their own rules, the same way
   the score rows do. */
.group-body > .actions {
  margin: 0;
  border: 0;
  border-radius: 0;
  background: none;
}
.group-body > .actions > .action-row:first-child { border-top: 0; }
.group-body > .actions > .action-row:last-child { border-bottom: 0; }

/* The list is the whole body so it takes the body's padding itself and its
   row rules run the full width of the card, matching the rule under the
   group header above them. */
#priorities .group-body { padding: 0; }
#priorities .action-row { padding-inline: 1.15rem; }

/* The summary links carry a figure, not a severity, how many actions are
   ranked and the overall score so their badge is neutral rather than
   borrowing the red/amber that means "this many problems". */
.nav-badge.neutral { background: var(--surface-2); color: var(--ink-2); }
.cat-total .cat-name {
  font-family: var(--display);
  font-size: 1.02rem; font-weight: 800; letter-spacing: -.02em;
}
.cat-total .cat-name small {
  font-family: var(--font);
  font-weight: 500; font-size: .76rem; margin-top: .15rem;
}
.cat-total .cat-bar { height: 12px; }
.cat-total .cat-score {
  font-family: var(--display);
  font-size: 1.45rem; font-weight: 800; letter-spacing: -.03em;
}
/* URLs are long and unbreakable: clip with an ellipsis and keep the full value
   in the title attribute rather than letting one path widen the whole table. */
.url-table .url-cell {
  /* Under table-layout:fixed the line column is pinned at 5.5rem so this
     one takes the remainder. max-width:0 stops a long path from widening
     the table; width:100% is what makes it claim the space that is left.
     The cell itself stays display:table-cell, making it a flex container
     removes it from the table layout and the column then collapses to its
     content instead of filling the row. */
  width: 100%;
  max-width: 0;
  font-size: .78rem;
}

/* The flex row lives one level in so the URL truncates while the repeat
   badge keeps its natural width. */
.url-table .url-cell .url-row {
  display: flex; align-items: baseline; gap: .5rem;
}

.url-table .url-cell .url-text {
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The sticky header needs the card's own background to hide rows behind it. */
.check.t-red { --card-bg: color-mix(in srgb, var(--red-100) 38%, var(--surface)); }
.check.t-amber { --card-bg: color-mix(in srgb, var(--amber-100) 42%, var(--surface)); }

/* Caption-to-table binding.

   Removing the table's outer box left each caption floating between the table
   above it and the one below so "Requests by type" read as a trailing label
   rather than the heading for the table under it. Tightening the caption's
   own gap and widening the space between blocks restores the grouping by
   proximity alone, no box needed. */
.check-evidence .evidence-block + .evidence-block { margin-top: 1.4rem; }
.check-evidence .evidence-block { margin-bottom: 0; }
.check-evidence .evidence-block h4 {
  margin: 0 0 .25rem;
  color: var(--ev-head, var(--ink-3));
}

/* ==========================================================================
   Score dial, SVG ring

   An SVG arc rather than a conic-gradient: stroke-linecap: round rounds BOTH
   ends of the arc by construction. The gradient version could only fake the
   caps with two positioned dots, which left the far end square.

   The arc is drawn from 12 o'clock: the circumference is 2*pi*42 = 263.89 so stroke-dasharray runs (score% of 263.89) on, the rest off.
   ========================================================================== */

.dial-lg {
  position: relative; width: 124px; height: 124px; flex: none;
}

.dial-lg-ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }

.dial-track,
.dial-arc {
  fill: none;
  stroke-width: 8;
  stroke-linecap: round;
}

.dial-track { stroke: var(--line-2); }

.dial-arc {
  stroke: var(--dial);
  stroke-dasharray: calc(var(--score) * 263.894 / 100) 263.894;
}

@media (prefers-reduced-motion: no-preference) {
  .dial-lg { transition: --score .8s ease-out; }
}

.dial-lg-readout { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; }
.dial-lg-readout .n { font-family: var(--display); font-size: 1.95rem; font-weight: 800; line-height: 1; letter-spacing: -.03em; color: var(--ink); }
.dial-lg-readout .l { font-size: .58rem; text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3); margin-top: .2rem; }

/* ---------------------------------------------------------------------------
   Real-visit distribution, under each vital's banded scale.

   The marker on the scale above answers "where did the 75th-percentile visitor
   land"; this answers "and how did everyone else do". A page can sit inside the
   target while a tenth of its audience has a poor time and only this shows it.
   --------------------------------------------------------------------------- */
.vital-dist { margin: .6rem 0 0; }

.vital-dist-bar {
  display: flex;
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
  /* A 2px surface gap keeps adjacent bands from reading as one block. */
  gap: 2px;
  background: var(--surface);
}

.vital-dist-bar i { display: block; height: 100%; }
.vital-dist-bar i.good { background: var(--green-fill); }
.vital-dist-bar i.ok   { background: var(--amber-fill); }
.vital-dist-bar i.poor { background: var(--red-fill); }

/* A zero-width band would otherwise collapse its 2px gap into the neighbour. */
.vital-dist-bar i[style*="width:0.00%"] { display: none; }

.vital-dist-label {
  display: block;
  margin-top: .35rem;
  font-size: .7rem;
  color: var(--ink-3);
}

.vital-dist-label b { color: var(--ink-2); font-weight: 600; }

/* Measured-delivery evidence tables: file paths need to wrap rather than
   stretch the table past the card. */
.evidence-table .url-cell {
  max-width: 24rem;
  word-break: break-all;
  line-height: 1.45;
}

/* The markup's own count, beside the measured one. Recessive: the loaded
   figure is the measurement, this is the comparison that explains it. */
.req-delta {
  display: block;
  font-size: .68rem;
  font-weight: 400;
  color: var(--ink-3);
  margin-top: .1rem;
}

/* ==========================================================================
   Rendered-page screenshots, the one artefact in the speed section that is
   an image rather than a number so it sits above the vitals cards it
   explains.
   ========================================================================== */

/* The screenshot panel introduces the vitals cards it explains so it owns
   the gap between them. Scoped to this pairing rather than set on every
   .visual-panel, which sits alone in the other sections. */
.shot-panel + .vital-cards { margin-top: 1.1rem; }

.shot-grid {
  /* Both devices are driven from one screen height rather than from their own
     widths: sizing by column width lets the portrait aspect run away and the
     phone ends up towering over the monitor with the captions out of line.
     Each device takes the width its own aspect needs at this height. */
  --screen-h: 19rem;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 2rem;
  padding: 1.25rem;
}

@media (max-width: 620px) {
  .shot-grid { flex-direction: column; align-items: center; gap: 1.75rem; --screen-h: 16rem; }
}

.shot { margin: 0; display: flex; flex-direction: column; align-items: center; gap: .7rem; min-width: 0; }

/* The device is a hairline outline and a radius, nothing more. Proportion is
   what identifies a phone against a monitor, bezels, speakers and traffic
   lights would be ornament competing with the screenshot inside. */
.device {
  border: 1px solid var(--line);
  background: var(--surface);
  overflow: hidden;
}

.device-screen {
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-2);
}

.device-screen img {
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
  display: block;
}

.device.phone {
  border-radius: 1.1rem;
  padding: .3rem;
}

.device.phone .device-screen {
  height: var(--screen-h);
  aspect-ratio: 9 / 19.5;
  border-radius: .85rem;
  align-items: flex-start;
}

.device.monitor {
  border-radius: .5rem;
  padding: .3rem;
  /* The monitor may give back width on a narrow panel; the phone never does,
     because a squeezed portrait screen stops looking like a phone. */
  min-width: 0;
}

.device.monitor .device-screen {
  height: var(--screen-h);
  aspect-ratio: 16 / 10;
  max-width: 100%;
  border-radius: .25rem;
}

/* The empty slot is drawn as an absence: the outline shows which device is
   missing, while the hatch keeps it from reading as a panel still loading. */
.device.empty .device-screen {
  background: repeating-linear-gradient(
    45deg,
    var(--surface) 0 9px,
    color-mix(in srgb, var(--line) 22%, var(--surface)) 9px 18px
  );
}

.shot-empty {
  font-size: .76rem; font-weight: 600;
  color: var(--ink-3);
  background: var(--surface);
  padding: .3rem .6rem;
  border-radius: 999px;
}

.shot figcaption {
  font-size: .76rem;
  color: var(--ink-3);
  line-height: 1.45;
  text-align: center;
}

.shot figcaption b { color: var(--ink-2); font-weight: 700; }

/* ==========================================================================
   The waiting page
   ==========================================================================
   Shown while a worker analyses a URL. Its job is to make an unavoidable wait
   legible: what is happening, roughly how far along and that the page will
   move on by itself.
   ========================================================================== */

/* The waiting page fills the viewport so the footer sits below the fold:
   this is a page you watch and marketing links under it invite leaving
   before the thing you asked for has arrived. */
/* ==========================================================================
   The waiting page
   ==========================================================================
   Fills the viewport so the footer sits below the fold: this is a page you
   watch and marketing links under it invite leaving before the thing you
   asked for has arrived.
   ========================================================================== */

.page-wait main { min-height: calc(100vh - 3.6rem); display: flex; }

.wait-shell {
  flex: 1;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center;
  gap: 1.5rem;
  padding: 2.5rem 1.25rem 3rem;
  min-width: 0;
}

/* ---- the card ---- */

.wait-card {
  width: min(36rem, 100%);
  box-sizing: border-box;
  padding: 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 12px 40px -24px rgb(0 0 0 / .35);
  text-align: center;
}

.wait-card h1 {
  font-size: 1.25rem;
  letter-spacing: -.02em;
  margin: 0 0 .7rem;
}

.wait-url {
  display: block;
  width: 100%;
  box-sizing: border-box;
  margin: 0 0 1.3rem;
  padding: .55rem .75rem;
  font-size: .75rem;
  line-height: 1.55;
  text-align: left;
  color: var(--ink-2);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 10px;
  /* break-word, not break-all: breaks at the slashes and dashes a URL already
     provides and only splits a token when it genuinely cannot fit. */
  overflow-wrap: break-word;
  word-break: normal;
}

.wait-scheme { color: var(--ink-3); }
.wait-rest { color: var(--ink); }

/* ---- the progress list ---- */

.wait-steps {
  list-style: none;
  margin: 0 0 1.4rem;
  padding: 0;
  text-align: left;
  display: flex; flex-direction: column; gap: .25rem;
}

.wait-steps li {
  display: flex; align-items: center; gap: .75rem;
  padding: .5rem .65rem;
  border-radius: 10px;
  font-size: .88rem;
  color: var(--ink-3);
  transition: background .25s, color .25s;
}

.wait-icon {
  flex: none;
  display: grid; place-items: center;
  width: 28px; height: 28px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--ink-3);
  transition: background .25s, border-color .25s, color .25s;
}

.wait-icon svg { width: 14px; height: 14px; }
.wait-label { flex: 1; min-width: 0; }

/* The progress marker. Three states share one slot so the row never reflows
   as they change: a hollow ring waiting, a spinner while the stage runs, a
   tick once it is done. Matches the dialog, which shows the same four stages. */
.wait-state {
  flex: none;
  position: relative;
  display: grid; place-items: center;
  width: 16px; height: 16px;
}

.wait-state::before {
  content: '';
  width: 9px; height: 9px;
  border-radius: 50%;
  border: 1.5px solid var(--line);
  transition: border-color .25s;
}

.wait-spin, .wait-check {
  position: absolute; inset: 0;
  margin: auto;
  opacity: 0;
  transition: opacity .25s;
}

.wait-spin {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--brand-600) 22%, transparent);
  border-top-color: var(--brand-600);
}

.wait-check {
  width: 13px; height: 13px;
  color: var(--green-text, #15803D);
  transform: scale(.6);
  transition: opacity .25s, transform .25s;
}

.wait-steps li.active { color: var(--ink); background: var(--surface-2); }

.wait-steps li.active .wait-icon {
  border-color: var(--brand-600);
  color: var(--brand-600);
  background: var(--brand-100);
  animation: wait-breathe 2s ease-in-out infinite;
}

@keyframes wait-breathe {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand-600) 30%, transparent); }
  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--brand-600) 0%, transparent); }
}

.wait-steps li.done { color: var(--ink-2); }

.wait-steps li.done .wait-icon {
  border-color: color-mix(in srgb, var(--green-text, #15803D) 35%, var(--line));
  color: var(--green-text, #15803D);
  background: var(--surface);
  animation: none;
}

.wait-steps li.active .wait-state::before { border-color: transparent; }
.wait-steps li.active .wait-spin { opacity: 1; animation: wait-spin .7s linear infinite; }

.wait-steps li.done .wait-state::before { border-color: transparent; }
.wait-steps li.done .wait-check { opacity: 1; transform: scale(1); }
/* A finished step never spins, whatever else it is marked as. */
.wait-steps li.done .wait-spin { opacity: 0; animation: none; }

@keyframes wait-spin { to { transform: rotate(360deg); } }

/* ---- the dots ---- */


.wait-note {
  margin: 0;
  font-size: .8rem; color: var(--ink-3);
  text-wrap: balance;
}

/* ---- what the report will cover ---- */

/* Matches the card above it so the chips wrap on the same measure the
   dialog uses and the two surfaces read as one component. */
.wait-groups-panel { width: min(36rem, 100%); text-align: center; padding: 0 1rem; box-sizing: border-box; }

.wait-groups-title {
  margin: 0 0 .7rem;
  font-size: .64rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3);
}

/* Chips wrapping in a centred row rather than a list: this is a glance, not
   something to read down, it sets expectations while the card holds focus. */
.wait-groups {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .25rem;
}

.wait-groups li {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .26rem .5rem .26rem .3rem;
  font-size: .72rem; color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
}

.wait-group-icon {
  display: grid; place-items: center;
  width: 18px; height: 18px;
  border-radius: 50%;
}

.wait-group-icon svg { width: 11px; height: 11px; }

/* Each section keeps the colour it has in the report so the chips are a
   preview of that page rather than a separate decorative palette. */
.wait-group-icon.t-brand  { background: var(--brand-100);  color: var(--brand-700); }
.wait-group-icon.t-teal { background: var(--teal-100,  #CCFBF1); color: var(--teal-text,  #0F766E); }
.wait-group-icon.t-violet { background: var(--violet-100,#EDE9FE); color: var(--violet-text,#6D28D9); }
.wait-group-icon.t-amber { background: var(--amber-100, #FEF3C7); color: var(--amber-text, #B45309); }
.wait-group-icon.t-green { background: var(--green-100, #DCFCE7); color: var(--green-text, #15803D); }
.wait-group-icon.t-coral { background: var(--coral-100, #FFE4E6); color: var(--coral-text, #BE123C); }

/* The spinner stops on failure: a stage still turning would say the analysis
   is continuing when it is not. */
.page-wait.failed .wait-spin { opacity: 0; animation: none; }
.page-wait.failed .wait-note { color: var(--red-text, #B91C1C); font-weight: 500; }
/* The promise is withdrawn when the analysis failed: there is no report
   coming so advertising its sections would be a lie on screen. */
.page-wait.failed .wait-groups-panel { display: none; }

/* Motion is the one decorative thing here. The states still change so a
   reader who asked for less motion still sees which step is running. */
@media (prefers-reduced-motion: reduce) {
  .wait-steps li.active .wait-icon { animation: none; }
  /* The spinner holds still rather than vanishing: it is what marks the
     running stage so it has to stay visible without turning. */
  .wait-spin { animation: none; }
  .wait-state, .wait-spin, .wait-check { transition: none; }
}

@media (max-width: 560px) {
  .wait-card { padding: 1rem; }
  .wait-groups li { font-size: .72rem; }
}

/* ==========================================================================
   Analysis progress dialog
   ==========================================================================
   Opens over whatever page the search was made from. A modal rather than a
   separate waiting page: the search lives in five places across the site and navigating away to watch means losing the page the reader was on.
   ========================================================================== */

/* The hidden attribute has to win. A browser's own [hidden] rule is a plain
   attribute selector so any class setting `display` outranks it and this
   dialog ships in the markup on every page so losing that fight means it
   appears over the homepage on load, empty. */
.pg-backdrop[hidden] { display: none; }

.pg-backdrop {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: 1.25rem;
  background: color-mix(in srgb, var(--ink) 45%, transparent);
  backdrop-filter: blur(3px);
  animation: pg-fade .18s ease;
}

@keyframes pg-fade { from { opacity: 0; } }

.pg-dialog {
  width: min(36rem, 100%);
  box-sizing: border-box;
  padding: 1rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 16px;
  box-shadow: 0 24px 60px -20px rgb(0 0 0 / .45);
  text-align: center;
  animation: pg-rise .22s cubic-bezier(.2, .8, .2, 1);
}

@keyframes pg-rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }

.pg-head h2 {
  font-size: 1.25rem; letter-spacing: -.02em;
  margin: 0 0 .7rem;
}


.pg-url {
  margin: 0 0 1.3rem;
  padding: .55rem .75rem;
  font-size: .75rem; line-height: 1.5;
  text-align: left;
  color: var(--ink-2);
  /* A wash of the brand rather than a neutral well: this is the one piece of
     the reader's own input in the dialog and the tint marks it as theirs. */
  background: color-mix(in srgb, var(--brand-500) 7%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--brand-500) 22%, var(--line));
  border-radius: 9px;
  /* break-word, not break-all: breaks at the slashes a URL already provides
     and only splits a token when it genuinely cannot fit. */
  overflow-wrap: break-word; word-break: normal;
}

/* ---- the stages ---- */

/* [hidden] has to win over the display below it. The UA rule is
   display:none, and any display on the element itself overrides it, which is
   how the stages carried on showing behind a queue position. */
.pg-steps[hidden], .pg-queue[hidden] { display: none !important; }

.pg-steps {
  list-style: none; margin: 0 0 1.1rem; padding: 0;
  text-align: left;
  display: flex; flex-direction: column; gap: .2rem;
}

.pg-steps li {
  display: flex; align-items: center; gap: .75rem;
  padding: .5rem .65rem;
  border-radius: 10px;
  color: var(--ink-3);
  transition: background .25s, color .25s;
}

.pg-icon {
  flex: none;
  display: grid; place-items: center;
  width: 28px; height: 28px;
  border-radius: 8px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  color: var(--ink-3);
  transition: background .25s, border-color .25s, color .25s;
}

.pg-icon svg { width: 14px; height: 14px; }

.pg-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.pg-label { font-size: .86rem; line-height: 1.35; }

/* The sections this stage produces. Held at zero height until the stage is
   running: four stages match where the time actually goes and naming the
   report sections only while they are being checked keeps the dialog from
   becoming a wall of text the reader has to parse at a glance. */
.pg-detail {
  font-size: .72rem;
  line-height: 1.45;
  color: var(--ink-3);
  max-height: 0;
  opacity: 0;
  overflow: hidden;
  transition: max-height .3s ease, opacity .25s ease;
}

.pg-steps li.active .pg-detail { max-height: 3rem; opacity: 1; }

/* The tick holds its slot from the start, invisible so a stage completing
   never changes the row's width and nudges the label. */
/* The progress marker. Three states share one slot so the row never reflows
   as they change: a hollow ring waiting, a spinner while the stage runs, a
   tick once it is done. The spinner replaces the head's three dots, progress
   belongs against the stage it describes rather than above the whole list. */
.pg-state {
  flex: none;
  position: relative;
  display: grid; place-items: center;
  width: 16px; height: 16px;
}

/* Pending: a hollow ring. Drawn on the element itself so the slot is never
   empty and the column always reads as a list of stages. */
.pg-state::before {
  content: '';
  width: 9px; height: 9px;
  border-radius: 50%;
  border: 1.5px solid var(--line);
  transition: border-color .25s;
}

.pg-spin, .pg-check {
  position: absolute; inset: 0;
  margin: auto;
  opacity: 0;
  transition: opacity .25s;
}

/* Running: an arc turning in the brand colour. */
.pg-spin {
  width: 14px; height: 14px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--brand-600) 22%, transparent);
  border-top-color: var(--brand-600);
}

.pg-check {
  width: 13px; height: 13px;
  color: var(--green-text);
  transform: scale(.6);
  transition: opacity .25s, transform .25s;
}

.pg-steps li.active { color: var(--ink); background: var(--surface-2); }

.pg-steps li.active .pg-icon {
  border-color: var(--brand-600);
  color: var(--brand-600);
  background: var(--brand-100);
  animation: pg-breathe 2s ease-in-out infinite;
}

@keyframes pg-breathe {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--brand-600) 30%, transparent); }
  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--brand-600) 0%, transparent); }
}

.pg-steps li.done { color: var(--ink-2); }

.pg-steps li.done .pg-icon {
  border-color: color-mix(in srgb, var(--green-text) 35%, var(--line));
  color: var(--green-text);
  background: var(--surface);
  animation: none;
}

/* Running: the ring gives way to the spinner. */
.pg-steps li.active .pg-state::before { border-color: transparent; }
.pg-steps li.active .pg-spin { opacity: 1; animation: pg-spin .7s linear infinite; }

/* Done: the tick takes the slot. */
.pg-steps li.done .pg-state::before { border-color: transparent; }
.pg-steps li.done .pg-check { opacity: 1; transform: scale(1); }

@keyframes pg-spin { to { transform: rotate(360deg); } }

.pg-note { margin: 0 0 1rem; font-size: .8rem; color: var(--ink-3); }

/* What the report will cover. Separated by a rule rather than a card: the
   dialog is already a raised surface and a panel inside a panel would add a
   second edge for no extra meaning. */
.pg-groups-panel {
  margin: 0 0 1.15rem;
  padding-top: 1rem;
  border-top: 1px solid var(--line);
}

.pg-groups-title {
  margin: 0 0 .7rem;
  font-size: .64rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--ink-3);
}

/* Chips wrapping in a centred row rather than a list: this is a glance, not
   something to read down, it sets expectations while the analysis runs. */
.pg-groups {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; justify-content: center; gap: .25rem;
}

.pg-groups li {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .26rem .5rem .26rem .3rem;
  font-size: .72rem; color: var(--ink-2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
}

.pg-group-icon {
  display: grid; place-items: center;
  width: 18px; height: 18px;
  border-radius: 50%;
}

.pg-group-icon svg { width: 11px; height: 11px; }

/* Each section keeps the colour it has in the report so the chips preview
   that page rather than introducing a separate decorative palette. */
.pg-group-icon.t-brand  { background: var(--brand-100);  color: var(--brand-700); }
.pg-group-icon.t-teal { background: var(--teal-100,  #CCFBF1); color: var(--teal-text,  #0F766E); }
.pg-group-icon.t-violet { background: var(--violet-100,#EDE9FE); color: var(--violet-text,#6D28D9); }
.pg-group-icon.t-amber { background: var(--amber-100, #FEF3C7); color: var(--amber-text, #B45309); }
.pg-group-icon.t-green { background: var(--green-100, #DCFCE7); color: var(--green-text, #15803D); }
.pg-group-icon.t-coral { background: var(--coral-100, #FFE4E6); color: var(--coral-text, #BE123C); }

/* The promise is withdrawn when the analysis failed: there is no report
   coming so listing what it would have covered reads as a taunt. */
.pg-backdrop.failed .pg-groups-panel { display: none; }

.pg-cancel {
  font: inherit; font-size: .8rem; font-weight: 500;
  padding: .4rem .9rem;
  color: var(--ink-3);
  background: none;
  border: 1px solid var(--line);
  border-radius: 999px;
  cursor: pointer;
  transition: color .15s, border-color .15s;
}

.pg-cancel:hover { color: var(--ink); border-color: var(--ink-3); }

/* The dialog takes focus when it opens so a keyboard reader is not left
   outside a modal. It is a container, not a control so it shows no ring:
   the outline would frame the whole panel and say nothing. */
.pg-dialog:focus { outline: none; }

/* Close keeps a ring but only for the keyboard. Restated rather than left to
   the global rule because that one squares off a pill. */
.pg-cancel:focus-visible {
  outline: none;
  color: var(--ink);
  border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 22%, transparent);
}

/* Sits under the button because it explains what the button does and a
   reader only looks for that once they are considering pressing it. */
.pg-keep {
  margin: .7rem 0 0;
  font-size: .72rem;
  line-height: 1.45;
  color: var(--ink-3);
  text-wrap: balance;
}

/* Nothing is still coming after a failure so the reassurance would be a
   lie, the error message in .pg-note is the whole story there. */
.pg-backdrop.failed .pg-keep { display: none; }

/* The spinner stops on failure: a stage still turning would say the analysis
   is continuing when it is not. */
.pg-backdrop.failed .pg-spin { opacity: 0; animation: none; }
.pg-backdrop.failed .pg-note { color: var(--red-text); font-weight: 500; }

/* Motion is the decorative part. The states still change so a reader who
   asked for less motion still sees which stage is running. */
@media (prefers-reduced-motion: reduce) {
  .pg-backdrop, .pg-dialog { animation: none; }
  .pg-steps li.active .pg-icon { animation: none; }
  /* The spinner holds still rather than vanishing: it is what marks the
     running stage so it has to stay visible without turning. */
  .pg-spin { animation: none; }
  .pg-detail, .pg-state, .pg-spin, .pg-check { transition: none; }
}

@media (max-width: 520px) {
  .pg-dialog { padding: 1.5rem 1.1rem 1.2rem; }
}

/* ==========================================================================
   FAQ and support pages
   ========================================================================== */

/* The FAQ reuses the report's own disclosure styling so an answer opens the
   same way a finding does. These are the few things it adds. */
.faq h2 {
  font-size: 1rem; margin: 2rem 0 .8rem;
  letter-spacing: -.01em;
}
.faq h2:first-child { margin-top: 0; }

.faq-more {
  margin-top: 2rem; padding: 1rem 1.15rem;
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 12px;
  font-size: .92rem; color: var(--ink-2);
}
.faq-more p { margin: 0; }

.support-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1rem;
}

.support-card {
  padding: 1.25rem 1.35rem;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 14px;
}

.support-card h2 { font-size: 1rem; margin: 0 0 .5rem; }
.support-card p { font-size: .92rem; color: var(--ink-2); margin: 0 0 .7rem; line-height: 1.6; }

/* The address is the point of the card so it is set larger than the prose
   explaining when to use it. */
.support-key { font-size: 1.02rem !important; font-weight: 600; margin-bottom: .4rem !important; }
.support-key a { color: var(--brand-600); text-decoration: none; }
.support-key a:hover { text-decoration: underline; }

.support-note { font-size: .84rem !important; color: var(--ink-3) !important; margin: 0 !important; }

.support-links { margin-top: 2rem; }
.support-links h2 { font-size: 1rem; margin: 0 0 .6rem; }
.support-links p { color: var(--ink-2); margin: 0 0 .7rem; }
.support-links ul { margin: 0; padding-left: 1.1rem; color: var(--ink-2); line-height: 1.7; }
.support-links li { margin-bottom: .4rem; }

/* A feature that is sold but not yet built. Quiet rather than alarming: it is
   a date, not a warning, and it should not compete with the tier's own price. */
.soon-tag {
  display: inline-block; margin-left: .35rem; padding: .05rem .4rem;
  font-size: .64rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-3); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 999px;
  vertical-align: .08em;
}

/* The pricing page's closing box.
 *
 * Same dark panel as the homepage CTA, but the right column holds two links
 * rather than a signup form: the tiers above already ask for the signup, and
 * asking again at the foot of the page reads as pressure rather than help.
 */
.cta-actions {
  display: flex; flex-direction: column; gap: .7rem;
  padding: 1.5rem;
  background: rgba(255,255,255,.08);
  border: 1px solid rgba(255,255,255,.14);
  border-radius: 16px;
}

.cta-primary {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  width: 100%; box-sizing: border-box;
}
.cta-primary svg { width: 17px; height: 17px; flex: none; }

/* A ghost button on a dark ground needs its own colours: the light-theme
   version borrows the page background and disappears here. */
.btn.ghost.cta-secondary {
  display: inline-flex; align-items: center; justify-content: center;
  width: 100%; box-sizing: border-box;
  background: transparent; color: #fff;
  border: 1px solid rgba(255,255,255,.28);
}
.btn.ghost.cta-secondary:hover { background: rgba(255,255,255,.1); border-color: rgba(255,255,255,.4); }

.cta-actions-note {
  margin: .3rem 0 0; font-size: .78rem; line-height: 1.55;
  color: rgba(255,255,255,.72);
}

/* What actually leaves the account, under the headline rate. The monthly
   figure is what people compare; this is what they are charged.
   flex-basis: 100% puts it on its own row inside the price block, so it sits
   above the card's divider rather than on top of the feature list. */
.price-billed {
  flex: 0 0 100%;
  margin: 0; font-size: .72rem; color: var(--ink-3); line-height: 1.35;
}

/* The line under Agency's button. Quiet: it is for the few people whose volume
   runs past the published tier, not a second call to action competing with the
   first. */
.ptier-aside {
  margin: 0 0 .6rem; font-size: .74rem; color: var(--ink-3); text-align: center;
}
.ptier-aside a { color: var(--ink-2); }

/* ---- alternating section grounds ----
 *
 * A tinted band behind every other section, so a long marketing page reads as
 * distinct chapters rather than one column of text.
 *
 * .section spaces itself with margin, which would leave a white strip above
 * and below a tinted band. So .section-tint trades that margin for padding:
 * the colour runs edge to edge and the rhythm is unchanged. The full-bleed
 * ground comes from a pseudo-element rather than a wrapper, because the
 * sections are .wrap elements and giving each one a parent purely to paint
 * behind it would be markup in service of a background.
 */
.section-tint { position: relative; margin-block: 0; padding-block: 5.5rem; }

.section-tint::before {
  content: ''; position: absolute; z-index: -1;
  top: 0; bottom: 0;
  /* 50vw either side of the container centre: full width whatever the
     container is, without depending on the page having no scrollbar. */
  left: 50%; right: 50%; margin-left: -50vw; margin-right: -50vw;
  background: var(--surface-2);
}

/* Cards on a tinted ground need the plain surface to stay separate from it. */
.section-tint .ptier,
.section-tint .finding,
.section-tint .support-card { background: var(--surface); }

/* The calculator is already a tinted panel, so on a tinted band it would
   disappear into the ground: it takes the plain surface, and its tiles take
   the tint, swapping the two so the nesting still reads. */
.section-tint .roi-card { background: var(--surface); }
.section-tint .roi-stat { background: var(--surface-2); border-color: var(--line); }
.section-tint .roi-plan { background: var(--brand-100); }

@media (max-width: 62rem) {
  .section-tint { padding-block: 3.5rem; }
}

/* ---- the pricing page's closing box ----
 *
 * The dark gradient panel is kept; only the ground behind it changes, so the
 * FAQ band above and this section read as one continuous surface rather than
 * the page ending on a strip of white.
 */
.cta-section { padding-block: 0 5.5rem; }
.cta-plain { margin-block: 0; }

/* ---- register and sign in ----
 *
 * Pitch left, form right, so the reasons to register stay on screen while
 * someone is typing. A form alone on a page asks for an address without
 * answering why, which is the moment people leave.
 */
.auth {
  display: grid; grid-template-columns: 1fr 25rem; gap: 3.5rem;
  align-items: center; padding-block: 5rem 6rem;
}

.auth-pitch h1 {
  font-size: clamp(1.9rem, 3.6vw, 2.6rem); line-height: 1.15; font-weight: 800;
  letter-spacing: -.03em; margin: 1rem 0 .9rem; text-wrap: balance;
}
.auth-pitch h1 .accent { color: var(--brand-600); }

.auth-lede {
  margin: 0 0 1.75rem; font-size: 1rem; line-height: 1.65;
  color: var(--ink-2); max-width: 48ch;
}

/* Claim in bold, the thing that backs it underneath: the pair reads as an
   assertion and its evidence rather than a list of adjectives. */
.auth-proof { list-style: none; margin: 0; padding: 0; display: grid; gap: .9rem; }
.auth-proof li {
  display: grid; grid-template-columns: 20px 1fr; gap: .1rem .65rem;
  align-items: start; font-size: .88rem;
}
.auth-proof b { color: var(--ink); font-weight: 700; }
.auth-proof span:last-child { grid-column: 2; color: var(--ink-3); font-size: .82rem; }

.auth-tick {
  grid-row: span 2; display: grid; place-items: center;
  width: 20px; height: 20px; margin-top: .1rem; border-radius: 50%;
  background: color-mix(in srgb, var(--green-fill) 14%, transparent);
  color: var(--green-text);
}
.auth-tick svg { width: 12px; height: 12px; display: block; }

/* The panel is lifted off the page rather than outlined: it is the one thing
   on the page to act on, and a shadow says that more quietly than a border. */
.auth-panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 18px; padding: 1.6rem;
  box-shadow: var(--elev-2);
}

/* Two modes, one panel. Links rather than JS tabs: each mode is a real URL, so
   it can be linked to, bookmarked and opened in a new tab. */
.auth-tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: .35rem;
  padding: .3rem; margin-bottom: 1rem;
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 12px;
}
.auth-tab {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  padding: .5rem .5rem; border-radius: 9px;
  font-size: .82rem; font-weight: 700; color: var(--ink-3);
  text-decoration: none; transition: background .15s, color .15s;
}
.auth-tab svg { width: 15px; height: 15px; flex: none; }
.auth-tab:hover { color: var(--ink); }
.auth-tab.is-on {
  background: var(--surface); color: var(--brand-700);
  box-shadow: 0 1px 3px rgb(0 0 0 / .08);
}

.auth-panel-title {
  margin: 0 0 .2rem; font-size: 1.3rem; font-weight: 800;
  letter-spacing: -.025em; text-align: center;
}
.auth-panel-lede {
  margin: 0 0 1.3rem; font-size: .84rem; color: var(--ink-3);
  text-align: center; line-height: 1.5;
}
.auth-panel-sub { margin: 0 0 1.1rem; font-size: .8rem; color: var(--ink-3); text-align: center; }

/* A select needs its own caret: the native one sits outside the field box we
   have drawn, so the arrow is drawn here and the browser's is removed. */
.auth-select { position: relative; }
.auth-select select {
  flex: 1; min-width: 0; padding: .52rem 1.4rem .52rem 0;
  font: inherit; font-size: .86rem; color: var(--ink);
  background: none; border: 0; outline: 0; cursor: pointer;
  appearance: none; -webkit-appearance: none;
}
.auth-select select:invalid, .auth-select select option[value=""] { color: var(--ink-3); }
.auth-caret {
  position: absolute; right: .6rem; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; pointer-events: none; color: var(--ink-3);
}
.auth-panel-sub a { color: var(--brand-700); font-weight: 600; }

/* One rhythm for the whole form: --field-gap between a field and the next
   label, and everything else expressed against it. */
.auth-form { --field-gap: .95rem; }

.auth-form label {
  display: flex; align-items: baseline; justify-content: space-between;
  font-size: .76rem; font-weight: 700; color: var(--ink-2); margin-bottom: .3rem;
}
.auth-forgot { font-weight: 600; font-size: .74rem; color: var(--ink-3); }
.auth-forgot:hover { color: var(--brand-700); }

/* The role picker is the shared dropdown rather than an .auth-field, so it
   takes the form's rhythm here or the button below sits flush against it. */
.auth-form > .dd { margin-bottom: var(--field-gap); }

.auth-field {
  display: flex; align-items: center; gap: .5rem;
  padding: 0 .65rem; margin-bottom: var(--field-gap);
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 10px;
  transition: border-color .15s, box-shadow .15s, background .15s;
}
.auth-field svg { width: 16px; height: 16px; flex: none; color: var(--ink-3); }
.auth-field input {
  flex: 1; min-width: 0; padding: .52rem 0;
  font: inherit; font-size: .86rem; color: var(--ink);
  background: none; border: 0; outline: 0;
}
.auth-field:focus-within {
  background: var(--surface);
  border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.auth-field.is-bad { border-color: var(--red-text); }

/* Reveal what was typed. A password field that cannot be checked is a reason
   to type something short and memorable rather than something good. */
.auth-eye {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; padding: 0; margin-right: -.25rem;
  background: none; border: 0; border-radius: 6px;
  color: var(--ink-3); cursor: pointer;
}
.auth-eye:hover { color: var(--ink-2); background: var(--surface); }
.auth-eye svg { width: 16px; height: 16px; display: block; }
.auth-eye[aria-pressed="true"] { color: var(--brand-700); }

/* No ring of its own: the field around it already shows focus, and a second
   outline inside that one reads as an error. Keyboard users still see the
   field light up, so nothing is lost. */
.auth-eye:focus { outline: none; }
.auth-eye:focus-visible { outline: none; color: var(--brand-700); }

/* Four bars rather than a score out of a hundred: the question is only whether
   this will be accepted and roughly how good it is. */
/* The bars: a 3px rule under the field, so the whole meter costs three pixels
   of height rather than a bar row plus a line of text. Invisible until there
   is something to say, but always occupying its sliver. */
.auth-strength {
  /* Sits tight under its field and carries the full gap below, so the row of
     bars costs nothing: the next label lands where it would with no meter. */
  margin: calc(var(--field-gap) * -1 + .3rem) 0 var(--field-gap);
  visibility: hidden;
}
.auth-strength[data-level] { visibility: visible; }
.auth-strength-bars { display: grid; grid-template-columns: repeat(4, 1fr); gap: .25rem; }
.auth-strength-bars span {
  height: 3px; border-radius: 999px; background: var(--line);
  transition: background .18s ease;
}

/* The verdict, in the label row opposite "Password", where it takes no
   vertical space at all. */
.auth-label-end { display: inline-flex; align-items: baseline; gap: .6rem; }
.auth-strength-tag { font-size: .72rem; font-weight: 700; }
.auth-strength-tag em { font-style: normal; font-weight: 600; color: var(--ink-3); }

.auth-strength[data-level="1"] .auth-strength-bars span:nth-child(-n+1),
.auth-strength[data-level="2"] .auth-strength-bars span:nth-child(-n+2),
.auth-strength[data-level="3"] .auth-strength-bars span:nth-child(-n+3),
.auth-strength[data-level="4"] .auth-strength-bars span { background: currentColor; }

.auth-strength[data-level="1"] { color: var(--red-text); }
.auth-strength[data-level="2"] { color: var(--amber-text); }
.auth-strength[data-level="3"],
.auth-strength[data-level="4"] { color: var(--green-text); }

.auth-strength-tag[data-level="1"] { color: var(--red-text); }
.auth-strength-tag[data-level="2"] { color: var(--amber-text); }
.auth-strength-tag[data-level="3"],
.auth-strength-tag[data-level="4"] { color: var(--green-text); }

@media (prefers-reduced-motion: reduce) {
  .auth-strength-bars span, .auth-tab { transition: none; }
}

/* The message sits under its own field, so it is obvious which input it is
   about without the reader matching a notice at the top to a box below. */
.auth-err {
  margin: calc(var(--field-gap) * -1 + .25rem) 0 var(--field-gap);
  font-size: .76rem; color: var(--red-text); line-height: 1.45;
}
.auth-err a { color: inherit; text-decoration: underline; }
.auth-hint { margin: -.5rem 0 .9rem; font-size: .74rem; color: var(--ink-3); line-height: 1.45; }

.auth-alert {
  margin: 0 0 1rem; padding: .6rem .8rem; border-radius: 10px;
  background: var(--red-100); color: var(--red-text);
  font-size: .8rem; line-height: 1.5;
}

.auth-submit {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  width: 100%; box-sizing: border-box; margin-top: .25rem;
}
.auth-submit svg { width: 16px; height: 16px; flex: none; }

/* The busy state.
 *
 * The button keeps its size while the label changes, so the panel does not
 * jump at the moment someone is watching it most closely: the spinner takes
 * the arrow's place rather than being added beside it.
 */
.auth-spinner {
  display: none; flex: none;
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid rgb(255 255 255 / .35);
  border-top-color: #fff;
  animation: auth-spin .6s linear infinite;
}

.auth-submit.is-busy { cursor: progress; }
.auth-submit.is-busy .auth-submit-arrow { display: none; }
.auth-submit.is-busy .auth-spinner { display: block; }

/* Dimmed rather than greyed out: it is still the button that was pressed, and
   a disabled-looking control invites a second click. */
.auth-submit[disabled] { opacity: .8; }

@keyframes auth-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .auth-spinner { animation-duration: 1.8s; }
}

.auth-alt { margin: .9rem 0 0; font-size: .82rem; color: var(--ink-2); text-align: center; }
.auth-alt a { color: var(--brand-700); font-weight: 700; }

.auth-trust {
  list-style: none; display: flex; flex-wrap: wrap; align-items: center;
  gap: .4rem 1rem; margin: 1.8rem 0 0; padding: 0;
  font-size: .78rem; color: var(--ink-2);
}
.auth-trust li { display: inline-flex; align-items: center; gap: .4rem; }
.auth-trust svg { width: 14px; height: 14px; flex: none; color: var(--green-text); }

/* A dot between items, drawn by the list so it never leads a wrapped line. */
.auth-trust li + li::before {
  content: ''; flex: none;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--ink-3); opacity: .5;
  margin-right: .6rem;
}

@media (max-width: 30rem) {
  .auth-trust li + li::before { display: none; }
}

@media (max-width: 62rem) {
  .auth { grid-template-columns: 1fr; gap: 2.5rem; padding-block: 3rem 3.5rem; max-width: 30rem; margin-inline: auto; }
}

/* ---- register page, matched to the reference ---- */

/* The page sits on the tinted ground rather than white, which is what lifts
   the panel in the reference. */
/* A brand wash rather than flat grey: two soft pools of the brand blue over
   a near-white ground, so the white panel still reads as lifted off it.
   Percentages, not fixed colours, so the dark theme's own tokens carry
   through instead of a light tint being painted on a dark page. */
.page-auth {
  background:
    radial-gradient(60rem 40rem at 12% -10%, color-mix(in srgb, var(--brand-500) 13%, transparent), transparent 62%),
    radial-gradient(50rem 36rem at 92% 108%, color-mix(in srgb, var(--brand-600) 10%, transparent), transparent 60%),
    var(--surface);
  background-attachment: fixed;
}

/* The auth section carries its own bottom padding, so the footer's margin on
   top of it doubles the gap. The footer keeps its margin everywhere else. */
.page-auth .site-footer { margin-top: 0; }

.auth-pitch h1 { font-size: clamp(2.1rem, 4.4vw, 3.1rem); line-height: 1.08; }

/* Optional means optional: the label says so rather than the field being
   silently skippable. */
.auth-optional {
  font-weight: 600; font-size: .68rem; color: var(--ink-3);
  text-transform: uppercase; letter-spacing: .05em;
}

/* Outside the panel and quiet: required, rarely read, and not a step in
   signing up. */
.auth-legal {
  grid-column: 1 / -1;
  display: flex; justify-content: center; gap: .55rem;
  margin: 1.5rem 0 0; font-size: .72rem; color: var(--ink-3);
}
.auth-legal a { color: var(--ink-3); text-decoration: none; }
.auth-legal a:hover { color: var(--ink-2); text-decoration: underline; }

/* One column, panel only: sign-in has no pitch beside it. */
.auth.is-single {
  grid-template-columns: minmax(0, 25rem);
  justify-content: center;
  /* Tighter than the two-column gap: this one separates a wordmark from the
     box under it, not two columns of content. */
  gap: 1.4rem;
}

/* The brand above the form, where the pitch column would otherwise carry it. */
.auth-brand {
  grid-column: 1 / -1;
  display: flex; align-items: center; justify-content: center; gap: .5rem;
  margin: 0; text-decoration: none;
}
/* In the pitch column it leads the text rather than centring over a box. */
.auth-brand.is-left { justify-content: flex-start; margin-bottom: 1.75rem; }
.auth-brand { gap: .6rem; }
.auth-brand .logo-mark { width: 38px; height: 38px; color: var(--brand-600); }
.auth-brand .logo-text { font-size: 1.4rem; font-weight: 800; letter-spacing: -.025em; color: var(--ink); }
.auth-brand .logo-text span { color: var(--brand-600); }

/* Signing in with Google.
 *
 * Google's brand guidelines want their mark unaltered on a light button, so
 * this stays neutral rather than taking the site's blue: a recoloured Google
 * button is both off-brand and harder to recognise as the thing it is.
 */
.auth-google {
  display: flex; align-items: center; justify-content: center; gap: .6rem;
  width: 100%; box-sizing: border-box;
  padding: .62rem 1rem; margin-bottom: 1rem;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 10px;
  font-size: .86rem; font-weight: 700; color: var(--ink);
  text-decoration: none;
  transition: background .15s, border-color .15s;
}
.auth-google:hover { background: var(--surface-2); border-color: var(--ink-3); }
.auth-google svg { flex: none; }

/* A rule with the label sitting in a gap in it, rather than text on top of a
   line, so no background colour has to be guessed to mask it. */
.auth-divider {
  display: flex; align-items: center; gap: .7rem;
  margin: 0 0 1rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-3);
}
.auth-divider::before, .auth-divider::after {
  content: ''; flex: 1; height: 1px; background: var(--line);
}

/* The button before Google is configured. Identical to the live one, because
   the point is to show the finished panel; what differs is what happens on
   click, and the dialog explains that. */
.auth-google[data-google-soon] { font: inherit; font-size: .86rem; font-weight: 700; cursor: pointer; }

/* A native dialog: the browser handles the backdrop, Escape and focus, which
   is a lot of behaviour not to have to write. */
.auth-dialog {
  max-width: 24rem; padding: 1.5rem;
  border: 1px solid var(--line); border-radius: 16px;
  background: var(--surface); color: var(--ink);
  box-shadow: var(--elev-3);
}
.auth-dialog::backdrop { background: rgb(8 18 33 / .45); }
.auth-dialog h2 { margin: 0 0 .5rem; font-size: 1.05rem; font-weight: 800; letter-spacing: -.02em; }
.auth-dialog p { margin: 0 0 1.2rem; font-size: .86rem; line-height: 1.6; color: var(--ink-2); }
.auth-dialog-actions { display: flex; justify-content: flex-end; }

/* The header on a standalone page: the logo, centred, and nothing else. */
.bare-header {
  display: flex; justify-content: center;
  padding: 2rem 1.25rem 0;
}
.bare-header .logo { display: inline-flex; align-items: center; gap: .55rem; text-decoration: none; }

/* With no footer the section carries the page, so it fills the height and
   centres what it holds rather than sitting under a header on a tall screen. */
.page-auth main { min-height: 100vh; display: flex; align-items: center; }
.page-auth .auth { width: 100%; }

/* ---- signed-in header ---- */

.account-chip {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .2rem .55rem .2rem .2rem; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
  text-decoration: none; color: var(--ink);
}
.account-chip:hover { border-color: var(--brand-600); }

.account-avatar {
  display: grid; place-items: center; flex: none;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--brand-600); color: #fff;
  font-size: .68rem; font-weight: 800; letter-spacing: .02em;
}
.account-name { font-size: .82rem; font-weight: 700; }

.account-out {
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: 8px;
  color: var(--ink-3);
}
.account-out:hover { background: var(--surface-2); color: var(--ink); }
.account-out svg { width: 16px; height: 16px; }

/* ---- the dashboard ---- */

.dash-head { padding: 2.5rem 1.25rem .25rem; }
.dash-head h1 { font-size: 1.7rem; font-weight: 800; letter-spacing: -.025em; margin: 0 0 .3rem; }
.dash-head .lede { margin: 0; color: var(--ink-2); font-size: .94rem; }

/* The analyzer first: it is what someone signed in came to do, and a list of
   past work is the second thing, not the first. */
.dash-analyze { padding-block: 1.5rem 2.25rem; }

.dash-grid {
  display: grid; grid-template-columns: 1fr 17rem; gap: 2rem;
  align-items: start; padding-bottom: 4rem;
}

.dash-section-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  max-width: none; margin: 0 0 1rem; text-align: left;
}
.dash-section-head h2 { font-size: 1.05rem; font-weight: 800; letter-spacing: -.02em; margin: 0; }
.dash-more { margin: .2rem 0 0; font-size: .78rem; color: var(--ink-3); }

/* The note belongs under the heading, not beside it, so it takes the full
   row and pushes the baseline alignment back to the top. */
.dash-section-head:has(.dash-section-note) { flex-wrap: wrap; align-items: flex-start; }
.dash-section-note {
  flex: 1 0 100%; margin: .3rem 0 0;
  font-size: .8rem; color: var(--ink-3); line-height: 1.5;
}

/* The tracked pages table. Rows are compared against each other, which is
   what columns are for: a card makes the eye re-find the same figure in a
   different place on every row. */
.dash-table-wrap {
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
  overflow-x: auto;
  /* Positioned, so hidden labels deep in a wide table are clipped by this
     scroller instead of stretching the page sideways on a phone. */
  position: relative;
}
.dash-table { width: 100%; border-collapse: collapse; font-size: .84rem; }
.dash-table th {
  padding: .7rem 1rem; text-align: left; white-space: nowrap;
  font-size: .66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-3);
  background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.dash-table td {
  padding: .75rem 1rem; vertical-align: middle;
  border-bottom: 1px solid var(--line-2);
}
.dash-table tbody tr:last-child td { border-bottom: 0; }
.dash-table tbody tr:hover { background: var(--surface-2); }

.t-page { min-width: 15rem; }
.t-page a { text-decoration: none; display: block; }
.t-page b { display: block; font-size: .86rem; font-weight: 700; color: var(--ink); overflow-wrap: anywhere; }
/* The URL is set in mono under the title: it is an address, and a
   proportional face makes a long path harder to scan than it needs to be. */
.t-page .t-url {
  display: block; margin-top: .15rem;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: .72rem; color: var(--ink-3); overflow-wrap: anywhere;
}

.t-c { white-space: nowrap; }
.t-score {
  display: inline-flex; align-items: baseline; gap: .1rem;
  padding: .28rem .55rem; border-radius: 8px;
  font-family: var(--display); font-size: .95rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: var(--surface-2); color: var(--ink-2);
}
.t-score small { font-size: .62rem; font-weight: 700; opacity: .7; }
/* Grades, one pair of tokens per variation.

   The letter picks the band, because the hue is what the band means, and the
   modifier picks the step within it: a B+ and a B are the same kind of
   result, so they share a hue and differ in weight.

   Tokens rather than a filter on the base colour. brightness() moved the tint
   and the ink together, which sounds right and is not: lightening a pale chip
   to mark a plus took it to near-white and the chip vanished into the page.
   Each step is written out and contrast-checked instead. */
.t-score.t-a-plus  { background: var(--grade-a-plus-bg);  color: var(--grade-a-plus-ink); }
.t-score.t-a       { background: var(--grade-a-bg);       color: var(--grade-a-ink); }
.t-score.t-a-minus { background: var(--grade-a-minus-bg); color: var(--grade-a-minus-ink); }
.t-score.t-b-plus  { background: var(--grade-b-plus-bg);  color: var(--grade-b-plus-ink); }
.t-score.t-b       { background: var(--grade-b-bg);       color: var(--grade-b-ink); }
.t-score.t-b-minus { background: var(--grade-b-minus-bg); color: var(--grade-b-minus-ink); }
.t-score.t-c-plus  { background: var(--grade-c-plus-bg);  color: var(--grade-c-plus-ink); }
.t-score.t-c       { background: var(--grade-c-bg);       color: var(--grade-c-ink); }
.t-score.t-c-minus { background: var(--grade-c-minus-bg); color: var(--grade-c-minus-ink); }
.t-score.t-d-plus  { background: var(--grade-d-plus-bg);  color: var(--grade-d-plus-ink); }
.t-score.t-d       { background: var(--grade-d-bg);       color: var(--grade-d-ink); }
.t-score.t-d-minus { background: var(--grade-d-minus-bg); color: var(--grade-d-minus-ink); }
.t-score.t-f       { background: var(--grade-f-bg);       color: var(--grade-f-ink); }
.t-score.is-pending,
/* A grade we do not recognise. Grey rather than unstyled, so a bad value
   looks deliberately neutral instead of inheriting whatever came before. */
.t-score.t-none { color: var(--ink-3); }

.t-vitals { white-space: nowrap; }
.vital {
  display: inline-block; margin-right: .3rem;
  padding: .18rem .45rem; border-radius: 6px;
  font-size: .68rem; font-weight: 600;
  background: var(--surface-2); color: var(--ink-3);
}
.vital b { font-weight: 800; font-variant-numeric: tabular-nums; }
.vital.is-good { background: var(--green-100); color: var(--green-text); }
.vital.is-warn { background: var(--amber-100); color: var(--amber-text); }
.vital.is-bad { background: var(--red-100); color: var(--red-text); }
.t-none { font-size: .74rem; color: var(--ink-3); }

.t-issues { white-space: nowrap; font-size: .78rem; }
.t-crit { color: var(--red-text); font-weight: 700; }
.t-warn { color: var(--amber-text); font-weight: 600; }
.t-sep { color: var(--ink-3); margin: 0 .3rem; }
.t-clean { color: var(--green-text); font-weight: 600; }

.t-when { white-space: nowrap; font-size: .76rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* Where a page started against where it is, in one cell. */
.t-movecell { white-space: nowrap; }
.t-move {
  display: inline-flex; align-items: baseline; gap: .35rem;
  font-size: .86rem; font-variant-numeric: tabular-nums;
}
.t-move b { font-weight: 700; color: var(--ink-2); }
.t-move b:last-of-type { color: var(--ink); }
.t-arrow { color: var(--ink-3); }
.t-delta { font-size: .78rem; font-weight: 800; color: var(--ink-3); margin-left: .15rem; }
.t-delta.is-up { color: var(--green-text); }
.t-delta.is-down { color: var(--red-text); }

.t-runs.is-link svg { width: .8rem; height: .8rem; vertical-align: -1px; }

.t-act { white-space: nowrap; text-align: right; }

/* The run count, whether or not it opens a timeline.

   One box for both, so a row with a single run lines up with a row that has
   several: the count sat 6px shorter and 3px higher when it was bare text,
   and the two rows disagreed about where the end of the table was.

   A page analysed once has no timeline to open, so that one is not a link.
   It keeps the shape and drops the colour, which is what says "this is the
   same kind of thing, and there is nothing behind it". */
.t-runs {
  display: inline-flex; align-items: center; gap: .1rem;
  height: 1.55rem; padding: 0 .6rem;
  border: 1px solid transparent; border-radius: 999px;
  font-size: .74rem; font-weight: 700; color: var(--ink-3);
}
.t-runs.is-link {
  text-decoration: none;
  color: var(--brand-700); background: var(--brand-100);
}
.t-runs.is-link:hover { background: var(--brand-100); text-decoration: underline; }
.t-act .dash-untrack { display: inline-flex; vertical-align: middle; }

/* Search and sort, above the table on the right.

   Two separate controls with a gap, not one joined group: they do different
   things to the same table, and a seam between them implied they were halves
   of a single thing. */
/* The sort sits beside the heading, because it describes the order of the
   list the heading names. Search goes to the far right, where a search box
   is looked for. One form still wraps both, so it submits as one thing and
   works without the script. */
.dash-section-head.has-tools { align-items: center; gap: .8rem; flex-wrap: wrap; }
.dash-section-head.has-tools h2 { flex: none; }

.table-tools {
  display: flex; align-items: center; gap: .5rem;
  flex: 1; min-width: 0;
}
.table-tools .tools-sort { order: -1; }
.table-tools .tools-search { margin-left: auto; }

/* The question mark beside a section heading.

   Opens on hover and on click. Hover is what a question mark invites, but it
   cannot be the only way in: a touch screen has no hover and neither does a
   keyboard, so the button carries an is-open class the script toggles and
   hover is an extra trigger on top of it.

   Not a <details>: a closed one is not painted at all, whatever the CSS on
   its child says, so the panel had a real box and full opacity and still
   showed nothing. */
.help-tip { position: relative; display: inline-block; vertical-align: middle; }

.help-tip-mark {
  display: grid; place-items: center;
  width: 16px; height: 16px; margin-left: .4rem; padding: 0;
  border-radius: 50%; border: 1px solid var(--line);
  background: var(--surface-2); color: var(--ink-3);
  font: inherit; font-size: .64rem; font-weight: 800; line-height: 1;
  cursor: help;
}
.help-tip-mark:hover,
.help-tip.is-open .help-tip-mark { border-color: var(--brand-600); color: var(--brand-700); }
.help-tip-mark:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }

/* Hidden with opacity rather than display, so it can fade and so its box is
   measurable for positioning before it is ever shown. */
.help-tip-body {
  position: absolute; z-index: 20; top: calc(100% + .5rem); left: -.6rem;
  /* A fixed measure rather than max-content, which sized the box to the
     longest unbroken line and then wrapped the text shorter than that, so
     the panel carried a band of empty space down its right edge.

     Wide enough for a comfortable line: at 20rem the copy broke after four
     or five words a line, which reads as a column rather than a sentence. */
  /* A tooltip, so it stays small: wide enough that a sentence is not chopped
     into fragments, narrow enough that it reads as a note hanging off the
     marker rather than a panel covering the table. */
  width: min(19rem, 70vw);
  font-family: var(--font);
  padding: .6rem .7rem; border-radius: 10px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 1px 2px rgb(15 23 42 / .06), 0 12px 28px rgb(15 23 42 / .13);
  opacity: 0; pointer-events: none;
  /* A few pixels of travel, so it reads as coming from the marker rather
     than fading in wherever it happens to be. */
  transform: translateY(-2px);
  transition: opacity .13s ease, transform .13s ease;
}

/* The arrow, tying the panel to the marker it belongs to. Square, rotated,
   with only the two facing borders drawn, so it inherits the panel's own
   surface and line rather than approximating them. */
.help-tip-body::after {
  content: ""; position: absolute; top: -5px; left: 1rem;
  width: 9px; height: 9px; transform: rotate(45deg);
  background: var(--surface);
  border-left: 1px solid var(--line); border-top: 1px solid var(--line);
  border-radius: 2px 0 0 0;
}
/*
 * The panel sits inside the h2 it explains, so it inherits the heading's
 * font, its tight letter-spacing and text-wrap: balance. Balance evens out
 * line lengths, which is right for a heading and wrong here: the copy
 * wrapped after five words with a third of the panel left empty.
 */
.help-tip-body {
  letter-spacing: normal; text-wrap: wrap;
}
.help-tip-body p {
  margin: 0;
  font-size: .73rem; font-weight: 400; line-height: 1.5;
  color: var(--ink-2); white-space: normal; text-wrap: wrap;
}

/* The question, answered in the panel rather than only in the aria-label:
   somebody who opens it should see what they clicked. */
.help-tip-title {
  display: block; margin-bottom: .3rem;
  /* Only a step above the body: it is a label on a small panel, not a page
     heading, and at .78rem against .76rem body it was competing with the h2
     it hangs off. */
  font-size: .74rem; font-weight: 700; letter-spacing: 0;
  color: var(--ink); white-space: normal;
}

/* The second paragraph is the contrast with History, which is the part people
   come here for. Set apart by a rule rather than a gap, so it reads as a
   separate fact instead of a continuation. */
/* Just the next paragraph.

   It had a rule above it and dimmed text, which together read as a warning
   footnote: a line plus grey is how a page says "be careful", and this is
   an ordinary second fact about where analyses go. Plain spacing says
   "related, and also true" without the tone. */
.help-tip-body p.help-tip-aside { margin-top: .4rem; }

.help-tip.is-open > .help-tip-body {
  opacity: 1; pointer-events: auto; transform: translateY(0);
}

@media (hover: hover) {
  .help-tip:hover > .help-tip-body,
  .help-tip:focus-within > .help-tip-body {
    opacity: 1; pointer-events: auto; transform: translateY(0);
  }

  /* The gap between marker and panel would drop the hover on the way down,
     so it is bridged, only as wide as the marker. */
  .help-tip:hover > .help-tip-body::before {
    content: ""; position: absolute; left: .5rem; width: 22px;
    top: -.45rem; height: .45rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .help-tip-body { transition: none; }
}
.help-tip-body p {
  margin: 0; font-size: .76rem; font-weight: 400; line-height: 1.55;
  color: var(--ink-2); white-space: normal;
}

/* Where a page comes from, under the list it is about. Below the pager
   because that is where the eye lands after reading the table, and because
   the heading row already carries a sort and a search. */
.dash-foot-note {
  margin: .8rem 0 0; font-size: .78rem; color: var(--ink-3); line-height: 1.55;
}
.dash-foot-note b { color: var(--ink-2); font-weight: 700; }



.tools-search { position: relative; display: flex; align-items: center; }
.tools-search-icon {
  position: absolute; left: .7rem; width: 15px; height: 15px;
  color: var(--ink-3); pointer-events: none;
}
.tools-search input {
  width: 15rem; max-width: 34vw; height: 36px;
  padding: 0 2rem 0 2.1rem; margin: 0;
  font: inherit; font-size: .82rem; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 9px;
}
.tools-search input::-webkit-search-cancel-button { display: none; }
.tools-search input:hover { border-color: var(--ink-3); }
.tools-search input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px var(--brand-100);
}

/* The submit, shown once there is a term. Sits inside the field at the
   right, with the clear button moved left of it. */
.tools-submit {
  position: absolute; right: .3rem;
  display: grid; place-items: center; width: 26px; height: 26px;
  border: 0; border-radius: 7px; cursor: pointer;
  color: var(--on-brand); background: var(--brand-600);
}
.tools-submit svg { width: 14px; height: 14px; }
.tools-submit:hover { background: var(--brand-700); }
.tools-submit[hidden] { display: none; }

/* Left of the submit when both are there, at the edge when it is alone. */
.tools-clear {
  position: absolute; right: .5rem;
  display: grid; place-items: center; width: 20px; height: 20px;
  border-radius: 6px; color: var(--ink-3);
}
.tools-clear svg { width: 13px; height: 13px; }
.tools-clear:hover { color: var(--ink); background: var(--surface-2); }

.tools-clear[hidden] { display: none; }

/* After .tools-clear, not before it: these override its position, and an
   override that comes first is not an override.
   
   Driven by a class on the form rather than :has(), because the script
   toggles both buttons together and a class says once what two selectors
   would have to agree on separately. */
.table-tools.has-term .tools-search input { padding-right: 3.9rem; }
.table-tools.has-term .tools-clear { right: 2.3rem; }

/* The shared dropdown, sized to match the search pill beside it. The icon
   carries the meaning, so there is no label: the chosen order is already
   written in the button. */
.tools-sort { display: flex; }
.tools-sort .dd { min-width: 0; }
.tools-sort .dd-button {
  height: 36px; padding: 0 .7rem 0 .8rem; gap: .45rem;
  border-radius: 9px; background: var(--surface);
  font-size: .82rem; font-weight: 700; color: var(--ink);
}
.tools-sort .dd-button:hover { border-color: var(--ink-3); }
.tools-sort-icon { width: 15px; height: 15px; color: var(--ink-3); flex: none; }
.tools-sort .dd-menu { min-width: 13rem; }

/* Only for the no-script case, where typing cannot submit on its own. */
.tools-go {
  height: 36px; padding: 0 1rem;
  font: inherit; font-size: .8rem; font-weight: 700;
  color: var(--brand-700); background: var(--brand-100);
  border: 1px solid transparent; border-radius: 9px; cursor: pointer;
}
.tools-go:hover { border-color: var(--brand-600); }

/* While a swap is in flight. Dimmed rather than emptied: replacing the rows
   with a spinner throws away what the reader was looking at to tell them
   something they will know in 200ms. */
[data-table][aria-busy="true"] { opacity: .55; transition: opacity .12s ease; }

/* Under a paged table, inside its card so it reads as part of the table
   rather than as something floating after it. */
.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap;
  padding: .7rem 1rem;
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}
/* Where you are, first: it is the part that answers the question a
   truncated list raises. */
.pager-count {
  display: flex; align-items: center; gap: .9rem; flex-wrap: wrap;
  font-size: .78rem; color: var(--ink-2); font-variant-numeric: tabular-nums;
}

/* How many rows to a page. Links rather than a select, so each size is one
   click and leaves a URL somebody can keep. */
.pager-sizes { display: flex; align-items: center; gap: .15rem; }
.pager-sizes-label { margin-right: .2rem; color: var(--ink-3); }
.pager-size {
  padding: .18rem .4rem; border-radius: 6px;
  font-size: .74rem; font-weight: 600;
  color: var(--ink-3); text-decoration: none;
}
.pager-size:hover { color: var(--ink); background: var(--surface); }
.pager-size.is-on { color: var(--brand-700); background: var(--brand-100); font-weight: 700; }
.pager-size:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 1px; }

.pager-nums { display: flex; align-items: center; gap: .2rem; }

.pager-step, .pager-n {
  display: grid; place-items: center;
  min-width: 30px; height: 30px; padding: 0 .4rem;
  border-radius: 8px; font-size: .8rem; font-weight: 600;
  color: var(--ink-2); text-decoration: none;
  background: var(--surface); border: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}
.pager-step svg { width: 15px; height: 15px; }
.pager-step:hover, .pager-n:hover { color: var(--ink); border-color: var(--ink-3); }
.pager-step:focus-visible, .pager-n:focus-visible,
.pager-jump input:focus-visible, .pager-jump button:focus-visible {
  outline: 2px solid var(--brand-600); outline-offset: 2px;
}
.pager-step.is-off { color: var(--line); background: transparent; border-color: var(--line-2); }

/* The page you are on is a state, not a destination, so it is filled rather
   than outlined and is not a link. */
.pager-n.is-on {
  background: var(--brand-600); color: #fff; border-color: transparent; font-weight: 700;
}
.pager-gap { padding: 0 .15rem; color: var(--ink-3); font-size: .8rem; }

/* For the reader who knows they want page 23. A GET form, so it works
   without JavaScript and leaves a real URL behind. */
.pager-jump { display: flex; align-items: center; gap: .35rem; }
.pager-jump label { font-size: .76rem; color: var(--ink-3); }
.pager-jump input {
  width: 3.6rem; height: 30px; padding: 0 .4rem;
  font: inherit; font-size: .8rem; text-align: center;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: 8px;
  font-variant-numeric: tabular-nums;
}
.pager-jump button {
  height: 30px; padding: 0 .7rem;
  font: inherit; font-size: .78rem; font-weight: 700;
  color: var(--brand-700); background: var(--brand-100);
  border: 1px solid transparent; border-radius: 8px; cursor: pointer;
}
.pager-jump button:hover { border-color: var(--brand-600); }

@media (max-width: 52rem) {
  .pager { justify-content: center; }
  .pager-count { width: 100%; text-align: center; }
}

.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

.dash-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }

.dash-row {
  display: grid; grid-template-columns: 2.6rem minmax(0, 1fr) auto auto auto auto;
  align-items: center; gap: .9rem;
  padding: .8rem 1rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}
.dash-row:hover { border-color: var(--brand-600); }

/* The score carries its grade's colour, the same one the report uses, so the
   list reads at a glance without a legend. */
.dash-score {
  display: grid; place-items: center;
  width: 2.6rem; height: 2.2rem; border-radius: 9px;
  font-family: var(--display); font-size: 1.05rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
  background: var(--surface-2); color: var(--ink-2);
}
.dash-score.t-a-plus  { background: var(--grade-a-plus-bg);  color: var(--grade-a-plus-ink); }
.dash-score.t-a       { background: var(--grade-a-bg);       color: var(--grade-a-ink); }
.dash-score.t-a-minus { background: var(--grade-a-minus-bg); color: var(--grade-a-minus-ink); }
.dash-score.t-b-plus  { background: var(--grade-b-plus-bg);  color: var(--grade-b-plus-ink); }
.dash-score.t-b       { background: var(--grade-b-bg);       color: var(--grade-b-ink); }
.dash-score.t-b-minus { background: var(--grade-b-minus-bg); color: var(--grade-b-minus-ink); }
.dash-score.t-c-plus  { background: var(--grade-c-plus-bg);  color: var(--grade-c-plus-ink); }
.dash-score.t-c       { background: var(--grade-c-bg);       color: var(--grade-c-ink); }
.dash-score.t-c-minus { background: var(--grade-c-minus-bg); color: var(--grade-c-minus-ink); }
.dash-score.t-d-plus  { background: var(--grade-d-plus-bg);  color: var(--grade-d-plus-ink); }
.dash-score.t-d       { background: var(--grade-d-bg);       color: var(--grade-d-ink); }
.dash-score.t-d-minus { background: var(--grade-d-minus-bg); color: var(--grade-d-minus-ink); }
.dash-score.t-f       { background: var(--grade-f-bg);       color: var(--grade-f-ink); }
.dash-score.is-pending, .dash-score.t-none { color: var(--ink-3); }

.dash-url { min-width: 0; text-decoration: none; display: block; }
.dash-url b { display: block; font-size: .88rem; font-weight: 700; color: var(--ink); overflow-wrap: anywhere; }
.dash-url span { display: block; font-size: .74rem; color: var(--ink-3); margin-top: .1rem; }

.dash-runs { font-size: .76rem; color: var(--ink-3); white-space: nowrap; }

/* On the history tab the run count is the way into the timeline, so it is
   styled as the affordance it is. A page with one run keeps the plain text:
   there is nothing to compare it against yet. */
.dash-runs.is-link {
  display: inline-flex; align-items: center; gap: .15rem;
  padding: .28rem .5rem .28rem .6rem; border-radius: 999px;
  font-weight: 700; color: var(--brand-700); text-decoration: none;
  background: var(--brand-100); border: 1px solid transparent;
  transition: border-color .15s ease, background .15s ease;
}
.dash-runs.is-link svg { width: .85rem; height: .85rem; }
.dash-runs.is-link:hover { border-color: var(--brand-600); }
.dash-runs.is-link:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }

/* The report page's History button carries the same count. */
.snapshot-actions .btn-count {
  margin-left: .1rem; padding: .05rem .34rem; border-radius: 999px;
  font-size: .72rem; font-weight: 800; font-variant-numeric: tabular-nums;
  background: var(--brand-100); color: var(--brand-700);
}
.dash-when { font-size: .76rem; color: var(--ink-3); white-space: nowrap; font-variant-numeric: tabular-nums; }

.dash-empty {
  padding: 2.5rem 1.5rem; text-align: center;
  background: var(--surface-2); border: 1px dashed var(--line); border-radius: 14px;
}
.dash-empty h3 { margin: 0 0 .4rem; font-size: 1rem; font-weight: 700; }

/* The class is used two ways: as a <div> wrapping a heading and a sentence,
   and as a bare <p> that is the sentence. Only the nested form carried the
   type, so the bare one inherited the body size and rendered noticeably
   larger than every other empty state. Setting it on the block itself covers
   both, since a nested paragraph inherits it. */
.dash-empty { font-size: .86rem; color: var(--ink-2); line-height: 1.6; }

/* Only the nested paragraph gets a measure. When .dash-empty is itself the
   <p>, the element is both the box and the text, so a max-width here would
   shrink the dashed box rather than the sentence inside it. */
.dash-empty p { margin: 0 auto; max-width: 38ch; }

/* An action inside an empty state is a suggestion, not the point of the
   screen, so it is sized down from the full button and given room to sit
   apart from the sentence above it. */
.dash-empty .btn {
  margin-top: 1.1rem; padding: .5rem 1rem;
  font-size: .82rem; border-radius: 9px;
}

.dash-side { display: flex; flex-direction: column; gap: .9rem; }

.dash-card {
  padding: 1.1rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.dash-card-label {
  display: block; font-size: .66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-3); margin-bottom: .5rem;
}
.dash-plan { margin: 0; font-size: 1.1rem; font-weight: 800; letter-spacing: -.02em; }
.dash-plan-price { margin: .1rem 0 0; font-size: .8rem; color: var(--ink-3); }
.dash-upgrade { display: inline-block; margin-top: .8rem; font-size: .78rem; font-weight: 700; color: var(--brand-700); text-decoration: none; }
.dash-upgrade:hover { text-decoration: underline; }

.dash-allowance { margin: 0 0 .55rem; font-size: .84rem; color: var(--ink-2); }
.dash-meter { height: 6px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.dash-meter span { display: block; height: 100%; background: var(--brand-600); border-radius: 999px; }
.dash-remaining { margin: .45rem 0 0; font-size: .76rem; color: var(--ink-3); }

@media (max-width: 62rem) {
  .dash-grid { grid-template-columns: 1fr; }
  .dash-row { grid-template-columns: 2.6rem minmax(0, 1fr); row-gap: .4rem; }
  .dash-flag, .dash-runs, .dash-when { grid-column: 2; justify-self: start; }
}

/* ---- the numbers ---- */

/* No padding of its own at the top.
 *
 * The same doubling .dash-pages carried: this is the first thing inside
 * .app-content, which already pads 1.5rem on every side, so adding 1.5rem
 * here opened the dashboard with 3rem of nothing above the figures. */
.dash-stats {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: .8rem;
}

/* Each card carries a figure, a supporting fact, and where there is one a
   bar. A number with nothing beside it cannot be judged. */
.stat-top {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: .5rem; margin-bottom: .55rem;
}
.stat-icon {
  display: grid; place-items: center; flex: none;
  width: 26px; height: 26px; border-radius: 8px;
}
.stat-icon svg { width: 14px; height: 14px; }
.stat-icon.is-green { background: var(--green-100); color: var(--green-text); }
.stat-icon.is-red { background: var(--red-100); color: var(--red-text); }
.stat-icon.is-blue { background: var(--brand-100); color: var(--brand-700); }
.stat-icon.is-amber { background: var(--amber-100); color: var(--amber-text); }

.stat-figure {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: .45rem;
  margin: 0 0 .5rem; line-height: 1;
}
.stat-figure b {
  font-family: var(--display); font-size: 1.85rem; font-weight: 800;
  letter-spacing: -.04em; color: var(--ink); font-variant-numeric: tabular-nums;
}
.stat-figure b.is-empty { color: var(--ink-3); }
.stat-unit { font-size: .78rem; color: var(--ink-3); }

/* The move since each page's previous run, where there is one. */
.stat-trend {
  display: inline-flex; align-items: center; gap: .1rem;
  font-size: .78rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.stat-trend svg { width: 12px; height: 12px; }
.stat-trend.is-up { color: var(--green-text); }
.stat-trend.is-down { color: var(--red-text); }

.stat-flag {
  padding: .16rem .45rem; border-radius: 999px;
  font-size: .68rem; font-weight: 700;
  background: var(--red-100); color: var(--red-text);
}
.stat-good { color: var(--green-text); font-weight: 600; }

.stat-track {
  height: 5px; border-radius: 999px; margin-bottom: .5rem;
  background: var(--surface-2); overflow: hidden;
}
.stat-track i { display: block; height: 100%; border-radius: 999px; background: var(--brand-600); }
.stat-track i.is-green { background: var(--green-fill); }
.stat-track i.is-teal { background: var(--teal-fill); }
.stat-track i.is-amber { background: var(--amber-fill); }
.stat-track i.is-red { background: var(--red-fill); }
.stat-track i.is-warm { background: var(--amber-fill); }
.stat-track i.is-high { background: var(--red-fill); }
.dash-stat {
  display: flex; flex-direction: column; gap: .15rem;
  padding: 1rem 1.1rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.dash-stat-label {
  font-size: .66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-3);
}
.dash-stat-value {
  font-family: var(--display); font-size: 1.8rem; font-weight: 800;
  line-height: 1.1; letter-spacing: -.03em; font-variant-numeric: tabular-nums;
}
/* Only this one is coloured: it is the number that means "stop and look". */
.dash-stat-value.t-critical { color: var(--red-text); }
.dash-stat-note { font-size: .72rem; color: var(--ink-3); }
.dash-stat.is-plan .dash-allowance { margin: .3rem 0 .4rem; font-size: .8rem; color: var(--ink-2); }

/* The shell already pads the top.
 *
 * .app-content carries 1.5rem on every side, and this repeated it, so every
 * dashboard page opened with 3rem of nothing above its heading: the same
 * number twice, which is what two rules each believing they own the spacing
 * looks like.
 *
 * The tail stays. It is not a doubling but a deliberate run-out, so the last
 * row of a table is not flush against the bottom of the window, and 2.5rem
 * here plus the shell's 1.5rem is the 4rem that was there before. */
.dash-pages { padding-block: 0 2.5rem; }

/* Separation belongs between the two, not on either.
 *
 * The figures and the list below them are siblings, and giving one of them
 * padding to hold them apart means it has to know what sits next to it. This
 * is the only place the pair occurs, so the gap is stated where the pair is. */
.dash-stats + .dash-pages { padding-top: 1.5rem; }
.dash-flag {
  font-size: .7rem; font-weight: 700; white-space: nowrap;
  padding: .12rem .45rem; border-radius: 999px;
  background: var(--red-100); color: var(--red-text);
}

/* ---- the gate ---- */

/* What a plan buys, said plainly.

   There used to be a blurred mock of the feature behind this text. A fake
   screenshot of invented rows reads as exactly that, and arguing for a
   feature with a picture of nothing is weaker than the sentence alone. */
.dash-gate {
  display: flex; flex-direction: column; align-items: center;
  gap: .55rem; padding: 3rem 1.5rem; text-align: center;
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
}
.dash-gate-lock {
  display: grid; place-items: center; width: 38px; height: 38px;
  border-radius: 50%; background: var(--brand-100); color: var(--brand-700);
}
.dash-gate-lock svg { width: 18px; height: 18px; }
.dash-gate h2 { margin: .3rem 0 0; font-size: 1.2rem; font-weight: 800; letter-spacing: -.025em; }
.dash-gate p { margin: 0; max-width: 46ch; font-size: .88rem; line-height: 1.6; color: var(--ink-2); }

/* What the tier adds, in the reader's terms rather than ours. Left aligned
   inside a centred block, because a list of ticks is read down its left edge
   and centring one makes every line start somewhere different. */
.gate-includes {
  list-style: none; margin: 1rem 0 .4rem; padding: 0;
  display: grid; gap: .45rem; text-align: left;
}
.gate-includes li {
  display: flex; align-items: flex-start; gap: .5rem;
  font-size: .85rem; color: var(--ink-2);
}
.gate-includes svg { width: 15px; height: 15px; flex: none; margin-top: .15rem; color: var(--green-fill); }
.dash-gate .btn { margin-top: .8rem; }

.dash-delta { font-size: .78rem; font-weight: 700; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.dash-delta.is-up { color: var(--green-text); }

/* ---- members ---- */

.dash-members {
  list-style: none; margin: 0 0 1.1rem; padding: 0;
  display: flex; flex-direction: column; gap: .5rem;
}
.dash-members li {
  display: grid; grid-template-columns: 2rem 1fr auto; align-items: center; gap: .8rem;
  padding: .8rem 1rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}
.dash-avatar {
  display: grid; place-items: center; width: 2rem; height: 2rem; border-radius: 50%;
  background: var(--brand-600); color: #fff; font-size: .7rem; font-weight: 800;
}
.dash-member b { display: block; font-size: .88rem; }
.dash-member span { display: block; font-size: .74rem; color: var(--ink-3); }
.dash-role { font-size: .74rem; font-weight: 700; color: var(--ink-3); }
.dash-seats { margin: 1rem 0 0; font-size: .82rem; color: var(--ink-2); }
.dash-seats a { color: var(--brand-700); font-weight: 700; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (max-width: 62rem) {
  .dash-stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 40rem) {
  .dash-stats { grid-template-columns: 1fr; }
  .dash-tabs { width: 100%; }
}

/* ---- application shell ----
 *
 * Sidebar beside the content, no marketing chrome. The sidebar is fixed and
 * the content scrolls under it: navigation that scrolls away is navigation you
 * have to scroll back for.
 */
.page-app { background: var(--surface-2); }

.app { display: grid; grid-template-columns: 16rem 1fr; min-height: 100vh; }

/* One header height for both columns, so the sidebar's rule meets the
   topbar's and the two read as one bar across the top rather than as two
   panels that happen to sit side by side. */
:root { --app-head: 64px; }

.app-side {
  position: sticky; top: 0; align-self: start;
  display: flex; flex-direction: column; gap: .8rem;
  height: 100vh; padding: 0 .8rem .9rem;
  background: var(--surface);
  border-right: 1px solid var(--line);
}

/* Matched to the topbar's height so the two columns start on the same line,
   but with no rule of its own: the sidebar's right border already separates
   the columns, and a second line under the logo would cut the sidebar into
   two panels that have no reason to be separate. */
.side-logo {
  display: flex; align-items: center; gap: .4rem;
  height: var(--app-head); flex: none;
  padding: 0 .4rem;
  text-decoration: none;
}
.side-logo .logo-mark { width: 34px; height: 34px; color: var(--brand-600); }
.side-logo .logo-text { font-size: 1.18rem; font-weight: 800; letter-spacing: -.03em; color: var(--ink); }

/* The plan tier beside the wordmark. Free reads as a state rather than a
   product, so it is set in neutral ink; the paid tiers carry the accent they
   were bought with. */
/* Smaller than the marketing header's SEO tag: there it is the only thing
   beside the wordmark, here it sits in a sidebar column where it should
   label the logo rather than compete with it. */
.side-logo .logo-tag {
  flex: none;
  font-size: .55rem; padding: .1rem .28rem; letter-spacing: .05em;
}
.logo-tag.plan-free, .logo-tag.plan-anonymous {
  background: var(--surface-2); color: var(--ink-3);
}
.logo-tag.plan-agency { background: var(--brand-600); color: #fff; }
.side-logo .logo-text span { color: var(--brand-600); }

/* The workspace, which is the context everything below it belongs to. */
.side-workspace {
  display: flex; align-items: center; gap: .55rem;
  padding: .5rem .55rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.side-ws-mark {
  display: grid; place-items: center; flex: none;
  width: 22px; height: 22px; border-radius: 6px;
  background: var(--brand-600); color: #fff; font-size: .68rem; font-weight: 800;
}
.side-ws-name {
  font-size: .82rem; font-weight: 700; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.side-switcher select {
  width: 100%; font: inherit; font-size: .82rem; font-weight: 700; color: var(--ink);
  padding: .5rem 1.8rem .5rem .6rem; border-radius: 10px;
  background: var(--surface-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23697586' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right .5rem center / 14px;
  border: 1px solid var(--line); cursor: pointer; appearance: none; -webkit-appearance: none;
}

.side-nav {
  flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; gap: .1rem;
  /* Fades the cut rather than slicing a row in half, which reads as a
     rendering fault rather than as "there is more below". */
  mask-image: linear-gradient(to bottom, #000 calc(100% - 1.2rem), transparent);
  scrollbar-width: thin;
}

/* Grouped, because five links in one undifferentiated column is a list to read
   rather than a structure to scan. */
.side-group {
  margin: .6rem 0 .25rem; padding: .6rem .6rem 0;
  border-top: 1px solid var(--line-2);
  font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-3);
}
.side-group:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* A heading, not a container: the uppercase and the tracking are for the one
   word, and a link that ends up inside one should still look like a link. */
.side-group .side-link { text-transform: none; letter-spacing: normal; }

.side-link {
  display: flex; align-items: center; gap: .6rem;
  min-height: 32px; padding: .3rem .6rem; border-radius: 8px;
  font-size: .85rem; font-weight: 600; color: var(--ink-2); text-decoration: none;
  transition: background .2s ease, color .2s ease;
}
.side-link svg { width: 16px; height: 16px; flex: none; opacity: .75; }
.side-link span:first-of-type {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.side-link:hover { background: var(--surface-2); color: var(--ink); }

/* Active is carried by weight and a tinted ground, not by a colour bar: the
   accent is spent on the one number that means "look here". */
.side-link.is-on { background: var(--brand-100); color: var(--brand-700); font-weight: 700; }
.side-link.is-on svg { opacity: 1; }
.side-lock { width: 12px !important; height: 12px !important; opacity: .55 !important; }

.side-foot { border-top: 1px solid var(--line); padding-top: .8rem; }
.side-plan { margin: 0 0 .5rem; padding: 0 .55rem; font-size: .74rem; color: var(--ink-3); }
.side-plan a { color: var(--brand-700); font-weight: 700; }
.side-out {
  display: flex; align-items: center; gap: .6rem;
  padding: .45rem .55rem; border-radius: 9px;
  font-size: .82rem; font-weight: 600; color: var(--ink-3); text-decoration: none;
}
.side-out svg { width: 15px; height: 15px; }
.side-out:hover { background: var(--surface-2); color: var(--ink); }

/* ---- top bar ---- */

.app-main { min-width: 0; display: flex; flex-direction: column; }

.app-topbar {
  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem;
  height: var(--app-head); padding: 0 1.5rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.app-topbar h1 { margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.025em; }

/* One height for every control in the row. The input and the button were
   sized by padding and landed on 37.4px, which nothing else could match by
   guessing, so the height is stated once and they all take it. */
.app-topbar { --topbar-control: 2.35rem; }
.topbar-right { display: flex; align-items: center; gap: .6rem; }

/* In the dashboard the theme control sits in a row of real controls, so it
   takes their shape: a 32px circle beside a 9px-radius button reads as a
   stray icon rather than as part of the row. The marketing header keeps the
   circle, where it stands alone. */
.app-topbar .theme-cycle {
  width: auto; min-width: var(--topbar-control); height: var(--topbar-control);
  padding: 0 .55rem; border-radius: 9px;
  border-color: var(--line); background: var(--surface);
}
.app-topbar .theme-cycle:hover { background: var(--surface-2); border-color: var(--ink-3); }
.topbar-analyze { display: flex; gap: .4rem; }
/* The field and its button are one control, so they share a radius and a
   height: the global .btn pill beside a 9px input reads as two unrelated
   things that happen to be adjacent. */
.topbar-analyze .btn {
  height: var(--topbar-control); padding: 0 1rem;
  border-radius: 9px; font-size: .82rem;
}
.topbar-analyze input {
  width: 17rem; max-width: 42vw;
  height: var(--topbar-control);
  font: inherit; font-size: .82rem; color: var(--ink);
  padding: 0 .7rem; border-radius: 9px;
  background: var(--surface); border: 1px solid var(--line);
}
.topbar-analyze input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}

.app-content { padding: 1.5rem; }

/* ---- plan page ---- */

.plan-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .8rem; margin-bottom: 1.75rem; }
.plan-card {
  padding: 1.2rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.plan-name { margin: .35rem 0 .1rem; font-family: var(--display); font-size: 1.5rem; font-weight: 800; letter-spacing: -.03em; }
.plan-price { margin: 0; font-size: .82rem; color: var(--ink-3); }

/* The plan and its price on the left, Upgrade pinned to the top corner on
   the right. It is the card's standing action rather than one of a pair, so
   it sits beside the plan it would replace instead of in a row at the foot.
   align-items start so a one-line button does not stretch to the height of
   the three lines beside it. */
.plan-card-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem;
}
.plan-card-head > .btn { flex: none; }
.plan-note { margin: .6rem 0 0; font-size: .76rem; color: var(--ink-3); line-height: 1.5; }
.plan-note a { color: var(--brand-700); font-weight: 700; }

/* The yearly offer: the button, then what it costs, on one line at the foot
   of the card. Together rather than split across the card, because the price
   is the argument for the button beside it. Wraps rather than squeezing
   either half on a narrow card. */
.plan-yearly {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .3rem .7rem; margin: 1rem 0 0;
}
.plan-yearly .btn { flex: none; }
.plan-yearly-price { font-size: .78rem; color: var(--ink-3); }
.plan-yearly-price b { color: var(--ink-2); font-weight: 700; }
/* The full price, struck. text-decoration-color rather than a lighter ink,
   so the figure stays readable while the line across it is clearly the
   thing being cancelled. */
.plan-yearly-price s {
  text-decoration-color: var(--ink-3);
  text-decoration-thickness: 1px;
}
/* The term note belongs to the price above it. */
.plan-card-head + .plan-note { margin-top: .5rem; }

/* The rupee equivalent under the charged amount, on the e-invoice desk.
   Both are needed there: one goes on the GST return, the other appears on
   the provider's statement. */
.inv-converted {
  display: block; margin-top: .1rem;
  font-size: .72rem; font-weight: 400; color: var(--ink-3);
}

/* The receipt button with the wait stated under it. Stacked so the button
   is the thing you reach for and the explanation sits quietly beneath,
   rather than a line of grey text with a button lost beside it. */
.inv-waiting {
  display: block; margin-top: .3rem;
  font-size: .7rem; color: var(--ink-3); line-height: 1.4;
}

/* Choosing a bigger plan, in the pricing page's own cards.

   Wide enough for two or three of them side by side, and capped at the
   viewport so a short window scrolls the grid rather than putting the
   Cancel button off the bottom of the screen. */
/* A dialog is shrink-to-fit, so "width" here is a maximum rather than a
   size: with one plan to offer it narrows to that card instead of leaving
   half the box empty, and with three it opens out to hold them in a row.

   Two classes because .ws-dialog sets width too and is declared later in
   this file, so a single class ties on specificity and loses on order. */
.ws-dialog.upg-dialog { width: max-content; max-width: min(96vw, 54rem); }
/* The intro wraps rather than setting the dialog's width. Left to itself it
   runs to one long line, and a one-plan dialog came out sized for the
   sentence with an empty half beside the single card. */
.upg-dialog > p { max-width: 34rem; margin-inline: auto; text-align: center; }
/* Focused on open so the switch is not, but a heading is not a control and
   should not look like one. */
.upg-dialog > h2 { text-align: center; }
.upg-dialog > h2:focus { outline: none; }
/* Flex rather than grid, so the row is as wide as the cards it actually
   holds. auto-fill counts the tracks that fit the dialog's maximum width
   instead of the cards present, which left a one-plan dialog sized for two
   with an empty half beside the card. */
.upg-grid {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .9rem;
  max-height: min(60vh, 34rem); overflow-y: auto;
  padding: .2rem;
}
.upg-grid > .upg-tier { flex: 0 1 16rem; min-width: 14rem; }
/* The pricing page's pill is pulled up into its hero, which there is none of
   here, and it carries a "Save 20%" badge the dialog states in words. */
.upg-billing { margin: 0 auto 1.1rem; }
/* Softer and tighter than a marketing card: inside a dialog these are a
   list of choices, not three competing offers on a page of their own, and
   the whole thing has to fit a window with a page behind it. */
.upg-tier { padding: 1rem; border-radius: 14px; }
.upg-tier .ptier-head { margin-bottom: .35rem; }
.upg-tier h3 {
  margin: 0; font-family: var(--display);
  font-size: 1rem; font-weight: 800; letter-spacing: -.02em;
}
/* 1.7rem rather than the page's 2.4rem: a headline price is doing the
   selling on the pricing page, where here the reader has already decided to
   upgrade and is comparing what each one gives them. */
.upg-tier .ptier-price { margin: 0 0 .75rem; padding-bottom: .7rem; }
.upg-tier .ptier-price .amount { font-size: 1.7rem; }
.upg-tier .ptier-list { margin: 0; gap: .4rem; }
.upg-tier .ptier-list li { font-size: .8rem; }
/* The button sits at the foot of every card whatever the list above it, so
   a short plan and a long one still line their buttons up. Narrower than a
   pricing page CTA: in a dialog it is one choice among a few, not the end
   of a sales pitch. */
.upg-tier .ptier-cta { margin-top: .9rem; height: 34px; font-size: .8rem; }

/* The dialog every purchase goes through. Wider than a confirmation because
   one of its panes is the billing form, and 27rem puts a two-line label above
   every field. */
/* Two classes for the same reason as .upg-dialog above: .ws-dialog sets a
   narrower width further down the file and would otherwise win. */
.ws-dialog.buy-dialog { width: min(94vw, 32rem); }
/* The form is already inside a padded, bordered dialog. Its own card border
   and padding would draw a second frame inside the first and indent every
   field twice. */
.buy-dialog .ws-form {
  margin: 0; padding: 0;
  background: none; border: 0; border-radius: 0;
}
/* Scrolls rather than growing past the viewport: the actions below it are
   how anybody leaves the dialog, so they must stay in view.

   The fade over its bottom edge is the only thing that says there is more
   below, and without it the form simply ends mid-field and reads as
   clipped. It has to be an overlay rather than a background on the
   scrolling box: the fields have opaque backgrounds of their own and would
   cover any gradient painted behind them. pointer-events none so it never
   swallows a click meant for the field underneath. */
.buy-dialog .buy-scroll { position: relative; }
.buy-dialog .buy-scroll .ws-form {
  max-height: min(48vh, 22rem); overflow-y: auto;
  padding: .1rem .9rem .1rem .1rem;
}
.buy-dialog .buy-scroll::after {
  content: ""; position: absolute; inset: auto 0 0 0; height: 2.6rem;
  pointer-events: none; border-radius: 0 0 8px 8px;
  background: linear-gradient(transparent, var(--surface) 82%);
  opacity: 1; transition: opacity .15s ease;
}
/* Set by script once the form is scrolled to its end. A fade that stays
   after the last field is reached is just a tint over the Address box,
   saying there is more when there is not. */
.buy-dialog .buy-scroll.is-ended::after { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .buy-dialog .buy-scroll::after { transition: none; }
}
.buy-dialog .form-errors { margin: 0 0 1rem; }

/* Waiting: while the price is checked, and again while the provider's
   checkout loads. A spinner rather than text alone, because the second of
   those ends in the page going blank for a second or two and a still
   sentence there reads as something having stalled. */
.buy-working { display: flex; align-items: center; gap: .6rem; margin: .4rem 0 1.4rem; }
.buy-spinner {
  flex: none; width: 17px; height: 17px; border-radius: 50%;
  border: 2px solid var(--line-2);
  border-top-color: var(--brand-600);
  animation: wait-spin .7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .buy-spinner { animation-duration: 2.4s; }
}

/* What the credit does to the price, as a sum rather than a sentence: three
   numbers that add up is the one form nobody has to re-read. */
.buy-sum { list-style: none; margin: 0 0 1.3rem; padding: 0; }
.buy-sum li {
  display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
  padding: .6rem 0; font-size: .88rem; color: var(--ink-2);
  border-bottom: 1px solid var(--line-2);
}
.buy-sum li span:last-child { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.buy-sum-total { border-bottom: 0; padding-top: .8rem; font-weight: 700; color: var(--ink); }
.buy-sum-total span:last-child { font-size: 1.15rem; letter-spacing: -.02em; }

.plan-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); overflow: hidden; }
.plan-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .75rem 1.1rem; border-bottom: 1px solid var(--line-2); font-size: .86rem;
}
.plan-list li:last-child { border-bottom: 0; }
.plan-key { color: var(--ink-2); }
.plan-val { font-weight: 700; color: var(--ink); text-align: right; }

/* ---- narrow ---- */

@media (max-width: 62rem) {
  /* The bar keeps its own height; the page takes whatever is left. Without
     this a short page shared the spare height with the bar and it grew. */
  .app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }

  /* The sidebar becomes a drawer. As a strip along the top it hid most of
     its links off the edge and the account menu opened upward off the
     screen; the bar above holds the burger, the name and the avatar. */
  .app > .app-mbar {
    position: sticky; top: env(safe-area-inset-top, 0px); z-index: 30;
    /* Measured off the marketing header at the same width, so moving between
       the site and the dashboard does not shift the burger, the name or the
       avatar: 63px tall, the burger 14px in, the controls 20px from the edge. */
    display: flex; align-items: center; gap: .625rem;
    height: 63px; padding: 0 20px 0 14px;
    background: var(--surface); border-bottom: 1px solid var(--line);
  }
  .app-mbar .mbar-logo { height: auto; padding: 0; gap: .45rem; margin-right: auto; min-width: 0; }
  .app-mbar .logo-mark { width: 30px; height: 32px; }
  .app-mbar .logo-text { font-size: 1.05rem; font-weight: 700; letter-spacing: -.01em; }

  .app-side {
    position: fixed; inset: 0 auto 0 0; z-index: 50;
    width: min(18rem, 86vw); height: 100%;
    padding-bottom: calc(.9rem + env(safe-area-inset-bottom, 0px));
    overflow-y: auto; overscroll-behavior: contain;
    transform: translateX(-100%); visibility: hidden;
    transition: transform .2s ease-out, visibility 0s linear .2s;
  }
  .app.side-open .app-side {
    transform: none; visibility: visible; box-shadow: var(--elev-3);
    transition: transform .2s ease-out;
  }
  .app.side-open .app-scrim { display: block; }
  /* The drawer scrolls as a whole, so the nav keeps its full height rather
     than shrinking into a scroll box of its own. */
  .app-side .side-nav, .app-side .side-foot { flex: none; overflow: visible; mask-image: none; }
  /* The bar carries the account menu; one copy is enough. */
  .app-side .account-menu { display: none; }

  /* Sticky belongs to the bar now; two sticky rows would stack. */
  .app-topbar { position: static; }
  .app-topbar, .app-content { padding-inline: 1rem; }
  /* The greeting keeps its line and the analyse field takes the next one,
     full width, rather than squeezing both into a row wider than the phone. */
  .app-topbar { height: auto; min-height: var(--app-head); flex-wrap: wrap; row-gap: .5rem; padding-block: .6rem; }
  /* Without script the field wraps onto its own line under the title. */
  html:not(.has-js) .topbar-right:has(.topbar-analyze),
  .topbar-right:has(.gs-bar) { flex: 1 1 100%; min-width: 0; }
  html:not(.has-js) .topbar-actions:has(.topbar-analyze) { flex: 1; min-width: 0; display: flex; }

  /* With it, one row: the title, a folded Analyze button, the bell and the
     theme. Pressing Analyze gives the whole row to the field. */
  .has-js .app-topbar:has(.topbar-analyze) { flex-wrap: nowrap; }
  .has-js .app-topbar h1 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .has-js .topbar-analyze-open { display: inline-flex; }
  .has-js .app-topbar:not(.is-analyzing) .topbar-analyze { display: none; }
  .has-js .app-topbar.is-analyzing h1,
  .has-js .app-topbar.is-analyzing .topbar-analyze-open,
  .has-js .app-topbar.is-analyzing .topbar-right > :not(.topbar-actions) { display: none; }
  .has-js .app-topbar.is-analyzing .topbar-right,
  .has-js .app-topbar.is-analyzing .topbar-actions { flex: 1; min-width: 0; display: flex; }
  .has-js .topbar-analyze-close { display: grid; }
  .topbar-analyze { flex: 1; min-width: 0; }
  .topbar-analyze input { width: auto; max-width: none; flex: 1; min-width: 0; }
  /* Sort and search take their own line under the section heading. */
  .dash-section-head .table-tools { flex: 1 1 100%; }
  .table-tools .tools-search { flex: 1; }
  .tools-search input { width: 100%; max-width: none; }
  /* Hidden panels still widen the page, so this one stays inside it. */
  .help-tip-body { width: min(19rem, 60vw); }
  .plan-grid { grid-template-columns: 1fr; }
}

/* A feature the plan includes but that is not finished. Quieter than the lock:
   a lock says "not yours", soon says "not yet", and they are different news. */
.side-soon {
  flex: none; padding: .05rem .3rem; border-radius: 4px;
  font-size: .58rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  background: var(--surface-2); color: var(--ink-3); border: 1px solid var(--line);
}
.side-link.is-soon span:first-of-type { color: var(--ink-3); }
.side-link.is-on .side-soon { background: var(--surface); border-color: transparent; }

/* The panel behind an unbuilt section. */
.soon-panel {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: .5rem; padding: 3rem 1.5rem;
  background: var(--surface); border: 1px dashed var(--line); border-radius: 16px;
}
.soon-mark {
  display: grid; place-items: center; width: 40px; height: 40px;
  border-radius: 50%; background: var(--surface-2); color: var(--ink-3);
}
.soon-mark svg { width: 20px; height: 20px; }
.soon-panel h2 { margin: .3rem 0 0; font-size: 1.1rem; font-weight: 800; letter-spacing: -.02em; }
.soon-panel p { margin: 0; max-width: 52ch; font-size: .88rem; line-height: 1.65; color: var(--ink-2); }
.soon-foot { margin-top: .6rem !important; font-size: .78rem !important; color: var(--ink-3) !important; }
.soon-foot a { color: var(--brand-700); font-weight: 700; }

.dash-export {
  font-size: .78rem; font-weight: 700; color: var(--brand-700);
  text-decoration: none; white-space: nowrap;
}
.dash-export:hover { text-decoration: underline; }

/* The branding preview behind its gate. */
.brand-preview { display: flex; flex-direction: column; gap: .7rem; }
.brand-row { display: flex; align-items: center; gap: .75rem; }
.brand-row b { display: block; font-size: .86rem; }
.brand-row span span { display: block; font-size: .76rem; color: var(--ink-3); }
.brand-swatch {
  flex: none; width: 34px; height: 34px; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.brand-swatch.is-color { background: var(--brand-600); border-color: transparent; }

/* ---- sidebar foot: plan card and account menu ---- */

.side-foot { display: flex; flex-direction: column; gap: .5rem; }

/* The account group sits in the pinned foot: it is the same wherever you are,
   so it should not scroll away under the workspace's own links. */
.side-account { display: flex; flex-direction: column; gap: .1rem; }
.side-account .side-group { margin-top: 0; padding-top: .7rem; }

.side-card {
  padding: .65rem .7rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.side-card:hover { border-color: var(--brand-600); }
.side-card-top { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
.side-card-plan {
  font-size: .78rem; font-weight: 800; letter-spacing: -.01em;
  color: var(--ink); text-decoration: none;
}
.side-card:hover .side-card-plan { color: var(--brand-700); }
.side-card-note { font-size: .66rem; color: var(--ink-3); }

.side-meter {
  height: 4px; margin: .45rem 0 .4rem; border-radius: 999px;
  background: color-mix(in srgb, var(--ink-3) 18%, transparent); overflow: hidden;
}
.side-meter span { display: block; height: 100%; border-radius: 999px; background: var(--brand-600); }

/* The bar earns a colour only when it is worth acting on. */
.side-meter.is-warm span { background: var(--amber-fill); }
.side-meter.is-high span { background: var(--red-fill); }

.side-card-foot { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
.side-card-foot:has(> :only-child) { justify-content: flex-start; }
.side-card-left { font-size: .68rem; color: var(--ink-3); text-decoration: none; }
.side-card-cta {
  position: relative; z-index: 1;
  font-size: .68rem; font-weight: 700; color: var(--brand-700); text-decoration: none;
}
.side-card-cta:hover { text-decoration: underline; }

/* A <details> dropdown: the browser handles open, close, focus and Escape,
   and it works before the JavaScript loads, which matters for "sign out". */
.account-menu { position: relative; }
.account-menu summary {
  display: flex; align-items: center; gap: .55rem;
  padding: .45rem .5rem; border-radius: 9px;
  cursor: pointer; list-style: none;
}
.account-menu summary::-webkit-details-marker { display: none; }
.account-menu summary:hover { background: var(--surface-2); }
.account-menu[open] summary { background: var(--surface-2); }

.account-id { flex: 1; min-width: 0; }
.account-id b {
  display: block; font-size: .8rem; font-weight: 700; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.account-id span { display: block; font-size: .68rem; color: var(--ink-3); }
.account-caret { width: 14px; height: 14px; flex: none; color: var(--ink-3); transition: transform .18s ease; }
.account-menu[open] .account-caret { transform: rotate(180deg); }

/* Opens upward: the menu lives at the bottom of the viewport. */
.account-pop {
  position: absolute; bottom: calc(100% + .4rem); left: 0; right: 0;
  padding: .35rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: var(--elev-3); z-index: 20;
}
.account-email {
  margin: 0; padding: .4rem .55rem .5rem;
  font-size: .72rem; color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.menu-group { padding-top: .25rem; border-top: 1px solid var(--line-2); }
.menu-group:first-of-type { border-top: 0; padding-top: 0; }

.menu-item {
  display: flex; align-items: center; gap: .55rem;
  padding: .45rem .55rem; border-radius: 8px;
  font-size: .82rem; font-weight: 600; color: var(--ink-2); text-decoration: none;
}
.menu-item svg { width: 15px; height: 15px; flex: none; opacity: .75; }
.menu-item:hover { background: var(--surface-2); color: var(--ink); }
.menu-item.is-accent { color: var(--brand-700); }
.menu-item.is-accent svg { opacity: 1; }
.menu-item.is-quiet { color: var(--ink-3); }

/* Marks a link that leaves for a new tab, so the jump is expected rather
   than something that just happens. Pushed to the trailing edge and smaller
   than the leading icon: it labels the link, it is not the link's subject. */
.menu-item .menu-out {
  width: 12px; height: 12px; margin-left: auto; opacity: .45;
}
.menu-item:hover .menu-out { opacity: .7; }

.plan-muted { color: var(--ink-3); font-weight: 400; }

@media (prefers-reduced-motion: reduce) {
  .account-caret { transition: none; }
}

/* ---- workspace management ---- */

/* Untracking appears on hover: it is a destructive-looking control on every
   row, and a row of crosses reads as a list of things to delete. */
.dash-untrack { margin: 0; }
.dash-untrack button {
  display: grid; place-items: center;
  width: 26px; height: 26px; padding: 0;
  background: none; border: 0; border-radius: 7px;
  color: var(--ink-3); cursor: pointer;
  transition: opacity .15s ease, background .15s ease, color .15s ease;
}
.dash-untrack svg { width: 14px; height: 14px; }
/*
 * Revealed on the row it belongs to.
 *
 * This only named .dash-row, which is the card list; the tracked-pages table
 * renders plain <tr>, so the selector never matched and the button sat at
 * opacity 0 forever. It was in the DOM and reachable by keyboard, and
 * invisible to everybody using a mouse: the only way to untrack a page was
 * to open its report.
 *
 * Kept quiet rather than always solid, so a row of results is not lined with
 * delete buttons, but it now has a resting state you can see.
 */
.dash-untrack button { opacity: .45; }

.dash-row:hover .dash-untrack button,
.dash-table tr:hover .dash-untrack button,
.dash-untrack button:focus-visible { opacity: 1; }
.dash-untrack button:hover { background: var(--red-100); color: var(--red-text); }

/* No hover on a touch screen, so there it is simply always visible. */
@media (hover: none) {
  .dash-untrack button { opacity: 1; }
}

.ws-form {
  margin: 0 0 1.5rem; padding: 1.1rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
}
.ws-form label {
  display: block; margin-bottom: .45rem;
  font-size: .78rem; font-weight: 700; color: var(--ink-2);
}
.ws-row { display: flex; gap: .5rem; }
.ws-row input {
  flex: 1; min-width: 0;
  font: inherit; font-size: .88rem; color: var(--ink);
  padding: .5rem .7rem; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.ws-row input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.ws-hint { margin: .55rem 0 0; font-size: .76rem; color: var(--ink-3); line-height: 1.5; }
.ws-hint a { color: var(--brand-700); font-weight: 700; }
.ws-limit { margin-bottom: 1.5rem; }

/* Deleting sits apart and reads as different: the same weight as the forms
   above it would make it look like another setting. */
.ws-form.is-danger { margin-top: 2rem; border-color: color-mix(in srgb, var(--red-text) 25%, var(--line)); }
.ws-form.is-danger label { color: var(--red-text); }
/* The ghost form: red text on the page's own background. */
.btn.ghost.is-danger {
  color: var(--red-text);
  border-color: color-mix(in srgb, var(--red-text) 35%, transparent);
}
.btn.ghost.is-danger:hover { background: var(--red-100); border-color: var(--red-text); }

/* The solid form, and only the solid form. Recolouring just the text left
   red on the brand blue, which is unreadable; setting a background without
   excluding .ghost left red on red, because the ghost rule above sets no
   background of its own to win with. */
.btn.is-danger:not(.ghost) {
  background: var(--red-text); color: #fff;
  border-color: transparent;
}
.btn.is-danger:not(.ghost):hover {
  background: color-mix(in srgb, var(--red-text) 85%, #000);
  color: #fff;
}

/* ---- the workspace chooser ---- */

.ws-index { max-width: 62rem; }
.ws-total { margin: 0 0 1.25rem; font-size: .86rem; color: var(--ink-2); }
.ws-total-critical { color: var(--red-text); }

.ws-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: .85rem; align-items: stretch;
}

.ws-card {
  display: flex; flex-direction: column; gap: .2rem;
  padding: 1.1rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
  text-decoration: none; color: inherit;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.ws-card:hover { border-color: var(--brand-600); box-shadow: var(--elev-1); }

.ws-card-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: .5rem; }
.ws-card-mark {
  display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px;
  background: var(--brand-600); color: #fff; font-size: .82rem; font-weight: 800;
}
.ws-card-name { font-size: .95rem; font-weight: 800; letter-spacing: -.015em; color: var(--ink); }
.ws-card-facts { font-size: .76rem; color: var(--ink-3); line-height: 1.5; }
.ws-card-facts a { color: var(--brand-700); font-weight: 700; }

.ws-card-flag {
  align-self: flex-start; margin-top: .6rem;
  padding: .12rem .45rem; border-radius: 999px;
  font-size: .7rem; font-weight: 700;
  background: var(--red-100); color: var(--red-text);
}
.ws-card-empty { margin-top: .6rem; font-size: .72rem; color: var(--ink-3); }

/* Adding one is the same kind of action as opening one, so it sits in the
   grid rather than above it. */
.ws-card.is-new {
  border-style: dashed; background: var(--surface-2);
  font: inherit; text-align: left; cursor: pointer;
  align-items: flex-start;
}
.ws-card.is-new:hover { border-style: solid; border-color: var(--brand-600); box-shadow: none; }
.ws-card.is-new:hover .ws-card-plus { border-color: var(--brand-600); color: var(--brand-700); }
.ws-card.is-new:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }
.ws-card-plus {
  display: grid; place-items: center; width: 30px; height: 30px; border-radius: 8px;
  background: var(--surface); border: 1px solid var(--line); color: var(--ink-3);
  margin-bottom: .5rem;
}
.ws-card-plus svg { width: 16px; height: 16px; }
.ws-card.is-limit { border-style: dashed; background: var(--surface-2); justify-content: center; }

/* The new-workspace dialog. Wider than the auth one because it carries a
   sentence of explanation rather than a single line. */
.ws-dialog {
  width: min(92vw, 27rem); padding: 1.5rem;
  border: 1px solid var(--line); border-radius: 16px;
  background: var(--surface); color: var(--ink);
  box-shadow: var(--elev-3);
}
.ws-dialog::backdrop { background: rgb(8 18 33 / .45); }
.ws-dialog h2 { margin: 0 0 .4rem; font-size: 1.1rem; font-weight: 800; letter-spacing: -.02em; }
.ws-dialog p { margin: 0 0 1.1rem; font-size: .85rem; line-height: 1.6; color: var(--ink-2); }
/* :not(.btn) because a label is not always a caption. The file picker wraps
   its hidden input in a label styled as a button, and this rule is two
   classes to that one, so it won the cascade and flattened the button back
   to a block. Excluding the button form leaves captions alone. */
.ws-dialog label:not(.btn) {
  display: block; margin-bottom: .35rem;
  font-size: .78rem; font-weight: 700; color: var(--ink-2);
}
/* Password too, or the one field in the product that guards something
   irreversible is the one that looks unfinished. Listed rather than using a
   bare "input" so the dropdown's own search box inside a dialog keeps its
   borderless shape. */
.ws-dialog input[type="text"],
.ws-dialog input[type="password"],
.ws-dialog input[type="email"] {
  width: 100%; box-sizing: border-box;
  font: inherit; font-size: .9rem; color: var(--ink);
  padding: .55rem .75rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.ws-dialog input[type="text"]:focus,
.ws-dialog input[type="password"]:focus,
.ws-dialog input[type="email"]:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.ws-dialog-actions { display: flex; justify-content: flex-end; gap: .5rem; margin-top: 1.25rem; }

/* ---- dropdown ----
 *
 * Stands in for a native select, which opens an OS popup that cannot be
 * styled, escapes its container, and cannot carry an icon or a second line.
 */
.dd { position: relative; }

.dd-button {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  box-sizing: border-box; padding: .5rem .65rem;
  font: inherit; font-size: .86rem; font-weight: 600; color: var(--ink);
  text-align: left; cursor: pointer;
  background: var(--surface-2);
  border: 1px solid var(--line); border-radius: 10px;
}
.dd-button:hover { border-color: var(--ink-3); }
.dd-button[aria-expanded="true"] { border-color: var(--brand-600); }
.dd-button:focus-visible {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.dd-button.is-empty .dd-value { color: var(--ink-3); font-weight: 500; }

.dd-icon { width: 16px; height: 16px; flex: none; color: var(--ink-3); }
.dd-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-caret { width: 14px; height: 14px; flex: none; color: var(--ink-3); transition: transform .18s ease; }
.dd-button[aria-expanded="true"] .dd-caret { transform: rotate(180deg); }

.dd-menu {
  position: absolute; z-index: 30; top: calc(100% + .35rem); left: 0; right: 0;
  padding: .3rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: var(--elev-3);
}

/* Direction is decided at open time, not by where the control sits: a menu
   pinned upward clips against the top of the viewport just as readily as a
   downward one clips against the bottom. */
.dd-menu.opens-up { top: auto; bottom: calc(100% + .35rem); }

.dd-search {
  display: flex; align-items: center; gap: .4rem;
  padding: .2rem .45rem .45rem;
  border-bottom: 1px solid var(--line-2); margin-bottom: .3rem;
}
.dd-search svg { width: 14px; height: 14px; flex: none; color: var(--ink-3); }

/* The dialog's own input rule is [type="text"] and this search box is one
   too, so it wins on specificity and drew a bordered field around a control
   meant to read as part of the menu. Matched rather than fought with
   !important, and the icon stays inside the row it labels. */
.dd-search input,
.ws-dialog .dd-search input[type="text"] {
  flex: 1; min-width: 0; width: auto; box-sizing: border-box;
  font: inherit; font-size: .84rem; color: var(--ink);
  background: none; border: 0; outline: 0; border-radius: 0;
  padding: .2rem 0; box-shadow: none;
}
.ws-dialog .dd-search input[type="text"]:focus {
  border: 0; box-shadow: none; outline: 0;
}

/* Room under the label, and between the field and the buttons below it. */
.ws-dialog input[type="password"] { margin-bottom: .2rem; }
.ws-dialog [data-password-error] {
  margin: .45rem 0 0; padding: 0; text-align: left;
  font-size: .78rem; color: var(--red-text);
}

.dd-list { max-height: 15rem; overflow-y: auto; }

/* The hidden attribute has to win. A browser's own [hidden] rule is a plain
   display:none on the element, which any display here beats, so filtering
   set the attribute and the row stayed on screen: typing narrowed nothing. */
.dd-item[hidden] { display: none !important; }

.dd-item {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  box-sizing: border-box; padding: .45rem .5rem;
  font: inherit; font-size: .84rem; color: var(--ink-2);
  text-align: left; cursor: pointer;
  background: none; border: 0; border-radius: 8px;
}
.dd-item:hover, .dd-item:focus-visible { background: var(--surface-2); color: var(--ink); outline: none; }
.dd-item.is-on { color: var(--brand-700); font-weight: 700; }

.dd-mark {
  display: grid; place-items: center; flex: none;
  width: 22px; height: 22px; border-radius: 6px;
  background: var(--brand-600); color: #fff; font-size: .66rem; font-weight: 800;
}
.dd-text { flex: 1; min-width: 0; }
.dd-label { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dd-note { display: block; font-size: .74rem; color: var(--ink-3); }
.dd-tick { width: 14px; height: 14px; flex: none; }

/* The empty state is centred and given the room a row would have taken, so
   the menu does not collapse to a thin sliver with a sentence jammed against
   its left edge. */
.dd-none {
  margin: 0; padding: 1.2rem .75rem;
  font-size: .82rem; color: var(--ink-3); text-align: center;
}

/* The list keeps its rows while filtering, hidden rather than removed, so
   :empty never matches: script marks it instead. Without this the hidden
   rows still contribute padding and read as a blank row under the message. */
.dd-list.is-empty { display: none; }

.dd-foot {
  display: block; margin-top: .3rem; padding: .45rem .5rem;
  border-top: 1px solid var(--line-2);
  font-size: .8rem; font-weight: 700; color: var(--brand-700); text-decoration: none;
}
.dd-foot:hover { text-decoration: underline; }

/* In the sidebar the button is the workspace, so it reads as a title. */
/* The switcher is the context everything under it belongs to, so it is
   separated from the nav rather than sitting flush against it. */
.side-switch { margin-bottom: .5rem; }
.side-switch .dd-button { background: var(--surface-2); font-weight: 700; }
.side-switch .dd-mark { width: 22px; height: 22px; }

@media (prefers-reduced-motion: reduce) {
  .dd-caret { transition: none; }
}

/* ---- report history ---- */

/* Wider than the 44rem reading measure: this page is an instrument, not
   prose. The run list and the chart both want horizontal room, and the diff
   rows read better with the label and its detail side by side. */
.hist-head, .hist-body { max-width: 62rem; margin-inline: auto; }
.hist-head { position: relative; padding-block: 1.75rem 0; }

/* The page is three sections that each answer a different question, so they
   are separated by a real gap rather than by card borders alone. */
/* No bottom padding of its own: .section already carries 5.5rem of margin
   at both ends, and adding padding on top of it doubled the gap before the
   footer. The top margin is trimmed because the header sits right above. */
.hist-body {
  display: flex; flex-direction: column; gap: 2.5rem;
  margin-block: 2.5rem 4.5rem;
}
.hist-back {
  display: inline-flex; align-items: center; gap: .4rem; margin-bottom: .9rem;
  font-size: .82rem; font-weight: 700; color: var(--ink-3); text-decoration: none;
}
.hist-back:hover { color: var(--brand-700); }
.hist-back svg { width: 15px; height: 15px; }

/* No wrapping: a long URL used to push the action onto its own line, where
   it sat alone across the full width. The identity column takes the slack and
   truncates, the button keeps its size. */
.hist-head-main {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem;
}
.hist-head-id { min-width: 0; }
.hist-eyebrow {
  display: block; margin-bottom: .25rem;
  font-size: .68rem; font-weight: 800; text-transform: uppercase;
  letter-spacing: .09em; color: var(--brand-600);
}
/* The whole URL, wrapped rather than truncated: this page is about one
   specific page, and the end of a path is often the part that identifies it.
   clamp() sets a long one smaller so it does not overwhelm the header, and
   anywhere lets it break inside a long slug instead of overflowing. */
.hist-head-main h1 {
  margin: 0; font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  font-weight: 800; letter-spacing: -.03em; line-height: 1.3;
  overflow-wrap: anywhere;
}
.hist-host { margin: .3rem 0 0; font-size: .82rem; color: var(--ink-3); }
.hist-latest { flex: none; }
.hist-latest svg { width: 15px; height: 15px; }

@media (max-width: 34rem) {
  .hist-head-main { flex-direction: column; align-items: stretch; gap: .9rem; }
  .hist-latest { align-self: flex-start; }
}

.hist-chart, .hist-diff {
  padding: 1.2rem; border-radius: 16px;
  background: var(--surface); border: 1px solid var(--line);
}
/* Spacing between sections is the container's gap, not each card's margin. */
.hist-chart-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;
}
.hist-diff-head { margin-bottom: 1rem; }

/* Today's score is the headline the chart is explaining, so it is set at
   display size and everything else on the card is secondary to it. */
.hist-chart-now {
  margin: .25rem 0 0; font-family: var(--display);
  font-size: 2.1rem; font-weight: 800; letter-spacing: -.04em; line-height: 1;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.hist-chart-now span {
  margin-left: .3rem; font-size: .8rem; font-weight: 600;
  letter-spacing: 0; color: var(--ink-3);
}

.hist-chart-stats { display: flex; gap: 1.5rem; }
.hist-stat { display: flex; flex-direction: column; gap: .15rem; text-align: right; }
.hist-stat b {
  font-family: var(--display); font-size: 1.05rem; font-weight: 800;
  letter-spacing: -.02em; color: var(--ink); font-variant-numeric: tabular-nums;
}
.hist-stat > span {
  font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-3);
}
.hist-chart-label {
  font-size: .68rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-3);
}
.hist-chart-span { margin: .2rem 0 0; font-size: .84rem; color: var(--ink-2); }

.hist-delta {
  font-family: var(--display); font-size: 1.1rem; font-weight: 800;
  letter-spacing: -.02em; color: var(--ink-3); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.hist-delta.is-up { color: var(--green-text); }
.hist-delta.is-down { color: var(--red-text); }

/* Drawn rather than charted with a library: one series, a few points, and a
   library would be more code than the chart. */
.hist-svg { display: block; width: 100%; height: auto; overflow: visible; }
.hist-axis-x, .hist-axis-y {
  font-size: 10px; font-weight: 600; fill: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* A dropline from each picked run to the axis, tying the chart to the two
   rows selected in the list below it. */
.hist-mark {
  stroke: var(--brand-600); stroke-width: 1.5;
  stroke-dasharray: 3 3; opacity: .45;
}
.hist-dot.is-picked { stroke-width: 3; fill: var(--surface); }

/* Invisible, wide hover targets: a 3.5px circle is not something a pointer
   can be expected to find. */
.hist-hit { fill: transparent; cursor: pointer; outline: none; }
.hist-hit:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 1px; }

.hist-chart { position: relative; }
/* display:flex would override the hidden attribute, so the hidden state is
   restated explicitly. */
.hist-tip[hidden] { display: none; }
.hist-tip {
  position: absolute; z-index: 5; transform: translate(-50%, -100%);
  display: flex; flex-direction: column; gap: .1rem;
  padding: .5rem .7rem; border-radius: 10px; pointer-events: none;
  background: var(--ink); color: var(--surface);
  box-shadow: 0 8px 20px rgba(15, 23, 32, .18);
  white-space: nowrap;
}
.hist-tip::after {
  content: ''; position: absolute; left: 50%; top: 100%;
  transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: var(--ink);
}
.hist-tip-head { display: flex; align-items: baseline; gap: .4rem; }
.hist-tip-score {
  font-family: var(--display); font-size: 1.05rem; font-weight: 800;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.hist-tip-delta {
  font-size: .76rem; font-weight: 700; opacity: .75;
  font-variant-numeric: tabular-nums;
}
.hist-tip-delta.is-up { color: var(--green-fill); opacity: 1; }
.hist-tip-delta.is-down { color: var(--red-fill); opacity: 1; }
.hist-tip-when { font-size: .74rem; opacity: .8; }
.hist-tip-crit { font-size: .7rem; font-weight: 700; opacity: .75; }
.hist-tip-crit.is-bad { color: var(--red-fill); opacity: 1; }
.hist-grid { stroke: var(--line-2); stroke-width: 1; }
.hist-area { fill: color-mix(in srgb, var(--brand-500) 9%, transparent); stroke: none; }
.hist-line { fill: none; stroke: var(--brand-600); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.hist-dot { fill: var(--surface); stroke: var(--brand-600); stroke-width: 2; }
.hist-dot.t-a, .hist-dot.t-a-plus, .hist-dot.t-a-minus { stroke: var(--green-fill); }
.hist-dot.t-f,
.hist-dot.t-d, .hist-dot.t-d-plus, .hist-dot.t-d-minus { stroke: var(--red-fill); }

/* The verdict, which is the answer the page exists to give. */
.hist-verdict {
  margin: .3rem 0 0; font-family: var(--display);
  font-size: 1.25rem; font-weight: 800; letter-spacing: -.03em;
  line-height: 1.3; color: var(--ink); text-wrap: balance;
}
/* The score move and the tallies on one line: they answer the same question
   at two levels of detail, so they read together. The dates moved up to the
   section subheading, where they say which comparison this is. */
.hist-diff-meta {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .9rem;
  margin-top: .7rem;
}
.hist-diff-scores {
  display: inline-flex; align-items: center; gap: .45rem;
  font-size: .95rem; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Read as a shape before it is read as a sentence: three green and one red
   is a different story from one green and three red. */
.hist-tallies { display: flex; gap: .4rem; }
.hist-tally {
  padding: .2rem .55rem; border-radius: 999px;
  font-size: .72rem; font-weight: 700;
}
.hist-tally.is-good { background: var(--green-100); color: var(--green-text); }
.hist-tally.is-bad { background: var(--red-100); color: var(--red-text); }

/* Where the total moved, category by category. */
.hist-cats {
  margin: 1.1rem 0 1.5rem; padding: 1rem 0 0; border-top: 1px solid var(--line);
}
.hist-cat-list {
  list-style: none; margin: .7rem 0 0; padding: 0;
  display: flex; flex-direction: column; gap: .5rem;
}
.hist-cat {
  display: grid; grid-template-columns: minmax(0, 13rem) 1fr 5.5rem 2.6rem;
  align-items: center; gap: .9rem;
}
.hist-cat-name {
  min-width: 0; font-size: .82rem; font-weight: 600; color: var(--ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.hist-cat-track {
  position: relative; height: 6px; border-radius: 999px;
  background: var(--surface-2); overflow: hidden;
}
/* The score that held, then the span that moved on top of it. */
.hist-cat-base {
  position: absolute; inset-block: 0; left: 0;
  background: var(--line); border-radius: 999px;
}
.hist-cat-move { position: absolute; inset-block: 0; border-radius: 999px; }
.hist-cat.is-up .hist-cat-move { background: var(--green-fill); }
.hist-cat.is-down .hist-cat-move { background: var(--red-fill); }

.hist-cat-scores {
  font-size: .78rem; color: var(--ink-3); text-align: right;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.hist-cat-scores b { color: var(--ink); font-weight: 700; }
.hist-cat-delta {
  font-size: .78rem; font-weight: 700; text-align: right;
  color: var(--ink-3); font-variant-numeric: tabular-nums;
}
.hist-cat.is-up .hist-cat-delta { color: var(--green-text); }
.hist-cat.is-down .hist-cat-delta { color: var(--red-text); }

@media (max-width: 44rem) {
  .hist-cat { grid-template-columns: minmax(0, 1fr) 4.5rem 2.4rem; }
  .hist-cat-track { grid-column: 1 / -1; order: 3; }
}

.hist-none { margin: 0; font-size: .86rem; color: var(--ink-3); }

.hist-changes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .45rem; }
.hist-change {
  display: grid; grid-template-columns: 5.5rem 1fr auto; gap: .75rem; align-items: baseline;
  padding: .6rem .75rem; border-radius: 10px; background: var(--surface-2);
}
.hist-change.is-bad { background: color-mix(in srgb, var(--red-100) 55%, var(--surface)); }
.hist-change.is-good { background: color-mix(in srgb, var(--green-100) 45%, var(--surface)); }
.hist-change-tag {
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
}
.hist-change.is-good .hist-change-tag { color: var(--green-text); }
.hist-change.is-bad .hist-change-tag { color: var(--red-text); }
.hist-change-body b { display: block; font-size: .86rem; color: var(--ink); }

/* Which part of the score this check belongs to, tying the row back to the
   breakdown above it. Set in the top right corner so the rows read down the
   left edge without the category interrupting the check name. */
.hist-change-cat {
  grid-column: 3; grid-row: 1; align-self: start; justify-self: end;
  font-size: .66rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-3); white-space: nowrap;
}
.hist-change-body span { display: block; font-size: .78rem; color: var(--ink-2); line-height: 1.5; margin-top: .1rem; }

/* What this check has done across every run. Amber rather than red: a fix
   that broke before is a caution, not a failure, and the row already carries
   its own pass or fail colour. */
.hist-change-body .hist-streak {
  display: inline-flex; align-items: center; gap: .3rem;
  margin-top: .35rem; padding: .15rem .45rem; border-radius: 999px;
  font-size: .7rem; font-weight: 700; line-height: 1.4;
  background: var(--amber-100); color: var(--amber-text);
}
.hist-streak svg { width: 12px; height: 12px; flex: none; }

/* Five rows, then scroll. The list is a picker, not the content: a page
   analysed weekly for a year would otherwise push the comparison it controls
   somewhere nobody scrolls to. The max-height is five rows plus their gaps,
   and the padding keeps the focus ring and picked border off the scroll edge.

   Capped this way the whole page height is fixed, so selecting a run never
   moves anything above it. */
.hist-runs {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: .4rem;
}
.hist-runs.is-scrollable {
  /* Five rows plus their gaps, and half a row of the sixth left showing so
     the list reads as scrollable without needing a scrollbar to say so. */
  max-height: calc(5.5 * 3.55rem + 5 * .4rem);
  overflow-y: auto; overscroll-behavior: contain;
  /* Room for a picked row's outline and focus ring at the scroll edges,
     pulled back out so the list still lines up with the cards above. */
  padding: 3px; margin: -3px;
  scrollbar-width: thin;
}
.hist-runs.is-scrollable::-webkit-scrollbar { width: 6px; }
.hist-runs.is-scrollable::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: 999px;
}
.hist-runs.is-scrollable::-webkit-scrollbar-thumb:hover { background: var(--ink-3); }
/* Fixed tracks for the last three columns, not auto: the oldest run has no
   delta to show, and an auto track collapses to nothing there, dragging its
   button out of line with every row above it. */
.hist-run {
  display: grid; grid-template-columns: 2.6rem 1fr auto 3rem 9.5rem;
  align-items: center; gap: .8rem;
  padding: .6rem .8rem; border-radius: 10px;
  background: var(--surface); border: 1px solid var(--line);
}
/* A picked row is outlined rather than given a thicker border: a border
   change would resize the box and shift every row below it, and an outline
   is drawn outside the box so it never clips against a neighbour or the
   scroll edge. */
.hist-run.is-picked {
  border-color: var(--brand-600);
  outline: 1px solid var(--brand-600);
  outline-offset: -1px;
}
.hist-run-when b { display: block; font-size: .86rem; }
.hist-run-when span { display: block; font-size: .74rem; color: var(--ink-3); }
.hist-run-delta {
  font-size: .8rem; font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums; text-align: right;
}
.hist-run-delta.is-up { color: var(--green-text); }
.hist-run-delta.is-down { color: var(--red-text); }

/* One toggle per run. A selected run stays clickable so the pair can be
   broken from either side, which is why it keeps the link affordance rather
   than reading as a disabled state. */
.hist-pick {
  grid-column: 5; justify-self: stretch; text-align: center;
  white-space: nowrap;
  padding: .28rem .7rem; border-radius: 999px;
  font-size: .72rem; font-weight: 700; color: var(--ink-3);
  text-decoration: none; border: 1px solid var(--line);
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.hist-pick:hover { background: var(--surface-2); color: var(--ink); border-color: var(--ink-3); }
.hist-pick.is-on { background: var(--brand-600); color: #fff; border-color: transparent; }
.hist-pick.is-on:hover { background: var(--brand-700); color: #fff; }
.hist-pick:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }

/* Mid-selection: one run held, waiting for its partner. Outlined rather than
   filled, because it is a pending state, not a finished comparison. */
.hist-pick.is-held {
  background: var(--surface); color: var(--brand-700);
  border-color: var(--brand-600); border-width: 1.5px;
}
.hist-pick.is-held:hover { background: var(--brand-100); color: var(--brand-700); }

.hist-choose {
  padding: 1.1rem 1.25rem; border-radius: 14px; text-align: center;
  background: var(--surface); border: 1px dashed var(--line);
}
.hist-choose p { margin: 0; font-size: .88rem; color: var(--ink-2); }
.hist-choose b { color: var(--ink); }

/* Its own heading rather than .section-head, which is a centred 45rem
   marketing block: here it labels a list that runs the full 62rem, so an
   inset heading reads as a different column from the rows under it. */
.hist-runs-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; margin: 0 0 1rem; max-width: none; text-align: left;
}
.hist-runs-head h2 {
  margin: 0; font-size: 1.35rem; font-weight: 800; letter-spacing: -.03em;
}
.hist-runs-note { margin: 0; font-size: .78rem; color: var(--ink-3); }

.hist-diff-actions { display: flex; align-items: center; gap: .9rem; }
.hist-export { padding: .3rem .7rem; font-size: .76rem; }
.hist-export svg { width: 14px; height: 14px; }

.hist-gate {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: .5rem; padding: 2.5rem 1.5rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 16px;
}
.hist-gate h2 { margin: .3rem 0 0; font-size: 1.1rem; font-weight: 800; letter-spacing: -.02em; }
.hist-gate p { margin: 0 0 .6rem; max-width: 48ch; font-size: .88rem; line-height: 1.6; color: var(--ink-2); }

@media (max-width: 40rem) {
  .hist-run { grid-template-columns: 2.6rem 1fr auto; row-gap: .4rem; }
  .hist-pick { grid-column: 2 / -1; justify-self: start; text-align: left; }
  .hist-change { grid-template-columns: 1fr auto; gap: .2rem .5rem; }
  .hist-change-body { grid-column: 1 / -1; }
}


/* ---- dashboard forms ---- */

/* The card fills the content area like every other page's does. Capping it
   made this one page narrower than the rest, which reads as a mistake rather
   than as a choice. The measure belongs on the fields inside, where a text
   input the full width of a wide screen really is harder to read. */
.dash-form-wrap { max-width: none; }
.dash-form > * { max-width: 40rem; }
/* The closing rule is the card's, not the form measure's: stopped at 40rem
   it read as a line that ran out rather than as a divider. */
.dash-form > .form-actions { max-width: none; }

.dash-form {
  padding: 1.5rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.form-head { margin-bottom: 1.4rem; }
.form-head h2 { margin: 0 0 .35rem; font-size: 1.05rem; font-weight: 800; letter-spacing: -.02em; }
.form-head p { margin: 0; font-size: .84rem; line-height: 1.55; color: var(--ink-2); }

.field { margin-bottom: 1.2rem; }
.field > label {
  display: block; margin-bottom: .35rem;
  font-size: .8rem; font-weight: 700; color: var(--ink);
}
.field input[type="text"] {
  width: 100%; max-width: 26rem;
  font: inherit; font-size: .86rem; color: var(--ink);
  padding: .55rem .7rem; border-radius: 9px;
  background: var(--surface); border: 1px solid var(--line);
}
.field input[type="text"]:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px var(--brand-100);
}
.field input[type="file"] { font-size: .8rem; color: var(--ink-2); max-width: 100%; }
.field-note { display: block; margin-top: .35rem; font-size: .75rem; color: var(--ink-3); line-height: 1.5; }

/* The swatch and its value together: a colour input alone shows the choice
   but not the number, which is the half someone pastes from a brand guide. */
.field-colour { display: flex; align-items: center; gap: .6rem; }
.field-colour input[type="color"] {
  width: 46px; height: 34px; padding: 2px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--line); border-radius: 9px;
}
.colour-value {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: .8rem; color: var(--ink-2); text-transform: uppercase;
}

/* The logo, its constraints and its actions in one box, because they are
   all about the same thing. Three stacked lines and a browser button read as
   three unrelated controls. */
.logo-field {
  display: flex; align-items: center; gap: 1rem;
  max-width: 26rem; padding: .85rem;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 10px;
}

/* A fixed frame, so choosing a wide logo does not resize the form. The
   chequerboard shows transparency, which is what most logos have and what
   the PDF will sit them on. */
.logo-preview {
  display: grid; place-items: center; flex: none;
  width: 92px; height: 60px; padding: .35rem;
  border-radius: 8px; overflow: hidden;
  background: var(--surface);
  background-image:
    linear-gradient(45deg, var(--surface-2) 25%, transparent 25%),
    linear-gradient(-45deg, var(--surface-2) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--surface-2) 75%),
    linear-gradient(-45deg, transparent 75%, var(--surface-2) 75%);
  background-size: 10px 10px;
  background-position: 0 0, 0 5px, 5px -5px, -5px 0;
  border: 1px solid var(--line);
}
.logo-preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.logo-preview img[hidden] { display: none; }
.logo-placeholder { display: grid; place-items: center; color: var(--ink-3); }
.logo-placeholder[hidden] { display: none; }
.logo-placeholder svg { width: 24px; height: 24px; }

.logo-body { min-width: 0; flex: 1; }
.logo-state { margin: 0; font-size: .82rem; font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }
.logo-state.is-bad { color: var(--red-text); }
.logo-hint { margin: .15rem 0 .55rem; font-size: .74rem; color: var(--ink-3); line-height: 1.45; }

.logo-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

/* The label is the button, and the input behind it is hidden rather than
   removed: a file input still has to be in the form to be submitted, and it
   still has to be reachable by keyboard. */
.logo-pick, .logo-remove {
  height: 30px; padding: 0 .7rem; gap: .35rem;
  font-size: .76rem; border-radius: 8px; cursor: pointer;
}
.logo-pick svg, .logo-remove svg { width: 13px; height: 13px; }
.logo-pick input[type="file"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;
}
.logo-pick:focus-within { border-color: var(--brand-600); box-shadow: 0 0 0 3px var(--brand-100); }
.logo-remove[hidden] { display: none; }
.logo-remove:hover { border-color: var(--red-fill); color: var(--red-text); }

/* Separated from the last field, so the buttons read as the end of the form
   rather than as one more thing in the list of things to fill in. */
.form-actions {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  margin-top: 1.9rem; padding-top: 1.4rem;
  border-top: 1px solid var(--line-2);
}

/* The global .btn is sized for a marketing page, where it is the only thing
   on screen worth pressing. In a form it sits under a column of inputs and
   should match them rather than tower over them. */
.dash-form .btn {
  height: 38px; padding: 0 1.1rem;
  font-size: .85rem; border-radius: 9px;
}

/* Outcomes. Both are ruled on the left rather than filled, so a saved
   message does not shout as loudly as a problem. */
.form-errors, .form-saved {
  margin-bottom: 1rem; padding: .8rem 1rem; border-radius: 10px;
  font-size: .84rem; border-left: 3px solid;
}
.form-errors { background: var(--red-100); color: var(--red-text); border-left-color: var(--red-fill); }
.form-errors b { display: block; margin-bottom: .3rem; }
.form-errors ul { margin: 0; padding-left: 1.1rem; }
.form-saved {
  display: flex; align-items: flex-start; gap: .5rem;
  background: var(--green-100); color: var(--green-text); border-left-color: var(--green-fill);
}
.form-saved svg { width: 15px; height: 15px; flex: none; margin-top: .15rem; }


/* ---- admin console ---- */

/* The badge that says which area this is. Amber rather than the brand
   colour, so a glance at the sidebar cannot confuse the console with the
   application it administers. */
.side-logo .logo-tag.is-staff {
  background: var(--amber-100); color: var(--amber-text);
}

/* A control inside a table row, sized to the row rather than to a form. */
.admin-inline { display: flex; align-items: center; gap: .35rem; }
.admin-inline select {
  appearance: none; height: 30px; padding: 0 1.6rem 0 .6rem;
  font: inherit; font-size: .78rem; font-weight: 600; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
  cursor: pointer;
  /* The chevron is drawn here because a native select arrow is a different
     shape on every system, and this one sits in a row of our own controls. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2377828F' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .45rem center;
  background-size: 13px 13px;
}
.admin-inline select:hover { border-color: var(--ink-3); }
.admin-inline select:focus {
  outline: none; border-color: var(--brand-600); box-shadow: 0 0 0 3px var(--brand-100);
}
.admin-inline .btn { height: 30px; padding: 0 .65rem; font-size: .76rem; border-radius: 8px; }

/* Removing somebody's access is not the same kind of act as granting it. */
.admin-inline .btn.is-danger { color: var(--red-text); }
.admin-inline .btn.is-danger:hover { border-color: var(--red-fill); background: var(--red-100); }

/* Marks the console's own users in a list of everybody. */
.admin-badge {
  display: inline-block; margin-left: .35rem; padding: .1rem .35rem;
  border-radius: 5px; vertical-align: middle;
  font-size: .62rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em;
  background: var(--amber-100); color: var(--amber-text);
}

/* Where a control would be for anybody else. Reads as an explanation rather
   than as a disabled button nobody can use. */
.admin-you { font-size: .78rem; color: var(--ink-3); }

/* The console link in the account menu, marked so it reads as a different
   kind of destination from the account's own settings. */
.menu-item.is-staff { color: var(--amber-text); }
.menu-item.is-staff svg { color: var(--amber-text); }

/* ---- invitations, members, notifications ---- */

/* The seat meter. States a number before offering to spend it, because the
   question an owner is actually asking is whether to buy more. */
.dash-seats {
  margin: 0 0 .9rem; font-size: .82rem; color: var(--ink-2);
}
.dash-seats b { color: var(--ink); }


/* The link, shown once. Monospaced and selected on click, because the one
   thing anybody does with it is copy it. */
.invite-sent { display: block; }
.invite-sent p { margin: .4rem 0 .5rem; font-size: .83rem; }
.invite-link input {
  width: 100%; padding: .5rem .6rem; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface-2);
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: .78rem; color: var(--ink);
}

/*
 * The row was a three column grid, built for a name, a detail and one thing
 * on the end. It grew a role control and two actions, which all landed in
 * that last cell on top of each other.
 *
 * Flex instead, so the controls lay themselves out in a row and the name
 * takes whatever is left. A grid would need a column count that changes
 * with the role, since an owner's row has no actions and a member's has
 * three.
 */
.dash-members li {
  position: relative;
  display: flex; flex-wrap: wrap;
}
.dash-members li > .dash-member { flex: 1; min-width: 8rem; }
.dash-members li > form { flex: none; }
.dash-member-act { margin-left: 0; }
.dash-avatar.is-pending {
  display: grid; place-items: center;
  background: var(--surface-2); color: var(--ink-3);
}
.dash-avatar.is-pending svg { width: 15px; height: 15px; }

/* A paused seat is still listed: this is the page where an owner finds out a
   downgrade cost somebody their access, so it says so rather than dropping
   them from the list. */
.dash-members li.is-suspended .dash-member b,
.dash-members li.is-suspended .dash-member span { color: var(--ink-3); }
.dash-members li.is-suspended .dash-avatar { opacity: .5; }
.dash-members li.is-suspended .dash-role {
  background: var(--amber-100); color: var(--amber-text); border-color: transparent;
}

.invite-accept { margin: 0 0 1rem; }

/* The bell. Positioned relative to the topbar rather than the button, so the
   panel has somewhere to hang on a narrow screen. */
.notice-menu { position: relative; }
.notice-menu summary {
  display: grid; place-items: center; position: relative;
  width: 34px; height: 34px; border-radius: 9px;
  color: var(--ink-2); cursor: pointer; list-style: none;
  transition: background .2s ease, color .2s ease;
}
.notice-menu summary::-webkit-details-marker { display: none; }
.notice-menu summary:hover { background: var(--surface-2); color: var(--ink); }
.notice-menu[open] summary { background: var(--surface-2); color: var(--ink); }
.notice-menu summary svg { width: 17px; height: 17px; }

.notice-dot {
  position: absolute; top: 2px; right: 2px;
  min-width: 15px; height: 15px; padding: 0 3px;
  display: grid; place-items: center;
  border-radius: 999px; background: var(--brand-600); color: #fff;
  font-size: .6rem; font-weight: 800; line-height: 1;
}

.notice-pop {
  position: absolute; top: calc(100% + .4rem); right: 0;
  width: min(21rem, calc(100vw - 2rem));
  border-radius: 12px; background: var(--surface);
  border: 1px solid var(--line); box-shadow: var(--elev-3); z-index: 30;
}
.notice-head {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  padding: .6rem .7rem; border-bottom: 1px solid var(--line-2);
  font-size: .8rem;
}
.notice-clear button {
  border: 0; background: none; padding: 0; cursor: pointer;
  font: inherit; font-size: .74rem; font-weight: 600; color: var(--brand-700);
}
.notice-clear button:hover { text-decoration: underline; }

.notice-list { margin: 0; padding: .25rem; list-style: none; max-height: 22rem; overflow-y: auto; }
.notice { border-radius: 8px; }
.notice a, .notice { display: block; padding: .5rem .55rem; text-decoration: none; color: inherit; }
.notice a:hover { background: var(--surface-2); }
.notice b { display: block; font-size: .8rem; font-weight: 650; color: var(--ink); }
.notice span { display: block; font-size: .76rem; color: var(--ink-2); }
.notice-when { margin-top: .15rem; font-size: .7rem !important; color: var(--ink-3) !important; }

/* Unread carries a mark rather than a background: a list where most rows are
   tinted reads as an error state. */
.notice.is-fresh b { font-weight: 800; }
.notice.is-fresh b::before {
  content: ""; display: inline-block; vertical-align: middle;
  width: 6px; height: 6px; margin-right: .4rem; border-radius: 50%;
  background: var(--brand-600);
}

/* The shared dropdown, sized to a table row rather than to a form. */
.team-access .dd-button {
  height: 28px; padding: 0 .45rem 0 .6rem;
  font-size: .76rem; font-weight: 600;
  border-radius: 7px;
}

/* Where the dropdown stands beside buttons it takes their height, or the
   row reads as two controls of different sizes that happen to be adjacent.
   38px is what .btn-sm settled on. */
.dash-member-role .dd-button,
.team-pick .dd-button,
.admin-inline .dd-button {
  height: 38px; padding: 0 .6rem 0 .75rem;
  font-size: .8rem; font-weight: 600;
  border-radius: var(--radius);
}
/* Wider than the control that opens it, because the useful half of a role is
   the sentence explaining it, and a menu sized to a narrow button wraps
   every description into a column of one-word lines. Anchored right so the
   extra width grows back over the row rather than off the edge of the card. */
.dash-member-role .dd-menu,
.team-access .dd-menu,
.team-pick .dd-menu,
.team-add .dd-menu,
.admin-inline .dd-menu { min-width: 17rem; left: auto; right: 0; }

/* The workspace picker is the wide control in its row, so its menu opens
   from the left instead: anchoring it right would push it off the dialog. */
.team-add .dd:first-of-type .dd-menu { left: 0; right: auto; }

.dash-member-role .dd { flex: none; width: 8rem; }
.admin-inline .dd { flex: none; width: 9rem; }

/* Actions sit together at the end of the row, and only the first one pushed
   away from the role: a gap before each would space them unevenly. */
/* The role control ends the name's run, so everything after it is an
   action and they sit together on the right. */
.dash-members li .dash-member-role { margin-left: auto; }

/* ---- the team page ---- */

/* A person is a row, and their workspaces sit under them: the shape says
   "one colleague, several clients", which is what a seat is. */
.team-list { margin: 0; padding: 0; list-style: none; display: grid; gap: .5rem; }
.team-person {
  padding: .7rem .8rem; border-radius: 10px;
  border: 1px solid var(--line); background: var(--surface);
}
.team-who { display: flex; align-items: center; gap: .6rem; }
.team-who .dash-member { flex: 1; min-width: 0; }
/* Pushed to the end and not allowed to shrink: without flex:none the form
   narrows under a long address and the button wraps its own label. */
.team-remove { margin-left: auto; flex: none; }

.team-spaces {
  display: flex; flex-wrap: wrap; gap: .4rem;
  margin-top: .6rem; padding-left: 2.4rem;
}
.team-person.is-suspended .team-who .dash-member b,
.team-person.is-suspended .team-who .dash-member span { color: var(--ink-3); }
.team-person.is-suspended .dash-avatar { opacity: .5; }

/* One workspace, its role, and a way off it. Grouped so the three read as
   one control rather than three things that happen to be adjacent. */
.team-access {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .2rem .25rem .2rem .55rem; border-radius: 8px;
  border: 1px solid var(--line-2); background: var(--surface-2);
}
.team-access.is-suspended { opacity: .55; }
.team-space { font-size: .76rem; font-weight: 600; color: var(--ink-2); }
.team-access .dd { flex: none; width: 7.5rem; }
.team-access .dd-button { height: 24px; font-size: .72rem; }
.team-paused { font-size: .72rem; font-weight: 600; color: var(--ink-3); padding: 0 .3rem; }
.team-drop {
  width: 20px; height: 20px; padding: 0; border: 0; border-radius: 6px;
  background: none; color: var(--ink-3); cursor: pointer;
  font-size: .95rem; line-height: 1;
}
.team-drop:hover { background: var(--red-100); color: var(--red-text); }

.team-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .2rem .55rem; border-radius: 8px;
  border: 1px solid var(--line-2); background: var(--surface-2);
  font-size: .74rem; color: var(--ink-2);
}
.team-chip b { font-weight: 700; color: var(--ink); }

/* The invite form: an address, then which workspaces and what in each. */
.team-form input[type="email"] { max-width: 26rem; }
.team-legend {
  margin: 1rem 0 .5rem; font-size: .8rem; font-weight: 650; color: var(--ink);
}
.team-picks { display: grid; gap: .4rem; }

/* Wider than a naming dialog, because each row carries a name and a role. */
.team-dialog { width: min(94vw, 34rem); }
.team-dialog .team-legend { margin-top: 1.1rem; }

/* The list scrolls rather than the dialog growing: fifty workspaces would
   otherwise push the buttons off the bottom of the screen, and the one
   control somebody needs after picking is the one they could not reach. */
.team-dialog .team-picks {
  max-height: min(34vh, 15rem); overflow-y: auto;
  padding: .15rem; margin: 0 -.15rem;
  scrollbar-width: thin;
}

/*
 * Room for the menu the picker opens.
 *
 * A dialog is its own containing block, so an absolutely positioned menu
 * cannot escape it: opening the workspace list inside a dialog sized to its
 * content left the list clipped to a couple of rows. The dialog keeps a
 * floor so the menu has somewhere to go, and the menu is shortened to fit
 * rather than the dialog growing past the screen.
 */
/* Enough that the menu has somewhere to open without the dialog jumping
   in size the moment somebody adds a row. */
.team-dialog form { min-height: 24rem; display: flex; flex-direction: column; }

/* Above the rows it overlaps. The direction it opens is decided in script,
   measured against the dialog rather than the viewport, so a menu that fits
   on screen but not in the card flips up instead of hanging over the edge. */
.team-dialog .team-add { position: relative; z-index: 3; }
.team-dialog .team-add .dd-list { max-height: 11rem; }

/* The actions stay at the bottom whatever the list above does, so Send is
   never somewhere different depending on how many workspaces were added. */
.team-dialog .ws-dialog-actions { margin-top: auto; padding-top: 1.25rem; }

.team-dialog input[type="email"] {
  width: 100%; box-sizing: border-box;
  font: inherit; font-size: .9rem; color: var(--ink);
  padding: .55rem .75rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.team-dialog input[type="email"]:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}

/* Choose a workspace, choose a role, add it. The dropdowns take the width
   and the button keeps its own, so the row does not reflow as names change. */
.team-add { display: flex; gap: .4rem; margin-bottom: .6rem; }
.team-add .dd:first-of-type { flex: 1 1 auto; min-width: 0; }
.team-add .dd:last-of-type { flex: none; width: 7.5rem; }
.team-add .btn { flex: none; }

/* What has been granted so far. A row, not a card: these are entries in a
   list somebody is building, not objects in their own right. */
.team-row {
  display: flex; align-items: center; gap: .5rem;
  padding: .45rem .5rem .45rem .7rem; border-radius: 9px;
  border: 1px solid var(--line); background: var(--surface-2);
}
.team-row-name {
  flex: 1; min-width: 0; font-size: .82rem; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.team-row-role {
  flex: none; padding: .1rem .45rem; border-radius: 6px;
  background: var(--surface); border: 1px solid var(--line-2);
  font-size: .72rem; font-weight: 600; color: var(--ink-2);
}

.dash-seats-cta { margin: 0 0 1.4rem; }
.team-pick {
  display: flex; align-items: center; gap: .6rem;
  padding: .5rem .65rem; border-radius: 9px;
  border: 1px solid var(--line); background: var(--surface);
  cursor: pointer;
}
.team-pick:hover { border-color: var(--ink-3); }
/* The label takes the row, so clicking the name ticks the box, and the
   dropdown sits outside it: opening a role must not untick the workspace. */
/* .ws-form label is display:block and 0-1-1, which beats a bare class
   however late it comes, so this matches it rather than relying on order.
   The margin goes too: that rule spaces a field's caption from its input,
   and these are not captions. */
.ws-form .team-pick-label,
.team-pick-label {
  flex: 1; min-width: 0; margin: 0;
  display: flex; align-items: center; gap: .55rem; cursor: pointer;
}

/* Drawn rather than native: the browser's checkbox ignores the palette and
   sits on its own baseline, so a row of them never lines up with the text
   beside it. Box-shadow makes the tick, so there is nothing to position. */
.team-pick input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  flex: none; width: 17px; height: 17px; margin: 0;
  border: 1.5px solid var(--line); border-radius: 5px;
  background: var(--surface); cursor: pointer;
  display: grid; place-content: center;
  transition: background .15s ease, border-color .15s ease;
}
.team-pick input[type="checkbox"]::before {
  content: ""; width: 9px; height: 9px;
  transform: scale(0); transition: transform .12s ease;
  background: var(--on-brand);
  clip-path: polygon(14% 45%, 0 60%, 39% 100%, 100% 22%, 84% 8%, 39% 70%);
}
.team-pick input[type="checkbox"]:checked {
  background: var(--brand-600); border-color: var(--brand-600);
}
.team-pick input[type="checkbox"]:checked::before { transform: scale(1); }
.team-pick input[type="checkbox"]:focus-visible {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px var(--brand-100);
}

.team-pick-name {
  flex: 1; min-width: 0;
  font-size: .84rem; font-weight: 600; color: var(--ink); line-height: 1.2;
}

/* The dropdown is a field, so it sizes itself: .dd-button is width:100% for
   a form, which collapses to nothing inside a flex row. */
.team-pick .dd { flex: none; width: 9.5rem; }

/* The role only matters once the workspace is ticked, so it stays quiet
   until then rather than presenting a decision nobody has reached yet. */
.team-pick .dd-button { color: var(--ink-3); }
.team-pick:has(input:checked) { border-color: var(--brand-600); background: var(--brand-100); }
.team-pick:has(input:checked) .dd-button { color: var(--ink); border-color: var(--brand-600); }

@media (max-width: 34rem) {
  .team-spaces { padding-left: 0; }
}

/* The call to action on an invitation, for somebody not signed in. The
   button leads and the explanation follows it, because the decision is
   already made by the time they reach this point.

   .plan-list sets margin:0, since everywhere else it is the last thing in
   its section, so the gap under it has to come from here. */
.invite-next { margin: 1.5rem 0 1.2rem; }
.invite-next .ws-hint { margin-top: .75rem; max-width: 34rem; }

/* .section carries margin-block: 5.5rem, which is right on a marketing page
   where each section is its own screenful and wrong directly under a page
   head: 88px between a lede and the thing it introduces reads as a missing
   element rather than as breathing room. */
.invite-body { margin-block: 1.75rem 4rem; }
.invite-tail { margin-top: 1rem; max-width: 34rem; }

/* Whose workspace a card is, for the ones that are not yours. Quiet, because
   it answers a question rather than announcing one: the name is still the
   thing being read. */
.ws-card-whose {
  display: block; margin: -.15rem 0 .35rem;
  font-size: .74rem; color: var(--ink-3);
}
.ws-card-whose b { font-weight: 700; color: var(--ink-2); }

/* No second mark for a guest card. The line naming the owner already says
   it in words, and a coloured edge beside it would be the same fact twice:
   .ws-card:hover sets border-color on every side anyway, so the edge would
   have vanished on hover and read as a rendering fault. */

/* Two groups on the workspaces page: yours, and the ones somebody invited
   you into. Separated because they are different kinds of thing, not
   different sizes of the same thing. */
.ws-group + .ws-group { margin-top: 2.25rem; }
.ws-group-head {
  margin: 0 0 .2rem; font-size: .95rem; font-weight: 700;
  letter-spacing: -.01em; color: var(--ink);
}
.ws-group-note {
  margin: 0 0 .9rem; max-width: 42rem;
  font-size: .8rem; color: var(--ink-3);
}
.ws-group-head + .ws-grid { margin-top: .85rem; }

/* Whose workspace you are in, under the switcher. Absent when it is yours,
   which is the ordinary case and needs no label. */
.side-ws-whose {
  margin: .4rem .15rem 0;
  font-size: .72rem; color: var(--ink-3); line-height: 1.3;
}
.side-ws-whose b { font-weight: 700; color: var(--ink-2); }

/* The plan card in a workspace somebody else owns: which plan applies, and
   no meter, because the allowance being spent is not the reader's. */
.side-card.is-shared .side-card-top { margin-bottom: .3rem; }
.side-card-whose {
  margin: 0; font-size: .72rem; color: var(--ink-3); line-height: 1.4;
}

/* Why a viewer's analysis goes somewhere else. At the top of the page rather
   than under the field: the topbar is a fixed height row and a second line
   there spills out of it. */
.dash-readonly {
  margin: 0 0 1.25rem; padding: .7rem .9rem;
  border-radius: 10px; border: 1px solid var(--line);
  background: var(--surface);
  font-size: .8rem; color: var(--ink-2); line-height: 1.5;
}
.dash-readonly a { color: var(--brand-700); font-weight: 600; }

/* The allowance page is where somebody decides whether this is worth paying
   for, so the offer is the page rather than a link at the bottom of it. */
.quota-offer {
  padding: 1.5rem 1.6rem;
  border-radius: 14px; border: 1px solid var(--line);
  background: var(--surface);
}
.quota-offer h2 {
  margin: 0 0 1rem; font-size: 1.15rem; font-weight: 800;
  letter-spacing: -.02em; color: var(--ink);
}

.quota-gains { list-style: none; margin: 0 0 1.4rem; padding: 0; display: grid; gap: .6rem; }
.quota-gains li {
  display: flex; align-items: flex-start; gap: .55rem;
  font-size: .88rem; color: var(--ink-2); line-height: 1.5;
}
.quota-gains svg {
  width: 15px; height: 15px; flex: none; margin-top: .2rem;
  color: var(--green-text, #0f7b4f);
}
.quota-gains b { font-weight: 700; color: var(--ink); }

.quota-actions { display: flex; align-items: center; flex-wrap: wrap; gap: .6rem; }

/* The tier above the one being sold, mentioned once and quietly: a second
   price competing with the first is how somebody decides to think about it
   later instead. */
/* Both paragraphs share one measure, or they wrap on different widths and
   read as belonging to different columns. */
.quota-ladder,
.quota-note {
  margin: 1.1rem 0 0; max-width: 40rem;
  font-size: .82rem; color: var(--ink-3); line-height: 1.6;
}
.quota-ladder a { color: var(--brand-700); font-weight: 700; }

/* The workspace settings page stacks a facts list, a create block and a
   delete block. .plan-list sets margin:0 because everywhere else it ends a
   section, and .ws-form only has a bottom margin, so the two met with no
   gap and read as one card. */
.dash-pages .plan-list + .ws-form { margin-top: 1.5rem; }
.dash-pages .plan-list { margin-bottom: 0; }

/* A button directly after the sentence explaining it. .ws-hint carries only
   a top margin, so without this the button sits on the text: the action and
   its explanation need to read as two things, not one block. */
.ws-form .ws-hint + .btn,
.ws-form .ws-hint + button { margin-top: .9rem; }

/* ---- scheduled re-runs ---- */

.sched-list { list-style: none; margin: 0 0 1.4rem; padding: 0; display: grid; gap: .5rem; }
.sched-row {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
  padding: .75rem .9rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}
.sched-page { flex: 1; min-width: 12rem; }
.sched-page b {
  display: block; font-size: .87rem; font-weight: 650; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sched-page > span {
  display: block; margin-top: .15rem;
  font-size: .75rem; color: var(--ink-3);
}

/* When the next run is, rather than a date: the question is how long, and a
   date makes the reader do the arithmetic. */
.sched-next { color: var(--ink-2); }
.sched-state { font-weight: 600; }
.sched-state.is-bad { color: var(--red-text); }

.sched-cadence {
  flex: none; padding: .2rem .55rem; border-radius: 7px;
  background: var(--surface-2); border: 1px solid var(--line-2);
  font-size: .75rem; font-weight: 600; color: var(--ink-2);
}

/* A paused row is still legible: it is a schedule somebody may resume, not
   a failure to be hidden. */
.sched-row.is-paused { background: var(--surface-2); }
.sched-row.is-paused .sched-page b { color: var(--ink-2); }

/* The empty state is a sentence, and .ws-hint carries only a top margin, so
   without this the form sits on the text that introduces it. */
.dash-pages > .ws-hint + .ws-form { margin-top: 1.1rem; }

/* Each action is its own form, because each posts to its own route, and a
   form is a block: without this the two buttons stack down the cell rather
   than sitting side by side as they read. */
/* The add form follows the list it adds to, so it needs air above it and
   none below: .ws-form carries a bottom margin for the stacked forms on the
   settings pages, and here it left a band of dead space under the last
   control while the form sat flush against the table. */
.dash-pages .sched-form { margin: 1.25rem 0 0; }

/* Row actions on a monitored page.

   Quieter and smaller than a .btn: these sit at the end of a two-line row and
   are things available on it, not the point of it. Centred on the row rather
   than pinned to the top of the cell, which is where a full-sized button in a
   tall cell ended up. */
.sched-act {
  display: inline-flex; align-items: center; gap: .3rem;
  height: 28px; padding: 0 .6rem;
  font: inherit; font-size: .76rem; font-weight: 700;
  color: var(--ink-2); background: var(--surface);
  border: 1px solid var(--line); border-radius: 8px; cursor: pointer;
  white-space: nowrap;
}
.sched-act svg { width: 13px; height: 13px; flex: none; }
.sched-act:hover { border-color: var(--brand-600); color: var(--brand-700); }
.sched-act.is-danger:hover {
  border-color: transparent; background: var(--red-100); color: var(--red-text);
}
.sched-act:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 1px; }

/*
 * The cell stays a table cell, and a box inside it does the laying out.
 *
 * display:flex on a <td> takes it out of the table's own row layout, so
 * vertical-align stops applying and the buttons sit at the top of a tall
 * row rather than on its centre line. Aligning the cell and letting an
 * inner flex box hold the buttons keeps both.
 */
/*
 * The page column yields, so the table fits.
 *
 * Auto layout hands a long title as much width as it asks for, and a page
 * name can be sixty characters: the table grew to 1193px inside a 974px
 * wrapper and scrolled sideways, cutting Remove in half. The other columns
 * are short and fixed, so the one that can wrap is the one that should.
 *
 * A max-width rather than table-layout: fixed, which would give every column
 * an equal share and leave "Every day" with as much room as the title.
 */
/* The notice sits under the form that caused it, not above one.

   .form-errors and .form-saved carry a bottom margin, which is right when a
   notice leads a form and leaves it flush against one when it follows. Only
   the wrapper is spaced, so an empty wrapper adds nothing. */
[data-sched-notice]:not(:empty) { margin-top: 1rem; }

/* The list's heading follows the add form, so it needs air between them:
   .dash-section-head only carries a bottom margin, which was enough when it
   was the first thing in the section and is not now.
 *
 * Only when something is actually above it. The class spread to nine places,
 * several of which open their section with it, and there the margin separated
 * the heading from nothing and read as white space at the top of the page. */
.sched-list-head:not(:first-child) { margin-top: 1.75rem; }

/* ---- one watched site, in detail ------------------------------------- */

.up-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin: .75rem 0 1.25rem;
}
.up-head-id { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; min-width: 0; }
.up-head h2 {
  margin: 0; font-size: 1.15rem; font-weight: 800; letter-spacing: -.02em;
  overflow-wrap: anywhere;
}

/* The ranges read as one control, so they sit in a single track rather than
   as four buttons that happen to be adjacent. */
.up-ranges {
  display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  margin-bottom: 1.1rem;
}

.up-range {
  padding: .35rem .7rem; border-radius: 999px;
  font-size: .78rem; font-weight: 700; text-decoration: none;
  color: var(--ink-2); background: var(--surface-2);
  border: 1px solid transparent;
}
.up-range:hover { border-color: var(--line); color: var(--ink); }
.up-range.is-on { background: var(--brand-100); color: var(--brand-700); }

/* The custom pair sits to the right, away from the named ranges: it answers
   the same question in a different way and running them together makes one
   row of six things. */
.up-custom {
  display: flex; align-items: center; gap: .4rem; margin-left: auto;
}
.up-custom input[type="date"] {
  height: 32px; padding: 0 .5rem;
  font: inherit; font-size: .78rem; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
}
.up-custom .up-to { font-size: .78rem; color: var(--ink-3); }
.up-custom .btn { height: 32px; }

/* Four figures across, stacking on a narrow screen rather than shrinking
   until the numbers wrap. */
.up-figures {
  display: grid; gap: .8rem; margin-bottom: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.up-figure {
  padding: .9rem 1rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}
.up-figure-label {
  display: block; font-size: .7rem; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-3);
}
.up-figure-value {
  display: block; margin: .3rem 0 .15rem;
  font-family: var(--display); font-size: 1.45rem; font-weight: 800;
  letter-spacing: -.02em; color: var(--ink); font-variant-numeric: tabular-nums;
}
.up-figure-value small { font-size: .78rem; font-weight: 700; color: var(--ink-3); }
.up-figure-note { display: block; font-size: .72rem; color: var(--ink-3); }

/*
 * A figure coloured by what its number means, not by what it counts.
 *
 * Four identical white boxes made "None" under Broken, which is the best
 * possible answer, look exactly like a number that needs attention. The
 * tone lets the row be read at a glance: green means nothing to do here.
 *
 * A left edge and a small icon rather than a filled card, so four of them
 * stay a row of figures rather than four competing alerts.
 */
/*
 * The tone is carried by the card, not by a bar stuck to its edge.
 *
 * A coloured stripe beside a bordered card reads as two competing frames,
 * and four of them in a row made the strip look like a stack of alerts. A
 * faint wash and a matching border tint the whole figure instead, which is
 * quieter and still answers "is this one fine" from across the screen.
 */
/*
 * The number carries the colour; the card stays a card.
 *
 * A filled panel is a lot of ink for one figure, and four of them read as
 * four alerts rather than a row of numbers. The tint moved to the border
 * and the value, which is enough to find the one that needs attention
 * without the row shouting.
 */
.up-figure { position: relative; }
.up-figure.is-warn { border-color: var(--amber-fill); }
.up-figure.is-bad { border-color: var(--red-fill); }

.up-figure.is-good .up-figure-value { color: var(--green-text); }
.up-figure.is-warn .up-figure-value { color: var(--amber-text); }
.up-figure.is-bad .up-figure-value { color: var(--red-text); }



/* Top right, out of the reading order: it repeats the tone for anyone who
   cannot use the colour, and is decoration for everyone else. */
.up-figure-icon {
  position: absolute; top: .85rem; right: .9rem;
  display: inline-flex; color: var(--ink-3); opacity: .45;
}
.up-figure-icon svg { width: 1rem; height: 1rem; }
.up-figure.is-good .up-figure-icon { color: var(--green-fill); opacity: .9; }
.up-figure.is-warn .up-figure-icon { color: var(--amber-fill); opacity: .9; }
.up-figure.is-bad .up-figure-icon { color: var(--red-fill); opacity: .9; }

/* The label leaves room for the icon beside it. */
.up-figure-icon ~ .up-figure-label { padding-right: 1.4rem; }

/* A bar per check. Thin on purpose: the shape of a row of them is the
   answer, and a wide bar turns fifty checks into a chart that needs
   scrolling to read. */
.up-bars-wrap {
  position: relative;
  /* Room above the bars for the tooltip, which sits clear of them rather
     than over the thing it describes. Less than the tooltip is tall: it may
     reach a little past the card, which reads as a panel floating over the
     page rather than as clipping. */
  padding: 2rem 1rem 1.25rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
}

/* Clear of the bars, not over them.
   
   The row is only 56px tall, so a tooltip that follows the pointer covers
   the thing it describes. It sits above by default and flips below when the
   bars are near the top of the viewport, which is where a sticky header
   would otherwise clip it. */
.up-tip {
  position: absolute; z-index: 5; bottom: calc(100% + 1rem);
  min-width: 11rem; max-width: 18rem;
  padding: .5rem .65rem; border-radius: 9px;
  background: var(--ink); color: var(--ground);
  box-shadow: 0 8px 20px rgb(15 23 42 / .22);
  pointer-events: none;
}
.up-tip.is-below { bottom: auto; top: calc(100% + 1rem); }

/* The arrow, so the panel points at the bar it belongs to rather than
   floating near it. */
.up-tip::after {
  content: ""; position: absolute; left: var(--tip-arrow, 50%);
  margin-left: -5px; border: 5px solid transparent;
}
.up-tip:not(.is-below)::after { top: 100%; border-top-color: var(--ink); }
.up-tip.is-below::after { bottom: 100%; border-bottom-color: var(--ink); }
.up-tip.is-down:not(.is-below)::after { border-top-color: var(--red-text); }
.up-tip.is-down.is-below::after { border-bottom-color: var(--red-text); }
.up-tip b {
  display: block; font-size: .72rem; font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.up-tip span {
  display: block; margin-top: .1rem;
  font-size: .72rem; opacity: .8; line-height: 1.4;
}
/* A failing bucket says so in the tooltip too, so the colour is not the only
   thing carrying it. */
.up-tip.is-down { background: var(--red-text); color: #fff; }
.up-tip[hidden] { display: none; }
/* Edge to edge inside the card, like the line chart below it: the axis
   labels hang in the card's own padding rather than pushing everything in
   from the left, which left the panel indented on one side only. */
.up-bars {
  position: relative;
  display: flex; align-items: flex-end; gap: 2px;
  height: 56px; overflow: visible;
}
.up-bar {
  flex: 1 1 3px; min-width: 2px; height: 100%;
  border-radius: 2px; background: var(--green-fill);
}
/* A handful of checks keep a readable width instead of each being stretched
   to a quarter of the panel. */
.up-bars.is-sparse { gap: 3px; justify-content: flex-start; }
.up-bars.is-sparse .up-bar { flex: 0 0 8px; }
/* Down bars are full height too: an outage is not a small version of
   working, and a short bar would read as slow rather than absent. */
.up-bar.is-down { background: var(--red-fill); }
.up-bar:hover { outline: 2px solid var(--ink-3); outline-offset: 1px; }
/* A wider hit area than the bar, so a 3px target is reachable with a mouse
   without making the bars themselves fat. */
.up-bars { padding-block: .25rem; }

/* Response time under the bars, sharing their x axis: a bump in the line
   sits under the bar it belongs to, which is the whole reason it is here
   rather than in a card of its own. */
.up-line {
  margin-top: 1.1rem; padding-top: .9rem;
  border-top: 1px solid var(--line-2, var(--line));
  color: var(--brand-600);
}
.up-line-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .75rem; margin-bottom: .6rem;
}
.up-line-head span:first-child {
  font-size: .72rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-3);
}
.up-line-scale { font-size: .74rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.up-line-scale b { color: var(--ink); font-weight: 800; }

/* The plot holds the axis labels and the readout, so both are positioned
   against the drawing rather than against the card. */
.up-line-plot { position: relative; }

/* preserveAspectRatio is none, so the drawing stretches to the panel and the
   stroke is kept honest by vector-effect rather than by the viewBox. */
.up-line svg { display: block; width: 100%; height: 96px; overflow: visible; }

.up-line-fill { fill: url(#up-line-grad); }
.up-grad-top { stop-color: var(--brand-500); stop-opacity: .28; }
.up-grad-bottom { stop-color: var(--brand-500); stop-opacity: 0; }

/* Gridlines recede: they are there to be measured against, not read. */
.up-grid { stroke: var(--line); stroke-width: 1; }
.up-grid.is-average { stroke: var(--ink-3); stroke-dasharray: 3 4; opacity: .55; }

/* The axis, outside the drawing so it is not stretched with it. */
/* Below its gridline rather than above it, so the chart keeps the full width
   of the card and the topmost label does not rise out of the plot into the
   heading. Sitting under the line also reads as labelling it rather than
   labelling whatever is above. */
.up-line-ticks { position: absolute; inset: 0; pointer-events: none; }
.up-line-tick {
  position: absolute; left: 0;
  padding: .15rem .35rem .15rem 0;
  font-size: .68rem; color: var(--ink-3); font-variant-numeric: tabular-nums;
  background: var(--surface);
}

/* A vertical rule that follows the pointer, so a peak can be lined up with
   the bar above it. */
.up-line-cursor { stroke: var(--ink-3); stroke-width: 1; stroke-dasharray: 2 3; }
.up-line-cursor[hidden] { display: none; }

/* Inside the plot, not above it: at the top of the card it sat over the
   "Response time" heading and the axis label beside it. */
.up-line-read {
  position: absolute; top: .35rem; transform: translateX(-50%);
  padding: .2rem .45rem; border-radius: 6px;
  background: var(--ink); color: var(--ground);
  font-size: .7rem; font-weight: 700; font-variant-numeric: tabular-nums;
  white-space: nowrap; pointer-events: none;
}
.up-line-read[hidden] { display: none; }

/* Under the line chart the labels need their own gap. The indent comes from
   .up-bars-ends, which every label row shares. */
.up-bars-ends.is-line { margin-top: .35rem; }

.up-bars-ends {
  display: flex; justify-content: space-between;
  margin-top: .5rem; font-size: .72rem; color: var(--ink-3);
}

/* An outage still running is the row that matters most. */
.sched-table tr.is-open .t-when:first-child { font-weight: 700; color: var(--ink); }

/* Uptime: the state of a watched site, and the figure behind it. */
.up-state {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .15rem .5rem; border-radius: 999px;
  font-size: .72rem; font-weight: 800; letter-spacing: .01em;
  background: var(--surface-2); color: var(--ink-3);
}
.up-state::before {
  content: ""; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor;
}
.up-state.is-up { background: var(--green-100); color: var(--green-text); }
.up-state.is-down { background: var(--red-100); color: var(--red-text); }
.up-state.is-paused { background: var(--amber-100); color: var(--amber-text); }

/* How long it has been down, under the badge: the state says what, this
   says since when, and the two together are the whole answer. */
.up-since { display: block; margin-top: .2rem; font-size: .7rem; color: var(--ink-3); }

/* Sparklines in the list: twenty pixels tall, no labels, no interaction.
   They answer "steady or spiky" while somebody scans the column, and the
   detail page is where a number gets read. */
.up-spark {
  display: flex; align-items: stretch; gap: 1px;
  height: 18px; margin-top: .35rem; max-width: 9rem;
}
.up-spark-bar { flex: 1 1 2px; border-radius: 1px; background: var(--green-fill); opacity: .55; }
.up-spark-bar.is-down { background: var(--red-fill); opacity: 1; }

/* Narrower than the address beside it: a name is a few words where a URL
   is a line, and giving them equal room makes the row look mis-set. */
.sched-label { flex: 0 1 12rem; min-width: 8rem; }

/* Breaks to its own line under the name: the pair reads as heading and
   subtitle rather than as two competing titles on one row. */
.up-head-url {
  /* Breaks to its own line after the name, then shares that line with the
     status chip, so the two facts about the site sit together. */
  margin-top: -.15rem;
  font-size: .82rem; color: var(--ink-3); text-decoration: none;
  overflow-wrap: anywhere;
}
.up-head-url:hover { color: var(--ink-2); text-decoration: underline; }

/* Two lines: the name, then one quiet row carrying everything about its
   current state. Three stacked lines at similar weight read as a list of
   equals, where only the name is a heading.

   The break comes from an empty full-width item rather than flex-basis on
   the form, which would stretch it across the row and carry the Save button
   anchored to it out to the far edge. */
.up-name { flex: 0 1 auto; }
.up-head-id::after { content: ''; flex-basis: 100%; height: 0; order: 1; }
.up-head-id { display: flex; flex-wrap: wrap; align-items: center; row-gap: .25rem; }

.up-head-meta {
  order: 2; flex-basis: 100%;
  display: flex; align-items: center; flex-wrap: wrap; gap: .3rem .55rem;
  min-width: 0;
}
.up-head-url { flex: 0 1 auto; min-width: 0; }

/* A divider between the state and the reading behind it, so the row reads as
   related facts rather than a sentence that lost its punctuation. */
.up-head-meta .up-last::before {
  content: ''; display: inline-block; vertical-align: middle;
  width: 1px; height: .8em; margin-right: .55rem;
  background: var(--line);
}

/* The last reading, on the state row: one fact in the same quiet register as
   the address beside it, so the name above stays the only heading. */
.up-last { font-size: .82rem; color: var(--ink-3); }
.up-last b { color: var(--ink-2); font-weight: 700; }
.up-last-at {
  margin-left: .4rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* The name is the heading, so the field wears the heading's type and no
   chrome until it is touched. A box drawn around it at rest would announce
   "form" on a page that is otherwise read, not filled in. */
.up-name {
  display: flex; align-items: center; gap: .4rem; min-width: 0;
  /* Anchors the Save button, which is positioned rather than laid out: it
     appears and disappears as the text changes, and in flow that resized
     the row and nudged everything beside it on every keystroke.
     width:fit-content matters as much as the position: as a full-width flex
     item this box ended at the edge of the page, and Save with it. */
  position: relative; width: fit-content; max-width: 100%;
}
.up-name-field {
  font: inherit; font-size: 1.05rem; font-weight: 800; letter-spacing: -.02em;
  color: var(--ink); background: transparent;
  border: 1px solid transparent; border-radius: .4rem;
  padding: .1rem .35rem; margin-left: -.35rem;
  max-width: 100%; min-width: 0; text-overflow: ellipsis;
}
.up-name-field::placeholder { color: var(--ink-3); font-weight: 700; }
.up-name-field:hover { border-color: var(--line); }
.up-name-field:focus {
  outline: none; background: var(--surface);
  border-color: var(--brand-500);
  box-shadow: 0 0 0 3px var(--brand-100);
}

/* Save appears once there is something to save, so the resting heading is a
   heading. The hiding is done by script rather than by a rule here: without
   JavaScript the button has to stay in flow, because it is the only way to
   submit and a button nobody can reach is worse than a button always shown. */
/* Sized to the field beside it rather than to the standard small button,
   which is built to stand on its own and towered over a trimmed input. */
.up-name-save {
  flex: none;
  /* btn-sm fixes its height at 38px, which towered over the trimmed field
     beside it, so this sets its own rather than adjusting padding. */
  height: auto; min-height: 0;
  padding: .38rem .75rem; font-size: .78rem; line-height: 1.35;
  border-radius: .4rem;
}

.up-name.is-live .up-name-save,
.up-name.is-live .up-name-said {
  position: absolute; left: 100%; top: 50%; margin-left: .4rem;
  transform: translateY(-50%);
  opacity: 0; pointer-events: none;
  transition: opacity .12s ease;
}

.up-name.is-live.is-dirty .up-name-save { opacity: 1; pointer-events: auto; }
.up-name.is-live .up-name-said { opacity: 1; }

/* What it saved, said once and quietly: the heading already shows the new
   name, so this only confirms that it was stored. */
.up-name-said { font-size: .8rem; color: var(--green-text); white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .up-name.is-live .up-name-save, .up-name.is-live .up-name-said { transition: none; }
}

/* On a narrow screen there is no room beside the field, so the button goes
   back into flow and the row wraps rather than running off the edge. */
@media (max-width: 560px) {
  .up-name { flex-wrap: wrap; }
  .up-name.is-live .up-name-save, .up-name.is-live .up-name-said {
    position: static; transform: none; margin-left: 0;
  }
  .up-name.is-live .up-name-save { display: none; }
  .up-name.is-live.is-dirty .up-name-save { display: inline-flex; }
}

/* The note column.
   Widest in the table, because it is the only one holding prose. The reason
   column is capped so it stops claiming room it never fills: "Answered 503"
   does not need 390px while the note beside it truncates. */
.up-outages .t-note { width: 42%; min-width: 15rem; }
.up-outages th:nth-child(4), .up-outages td:nth-child(4) { width: 20%; }

/* Aligned to the top rather than the middle: once the note wraps, a Save
   button centred against three lines sits nowhere in particular. */
.up-note-form { display: flex; align-items: flex-start; gap: .4rem; flex-wrap: wrap; }
/* A textarea, because a note is prose and 500 characters on one line is a
   note nobody can read back. It carries no resize handle and no scrollbar:
   the script sets its height from its content, so it is exactly as tall as
   what is written in it. */
.up-note-field {
  flex: 1 1 8rem; min-width: 0;
  font: inherit; font-size: .82rem; line-height: 1.5; color: var(--ink-2);
  background: transparent; border: 1px solid transparent;
  border-radius: .4rem; padding: .28rem .4rem; margin-left: -.4rem;
  resize: none; overflow: hidden;
  /* Without this a one-row textarea sits on the text baseline and the Save
     button beside it floats above the first line. */
  display: block; vertical-align: top;
}
.up-note-field::placeholder { color: var(--ink-3); }
.up-note-field:hover { border-color: var(--line); }
.up-note-field:focus {
  outline: none; background: var(--surface);
  border-color: var(--brand-500); box-shadow: 0 0 0 3px var(--brand-100);
}

.up-note-save {
  flex: none; height: auto; min-height: 0;
  padding: .28rem .6rem; font-size: .74rem; line-height: 1.35;
  border-radius: .4rem;
}
.up-note-form.is-live .up-note-save { display: none; }
.up-note-form.is-live.is-dirty .up-note-save { display: inline-flex; }

.up-note { margin: 0; font-size: .82rem; line-height: 1.5; color: var(--ink-2); }

/* Under the field rather than beside it: the cell is not wide enough to
   carry a name and a date on the same line as the note itself. */
.up-note-sig {
  flex-basis: 100%; font-size: .74rem; color: var(--ink-3);
}

/* Where one outage ends and the next begins.
   Heavier than the hairline between rows, because it separates groups rather
   than lines, and set in the ink colour so it reads as structure instead of
   as another row of content. */
.dash-table tbody tr.up-break > td { border-top: 2px solid var(--line); }

.up-ms { font-variant-numeric: tabular-nums; }
.up-sparkline {
  display: block; width: 100%; max-width: 7rem; height: 20px;
  margin-top: .3rem; color: var(--brand-600); opacity: .75;
}

.up-pct { font-weight: 800; font-variant-numeric: tabular-nums; }
.up-of { display: block; margin-top: .1rem; font-size: .7rem; color: var(--ink-3); }

/* A paused row is quieter, because nothing it says is current. */
.sched-table tr.is-paused .t-page b { color: var(--ink-2); }

.sched-table .t-page { max-width: 0; width: 55%; }

/*
 * The crawl results table sizes for a short path and a long title, which is
 * the reverse of the schedule tables above: there the page cell holds a full
 * URL and leads the row, here the host is in the heading and only the path
 * is shown, so Page should take what a path needs and no more. table-layout
 * fixed makes the widths hold rather than letting the browser hand the empty
 * space back to the first column.
 */
/*
 * The crawl results table.
 *
 * Every column but the two text ones is given a fixed width, and Page and
 * Title are left to divide whatever remains. Setting percentages on those
 * two as well overflowed the wrapper by thirty pixels, because percentages
 * under table-layout:fixed are of the table rather than of the space left
 * after the fixed columns, so they and the rest added up past a hundred.
 */
.crawl-table { table-layout: fixed; width: 100%; }

/*
 * Filters and search on one row: tabs left, search right.
 *
 * They are two ways of narrowing the same table, so a heading between them
 * made one look like a section and the other like a control belonging to
 * something else. Wrapping rather than shrinking, because the tabs grow
 * with the number of problems a site has and the search needs a usable
 * width more than it needs to stay on the line.
 */
.crawl-tools {
  display: flex; align-items: center; gap: .75rem 1rem; flex-wrap: wrap;
  margin: .1rem 0 .7rem;
}
.crawl-tools .up-ranges { margin: 0; flex: 1 1 auto; min-width: 0; }
.crawl-tools .table-tools { flex: 0 1 18rem; }

@media (max-width: 720px) {
  .crawl-tools .table-tools { flex-basis: 100%; }
}

/* The caveat above the controls: it explains the column the filters and the
   search both act on, so it is read before either of them. */
/*
 * Site issues.
 *
 * Three across, as tiles. Two earlier shapes failed for the same reason:
 * anything built from shared borders has to decide where a row begins, and
 * a grid does not know. Full-width rows avoided that but cost the height of
 * seven lines, and a two-across grid drew dividers that stopped partway
 * across.
 *
 * Tiles with their own ground have no shared edges to get wrong, so the
 * ragged last row is whitespace instead of a hole. The tone lives on a left
 * rail: a filled block for every finding was too heavy at this size, but
 * three amber rails still sort the list at a glance.
 */
.site-finds {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .55rem;
  margin: 0 0 1.4rem;
}

/* A tile, not a row in a table.
 *
 * Three across leaves a gap in the last row whenever the count is not a
 * multiple of three, and an earlier attempt with shared borders made that
 * gap read as a line stopping halfway. Each tile carries its own ground
 * instead, so a missing one is spacing rather than a hole.
 */
.site-find {
  display: block;
  padding: .65rem .8rem .7rem;
  border-radius: .55rem;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-left: 2px solid var(--ink-3);
  text-decoration: none; color: inherit;
  min-width: 0;
}

/* Tone on the rail rather than in the fill. A coloured block for every
   finding was too much at three across; a rail still sorts them at a
   glance without turning the section into a traffic light. */
.site-find.is-warn { border-left-color: var(--amber-fill); }
.site-find.is-bad { border-left-color: var(--red-fill); }
.site-find.is-good { border-left-color: var(--green-fill); }

.site-find b {
  display: block;
  font-size: .82rem; font-weight: 700; color: var(--ink);
  line-height: 1.3;
  margin-bottom: .18rem;
}
.site-find span {
  display: block;
  font-size: .76rem; color: var(--ink-3); line-height: 1.45;
}

/* Only the ones with pages behind them are links.
 *
 * The hover ground is a brand tint, not the surface grey. Grey is what the
 * section itself sits on, so hovering made the tile sink into the page and
 * read as disabled. A tint in the brand hue moves the other way, and it is
 * the same ground `.btn.ghost:hover` already uses, cut with the tile's own
 * white so it stays quiet at this size.
 *
 * The tint is the whole signal. Geometry does not change, and the title is
 * not underlined on top of it: two signals for one hover read as louder
 * than the thing being offered.
 */
a.site-find { transition: background-color .12s ease, border-color .12s ease; }
a.site-find b { color: var(--brand-600); }

a.site-find:hover {
  background: color-mix(in srgb, var(--brand-100) 55%, var(--surface));
  /* Three sides, not the shorthand: the left border is the tone rail, and
     `border-color` would repaint amber or red as brand blue, throwing away
     the severity the rail is there to carry. */
  border-top-color: color-mix(in srgb, var(--brand-500) 34%, transparent);
  border-right-color: color-mix(in srgb, var(--brand-500) 34%, transparent);
  border-bottom-color: color-mix(in srgb, var(--brand-500) 34%, transparent);
}

a.site-find:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  a.site-find { transition: none; }
}

/* The passes, below the grid rather than inside it.
 *
 * As a grid child it either left holes or spanned every column and drew a
 * line under tiles it was not part of. It belongs to the whole group, so
 * it sits under the whole group. */
.site-passed {
  margin: 0 0 1.4rem;
  border: 1px solid var(--line-2); border-radius: .55rem;
  background: var(--surface);
  overflow: hidden;
}
.site-passed > summary {
  display: flex; align-items: center; gap: .5rem;
  padding: .55rem .8rem; cursor: pointer; list-style: none;
}
.site-passed > summary::-webkit-details-marker { display: none; }
/* The same tint as a tile, for the same reason: the surface grey is what
   the section sits on, so hovering made the strip vanish into the page.
   The border darkens with it, because `--line-2` is close enough to the
   tint that the edge disappeared and the strip looked like it was bleeding
   into the page. It sits on the details element rather than the summary so
   the whole block keeps one outline, open or shut. */
.site-passed { transition: border-color .12s ease; }
.site-passed > summary { transition: background-color .12s ease; }
.site-passed:has(> summary:hover) {
  border-color: color-mix(in srgb, var(--brand-500) 34%, transparent);
}
.site-passed > summary:hover {
  background: color-mix(in srgb, var(--brand-100) 55%, var(--surface));
}
.site-passed > summary:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
  .site-passed,
  .site-passed > summary { transition: none; }
}
.site-passed > summary b { font-size: .8rem; font-weight: 600; color: var(--ink-2); }
.site-passed > summary::after {
  content: 'Show'; margin-left: auto;
  font-size: .74rem; color: var(--ink-3);
}
.site-passed[open] > summary::after { content: 'Hide'; }

/* Opened, the passes are a plain list. They are evidence the check ran,
   not something to act on, so they do not get tiles of their own. */
.site-passed-list { border-top: 1px solid var(--line-2); }
.site-pass {
  display: flex; align-items: baseline; gap: .5rem;
  padding: .5rem .8rem;
  border-top: 1px solid var(--line-2);
}
.site-pass:first-child { border-top: 0; }
.site-pass b { font-size: .8rem; font-weight: 600; color: var(--ink-2); flex: none; }
.site-pass span { font-size: .76rem; color: var(--ink-3); line-height: 1.45; }

.site-find-tick {
  flex: none; width: .75rem; height: .75rem;
  color: var(--green-fill); transform: translateY(.05em);
}

@media (max-width: 900px) {
  .site-finds { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .site-finds { grid-template-columns: minmax(0, 1fr); }
  .site-pass { flex-wrap: wrap; }
  .site-pass span { flex-basis: 100%; }
}

/* Nothing found, said plainly: an empty section reads as one that did not
   run, and these are checks a reader cannot make by opening pages. */
.site-clean {
  display: flex; align-items: flex-start; gap: .5rem;
  margin: 0 0 1.4rem; padding: .75rem .9rem;
  font-size: .82rem; line-height: 1.5;
  color: var(--green-text); background: var(--green-100);
  border-radius: .6rem;
}
.site-clean svg { width: 1rem; height: 1rem; flex: none; margin-top: .15rem; }

.crawl-note {
  margin: 0 0 .75rem; font-size: .78rem; color: var(--ink-3); line-height: 1.5;
}

/*
 * The help tip on the Quick score heading.
 *
 * The wrapper scrolls horizontally, and a scroll box clips anything
 * positioned inside it. This table is sized to fit exactly, so it has
 * nothing to scroll and the clipping is pure loss: letting the wrapper
 * show its overflow lets the panel out, and the table still cannot spill
 * because its columns add up to the width available.
 *
 * The heading's own casing and spacing are undone inside the panel, which
 * is prose rather than a column label.
 */
.dash-table-wrap:has(.crawl-table) { overflow: visible; }
.crawl-table thead .help-tip { margin-left: .25rem; }
.crawl-table thead .help-tip-body {
  z-index: 60;
  text-transform: none; letter-spacing: normal; font-weight: 400;
}
.crawl-table th:nth-child(2), .crawl-table td:nth-child(2) { width: 8rem; }   /* Quick score, plus its help mark */
.crawl-table th:nth-child(3), .crawl-table td:nth-child(3) { width: 4.5rem; } /* Status */
.crawl-table th:nth-child(5), .crawl-table td:nth-child(5) { width: 4.5rem; } /* Words */
.crawl-table th:nth-child(6), .crawl-table td:nth-child(6) { width: 4rem; }   /* Issues */
.crawl-table th:nth-child(7), .crawl-table td:nth-child(7) { width: 6.5rem; } /* The row's actions */

/* A word count under the thin threshold, marked where it is read. */
.t-thin { color: var(--amber-text); font-weight: 600; }

/* Details and refetch, side by side. The refetch is an icon because it is
   the same action on every row and the word would double the column. */
/* The refetch is wrapped in a form, which is the flex item rather than the
   button inside it, so the two align by their boxes and not by the text
   baseline of one and the edge of the other. */
.crawl-row-acts { display: inline-flex; align-items: stretch; gap: .3rem; }
.crawl-row-acts > form { display: flex; }

/* Square, and the same height and radius as Details beside it: the two sit
   on one line and a shorter or rounder neighbour reads as misaligned even
   when the heights match to the pixel. */
.crawl-redo {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 27px; height: 27px; padding: 0;
  border: 1px solid var(--line); border-radius: .35rem;
  background: transparent; color: var(--ink-3); cursor: pointer;
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}

/*
 * What it does, on hover and on keyboard focus.
 *
 * An icon alone is a guess, and the native title arrives after a second of
 * hovering and cannot be reached by keyboard at all. aria-label carries the
 * name for a screen reader; this is for everybody else.
 */
.crawl-redo::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + .4rem); right: 0;
  padding: .3rem .5rem; border-radius: .35rem;
  font-size: .72rem; font-weight: 600; line-height: 1.3; white-space: nowrap;
  color: var(--on-brand); background: var(--ink);
  opacity: 0; pointer-events: none; transition: opacity .12s ease;
  z-index: 30;
}
.crawl-redo:hover::after, .crawl-redo:focus-visible::after { opacity: 1; }
.crawl-redo.is-busy::after { content: 'Fetching...'; opacity: 1; }
.crawl-redo svg { width: .9rem; height: .9rem; }
.crawl-table tr:hover .crawl-redo { border-color: var(--ink-3); color: var(--ink-2); }
.crawl-redo:hover, .crawl-redo:focus-visible {
  color: var(--brand-600); border-color: var(--brand-500); background: var(--brand-100);
}
.crawl-redo:focus-visible { outline: none; }

/* Spinning while it works, so a row that takes a second says so. */
.crawl-redo.is-busy { pointer-events: none; color: var(--brand-600); }
.crawl-redo.is-busy svg { animation: crawl-redo-spin .9s linear infinite; }

@keyframes crawl-redo-spin { to { transform: rotate(360deg); } }

/* A row that has just been re-read, said for a moment: without it a page
   whose numbers did not change looks like a button that did nothing. */
.crawl-table tr.is-refetched > td { background: var(--brand-100); }
.crawl-table tr.is-refetched > td { transition: background .5s ease; }

@media (prefers-reduced-motion: reduce) {
  .crawl-redo.is-busy svg { animation: none; }
}
.crawl-table th:nth-child(1), .crawl-table td:nth-child(1),
.crawl-table th:nth-child(4), .crawl-table td:nth-child(4) { width: auto; }

/* The action column holds an icon here too. */
.sched-table:not(.crawl-table) td.t-look:last-child { width: 3.5rem; }

/* The full URL, host quiet and path plain, wrapping on the natural breaks
   in a path rather than mid-word. */
.crawl-table .t-page b { overflow-wrap: anywhere; }
.crawl-table .t-host { color: var(--ink-3); }

/*
 * The action column.
 *
 * Wide enough for the button and the cell's own padding: at 4.5rem the
 * content box was 40px for a 67px button, so every row's button hung past
 * the table edge and was clipped. The padding is trimmed on this side too,
 * since the button carries its own.
 */
.crawl-table th:last-child, .crawl-table td.t-look { width: 6.5rem; text-align: right; }

/* The runs table's action column holds an icon, so it needs far less.
   Scoped away from .crawl-table, whose last column holds a Details button
   and was being squeezed to an icon's width by this. */
.sched-table:not(.crawl-table) td.t-look:last-child,
.sched-table:not(.crawl-table) th:last-child { width: 3.5rem; text-align: right; }
.crawl-table td.t-look { padding-left: .35rem; padding-right: .85rem; }

/*
 * Quiet, but never dimmed.
 *
 * Opacity was the wrong tool: a faded control reads as disabled, and a
 * reader who thinks a button is disabled does not press it. Colour carries
 * the quietness instead, so the button is plainly live at rest and simply
 * does not shout over the data beside it.
 */
/* Smaller than btn-sm, which is sized to stand alone: forty of these down
   a table is a column of buttons competing with the figures beside them,
   and a row only needs one small way in. */
.crawl-look {
  white-space: nowrap;
  /* A stated height rather than one that falls out of padding and
     line-height: those added to 26.8px against the icon's 27, which is
     invisible in a rounded measurement and plain to the eye on two
     buttons sharing a line. */
  height: 27px; min-height: 0;
  padding: 0 .6rem; font-size: .75rem; line-height: 1;
  border-radius: .35rem;
  color: var(--ink-2); border-color: var(--line);
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.crawl-table tr:hover .crawl-look,
.crawl-look:hover,
.crawl-look:focus-visible {
  color: var(--brand-600); border-color: var(--brand-500);
}

/*
 * The quick-look panel.
 *
 * Wider than the confirm dialogs because it holds a page's worth of facts,
 * and scrollable because a page with many inbound links should not push its
 * own actions off the bottom.
 */
/* Removing a crawl: an icon, because it is the same action on every row and
   the word was claiming more column width than the figures beside it. The
   label lives on aria-label and the tooltip, so nothing is lost. */
.crawl-drop {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; padding: 0;
  border: 1px solid var(--line); border-radius: .4rem;
  background: transparent;
  color: var(--ink-3); cursor: pointer;
  transition: color .12s ease, background .12s ease, border-color .12s ease;
}
.crawl-drop svg { width: 1rem; height: 1rem; }

/* Red, never the brand blue: this is the one control on the row that
   destroys something, and it should not look like the ones that navigate.
   The row hover already reveals it, so the outline turns red with it. */
.dash-table tr:hover .crawl-drop { border-color: var(--red-fill); color: var(--red-text); }
.crawl-drop:hover,
.crawl-drop:focus-visible {
  color: var(--red-text); background: var(--red-100);
  border-color: var(--red-fill);
}
.crawl-drop:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--red-100); }

/* Starting a crawl: three fields and a limit, which is why it is a dialog
   rather than a row above the table. */
.crawl-dialog { width: min(32rem, calc(100vw - 2rem)); }

/* A refusal, shown where the reader is rather than on the page behind a
   dialog that has closed. Under the heading, so it is read before the
   fields it is about. */
.crawl-problem {
  /* Room under it: the paragraph below explains the form and was sitting
     against the refusal as though it were part of the same thought. */
  margin: .2rem 0 1.1rem; padding: .75rem .85rem;
  font-size: .84rem; line-height: 1.5;
  color: var(--red-text); background: var(--red-100);
  border-radius: .5rem;
}

/* The way out, on its own line so it reads as an action rather than as more
   of the sentence. */
.crawl-problem-go {
  display: inline-block; margin-top: .4rem;
  font-weight: 700; color: var(--red-text);
  text-underline-offset: 2px;
}
.crawl-problem-go:hover { color: var(--red-dark); }

/* A label belongs to the field under it, not the one above.
   Without this the shared dialog styling put "Name" flush against the
   Address input, so the form read as three fields with the labels one row
   out of step. */
.crawl-dialog label[for] {
  display: block; margin-top: 1rem; margin-bottom: .35rem;
  font-size: .84rem; font-weight: 600; color: var(--ink);
}
.crawl-dialog label[for]:first-of-type { margin-top: 1.2rem; }
.crawl-dialog input[type="text"] { margin-bottom: 0; }
.crawl-optional { font-weight: 400; color: var(--ink-3); font-size: .78rem; }
/* The same gap and weight a label gets: it names the choice under it and is
   one of the form's three questions, whatever element it happens to be.

   Qualified by .crawl-dialog because ".ws-dialog p" sets margin:0 at the
   same specificity and later in the file, so an unqualified rule here lost
   and the legend sat flush against the field above it. */
.crawl-dialog .crawl-legend {
  margin: 1rem 0 .5rem;
  font-size: .84rem; font-weight: 600; color: var(--ink); line-height: 1.4;
}

/* The note under the choices, and the room before the buttons. */
.crawl-dialog .ws-hint { margin: .7rem 0 0; }
.crawl-dialog .ws-dialog-actions { margin-top: 1.4rem; }
.crawl-picks { display: flex; flex-wrap: wrap; gap: .4rem; }

/* The radio itself is hidden and the label carries the state, so the choice
   reads as a row of buttons while staying a real radio group for the
   keyboard and for a form posted without JavaScript. */
.crawl-pick { position: relative; }
.crawl-pick input[type="radio"] { position: absolute; opacity: 0; width: 0; height: 0; }
.crawl-pick > span {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 3rem; padding: .4rem .7rem;
  font-size: .84rem; font-variant-numeric: tabular-nums;
  border: 1px solid var(--line); border-radius: .45rem;
  cursor: pointer; transition: border-color .12s ease, background .12s ease;
}
.crawl-pick:hover > span { border-color: var(--ink-3); }
.crawl-pick input:checked + span {
  border-color: var(--brand-500); background: var(--brand-100);
  color: var(--brand-700); font-weight: 700;
}
.crawl-pick input:focus-visible + span { box-shadow: 0 0 0 3px var(--brand-100); }

/* The box for a number nobody offered, sitting inside its own choice. */
.crawl-pick-own { display: inline-flex; align-items: center; gap: .4rem; }
.crawl-pick-own input[type="number"] {
  width: 5.5rem; padding: .38rem .5rem;
  font: inherit; font-size: .84rem;
  border: 1px solid var(--line); border-radius: .45rem;
}
.crawl-pick-own input[type="number"]:focus {
  outline: none; border-color: var(--brand-500); box-shadow: 0 0 0 3px var(--brand-100);
}

.qv-dialog {
  /* Wide enough for a full URL and a finding on one line, which is what the
     issue list and the inbound links both want; the label column beside it
     then has room without the values wrapping. */
  width: min(48rem, calc(100vw - 2rem));
  max-height: min(82vh, 46rem);
  padding: 0; overflow: hidden;
}

/*
 * A column: head and actions hold, the middle scrolls.
 *
 * min-height:0 on the scrolling child is what makes it work. A flex item
 * defaults to min-height:auto, which refuses to shrink below its content,
 * so without it the facts push the actions past the bottom of the dialog
 * and nothing scrolls at all.
 */
.qv-body { display: flex; flex-direction: column; max-height: inherit; min-height: 0; }
.qv-scroll {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: .2rem 1.6rem 0; overscroll-behavior: contain;

  /* A little room at the top so a group heading scrolled to the boundary
     does not sit flush against the header above it, half cut. */
  scroll-padding-top: .6rem;
}
.qv-loading { color: var(--ink-3); padding: 1.6rem; }

.qv-close {
  position: absolute; top: .7rem; right: .7rem; z-index: 2;
  width: 2rem; height: 2rem; display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: transparent; color: var(--ink-3); cursor: pointer;
}
.qv-close:hover { background: var(--surface-2); color: var(--ink); }
.qv-close svg { width: 1.1rem; height: 1.1rem; }

/* The address, held above the scrolling content.
   Padded clear of the close button on the right, which otherwise sits on
   top of a long URL. */
.qv-head {
  flex: none;
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: 1.3rem 3.2rem 1rem 1.6rem;
  border-bottom: 1px solid var(--line-2);
  background: var(--surface);
}
.qv-open {
  font-size: .88rem; font-weight: 700; color: var(--brand-600);
  text-decoration: none; overflow-wrap: anywhere; min-width: 0;
}
.qv-open:hover { text-decoration: underline; }

/* ---------------------------------------------------------------------------
   The quick look panel.

   The shape follows the reading order: what the verdict is, what is wrong,
   the numbers at a glance, then the detail grouped by what it is about. The
   first version gave all of that the same weight, so a missing h1 and a
   text-to-HTML ratio looked equally important and neither stood out.
   --------------------------------------------------------------------------- */

/* The verdict band: score, sentence, and an honest note about the limits. */
.qv-verdict {
  display: flex; gap: 1rem; align-items: center;
  padding: 1rem 1.1rem; margin-bottom: 1.1rem;
  background: var(--surface-2); border-radius: .7rem;
}
.qv-dial { flex: none; font-size: 1.1rem; }
.qv-verdict-said { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
.qv-verdict-said b { font-size: .92rem; }
.qv-verdict-said > span { font-size: .82rem; color: var(--ink-2); }

/* Folded away, because it qualifies the score rather than competing with it.
   A reader who trusts the number should not have to scroll past four
   caveats to reach the findings. */
.qv-limits { margin-top: .25rem; }
.qv-limits summary {
  font-size: .78rem; color: var(--ink-3); cursor: pointer;
  list-style: none; text-decoration: underline; text-underline-offset: 2px;
}
.qv-limits summary::-webkit-details-marker { display: none; }
.qv-limits summary:hover { color: var(--ink-2); }
.qv-limits ul {
  margin: .4rem 0 0; padding-left: 1.05rem;
  font-size: .78rem; color: var(--ink-3); line-height: 1.6;
}

/* The findings, ranked. Severity carries the colour, and the label on the
   right names it in words so colour is never the only signal. */
.qv-issues { margin: 0 0 1.2rem; padding: 0; list-style: none; display: grid; gap: .3rem; }
.qv-issue {
  display: grid; grid-template-columns: auto 1fr auto; align-items: baseline;
  gap: .55rem; padding: .5rem .7rem;
  font-size: .85rem; line-height: 1.5;
  background: var(--surface-2); border-radius: .5rem;
  border-left: 3px solid var(--line);
}
.qv-dot { width: .4rem; height: .4rem; border-radius: 50%; background: var(--ink-3); }
.qv-issue-level {
  font-size: .7rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-3); white-space: nowrap;
}
.qv-issue.is-critical { border-left-color: var(--red-fill); }
.qv-issue.is-critical .qv-dot { background: var(--red-fill); }
.qv-issue.is-critical .qv-issue-level { color: var(--red-text); }
.qv-issue.is-recommended { border-left-color: var(--amber-fill); }
.qv-issue.is-recommended .qv-dot { background: var(--amber-fill); }
.qv-issue.is-recommended .qv-issue-level { color: var(--amber-text); }
.qv-issue.is-optional { border-left-color: var(--line); }

.qv-clean {
  margin: 0 0 1.2rem; padding: .7rem .8rem;
  font-size: .85rem; color: var(--green-text);
  background: var(--green-100); border-radius: .5rem;
}

/* The numbers, as tiles. Six of these read faster than six table rows and
   take a third of the height. */
.qv-stats {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: .5rem; margin-bottom: 1.3rem;
}
.qv-stat {
  padding: .6rem .7rem; border: 1px solid var(--line-2); border-radius: .5rem;
  display: flex; flex-direction: column; gap: .1rem;
}
.qv-stat b { font-size: 1rem; font-weight: 800; letter-spacing: -.01em; font-variant-numeric: tabular-nums; }
.qv-stat span { font-size: .72rem; color: var(--ink-3); }
.qv-stat.is-warn { border-color: var(--amber-fill); }
.qv-stat.is-warn b { color: var(--amber-text); }

/* The detail, grouped by what it is about. A heading per group does the work
   the reader was doing: knowing which "None" is a problem. */
.qv-group { margin-bottom: 1.3rem; }
.qv-group > h4 {
  margin: 0 0 .5rem; font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3);
}
.qv-group dl { margin: 0; }

.qv-row {
  display: grid; grid-template-columns: 10rem 1fr; gap: 1rem;
  padding: .45rem 0; border-top: 1px solid var(--line-2);
}
.qv-row:first-child { border-top: 0; }
.qv-row dt { color: var(--ink-3); font-size: .8rem; }
.qv-row dd { margin: 0; font-size: .85rem; color: var(--ink); overflow-wrap: anywhere; }

/* A problem marked where it sits, so the reader need not hold the rules in
   their head to know that this "None" is the bad one.

   The inner span has to be named too: a value like "None" is wrapped in
   .t-none, whose own grey beats a colour set on the dd alone, so the row
   read as neutral exactly where it was trying to say otherwise. */
.qv-row.is-warn dd,
.qv-row.is-warn dd .t-none { color: var(--amber-text); }
.qv-row.is-bad dd,
.qv-row.is-bad dd .t-none { color: var(--red-text); font-weight: 600; }
.qv-missing { color: var(--red-text); font-weight: 600; }

/* The measurement hanging off a flagged value stays quiet: "14 characters"
   is the evidence, not the complaint. */
.qv-row.is-warn dd .qv-count,
.qv-row.is-bad dd .qv-count { color: var(--ink-3); font-weight: 400; }

/* A measurement hanging off the value it qualifies, rather than a row of
   its own: "58 characters" belongs to the title, not beside it. */
.qv-count {
  display: inline-block; margin-left: .5rem;
  font-size: .74rem; color: var(--ink-3); white-space: nowrap;
}

.qv-url { font-family: var(--mono, ui-monospace, monospace); font-size: .78rem; overflow-wrap: anywhere; }

.qv-links ul { margin: 0; padding: 0; list-style: none; display: grid; gap: .1rem; }

/* URL and anchor text on one line: the URL takes what it needs and the
   anchor takes the rest, ellipsised rather than wrapping, so every row is
   one line tall and the list stays scannable at twenty-five entries. */
.qv-links li {
  display: flex; align-items: baseline; gap: .5rem;
  font-size: .8rem; padding: .25rem 0;
  border-top: 1px solid var(--line-2);
}
.qv-links li:first-child { border-top: 0; }
.qv-links .qv-url { flex: 0 1 auto; min-width: 0; overflow-wrap: anywhere; }
.qv-anchor {
  flex: 1 1 auto; min-width: 0;
  font-size: .76rem; color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qv-anchor::before { content: '\201C'; }
.qv-anchor::after { content: '\201D'; }

/* A link with no text is a finding rather than a quotation. */
.qv-anchor.is-none { font-style: italic; }
.qv-anchor.is-none::before, .qv-anchor.is-none::after { content: none; }
.qv-more { margin: .6rem 0 0; font-size: .78rem; color: var(--ink-3); }

/* The audit form sits in the actions row as though it were the button it
   contains, because that is all it is. */
.qv-audit, .qv-refetch { display: contents; }

.qv-actions {
  flex: none;
  display: flex; gap: .5rem; flex-wrap: wrap;
  padding: 1rem 1.6rem 1.2rem;
  border-top: 1px solid var(--line);
  background: var(--surface);
}

/* The last fact should not sit flush against the pinned actions. */
.qv-scroll > :last-child { margin-bottom: 1.2rem; }

@media (max-width: 480px) {
  .qv-row { grid-template-columns: 1fr; gap: .1rem; }
}

.sched-table .t-act { vertical-align: middle; text-align: right; }
.sched-table .t-act form { display: inline-flex; }
.sched-table .t-act .sched-acts {
  display: inline-flex; align-items: center; justify-content: flex-end; gap: .4rem;
}

.sched-add { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.sched-add .dd:first-of-type { flex: 1 1 18rem; min-width: 0; }
.sched-add .dd:last-of-type { flex: none; width: 11rem; }

/* The field and its suggestions. The wrapper is what the menu is positioned
   against, so it takes the flex sizing and the field fills it. */
.sched-pick { position: relative; flex: 1 1 18rem; min-width: 0; }

/* Against the field rather than the whole row, and wide enough for a title
   above a URL: .dd-menu stretches left to right of its parent by default,
   which here is the wrapper, so only the offset needs saying. */
.sched-menu { top: calc(100% + .35rem); }
.sched-menu .dd-item { align-items: flex-start; }
.sched-menu .dd-label { display: block; }
.sched-menu .dd-note {
  display: block; margin-top: .1rem;
  font-family: var(--mono); font-size: .72rem; color: var(--ink-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The URL field, matched to the cadence control beside it: the same height,
   radius and type, so the row reads as one set of inputs. */
/* Fills its wrapper on monitoring, where .sched-pick positions a menu
   against it, and flexes on uptime, where it is a direct child of the row.
   width:100% alone made the second case take the whole line and push the
   button under it. */
.sched-url, .sched-label {
  flex: 1 1 18rem; width: 100%; min-width: 0; box-sizing: border-box;
  height: 38px; padding: 0 .75rem;
  font: inherit; font-size: .84rem; color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius);
}
.sched-url::placeholder, .sched-label::placeholder { color: var(--ink-3); }
.sched-url:hover, .sched-label:hover { border-color: var(--ink-3); }
.sched-url:focus, .sched-label:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.sched-add .dd-button { height: 38px; border-radius: var(--radius); font-size: .84rem; }
.sched-add .dd-menu { min-width: 17rem; }
.sched-add .btn { flex: none; }

/* The run count and Track, side by side at the end of a history row.
   
   One flex row so the two sit on a shared centre line. They were inline
   boxes before, each finding its own baseline, and the pill carries a chevron
   while the button does not: the two ended up at visibly different heights
   with the gap between them coming from a margin on one side only. */
.t-act-pair { display: flex; align-items: center; justify-content: flex-end; gap: .4rem; }

/* Track, on a history row. A button rather than a link because it changes
   something, styled as the quiet row action it is.

   Matched to .t-runs.is-link beside it: same height, padding, radius and
   type, so the pair reads as two controls of one kind rather than a pill
   next to a box. */
.t-track { display: inline-flex; }
.t-track-btn {
  display: inline-flex; align-items: center;
  height: 1.55rem; padding: 0 .6rem;
  border: 1px solid var(--line); background: var(--surface);
  border-radius: 999px; cursor: pointer;
  font: inherit; font-size: .74rem; font-weight: 700; color: var(--ink-3);
}
.t-track-btn:hover { border-color: var(--brand-600); color: var(--brand-700); }
.t-track-btn.is-on {
  background: var(--brand-100); border-color: transparent; color: var(--brand-700);
}

/* Track, on a report. Sits in the action row beside Download and Re-analyze,
   so the choice is made while looking at the page it is about. */
/* Both of these are forms sitting in the actions row beside plain links:
   without this they are block elements and take a whole line to themselves. */
.snap-track,
.snap-fresh { display: inline-flex; }
.snap-track .btn.is-on {
  background: var(--brand-100); border-color: transparent; color: var(--brand-700);
}

/* ---- workspace chooser, in the site header -------------------------------
   Which workspace the reader is working in, on every page rather than only
   the dashboard: the header carries the analyse box, so "where will this be
   tracked" is asked from everywhere and was previously answered by whichever
   workspace sorted first.

   Built to read as a sibling of .account-chip, which sits beside it: the same
   pill, the same square mark, the same border. The two together say who you
   are and which workspace you are in, which is one thought. */
.ws-chooser { position: relative; flex: none; }

.ws-current {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .2rem .5rem .2rem .2rem; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--ink); font: inherit; cursor: pointer;
  max-width: 13rem;
}
.ws-current:hover { border-color: var(--brand-600); }

/* Round, like the avatar beside it: a rounded square inside a pill left
   its corners crowding the curve. */
.ws-current-mark {
  display: grid; place-items: center; flex: none;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--brand-600); color: #fff;
  font-size: .68rem; font-weight: 800; letter-spacing: .02em;
}
.ws-current-name {
  font-size: .82rem; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-caret { width: 14px; height: 14px; flex: none; color: var(--ink-3); }

/* Right-aligned: the chooser sits near the end of the header, so a menu
   anchored left would hang off the edge on a narrow window. */
.ws-menu {
  position: absolute; z-index: 30; top: calc(100% + .35rem); right: 0;
  min-width: 15rem; padding: .3rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 12px 28px rgb(15 23 42 / .13);
}

.ws-menu-head {
  margin: 0; padding: .4rem .5rem .3rem;
  font-size: .68rem; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-3);
}

.ws-opt {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  padding: .4rem .5rem; border: 0; border-radius: 9px;
  background: none; color: var(--ink); font: inherit; text-align: left;
  cursor: pointer;
}
.ws-opt:hover { background: var(--surface-2); }
.ws-opt.is-on { background: var(--brand-100); }

.ws-opt-mark {
  display: grid; place-items: center; flex: none;
  width: 22px; height: 22px; border-radius: 6px;
  background: var(--brand-600); color: #fff;
  font-size: .66rem; font-weight: 800;
}

/* The name and the role stack, so a long client name keeps its own line
   rather than pushing the role off the menu. */
.ws-opt-text { display: grid; gap: .05rem; min-width: 0; flex: 1; }
.ws-opt-name {
  font-size: .82rem; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ws-opt-role { font-size: .7rem; color: var(--ink-3); }
.ws-opt-tick { width: 15px; height: 15px; flex: none; color: var(--brand-700); }

.ws-menu-foot {
  display: block; margin-top: .25rem; padding: .45rem .5rem;
  border-top: 1px solid var(--line);
  font-size: .76rem; font-weight: 700; color: var(--brand-700);
  text-decoration: none;
}
.ws-menu-foot:hover { text-decoration: underline; }

/* The name is the first thing to go when the header runs out of room: the
   mark still says which workspace is active, and the menu still opens. */
@media (max-width: 900px) {
  .ws-current-name { display: none; }
  .ws-current { max-width: none; padding-right: .3rem; }
}

/* ---------------------------------------------------------------------------
 * API keys
 *
 * Two things on one page because they are one question: somebody is here to
 * make a key or to find out why something stopped, and the second is nearly
 * always the allowance. The meters come first for that reason.
 * ------------------------------------------------------------------------ */
.api-usage {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .7rem;
  margin: 0 0 .6rem;
}
.api-meter {
  padding: .75rem .9rem .8rem;
  border: 1px solid var(--line-2); border-radius: .55rem;
  background: var(--surface);
}
.api-meter-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: .6rem;
  margin-bottom: .5rem;
}
.api-meter-head b { font-size: .82rem; font-weight: 700; color: var(--ink); }
.api-meter-head span {
  font-size: .78rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* A track rather than a bare bar: an empty allowance and a missing bar look
   the same without something behind it. */
.api-meter-track {
  height: .35rem; border-radius: .2rem;
  background: var(--line-2);
  overflow: hidden;
}
.api-meter-track span {
  display: block; height: 100%;
  /* A sliver rather than nothing at 13 of 4,000: a bar that rounds to zero
     width is indistinguishable from one that failed to render. */
  min-width: .35rem;
  background: var(--brand-500);
  border-radius: inherit;
}
.api-meter.is-warn .api-meter-track span { background: var(--amber-fill); }
.api-meter.is-bad .api-meter-track span { background: var(--red-fill); }

.api-meter-note {
  margin: .45rem 0 0;
  font-size: .75rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.api-usage-note {
  margin: 0 0 1.1rem;
  font-size: .78rem; color: var(--ink-3); line-height: 1.5;
}

.api-head {
  display: flex; align-items: center; justify-content: space-between; gap: .8rem;
  margin: 0 0 .7rem;
}
.api-count {
  font-size: .78rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
/* Amber only when it is the reason the button is off, not as a warning at
   two of three: a count that shouts before it matters gets ignored. */
.api-count.is-full { color: var(--amber-text); font-weight: 600; }


/* The name leads, the key follows.
 *
 * The prefix is how the system identifies a row and the name is how a person
 * does, and a person is who reads this table: "Deploy pipeline" is what
 * somebody scans for when deciding what to revoke, not wrp_1V6njbLH. The key
 * is still there, quiet, for matching against a log. */
.api-name {
  display: block;
  font-size: .84rem; font-weight: 600; color: var(--ink);
}
.api-prefix {
  display: block; margin-top: .15rem;
  font-size: .74rem;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  color: var(--ink-3);
}
.api-by { display: block; font-size: .74rem; color: var(--ink-3); }

.api-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.api-table .row-actions { text-align: right; white-space: nowrap; }

/* Centred, not top-aligned.
 *
 * Two cells carry a second line (the key under its name, who created it under
 * the date) and the other three carry one. Aligned to the top, those three sit
 * level with the first line of a two-line cell and the row reads as though it
 * is hanging off its own heading. */
.api-table td { vertical-align: middle; }

/* Quiet text carries its own size as well as its colour. It stands in for a
   button in a few places, notably where the IRN export has nothing to offer,
   and without a size it inherited the page's base and read as a heading next
   to the control it replaces. */
.t-quiet { color: var(--ink-3); font-size: .84rem; }

/* A revoked key stays because it stays accountable, but it is history: struck
   through so the row reads as past tense without having to be read. */
.api-table tr.is-revoked .api-name {
  text-decoration: line-through;
  color: var(--ink-3);
  font-weight: 500;
}
.api-revoked {
  margin: .9rem 0 0;
  border: 1px solid var(--line-2); border-radius: .55rem;
  background: var(--surface);
  overflow: hidden;
}
.api-revoked > summary {
  padding: .55rem .8rem; cursor: pointer; list-style: none;
  font-size: .8rem; color: var(--ink-2);
}
.api-revoked > summary::-webkit-details-marker { display: none; }
.api-revoked > summary:hover {
  background: color-mix(in srgb, var(--brand-100) 55%, var(--surface));
}
.api-revoked > summary:focus-visible {
  outline: 2px solid var(--brand-600);
  outline-offset: -2px;
}

/* The nested table brings its own border and 14px radius, which inside this
   one draws a second rounded box over the first: the white corners of the
   inner wrapper sit proud of the outer curve. Stripped back to a plain
   region, so only the outer edge is drawn. */
.api-revoked .dash-table-wrap {
  border: 0; border-top: 1px solid var(--line-2);
  border-radius: 0;
  background: transparent;
}
.api-revoked .dash-table { background: var(--surface); }

/* The one look at a new key.
 *
 * Wider than a normal dialog because the key is 46 characters and wrapping it
 * across two lines invites a half-copy. The warning is above the key, not
 * below: it has to be read before the reader reaches for Copy and leaves. */
.api-reveal { max-width: 34rem; }
.api-reveal-warn {
  padding: .6rem .75rem;
  border-radius: .45rem;
  background: color-mix(in srgb, var(--amber-fill) 14%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--amber-fill) 34%, transparent);
  color: var(--ink-2);
  font-size: .8rem; line-height: 1.5;
}
.api-reveal-key {
  display: flex; align-items: center; gap: .6rem;
  margin: .8rem 0 .6rem;
  padding: .6rem .7rem;
  border: 1px solid var(--line-2); border-radius: .45rem;
  background: var(--surface-2);
}
.api-reveal-key code {
  flex: 1 1 auto; min-width: 0;
  font-size: .8rem;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  word-break: break-all;
}
.api-reveal-use {
  margin: 0;
  font-size: .75rem; color: var(--ink-3); line-height: 1.6;
}
.api-reveal-use code {
  display: block; margin-top: .3rem;
  font-size: .72rem; word-break: break-all;
  color: var(--ink-2);
}

@media (max-width: 700px) {
  .api-usage { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------------------------------------------------------------------
 * Plans you can buy from the dashboard
 *
 * Cards rather than a list, because a price is a thing you compare: two
 * figures on adjacent rows read as a sequence, where two side by side read as
 * a choice, which is what they are.
 * ------------------------------------------------------------------------ */
.offers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: .7rem;
  margin: 0 0 1.4rem;
}
.offer {
  padding: 1rem 1.1rem 1.1rem;
  border: 1px solid var(--line-2); border-radius: .6rem;
  background: var(--surface);
}

/* An upgrade is the one being offered; a downgrade is one being permitted.
   The border says which without a badge having to. */
.offer.is-up { border-color: color-mix(in srgb, var(--brand-500) 30%, transparent); }

.offer h3 {
  margin: 0 0 .5rem;
  font-size: .95rem; font-weight: 700; color: var(--ink);
}
.offer-price {
  margin: 0 0 .15rem;
  font-size: 1.5rem; font-weight: 700; color: var(--ink);
  line-height: 1.1;
}
.offer-price span {
  font-size: .8rem; font-weight: 500; color: var(--ink-3);
}
.offer-year {
  margin: 0 0 .7rem;
  font-size: .78rem; color: var(--ink-3);
}
.offer-blurb {
  margin: 0 0 .9rem;
  font-size: .82rem; color: var(--ink-2); line-height: 1.5;
}

/* Two buttons where there is a real choice of term, one where there is not.
   They share the row rather than stacking, so the card height does not depend
   on how many a plan happens to offer. */
.offer-actions { display: flex; gap: .4rem; flex-wrap: wrap; }
.offer-actions form { display: flex; }
.offer-actions .btn { white-space: nowrap; }

/* The term line under the current plan, which is the thing a paying customer
   opens this page to read. Amber only when it is a warning. */
.plan-note-warn { color: var(--amber-text); font-weight: 600; }

/* ---- billing details ---- */
.bill-form select,
.bill-form textarea,
.bill-form input[type="text"] { width: 100%; }
.bill-form textarea { resize: vertical; min-height: 4.5rem; }

/* ---------------------------------------------------------------------------
 * The e-invoice desk
 *
 * A state per row, and the preview before a batch is stamped. The colours are
 * the same three the rest of the product uses for good, waiting and wrong, so
 * a pill here means what a pill means anywhere else.
 * ------------------------------------------------------------------------ */
.pill {
  display: inline-block;
  padding: .12rem .45rem;
  border-radius: 999px;
  font-size: .72rem; font-weight: 600; white-space: nowrap;
}
.pill-good { background: var(--green-100); color: var(--green-text); }
.pill-warn { background: var(--amber-100); color: var(--amber-text); }
.pill-bad { background: var(--red-100); color: var(--red-text); }

.filter-row { display: flex; gap: .4rem; flex-wrap: wrap; margin: 0 0 .9rem; }
.filter-chip {
  padding: .3rem .6rem;
  border: 1px solid var(--line-2); border-radius: 999px;
  font-size: .78rem; color: var(--ink-2); text-decoration: none;
}
.filter-chip:hover { border-color: var(--line); background: var(--surface); }
.filter-chip.is-on {
  background: var(--brand-600); border-color: var(--brand-600); color: #fff;
}

/* Wider than a normal dialog: the preview is a list of invoice numbers, and
   wrapping them would make a batch harder to scan than it needs to be. */
.irn-dialog { max-width: 32rem; }
.irn-dialog input[type="file"] { width: 100%; margin-bottom: .6rem; }
/* What the file would do, once it has been read. Its own block rather than
   a bare paragraph after the picker: without a gap the summary sat flush
   against the chosen filename and read as part of the control. */
.irn-dialog [data-irn-preview] { margin-top: .9rem; }
.irn-dialog [data-irn-preview] > p { margin: 0; }

.irn-rows {
  max-height: 14rem; overflow-y: auto;
  margin: .6rem 0 0; padding: 0; list-style: none;
  border: 1px solid var(--line-2); border-radius: .45rem;
}
.irn-rows li {
  display: flex; align-items: baseline; justify-content: space-between; gap: .6rem;
  padding: .35rem .6rem;
  border-top: 1px solid var(--line-2);
  font-size: .78rem;
}
.irn-rows li:first-child { border-top: 0; }
.irn-rows code { font-size: .75rem; color: var(--ink-2); }

/* The correction control, under the Stamped pill it undoes.
   
   Grey and smaller than an ordinary button: it is the rare corrective case,
   and the accent-bordered button it started as was wider than the pill above
   it and drew the eye to undoing something that is usually correct. Red only
   on hover, where the intent is already clear. */
.irn-clear { margin-top: .25rem; }
.irn-clear-go {
  display: inline-flex; align-items: center; justify-content: center;
  height: 22px; padding: 0 .5rem; cursor: pointer;
  font: inherit; font-size: .7rem; font-weight: 600;
  color: var(--ink-3);
  /* Outline: no fill, so it reads as a secondary action next to the pill
     above it rather than another filled chip. */
  background: none;
  border: 1px solid var(--line); border-radius: 6px;
  opacity: 0; transition: opacity .12s ease, color .12s ease, border-color .12s ease;
}
.irn-clear-go:hover {
  color: var(--red-text);
  border-color: color-mix(in srgb, var(--red-text) 45%, transparent);
}
/* Only while the row is under the pointer. A control for undoing something
   that is usually correct should not be on screen for every stamped row at
   once, inviting a click nobody meant. */
tr:hover .irn-clear-go { opacity: 1; }
/* Reachable by keyboard, where there is no hover to reveal it. */
.irn-clear-go:focus-visible {
  opacity: 1;
  outline: 2px solid var(--brand-600); outline-offset: 2px;
}
@media (hover: none) {
  /* A touch screen has no hover, so hiding it would hide it for good. */
  .irn-clear-go { opacity: 1; }
}

.irn-match { color: var(--green-text); font-weight: 600; }
.irn-already_stamped { color: var(--ink-3); }
.irn-not_found { color: var(--amber-text); font-weight: 600; }
.irn-bad { color: var(--red-text); font-size: .82rem; }

/* ============================================================================
   Legal pages (/terms, /privacy, /cookies, /gdpr, /dpa)

   A narrower measure than the rest of the site and a plainer hierarchy: these
   are read start to finish rather than scanned, and the contents list at the
   top is how anybody actually uses them, jumping to the one clause they came
   for.
   ============================================================================ */
.legal-hero { padding: clamp(48px,6vw,80px) 0 clamp(24px,3vw,36px); text-align: center; }
.legal-hero h1 { font-size: clamp(30px,4vw,42px); font-weight: 800; margin: 0 0 10px; letter-spacing: -0.02em; }
.legal-subtitle { font-size: 17px; color: var(--ink-2); max-width: 640px; margin: 0 auto; line-height: 1.6; }
.legal-content { max-width: 820px; margin: 0 auto; padding: 0 20px clamp(48px,6vw,88px); }
.legal-updated { font-size: 13.5px; color: var(--ink-3); margin-bottom: 32px; text-align: center; }
.legal-toc { background: var(--surface-2); border: 1px solid var(--border); border-radius: 14px; padding: 20px 24px; margin-bottom: 40px; }
.legal-toc h3 { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-3); margin: 0 0 12px; }
.legal-toc ol { columns: 2; column-gap: 28px; padding-left: 20px; margin: 0; }
.legal-toc li { font-size: 13.5px; margin-bottom: 8px; break-inside: avoid; }
.legal-toc a { color: var(--ink-2); text-decoration: none; }
.legal-toc a:hover { color: var(--brand-text); }
.legal-section { margin-bottom: 40px; scroll-margin-top: 90px; }
.legal-section h2 { font-size: 21px; font-weight: 700; margin: 0 0 16px; padding-bottom: 8px; border-bottom: 2px solid var(--border); }
.legal-section h3 { font-size: 15.5px; font-weight: 650; margin: 24px 0 8px; }
.legal-section p, .legal-section li { font-size: 14.5px; color: var(--ink-2); line-height: 1.75; margin-bottom: 12px; }
.legal-section ul, .legal-section ol { padding-left: 22px; margin-bottom: 16px; }
.legal-section li { margin-bottom: 8px; }
.legal-section strong { color: var(--ink); font-weight: 650; }
.legal-section a { color: var(--brand-text); text-decoration: none; }
.legal-section a:hover { text-decoration: underline; }
.legal-section code { font-family: var(--mono); font-size: 12.5px; background: var(--surface-2);
  border: 1px solid var(--border); border-radius: 5px; padding: 1px 5px; color: var(--ink); }
.legal-table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13.5px; }
.legal-table th, .legal-table td { padding: 10px 14px; text-align: left; border: 1px solid var(--border); vertical-align: top; }
.legal-table th { background: var(--surface-2); font-weight: 650; color: var(--ink); }
.legal-table td { color: var(--ink-2); }
.legal-table-wrap { overflow-x: auto; }
.legal-highlight { background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--brand-text);
  border-radius: 10px; padding: 16px 18px; margin: 18px 0; }
/* The box sets its own padding, so the first paragraph must not bring a top
   margin with it: the two stack and the text sits low in the box. Zeroing
   both and reinstating the gap only between paragraphs keeps it centred. */
.legal-highlight p { margin-top: 0; margin-bottom: 0; }
.legal-highlight p + p { margin-top: 8px; }
@media (max-width: 640px) { .legal-toc ol { columns: 1; } }

/* A date input in a ws-form.
   The styled inputs on these pages all sit inside .ws-row, which this one does
   not need: it is a single control, not a field and a button sharing a line.
   Without this it renders as the browser's default and reads as though it
   belongs to a different page. */
/* Fields sitting directly in a .ws-form, rather than inside a .ws-row.
   Only date was listed here, so a plain text input or a textarea fell back
   to the browser's own control and rendered as a bare box beside the
   themed selects around it.

   Child combinator throughout. .ws-row nests inside .ws-form on several
   pages, and a descendant selector here carries higher specificity than
   .ws-row input, so it would replace that row's flex sizing with a full
   width field and break every inline row on the site. */
/* Matched by depth rather than by level, because the wrappers nest: the
   Indian fields sit in one, and the GSTIN inside that sits in a second of
   its own so it can be hidden for an individual. Selectors naming one level
   of div left that field and the foreign Tax ID unstyled.

   Two exclusions, both load-bearing. .ws-row lays its fields out inline and
   sizes them itself, so a full-width rule here would break every inline row
   on the site. .dd-search is the themed dropdown's own filter box, which is
   deliberately borderless inside its menu. */
.ws-form input[type="date"]:not(.ws-row *):not(.dd *),
.ws-form input[type="text"]:not(.ws-row *):not(.dd *),
.ws-form input[type="email"]:not(.ws-row *):not(.dd *),
.ws-form input[type="url"]:not(.ws-row *):not(.dd *),
.ws-form input[type="number"]:not(.ws-row *):not(.dd *),
.ws-form input[type="password"]:not(.ws-row *):not(.dd *),
.ws-form textarea {
  width: 100%; box-sizing: border-box;
  font: inherit; font-size: .88rem; color: var(--ink);
  padding: .5rem .7rem; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--line);
}
/* Date sizes to its own content: stretched to the full row it is a wide
   box holding a short value. */
.ws-form input[type="date"]:not(.ws-row *) { width: auto; }
.ws-form textarea { resize: vertical; line-height: 1.55; }
.ws-form input:not(.ws-row *):not(.dd *):focus,
.ws-form textarea:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.ws-form input:not(.ws-row *):not(.dd *)::placeholder,
.ws-form textarea::placeholder { color: var(--ink-3); }

/* Fields stack, so each label needs air above it.

   Set on the label rather than as a sibling rule on the field before it.
   A label can follow an input, a textarea, a themed dropdown or a wrapper
   div, and a sibling selector has to name every one of those; miss one and
   that gap alone collapses. :first-child keeps the top of the form flush. */
/* Any depth of wrapper, because the blocks nest: the Indian fields sit in
   one, and the GSTIN inside that sits in a second of its own so it can be
   hidden for an individual. A selector naming one level of div left that
   label with no gap above it. .ws-row is excluded because a row lays its
   own labels out side by side. */
.ws-form label:not(.ws-row label):not(.btn) { margin-top: .95rem; }

/* Only the label that opens the form loses its margin.

   A wrapper div mid-form, the block holding the Indian fields, is a grouping
   and not a new start, so the first label inside it is spaced from whatever
   sits above the wrapper like any other. Zeroing every wrapper's first label
   made that one gap collapse. */
.ws-form > label:first-child,
.ws-form > div:not(.ws-row):first-child > label:first-child,
.ws-form > [type="hidden"] + label { margin-top: 0; }


/* ==========================================================================
   System health (admin)

   The page has one job above the fold: say whether work is getting done. The
   alarm is a band rather than a pill because it has to survive being skimmed,
   and the chart is bars rather than a line because the useful reading is the
   cliff where a working system stopped, which a smoothed line hides.
   ========================================================================== */

.health-alarm {
  margin: 0 0 1.2rem; padding: .85rem 1rem;
  border-radius: 12px; font-size: .88rem; line-height: 1.55;
  color: var(--red-text);
  background: var(--red-100);
  border: 1px solid color-mix(in srgb, var(--red-text) 30%, transparent);
  border-left: 3px solid var(--red-text);
}
.health-alarm b { font-weight: 700; }
.health-alarm code {
  font-family: var(--mono); font-size: .82rem;
  padding: .1rem .35rem; border-radius: 5px;
  background: color-mix(in srgb, var(--red-text) 12%, transparent);
}
/* Nothing is waiting yet, so it is a warning rather than a failure. */
.health-alarm.is-quiet {
  color: var(--amber-text); background: var(--amber-100);
  border-color: color-mix(in srgb, var(--amber-text) 30%, transparent);
  border-left-color: var(--amber-text);
}

.health-chart {
  display: flex; align-items: flex-end; gap: 3px;
  height: 92px; margin: 0 0 1.5rem; padding: .7rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
}
.health-bar { flex: 1; min-width: 0; height: 100%; display: flex; align-items: flex-end; }
.health-bar-fill {
  width: 100%; border-radius: 3px 3px 0 0;
  background: var(--brand-500);
}
/* An hour with a failure in it is worth finding by eye. */
.health-bar-fill.has-failed { background: var(--red-text); }

.health-error {
  font-family: var(--mono); font-size: .76rem;
  color: var(--ink-2); line-height: 1.5;
  max-width: 30rem;
}
.health-holding { font-size: .76rem; color: var(--ink-3); margin-right: .4rem; }

/* .t-url is only blocked out inside .t-page, so the same span in a .t-when
   cell ran on the end of the date as one string. The health tables use it for
   a second line under the time, which needs the same treatment. */
.t-when .t-url,
.t-act .t-url {
  display: block; margin-top: .15rem;
  font-family: var(--mono); font-size: .72rem; color: var(--ink-3);
}

/* An hour with one job in it still has to be visible: without a floor the
   fill rounds to a hairline and reads as an empty hour, which is the one
   thing this chart must not get wrong. */
.health-bar-fill { min-height: 3px; }

/* ==========================================================================
   One customer (admin)

   A profile rather than a table: the header answers who and what plan, the
   meters answer whether they have run out, and everything below is detail
   somebody scrolls to only when the top did not settle the question.
   ========================================================================== */

.cust-back { margin: 0 0 1rem; font-size: .82rem; }
.cust-back a { color: var(--ink-3); text-decoration: none; }
.cust-back a:hover { color: var(--brand-600); }

.cust-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 1.5rem;
  margin: 0 0 1.2rem; padding: 1.1rem 1.25rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
}
.cust-name { margin: 0; font-size: 1.25rem; font-weight: 750; letter-spacing: -.01em; }
.cust-sub { margin: .3rem 0 0; font-size: .84rem; color: var(--ink-3); }
.cust-badges { margin: .6rem 0 0; display: flex; flex-wrap: wrap; gap: .35rem; }
.cust-plan { text-align: right; flex: 0 0 auto; }
.cust-plan .stat-figure { margin: .2rem 0 .1rem; }

/* The billing sentence reuses the legal callout, which is already a quiet
   bordered band, rather than introducing a third box style. */
.cust-billing { margin: 0 0 1.4rem; }
.cust-billing p { font-size: .86rem; margin: 0; }

.cust-of { font-size: .82rem; font-weight: 500; color: var(--ink-3); margin-left: .2rem; }
.cust-meter {
  height: 4px; margin: .5rem 0 .45rem; border-radius: 2px;
  background: var(--line-2); overflow: hidden;
}
.cust-meter-fill { height: 100%; background: var(--brand-500); border-radius: 2px; }
/* Approaching a cap is worth noticing before it bites, and over it explains
   the support email they are about to send. */
.cust-meter-fill.is-near { background: var(--amber-text); }
.cust-meter-fill.is-over { background: var(--red-text); }

.cust-totals { margin: 0 0 .7rem; font-size: .82rem; color: var(--ink-3); }
.cust-total { font-weight: 700; color: var(--ink); margin-left: .45rem; }

/* A revoked key is still information, just not current information. */
.dash-table tr.is-muted td { opacity: .58; }

@media (max-width: 40rem) {
  .cust-head { flex-direction: column; }
  .cust-plan { text-align: left; }
}

/* ==========================================================================
   Growth (admin)

   Every figure is stated against the period before it, so the cards carry a
   delta rather than a bare number. The charts are bars because the reading
   that matters is which days were dead, and a smoothed line hides exactly
   that.
   ========================================================================== */

.grow-up { font-size: .8rem; font-weight: 700; color: var(--green-text); }
.grow-down { font-size: .8rem; font-weight: 700; color: var(--red-text); }
.grow-flat { font-size: .8rem; font-weight: 600; color: var(--ink-3); }

.grow-chart {
  display: flex; align-items: flex-end; gap: 2px;
  height: 110px; padding: .7rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
}
.grow-bar { flex: 1; min-width: 0; height: 100%; display: flex; align-items: flex-end; }
.grow-bar-fill { width: 100%; border-radius: 2px 2px 0 0; min-height: 2px; }
.grow-bar-fill.is-brand { background: var(--brand-500); }
.grow-bar-fill.is-ink { background: var(--ink-3); }
/* A day with nothing in it gets a floor so the axis stays legible as a line
   rather than disappearing where the data does. */
.grow-bar-fill[style*="height:0"] { background: var(--line-2); min-height: 2px; }

.grow-axis {
  display: flex; justify-content: space-between;
  margin: .4rem .2rem 1.5rem;
  font-size: .72rem; color: var(--ink-3); font-variant-numeric: tabular-nums;
}

.grow-share {
  height: 6px; min-width: 6rem; border-radius: 3px;
  background: var(--line-2); overflow: hidden;
}
.grow-share-fill { height: 100%; background: var(--brand-500); border-radius: 3px; }

.grow-money { display: block; font-variant-numeric: tabular-nums; }
.grow-money + .grow-money { margin-top: .35rem; }

.dash-table tfoot td {
  border-top: 2px solid var(--line);
  font-variant-numeric: tabular-nums;
}

/* Ops: a report shared across workspaces is worth flagging before somebody
   deletes it, since removing it removes it for all of them. */
.ops-shared {
  display: block; margin-bottom: .3rem;
  font-size: .72rem; color: var(--amber-text); font-weight: 600;
}

/* ==========================================================================
   Email catalogue (admin)

   A list beside a preview, because the question is always comparative: does
   this one still match the others. The preview is an iframe since an email's
   own styles are written to survive Outlook and would otherwise leak into the
   console around it.
   ========================================================================== */

.mailcat { display: grid; grid-template-columns: 19rem 1fr; gap: 1.25rem; align-items: start; }

.mailcat-list { display: flex; flex-direction: column; gap: .5rem; }
.mailcat-item {
  display: block; padding: .7rem .85rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  text-decoration: none; color: inherit;
}
.mailcat-item:hover { border-color: var(--brand-400, var(--brand-500)); }
.mailcat-item.is-on { border-color: var(--brand-500); background: var(--brand-100); }
.mailcat-item b { display: block; font-size: .86rem; font-weight: 700; }
.mailcat-item span { display: block; margin-top: .25rem; font-size: .76rem; color: var(--ink-3); line-height: 1.5; }
.mailcat-to { font-variant-numeric: tabular-nums; }
.mailcat-plain { color: var(--amber-text) !important; font-weight: 600; }

.mailcat-panel { min-width: 0; }
.mailcat-subject {
  margin-bottom: .8rem; padding: .7rem .85rem;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 10px;
}
.mailcat-subject p { margin: .2rem 0 0; font-size: .95rem; font-weight: 650; }

.mailcat-frame {
  width: 100%; height: 34rem; border: 1px solid var(--line); border-radius: 12px;
  /* The email paints its own ground, which is a light one: without this the
     iframe sits transparent on the dark console and the text vanishes. */
  background: #fff;
}
.mailcat-text {
  margin: 0; padding: 1rem; overflow-x: auto;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  font-family: var(--mono); font-size: .78rem; line-height: 1.65; color: var(--ink-2);
  white-space: pre-wrap;
}
.mailcat-details { margin-top: .8rem; }
.mailcat-details summary {
  cursor: pointer; font-size: .8rem; color: var(--ink-3);
  padding: .3rem 0; user-select: none;
}
.mailcat-details summary:hover { color: var(--brand-600); }
.mailcat-details .mailcat-text { margin-top: .5rem; }

@media (max-width: 56rem) {
  .mailcat { grid-template-columns: 1fr; }
  .mailcat-frame { height: 26rem; }
}

/* ==========================================================================
   Errors (admin)

   A list of faults, not of occurrences. The trace opens in place under its
   row rather than on a page of its own, because comparing two faults means
   seeing both and a separate page makes that two navigations.
   ========================================================================== */

.errtabs { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .7rem; }
.errtabs-kind { margin-bottom: 1rem; }
.errtab {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .32rem .7rem; border-radius: 999px;
  font-size: .78rem; font-weight: 600; text-decoration: none;
  color: var(--ink-2); background: var(--surface); border: 1px solid var(--line);
}
.errtab:hover { border-color: var(--brand-500); color: var(--brand-600); }
.errtab.is-on { background: var(--brand-500); border-color: var(--brand-500); color: var(--on-brand); }
.errtab span {
  font-size: .7rem; font-weight: 700; padding: 0 .3rem;
  border-radius: 999px; background: var(--line-2); color: var(--ink-3);
}

/* Plan and usage, and Billing: two halves of one subject, so each page
   carries the way to the other rather than both hiding in the account menu. */
.account-tabs { margin-bottom: 1.25rem; }

/* Lower plans, put away rather than removed.
   Reads as an aside, so the summary is quiet text rather than a button: it
   is an answer to a question somebody already has, not an offer. */
.offer-lower { margin-top: 1.5rem; }
/* Alone in its section, directly under the heading that explains it. */
.offer-lower.is-only { margin-top: 0; }
.offer-lower > summary {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem 0; cursor: pointer; list-style: none;
  font-size: .82rem; font-weight: 600; color: var(--ink-3);
}
.offer-lower > summary::-webkit-details-marker { display: none; }
.offer-lower > summary::before {
  content: ''; width: 0; height: 0; flex: none;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transition: transform .15s ease;
}
.offer-lower[open] > summary::before { transform: rotate(90deg); }
.offer-lower > summary:hover { color: var(--ink-2); }
.offer-lower > .dash-section-note { margin: .5rem 0 1rem; }
.errtab.is-on span { background: rgba(255,255,255,.22); color: inherit; }

.err-link { text-decoration: none; color: inherit; }
.err-link:hover b { color: var(--brand-600); }
.err-where { color: var(--ink-3); opacity: .8; }

.err-detail td { background: var(--surface-2); }
.err-panel { padding: .3rem 0 .6rem; }
.err-message {
  margin: 0 0 .8rem; font-size: .86rem; line-height: 1.6; color: var(--ink);
  font-weight: 600;
}
.err-facts { display: flex; flex-wrap: wrap; gap: .5rem 1.4rem; margin-bottom: .9rem; }
.err-fact span {
  display: block; font-size: .68rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--ink-3); font-weight: 700; margin-bottom: .15rem;
}
.err-fact code {
  font-family: var(--mono); font-size: .76rem; color: var(--ink-2);
  overflow-wrap: anywhere;
}
.err-trace {
  margin: 0 0 .9rem; padding: .8rem; max-height: 22rem; overflow: auto;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
  font-family: var(--mono); font-size: .72rem; line-height: 1.6; color: var(--ink-2);
  white-space: pre;
}
.err-actions { display: flex; gap: .4rem; align-items: center; }

/* Search engines (admin): the generated files shown as files, since that is
   how a crawler reads them and how a mistake in one becomes visible. */
.seo-file {
  margin: 0 0 1.5rem; padding: 1rem; overflow-x: auto;
  background: var(--surface); border: 1px solid var(--line); border-radius: 12px;
  font-family: var(--mono); font-size: .78rem; line-height: 1.7; color: var(--ink-2);
  white-space: pre;
}
.seo-blocks { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1.5rem; }
.seo-block {
  padding: .2rem .5rem; border-radius: 6px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-family: var(--mono); font-size: .76rem; color: var(--ink-2);
}

/* ==========================================================================
   Global search (admin)

   A box in the bar and a page of grouped results. Grouped rather than ranked,
   because "three reports and one account" is a more useful answer than a
   single list whose ordering nobody can explain.
   ========================================================================== */

.gs-bar { position: relative; display: flex; align-items: center; }
.gs-bar svg {
  position: absolute; left: .6rem; width: 15px; height: 15px;
  color: var(--ink-3); pointer-events: none;
}
.gs-bar input {
  width: 15rem; max-width: 40vw;
  padding: .4rem .7rem .4rem 1.9rem;
  font: inherit; font-size: .82rem; color: var(--ink);
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px;
}
.gs-bar input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}

.gs-form { margin-bottom: 1.5rem; }

.gs-group { margin-bottom: 1.6rem; }
.gs-group h3 {
  display: flex; align-items: center; gap: .45rem;
  margin: 0 0 .6rem; font-size: .78rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3);
}
.gs-group h3 span {
  padding: 0 .35rem; border-radius: 999px;
  background: var(--line-2); color: var(--ink-3); font-size: .7rem;
}
.gs-hits { display: flex; flex-direction: column; gap: .4rem; }
.gs-hit {
  display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: .2rem 1rem;
  padding: .6rem .85rem; text-decoration: none; color: inherit;
  background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
}
.gs-hit:hover { border-color: var(--brand-500); }
.gs-hit b { font-size: .86rem; font-weight: 650; overflow-wrap: anywhere; }
.gs-hit span {
  grid-column: 1; font-family: var(--mono); font-size: .73rem;
  color: var(--ink-3); overflow-wrap: anywhere;
}
.gs-hit .gs-meta {
  grid-column: 2; grid-row: 1; font-family: inherit; font-size: .76rem;
  font-weight: 600; color: var(--ink-2); white-space: nowrap;
}

@media (max-width: 46rem) {
  .gs-bar input { width: 9rem; }
}

/* A dropdown sharing a row with its own submit button.
   .dd-button is width:100% so the control fills whatever holds it, which is
   right in a stacked form and wrong beside a button: the pair should read as
   one control and an action, not a full-width field with a button under it.
   Sized to its content with a ceiling, so a long option does not push the
   button off the row. */
.ws-row > .dd { flex: 0 1 auto; min-width: 0; max-width: 22rem; }
.ws-row > .dd .dd-button { height: 38px; }
.ws-row > .btn { flex: none; }

/* The actions at the foot of a settings form.
   The class was used and never styled, so the buttons sat hard against the
   hint line above them and read as part of it. A row with its own gap, and
   clear air above, separates the thing you are setting from the act of
   saving it. */
.ws-form-actions {
  display: flex; flex-wrap: wrap; gap: .5rem;
  margin-top: 1rem;
}

/* A file picker that matches the rest of the form.
   A browser's own file control cannot be styled, so it arrives looking like
   nothing else on the page. The input is hidden inside a label, which keeps
   the click target and the keyboard behaviour the browser gives for free,
   and the chosen filename is written beside it. */
.file-pick { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; }
/* A <label> is not a <button>, so it does not inherit the centring the button
   classes assume. Stated here so the icon and its text sit on the same line
   as each other and in the middle of the fixed height .btn-sm gives it. */
.file-pick-button {
  display: inline-flex; align-items: center; align-content: center;
  justify-content: center; gap: .4rem; line-height: 1;
  cursor: pointer;
}
.file-pick-button svg { width: 14px; height: 14px; flex: none; }
.file-pick-button input[type="file"] {
  position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;
}
/* Focus lands on the hidden input, so the ring has to be drawn by the label.
   :has(:focus-visible) rather than :focus-within, because a dialog gives
   focus to its first control on opening and the plain form painted a ring
   around a button nobody had touched. This shows one only when the browser
   judges it warranted, which is keyboard navigation and not a click. */
.file-pick-button:has(:focus-visible) {
  border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.file-pick-name {
  font-size: .78rem; color: var(--ink-3);
  overflow-wrap: anywhere; min-width: 0;
}
.file-pick-name.has-file { color: var(--ink-2); font-weight: 600; }

/* The customer page's tabbed detail.
   The tab strip carries its own bottom border and margin, so a panel needs
   no rule of its own beyond spacing its explanatory line from the table it
   explains. */
/* The strip needs air above it: butted against the meter cards it read as
   part of them rather than as the start of a new section. */
.dash-stats + .tabs { margin-top: 1.5rem; }
/* The count badge sits on --slate-100, which in light mode is a hair off the
   page ground, so the badge vanished and the number read as loose text. A
   surface with a border makes it a badge in both themes. */
.tab .count {
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--ink-3);
}
.tab[aria-selected="true"] .count {
  background: var(--brand-100); border-color: transparent; color: var(--brand-600);
}

.cust-panel-note { margin: 0 0 .9rem; }
/* Without JavaScript every panel shows, and each needs separating from the
   next. With JavaScript only one is visible and the rule costs nothing. */
.cust-panel + .cust-panel { margin-top: 1.6rem; }

/* ==========================================================================
   Account actions (admin)

   Cards rather than one long form, because these are unrelated decisions and
   stacking them invites somebody to fill in two and submit the wrong one.
   Each card carries its own form and its own button.
   ========================================================================== */

.act-grid {
  display: grid; gap: .9rem; margin-bottom: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}
.act-card {
  padding: 1rem 1.1rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
}
.act-card h3 { margin: 0 0 .3rem; font-size: .92rem; font-weight: 700; }
.act-card p {
  margin: 0 0 .9rem; font-size: .78rem; line-height: 1.55; color: var(--ink-3);
}
.act-card .dash-table-wrap { margin-bottom: .9rem; }

.act-form + .act-form { margin-top: .6rem; }
.act-form input[type="text"],
.act-form input[type="number"] {
  font: inherit; font-size: .84rem; color: var(--ink);
  padding: .45rem .65rem; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--line);
}
.act-form > input[type="text"] { width: 100%; margin-top: .5rem; }
/* A day or limit count is at most three digits, so it does not need the room
   a flexible field would take from the control beside it. */
.act-form .ws-row input[type="number"] { flex: 0 0 6rem; min-width: 0; }
.act-form input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}

/* A grant in force is stated before the controls, not beside them: somebody
   about to change a plan needs to know a comp is overriding it. */
.act-banner { margin: 0 0 1.2rem; }
.act-banner p { font-size: .84rem; }

/* An invoice row leads two ways: the number opens the document, the address
   opens the account it belongs to. */
.inv-open { text-decoration: none; color: inherit; }
.inv-open:hover .api-name { color: var(--brand-600); }
.inv-email {
  display: block; margin-top: .15rem;
  font-family: var(--mono); font-size: .72rem; color: var(--ink-3);
  text-decoration: none; overflow-wrap: anywhere;
}
a.inv-email:hover { color: var(--brand-600); }

/* ==========================================================================
   Documentation

   Three columns: every page on the left, this page's content in the middle,
   its headings on the right. The middle column holds a reading measure rather
   than filling the space, because documentation is read in sentences and a
   140-character line is read twice.
   ========================================================================== */

/* The site header is sticky, so every sticky thing inside the docs has to
   start below it rather than under it. One custom property, because the two
   rails and the scroll-margin on headings all need the same number. */
.docs { --docs-top: 4.6rem; }

.docs {
  display: grid;
  grid-template-columns: 16rem minmax(0, 1fr) 13rem;
  gap: 2.5rem;
  max-width: 84rem; margin: 0 auto; padding: 1.5rem 1.25rem 4rem;
  align-items: start;
}

/* ---- the sidebar ---- */

/* The sidebar is a column that fits the viewport, and the nav takes
   whatever is left under the search box.

   The nav used to carry its own max-height of 100vh minus the sticky
   offset, which ignored the search box sitting above it inside this same
   container. The nav was therefore about a search box taller than the room
   it had, so its last couple of links sat below the fold with no way to
   scroll to them. Letting flex do the arithmetic means nothing has to know
   the height of anything else. */
.docs-side {
  position: sticky; top: var(--docs-top);
  display: flex; flex-direction: column;
  max-height: calc(100vh - var(--docs-top) - 1rem);
}
.docs-nav {
  flex: 1; min-height: 0;
  overflow-y: auto;
  padding: 0 .5rem .75rem 0;
}
.docs-nav h3 {
  margin: 1.2rem 0 .4rem; padding-left: .7rem;
  font-size: .7rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3);
}
.docs-nav h3:first-child { margin-top: 0; }
/* The home link now precedes the first section, so that heading needs its
   own reset: :first-child no longer matches it. */
.docs-nav .nav-home + h3 { margin-top: 0; }
.docs-nav ul { list-style: none; margin: 0; padding: 0; }
.docs-nav li { margin: 0; }
.docs-nav a {
  display: block; padding: .38rem .7rem;
  font-size: .85rem; color: var(--ink-2); text-decoration: none;
  border-radius: 8px; line-height: 1.45;
}
.docs-nav a:hover { background: var(--surface-2); color: var(--ink); }
.docs-nav a.is-on { background: var(--brand-100); color: var(--brand-600); font-weight: 650; }

/* Back to the index. Padding matches the section links below it, so the
   hover fill is a symmetrical box. Set apart by weight and by the gap
   beneath it rather than by a rule: the section heading that follows is
   already a divider, and a second one reads as clutter. */
.docs-nav a.nav-home {
  display: flex; align-items: center; gap: .45rem;
  margin-bottom: .55rem; padding: .38rem .7rem;
  font-weight: 600; color: var(--ink);
}
.docs-nav a.nav-home svg { flex: none; color: var(--ink-3); }
.docs-nav a.nav-home:hover svg { color: var(--brand-600); }

/* ---- the article ---- */

.docs-main { min-width: 0; }
.docs-crumb {
  display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  margin: 0 0 .5rem; font-size: .76rem; color: var(--ink-3);
}
.docs-crumb a { color: var(--ink-3); text-decoration: none; }
.docs-crumb a:hover { color: var(--brand-600); }
.docs-crumb b { font-weight: 600; color: var(--ink-2); }
.docs-crumb span { color: var(--ink-3); opacity: .5; }

/* The index sections are jump targets for the breadcrumb, so they clear the
   sticky header the same way the article headings do. */
.docs-group { scroll-margin-top: 5rem; }
.docs-main h1 { margin: 0 0 .5rem; font-size: 1.9rem; font-weight: 780; letter-spacing: -.02em; }
.docs-lede { margin: 0 0 2rem; font-size: 1rem; color: var(--ink-2); line-height: 1.6; }
.docs-main h2 {
  margin: 2.4rem 0 .8rem; padding-top: .6rem;
  font-size: 1.22rem; font-weight: 720; scroll-margin-top: var(--docs-top);
}
.docs-main h3 { margin: 1.8rem 0 .6rem; font-size: 1rem; font-weight: 700; }
.docs-main h4 { margin: 1.4rem 0 .4rem; font-size: .92rem; font-weight: 700; }
.docs-main p, .docs-main li {
  font-size: .92rem; line-height: 1.72; color: var(--ink-2);
  max-width: 40rem;
}
.docs-main p { margin: 0 0 1rem; }
.docs-main a { color: var(--brand-600); }
/* :not(pre code) because a sample block already has its own dark ground and
   its own colouring. Without this the inline rule paints a light box behind
   every highlighted token, which is how the samples became unreadable. */
.docs-main :not(pre) > code {
  font-family: var(--mono); font-size: .84em;
  padding: .1rem .35rem; border-radius: 5px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--ink);
}
.docs-main pre code {
  background: none; border: 0; padding: 0;
  font-size: inherit; color: inherit;
}

/* ---- the contents rail ---- */

.docs-toc {
  position: sticky; top: var(--docs-top); font-size: .82rem;
  max-height: calc(100vh - var(--docs-top) - 2rem);
  overflow-y: auto;
}
.docs-toc-head {
  margin: 0 0 .5rem; font-size: .68rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .07em; color: var(--ink-3);
}
.docs-toc ul { list-style: none; margin: 0; padding: 0; border-left: 2px solid var(--line); }
.docs-toc a {
  display: block; padding: .22rem 0 .22rem .75rem; margin-left: -2px;
  border-left: 2px solid transparent;
  color: var(--ink-3); text-decoration: none; line-height: 1.45;
}
.docs-toc a:hover { color: var(--brand-600); border-left-color: var(--brand-500); }

/* ---- code samples ---- */

.code-set {
  margin: 0 0 1.4rem;
  border: 1px solid var(--code-line); border-radius: 12px;
  background: var(--code-bg);
  overflow: hidden;
}
/* No padding on the strip, so a tab meets the block's own edges.
   Padding here left the open tab floating with a gap above and to its left,
   which read as a misalignment rather than a raised tab. The active state is
   an underline instead of a filled shape, which needs no gap to work. */
.code-tabs {
  display: flex; flex-wrap: wrap;
  background: var(--code-head);
  border-bottom: 1px solid var(--code-line);
}
.code-tab {
  padding: .5rem .85rem; border: 0;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  font: inherit; font-size: .78rem; font-weight: 600;
  color: var(--ink-3); background: none; cursor: pointer;
}
.code-tab:hover { color: var(--ink); }
.code-tab.is-on {
  color: var(--brand-600); border-bottom-color: var(--brand-500);
  background: var(--code-bg);
}
.code-panel { position: relative; }
.code-panel pre {
  margin: 0; padding: 1rem 1.1rem; overflow-x: auto;
  font-family: var(--mono); font-size: .8rem; line-height: 1.65;
  color: var(--code-ink);
}
.code-set.is-single { border-radius: 12px; }

.code-copy {
  position: absolute; top: .5rem; right: .5rem;
  padding: .25rem .6rem; border-radius: 7px;
  font: inherit; font-size: .72rem; font-weight: 600;
  color: var(--ink-3); cursor: pointer;
  background: var(--surface); border: 1px solid var(--code-line);
  opacity: 0; transition: opacity .12s;
}
.code-panel:hover .code-copy, .code-copy:focus { opacity: 1; }
.code-copy:hover { color: var(--brand-600); border-color: var(--brand-500); }

/* Token colours, light theme.
   Darkened from the usual editor palette because these sit on a near-white
   ground: a pastel green that reads well on slate is invisible here. Each
   has to separate from the body ink at a glance and stay legible in print. */
/* On a dark ground the same roles need the light end of each hue instead. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --tok-str: #86EFAC; --tok-key: #7DD3FC; --tok-num: #FDBA74;
    --tok-lit: #C4B5FD; --tok-kw: #F0ABFC; --tok-fn: #93C5FD;
    --tok-flag: #FCD34D; --tok-com: #7C8798;
  }
}
:root[data-theme="dark"] {
  --tok-str: #86EFAC; --tok-key: #7DD3FC; --tok-num: #FDBA74;
  --tok-lit: #C4B5FD; --tok-kw: #F0ABFC; --tok-fn: #93C5FD;
  --tok-flag: #FCD34D; --tok-com: #7C8798;
}
.tok-str  { color: var(--tok-str, #0F7B4F); }
.tok-key  { color: var(--tok-key, #0A5EA8); }
.tok-num  { color: var(--tok-num, #A8530B); }
.tok-lit  { color: var(--tok-lit, #6D28D9); }
.tok-kw   { color: var(--tok-kw, #A21A7C); }
.tok-fn   { color: var(--tok-fn, #1D4FD8); }
.tok-flag { color: var(--tok-flag, #8A5A05); }
.tok-com  { color: var(--tok-com, #6B7787); font-style: italic; }

/* ---- endpoint headings ---- */

.api-verb {
  display: inline-block; padding: .12rem .5rem; border-radius: 6px;
  font-family: var(--mono); font-size: .72rem; font-weight: 700;
  letter-spacing: .02em; vertical-align: .12em;
}
.api-verb.is-get { background: var(--green-100, #dcfce7); color: var(--green-text, #15803d); }
.api-verb.is-post { background: var(--brand-100); color: var(--brand-600); }
.api-verb.is-delete { background: var(--red-100, #fee2e2); color: var(--red-text, #b91c1c); }
.api-title { margin: .1rem 0 .8rem !important; font-weight: 650; color: var(--ink) !important; }
.api-type { font-family: var(--mono); font-size: .78rem; color: var(--ink-3); }
.api-req { font-size: .72rem; font-weight: 700; color: var(--red-text, #b91c1c); }
.api-opt { font-size: .72rem; color: var(--ink-3); }

/* ---- tables, notes, screenshots ---- */

/* The radius lives on the wrapper because border-collapse: collapse ignores
   a radius on the table itself: the cells' own borders square the corners
   off again. The wrapper rounds and clips, and the cells drop their outer
   borders so nothing pokes through the curve. */
.doc-table-wrap {
  overflow: hidden; overflow-x: auto; margin: 0 0 1.4rem;
  border: 1px solid var(--line); border-radius: 12px;
}
.doc-table { width: 100%; border-collapse: collapse; font-size: .86rem; }
.doc-table th, .doc-table td {
  padding: .6rem .8rem; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--line); line-height: 1.55;
}
.doc-table th + th, .doc-table td + td { border-left: 1px solid var(--line); }
.doc-table tbody tr:last-child th,
.doc-table tbody tr:last-child td { border-bottom: 0; }
.doc-table th { background: var(--surface-2); font-weight: 650; color: var(--ink); }
.doc-table td { color: var(--ink-2); }

.doc-note {
  margin: 0 0 1.4rem; padding: .85rem 1rem;
  background: var(--surface-2); border: 1px solid var(--line);
  border-left: 3px solid var(--brand-500); border-radius: 10px;
}
.doc-note p { margin: 0 !important; font-size: .87rem !important; }
.doc-note.is-warn {
  background: var(--amber-100); border-color: color-mix(in srgb, var(--amber-text) 26%, transparent);
  border-left-color: var(--amber-text);
}
.doc-note.is-warn p { color: var(--amber-text) !important; }

.doc-shot { margin: 0 0 1.6rem; }
.doc-shot-open {
  display: block; position: relative; width: 100%; padding: 0;
  background: none; border: 0; cursor: zoom-in; border-radius: 12px;
}
.doc-shot img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--line); border-radius: 12px;
}
.doc-shot-open:hover img { border-color: var(--brand-500); }
/* The magnifier says the image opens, which a cursor alone only tells
   somebody once they are already over it. */
.doc-shot-zoom {
  position: absolute; right: .6rem; bottom: .6rem;
  display: grid; place-items: center; width: 28px; height: 28px;
  border-radius: 8px; color: var(--ink-2);
  background: var(--surface); border: 1px solid var(--line);
  opacity: 0; transition: opacity .12s;
}
.doc-shot-zoom svg { width: 15px; height: 15px; }
.doc-shot-open:hover .doc-shot-zoom,
.doc-shot-open:focus-visible .doc-shot-zoom { opacity: 1; }
.doc-shot figcaption {
  margin-top: .5rem; font-size: .78rem; color: var(--ink-3); text-align: center;
}

/* The lightbox. Sized to the viewport rather than the image, so a tall
   dashboard shot scrolls instead of overflowing off screen. */
/* A closed <dialog> is display:none by the browser's own stylesheet, and
   setting display on the element overrides that, so the dialog renders inline
   in the page. Both dialogs below lay themselves out with flex, so both need
   this: the rule is one class more specific and wins. */
.shot-box:not([open]),
.docs-dialog:not([open]) { display: none; }

/* A fixed header over a scrolling body, so the title and the way out stay
   put however tall the screenshot is. */
.shot-box {
  width: min(96vw, 78rem); max-height: 92vh;
  padding: 0; overflow: hidden;
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 14px;
}
.shot-box::backdrop { background: rgb(8 18 33 / .72); }

.shot-box-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .7rem .8rem .7rem 1rem;
  border-bottom: 1px solid var(--line); background: var(--surface-2);
}
.shot-box-title {
  margin: 0; font-size: .86rem; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shot-box-close {
  flex: none; width: 1.9rem; height: 1.9rem; border-radius: 8px; cursor: pointer;
  font-size: 1.15rem; line-height: 1; color: var(--ink-2);
  background: var(--surface); border: 1px solid var(--line);
}
.shot-box-close:hover { color: var(--ink); border-color: var(--brand-500); }

/* The padding is what stopped the screenshot meeting the dialog's edges,
   which read as an image that had escaped its frame. */
.shot-box-body { overflow: auto; padding: 1rem; }
.shot-box img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: 10px;
}

/* ---- copy a link to a heading ---- */

.docs-main h2, .docs-main h3 { position: relative; }
.doc-anchor {
  margin-left: .45rem; padding: .1rem; vertical-align: middle;
  border: 0; background: none; cursor: pointer; border-radius: 6px;
  color: var(--ink-3); opacity: 0; transition: opacity .12s;
}
.doc-anchor svg { width: 14px; height: 14px; display: block; }
h2:hover > .doc-anchor, h3:hover > .doc-anchor,
.doc-anchor:focus-visible { opacity: 1; }
.doc-anchor:hover { color: var(--brand-600); background: var(--surface-2); }
/* Held visible for a moment after copying, so the confirmation is seen even
   if the pointer has already moved on. */
.doc-anchor.is-copied { opacity: 1; color: var(--green-text, #15803d); }

.doc-check {
  margin: 0 0 1.2rem; padding: .9rem 1rem;
  border: 1px solid var(--line); border-radius: 12px; background: var(--surface);
  scroll-margin-top: var(--docs-top);
}
.doc-check h4 { margin: 0 0 .4rem; }
.doc-check-what { font-weight: 600; color: var(--ink) !important; }
.doc-check p:last-child { margin-bottom: 0 !important; }

/* ---- next and previous ---- */

.doc-steps { display: flex; gap: .8rem; margin-top: 3rem; }
.doc-step {
  flex: 1; padding: .8rem 1rem; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface);
  text-decoration: none; color: inherit;
}
.doc-step:hover { border-color: var(--brand-500); }
.doc-step span { display: block; font-size: .72rem; color: var(--ink-3); margin-bottom: .15rem; }
.doc-step b { font-size: .88rem; color: var(--ink); }
.doc-step.is-next { text-align: right; }

/* ---- search ---- */

/* The trigger, which is a link until script upgrades it. */
.docs-search-open {
  display: flex; align-items: center; gap: .5rem; width: 100%;
  margin-bottom: .7rem; padding: .5rem .55rem .5rem .7rem;
  font-size: .84rem; color: var(--ink-3); text-decoration: none;
  background: var(--surface); border: 1px solid var(--line-3); border-radius: 10px;
  cursor: pointer;
}
.docs-search-open:hover { border-color: var(--brand-500); color: var(--ink-2); }
.docs-search-open svg { width: 15px; height: 15px; flex: none; }
.docs-search-open span { flex: 1; text-align: left; }
/* Sized rather than padded. A monospace glyph sits high in its line box,
   so equal padding renders visibly bottom-heavy; centring in a fixed box
   puts the label where the eye expects it. */
.docs-search-open kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.55rem; height: 1.3rem; padding: 0 .3rem;
  font-family: var(--mono); font-size: .7rem; line-height: 1;
  border-radius: 5px; background: var(--surface-2);
  border: 1px solid var(--line); color: var(--ink-3);
}

/* The dialog.
   Wide and tall because the whole reason it exists is room: a result carries
   its section, its heading and the sentence that matched, and none of that
   fits in a sidebar-width dropdown. Pinned near the top rather than centred,
   so the list grows downwards instead of shifting under the cursor. */
.docs-dialog {
  width: min(94vw, 40rem);
  max-height: min(80vh, 40rem);
  margin: 6vh auto auto;
  padding: 0; overflow: hidden;
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: 16px;
  box-shadow: var(--elev-3, var(--shadow));
}
.docs-dialog::backdrop { background: rgb(8 18 33 / .45); }

.docs-dialog-head {
  display: flex; align-items: center; gap: .6rem;
  padding: .8rem 1rem; border-bottom: 1px solid var(--line);
}
.docs-dialog-head svg { width: 17px; height: 17px; color: var(--ink-3); flex: none; }
.docs-dialog-head input {
  flex: 1; min-width: 0; padding: .2rem 0;
  font: inherit; font-size: .98rem; color: var(--ink);
  background: none; border: 0; outline: none;
}
.docs-dialog-close {
  padding: .2rem .45rem; border-radius: 6px; cursor: pointer;
  font-family: var(--mono); font-size: .7rem; color: var(--ink-3);
  background: var(--surface-2); border: 1px solid var(--line);
}
.docs-dialog-close:hover { color: var(--ink); }

.docs-dialog-body { flex: 1; overflow-y: auto; padding: .4rem; }
.docs-dialog-hint,
.docs-dialog-none {
  margin: 0; padding: 1.6rem 1rem; text-align: center;
  font-size: .84rem; color: var(--ink-3); line-height: 1.6;
}

.docs-dialog-body a {
  display: block; padding: .6rem .7rem; border-radius: 9px;
  text-decoration: none; color: inherit;
}
.docs-dialog-body a:hover,
.docs-dialog-body a.is-on { background: var(--surface-2); }
.docs-dialog-body a.is-on { box-shadow: inset 2px 0 0 var(--brand-500); }
.docs-dialog-body .where {
  display: block; font-size: .7rem; color: var(--ink-3); margin-bottom: .1rem;
}
.docs-dialog-body b { display: block; font-size: .9rem; color: var(--ink); }
.docs-dialog-body .text {
  display: block; margin-top: .15rem;
  font-size: .78rem; color: var(--ink-2); line-height: 1.5;
}

.docs-dialog-foot {
  display: flex; flex-wrap: wrap; gap: 1rem;
  margin: 0; padding: .5rem 1rem;
  border-top: 1px solid var(--line); background: var(--surface-2);
  font-size: .72rem; color: var(--ink-3);
}
.docs-dialog-foot kbd {
  font-family: var(--mono); font-size: .68rem; padding: .05rem .3rem;
  margin-right: .15rem; border-radius: 4px;
  background: var(--surface); border: 1px solid var(--line);
}

/* The hero uses the same trigger, centred and narrower. */
.docs-hero .docs-search-open { max-width: 26rem; margin-inline: auto; margin-bottom: 1.2rem; }

/* ---- index and results ---- */

/* ---- the documentation home ---- */

.docs-home { padding-bottom: 5rem; }

/* A tinted band rather than white on white, so the hero is a place rather
   than just the top of a list.

   Two layers: a soft brand glow centred behind the pill and headline, over
   a vertical fade to the page's own ground. The glow puts the colour where
   the content is and lets it fall away at the edges, which a flat band
   cannot do. The previous surface-2 to ground fade was a four point shift
   and read as no colour at all.

   Mixed from brand-500 rather than brand-100: brand-100 is only three
   points off the page ground, so any amount of it renders as nothing. The
   strength lives in the percentage, which is where it can be tuned.

   The base layer holds a low tint all the way to the baseline rather than
   fading out, so the band still has a bottom edge where the radial above
   it has already fallen away.

   Kept faint deliberately. The home page hero dropped its wash in favour of
   a hairline and space, so a heavy tint here would fight the site. */
.docs-hero {
  position: relative;
  padding: 3.5rem 0 3rem; text-align: center;
  background:
    radial-gradient(62rem 24rem at 50% -2rem,
      color-mix(in srgb, var(--brand-500) 16%, transparent) 0%,
      color-mix(in srgb, var(--brand-500) 7%, transparent) 42%,
      transparent 72%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--brand-500) 5%, var(--surface-2)) 0%,
      color-mix(in srgb, var(--brand-500) 4%, var(--surface-2)) 100%);
  border-bottom: 1px solid var(--line);
}
.docs-hero.is-tight { padding: 2.5rem 0 1rem; background: none; border-bottom: 0; }
/* The hero uses the site's eyebrow-pill. Its own 1.5rem bottom margin is
   more than this hero wants above a large h1, so it is tightened here
   rather than changed at the component, which other pages rely on. */
.docs-hero .eyebrow-pill { margin-bottom: 1rem; }
.docs-hero h1 {
  margin: 0 0 .6rem; font-size: clamp(1.9rem, 4vw, 2.5rem);
  font-weight: 800; letter-spacing: -.03em;
}
.docs-hero-lede {
  margin: 0 auto 1.6rem; max-width: 36rem;
  font-size: 1rem; color: var(--ink-2); line-height: 1.65;
}
.docs-hero p { margin: 0 auto 1.4rem; max-width: 34rem; color: var(--ink-2); line-height: 1.6; }
.docs-count { font-size: .82rem; color: var(--ink-3); }

/* Held to a readable width rather than the page's own.
   These are lists of short titles and one-line summaries, and stretched to a
   wide viewport a row becomes a title on the left and its summary marooned in
   the middle of nowhere. */
.docs-home-body { padding-top: 2.5rem; max-width: 64rem; }
.docs-hero .wrap { max-width: 48rem; }
.docs-h {
  margin: 0 0 1rem; font-size: .76rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .08em; color: var(--ink-3);
}

/* Start here: three routes in, one per intention. */
.docs-starts { margin-bottom: 3rem; }
/* Two by two rather than auto-fill, which lays four cards out as three and
   a stray one on a wide screen. */
.docs-start-grid {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (max-width: 40rem) {
  .docs-start-grid { grid-template-columns: minmax(0, 1fr); }
}
/* Tinted rather than white.
   These sit on the page's own white ground, so a white card with a hairline
   border reads as a rectangle drawn on the page rather than an object on
   it. The wash is faint enough not to compete with the panels below. */
.docs-start {
  display: flex; flex-direction: column; padding: 1.2rem;
  border: 1px solid var(--line); border-radius: 14px;
  background: linear-gradient(160deg, var(--surface-2), var(--surface) 62%);
  text-decoration: none; color: inherit;
  transition: border-color .12s, transform .12s, box-shadow .12s;
}
.docs-start:hover {
  border-color: var(--brand-500); transform: translateY(-2px);
  box-shadow: 0 6px 16px rgb(0 0 0 / .06);
}
.docs-start-kicker {
  font-size: .68rem; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--brand-600); margin-bottom: .5rem;
}
.docs-start b { font-size: 1.05rem; font-weight: 700; color: var(--ink); margin-bottom: .35rem; }
.docs-start-text { font-size: .84rem; color: var(--ink-2); line-height: 1.6; flex: 1; }
.docs-start-go {
  display: inline-flex; align-items: center; gap: .35rem; margin-top: .9rem;
  font-size: .82rem; font-weight: 650; color: var(--brand-600);
}
.docs-start-go svg { width: 14px; height: 14px; transition: transform .12s; }
.docs-start:hover .docs-start-go svg { transform: translateX(3px); }

/* Browse everything: one panel per section. */
/* Columns rather than a grid.
   The sections are 3, 8, 9, 8 and 3 pages long, so a row-filled grid puts a
   three-item panel beside a nine-item one and leaves most of a column empty.
   Multi-column flows by height instead, so the columns end up close to level
   whatever the section lengths are. break-inside keeps a panel whole. */
.docs-index {
  column-count: 2; column-gap: 1.2rem;
}
@media (max-width: 52rem) {
  .docs-index { column-count: 1; }
}
/* Padding moves off the panel and onto its parts, so the header can carry
   its own ground to the panel's edges. Without that the header and the list
   are one field of white and the panel reads as an undifferentiated block. */
.docs-group {
  border: 1px solid var(--line); border-radius: 14px; background: var(--surface);
  overflow: hidden;
  scroll-margin-top: 5rem;
  break-inside: avoid; margin-bottom: 1.2rem;
}
.docs-group-head {
  display: flex; align-items: flex-start; gap: .7rem;
  padding: 1rem 1.2rem;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}
.docs-group-icon {
  display: grid; place-items: center; flex: none;
  width: 2rem; height: 2rem; border-radius: 9px;
  background: var(--brand-100); color: var(--brand-600);
}
.docs-group-icon svg { width: 16px; height: 16px; }
.docs-group-head > div { flex: 1; min-width: 0; }
.docs-group h2 {
  margin: 0; font-size: .98rem; font-weight: 700;
  text-transform: none; letter-spacing: -.01em; color: var(--ink);
}
.docs-group-head p {
  margin: .15rem 0 0; font-size: .78rem; color: var(--ink-3); line-height: 1.5;
}
/* On the header's own ground, so it takes the panel's white to stay
   visible rather than matching what is behind it. */
.docs-group-count {
  flex: none; padding: .1rem .45rem; border-radius: 999px;
  font-size: .7rem; font-weight: 700;
  background: var(--surface); border: 1px solid var(--line); color: var(--ink-3);
}
.docs-group ul { list-style: none; margin: 0; padding: .8rem 1.2rem 1rem; }
.docs-group li { margin: 0; }
/* Rows rather than cards: a card inside a card is a border inside a border,
   and the panel already says these belong together. */
.docs-group li a {
  display: block; padding: .55rem .6rem; margin: 0 -.6rem;
  border-radius: 9px; text-decoration: none; color: inherit;
  transition: background .12s ease;
}
.docs-group li a:hover { background: var(--surface-2); }
.docs-group b { display: block; font-size: .87rem; color: var(--ink); }
.docs-group li span {
  display: block; margin-top: .1rem;
  font-size: .77rem; color: var(--ink-3); line-height: 1.5;
}

.docs-results { padding-bottom: 4rem; }
.docs-hits { list-style: none; margin: 0 auto; padding: 0; max-width: 46rem; }
.docs-hits li { margin-bottom: .6rem; }
.docs-hits a {
  display: block; padding: .8rem 1rem; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface);
  text-decoration: none; color: inherit;
}
.docs-hits a:hover { border-color: var(--brand-500); }
.docs-hit-where { display: block; font-size: .72rem; color: var(--ink-3); margin-bottom: .15rem; }
.docs-hits b { display: block; font-size: .95rem; color: var(--ink); margin-bottom: .2rem; }
.docs-hit-text { display: block; font-size: .82rem; color: var(--ink-2); line-height: 1.55; }
.docs-none { max-width: 34rem; margin: 0 auto; text-align: center; color: var(--ink-2); }

@media (max-width: 68rem) {
  .docs { grid-template-columns: 14rem minmax(0, 1fr); }
  .docs-toc { display: none; }
}

@media (max-width: 50rem) {
  .docs { grid-template-columns: 1fr; gap: 1.5rem; }
  /* Stacked above the article rather than beside it, so the column runs to
     its natural height and the nav scrolls with the page. */
  .docs-side { position: static; max-height: none; display: block; }
  .docs-nav { flex: none; max-height: none; overflow-y: visible; }
  .doc-steps { flex-direction: column; }
}

/* ============================================================================
   Features

   What the product does, shown with real screenshots of it doing that. The
   page alternates text and picture down the page rather than stacking cards,
   because each capability needs a sentence of context and a photograph, and
   a grid of those is unreadable.

   Everything is visible at rest. The reveal below only moves things that are
   already there, so a reader who lands mid-page, prints, or has motion turned
   off sees a finished page rather than an empty one waiting for a scroll.
   ========================================================================== */

/* The tinted, centred page opener.
   
   Shared by /features and the two contact pages rather than copied, because
   three hand-written copies of one gradient is three chances for a page to
   end up slightly the wrong blue. .ft-hero keeps its name for the features
   page, which was here first and whose other rules hang off it. */
.ft-hero,
.page-hero {
  position: relative;
  padding: 4rem 0 3.5rem; text-align: center;
  background:
    radial-gradient(58rem 26rem at 50% -4rem,
      color-mix(in srgb, var(--brand-500) 18%, transparent) 0%,
      color-mix(in srgb, var(--brand-500) 8%, transparent) 45%,
      transparent 74%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--brand-500) 5%, var(--surface-2)) 0%,
      var(--surface-2) 100%);
  border-bottom: 1px solid var(--line);
}

/* A page whose opener is a statement rather than a pitch: smaller than the
   features headline, which shares its line with a product promise. */
.page-hero h1 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.9rem, 4.2vw, 2.7rem); font-weight: 800;
  letter-spacing: -.032em; line-height: 1.15;
  text-wrap: balance;
}
.page-hero .lede {
  margin: .9rem auto 0; max-width: 38rem;
  font-size: 1rem; line-height: 1.65; color: var(--ink-2);
}
.ft-hero h1 {
  margin: 1.1rem 0 0;
  font-family: var(--display);
  font-size: clamp(2rem, 5.2vw, 3.2rem); font-weight: 800;
  letter-spacing: -.035em; line-height: 1.1;
  text-wrap: balance;
}
.ft-hero h1 .accent { color: var(--brand-700); }
.ft-hero .lede {
  margin: 1.1rem auto 0; max-width: 42rem;
  font-size: 1.02rem; line-height: 1.65; color: var(--ink-2);
}
.ft-analyze { margin: 1.8rem auto 0; max-width: 34rem; }

/* Three promises under the fold-line.
   
   Sentences rather than figures. They were counters, and a number invites
   counting: "7 check groups" reads as a quantity somebody should compare,
   and "0 findings withheld" turns a promise into a statistic that scans as
   nothing at all. None of the three is really a measurement. */
.ft-facts {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .5rem 1.4rem; margin: 1.9rem 0 0; padding: 0; list-style: none;
  font-size: .84rem; color: var(--ink-3);
}
.ft-facts li { display: flex; align-items: center; gap: .45rem; }
/* A tick before each, drawn rather than typed so it carries no text to a
   screen reader: the sentence already says what it says. */
.ft-facts li::before {
  content: ""; flex: none; width: 13px; height: 13px;
  background: currentColor; color: var(--green-text);
  -webkit-mask: var(--tick-mask) center / contain no-repeat;
  mask: var(--tick-mask) center / contain no-repeat;
}
/* Separated on a wide screen, stacked on a narrow one where the dots would
   run together. */
@media (max-width: 640px) {
  .ft-facts { flex-direction: column; align-items: center; gap: .4rem; }
}

/* ---- a capability: words one side, the thing itself on the other ---- */

.ft-lead, .ft-feature {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  align-items: center; gap: 2.5rem;
}
.ft-lead-text h2, .ft-feature-text h2 {
  margin: .6rem 0 .8rem;
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.6vw, 1.85rem); font-weight: 800;
  letter-spacing: -.028em; line-height: 1.2;
}
.ft-lead-text p, .ft-feature-text p {
  margin: 0 0 .9rem; font-size: .95rem; line-height: 1.7; color: var(--ink-2);
  max-width: 34rem;
}
.ft-lead-text .btn, .ft-feature-text .btn { margin-top: .4rem; }
.ft-note { font-size: .84rem; color: var(--ink-3); }
.ft-note a { color: var(--brand-700); font-weight: 700; }

/* Alternating sides. order rather than direction, so the reading order in
   the markup stays text-then-picture for a screen reader. */
.ft-feature.is-flipped .ft-feature-text { order: 2; }

.ft-icon {
  display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 11px;
  color: var(--brand-700);
  background: color-mix(in srgb, var(--brand-500) 12%, transparent);
}
.ft-icon svg { width: 20px; height: 20px; }

/* ---- the screenshots ---- */

.ft-shot { margin: 0; }
.ft-shot img {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--line); border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 24px 50px -30px color-mix(in srgb, var(--ink) 45%, transparent);
}
.ft-shot figcaption {
  margin-top: .6rem; font-size: .78rem; color: var(--ink-3); text-align: center;
}

/* ---- what a finding looks like ----

   The one place on the page that shows the product's actual output rather
   than a photograph of it, because this is the thing being sold: a named
   problem, the evidence, and the fix. Uses the severity palette the report
   itself uses, so somebody who arrives here and then runs a report meets
   the same colours meaning the same things. */

.ft-anatomy { text-align: center; }
.ft-anatomy h2 {
  margin: 0 0 .6rem;
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.6vw, 1.85rem); font-weight: 800; letter-spacing: -.028em;
}
.ft-anatomy > .lede { margin: 0 auto 2rem; max-width: 40rem; color: var(--ink-2); }

.ft-finding {
  max-width: 54rem; margin: 0 auto; padding: 1.4rem;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 16px;
  box-shadow: 0 20px 44px -34px color-mix(in srgb, var(--ink) 50%, transparent);
}
.ft-finding-head {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .7rem;
  margin-bottom: .7rem;
}
.ft-finding-head h3 {
  margin: 0; font-size: 1.02rem; font-weight: 700; letter-spacing: -.015em;
}
.ft-group {
  margin-left: auto;
  font-size: .74rem; color: var(--ink-3);
}
.ft-sev {
  padding: .15rem .5rem; border-radius: 999px;
  font-size: .68rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.ft-sev-crit { color: var(--red-text); background: var(--red-100); }
.ft-finding-why {
  margin: 0 0 1.1rem; font-size: .9rem; line-height: 1.65; color: var(--ink-2);
}

.ft-finding-split {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: .9rem;
}
.ft-label {
  display: block; margin-bottom: .35rem;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--ink-3);
}
/* Scrolls rather than wrapping: markup that wraps mid-attribute is harder to
   read than markup you have to nudge sideways. */
.ft-code {
  margin: 0; padding: .75rem .85rem; overflow-x: auto;
  font-family: var(--mono); font-size: .76rem; line-height: 1.6;
  border-radius: 10px; border: 1px solid var(--line);
  background: var(--surface-2); color: var(--ink-2);
}
.ft-code b { font-weight: 700; }
/* The problem line and the fixed line, tinted by what they are. */
.ft-code-bad {
  border-color: color-mix(in srgb, var(--red-text) 25%, transparent);
  background: color-mix(in srgb, var(--red-100) 55%, var(--surface));
}
.ft-code-bad b { color: var(--red-text); }
.ft-code-good {
  border-color: color-mix(in srgb, var(--green-text) 28%, transparent);
  background: color-mix(in srgb, var(--green-100) 55%, var(--surface));
}
.ft-code-good b { color: var(--green-text); }

.ft-sev-key {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: .5rem 1.6rem; margin: 1.6rem 0 0; padding: 0; list-style: none;
  font-size: .8rem; color: var(--ink-3);
}
.ft-sev-key li { display: flex; align-items: center; gap: .4rem; }
.ft-sev-key b { color: var(--ink-2); font-weight: 700; }
.ft-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.ft-dot-crit { background: var(--red-fill); }
.ft-dot-warn { background: var(--amber-fill); }
.ft-dot-note { background: var(--brand-500); }
.ft-dot-pass { background: var(--green-fill); }

/* ---- the banded sections ---- */

.ft-band {
  margin-block: 5.5rem; padding: 3.5rem 0;
  background: var(--surface-2);
  border-block: 1px solid var(--line);
}
.ft-band h2 {
  margin: 0 0 1.8rem; text-align: center;
  font-family: var(--display);
  font-size: clamp(1.35rem, 2.6vw, 1.85rem); font-weight: 800;
  letter-spacing: -.028em;
}
.ft-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1rem;
}
.ft-card {
  padding: 1.35rem;
  background: var(--surface);
  border: 1px solid var(--line); border-radius: 16px;
}
.ft-card h3 {
  margin: .85rem 0 .45rem;
  font-size: 1rem; font-weight: 700; letter-spacing: -.015em;
}
.ft-card p { margin: 0; font-size: .875rem; line-height: 1.65; color: var(--ink-2); }

/* ---- reveal on scroll ----

   Shifted a few pixels and faded, never hidden: an element parked at
   opacity 0 waiting for an observer is invisible to anybody whose scroll
   never fires it, and invisible in print. is-seen is added by script; with
   no script the rule below never applies and everything is simply visible. */
@media (prefers-reduced-motion: no-preference) {
  .js .ft-reveal {
    opacity: 0; transform: translateY(14px);
    transition: opacity .5s ease, transform .5s cubic-bezier(.2, .7, .3, 1);
  }
  .js .ft-reveal.is-seen { opacity: 1; transform: none; }
}

@media (max-width: 860px) {
  .ft-lead, .ft-feature { grid-template-columns: 1fr; gap: 1.5rem; }
  /* Picture under its words on a narrow screen, whichever side it was on. */
  .ft-feature.is-flipped .ft-feature-text { order: 0; }
  .ft-band { margin-block: 3.5rem; padding: 2.5rem 0; }
}

/* ---- the contact and support forms ----

   Both pages offered a mailto: link and nothing else, which does nothing
   visible for anybody without a mail client configured. */

/* The form beside the cards rather than under them.
   
   Both pages were 704px of a 1280px page, which left the form in a narrow
   column with two thirds of the width empty beside it. The form leads
   because writing is what the page is for; the cards answer the questions
   somebody has while filling it in, which is where they are useful. */
.contact-layout {
  display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: 2.5rem; align-items: start;
}
.contact-aside { display: grid; gap: 1rem; }
.contact-aside .support-card { margin: 0; }
.support-list { margin: 0; padding-left: 1.1rem; }
.support-list li {
  font-size: .9rem; color: var(--ink-2); line-height: 1.6; margin-bottom: .5rem;
}
.support-list li:last-child { margin-bottom: 0; }

/* One column below the point where two would squeeze the form. The form
   stays first, because it is still the thing the page is for. */
@media (max-width: 900px) {
  .contact-layout { grid-template-columns: 1fr; gap: 1.75rem; }
}

.contact-form-wrap { margin-top: 0; max-width: none; }
.contact-form-wrap h2 {
  margin: 0 0 1rem;
  font-size: 1.1rem; font-weight: 700; letter-spacing: -.015em;
}
.contact-form .ws-form-actions {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .9rem;
}
.contact-form .ws-hint { margin: 0; }

/* The honeypot. Positioned off-screen rather than display:none, which some
   bots check for, and taken out of the accessibility tree so nobody using a
   screen reader is asked to leave a field empty. */
.contact-trap {
  position: absolute !important;
  width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap;
}

/* ---- the pages a mailed link leads to ----

   These reuse the sign-in shell (.auth.is-single > .auth-panel) rather than a
   card of their own: they sit one click from signing in, and a second style
   on the pages where we ask for a password invites people to wonder whether
   they are still on our site. Only the few bits the sign-in page has no
   equivalent for are defined here. */

.auth-panel .auth-hint { margin: .1rem 0 .2rem; font-size: .78rem; line-height: 1.55; color: var(--ink-3); }
.auth-panel .auth-form > .auth-hint:last-of-type { margin-bottom: .6rem; }
.auth-panel .form-errors, .auth-panel .form-saved { margin: 0 0 1rem; text-align: left; }


/* ---- two step sign-in ----

   The settings section, the enrolment dialog, and the code field at sign-in.
   Scoped under .tfa and .tfa-dialog throughout: .auth-form is shared with the
   sign-in page, and styling it loosely is what put a second border inside the
   bordered field wrapper last time. */

.tfa { margin-top: 1.5rem; }
.tfa-state {
  display: flex; gap: .6rem; align-items: flex-start;
  margin: 0 0 .6rem; font-size: .92rem; line-height: 1.55; color: var(--ink-2);
}
.tfa-state svg { width: 1.15rem; height: 1.15rem; flex: none; margin-top: .12rem; color: var(--ink-3); }
.tfa-state.is-on svg { color: var(--ok-600, #15803d); }
.tfa-warn {
  margin: .6rem 0 0; padding: .7rem .85rem; border-radius: 10px;
  font-size: .85rem; line-height: 1.55;
  background: color-mix(in srgb, var(--warn-500, #f59e0b) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn-500, #f59e0b) 35%, transparent);
  color: var(--ink);
}
.tfa-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1rem; }
.tfa-actions .is-danger { color: var(--bad-700, #b91c1c); }
.tfa-actions .is-danger:hover { border-color: var(--bad-500, #ef4444); }

.tfa-devices { margin-top: 1.1rem; }
.tfa-legend { margin: 0 0 .4rem; font-size: .78rem; font-weight: 700; color: var(--ink-2); }
.tfa-device-list { list-style: none; margin: 0 0 .6rem; padding: 0; display: grid; gap: .35rem; }
.tfa-device-list li {
  display: flex; justify-content: space-between; gap: 1rem;
  font-size: .82rem; color: var(--ink-2);
}
.tfa-device { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The dialog. Panes are swapped with [hidden], so nothing here may set a
   display on a pane: the UA rule is what hides them. */
.tfa-dialog { max-width: 27rem; }
.tfa-dialog label { display: block; margin: .9rem 0 .35rem; font-size: .8rem; font-weight: 700; color: var(--ink-2); }
.tfa-dialog input[type="password"], .tfa-dialog input[type="text"] {
  width: 100%; box-sizing: border-box; font: inherit; font-size: .92rem;
  padding: .55rem .75rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--ink);
}
.tfa-dialog input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
.tfa-error {
  margin: .6rem 0 0; padding: .55rem .7rem; border-radius: 9px;
  font-size: .84rem; line-height: 1.5;
  background: color-mix(in srgb, var(--bad-500, #ef4444) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--bad-500, #ef4444) 32%, transparent);
  color: var(--bad-700, #b91c1c);
}

.tfa-qr {
  display: flex; justify-content: center; margin: 1rem 0 .6rem;
  padding: .9rem; border-radius: 12px; background: #fff;
  border: 1px solid var(--line);
}
/* The QR is drawn black on white and must stay that way in dark mode: an
   inverted code still scans on some readers and not others. */
.tfa-qr svg { width: 11rem; height: 11rem; display: block; }

.tfa-manual { margin: .2rem 0 0; font-size: .84rem; color: var(--ink-2); }
.tfa-manual summary { cursor: pointer; font-weight: 600; }
.tfa-manual p { margin: .5rem 0 .35rem; }
.tfa-secret {
  display: block; padding: .5rem .65rem; border-radius: 8px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem; letter-spacing: .06em; word-break: break-all;
}

.tfa-codes {
  list-style: none; margin: 1rem 0 .75rem; padding: .9rem;
  display: grid; grid-template-columns: repeat(2, 1fr); gap: .45rem .9rem;
  border-radius: 12px; background: var(--surface-2); border: 1px solid var(--line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .88rem; letter-spacing: .04em;
}
/*
 * A busy state for the dialog's own buttons.
 *
 * Every one of these round-trips to the server, and two of them verify a
 * password, which is deliberately slow. Without this the button sat inert
 * long enough that people press it again or assume nothing happened.
 *
 * The spinner takes the label's place rather than sitting beside it, so the
 * button does not change width at the moment somebody is watching it most
 * closely.
 */
/* The colour is repeated for .is-danger because .btn.is-danger:not(.ghost)
   sets color:#fff at a higher specificity than a plain .is-busy, which left
   the label sitting under the spinner on the "Turn it off" button. */
.tfa-dialog button.is-busy,
.tfa-dialog button.btn.is-danger.is-busy,
.tfa-dialog button.btn.is-danger.is-busy:hover {
  cursor: progress; position: relative; color: transparent;
}
.tfa-dialog button.is-busy::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid rgb(255 255 255 / .35); border-top-color: #fff;
  animation: auth-spin .6s linear infinite;
}
/* The ghost buttons are dark on light, so the ring has to be too. */
.tfa-dialog button.ghost.is-busy::after {
  border-color: color-mix(in srgb, var(--ink) 25%, transparent);
  border-top-color: var(--ink-2);
}
.tfa-dialog button[disabled] { opacity: .85; }

.tfa-code-actions { display: flex; gap: .5rem; }
/* The copy button swaps its label to "Copied" and back. Reserving the width
   stops the row reflowing under the pointer at the moment somebody clicks,
   which reads as the button moving away from them. */
.tfa-code-actions [data-tfa-copy] { min-width: 7.5rem; justify-content: center; }
/* justify-content is reset because .auth-form label sets space-between, to
   put "Forgotten it?" opposite the field's name. Inherited here it threw the
   tick box to one edge and its wording to the other. font-weight and
   text-transform likewise: this is a sentence, not a field label. */
/*
 * The tick box rows, on the challenge page and in the dialog.
 *
 * Written as .auth-form label.tfa-trust rather than .tfa-trust alone: these
 * are labels inside .auth-form, and that rule sets display:flex with
 * baseline alignment and space-between so a field's name sits opposite
 * "Forgotten it?". Inherited here it threw the box to one edge and the
 * wording to the other, and every attempt to correct it from a single class
 * quietly lost the cascade. Matching its specificity is the fix; changing
 * values at lower specificity was not.
 */
.auth-form label.tfa-trust,
.auth-form label.tfa-kept,
.tfa-dialog label.tfa-kept,
.tfa-dialog label.tfa-trust,
.tfa-kept, .tfa-trust {
  display: flex; gap: .55rem;
  align-items: center; justify-content: flex-start;
  /* Smaller and lighter than the field labels around it. This is an
     optional convenience rather than something anybody has to decide on, so
     it sits back from the field name above it and the button below. */
  margin: 1rem 0 0; font-size: .8rem; font-weight: 400; line-height: 1.4;
  color: var(--ink-2); cursor: pointer;
}

.auth-form label.tfa-trust input,
.auth-form label.tfa-kept input,
.tfa-kept input, .tfa-trust input { flex: none; margin: 0; }

.auth-form label.tfa-trust span,
.auth-form label.tfa-kept span,
.tfa-kept span, .tfa-trust span { flex: 1; }

/* If the wording wraps, the box belongs beside the first line rather than
   floating in the middle of the block. */
@media (max-width: 26rem) {
  .auth-form label.tfa-trust,
  .auth-form label.tfa-kept,
  .tfa-kept, .tfa-trust { align-items: flex-start; }

  .auth-form label.tfa-trust input,
  .auth-form label.tfa-kept input,
  .tfa-kept input, .tfa-trust input { margin-top: .15rem; }
}
.auth-form label.tfa-trust, .tfa-trust { margin: .8rem 0 .2rem; }

@media (max-width: 30rem) {
  .tfa-codes { grid-template-columns: 1fr; }
}

/* The queue block inside the progress dialog. */
.pg-queue { margin: 0 0 1.1rem; }
.pg-queue-place b { font-weight: 800; }

/* ---- toasts ----

   For actions that reload the page, where an inline banner would be drawn
   and then thrown away by the navigation it is reporting on. Fixed to the
   bottom so it never displaces what the reader came back to look at. */

.toast {
  position: fixed; z-index: 90;
  left: 50%; transform: translateX(-50%);
  bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
  display: flex; align-items: center; gap: .6rem;
  max-width: min(30rem, calc(100vw - 2rem));
  padding: .7rem .8rem .7rem .85rem;
  border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  box-shadow: 0 18px 40px -22px color-mix(in srgb, var(--ink) 55%, transparent);
  font-size: .88rem; line-height: 1.45; color: var(--ink);
  animation: toast-in .22s ease-out;
}
.toast-icon { display: flex; flex: none; }
.toast-icon svg { width: 1.05rem; height: 1.05rem; }
.toast.is-good .toast-icon { color: var(--ok-600, #15803d); }
.toast.is-bad .toast-icon { color: var(--red-text, #b91c1c); }
.toast-text { flex: 1; }
.toast-close {
  flex: none; display: flex; padding: .2rem; margin: -.2rem -.15rem -.2rem 0;
  background: none; border: 0; border-radius: 6px;
  color: var(--ink-3); cursor: pointer;
}
.toast-close:hover { color: var(--ink); background: var(--surface-2); }
.toast-close svg { width: .95rem; height: .95rem; }

/* Faded out by the script before it is removed, so it does not simply
   vanish under the pointer. */
.toast.is-going { opacity: 0; transition: opacity .3s ease, transform .3s ease; transform: translateX(-50%) translateY(6px); }

@keyframes toast-in {
  from { opacity: 0; transform: translateX(-50%) translateY(10px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.is-going { transition: none; }
}

/* ---- the runbook, and rendered markdown generally ---- */

.rb-tabs { display: flex; flex-wrap: wrap; gap: .6rem; margin: 0 0 1.5rem; }
.rb-tab {
  flex: 1 1 13rem; display: grid; gap: .15rem;
  padding: .7rem .85rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink); text-decoration: none;
}
.rb-tab b { font-size: .92rem; font-weight: 700; }
.rb-tab span { font-size: .8rem; color: var(--ink-3); }
.rb-tab:hover { border-color: var(--ink-3); }
.rb-tab.is-on { border-color: var(--brand-600); background: var(--brand-50, var(--surface-2)); }

/* The contents beside the document, and above it on a narrow screen: a
   sidebar that becomes a column of links pushing the text off the first
   screen is worse than no sidebar. */
.rb-layout { display: grid; grid-template-columns: minmax(0, 1fr) 15rem; gap: 2rem; align-items: start; }

.rb-toc {
  position: sticky; top: 1.5rem; order: 2;
  display: grid; gap: .1rem;
  padding: .85rem; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--line);
  max-height: calc(100vh - 3rem); overflow-y: auto;
}
.rb-toc-item {
  font-size: .8rem; line-height: 1.4; color: var(--ink-2);
  padding: .2rem 0; text-decoration: none;
}
.rb-toc-item:hover { color: var(--brand-700); }
.rb-toc-item.is-l3 { padding-left: .7rem; font-size: .76rem; color: var(--ink-3); }

.rb-doc {
  order: 1; min-width: 0;
  padding: 1.6rem 1.8rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}

/* Rendered markdown. Named for what produced it rather than where it sits,
   so a second document elsewhere gets the same treatment. */
.rb-doc h1 { margin: 0 0 1rem; font-size: 1.5rem; font-weight: 800; letter-spacing: -.02em; }
.rb-doc h2 {
  margin: 2.2rem 0 .8rem; padding-top: 1.2rem;
  font-size: 1.15rem; font-weight: 750;
  border-top: 1px solid var(--line);
}
.rb-doc h1 + h2, .rb-doc hr + h2 { border-top: 0; padding-top: 0; }
.rb-doc h3 { margin: 1.6rem 0 .6rem; font-size: 1rem; font-weight: 700; }
.rb-doc h4 { margin: 1.2rem 0 .5rem; font-size: .92rem; font-weight: 700; color: var(--ink-2); }
.rb-doc p { margin: 0 0 .9rem; font-size: .92rem; line-height: 1.65; color: var(--ink-2); }
.rb-doc b { color: var(--ink); font-weight: 700; }
.rb-doc a { color: var(--brand-700); font-weight: 600; }
.rb-doc hr { margin: 2rem 0; border: 0; border-top: 1px solid var(--line); }

.rb-doc .md-list { margin: 0 0 1rem; padding-left: 1.2rem; }
.rb-doc .md-list li { margin: 0 0 .4rem; font-size: .92rem; line-height: 1.6; color: var(--ink-2); }

.rb-doc .md-tasks { list-style: none; margin: 0 0 1rem; padding: 0; display: grid; gap: .45rem; }
.rb-doc .md-task {
  position: relative; padding-left: 1.65rem;
  font-size: .92rem; line-height: 1.55; color: var(--ink-2);
}
/* Drawn rather than an input: this is a printed checklist to work down, not
   a form, and a tick box that does not remember being ticked is a lie. */
.rb-doc .md-task::before {
  content: ''; position: absolute; left: 0; top: .18rem;
  width: 1rem; height: 1rem; border-radius: 4px;
  border: 1.5px solid var(--ink-3);
}

.rb-doc .md-code {
  margin: 0 0 1.1rem; padding: .85rem 1rem; border-radius: 10px;
  background: var(--surface-2); border: 1px solid var(--line);
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .82rem; line-height: 1.55; color: var(--ink);
}
.rb-doc .md-code code { background: none; border: 0; padding: 0; font-size: inherit; }
.rb-doc code {
  padding: .1rem .3rem; border-radius: 5px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .84em;
}

.rb-doc .md-table-wrap { overflow-x: auto; margin: 0 0 1.1rem; }
.rb-doc .md-table { width: 100%; border-collapse: collapse; font-size: .86rem; }
.rb-doc .md-table th, .rb-doc .md-table td {
  padding: .5rem .7rem; text-align: left; vertical-align: top;
  border-bottom: 1px solid var(--line);
}
.rb-doc .md-table th { font-weight: 700; color: var(--ink); white-space: nowrap; }
.rb-doc .md-table td { color: var(--ink-2); }

@media (max-width: 62rem) {
  .rb-layout { grid-template-columns: minmax(0, 1fr); }
  .rb-toc { position: static; order: 0; max-height: none; }
  .rb-doc { padding: 1.2rem; }
}

/* ---- clearing test data ---- */

.td-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); gap: 1rem; }
.td-card {
  display: grid; gap: .5rem; align-content: start;
  padding: 1.1rem 1.2rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.td-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
.td-card-head h3 { margin: 0; font-size: .98rem; font-weight: 700; }
.td-count { font-size: 1.05rem; font-weight: 800; color: var(--ink); white-space: nowrap; }
.td-count span { font-size: .78rem; font-weight: 600; color: var(--ink-3); }
.td-detail { margin: 0; font-size: .85rem; line-height: 1.55; color: var(--ink-2); }
.td-word { margin: .3rem 0 0; font-size: .78rem; color: var(--ink-3); }
.td-word b { color: var(--ink-2); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.td-row { display: flex; gap: .5rem; }
.td-row input {
  flex: 1; min-width: 0; box-sizing: border-box;
  font: inherit; font-size: .88rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  padding: .45rem .6rem; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--line); color: var(--ink);
}
.td-row input:focus {
  outline: none; border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 18%, transparent);
}
/* Plainly unavailable until the word is typed, rather than merely inert. */
.td-row button[disabled] { opacity: .45; cursor: not-allowed; }

.td-refused {
  max-width: 34rem; padding: 1.3rem 1.5rem; border-radius: 14px;
  background: var(--surface); border: 1px solid var(--line);
}
.td-refused h2 { margin: 0 0 .5rem; font-size: 1.05rem; font-weight: 700; }
.td-refused p { margin: 0 0 .7rem; font-size: .9rem; line-height: 1.6; color: var(--ink-2); }
.td-refused p:last-child { margin: 0; font-size: .82rem; }

.rb-aside { margin: -.6rem 0 1.2rem; font-size: .85rem; }
.rb-aside a { color: var(--brand-700); font-weight: 600; }

/* ---- the server check ---- */

.rd-summary {
  display: grid; gap: .2rem; margin: 0 0 1.4rem;
  padding: .9rem 1.1rem; border-radius: 12px;
  border: 1px solid var(--line); background: var(--surface);
}
.rd-summary b { font-size: 1rem; font-weight: 700; }
.rd-summary span { font-size: .84rem; color: var(--ink-3); }
.rd-summary.is-bad { border-color: color-mix(in srgb, var(--red-text) 35%, transparent); }
.rd-summary.is-bad b { color: var(--red-text); }
.rd-summary.is-warn { border-color: color-mix(in srgb, var(--warn-500, #f59e0b) 40%, transparent); }

.rd-list {
  border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line); background: var(--surface);
}
.rd-group {
  margin: 0; padding: .7rem 1.1rem .5rem;
  font-size: .76rem; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-3);
  background: var(--surface-2); border-bottom: 1px solid var(--line);
}
.rd-group:not(:first-child) { border-top: 1px solid var(--line); }

/* Label and detail on one line, the fix underneath spanning both: the
   advice is a sentence and a sentence in a narrow column is unreadable. */
.rd-row {
  display: grid; grid-template-columns: 1.4rem 1fr auto; gap: .2rem .7rem;
  align-items: baseline;
  padding: .6rem 1.1rem; border-bottom: 1px solid var(--line);
}
.rd-row:last-child { border-bottom: 0; }
.rd-icon { display: flex; align-self: center; }
.rd-icon svg { width: 1rem; height: 1rem; }
.rd-row.is-ok .rd-icon { color: var(--ok-600, #15803d); }
.rd-row.is-warn .rd-icon { color: var(--warn-600, #b45309); }
.rd-row.is-bad .rd-icon { color: var(--red-text, #b91c1c); }
.rd-label { font-size: .9rem; color: var(--ink); }
.rd-detail { font-size: .84rem; color: var(--ink-3); text-align: right; }
.rd-fix {
  grid-column: 2 / -1; margin: .3rem 0 0;
  font-size: .84rem; line-height: 1.55; color: var(--ink-2);
}
.rd-row.is-bad { background: color-mix(in srgb, var(--red-text, #ef4444) 4%, transparent); }

@media (max-width: 40rem) {
  .rd-row { grid-template-columns: 1.4rem 1fr; }
  .rd-detail { grid-column: 2; text-align: left; }
}

/* ---- the daily reading limit ---- */

.vlimit { text-align: center; padding-block: 3.5rem 4rem; }
.vlimit-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3.2rem; height: 3.2rem; margin: 0 0 1.2rem;
  border-radius: 50%; background: var(--surface-2); border: 1px solid var(--line);
  color: var(--ink-3);
}
.vlimit-mark svg { width: 1.5rem; height: 1.5rem; }
.vlimit h1 { margin: 0 0 .7rem; }
.vlimit .lede { margin: 0 auto 1.4rem; max-width: 34rem; }
.vlimit-wanted {
  margin: 0 auto 1.8rem; max-width: 34rem;
  padding: .8rem 1rem; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: .92rem; line-height: 1.6; color: var(--ink-2);
}
.vlimit-actions { display: flex; flex-wrap: wrap; gap: .6rem; justify-content: center; }
.vlimit-note {
  margin: 1.6rem auto 0; max-width: 32rem;
  font-size: .84rem; line-height: 1.6; color: var(--ink-3);
}

/* The queue bar, and its shimmer.

   Motion because a number that changes every few seconds looks stalled in
   between: the shimmer says the wait is being worked on, and the fill says
   how far through it is. */
.pg-queue-bar {
  height: 6px; margin: 0 0 .9rem; border-radius: 99px;
  background: var(--surface-2); border: 1px solid var(--line);
  overflow: hidden;
}
.pg-queue-fill {
  display: block; height: 100%; width: 4%;
  border-radius: 99px;
  background: linear-gradient(
    90deg,
    var(--brand-600) 0%,
    color-mix(in srgb, var(--brand-500) 55%, #fff) 50%,
    var(--brand-600) 100%
  );
  background-size: 220% 100%;
  animation: pg-queue-shimmer 1.6s linear infinite;
  transition: width .6s ease;
}
@keyframes pg-queue-shimmer {
  from { background-position: 100% 0; }
  to   { background-position: -120% 0; }
}
.pg-queue-place { margin: 0 0 .2rem; font-size: 1.02rem; color: var(--ink); }
.pg-queue-wait { margin: 0 0 .2rem; font-size: .88rem; color: var(--ink-3); }

@media (prefers-reduced-motion: reduce) {
  .pg-queue-fill { animation: none; transition: none; }
}
.pg-queue-choice {
  margin: .6rem 0 0; padding: .55rem .7rem; border-radius: 9px;
  font-size: .84rem; line-height: 1.5; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--line);
}

/* The allowance, spent, inside the progress dialog. */
.pg-limit[hidden], .pg-keep[hidden] { display: none !important; }
.pg-limit { margin: 0 0 1.1rem; }
.pg-limit-text { margin: 0 0 .9rem; font-size: .95rem; line-height: 1.6; color: var(--ink-2); }
.pg-limit-actions { margin: 0 0 .9rem; }
.pg-limit-note {
  margin: 0; padding: .55rem .7rem; border-radius: 9px;
  font-size: .82rem; line-height: 1.5; color: var(--ink-3);
  background: var(--surface-2); border: 1px solid var(--line);
}
.pg-limit-plans[hidden] { display: none !important; }
.pg-limit-plans { margin: 0 0 .9rem; font-size: .85rem; line-height: 1.55; color: var(--ink-3); }
.pg-limit-plans a { color: var(--brand-700); font-weight: 600; }
.vlimit-plans { margin: 1.2rem auto 0; max-width: 32rem; font-size: .86rem; line-height: 1.6; color: var(--ink-3); }
.vlimit-plans a { color: var(--brand-700); font-weight: 600; }

/* The plan somebody chose before the account existed. */
.offer.is-chosen {
  border-color: var(--brand-600);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand-500) 16%, transparent);
}
.offer-chosen {
  margin: 0 0 .5rem; font-size: .72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--brand-700);
}
/* Both terms offered on the pricing page, so the choice is made beside the
   prices rather than three screens later beside one of them. */
.ptier-ctas { display: flex; gap: .5rem; }
.ptier-ctas .ptier-cta { flex: 1; justify-content: center; }

/* ---- the social accounts, in the footer's bottom bar ---- */

/* The description column is wider than the link columns, because it holds a
   paragraph rather than a list. */
.footer-brand { max-width: 30rem; }

.footer-social { display: flex; gap: .5rem; margin-top: 1.1rem; }

/* Boxed, so the marks read as a set of controls rather than three glyphs
   floating under a paragraph. */
.footer-social-link {
  display: flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; border-radius: 10px;
  background: var(--surface); border: 1px solid var(--line);
  color: var(--ink-2);
}
.footer-social-link svg { width: 1rem; height: 1rem; }
/* The brand blue on a light tint of itself, so a hovered icon reads as ours
   rather than as a grey box getting slightly darker. */
.footer-social-link:hover {
  color: var(--brand-600);
  border-color: color-mix(in srgb, var(--brand-500) 35%, transparent);
  background: color-mix(in srgb, var(--brand-500) 10%, transparent);
}
/* Keyboard focus has to be visible: these sit at the end of the tab order
   and are easy to lose track of. */
.footer-social-link:focus-visible {
  outline: 2px solid var(--brand-600); outline-offset: 2px;
}

/* ---- the phone menu ----

   A burger on the left of the marketing header, shown from the width where
   the nav stops fitting. It opens a dialog that slides in from the same
   side, holding the nav, the way in or out of the account, and the theme,
   which the header also sheds at phone width. */
.nav-burger {
  display: none; flex: none; place-items: center;
  width: 38px; height: 38px; margin-left: -.35rem; padding: 0;
  border: 0; border-radius: 9px; background: none; color: var(--ink);
  cursor: pointer;
}
.nav-burger:hover { background: var(--surface-2); }
.nav-burger:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }
.nav-burger svg { width: 22px; height: 22px; }

@media (max-width: 56rem) {
  .nav-burger { display: grid; }
}

.nav-drawer {
  position: fixed; inset: 0 auto 0 0;
  width: min(20rem, 86vw); height: 100%; max-height: 100%; max-width: none;
  margin: 0; padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
  display: none; flex-direction: column; gap: 1rem;
  background: var(--surface); color: var(--ink);
  border: 0; border-right: 1px solid var(--line);
  box-shadow: var(--elev-3);
}
.nav-drawer[open] { display: flex; animation: drawer-in .2s ease-out; }
.nav-drawer::backdrop { background: rgb(8 18 33 / .45); }
@keyframes drawer-in { from { transform: translateX(-100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .nav-drawer[open] { animation: none; } }

.drawer-head { display: flex; align-items: center; justify-content: space-between; }
.drawer-title {
  margin: 0; font-size: .72rem; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-3); outline: none;
}
.drawer-close {
  display: grid; place-items: center; width: 36px; height: 36px; padding: 0;
  border: 0; border-radius: 9px; background: none; color: var(--ink-2); cursor: pointer;
}
.drawer-close:hover { background: var(--surface-2); color: var(--ink); }
.drawer-close svg { width: 20px; height: 20px; }

.drawer-nav, .drawer-account { display: flex; flex-direction: column; gap: .15rem; }
.drawer-account { padding-top: 1rem; border-top: 1px solid var(--line); }
.drawer-link {
  display: block; padding: .7rem .75rem; border-radius: 10px;
  font-size: 1rem; font-weight: 600; color: var(--ink); text-decoration: none;
}
.drawer-link:hover { background: var(--surface-2); }
.drawer-link.is-quiet { color: var(--ink-3); }
.drawer-cta { margin-top: .5rem; width: 100%; }

.drawer-foot {
  margin-top: auto; padding-top: 1rem; border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between;
  font-size: .85rem; font-weight: 600; color: var(--ink-2);
}
/* The header hides its toggle at phone width; this copy is the one to use. */
.nav-drawer .theme-cycle { margin: 0; border-color: var(--line); }

/* ---- the dashboard's phone bar ---- */
.app-mbar, .app-scrim { display: none; }
.app-burger {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; padding: 0;
  border: 0; border-radius: 9px; background: none; color: var(--ink); cursor: pointer;
}
.app-burger:hover { background: var(--surface-2); }
.app-burger:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }
.app-burger svg { width: 22px; height: 22px; }
.app-scrim {
  position: fixed; inset: 0; z-index: 45; padding: 0; border: 0;
  background: rgb(8 18 33 / .45); cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
  .app-side { transition: none !important; }
}

/* In the bar the account menu is an avatar alone, and its panel opens
   downward from the top right: the name and plan move inside it. */
.account-pop-id { display: none; }
.app-mbar .account-menu summary {
  /* The marketing header's account chip: a ringed avatar, the same size. */
  padding: .2rem; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface-2);
}
.app-mbar .account-menu summary:hover,
.app-mbar .account-menu[open] summary { border-color: var(--brand-600); background: var(--surface-2); }
.app-mbar .account-id, .app-mbar .account-caret { display: none; }
.app-mbar .account-avatar { width: 24px; height: 24px; font-size: .68rem; }
.app-mbar .account-pop {
  top: calc(100% + .45rem); bottom: auto; left: auto; right: 0;
  width: min(16rem, calc(100vw - 2rem));
}
.app-mbar .account-pop-id {
  display: block; padding: .45rem .55rem 0;
}
.app-mbar .account-pop-id b { display: block; font-size: .88rem; color: var(--ink); }
.app-mbar .account-pop-id span { display: block; font-size: .74rem; color: var(--ink-3); }
.app-mbar .account-email { padding-top: .15rem; }

/* All workspaces, beside the avatar in the phone bar. */
.mbar-icon {
  display: grid; place-items: center; flex: none;
  width: 38px; height: 38px; border-radius: 9px; color: var(--ink-2);
}
.mbar-icon:hover { background: var(--surface-2); color: var(--ink); }
.mbar-icon:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }
.mbar-icon svg { width: 20px; height: 20px; }

/* The plan badge goes on the narrowest phones, where the bar holds the same
   row as the marketing header plus one more icon. The drawer still shows it. */
@media (max-width: 400px) {
  .app-mbar .logo-tag { display: none; }
}

/* Sign out is in the burger menu, and at this width the icon was pushed
   past the edge. Without it the avatar lands where the dashboard puts it. */
@media (max-width: 400px) {
  body:not(.page-report) .site-header .account-out { display: none; }
}

/* The folded Analyze button and the field's close button exist only for the
   phone topbar; on a wide screen the field is always open. */
.topbar-analyze-open, .topbar-analyze-close { display: none; }
.topbar-actions .topbar-analyze-open {
  height: var(--topbar-control); padding: 0 .85rem; gap: .35rem;
  border-radius: 9px; font-size: .82rem; white-space: nowrap;
}
.topbar-analyze-open svg { width: 15px; height: 15px; }
.topbar-analyze-close {
  place-items: center; flex: none;
  width: var(--topbar-control); height: var(--topbar-control); padding: 0;
  border: 1px solid var(--line); border-radius: 9px;
  background: var(--surface); color: var(--ink-2); cursor: pointer;
}
.topbar-analyze-close:hover { background: var(--surface-2); color: var(--ink); }
.topbar-analyze-close svg { width: 16px; height: 16px; }

/* A heading with a sentence beside it: on a phone the two squeezed each
   other into narrow columns, so the sentence goes under the heading. */
@media (max-width: 560px) {
  .dash-section-head:has(> p.dash-more) { flex-direction: column; align-items: stretch; gap: .15rem; }
}

/* Monitor a page, in its dialog: the field and the schedule each take the
   full width, stacked, the way the crawl dialog lays out its fields. */
.sched-dialog .sched-pick { flex: none; }
.sched-dialog .sched-url { width: 100%; }
.sched-dialog .dd { display: block; width: 100%; }
.sched-dialog .crawl-legend { margin-top: 1rem; }
.sched-dialog .ws-hint { margin: .7rem 0 0; }

/* The heading a dialog focuses on opening, so a screen reader announces it.
   It is not a control, so it gets no ring. */
.ws-dialog h2[tabindex="-1"]:focus { outline: none; }

/* With the form moved into a dialog, the list heading opens the page. */
[data-sched-notice]:empty + .sched-list-head { margin-top: 0; }

/* On a phone the button stays beside the heading and the search takes the
   next line, rather than the button ending up under the search. */
@media (max-width: 62rem) {
  .sched-list-head.has-tools > .btn { margin-left: auto; }
  .sched-list-head.has-tools > .table-tools { order: 2; }
}

/* ---- PDF exports ---- */

/* The sites, one tap each. A row that wraps rather than scrolls, so every
   client is visible without hunting sideways. */
.site-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; }
.site-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .35rem .75rem; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  font-size: .8rem; font-weight: 600; color: var(--ink-2); text-decoration: none;
}
.site-chip span {
  font-size: .7rem; font-weight: 700; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.site-chip:hover { border-color: var(--ink-3); color: var(--ink); }
.site-chip.is-on { background: var(--brand-600); border-color: var(--brand-600); color: #fff; }
.site-chip.is-on span { color: rgb(255 255 255 / .75); }
.site-chip:focus-visible { outline: 2px solid var(--brand-600); outline-offset: 2px; }

/* The site a group of reports belongs to, when the list is sorted by site. */
.export-site {
  margin: 1.25rem 0 .5rem; font-size: .78rem; font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-3);
}
[data-table] > .export-site:first-child { margin-top: 0; }
.dash-seats.export-intro { margin: 0 0 1rem; }

/* On a phone a dozen clients wrapped into a screenful of chips before the
   first report. One row that scrolls sideways instead, inside its own box. */
@media (max-width: 560px) {
  .site-chips {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    margin-inline: -1rem; padding-inline: 1rem;
  }
  .site-chips::-webkit-scrollbar { display: none; }
  .site-chip { flex: none; }
}

/* Past ten sites: one picker with search, the width of a site name. */
.site-picker { display: flex; align-items: center; gap: .6rem; margin: 0 0 1rem; }
.site-picker-label { font-size: .8rem; font-weight: 700; color: var(--ink-2); }
.site-picker .dd { width: min(20rem, 100%); }

/* The runbook's copy button, shown on hover like the docs' one, and while
   focused so a keyboard can reach it. A touch screen has no hover, so there
   it stays visible. */
.rb-doc .md-code-wrap { position: relative; }
.rb-doc .md-code-wrap .code-copy { top: .45rem; right: .45rem; border-color: var(--line); }
.rb-doc .md-code-wrap:hover .code-copy,
.rb-doc .md-code-wrap .code-copy:focus-visible { opacity: 1; }
@media (hover: none) {
  .rb-doc .md-code-wrap .code-copy { opacity: 1; }
  .rb-doc .md-code-wrap .md-code { padding-right: 4.5rem; }
}

/* ---- checking before analysing ----

   The button that was pressed shows a spinner while the server answers, in
   place of its label so it keeps its width. A report that already exists is
   then simply opened; the progress panel appears only for a real analysis.
   Doubled class for weight: .btn.ghost sets its own colour at two classes. */
button.is-checking.is-checking {
  color: transparent; cursor: progress; pointer-events: none;
}
button.is-checking.is-checking:not(.rebar-go) { position: relative; }
button.is-checking.is-checking > * { visibility: hidden; }
button.is-checking.is-checking::after {
  content: ''; position: absolute; inset: 0; margin: auto;
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid rgb(255 255 255 / .35); border-top-color: #fff;
  animation: auth-spin .6s linear infinite;
}
/* On a light button the ring has to be dark to be seen. */
button.ghost.is-checking.is-checking::after,
button.rebar-go.is-checking.is-checking::after {
  border-color: color-mix(in srgb, var(--ink) 25%, transparent);
  border-top-color: var(--ink-2);
}
@media (prefers-reduced-motion: reduce) {
  button.is-checking.is-checking::after { animation-duration: 1.6s; }
}
