/* IBM Plex (latin), self-hosted: vendored from jsDelivr into assets/fonts/plex
   so no visitor IP reaches fonts.googleapis.com or fonts.gstatic.com (GC-268).
   SIL OFL 1.1, licence alongside the files.

   Sans is the variable weight axis (45KB, one file, all weights). Mono has no
   variable build, so 400 for numbers and 600 for KPI figures and emphasised
   cells; adding a weight later means another vendoring round, which is why
   there are two rather than one.

   These are the app's faces: GC-270 flipped --font-sans to "IBM Plex Sans"
   on 2026-09-11 and retired roboto-latin.woff2 with it. The tail behind them is
   the system stack, never a second webfont - shipping a fallback face means
   shipping it forever. */
@font-face {
    font-family: "IBM Plex Sans";
    font-style: normal;
    font-weight: 100 700;
    font-display: swap;
    src: url("/assets/fonts/plex/plex-sans-latin-var.woff2") format("woff2");
}

@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/assets/fonts/plex/plex-mono-latin-400.woff2") format("woff2");
}

@font-face {
    font-family: "IBM Plex Mono";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/assets/fonts/plex/plex-mono-latin-600.woff2") format("woff2");
}

/* GC-270: the design tokens themselves now live in src/gencharts/design_tokens.py
   and are emitted to _tokens.css, which Dash loads before this file ("_" sorts
   ahead of letters). Surfaces, text greys, accents, the gutter, the radius, the
   public measure and the chart background are all defined there — consume them
   as var(--…) below and never repeat a literal.

   What stays here is *wiring*: values derived from a token, and the Bootstrap
   variables we pin to one. Regenerate the tokens with `make tokens`. */
:root {
    --background-color: var(--surface-page);
    --bs-primary: #50748f;
    --bs-primary-rgb: 80, 116, 143;
    /* One link blue app-wide (GC-270, 2026-09-16). This was `#50748f` resting
       and `#003f5c` on hover, which was wrong twice over.

       Resting at 3.41:1 on a card cleared the 3:1 floor for a graphical object
       but not the 4.5:1 that TEXT needs, so a unit link in an outage grid read
       dimmer than the body text beside it - the one clickable thing on the row
       was the least legible thing on it.

       And hover went DARKER: `#003f5c` is 1.50:1, so hovering a link made it
       harder to read than not hovering it. Same defect as the sidebar's
       `--hover-text-color` the same day, and the same cause - hover states
       written as "go darker", which is a light-theme instinct.

       `--tertiary-color` (#9ecbed) was already the house hyperlink blue, just
       scoped to the auth pages. 9.85:1 on card, 10.38:1 on page: AAA for text,
       clearly above body copy, and now one value everywhere instead of two. */
    --bs-link-color: var(--tertiary-color);
    /* Bootstrap's `a` rule is `rgba(var(--bs-link-color-rgb), …)`, NOT
       `var(--bs-link-color)` - so setting the colour alone changes nothing and
       cyborg's own triple keeps winning. Exactly the shape of the
       `--bs-body-color-rgb` / `.text-muted` trap already recorded in GC-270.
       #9ecbed = 158, 203, 237. */
    --bs-link-color-rgb: 158, 203, 237;
    --bs-link-hover-color: #cbe3f5;
    /* Defensive pin. The vendored Bootswatch cyborg (5.3.6) redefines --bs-body-bg
       (#060606) on :root; if it loads after this file it wins the specificity tie
       and turns the page background black. Root cause is fixed in app_factory
       (assets_path_ignore keeps vendor/ out of auto-loaded assets so cyborg loads
       before this file); !important keeps us safe if that ordering ever regresses. */
    --bs-body-bg: var(--surface-page) !important;
    /* The decomposed triple Bootstrap uses for rgba() compositing. It cannot
       read the token, so it is a literal that has to be kept in step by hand:
       this said 23, 23, 31 (#17171f) against a --surface-page of #17171e, one
       level out in blue. Same stale-copy class as the var() fallbacks dropped
       on 2026-09-12. */
    --bs-body-bg-rgb: 23, 23, 30 !important;
    /* The foreground half of the pin above, and it was missing until
       2026-09-12: `--bs-body-bg` read `--surface-page` while this sat commented
       out, so Bootstrap's `body { color }` fell through to cyborg's #adafae and
       everything inheriting from body rendered it instead of the token. Measured
       on /skip-rate-overview before the fix: 73 text-bearing elements at #adafae
       (8.08 on page, 7.67 on card) against ADR-04's designed 10.5. Pointing it
       here moves 69 of them to 10.58 / 10.04 / 11.16, exactly ADR-04's targets.
       The dead value it replaces was #e0e0e0 - neither --text-color (#c7c7cb)
       nor --text-strong (#e0e0e2), just a stale literal. */
    --bs-body-color: var(--text-color);
    --bs-body-font-family: var(--font-sans);
    /* Cyborg also defines --bs-font-sans-serif (still Roboto), and three
       Bootstrap components - .tooltip, .popover, and one more - read *that*
       rather than --bs-body-font-family. Overriding only the body variable left
       those three on a face the app no longer ships, so they fell through to
       the system stack. Pin both (GC-270). */
    --bs-font-sans-serif: var(--font-sans);
}

/* The root is the reader's, not ours (GC-270 / ADR-05).

   This was `14px`, which overrode the browser font size someone may have
   set because they need it. `100%` inherits it instead. It is not a text
   setting: Bootstrap expresses ~1,100 dimensions in rem, so the root also
   scales every control height, padding and radius. dmc computes rem
   against a 16px base, so at 14px every Mantine component rendered ~12.5%
   under its designed size - this restores them rather than inflating them.

   Density is bought back from the spacing ladder (ours to own), not by
   shrinking controls people click. */
html {
    font-size: 100%;
}

/* Drop card padding */
.card-body {
    padding: 0.75rem !important;  /* 12px */
}

/* GC-305: drop card borders. Cards read as a raised surface, never a border —
   "a border OR a raised surface, never both, never nested." No card sets an
   inline border, so this global rule removes Bootstrap's default .card hairline
   everywhere (framed_card and raw dbc.Card alike). */
.card {
    /* Driven by the --card-border token so the two-surface model (no card tint,
       a hairline instead) is reachable without editing this rule. Default none:
       GC-305's "a border OR a raised surface, never both". */
    border: var(--card-border) !important;
}

/* GC-305: card headers are part of the one flat card surface — drop Bootstrap's
   default border-bottom hairline (a border) and cap tint. */
.card-header {
    border-bottom: none !important;
    background-color: transparent !important;
}

/* GC-365: the page band — `components/page_band.py`. The page's own heading,
   one level above the chart headings below it, and the home for tokens that are
   true of the whole page rather than of any chart on it.

   28px is the "today" scale's page-title step (pages/design/hierarchy.py), the
   one size in that scale no page has ever rendered — every app page opened
   straight onto its controls. It sits a clear step above the 20px chart title
   without reaching the 2.5rem the public content pages use, which would read as
   a marketing headline on a dense grid.

   Left-aligned and flush with the chart headings below, so the page has one
   vertical line down its left edge: band, controls, every chart heading. */
.gc-page-band {
    /* The page's title block is a page-level block, and what follows it is
       another, so it takes `--gap-block` like any other. 20px was off the
       ladder entirely. */
    margin-bottom: var(--gap-block);
    /* Own formatting context, same reason as `.gc-chart-head`: without it the
       title's bottom margin escapes the wrapper and the gap to the controls
       below changes depending on whether a units span is present. */
    display: flow-root;
}

/* Baseline-aligned, so the units sit on the heading's baseline rather than
   centring against its cap height. `flex-wrap` keeps a long heading from
   pushing the unit off the row on a narrow viewport. */
.gc-page-band-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
}

.gc-page-title {
    /* `--text-strong` is what ADR-04 added it for: "text hierarchy is meant to
       grow at the top", and this is the top. First consumer of the token. */
    color: var(--text-strong);
    font-size: var(--type-page-title);
    font-weight: var(--weight-page-title);
    line-height: var(--leading-page-title);
    letter-spacing: -.01em;
    margin: 0;
}

/* Section band — `components/page_band.section_band()`. One level down: what
   this block of charts is, for a token constant across a section but not
   across the page.

   22px is the "today" scale's section step, and the point is that it sits
   BETWEEN the band (28) and the chart title (20). The two section headings the
   app already had (`ic_group`, /solar's bias test) were both `1.25rem`, which
   against the 14px root renders 17.5px — smaller than the chart titles they
   contain. An inverted hierarchy is worse than a flat one: it misstates what
   contains what. */
.gc-section-band {
    /* A new section is a new idea, so it takes the top of the ladder rather
       than the same gutter the cards inside it use. It was on `--gc-gutter`,
       which made a section boundary and a card gap the same size - no signal
       at all that a section had started. */
    margin-top: var(--gap-section);
    margin-bottom: var(--gap-heading);
    display: flow-root;
}

/* Opt-in via `section_band(sticky=True)`. Pins the band while its *section* is
   on screen, which is the behaviour a hoisted key actually wants: a sticky
   element sticks only within its containing block, so a band sharing a wrapper
   with its grid arrives with the grid and leaves with it.

   `top: 0` for the same reason `.gc-page-band-sticky` uses it - the navbar is
   in normal flow and scrolls away, so there is nothing above to clear. A page
   pinning BOTH bands would have to give this one the page band's height
   instead, which is why nothing does: once the key has moved down here, the
   page band is carrying only a title and has no case for being pinned.

   Below `.gc-page-band-sticky`, so a page that somehow pins both scrolls this
   one under that one rather than over it. Both sit BELOW Bootstrap's
   `.dropdown-menu` (1000) - see the page band's note. */
.gc-section-band-sticky {
    position: sticky;
    top: 0;
    z-index: 900;
    /* Opaque, or the grid scrolls through it. Same reasoning as the page
       band: the band and the charts share the container's content width, so
       painting its own box is enough. */
    background-color: var(--surface-page);
    /* Vertical only - the band's left edge stays on the page's, with every
       chart heading below it. The top margin `.gc-section-band` carries would
       leave a transparent strip above the pinned band for content to show
       through, so it becomes padding here. */
    margin-top: 0;
    padding-top: var(--gap-section);
    padding-bottom: 10px;
}

.gc-section-title {
    color: var(--text-strong);
    font-size: var(--type-section);
    font-weight: var(--weight-section);
    line-height: var(--leading-section);
    letter-spacing: -.01em;
    margin: 0;
}

/* Facility metadata as page text, not a card (GC-399).

   ADR-02 reserves the card for data - a figure, a grid, a table - and this is
   five label/value pairs. In a card it read as an empty panel at any shallow
   tint, the same failure the collapsible strip headers had before GC-270.

   A real `<dl>` replaces runs of `html.Div([html.Strong("Label: "), value])`,
   where the pairing was carried only by a colon inside the bold text and the
   values could not align. The grid buys that alignment.

   `max-content` on the terms rather than a fixed width: the two facility twins
   carry different labels ("Import capacity" against "Gen capacity"), and a
   column tuned to one would wrap or strand the other. */
.gc-facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: var(--space-4);
    row-gap: var(--space-2);
    margin: 0;
}

/* The term recedes and the value does not: the labels are the same five words
   on every facility, and the reader is here for the values. Body weight, not
   bold - the old markup used `<strong>` to mean "this is the label", which is
   emphasis standing in for structure the `<dl>` now carries. */
.gc-facts dt {
    color: var(--inactive-text-color);
    font-weight: var(--weight-body);
}

.gc-facts dd {
    color: var(--text-color);
    margin: 0;
}

/* The beta disclaimer: a caption on the block above it rather than another
   fact, so it takes the caption role and sits outside the list. */
.gc-facts-note {
    color: var(--inactive-text-color);
    font-size: var(--type-caption);
    line-height: var(--leading-caption);
    margin: var(--space-4) 0 0;
}

/* The unit shared by the charts below, set beside the heading rather than
   inside it — it is a note about the charts, not part of the name, and styling
   the halves apart is the reason both bands take them as two values. Shared by
   page and section: the same note at two altitudes reads the same way.

   Muted at `--inactive-text-color` (5.56:1), NOT `--text-faint` (3.0:1).
   Faint is the disabled/placeholder step, and ADR-04 is explicit that captions
   do not get dimmer to make room at the top. A unit is information: drop it
   below 4.5:1 and the one word telling the reader what scale they are looking
   at is the least legible thing on the page. Size does the subordinating. */
.gc-band-units {
    color: var(--inactive-text-color);
    font-size: var(--type-chart-subtitle);
    font-weight: var(--weight-chart-subtitle);
}

/* One line under the heading saying what the page is. Replaces the short
   `page_summary` blocks, which put a 12-word sentence behind a disclosure
   toggle in 17px article type - chrome heavier than its content, and the wrong
   job for that component. Long descriptions keep the strip; orientation comes
   up here.

   16px muted, matching the chart subtitle's register rather than the article
   type: this is a caption on the page, not prose to read. `max-width` in ch
   keeps the measure readable on a wide screen - a single line running the full
   width of a 1500px page is hard to track back from. */
.gc-page-deck {
    color: var(--inactive-text-color);
    font-size: var(--type-chart-subtitle);
    line-height: var(--leading-chart-subtitle);
    margin: 6px 0 0;
    max-width: 90ch;
}

/* Reserved: a statement of what the charts are currently showing, as opposed to
   what the controls are set to. Not rendered unless a page passes one. */
.gc-page-readout {
    color: var(--inactive-text-color);
    font-size: var(--type-caption);
    margin-top: 4px;
}

/* Untested — opt-in via `page_band(sticky=True)`, and off everywhere today.
   The case for it is the hoisted legend, not the title: a legend is required to
   read the charts beneath it, so scrolling it away leaves a 50-chart grid
   unreadable. `top: 0` assumes the navbar is `position: fixed`; if it turns out
   to be in flow this needs its height as an offset, and the band needs the page
   background or content will scroll through it. */
.gc-page-band-sticky {
    position: sticky;
    top: 0;
    /* Above the page's own content, below anything the navbar opens.
       The navbar is NOT sticky - `make_navbar` passes no `sticky` or `fixed`
       prop, so it is in normal flow and scrolls away (app_factory.py:534 says
       as much about the sidebar: top:0 "still sticks to the top on scroll once
       the navbar scrolls away"). That is why `top: 0` is right rather than a
       navbar height, and why the band costs no viewport it has to share.

       Was 1010, reasoned as "above the page's own content, and there is nothing
       else pinned to clear". The thing that missed is the user menu: a navbar
       dropdown hangs DOWN into the band's territory while both are on screen,
       and at Bootstrap's `.dropdown-menu` z-index of 1000 it lost. The menu
       rendered with its middle masked by the band - the top of "Chris Page" and
       "Logout" visible either side of an opaque strip (GC-270, 2026-09-17).

       The rule this encodes: page chrome sits below app chrome. A band belongs
       to the page and a navbar menu belongs to the shell, so the band clears
       page content and nothing more. 910 keeps it one rung above the section
       band, which is the only ordering that was ever load-bearing. */
    z-index: 910;
    /* Opaque, or the grid scrolls through the pinned band. The band spans the
       container's content width and so does everything below it, so painting
       its own box is enough - no full-bleed negative margin, which would have
       to match the container's 1.5rem gutter and not the app's 1.6rem one. */
    background-color: var(--surface-page);
    /* Vertical padding only: the band's left edge stays on the page's, with the
       controls and every chart heading below it.

       Bumped by ~10px each side 2026-09-16: once pinned, the grid scrolled
       right up against the title and the band stopped reading as a separate
       layer. Off the 4px ladder deliberately - this is optical padding on a
       floating element, sized so the band clears the content moving under it,
       not a composition gap between two blocks. */
    padding-top: 18px;
    padding-bottom: var(--band-pad-bottom);
    /* The band was paying TWO separators at rest: this optical padding, and
       `.gc-page-band`'s `--gap-block` composition margin below it - 52px
       between the title and the controls it heads. house-rules, "A boundary
       takes one marker, not two". The margin absorbs the padding instead, so
       the gap under the title block is `--gap-block` whether the band is
       pinned or at rest, and the optical padding still does its job the moment
       content scrolls under it. */
    --band-pad-bottom: 20px;
    margin-bottom: calc(var(--gap-block) - var(--band-pad-bottom));
}

/* ── Legend (components/chart_legend.py, ADR-02, GC-374) ──────────────────────
   HTML on the page, not in the figure. One row, wrapping. `gap` carries all the
   spacing so an entry has no margins of its own and a wrapped second row sits at
   the same rhythm as the first.

   The ceiling is 14 entries at a ~400px grid cell, judged acceptable on the
   switchboard - so no cap, no "+4 more", no truncation. */
.gc-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 16px;
}

/* Above the chart, between the heading and the card (GC-374, reversing ADR-02's
   "below in the app").

   The switchboard judged position on a single specimen, where below the chart is
   unambiguous. In a stacked grid it is not: below chart N is also directly above
   chart N+1's TITLE, so the key sits nearer the heading it does not belong to
   than the one it does. Proximity decides what a reader takes a block to
   contain, and it beats reading order.

   `-8px` pulls the key up against its own title: `.gc-chart-head` carries a 12px
   bottom margin sized for the gap between a heading and a chart, and that is too
   much between a heading and the key that belongs to it. The remaining 4px
   groups them, and `margin-bottom` opens the gap to the card.

   `below` is kept for the export presentation and for any single chart standing
   alone, where the fault above does not exist. The band case is neither: it is
   above every chart once, inside a block that already owns its spacing. */
/* Names one of two keys in a band that carries more than one (GC-374,
   `/facility-overview`). The page lays the keys out in the same two columns as
   the grid below, so position already says which key belongs to which column -
   but `md=6` collapses to a single column on a narrow viewport, and the moment
   it does, position explains nothing and two unlabelled keys become a guess.

   Set like `.gc-band-units`, and for the same reason: one visual form for
   "this is a label on a thing", at every altitude. */
.gc-band-key-label {
    display: block;
    color: var(--inactive-text-color);
    font-size: var(--type-label);
    font-weight: 500;
    margin-top: 8px;
}

.gc-band-key-label + .gc-legend {
    margin-top: 2px;
}

.gc-legend-above {
    margin-top: -8px;
    margin-bottom: 10px;
}
.gc-legend-below { margin-top: 10px; }
.gc-legend-band  { margin-top: 8px; }

/* Card tops line up across a grid row, whatever the keys above them do.
   (`components.cards.chart_card`, GC-374.)

   Moving the key above the card put a variable-height element between every
   heading and its chart, so a chart whose key wraps to two lines started lower
   than the one beside it. Which charts are "beside" each other is not knowable
   from here - the grid is `repeat(auto-fit, minmax(...))`, so the column count,
   and therefore the row membership, changes with the viewport.

   Nothing needs to know. A grid item stretches to its row's height by default,
   and every card in the grid is the same fixed height, so the slack between them
   is a constant per row. Make the block a flex column and let the legend slot
   absorb that slack: the card is last and sized, so its top lands at the same
   offset in every block of the row. The browser redoes it on every reflow, with
   no measuring and no reserved band to guess at.

   It corrects heading drift by the same mechanism, which the `.gc-chart-head`
   min-height rule above handles only for the title-vs-title+subtitle case.

   `:has` is a guard, not a filter: `cards.chart_card` gives every block a slot
   (empty when the chart has no key), because a keyless chart in a grid row has
   the same slack to absorb as a keyed one. */
/* GC-374: the block is a `figure` now, so it inherits Bootstrap Reboot's
   `figure { margin: 0 0 1rem }` - a bottom margin the `div` it replaced never
   had, on every chart in the app. Spacing here is the grid's job (`gap`) and
   the page's (`spacer`), so the element contributes none of its own. */
.gc-chart-block {
    margin: 0;
}

.gc-chart-block:has(> .gc-chart-legend-slot) {
    display: flex;
    flex-direction: column;
}

.gc-chart-legend-slot {
    /* Grow into the slack, never shrink below the key's own height. */
    flex: 1 0 auto;
}

/* Pass the grid's stretch down to the block through a per-chart loading shim.

   The rule above only works when `.gc-chart-block` is the grid item, which is
   true on /balance and /by-fuel - they put `chart_card` straight into the grid.
   /solar and /wind wrap each cell in a per-chart `dcc.Loading` plus the div the
   callback writes into, so three boxes sit between the grid and the block. The
   grid stretches the outer one and the rest keep their content height, so the
   slot has no slack to absorb and the cards in a row start at different heights
   (GC-416, 2026-09-18).

   `display: contents` takes the shim boxes out of layout entirely - the block
   becomes a child of the cell, exactly as on /balance - and `:has` dissolves
   only the boxes on the path to a block, so `dcc.Loading`'s sibling spinner
   container keeps its box and the placeholder state is untouched. */
.gc-chart-grid > :not(.gc-chart-block) {
    display: flex;
    flex-direction: column;
}
.gc-chart-grid > :not(.gc-chart-block) div:has(.gc-chart-block) {
    display: contents;
}
.gc-chart-grid > :not(.gc-chart-block) .gc-chart-block {
    flex: 1 1 auto;
}

/* A real <button> (tabbable, takes the global button:focus-visible ring),
   styled back to plain text. A key that looked like a row of buttons would be
   louder than the chart it explains, and the legend is chrome. */
.gc-legend-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 0;
    background: none;
    border: none;
    color: var(--text-color);
    font-size: var(--type-legend);
    line-height: 1.3;
    cursor: pointer;
}

/* Non-interactive legends render a span, which has no cursor to reset but does
   inherit the flex box above. */
span.gc-legend-item { cursor: default; }

.gc-legend-item:hover .gc-legend-label { color: var(--text-strong); }

/* Hidden traces. The entry stays legible rather than fading out: it is the only
   way back, so the state has to read as "off", not as "unavailable". The swatch
   hollows out and the label goes muted with a strike - three signals, none of
   them colour alone (ADR-01). */
.gc-legend-item.is-hidden .gc-legend-label {
    color: var(--inactive-text-color);
    text-decoration: line-through;
}
.gc-legend-item.is-hidden .gc-legend-swatch { opacity: .35; }

/* Swatch by mark type. Plotly draws the same one for every scatter trace, so a
   filled band and the line over it get identical keys; they are not the same
   kind of mark and the key should not say they are. */
.gc-legend-swatch { flex: 0 0 auto; }
.gc-legend-swatch-area,
.gc-legend-swatch-marker {
    width: 11px;
    height: 11px;
    border-radius: 2px;
}
.gc-legend-swatch-line {
    width: 16px;
    height: 3px;
    border-radius: 1px;
}

/* GC-347: chart headings as HTML rather than a Plotly title. ADR-02 moved them
   out of the card onto the page: heading and card are siblings inside
   `.gc-chart-block` (components.cards.chart_card), sharing the page's left
   edge.

   Left-aligned on purpose. A Plotly title is centred over the plot area, which
   gives it no shared edge with the controls above it or the prose beside it, so
   a column of charts reads as a stack of posters. Flush with the card's left
   edge puts every heading on one vertical line down the page.

   The subtitle is not decoration - it carries the population and the scale
   caveat, and on the stage grids the caveat is what stops the accepted grid
   being read as comparable with the availability grid. Muted, but present. */
.gc-chart-title {
    color: var(--text-color);
    /* 20px: between the published article scale's h3 (18px) and h2 (22px).
       18 read small against a 560px grid, 22 reads as leading a section rather
       than labelling a chart. Set explicitly rather than left to Bootstrap's
       h3, which is 1.75rem / 24.5px and shouts over the chart it names. */
    font-size: var(--type-card-title);
    font-weight: var(--weight-card-title);
    line-height: var(--leading-card-title);
    letter-spacing: -.01em;
    margin: 0;
    text-align: left;
}

.gc-chart-subtitle {
    color: var(--inactive-text-color);
    /* 16px, a step ABOVE the 14px root. Deliberately not small print: this
       line carries the scale caveat, which a reader has to actually take in for
       the chart above it to be read correctly. Colour does the subordinating
       (muted against the title's full-strength text), size does not. */
    font-size: var(--type-chart-subtitle);
    font-weight: var(--weight-chart-subtitle);
    line-height: var(--leading-chart-subtitle);
    margin: 0;
    text-align: left;
}

/* Space below the heading unit, carried by the `.gc-chart-head` wrapper, not
   by the last line inside it. A child's bottom margin escapes the wrapper by
   margin collapse — and an EMPTY subtitle div (a callback-filled span before
   data, or an errored chart) self-collapses its margin entirely, so blocks in
   one grid row drifted 12px by whether their heading had arrived. The
   wrapper's own margin applies identically in every state. Title and subtitle
   stay tight to each other (2px). */
.gc-chart-title { margin-bottom: 2px; }
.gc-chart-subtitle { margin-bottom: 0; }
.gc-chart-head {
    margin-bottom: 12px;
    /* Own formatting context so children's margins are contained: without it
       a title-only head lets the title's 2px escape below the wrapper and
       card tops drift 2px against a titled+subtitled sibling. */
    display: flow-root;
}
/* A truly childless head (untitled chart) draws nothing and spaces nothing.
   Heads holding empty callback-target spans are NOT :empty — they keep the
   band and the gap, so nothing jumps when the heading text arrives. */
.gc-chart-head:empty { margin-bottom: 0; }

/* The chart card IS the chart surface (ADR-02).

   Until now there were two owners of that surface and they had to agree: the
   figure painted its own `paper_bgcolor` from Python, baked at import, and the
   card painted `--surface-card` from CSS. Wherever the two disagreed - which is
   every switchboard candidate the house tokens do not match - the card's
   padding rendered as a ring around the plot. ADR-02 sends the paper
   transparent, so this rule is now the only thing painting a chart's
   background, and the ring cannot come back.

   Corner and edge follow from the same change. `--corner-radius: 0` (the token
   at the top of this file) was conditional reasoning, not a flat preference:
   the card background and the figure's paper were the SAME token, so a rounded
   corner cut a notch out of a surface that visually continued past it and read
   as a stray line across the top of the chart. With a transparent paper the
   card is a real region with a real boundary, so 4px is a corner rather than an
   artefact - and a hairline is what makes the boundary legible at a surface
   step this shallow. Native radius, not `overflow: hidden`, so the figure is
   never clipped.

   Chart cards get their own radius token rather than `--corner-radius`, which
   has two live consumers and both are nav links. See design_tokens.CHART.

   Scoped to `.gc-chart-card`, not `.card`, deliberately: this is a chart-card
   decision, not a global one.

   The padding has to be a rule rather than an inline style because the global
   `.card-body { padding: 0.75rem !important }` beats anything a call site sets. */
.gc-chart-card {
    /* Bootstrap derives a card's header/footer/image corners from these, so
       setting only `border-radius` leaves a `.card-header` rounding at the
       framework's ~6px inside a 4px card - its background peeks past the
       corner. Set the vars and the children follow. */
    --bs-card-border-radius: var(--chart-radius);
    --bs-card-inner-border-radius: var(--chart-radius);
    background-color: var(--surface-card);
    /* !important because `framed_card` always adds Bootstrap's `border-0`
       utility, which is itself !important. style.css loads after the vendored
       bootstrap, so at equal weight this wins on order. */
    border-radius: var(--chart-radius) !important;
    border: var(--chart-border) !important;
    /* GC-347 pinned IBM Plex Sans here so the chart card matched the figure's
       own typeface while the rest of the app was still Roboto. GC-270 flipped
       --font-sans site-wide, so the card inherits it and the override is gone -
       the exception existed only for the gap between the two. */
}

/* The body is inside the radius, so it must not repaint the corners square.
   `framed_card` sets an inline `backgroundColor` on both the card and the body;
   the card's is what the radius clips, and the body's would square it off
   again at the corners if it stayed opaque. Let the card show through.

   Padding 0 (ADR-02): the figure's paper is transparent and the card paints
   the surface, so a padding band around the plot is card-coloured on
   card-coloured — 12px a side spending 24px of plot width (~6% of a 400px
   grid cell) to render nothing. Breathing room belongs in the figure's own
   margins, where Plotly spends it on axis labels. Scoped to chart cards:
   `framed_card` tables and summaries keep the global 12px, which is the
   "an AG Grid paints its own background and would show the band" caveat
   answered by scope rather than by a parameter. `!important` to beat the
   equally-!important global `.card-body` rule on specificity. */
.gc-chart-card > .card-body {
    background-color: transparent !important;
    padding: 0 !important;
}

/* A chart with no card still needs a surface (ADR-02 / GC-270).

   Transparent paper took the figure's own background away, and ~36 non-map
   charts were never in a card - they read as a panel only because the paper
   painted one. This gives them the same surface, corner and edge a carded
   chart gets, applied to the graph element itself so **no padding is added**:
   the rectangle is identical to the one the paper used to fill.

   That matters beyond tidiness. `/skip-rate-overview` puts a chart beside a
   map in one row and went bare precisely because a card's padding made the two
   content rectangles impossible to line up (see its comment). Padding here
   would reopen that. `box-sizing: border-box` is global, so the hairline eats
   into the declared height rather than adding to it.

   Maps stay bare. Their tiles are their own surface and GC-305 uncarded them
   for that reason. */
.gc-chart-surface {
    background-color: var(--surface-card);
    border-radius: var(--chart-radius);
    border: var(--chart-border);
}

/* `.gc-chart-card-headed` is retired (ADR-02 titles move): its only job was
   padding a heading drawn *inside* the card, and headings now sit on the page
   grouped in `.gc-chart-head`, a sibling of the card inside `.gc-chart-block`.
   The block needs no rule of its own — the heading's margins space it from
   the card — but the class names the heading+well pair as one addressable
   thing, and is where the HTML legend and `aria-labelledby` attach later. */

/* One consistent heading band inside a grid. A title-only heading (~26px) and
   a title+subtitle heading (~50px) are different heights, so card tops drift
   within a grid row by whether a chart happens to carry a subtitle. Reserve
   the two-line band: one title line (20px x 1.3) + its 2px gap + one subtitle
   line (16px x 1.4). Real content taller than the band (a wrapping title at a
   400px cell) still wins — min-height, not height. The 12px gap below the
   heading comes from the title/subtitle margin either way, so it needs no
   reserving. Scoped to grids because alignment is a grid problem: a
   standalone block should not pay a blank band for a subtitle it lacks.
   `:not(:empty)` spares untitled blocks entirely — the ONE case this does
   not solve is a grid mixing titled and untitled charts, which no page
   currently builds.

   Chosen over CSS subgrid (the elegant fix) because wind/solar grids nest
   blocks inside dcc.Loading wrappers, and subgrid only reaches a grid's
   direct children. Revisit if the loading wrappers ever go.

   `gc-align-heads` opts a non-grid row into the same band — for hand-built
   rows of blocks (a chart beside a map on /boa-by-unit) whose card tops
   should stay level when one heading has a subtitle and the other doesn't.

   GC-365: the `:has()` is on the CONTAINER, not the head. Once the page band
   took the unit off `/by-fuel` and `/interconnectors`, no chart in those grids
   has a subtitle at all, and every head was still paying 22px for a line
   nothing uses — twelve charts, six rows, ~130px of page. A container with no
   subtitle anywhere in it needs no band: its heads are all one line, so they
   are all the same height and alignment holds for free.

   Not `:has()` on the head itself, which is the obvious spelling and is wrong:
   it would size each head to its own content and let siblings drift, which is
   precisely what `gc-align-heads` exists to prevent. The question is not "does
   this head have a subtitle" but "does anything in this row", and only the
   container can answer it.

   Unsupported `:has()` drops the reservation rather than keeping it — the
   opposite fallback from the `.gc-markdown` rule below, and worth knowing:
   mixed grids would drift instead of aligning. Baseline since Firefox 121. */
.gc-chart-grid:has(.gc-chart-subtitle) .gc-chart-head:not(:empty),
.gc-align-heads:has(.gc-chart-subtitle) .gc-chart-head:not(:empty) {
    min-height: calc(20px * 1.3 + 2px + 16px * 1.4);
}

/* Row-of-card-columns: one gutter horizontally AND vertically so a stack of
   card rows has equal gaps in both directions. `--bs-gutter-x` spaces the
   columns; `margin-bottom` spaces this row from the next (replaces `mb-3`,
   which was 1rem — a different value, and !important, so it must be dropped
   from the className). Only gutter-x, not -y (gutter-y's negative top-margin
   would pull the row up into anything above it). */
.gc-row-gutter {
    --bs-gutter-x: var(--gc-gutter);
    margin-bottom: var(--gc-gutter);
}

/* GC-305: shared chart-grid rule. Cards have no borders, so the gutter is the
   only separator — keep it wider than the 0.75rem card padding. `gap` sets the
   row gap == column gap. Columns auto-fit to a min width, overridable per page
   via `--gc-chart-min`. Replaces the per-page inline `d-grid` + gap + template. */
.gc-chart-grid {
    display: grid;
    /* Tracks always fill (`1fr`). There is deliberately NO per-page track-max
       override any more - `--gc-chart-max` was removed 2026-09-16 (GC-270).

       It was a name-trap: it reads as "cap the chart", but it sizes the grid
       *track*, and `repeat(auto-fit, minmax(min, max))` counts repetitions from
       the max when that is definite. So setting it did not narrow a card, it
       deleted a column. Its only ever caller (`/interconnectors`) rendered ONE
       580px chart in a 1165px container - `floor((1165+24)/(580+24)) = 1` -
       where /balance fit two at 570.7px.

       The two things anyone actually wants are both still here:
         - narrower cards  -> `--gc-card-max` (clamps the item, column count
           unchanged; `--gc-card-max-narrow` is the small-multiples value)
         - fewer, wider columns -> raise `--gc-chart-min`, which reduces the
           track count and lets each track still expand to 1fr. */
    grid-template-columns: repeat(auto-fit, minmax(var(--gc-chart-min, 380px), 1fr));
    /* Row gap > column gap, deliberately (GC-270, 2026-09-16). They were both
       `--gc-gutter`, which made the vertical and horizontal relationships look
       identical - but they are not. Two cards side by side in one row are
       peers; a card and the *title of the chart below it* are unrelated, and
       that title needs to read as belonging to what follows it rather than to
       what it sits under.

       This does not fix the grouping on its own and is not meant to: a title
       sits ~87px above its own chart (subtitle slot + legend slot) against this
       gap above it, so no plausible row gap wins that contest. What closes it
       is filling the reserved subtitle space with content - see the note on
       `.gc-chart-head`'s min-height. This is the half that CSS can do. */
    column-gap: var(--gc-gutter);
    row-gap: var(--gap-block);
    margin-top: var(--gc-gutter);  /* separate the grid from the controls above it */
}

/* Clamps the card, not the track: auto-fit counts columns from the track's max
   when that is definite, so capping the track would cost a column (1523px
   fits 3x493 at `1fr`, only 2x720 at 720px). Only fires when a grid holds fewer
   cards than it has columns, where the survivors absorb the collapsed track and
   stretch to 750px+. */
.gc-chart-grid > * {
    max-width: var(--gc-card-max);
}

/* Every page's content ends flush against the footer without this: the grid's
   margin-bottom, `.container-fluid`'s padding-bottom and the body's were all
   zero, so the last chart card's edge and the footer's top edge were the same
   line (measured 2026-09-16, gap = 0px).

   It belongs here rather than on `.gc-chart-grid` because the last element on a
   page is not always a grid - it can be a table, a map or a prose block - and
   all of them want the same room before the chrome band starts.

   `--gap-section` rather than `--gap-block`: this is the boundary between all
   of a page's content and a different surface, which out-ranks a break between
   two blocks within it. */
#page-content {
    padding-bottom: var(--gap-section);
}

/* GC-270: the horizontal rule between page blocks (`components/divider.py`).

   The values live here rather than in Python for the same reason `.gc-spacer`'s
   do: a spacing ladder lands as a token swap in one rule, and a divider is pure
   chrome that never reaches a Plotly layout arg.

   It used to carry an inline `margin: 1.0rem 0`, which was wrong twice over -
   a literal, and in `rem`, so every rule in the app silently grew when ADR-05
   moved the root from 14px to 16px. Composition follows the ladder, not the
   reader (house-rules).

   `--hairline-width` and `--divider` rather than `1px` and a Python constant,
   so a thickness or tint change reaches all 57 pages that draw one. */
.gc-divider {
    height: var(--hairline-width);
    background-color: var(--divider);
    margin: var(--gap-rule) 0;
}

/* GC-270: vertical spacing between page-level blocks
   (`components/spacer.py`). Replaces 40 identical per-page
   `html.Div(style={"height": "20px", ...})` definitions.

   The value lives here rather than in Python so batch one's spacing ladder
   lands as a token swap in this one rule. A spacer is pure chrome and never
   reaches a Plotly layout arg, so unlike a surface colour it has no reason to
   exist on the Python side at all.

   No background: every spacer sits directly on the page, so the old
   `body_background` fill was page-on-page and painted nothing. */
.gc-spacer {
    /* `--gap-block`, not a literal: 20px was narrower than the 22.4px gutter
       beside it, so two whole page blocks sat closer together than two cards
       inside one of them. The composition claimed the blocks were the more
       closely related pair, which is backwards. Now 32px against a 24px
       gutter, so the break out-ranks the gap it contains (GC-270). */
    height: var(--gap-block);
}

/* `.gc-spacer-lg` retired 2026-09-16. It had **zero consumers** - its only
   reference anywhere was the re-export in `components/__init__.py` - and its
   32px is now what a plain `.gc-spacer` is worth, so it had become a synonym
   for the default as well as being unused. Same disposal as `--gc-chart-bg`
   and `--hover-text-color`: a rung nothing stands on cannot be judged, so it
   should not be tuned. */

/* GC-305: map (MapLibre/Mapbox) attribution pill — match the card surface and
   chart text instead of the default white pill with blue links. */
.maplibregl-ctrl-attrib,
.maplibregl-ctrl-attrib.maplibregl-compact,
.mapboxgl-ctrl-attrib,
.mapboxgl-ctrl-attrib.mapboxgl-compact {
    background-color: var(--surface-card) !important;
}
.maplibregl-ctrl-attrib,
.maplibregl-ctrl-attrib a,
.mapboxgl-ctrl-attrib,
.mapboxgl-ctrl-attrib a {
    color: var(--text-color) !important;  /* == colors.chart_text */
}
/* The ⓘ toggle: MapLibre bakes a near-black icon SVG — replace it with a
   chart_text one and match the surface, for both open and closed.
   The colour is inside a data: URI, so it is the one place on this page that
   cannot read a token and has to be kept in step by hand. It was #dadada, the
   pre-ADR-04 --text-color, until 2026-09-12. */
.maplibregl-ctrl-attrib-button,
.mapboxgl-ctrl-attrib-button {
    background-color: var(--surface-card) !important;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccccd0' d='M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z'/%3E%3C/svg%3E") !important;
}


/* Base / inactive navbar link */
.ec-nav-link {
  color: var(--inactive-text-color);
  border: 1px solid transparent;         /* optional subtle underline or border */
  border-radius: 0.25rem;                /* optional for focus ring */
  text-decoration: none;                 /* remove underline */
  transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
  line-height: 1.2;
  /* padding-top: 0.5rem;  */
  /* padding-bottom: 0.5rem; */
}

/* Base (inactive) */
/* Bootstrap's own variable, which is what every `.nav-link` already reads -
   so one declaration sizes all 52 sidebar links. It defaulted to 1rem, which
   put page links level with their section headers once the root moved to
   100%, and the column started shouting. One step under body (GC-270). */
:root {
    --bs-nav-link-font-size: var(--type-nav);
}

a.ec-nav-link {
  color: var(--inactive-text-color);
  background-color: transparent;
  border-radius: var(--corner-radius);
  text-decoration: none;
  transition: color .15s ease, filter .15s ease, background-color .15s ease;
  font-weight: 400;
}

/* Hover – DIM (to match button feel) */
/* Hover BRIGHTENS. It used to take `--hover-text-color` (#222222), whose own
   note says "text on a light/inverted fill" - but the background here stays
   transparent, so that near-black landed on the chrome band and a hovered link
   measured 1.18:1, down from 5.52:1 at rest. Hovering a link made it harder to
   read than not hovering it, which is why it looked disabled rather than
   highlighted (GC-270).

   `--text-strong` (13.51:1 on chrome) rather than `--text-color`: the active
   link already takes body colour, so matching it would make any hovered link
   look like the current page. Hover is transient direct feedback and wants to
   be unambiguous; active is marked by weight 600, which hover does not touch.
   Ladder: muted at rest, strong on hover, body + 600 when active. */
a.ec-nav-link:hover {
  color: var(--text-strong);
  background-color: transparent;
  text-decoration: none;
  font-weight: 400;
}

/* Active (current page) – persistent “selected” state */
a.ec-nav-link.active,
.navbar-nav .nav-link.active {
  color: var(--text-color);
  background-color: transparent;
  font-weight: 600;
  filter: none;                       /* don’t dim when active */
}

/* Keyboard focus (accessibility) */
a.ec-nav-link:focus-visible {
  outline: 2px solid var(--text-color);
  outline-offset: 2px;
}


/* a.ec-nav-link { */
/*     background-color: transparent !important; */
/*     border-radius: var(--corner-radius) !important; */
/*     color: var(--inactive-text-color) !important; */
/* } */
/**/
/* a.ec-nav-link.active { */
/*     background-color: transparent;  */
/*     color: var(--text-color) !important; */
/*     font-weight: 600; */
/* } */

/* a.ec-nav-link:hover { */
/*     background-color: transparent;  */
/*     color: var(--text-color) !important; */
/*     font-weight: 600; */
/* } */
/**/

/* `.text-muted` is the house control-label class - `text-muted mb-1` sits above
   roughly 18 filters and dropdowns - so it is the muted *role*, and ADR-04
   solves that role per surface at 5.5:1.

   Bootstrap instead derives it as `rgba(var(--bs-body-color-rgb), 0.75)`, and
   that triple is still cyborg's `173,175,174` (the same stale-decomposed-copy
   shape as `--bs-body-bg-rgb`). Pointing the triple at our token would fix the
   staleness but not the model: an alpha of body text lands at 6.44 on the page
   and 6.19 on a card - brighter than the role is designed to be, and different
   on each surface, because compositing depends on what is behind it. The ramp's
   own muted value is 5.56 on both by construction.

   So the role wins over the formula. `!important` because Bootstrap's own rule
   carries one. Pinned to a token, so changing which rung it takes is one word.
   (GC-270, 2026-09-12.) */
.text-muted {
  color: var(--inactive-text-color) !important;
}

/* Base (inactive) */
.btn-outline-light {
  --bs-btn-bg: transparent;
  color: var(--inactive-text-color);
  border-color: var(--inactive-text-color);
}

/* Hover */
.btn-outline-light:hover {
  color: var(--text-color);
  border-color: var(--text-color);
  background-color: transparent;
  filter: brightness(1.0); /* increase if you want brightness at instance of hover */
}

/* Active / checked (covers both toggle + manual .active) */
.btn-check:checked + .btn-outline-light,
.btn-check:active + .btn-outline-light,
.btn-outline-light.active,
.btn-outline-light:active {
  color: var(--text-color);
  border-color: var(--text-color);
  background-color: transparent;
}

.form-select.bg-dark.text-light {
  /* GC-270: the field reads its token so the switchboard can re-tint it. The
     literal #111117 here and the six in mantine_styles.py were why every
     surface candidate moved the app except the inputs. */
  background-color: var(--surface-field) !important;
  color: var(--text-color) !important;
  border: var(--hairline-width) solid var(--control-border-color) !important;
  transition: background-color 0.2s ease, border-color 0.2s ease;
  /* filter: invert(1) brightness(1.2); */
}

/* in assets/custom.css */
.form-select.bg-dark.text-light {
  /* GC-270: the field reads its token so the switchboard can re-tint it. The
     literal #111117 here and the six in mantine_styles.py were why every
     surface candidate moved the app except the inputs. */
  background-color: var(--surface-field) !important;
  color: var(--text-color) !important;
  border: var(--hairline-width) solid var(--control-border-color) !important;
  transition: background-color 0.2s ease, border-color 0.2s ease;

  /* Replace the default chevron */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%3E%3Cpath d='M3.204 5.793a1 1 0 0 1 1.414 0L8 9.172l3.382-3.379a1 1 0 0 1 1.415 1.414l-4.09 4.086a1 1 0 0 1-1.414 0L3.204 7.207a1 1 0 0 1 0-1.414z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.8em center;
  background-size: 0.9em auto;
  padding-right: 2em; /* leave room for the icon */
  appearance: none;   /* hide default arrow */
}

/* Bootstrap Collapse transition override - CHANGE HERE to adjust collapse animation speed */
.collapse {
    transition: height 350ms ease;  /* Default is ~350ms, adjust as needed */
}

.collapsing {
    transition: height 350ms ease;  /* Default is ~350ms, adjust as needed */
}

/* Collapsible section header - clickable */
.ec-nav-section-toggle {
    /* Same token as the page links below it (`--type-nav`), deliberately.
       Uppercase plus 600 weight plus 0.08em tracking is already three
       channels of separation - adding a fourth made the column shout, which
       is what it did at body size. Size is the channel to spend here, not
       the one to add.

       Not an inverted hierarchy despite being level with its own children:
       caps is what says "container", and it reads as a label rather than as
       a bigger link. Judged on the page 2026-09-16 (GC-270). */
    font-size: var(--type-nav);
    font-weight: 600;
    letter-spacing: 0.08em;
    color: var(--inactive-text-color);
    margin-top: 0.75rem;
    padding-left: 1rem;
    cursor: pointer;
    transition: opacity 0.15s ease;
    user-select: none;  /* Prevent text selection on click */
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.ec-nav-section-toggle:hover {
    opacity: 0.9;
}

/* Chevron icon in section header */
.ec-nav-section-toggle .bi {
    font-size: 0.875rem;
    transition: transform 0.15s ease;
    min-width: 1rem;  /* Prevent layout shift */
}

/* Keep chevron visible and subtle */
.ec-nav-section-toggle .bi.bi-chevron-right {
    opacity: 0.6;
}

.ec-nav-section-toggle .bi.bi-chevron-down {
    opacity: 0.6;
}

/* GC-305: mobile nav drawer (#mobile-sidebar) rides the chrome surface, set
   inline in make_navbar. Default offcanvas header carries a light border and a
   dark (black) close icon — invisible on the dark surface. Whiten the close
   icon, make the title light, and mute the header divider to match the chrome. */
#mobile-sidebar .offcanvas-title {
    color: var(--text-color);
}
#mobile-sidebar .offcanvas-header {
    border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
#mobile-sidebar .btn-close {
    /* Bootstrap's .btn-close-white treatment: invert the dark SVG to white. */
    filter: invert(1) grayscale(100%) brightness(200%);
}

/* GC-319: public auth pages (login, get-access, request-trial, auth/magic)
   render without app-shell chrome. <html> gets gc-hide-sidebar before first
   paint (the pre-paint script in app_factory's index_string) and it's kept in
   sync across SPA nav by a clientside callback. Hide the desktop sidebar column
   and the mobile hamburger, and let the content column run full width. */
html.gc-hide-sidebar #sidebar-col,
html.gc-hide-sidebar #open-sidebar {
    display: none !important;
}
html.gc-hide-sidebar #page-container {
    /* Override the Bootstrap col-lg-9/col-xl-10 width with the sidebar gone. */
    flex: 0 0 100% !important;
    width: 100% !important;
    max-width: 100% !important;
}
/* With the sidebar gone the content column is full-bleed, so the public-content
   twins (About/Glossary/Explainers) — which the static Flask pages centre via
   body.gc-public-page — would otherwise jam to the left gutter. Re-centre their
   .gc-public-page wrapper to the same --gc-public-max the static pages use, so
   the in-shell twin matches its crawlable static twin. Auth pages centre their
   own cards (no .gc-public-page) and static pages never carry gc-hide-sidebar,
   so both are untouched. */
html.gc-hide-sidebar #page-container .gc-public-page {
    max-width: var(--gc-public-max);
    margin-inline: auto;
}

/* Disclosure strip (GC-270 / ADR-02): a section heading that toggles hidden
   content. One treatment for what used to be two — the card-look collapse
   headers on /balance, /by-unit and /interconnectors, and the page summary's
   "Description" toggle. The strip is page chrome: no fill, no border, no
   radius — a painted panel is the chart-card look, which ADR-02 reserves for
   data wells. The affordance is the chevron plus the hover brightening.

   A real <button> (tabbable, takes the global button:focus-visible ring), so
   this has to undo the UA button styling. Muted via the ramp role rather than
   opacity: an alpha of text composites differently on every surface (ADR-04).
   Hover lifts to full body text, the same one-rung move as nav links. */
.gc-strip-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: none;
    border: none;
    padding: 8px 0;
    color: var(--inactive-text-color);
    font-family: inherit;
    font-size: 0.95rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    cursor: pointer;
    user-select: none;
    transition: color 0.15s ease;
}

.gc-strip-toggle:hover {
    color: var(--text-color);
}

.gc-strip-chevron {
    font-size: 0.875rem;
    min-width: 1rem;  /* Prevent layout shift between right/down glyphs */
    display: inline-block;
}

/* AG Grid dark theme overrides */
.ag-theme-alpine-dark {
    --ag-background-color: var(--surface-card);
    /* Header + footer sit on --surface-header (a half-step below the card) so the
       title row and pagination read as a distinct band, not flush with the data. */
    --ag-header-background-color: var(--surface-header);
    --ag-odd-row-background-color: var(--surface-raised);  /* zebra stripe */
    --ag-row-hover-color: var(--surface-hover);
    --ag-foreground-color: var(--text-color);
    --ag-header-foreground-color: var(--text-color);
    --ag-border-color: var(--divider);
    /* The grid's rules take our hairline *colour* but keep AG's own 1px width.
       Setting `--ag-borders: var(--hairline-width) solid` here does nothing:
       the alpine theme declares --ag-borders itself and dash_ag_grid's
       stylesheet loads after this one, so at equal specificity AG wins on
       order. Same shape as the dmc load-order problem. Measured, not assumed -
       the row border stays 1px while every other hairline moves. */
    --ag-font-family: var(--font-sans);
    --ag-font-size: 13px;
    --ag-header-font-size: 13px;
    --ag-header-font-weight: 600;
}

/* GC-305: differentiate the grid's title row and pagination footer. 2px rule
   beneath the header (and above the footer) to bound the band; footer shares
   the --surface-header band. Global — applies to every AG grid. */
.ag-theme-alpine-dark .ag-header {
    border-bottom: 2px solid var(--surface-hover);
}
.ag-theme-alpine-dark .ag-paging-panel {
    background-color: var(--surface-header);
    border-top: 2px solid var(--surface-hover);
}

/* Drop the resize-handle line trailing the last column — there's nothing beyond
   it to resize against, so it reads as a stray pipe after the final header. */
.ag-theme-alpine-dark .ag-header-cell.ag-column-last .ag-header-cell-resize::after {
    display: none;
}

/* Markdown tables (used in explainer pages and page summaries) */
.gc-markdown table {
    border-collapse: collapse;
    margin: 1rem 0;
}

.gc-markdown th,
.gc-markdown td {
    border: 1px solid #2a2a2a;
    padding: 0.4rem 0.8rem;
    text-align: left;
}

.gc-markdown th {
    font-weight: 600;
    background-color: var(--surface-page);
}

.gc-markdown td {
    background-color: var(--surface-card);
}

/* Summary stats tables (dbc.Table.gc-summary-table). Align them with the AG
   grids on the same/adjacent pages: horizontal row rules only (drop the vertical
   cell borders from bordered=True), AG-matched row height, header band on
   --surface-header, and the AG border colour. */
.gc-summary-table {
    --bs-table-border-color: var(--divider);  /* == --ag-border-color */
    /* Thin edge around the table, like the AG grid's --ag-root-wrapper border,
       in --surface-raised. */
    border: 1px solid var(--surface-raised) !important;
}
/* Row rules only (drop the left/right verticals from bordered=True), and AG-
   matched row height via padding. Padding is set directly with !important —
   .table-sm ties on specificity, so the --bs-table-cell-padding-* vars alone
   lose. In px (root font is 14px here): ~11.8px y → 42px rows to match
   --ag-row-height; 15px x matches AG's cell padding. */
.gc-summary-table td,
.gc-summary-table th {
    border-left: 0 !important;
    border-right: 0 !important;
    padding: 11.8px 15px !important;
    font-size: 13px !important;  /* == --ag-font-size; overrides the inline 0.85rem (~12px) */
}

/* Header band on --surface-header, matching the AG grid header. 2px surface-
   hover rule beneath the header row (matches the AG header rule; collapses over
   the 1px cell border). */
.gc-summary-table thead th {
    background-color: var(--surface-header) !important;
    --bs-table-accent-bg: var(--surface-header) !important;
    --bs-table-bg: var(--surface-header) !important;
    --bs-table-striped-bg: var(--surface-header) !important;
    border-bottom: 2px solid var(--surface-hover) !important;
}
.gc-summary-table thead tr {
    --bs-table-accent-bg: var(--surface-header) !important;
}

/* Body zebra matches the AG grid exactly: base rows = --surface-card, alternate
   = --surface-raised, hover = --surface-hover. AG's first data row is card, so
   stripe the 2nd/4th (nth-of-type even). Kill Bootstrap's translucent ±5% accent
   overlay (the box-shadow) so the solid tokens paint through. */
.gc-summary-table tbody tr > * {
    background-color: var(--surface-card) !important;
    box-shadow: none !important;
}
.gc-summary-table tbody tr:nth-of-type(even) > * {
    background-color: var(--surface-raised) !important;
}
.gc-summary-table tbody tr:hover > * {
    background-color: var(--surface-hover) !important;
}

/* Description prose measure. Started (GC-305) as a hand-maintained allowlist of
   the full-width pages, whose descriptions would otherwise run the whole screen
   width; every new full-width page had to remember to join it, and
   neso-monthly-stacks never did. Now applied to every description instead: on a
   narrow page the container is already inside 80ch, so the cap simply does
   nothing there and the allowlist bought nothing.
   680px, not the old 80ch, to match the published article measure exactly (see
   `.gc-flat .gc-markdown` below) now that descriptions carry the same 17px prose.
   `ch` was doing something subtly wrong here: it resolves against the font-size
   of the element it sits on, so on the 14px wrapper it capped 17px text at 629px,
   and moving it to the prose element overshot to 764px. A fixed measure is the
   honest way to say "the same column width as the articles". Set on the inner
   .gc-markdown so it constrains the prose, not the collapsible block. */
.gc-page-summary.gc-public-page .gc-markdown,
.gc-page-summary .gc-markdown {
    max-width: 680px;
    /* An in-app description is app furniture, not a published article.
       `page_summary.py` opts the block into `gc-public-page` to borrow the
       prose rules (lists, links, spacing), and that carried the article
       *size* in with them - 17px, a step above the app's own body. Both
       selectors are 0,2,0 and the public block sits later in this file, so it
       was winning on source order alone rather than on any decision.

       Scoped on both classes (0,3,0) so it wins wherever either rule moves,
       and so the selector states the case: article styles borrowed, app size
       kept. Published articles keep 17px - see the public section below. */
    font-size: var(--type-body);
    line-height: var(--leading-body);
}

/* Descriptions open with `##### Page Name`, so h5 is the page-summary TITLE
   convention rather than a deep sub-heading, and it has to sit above the 17px
   body rather than continue the h2→h4 ramp downward. h5 is unused in published
   prose (no explainer or report .md has one), so this is scoped to the
   description component and leaves the article pages free to define their own
   if one is ever wanted. First element in the block, hence no top margin. */
.gc-page-summary .gc-markdown h5 {
    font-weight: var(--weight-card-title);
    font-size: var(--type-card-title);
    margin: 0 0 .6rem;
    color: #f0f0f2;
}

/* Styled links using primary color */
/* GC-263: no-underline is the app-wide default (colour carries the link
   affordance), matching the public pages. Covers plain anchors and Bootstrap's
   link-styled buttons (.btn-link), which Reboot would otherwise underline. */
a,
.btn-link {
    text-decoration: none;
}

.gc-link {
    color: #50748f;
    transition: color 0.15s ease;
}

.gc-link:hover {
    color: #003f5c;
}

/* Override link colors in markdown content and cards. No underline (GC-263 Layer
   2: the app follows the public no-underline stance; colour carries the link
   affordance) — these are the only app content links that would otherwise inherit
   Bootstrap Reboot's default underline. */
.card a, .gc-markdown a {
    /* Was a hardcoded `#50748f` / `#003f5c` pair, which is why the outage
       grid's unit links stayed dark after `--bs-link-color` moved: this rule
       is more specific than `a` and never read the variable (GC-270). */
    color: var(--bs-link-color);
    text-decoration: none;
    transition: color 0.15s ease;
}

.card a:hover, .gc-markdown a:hover {
    color: var(--bs-link-hover-color);
}

/* Override Bootstrap primary button colors (Cyborg theme uses hardcoded classes) */
.btn-primary {
    background-color: #003f5c !important;
    border-color: #003f5c !important;
    color: #ffffff !important;
}

.btn-primary:hover, .btn-primary:focus, .btn-primary:active {
    background-color: #50748f !important;
    border-color: #50748f !important;
}

/* Override Bootstrap warning button (used for logout) */
.btn-warning {
    background-color: #ee7201 !important;
    border-color: #ee7201 !important;
    color: #000000 !important;
}

.btn-warning:hover, .btn-warning:focus, .btn-warning:active {
    background-color: #cb6101 !important;
    border-color: #cb6101 !important;
}

.alert-warning {
    background-color: #ee7101 !important;
    border-color: #ee7101 !important;
    color: #000000 !important;
}

.alert-success {
    background-color: #50723C !important;
    border-color: #50723C !important;
    color: #ffffff !important;
}

/* CTA button hover animation (matches user avatar toggle) */
.gc-cta-btn {
    transition: transform 0.15s ease;
}

.gc-cta-btn:hover {
    transform: scale(1.04);
}

/* Full-width CTAs (e.g. the login magic-link button) are much wider, so the
   1.04 scale moves them far more in absolute px. Use a gentler factor so the
   hover growth reads similar to the content-width buttons (Get Access, Logout). */
.gc-cta-btn.w-100:hover {
    transform: scale(1.01);
}

/* Trial button: filled green, lightens on hover */
/* White CTA: mirrors the landing .gc-lp-cta — white fill, near-black text.
   Pair with .gc-cta-btn for the scale-on-hover. */
.gc-cta-white {
    /* `--text-strong`, not `#ffffff` (GC-270, 2026-09-16). Same argument as
       `.gc-btn-primary`: an absolute white belongs to no surface, so against a
       ramp tinted 0.35 toward the base it reads as a hole rather than a
       control. `--text-strong` is that ramp's top rung and ADR-04 solves it per
       surface, so it tints to wherever the button sits.

       This is the PRIMARY ACTION treatment, and it is deliberately achromatic:
       GC-270's hierarchy is "solid white, then grey outline, then plain text,
       so the brand colour never has to work as an interaction signal". Blue
       (`--tertiary-color`) marks *state* - a checked box, a toggle that is on -
       and never an action. A toggle should not look like a button.

       Only the fill and text move; the 16px type below is right for a
       promotional action and stays.

       `border-radius` was `0`, justified here as "right for a landing-page
       CTA" - but no landing page uses this class. All 8 consumers are app and
       auth surfaces (sign-in, request access, logout, subscriber wall, trial
       teaser), and on the two inside `.gc-access-form` a square button sat
       under a rounded field at identical width. The exception was protecting a
       use that never existed, so it takes `--control-radius` like every other
       discrete surface (GC-270, 2026-09-17). */
    background-color: var(--text-strong) !important;
    color: var(--hover-text-color) !important;
    border: 1px solid var(--text-color) !important;
    border-radius: var(--control-radius) !important;
    font-size: var(--gc-public-font) !important;
    transition: filter 0.15s ease, transform 0.15s ease;
}
.gc-cta-white:hover, .gc-cta-white:focus, .gc-cta-white:active {
    background-color: var(--text-strong) !important;
    color: var(--hover-text-color) !important;
    /* Darken, matching `.gc-btn-primary`. The usual "hover goes brighter" rule
       does not apply to a light fill - there is nowhere brighter to go. */
    filter: brightness(0.92);
}

/* Checkboxes (GC-270, 2026-09-16).

   `dbc.Checklist` renders `.form-check-input`, which we styled nowhere - so a
   checked box was `#2a9fd6`, **cyborg's own primary**, hardcoded in the
   vendored theme. Not `--bs-primary` (#50748f), not ours at all: the last
   visible piece of the vendor palette leaking into the app.

   `--tertiary-color` is the house blue and is now the app-wide link colour too,
   so a checked control and a link share a hue. That is a deliberate narrowing
   rather than an accident: both mean "this is interactive", and the app has one
   blue for that. Data hues stay out of chrome (rule 1).

   The tick has to move with the fill. Bootstrap's glyph is a white SVG, which
   is 1.71:1 on this pale blue and effectively invisible; this is the same
   stroke recoloured to `--control-glyph`, 4.85:1 - lighter than
   `--hover-text-color`'s 9.26:1, which read heavy at tick size. The hex is
   inlined because it sits inside a data: URI, where `var()` cannot reach. `accent-color` cannot do
   this job - `.form-check-input` is `appearance: none` with a background
   image. */
/* The secondary button (GC-270, 2026-09-16).

   GC-270's button hierarchy is "solid white, then grey outline, then plain
   text" - so `.btn-outline-secondary` IS the secondary rung, and it was
   rendering cyborg's `#555555` for BOTH border and text. 2.43:1 on the page,
   under SC 1.4.11's 3:1 for a UI component boundary, and visibly fainter than
   the date input sitting next to it.

   Now the same two tokens every other resting control uses: the edge at 3.00:1
   and the label at body strength, so a button and a field in one filter row
   read as the same family. */
.btn-outline-secondary {
    border-color: var(--control-border-color) !important;
    color: var(--text-color) !important;
}

.btn-outline-secondary:hover,
.btn-outline-secondary:focus {
    border-color: var(--text-color) !important;
    color: var(--text-strong) !important;
    background-color: transparent !important;
}

/* Unchecked: a light grey, not white. Cyborg leaves `--bs-form-check-bg`
   unset so the box falls back to pure white - the brightest thing on the page,
   for a control that is switched OFF. `--inactive-text-color` is the same grey
   the control labels use, so an unchecked box reads as the same weight as the
   word beside it (5.56:1 on the page, clear of SC 1.4.11's 3:1).

   Note this keeps a *filled* box rather than ADR-03's "resting controls are
   transparent with a border". A checkbox is a different case from a field: the
   fill IS the state, so an empty outline reads as absence rather than as off. */
.form-check-input {
    background-color: var(--inactive-text-color);
    border-color: var(--inactive-text-color);
}

.form-check-input:checked {
    background-color: var(--tertiary-color) !important;
    border-color: var(--tertiary-color) !important;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%234d4d58' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e") !important;
}

/* The in-app primary button (GC-270, 2026-09-16).

   Same white fill as `.gc-cta-white`, which GC-270 already names as the house
   primary: "solid white, then grey outline, then plain text, so the brand
   colour never has to work as an interaction signal." The Apply buttons were
   `color=colors.primary` - NAVY - which contradicted that rule and measured
   **1.50:1** against the card, so the button's own boundary was invisible
   (SC 1.4.11 asks 3:1 of a UI component). White is 16.92:1.

   A separate class rather than reusing `.gc-cta-white` because that one also
   pins `font-size: var(--gc-public-font)` (16px), which is wrong beside a
   14.4px date field. The fill and the hover are the shared part; the sizing is
   not. Both now take `--control-radius`, so the corner is no longer part of
   what separates them.

   `!important` to beat Mantine's own `.mantine-Button-root`, which loads after
   this file - the same load-order problem dash-ag-grid has. */
.gc-btn-primary {
    /* `--text-strong`, not `#ffffff`. Refactoring UI's point about not using
       pure white on a tinted UI applies to a fill as much as to text: an
       absolute white belongs to no surface, and next to a 0.35-tinted grey
       ramp it reads as a hole rather than a control.

       `--text-strong` is that ramp's top rung and ADR-04 solves it PER SURFACE,
       so the variable tints itself to whatever the button is sitting on -
       #e0e0e2 on the page, #e6e6e8 on a card. 13.53:1 as a fill against the
       page, against pure white's 17.83:1.

       `--hover-text-color` is the dark text for it. That token's note has
       always read "text on a light/inverted fill" and until now it had ZERO
       consumers - this is the job it was defined for. (Its name is about the
       wrong thing; that belongs to the naming review, not here.) */
    background-color: var(--text-strong) !important;
    color: var(--hover-text-color) !important;
    border: 0 !important;
    transition: filter 0.15s ease;
}

.gc-btn-primary:hover,
.gc-btn-primary:focus,
.gc-btn-primary:active {
    background-color: var(--text-strong) !important;
    color: var(--hover-text-color) !important;
    /* Darken on hover, matching `.gc-cta-white`. The usual "go brighter" rule
       does not apply to a white fill - there is nowhere brighter to go. */
    filter: brightness(0.92);
}

/* Get Access form: inputs + textareas sit on the raised card surface
   (surface-card) against the flat surface-page form container. */
/* Get Access + Login + Logout page text sits at the public 16px scale (in-app base is 14px). */
#req-form,
#login-page,
#logout-page {
    font-size: var(--gc-public-font);
}

/* The password-reveal toggle reads as body text, not a small link
   (id beats Bootstrap's .btn-sm font-size). */
#login-password-toggle {
    font-size: var(--gc-public-font);
}

/* Redundant since `--bs-link-color` became `--tertiary-color` globally
   (GC-270, 2026-09-16) - these now restate the inherited value. Kept for the
   `!important`, which is still doing work against cyborg's own `a` rules on
   these two pages, and because `.btn-link` resolves its colour through
   `--bs-btn-color` rather than `--bs-link-color`. Retire with the cyborg
   dependency, not before. */
#login-page a,
#login-page .btn-link,
#req-form a,
#req-form .btn-link {
    color: var(--tertiary-color) !important;
}
#login-page a:hover,
#login-page .btn-link:hover,
#req-form a:hover,
#req-form .btn-link:hover {
    color: var(--tertiary-color) !important;
    filter: brightness(0.9);
}

/* Password "Sign in" button: public hyperlink-blue text + border; fills blue on
   hover. Corner follows `--control-radius` with every other control (GC-270,
   2026-09-17) - it was pinned square, on the same form as the magic-link CTA
   and the email field. */
#login-submit {
    color: var(--tertiary-color) !important;
    border-color: var(--tertiary-color) !important;
    border-radius: var(--control-radius) !important;
}
#login-submit:hover,
#login-submit:focus,
#login-submit:active {
    background-color: var(--tertiary-color) !important;
    border-color: var(--tertiary-color) !important;
    color: var(--surface-page) !important;
}

/* The auth forms are a THIRD control appearance (GC-270). The Mantine and
   `.form-select` paths were tokenised in Sept 2026; this one sets a
   `border-color` on a border cyborg gives `0px` width, so the colour has never
   painted - the sign-in field has had no edge at all, which is the SC 1.4.11
   failure the control-edge decision was supposed to close. Width and style are
   what was missing; the colour was also wrong (`--surface-raised` is a surface
   token doing a border's job, and it is 1.4:1 here). */
.gc-access-form .form-control {
    background-color: var(--surface-card) !important;
    color: var(--text-color) !important;
    border-color: var(--control-border-color) !important;
    border-width: var(--hairline-width) !important;
    border-style: solid !important;
    border-radius: var(--control-radius) !important;
    font-size: var(--gc-public-font) !important;
}
.gc-access-form .form-control:focus {
    background-color: var(--surface-card) !important;
    color: var(--text-color) !important;
    border-color: var(--tertiary-color) !important;
    border-width: var(--hairline-width) !important;
    border-style: solid !important;
}

/* Chrome paints autofilled inputs yellow and ignores `background-color`.
 * The -webkit-box-shadow inset trick is the standard workaround. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active {
    -webkit-box-shadow: 0 0 0 1000px #ffffff inset !important;
    -webkit-text-fill-color: #000000 !important;
    caret-color: #000000;
}

/* Footer — shared by the app (.gc-app-footer) and the public site
   (.gc-public-footer): a full-bleed surface-chrome band with links + copyright,
   so the two surfaces read consistently. Grouped selectors carry everything the
   two share; the thin context modifiers below carry only the genuine deltas.
   flex-shrink keeps the footer pinned by the sticky-footer flex column. */
.gc-app-footer, .gc-public-footer {
    background: var(--surface-chrome);
    /* ADR-03: the same hairline the navbar and the sidebar carry. A chrome
       boundary is a chrome boundary wherever it falls. */
    border-top: var(--chrome-rule);
    flex-shrink: 0;
}
.gc-app-footer-inner, .gc-public-footer-inner {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem 1.25rem;
    align-items: center;
    justify-content: space-between;
}
.gc-app-footer-links, .gc-public-footer-links {
    display: flex;
    flex-wrap: wrap;
    gap: 1.25rem;
}
.gc-app-footer a, .gc-public-footer a {
    color: #c9c9cf;              /* orphan grey — token deferred to Layer 3 */
    text-decoration: none;
}
.gc-app-footer a:hover, .gc-public-footer a:hover {
    /* Converged on the public no-underline stance. App was #dadada + underline,
       and #dadada was --text-color at the time, so only the underline was
       removed. The token has since moved to #c7c7cb (ADR-04); this reads it,
       so the link follows. Weight bump on hover gives prominence in place of
       the dropped underline. */
    color: var(--text-color);
    font-weight: 600;
    text-decoration: none;
}
.gc-app-footer-copy, .gc-public-footer-copy {
    color: var(--inactive-text-color);   /* app was literal #86868b; token has since moved */
    font-size: 0.875rem;
}

/* Footer context modifiers — only the genuine deltas between the two surfaces. */
/* App: butts directly against the data row so the sidebar chrome meets the
   footer with no page-bg gap; spans full-bleed to match the navbar/sidebar. */
.gc-app-footer-inner { padding: 1.25rem 2rem; }
/* Public: breathes after prose, and aligns its content to the shared edge. */
.gc-public-footer { margin-top: 3rem; }
.gc-public-footer-inner {
    max-width: var(--gc-public-max);
    margin: 0 auto;
    padding: 1.25rem 1rem;
}

/* Shared nav-link idiom (GC-263 Layer 2) — the top app navbar (.gc-nav-link, a
 * dbc.Button) and the public site header nav (.gc-public-nav a) render
 * identically: plain text links, muted grey brightening to --text-color on hover,
 * no underline. Grouped here as one source of truth. The public selector is
 * qualified with .gc-public-header so it out-specifies the broader
 * `.gc-public-header a` colour rule regardless of source order. The inline
 * style="color" the app buttons used to fight was removed from app_factory, so no
 * !important is needed. Items sit ~1.25rem apart via the navbar flex gap. */
.gc-nav-link.btn,
.gc-public-header .gc-public-nav a {
    color: #c9c9cf;              /* orphan grey — token deferred to Layer 3 */
    text-decoration: none;
}
.gc-nav-link.btn:hover,
.gc-nav-link.btn:focus,
.gc-public-header .gc-public-nav a:hover {
    color: var(--text-color);
    font-weight: 600;
    text-decoration: none;
}
/* App-only: strip the dbc.Button chrome so it reads as a plain text link, matching
 * the public header's <a>. .btn lifts specificity above Bootstrap's .btn rules. */
.gc-nav-link.btn {
    padding: 0.35rem 0;
    border: 0;
    font-weight: 400;
    box-shadow: none;
    /* Bootstrap's `.btn` 6px was still here. It paints nothing at rest - the
       fill is transparent and the border is 0 - but `outline` TRACES the border
       radius, so a keyboard user tabbing the navbar got a 6px-rounded focus
       ring while every other control in the app rings at 2px. Invisible to a
       mouse, visible only to the people who depend on the ring (GC-270). */
    border-radius: var(--control-radius);
}
.gc-nav-link.btn:hover,
.gc-nav-link.btn:focus {
    box-shadow: none;
}

/* ------------------------------------------------------------------------
 * Public section (server-rendered pages built in public_content.py:
 * _page_shell + the landing page). Relocated from an inline <style> block
 * (GC-263, Aug 2026) so the app and public site share one stylesheet.
 *
 * Colours here are being tokenised against :root incrementally. Exact-match
 * hex have been aliased to vars; the remaining literals (#c9c9cf, #d7d7d9,
 * #f0f0f2, #9a9aa6, #e6e6e8, rgba(255,255,255,...)) are orphan greys with no
 * token yet -- reconcile against the ramp in the Layer 2/3 consistency pass.
 * ------------------------------------------------------------------------ */

/* --- shared shell: header, footer, prose card, directories --- */
/* Shared content boundary for the public section (header, footer, landing
   and card-less pages all align their left/right edge to this). */
/* Sticky footer: body fills the viewport and the main grows, so the footer
   pins to the bottom on short pages instead of floating mid-page. Scoped to
   .gc-public-page (set on every public <body>) so these globals don't reflow
   the Dash app, which shares this stylesheet. */
body.gc-public-page { min-height: 100vh; display: flex; flex-direction: column; }
body.gc-public-page > main { flex: 1 0 auto; }
.gc-public-header, .gc-public-footer { flex-shrink: 0; }
/* Header + footer are full-bleed chrome bands (surface-chrome, matching the
   app navbar). The header spans the full width like the app navbar — brand
   hard-left, nav hard-right, with a small edge gutter — rather than being
   centred to --gc-public-max like the page bodies. ADR-03 added the hairline:
   the band's own tint is 1.055 against the page, which is not enough to carry
   the boundary on its own at this step. */
.gc-public-header {
  display: flex; align-items: baseline; gap: 1.5rem;
  padding: 1.1rem 1.25rem;
  background: var(--surface-chrome);
  border-bottom: var(--chrome-rule);
}
/* Brand wordmark: brighter than the nav links (#f0f0f2 vs #c9c9cf). Qualified
   with .gc-public-header to out-specify the `.gc-public-header a` colour rule
   that would otherwise dim it to the nav-link grey. */
.gc-public-header .gc-public-brand { font-weight: 700; font-size: 1.25rem; color: #f0f0f2; text-decoration: none; }
.gc-public-nav { display: flex; gap: 1.25rem; margin-left: auto; }
/* No underlines on any public-page link (nav, prose, footer, directory).
   Scoped to .gc-public-page so the app's link styling is untouched. */
.gc-public-page a { text-decoration: none; }
.gc-public-header a { color: #c9c9cf; }
.gc-public-header a:hover { color: var(--text-color); }
/* Mobile: the desktop header is a single flex row (brand + 5 nav links pushed
   hard-right with margin-left:auto). Below 640px that row is wider than the
   viewport, so the last link ("Sign In") clips off the right edge. Drop the
   auto margin, give the nav full width so its links wrap left-aligned under the
   brand, and tighten the gaps/padding. */
@media (max-width: 640px) {
  .gc-public-header { flex-wrap: wrap; gap: 0.5rem 1rem; padding: 0.9rem 1rem; }
  .gc-public-nav { margin-left: 0; width: 100%; flex-wrap: wrap; gap: 0.5rem 1rem; }
}
/* Public footer links are styled with the app footer (shared .gc-*-footer a rule). */
.gc-public {
  max-width: 640px; margin: 2rem auto; padding: 2rem 2.25rem 2.5rem;
  background: var(--surface-card); border: 1px solid rgba(255,255,255,.06);
  border-radius: 0;
}
/* Card-less prose pages: text sits directly on the page (surface-page). */
.gc-flat .gc-public {
  /* width:100% is load-bearing: body is a flex column, and `margin: auto`
     on a flex child would otherwise shrink it to its content (the 680px
     markdown), so pin the wrapper to the full --gc-public-max boundary; the
     markdown then centres *within* that boundary (see .gc-flat .gc-markdown). */
  width: 100%; max-width: var(--gc-public-max); margin: 2rem auto;
  padding: 0 1rem 2rem; background: transparent; border: 0;
}
/* Centre the narrow prose column within the 1100px boundary. Explainers (a wide
   directory, .gc-dir-main) fills that boundary, so left-aligning prose to its
   edge left a lopsided right-hand gap on the narrow prose pages; centring keeps
   the reading column balanced instead. */
.gc-flat .gc-markdown { max-width: 680px; margin-inline: auto; }
/* Explainers directory: full-width clickable rows with hairline dividers
   (it's a directory, not prose, so it spans the full --gc-public-max width). */
.gc-dir-main { max-width: var(--gc-public-max); margin: 2rem auto;
  padding: 0 1rem 3rem; color: var(--text-color); font-size: 1.0625rem; }
.gc-dir-main h1 { font-weight: var(--weight-page-title); font-size: var(--type-page-title); letter-spacing: -.01em;
  margin: 0 0 1rem; color: var(--text-color); }
.gc-dir-intro { max-width: 680px; color: #d7d7d9; line-height: 1.7;
  margin: 0 0 1.75rem; }
/* The glossary filter is the one control on the public pages, and it is built
   twice — the Flask-served page (public_content.py) and the Dash one
   (public_content_pages.py) — so its resting appearance is set inline at both
   call sites, on ADR-03's terms: --surface-field (transparent) plus a 3.00
   edge. The placeholder cannot be set inline, so it lives here and covers both.
   :root's row is the right one: the field is transparent, so the placeholder
   lands on the page. --text-faint and --control-border-color are the same
   #63636d by construction, both solved at 3.00 against the page. */
#glossary-filter::placeholder { color: var(--text-faint); opacity: 1; }

.gc-dir { border-bottom: 1px solid rgba(255,255,255,.12); }
.gc-dir-row { display: grid; gap: 0.35rem 1.5rem; align-items: baseline;
  grid-template-columns: minmax(160px, 200px) 1fr auto;
  padding: 1.1rem 0; border-top: 1px solid rgba(255,255,255,.12);
  text-decoration: none; color: inherit; transition: background .12s ease; }
.gc-dir-row:hover { background: rgba(255,255,255,.04); }
.gc-dir-title { font-weight: 600; font-size: 1.25rem; color: var(--text-color); }
.gc-dir-desc { color: #c9c9cf; line-height: 1.55; max-width: 80ch; }
.gc-dir-arrow { color: var(--inactive-text-color); font-size: 1.25rem; align-self: center; }
.gc-dir-row:hover .gc-dir-arrow { color: var(--text-color); }
.gc-dir-foot { margin: 1.5rem 0 0; color: #c9c9cf; }
.gc-dir-foot a { color: var(--tertiary-color); }
@media (max-width: 640px) {
  .gc-dir-row { grid-template-columns: 1fr auto; }
  .gc-dir-desc { grid-column: 1 / -1; }
}
/* "What GenCharts offers" directory: same row pattern as .gc-dir — name +
   description left, feature list middle, access tier right, hairlines. */
.gc-offer { border-bottom: 1px solid rgba(255,255,255,.12); margin: 1.25rem 0 1.75rem; }
.gc-offer-row { display: grid; gap: 0.35rem 1.5rem; align-items: start;
  grid-template-columns: minmax(150px, 190px) 1fr auto;
  padding: 1.25rem 0; border-top: 1px solid rgba(255,255,255,.12); }
.gc-offer-name { font-weight: 600; font-size: 1.125rem; color: var(--text-color); }
.gc-offer-desc { color: #9a9aa6; font-size: 0.875rem; line-height: 1.5; margin-top: .3rem; }
.gc-offer-features { margin: 0; padding-left: 1.1rem; color: #c9c9cf;
  font-size: 0.9375rem; line-height: 1.6; }
.gc-offer-features li { margin: .15rem 0; }
.gc-offer-access { justify-self: end; align-self: start; white-space: nowrap;
  display: flex; flex-direction: column; align-items: flex-end; gap: .3rem; }
.gc-offer-tier { font-size: 0.75rem; letter-spacing: .04em; text-transform: uppercase;
  color: #9a9aa6; }
.gc-offer-note { border-top: 1px solid rgba(255,255,255,.12); padding: 1.1rem 0;
  color: #c9c9cf; font-size: 0.9375rem; }
.gc-offer-note a { color: var(--tertiary-color); font-weight: 500; }
@media (max-width: 640px) {
  .gc-offer-row { grid-template-columns: 1fr; }
  .gc-offer-access { justify-self: start; align-items: flex-start; }
}
/* .gc-markdown is shared: public prose pages AND in-app markdown
   (components/explainer_card.py). These prose rules are scoped to
   .gc-public-page so they style the public pages only and leave the app's
   markdown (which keeps the .gc-markdown table/link rules earlier in this file)
   exactly as it was before GC-263. */
.gc-public-page .gc-markdown { font-size: 1.0625rem; line-height: 1.7; color: #d7d7d9; }
.gc-public-page .gc-markdown h1 {
  font-weight: 600; font-size: 1.875rem; line-height: 1.25;
  letter-spacing: -.01em; margin: 0 0 1rem; color: var(--text-color);
}
.gc-public-page .gc-markdown h2 { font-weight: 600; font-size: 1.375rem; margin: 2rem 0 .6rem; color: #f0f0f2; }
.gc-public-page .gc-markdown h3 { font-weight: 600; font-size: 1.125rem; margin: 1.5rem 0 .4rem; color: #f0f0f2; }
.gc-public-page .gc-markdown h4 { font-weight: 600; font-size: 1rem; margin: 1.25rem 0 .4rem; color: #e6e6e8; }
.gc-public-page .gc-markdown p { margin: 0 0 1.1rem; }
.gc-public-page .gc-markdown ul, .gc-public-page .gc-markdown ol { margin: 0 0 1.1rem; padding-left: 1.3rem; }
.gc-public-page .gc-markdown li { margin: .35rem 0; }
.gc-public-page .gc-markdown a { color: var(--tertiary-color); }
.gc-public-page .gc-markdown hr { border: 0; border-top: 1px solid rgba(255,255,255,.10); margin: 2rem 0; }
.gc-public-page .gc-markdown img { max-width: 100%; height: auto; display: block; margin: 1.5rem 0; }
.gc-public-page .gc-markdown video { max-width: 100%; height: auto; display: block; margin: 1.5rem 0; border-radius: 8px; }
.gc-public-page .gc-markdown table { border-collapse: collapse; width: 100%; margin: 1.25rem 0; }
.gc-public-page .gc-markdown th, .gc-public-page .gc-markdown td {
  border: 1px solid rgba(255,255,255,.12); padding: .5rem .7rem; text-align: left;
}
.gc-public-page .gc-markdown th { background: rgba(255,255,255,.04); font-weight: 600; }
/* Headerless tables. Markdown has no way to write a table without a header row,
   so a term/definition table is authored with an empty one (`|  |  |`) and would
   otherwise render as a blank shaded band above the first term. Hide a thead only
   when *every* th in it is empty, so a real header is never touched.
   Both renderers emit truly empty `<th></th>` for that row — python-markdown on
   the Flask route and marked in the dcc.Markdown twin — so one rule covers both.
   Unsupported :has() just leaves the band visible, which is the status quo. */
.gc-markdown thead:not(:has(th:not(:empty))) { display: none; }
/* Public footer styling now lives in the shared "Footer" block above
   (grouped .gc-app-footer / .gc-public-footer selectors + context modifiers). */

/* --- landing page (body.gc-landing) --- */
body.gc-landing { background: var(--surface-page);
  font-family: var(--bs-body-font-family, var(--font-sans)); }
.gc-lp { max-width: var(--gc-public-max); margin: 0 auto; padding: 0 1rem 4rem;
  color: var(--text-color); }
.gc-lp h1, .gc-lp h2 { letter-spacing: -.01em; }
.gc-lp-hero { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem;
  align-items: center; padding: 3.5rem 0 3rem; }
.gc-lp-hero-copy h1 { font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1;
  letter-spacing: -.02em; font-weight: 700; color: var(--text-color); margin: 0 0 1rem; }
.gc-lp-lede { font-size: 1.0625rem; line-height: 1.6; color: #c9c9cf;
  margin: 0 0 1.75rem; max-width: 36ch; }
.gc-lp-actions { display: flex; align-items: center; gap: 1.25rem; margin: 0; }
.gc-lp-actions--center { justify-content: center; }
/* GC-270 (2026-09-17): was a pure `#ffffff` fill with a `#121212` label, the
   last CTA not on the tinted ramp. `.gc-cta-white` and `.gc-btn-primary` both
   argue the Refactoring UI point that an absolute white belongs to no surface
   and reads as a hole next to a 0.35-tinted grey ramp - and this button, one
   click away on `/get-access`, was what the tinted ones were being compared
   against. `--hover-text-color` is the token for a label on a light/inverted
   fill, which is the job it was defined for. */
.gc-lp-cta { display: inline-block; background: var(--text-strong);
  color: var(--hover-text-color); border-radius: var(--control-radius);
  font-weight: 600; padding: .7rem 1.5rem; text-decoration: none;
  border: 1px solid var(--text-color); transition: filter .15s ease; }
.gc-lp-cta:hover { filter: brightness(0.92); text-decoration: none;
  color: var(--hover-text-color); }
.gc-lp-signin { color: #c9c9cf; font-size: var(--gc-public-font); }
.gc-lp-media { display: flex; align-items: center; justify-content: center;
  min-height: 240px; padding: 1rem; text-align: center;
  background: var(--surface-raised);
  border: 1px solid rgba(255,255,255,.08); }
.gc-lp-media--hero { min-height: 300px; }
.gc-lp-media-label { color: var(--inactive-text-color); font-size: .9rem; max-width: 26ch; }
.gc-lp-media-img { width: 100%; height: auto; display: block; }
.gc-lp-band { padding: 1rem 0 1.5rem; }
.gc-lp-band-img { width: 100%; height: auto; display: block; }
.gc-lp-band-cap { color: var(--inactive-text-color); font-size: .9rem; text-align: center;
  margin: .75rem 0 0; }
.gc-lp-row { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem;
  align-items: center; padding: 2.75rem 0;
  border-top: 1px solid rgba(255,255,255,.08); }
.gc-lp-row--reverse { direction: rtl; }
.gc-lp-row--reverse > * { direction: ltr; }
.gc-lp-row-copy h2 { font-size: 1.6rem; font-weight: 600; color: #f0f0f2;
  margin: 0 0 .6rem; }
.gc-lp-row-copy p { color: #c9c9cf; line-height: 1.65; margin: 0 0 .9rem;
  font-size: var(--gc-public-font); }
.gc-lp-link { color: var(--tertiary-color); text-decoration: none; font-weight: 500; }
.gc-lp-closing { text-align: center; padding: 3.5rem 0 1rem; margin-top: 1rem;
  border-top: 1px solid rgba(255,255,255,.08); }
.gc-lp-closing h2 { color: var(--text-color); font-size: 1.8rem; margin: 0 0 1.25rem; }
.gc-lp-closing p { margin: 0 0 1rem; }
.gc-lp-closing-sub { color: #c9c9cf; font-size: var(--gc-public-font); }
.gc-lp-closing-sub a { color: var(--tertiary-color); }
@media (max-width: 720px) {
  .gc-lp-hero, .gc-lp-row { grid-template-columns: 1fr; gap: 1.5rem;
    padding: 2rem 0; }
  .gc-lp-row--reverse { direction: ltr; }
}


/* Keyboard focus indicator (GC-267, WCAG SC 2.4.7 Focus Visible, Level A)
 *
 * Measured before writing this: Bootstrap Cyborg's ring is
 * rgba(42,159,214,.25), which blends to 1.45-1.48:1 against every one of our
 * surfaces. It is present in the CSS and invisible on screen, so the app had no
 * usable focus indicator anywhere except a.ec-nav-link.
 *
 * :focus-visible, not :focus. That is the whole reason the Bootstrap ring was
 * stripped in the first place - a ring on every mouse click looks broken. The
 * browser only matches :focus-visible for keyboard interaction, so this shows
 * up for the people who need it and nobody else.
 *
 * outline, not box-shadow. It sidesteps the .gc-nav-link.btn and .gc-help-btn
 * rules that set box-shadow:none to strip Bootstrap's button chrome - those are
 * deliberate and stay. Two different properties, no fight.
 *
 * outline-offset keeps the ring clear of the element's own fill, which matters
 * for .gc-cta-white: a light ring drawn on a white button would vanish, but at
 * 2px out it sits on the page background at 12.74:1.
 *
 * Achromatic on purpose (rule 1: chrome is achromatic, data owns the hues).
 * GC-270 reserved the focus ring as the one place the brand colour may appear;
 * that is a batch-one decision and this is the safe default until then.
 */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
.btn:focus-visible,
.form-control:focus-visible,
.form-select:focus-visible,
.nav-link:focus-visible,
.page-link:focus-visible,
.dropdown-item:focus-visible,
.js-plotly-plot:focus-visible {
  /* Two-tone ring: a dark collar hugging the element, then a light outline in
   * the 2px gap the offset opens up. The two are adjacent to each other, so the
   * indicator carries its own 12.74:1 contrast and does not depend on what is
   * behind it.
   *
   * That is not belt-and-braces. Measured on the signed-in app: a single light
   * ring scores 13.46 on the sidebar and 12.74 on .gc-help-btn, but only **2.10**
   * on .notifier-close, because the toast behind it is light. Same ring, same
   * page, one of them failing 3:1. The white .gc-cta-white button has the same
   * problem. A one-colour ring cannot be correct on both surfaces.
   *
   * !important, reluctantly and deliberately. dash-mantine-components injects
   * its stylesheets as inline <style> tags that land *after* style.css in the
   * document (measured: style.css is sheet 40, Mantine's are 42+), so an
   * equal-specificity Mantine rule wins on order and we do not control that
   * order. Without this the date picker - a control on every page - silently
   * has no focus indicator, which is exactly the failure this rule exists to
   * prevent. A focus ring is a floor, not a preference, so it refuses to be
   * overridden.
   */
  outline: 2px solid var(--text-color) !important;
  outline-offset: 2px !important;
  box-shadow: 0 0 0 2px var(--surface-chrome) !important;
}


/* ── Mantine floating panels: the calendar (GC-270) ───────────────────────────
 *
 * `mantine_styles.GC_DATEPICKER_STYLES` sets `dropdown` and `calendar` to
 * `--surface-control`, and NEITHER reaches the element. The panel that actually
 * paints is `.mantine-Popover-dropdown`, which the DatePickerInput's Styles API
 * does not address - measured on /balance: fill `#2e2e2e`
 * (`--mantine-color-dark-6`) and border `#424242` (`--mantine-color-dark-4`),
 * both raw Mantine neutrals. Against a blue-black app they read as a different
 * material, and neither tracks a surface change, so the switchboard could never
 * have re-tinted the calendar.
 *
 * Fixed here rather than by repointing `--mantine-color-dark-6`: that scale rung
 * is shared by every dmc component, so moving it to fix a calendar changes
 * things nobody has looked at.
 *
 * Specificity, not `!important`: dmc's stylesheets land after style.css, so an
 * equal-specificity rule of ours loses on order. Doubling the class gets us
 * (0,2,0) against their (0,1,0) and the cascade does the rest - the focus ring
 * above needs `!important` only because it competes with a rule of the same
 * doubled shape.
 */
.mantine-Popover-dropdown.mantine-Popover-dropdown {
  background-color: var(--surface-control);
  border-color: var(--control-border-color);
  /* Raised one level, and the shadow is what says "above" rather than "beside".
     A floating panel one step lighter than the page is ambiguous on its own;
     the page scrolls under it, so it needs a cast edge too. */
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
  border-radius: var(--control-radius);
}

/* Weekends are VALID EFA days. Mantine paints them `--mantine-color-red-6`
 * (#fa5252), which is the colour this app uses for nothing else and which reads
 * as "unavailable" - the one thing a weekend is not. Demarcated instead with
 * the muted text role, so a reader can still see the week's shape.
 *
 * Deliberately NOT the same as a weekday: the distinction is real and worth
 * keeping, it was the alarm that was wrong. Disabled days keep their own
 * treatment below - a future date IS unavailable and should look it. */
.mantine-Popover-dropdown.mantine-Popover-dropdown [data-weekend] {
  color: var(--inactive-text-color);
}

.mantine-Popover-dropdown.mantine-Popover-dropdown [data-disabled] {
  color: var(--text-faint);
}


/* Skip-to-content (GC-267, WCAG SC 2.4.1 Bypass Blocks)
 *
 * Off-screen until focused, then pinned over the navbar. Positioned rather than
 * display:none / visibility:hidden, because both of those take it out of the
 * tab order and a skip link that cannot be tabbed to is not a skip link.
 *
 * :focus, not :focus-visible - the link exists only for keyboard users, and
 * :focus-visible would be correct but leaves nothing to check when testing by
 * clicking. Anything reaching it is already navigating by keyboard.
 *
 * z-index clears the navbar (dbc sticky-top uses 1020).
 */
.gc-skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.gc-skip-link:focus {
  left: 1rem;
  top: 1rem;
  width: auto;
  height: auto;
  z-index: 1050;
  padding: 0.5rem 1rem;
  background-color: var(--surface-raised);
  color: var(--text-color);
  border-radius: var(--corner-radius);
  outline: 2px solid var(--text-color);
  outline-offset: 2px;
  text-decoration: none;
  font-weight: 600;
}


/* SEE ALSO
 *
 * zstyles.css
 *
 * used to resolve load-order issued
 *
 */


/* ── Radius sweep (GC-270, 2026-09-17) ────────────────────────────────────────
 *
 * The house rule is: a discrete surface takes `--control-radius` / `--chart-radius`
 * (both 2px); structural page chrome stays square at `--corner-radius: 0`.
 *
 * An audit of every radius actually rendering on /balance found FOUR values in
 * play and the token in the minority: 6px (Bootstrap's default, on nav links,
 * the user menu, the mobile nav toggle and any bare `.card`), 4px (the help
 * button), 3.6px (an `0.25em` literal that drifted with font size) and 2px.
 * Deliberate exceptions that stay: `50%` on the avatar, `1px` on the line
 * legend swatch. ag-grid and the Dash debug menu are third-party and untouched.
 */

/* Floating panels: the user menu is the same object as the calendar dropdown -
   something rendered OVER the page - so it takes the same treatment.
   It was painting cyborg's `#282828` with no edge and no shadow: a flat grey
   rectangle in a blue-black app, reading as beside the page rather than above
   it, and tracking no surface token. Same fault the calendar had. */
.dropdown-menu {
    border-radius: var(--control-radius);
    background-color: var(--surface-control);
    border-color: var(--control-border-color);
    box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
}

/* The mobile nav toggle is a control like any other. */
.btn-outline-secondary.d-lg-none {
    border-radius: var(--control-radius);
}

.gc-help-btn {
    border-radius: var(--control-radius);
}

/* `.card` globally, superseding the narrower scoping recorded on
   `.gc-chart-card` above ("scoped to `.gc-chart-card`, not `.card`,
   deliberately: this is a chart-card decision, not a global one").
 *
 * That was right when the corner was a chart-card question and the house
 * position was `--corner-radius: 0`. It is not right now that there is a rule
 * covering every discrete surface: it left the collapsible strips' content
 * cards on /balance (Capacity Summary, Outage Changes) at Bootstrap's 6px,
 * beside chart cards at 2px in the same column. `.gc-chart-card` keeps its own
 * declaration because it also needs the `--bs-card-*` vars for header corners. */
.card {
    border-radius: var(--chart-radius);
}
