/* Reporting-specific layout, sharing the approved Alchemy admin appearance. */
.alchemy-ui.reporting-ui {
  --al-cell-y: .625rem;
  --al-content-width: 90rem;
  /* Categorical palette for the per-product stacked chart (static/
     product_chart.js) -- fixed 7-hue order, validated (dataviz skill:
     node scripts/validate_palette.js) against this page's own light/dark
     chart surfaces, never reordered or cycled per-render. A product past
     slot 7 gets a procedurally generated hue instead (see
     product_chart.js's generatedColor) -- not run through the validator. */
  --al-series-1: #2a78d6;
  --al-series-2: #eb6834;
  --al-series-3: #1baf7a;
  --al-series-4: #eda100;
  --al-series-5: #e87ba4;
  --al-series-6: #008300;
  --al-series-7: #4a3aa7;
  /* The volumetric overlay's ink, and deliberately NOT an eighth
     categorical hue. The palette above is full: run the validator over
     those seven plus any candidate eighth -- deep teal, cyan, fuchsia,
     brown -- and it fails the all-pairs normal-vision floor every time,
     because the eighth has to land within dE 15 of one of the seven
     whatever it is. So the overlay uses a different *channel* instead: one
     near-neutral high-contrast ink for every line, with machines told
     apart by dash pattern and marker shape (product_chart.js's
     OVERLAY_DASHES/OVERLAY_POINTS). It reads as an annotation layer drawn
     over the bars, which is what it is, and it can never be mistaken for a
     product. */
  --al-overlay-ink: #2f2e2e;
}
.alchemy-ui.reporting-ui[data-theme="dark"] {
  --al-bg: #2b2a2a;
  --al-surface: #333232;
  --al-surface-raised: #3b3939;
  --al-border: #504c4b;
  --al-series-1: #3987e5;
  --al-series-2: #d95926;
  --al-series-3: #199e70;
  --al-series-4: #c98500;
  --al-series-5: #d55181;
  --al-series-6: #008300;
  --al-series-7: #9085e9;
  --al-overlay-ink: #f1eeec;
}
.reporting-ui .report-topbar { background: #232323; color: white; border-top: 3px solid var(--al-brand-green); border-bottom: 1px solid var(--al-border); }
.reporting-ui .report-topbar .al-topbar__inner { min-height: 4.5rem; gap: 1.5rem; }
.reporting-ui .report-topbar .al-brand { color: white; text-decoration: none; gap: .875rem; flex-shrink: 0; }
.reporting-ui .report-topbar .al-brand img { width: 2.75rem; height: auto; }
.reporting-ui .report-topbar .al-brand__name { font-size: 1.5rem; letter-spacing: .04em; }
.reporting-ui .report-topbar .al-brand__sub { color: #f1eeec; font-size: .75rem; letter-spacing: .12em; margin-top: .3rem; }
.reporting-ui .report-topbar :focus-visible { outline-color: var(--al-brand-yellow); }
/* Groups the modal trigger buttons (Export, Settings) so they stay adjacent
   -- .al-topbar__inner itself is `justify-content: space-between`
   (alchemy.css), which would otherwise space them apart from each other,
   not just from their neighbors. */
.reporting-ui .al-topbar__modal-triggers { display: flex; align-items: center; gap: .5rem; }
/* Page nav (Reports / Compare). Underlining the active link rather than only
   tinting it keeps the current page legible without relying on color, and
   matches aria-current="page" on the same element -- the two are set together
   in base.html. */
.reporting-ui .al-topbar__nav { display: flex; align-items: center; gap: .25rem; }
.reporting-ui .al-topbar__nav-link {
  color: #f1eeec;
  text-decoration: none;
  font-size: .9375rem;
  font-weight: 600;
  padding: .375rem .75rem;
  border-radius: var(--al-radius-sm, .25rem);
  border-bottom: 2px solid transparent;
}
.reporting-ui .al-topbar__nav-link:hover { background: rgba(255,255,255,.08); color: white; }
.reporting-ui .al-topbar__nav-link--active { color: white; border-bottom-color: var(--al-brand-green); }
/* The global "Times shown in" control: every from/to filter and every chart
   label on the page reads this one setting (main.py's ReportRequest.
   display_mode/browser_timezone), so it lives in the header rather than
   inside any one form -- changing it re-submits the whole current query
   with only display_tz/tz replaced (see the hidden-field replication in
   base.html), taking effect immediately via app.js. */
.reporting-ui .report-topbar__tz { display: flex; align-items: center; flex-wrap: wrap; gap: .625rem; margin: 0; }
.reporting-ui .al-field--inline { flex-direction: row; align-items: center; gap: .5rem; font-size: .8125rem; white-space: nowrap; }
.reporting-ui .al-topbar__auth { display: flex; align-items: center; gap: .625rem; white-space: nowrap; }
.reporting-ui .al-topbar__logout-form { margin: 0; }
.reporting-ui .al-field--inline .al-select { width: auto; min-height: 2.25rem; padding: .375rem .5rem; font-size: .8125rem; }
/* The two filter panes' widths, declared on <body> rather than on .content
   because both the report column and the page header above it inset by
   exactly these amounts -- one pair of numbers, read in two places, so they
   cannot drift apart. Zero by default: a page with no panes (the sign-in
   page) must not reserve room for them. The collapsed states are already
   <body> classes rather than anything inside the report (static/sidebar.js
   sets them there so a pane cannot flash open when
   static/dashboard-nav.js re-renders #dashboard-root), which is what lets
   them zero a variable the header can see. */
.reporting-ui { --pane-left: 0rem; --pane-right: 0rem; }
.reporting-ui--paned { --pane-left: 22rem; --pane-right: 18rem; }
.reporting-ui.reporting-filters-collapsed { --pane-left: 0rem; }
.reporting-ui.reporting-product-filter-collapsed { --pane-right: 0rem; }
/* Padding on the <header>, not margin on its inner container: the green bar
   still runs edge to edge behind the panes (which carry their own
   background on top of it), while .al-topbar__inner keeps the shared kit's
   centered max-width behaviour inside whatever width is left. That is what
   moves the brand and the Export/Settings buttons inboard as a pane opens,
   instead of leaving them underneath it. */
.reporting-ui .report-topbar {
  padding-inline: var(--pane-left) var(--pane-right);
  transition: padding .18s;
}
/* The report column is inset by whichever filter panes are open, so an open
   pane narrows the report rather than covering it (see .filter-pane below).
   This overrides .al-container's own centered `width: min(100% - 3rem,
   --al-content-width)` from the shared kit -- same specificity, won by
   source order, since styles.css is linked after alchemy.css. The kit's max
   width is re-applied to the children instead, so a wide monitor still
   centers the report inside whatever space the panes leave. */
.reporting-ui .content {
  width: auto;
  max-width: none;
  margin-inline: var(--pane-left) var(--pane-right);
  /* Wide enough to clear a .filter-pane__toggle handle, which hangs 2.25rem
     past its pane's inner edge (and, once that pane has slid away, the same
     distance past the viewport edge) -- otherwise the handle would sit on
     top of the report at mid-height in both states. */
  padding-inline: 2.5rem;
  padding-block: 2.5rem 3rem;
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  transition: margin .18s;
}
.reporting-ui .content > * { min-width: 0; width: 100%; max-width: var(--al-content-width); margin-inline: auto; }
.reporting-ui .content:focus { outline: none; }
.reporting-ui .report-header { padding-bottom: 1.75rem; border-bottom: 1px solid var(--al-border); gap: 1.5rem; align-items: flex-start; }
.reporting-ui .report-header > .al-stack { flex: 1 1 24rem; min-width: 0; }
.reporting-ui .report-header h1 { font-size: clamp(1.75rem, 2.5vw, 2.375rem); letter-spacing: -.025em; }
.reporting-ui .report-header .al-eyebrow { color: var(--al-link); }
.reporting-ui .report-header .al-muted { font-size: .9375rem; line-height: 1.6; }
.reporting-ui .report-metrics { grid-template-columns: repeat(4,minmax(0,1fr)); gap: 1rem; padding-top: 1rem; padding-bottom: 1rem; }
.reporting-ui .report-metrics .al-metric { padding: 1.25rem 1.5rem; border-top: 2px solid var(--al-brand-green); }
.reporting-ui .report-metrics .al-metric__value { font-size: clamp(1.75rem, 2.5vw, 2.25rem); overflow-wrap: anywhere; }
.reporting-ui .report-metrics .al-metric__unit { font-size: .875rem; }
/* The unit tag attached to a metric's own number, not just implied by the
   tile's heading above it -- a value must read correctly on its own (a
   screenshot, a quick glance) without needing the heading for context. */
.reporting-ui .report-metrics .al-metric__value-unit { font-size: 1rem; font-family: var(--al-font-body); font-weight: 400; color: var(--al-muted); }
.reporting-ui .report-metrics small { font-size: .8125rem; }
.reporting-ui .dashboard-main { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
/* The two filter panes: #report-filters-sidebar (left -- customer, site,
   machine type, machine index, View range) and #product-filter-sidebar
   (right -- Reported units, Granularity/By/Measured-on, Product). Each
   slides fully off-viewport when collapsed, leaving only its own
   .filter-pane__toggle handle behind.
 *
 * Fixed, but NOT an overlay: an open pane pushes the report inwards via
 * .content's margins above, rather than floating on top of it -- with two
 * panes open, ~39rem of a 1280px viewport would otherwise sit behind them,
 * burying the trend chart and both ends of the slicer's own drag surface.
 *
 * Fixed also means the panes resolve against the viewport, not against
 * #dashboard-root -- which only holds while no ancestor gains `transform`,
 * `filter` or `contain`, any of which would make itself the containing
 * block. #dashboard-root[aria-busy] applies `opacity` (below), which creates
 * a stacking context but not a containing block, so the panes stay put and
 * correctly dim with the report during a navigation.
 *
 * z-index ladder on this page: .al-skip 100 (alchemy.css) > flatpickr's own
 * calendar > .modal-backdrop/.al-modal 50 > .filter-pane 30 >
 * .overview-panel 5 > .overview-nav-arrow 2. */
.reporting-ui .filter-pane {
  position: fixed;
  top: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  width: 22rem;
  max-width: 100vw;
  /* Deliberately no overflow: hidden -- .filter-pane__toggle sits *outside*
     this box so it rides along with the slide and stays on screen in both
     states. A fixed element translated off-viewport is excluded from the
     document's scrollable overflow, so this creates no horizontal scrollbar
     (which would NOT hold if this ever became position: absolute). */
}
.reporting-ui .filter-pane--left { left: 0; }
/* Narrower: it holds one list. Not narrower still, because its own
   Reset/Run row wraps below about 18rem. */
.reporting-ui .filter-pane--right { right: 0; width: 18rem; }
.reporting-ui .filter-pane__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  border-radius: 0;
}
.reporting-ui .filter-pane--left .filter-pane__body { border-left: 0; }
.reporting-ui .filter-pane--right .filter-pane__body { border-right: 0; }
/* The handle: the same small round pill the slicer's nav arrows use
   (.overview-nav-arrow), but its own class rather than a reuse -- this one
   is vertically centred with translateY(-50%), and sharing a rule with
   anything that transforms on state would wipe that out. */
.reporting-ui .filter-pane__toggle {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  border: 1px solid var(--al-border);
  background: var(--al-surface-raised);
  color: var(--al-text);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}
.reporting-ui .filter-pane__toggle:hover { background: var(--al-surface-hover); }
.reporting-ui .filter-pane--left .filter-pane__toggle { right: -2.25rem; }
.reporting-ui .filter-pane--right .filter-pane__toggle { left: -2.25rem; }
/* Collapsed state lives on <body>, not on the <aside>: static/dashboard-nav.js
   replaces #dashboard-root's innerHTML (and both panes with it) on every
   slicer drag/preset/granularity change, so an aside-borne class would render
   the pane open and animate it shut on every single interaction. <body> is
   never re-rendered, which makes that flash structurally impossible. */
.reporting-ui.reporting-filters-collapsed .filter-pane--left { transform: translateX(-100%); }
.reporting-ui.reporting-product-filter-collapsed .filter-pane--right { transform: translateX(100%); }
.reporting-ui.reporting-filters-collapsed .filter-pane--left .filter-pane__toggle,
.reporting-ui.reporting-product-filter-collapsed .filter-pane--right .filter-pane__toggle {
  transform: translateY(-50%) rotate(180deg);
}
.reporting-ui .filter-pane,
.reporting-ui .filter-pane__toggle { transition: transform .18s; }
/* Field layout shared by both panes. Keyed on .filter-pane/.filter-pane__body
   rather than .report-filters, because the right pane's body is a <div>, not
   the form. */
.reporting-ui .filter-pane__body { gap: 1.25rem; }
/* A 22rem pane can't fit the 5-across .filter-dimensions grid the dimension
   filters use everywhere else this form has appeared. */
.reporting-ui .filter-pane .filter-dimensions { grid-template-columns: 1fr; }
.reporting-ui .filter-heading h2 { font: 600 1rem/1.4 var(--al-font-body); }
.reporting-ui .filter-heading { padding-bottom: 1rem; border-bottom: 1px solid var(--al-border); }
.reporting-ui .filter-dimensions { display: grid; grid-template-columns: repeat(5,minmax(0,1fr)); gap: 1rem; }
.reporting-ui .filter-pane .al-field { display: grid; grid-template-columns: auto 1fr; align-content: start; gap: .5rem; font-size: .875rem; }
.reporting-ui .filter-pane .al-field > .al-help { justify-self: end; align-self: end; font-size: .8125rem; }
.reporting-ui .filter-pane .al-field > :is(input,select,.filter-dropdown) { grid-column: 1 / -1; }
/* The dimension filters (templates/_filter_set.html): a <details> whose
   summary is drawn by the .live-dropdown__button rules below so it reads
   as the same select-styled row as the live dashboard's Settings modal.
   With a script the open list floats over the page like that modal's
   (.filter-dropdown--floating, placed by static/filter-cascade.js);
   without one it opens inline, which needs no positioning at all. */
/* Overrides the kit's own details rule (alchemy.css: border-block, 1rem
   padding, a 1rem gap under an open summary, bold summary text). */
.reporting-ui .filter-dropdown { border-block: 0; padding: 0; }
.reporting-ui .filter-dropdown > summary { list-style: none; cursor: pointer; font-weight: 400; }
.reporting-ui .filter-dropdown[open] > summary { margin-bottom: 0; }
.reporting-ui .filter-dropdown > summary::-webkit-details-marker { display: none; }
.reporting-ui .filter-dropdown[open] > .live-dropdown__button::after { transform: translateY(-30%) rotate(-135deg); }
.reporting-ui .filter-dropdown__panel {
  display: grid;
  gap: .375rem;
  padding: .5rem .75rem;
  border: 1px solid var(--al-control-border);
  background: var(--al-surface-raised);
}
.reporting-ui .filter-dropdown__panel .live-dropdown__list { max-height: 16rem; }
/* Fixed, not absolute: the filter pane scrolls (overflow-y: auto), which
   would clip an absolutely positioned list at its edge. The script sets
   left/top/width/max-height from the summary row's on-screen rect, and the
   list inside scrolls once the panel hits that max-height. */
.reporting-ui .filter-dropdown--floating > .filter-dropdown__panel {
  position: fixed;
  z-index: 60;
  display: flex;
  flex-direction: column;
  box-shadow: var(--al-shadow, 0 8px 24px rgba(0, 0, 0, .35));
  overflow: hidden;
}
.reporting-ui .filter-dropdown--floating .live-dropdown__list { max-height: none; }
.reporting-ui .filter-dropdown__panel .al-help { margin: 0; }
/* The search box's own hiding, kept apart from the cascade's `hidden`. */
.reporting-ui .filter-dropdown [data-search-miss] { display: none; }
/* Measured on, while By == Granularity leaves it with nothing to divide.
   Greyed rather than disabled on purpose: a disabled control is dropped from
   FormData, so static/overview.js's navigate() would lose "measure" on the
   next slicer drag (see reconcileMeasureState). */
.reporting-ui .al-field--inert { opacity: .5; }
/* Reported Units, in the right pane above Product. A bare fieldset -- no
   border or padding of its own -- because the pane's own heading already
   frames the group and alchemy.css's default bordered fieldset would draw a
   second box inside it. Each option is a two-column grid: the radio and its
   label on the first row, the muted explanation spanning underneath, so the
   three explanations line up with each other rather than with their own
   labels' varying widths. */
.reporting-ui .report-units { border: 0; padding: 0; margin: 0; display: grid; gap: .625rem; }
.reporting-ui .al-radio {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: .125rem .5rem;
  cursor: pointer;
  font-size: .875rem;
}
.reporting-ui .al-radio input { margin: 0; accent-color: var(--al-brand-green); cursor: pointer; }
.reporting-ui .al-radio__label { font-weight: 600; }
.reporting-ui .al-radio > .al-help { grid-column: 2; font-size: .8125rem; }
.reporting-ui .al-select[multiple] { height: 6rem; padding: .375rem; line-height: 1.4; scrollbar-width: thin; }
.reporting-ui .al-select[multiple] option { padding: .25rem .5rem; }
.reporting-ui .al-select[multiple] option:checked { background: var(--al-selected); color: var(--al-text); }
.reporting-ui .filter-pane .al-input, .reporting-ui .filter-pane .al-select { font-size: .9375rem; min-width: 0; }
/* Reset / Run, at the foot of each pane: equal-width halves of one row
   rather than the default left/right split, which leaves them oddly far
   apart in a pane. The 8rem basis is each button's own comfortable width --
   above it they share a row, below it they stack, so this degrades on its
   own at any pane width rather than overflowing. */
.reporting-ui .filter-pane__body > .al-row:last-child { padding-top: .25rem; gap: .5rem; }
.reporting-ui .filter-pane__body > .al-row:last-child .al-btn { flex: 1 1 8rem; justify-content: center; text-align: center; padding-inline: .75rem; white-space: nowrap; }
/* The two centered modal dialogs (Settings, Export) that replaced the old
   right-docked Settings/Filters/Export drawer -- static/modals.js
   coordinates open/close/backdrop/Escape/mutual-exclusion between the two
   remaining ones (Filters became the two .filter-pane asides above, not a
   modal). Neither modal's markup lives inside #dashboard-root, so unlike the
   filter panes, nothing here needs restoring across an AJAX slicer swap. */
.reporting-ui .modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: rgba(0, 0, 0, .5);
}
.reporting-ui .al-modal {
  position: relative;
  z-index: 51;
  width: 28rem;
  max-width: 100%;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--al-surface);
  border: 1px solid var(--al-border);
  border-radius: var(--al-radius);
  box-shadow: var(--al-shadow);
  overflow-y: auto;
}
.reporting-ui .al-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex: none;
  padding: 1.25rem 1.5rem;
  border-bottom: 1px solid var(--al-border);
}
.reporting-ui .al-modal__header h2 { font: 600 1.125rem/1.4 var(--al-font-body); margin: 0; }
.reporting-ui .al-modal__close {
  background: none;
  border: none;
  margin: -.25rem -.5rem;
  padding: .25rem .5rem;
  border-radius: var(--al-radius);
  font-size: 1.375rem;
  line-height: 1;
  color: var(--al-muted);
  cursor: pointer;
}
.reporting-ui .al-modal__close:hover { color: var(--al-text); background: var(--al-surface-hover); }
.reporting-ui .al-modal__body { flex: 1 1 auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1.25rem; }
.reporting-ui .trend-card h2 { font-size: 1.25rem; margin-top: .5rem; }
.reporting-ui .product-chart-container { position: relative; height: 25rem; }
/* The overview/minimap slicer is a navigation strip, not a metric chart --
   shorter than a trend chart, its own canvas cursor is set from JS
   (overview.js) depending on where the pointer sits relative to the current
   highlighted selection (resize handles, pan, or a fresh drag). */
.reporting-ui .overview-container { position: relative; height: 7.5rem; transition: height .15s; }
/* Explicit, because the canvas is no longer sized by Chart.js's responsive
   handling -- static/overview.js reads this box and sets the backing store
   to it times devicePixelRatio itself. display:block kills the inline
   descender gap that would otherwise make the element a few px taller than
   its own height every frame, which a ResizeObserver reads as a resize. */
.reporting-ui .overview-container > canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* Prev/next arrows overlaid at either end of the slicer (page back/forward
   by one full View range width -- see overview.js's shiftViewRange) --
   absolutely
   positioned within .overview-container so they sit on top of the canvas
   without affecting its layout/size. */
.reporting-ui .overview-nav-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 999px;
  border: 1px solid var(--al-border);
  background: var(--al-surface-raised);
  color: var(--al-text);
  font-size: 1.125rem;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Hidden until the pointer is over the slicer (or, for keyboard users who
     tab to it without a hover event, until it holds focus) -- an idle
     minimap shouldn't permanently show chrome over its own edges. */
  opacity: 0;
  transition: opacity .12s;
}
.reporting-ui .overview-container:hover .overview-nav-arrow,
.reporting-ui .overview-nav-arrow:focus-visible {
  opacity: 1;
}
.reporting-ui .overview-nav-arrow:hover { background: var(--al-surface-hover); }
.reporting-ui .overview-nav-arrow--prev { left: .25rem; }
.reporting-ui .overview-nav-arrow--next { right: .25rem; }
/* The Selection's two bounds, each centred over its own edge of the
   highlight box. static/overview.js writes the left offset in px from the
   same pixel math that draws the box, so they move with it through a drag,
   a scroll and a zoom; it clamps them into the container and merges them
   into a single combined pill when the box is too narrow for two.

   They sit in a band at the top of the container that the canvas leaves
   clear (LABEL_BAND_PX there), so a pill never covers a bar. Brand-green to
   match the box's own stroke, so the pairing needs no explaining. */
.reporting-ui .overview-selection-labels {
  position: absolute;
  inset: 0 0 auto 0;
  height: 15px;
  pointer-events: none;
  z-index: 1;
}
.reporting-ui .overview-selection-label {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  font-size: .6875rem;
  font-weight: 600;
  line-height: 15px;
  color: var(--al-brand-green);
  background: var(--al-surface);
  padding-inline: .25rem;
  border-radius: 999px;
  white-space: nowrap;
}
/* The View range popover, anchored to its button in the slicer panel's
   header row. Follows the gateway's existing anchored-dropdown idiom
   (.live-dropdown) rather than .al-modal: this is a small panel hanging off
   a control, not a centred dialog over a backdrop. */
.reporting-ui .overview-range { position: relative; }
.reporting-ui .overview-range__toggle { font-size: .8125rem; }
.reporting-ui .overview-range__toggle::after {
  content: "";
  display: inline-block;
  margin-left: .375rem;
  border: solid currentColor;
  border-width: 0 1.5px 1.5px 0;
  padding: 2px;
  transform: translateY(-20%) rotate(45deg);
  transition: transform .15s;
}
.reporting-ui .overview-range--open .overview-range__toggle::after {
  transform: translateY(-30%) rotate(-135deg);
}
.reporting-ui .overview-range__panel {
  display: none;
  position: absolute;
  top: calc(100% + .375rem);
  right: 0;
  z-index: 10;
  width: 26rem;
  max-width: calc(100vw - 2rem);
  max-height: 70vh;
  overflow-y: auto;
  padding: .875rem;
  gap: .75rem;
  border: 1px solid var(--al-border);
  border-radius: .5rem;
  background: var(--al-surface-raised);
  box-shadow: 0 6px 18px rgba(0, 0, 0, .18);
}
.reporting-ui .overview-range--open .overview-range__panel { display: grid; }
.reporting-ui .overview-range__presets { display: flex; flex-wrap: wrap; gap: .5rem; }
.reporting-ui .overview-range__presets .al-btn { font-size: .8125rem; padding: .375rem .75rem; }
/* Option colours echo what they control: View range options take the
   minimap bars' colour (--al-muted, as drawColumns paints them), Selection
   range options the highlight box's green (drawSelection). */
.reporting-ui .overview-range__presets .al-btn[data-range-preset] {
  color: var(--al-muted);
  /* border-color: var(--al-muted); */
}
.reporting-ui .overview-range__presets .al-btn[data-selection-preset] {
  color: var(--al-brand-green);
  /* border-color: rgba(154, 227, 131, 0.9); */
  /* background: rgba(154, 227, 131, 0.20); */
}
.reporting-ui .overview-range__presets .al-btn[data-selection-preset]:hover {
  background: rgba(154, 227, 131, 0.35);
}
.reporting-ui .overview-range__presets .al-btn[hidden] { display: none; }
.reporting-ui .overview-range--selection .overview-range__panel { width: 20rem; }
/* Two columns with the button on its own row, rather than three across:
   a datetime-local is a wide control in every browser, and flatpickr's
   altInput ("Aug 10, 2026 12:00 AM") is wider still -- squeezing a third
   column in truncates both fields to uselessness. */
.reporting-ui .overview-range__custom {
  display: grid;
  grid-template-columns: minmax(0,1fr) minmax(0,1fr);
  align-items: end;
  gap: .625rem .75rem;
  padding-top: .75rem;
  border-top: 1px solid var(--al-border);
}
.reporting-ui .overview-range__custom > .al-btn { grid-column: 1 / -1; justify-self: end; }
.reporting-ui .overview-range__custom .al-field {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .375rem;
  font-size: .875rem;
}
.reporting-ui .overview-range__custom .al-field > .al-help { justify-self: end; font-size: .75rem; }
.reporting-ui .overview-range__custom .al-field > input { grid-column: 1 / -1; }
/* Granularity/By/Measured-on sit in the right-hand drawer (they describe
   what a reported value is, not the slicer), so they take
   .filter-pane .al-field's own grid layout; the slicer's header carries no
   controls of its own any more. */
.reporting-ui .filter-value-shape { margin-bottom: .75rem; }
/* Pins the slicer to the top of the viewport once the page scrolls past it,
   like an editor minimap staying reachable regardless of scroll position --
   the page header (.al-topbar) is not itself sticky, so this sticks to the
   literal viewport top, independent of it. Needs its own opaque background
   (the panel otherwise relies on the page background showing through) and a
   shadow so it reads as docked once stuck, not just stuck in place. */
.reporting-ui .overview-panel {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--al-surface);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .15);
  transition: padding-block .15s, gap .15s;
}
/* Compacting the panel below removes a few hundred pixels of reserved flow
   height in one step, right as the panel becomes stuck -- which, being a
   sticky element pinned near the very top of the page, happens after only a
   small scroll distance. Chrome's CSS scroll anchoring then "corrects" for
   that shift by adjusting scrollY to keep some node below the panel visually
   stationary, and since the shift is bigger than the scroll distance that
   triggered it, the correction overshoots and clamps scrollY back to 0 --
   perceived as the page flickering and jumping to the top on the very first
   scroll tick. overflow-anchor: none opts the whole page out of that
   compensation (there's nothing elsewhere on this page relying on it). */
.reporting-ui { overflow-anchor: none; }
/* Zero-net-height marker sibling placed immediately above .overview-panel,
   purely for static/overview.js's sticky-detection IntersectionObserver --
   it scrolls out of the viewport at exactly the moment .overview-panel's own
   `position: sticky; top: 0` starts actually pinning it there, which is the
   standard way to detect "is this sticky element currently stuck" (there is
   no CSS/DOM event for that directly). A real (if tiny) 1px height, cancelled
   by an equal negative margin so it never actually displaces layout -- a
   true 0px target can make IntersectionObserver's isIntersecting flicker at
   the boundary in some browsers (zero-area intersection rect), which this
   avoids. Never rendered otherwise. */
.reporting-ui .overview-sticky-sentinel { height: 1px; margin-bottom: -1px; }
/* Compact mode -- applied by overview.js once the sentinel above confirms
   the panel is pinned to the viewport top. The title isn't needed to keep
   using an already-open slicer, only to set one up -- hiding it and
   shrinking the graph itself keeps the docked slicer from eating space a
   scrolled-down graph/table view needs. Everything keeps working at this
   size: the nav arrows, the View range popover, both Selection pills, the
   drag/resize/new gestures, and -- the one that matters most here, since a
   docked slicer is by definition being used by someone who has scrolled
   away from it -- the scrollable axis strip.

   4.5rem rather than the 3rem this used to be: the canvas now reserves a
   band at each end (15px for the Selection pills, 18px for the axis), so
   48px would have left the bars about fifteen pixels to live in. */
.reporting-ui .overview-panel--stuck { padding-block: .5rem; gap: .5rem; }
.reporting-ui .overview-panel--stuck .overview-panel__title { display: none; }
.reporting-ui .overview-panel--stuck .overview-container { height: 4.5rem; }
/* A slicer-driven update is in flight (static/dashboard-nav.js) -- dim and
   disable interaction with the whole dashboard rather than just the slicer,
   since every section on the page is about to be replaced together. */
#dashboard-root[aria-busy="true"] {
  opacity: .6;
  pointer-events: none;
  transition: opacity .15s;
}
.reporting-ui .al-empty { padding: 2.5rem 1rem !important; color: var(--al-muted); }
.reporting-ui .al-alert .al-btn { align-self: flex-start; }
.reporting-ui .detail-heading { padding: 1.25rem 1.5rem; border-bottom: 1px solid var(--al-border); }
.reporting-ui .detail-heading h2 { font-size: 1.25rem; margin-top: .5rem; }
.reporting-ui .detail-heading h3 { font-size: 1rem; margin: 0; }
.reporting-ui .al-table-wrap + .detail-heading { border-top: 1px solid var(--al-border); }
.reporting-ui .al-table { --al-cell-y: .625rem; font-size: .875rem; }
.reporting-ui .al-table :where(th,td) { padding: var(--al-cell-y) .875rem; vertical-align: middle; }
.reporting-ui .al-table th { font-size: .8125rem; }
.reporting-ui .al-table td { line-height: 1.45; }
.reporting-ui .al-table small { font-size: .8125rem; }
.reporting-ui .al-table tbody tr { background: var(--al-surface); }
.reporting-ui .al-table tbody tr:nth-child(even) { background: var(--al-surface-raised); }
.reporting-ui .al-table tbody tr:hover { background: var(--al-surface-hover); }
.reporting-ui .al-table-wrap { scrollbar-width: thin; }
/* The breakdown table (bucket rows x product columns) has no row-count cap
   of its own -- a wide View range at minute granularity can produce
   thousands of rows -- so it scrolls vertically inside a bounded panel
   instead of pushing the rest of the page down indefinitely. The header
   stays pinned via `sticky` rather than living outside the scroll
   container, so column headers are still visible however far the reader
   has scrolled down the bucket list. */
.reporting-ui .product-bucket-table .al-table-wrap { max-height: 26rem; overflow-y: auto; }
/* A product column the legend / heading focus has left out of the totals
   (see static/product_chart.js' sliceTable). Dimmed through the text colour
   rather than `opacity`: the header and footer cells are sticky with an
   opaque background, and a translucent cell would let the rows scrolling
   beneath it show through. The em dash placeholder is already muted. */
.reporting-ui .product-bucket-table [data-sliced-out] { color: color-mix(in srgb, var(--al-text) 35%, transparent); }
/* The product headings double as slicer toggles: a bare button that reads
   as the heading text it replaced, inheriting the cell's (possibly dimmed)
   colour, with the pointer and an underline on hover as its only cue. */
.reporting-ui .product-bucket-table .breakdown-col-toggle { all: unset; cursor: pointer; color: inherit; font: inherit; white-space: nowrap; }
.reporting-ui .product-bucket-table .breakdown-col-toggle:hover { text-decoration: underline; }
.reporting-ui .product-bucket-table .breakdown-col-toggle:focus-visible { outline: 2px solid var(--al-focus); outline-offset: 2px; border-radius: 2px; }
.reporting-ui .product-bucket-table .breakdown-col-toggle[aria-pressed="true"] { color: var(--al-text); }
.reporting-ui .product-bucket-table .breakdown-slice-status { display: flex; align-items: center; gap: .5rem; margin: 0; }
.reporting-ui .product-bucket-table .breakdown-slice-status[hidden] { display: none; }
/* z-index 2 rather than 1: the frozen Bucket column below pins at 1, and the
   header has to paint over it as the reader scrolls down. */
.reporting-ui .product-bucket-table .al-table thead th { position: sticky; top: 0; z-index: 2; box-shadow: 0 1px 0 var(--al-border); }
/* The Average footer row -- pinned to the bottom of the same scroll
   container as the sticky header above, so both ends of the table stay
   readable no matter how far into the bucket list the reader has scrolled.
   In totals mode a Total row joins it above (see dashboard_content.html),
   and both have to pin without landing on top of each other. */
.reporting-ui .product-bucket-table .al-table tfoot th,
.reporting-ui .product-bucket-table .al-table tfoot td { position: sticky; bottom: 0; z-index: 2; background: var(--al-surface-raised); box-shadow: 0 -1px 0 var(--al-border); }
/* Both footer rows are forced to one typography, which is what makes the
   offset below exact rather than a guess: every footer cell is then
   (2 x --al-cell-y) + (1.45 x its own font-size) tall, and 1.45em resolves
   to the same number in either row. */
.reporting-ui .product-bucket-table .al-table tfoot :where(th,td) { font-size: .8125rem; line-height: 1.45; }
/* Any row but the last pins one row-height higher, so the stack reads
   Total-then-Average with the Average row still flush against the bottom
   edge. The -1px biases the offset *downwards*: sub-pixel rounding of a
   line box could otherwise leave a hairline of scrolling content showing
   between the two, whereas an overlap is simply painted over by the
   bottom-most row, which comes later in the document. */
.reporting-ui .product-bucket-table .al-table tfoot tr:not(:last-child) :where(th,td) {
  bottom: calc(var(--al-cell-y) * 2 + 1.45em - 1px);
}
/* The Bucket column is frozen against the left edge of the same scroll
   container, so a report wide enough to scroll sideways -- one column per
   product, and a site can easily run dozens -- never leaves a row of numbers
   with nothing on screen saying which time bucket they belong to.
   `background: inherit` rather than a fixed colour: the row stripe and the
   hover highlight are both set on the `tr`, so inheriting keeps the frozen
   cell the same shade as the rest of its row instead of cutting a flat
   stripe down the table. The separator is an inset shadow rather than a
   border because `border-collapse: collapse` paints borders on the table,
   not on the cell, so a real border would stay behind while the cell
   travels. The row's own bottom border is unaffected -- it runs the full
   width of the row, so whichever cell is underneath draws the same line.
   The empty-state row is excluded: its single `colspan` cell is both the
   first and the last child, and freezing it would pin the "no data" message
   to one edge of a table that has nothing to scroll past it anyway. */
.reporting-ui .product-bucket-table .al-table tbody td:first-child:not(.al-empty) {
  position: sticky;
  left: 0;
  z-index: 1;
  background: inherit;
  white-space: nowrap;
  box-shadow: inset -1px 0 0 var(--al-border);
}
/* The Total column is frozen against the right edge for the mirror-image
   reason: the row total is the one number a reader compares buckets on, and
   scrolling right to find it meant losing the products it was a total of.
   Same inherit/inset-shadow treatment as the Bucket column, and the same
   empty-state carve-out (that one cell is first and last child at once). */
.reporting-ui .product-bucket-table .al-table tbody td:last-child:not(.al-empty) {
  position: sticky;
  right: 0;
  z-index: 1;
  background: inherit;
  box-shadow: inset 1px 0 0 var(--al-border);
}
/* The four corners. Each is frozen on both axes at once and has to paint
   above both the row it is in and the column it is in, so it carries both
   separators and the highest z-index of the three. */
.reporting-ui .product-bucket-table .al-table thead th:first-child { left: 0; z-index: 3; box-shadow: 0 1px 0 var(--al-border), inset -1px 0 0 var(--al-border); }
.reporting-ui .product-bucket-table .al-table tfoot th:first-child { left: 0; z-index: 3; box-shadow: 0 -1px 0 var(--al-border), inset -1px 0 0 var(--al-border); }
.reporting-ui .product-bucket-table .al-table thead th:last-child { right: 0; z-index: 3; box-shadow: 0 1px 0 var(--al-border), inset 1px 0 0 var(--al-border); }
.reporting-ui .product-bucket-table .al-table tfoot td:last-child { right: 0; z-index: 3; box-shadow: 0 -1px 0 var(--al-border), inset 1px 0 0 var(--al-border); }

/* -- The comparison page (/compare). Set A is the baseline throughout and is
   coloured --al-series-1; set B is the comparison and is --al-series-2. The
   same two hues key the drawers, the window captions and (chunk 4) the two
   slicer strips, so "which one is A" is answered once, visually, everywhere. */
.reporting-ui .compare-windows { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 1rem 1.5rem; padding: 1rem 1.25rem; }
.reporting-ui .compare-window { border-left: 3px solid var(--al-border); padding-left: .875rem; min-width: 0; }
.reporting-ui .compare-window--a { border-left-color: var(--al-series-1); }
.reporting-ui .compare-window--b { border-left-color: var(--al-series-2); }
.reporting-ui .compare-window__range { font-family: var(--al-font-mono, ui-monospace, monospace); font-size: .9375rem; margin: .125rem 0; overflow-wrap: anywhere; }
.reporting-ui .compare-warning { border-left: 3px solid var(--al-warning); padding: .875rem 1.25rem; font-size: .9375rem; line-height: 1.6; }

/* auto-fit rather than the dashboard's fixed repeat(4): this row carries five
   tiles whatever By is set to, and each tile is taller than the dashboard's
   (two values plus a badge), so a fixed column count would strand one tile
   alone on a second row at most widths. */
.reporting-ui .compare-metrics { grid-template-columns: repeat(auto-fit,minmax(13rem,1fr)); gap: 1rem; padding-top: 1rem; padding-bottom: 1rem; }
.reporting-ui .compare-metric { padding: 1.125rem 1.25rem; border-top: 2px solid var(--al-brand-green); display: flex; flex-direction: column; gap: .5rem; }
.reporting-ui .compare-metric .al-metric__unit { font-size: .875rem; }
.reporting-ui .compare-metric__sides { display: flex; flex-direction: column; gap: .25rem; }
.reporting-ui .compare-metric__side { display: flex; align-items: baseline; gap: .5rem; font-size: 1.375rem; overflow-wrap: anywhere; }
/* A/B letter, not just a colour swatch -- the key has to survive a monochrome
   screenshot and a colour-vision deficiency. */
.reporting-ui .compare-metric__key { flex: none; width: 1.25rem; height: 1.25rem; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; font-size: .75rem; font-weight: 700; color: var(--al-brand-charcoal, #20251e); }
.reporting-ui .compare-metric__side--a .compare-metric__key { background: var(--al-series-1); }
.reporting-ui .compare-metric__side--b .compare-metric__key { background: var(--al-series-2); }
.reporting-ui .compare-metric .al-metric__value-unit { font-size: .8125rem; font-family: var(--al-font-body); font-weight: 400; color: var(--al-muted); }
.reporting-ui .compare-metric small { font-size: .8125rem; }

/* The percentage badge. Colour reinforces the glyph and the signed number; it
   never carries the direction on its own. */
.reporting-ui .compare-delta { display: inline-flex; align-items: center; gap: .25rem; font-weight: 700; font-size: 1rem; white-space: nowrap; }
.reporting-ui .compare-delta--up { color: var(--al-success); }
.reporting-ui .compare-delta--down, .reporting-ui .compare-delta--gone { color: var(--al-danger); }
.reporting-ui .compare-delta--flat, .reporting-ui .compare-delta--none { color: var(--al-muted); }
.reporting-ui .compare-delta--new { color: var(--al-link); font-style: italic; }

/* The dual-window slicer: two strips sharing one x-axis and one bucket grid,
   with the prev/next arrows flanking both (they page the shared View range, so
   they belong to the pair, not to either strip). */
.reporting-ui .compare-slicer { display: flex; align-items: stretch; gap: .25rem; position: relative; }
/* The arrows are absolutely positioned over the dashboard's single canvas.
   Here there are two canvases and the arrows flank both, so they become
   ordinary flex items instead -- otherwise they would sit on top of the
   strips. The hover reveal has to key off this container rather than
   .overview-container, which this page has none of. */
.reporting-ui .compare-slicer .overview-nav-arrow { position: static; transform: none; align-self: center; flex: none; }
.reporting-ui .compare-slicer:hover .overview-nav-arrow { opacity: 1; }
.reporting-ui .compare-slicer__strips { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: .5rem; }
/* --compare-strip is one strip's height without an axis band. The bottom
   strip carries the page's one axis (data-axis), and is taller by exactly
   that band (18px, AXIS_BAND_PX in static/compare-slicer.js), so the two
   plots stay the same height and their bars read on one scale of space. */
.reporting-ui .compare-slicer__strips { --compare-strip: 4.5rem; }
.reporting-ui .compare-slicer__strip { display: flex; gap: .5rem; height: var(--compare-strip); }
.reporting-ui .compare-slicer__strip--axis { height: calc(var(--compare-strip) + 18px); }
/* Level with the plot, below the 15px band the edge pills sit in. */
.reporting-ui .compare-slicer__strip > .compare-metric__key { margin-top: 15px; }
.reporting-ui .compare-slicer__plot { position: relative; flex: 1 1 auto; min-width: 0; }
/* Sized by this box, not by Chart.js -- see .overview-container > canvas. */
.reporting-ui .compare-slicer__plot > canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* Each window's edge pills in its own side's colour, the box's own stroke,
   rather than the dashboard's brand green. */
.reporting-ui .compare-slicer__strip--a .overview-selection-label { color: var(--al-series-1); }
.reporting-ui .compare-slicer__strip--b .overview-selection-label { color: var(--al-series-2); }
.reporting-ui .compare-slicer__controls { gap: .5rem; align-items: center; }
/* The window-size options size both windows, which are blue and orange, so
   the dashboard's green for "the Selection" would point at neither. */
.reporting-ui .compare-slicer-panel .overview-range__presets .al-btn[data-selection-preset] { color: var(--al-text); }
.reporting-ui .compare-slicer-panel .overview-range__presets .al-btn[data-selection-preset]:hover { background: var(--al-surface-hover); }
/* Stuck (sticky) mode shrinks the strips, the same trade the dashboard's own
   slicer makes to stay out of the way while scrolling; everything still
   works at this size, the axis strip included. */
.reporting-ui .compare-slicer-panel.overview-panel--stuck .compare-slicer__strips { --compare-strip: 2.75rem; gap: .25rem; }

/* The left pane's two filter sets, each a <details> that collapses on its
   own. The summary carries the same A/B circle as the tiles and the slicer
   strips, so the letter means the same thing everywhere on the page. */
/* Overrides the kit's own details rule (alchemy.css: border-block and 1rem
   of padding), which would double the divider between the two sets. */
.reporting-ui .compare-filter-set { border-top: 0; padding: 0 0 1rem; }
.reporting-ui .compare-filter-set[open] > .compare-filter-set__summary { margin-bottom: 0; }
.reporting-ui .compare-filter-set:not([open]) { padding-bottom: 0; }
.reporting-ui .compare-filter-set__summary {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .25rem 0 .75rem;
  cursor: pointer;
  list-style: none;
  font: 600 1rem/1.4 var(--al-font-body);
}
.reporting-ui .compare-filter-set:not([open]) > .compare-filter-set__summary { padding-bottom: .25rem; }
.reporting-ui .compare-filter-set__summary::-webkit-details-marker { display: none; }
/* The same chevron as the View range toggle, pointing down when open. */
.reporting-ui .compare-filter-set__summary::after {
  content: "";
  margin-left: auto;
  border: solid currentColor;
  border-width: 0 1.5px 1.5px 0;
  padding: 3px;
  transform: rotate(-45deg);
  transition: transform .15s;
}
.reporting-ui .compare-filter-set[open] > .compare-filter-set__summary::after { transform: translateY(-25%) rotate(45deg); }
.reporting-ui .compare-filter-set__summary:focus-visible { outline: 2px solid var(--al-brand-green); outline-offset: 2px; border-radius: .25rem; }
.reporting-ui .compare-key--a { background: var(--al-series-1); }
.reporting-ui .compare-key--b { background: var(--al-series-2); }
/* Two sets of six multi-selects in one pane: it scrolls (.filter-pane
   already sets overflow-y), and collapsing a set is how a reader makes room
   for the other. */
.reporting-ui .compare-filters .filter-dimensions { gap: .875rem; }

/* The diverging bar chart. Its canvas grows with the product count (set in
   comparison_chart.js from the payload length) and this container caps and
   scrolls, rather than compressing fifty products into unreadable slivers --
   the same treatment the product breakdown table already gets. */
.reporting-ui .comparison-chart-container { position: relative; max-height: 32rem; overflow-y: auto; overflow-x: hidden; }
.reporting-ui .comparison-chart-key { display: flex; align-items: center; gap: .375rem; white-space: nowrap; }
.reporting-ui .comparison-chart-key__swatch { width: .75rem; height: .75rem; border-radius: 2px; display: inline-block; }
.reporting-ui .comparison-chart-key__swatch--a { background: var(--al-series-1); }
.reporting-ui .comparison-chart-key__swatch--b { background: var(--al-series-2); }

.reporting-ui .product-comparison-table .al-table-wrap { max-height: 30rem; overflow-y: auto; }
.reporting-ui .product-comparison-table .al-table thead th { position: sticky; top: 0; z-index: 1; box-shadow: 0 1px 0 var(--al-border); }
@media (max-width: 65rem) {
  .reporting-ui .report-topbar .al-topbar__inner { padding-block: 1rem; }
  .reporting-ui .report-metrics { grid-template-columns: repeat(2,minmax(0,1fr)); }
  .reporting-ui .compare-windows { grid-template-columns: minmax(0,1fr); }
  /* Narrower panes here: at 1024px two full-width ones would leave the
     report itself barely 330px. */
  .reporting-ui--paned { --pane-left: 18rem; --pane-right: 16rem; }
  .reporting-ui .filter-pane { width: 18rem; }
  .reporting-ui .filter-pane--right { width: 16rem; }
}
@media (max-width: 40rem) {
  .reporting-ui .report-topbar .al-topbar__inner { flex-direction: row; }
  .reporting-ui .content { padding-block: 1.5rem 2rem; }
  .reporting-ui .report-metrics .al-metric { padding: 1rem; }
  .reporting-ui .report-header > .al-stack { flex-basis: 100%; }
  .reporting-ui .product-chart-container { height: 14rem; }
  .reporting-ui .overview-container { height: 5.5rem; }
  .reporting-ui .detail-heading { padding: 1rem; }
  .reporting-ui .al-table { min-width: 44rem; }
  /* Phones get the overlay behavior back, because there is no width left to
     give away: the panes float over a full-width report instead of insetting
     it, and static/sidebar.js starts them both collapsed and keeps them
     mutually exclusive here. */
  .reporting-ui--paned { --pane-left: 0rem; --pane-right: 0rem; }
  .reporting-ui .content { margin-inline: 0; padding-inline: 1.75rem; }
  .reporting-ui .filter-pane,
  .reporting-ui .filter-pane--right { width: min(20rem, 85vw); }
  /* Flush rather than offset, so the 1.75rem gutter above clears them
     exactly -- 2.5rem each side is too much of a 375px screen to give
     away. */
  .reporting-ui .filter-pane--left .filter-pane__toggle { right: -1.75rem; }
  .reporting-ui .filter-pane--right .filter-pane__toggle { left: -1.75rem; }
}
/* Last, so it wins on source order against the equal-specificity transition
   rules above. */
@media (prefers-reduced-motion: reduce) {
  .reporting-ui .content,
  .reporting-ui .filter-pane,
  .reporting-ui .filter-pane__toggle { transition: none; }
}
/* flatpickr appends its calendar popup as a direct child of <body> (which
   itself carries data-theme, per base.html), not inside .overview-container
   or any other reporting-ui-scoped element -- so it needs its own explicit
   re-theme rather than inheriting --al-* vars through descendant selectors.
   Only the dark variant is overridden; flatpickr's own default light theme
   already matches this page's light mode closely enough. */
body[data-theme="dark"] .flatpickr-calendar {
  background: var(--al-surface-raised);
  border-color: var(--al-border);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .35);
}
body[data-theme="dark"] .flatpickr-calendar.arrowTop::before,
body[data-theme="dark"] .flatpickr-calendar.arrowTop::after { border-bottom-color: var(--al-surface-raised); }
body[data-theme="dark"] .flatpickr-months,
body[data-theme="dark"] .flatpickr-weekdays { background: var(--al-surface-raised); }
body[data-theme="dark"] .flatpickr-current-month,
body[data-theme="dark"] .flatpickr-weekday,
body[data-theme="dark"] .flatpickr-day { color: var(--al-text); }
body[data-theme="dark"] .flatpickr-day.flatpickr-disabled,
body[data-theme="dark"] .flatpickr-day.prevMonthDay,
body[data-theme="dark"] .flatpickr-day.nextMonthDay { color: var(--al-muted); }
body[data-theme="dark"] .flatpickr-day:hover { background: var(--al-surface-hover); }
body[data-theme="dark"] .flatpickr-day.selected,
body[data-theme="dark"] .flatpickr-day.selected:hover { background: var(--al-brand-green); border-color: var(--al-brand-green); }
body[data-theme="dark"] .flatpickr-time input,
body[data-theme="dark"] .numInputWrapper span.arrowUp,
body[data-theme="dark"] .numInputWrapper span.arrowDown { color: var(--al-text); }

/* Settings-modal-specific content: page-wide (per-browser,
   localStorage-backed -- see static/settings-storage.js) display
   preferences, consolidated out of the old topbar tz/weight-unit form and
   standalone dark-mode toggle (base.html used to render both directly in
   the header). Positioning/backdrop come from the shared .al-modal rules
   above; this is only the vertical stacking .report-topbar__tz's normal
   (horizontal, topbar) layout doesn't have on its own. */
.reporting-ui .settings-modal .report-topbar__tz { flex-direction: column; align-items: stretch; gap: 1rem; }
.reporting-ui .settings-modal .al-field { gap: .375rem; }
/* theme.css paints the toggle's label white for the always-dark top bar;
   inside a (possibly light) modal it has to follow the text colour. */
.reporting-ui .settings-modal .al-theme-toggle { color: var(--al-text); justify-content: flex-start; align-self: flex-start; }

/* -- The live dashboard (/live, templates/live.html, static/live.js) --------
   Deliberately sparse: a title, a clock, four panels. Meant to be left up on
   a wall screen, so the clock is big, the panels share the viewport evenly,
   and nothing competes with the charts for attention. Every colour is a
   token from alchemy-tokens.css/the .reporting-ui overrides above, so light
   and dark both follow the shared theme toggle. */
.live-ui .content { padding-block: 1.75rem 2rem; gap: 1.25rem; }
/* Edge to edge, less the content padding: the report pages' 90rem cap
   (.reporting-ui .content > *) leaves wide bands either side on a
   full-screen wall display, where the charts want every pixel. */
.live-ui .content > * { max-width: none; }
.live-ui .live-dashboard { display: flex; flex-direction: column; gap: 1.25rem; }
.live-ui .live-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--al-border);
}
.live-ui .live-header__titles { min-width: 0; display: flex; flex-direction: column; gap: .5rem; }
.live-ui .live-header h1 { font-size: clamp(1.75rem, 2.5vw, 2.375rem); letter-spacing: -.025em; }
.live-ui .live-title { display: flex; align-items: center; gap: .75rem; }
.live-ui .live-title__mark { width: 1.4em; height: auto; flex: none; }
.live-ui .live-subtitle { display: flex; flex-wrap: wrap; gap: .25rem .75rem; color: var(--al-muted); font-size: 1rem; }
.live-ui .live-subtitle span:empty { display: none; }
.live-ui .live-subtitle span + span::before { content: "·"; margin-right: .75rem; color: var(--al-border); }
.live-ui .live-clock { text-align: right; display: flex; flex-direction: column; gap: .125rem; }
.live-ui .live-clock__time { font: 400 clamp(2rem, 4vw, 3rem)/1 var(--al-font-display); letter-spacing: .02em; }
.live-ui .live-clock__date { font-size: 1rem; }
.live-ui .live-status { margin: 0; padding: .5rem .75rem; background: var(--al-surface-raised); color: var(--al-muted); border-inline-start: 3px solid var(--al-brand-green); }
.live-ui .live-status--stale { border-inline-start-color: var(--al-warning); color: var(--al-warning); }
.live-ui .live-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2.5rem 3rem; }
/* Unframed: no border, no surface fill, no padding -- the four panels sit
   directly on the page background so the dashboard reads as one surface
   rather than four tiles. Overrides .al-panel from the shared kit. */
.live-ui .live-panel { display: flex; flex-direction: column; gap: .75rem; padding: 0; background: transparent; border: 0; }
.live-ui .live-panel__header { display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
.live-ui .live-panel__header h2 { font: 600 1.0625rem/1.4 var(--al-font-body); }
/* Technician-mode tools set their own display, which would otherwise
   beat the [hidden] attribute that hides them while the mode is off. */
.live-ui [data-technician-only][hidden] { display: none; }
/* The deviation panel's "vs rest of mix / vs own average" switch: two
   buttons, one pressed, sitting beside the heading. */
.live-ui .live-toggle { display: inline-flex; border: 1px solid var(--al-border); border-radius: 999px; padding: 2px; }
.live-ui .live-toggle__option {
  border: 0; border-radius: 999px; padding: .125rem .625rem;
  background: transparent; color: var(--al-muted); font: inherit; font-size: .8125rem; cursor: pointer;
}
.live-ui .live-toggle__option[aria-pressed="true"] { background: var(--al-surface-raised); color: var(--al-text); }
.live-ui .live-toggle__option:focus-visible { outline: 2px solid var(--al-link); outline-offset: 1px; }
.live-ui .live-panel__caption { flex-basis: 100%; }
/* Replay: a quiet button until it is on, then a bar of controls whose
   left rule marks the whole page as "not live". */
.live-ui .live-replay { display: flex; justify-content: flex-end; margin-top: -.75rem; }
.live-ui .live-replay__controls { flex: 1 1 auto; display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .75rem; }
.live-ui .live-replay--on {
  justify-content: flex-start; margin-top: 0; padding: .5rem .75rem;
  background: var(--al-surface-raised); border-inline-start: 3px solid var(--al-warning);
}
.live-ui .live-replay__date { display: inline-flex; align-items: center; gap: .5rem; color: var(--al-muted); }
.live-ui .live-replay__slider { flex: 1 1 14rem; min-width: 10rem; accent-color: var(--al-brand-green); }
.live-ui .live-replay__time { font: 600 1.125rem/1 var(--al-font-display); min-width: 3.5rem; text-align: center; }
.live-ui .live-chart { position: relative; height: clamp(15rem, 32vh, 24rem); }
.live-ui .live-empty { display: flex; align-items: center; justify-content: center; min-height: clamp(15rem, 32vh, 24rem); margin: 0; text-align: center; }
@media (max-width: 64rem) {
  .live-ui .live-grid { grid-template-columns: minmax(0, 1fr); }
}
/* The site header slides out of view so the panels get the whole screen,
   and slides back while the pointer is at the top edge or over it (or
   while anything in it has keyboard focus). The ::after strip is that top
   edge: it hangs below the hidden bar, so hovering it hovers the header,
   and its green rule is the hint that something is up there. The hide
   waits a moment so a pointer briefly overshooting doesn't snap it shut.
   Touch screens have no hover to bring it back, so they keep it in place.
   z-index 40 sits under the modals' 50, as the Settings modal opens from it. */
@media (hover: hover) {
  .live-ui .report-topbar {
    position: fixed;
    inset: 0 0 auto;
    z-index: 40;
    transform: translateY(-100%);
    transition: transform .2s ease .4s;
  }
  .live-ui .report-topbar::after {
    content: "";
    position: absolute;
    inset: 100% 0 auto;
    height: .75rem;
    border-top: 3px solid var(--al-brand-green);
  }
  .live-ui .report-topbar:hover,
  .live-ui .report-topbar:focus-within {
    transform: none;
    transition-delay: 0s;
    box-shadow: 0 .5rem 1.5rem rgba(0, 0, 0, .35);
  }
  .live-ui .report-topbar:hover::after,
  .live-ui .report-topbar:focus-within::after { border-top-color: transparent; }
}
/* Desktop: the whole dashboard fits the window, no scrolling. The body is
   the viewport's height, and the space left under the title and any status
   or replay bar is split evenly between the two rows of panels, each chart
   taking what its panel's heading leaves. Chart.js follows (responsive,
   maintainAspectRatio: false). Too short a window would squash the charts
   past reading, so below 32rem tall the page scrolls as before. */
@media (min-width: 64.0625rem) and (min-height: 32rem) {
  .live-ui { height: 100vh; height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }
  .live-ui .content { flex: 1 1 auto; min-height: 0; padding-block: 1.25rem; }
  .live-ui .live-dashboard { flex: 1 1 auto; min-height: 0; gap: 1rem; }
  .live-ui .live-header { padding-bottom: 1rem; }
  .live-ui .live-replay { margin-top: -.5rem; }
  .live-ui .live-grid { flex: 1 1 auto; min-height: 0; grid-template-rows: repeat(2, minmax(0, 1fr)); gap: 1.5rem 3rem; }
  .live-ui .live-panel { min-height: 0; }
  .live-ui .live-chart,
  .live-ui .live-empty { flex: 1 1 0; height: auto; min-height: 0; }
  .live-ui .live-chart { overflow: hidden; }
}
@media (max-width: 40rem) {
  .live-ui .content { padding-inline: 1rem; }
  .live-ui .live-clock { text-align: left; }
}

/* Its Settings modal: wider than the report's, since it carries checklists. */
.live-ui .live-settings { width: 34rem; }
/* Scope and Products: five checklist dropdowns (static/live.js
   createDropdown), each one select-styled row whose list floats over the
   modal rather than pushing it taller. One per row; the selects from
   Units onward stay two to a row (.live-settings__grid). The .live-dropdown
   rules are scoped to .reporting-ui rather than .live-ui because the report
   and comparison pages' filter panes use the same control
   (static/filter-cascade.js). */
.live-ui .live-settings__scope { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.live-ui .live-field { display: grid; gap: .375rem; min-width: 0; font-size: .9375rem; }
.live-ui .live-field .al-label { font-weight: 400; font-size: .9375rem; }
.reporting-ui .live-dropdown { display: grid; gap: .375rem; min-width: 0; }
.reporting-ui .live-dropdown__button {
  position: relative;
  display: flex;
  align-items: center;
  text-align: left;
  padding-right: 2.25rem;
}
.reporting-ui .live-dropdown__value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The caret a native <select> draws, so these read as the same control. */
.reporting-ui .live-dropdown__button::after {
  content: "";
  position: absolute;
  right: .875rem;
  top: 50%;
  width: .5rem;
  height: .5rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-70%) rotate(45deg);
  transition: transform var(--al-motion);
}
.reporting-ui .live-dropdown--open .live-dropdown__button::after { transform: translateY(-30%) rotate(-135deg); }
.reporting-ui .live-dropdown__panel {
  /* Fixed, not absolute: the modal scrolls (overflow-y: auto), which would
     clip an absolutely positioned list at its edge. live.js places it
     against its button's on-screen rect -- below, or above when there's
     more room there -- and sets left/top/width/max-height. */
  position: fixed;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: .375rem;
  padding: .5rem .75rem;
  border: 1px solid var(--al-control-border);
  background: var(--al-surface-raised);
  box-shadow: var(--al-shadow, 0 8px 24px rgba(0, 0, 0, .35));
  overflow: hidden;
}
.reporting-ui .live-dropdown__actions { gap: .25rem; }
.reporting-ui .live-dropdown__actions .al-btn { min-height: 2rem; padding: .25rem .5rem; font-size: .875rem; }
.reporting-ui .live-dropdown__list { display: grid; min-height: 0; flex: 1 1 auto; overflow-y: auto; scrollbar-width: thin; }
.reporting-ui .live-dropdown__list .al-check { min-height: 2.25rem; font-size: .9375rem; font-weight: 400; }
.reporting-ui .live-dropdown__empty { margin: .5rem 0; }
.live-ui .live-settings__note { margin: 0; }
.live-ui .live-settings__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.live-ui .live-settings .al-field { gap: .375rem; font-size: .9375rem; }
.live-ui .live-settings__actions { justify-content: flex-end; gap: .5rem; padding-top: .25rem; }
.live-ui .live-settings__actions #live-settings-save { margin-right: auto; }
/* theme.css paints the toggle's label white for the always-dark top bar;
   inside a (possibly light) modal it has to follow the text colour. */
.live-ui .live-settings .al-theme-toggle { color: var(--al-text); justify-content: flex-start; align-self: flex-start; }
@media (max-width: 40rem) {
  .live-ui .live-settings__grid,
  .live-ui .live-settings__scope { grid-template-columns: minmax(0, 1fr); }
}
