/* SecureWeb dashboard — brand: #1770FF primary, #9A00FF accent, #000033 navy. */

:root {
  --brand:        #1770ff;
  --brand-dark:   #0f52c4;
  --brand-soft:   #e8f0ff;
  --accent:       #9a00ff;
  --navy:         #000033;

  --ok:           #0f9d58;
  --ok-soft:      #e6f6ed;
  --warn:         #c77700;
  --warn-soft:    #fdf3e2;
  --danger:       #d92d20;
  --danger-soft:  #fdecea;
  --idle:         #667085;
  --idle-soft:    #f1f3f7;

  /* Chart series. These are not free choices: amber against red is the pair
     colour-blind readers lose first, so --chart-suspicious was measured for
     OKLab separation from --chart-blocked under simulated protanopia and
     deuteranopia rather than picked by eye. Allowed is green, but not the
     --ok green: that one collapses into amber under protanopia (dE 5.5, below
     the floor). This step is the closest green to it that keeps every pair
     apart under both simulations, on both surfaces. */
  --chart-blocked:    #d92d20;
  --chart-suspicious: #c68206;
  --chart-allowed:    #34a863;
  --chart-traffic:    #1770ff;

  --bg:           #f5f7fb;
  --surface:      #ffffff;
  --surface-2:    #fafbfd;
  --border:       #e4e8f0;
  --border-strong:#d0d7e4;
  --text:         #111a2e;
  --text-muted:   #5b6780;
  --text-faint:   #8a94a8;

  --topbar-h:     56px;
  --radius:       14px;
  --radius-sm:    9px;
  --shadow:       0 1px 2px rgba(16,24,40,.04), 0 4px 16px rgba(16,24,40,.06);
  --shadow-lg:    0 8px 32px rgba(16,24,40,.12);

  --font-head: 'Montserrat', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-body: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, 'SFMono-Regular', 'JetBrains Mono', Menlo, monospace;

  color-scheme: light;
}

/* Dark is opt-in, not automatic. The dashboard is a light interface that a
   customer is often looking at beside their own site's admin, and following
   the machine's setting meant half of them met a dark one they never asked
   for. Set data-theme="dark" on <html> to get this palette back. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --brand-soft:   #10203f;
  --bg:           #080b16;
  --surface:      #0f1424;
  --surface-2:    #131930;
  --border:       #222a44;
  --border-strong:#303a5c;
  --text:         #eef1f8;
  --text-muted:   #9aa5be;
  --text-faint:   #6b7794;
  --ok-soft:      #0d2a1d;
  --warn-soft:    #2c2008;
  --danger-soft:  #2e1210;
  --idle-soft:    #171d31;
  --shadow:       0 1px 2px rgba(0,0,0,.3), 0 4px 16px rgba(0,0,0,.35);
  --shadow-lg:    0 8px 32px rgba(0,0,0,.5);
}

* { box-sizing: border-box; }

/* Any class here that sets `display` — .btn is inline-flex — outranks the
   browser's own [hidden] rule, so an element hidden from JavaScript would stay
   on screen. Settle it once, for everything. */
[hidden] { display: none !important; }

/* The page is at least the window's height and grows with its content. A
   body held to exactly the window's height ended there, and the sticky
   header, which can only stick inside its parent, left with it after one
   screen of scrolling. */
html { height: 100%; }
body { min-height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { font-family: var(--font-head); font-weight: 700; letter-spacing: -.015em; margin: 0; }
h1 { font-size: 1.65rem; }
h2 { font-size: 1.15rem; }
h3 { font-size: 1rem; }
p  { margin: 0 0 .75rem; }
a  { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

.mono { font-family: var(--font-mono); font-size: .875em; }
.muted { color: var(--text-muted); }
.faint { color: var(--text-faint); font-size: .85rem; }
.nowrap { white-space: nowrap; }
.hidden { display: none !important; }

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

.topbar {
  position: sticky; top: 0; z-index: 80;
  display: flex; align-items: center; gap: 1rem;
  min-height: var(--topbar-h);
  padding: .7rem 1.5rem;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
}

.brand { display: flex; align-items: center; gap: .6rem; font-family: var(--font-head);
  font-weight: 800; font-size: 1.05rem; color: var(--text); }
.brand:hover { text-decoration: none; }
.brand img { width: 30px; height: 30px; border-radius: 8px; }
.brand span b { color: var(--brand); }

.topbar-spacer { flex: 1; }

.account { display: flex; align-items: center; gap: .55rem; }
.account img { width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--border); }

.topbar-nav { display: flex; align-items: center; gap: 1rem; }
.topbar-nav > a { font-size: .9rem; font-weight: 600; color: var(--text-muted); }

/* Hamburger. Only ever visible on narrow screens, where the sidebar slides in. */
.nav-toggle {
  display: none; flex-direction: column; justify-content: center; gap: 4px;
  width: 34px; height: 34px; flex: none; padding: 0 7px; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
.nav-toggle span { display: block; height: 2px; border-radius: 2px; background: var(--text); }

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

.shell { display: flex; align-items: flex-start; }
.shell > .page { flex: 1; min-width: 0; }

.sidebar {
  position: sticky; top: var(--topbar-h); z-index: 70;
  flex: none; width: 236px;
  height: calc(100vh - var(--topbar-h));
  display: flex; flex-direction: column;
  padding: 1.1rem .7rem;
  background: var(--surface);
  border-right: 1px solid var(--border);
  overflow-y: auto;
}

.side-nav { display: flex; flex-direction: column; gap: .1rem; }
.side-link {
  display: flex; align-items: center; gap: .6rem;
  padding: .5rem .65rem; border-radius: var(--radius-sm);
  font-size: .9rem; font-weight: 600; color: var(--text-muted);
}
.side-link:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.side-link.is-active { background: var(--brand-soft); color: var(--brand); }
.side-link svg {
  width: 17px; height: 17px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

.side-section { margin-top: 1.2rem; }
/* The admin sidebar opens on a titled section rather than a bare list of
   links, so the first one must not push itself away from the top. */
.side-section:first-child { margin-top: 0; }
/* The website list is the part that gives when the sidebar runs out of room —
   and giving means scrolling inside its own box. Without the column layout and
   the overflow below it simply overran the links under it, which is what a
   long list, or one more link in the footer, actually produces. */
.side-section:has(.side-sites) {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.side-title {
  font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
  color: var(--text-faint); padding: 0 .65rem; margin-bottom: .35rem;
}
.side-sites { list-style: none; margin: 0; padding: 0; min-height: 0; overflow-y: auto; }
.side-site {
  display: flex; align-items: center; gap: .5rem;
  padding: .4rem .65rem; border-radius: var(--radius-sm);
  font-size: .875rem; color: var(--text-muted);
}
.side-site:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.side-site.is-active { background: var(--surface-2); color: var(--text); font-weight: 600; }
.side-site .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.side-empty { padding: .4rem .65rem; font-size: .83rem; color: var(--text-faint); }

.dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--idle); }
.dot.ok    { background: var(--ok); }
.dot.warn  { background: var(--warn); }
.dot.error { background: var(--danger); }

/* auto rather than a fixed gap: where a sidebar has a growing section — the
   customer's website list — this resolves to nothing and the foot sits under
   it as before, and where none grows, as in the admin panel, it pins the way
   out and Sign out to the bottom instead of leaving them mid-air. */
.side-foot { flex: none; margin-top: auto; padding-top: .9rem; border-top: 1px solid var(--border); }
/* Sign out posts a form, so it is a button wearing a nav link's clothes. */
.side-signout {
  width: 100%; background: none; border: none; cursor: pointer; font: inherit;
  font-weight: 600; font-size: .9rem; text-align: left;
}
/* An in-page link (there are few now that each section is its own page) must
   not drop its target behind the sticky topbar. */
[id] { scroll-margin-top: calc(var(--topbar-h) + 14px); }

.sidebar-backdrop { position: fixed; inset: 0; z-index: 60; background: rgba(6, 10, 24, .45); }
@media (min-width: 901px) { .sidebar-backdrop { display: none; } }

@media (max-width: 900px) {
  .nav-toggle { display: flex; }
  /* The open menu slides in under the sticky header, which stays on top so
     its button can close the menu again, so the menu's first link has to
     start below it. With only its own padding, "My websites" sat behind the
     header on every phone. */
  .sidebar {
    position: fixed; top: 0; left: 0; height: 100dvh; width: 268px;
    padding-top: calc(var(--topbar-h) + 1.1rem); box-shadow: var(--shadow-lg);
    transform: translateX(-100%); transition: transform .18s ease;
  }
  .sidebar.is-open { transform: none; }
}
@media (prefers-reduced-motion: reduce) { .sidebar { transition: none; } }

.page { max-width: 1180px; margin: 0 auto; padding: 1.75rem 1.5rem 4rem; }
/* 1180px is a reading width, chosen for pages that are mostly prose and cards.
   A page whose subject is one wide table is not that: capped at 1180 the
   security log scrolled sideways on a 27" monitor, with the space it needed
   sitting empty to the right of it. Still capped, because a row stretched
   across 3800px of 4K is no easier to read across than one that is cut off. */
.page-wide { max-width: 1600px; }
.page-head { display: flex; align-items: flex-start; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.page-head .grow { flex: 1; min-width: 220px; }
/* The greeting sits above the page title and stays smaller than it: being
   welcomed is pleasant, but the heading is what the page is about. */
.greeting { margin: 0 0 .1rem; font-size: .85rem; font-weight: 600; color: var(--text-muted); }
/* Named emoji fonts first: without them a system that has one still picks the
   page's text font, which has no wave in it. */
.greeting .wave { font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; }

.grid { display: grid; gap: 1.1rem; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* The ring needs less width than the addresses beside it: an IP and a request
   count is a wider line than three labelled figures. */
.grid-split { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); }
@media (max-width: 1100px) { .grid-split { grid-template-columns: 1fr; } }
@media (max-width: 900px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }
@media (max-width: 620px) { .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* The overview's headline row is not four equal quarters. The blocked figure
   is what somebody opens the page to see, so it takes twice the width of the
   two tiles that support it, and below 1100px it takes a whole row of its own
   rather than shrinking to a quarter of one. */
.stat-row { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); }
@media (max-width: 1100px) {
  .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat-row .stat-hero { grid-column: 1 / -1; }
}
@media (max-width: 620px) { .stat-row { grid-template-columns: minmax(0, 1fr); } }

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

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.card-head {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .95rem 1.15rem; border-bottom: 1px solid var(--border);
}
/* The title takes the width its words need and then grows into what is left.
   With a basis of zero it split the free space evenly with .topbar-spacer
   instead, so a head holding both wrapped its title onto a second line while
   half the row sat empty beside it. */
.card-head h2 { flex: 1 1 auto; }
/* A head is a title with its controls beside it, and on a phone they do not
   fit on one line. Unwrapped, the last one ran off the edge of the card —
   overflow:hidden on .card meant it could not be scrolled to either, so the
   security log's search box simply did not exist on a phone. */
.card-head > select, .card-head > input { max-width: 100%; }
/* Once wrapped they take the line they landed on and share it, rather than
   each keeping the pixel width it was given for a desktop: 240px of search box
   plus a filter sized to name every action we have is wider than the screen. */
@media (max-width: 620px) {
  .card-head > select, .card-head > input { flex: 1 1 150px; min-width: 0; }
}
.card-body { padding: 1.15rem; }
.card-body.tight { padding: .5rem 0; }
/* A list that draws its own full-width bands — the scan's severity headings —
   has to start at the card's edge, or the first band floats on a strip of
   card background and stops reading as a divider. */
.card-body.flush { padding: 0; }

/* ----------------------------------------------- protection status banner */

.status-banner {
  display: flex; align-items: center; gap: 1.1rem;
  padding: 1.35rem 1.5rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
  box-shadow: var(--shadow);
  position: relative; overflow: hidden;
}
.status-banner::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 5px; background: var(--idle);
}
.status-banner.ok::before    { background: var(--ok); }
.status-banner.warn::before  { background: var(--warn); }
.status-banner.error::before { background: var(--danger); }
.status-banner.ok    { background: linear-gradient(100deg, var(--ok-soft) 0%, var(--surface) 45%); }
.status-banner.warn  { background: linear-gradient(100deg, var(--warn-soft) 0%, var(--surface) 45%); }
.status-banner.error { background: linear-gradient(100deg, var(--danger-soft) 0%, var(--surface) 45%); }

.status-dot {
  flex: none; width: 16px; height: 16px; border-radius: 50%;
  background: var(--idle); box-shadow: 0 0 0 5px color-mix(in srgb, var(--idle) 18%, transparent);
}
.status-banner.ok .status-dot    { background: var(--ok);     box-shadow: 0 0 0 5px color-mix(in srgb, var(--ok) 20%, transparent); }
.status-banner.warn .status-dot  { background: var(--warn);   box-shadow: 0 0 0 5px color-mix(in srgb, var(--warn) 20%, transparent); }
.status-banner.error .status-dot { background: var(--danger); box-shadow: 0 0 0 5px color-mix(in srgb, var(--danger) 20%, transparent); }
.status-banner.ok .status-dot { animation: pulse 2.6s ease-out infinite; }
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 45%, transparent); }
  70%  { box-shadow: 0 0 0 12px color-mix(in srgb, var(--ok) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ok) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .status-banner.ok .status-dot { animation: none; }
}

.status-text { flex: 1; min-width: 0; }
.status-text h2 { font-size: 1.25rem; margin-bottom: .15rem; }
.status-text p { margin: 0; color: var(--text-muted); font-size: .92rem; }

/* -------------------------------------------------------------- switches */

.switch { display: inline-flex; align-items: center; gap: .6rem; cursor: pointer; user-select: none; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-track {
  width: 46px; height: 26px; border-radius: 999px; background: var(--border-strong);
  position: relative; transition: background .18s ease; flex: none;
}
.switch-track::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.25);
  transition: transform .18s ease;
}
.switch input:checked + .switch-track { background: var(--ok); }
.switch input:checked + .switch-track::after { transform: translateX(20px); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--brand); outline-offset: 2px; }
.switch-label { font-size: .9rem; font-weight: 600; }

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

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
  font-family: var(--font-body); font-size: .9rem; font-weight: 600;
  padding: .55rem 1rem; border-radius: var(--radius-sm);
  border: 1px solid transparent; cursor: pointer; white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, transform .05s ease;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn-primary { background: var(--brand); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--brand-dark); }
.btn-ghost { background: var(--surface); border-color: var(--border-strong); color: var(--text); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); border-color: var(--brand); color: var(--brand); }
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { background: #b4231a; }
.btn-sm { padding: .32rem .7rem; font-size: .82rem; }
.btn-block { width: 100%; }

.btn-google {
  background: #fff; color: #1f1f1f; border: 1px solid #dadce0;
  font-weight: 600; padding: .7rem 1.4rem; font-size: .95rem;
}
.btn-google:hover { background: #f7f8f8; box-shadow: var(--shadow); }
.btn-google svg { width: 18px; height: 18px; }

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

.pill {
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .12rem .55rem; border-radius: 999px;
  font-size: .74rem; font-weight: 700; letter-spacing: .01em;
  background: var(--idle-soft); color: var(--idle);
}
.pill.ok      { background: var(--ok-soft);     color: var(--ok); }
.pill.warn    { background: var(--warn-soft);   color: var(--warn); }
.pill.error   { background: var(--danger-soft); color: var(--danger); }
.pill.brand   { background: var(--brand-soft);  color: var(--brand); }

/* ------------------------------------------------------------- stat tile */

.stat { padding: 1.05rem 1.15rem; }
.stat-label { font-size: .76rem; font-weight: 600; text-transform: uppercase;
  letter-spacing: .04em; color: var(--text-faint); margin-bottom: .3rem; }
.stat-value { font-family: var(--font-head); font-size: 1.9rem; font-weight: 800;
  line-height: 1.1; letter-spacing: -.02em; }
.stat-value.danger { color: var(--danger); }
.stat-value.brand { color: var(--brand); }
.stat-sub { font-size: .82rem; color: var(--text-muted); margin-top: .15rem; }

/* --------------------------------------------------------- health checks */

.health-list { list-style: none; margin: 0; padding: 0; }
.health-item {
  display: flex; align-items: flex-start; gap: .75rem;
  padding: .8rem 1.15rem; border-bottom: 1px solid var(--border);
}
.health-item:last-child { border-bottom: none; }
.health-icon {
  flex: none; width: 11px; height: 11px; border-radius: 50%; margin-top: .42rem;
  background: var(--idle);
}
.health-item.ok    .health-icon { background: var(--ok); }
.health-item.warn  .health-icon { background: var(--warn); }
.health-item.error .health-icon { background: var(--danger); }
.health-main { flex: 1; min-width: 0; }
.health-label { font-weight: 600; font-size: .93rem; }
.health-detail { font-size: .85rem; color: var(--text-muted); }
.health-hint { font-size: .82rem; color: var(--text-faint); margin-top: .2rem; }

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

.table { width: 100%; border-collapse: collapse; font-size: .88rem; }
.table th {
  text-align: left; font-size: .74rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .05em; color: var(--text-faint);
  padding: .55rem 1.15rem; border-bottom: 1px solid var(--border); white-space: nowrap;
}
.table td { padding: .65rem 1.15rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
/* Seven columns of security log do not fit in 1.15rem of padding either side
   of each one — that alone was 160px of the overflow. Tightened only where a
   table is this wide, so the shorter tables keep the roomier rhythm. */
.table-dense th { padding-left: .8rem; padding-right: .8rem; }
/* Headings here are allowed to wrap. On an auto-layout table a nowrap heading
   is a floor under its column, and "What was detected" was holding open more
   width than the request that caused the entry got to use. */
.table-dense th { white-space: normal; }
.table-dense td { padding-left: .8rem; padding-right: .8rem; }
/* The repeat count is allowed to drop under its pill rather than hold the
   column open for every row that has none. The time beside it is already two
   lines, so the wrap costs no row height at all. */
.table-dense td .pill { white-space: nowrap; }
.table tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--surface-2); }
.table-wrap { overflow-x: auto; }

/* The row of controls under a paged table: page size on the left, where it is
   read as a property of the table, and the pager on the right, where the hand
   already is after reading down the last column. */
.table-foot {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  border-top: 1px solid var(--border);
}
.rows-per-page { display: flex; align-items: center; gap: .5rem; }
.rows-per-page > span { font-size: .82rem; color: var(--text-muted); }
/* A disabled pager button must read as unavailable rather than broken: it stays
   in place at the ends of the list so the row does not reflow under the cursor
   on the click that reaches the last page. */
.table-foot .btn:disabled { opacity: .45; cursor: default; }
.table-foot .btn:disabled:hover { background: none; }

/* The numbers themselves. They wrap on a narrow screen rather than pushing the
   row wider than the card — with fifty pages there are never more than seven of
   them on screen, but Previous and Next sit either side and a phone is narrow. */
.pager { display: flex; align-items: center; gap: .25rem; flex-wrap: wrap; }
/* Square-ish, so 1 and 10 are the same size and the row does not jump about as
   the page number grows. */
.page-btn { min-width: 2rem; padding-left: .4rem; padding-right: .4rem; }
.page-btn.is-current {
  background: var(--brand-soft); border-color: var(--brand); color: var(--brand);
  font-weight: 700; cursor: default;
}
.page-gap { padding: 0 .15rem; color: var(--text-faint); user-select: none; }

.empty { padding: 2.5rem 1.15rem; text-align: center; color: var(--text-faint); }
.empty strong { display: block; color: var(--text-muted); font-weight: 600; margin-bottom: .2rem; }

/* ----------------------------------------- country flags and user agents */

/* Flags are SVG files, not emoji. Windows ships no country-flag glyphs at all
   and many Linux desktops ship none either, so the emoji route left the
   browser holding a character it could not paint; the font we shipped to cover
   that drew Twemoji's 2013 artwork, which reads as dated next to the rest of
   the dashboard. These are the flat 4:3 flags a reader recognises from every
   other product (flag-icons, MIT — see /static/flags/LICENSE.txt).

   A file is fetched only when a row actually shows that country, so a page of
   Moroccan traffic costs one small request rather than a whole sprite. Treat
   the files as immutable: Cloudflare pins untagged /static/* for 31 days, so a
   corrected flag ships under a new path instead of overwriting one. */
.flag {
  display: inline-block;
  width: 1.3em; height: .975em;      /* 4:3, tied to the row's font size */
  margin-right: .4rem;
  vertical-align: -.16em;
  border-radius: 2px;
  box-sizing: border-box;
  object-fit: cover;
  background: var(--surface-2);      /* placeholder while the file loads */
}

/* The two-letter code stays next to the flag: it is the label people read. */
.cc {
  font-size: .72rem; font-weight: 700; letter-spacing: .04em; color: var(--text-faint);
  margin-right: .4rem;  /* the address follows immediately; without this they touch */
}

/* One column takes the width the others did not, and clips its text to it.

   max-width:0 is not a typo. On an auto-layout table it stops the cell growing
   to fit its longest string, and width:100% then hands it everything the other
   columns left over. Without the pair the column sizes to its content and the
   whole row scrolls sideways; with only a fixed max-width, a wide screen leaves
   the text cut short beside empty space, which is what this replaced.

   The text is only ever clipped by the width actually available, so on a roomy
   screen most rules are simply readable and nothing is marked at all. */
.table td.cell-grow { max-width: 0; width: 100%; }
/* Two columns can share the surplus: with both at max-width:0 the browser
   splits what the fixed columns left between them. The security log needs
   this — the request and what was found in it are both long, and picking one
   to clip while the other sizes to its content is what made the row wider
   than the screen. */
.table td.cell-grow-half { max-width: 0; width: 50%; }
.cell-clamp { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Added by markClipped, and only to text that really is cut off. The cursor is
   the whole hint: hovering shows the rest, the same as the Browser / tool
   column, so there is no underline to compete with the reading. */
.cell-clamp.is-clipped { cursor: help; }

/* Time on two lines. One line of toLocaleString() was the widest column in the
   security log — wider than the request that caused the entry — and a log is
   read down the clock, so the clock leads and the date sits under it. */
.stack-time { line-height: 1.25; }
.stack-time .date { display: block; font-size: .78rem; color: var(--text-faint); }

/* The reference the blocked visitor was shown, under the action it belongs to.
   It sits there rather than in a column of its own because it is read on only
   two occasions — recognising the row a complaint is about, and quoting it back
   — and neither is worth the width. Selecting all of it in one click is the
   point of user-select: the whole use of a reference is copying it somewhere. */
.row-ref {
  display: block;
  margin-top: .25rem;
  font-family: var(--font-mono);
  font-size: .72rem;
  color: var(--text-faint);
  user-select: all;
}

.ua { max-width: 240px; }
/* The raw user agent is already ellipsised, so the column only has to be wide
   enough to name the tool; the rest is one hover away. */
.table-dense .ua { max-width: 150px; }
/* And the name of the tool is held to one line. Wrapped, it made a row half as
   tall again as the ones around it, and a log whose rows are all different
   heights is markedly harder to read down. */
.table-dense .ua-label {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ua-label { font-weight: 600; font-size: .85rem; }
.ua-raw {
  display: block; font-family: var(--font-mono); font-size: .72rem; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ----------------------------------------------------------- live feed */

.feed { list-style: none; margin: 0; padding: 0; max-height: 460px; overflow-y: auto; }
.feed-item {
  display: flex; gap: .7rem; padding: .6rem 1.15rem;
  border-bottom: 1px solid var(--border); font-size: .85rem; align-items: baseline;
}
.feed-item:last-child { border-bottom: none; }
.feed-item.is-new { animation: flash 1.4s ease-out; }
@keyframes flash {
  from { background: color-mix(in srgb, var(--brand) 14%, transparent); }
  to   { background: transparent; }
}
@media (prefers-reduced-motion: reduce) { .feed-item.is-new { animation: none; } }
.feed-time { flex: none; color: var(--text-faint); font-family: var(--font-mono); font-size: .78rem; }
.feed-main { flex: 1; min-width: 0; }
.feed-path { font-family: var(--font-mono); font-size: .8rem; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
.feed-msg { font-weight: 500; }

.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: none;
  animation: pulse-live 1.8s ease-in-out infinite; }
.live-dot.off { background: var(--text-faint); animation: none; }
@keyframes pulse-live { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

/* -------------------------------------------------------------- chart */

.chart { width: 100%; min-height: 260px; }



/* --------------------------------------------------------------- forms */

label.field { display: block; margin-bottom: 1rem; }
label.field > span { display: block; font-weight: 600; font-size: .88rem; margin-bottom: .3rem; }
input[type=text], input[type=number], select, textarea {
  width: 100%; padding: .6rem .8rem; font-family: var(--font-body); font-size: .95rem;
  color: var(--text); background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--brand); outline-offset: -1px; border-color: var(--brand); }
.field-hint { font-size: .82rem; color: var(--text-faint); margin-top: .25rem; }
textarea { min-height: 5.5rem; resize: vertical; line-height: 1.5; font-family: var(--font-mono); font-size: .85rem; }

/* A button that reads as a link, for the "use this value" shortcuts in forms. */
.linkish {
  background: none; border: none; padding: 0; font: inherit; color: var(--brand);
  cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}

.code-box {
  position: relative; background: var(--navy); color: #dbe6ff;
  border-radius: var(--radius-sm); padding: .95rem 3rem .95rem 1rem;
  font-family: var(--font-mono); font-size: .82rem; line-height: 1.6;
  word-break: break-all; border: 1px solid var(--border);
}
.code-box .copy-btn {
  position: absolute; top: .55rem; right: .55rem;
  background: rgba(255,255,255,.12); color: #fff; border: none;
  border-radius: 6px; padding: .3rem .6rem; font-size: .78rem; font-weight: 600; cursor: pointer;
}
.code-box .copy-btn:hover { background: rgba(255,255,255,.22); }

/* --------------------------------------------------------------- notices */

.notice {
  display: flex; gap: .65rem; padding: .8rem 1rem; border-radius: var(--radius-sm);
  font-size: .89rem; border: 1px solid transparent; margin-bottom: 1rem;
}
.notice.info  { background: var(--brand-soft);  border-color: color-mix(in srgb, var(--brand) 25%, transparent); }
.notice.warn  { background: var(--warn-soft);   border-color: color-mix(in srgb, var(--warn) 30%, transparent); color: var(--warn); }
.notice.error { background: var(--danger-soft); border-color: color-mix(in srgb, var(--danger) 30%, transparent); color: var(--danger); }
.notice.ok    { background: var(--ok-soft);     border-color: color-mix(in srgb, var(--ok) 30%, transparent); color: var(--ok); }
/* The offline line on a website card. Full width under the figures, which is
   where the eye lands after reading them. */
.site-offline { margin-top: .9rem; font-size: .82rem; }
/* "Website offline" is longer than "Protected", and a status that breaks
   across two lines beside a domain that is already wrapping reads as damage.
   The pill keeps its line; the domain, which has somewhere to wrap to, gives. */
.site-card .pill { white-space: nowrap; }

/* ---------------------------------------------------------------- modal */

.modal-backdrop {
  position: fixed; inset: 0; z-index: 90; background: rgba(8,12,24,.55);
  display: flex; align-items: center; justify-content: center; padding: 1.5rem;
  backdrop-filter: blur(2px);
}
.modal {
  background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow-lg);
  width: 100%; max-width: 440px; border: 1px solid var(--border);
}
/* A refused request is lines of text, not a form, and at 440px every one of
   them wraps into an unreadable stack. */
.modal-wide { max-width: 760px; }
/* The refused request itself. It scrolls rather than growing, so a long body
   cannot push the Close button off the bottom of a laptop screen, and it wraps
   rather than scrolling sideways: a query string is one enormous line, and
   reading it two characters at a time through a horizontal bar is no reading
   at all. */
.capture {
  margin: 0; padding: .8rem .9rem; max-height: 46vh; overflow: auto;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text);
  font-family: var(--font-mono); font-size: .8rem; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.modal-head { padding: 1.15rem 1.35rem .4rem; }
.modal-head h2 { margin-bottom: .25rem; }
.modal-body { padding: .6rem 1.35rem 1.1rem; }
.modal-foot { display: flex; gap: .6rem; justify-content: flex-end; padding: 0 1.35rem 1.35rem; }

.choice-list { display: grid; gap: .5rem; margin: .8rem 0 0; }
.choice {
  display: flex; align-items: center; gap: .65rem; padding: .7rem .9rem; cursor: pointer;
  border: 1px solid var(--border-strong); border-radius: var(--radius-sm); font-size: .9rem;
}
.choice:hover { border-color: var(--brand); background: var(--brand-soft); }
.choice input { accent-color: var(--brand); }
.choice.selected { border-color: var(--brand); background: var(--brand-soft); }
.choice-title { font-weight: 600; }
.choice-sub { font-size: .8rem; color: var(--text-muted); }

/* --------------------------------------------------------------- toasts */

#toasts { position: fixed; bottom: 1.25rem; right: 1.25rem; z-index: 100;
  display: flex; flex-direction: column; gap: .55rem; max-width: 360px; }
.toast {
  background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--brand);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
  padding: .7rem .95rem; font-size: .88rem; animation: slide-in .2s ease-out;
}
.toast.ok    { border-left-color: var(--ok); }
.toast.error { border-left-color: var(--danger); }
@keyframes slide-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }

/* ------------------------------------------------------------- tooltips */

/* The "?" itself is a button so it answers to the keyboard as well as the
   mouse. The bubble it opens is NOT a child of it: cards clip their contents
   (`overflow: hidden` keeps tables inside the rounded border), which silently
   swallowed the old ::after tooltip. It now lives at the end of <body> and is
   positioned against the viewport, so no ancestor can crop it. */
.tip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; padding: 0; flex: none;
  border-radius: 50%; border: 1px solid var(--border-strong); background: none;
  color: var(--text-faint); font-family: var(--font-body); font-size: 10px;
  font-weight: 700; line-height: 1; cursor: help;
}
.tip:hover, .tip:focus-visible { color: var(--brand); border-color: var(--brand); }

#tooltip {
  position: fixed; z-index: 200; max-width: 280px;
  background: var(--navy); color: #fff;
  padding: .5rem .7rem; border-radius: 8px;
  font-size: .78rem; font-weight: 400; line-height: 1.45; text-align: left;
  box-shadow: var(--shadow-lg);
  opacity: 0; visibility: hidden; transform: translateY(2px);
  transition: opacity .12s ease, transform .12s ease;
  pointer-events: none; /* the bubble must never sit between cursor and anchor */
}
#tooltip.is-open { opacity: 1; visibility: visible; transform: none; }
@media (prefers-reduced-motion: reduce) { #tooltip { transition: none; } }

/* --------------------------------------------------------- auth screens */

.auth-shell { min-height: 100vh; display: grid; place-items: center; padding: 1.5rem;
  background: radial-gradient(1000px 500px at 50% -10%, var(--brand-soft), var(--bg)); }
.auth-card { width: 100%; max-width: 420px; background: var(--surface);
  border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg);
  padding: 2.25rem 2rem; text-align: center; }
.auth-card img.logo { width: 52px; height: 52px; border-radius: 13px; margin-bottom: 1rem; }
.auth-card h1 { font-size: 1.45rem; margin-bottom: .4rem; }
.auth-card p.sub { color: var(--text-muted); font-size: .92rem; margin-bottom: 1.5rem; }
.auth-foot { margin-top: 1.5rem; font-size: .8rem; color: var(--text-faint); }

/* The promises under the sign-up button. Left-aligned inside a centred card,
   because a centred list of four sentences reads as a poem, not a list. */
.trust-list {
  list-style: none; margin: 1.15rem auto 0; padding: 0; display: grid; gap: .45rem;
  width: fit-content; max-width: 100%; text-align: left; font-size: .9rem;
}
.trust-list li { display: flex; gap: .55rem; align-items: baseline; }
.trust-list li::before { content: "✓"; flex: none; color: var(--ok); font-weight: 700; }
.price-line { margin: 1.1rem 0 0; font-size: .88rem; color: var(--text-muted); }

.code-input {
  font-family: var(--font-mono); font-size: 1.6rem; letter-spacing: .5rem;
  text-align: center; padding: .7rem; width: 100%;
}

/* --------------------------------------------------------------- steps */

.steps { list-style: none; margin: 0; padding: 0; counter-reset: step; }
.step { display: flex; gap: 1rem; padding-bottom: 1.6rem; position: relative; }
.step:not(:last-child)::before {
  content: ""; position: absolute; left: 15px; top: 34px; bottom: 4px; width: 2px; background: var(--border);
}
.step-num {
  flex: none; width: 32px; height: 32px; border-radius: 50%; background: var(--brand-soft);
  color: var(--brand); display: grid; place-items: center; font-weight: 800; font-size: .9rem;
  font-family: var(--font-head); z-index: 1; border: 2px solid var(--surface);
}
.step.done .step-num { background: var(--ok); color: #fff; }
.step.active .step-num { background: var(--brand); color: #fff; }
.step-body { flex: 1; min-width: 0; padding-top: .2rem; }
.step-body h3 { margin-bottom: .3rem; }

/* -------------------------------------------------------------- site card */

/* The whole card is clickable without the card being a link: .stretch throws an
   invisible overlay across the card from the domain's own anchor. Wrapping the
   card in an <a> instead would put the "Finish setup" link inside another link,
   which the HTML parser refuses to nest and tears the card in half. */
.site-card { position: relative; display: block; color: inherit; }
.site-card:hover { text-decoration: none; border-color: var(--brand); }
.site-card:hover .site-domain { color: var(--brand); }
.site-domain { font-family: var(--font-head); font-weight: 700; font-size: 1.05rem; color: inherit; }
/* The site's own favicon. It is somebody else's artwork at an unknown size and
   aspect, so it is boxed rather than trusted to lay itself out, and the box is
   reserved whether or not the image ever decodes — a card that reflows when a
   customer's server is slow is worse than one that starts with a gap. */
.site-favicon {
  width: 16px; height: 16px; flex: none;
  object-fit: contain; border-radius: 3px;
}
.site-domain:hover { text-decoration: none; }
.stretch::after { content: ''; position: absolute; inset: 0; border-radius: inherit; }
/* Anything the visitor is meant to click in its own right has to sit above
   that overlay, or the card's link swallows it. */
.site-cta { position: relative; z-index: 1; margin-top: .8rem; }
/* A trial's days left, under the figures: a fact, not a warning. */
.site-plan { margin-top: .75rem; font-size: .82rem; color: var(--text-muted); }

/* ------------------------------------------------------------------ tour */

/* The guided tour for people opening a page for the first time. The overlay
   covers everything so a half-finished tour cannot be clicked through, and the
   dimming is done by an enormous shadow around the spotlight rather than by a
   backdrop with a hole in it, which no browser gives us. */

.guide-btn { display: inline-flex; align-items: center; gap: .4rem; }
.guide-btn svg {
  width: 15px; height: 15px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
}
@media (max-width: 620px) { .guide-btn span { display: none; } }

.tour { position: fixed; inset: 0; z-index: 150; }
.tour[hidden] { display: none; }

.tour-spot {
  position: fixed; border-radius: var(--radius-sm);
  box-shadow: 0 0 0 9999px rgba(6, 10, 22, .62);
  outline: 2px solid var(--brand);
  outline-offset: 3px;
  transition: top .22s ease, left .22s ease, width .22s ease, height .22s ease;
  pointer-events: none;
}
/* A step with nothing to point at dims the whole page instead. */
.tour-spot.is-flat { inset: 0; border-radius: 0; outline: none; box-shadow: none;
  background: rgba(6, 10, 22, .62); width: auto; height: auto; }

.tour-pop {
  position: fixed; width: min(340px, calc(100vw - 2rem));
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  padding: 1.05rem 1.15rem .9rem;
  transition: top .22s ease, left .22s ease;
}
.tour-count {
  font-size: .72rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--brand);
}
.tour-pop h3 { font-size: 1.02rem; margin: .3rem 0 .35rem; }
.tour-pop p { font-size: .87rem; line-height: 1.5; color: var(--text-muted); margin: 0; }
.tour-foot {
  display: flex; align-items: center; gap: .5rem; margin-top: 1rem;
  padding-top: .8rem; border-top: 1px solid var(--border);
}
.tour-foot .grow { flex: 1; }
.tour-skip {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-size: .82rem; color: var(--text-faint);
}
.tour-skip:hover { color: var(--text-muted); text-decoration: underline; }

@media (prefers-reduced-motion: reduce) {
  .tour-spot, .tour-pop { transition: none; }
}

/* ------------------------------------------------- health panel fix links */

/* "Turn rate limiting back on →" under a warning. Naming the problem without
   naming its cure just moves the search to the settings page. */
.health-fix {
  display: inline-block; margin-top: .35rem;
  font-size: .82rem; font-weight: 600; color: var(--brand);
}
.health-fix:hover { text-decoration: underline; }

/* The control a fix link points at. The browser scrolls to a fragment but
   gives no sign of which element it meant, so say so for a moment. */
.is-target {
  animation: target-flash 2.4s ease-out;
  border-radius: var(--radius-sm);
}
@keyframes target-flash {
  0%, 55% { box-shadow: 0 0 0 3px var(--brand), 0 0 0 8px color-mix(in srgb, var(--brand) 25%, transparent); }
  100%    { box-shadow: 0 0 0 3px transparent, 0 0 0 8px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .is-target { animation: none; box-shadow: 0 0 0 3px var(--brand); }
}

/* ------------------------------------------------- blocked file types */

.ext-groups { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); }
.ext-group {
  display: flex; align-items: flex-start; gap: .65rem; cursor: pointer;
  padding: .7rem .85rem; border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
.ext-group:hover { border-color: var(--brand); }
.ext-group.is-on { border-color: var(--brand); background: var(--brand-soft); }
.ext-group input { accent-color: var(--brand); margin-top: .15rem; flex: none; }
.ext-main { display: flex; flex-direction: column; gap: .15rem; min-width: 0; }
.ext-label { font-weight: 600; font-size: .89rem; }
.ext-label em { font-style: normal; font-weight: 400; color: var(--text-faint); }
.ext-hint { font-size: .8rem; color: var(--text-muted); line-height: 1.4; }
.ext-list {
  font-family: var(--font-mono); font-size: .74rem; color: var(--text-faint);
  word-break: break-word; margin-top: .1rem;
}

/* --- email preference groups ---------------------------------------------- */
/* The switches themselves stay separate — one heading is not one setting — so
   the heading is quiet: it sorts the list without competing with the labels. */
.pref-group-head {
  margin: 0 0 .5rem; font-size: .74rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .05em; color: var(--text-faint);
}
.pref-group-rows { display: grid; gap: .5rem; }
/* The container is a grid with its own gap, so a group only needs the extra
   breathing room that separates one heading from the rows above it. */
.pref-group + .pref-group { margin-top: .55rem; }

/* --- vulnerability scan ---------------------------------------------------- */

/* Severity is the one thing on this page allowed to shout. A database dump
   anyone can download and a cookie missing an attribute are the same shape of
   object — a title, an explanation, a fix — so if the page does not separate
   them by weight, the reader has to separate them by reading, which is the
   work the scan was supposed to do.
   The ladder is built from three things at once, not from hue alone: how wide
   the rail is, whether the row is tinted, and the wording of the heading above
   it. Red-green blindness costs the reader one of the three. */
.sev-critical { --sev: var(--danger); }
.sev-high     { --sev: var(--danger); }
.sev-medium   { --sev: var(--warn); }
.sev-low      { --sev: var(--idle); }
.sev-info     { --sev: var(--text-faint); }

/* The verdict block ------------------------------------------------------- */
/* The headline is the answer; the strip under it is where the answer came
   from. One container, because "5 findings" and "scanned three weeks ago" are
   the same sentence. */
.scan-verdict {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.scan-verdict > .status-banner { border: 0; border-radius: 0; box-shadow: none; }

.scan-facts {
  display: flex; flex-wrap: wrap; gap: .3rem 1.9rem;
  margin: 0; padding: .75rem 1.5rem;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  font-size: .85rem;
}
.scan-facts > div { display: flex; align-items: baseline; gap: .4rem; min-width: 0; }
.scan-facts dt { flex: none; color: var(--text-faint); }
.scan-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* Score ------------------------------------------------------------------- */
/* Three bands, sharing the tones the rest of the dashboard already uses for
   good, watch-this and act-now. No fourth colour: a score is read in half a
   second and a palette with two ambers in it costs more than that. */
.scan-score.good { --score: var(--ok); }
.scan-score.warn { --score: var(--warn); }
.scan-score.risk { --score: var(--danger); }

.scan-score {
  display: flex; flex-direction: column; align-items: center; gap: .25rem;
  flex: none;
}
.scan-score-dial { position: relative; width: 68px; height: 68px; }
/* Rotated so the arc starts at twelve o'clock, where a dial is read from. */
.scan-score-dial svg { width: 100%; height: 100%; transform: rotate(-90deg); display: block; }
.scan-ring-track {
  fill: none; stroke: var(--border-strong); stroke-width: 5; opacity: .5;
}
/* Thick enough that a score of 6 still draws an arc somebody can see is an
   arc, rather than a speck at twelve o'clock that reads as a rendering fault. */
.scan-ring-value {
  fill: none; stroke: var(--score, var(--idle)); stroke-width: 5; stroke-linecap: round;
}
.scan-score-num {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font-family: var(--font-head); font-weight: 800; font-size: 1.4rem;
  letter-spacing: -.03em; color: var(--text);
}
.scan-score-band {
  font-size: .76rem; font-weight: 700; color: var(--score, var(--text-muted));
  white-space: nowrap;
}

/* The direction of travel, under the verdict it belongs to. Set in ordinary
   muted ink rather than green-for-up: a score that rose because somebody
   deleted a file is the same sentence as one that rose because a check could
   not run, and only the first deserves a colour. */
.scan-trend { margin: .3rem 0 0; font-size: .86rem; color: var(--text-muted); }

.scan-partial { border-bottom: 1px dotted var(--border-strong); cursor: help; }

/* The score in the history table. Compact, because the column is read down
   rather than across — the numbers have to line up more than they have to be
   large. */
.score-cell { display: inline-flex; align-items: baseline; gap: .4rem; white-space: nowrap; }
.score-cell b {
  font-family: var(--font-head); font-weight: 800; font-size: 1rem;
  letter-spacing: -.02em; color: var(--score, var(--text));
}
.score-cell.good { --score: var(--ok); }
.score-cell.warn { --score: var(--warn); }
.score-cell.risk { --score: var(--danger); }

/* Severity filter --------------------------------------------------------- */
/* This replaces a row of four stat tiles. The counts were never a metric —
   nobody watches their medium-severity total move week to week — they are how
   you narrow a long report, so they sit on the list they narrow and do the
   narrowing when pressed. */
.sev-filter {
  display: flex; flex-wrap: wrap; gap: .45rem;
  padding: .8rem 1.15rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.sev-chip {
  display: inline-flex; align-items: center; gap: .45rem;
  padding: .38rem .8rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-muted);
  font-family: var(--font-body); font-size: .85rem; font-weight: 600;
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.sev-chip::before {
  content: ""; flex: none;
  width: .5rem; height: .5rem; border-radius: 2px;
  background: var(--sev, var(--border-strong));
}
.sev-chip[data-sev="all"]::before { display: none; }
/* Critical and important share the red — they are both "today" — and are told
   apart in the list by a tinted row against a plain one. The chips have no row
   to tint, so they borrow the same filled-against-hollow distinction rather
   than inventing a second red nobody would read as a rank. */
.sev-chip.sev-high::before { background: transparent; box-shadow: inset 0 0 0 2px var(--sev); }
.sev-chip b {
  font-family: var(--font-head); font-weight: 800; font-size: .95rem;
  color: var(--text); letter-spacing: -.01em;
}
.sev-chip:hover:not(:disabled) { border-color: var(--brand); color: var(--text); }
.sev-chip[aria-pressed="true"] {
  background: var(--brand-soft); border-color: var(--brand); color: var(--brand);
}
.sev-chip[aria-pressed="true"] b { color: var(--brand); }
.sev-chip:disabled { opacity: .5; cursor: default; }
.sev-chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Findings ---------------------------------------------------------------- */
/* A finding is a short argument, not a table row: what was found, what it
   means, what to do about it. Laying it out as a table would force the
   explanation into a cell too narrow to read it in. */
.findings { list-style: none; margin: 0; padding: 0; }

/* Where one severity ends and the next begins, said once at the boundary
   instead of on every row. It also carries the count, so the group answers
   "how much of this is there" before any of it is read. */
.sev-heading {
  display: flex; align-items: baseline; gap: .5rem;
  padding: .6rem 1.25rem .55rem 1.6rem;
  background: var(--surface-2);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  position: relative;
  font-family: var(--font-head); font-size: .85rem; font-weight: 700;
}
.sev-heading:first-child { border-top: 0; }
.sev-heading::before {
  content: ""; position: absolute; inset: 0 auto 0 0;
  width: 4px; background: var(--sev, var(--border-strong));
}
/* The heading's rail is the top of the band its findings continue, so a
   quieter severity has to be quieter here too or the column steps in and out. */
.sev-heading.sev-low::before, .sev-heading.sev-info::before { width: 2px; }
.sev-heading-count {
  font-family: var(--font-body); font-size: .8rem; font-weight: 500;
  color: var(--text-faint);
}

.finding {
  position: relative;
  padding: 1.05rem 1.25rem 1.1rem 1.6rem;
  border-top: 1px solid var(--border);
}
.finding:first-child, .sev-heading + .finding { border-top: 0; }
.finding::before {
  content: ""; position: absolute; inset: 0 auto 0 0;
  width: 4px; background: var(--sev, var(--border-strong));
}
/* Minor and informational notes get a hairline instead of a band. They are on
   the page so the report can be trusted as complete, not because anyone has to
   act on them today. */
.finding.sev-low::before, .finding.sev-info::before { width: 2px; }
/* Only the top of the ladder is tinted. A second colour of row would make the
   tint a decoration rather than a warning. */
.finding.sev-critical { background: color-mix(in srgb, var(--danger-soft) 60%, var(--surface)); }

.finding-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .3rem .55rem; }
.finding-title {
  font-family: var(--font-head); font-size: 1rem; font-weight: 700;
  letter-spacing: -.01em; margin: 0;
}
.finding-detail {
  color: var(--text-muted); font-size: .93rem;
  margin: .3rem 0 0; max-width: 68ch;
}
.finding-evidence {
  display: block; width: fit-content; max-width: 100%;
  margin-top: .6rem;
  font-family: var(--font-mono); font-size: .84rem;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: .3rem .55rem;
  overflow-wrap: anywhere;
}
.finding.sev-critical .finding-evidence { background: var(--surface); }

/* The advice and the control that carries it out, in one box. This is the
   argument for building the scanner into the product instead of buying one:
   the sentence telling you what to do sits against the button that does it. */
.finding-fix {
  display: flex; flex-wrap: wrap; align-items: center; gap: .55rem .9rem;
  margin-top: .75rem; padding: .6rem .75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.finding-fix .btn { flex: none; }
.finding-remedy {
  margin: .75rem 0 0; font-size: .9rem; color: var(--text-muted); max-width: 68ch;
}
.finding-remedy strong { color: var(--text); font-weight: 600; }
.finding-fix .finding-remedy { flex: 1 1 22rem; min-width: 0; margin: 0; }

/* Nothing found is a result, and it is the one the owner is hoping for, so it
   is drawn as an answer rather than as an absence of rows. */
.findings-clear { padding: 2.25rem 1.5rem; text-align: center; }
.findings-clear-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--ok-soft); color: var(--ok);
  font-size: 1.5rem; font-weight: 700; line-height: 1;
  margin-bottom: .6rem;
}
.findings-clear strong { display: block; font-family: var(--font-head); font-size: 1.05rem; }
.findings-clear p { margin: .25rem auto 0; max-width: 46ch; color: var(--text-muted); font-size: .92rem; }

/* The checked list is reassurance, so it is deliberately quiet. Green pills
   for all thirteen of them spent the colour that the "nothing found" answer
   needs, and left the reader with a wall to read past. */
.checked-list {
  list-style: none; margin: .2rem 0 0; padding: 0;
  display: grid; gap: .3rem 1.4rem;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
}
.checked-list li {
  display: flex; align-items: baseline; gap: .45rem;
  font-size: .88rem; color: var(--text-muted);
}
.checked-list li::before { content: "✓"; flex: none; color: var(--ok); font-weight: 700; }

.scan-inventory { margin: 1rem 0 0; font-size: .88rem; max-width: 76ch; }

@media (max-width: 620px) {
  .scan-facts { padding: .75rem 1.15rem; gap: .3rem 1.2rem; }
  /* The banner is a dial, a verdict and a button on one row, and on a phone
     that row is about 200px short. Nothing wrapped before, so the verdict took
     what was left after the other two — roughly one word per line. The button
     gets its own line and the verdict gets a floor it cannot shrink below.
     Scoped to this page's block: the overview's banner is a different shape
     and is not this one's to re-lay-out. */
  .scan-verdict .status-banner { flex-wrap: wrap; padding: 1.1rem 1.15rem; }
  .scan-verdict .status-text { flex: 1 1 55%; min-width: 11rem; }
  .scan-verdict .status-banner > .btn { flex: 1 1 100%; }
  /* Lying down, the dial and its band read as one object beside the verdict
     rather than as a stack competing with it for height. */
  .scan-score { flex-direction: row; align-items: center; gap: .55rem; }
  .scan-score-dial { width: 54px; height: 54px; }
  .scan-score-num { font-size: 1.15rem; }
  .finding { padding-right: 1.15rem; }
  .finding-fix .finding-remedy { flex-basis: 100%; }
}

/* --- traffic breakdown ---------------------------------------------------- */

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

/* The icon exists so the tiles can be told apart at a glance, not to explain
   anything: the label is what names the number, which is why the glyph is
   aria-hidden and never carries meaning on its own. */
/* The floor keeps the two narrow tiles' numbers on one line as the column
   gets tighter: "Watched, not blocked" wraps to two lines before "Your block
   list" does, and without a floor the pair would drift out of step at exactly
   the widths where the row is hardest to read anyway. The hero sits higher
   than both by design, so it is the narrow pair this is for. */
.stat-top {
  display: flex;
  align-items: center;
  gap: .55rem;
  margin-bottom: .55rem;
  min-height: 2.5rem;
}
.stat-top .stat-label { margin-bottom: 0; }
.stat-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: none;
  border-radius: 8px;
  background: var(--idle-soft);
  color: var(--idle);
}
.stat-icon svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.stat-icon.danger { background: var(--danger-soft); color: var(--danger); }
.stat-icon.warn   { background: var(--warn-soft);   color: var(--warn); }
.stat-icon.brand  { background: var(--brand-soft);  color: var(--brand); }

/* The comparison is set in ordinary ink. More attacks blocked is a firewall
   working and a website under more pressure at once, so green-for-up would
   assert a verdict the number cannot support. */
.stat-delta {
  font-size: .8rem;
  color: var(--text-muted);
  margin-top: .3rem;
  min-height: 1.2em;
}
.stat-delta.flat { color: var(--text-faint); }
.delta-arrow { font-weight: 700; margin-right: .1rem; }
.delta-vs { color: var(--text-faint); }

/* The hero is bigger because it answers the question the page is opened for.
   Nothing else on the row is set this large, which is what tells a reader
   where to look first without a word of instruction. */
.stat-hero .stat-value { font-size: 2.6rem; }
.stat-hero .stat-sub   { font-size: .9rem; }
.stat-hero .stat-unit  { font-size: 1.05rem; }

/* The word for what a number counts, riding beside it at reading size. A bare
   "4" next to a bare "12" invites the reader to compare the two as though
   they measured the same thing, which is how this row came to look like it
   was contradicting itself. "4 attackers" beside "12 sources" cannot be
   misread that way. It overrides the display face, the tightened tracking and
   the red, because it is a word in a row of figures and should read as one. */
.stat-unit {
  font-family: var(--font-body);
  font-size: .85rem;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--text-muted);
  margin-left: .35rem;
}

/* The window the figures cover, printed beside the label instead of left for
   the picker at the top of the page to imply. */
.stat-window {
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-faint);
}

/* The block list is a setting rather than a measurement — it is the one number
   in this row the range picker does not change — so its tile ends in the way
   out to the page that does change it. */
.stat-link {
  display: inline-block;
  margin-top: .35rem;
  font-size: .8rem;
  font-weight: 600;
  color: var(--brand);
  text-decoration: none;
}
.stat-link:hover { text-decoration: underline; }

/* --- traffic ring --------------------------------------------------------- */

.donut-body { display: flex; align-items: center; gap: 1.4rem; flex-wrap: wrap; }
.donut { flex: 0 0 210px; min-width: 190px; }
.donut-legend { flex: 1 1 190px; display: flex; flex-direction: column; gap: .6rem; }
.donut-row {
  display: grid;
  grid-template-columns: 10px 1fr auto auto;
  align-items: baseline;
  gap: .55rem;
}
.donut-dot { width: 10px; height: 10px; border-radius: 3px; align-self: center; }
.donut-row.allowed   .donut-dot { background: var(--chart-allowed); }
.donut-row.suspected .donut-dot { background: var(--chart-suspicious); }
.donut-row.blocked   .donut-dot { background: var(--chart-blocked); }
.donut-key { font-size: .88rem; color: var(--text-muted); }
.donut-val { font-size: 1rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.donut-pct { font-size: .8rem; color: var(--text-faint); min-width: 2.8rem; text-align: right; }

/* --- ranked list ---------------------------------------------------------- */

.rank-list { list-style: none; margin: 0; padding: 0; }
.rank-row {
  display: grid;
  grid-template-columns: 1.4rem 1fr auto;
  align-items: center;
  gap: .7rem;
  padding: .55rem 1.15rem;
  border-bottom: 1px solid var(--border);
}
.rank-row:last-child { border-bottom: 0; }
.rank-n {
  font-size: .78rem;
  font-weight: 600;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}
.rank-ip { display: flex; align-items: center; gap: .45rem; min-width: 0; }
.rank-ip .mono { font-family: var(--font-mono); font-size: .85rem; }
.rank-hits {
  text-align: right;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  font-size: .92rem;
}
.rank-unit { display: block; font-size: .72rem; font-weight: 400; color: var(--text-faint); }
.rank-list .empty { padding: 1.15rem; color: var(--text-faint); font-size: .88rem; }
.rank-list .empty strong { display: block; color: var(--text); margin-bottom: .15rem; }

/* --- charts --------------------------------------------------------------- */

/* ApexCharts ships its own tooltip chrome and its own idea of light and dark.
   Both are stripped back to nothing here so the card below is the only thing
   drawn, which is what keeps a chart tooltip looking like the rest of the
   dashboard instead of like the library. */
.apexcharts-tooltip,
.apexcharts-tooltip.apexcharts-theme-light,
.apexcharts-tooltip.apexcharts-theme-dark {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  overflow: visible !important;
}
.apexcharts-xaxistooltip, .apexcharts-yaxistooltip { display: none !important; }
.apexcharts-legend-marker { border-radius: 2px !important; }
.apexcharts-legend-text { font-family: var(--font-body) !important; }

/* Cards clip their children so that tables and lists sit inside the rounded
   corner. A chart tooltip is taller than the bar it describes and has to be
   allowed out; nothing else in these two cards reaches an edge. */
#traffic-card, #activity { overflow: visible; }

.chart-tip {
  min-width: 172px;
  padding: .6rem .7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-body);
  font-size: .82rem;
  color: var(--text);
  text-align: left;
}
.chart-tip-head {
  font-weight: 600;
  margin-bottom: .4rem;
  padding-bottom: .35rem;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.chart-tip-row { display: flex; align-items: center; gap: .45rem; line-height: 1.7; }
.chart-tip-dot { width: 9px; height: 9px; border-radius: 2px; flex: none; }
/* The label is ink, never the series colour — the swatch beside it already
   carries the identity, and coloured text is the harder of the two to read. */
.chart-tip-key { color: var(--text-muted); }
.chart-tip-val { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
.chart-tip-pct { font-weight: 400; color: var(--text-faint); }

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

/* A count on a navigation link. It sits at the end of the row rather than on
   the icon, because the sidebar collapses to icons on no breakpoint we ship
   and a number is easier to read than a dot is to interpret. */
.side-badge {
  margin-left: auto;
  min-width: 20px;
  padding: 0 .35rem;
  border-radius: 999px;
  background: var(--brand);
  color: #fff;
  font-size: .72rem;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

/* --- operator banner ------------------------------------------------------ */

/* Drawn at the top of every website page an operator opened that is not their
   own. It is deliberately the loudest thing on the page: the failure it exists
   to prevent is somebody changing a setting while believing the website in
   front of them is one of theirs. */
.operator-banner {
  display: flex;
  align-items: center;
  gap: .9rem;
  margin-bottom: 1.1rem;
  padding: .8rem 1rem;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}
.operator-banner svg {
  width: 22px;
  height: 22px;
  flex: none;
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.operator-banner div { flex: 1; min-width: 0; font-size: .88rem; }
.operator-banner strong { display: block; color: var(--accent); }
.operator-banner span { color: var(--text-muted); }

/* --- plans ---------------------------------------------------------------- */

/* The line every page of a website carries in the last week of a trial or a
   paid period, and after it ends. It borrows the operator banner's shape so
   the two read as the same kind of thing — something about this website that
   is true on every page of it — but takes its colour from how much it matters. */
.plan-banner {
  display: flex;
  align-items: center;
  gap: .9rem;
  margin-bottom: 1.1rem;
  padding: .8rem 1rem;
  border: 1px solid var(--border);
  border-left: 4px solid var(--idle);
  border-radius: var(--radius-sm);
  background: var(--surface);
}
.plan-banner svg {
  width: 22px; height: 22px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.8;
  stroke-linecap: round; stroke-linejoin: round;
}
.plan-banner div { flex: 1; min-width: 0; font-size: .88rem; }
.plan-banner strong { display: block; }
.plan-banner span { color: var(--text-muted); }
.plan-banner.warn {
  border-color: color-mix(in srgb, var(--warn) 30%, transparent); border-left-color: var(--warn);
  background: color-mix(in srgb, var(--warn) 7%, var(--surface));
}
.plan-banner.warn svg, .plan-banner.warn strong { color: var(--warn); }
.plan-banner.danger {
  border-color: color-mix(in srgb, var(--danger) 30%, transparent); border-left-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface));
}
.plan-banner.danger svg, .plan-banner.danger strong { color: var(--danger); }
@media (max-width: 620px) { .plan-banner { flex-wrap: wrap; } }

/* The quiet version, beside the protection switch: a trial with time left. */
.plan-pill { white-space: nowrap; }
.plan-pill:hover { text-decoration: none; filter: brightness(.96); }

.plan-offer.is-featured { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand), var(--shadow); }
.plan-offer-head { display: flex; align-items: center; gap: .6rem; }
.plan-offer-head h3 { margin: 0; font-size: 1.05rem; }
.plan-price {
  margin: .55rem 0 .3rem;
  font-family: var(--font-head); font-size: 2.1rem; font-weight: 800; line-height: 1.1;
}
.plan-price span { font-family: var(--font-body); font-size: .95rem; font-weight: 500; color: var(--text-muted); }
.plan-note { margin: .8rem 0 0; }
.plan-facts { margin: 0; padding-left: 1.1rem; display: grid; gap: .5rem; font-size: .92rem; }
/* A payment taken back stays in the ledger, visibly spent. */
.table tr.is-voided td { color: var(--text-faint); }

/* --- support threads ------------------------------------------------------ */

.thread { list-style: none; margin: 0; padding: .3rem 0; }
.msg { padding: .95rem 1.15rem; border-bottom: 1px solid var(--border); }
.msg:last-child { border-bottom: none; }
.msg-head {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  flex-wrap: wrap;
  margin-bottom: .3rem;
  font-size: .82rem;
  color: var(--text-faint);
}
.msg-author { font-weight: 600; font-size: .9rem; color: var(--text); }
/* An operator's reply is tinted so a customer can tell at a glance which lines
   are ours, without reading a name on every one. */
.msg.operator { background: color-mix(in srgb, var(--brand) 4%, transparent); }
.msg.operator .msg-author { color: var(--brand); }
/* A system line records a close or a reopen. It is a note about the
   conversation rather than part of it, so it is set apart from both sides. */
.msg.system {
  padding: .5rem 1.15rem;
  text-align: center;
  font-size: .82rem;
  color: var(--text-faint);
  background: var(--surface-2);
}
/* Whitespace is the only formatting a message body carries: it is rendered as
   text, never as markup, because it comes from whoever typed it. */
.msg-body { white-space: pre-wrap; overflow-wrap: anywhere; }

/* inline-block, not block: the unread dot sits beside the subject and a block
   would push it onto a line of its own. */
.ticket-subject {
  display: inline-block;
  vertical-align: middle;
  max-width: 30ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}
/* Subject cell: the unread dot and the subject are one row, so a subject long
   enough to be clipped cannot push the dot onto a line of its own. */
.ticket-link { display: flex; align-items: center; min-width: 0; }

/* The dot that says a row has something in it you have not read. */
.unread-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--brand);
  margin-right: .45rem;
  vertical-align: middle;
  flex: none;
}

/* --- the public demo ------------------------------------------------------ */

/* The strip that says so, inside the sticky header so it never scrolls out of
   view. It is a band of its own across the header's top edge, and the header
   grows by its height: --topbar-h is what the sidebar sticks below, so it is
   told. The strip's height is fixed — its sentence is cut short rather than
   wrapped — so the number below stays true at every width. */
body[data-demo] { --topbar-h: 98px; }
.topbar.has-demo { flex-wrap: wrap; row-gap: .7rem; }
.demo-strip {
  flex: 1 0 100%;
  display: flex; align-items: center; gap: .75rem;
  height: 40px; margin: -.7rem -1.5rem 0; padding: 0 1.5rem;
  background: linear-gradient(90deg, var(--navy), #22106b);
  color: #fff; font-size: .85rem;
}
.demo-tag {
  flex: none; padding: .12rem .55rem; border-radius: 999px;
  background: var(--accent); color: #fff;
  font-size: .7rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
}
.demo-text {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: rgba(255, 255, 255, .86);
}
.demo-cta { flex: none; padding: .22rem .7rem; }
@media (max-width: 760px) {
  .demo-text { display: none; }
  .demo-strip { justify-content: space-between; }
}
/* On a phone the header cannot hold the logo, the guide and "Leave demo" on
   one line, and a third line of sticky header is a third of the screen. The
   menu says "Leave demo" too, so the header's copy is the one that goes. */
@media (max-width: 620px) {
  .topbar.has-demo .account { display: none; }
}

/* The way into the demo from the front page, under the trial it leads to. */
.demo-link { margin-top: .6rem; padding: .65rem; }
.demo-aside { margin: 1rem 0 0; font-size: .85rem; color: var(--text-muted); }

/* A change refused because this is the demo. It is not an error, so it does
   not wear one's red: it is the demo explaining itself, with the trial
   beside it. */
.toast.demo { border-left-color: var(--accent); }
.toast.demo a { display: inline-block; margin-top: .35rem; font-weight: 600; }
