/* ===========================================================================
   Statistics — shared chrome and chart palette
   ===========================================================================

   Used by /statistics and /stats. Phase 5 moved this out of an inline <style>
   block in statistics.php so both pages read from one place and the charts on
   them look like one system rather than a collection of widgets.

   The palette below is not hand-picked. Every value comes from the project's
   dataviz reference palette and was run through its validator against this
   site's real surfaces — white cards in light mode — rather than eyeballed:

     donut segments   #2a78d6,#eb6834,#1baf7a   all-pairs, light on #ffffff
                      PASS band · PASS chroma · PASS CVD (worst ΔE 9.2)
                      PASS normal-vision (worst ΔE 24.0)
                      WARN contrast: aqua 2.82:1 — see "the relief rule" below

     heatmap bins     #86b6ef,#3987e5,#1c5cab,#0d366b   ordinal, light on #fff
                      PASS monotone L · PASS ΔL gaps · PASS light end 2.11:1
                      PASS single hue (spread 4°)

   THE RELIEF RULE. Slot 3 (aqua) sits below 3:1 on white. That is legal only
   because every chart here ships visible labels and a table view, so no value
   is reachable by colour alone. If you remove a chart's table view, you break
   the palette's validation, not just its convenience.

   Re-run before changing any colour below:
     node scripts/validate_palette.js "<hex,hex,…>" --mode light --surface "#ffffff"

   ---------------------------------------------------------------------------
   DARK MODE IS OPT-IN, DELIBERATELY — and there is no prefers-color-scheme
   block here on purpose.

   The site has no dark theme: every card is a hard white Bootstrap surface. An
   earlier draft of this file did define the dark tokens under
   @media (prefers-color-scheme: dark), and it was wrong in a way that only
   showed up on screenshotting it — a visitor whose OS prefers dark got the dark
   chart palette painted onto the light cards. Chart titles went white on white
   and the heatmap's "no sightings" step went near-black, so a quiet month read
   as the busiest thing on the page.

   The chart tokens must follow the SITE's theme, never the OS's. So the dark
   values live under [data-theme="dark"] only, ready for a real theme toggle,
   and nothing switches until the page itself does. The steps below were chosen
   for the dark surface and validated against it separately — not an automatic
   flip of the light values.
   =========================================================================== */

.viz {
   color-scheme: light;

   /* chart chrome */
   --viz-surface: #ffffff;
   --viz-ink: #0b0b0b;
   --viz-ink-2: #52514e;
   --viz-muted: #898781;
   --viz-grid: #e1e0d9;
   --viz-axis: #c3c2b7;

   /* categorical — identity. Assigned in fixed order, never cycled. */
   --viz-cat-1: #2a78d6;
   --viz-cat-2: #eb6834;
   --viz-cat-3: #1baf7a;
   --viz-cat-other: #c3c2b7;   /* the folded tail — a remainder, not an identity */

   /* sequential — magnitude. One hue, light to dark. */
   --viz-seq-0: #f0efec;       /* nothing happened: neutral, not a pale blue */
   --viz-seq-1: #86b6ef;
   --viz-seq-2: #3987e5;
   --viz-seq-3: #1c5cab;
   --viz-seq-4: #0d366b;
}

/* Only when the site says so — see the note at the top of this file. */
:root[data-theme="dark"] .viz {
   color-scheme: dark;

   --viz-surface: #1a1a19;
   --viz-ink: #ffffff;
   --viz-ink-2: #c3c2b7;
   --viz-muted: #898781;
   --viz-grid: #2c2c2a;
   --viz-axis: #383835;

   --viz-cat-1: #3987e5;
   --viz-cat-2: #d95926;
   --viz-cat-3: #199e70;
   --viz-cat-other: #383835;

   --viz-seq-0: #242423;
   --viz-seq-1: #cde2fb;
   --viz-seq-2: #86b6ef;
   --viz-seq-3: #3987e5;
   --viz-seq-4: #184f95;
}


/* ---------------------------------------------------------------------------
   Page and card chrome (carried over from statistics.php's inline block)
   --------------------------------------------------------------------------- */

.stats-page {
   padding-top: 5rem;
   padding-bottom: 5rem;
}

.stats-section + .stats-section {
   margin-top: 2.5rem;
}

.stats-card {
   border: 0;
   border-radius: 1rem;
   box-shadow: 0 1rem 2.5rem rgba(16, 24, 40, 0.08);
   height: 100%;
}

.stats-card .card-body {
   padding: 1.5rem;
}

.stats-label {
   color: #6c757d;
   font-size: 0.78rem;
   font-weight: 700;
   letter-spacing: 0.08em;
   margin-bottom: 0.75rem;
   text-transform: uppercase;
}

/* Proportional figures, deliberately: tabular-nums gives every digit the width
   of a zero, which makes a number like 121 look loose at display sizes. Tabular
   is for columns that must align — the table views below, and axis ticks. */
.stats-value {
   color: #1f2937;
   font-size: 2rem;
   font-weight: 700;
   line-height: 1.1;
   margin-bottom: 0.5rem;
}

.stats-hero {
   font-size: 3rem;
   font-weight: 700;
   line-height: 1;
}

.stats-meta {
   color: #6c757d;
   font-size: 0.92rem;
   margin-bottom: 0;
}

.stats-section-title {
   font-size: 1.5rem;
   font-weight: 700;
   margin-bottom: 1rem;
}


/* ---------------------------------------------------------------------------
   Charts
   --------------------------------------------------------------------------- */

.viz {
   position: relative;
}

/* Containers grow with their content. A fixed height that excludes the x-axis
   band gives the card a tiny nested scrollbar. */
.viz svg {
   display: block;
   width: 100%;
   height: auto;
   overflow: visible;
}

.viz-title {
   color: var(--viz-ink);
   font-size: 1rem;
   font-weight: 600;
   margin-bottom: 0.15rem;
}

.viz-subtitle {
   color: var(--viz-muted);
   font-size: 0.82rem;
   margin-bottom: 0.9rem;
}

/* Text wears text tokens, never the series colour. Identity comes from the
   coloured mark beside the label. */
.viz-axis-text {
   fill: var(--viz-muted);
   font-size: 11px;
   font-variant-numeric: tabular-nums;
}

.viz-value-text {
   fill: var(--viz-ink-2);
   font-size: 11px;
   font-weight: 600;
}

.viz-gridline {
   stroke: var(--viz-grid);
   stroke-width: 1;
   shape-rendering: crispEdges;
}

.viz-axisline {
   stroke: var(--viz-axis);
   stroke-width: 1;
   shape-rendering: crispEdges;
}

/* The hovered mark lifts, so the reader sees it respond. */
.viz-mark {
   transition: opacity 0.12s ease;
}

.viz-mark:hover,
.viz-mark:focus-visible {
   opacity: 0.78;
   outline: none;
}

.viz-hit {
   fill: transparent;
   cursor: default;
}

/* Legend — always present for two or more series. Mirrors the mark: a rect for
   bars and areas, a line for lines. */
.viz-legend {
   display: flex;
   flex-wrap: wrap;
   gap: 0.35rem 1rem;
   margin-top: 0.85rem;
   padding: 0;
   list-style: none;
   font-size: 0.85rem;
   color: var(--viz-ink-2);
}

.viz-legend li {
   display: flex;
   align-items: center;
   gap: 0.4rem;
}

.viz-legend .viz-key {
   width: 12px;
   height: 12px;
   border-radius: 3px;
   flex: 0 0 auto;
}

.viz-legend .viz-legend-value {
   color: var(--viz-muted);
   font-variant-numeric: tabular-nums;
}

/* Heatmap scale legend — a sequential encoding always carries its scale. */
.viz-scale {
   display: flex;
   align-items: center;
   gap: 0.35rem;
   margin-top: 0.75rem;
   font-size: 0.78rem;
   color: var(--viz-muted);
}

.viz-scale span[data-step] {
   width: 12px;
   height: 12px;
   border-radius: 2px;
}

/* The table-view twin. Every chart has one; it is how values stay reachable
   without hovering, and it is the relief channel the palette's contrast WARN
   depends on. */
.viz-table {
   margin-top: 0.9rem;
   font-size: 0.85rem;
}

.viz-table > summary {
   color: var(--viz-muted);
   cursor: pointer;
   font-size: 0.8rem;
}

.viz-table > summary:hover {
   color: var(--viz-ink-2);
}

.viz-table table {
   margin-top: 0.6rem;
   margin-bottom: 0;
   width: 100%;
   font-variant-numeric: tabular-nums;
}

.viz-table th,
.viz-table td {
   padding: 0.28rem 0.5rem;
   border-bottom: 1px solid var(--viz-grid);
}

.viz-table td:last-child,
.viz-table th:last-child {
   text-align: right;
}

/* Tooltip. Values lead, labels follow — the reader already has the series and
   wants the number. */
.viz-tooltip {
   position: absolute;
   z-index: 20;
   min-width: 6rem;
   padding: 0.45rem 0.6rem;
   border-radius: 0.5rem;
   background: var(--viz-ink);
   color: var(--viz-surface);
   font-size: 0.8rem;
   line-height: 1.35;
   pointer-events: none;
   opacity: 0;
   transform: translate(-50%, -115%);
   transition: opacity 0.1s ease;
   box-shadow: 0 0.5rem 1.5rem rgba(16, 24, 40, 0.22);
}

.viz-tooltip[data-show="1"] {
   opacity: 1;
}

.viz-tooltip .viz-tip-value {
   font-weight: 700;
   font-size: 0.95rem;
   display: block;
}

.viz-tooltip .viz-tip-label {
   opacity: 0.8;
}

/* An empty state describes what the stat will show and what fills it, rather
   than a grid of dashes. With one active spotter, most new users see this. */
.viz-empty {
   display: flex;
   align-items: center;
   justify-content: center;
   min-height: 8rem;
   padding: 1.25rem;
   border: 1px dashed var(--viz-grid);
   border-radius: 0.75rem;
   color: var(--viz-muted);
   font-size: 0.88rem;
   text-align: center;
}

@media (prefers-reduced-motion: reduce) {
   .viz-mark,
   .viz-tooltip {
      transition: none;
   }
}

/* Listing — the form for a "ranking" with nothing to rank by (newest arrivals,
   one-hit wonders). The order carries the information; the value is a detail
   beside it, so there is no mark to compare and no bar to mislead. */
.viz-list {
   margin: 0;
   padding: 0;
   list-style: none;
   counter-reset: viz-list;
}

.viz-list li {
   display: flex;
   align-items: baseline;
   justify-content: space-between;
   gap: 1rem;
   padding: 0.42rem 0;
   border-bottom: 1px solid var(--viz-grid);
   counter-increment: viz-list;
}

.viz-list li:last-child {
   border-bottom: 0;
}

.viz-list li::before {
   content: counter(viz-list);
   color: var(--viz-muted);
   font-size: 0.75rem;
   font-variant-numeric: tabular-nums;
   min-width: 1.4rem;
   flex: 0 0 auto;
}

.viz-list li > :nth-child(1) {
   flex: 1 1 auto;
}

/* The counter is a rank, so it comes off wherever the order is not one.
   Milestones are the case that forced it: "1  Sighting 1,000" puts two numbers
   side by side that mean entirely different things, and the one the eye reads
   first is the one that means nothing. */
.viz-list--plain {
   counter-reset: none;
}

.viz-list--plain li::before {
   content: none;
}

.viz-list .viz-list-value {
   color: var(--viz-muted);
   font-size: 0.85rem;
   font-variant-numeric: tabular-nums;
   flex: 0 0 auto;
}


/* ---------------------------------------------------------------------------
   Leaderboard (Phase 7)
   ---------------------------------------------------------------------------

   The one page here whose subject is people rather than balloons, and the only
   table on the site that has to stay readable while being scanned for a single
   name. Hence the two departures from the chart chrome above:

   · Numbers are tabular. Every rule elsewhere in this file prefers proportional
     figures, because tabular digits look loose at display sizes. A ranked
     column is exactly the case that rule excepts: the digits have to line up
     for two totals to be comparable at a glance.
   · The viewer's own row is marked with the *neutral* accent, not a series
     colour. The chart palette's slots mean identity within a chart; borrowing
     one here would imply a relationship to a mark that is not there.
   --------------------------------------------------------------------------- */

.lb-tabs {
   display: flex;
   flex-wrap: wrap;
   gap: 0.5rem;
}

.lb-tabs--sub {
   margin-top: 0.5rem;
}

.lb-tab {
   border: 1px solid var(--viz-grid, #e1e0d9);
   border-radius: 2rem;
   color: #52514e;
   font-size: 0.9rem;
   font-weight: 600;
   padding: 0.35rem 0.95rem;
   text-decoration: none;
}

.lb-tab:hover,
.lb-tab:focus-visible {
   border-color: #1c5cab;
   color: #1c5cab;
}

.lb-tab--on {
   background: #1c5cab;
   border-color: #1c5cab;
   color: #ffffff;
}

.lb-tab--on:hover,
.lb-tab--on:focus-visible {
   color: #ffffff;
}

/* The table scrolls inside its own card. Six columns do not fit a phone, and a
   page that scrolls sideways as a whole loses the nav with it. */
.lb-scroll {
   overflow-x: auto;
}

.lb-table {
   border-collapse: collapse;
   font-size: 0.95rem;
   min-width: 34rem;
   width: 100%;
}

.lb-table th,
.lb-table td {
   border-bottom: 1px solid var(--viz-grid, #e1e0d9);
   padding: 0.55rem 0.75rem;
   text-align: left;
   white-space: nowrap;
}

.lb-table thead th {
   color: #6c757d;
   font-size: 0.72rem;
   font-weight: 700;
   letter-spacing: 0.06em;
   text-transform: uppercase;
}

.lb-table tbody tr:last-child td {
   border-bottom: 0;
}

.lb-rank,
.lb-num {
   font-variant-numeric: tabular-nums;
}

.lb-num {
   text-align: right;
}

.lb-rank {
   color: #6c757d;
   width: 3rem;
}

/* The selected board's column, so the ordering has something to point at.
   Weight rather than colour: the other five columns are still values, not
   background. */
.lb-num--on {
   color: #1f2937;
   font-weight: 700;
}

.lb-you td {
   background: #eef4fc;
}

.lb-badge {
   background: #1c5cab;
   border-radius: 2rem;
   color: #ffffff;
   font-size: 0.68rem;
   font-weight: 700;
   letter-spacing: 0.04em;
   margin-left: 0.4rem;
   padding: 0.1rem 0.45rem;
   text-transform: uppercase;
   vertical-align: middle;
}

/* The break before a pinned row. It marks the rows that were skipped, which is
   not the same thing as a rank that was skipped — nobody is hidden in here. */
.lb-gap td {
   color: var(--viz-muted, #898781);
   letter-spacing: 0.3em;
   padding-bottom: 0.2rem;
   padding-top: 0.2rem;
   text-align: center;
}

:root[data-theme="dark"] .lb-table th,
:root[data-theme="dark"] .lb-table td {
   border-bottom-color: #33322f;
}

:root[data-theme="dark"] .lb-num--on {
   color: #ffffff;
}

:root[data-theme="dark"] .lb-you td {
   background: #17253a;
}


/* ---------------------------------------------------------------------------
   Coverage and colour (Phase 8)
   ---------------------------------------------------------------------------

   A coverage badge is a statement about the data, not a mark encoding it, so it
   wears the muted text tokens rather than any series colour. Giving it one of
   the categorical slots would make it look like a legend key for a bar that
   does not exist.
   --------------------------------------------------------------------------- */

.coverage-badge {
   background: var(--viz-seq-0, #f0efec);
   border-radius: 2rem;
   color: var(--viz-ink-2, #52514e);
   display: inline-block;
   font-size: 0.72rem;
   font-weight: 700;
   letter-spacing: 0.04em;
   margin-top: 0.75rem;
   padding: 0.15rem 0.6rem;
   text-transform: uppercase;
}

/* Swatches — the one place a mark takes a real-world colour rather than one
   from the palette, because here the colour is the measurement. The border is
   not decoration: white, silver and grey are in this vocabulary and would
   otherwise be invisible on a white card. */
.viz-swatches {
   display: flex;
   flex-wrap: wrap;
   gap: 0.35rem 1rem;
   list-style: none;
   margin: 1rem 0 0;
   padding: 0;
}

.viz-swatches li {
   align-items: center;
   display: flex;
   gap: 0.4rem;
}

.viz-swatch {
   background: var(--swatch, #c3c2b7);
   border: 1px solid rgba(11, 11, 11, 0.25);
   border-radius: 3px;
   display: inline-block;
   flex: 0 0 auto;
   height: 0.85rem;
   width: 0.85rem;
}

.viz-swatch-name {
   color: var(--viz-ink-2, #52514e);
   font-size: 0.85rem;
}

.viz-swatch-value {
   color: var(--viz-muted, #898781);
   font-size: 0.8rem;
   font-variant-numeric: tabular-nums;
}

:root[data-theme="dark"] .viz-swatch {
   border-color: rgba(255, 255, 255, 0.35);
}

/* The colour picker in /admin/edit-balloon.php. Reuses .viz-swatch so the
   colours an admin ticks look exactly like the ones the statistic draws. */
.admin-colours {
   display: grid;
   gap: 0.35rem 1rem;
   grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
}

.admin-colour {
   align-items: center;
   cursor: pointer;
   display: flex;
   font-weight: 400;
   gap: 0.4rem;
   margin: 0;
}

/* ---------------------------------------------------------------------------
   The archive contribution panel (/stats)
   --------------------------------------------------------------------------- */

/* The balloons this account was first to photograph. Thumbnails rather than a
   list of registrations: the point of the panel is "look what you put here",
   and six letters of a registration cannot make that point the way the
   photograph itself does. They wrap and they are small, because there may be
   four hundred of them and the panel is a boast, not an index. */
.archive-firsts {
   display: flex;
   flex-wrap: wrap;
   gap: 0.35rem;
}

.archive-thumb {
   border-radius: 0.4rem;
   height: 3rem;
   object-fit: cover;
   width: 3rem;
}

.archive-firsts a:hover .archive-thumb {
   outline: 2px solid #0d6efd;
   outline-offset: 1px;
}
