:root {
    --bg: #192438;
    --panel: #202e47;
    --card: #2a3a56;
    --border: #3c4e6d;
    --text: #eef2f8;
    --muted: #9fb0c9;
    --accent: #08944e;
    --link: #69f0ae;
    --warn: #ffaa00;
    --danger: #ff6b6b;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    /* Spacing scale. Structural rhythm comes from these rather than ad-hoc
       values, so vertical spacing stays consistent between sections. */
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;
    --sp-7: 3rem;
    --radius: 8px;
    --container: 1180px;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    padding: var(--sp-6) var(--sp-4) var(--sp-7);
    background: var(--bg);
    color: var(--text);
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    line-height: 1.5;
}

header, main, footer { max-width: var(--container); margin: 0 auto; }

h1 {
    margin: 0;
    font-family: var(--mono);
    font-size: clamp(1.6rem, 4vw, 2.4rem);
    color: var(--link);
}

.tagline { margin: var(--sp-1) 0 0; font-size: 1.05rem; }
.sub { margin: var(--sp-1) 0 var(--sp-5); color: var(--muted); font-size: 0.9rem; font-family: var(--mono); }

.controls {
    /* Flex, not grid: the search field wants to be wider than a select, and a
       grid either clips the sixth control or orphans it onto its own row.
       Flex lets the selects size to their content and the search absorb slack. */
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}

.controls input[type="search"] {
    flex: 3 1 15rem;
}

.controls select {
    flex: 1 1 10.5rem;
    min-width: 0;
    /* Reserve space for the native dropdown arrow; without it the longer
       labels ("Self-hosted only", "Any safe harbour") run under the chevron. */
    padding-right: 1.9rem;
}

@media (max-width: 560px) {
    .controls input[type="search"] { flex: 1 1 100%; }
    .controls select { flex: 1 1 calc(50% - 0.3rem); }
}

input, select {
    padding: 0.55rem 0.6rem;
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 5px;
    font-family: var(--mono);
    font-size: 0.9rem;
}

input:focus, select:focus { outline: 2px solid var(--warn); outline-offset: 1px; }

.count { color: var(--muted); font-family: var(--mono); font-size: 0.85rem; margin: 0; }

/* Count and paging control share one region, so the gap above the table is a
   single consistent step rather than two stacked margins. */
.results-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-4);
    margin-bottom: var(--sp-3);
}

.table-wrap {
    overflow-x: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
}

table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
    /* Fixed layout so the declared proportions hold instead of one column
       taking every spare pixel. */
    table-layout: fixed;
}

.c-domain  { width: 20%; }
.c-country { width: 7%; }
.c-contact { width: 30%; }
.c-reward  { width: 12%; }
.c-harbour { width: 13%; }
.c-status  { width: 10%; }
.c-policy  { width: 8%; }

/* Fixed layout means cells clip rather than expand, so every cell needs to
   degrade gracefully. JS already truncates the text; this is the backstop. */
th, td { overflow: hidden; text-overflow: ellipsis; }

th, td {
    text-align: left;
    padding: 0.4rem 0.7rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
    white-space: nowrap;
    line-height: 1.35;
}

/* Zebra striping earns its keep at this width — it keeps the eye on one row
   while crossing seven columns. */
tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.018); }

/* Status-like columns read faster centred under their headings. */
td:nth-child(4), td:nth-child(5), td:nth-child(6),
th:nth-child(4), th:nth-child(5), th:nth-child(6) { text-align: center; }

th:last-child, td:last-child { text-align: center; }

th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--card);
    box-shadow: inset 0 -1px 0 var(--border);
    font-family: var(--mono);
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
}

tbody tr:hover { background: rgba(105, 240, 174, 0.07); }
td.cc, td.contact { font-family: var(--mono); }
a { color: var(--link); text-decoration: none; }
a:hover { text-decoration: underline; }

.pill {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    font-family: var(--mono);
    font-size: 0.74rem;
    border: 1px solid transparent;
}

.pill.pay    { color: #062; background: var(--link); }
.pill.rec    { color: var(--warn); border-color: rgba(255, 170, 0, 0.4); }
.pill.none   { color: var(--muted); border-color: var(--border); }
.pill.unk    { color: var(--muted); border-color: transparent; }
.pill.safe   { color: #062; background: var(--link); }
.pill.nosafe { color: var(--danger); border-color: rgba(255, 107, 107, 0.4); }

.notes { margin-top: var(--sp-7); color: var(--muted); font-size: 0.9rem; }
.notes h2 { font-family: var(--mono); font-size: 1rem; color: var(--text); }
.notes li { margin-bottom: 0.45rem; }

footer { margin-top: var(--sp-7); color: var(--muted); font-size: 0.85rem; font-family: var(--mono); }

/* Paging control, welded to the bottom edge of the table.

   It used to be a solid green pill *above* the table, which asked the visitor
   to "show all" before they had seen a single row, and competed with the reward
   and status pills for attention. As the table's closing row it reads as "the
   list ends here, continue" instead of as a call to action. */

.more-wrap {
    position: relative;
    display: block;
    margin: -1px 0 0;
}

.more-wrap button {
    width: 100%;
    padding: 0.7rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 var(--radius) var(--radius);
    color: var(--muted);
    font-family: var(--mono);
    font-size: 0.82rem;
    cursor: pointer;
    box-shadow: none;
}

.more-wrap button:hover {
    background: var(--card);
    color: var(--link);
    box-shadow: none;
}

.more-note {
    position: absolute;
    right: var(--sp-4);
    line-height: 2.6;
    pointer-events: none;
    font-family: var(--mono);
    font-size: 0.74rem;
    color: var(--muted);
}

@media (max-width: 560px) {
    /* No room for the note beside the label at this width. */
    .more-note { display: none; }
}

/* Header stats: discrete chips rather than a run-on sentence, so the numbers
   can be read at a glance. Safe harbour is highlighted because it is the field
   no comparable dataset publishes. */
.sub {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem 0.55rem;
}

.stat {
    display: inline-flex;
    align-items: baseline;
    gap: 0.32rem;
    padding: 0.12rem 0.55rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--panel);
    font-size: 0.78rem;
    color: var(--muted);
    white-space: nowrap;
}

.stat b {
    font-size: 0.9rem;
    color: var(--text);
    font-weight: 600;
}

.stat.pay  b { color: var(--link); }
.stat.safe { border-color: rgba(105, 240, 174, 0.35); }
.stat.safe b { color: var(--link); }

.sub .meta {
    font-size: 0.78rem;
    color: var(--muted);
}

/* On a phone the table is what people came for, so the timing line drops
   rather than pushing the data further down the page. */
@media (max-width: 560px) {
    .sub .meta { display: none; }
    header h1 { font-size: 1.7rem; }
}

/* ===================================================================
   Shared visual language across the two properties, so they
   properties read as one product. Ported from that project's effects.css:
   the same glow hues, particle layer, gradient title and module glow.
   =================================================================== */

:root {
    --glow-green: 8, 148, 78;
    --glow-mint: 105, 240, 174;
    --glow-teal: 13, 143, 138;
    --glow-blue: 42, 111, 174;
}

/* Bottom glow. Two slow cycles at different periods: the hue drifts across the
   green-teal-blue range while the whole layer breathes. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -2;
    pointer-events: none;
    background:
        radial-gradient(130% 60% at 50% 100%,
            rgba(var(--glow-green), 0.34) 0%,
            rgba(var(--glow-teal), 0.14) 34%,
            rgba(var(--glow-blue), 0.06) 56%,
            transparent 76%);
    animation: glowBreathe 16s ease-in-out infinite,
               glowShift 26s ease-in-out infinite;
}

@keyframes glowBreathe {
    0%, 100% { opacity: 0.8; transform: translateY(6px) scale(1); }
    50%      { opacity: 1;   transform: translateY(-10px) scale(1.04); }
}

@keyframes glowShift {
    0%, 100% { filter: hue-rotate(0deg)   saturate(1); }
    33%      { filter: hue-rotate(-26deg) saturate(1.12); }
    66%      { filter: hue-rotate(14deg)  saturate(0.94); }
}

/* A second, dimmer pool keeps the very bottom edge from banding. */
body::after {
    content: '';
    position: fixed;
    left: 50%;
    bottom: -22vh;
    width: 84vw;
    height: 44vh;
    z-index: -2;
    pointer-events: none;
    transform: translateX(-50%);
    background: radial-gradient(closest-side,
        rgba(var(--glow-mint), 0.10), transparent 72%);
    filter: blur(28px);
    animation: glowShift 26s ease-in-out infinite reverse;
}

.fx-particles {
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    overflow: hidden;
}

.fx-dot {
    position: absolute;
    bottom: -14px;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: rgba(var(--glow-mint), 0.9);
    box-shadow: 0 0 6px rgba(var(--glow-mint), 0.5);
    opacity: 0;
    will-change: transform, opacity;
    animation: dotRise linear infinite;
}

@keyframes dotRise {
    0%   { transform: translateY(0) scale(0.55); opacity: 0; }
    14%  { opacity: 0.5; }
    60%  { opacity: 0.26; }
    100% { transform: translateY(-56vh) scale(1); opacity: 0; }
}

/* Header: logo beside a gradient-filled title. The gradient sits on the span
   rather than the h1, so the logo is not clipped by background-clip: text. */
header h1 {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
    font-size: clamp(27px, 5.4vw, 42px);
    font-weight: 800;
    letter-spacing: -0.028em;
    font-family: var(--mono);
}

.title {
    background: linear-gradient(102deg,
        #ffffff 0%,
        #d8f5e6 34%,
        rgba(var(--glow-mint), 1) 68%,
        rgba(var(--glow-green), 1) 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
}

.logo {
    flex: none;
    filter: drop-shadow(0 0 10px rgba(var(--glow-green), 0.45));
}

.tagline {
    color: #93a6c0;
    letter-spacing: 0.01em;
}

/* Module glow lifts the panels off the background wash. */
.controls,
.table-wrap {
    box-shadow:
        0 1px 2px rgba(0, 0, 0, 0.30),
        0 12px 34px rgba(0, 0, 0, 0.26),
        0 0 26px -8px rgba(var(--glow-green), 0.30);
    transition: box-shadow 260ms ease, border-color 260ms ease;
}

.controls {
    padding: 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
}

.table-wrap,
.controls { animation: riseIn 420ms cubic-bezier(0.22, 0.8, 0.3, 1) backwards; }

@keyframes riseIn {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: translateY(0); }
}

.more-wrap button { box-shadow: 0 4px 14px -3px rgba(var(--glow-green), 0.55); }
.more-wrap button:hover { box-shadow: 0 6px 20px -3px rgba(255, 170, 0, 0.5); }

@media (prefers-reduced-motion: reduce) {
    body::before,
    body::after,
    .fx-dot,
    .table-wrap,
    .controls { animation: none !important; }

    .fx-particles { display: none !important; }
}

/* Swag sits between money and recognition, so it gets its own colour rather
   than borrowing either one's. */
.pill.swag {
    color: var(--accent-hover);
    border-color: rgba(255, 170, 0, 0.45);
    background: rgba(255, 170, 0, 0.08);
}

.stat.swagstat { border-color: rgba(255, 170, 0, 0.35); }
.stat.swagstat b { color: var(--accent-hover); }

/* ===================================================================
   Background depth. Five fixed layers, none of which may compete with
   the table — this is a working surface, so the backdrop recedes.

     html::before   technical grid, fading out with distance
     html::after    vignette + film grain
     body::before   bottom glow          (defined above)
     body::after    secondary glow pool  (defined above)
     .fx-particles  rising motes         (defined above)

   The grain matters more than it sounds: large flat gradients band badly
   on 8-bit displays, and a little noise dissolves the steps.
   =================================================================== */

html::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -4;
    pointer-events: none;
    background-image:
        linear-gradient(rgba(105, 240, 174, 0.045) 1px, transparent 1px),
        linear-gradient(90deg, rgba(105, 240, 174, 0.045) 1px, transparent 1px);
    background-size: 46px 46px, 46px 46px;
    /* Strongest near the top where the header sits, gone by mid-page, so it
       never reads as lines through the data. */
    -webkit-mask-image: radial-gradient(120% 80% at 50% -10%, #000 0%, transparent 62%);
    mask-image: radial-gradient(120% 80% at 50% -10%, #000 0%, transparent 62%);
}

html::after {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -3;
    pointer-events: none;
    /* Vignette only: darker at the edges, which pushes attention to the centre
       column and stops the glow reading as a flat wash. */
    background: radial-gradient(130% 110% at 50% 40%,
        transparent 0%,
        rgba(9, 14, 24, 0.30) 74%,
        rgba(5, 9, 16, 0.70) 100%);
}

/* Film grain, kept as its own layer at very low opacity. An earlier attempt
   blended noise with `overlay`, which on a dark theme lightens rather than
   textures — it turned the navy grey. Grain has to be barely perceptible: its
   only job is to dissolve the banding steps in large flat gradients. */
.fx-particles::after {
    content: '';
    position: fixed;
    inset: 0;
    pointer-events: none;
    opacity: 0.035;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
    background-size: 140px 140px;
}

/* The glow gains a third, wider and slower pool so the light has depth
   rather than a single obvious hotspot. */
body {
    background-image:
        radial-gradient(70% 45% at 18% 8%, rgba(42, 111, 174, 0.13), transparent 70%),
        radial-gradient(60% 40% at 88% 4%, rgba(13, 143, 138, 0.10), transparent 68%);
    background-attachment: fixed;
}

@media (prefers-reduced-motion: reduce) {
    html::before, html::after { animation: none !important; }
}

/* Skip link: standard on the portfolio, and the results table is the thing a
   keyboard user actually wants to reach. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    padding: var(--sp-2) var(--sp-4);
    background: var(--link);
    color: #062;
    font-family: var(--mono);
    font-weight: 700;
    text-decoration: none;
    border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus { left: 0; }

/* ===================== Header arrangement ==========================
   Brand block left, live status right, stat chips spanning below. The
   status reads as a running indicator rather than another line of prose.
   =================================================================== */

.head-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-3) var(--sp-5);
}

.brand { min-width: 0; }

.brand h1 { margin-bottom: var(--sp-1); }

/* Freshness reads as plain text, not a chip. The pill outline made it look like
   one more filter chip in the row beside it, when it is a timestamp — the dot
   already carries the "live" signal without a border around the whole thing. */
.live {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    margin: var(--sp-2) 0 0;
    padding: 0;
    border: 0;
    background: none;
    font-family: var(--mono);
    font-size: 0.78rem;
    color: var(--muted);
    white-space: nowrap;
}

/* Pulsing dot: the crawl really is running on a schedule, so a live
   indicator is accurate rather than decorative. */
.live-dot {
    position: relative;
    flex: none;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--link);
    box-shadow: 0 0 7px rgba(var(--glow-mint), 0.85);
}

.live-dot::after {
    content: '';
    position: absolute;
    inset: -3px;
    border-radius: 50%;
    border: 1px solid rgba(var(--glow-mint), 0.55);
    animation: livePulse 2.4s ease-out infinite;
}

@keyframes livePulse {
    0%   { transform: scale(0.6); opacity: 0.9; }
    70%  { transform: scale(1.9); opacity: 0; }
    100% { transform: scale(1.9); opacity: 0; }
}

@media (max-width: 640px) {
    .head-row { flex-direction: column; align-items: flex-start; }
    .live { margin-top: var(--sp-1); }
}

@media (prefers-reduced-motion: reduce) {
    .live-dot::after { animation: none; opacity: 0.5; transform: scale(1); }
}

/* ============== Header hierarchy and stat spacing ==================
   The tagline sat 4px under a 42px/800 title with no breathing room, so
   the two read as one block rather than heading + subordinate line.
   The chips were packed at a 6px gap, which made the numbers run
   together instead of reading as distinct figures.
   =================================================================== */

.brand h1 {
    margin-bottom: 0;
    line-height: 1.1;
}

.tagline {
    margin: var(--sp-2) 0 0;
    font-size: 0.94rem;
    font-weight: 400;
    line-height: 1.45;
    letter-spacing: 0.012em;
    color: #8fa3bd;
}

/* Chips get room to breathe, and separation from the brand block above. */
.sub {
    gap: var(--sp-2) var(--sp-3);
    margin: var(--sp-5) 0 var(--sp-5);
}

.stat {
    gap: 0.42rem;
    padding: 0.3rem 0.7rem;
    font-size: 0.8rem;
    line-height: 1.25;
}

.stat b {
    font-size: 0.95rem;
    letter-spacing: -0.01em;
}

@media (max-width: 640px) {
    /* Two clean columns rather than a ragged wrap. */
    .sub {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--sp-2);
        margin: var(--sp-4) 0;
    }

    .stat { justify-content: flex-start; }
}

/* =============== Brand lockup: logo beside stacked text ============
   The logo used to live inside the h1, so the h1's left edge was the
   logo and the tagline — a sibling — started further left, at the
   container edge. Aligning them by hand would mean hardcoding the logo
   width; making the text a column beside the logo means the two lines
   share a left edge whatever size the logo is.
   =================================================================== */

.brand {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    min-width: 0;
}

.brand .logo {
    flex: none;
    /* Optically centred against the cap-height of the title rather than
       its full line box, which sits a touch low. */
    margin-top: 0.32em;
}

.brand-text { min-width: 0; }

.brand-text h1 {
    display: block;
    margin: 0;
    line-height: 1.05;
    font-size: clamp(27px, 5.4vw, 42px);
    font-weight: 800;
    letter-spacing: -0.028em;
    font-family: var(--mono);
}

.brand-text .tagline {
    margin: var(--sp-2) 0 0;
}

/* ======================= Site nav + content pages ================== */

.site-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    max-width: var(--container);
    margin: 0 auto var(--sp-5);
    padding-bottom: var(--sp-2);
    border-bottom: 1px solid var(--border);
}

.site-nav a {
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius) var(--radius) 0 0;
    font-family: var(--mono);
    font-size: 0.85rem;
    color: var(--muted);
    text-decoration: none;
}

.site-nav a:hover { color: var(--text); background: rgba(255, 255, 255, 0.04); }

.site-nav a[aria-current="page"] {
    color: var(--link);
    background: var(--panel);
    box-shadow: inset 0 -2px 0 var(--link);
}

/* Field-reference table: prose, so it wraps rather than clipping like the
   fixed-layout results table. */
.doc-table { table-layout: auto; font-size: 0.84rem; }
.doc-table th, .doc-table td { white-space: normal; vertical-align: top; }
.doc-table td:first-child { white-space: nowrap; }

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

.chart { display: grid; gap: var(--sp-1); }

.bar-row {
    display: grid;
    grid-template-columns: 9.5rem 1fr 7.5rem;
    align-items: center;
    gap: var(--sp-3);
    font-family: var(--mono);
    font-size: 0.8rem;
}

.bar-label { color: var(--muted); text-align: right; }

.bar-track {
    height: 16px;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.05);
    overflow: hidden;
}

.bar-fill {
    display: block;
    height: 100%;
    border-radius: 3px;
    background: var(--accent);
    transition: width 400ms cubic-bezier(0.22, 0.8, 0.3, 1);
}

.bar-fill.pay  { background: linear-gradient(90deg, var(--accent), var(--link)); }
.bar-fill.swag { background: linear-gradient(90deg, #b06f00, var(--accent-hover)); }
.bar-fill.rec  { background: rgba(255, 170, 0, 0.45); }
.bar-fill.bad  { background: rgba(255, 107, 107, 0.55); }
.bar-fill.none { background: rgba(159, 176, 201, 0.28); }
.bar-fill.unk  { background: rgba(159, 176, 201, 0.18); }

.bar-value { color: var(--text); font-size: 0.78rem; }

@media (max-width: 560px) {
    .bar-row { grid-template-columns: 7rem 1fr; }
    .bar-value { grid-column: 2; text-align: right; font-size: 0.72rem; }
}

/* The results table centres its pill columns; that must not leak into the
   field-reference table, whose last column is prose. */
.doc-table th,
.doc-table td,
.doc-table th:last-child,
.doc-table td:last-child,
.doc-table td:nth-child(4),
.doc-table td:nth-child(5),
.doc-table td:nth-child(6) {
    text-align: left;
}

.doc-table td:nth-child(2) { color: var(--muted); }

/* ================= Navigation: N3, on the brand row =================
   Uppercase micro-caps with thin dividers, sitting on the same row as the
   brand. Chosen over bordered or pill treatments because the header already
   carries several pill shapes — another container competes with the stats.
   =================================================================== */

.head-row { align-items: center; }

.site-nav {
    margin: 0;
    padding: 0;
    gap: 0;
    border: 0;
    align-items: baseline;
}

.site-nav a {
    padding: 0 var(--sp-3);
    border-radius: 0;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    color: var(--muted);
}

.site-nav a + a { border-left: 1px solid var(--border); }

/* These read as plain grey text at rest, and the only hover feedback was
   #9fb0c9 -> #eef2f8 — two light greys, on items with no underline. Nothing
   said "link".

   Hover and keyboard focus now change colour *and* gain an underline, so there
   are two signals rather than a subtle one. The underline sits below the
   baseline via an offset and does not affect layout, so nothing shifts.
   `currentColor` keeps it in step with whatever the text is doing. */
.site-nav a:hover,
.site-nav a:focus-visible {
    background: none;
    color: var(--text);
    text-decoration: underline;
    text-decoration-color: var(--link);
    text-decoration-thickness: 1px;
    text-underline-offset: 5px;
}

/* The current page keeps the accent to itself and is bolder, so it stays
   distinguishable from a link being hovered. It is not underlined: you are
   already here. */
.site-nav a[aria-current="page"] {
    background: none;
    color: var(--link);
    box-shadow: none;
    font-weight: 700;
    text-decoration: none;
}

@media (max-width: 640px) {
    .site-nav { margin-top: var(--sp-2); }
    .site-nav a:first-child { padding-left: 0; }
}

/* ---- stat chips: smaller, with the status line directly beneath ---- */

.sub {
    gap: var(--sp-2) var(--sp-2);
    margin: var(--sp-4) 0 var(--sp-2);
}

.stat {
    gap: 0.34rem;
    padding: 0.2rem 0.58rem;
    font-size: 0.72rem;
    line-height: 1.2;
}

.stat b { font-size: 0.82rem; }

/* Chips and status share one row: metrics left, timing right, both on the
   same baseline. Sitting them on separate lines made the status read as a
   second-class caption; on one row they are peers. */
.meta-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2) var(--sp-4);
    margin: var(--sp-6) 0 var(--sp-5);
}

.meta-row .sub { margin: 0; }

.live {
    display: inline-flex;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 0.74rem;
}

@media (max-width: 700px) {
    /* Stacked, so the status still reads left-aligned under the chips. */
    .meta-row { justify-content: flex-start; }
}

/* ================= Content pages: cards, KPIs, rows ================ */

.lede {
    max-width: 62ch;
    margin: 0 0 var(--sp-5);
    color: var(--muted);
    font-size: 0.95rem;
}

.card {
    max-width: 860px;
    margin: 0 0 var(--sp-4);
    padding: var(--sp-5);
    border: 1px solid var(--border);
    border-radius: 10px;
    background: rgba(32, 46, 71, 0.5);
}

.card h2 {
    margin: 0 0 var(--sp-3);
    font-size: 0.98rem;
    font-weight: 700;
    color: var(--text);
}

.card p { margin: 0 0 var(--sp-3); color: var(--muted); font-size: 0.9rem; }
.card p:last-child { margin-bottom: 0; }
.card code { font-family: var(--mono); font-size: 0.85em; color: var(--link); }

/* Definition rows: the chip itself is the term, so the reader matches what they
   saw in the table to its explanation without a second naming of it. Two
   columns above 620px, stacked below, where a fixed term column would squeeze
   the text to a few words per line. */
.defs {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--sp-3) var(--sp-4);
    margin: 0;
    align-items: baseline;
}

.defs dt { margin: 0; }
.defs dd { margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.55; }
.defs dd b { color: var(--text); font-weight: 600; }

@media (max-width: 620px) {
    .defs { grid-template-columns: 1fr; gap: var(--sp-1); }
    .defs dd { margin-bottom: var(--sp-3); }
    .defs dd:last-child { margin-bottom: 0; }
}

/* The default value shown under a control's name: identifies the dropdown by
   what the reader actually sees on the page, without competing with the name. */
.dim {
    color: var(--muted);
    font-family: var(--mono);
    font-size: 0.78rem;
}

/* A trailing caveat inside a card: quieter than body text, since it explains a
   decision rather than telling you how to use the thing. */
.note {
    margin: var(--sp-3) 0 0;
    color: var(--muted);
    font-size: 0.82rem;
    line-height: 1.55;
}

ul.tight, ol.tight { margin: 0 0 var(--sp-3); padding-left: 1.25rem; }
ul.tight li, ol.tight li { margin-bottom: var(--sp-2); color: var(--muted); font-size: 0.9rem; }
ul.tight li:last-child, ol.tight li:last-child { margin-bottom: 0; }

.card pre {
    margin: 0;
    padding: var(--sp-3);
    border: 1px solid var(--border);
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.25);
    overflow-x: auto;
    font-size: 0.78rem;
}

.card pre code { color: var(--text); }

/* ---- contact: icon rows ------------------------------------------- */

.link-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3);
    margin-bottom: var(--sp-2);
    border: 1px solid var(--border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.18);
    text-decoration: none;
    transition: border-color 160ms ease, background 160ms ease;
}

.link-row:last-child { margin-bottom: 0; }
.link-row:hover { border-color: var(--link); background: rgba(105, 240, 174, 0.05); text-decoration: none; }

.link-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 34px;
    height: 34px;
    border-radius: 7px;
    background: var(--card);
    color: var(--text);
}

.link-icon svg { width: 17px; height: 17px; }
.link-icon.x { color: #fff; }
.link-icon.gh { color: #e8eef7; }

.link-text { color: var(--text); font-size: 0.9rem; }

/* ---- stats: headline figures -------------------------------------- */

.kpi-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: var(--sp-3);
    margin-bottom: var(--sp-6);
}

.kpi {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: var(--sp-4);
    border: 1px solid var(--border);
    border-radius: 10px;
    background: rgba(32, 46, 71, 0.5);
}

.kpi-num {
    font-family: var(--mono);
    font-size: 1.85rem;
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.02em;
    color: var(--text);
}

.kpi-label {
    font-size: 0.76rem;
    color: var(--muted);
    letter-spacing: 0.02em;
}

.kpi.pay .kpi-num { color: var(--link); }
.kpi.swag .kpi-num { color: var(--accent-hover); }
.kpi.bad .kpi-num { color: var(--danger); }

.chart-block { max-width: 860px; margin-bottom: var(--sp-6); }
.chart-block h2 { font-family: var(--mono); font-size: 0.95rem; margin: 0 0 var(--sp-3); }

/* ================= Heading section spacing =========================
   The header carried no bottom margin, so stats/docs/contact had a
   measured gap of 0 to their first content block; index only looked
   right because .meta-row happened to add its own top margin. One token
   on the header fixes every page, and .meta-row no longer doubles it.

   The lockup text also sat 48px right of the content, because the logo
   pushed the text column across. Above 900px the logo hangs into the
   left margin instead, so title, tagline and content share one left
   edge. Below that there is no margin to hang into, so it reverts.
   =================================================================== */

header {
    margin-bottom: var(--sp-6);
}

/* The chips row describes the table, not the title, so its spacing has to
   group it downwards: a full gap above detaches it from the lockup, and the
   header's own bottom gap shrinks so the row reads as the top of the filter
   block rather than the last line of the heading. Before this it sat 10px
   under the tagline with 56px of air beneath it, which grouped it the wrong
   way round.

   :has() scopes the shrink to the one page that carries a .meta-row; the
   content pages keep the full sp-6 below their heading. Without :has()
   support the row simply keeps the old 32px gap below — cosmetic, not
   broken. */
.meta-row {
    margin: var(--sp-6) 0 0;
}

header:has(.meta-row) {
    margin-bottom: var(--sp-3);
}

/* The hang needs real margin to hang into. The container is 1180px centred,
   so only above ~1290px is there room for a 48px offset — at 1024px the logo
   was rendering at x=-32 and being clipped. Below the threshold the lockup
   keeps its indent, which is cosmetic rather than broken. */
@media (min-width: 1290px) {
    .brand { margin-left: calc(-1 * (36px + var(--sp-3))); }
}

/* Content pages: the tagline already introduces the page, so a lede
   directly beneath it repeated the same idea at a different indent.
   Docs keeps one because it carries information the tagline does not. */
.lede {
    margin-top: 0;
    margin-bottom: var(--sp-5);
}

/* Plain text, not a chip. This rule used to give it a pill outline to match the
   five figures beside it, but the figures are facts about the dataset and this
   is a timestamp — enclosing it implied it was a sixth one of them. The pulsing
   dot already says "live" without a border doing it again.

   This is the last of three .live rules in the file and the one that wins; the
   earlier two are already borderless. */
.live {
    display: inline-flex;
    align-items: center;
    gap: 0.42rem;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-family: var(--mono);
    font-size: 0.72rem;
    line-height: 1.2;
    color: var(--muted);
    white-space: nowrap;
}

.live .live-dot { width: 6px; height: 6px; }

/* Companion-tool card: accented on the leading edge so it reads as a pointer
   elsewhere rather than another reference section. */
.card.promo {
    border-left: 3px solid var(--accent);
    background: rgba(8, 148, 78, 0.07);
}

.link-icon.tool { color: var(--link); background: rgba(105, 240, 174, 0.1); }

/* A program found since the dataset was first assembled.

   The dot sits inside the cell's existing left padding rather than in the text
   flow, so it costs the domain no width — a badge would have pushed longer
   domains further into truncation on a column that is only 20% wide. */
td.dom { position: relative; }

td.dom.is-new::before {
    content: '';
    position: absolute;
    /* The cell has 0.7rem (~11px) of left padding. A 6px dot at 4px left it a
       single pixel from the domain text, which read as attached to the word
       rather than as a marker in the gutter. 5px at 2px leaves ~4px of air. */
    left: 2px;
    top: 50%;
    width: 5px;
    height: 5px;
    margin-top: -2.5px;
    border-radius: 50%;
    background: var(--link);
    /* Static, unlike the pulsing freshness dot in the header — that one means
       "the crawl is live", this one means "this row is recent". Same colour, and
       the animation is what distinguishes them. */
    box-shadow: 0 0 4px rgba(var(--glow-mint), 0.55);
}

td.dom.is-new { cursor: help; }

/* A ::before is invisible to a screen reader, so the meaning is carried in text
   for anyone who cannot see the dot. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* An em dash standing for "nothing usable here". Carries a tooltip when there
   is a reason worth reading, and marks itself as hoverable only then. */
.none-mark { color: var(--muted); }
.none-mark[title] { cursor: help; }

/* A value known to be unusable. Retained for any remaining use, though dead
   policy URLs are now omitted rather than struck through. */
.dead {
    color: var(--muted);
    text-decoration: line-through;
    text-decoration-color: rgba(255, 107, 107, 0.55);
    cursor: help;
}

/* The grounds for a status, under its chip: the date the file expired, or the
   date it stopped being served. Deliberately quiet — it qualifies the chip
   rather than competing with it — and mono so dates align down the column. */
.why {
    display: block;
    margin-top: 3px;
    color: var(--muted);
    font-family: var(--mono);
    font-size: 0.66rem;
    letter-spacing: 0.01em;
    white-space: nowrap;
}

/* A chip carrying an explanation is worth hovering, so it should say so. */
.pill[title] { cursor: help; }

footer p { margin: 0 0 var(--sp-1); }
footer p:last-child { margin-bottom: 0; }

/* ==================== Sticky footer ================================
   Fill the viewport so the footer sits on the bottom edge when content is
   short — Contact and Stats are short enough to leave it floating mid-page
   otherwise — and follow the content when it is not.

   dvh accounts for mobile browser chrome; vh is the fallback for browsers
   that do not support it.
   =================================================================== */

body {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

/* Absorbs the slack, which is what pushes the footer down. */
main {
    flex: 1 0 auto;
}

footer {
    flex: none;
    margin-top: var(--sp-7);
    padding-top: var(--sp-4);
    border-top: 1px solid var(--border);
    /* Centred across the container, not left-aligned under the content. The
       footer is a closing line about the site rather than part of the page's
       column of information, so it should not share the content's left edge.
       Applies to every page, including the extra dataset line on Docs. */
    text-align: center;
    color: var(--muted);
    font-family: var(--mono);
    font-size: 0.76rem;
}

/* header/main/footer are flex items now, so `margin: 0 auto` no longer
   centres them — the flex container needs to do it. */
body > header,
body > main,
body > footer {
    width: 100%;
    max-width: var(--container);
    margin-left: auto;
    margin-right: auto;
}

/* ============ Centring the narrow content column ======================
   header, main and footer are already centred on the 1180px container. The
   blocks *inside* main are not: .card and .chart-block cap at 860px and were
   left-aligned, so on a wide screen every card sat against the left edge while
   the nav sat against the right — 320px of dead space between them, and a page
   that read as left-weighted even though its container was centred.

   Centring the blocks rather than narrowing the container leaves the header
   lockup and the nav exactly where they are, which is the point: only the body
   content moves.

   `margin-inline: auto` overrides just the horizontal margins, so each rule's
   own vertical spacing survives untouched.
   ===================================================================== */

.card,
.kpi-grid,
.chart-block,
main > .lede,
main > .table-wrap,
main > .controls,
main > .results-bar,
main > .more-wrap,
main > .notes {
    margin-inline: auto;
}

/* The lede is measure-limited at 62ch for readability, so centring the block
   would leave its text ragged against a centred card below it. Matching the
   card width keeps both left edges on one line. */
main > .lede { max-width: 860px; }

/* One content width per page, so nothing inside main steps in or out.
   Stats is built from a KPI row that needs the full container to fit seven
   figures, so its charts take that width too — capping them at 860 left a
   visible step between the row above and the bars below. Docs and Contact are
   prose, so they keep the narrower measure, and their lede matches. */
.kpi-grid, .chart-block { max-width: var(--container); }
body:has(.kpi-grid) main > .lede { max-width: var(--container); }

/* The one chip that is a control rather than a figure, so it has to look like
   one without shouting: accent border and text against the muted row, and a
   pointer. Its "on" state is filled, because a filter that is silently active is
   the kind of thing a visitor blames on the data. */
.newstat {
    color: var(--link);
    border-color: rgba(105, 240, 174, 0.45);
    text-decoration: none;
    cursor: pointer;
}

.newstat b { color: var(--link); }

.newstat:hover,
.newstat:focus-visible {
    background: rgba(105, 240, 174, 0.12);
    border-color: var(--link);
    text-decoration: none;
}

.newstat.is-on {
    background: var(--link);
    border-color: var(--link);
    color: #062;
}

.newstat.is-on b { color: #062; }


/* Explanatory line above a chart: quieter than the bars it describes. */
.chart-note {
    margin: 0 0 var(--sp-3);
    color: var(--muted);
    font-size: 0.82rem;
    line-height: 1.55;
}

/* ==================== Phones: the table becomes cards ==============
   Seven columns in 358px gave Domain 71px and Contact 107px, so every value
   was truncated past usefulness — `gazetakrakowska.pl` rendered as a stub.
   `table-layout: fixed` made it *look* fine because it clipped rather than
   overflowed, which is why a scroll-width check missed it.

   Below 640px each row becomes a block: the domain as a heading, then one
   labelled line per field. The labels come from data-label attributes, so
   there is a single row template rather than two code paths that can drift.
   =================================================================== */
@media (max-width: 640px) {
    #results, #results tbody, #results tr, #results td { display: block; width: auto; }

    /* The header row has nothing to label once the columns are gone. */
    #results thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

    .table-wrap { overflow-x: visible; border: 0; background: none; }

    #results tr {
        margin-bottom: var(--sp-3);
        padding: var(--sp-3);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--panel);
    }

    #results tbody tr:nth-child(even) { background: var(--panel); }
    #results tbody tr:hover { background: var(--panel); }

    #results td {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: var(--sp-3);
        padding: 0.3rem 0;
        border: 0;
        white-space: normal;          /* let long values wrap instead of clipping */
        overflow: visible;
        text-align: right;
        font-size: 0.88rem;
    }

    #results td::before {
        content: attr(data-label);
        flex: none;
        color: var(--muted);
        font-family: var(--mono);
        font-size: 0.72rem;
        text-transform: uppercase;
        letter-spacing: 0.05em;
    }

    /* The domain is the row's identity, so it leads rather than sitting in a
       labelled pair like the rest. */
    #results td.dom {
        display: block;
        margin-bottom: var(--sp-2);
        padding-bottom: var(--sp-2);
        border-bottom: 1px solid var(--border);
        text-align: left;
        font-size: 1rem;
    }

    #results td.dom::before { content: none; }
    #results td.dom.is-new::before { content: ''; }   /* keep the new dot */

    /* A flex item defaults to min-width:auto, so it refuses to shrink below its
       content and overflows the card instead. Long contacts did exactly that —
       `security@clawsecure.ai` ran past the right edge. Every value can now
       shrink, and the contact stacks under its label so an address has the full
       card width to wrap into rather than half of it. */
    #results td > *, #results td > a { min-width: 0; }

    #results td.contact {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--sp-1);
        text-align: left;
    }

    #results td.contact a,
    #results td.contact span { overflow-wrap: anywhere; }

    /* 44px is the accessibility floor for a touch target; these were 17px. */
    #results td.dom a,
    #results td.contact a { display: inline-block; padding: 0.35rem 0; }

}

/* Tap targets, keyed on the input device rather than the viewport width: an
   iPad is touch at 768px and a narrow desktop window is not touch at 500px.
   44px is the accessibility floor; these links were 17px. */
@media (hover: none) and (pointer: coarse) {
    .site-nav a {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    .controls select,
    .controls input[type="search"] { min-height: 44px; }

    #results td.dom a,
    #results td.contact a,
    #results td a,
    .newstat,
    .more-wrap button {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
    }

    /* The row is already a 44px-tall block on a phone, so the links inside it
       do not need to stack extra height on top. */
    #results td { padding-top: 0; padding-bottom: 0; }
}

