/* BonBonBot Console: the theme layer.
   ===================================================================
   THE IDEA. This is an operations room, not a marketing page. Somebody
   has it open all day while audits run, so it is dark by default: the
   brand's own editorial ground (blackcurrant) with its companion panel
   colour (scarletGum), scarlet kept for the few things that are
   genuinely live, and everything else in quiet ceramic ink. Nothing
   here is a new colour. Every value below resolves to a --cb-* token.

   WHY A SECOND LAYER. --cb-* is the Candybox token set and is not the
   console's to reinterpret. So the console names what it needs in its
   own terms (--cbc-surface, --cbc-ink, --cbc-line) and maps those onto
   brand tokens once, here. Switching light and dark is then a change of
   twelve lines rather than a search through five stylesheets.

   Every value is a --cb-* token. The exceptions are the ones the token
   scale deliberately does not model: hairlines (1px), 100%/0, opacity
   and colour-mix percentages, ch widths for inputs sized to their
   content, and breakpoint literals in media queries. */

/* ---- THE LADDER ----
   Four surfaces, and they have to step in one direction or the depth reads as
   noise. Darkest to lightest in dark, lightest to darkest in light:

     base     the page itself
     sunken   an inset inside a card: a note, a table head, a track
     surface  a card
     raised   something above a card: a dialog, the save bar

   The dark set used to have the ground sitting between sunken and surface,
   which is why cards looked washed rather than lifted. It is an even ladder
   now, and every rung is still a --cb-* brand colour mixed with black or with
   its own companion, so nothing here is a new hue.

   No decorative gradients anywhere. Depth is a step in lightness and a
   hairline, which survives being screenshotted, printed and colour-managed;
   a bloom behind a header does not, and it was the first thing to look dated. */

:root,
[data-theme="dark"] {
  color-scheme: dark;

  --cbc-base: color-mix(in srgb, var(--cb-color-black) 72%, var(--cb-color-blackcurrant));
  --cbc-sunken: color-mix(in srgb, var(--cb-color-black) 46%, var(--cb-color-blackcurrant));
  --cbc-surface: var(--cb-color-blackcurrant);
  --cbc-raised: color-mix(in srgb, var(--cb-color-scarlet-gum) 62%, var(--cb-color-blackcurrant));

  --cbc-ink: var(--cb-color-ceramic-grey);
  --cbc-ink-muted: color-mix(in srgb, var(--cb-color-ceramic-grey) 72%, transparent);
  /* 57%, not the 48% this was, and the nine points are load-bearing.
     MEASURED, not chosen. Every faint label in this console is 12px, which WCAG
     counts as small text and holds to 4.5:1. At 48% it came to 4.31 on the plain
     surface, 3.77 on a selected row and 3.69 on a hovered one: three failures on
     the three grounds it actually sits on. The first axe scan the console has
     ever had found ten of them in the command palette alone, and the palette is
     not special. It is the hint on every row, every group heading, every
     keyboard legend and every secondary line on every screen.
     57% is the lowest whole percent that clears 4.5:1 on the worst of the three.
     Shipping a product that sells an automated accessibility scan on a console
     that fails AA on its own secondary text is not a defensible place to be. */
  --cbc-ink-faint: color-mix(in srgb, var(--cb-color-ceramic-grey) 57%, transparent);

  /* A touch stronger than before. On the deeper ground the old hairline was
     doing almost nothing, which is what made panels merge into the page. */
  --cbc-line: color-mix(in srgb, var(--cb-color-ceramic-grey) 15%, transparent);
  --cbc-line-strong: color-mix(in srgb, var(--cb-color-ceramic-grey) 28%, transparent);

  /* ---- Hover ----
     A translucent overlay rather than a named surface, so it reads on whatever
     is behind it. Hover used to be "paint it --cbc-surface", which works on the
     page and does nothing at all on a white panel in the light theme: the menu
     and its hover were the same colour, and the links looked dead. */
  --cbc-hover: color-mix(in srgb, var(--cb-color-ceramic-grey) 9%, transparent);
  --cbc-hover-strong: color-mix(in srgb, var(--cb-color-ceramic-grey) 14%, transparent);
  /* ---- What a stand-in is drawn in ----
     ONE FLAT NEUTRAL GREY, and neutral is the whole specification. Every
     application that ships these uses a grey with no hue in it: a skeleton is
     the absence of content, and the moment it carries a colour it starts
     reading as content of its own. Two earlier passes used a wash of the ink,
     which here is a warm off-white, and then scarlet gum, which is purple.
     Both tinted the page.

     THE TWO THEMES ARE NOT THE SAME NUMBER, and that is the point of tuning
     them separately. What has to match is the STEP THE EYE SEES, not the
     alpha. Measured in L*, against a blackcurrant card this lands 10.5 away
     from its ground; the light theme lands 8.5 away from white. Those are not
     a compromise, they are where the references sit: Spotify and YouTube are
     at 10.7 and 11.3 on their dark grounds, Facebook and Stripe at 8.8 and 8.2
     on white. Dark surfaces compress, so a dark skeleton needs the bigger step
     to read as the same thing.

     Everything a stand-in draws sits on --cbc-surface, in a .cb-card or a
     .cb-panel, so there is exactly one ground per theme and these can be
     solved rather than eyeballed. */
  --cbc-stand: color-mix(in srgb, var(--cb-color-white) 11%, transparent);
  /* THE SHEEN IS WHITE IN BOTH THEMES, because a highlight adds light. That is
     the one thing about a shimmer that does not flip with the theme: on the
     dark ground it lifts the mark further off the page, on white it washes the
     mark toward the page, and both read as a shine crossing a surface. Made
     the theme's own ink instead, the light-mode band darkened as it passed,
     which is a shadow sweeping the page rather than a light. */
  --cbc-sheen: color-mix(in srgb, var(--cb-color-white) 7%, transparent);

  --cbc-accent: var(--cb-color-accent-500);
  --cbc-accent-hover: var(--cb-color-accent-400);
  --cbc-accent-wash: color-mix(in srgb, var(--cb-color-accent-500) 18%, transparent);
  --cbc-accent-quiet: color-mix(in srgb, var(--cb-color-accent-500) 9%, transparent);
  /* The accent as TEXT, which is a different job from the accent as a surface.
     MEASURED. accent-500 is the brand's colour for buttons and links, where it
     is either large, bold, or on white. As small text on this dark ground it is
     4.11:1, and 3.32 on a selected row: the command palette marks the matched
     letters of a search in it, so the one thing on the row you most need to read
     was the one thing failing AA. accent-300 is the lightest step on the brand's
     own scale that clears 4.5:1 on all three grounds (surface, hover, selected).
     Not a new colour: a step of the official set, used where the ground is dark. */
  --cbc-accent-text: var(--cb-color-accent-300);
  --cbc-on-accent: var(--cb-color-white);

  --cbc-good: var(--cb-color-score-good);
  --cbc-warn: var(--cb-color-orange-peel);
  --cbc-poor: var(--cb-color-accent-500);

  /* ---- The same three, for TEXT ----
     A tone has two jobs and they are held to different bars. As a bar, a dot,
     a fill or a border it is a graphical object and WCAG asks 3:1 of it; as a
     word it is text and the bar is 4.5:1. --cbc-warn is orange peel, which is
     a fine bar on any ground here and 2.28:1 as a sentence on the light
     theme's surface, and warn is asked for as `color:` 69 times.
     This is not a new idea in this file: --cbc-accent-text twelve lines above
     is exactly the same split, made for exactly this reason, and these follow
     it rather than inventing a pattern. The brand values are untouched (rule
     13): every one of these is another step of a scale the brand already
     supplies, or the tone itself where the tone already clears the bar.
     ink-ladder.test.ts measures all three on every ground in both themes. */
  /* Solved rather than picked: the score green is 3.60:1 as a word on this
     ground and the brand has no green scale to step down, so it is lifted
     toward the ink this theme already writes in. 75% is the honest number, not
     the minimum (80% scrapes 4.64). */
  --cbc-good-text: color-mix(in srgb, var(--cb-color-score-good) 75%, var(--cb-color-ceramic-grey));
  --cbc-warn-text: var(--cbc-warn);
  --cbc-poor-text: var(--cb-color-accent-300);

  /* ---- The sixteen audit categories ----
     A spectrum, not a ramp. Stepping one hue through sixteen shares says one of
     them is more than another, which is the wrong thing to say about Speed
     against Accessibility: they are different, not ordered. Every tint holds
     blackcurrant text at better than 5:1, so a label sits inside its colour.
     Light and dark are the same hues, a step deeper where the page is white.

     EVERY TINT IN THE SET GETS A LINE HERE, AND THAT IS NOT OPTIONAL. Four
     screens build the name in script rather than writing it out (weight-bar.ts
     and settings-scoring.ts, and now the two that draw category rows and bars,
     report-scores.ts and business-weakness.ts, all through categoryTint() in
     views/meter.ts), so a tint that exists in the token set and was never
     mapped here resolves to nothing: CSS drops the declaration, the swatch
     loses its colour, and no guard sees it, because check:tokens cannot follow
     a name that was assembled in code. admin/src/category-tint.test.ts is what
     catches it, in both themes and in both directions.

     Written in the token set's own wheel order rather than grouped, for the
     reason tokens.ts gives: the eight newer tints are midpoints between the
     eight original ones, and reading them in order is the only way the set
     reads as one journey round the hues. */
  --cbc-cat-performance: var(--cb-category-tint-performance-dark);
  --cbc-cat-mobileUsability: var(--cb-category-tint-mobile-usability-dark);
  --cbc-cat-accessibility: var(--cb-category-tint-accessibility-dark);
  --cbc-cat-clarity: var(--cb-category-tint-clarity-dark);
  --cbc-cat-crawlability: var(--cb-category-tint-crawlability-dark);
  --cbc-cat-landingPages: var(--cb-category-tint-landing-pages-dark);
  --cbc-cat-seo: var(--cb-category-tint-seo-dark);
  --cbc-cat-linkHealth: var(--cb-category-tint-link-health-dark);
  --cbc-cat-structuredData: var(--cb-category-tint-structured-data-dark);
  --cbc-cat-conversion: var(--cb-category-tint-conversion-dark);
  --cbc-cat-aiVisibility: var(--cb-category-tint-ai-visibility-dark);
  --cbc-cat-aiReadiness: var(--cb-category-tint-ai-readiness-dark);
  --cbc-cat-design: var(--cb-category-tint-design-dark);
  --cbc-cat-security: var(--cb-category-tint-security-dark);
  --cbc-cat-techStack: var(--cb-category-tint-tech-stack-dark);
  --cbc-cat-tracking: var(--cb-category-tint-tracking-dark);
  --cbc-cat-localVisibility: var(--cb-category-tint-local-visibility-dark);
  --cbc-cat-languages: var(--cb-category-tint-languages-dark);
  --cbc-cat-privacy: var(--cb-category-tint-privacy-dark);
  --cbc-cat-commerce: var(--cb-category-tint-commerce-dark);
  --cbc-cat-marketingFootprint: var(--cb-category-tint-marketing-footprint-dark);
  --cbc-cat-contentFreshness: var(--cb-category-tint-content-freshness-dark);
  /* One ink for every one of them, in both themes: the tints are pale in both
     sets by design, which is the property that lets a single ink work. */
  --cbc-on-tint: var(--cb-color-blackcurrant);

  /* ---- The three palettes derived from those tints ----
     The bill, the run states and the funnel. Each one NAMES the category whose
     colour it borrows, and that is the whole rule: never a position, never "the
     third tint". The set went from eight to sixteen the day the focused audit
     kinds landed, and a palette that had reached for an index would have come
     out of that change a different colour with nothing anywhere saying so.

     They all borrow from the ORIGINAL eight, and that is a decision rather than
     an accident of when they were written. Those eight are spaced right round
     the wheel; the eight added since sit at the midpoints, close enough to
     their neighbours that a run state in one and a run state in the other read
     as the same colour at swatch size. A palette here needs five or six things
     that are unmistakably different, so it takes the widely spaced set. */

  /* The bill, in the same pastels. Four is enough: a fifth call is drawn in
     the first tint again, and by then the segments are slivers the eye reads
     as "everything else" rather than as a fifth named thing. */
  --cbc-cost-1: var(--cbc-cat-marketingFootprint);
  --cbc-cost-2: var(--cbc-cat-localVisibility);
  --cbc-cost-3: var(--cbc-cat-techStack);
  --cbc-cost-4: var(--cbc-cat-design);

  /* What became of a scan, in the same pastels. Not good/warn/poor: three
     tones cannot tell Scanning from Waiting, and those two are the pair
     somebody is actually trying to separate when a run is stuck. Ordered the
     way the funnel is, cool to green with the two kinds of loss in the warm
     end, because a scan is a journey too: queued, running, done, and the two
     ways it does not get there. Named for what they mean here and pointed at
     the official tints, so nothing off the scale is introduced. */
  --cbc-run-queued: var(--cbc-cat-techStack);
  --cbc-run-running: var(--cbc-cat-design);
  --cbc-run-complete: var(--cbc-cat-aiVisibility);
  --cbc-run-review: var(--cbc-cat-accessibility);
  --cbc-run-blocked: var(--cbc-cat-performance);

  /* The funnel, in the same pastels the scoring screen grades categories with.
     good/warn/poor are the wrong palette for it: a prospect who did not leave
     an address has not failed a check, and colouring that loss the same red as
     a broken stage reads as an alarm about something nobody did wrong. These
     are stages of a journey, so they run cool to green with the two kinds of
     loss in the warm end. Named for what they mean here, pointed at the
     official tints, so nothing off the scale is ever introduced.

     It sat above the bill's four rather than above its own seven, which is a
     comment describing the block after next: the funnel is `--cbc-flow-*` and
     the four lines under it were the cost bands. Moved to the vars it is about. */
  --cbc-flow-in: var(--cbc-cat-techStack);
  --cbc-flow-mid: var(--cbc-cat-design);
  --cbc-flow-warm: var(--cbc-cat-seo);
  --cbc-flow-out: var(--cbc-cat-aiVisibility);
  /* The warmest step of all: somebody who wanted the report and the mailing
     list. It gets the one tint in the set that is not on the cool-to-green run,
     because it is not another step along the same road. */
  --cbc-flow-best: var(--cbc-cat-marketingFootprint);
  --cbc-flow-held: var(--cbc-cat-accessibility);
  --cbc-flow-lost: var(--cbc-cat-performance);

  /* ---- How long a loop takes ----
     Two speeds, for the animations that never stop. There were nine, at nine
     literal durations nobody had compared against each other: 900ms, 1s, 1.1s,
     1.2s, 1.5s, 1.8s twice and 2.4s twice. check:tokens guards colour, spacing
     and type, and duration was never in its scope, so this is the one corner
     rule 13 could not see into.

     Quick is for something the reader is waiting on: a switch mid-request, a
     caret, a skeleton. Calm is for something getting on with its work that
     nobody is waiting for: a live pulse, a marker holding a state. Built off
     the brand's own work-loop rather than picked, so the pair stays related. */
  --cbc-loop: var(--cb-motion-work-loop);
  --cbc-loop-calm: var(--cb-motion-work-loop-calm);

  /* ---- How long a screen may take to answer before anything stands in for it
     ----
     Under this, nothing is drawn at all: the stand-in is in the document
     holding its space, but it is still at opacity zero when the real content
     replaces it, so the wait passes with the page simply still. Over it, the
     stand-in fades up and says what shape is coming.

     A quarter second is the number because it is roughly where a wait stops
     feeling like a response and starts feeling like a delay, and because
     almost every call this console makes comes back well inside it. Shorter
     and the fast answers start flickering again; much longer and a genuinely
     slow screen sits blank long enough to look broken.

     One value, at the root, used by every stand-in through .cb-stand-in. No
     screen sets its own: a console where one list waits longer than another
     before admitting it is loading is a console that feels inconsistent
     without anybody being able to say why. */
  --cbc-grace: var(--cb-motion-base);

  /* ---- The stagger ----
     How long each thing in a list waits before its turn. Two speeds, and both
     have a ceiling.

     Twenty-two lists on the board used to work this out for themselves, every
     one of them writing its own divisor: --i * fast/3 through --i * fast/24,
     which is 6ms a row at one end and 50ms at the other for what is visually
     one gesture. Twenty of the twenty-two had no ceiling either, so Market and
     Funnel were still arriving 450ms after they started, which is longer than
     the card's own entrance and reads as lag rather than as sequence.

     A custom property rather than a class, because var(--i) is substituted
     where it is USED: --cbc-step resolves against each element's own index,
     so twenty-two rules become one value and no markup has to change.

       --cbc-step       rows, tiles, panels: things you count
       --cbc-step-fine  ticks, bars, cells: things there are dozens of, where
                        30ms each would put the last one a second behind

     The ceiling is --cb-motion-base in both. Past a quarter second the eye has
     stopped reading it as sequence and started reading it as slow. Under about
     forty it has not read it as a sequence at all: the first pass through this
     used a fifth of --cb-motion-fast, six rows landed inside a tenth of a
     second, and what that looks like is one row arriving with a blur on it.

     They are DECLARED further down, on every element rather than here. A
     var() inside a custom property resolves where the property is declared,
     so a copy at :root would resolve --i against the root and hand every
     element a zero. */
  /* ---- How long a plot takes to grow ----
     Longer than an entrance, and on the flat ease rather than the spring.
     --cbc-spring is fast out and slow to settle, which is exactly right for a
     panel arriving and exactly wrong for a quantity being drawn: it reaches
     nine tenths of its height in the first hundred milliseconds, so a chart
     growing out of its baseline was over before anybody saw it start. A
     reading being taken should be watchable. */
  --cbc-plot: calc(var(--cb-motion-slow) * 2);

  --cbc-shadow: 0 var(--cb-space-lg) var(--cb-space-4xl)
    color-mix(in srgb, var(--cb-color-black) 60%, transparent);

  /* ---- The second step, and the reason there has to be one ----
     --cbc-shadow is where a raised thing RESTS. Nothing could be raised ABOVE
     it, so anything wanting to lift on hover had nowhere to lift to. The team
     cluster shows what that costs: its disc rests on --cbc-shadow, carries
     `transition: box-shadow` to animate a change, and asked hover for
     `var(--cbc-raised)`, which is the SURFACE COLOUR whose name happens to say
     raised. A colour is not a shadow, so the declaration was dropped and the
     disc never lifted; naming the step it actually wanted is what stops the
     next person reaching for the colour again.
     One step further out and a little deeper, on the same colour, so the two
     read as one object at two heights rather than as two different lights. */
  --cbc-shadow-high: 0 var(--cb-space-2xl) var(--cb-space-6xl)
    color-mix(in srgb, var(--cb-color-black) 70%, transparent);

  /* ---- The veil ----
     What the page is dimmed with behind anything that opens over it: a modal
     dialog, the command palette, the card library. There were three of these,
     each tuned on its own screen, and the console had reached the point where
     a new modal meant guessing which of the three to copy. The palette's was
     the one that was right, so the palette's is what is written down here.

     Black, not a wash of a named surface. A dialog dimmed with 55% of
     --cbc-raised sat on a page of raised panels and had no edge at all, which
     is what drove the card library to invent a darker backdrop of its own.
     Black and a blur put whatever is behind the modal behind glass, and it
     works the same over a board of cards as over a form.

     The blur is its own property so a theme can drop it (`none`) without
     having to restate the colour to do it. */
  --cbc-veil: color-mix(in srgb, var(--cb-color-black) 55%, transparent);
  /* NONE, AND IT IS A MEASUREMENT RATHER THAN A PREFERENCE.
     
     This was blur(2px), and two pixels of blur over a full viewport is not a
     small thing to ask for. backdrop-filter draws everything behind the veil
     into a texture and filters it again whenever any of it changes, at the
     screen's real pixel count: on a 2x display that is four times the work of
     the 1x one it is easy to measure on, and the console's footer alone changes
     something behind the veil every few seconds.
     
     Measured on the command palette at 2x, opening over the dashboard:
     
                             p99      frames over 20ms
       idle board            11.6ms   0
       open, with blur       24.3ms   4
       open, no blur          9.3ms   0
     
     Four slow frames for sitting still, and eleven while opening against three.
     That is the whole of what people meant by the modals being laggy.
     
     console-walkthrough.css reached this conclusion first and wrote it down:
     backdrop-filter is "the most expensive thing a stylesheet can ask a browser
     for", and that veil has never blurred for exactly this reason. It sits over
     a live console, and so do these.
     
     THE COLOUR DOES ALL THE WORK ANYWAY. The veil is 55% black; what separates
     a modal from the page is the dim, and two pixels of softening underneath it
     is a detail nobody could describe with the dialog closed. Kept as its own
     property, so putting it back is this one line and nothing else. */
  --cbc-veil-blur: none;

  /* The globe. Land in the brand's scarlet rather than a map green, because
     this is a picture of where the leads are, not an atlas. The limb is lit in
     the same hue so the sphere reads as one object under one light. */
  --cbc-globe-land: color-mix(
    in srgb,
    var(--cb-color-accent-400) 82%,
    var(--cb-color-ceramic-grey)
  );
  --cbc-globe-grid: color-mix(in srgb, var(--cb-color-accent-500) 22%, transparent);
  --cbc-globe-glow: color-mix(in srgb, var(--cb-color-accent-500) 55%, transparent);
  --cbc-globe-star: var(--cb-color-ceramic-grey);
  /* How far the field is allowed to come up, and how big each point is. Both
     are read by the canvas rather than fixed in it, because a field that reads
     as stars on a near-black ground is invisible on a near-white one. */
  --cbc-globe-star-lift: 0.5;
  --cbc-globe-star-size: 1.4;

  /* The lead map. The wash and the grid are what shows through before a tile
     arrives, so they have to read as a map on their own rather than as a hole
     in the card. The route is the one coloured thing on the surface, because
     the route is the finding: everything else is context under it. */
  --cbc-map-wash: color-mix(in srgb, var(--cb-color-black) 40%, var(--cb-color-blackcurrant));
  --cbc-map-grid: color-mix(in srgb, var(--cb-color-ceramic-grey) 10%, transparent);
  --cbc-map-line: color-mix(in srgb, var(--cb-color-ceramic-grey) 22%, transparent);
  --cbc-map-route: var(--cb-color-accent-400);
  /* Laid over the tiles before the route is drawn. Map tiles are designed to
     be read on white and arrive far brighter than anything else on a dark
     screen, which makes the card the loudest thing on the page and the route
     the quietest thing on the card. The veil puts them back in their place. */
  --cbc-map-veil: color-mix(in srgb, var(--cb-color-blackcurrant) 52%, transparent);
  --cbc-map-halo: var(--cb-color-blackcurrant);
}

[data-theme="light"] {
  color-scheme: light;

  /* The same ladder, walked the other way. White cards on a near-white page
     read as one undivided surface, so the ground takes the brand's light
     purple and the cards keep white above it. */
  --cbc-base: color-mix(in srgb, var(--cb-color-bg-light-purple) 90%, var(--cb-color-text-primary));
  --cbc-sunken: color-mix(
    in srgb,
    var(--cb-color-bg-light-purple) 94%,
    var(--cb-color-text-primary)
  );
  --cbc-surface: var(--cb-color-white);
  --cbc-raised: var(--cb-color-white);

  --cbc-ink: var(--cb-color-text-primary);
  --cbc-ink-muted: var(--cb-color-text-secondary);
  --cbc-ink-faint: var(--cb-color-text-tertiary);

  /* Stronger than the brand's border-light, which was tuned for a white ground
     and vanishes against a tinted one. */
  --cbc-line: color-mix(in srgb, var(--cb-color-text-primary) 14%, transparent);
  --cbc-line-strong: color-mix(in srgb, var(--cb-color-text-primary) 26%, transparent);

  /* Darkening, and further than the dark theme lightens: ink on white has more
     room to move before it stops being white, and a 4% wash on a white panel is
     not a hover, it is a rendering artefact. */
  --cbc-hover: color-mix(in srgb, var(--cb-color-text-primary) 8%, transparent);
  --cbc-hover-strong: color-mix(in srgb, var(--cb-color-text-primary) 13%, transparent);
  /* The same flat neutral, in the only direction available on white: a step
     down rather than up, taken in the tertiary text grey, which is the one
     genuinely neutral colour in the set. Solved to sit 8.5 in L* below white,
     which is where Facebook and Stripe sit on the same ground.

     The sheen is white here too, and it is the larger number precisely because
     the mark it lifts is already close to white: 52% of it is what moves the
     band 4.5 in L*, which halves the mark's contrast as it passes and then
     gives it back. That is what a light-mode shimmer looks like everywhere it
     ships. */
  --cbc-stand: color-mix(in srgb, var(--cb-color-text-tertiary) 16%, transparent);
  --cbc-sheen: color-mix(in srgb, var(--cb-color-white) 52%, transparent);

  --cbc-accent: var(--cb-color-accent-600);
  --cbc-accent-hover: var(--cb-color-accent-700);
  --cbc-accent-wash: var(--cb-color-accent-50);
  /* The other way on a pale ground: accent-500 is 3.37:1 on white and accent-600
     only 4.79, which fails on the washed row behind a selected result.
     accent-700 clears every ground here. */
  --cbc-accent-text: var(--cb-color-accent-700);
  --cbc-accent-quiet: color-mix(in srgb, var(--cb-color-accent-50) 60%, var(--cb-color-white));
  --cbc-on-accent: var(--cb-color-white);

  --cbc-good: var(--cb-color-score-good);
  --cbc-warn: var(--cb-color-orange-peel);
  --cbc-poor: var(--cb-color-accent-600);

  /* The text siblings on the pale ground, where the gap is widest: orange peel
     is 2.28:1 as a word on white and accent-600 falls to 3.98 on the sunken
     ground. Mixed toward the primary ink rather than picked by eye, so the hue
     is still the tone's own and only the lightness moves. */
  --cbc-good-text: color-mix(in srgb, var(--cb-color-score-good) 60%, var(--cb-color-text-primary));
  --cbc-warn-text: color-mix(
    in srgb,
    var(--cb-color-orange-peel) 45%,
    var(--cb-color-text-primary)
  );
  --cbc-poor-text: var(--cb-color-accent-700);

  /* The same sixteen, a step deeper because the page underneath is white. The
     list is restated in full rather than patched: a theme that mapped only the
     ids somebody remembered would leave the rest showing the dark ground's tint
     on a pale page, which is the one failure here that looks deliberate. */
  --cbc-cat-performance: var(--cb-category-tint-performance-light);
  --cbc-cat-mobileUsability: var(--cb-category-tint-mobile-usability-light);
  --cbc-cat-accessibility: var(--cb-category-tint-accessibility-light);
  --cbc-cat-clarity: var(--cb-category-tint-clarity-light);
  --cbc-cat-crawlability: var(--cb-category-tint-crawlability-light);
  --cbc-cat-landingPages: var(--cb-category-tint-landing-pages-light);
  --cbc-cat-seo: var(--cb-category-tint-seo-light);
  --cbc-cat-linkHealth: var(--cb-category-tint-link-health-light);
  --cbc-cat-structuredData: var(--cb-category-tint-structured-data-light);
  --cbc-cat-conversion: var(--cb-category-tint-conversion-light);
  --cbc-cat-aiVisibility: var(--cb-category-tint-ai-visibility-light);
  --cbc-cat-aiReadiness: var(--cb-category-tint-ai-readiness-light);
  --cbc-cat-design: var(--cb-category-tint-design-light);
  --cbc-cat-security: var(--cb-category-tint-security-light);
  --cbc-cat-techStack: var(--cb-category-tint-tech-stack-light);
  --cbc-cat-tracking: var(--cb-category-tint-tracking-light);
  --cbc-cat-localVisibility: var(--cb-category-tint-local-visibility-light);
  --cbc-cat-languages: var(--cb-category-tint-languages-light);
  --cbc-cat-privacy: var(--cb-category-tint-privacy-light);
  --cbc-cat-commerce: var(--cb-category-tint-commerce-light);
  --cbc-cat-marketingFootprint: var(--cb-category-tint-marketing-footprint-light);
  --cbc-cat-contentFreshness: var(--cb-category-tint-content-freshness-light);
  --cbc-on-tint: var(--cb-color-blackcurrant);

  --cbc-shadow: var(--cb-shadow-raised);
  /* The pale ground's second step. Written out rather than taken from the token
     set because that set stops at two (card, raised) and raised is already the
     resting one here. */
  --cbc-shadow-high: 0 var(--cb-space-2xl) var(--cb-space-6xl)
    color-mix(in srgb, var(--cb-color-blackcurrant) 20%, transparent);

  /* The same veil, and black here too. A dim is the absence of the page, so it
     darkens in both themes; a white veil over a white page is a fog, and the
     modal standing in it loses the edge the veil is there to give it. Restated
     rather than inherited so the light theme can be softened one day without
     anybody having to work out where the value came from. */
  --cbc-veil: color-mix(in srgb, var(--cb-color-black) 55%, transparent);
  /* NONE, AND IT IS A MEASUREMENT RATHER THAN A PREFERENCE.
     
     This was blur(2px), and two pixels of blur over a full viewport is not a
     small thing to ask for. backdrop-filter draws everything behind the veil
     into a texture and filters it again whenever any of it changes, at the
     screen's real pixel count: on a 2x display that is four times the work of
     the 1x one it is easy to measure on, and the console's footer alone changes
     something behind the veil every few seconds.
     
     Measured on the command palette at 2x, opening over the dashboard:
     
                             p99      frames over 20ms
       idle board            11.6ms   0
       open, with blur       24.3ms   4
       open, no blur          9.3ms   0
     
     Four slow frames for sitting still, and eleven while opening against three.
     That is the whole of what people meant by the modals being laggy.
     
     console-walkthrough.css reached this conclusion first and wrote it down:
     backdrop-filter is "the most expensive thing a stylesheet can ask a browser
     for", and that veil has never blurred for exactly this reason. It sits over
     a live console, and so do these.
     
     THE COLOUR DOES ALL THE WORK ANYWAY. The veil is 55% black; what separates
     a modal from the page is the dim, and two pixels of softening underneath it
     is a detail nobody could describe with the dialog closed. Kept as its own
     property, so putting it back is this one line and nothing else. */
  --cbc-veil-blur: none;

  /* Darker land on the light ground, and a much quieter glow: a halo that
     reads as light in a dark room reads as a smudge on a white one. */
  --cbc-globe-land: var(--cb-color-accent-600);
  --cbc-globe-grid: color-mix(in srgb, var(--cb-color-accent-600) 20%, transparent);
  --cbc-globe-glow: color-mix(in srgb, var(--cb-color-accent-500) 30%, transparent);
  /* On a pale ground the field has to darken rather than lighten, and it has to
     come up much further and sit on bigger points: a forty-five percent ink at
     half opacity on white is a smudge nobody can see, which is exactly what it
     was. */
  --cbc-globe-star: var(--cb-color-text-tertiary);
  --cbc-globe-star-lift: 0.95;
  --cbc-globe-star-size: 1.8;

  --cbc-map-wash: color-mix(in srgb, var(--cb-color-bg-light-purple) 70%, var(--cb-color-white));
  --cbc-map-grid: color-mix(in srgb, var(--cb-color-text-primary) 8%, transparent);
  --cbc-map-line: color-mix(in srgb, var(--cb-color-text-primary) 18%, transparent);
  --cbc-map-route: var(--cb-color-accent-600);
  --cbc-map-veil: color-mix(in srgb, var(--cb-color-white) 22%, transparent);
  --cbc-map-halo: var(--cb-color-white);
}

/* ---- hidden means hidden ----
   The browser's own rule for [hidden] is a plain `display: none` on the element
   selector, so any class that sets a display beats it and the attribute does
   nothing. That had already been patched six separate times in five
   stylesheets, once per element somebody noticed. Said once here instead, with
   the weight to actually win. */
[hidden] {
  display: none !important;
}

/* ---- The focus ring ----

   ONE RING, WRITTEN OUT EIGHTEEN TIMES. `outline: 2px solid var(--cbc-accent)`
   appeared in eight stylesheets, and the gap beside it had already drifted into
   four spellings: 2px, -2px, var(--cb-space-xs), and
   calc(var(--cb-space-xxs) * -1), which is -2px said the long way. That is the
   drift the note on --cbc-spring is about, and a ring is worse to leave alone
   than an easing: it is the only thing telling somebody on a keyboard where
   they are.

   The colour comes from --cbc-accent, so it follows the theme on its own
   (accent-500 on the dark ground, accent-600 on the light one) and clears
   WCAG 1.4.11's 3:1 on every surface either theme has. ink-ladder.test.ts
   measures that rather than trusting it, because a ring is a contrast between
   two colours and "use the accent" does not carry from one ground to another:
   the widget sits on ceramic grey and needs a darker step than this one
   (web/landing.css).

   THE INSET IS ITS OWN TOKEN rather than a calc repeated at each site. The
   places that need it are the ones where a parent would clip the ring, and
   they should all pull in by the same amount. 2px stays a literal here for the
   reason the token guard admits it: below 4px is a border or a ring, not a
   spacing decision, and this file is where the console's vocabulary is minted
   rather than copied. */
:root {
  --cbc-focus: 2px solid var(--cbc-accent);
  --cbc-focus-gap: var(--cb-space-xxs);
  --cbc-focus-inset: calc(var(--cb-space-xxs) * -1);

  /* ---- The mark that means THIS ONE IS YOU ----
     Identical to the focus ring today and NOT the same thing, which is the
     whole reason it is written down separately. A ring around a colleague's
     mark in the crew strip says "this one is you"; a ring around a control
     says "you are here". They happen to be drawn the same way, so a sweep
     moving every `2px solid var(--cbc-accent)` onto --cbc-focus would have
     swept these up too, and from then on any change to how this console shows
     keyboard focus would have silently changed how it points at you.
     Pulled in rather than out, because it rings a mark that is already at the
     edge of its own row. */
  --cbc-self: 2px solid var(--cbc-accent);
  --cbc-self-inset: calc(var(--cb-space-xxs) * -1);
}

/* ---- Motion ----
   One easing for everything that ARRIVES, and one for anything large enough to
   be a place rather than a thing. Durations come from the token set. */
:root {
  /* From the token set now, so the widget can use the same curve. */
  --cbc-spring: var(--cb-motion-spring);
  /* ---- The other easing, and the only other one ----

     WHAT --cbc-spring IS FOR. cubic-bezier(0.16, 1, 0.3, 1) is a hard ease-out:
     it is half way in a tenth of its duration and nine tenths of the way in a
     fifth. On a row, a tile, a toast or a panel arriving, that reads as
     answered: the thing is simply THERE and the last few pixels are it
     settling. Everything small keeps it.

     WHY IT IS WRONG FOR A CONTAINER RESHAPING. The same curve on a plane
     crossing a third of the screen is a snap followed by a crawl, and it is
     read as two events rather than one: the eye catches the jump, loses the
     object, and then watches an edge inch the rest of the way. --cbc-plot
     already made this argument once for a quantity being drawn; this is the
     same argument for a place being moved. A move you are meant to FOLLOW has
     to be watchable for most of its length, which means the distance has to be
     spread across the duration rather than spent at the front of it.

     WHAT THIS ONE DOES INSTEAD. It leaves at about a third of --cbc-spring's
     opening speed, holds a near-constant pace through the middle, and flattens
     into its resting place: two thirds of the distance at a fifth of the time
     rather than nine tenths, and no visible stop before the end. It is the
     curve Linear and Vercel move panels and sheets on, which is where the bar
     for this kind of motion is set (docs/04 rule 14).

     WHERE EACH GOES. --cbc-carry is for a plane, a sheet or a pane changing
     where it is or how much room it takes: the setup surface opening out of the
     sign-in wall is the whole of its use today. --cbc-spring stays on
     everything that arrives, including every part of the content that lands
     INSIDE a carried plane once it has stopped. A screen should never be
     running both on the same object. */
  --cbc-carry: cubic-bezier(0.32, 0.72, 0, 1);
  /* ---- and one for a hand ----

     WHAT A PEN DOES IS NOT WHAT A PANEL DOES, and the two curves above are both
     wrong for it in the same way. --cbc-spring and --cbc-carry are ease-outs:
     they leave fast and spend most of their time arriving, which is exactly
     right for a thing being PUT somewhere and exactly wrong for a thing being
     DRAWN. On the signature mark the nib shot through the loop and then crawled
     through the flourish, which is the expressive part, and the whole stroke
     read as juddering even though no frame was ever dropped.

     A hand accelerates into a stroke and eases out of it, fairly evenly, and
     spends the middle at speed. This is that: gentle at both ends, honest
     through the middle, and closer to linear than anything else in this file.
     For marks that draw themselves, and nothing else. */
  --cbc-hand: cubic-bezier(0.55, 0.06, 0.3, 0.96);
  /* ---- and one PAIR, for a picture leaving and a picture coming back ----

     A LUMINANCE FADE IS NOT AN ARRIVAL, and every curve above is wrong for it
     for the same reason --cbc-hand exists. --cbc-spring and --cbc-carry are
     hard ease-outs: on a photograph fading OUT they take most of the picture
     away in the first fifth of the duration, so a fade meant to accompany a
     plane crossing the screen reads as the picture being switched off and the
     plane still moving afterwards.

     WHAT THESE TWO ARE. --cbc-lift is a gentle ease-out for a picture coming UP
     out of the plane's own dark; --cbc-dim is its EXACT INVERSE, mirrored
     through the diagonal (a cubic-bezier reversed is 1-x2, 1-y2, 1-x1, 1-y1),
     for the same picture going back down into it. Being mirrors is the point
     rather than a coincidence: the sign-in wall dissolving to --cbc-sunken and
     the room's own wall rising out of it are two halves of one gesture with the
     dark as its hinge, and a pair that is not symmetrical reads as two
     different fades that happen to meet.

     Gentler than a mirrored --cbc-carry, which was tried first: that one holds
     ninety-six per cent of the picture for two thirds of the move and then
     drops it, which is a blink rather than a dissolve. These spend about thirty
     per cent of the fade in the first half and the rest in the second, so the
     picture holds, then leaves, and never snaps.

     For opacity on a photograph, and nothing else. Anything that MOVES stays on
     --cbc-carry, and anything that ARRIVES stays on --cbc-spring. */
  --cbc-lift: cubic-bezier(0.4, 0.1, 0.35, 1);
  --cbc-dim: cubic-bezier(0.65, 0, 0.6, 0.9);
  /* Wide enough that the longest settings label, "Service catalogue", sits on
     one line in the sub-list. A navigation label that wraps reads as two. */
  /* ---- The corner ----
     Three things want the bottom right: the assistant's launcher, the save
     sign, and the toasts. The launcher is the only permanent one, so it keeps
     the corner and the other two start above it. Stated once here rather than
     as a number repeated in three stylesheets, which is how the save sign and
     the toasts came to disagree in the first place. */
  --cbc-dock-size: var(--cb-space-6xl);
  --cbc-dock-inset: var(--cb-space-3xl);
  --cbc-dock-gap: var(--cb-space-lg);
  --cbc-dock-lift: calc(var(--cbc-dock-inset) + var(--cbc-dock-size) + var(--cbc-dock-gap));

  /* ---- The stacking order ----
     Written down once, because there are now enough floating surfaces that
     reading any one stylesheet gives the wrong answer about which covers which.
     Reading up: the frame, then things the page raises, then the assistant,
     then the modal search over all of it, then the tooltip over everything.
     Popovers in the top layer (the notice panel, menus, the lightbox) sit above
     all of these by definition and are not in the ladder. */
  --cbc-z-rail: 20;
  --cbc-z-savebar: 30;
  --cbc-z-account: 50;
  --cbc-z-toast: 60;
  --cbc-z-saver: 70;
  --cbc-z-scoop-scrim: 78;
  --cbc-z-scoop: 80;
  /* The walkthrough's stage. Over the assistant, which it dims along with
     everything else, and under the search somebody deliberately opened: the
     tour stands down whole for the palette rather than fighting it for the top
     (walkthrough/stage.ts, blocked()). Its card is a popover and so is not in
     this ladder, per the note above. */
  --cbc-z-tour: 82;
  --cbc-z-scrim: 84;
  --cbc-z-menu: 86;
  --cbc-z-notices: 88;
  --cbc-z-tip: 90;
  /* The security gate, and it is the top of the ladder on purpose.
     Everything below it is something the console is showing somebody who is
     allowed to be here; this is the screen that decides whether they are. A
     tooltip or a toast drawn over it would be the console talking across its
     own lock, and the palette opening behind it would offer destinations that
     every call to the server is currently refusing. */
  --cbc-z-gate: 95;

  --cbc-rail: calc(var(--cb-space-11xl) + var(--cb-space-8xl));
  --cbc-rail-tight: var(--cb-space-8xl);

  /* ---- The shelf tools' lockups ----
     ONE WIDTH, SHARED BY ALL SIX, and Momentum is the mark that sets it: it is
     the widest artboard on the shelf, so anything wider would stick out past it
     and anything narrower would crop it. The six are drawn as a set (four of
     them are one artboard within rounding), so matching their widths is what
     the artwork already asks for; src/tool-marks.ts has the full argument.

     THE RAIL FIGURE IS DELIBERATELY UNDER THE ROW. A rail row is a flex line
     holding the mark, a gap and the chevron, so a mark at 100% overflows the
     line and hands a vertical menu a horizontal scrollbar. This leaves room for
     both by construction. */
  --cbc-mark-span: 58%;
  /* The same idea in a page header, where the mark stands alone: the width at
     which Momentum's letters come out the cap height of the h1 it replaces, so
     a screen headed by a logo opens with the weight of one headed by a word.
     0.7 is Poppins' cap height as a share of its em, a fact about the typeface
     rather than a value anybody may pick, and 0.095 is Momentum's own measured
     cap per unit of width. 3xl is what `.cb-page-head h1` is set at, so
     changing the headline's size moves every mark with it. */
  --cbc-mark-span-head: calc(var(--cb-type-2xl-size) * 0.7 / 0.095);

  /* ---- The measure ----
     ONE WIDTH, FOR EVERY SCREEN. Content is capped and centred rather than
     stretched to the window: without a cap a settings row is a label at one end
     of the monitor and its control at the other, and the gap grows with the
     screen.

     There used to be two, a narrow one for reading and forms and a wider one a
     table could ask for with data-width. The argument for it was real enough on
     its own terms, and it was still wrong: navigating between a table and a
     settings screen moved the left edge of the page by eighty pixels each way,
     so the whole console appeared to shift under the pointer for a reason
     nobody could see. Consistency down the column is worth more than a hundred
     and sixty pixels on the one screen that wanted them.

     The wider of the two is the one that survived, because a table is the thing
     with a real claim on width and a form has none: the words in a settings row
     are already capped at the paragraph measure, so all the extra room does is
     sit between them and the control column. */
  --cbc-measure: var(--cb-width-4xl);

  /* The control column in a settings row. Fixed, so a panel of settings has a
     column of controls rather than a ragged edge. */
  --cbc-control-col: var(--cb-width-xxs);

  /* One row of the dashboard board. Every card is one, two or three of these
     tall, so every height on that screen is a multiple of one number instead of
     whatever each card's contents came to. */
  --cbc-card-row: var(--cb-space-11xl);

  /* The room a chart leaves down its left edge for the scale. Fixed rather
     than a share of the card: "$5.00" needs the same number of pixels in a
     half-width card as in a full-width one, and as a percentage it was clipped
     to "$5.0" in the narrow one, which is a different number. */
  --cbc-axis-gutter: var(--cb-space-6xl);

  /* ---- Marks in pseudo-elements ----
     Every icon in this console is a lucide icon (docs/10). These two exist as
     masks rather than as elements because they belong to `::after` on a
     summary, a sort button and a resize corner, where there is no node to hang
     an <svg> off without changing every one of those components.

     They are lucide's own paths, copied verbatim from
     node_modules/lucide-static/icons/chevron-down.svg and move-diagonal-2.svg,
     not redrawn. A mask takes currentColor, so they tint and rotate the way the
     border-and-rotate trick they replaced did, at lucide's weight rather than
     at a weight somebody guessed. */
  --cbc-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  /* The tick inside a checkbox, drawn as a mask so it takes whatever colour the
     box is sitting on rather than being baked into an image. Same trick as the
     chevron above, and the same reason: currentColor does not work in a data
     URI. */
  --cbc-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  --cbc-resize: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 13v6h-6'/%3E%3Cpath d='M5 11V5h6'/%3E%3Cpath d='m5 5 14 14'/%3E%3C/svg%3E");
}

/* ---- A plot moving to new numbers ----

   The entrance grows a chart out of its baseline, which is right the first
   time anybody sees it and wrong every time after: a period changed is the
   same reading over a different window, and the answer to that is the shape
   changing into the new one.

   So every value-carrying part of a plot transitions. carry.ts puts the new
   element back to what its predecessor was worth, takes a box, and sets the
   real values again; these declarations are what turns that into a move. `d`
   is a real animatable property in this browser as long as both paths have the
   same command structure, which two plots over the same number of days always
   do. `holding` is the frame in between, where the element is being put back
   and nothing must be watched. */
.cb-chart-line,
.cb-chart-area,
.cb-trend-line,
.cb-trend-fill,
.cb-trend-bridge,
.cb-trend-stub {
  transition: d var(--cbc-plot) var(--cb-motion-ease);
}
/* The bars and the day columns declare theirs beside their own hover
   transitions in console-charts.css: that sheet loads after this one, so a
   second copy here would be quietly overridden. */
.cb-yield-through,
.cb-yield-loss {
  transition: stroke-dashoffset var(--cbc-plot) var(--cb-motion-ease);
}
[data-holding] {
  transition: none !important;
}

/* ---- Timings that depend on where a thing sits ----

   Declared on every element, and that is not laziness: it is the only way this
   works. A var() written inside a custom property is substituted where the
   property is DECLARED, not where it is used. Put --cbc-step at :root and its
   var(--i) resolves once, against the root, where --i is zero; every element
   then inherits an already-computed zero. Every stagger on the board silently
   flattened to nothing that way, cards included, and it looked exactly like a
   board with no stagger rather than like a bug.

   Declared here, each element computes its own against its own --i and --r.
   Four properties per element is the same shape of cost as inheriting them.

     --cbc-step       rows, tiles, panels: things you count
     --cbc-step-fine  ticks, bars, cells: things there are dozens of
     --cbc-settle     how long a card waits for its turn to arrive
     --cbc-landed     when it has settled enough to put something inside it.
                      Not when its transition ends: --cbc-spring is 95% done
                      in the first third, so a card is visually parked long
                      before it technically stops. Waiting for the full 400ms
                      and then adding a beat on top made every board look like
                      it was thinking about it.
     --cbc-inside     landed, then this thing's own place in the queue. What
                      almost everything on the board uses.

   THE LAST ONE IS THE IMPORTANT ONE, and leaving it out was a real mistake
   that survived several rounds of looking at this. Everything inside a card
   was staggering from zero, which is the same instant the card starts rising,
   so a grid assembling itself or a list of rows arriving one by one happened
   entirely underneath a panel that was still travelling. All of that work was
   being done and none of it could be watched: what reached the eye was a
   glimpse of something moving and then a finished card.

   --cbc-step alone is for the few things that are NOT inside a card and are
   therefore the thing arriving: the stat strip's tiles, the nav's links.

   --cbc-landed reads --r, which the observer sets on the card, so it covers a
   chart on screen at load and a chart scrolled to ten minutes later with one
   value. That is not a coincidence: everything inside a card is PAUSED until
   the card is shown, so a delay counts from the moment its own card arrived
   rather than from the moment the page did.

   The ceiling on both steps is --cb-motion-base. Past a quarter second the eye
   has stopped reading it as sequence and started reading it as slow. */
*,
*::before,
*::after {
  --cbc-step: min(calc(var(--i, 0) * var(--cb-motion-fast) / 3), var(--cb-motion-base));
  --cbc-step-fine: min(calc(var(--i, 0) * var(--cb-motion-fast) / 12), var(--cb-motion-base));
  /* ---- and one for letters ----
     Tighter than a row and looser than a tick: twenty-five milliseconds each,
     which puts the longest heading in this console ("Notifications", thirteen
     letters) at three hundred and twenty. --cbc-step would have taken it to
     six hundred and fifty, which is long enough to read the word before the
     word has finished arriving; --cbc-step-fine puts thirteen letters inside a
     hundred and sixty, which is a blur rather than a sequence. */
  --cbc-step-letter: min(calc(var(--i, 0) * var(--cb-motion-fast) / 6), var(--cb-motion-base));
  --cbc-settle: min(calc(var(--r, 0) * var(--cb-motion-fast) / 4), var(--cb-motion-base));
  --cbc-landed: calc(var(--cbc-settle) + var(--cb-motion-base));
  --cbc-inside: calc(var(--cbc-landed) + var(--cbc-step));
  --cbc-inside-fine: calc(var(--cbc-landed) + var(--cbc-step-fine));
  /* ---- and one for a mark that draws itself ----

     THE THREE BIG MARKS OF THE FIRST RUN SHARE ONE CLOCK, and it is here rather
     than in each of their files because it was in each of their files: the
     second step's shield, the profile stop's signature and the team stop's pair
     of people each wrote `calc(var(--cb-motion-slow) * 3)` out for themselves,
     which is one number in three places and three chances for one screen of a
     run to draw at a different speed from the one before it.

     WHAT IT PACES IS A CONSTRUCTION AS WELL AS A LENGTH. Each of those marks is
     several parts on this one clock, with every part's wait written as a stop
     INSIDE its own keyframe rather than as an `animation-delay`. That is not a
     style: a delay with `backwards` holds an element out of its resting state,
     so a part given a delay of its own would sit out of that state while the
     rest of the mark drew around it. Waits inside the keyframes mean the whole
     mark is one animation with several voices, and it collapses to one frame at
     its resting value for a reader who has asked for less motion, which is what
     a cut means.

     THE ONE DELAY THESE MARKS DO CARRY IS THEIR CARD'S, and it is a single
     number every part of every mark reads (--mark-in, console-ask.css). A mark
     is a band of the card it is on and should arrive on the card's own stagger
     like any other band. What it must not do is start drawing while that box is
     still travelling, and what it must REALLY not do is what it used to: each
     mark declared its own `animation` shorthand, which landed later in the
     cascade at the same weight as the entrance every other band was given and
     silently replaced it, so the mark appeared at full opacity on frame zero,
     over the top of the card that had not finished leaving, while the heading
     underneath it was still held invisible.

     Four slow steps. It was three, and three was too fast for the one of these
     that is a hand rather than a shape: a signature written in six hundred
     milliseconds is a line being wiped on. Sixteen hundred is still over before
     anybody has finished the heading underneath it, and the other two marks
     read as more deliberate for it rather than slower. */
  --cbc-mark-run: calc(var(--cb-motion-slow) * 4);
}

/* ---- The page load ----
   One orchestrated entrance rather than scattered micro-animations:
   panels rise in sequence, and the eye follows the order they matter
   in. Anything marked --i gets its place in the queue.

   BACKWARDS, NOT BOTH, AND IT IS NOT A STYLE CHOICE. `both` holds the
   animation's final value after it ends, and a held value outranks
   every ordinary declaration on the element, its hover states
   included. So an entrance ending at `opacity: 1` pins the element at
   1 forever, whatever the element says about itself.

   It cost us twice before anybody worked out what it was. The prompts
   card's tinted quadrant rendered as a solid red block over its own
   `opacity: 0.09`. The trend card's dashed bridge, drawn deliberately
   at half strength so a gap in the data could not be mistaken for a
   reading, was being drawn at full strength across every gap.

   The rule: an entrance that ends where the element already rests
   RELEASES (`backwards`). Only an animation whose end state is not the
   resting one holds (`both`): the odometer stops mid-strip, a drawn
   line stops with its dash closed, a card signature stops mid-sweep.
   There are 24 of those and 122 of the other.

   THREE ENTRANCES, AND ONLY THREE. There were nine, and they were the
   same three ideas typed out again on each screen that wanted one:
   four keyframes doing an identical fade under four names, and five
   rises travelling 6, 8, 12 and 20 pixels with one of them going the
   other way. Nobody chose those distances against each other; each was
   chosen against nothing.

     cbc-fade    it appears
     cbc-rise    it appears, coming up 20px: a card, a row, a tile
     cbc-emerge  the same, coming up 8px: something small INSIDE one of
                 those, and the pointer note, where a long travel would
                 read as the panel arriving from somewhere else

   Anything that needs a fourth should be a card's own signature
   (a sweep, a crosshair, a ribbon), not a fourth general entrance. */
/* NO `to`, HERE OR IN ANY OF THE ENTRANCES BELOW, and that is the whole
   design rather than an omission. An implicit final keyframe is built from the
   element's own computed values, so an entrance always ends exactly where the
   element says it rests.

   Written out, it ends somewhere the keyframe decided. Every element with an
   opacity of its own then animated up to 1 and corrected itself afterwards:
   the punch card's quiet rows (a day with nothing on it sits at 0.55) rose to
   full brightness and then visibly dimmed, and the trend card's dashed bridge,
   drawn at half strength so a gap could not be mistaken for a reading, was
   pulled to full every time. Anything centred with a transform of its own had
   the same done to it in the other direction.

   One `from` per entrance. The destination is not the animation's business. */
@keyframes cbc-rise {
  from {
    opacity: 0;
    transform: translate3d(0, var(--cb-space-2xl), 0);
  }
}
@keyframes cbc-fade {
  from {
    opacity: 0;
  }
}
/* cbc-rise, the other way up: it comes DOWN into place instead of up.

   One use, and it is a hand-off rather than an entrance. The lockup lifts off
   the sign-in screen as the room opens (console-signin.css) and the next
   screen's lockup arrives into the space it left, from the direction it went.
   Up and away, then down and in: the two halves of one movement, on two
   elements that are never on screen at the same time. Which is why this is not
   cbc-rise with a minus sign bolted on at the call site: the sign says which
   way the gesture goes, and it belongs next to the others. */
@keyframes cbc-drop {
  from {
    opacity: 0;
    transform: translate3d(0, calc(var(--cb-space-2xl) * -1), 0);
  }
}
@keyframes cbc-grow {
  from {
    transform: scaleY(0);
  }
}
/* The same, sideways, for anything that fills along a row. */
@keyframes cbc-grow-x {
  from {
    transform: scaleX(0);
  }
}
/* cbc-pulse lived here and was used by nothing: a scale-and-fade swell, which
   is the shape the paragraph below argues against in as many words. Deleted,
   because nine entrances came back last time precisely by leaving the unused
   ones in the file for somebody to reach for. */
/* Something working, rather than something alerting.
   Opacity only, and slowly: a mark that swells and shrinks reads as an alarm
   demanding attention, and a mark that simply comes and goes reads as
   something quietly getting on with it. The live card's rail and its header
   dot are the only two things on the board that use it.

   HOW FAR DOWN IT BREATHES IS THE CALLER'S (--cbc-breath). There were four
   keyframes here doing this, three of them named cbc-breathe in three
   different files: one dipping to 0.3, one to 0.45, one starting at 0.45 and
   rising, and cbc-alive with a floor at 0.6. Keyframes are global, so the last
   file to load was quietly winning for everybody, and the live card's pulse ran
   a curve written for a form skeleton. One keyframe, one variable.

   THE VARIABLE IS --cbc-breath AND IT USED TO BE --cbc-dim, which is a name
   this file had already spent on the fade curve twenty lines up. One name held
   a cubic-bezier at the root and a number in five callers, and the two are not
   interchangeable: the fallback below could never fire, because the root always
   supplied the curve, so the sixth caller to forget its own number would have
   got opacity: cubic-bezier(...) and CSS would have dropped the declaration
   without a word. Nothing was visibly wrong, which is the only reason it lasted.
   Two meanings on one name is a trap set for whoever writes the next one. */
@keyframes cbc-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: var(--cbc-breath, 0.4);
  }
}
/* Navigation arrives from the side it lives on, which is the direction the eye
   already expects things in a rail to come from. A few pixels only: the point
   is that it settles, not that it travels. */
@keyframes cbc-slide-in {
  from {
    opacity: 0;
    transform: translate3d(calc(var(--cb-space-md) * -1), 0, 0);
  }
}
/* cbc-mark lived here and was used by nothing. It encoded the accent bar down
   the navigation's left edge, which console.css records deleting and docs/10
   forbids outright ("never a coloured bar or hairline down a left edge... the
   single most recognisable tell of a generated interface"). A keyframe for a
   thing the design rules out is a keyframe waiting to be reached for. */
/* A popover comes from the thing that opened it. Scaling from the bottom edge
   is what ties it to the button underneath rather than to the page. */
/* --cbc-pop-from is the DIRECTION it grows out of, and it is why there is one
   of these rather than three. cbc-menu-in and cbc-cop-rise were this keyframe
   retyped with the sign flipped and 0.96 against 0.97: a menu hanging below the
   thing that opened it comes DOWN, a panel rising from an edge comes UP, and
   that is the only difference there ever was between them. Callers set the
   travel; the shape is decided once. */
@keyframes cbc-pop {
  from {
    opacity: 0;
    transform: translate3d(0, var(--cbc-pop-from, var(--cb-space-md)), 0) scale(0.97);
  }
}
/* A caret on a live log. Two steps, not a fade: a terminal cursor is on or it
   is off, and a cursor that breathes reads as a decoration. */
@keyframes cbc-blink {
  to {
    opacity: 0;
  }
}
@keyframes cbc-spin {
  to {
    transform: rotate(1turn);
  }
}
/* An arc drawn round rather than switched on. A conic gradient cannot be
   animated, so the ring is revealed through a cone-shaped clip that opens from
   twelve o'clock: the paint is already there and what moves is how much of it
   can be seen, which is the same thing to look at and costs no repaint. */
@keyframes cbc-sweep {
  from {
    clip-path: polygon(50% 50%, 50% 0%, 50% 0%);
  }
  25% {
    clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%);
  }
  50% {
    clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 100%, 50% 100%);
  }
  75% {
    clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 50%);
  }
  to {
    clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, 50% 0%);
  }
}
/* A ring of segments arriving: each one grows out of the middle it belongs to,
   so the ring assembles rather than appearing already assembled. */
@keyframes cbc-ring-in {
  from {
    opacity: 0;
    transform: scale(0.86);
  }
}
/* The save sign comes up from the corner it lives in and goes back down into
   it, rather than fading in place: the direction is what says "this is the
   edge of the screen talking" instead of "something has appeared over the
   work". */
@keyframes cbc-saver-in {
  from {
    opacity: 0;
    transform: translate3d(0, var(--cb-space-lg), 0) scale(0.96);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes cbc-saver-out {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translate3d(0, var(--cb-space-md), 0) scale(0.98);
  }
}
/* A bar that fills rather than appears, as one object: the sweep runs across
   the whole run, not eight times in sequence. Clipped rather than scaled,
   because scaling squashes the numbers written inside it on the way in. The
   round keeps the capsule's corners through the sweep. */
@keyframes cbc-wipe-x {
  from {
    clip-path: inset(0 100% 0 0 round var(--wipe-round, var(--cb-radius-2xl)));
  }
  to {
    clip-path: inset(0 0 0 0 round var(--wipe-round, var(--cb-radius-2xl)));
  }
}
/* What rides in behind a sweep. A few pixels of lift and a fade: the content
   settles into a container that has already arrived, rather than arriving
   with it. */
/* A thing that lands in place rather than travelling to it: a cell in a grid,
   a reading in a field. Scale only, no fade, because these carry a value as a
   shade and fading a shade in means passing through every lighter shade on the
   way, which is a grid that appears to change its own readings while you look. */
@keyframes cbc-land {
  from {
    transform: scale(0.4);
  }
}
@keyframes cbc-emerge {
  from {
    opacity: 0;
    transform: translate3d(0, var(--cb-space-md), 0);
  }
}
/* Work being done. Bars at different heights running out of phase read as
   activity; the same bars in step read as an alarm. */
@keyframes cbc-wave {
  0%,
  100% {
    transform: scaleY(0.3);
  }
  50% {
    transform: scaleY(1);
  }
}

/* ==================================================================
   THE ODOMETER (views/odometer.ts)

   The console's fifth global motion, and the one for a figure whose
   job is to be the largest thing on the screen. Each digit is its own
   window with a strip of digits behind it, and what moves is the
   strip: one animated transform per digit, none of it on the main
   thread.

   A GEAR TRAIN, NOT A ROW OF SLIDING TEXT. Three things do that, and
   all three are what a real meter does:

     the units wheel turns more times than the one beside it (--t),
     it takes longer to stop (--n), so the figure settles left to
     right, and what is moving fast is not legible, so it blurs and
     clears exactly as it lands.

   No column rolls a distance that means anything. Every one turns
   whole cycles before it stops, or 9 would look like a bigger number
   than 1, which is a lie the shape would tell for free.
   ================================================================== */
.cb-odo {
  display: inline-flex;
  align-items: flex-start;
  font-variant-numeric: tabular-nums;
  line-height: 1em;
}
/* The window. One digit tall, one digit wide, everything else clipped.

   AND THE WINDOW IS WHAT BLURS, not the strip behind it. That is the whole of
   this change and it is worth thirty times the work: a strip is three turns of
   ten digits, so it is thirty ems tall, and a filter on it asks the compositor
   to blur fourteen hundred pixels of glyphs to show one digit's worth. The
   window is one em, already clipped, and blurring it blurs exactly the digit
   somebody is looking at. The reading is the same, and softening the clip edges
   with it is if anything closer to what a wheel does as it curves away.

   The timing still comes off --n, which is this element's own property (it is
   set here by views/odometer.ts, not on the strip), so the cascade that makes
   the units column stop last is unchanged. */
.cb-odo-d {
  display: block;
  block-size: 1em;
  inline-size: 1ch;
  overflow: hidden;
  text-align: center;
  animation: cbc-odo-spin var(--cb-motion-slow) var(--cb-motion-ease) backwards;
  animation-duration: calc(var(--cb-motion-slow) + var(--n, 0) * var(--cb-motion-fast));
}
.cb-odo-fix {
  display: block;
  block-size: 1em;
  line-height: 1em;
}
/* One text node, pre-wrapped at one digit per line, so a column costs a
   layout rather than an element per digit. */
.cb-odo-strip {
  display: block;
  white-space: pre;
  line-height: 1em;
  /* WHERE THE WHEEL IS RESTS ON THE ELEMENT, not at the end of a keyframe, and
     that is what lets the figure be turned again later. --t is whole turns and
     --d is the digit it stops on; move either and the transition below carries
     the wheel there. A card rebuilt for a tab switch hands its new odometer
     what the old one read and only the columns that changed turn (reroll, in
     views/odometer.ts). Ending the entrance inside a keyframe made every
     figure a one-shot: it could arrive, and after that it could only be
     replaced. */
  transform: translate3d(0, calc((var(--t, 1) * 10 + var(--d, 0)) * -1em), 0);
  transition: transform calc(var(--cb-motion-slow) + var(--n, 0) * var(--cb-motion-fast))
    var(--cbc-spring);
  animation: cbc-odo-roll var(--cb-motion-slow) var(--cbc-spring) backwards;
  /* The further right a column sits the longer it takes, so the leading digit
     settles first and the units last. That cascade is the whole reason the
     shape reads as a dial turning instead of as text being retyped. */
  animation-duration: calc(var(--cb-motion-slow) + var(--n, 0) * var(--cb-motion-fast));
  /* NO will-change. It said `transform`, permanently, on every digit on the
     board, which is the mistake the reveal rule two hundred lines down names in
     as many words: a promotion that outlives the thing it was for is a layer
     that outlives it too. A wheel turns for four hundred milliseconds when a
     figure is first drawn and then only when the number changes, and a browser
     promotes an element the moment a transform animation or transition starts
     on it anyway. What will-change bought was a fraction of the first frame of
     something that happens twice a session; what it cost was a permanent layer
     per digit for the life of the page. (`filter` was taken off this list for
     the same reason before it, and now the blur is not on this element at all:
     see the window above.) */
}
/* Putting a wheel back where it was before turning it forward: that step is
   bookkeeping, not motion, and must not be watched. */
.cb-odo-d[data-jump] .cb-odo-strip {
  transition: none;
}
/* No `to`: the spin ends wherever the element says the wheel rests. */
@keyframes cbc-odo-roll {
  from {
    transform: translate3d(0, 0, 0);
  }
}
/* What a spinning wheel looks like: not legible, and not fully lit either.
   Blurred and dimmed while it is moving fast, both clearing by four fifths of
   the way through. The clear has to finish before the roll does, or the digit
   lands and then focuses, which reads as the page catching up rather than as a
   wheel stopping.

   Measured in em rather than pixels, so the same amount of speed shows on a
   figure set at 48px and on one set at 14. A fixed blur is invisible on the
   first and a smear on the second. */
@keyframes cbc-odo-spin {
  0% {
    filter: blur(0.055em);
    opacity: 0.5;
  }
  60% {
    filter: blur(0.03em);
    opacity: 0.8;
  }
  85%,
  100% {
    filter: blur(0);
    opacity: 1;
  }
}

.cbc-enter {
  animation: cbc-rise var(--cb-motion-slow) var(--cbc-spring) backwards;
  animation-delay: var(--cbc-step);
}

/* Things that arrive when they are scrolled to (views/reveal.ts).

   A transition rather than an animation, and the difference matters. An
   animation runs the moment the rule applies, which for anything below the
   fold is a performance to an empty room; a transition sits at the from-state
   until the observer stamps data-shown, so the arrival happens where somebody
   is looking at it.

   The travel is small and the fade is the larger part of the effect. A card
   sliding a long way reads as a page assembling itself; a card easing up a few
   pixels while it resolves reads as a card that was already there. The stagger
   is off --r, the element's place in whatever came into view together, capped
   so a row of four never turns into a queue. */
.cbc-reveal {
  opacity: 0;
  transform: translate3d(0, var(--cb-space-2xl), 0) scale(0.985);
  transition:
    opacity var(--cb-motion-slow) var(--cb-motion-ease),
    transform var(--cb-motion-slow) var(--cbc-spring);
  transition-delay: var(--cbc-settle);
  /* The card is a query container and a stacking context already; promoting it
     keeps a board of a dozen arriving at once off the main thread. */
  will-change: opacity, transform;
}
/* Everything inside waits with it.

   Each card's contents already stagger themselves in on their own animations,
   and those were firing at page load, so by the time somebody scrolled to a
   card its bars had long since finished and only the card itself arrived.
   Pausing descendant animations until the card is shown makes the whole thing
   one sequence: the card comes up, and its contents come up inside it. One
   rule, and every card that already animated its contents gets it for free. */
.cbc-reveal:not([data-shown]) *,
.cbc-reveal:not([data-shown]) *::before,
.cbc-reveal:not([data-shown]) *::after {
  animation-play-state: paused;
}
.cbc-reveal[data-shown] {
  opacity: 1;
  transform: none;
  /* Released once it has arrived: a permanent promotion on every card on the
     board is a permanent layer on every card on the board. */
  will-change: auto;
}
/* Nothing waits for an observer that will never fire. */
@media (prefers-reduced-motion: reduce) {
  .cbc-reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* Everything above is decoration on top of a working page. Somebody who
   has asked their system to stop animating things gets the working page.

   THE DELAY HAS TO GO TOO, and it was the half that was missing. Every
   entrance here fills backwards, so an element with a delay is held at
   opacity 0 until its turn comes: cutting the duration to a millisecond and
   leaving the delay alone turns a smooth stagger into things popping into
   existence one after another, which is more motion to somebody who asked for
   less, not less. Anything that used a delay to phase a loop is already
   stopped by the iteration count above. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    /* The transition half of the same argument the comment above makes about
       animations. A delayed transition still waited out its full delay and then
       moved in a millisecond, which is the same "popping into existence one
       after another" for anybody who asked for less motion. */
    transition-delay: 0ms !important;
    /* And smooth scrolling, which is motion nobody declared as an animation:
       scrollIntoView({ behavior: "smooth" }) obeys this, so one line here
       covers every caller rather than each of them reading the query. */
    scroll-behavior: auto !important;
  }
}

/* ---- While a modal is up, the page behind it holds still ----

   THE BLUR IS ONLY EXPENSIVE IF WHAT IS BEHIND IT MOVES. --cbc-veil-blur is a
   backdrop-filter, which draws everything behind the veil into a texture and
   filters it again on every frame that anything back there changes. Blur a
   still picture and the browser does that once and reuses the result; blur a
   live dashboard and it does the whole viewport again, sixty times a second,
   for as long as the dialog is open.

   The dashboard behind a dialog is never still: a caret blinks, a rail
   breathes, two cards paint to canvas. Every one of those was paying for a
   full-screen re-blur nobody could see through the veil that caused it.

   So everything under the console stops advancing while a modal is open.
   animation-play-state is not a repaint and not a layout: the timelines stop
   and the composited layers sit exactly where they were, so this costs one
   style pass on the way in and one on the way out. The canvases cannot be
   stopped from here and stop themselves (src/modal-still.ts).

   NOT INSIDE THE MODAL. `.cb-console` and not the document, so a spinner on the
   button somebody just pressed, and every entrance the dialog itself runs, are
   untouched: the dialog is a body child and lives outside this. What stops is
   the thing behind the glass.

   The attribute is set by an observer watching for any dialog's `open`
   (modal-still.ts), so this covers the card library, a card's options, every
   confirmation, and whatever gets added next, without a single call site having
   to remember. */
:root[data-modal] .cb-console *,
:root[data-modal] .cb-console *::before,
:root[data-modal] .cb-console *::after {
  animation-play-state: paused !important;
}

/* ---- Focus ----
   Visible, on brand, and never removed. Keyboard is how this console is
   actually driven once somebody knows it. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--cbc-focus);
  outline-offset: var(--cbc-focus-gap);
  border-radius: var(--cb-radius-sm);
}

::selection {
  background: var(--cbc-accent-wash);
  color: var(--cbc-ink);
}

/* Scrollbars, so a dark page does not sprout two bright white gutters. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--cbc-line-strong) transparent;
}

/* ---- How much room, and whether anything moves ----
   Two per-person preferences that a stylesheet can answer on its own, set as
   attributes on <html> by prefs.ts. They are here rather than beside the
   components they affect because both are one decision applied in several
   places, and a density that is defined in four files is a density that drifts.

   The variables are declared for everybody first and overridden second: a
   console whose profile has not loaded yet, or failed to, is a comfortable one
   rather than an unstyled one. `:root` and an attribute selector carry the same
   specificity, so the override below wins on source order alone. */
:root {
  --cbc-row-y: var(--cb-space-lg);
  --cbc-set-y: var(--cb-space-lg);
  --cbc-flow-gap: var(--cb-space-2xl);
}
[data-density="compact"] {
  --cbc-row-y: var(--cb-space-sm);
  --cbc-set-y: var(--cb-space-md);
  --cbc-flow-gap: var(--cb-space-lg);
}

/* Motion, switched off here without being switched off everywhere else.
   The operating system's setting is honoured by every component's own
   prefers-reduced-motion block and always will be; this is for the person who
   wants their photos to animate and their work tool to sit still.

   A blanket override is the one honest way to do this: the alternative is
   every animated rule in eight stylesheets remembering to check a second
   condition, which is a rule somebody forgets on the day they add the ninth.
   1ms rather than 0 so a transitionend still fires for anything waiting on one
   (scoop/focus-mode.ts does), and !important because it has to beat rules
   that set a duration inline on an element mid-animation. The motion guard
   admits 0s, 0ms and 1ms by name for exactly this: a switch being thrown, not
   a duration anybody chose (scripts/check-motion.ts). */
[data-motion="reduced"] *,
[data-motion="reduced"] *::before,
[data-motion="reduced"] *::after {
  animation-duration: 1ms !important;
  /* AND THE DELAYS GO WITH THEM, which is the half that was missing here and
     was never missing from the media query above. The argument is written out
     in full up there and it is the same argument: every entrance in this
     console fills BACKWARDS, so an element with a delay is held at opacity 0
     until its turn comes. Cutting the duration to a millisecond and leaving the
     delay alone does not remove the stagger, it removes the only part of it
     that was smooth: six bands of a card sat invisible for 150, 175, 200, 225,
     250 and 275 milliseconds and then appeared, one at a time, with nothing in
     between. That is more motion to somebody who asked for less, and it is what
     the console's own Motion setting did while the operating system's setting
     did the right thing two rules away. */
  animation-delay: 0ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 1ms !important;
  transition-delay: 0ms !important;
  scroll-behavior: auto !important;
}

/* ---- THE THEME WIPE ----
   Changing the theme unwraps a sweet from the control that was pressed: one
   peppermint arm, starting at nothing, spiralling out over three revolutions
   until the incoming theme has covered the screen. The shape is drawn in
   admin/src/swirl-path.ts and driven by admin/src/theme-swirl.ts; the two
   decisions that are design rather than mechanism are here, with every other
   motion value the console owns.

   BOTH LAYERS ARE HELD STILL and the browser's own crossfade is switched off.
   A crossfade underneath the wipe would mean the outgoing theme dissolving
   while the arm sweeps over it, which is two changes happening at once and
   reads as a smear. The old theme sits exactly where it was until the arm has
   passed over it, which is what makes the edge legible.

   THE CLIP ARRIVES THROUGH A CUSTOM PROPERTY because a script cannot set a
   style on a pseudo-element. --cbc-swirl is set on :root a frame at a time and
   inherits down the view-transition tree to here. Its fallback is `none`, so
   any path where the script does not get to run shows the new theme whole
   rather than showing nothing at all. */
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}
::view-transition-old(root) {
  z-index: 0;
}
::view-transition-new(root) {
  z-index: 1;
  clip-path: var(--cbc-swirl, none);
}

:root {
  /* Longer than anything else here, and deliberately: this is the one moment
     in the console that exists to be enjoyed rather than got through, and
     three revolutions need the room. It is off the shared scale for the same
     reason --cbc-plot is, so it is written out rather than derived. */
  --cbc-swirl-time: 1000ms;
  /* Not the console's spring, which is fast out and slow to settle. A sweet
     being unwrapped does the opposite: it leaves the button at a moderate rate
     and picks up as the arm gets bigger, then leaves at speed. Evaluated in
     TypeScript rather than handed to the browser, because the shape is worked
     out a frame at a time (views/motion-tokens.ts, curveOf). */
  --cbc-swirl-pace: cubic-bezier(0.4, 0.22, 0.75, 0.55);

  /* ---- The working screen's rotating line ----
     How long one line stays up before the next crosses over
     (onboarding/working.ts asks for this number, console-working.css fades on
     it, and one value is what keeps the fade and the swap from drifting apart).
     Long enough to read six words without hurrying, short enough that the
     screen's own dwell shows three of them rather than one. */
  --cbc-word-hold: 900ms;
}

/* ==================================================================
   THE THIRD THEME: PAPER

   AND IT LIVES HERE, BESIDE THE OTHER TWO, RATHER THAN IN THE
   REPORTS STYLESHEET. Print is not a feature of one screen. It is a
   third set of answers to the same questions the two blocks at the
   top of this file answer: what the ground is, what the ink is, what
   a hairline is, whether anything has a shadow. Written in
   console-reports.css it would be one screen's opinion about the
   whole console, and the day somebody prints a submission or the
   activity trail it would either not apply or apply wrongly. Written
   here it is the ladder walked a third way, and every screen gets it.

   THE BROWSER'S OWN SAVE AS PDF IS THE PDF. That is the whole
   argument for thirty lines instead of a library: it exists on every
   platform, it costs nothing in the bundle, it honours the reader's
   own paper size, and it prints what is genuinely on the screen
   rather than a second rendering that can drift from it.

   THE ONE LINE THAT MATTERS MOST is .cb-report-cover. Everything else
   here is tidying; that is a correctness rule. A total taken from a
   capped read is a floor and a cost computed from fixtures is not a
   bill, so a figure that leaves this building without its
   qualification is a figure somebody will quote. It prints, always,
   whatever else is hidden. */
@media print {
  /* The ladder, walked onto paper: one ground, ink from the text tokens, and
     no rung above another. Depth on screen is a step in lightness, and a step
     in lightness on paper is toner spent saying nothing. */
  :root,
  :root[data-theme="dark"],
  :root[data-theme="light"] {
    color-scheme: light;
    --cbc-base: var(--cb-color-white);
    --cbc-sunken: var(--cb-color-white);
    --cbc-surface: var(--cb-color-white);
    --cbc-raised: var(--cb-color-white);
    --cbc-ink: var(--cb-color-text-primary);
    --cbc-ink-muted: var(--cb-color-text-secondary);
    --cbc-ink-faint: var(--cb-color-text-tertiary);
    --cbc-line: var(--cb-color-border-light);
    --cbc-line-strong: var(--cb-color-text-tertiary);
    --cbc-hover: var(--cb-color-transparent);
    /* Paper is a white page, so accent text takes the step this file already
       chose for a white ground rather than the dark theme's accent-300, which
       is 1.89:1 on paper and would print as a smudge at --cb-type-xs-size. */
    --cbc-accent-text: var(--cb-color-accent-700);
    /* ---- The values a WHITE PAGE needs, which paper was not being given ----
       This block already restates the grounds and the ink ladder for paper, and
       stopped there: everything else fell through to the DARK theme, whose
       colours are chosen to sit on near-black. Printed on white that is not a
       near miss, it is unreadable. Measured on white paper before this:
         --cbc-poor-text 1.89   --cbc-warn-text 2.17   --cbc-good-text 2.48
       so every figure the report colours by band came out of the printer as a
       pale smudge, on the one artefact somebody hands to a client.
       The category tints are the same argument in the other direction. They hold
       blackcurrant above 5:1 in BOTH variants, so a printed chip keeps its label
       either way, but the dark variant is tuned to sit on a dark ground and the
       set has a variant meant for a pale one. Paper takes it.
       Lifted verbatim from the light theme rather than solved again: paper IS a
       white page, so a second answer to the same question could only drift. */
    --cbc-accent: var(--cb-color-accent-600);
    --cbc-good: var(--cb-color-score-good);
    --cbc-warn: var(--cb-color-orange-peel);
    --cbc-poor: var(--cb-color-accent-600);
    --cbc-good-text: color-mix(
      in srgb,
      var(--cb-color-score-good) 60%,
      var(--cb-color-text-primary)
    );
    --cbc-warn-text: color-mix(
      in srgb,
      var(--cb-color-orange-peel) 45%,
      var(--cb-color-text-primary)
    );
    --cbc-poor-text: var(--cb-color-accent-700);
    --cbc-cat-performance: var(--cb-category-tint-performance-light);
    --cbc-cat-mobileUsability: var(--cb-category-tint-mobile-usability-light);
    --cbc-cat-accessibility: var(--cb-category-tint-accessibility-light);
    --cbc-cat-clarity: var(--cb-category-tint-clarity-light);
    --cbc-cat-crawlability: var(--cb-category-tint-crawlability-light);
    --cbc-cat-landingPages: var(--cb-category-tint-landing-pages-light);
    --cbc-cat-seo: var(--cb-category-tint-seo-light);
    --cbc-cat-linkHealth: var(--cb-category-tint-link-health-light);
    --cbc-cat-structuredData: var(--cb-category-tint-structured-data-light);
    --cbc-cat-conversion: var(--cb-category-tint-conversion-light);
    --cbc-cat-aiVisibility: var(--cb-category-tint-ai-visibility-light);
    --cbc-cat-aiReadiness: var(--cb-category-tint-ai-readiness-light);
    --cbc-cat-design: var(--cb-category-tint-design-light);
    --cbc-cat-security: var(--cb-category-tint-security-light);
    --cbc-cat-techStack: var(--cb-category-tint-tech-stack-light);
    --cbc-cat-tracking: var(--cb-category-tint-tracking-light);
    --cbc-cat-localVisibility: var(--cb-category-tint-local-visibility-light);
    --cbc-cat-languages: var(--cb-category-tint-languages-light);
    --cbc-cat-privacy: var(--cb-category-tint-privacy-light);
    --cbc-cat-commerce: var(--cb-category-tint-commerce-light);
    --cbc-cat-marketingFootprint: var(--cb-category-tint-marketing-footprint-light);
    --cbc-cat-contentFreshness: var(--cb-category-tint-content-freshness-light);
    --cbc-shadow: none;
    /* A theme that draws no shadows has no second height either. */
    --cbc-shadow-high: none;
  }
  body {
    background: var(--cbc-base);
    color: var(--cbc-ink);
  }
  /* A shadow is a light source, and paper has one already. */
  *,
  *::before,
  *::after {
    box-shadow: none !important;
  }

  /* Chrome is what you press, and paper cannot be pressed. The rail, the
     assistant, the toasts, the save sign, the query bar and the page actions
     are all controls; none of them is part of the answer. */
  .cb-rail,
  .cb-cop,
  .cb-scoop-launch,
  .cb-toast-stack,
  .cb-saver,
  .cb-savebar,
  .cb-page-actions,
  .cb-report-bar {
    display: none !important;
  }
  /* The rail's column and the ground painted behind it go with it, or the
     page prints into the narrow half of a two-column grid with the wide half
     left blank.

     !important THROUGHOUT THIS BLOCK, AND IT IS ORDERING RATHER THAN FORCE.
     This sheet is the second of nine the console loads, so every rule below
     is written BEFORE the rules it has to overturn, and at equal specificity
     the later sheet wins: console.css states this exact selector and would
     keep its two columns on paper. Raising specificity instead would mean
     inventing a selector per rule and guessing which sheet lands last, which
     is the same fight fought once per line. A print block IS a mode switch,
     so it says so. Measured on the harness before this was added: the rail's
     column survived, and the report printed 240px wide. */
  .cb-console[data-state="ready"] {
    grid-template-columns: minmax(0, 1fr) !important;
  }
  .cb-console[data-state="ready"]::before {
    display: none !important;
  }
  /* Written as descendants of the shell, not as bare classes. A bare
     `.cb-main {` here is a second file DEFINING a component that console.css
     owns, which is the collision check:classes exists to catch, and it cannot
     see that an @media print wrapper makes it a third theme rather than a
     second opinion. Scoping says the same thing and stays honest. */
  .cb-console .cb-main {
    padding: 0 !important;
  }

  /* THE QUALIFICATION PRINTS. See the note above: this is the line that stops
     a floor being read as a total. The !important overturns the browser's own
     [hidden] rule as well as any screen styling, which is what lets a run with
     nothing to qualify hold the note back on screen and still print the two
     lines below. */
  .cb-console .cb-report-cover {
    display: flex !important;
  }

  /* AND SO DOES THE PERIOD. .cb-report-bar is the only element on that screen
     that states the range, and it is chrome, so it goes with the chrome above:
     the printed report came off the printer with no dates on it and no run
     time, while every file the same Export menu writes leads with both
     (export-rows.ts, and words.ts printHeadWords is the same two sentences).
     They are drawn hidden, so the screen never carries a second copy of what
     the bar already says, and this rule is the half that turns them on for
     paper. `order` rather than trusting the markup: the cover is a wrapped row
     of sentences, and on a printed page the subject and the period belong
     above the caveats about them wherever they were appended. */
  .cb-console .cb-report-said {
    display: block !important;
    order: -1;
    flex: 1 0 100%;
    color: var(--cbc-ink);
  }

  /* AND THE CAVEATS THAT BECAME MARKS. On screen a capped read, an unfetched
     rate or a fixture install is a small pill you can hover; a pill on paper is
     two words with nowhere to ask what they mean. So the marks go and the
     sentences behind them come back (coverage.ts builds both from one list, so
     they cannot disagree). Same specificity trick as the headline above, to
     beat the [hidden] these are drawn with. */
  .cb-console .cb-cover-chips {
    display: none !important;
  }
  .cb-console .cb-cover-said {
    display: block !important;
    flex: 1 0 100%;
    color: var(--cbc-ink);
  }

  /* The two rules that make a printed table readable. A header group repeats
     at the top of every sheet, so page four is not a grid of unlabelled
     numbers; and a row that may not be split is a row whose figure and whose
     name are never on different pages. */
  .cb-table thead {
    display: table-header-group;
  }
  .cb-table tr {
    break-inside: avoid;
  }
  /* A scroll container prints exactly what was in view and cuts the rest. On
     paper there is no scrolling, so the columns are simply shown. Important
     for the ordering reason above: console-data.css sets overflow-x on this
     same class, later. */
  .cb-table-wrap {
    overflow: visible !important;
  }

  /* CHARTS PRINT. A plot is a reading, not decoration, and browsers drop
     backgrounds and fills on paper unless told otherwise: without this the
     one picture on the report comes out as an empty pair of axes. */
  .cb-report-plot,
  .cb-report-plot svg,
  .cb-chart,
  .cb-chart svg {
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    break-inside: avoid;
  }
}
