/* =============================================================================
   Geopolitical indicator dashboard — stylesheet
   -----------------------------------------------------------------------------
   Scoped under .board so nothing here leaks into the rest of the site. Plain
   CSS on purpose (the same call the semiconductor read makes): the fit-to-
   viewport mechanics below are custom properties, container queries and five
   attribute-driven state rules, which as Tailwind class soup would be
   unreadable — and editing them needs no `npm run build:css` round-trip.
   Tailwind's compiled build is still loaded by the template and still does the
   coarse spacing/flex work in the markup.

   Loaded with {% static_v %} (mtime ?v=) — the cache-busting every plain
   /static/ asset on this site uses. See CLAUDE.md "Cache-busting".

   Layout model — one viewport, no page scroll at >=1024px:

     ┌──────────────────────────────────────┬─────────┬─────────┐
     │ control bar     view · timeframe · … │         │         │  single row
     ├──────────────────────────────────────┼─────────┼─────────┤
     │ Currencies & crypto     source credit│ AI Labs │AI Models│
     │ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢                  │ ▬▬▬▬▬▬  │ ▬▬▬▬▬▬  │
     │ Equity indices          source credit│ ▬▬▬▬▬   │ ▬▬▬▬    │
     │ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢                  │ ▬▬▬▬    │ ▬▬▬     │
     │ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢                  │ …       │ …       │
     │ Resources               source credit│         │         │
     │ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢                  │         │         │
     │ Stock markets           source credit│         │         │
     │ ┌────────┐ ┌────────┐ ┌────────┐     │         │         │
     │ │███  ▢▢ │ │████  ▢▢│ │████  ▢▢│     │         │         │  treemaps
     │ │███  ▢ ▢│ │████ ▢ ▢│ │████ ▢ ▢│     │         │         │
     │ └────────┘ └────────┘ └────────┘     │         │         │
     ├──────────────────────────────────────┴─────────┴─────────┤
     │ every source, with links                                 │  attribution
     └──────────────────────────────────────────────────────────┘

   The grid fills whatever height is left over, and the TILES take their size
   from that — not the other way round. Each group's flex-grow is its row
   count (written by board.js, which is the only thing that knows how many
   columns the current breakpoint produced), and its flex-basis is the header
   height, so the leftover space divides by rows and every tile in every group
   comes out exactly the same height. The treemap block is a group like any
   other and takes a fixed weight of three rows (section 3e), which is what
   splits the board roughly half tiles, half maps.

   Every tile group holds a multiple of ten tiles (10 / 20 / 10) and the desktop
   column count is ten, so each group fills whole rows and no row ends in a
   hole. Four tile rows plus the maps is the density this page is designed at;
   the type throughout is container-query-sized, so the tiles shrink their own
   contents rather than overflowing when the viewport gets shorter.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   1. Font — self-hosted variable JetBrains Mono (SIL OFL 1.1, see fonts/OFL.txt)
   --------------------------------------------------------------------------
   Self-hosted, not a CDN: no third-party request, no DNS dependency (this site
   has been burned by a font/CSS CDN before — see CLAUDE.md). One variable file
   per subset covers weights 100-800, so the whole page needs two requests
   totalling ~55KB. The fallback stack is the generic `monospace` and nothing
   else, so a failed load still lands on a typewriter face rather than sliding
   into whatever sans the OS offers.
   -------------------------------------------------------------------------- */
@font-face {
    font-family: 'JetBrains Mono Variable';
    font-style: normal;
    font-weight: 100 800;
    font-display: swap;
    src: url("../fonts/jetbrains-mono-latin-var.b058178d7f30.woff2") format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                   U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                   U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
    font-family: 'JetBrains Mono Variable';
    font-style: normal;
    font-weight: 100 800;
    font-display: swap;
    src: url("../fonts/jetbrains-mono-latin-ext-var.e36841ed93d5.woff2") format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                   U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                   U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* -----------------------------------------------------------------------------
   2. Tokens
   --------------------------------------------------------------------------
   Dark ink, warm off-white, and three signal colours: a muted green for up, a
   muted warm red for down, a neutral grey for flat. Light mode paints ink on
   off-white; dark mode swaps the two. Indigo survives as the *measurement*
   colour — the ranking bars — which keeps green and red meaning direction and
   nothing else.

   Both signal colours are earthy rather than traffic-light: a saturated
   #00c853 next to a saturated #ff1744 across thirty tiles is a slot machine,
   and every tile shouting equally loudly is the same as none of them shouting.

   The signals are declared as bare `R G B` triplets, not hex, because the tiles
   need the same colour at four different alphas (text, fill, border, bar) and
   `rgb(var(--up-rgb) / 0.1)` is the one form that composes without a second
   token per alpha.

   Each signal gets a lighter step in dark mode. That is the same colour,
   re-weighted for the ground it sits on: the green that clears 4.5:1 on paper
   is 2.6:1 on ink, and the delta is the one thing on this page that must never
   be hard to read.
   -------------------------------------------------------------------------- */
.board {
    --ink: #232b3a;
    --paper: #F5F1E8;
    --indigo: #5b6ee0;

    /* Light mode: ink on paper. */
    --bg: var(--paper);
    --fg: var(--ink);
    --fg-soft: rgba(35, 43, 58, 0.66);
    --fg-mute: rgba(35, 43, 58, 0.44);
    --line: rgba(35, 43, 58, 0.13);
    --line-soft: rgba(35, 43, 58, 0.07);
    --tile-bg: rgba(35, 43, 58, 0.035);
    --tile-bg-hover: rgba(35, 43, 58, 0.065);
    --bar: 91 110 224;

    --up-rgb: 45 122 88;      /* #2d7a58 — 4.9:1 on paper */
    --down-rgb: 178 75 62;    /* #b24b3e — 5.1:1 on paper */
    --flat-rgb: 138 143 154;

    /* Geometry. --cols is the tile grid's column count for the current
       breakpoint (section 4); --head-h doubles as each group's flex-basis, so
       it has to be a real length, not `auto`.

       --side-w is the width of ONE ranking panel, and there are two of them.
       It is deliberately tight: the panels lost their delta column (the
       rankings carry no delta — see mockdata.py), which is the only reason two
       of them fit in barely more room than the single wider one took. The tile
       grid is the page; the panels are the margin note. */
    --cols: 10;
    --gap: 6px;
    --head-h: 1.15rem;
    --pad-x: clamp(0.75rem, 1.6vw, 1.75rem);
    --side-w: clamp(8.75rem, 10.75vw, 12rem);

    /* One tile row. Deliberately short — three lines of type is all a tile
       has to hold, and every pixel it gives up goes to the treemaps, which
       turn height directly into how many of a hundred blocks are big enough
       to carry a mark. Viewport-relative so a tall screen still gets roomier
       tiles rather than banking the whole gain in the maps. */
    --tile-h: clamp(52px, 6.2vh, 74px);

    /* The hover card floats over both themes' fields, so it cannot be the
       page background at some alpha — it has to be opaque and a step apart
       from whatever it covers. */
    --card-bg: #fffdf8;
    --card-line: rgba(35, 43, 58, 0.16);
    --card-shadow: 0 10px 30px -8px rgba(35, 43, 58, 0.3);

    font-family: 'JetBrains Mono Variable', monospace;
    /* Values are re-rendered on every refresh; tabular figures keep a column of
       them from jittering when a 1 becomes an 8. */
    font-variant-numeric: tabular-nums;
    font-feature-settings: "tnum" 1;
    background: var(--bg);
    color: var(--fg);
}

html.dark .board {
    /* Dark mode: paper on ink. */
    --bg: var(--ink);
    --fg: var(--paper);
    --fg-soft: rgba(245, 241, 232, 0.62);
    --fg-mute: rgba(245, 241, 232, 0.40);
    --line: rgba(245, 241, 232, 0.12);
    --line-soft: rgba(245, 241, 232, 0.06);
    --tile-bg: rgba(245, 241, 232, 0.04);
    --tile-bg-hover: rgba(245, 241, 232, 0.075);
    --bar: 139 152 236;

    --up-rgb: 106 197 148;    /* #6ac594 — the same green, lightened for ink */
    --down-rgb: 222 122 104;  /* #de7a68 — the same warm red, likewise */
    --flat-rgb: 154 160 171;

    --card-bg: #1b2231;
    --card-line: rgba(245, 241, 232, 0.16);
    --card-shadow: 0 10px 34px -8px rgba(0, 0, 0, 0.62);
}

/* -----------------------------------------------------------------------------
   3. Page frame
   -------------------------------------------------------------------------- */
html, body {
    margin: 0;
    padding: 0;
    background: #F5F1E8;
}

html.dark, html.dark body {
    background: #232b3a;
}

.board {
    height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0 var(--pad-x);
    box-sizing: border-box;
}

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

/* -----------------------------------------------------------------------------
   3a. Control bar — fixed, single row
   -------------------------------------------------------------------------- */
.board-bar {
    flex: none;
    display: flex;
    align-items: center;
    gap: clamp(0.75rem, 2vw, 2rem);
    height: 3.1rem;
    border-bottom: 1px solid var(--line);
    /* sticky rather than position:fixed — at >=1024px the page cannot scroll
       at all, so it is fixed by construction; below that the page does scroll
       and sticky keeps the bar pinned without taking it out of flow. */
    position: sticky;
    top: 0;
    z-index: 20;
    background: var(--bg);
}

.board-brand {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    text-decoration: none;
    color: var(--fg);
    white-space: nowrap;
}

.board-brand .mark {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.22em;
    text-transform: uppercase;
}

.board-brand .back {
    font-size: 10px;
    letter-spacing: 0.12em;
    color: var(--fg-mute);
    transition: color .25s ease;
}

.board-brand:hover .back { color: var(--fg); }

.board-switches {
    display: flex;
    align-items: center;
    gap: clamp(0.75rem, 2vw, 2rem);
    margin-left: auto;
}

.switch {
    display: flex;
    align-items: center;
    gap: 0.55rem;
}

.switch-label {
    font-size: 8.5px;
    font-weight: 500;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--fg-mute);
    white-space: nowrap;
}

.switch-set {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.switch-btn {
    appearance: none;
    border: 1px solid transparent;
    background: none;
    font: inherit;
    font-size: 10.5px;
    font-weight: 500;
    letter-spacing: 0.1em;
    color: var(--fg-soft);
    padding: 0.28rem 0.55rem;
    border-radius: 3px;
    cursor: pointer;
    white-space: nowrap;
    transition: color .2s ease, background-color .2s ease, border-color .2s ease;
}

.switch-btn:hover:not([disabled]):not([aria-pressed="true"]) {
    color: var(--fg);
    background: var(--tile-bg);
}

.switch-btn[aria-pressed="true"] {
    color: var(--fg);
    font-weight: 700;
    border-color: var(--line);
    background: var(--tile-bg-hover);
}

.switch-btn[disabled] {
    color: var(--fg-mute);
    opacity: 0.5;
    cursor: not-allowed;
    text-decoration: line-through;
    text-decoration-thickness: 1px;
}

/* Tooltip for the disabled Industry mode. It hangs off a wrapper rather than
   the button itself: a disabled button swallows pointer events, so a native
   title="" on it never appears in Chrome. */
.tip { position: relative; display: inline-flex; }

.tip::after {
    content: attr(data-tip);
    position: absolute;
    top: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    padding: 0.3rem 0.5rem;
    border-radius: 3px;
    background: var(--fg);
    color: var(--bg);
    font-size: 9px;
    letter-spacing: 0.08em;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity .2s ease;
    z-index: 30;
}

.tip:hover::after { opacity: 1; }

.board-meta {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    font-size: 9px;
    letter-spacing: 0.1em;
    color: var(--fg-mute);
    white-space: nowrap;
}

.theme-btn {
    appearance: none;
    background: none;
    border: none;
    padding: 0.25rem;
    color: var(--fg-mute);
    cursor: pointer;
    display: inline-flex;
    transition: color .25s ease;
}

.theme-btn:hover { color: var(--fg); }
.theme-btn .icon-sun { display: none; }
html.dark .theme-btn .icon-sun { display: inline; }
html.dark .theme-btn .icon-moon { display: none; }

/* -----------------------------------------------------------------------------
   3b. Groups + tile grid
   -------------------------------------------------------------------------- */
.board-main {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    /* `auto` rather than a second explicit track: the panel widths live on
       .board-side, so the mobile fallback re-flows them in one place. */
    grid-template-columns: minmax(0, 1fr) auto;
    gap: clamp(0.75rem, 2vw, 2rem);
    padding: clamp(0.4rem, 1vh, 0.9rem) 0;
    overflow: hidden;
}

/* The tile column keeps the old flex behaviour verbatim — groups still divide
   the available height between themselves by row count. It just divides a
   narrower column now that the ranking sits beside it. */
.board-grid {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: clamp(0.4rem, 1vh, 0.9rem);
    overflow: hidden;
}

/* Tile groups take exactly the height their rows need and no more; the treemap
   group (section 3e) takes everything left.

   This inverts what the board used to do — all four groups sharing the
   leftover height in proportion to their row count — and the reason is that
   the two halves stopped being the same kind of thing. A tile needs enough
   height for three lines and gains nothing from more. A treemap of a hundred
   blocks turns every pixel into resolution: at 300px the smallest Chinese
   constituents are 6px squares, at 550px they are blocks you can point at.
   So the tiles now declare a height and the maps get the remainder. */
.group {
    flex: none;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.group-grid {
    grid-auto-rows: var(--tile-h);
}

.group-head {
    flex: none;
    height: var(--head-h);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    border-bottom: 1px solid var(--line-soft);
    margin-bottom: var(--gap);
}

.group-name {
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: 0.26em;
    text-transform: uppercase;
    color: var(--fg-soft);
    white-space: nowrap;
}

/* Per-group source credit — a licensing obligation, so it stays visible at
   every breakpoint rather than being the first thing dropped. */
.group-credit {
    font-size: 8.5px;
    letter-spacing: 0.08em;
    color: var(--fg-mute);
    text-align: right;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* --cols is the breakpoint's *maximum* column count; --group-cols is what this
   particular group actually uses, written by board.js so the group divides the
   full width by its own tile count and no row ends short. A group with 5 tiles
   under a 7-column maximum gets 5 wide ones rather than 5 narrow ones and two
   holes. Tile widths therefore differ between groups; tile heights do not.
   Without JS this falls back to the uniform maximum. */
.group-grid {
    flex: none;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(var(--group-cols, var(--cols)), minmax(0, 1fr));
    gap: var(--gap);
}

/* -----------------------------------------------------------------------------
   3c. Tile
   --------------------------------------------------------------------------
   Every tile is a size container, so its type scales with whatever height the
   grid handed it (cqh) rather than with the viewport. Each font-size carries a
   plain px fallback first, for engines without container query units.

   Fill, border and delta all read the same two custom properties: --c, the
   active window's signal colour, and --a, how strongly it is applied. Both are
   resolved in section 3d off the tile's own tokens, so the tile itself never
   restates which direction it is showing.
   -------------------------------------------------------------------------- */
.tile {
    container-type: size;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
    /* A row, not a column: the text stack on the left, the identity slot
       (currency sign or crypto disc) on the right as a real flex sibling. An
       absolutely positioned glyph would have been simpler and wrong — it would
       have sat on top of the delta at exactly the widest strings, which is the
       one number this tile exists to print. */
    display: flex;
    align-items: stretch;
    gap: clamp(2px, 3cqw, 9px);
    padding: clamp(4px, 9cqh, 11px) clamp(6px, 7cqw, 13px);
    /* The unset default is what a flat or unanswerable window shows. It used
       to be a 3% wash, which left a tile that did nothing looking like an
       empty slot; it now matches the lowest signal bucket's weight in grey,
       so "no change" reads as a measurement, not as a missing tile. */
    border: 1px solid rgb(var(--c, var(--flat-rgb)) / calc(var(--a, 0.07) * 2.6));
    border-radius: 4px;
    background: rgb(var(--c, var(--flat-rgb)) / var(--a, 0.07));
    transition: background-color .25s ease, border-color .25s ease, opacity .25s ease;
}

/* The three lines every tile has, spread over the tile's full height. This was
   the tile itself until the identity slot moved beside it. */
.tile-stack {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 2px;
}

.tile:hover {
    /* Hover lifts the tint rather than replacing it — a tile that turned grey
       under the cursor would drop the one thing it is on screen to say. */
    background: rgb(var(--c, var(--flat-rgb)) / calc(var(--a, 0.07) + 0.06));
    border-color: rgb(var(--c, var(--flat-rgb)) / calc(var(--a, 0.07) * 4));
}

/* Icon and ticker on one line. The icon is a fixed disc rather than a share of
   the row, because at ten columns the tile is narrow enough that a
   proportional flag would shrink below the point where a flag is still a flag,
   and a smaller ticker beside a legible flag is the better trade — the ticker
   has a fallback (the hover card), the flag is the fallback. */
.tile-label {
    display: flex;
    align-items: center;
    gap: 0.32em;
    min-width: 0;
    font-size: 9px;
    font-size: clamp(7.5px, 11cqh, 11px);
    font-weight: 500;
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: var(--fg-soft);
    line-height: 1.2;
}

.tile-name {
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* -- the identity slot: currency signs, and the two crypto discs ------------ */

/* One big glyph on the right of every currency tile. The sign IS the currency
   in a way a flag never was: € is read faster than "EUR" and much faster than a
   13px circle of blue, and it does not make the false claim the flags were
   making — that a currency is a country. (It isn't: the euro is not the EU, and
   the franc's holders are mostly outside Switzerland.)

   Set below full strength on purpose. It is identity, not signal: the delta is
   the loudest thing on the tile and has to stay that way, so the sign reads as
   a watermark the eye lands on second. Sized against both axes like everything
   else here, and dropped entirely on narrow tiles (see below) rather than
   allowed to squeeze the delta. */
.tile-sym {
    flex: none;
    align-self: center;
    font-size: clamp(13px, min(70cqh, 34cqw), 40px);
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1;
    color: var(--fg);
    opacity: 0.38;
    white-space: nowrap;
}

/* "A$", "HK$" and "Fr" are two and three characters where "€" is one, so a
   single size would have let the longest form set the width for all eight. Each
   step down is roughly proportional to the character count, which keeps the
   eight signs at about the same optical weight rather than the same height. */
.tile-sym[data-len="2"] { font-size: clamp(11px, min(50cqh, 21cqw), 28px); }
.tile-sym[data-len="3"] { font-size: clamp(9px, min(38cqh, 15cqw), 21px); }

/* BTC and ETH: the same slot at the same size, but a round brand disc, because
   neither has a sign this font can set (₿ and Ξ are outside the subset) and
   neither has a country. These are the only brand colours on the board and the
   exception is deliberate — the row needs a way to say "these two are not
   currencies", and colour says it before the ticker is read. */
.tile-sym-mark {
    flex: none;
    align-self: center;
    display: block;
    width: clamp(13px, min(70cqh, 34cqw), 40px);
    height: clamp(13px, min(70cqh, 34cqw), 40px);
    border-radius: 50%;
    opacity: 0.9;
}

/* The satellite row's country flags, in the same slot at the same size as the
   crypto discs. A flag was squeezed into the label line before; here it has
   the room to read as a flag. The hairline ring keeps the white-field flags
   (Japan, Canada) from dissolving into a light tile. */
.tile-sym-flag {
    box-shadow: 0 0 0 1px var(--line);
}

/* Below this the tile is narrower than the delta plus a sign, and the delta
   wins — it is the measurement, the sign is the label, and the label is
   repeated in the ticker one line above it. Same call the note line makes.
   Against the tile's content box, which is what a size container queries. */
@container (max-width: 66px) {
    .tile-sym { display: none; }
}

/* The delta is the point of the page: biggest type on the tile, and the
   loudest instance of the tile's colour.

   Sized against the tile's height AND its width, whichever is tighter. Height
   alone was enough when the board ran six tiles across; at ten the tiles are
   much narrower than they are short, and "-12.34%" plus its triangle is about
   five ems wide — so a height-only size overflowed the tile at exactly the
   breakpoints this site is most often read at (see the CLAUDE.md note on 1080p
   at 125% scaling). The cqw half is what keeps the widest string inside the
   padding; the ten-column row is why its coefficient is the binding one at
   every desktop width rather than only at the narrow end. */
.tile-delta {
    font-size: 1.2rem;
    font-size: clamp(10px, min(28cqh, 16cqw), 1.9rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1;
    white-space: nowrap;
    color: rgb(var(--c, var(--flat-rgb)));
}

/* Direction is never carried by hue alone. Red/green is the one pairing a
   twelfth of men cannot separate, so every delta also states its direction in
   the sign it already prints and in a triangle set ahead of it — the colour is
   the fast read, the glyph is the reliable one. Keyed off `.win` rather than
   `.tile-delta`, so the treemap blocks get the same treatment from the same
   three rules. */
.win[data-dir="na"] { font-weight: 400; }

.win::before {
    line-height: 1;
    vertical-align: 0.28em;
    margin-right: 0.3em;
    font-size: 0.52em;
}

.win[data-dir="up"]::before { content: "\25B2"; }
.win[data-dir="down"]::before { content: "\25BC"; }
.win[data-dir="flat"]::before { content: "\2013"; vertical-align: 0.18em; }

.tile-foot {
    display: flex;
    align-items: baseline;
    gap: 0.35rem;
    min-width: 0;
}

/* Never shrinks: a truncated number is a wrong number. When the pair does not
   fit — "148 620 pts" in a tile eight columns wide on a 1024px screen — the
   unit is what gives, which is the right order (the unit repeats down the whole
   group; the value does not). */
.tile-value {
    flex: none;
    font-size: 12px;
    font-size: clamp(9.5px, 15cqh, 15px);
    font-weight: 500;
    line-height: 1.1;
    color: var(--fg);
    white-space: nowrap;
}

/* min-width: 0 is what lets it ellipsis at all — a nowrap flex item defaults to
   min-width: auto and refuses to shrink below its own text. */
.tile-unit {
    min-width: 0;
    font-size: 9px;
    font-size: clamp(7px, 10cqh, 10.5px);
    color: var(--fg-mute);
    letter-spacing: 0.06em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The thing the ticker stands for — "Euro", "United States", "Dogecoin". First
   thing to go when a tile gets short (the label, value and delta are not); the
   label's title attribute still carries it when it goes. */
.tile-note {
    font-size: 8px;
    font-size: clamp(6.5px, 8.5cqh, 9px);
    color: var(--fg-mute);
    letter-spacing: 0.04em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Against the tile's *content* box, which is what a size container queries —
   some 18px shorter than the tile itself. Six tile rows on a 900px-tall screen
   leaves ~80px of content per tile, so the threshold sits below that: at this
   density the note is the whole reason "TADAWUL" or "STI" is legible at all. */
@container (max-height: 66px) {
    .tile-note { display: none; }
}

/* -----------------------------------------------------------------------------
   3d. Timeframe state
   --------------------------------------------------------------------------
   Switching the window is a CSS state change, not a re-render: all five deltas
   are already in the DOM, and the active one is the only one displayed. No
   request, no reflow of anything but the element's own text.

   The colour works the same way. Every signal element (`.sig` — a tile or a
   treemap block) carries all five windows' directions in one token list, and
   the rules below pick the active window's out of it into --c. Fifteen
   selectors recolour eighty-odd elements, and the alternative — per-element
   inline styles for five windows, or a JS pass that rewrites classes on every
   switch — would be both heavier markup and a second source of truth for what
   the board is currently showing.

   `.win` is the matching marker for the *text*: one span per window, only the
   active one displayed. Tiles and treemap blocks both use it, so this stays
   five selectors rather than ten.

   The ranking panels are outside this entirely: they have no delta and no
   signal, so the timeframe switcher does not touch them (see mockdata.py).
   -------------------------------------------------------------------------- */
.win { display: none; }

.board[data-tf="1d"] .win[data-tf="1d"],
.board[data-tf="7d"] .win[data-tf="7d"],
.board[data-tf="1m"] .win[data-tf="1m"],
.board[data-tf="3m"] .win[data-tf="3m"],
.board[data-tf="6m"] .win[data-tf="6m"] {
    display: block;
}

/* Hue: which way the active window moved. */
.board[data-tf="1d"] .sig[data-dirs~="1d:up"],
.board[data-tf="7d"] .sig[data-dirs~="7d:up"],
.board[data-tf="1m"] .sig[data-dirs~="1m:up"],
.board[data-tf="3m"] .sig[data-dirs~="3m:up"],
.board[data-tf="6m"] .sig[data-dirs~="6m:up"] {
    --c: var(--up-rgb);
}

.board[data-tf="1d"] .sig[data-dirs~="1d:down"],
.board[data-tf="7d"] .sig[data-dirs~="7d:down"],
.board[data-tf="1m"] .sig[data-dirs~="1m:down"],
.board[data-tf="3m"] .sig[data-dirs~="3m:down"],
.board[data-tf="6m"] .sig[data-dirs~="6m:down"] {
    --c: var(--down-rgb);
}

/* Strength: how far it moved. Flat and unknown windows emit no magnitude token
   at all and so keep the neutral default — a tile that did nothing should look
   like a tile that did nothing. */
.board[data-tf="1d"] .sig[data-mag~="1d:1"],
.board[data-tf="7d"] .sig[data-mag~="7d:1"],
.board[data-tf="1m"] .sig[data-mag~="1m:1"],
.board[data-tf="3m"] .sig[data-mag~="3m:1"],
.board[data-tf="6m"] .sig[data-mag~="6m:1"] {
    --a: 0.07;
}

.board[data-tf="1d"] .sig[data-mag~="1d:2"],
.board[data-tf="7d"] .sig[data-mag~="7d:2"],
.board[data-tf="1m"] .sig[data-mag~="1m:2"],
.board[data-tf="3m"] .sig[data-mag~="3m:2"],
.board[data-tf="6m"] .sig[data-mag~="6m:2"] {
    --a: 0.13;
}

.board[data-tf="1d"] .sig[data-mag~="1d:3"],
.board[data-tf="7d"] .sig[data-mag~="7d:3"],
.board[data-tf="1m"] .sig[data-mag~="1m:3"],
.board[data-tf="3m"] .sig[data-mag~="3m:3"],
.board[data-tf="6m"] .sig[data-mag~="6m:3"] {
    --a: 0.2;
}

/* A monthly series has no 1D or 7D move. Those windows print a dash — never a
   zero, which would be a number nobody measured — on the same neutral surface
   as a flat tile. They used to fade the whole tile to 42%, which made a row of
   perfectly current gold holdings look like it had failed to load: the dash
   already says "not answerable", and the value and flag are still true. */

/* -----------------------------------------------------------------------------
   3e. Market treemaps
   --------------------------------------------------------------------------
   Three whole stock markets, drawn to area: the hundred largest companies plus
   one block for everything else.

   **Geometry is board.js's job here, not this stylesheet's and not Python's.**
   A squarified treemap's entire objective is squareness, and it cannot deliver
   that without the box's true proportions; solving on an assumed square and
   rendering into a 1.2:1 plot stretches every block by that factor. Invisible
   at eleven blocks — the difference between a mark fitting its block and not
   at a hundred. So the server ships each block's weight and nothing else, and
   the client writes `left/top/width/height` in pixels against the plot it can
   actually measure, re-solving on resize.

   The rects tile the plot exactly, with no room for a grid gap: the areas ARE
   the data, so a gutter has to be cut out of the blocks rather than added
   between them. board.js does that cutting — it snaps every rect to whole
   device pixels and then takes the gutter off the width and height, so no two
   blocks can ever paint the same pixel and none can escape the plot. It used to
   be a 1px border in the page background colour here, and that was the bug: a
   border cannot be thinner than 1px, so any block the packing made narrower
   than ~2px still rendered 2px wide and overlapped its neighbours, and the
   tail of a hundred-block map is full of those. The inset ring below is what
   puts the signal colour's own edge back inside the block.

   Every block is a `.sig` element, so section 3d colours it — the treemaps and
   the tiles change together on one attribute write, and there is no second
   copy of the red/green logic here. What is different is the strength: on a
   tile the tint is a wash behind text, on a block it is the mark itself, so
   the same three magnitude steps are multiplied up.
   -------------------------------------------------------------------------- */

/* Everything the tile rows leave. The tiles take a declared height (section
   3b) and the maps take the remainder, because height is resolution here:
   every extra pixel moves more of the hundred blocks above the size where a
   mark fits. */
.group-maps {
    flex: 1 1 auto;
    min-height: 0;
}

.maps {
    flex: 1 1 auto;
    min-height: 0;
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: clamp(0.5rem, 1.2vw, 1.25rem);
}

/* No caption element. The bloc name and the market total live inside the
   remainder block (`.map-title` / `.map-total` below), which is the only block
   with room for them — and folding them in buys a whole header row per map
   back for the plot. */
.map {
    margin: 0;
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

.map-plot {
    position: relative;
    flex: 1 1 auto;
    min-height: 0;
}

/* Until board.js has solved the packing the blocks have no geometry at all, so
   they would pile up at auto position in source order. Hide them until the
   first successful pass and say why — an absent map with a reason beats a
   heap of overlapping rectangles. This is also the whole no-JS story for the
   treemaps; the tiles and rankings above degrade fine without it. */
.map-plot:not([data-solved]) .map-block { display: none; }

.map-plot:not([data-solved])::after {
    content: "treemap needs JavaScript";
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px dashed var(--line);
    border-radius: 4px;
    font-size: 9px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--fg-mute);
}

/* The padding here is in plain pixels, and that is not an oversight — it is the
   one place on this page where container-query units CANNOT be used.

   A container's own declarations do not resolve `cq*` against itself; that
   would be circular, so they resolve against the nearest ANCESTOR container,
   and with none above `.map-block` they fell back to the viewport. So
   `clamp(1px, 6cqh, 9px)` was never a sixth of the block — it was 6% of the
   viewport, pinned at the clamp's 9px maximum for every block on every map.
   With `box-sizing: border-box` a box cannot be narrower than its own padding,
   so that constant 9px silently made 18x18 the smallest a block could be. Any
   block the packing sized under that inflated to 18px and painted straight
   over its neighbours, and the tail of a hundred-block map is nothing but
   those: at 1280x720 CSS (a 1080p screen at 150% scale) it was ~270 overlapping
   pairs per map and ~30 blocks hanging outside the plot. Only the DESCENDANTS
   of a block may use `cq` units against it — the type and the disclosure
   queries below do, and those are correct.

   The value is small and flat because this rule has to serve the remainder
   block, the only one big enough to hold text; company blocks below drop it
   entirely and inset their mark proportionally instead. */
.map-block {
    position: absolute;
    container-type: size;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 1px;
    padding: 6px 7px;
    /* Proportional below ~14px, so a 4px block is a small rounded square and
       not a dot. A flat 3px radius on a 4px block is a circle, and a field of
       circles is the other half of what read as "messy". */
    border-radius: min(3px, 22%);
    background: rgb(var(--c, var(--flat-rgb)) / calc(var(--a, 0.035) * 2.6));
    box-shadow: inset 0 0 0 1px
                rgb(var(--c, var(--flat-rgb)) / calc(var(--a, 0.035) * 3.4));
    transition: background-color .25s ease, box-shadow .25s ease;
}

.map-block:hover {
    background: rgb(var(--c, var(--flat-rgb)) / calc(var(--a, 0.035) * 2.6 + 0.12));
    box-shadow: inset 0 0 0 1px rgb(var(--c, var(--flat-rgb)) / 0.7);
    z-index: 2;
}

/* -- the company blocks ---------------------------------------------------- */

/* A company block is its mark and nothing else.

   It used to print a ticker, a delta and a market cap, and all three were the
   wrong things to spend a 30px box on: the block's fill already IS the delta,
   its area already IS the cap, and three lines of 7px type in a box that small
   is texture rather than reading. Every one of those figures is in the hover
   card at full size. So the block carries the one thing nothing else on the
   page can carry — which company this is — and gives it the whole box.

   The block itself carries NO padding: padding is a floor on how small a
   border-box can be, and these are the blocks that genuinely need to reach
   3px. The mark is held off the edges by a percentage inset on `.map-mark`
   instead, which costs a large block the same breathing room a fixed padding
   gave it and costs a 4px block almost nothing. */
.map-block:not(.is-rest) {
    align-items: center;
    justify-content: center;
    padding: 0;
}

/* The deltas stay in the DOM and go dark. The hover card builds its five-window
   table by reading these very spans off the block under the cursor, which is
   what stops the card from ever contradicting the block it describes — so they
   are hidden, not deleted. Hiding the wrapper rather than the `.win` spans is
   the same call the disclosure queries below make: the spans are driven by the
   timeframe selectors in section 3d and a rule here would have to out-specify
   them, while the wrapper has no competing rule at all. */
.map-block:not(.is-rest) .map-deltas { display: none; }

/* The box the mark is fitted into: the block inset by a fraction of itself.
   A percentage rather than a length so it scales — 8% is ~12px of margin on a
   150px block and a third of a pixel on a 4px one, where a fixed inset would
   have swallowed the block whole (and, as padding, would have stopped the
   block from being 4px at all). */
.map-mark {
    position: absolute;
    inset: 8%;
    min-width: 0;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--fg);
    opacity: 0.88;
}

/* Drawn as a mask filled with currentColor, not as an <img>. The vendored
   marks are `fill="currentColor"`, but an <img> renders its SVG in a separate
   document where currentColor resolves to that document's initial black — so
   in dark mode every mark would have been a black smudge on ink. Masking makes
   the mark take the page's own text colour in both themes from one file.

   Absolutely filling its box, with `contain` doing the fitting: that scales the
   mark up to whichever of the two dimensions binds and centres it in the other,
   so a square mark fills a square block and a wide wordmark spans a wide one —
   without either being distorted, and without the stylesheet needing to know
   which shape any given mark is. */
.map-logo {
    position: absolute;
    inset: 0;
    background: currentColor;
    -webkit-mask: var(--mark) center / contain no-repeat;
    mask: var(--mark) center / contain no-repeat;
}

/* The fallback for a company with no vendored mark. Same slot, centred the same
   way, and sized to the block rather than to a line of text — so a map of marks
   and tickers still reads as one field rather than as marks next to holes. */
.map-ticker {
    max-width: 100%;
    font-size: 8px;
    font-size: clamp(6.5px, min(30cqh, 20cqw), 15px);
    font-weight: 700;
    letter-spacing: -0.02em;
    line-height: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: clip;
}

/* -- the remainder block, which carries the map's caption ------------------ */

/* Largest block on all three maps by a wide margin, so it gets its own type
   scale: at the company-block scale — sized for a 20px square — the one block
   the reader is meant to notice would have been the emptiest thing on screen. */
.map-title {
    font-size: 11px;
    font-size: clamp(9px, min(10cqh, 7.5cqw), 17px);
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    line-height: 1.15;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The TOTAL market cap, not this block's own value. The block is the leftover
   and looks like it; the number the whole map is a division of has nowhere
   else left to go, and it is the only figure that lets one map be compared
   with the next (each is drawn to its own box, so their areas are not
   comparable by eye). The block's own value is in the hover card. */
.map-total {
    font-size: 13px;
    font-size: clamp(11px, min(13cqh, 10cqw), 27px);
    font-weight: 600;
    line-height: 1;
    color: var(--fg);
    white-space: nowrap;
}

.map-sub {
    font-size: 8px;
    font-size: clamp(6.5px, min(4.5cqh, 3.4cqw), 10px);
    letter-spacing: 0.04em;
    line-height: 1.3;
    color: var(--fg-mute);
    overflow: hidden;
}

.is-rest .map-delta {
    font-size: clamp(11px, min(11cqh, 9cqw), 26px);
}

.is-rest .map-foot {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 3px;
    min-width: 0;
}

/* -- progressive disclosure ------------------------------------------------ */

/* The caption block's own disclosure. It is the largest block on all three maps
   by a wide margin, so these thresholds are a floor it never reaches at desktop
   sizes — they exist for the phone layout, where a map is 340px tall and the
   remainder of the US map is genuinely small. Order: the total first (the area
   already says it), then the delta (the fill already says it). */
@container (max-height: 44px), (max-width: 58px) {
    .is-rest .map-foot { display: none; }
}

@container (max-height: 30px), (max-width: 44px) {
    .is-rest .map-deltas { display: none; }
}

/* A mark and a ticker do not survive to the same size, so they do not get the
   same threshold. A 12px square is still a recognisable NVIDIA eye; 12px of
   text is one and a half characters, which is worse than nothing — it reads as
   a different company. The text-only blocks therefore go dark first, and that
   is exactly where a vendored mark earns its place: with the mark now filling
   the whole block, the marks stay legible a long way past the point where a
   ticker has to be dropped. */
@container (max-height: 14px), (max-width: 22px) {
    .map-ticker { display: none; }
}

/* Below this even a mark is a smudge, and the block is pure colour with the
   hover card as its only reading. Nothing is lost that the card does not have. */
@container (max-height: 9px), (max-width: 9px) {
    .map-mark { display: none; }
}

/* -----------------------------------------------------------------------------
   3f. AI ranking panels
   --------------------------------------------------------------------------
   Two rankings, not sets of levels, so they are drawn as one: rank, flag, name,
   score, bar. Ten labs as ten tiles would have said "here are ten numbers" and
   left the reader to sort them by eye; the bars sort them before the reader
   arrives, which is the whole reason this block is not part of the grid.

   Neither panel carries a delta or a signal colour (see mockdata.py), which is
   what makes two of them fit here: the row is rank + flag + name + score, and a
   red/green column would have cost the width the second panel now occupies.
   With no direction to show, the rows stay neutral and the bars are indigo —
   the brand colour, kept as the *measurement* colour, so green and red go on
   meaning direction and nothing else anywhere on this page.
   -------------------------------------------------------------------------- */
.board-side {
    min-width: 0;
    min-height: 0;
    display: grid;
    grid-template-columns: var(--side-w) var(--side-w);
    gap: clamp(0.5rem, 1.2vw, 1.25rem);
    overflow: hidden;
}

.panel {
    min-width: 0;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

/* The panel head is the group head at panel width, so the credit and the title
   have to share ~140px in the worst case. */
.panel .group-head { gap: 0.5rem; }

.rank-list {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: var(--gap);
    margin: 0;
    padding: 0;
    list-style: none;
    overflow: hidden;
}

/* Same deal as the tiles: rows divide the panel's leftover height between
   themselves, and each is a size container so its type follows suit. */
.rank-row {
    container-type: size;
    flex: 1 1 0;
    min-height: 0;
    overflow: hidden;
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr);
    align-items: center;
    column-gap: clamp(4px, 4cqw, 8px);
    padding: clamp(4px, 8cqh, 9px) clamp(6px, 5cqw, 10px);
    border: 1px solid var(--line-soft);
    border-radius: 4px;
    background: var(--tile-bg);
}

.rank-num {
    font-size: 8.5px;
    font-size: clamp(7px, 17cqh, 10px);
    font-weight: 500;
    color: var(--fg-mute);
    letter-spacing: 0.04em;
}

/* The site-wide round flag set — same files the globe tooltip uses. */
.rank-flag {
    width: clamp(10px, 24cqh, 14px);
    height: clamp(10px, 24cqh, 14px);
    border-radius: 50%;
    display: block;
    box-shadow: 0 0 0 1px var(--line);
}

/* A lab with no known home country. Keeps the flag slot's width so the
   column of names stays straight, and reads as an absence rather than as a
   missing image. */
.rank-flag-none {
    box-shadow: inset 0 0 0 1px var(--line);
    opacity: 0.5;
}

.rank-body {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: clamp(1px, 3cqh, 3px);
}

/* The name gets a line to itself, because at panel width it is the longest
   string in the row: "Gemini 3 Flash" beside "11.2%" ellipses at every desktop
   size, and a truncated name in a ranking is the one thing the row cannot
   afford to lose. The score drops to the sub-line, where it is short enough to
   sit beside the lab name without either of them giving way. */
.rank-name {
    font-size: 11px;
    font-size: clamp(8.5px, 21cqh, 12px);
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.rank-meta {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    min-width: 0;
}

/* Pinned right by its own margin rather than by space-between, because the sub
   beside it is dropped on short rows and the score must not slide left to the
   start of the line when that happens. It is the bar's actual value, so it
   never goes. */
.rank-score {
    margin-left: auto;
    flex: none;
    font-size: 9.5px;
    font-size: clamp(8px, 17cqh, 10.5px);
    font-weight: 500;
    color: var(--fg-soft);
    white-space: nowrap;
}

/* Which model earned the lab its rank, and which lab owns the model. Context,
   not the ranking — first thing to go when the rows get short. The threshold is
   against the row's *content* box, which is what a size container queries. */
.rank-sub {
    min-width: 0;
    font-size: 8.5px;
    font-size: clamp(7px, 15cqh, 9.5px);
    color: var(--fg-mute);
    letter-spacing: 0.03em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

@container (max-height: 30px) {
    .rank-sub { display: none; }
}

.rank-bar {
    height: clamp(2px, 6cqh, 4px);
    border-radius: 2px;
    background: var(--line-soft);
    overflow: hidden;
}

.rank-bar > span {
    display: block;
    height: 100%;
    border-radius: 2px;
    background: rgb(var(--bar) / 0.85);
}

/* What the bars are drawn against — floored below the last row for Elo, a true
   zero for usage share. A choice a reader is entitled to know about instead of
   having to infer it from lengths that do not divide the way they look. */
.rank-axis {
    flex: none;
    margin: 0;
    padding-top: 5px;
    font-size: 8px;
    letter-spacing: 0.06em;
    line-height: 1.35;
    color: var(--fg-mute);
    text-align: right;
}

/* A panel whose source has never returned anything. It keeps its heading and
   its credit and says what is missing, rather than disappearing and leaving
   the board looking complete — the same reason a tile with no data prints a
   dash instead of being dropped from its row. Centred in the space the rows
   would have filled, so the gap reads as deliberate. */
.rank-empty {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    font-size: 9px;
    text-transform: uppercase;
    opacity: 0.55;
}

/* -----------------------------------------------------------------------------
   3g. Sources popover
   --------------------------------------------------------------------------
   Was a footer row under the board; now one click away behind the info
   button, which hands that row's height to the treemaps. A native popover
   sits in the top layer, so .board's overflow cannot clip it. Pinned under
   the control bar at the right, where the button that opens it lives. */
.sources-pop {
    position: fixed;
    inset: 3.2rem 1.25rem auto auto;
    margin: 0;
    width: min(34rem, calc(100vw - 2.5rem));
    max-height: calc(100dvh - 5rem);
    overflow: auto;
    padding: 0.7rem 0.9rem 0.8rem;
    border: 1px solid var(--card-line);
    border-radius: 5px;
    background: var(--card-bg);
    box-shadow: var(--card-shadow);
    color: var(--fg);
    font-family: 'JetBrains Mono Variable', monospace;
}

.sources-pop::backdrop { background: transparent; }

.sources-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding-bottom: 0.45rem;
    border-bottom: 1px solid var(--card-line);
}

.sources-head .theme-btn { font-size: 16px; line-height: 1; }

.sources-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.sources-list li {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0.1rem 1rem;
    padding: 0.45rem 0;
    border-bottom: 1px solid var(--line-soft);
    font-size: 10px;
    line-height: 1.4;
}

.sources-list a {
    grid-column: 1;
    color: var(--fg);
    font-weight: 600;
    text-decoration: none;
    border-bottom: 1px solid var(--line);
    justify-self: start;
}

.sources-list a:hover { border-color: var(--fg-mute); }

.sources-detail {
    grid-column: 1;
    font-size: 9px;
    color: var(--fg-mute);
}

.sources-licence {
    grid-column: 2;
    grid-row: 1 / span 2;
    align-self: center;
    font-size: 9px;
    letter-spacing: 0.04em;
    color: var(--fg-soft);
    text-align: right;
}

.sources-note {
    margin: 0.55rem 0 0;
    font-size: 9px;
    color: var(--fg-mute);
}

.foot-flag {
    margin-left: auto;
    color: var(--fg-mute);
    text-transform: uppercase;
    letter-spacing: 0.18em;
}

/* -----------------------------------------------------------------------------
   3h. Hover card
   --------------------------------------------------------------------------
   Replaces the native `title` tooltip, which was the wrong instrument three
   ways: it waits half a second, it renders in the OS font at the OS size in a
   grey box belonging to no design, and it is a single string — so a 20px
   treemap block could say "NVIDIA — $4.92T" and nothing about the four
   windows the reader cannot see.

   This card is the page's own type and palette, appears on contact, and shows
   every window at once with the active one marked. board.js builds it from the
   hovered element's own delta spans, so it cannot drift out of step with the
   block it is describing.

   One card for the whole board, moved by transform. `position: fixed` escapes
   the board's `overflow: hidden` because nothing between them establishes a
   containing block (no transform, no filter, no will-change) — if one is ever
   added to an ancestor this card will start clipping, and this is the note
   that explains why.
   -------------------------------------------------------------------------- */
.tipcard {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 60;
    width: max-content;
    max-width: 17rem;
    padding: 0.6rem 0.7rem 0.55rem;
    border: 1px solid var(--card-line);
    border-radius: 5px;
    background: var(--card-bg);
    box-shadow: var(--card-shadow);
    color: var(--fg);
    /* Not display:none — the card is measured before it is placed (placeCard
       reads its own box to decide which way to flip), and a display:none
       element measures zero. */
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity .12s ease;
}

.tipcard.is-on {
    opacity: 1;
    visibility: visible;
}

.tipcard-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}

/* Round when it is a flag, square when it is a brand mark — the shape says
   which kind of thing it is before the label is read. */
.tipcard-icon {
    flex: none;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    box-shadow: 0 0 0 1px var(--card-line);
}

.tipcard-icon.is-mark {
    border-radius: 3px;
    box-shadow: none;
}

.tipcard-heading {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.tipcard-title {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1.2;
}

.tipcard-sub {
    font-size: 9px;
    letter-spacing: 0.06em;
    color: var(--fg-mute);
}

/* The headline number and what qualifies it — "$4.92T" beside "#1 · 6.91% of
   market". The rank and the share are the two facts a treemap block encodes in
   its position and its area and can never print. */
.tipcard-figure {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    margin-top: 0.5rem;
    padding-top: 0.45rem;
    border-top: 1px solid var(--card-line);
}

.tipcard-value {
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
}

.tipcard-meta {
    font-size: 9px;
    letter-spacing: 0.04em;
    color: var(--fg-soft);
    text-align: right;
    white-space: nowrap;
}

/* All five windows. This is the part a native tooltip could never have been:
   the board shows one window at a time by design, and the question hovering
   asks is what the other four say. */
/* Six-month line. Neutral ink for the line; the active window is shaded at
   the right edge in its direction's colour, and the newest point is dotted,
   so the chart answers "where does the number on the tile sit in its recent
   history" without a second legend. */
.tipcard-chart {
    margin-top: 0.5rem;
}

.tipcard-spark {
    display: block;
    width: 208px;
    max-width: 100%;
    height: 48px;
    overflow: visible;
    --spark-rgb: var(--flat-rgb);
}

.tipcard-spark[data-dir="up"] { --spark-rgb: var(--up-rgb); }
.tipcard-spark[data-dir="down"] { --spark-rgb: var(--down-rgb); }

.tipcard-spark-window { fill: rgb(var(--spark-rgb) / 0.16); }
.tipcard-spark-area { fill: rgb(var(--flat-rgb) / 0.10); stroke: none; }
.tipcard-spark-line {
    fill: none;
    stroke: var(--fg-soft);
    stroke-width: 1.3;
    stroke-linejoin: round;
    stroke-linecap: round;
}
.tipcard-spark-dots circle { fill: var(--fg-soft); }
.tipcard-spark-end { fill: rgb(var(--spark-rgb)); stroke: var(--card-bg); stroke-width: 1; }

.tipcard-spark-axis {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    margin-top: 2px;
    font-size: 8px;
    letter-spacing: 0.08em;
    color: var(--fg-mute);
    white-space: nowrap;
}

.tipcard-windows {
    width: 100%;
    margin-top: 0.45rem;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}

.tipcard-windows th,
.tipcard-windows td {
    padding: 1.5px 0;
    font-weight: 500;
    line-height: 1.35;
}

.tipcard-windows th {
    text-align: left;
    font-size: 8.5px;
    letter-spacing: 0.16em;
    color: var(--fg-mute);
}

.tipcard-windows td {
    text-align: right;
    font-size: 10.5px;
    white-space: nowrap;
    color: var(--fg-soft);
}

/* Same colour vocabulary as the board — green up, warm red down — but read
   straight off each row's own direction rather than through the .sig
   machinery, because the card shows all five directions at once and --c only
   ever holds the active one. */
.tipcard-windows tr[data-dir="up"] td { color: rgb(var(--up-rgb)); }
.tipcard-windows tr[data-dir="down"] td { color: rgb(var(--down-rgb)); }
.tipcard-windows tr[data-dir="na"] td { color: var(--fg-mute); }

/* The window the board is currently showing, so the card is anchored to what
   is on screen instead of being five numbers with no reference point. */
.tipcard-windows tr.is-on th,
.tipcard-windows tr.is-on td {
    font-weight: 700;
}

.tipcard-windows tr.is-on th {
    color: var(--fg);
}

.tipcard-windows tr.is-on th::before {
    content: "\203A";
    margin-right: 0.3em;
    color: var(--fg-mute);
}

/* The other rows are indented past where that marker sits, or the whole label
   column jitters sideways every time the timeframe changes. */
.tipcard-windows tr:not(.is-on) th {
    padding-left: 0.72em;
}

.tipcard-note {
    margin: 0.45rem 0 0;
    padding-top: 0.4rem;
    border-top: 1px solid var(--card-line);
    font-size: 8px;
    letter-spacing: 0.05em;
    line-height: 1.4;
    color: var(--fg-mute);
}

/* -----------------------------------------------------------------------------
   4. Breakpoints
   --------------------------------------------------------------------------
   Column count per breakpoint. board.js reads the resolved count back out of
   the grid, so these numbers live in exactly one place. Ten is the desktop
   count all the way down to 1024px rather than stepping down with the width,
   because ten is what the *content* is built on: each group holds a multiple
   of ten tiles, and dropping to nine turns the money row into 9 + 1 and the
   index block into three ragged rows. The type is container-query-sized, so
   what narrowing costs is a smaller delta, not a broken row — and a smaller
   delta beats a layout that stops meaning anything.

   Below 1024px the page is allowed to scroll: forty tiles and three treemaps
   crushed into a phone viewport would be a grid of unreadable specks, and an
   honest scroll beats a layout that technically fits. The rankings stack under
   the tiles there, tiles and treemap plots take explicit heights, and the
   groups stop competing for space.
   -------------------------------------------------------------------------- */
@media (max-width: 1023px) {
    .board {
        --cols: 4;
        --gap: 7px;
        height: auto;
        min-height: 100dvh;
        overflow: visible;
    }

    .board-main {
        grid-template-columns: minmax(0, 1fr);
        gap: clamp(0.4rem, 1vh, 0.9rem);
        overflow: visible;
    }

    .board-grid { overflow: visible; }

    .group-grid {
        grid-auto-rows: 86px;
    }

    /* The plot is absolutely positioned children and nothing else, so it has
       no intrinsic height at all. On a desktop it inherits one from the flex
       column; here the groups no longer divide a fixed viewport, so it has to
       be told. A hundred blocks in 320px means the tail is single pixels —
       that is the honest picture of a market this concentrated, and the hover
       card is how you read the small end anyway. */
    .group-maps { flex: none; }
    .map-plot { height: 320px; }
    .maps { gap: 8px; }

    /* Size containment needs a height that does not come from the content, so
       the rows take an explicit one here rather than the share-of-panel they
       get on a desktop — an auto-height size container collapses to nothing. */
    .board-side {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        overflow: visible;
    }

    .panel { overflow: visible; }
    .rank-list { overflow: visible; }

    .rank-row {
        flex: none;
        height: 52px;
    }

    .board-bar {
        height: auto;
        flex-wrap: wrap;
        gap: 0.5rem 1rem;
        padding: 0.55rem 0;
    }

    .board-switches { margin-left: 0; width: 100%; }
    .board-meta { margin-left: auto; }
}

@media (max-width: 719px) {
    .board { --cols: 3; }
    .board-switches { gap: 0.75rem; flex-wrap: wrap; }

    /* Two ten-row rankings side by side at phone width is two columns of
       ellipsis; they go one under the other and keep their names. */
    .board-side { grid-template-columns: minmax(0, 1fr); }

    /* A third of a phone width is ~110px, in which a hundred blocks are sub-
       pixel. One map per row, taller, is the only arrangement where the top
       twenty or so stay blocks rather than a texture. */
    .maps {
        grid-template-columns: minmax(0, 1fr);
        gap: 1rem;
    }

    .map-plot { height: 340px; }

    /* Touch has no hover, and board.js already ignores touch pointers — but a
       stylus or a trackpad on a small screen would still summon a card wider
       than the viewport. */
    .tipcard { max-width: min(17rem, calc(100vw - 2rem)); }
}

@media (max-width: 479px) {
    .board { --cols: 2; }

    /* The credit stacks under the group name rather than being dropped — it is
       the attribution the data is licensed on, so it does not get to be the
       thing that disappears when space runs short. */
    .group-head {
        flex-direction: column;
        align-items: flex-start;
        justify-content: center;
        gap: 1px;
        height: auto;
        padding-bottom: 3px;
    }

    .group-credit { text-align: left; }
}

/* -----------------------------------------------------------------------------
   5. Motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .board * {
        transition-duration: 0.01ms !important;
    }
}
