/* =============================================================================
   Heliotec design-system components + style-guide page chrome.
   All values reference tokens from tokens.css — no hard-coded hex here.
   ============================================================================= */

/* --- Page chrome ---------------------------------------------------------- */
.ds-board {
  /* Ambient background reproduced from the source HTML prototype: a dark base
     (--ds-bg = #0a0d1a) with two soft, fixed radial "glow" blobs below. */
  background: var(--ds-bg);
  color: var(--ds-text);
  font-family: var(--ds-font);
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  margin: 0;
  position: relative;
  overflow-x: hidden;
}
.ds-board::before {
  content: "";
  position: fixed;
  top: -20%;
  left: -10%;
  width: 60%;
  height: 80%;
  background: radial-gradient(ellipse at center, rgba(90, 60, 180, 0.18), transparent 60%);
  pointer-events: none;
  z-index: 0;
}
.ds-board::after {
  content: "";
  position: fixed;
  top: 10%;
  right: -10%;
  width: 50%;
  height: 70%;
  background: radial-gradient(ellipse at center, rgba(40, 80, 200, 0.15), transparent 60%);
  pointer-events: none;
  z-index: 0;
}
.ds-page { max-width: 1280px; margin: 0 auto; padding: 56px 48px 96px; position: relative; z-index: 1; }
.ds-header { margin-bottom: 12px; }
.ds-header h1 { font-size: 32px; font-weight: 700; letter-spacing: -0.8px; margin: 0 0 8px; }
.ds-header p { color: var(--ds-text-dim); font-size: 16px; margin: 0; max-width: 760px; line-height: 1.5; }
.ds-section { margin-top: 56px; }
.ds-section > h2 { font-size: var(--ds-fs-section); font-weight: var(--ds-fw-section); letter-spacing: -0.5px; margin: 0 0 6px; }
.ds-section > .ds-sub { color: var(--ds-text-dim); font-size: 14px; margin: 0 0 18px; }
.ds-eyebrow { font-size: var(--ds-fs-eyebrow); font-weight: 600; letter-spacing: 1.4px; color: var(--ds-text-muted); text-transform: uppercase; margin: 22px 0 12px; }
.ds-card { background: var(--ds-surface); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border: 1px solid var(--ds-border); border-radius: var(--ds-radius-lg); }

/* --- Colour swatches ------------------------------------------------------ */
.ds-swatch-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.ds-swatch__chip { height: 76px; border-radius: 12px; border: 1px solid var(--ds-border-strong); }
.ds-swatch__name { font-size: 13px; font-weight: 600; margin-top: 8px; }
.ds-swatch__meta { font-size: 12px; color: var(--ds-text-muted); }

/* --- Brand palette ramps -------------------------------------------------- */
.ds-ramp-grid { display: flex; gap: 12px; flex-wrap: wrap; }
.ds-ramp { width: 156px; border-radius: 12px; overflow: hidden; border: 1px solid var(--ds-border-strong); }
.ds-ramp__head { padding: 16px 14px 20px; }
.ds-ramp__role { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; opacity: 0.75; }
.ds-ramp__name { font-size: 13px; font-weight: 700; letter-spacing: 0.4px; margin-top: 2px; }
.ds-ramp__hex { font-size: 11px; opacity: 0.85; margin-top: 2px; }
.ds-ramp__var { font-size: 10.5px; opacity: 0.75; margin-top: 3px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.ds-ramp__cell { padding: 8px 14px; display: flex; flex-direction: column; gap: 1px; }
.ds-ramp__cellhex { font-size: 10.5px; }
.ds-ramp__cellvar { font-size: 9.5px; opacity: 0.7; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

/* --- Typography specimens ------------------------------------------------- */
.ds-type-list { display: flex; flex-direction: column; }
.ds-type-row { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 0; border-bottom: 1px solid var(--ds-border); }
.ds-type-row:last-child { border-bottom: none; }
.ds-type-row .ds-type-spec { font-size: 12px; color: var(--ds-text-muted); white-space: nowrap; }

/* --- Spacing & radius ----------------------------------------------------- */
.ds-scale-cols { display: flex; gap: 80px; flex-wrap: wrap; }
.ds-radius-row { display: flex; gap: 16px; align-items: flex-end; }
.ds-radius-tile { width: 76px; height: 76px; background: rgba(234, 246, 114, 0.12); border: 1px solid rgba(234, 246, 114, 0.5); }
.ds-radius-label { font-size: 12px; color: var(--ds-text-muted); margin-top: 8px; }
.ds-space-row { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; }
.ds-space-bar { height: 18px; background: var(--ds-accent); border-radius: 3px; }
.ds-space-label { font-size: 12px; color: var(--ds-text-dim); }

/* --- Iconography ---------------------------------------------------------- */
.ds-icon-row { display: flex; gap: 16px; flex-wrap: wrap; }
.ds-icon-tile { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 120px; }
.ds-icon-tile .ds-icon-box { width: 56px; height: 56px; border-radius: 12px; background: var(--ds-surface); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border: 1px solid var(--ds-border); display: flex; align-items: center; justify-content: center; color: var(--ds-text-dim); font-size: 22px; }
.ds-icon-tile span { font-size: 12px; color: var(--ds-text-muted); }

/* --- Component demo layout ------------------------------------------------ */
.ds-demo { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
.ds-demo--stack { flex-direction: column; }
.ds-usage { font-size: 12px; color: var(--ds-text-muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; margin: 6px 0 0; }

/* =============================================================================
   Components
   ============================================================================= */

/* CountChip */
.ds-count-chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: var(--ds-radius-pill); background: rgba(255, 255, 255, 0.06); font-size: var(--ds-fs-eyebrow); font-weight: 600; color: var(--ds-text-muted); }
.ds-count-chip--active { background: rgba(234, 246, 114, 0.16); color: var(--ds-accent); }

/* Status dot helpers */
.ds-dot--alert { background: var(--ds-alert); }
.ds-dot--warning { background: var(--ds-warning); }
.ds-dot--positive { background: var(--ds-positive); }

/* RagPill */
.ds-rag-pill { display: inline-flex; align-items: center; gap: 8px; padding: 5px 11px 5px 8px; border-radius: var(--ds-radius-pill); font-size: var(--ds-fs-eyebrow); font-weight: 600; letter-spacing: 1.2px; text-transform: uppercase; }
.ds-rag-pill__dot { width: 7px; height: 7px; border-radius: 50%; }
.ds-rag-pill--alert { background: rgba(255, 106, 97, 0.12); color: var(--ds-alert-soft); }
.ds-rag-pill--alert .ds-rag-pill__dot { background: var(--ds-alert); }
.ds-rag-pill--warning { background: rgba(243, 168, 38, 0.12); color: var(--ds-warning-soft); }
.ds-rag-pill--warning .ds-rag-pill__dot { background: var(--ds-warning); }
.ds-rag-pill--positive { background: rgba(108, 212, 116, 0.10); color: var(--ds-positive-soft); }
.ds-rag-pill--positive .ds-rag-pill__dot { background: var(--ds-positive); }
.ds-rag-pill--grey { background: rgba(255, 255, 255, 0.05); color: var(--ds-text-muted); }
.ds-rag-pill--grey .ds-rag-pill__dot { background: var(--ds-tech); }

/* FilterPill */
.ds-filter-pill { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px 6px 10px; border-radius: var(--ds-radius-pill); background: rgba(255, 255, 255, 0.03); border: 1px solid var(--ds-border-strong); color: var(--ds-text-dim); font-size: 12.5px; font-weight: 500; }
.ds-filter-pill--active { background: rgba(234, 246, 114, 0.08); border-color: rgba(234, 246, 114, 0.3); color: var(--ds-accent); }
.ds-filter-pill__dot { width: 7px; height: 7px; border-radius: 50%; }

/* Control — one definition of what a filter control beside a chart looks like.

   Four copies of these declarations used to exist: .ds-dropdown__trigger,
   .ds-multiselect__trigger, and `select.chart-control` written out again in
   both meter_chart.html and reports.html. They were written to agree and did
   not, because a native <select> left on `appearance: auto` lets the OS paint
   its own chrome over every declaration below — macOS draws an inner well and
   a double-chevron stepper, and the control came out 35px tall beside the
   multi-select's 39.5px in the same filter row.

   `line-height` is stated here rather than inherited: it is what makes a
   <select>, a <button> trigger and a <div> resolve to the same height, which
   is the thing that drifted. Members add only what is genuinely their own
   (the dropdown's wider left padding for its kicker, the multi-select's
   max-width). */
.ds-control,
.ds-dropdown,
.ds-dropdown__trigger,
.ds-multiselect__trigger {
    appearance: none;
    -webkit-appearance: none;
    display: inline-flex;
    align-items: center;
    gap: 10px;
    padding: 9px 14px;
    line-height: 1.5;
    border-radius: var(--ds-radius-md);
    background: rgba(255, 255, 255, 0.03);
    border: 1px solid var(--ds-border-strong);
    font-family: var(--ds-font);
    font-size: var(--ds-fs-secondary);
    color: var(--ds-text);
}
/* The three that respond to a pointer. .ds-dropdown is the static display
   variant and deliberately stays out of this. */
.ds-control,
.ds-dropdown__trigger,
.ds-multiselect__trigger { cursor: pointer; transition: background 0.15s, border-color 0.15s; }
.ds-control:hover,
.ds-dropdown__trigger:hover,
.ds-multiselect__trigger:hover { background: rgba(255, 255, 255, 0.07); border-color: var(--ds-border-strong); }
.ds-control:focus-visible,
.ds-dropdown__trigger:focus-visible,
.ds-multiselect__trigger:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }

/* `appearance: none` removes the OS stepper, so the component draws the caret.
   The wrapper exists only to position it: a <select> cannot have a ::after of
   its own. Same glyph, size and token as .ds-multiselect__caret, so a native
   select and a multi-select read as one kind of control. */
.ds-control-wrap { position: relative; display: inline-flex; }
.ds-control-wrap::after {
    content: "▼";
    position: absolute;
    right: 14px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 9px;
    color: var(--ds-text-dim);
    pointer-events: none;
}
/* Room for that caret. A <select> still sizes itself to its widest option, so
   the gap before the caret varies between controls — that is the native
   behaviour and not worth fighting. */
/* The triggers weight their own `__value` span; a <select> has no child to
   weight, so it carries the same 500 itself. */
.ds-control { padding-right: 32px; font-weight: 500; }
/* A <select>'s own dropdown list is drawn by the OS and inherits nothing from
   the control, so its colours have to be stated on the options themselves. */
.ds-control option { background: var(--ds-surface-card-solid); color: var(--ds-text); }

/* DropdownControl */
.ds-dropdown { padding-left: 16px; }
.ds-dropdown__kicker { font-size: var(--ds-fs-eyebrow); letter-spacing: 1px; text-transform: uppercase; color: var(--ds-text-muted); }
.ds-dropdown__value { font-weight: 500; color: var(--ds-text); }
.ds-dropdown__caret { font-size: 9px; color: var(--ds-text-dim); transition: transform 0.15s; }

/* DropdownControl — interactive variant (menu wired up by dropdown_control.js) */
.ds-dropdown--interactive { position: relative; display: inline-block; padding: 0; border: 0; background: transparent; }
/* Appearance comes from the shared Control block above; the wider left padding
   is this variant's own, to sit its uppercase kicker off the edge. */
.ds-dropdown__trigger { padding-left: 16px; }
/* When open, lift the whole control into its own stacking context above sibling
   cards/charts so the menu is unambiguously on top for paint AND hit-testing. */
.ds-dropdown--interactive.is-open { z-index: 1000; }
.ds-dropdown--interactive.is-open .ds-dropdown__caret { transform: rotate(180deg); }
.ds-dropdown__menu { position: absolute; top: calc(100% + 6px); right: 0; min-width: 100%; max-height: 320px; overflow-y: auto; margin: 0; padding: 6px; list-style: none; z-index: 40; background: var(--ds-surface-card-solid); border: 1px solid var(--ds-border-strong); border-radius: var(--ds-radius-md); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); display: none; }
.ds-dropdown--interactive.is-open .ds-dropdown__menu { display: block; }
/* Group heading inside the menu (e.g. "Rolling Window" / "Calendar Month").
   Rendered for `{'header': ...}` items — never selectable, so it deliberately
   carries neither .ds-dropdown__item nor a cursor. */
.ds-dropdown__group { padding: 10px 12px 4px; font-size: var(--ds-fs-eyebrow); letter-spacing: 1px; text-transform: uppercase; color: var(--ds-text-muted); white-space: nowrap; }
.ds-dropdown__group:first-child { padding-top: 4px; }
.ds-dropdown__item { padding: 8px 12px; border-radius: var(--ds-radius-sm); font-size: var(--ds-fs-secondary); color: var(--ds-text-dim); white-space: nowrap; cursor: pointer; }
.ds-dropdown__item:hover, .ds-dropdown__item.is-active { background: rgba(255, 255, 255, 0.07); color: var(--ds-text); }
.ds-dropdown__item.is-selected { color: var(--ds-accent); background: rgba(234, 246, 114, 0.08); }

/* MultiSelect — searchable multi-select (e.g. Carbon page site switcher).
   Behaviour wired up by multi_select.js; emits ds:multiselect-change. */
.ds-multiselect { position: relative; display: inline-block; }
.ds-multiselect.is-open { z-index: 1000; }
/* Appearance comes from the shared Control block above; the cap on width is
   this variant's own, for the long site names it has to show. */
.ds-multiselect__trigger { max-width: 360px; }
.ds-multiselect__kicker { font-size: var(--ds-fs-eyebrow); letter-spacing: 1.1px; text-transform: uppercase; color: var(--ds-text-muted); }
.ds-multiselect__value { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-multiselect__caret { margin-left: 2px; font-size: 9px; color: var(--ds-text-dim); transition: transform 0.18s; }
.ds-multiselect.is-open .ds-multiselect__caret { transform: rotate(180deg); }
.ds-multiselect__menu { position: absolute; top: calc(100% + 6px); right: 0; min-width: 320px; z-index: 40; padding: 8px; background: var(--ds-surface-card-solid); border: 1px solid var(--ds-border-strong); border-radius: var(--ds-radius-lg); box-shadow: 0 16px 40px rgba(0, 0, 0, 0.55); display: none; }
.ds-multiselect.is-open .ds-multiselect__menu { display: block; }
.ds-multiselect__search { padding: 2px 2px 8px; }
.ds-multiselect__search-input { width: 100%; padding: 9px 11px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--ds-border-strong); border-radius: var(--ds-radius-sm); color: var(--ds-text); font-family: var(--ds-font); font-size: var(--ds-fs-secondary); outline: none; }
.ds-multiselect__search-input:focus { border-color: var(--ds-accent); }
.ds-multiselect__search-input::placeholder { color: var(--ds-text-muted); }
.ds-multiselect__count { padding: 2px 6px 8px; font-size: var(--ds-fs-eyebrow); letter-spacing: 1.2px; text-transform: uppercase; color: var(--ds-text-muted); font-weight: 600; }
.ds-multiselect__list { max-height: 340px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.ds-multiselect__item { display: flex; align-items: center; gap: 11px; width: 100%; padding: 7px 10px; border: none; border-radius: var(--ds-radius-sm); background: transparent; font-family: var(--ds-font); font-size: var(--ds-fs-secondary); color: var(--ds-text); text-align: left; cursor: pointer; }
.ds-multiselect__item:hover { background: rgba(255, 255, 255, 0.05); }
.ds-multiselect__item.is-selected { background: rgba(234, 246, 114, 0.06); }
.ds-multiselect__dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.ds-multiselect__dot--alert { background: var(--ds-alert); box-shadow: 0 0 6px var(--ds-alert); }
.ds-multiselect__dot--warning { background: var(--ds-warning); box-shadow: 0 0 6px var(--ds-warning); }
.ds-multiselect__dot--positive { background: var(--ds-positive); box-shadow: 0 0 6px var(--ds-positive); }
.ds-multiselect__dot--grey { background: var(--ds-text-muted); }
.ds-multiselect__dot--all { background: var(--ds-accent); box-shadow: 0 0 6px rgba(234, 246, 114, 0.4); }
.ds-multiselect__text { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.ds-multiselect__name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-multiselect__item.is-selected .ds-multiselect__name { color: var(--ds-accent); }
.ds-multiselect__sub { font-size: 11px; color: var(--ds-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-multiselect__check { width: 14px; height: 14px; flex: none; color: var(--ds-accent); opacity: 0; }
.ds-multiselect__item.is-selected .ds-multiselect__check { opacity: 1; }
.ds-multiselect__item--disabled { opacity: 0.4; cursor: not-allowed; }
.ds-multiselect__item--disabled:hover { background: transparent; }
.ds-multiselect__empty { padding: 16px 12px; text-align: center; font-size: var(--ds-fs-secondary); color: var(--ds-text-muted); }

/* AiBadge */
.ds-ai-badge { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 7px; background: rgba(234, 246, 114, 0.13); border: 1px solid rgba(234, 246, 114, 0.25); color: var(--ds-accent); font-size: 13px; }

/* MetricBlock */
.ds-metric { display: flex; flex-direction: column; gap: 10px; min-width: 280px; }
.ds-metric__top { display: flex; align-items: flex-start; gap: 12px; }
.ds-metric__value { font-size: var(--ds-fs-metric); font-weight: var(--ds-fw-metric); letter-spacing: -1.3px; line-height: 1.05; }
.ds-metric__unit { font-size: 0.4em; font-weight: 500; letter-spacing: 0; color: var(--ds-text-muted); margin-left: 6px; }
.ds-metric__chip { align-self: center; padding: 5px 11px; border-radius: var(--ds-radius-pill); background: rgba(234, 246, 114, 0.08); border: 1px solid rgba(234, 246, 114, 0.18); color: var(--ds-accent); font-size: var(--ds-fs-secondary); font-weight: 500; }
.ds-metric__label { font-size: var(--ds-fs-body); color: var(--ds-text-dim); }
.ds-metric__label strong { color: var(--ds-text); font-weight: 500; }
.ds-metric__delta { display: inline-flex; align-items: center; gap: 7px; font-size: 14px; font-weight: 500; }
.ds-metric__glyph { font-size: 10px; }
.ds-delta--down { color: var(--ds-positive); }
.ds-delta--up { color: var(--ds-alert); }
/* For figures where MORE is good (clients onboarded): a rise is good news. */
.ds-delta--gain { color: var(--ds-positive); }
.ds-delta--loss { color: var(--ds-alert); }
/* Unchanged vs previous period (no colour / no direction) */
.ds-delta--neutral { color: var(--ds-text-muted); }
/* No comparable previous period — muted, no arrow */
.ds-delta--none { color: var(--ds-text-muted); font-style: italic; }

/* Loading state — a small spinner replaces the number while a metric tile refetches
   (e.g. changing the header Period filter on the Site / Carbon pages). The value text
   goes transparent and a ring spins where it was; the chip/delta hide so stale figures
   don't linger. The spinner fades in over ~0.25s so a fast response shows only a faint
   hint rather than a hard flash. */
.ds-metric.is-loading .ds-metric__value {
  color: transparent;
  position: relative;
}
.ds-metric.is-loading .ds-metric__value::after {
  content: '';
  position: absolute;
  left: 2px;
  top: 50%;
  width: 22px;
  height: 22px;
  margin-top: -11px;
  border: 2px solid var(--ds-border-strong);
  border-top-color: var(--ds-text-dim);
  border-radius: 50%;
  animation: ds-metric-spin 0.7s linear infinite, ds-metric-spin-in 0.25s ease-out;
}
.ds-metric.is-loading .ds-metric__chip,
.ds-metric.is-loading .ds-metric__delta {
  visibility: hidden;
}
@keyframes ds-metric-spin {
  to { transform: rotate(360deg); }
}
@keyframes ds-metric-spin-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* StatusBar (proportional RAG segments — e.g. Sites by status).
   Display-only by default; interactive (clickable/filtering) styling is
   dormant under .ds-status-bar--interactive until the page-wide filtering
   pattern is decided. */
.ds-status-bar { display: flex; height: 56px; border-radius: var(--ds-radius-md); overflow: hidden; gap: 2px; }
.ds-status-bar__seg { display: flex; align-items: center; justify-content: center; gap: 10px; min-width: 0; padding: 0 12px; border: 0; font-family: var(--ds-font); color: var(--ds-bg); transition: filter 0.15s, opacity 0.15s; }
.ds-status-bar--interactive .ds-status-bar__seg { cursor: pointer; }
.ds-status-bar--interactive .ds-status-bar__seg:hover { filter: brightness(1.08); }
.ds-status-bar--interactive .ds-status-bar__seg:focus-visible { outline: 2px solid var(--ds-text); outline-offset: -3px; }
.ds-status-bar__num { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; line-height: 1; }
.ds-status-bar__name { font-size: var(--ds-fs-meta); font-weight: 600; letter-spacing: 1.6px; text-transform: uppercase; opacity: 0.85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-status-bar__seg--alert { background: var(--ds-alert); }
.ds-status-bar__seg--warning { background: var(--ds-warning); }
.ds-status-bar__seg--positive { background: var(--ds-positive); }
.ds-status-bar__seg--grey { background: var(--ds-tech); color: var(--ds-text); }
/* Selected/filter state (dormant) — page toggles .has-filter on the bar +
   .is-active on a seg once interactive filtering is enabled. */
.ds-status-bar--interactive.has-filter .ds-status-bar__seg:not(.is-active) { opacity: 0.4; }
.ds-status-bar--interactive .ds-status-bar__seg.is-active { outline: 2px solid var(--ds-text); outline-offset: -3px; }
/* Empty state — no live status data */
.ds-status-bar__placeholder { display: flex; align-items: center; height: 56px; padding: 0 16px; border-radius: var(--ds-radius-md); border: 1px dashed var(--ds-border-strong); color: var(--ds-text-muted); font-size: var(--ds-fs-secondary); }

/* SearchControl (standalone free-text filter, for a filter bar rather than a
   menu). Sized and coloured to sit level with ds-dropdown/ds-multiselect
   triggers beside it. */
.ds-search {
    position: relative;
    display: flex; align-items: center; gap: var(--ds-space-2);
    flex: 1 1 240px; min-width: 0; max-width: 320px;
    height: 44px; padding: 0 var(--ds-space-3);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-md);
    transition: border-color 0.12s;
}
.ds-search:focus-within { border-color: var(--ds-accent); }
.ds-search__icon { flex: none; font-size: 13px; color: var(--ds-text-muted); }
.ds-search__input {
    flex: 1 1 auto; min-width: 0;
    background: transparent; border: 0; outline: none; padding: 0;
    color: var(--ds-text); font-family: var(--ds-font); font-size: var(--ds-fs-secondary);
}
.ds-search__input::placeholder { color: var(--ds-text-muted); }
/* The browser's own clear affordance would sit beside ours. */
.ds-search__input::-webkit-search-cancel-button { display: none; }
/* Always in the DOM, hidden until there is something to clear, so the first
   keystroke does not shift the control's contents sideways. */
.ds-search__clear {
    display: none; flex: none;
    padding: 4px 2px; border: 0; background: none; cursor: pointer;
    color: var(--ds-text-muted); font-size: 12px; line-height: 1;
}
.ds-search.has-value .ds-search__clear { display: block; }
.ds-search__clear:hover { color: var(--ds-text); }
.ds-search__clear:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; border-radius: var(--ds-radius-sm); }

/* RagDonut (portfolio status — the Org Overview's third headline widget).
   Replaces the full-width StatusBar above. The ring is server-rendered SVG, so
   everything here is presentation only; the arc maths lives in the tag. */
.ds-donut { display: flex; flex-direction: column; gap: var(--ds-space-3); }
.ds-donut__head { display: flex; align-items: baseline; gap: var(--ds-space-2); flex-wrap: wrap; }
.ds-donut__title { font-size: var(--ds-fs-card); font-weight: var(--ds-fw-card); color: var(--ds-text); }
.ds-donut__subtitle { font-size: var(--ds-fs-meta); color: var(--ds-text-muted); }
/* The ring and its centred total share a cell, so the number stays centred
   whatever the ring's size — no absolute offsets to re-tune. */
.ds-donut__body { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: var(--ds-space-4); }
.ds-donut__ring { grid-column: 1; grid-row: 1; display: block; }
.ds-donut__centre {
    grid-column: 1; grid-row: 1;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    pointer-events: none;
}
.ds-donut__total { font-size: 30px; font-weight: 700; letter-spacing: -1px; line-height: 1.05; color: var(--ds-text); }
.ds-donut__unit { font-size: var(--ds-fs-eyebrow); color: var(--ds-text-muted); }
.ds-donut__track { stroke: rgba(255, 255, 255, 0.07); }
.ds-donut__arc--alert { stroke: var(--ds-alert); }
.ds-donut__arc--warning { stroke: var(--ds-warning); }
.ds-donut__arc--positive { stroke: var(--ds-positive); }
.ds-donut__arc--grey { stroke: var(--ds-tech); }

.ds-donut__legend { grid-column: 2; grid-row: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ds-donut__row {
    display: flex; align-items: center; gap: var(--ds-space-2);
    min-height: 36px; padding: 0 var(--ds-space-2);
    border-radius: var(--ds-radius-sm);
    color: var(--ds-text-dim); font-size: var(--ds-fs-secondary);
    text-decoration: none; transition: background 0.12s, color 0.12s;
}
a.ds-donut__row:hover { background: rgba(255, 255, 255, 0.05); color: var(--ds-text); }
a.ds-donut__row:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: -2px; }
.ds-donut__dot { width: 9px; height: 9px; border-radius: var(--ds-radius-pill); flex: none; }
.ds-donut__dot--alert { background: var(--ds-alert); }
.ds-donut__dot--warning { background: var(--ds-warning); }
.ds-donut__dot--positive { background: var(--ds-positive); }
.ds-donut__dot--grey { background: var(--ds-tech); }
.ds-donut__label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-donut__count { font-weight: 600; color: var(--ds-text); font-variant-numeric: tabular-nums; }
.ds-donut__pct { width: 40px; flex: none; text-align: right; color: var(--ds-text-muted); font-variant-numeric: tabular-nums; }
.ds-donut__placeholder {
    display: flex; align-items: center; min-height: 146px; padding: 0 var(--ds-space-4);
    border-radius: var(--ds-radius-md); border: 1px dashed var(--ds-border-strong);
    color: var(--ds-text-muted); font-size: var(--ds-fs-secondary);
}
/* Below the headline grid's own breakpoint the card is full width, so the ring
   and its breakdown stack rather than squeezing the labels. */
@media (max-width: 420px) {
    .ds-donut__body { grid-template-columns: 1fr; justify-items: center; }
    .ds-donut__legend { grid-column: 1; grid-row: 2; width: 100%; }
}

/* BreakdownBar (avoidable-spend Out-of-Hours / Startup / Shutdown split —
   stacked bar + one row per category). Shared by the client Site Overview
   header tile and the Org Overview headline. Segment colour is fixed per
   category (red/yellow/blue), not RAG-driven like StatusBar above, so each
   category reads the same wherever the bar appears. Missing-state
   (.ds-breakdown__prompt) swaps in for Startup/Shutdown when a site has no
   business hours configured — never rendered as £0 rows. Tooltip text is the
   native `title` attribute on both the segment and its row — no custom
   JS/CSS popover, so the component owns no interactive behaviour. */
.ds-breakdown { display: flex; flex-direction: column; gap: var(--ds-space-2); margin-top: var(--ds-space-3); }
.ds-breakdown__bar { display: flex; height: 10px; border-radius: var(--ds-radius-sm); overflow: hidden; gap: 2px; background: rgba(255, 255, 255, 0.05); }
.ds-breakdown__seg { min-width: 4px; }
.ds-breakdown__rows { display: flex; flex-direction: column; gap: 6px; }
.ds-breakdown__row { display: flex; align-items: center; gap: 8px; font-size: var(--ds-fs-meta); color: var(--ds-text-dim); cursor: default; }
.ds-breakdown__swatch { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.ds-breakdown__row-label { flex: 1; min-width: 0; color: var(--ds-text); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-breakdown__row-cost { font-weight: 600; color: var(--ds-text); }
.ds-breakdown__row-pct { color: var(--ds-text-dim); min-width: 32px; text-align: right; }
.ds-breakdown__row-alerts { color: var(--ds-text-muted); white-space: nowrap; }
/* Stacked, not side-by-side: the prompt sentence is long and the card it sits
   in is narrow, so a row layout wrapped the text to five lines with the CTA
   floating beside the middle of them. On its own line the CTA reads as the
   next thing to do rather than an aside. */
.ds-breakdown__prompt {
  display: flex; flex-direction: column; align-items: flex-start; gap: 6px;
  padding: 8px 10px; border: 1px dashed var(--ds-border-strong); border-radius: var(--ds-radius-sm);
  font-size: var(--ds-fs-meta); color: var(--ds-text-dim);
}
.ds-breakdown__prompt-text { min-width: 0; }
.ds-breakdown__prompt-cta { color: var(--ds-accent); font-weight: 600; white-space: nowrap; text-decoration: none; }
/* Button variant (Site Overview PR 3 — CTA opens the business-hours modal in
   place instead of linking out) resets native <button> chrome so it looks
   identical to the <a> variant above. */
button.ds-breakdown__prompt-cta { background: none; border: none; padding: 0; font: inherit; cursor: pointer; }
.ds-breakdown__prompt-cta:hover { text-decoration: underline; }
/* Hover/focus tooltip (design_system/js/breakdown_tooltip.js). Replaces the
   native `title`: styled to match the consistency chart's tooltip on the same
   page, instant rather than after an OS delay, and reachable by keyboard.
   The tip is `position: fixed` so it can sit ABOVE the bar: the bar is the
   first thing in .ds-breakdown, and .site-stat-card sets overflow:hidden to
   clip its accent gradient to the corner radius — an absolutely positioned
   tip would be cut off the instant it rose above the bar. */
.ds-breakdown__seg, .ds-breakdown__row { cursor: default; }
.ds-breakdown__seg:focus-visible, .ds-breakdown__row:focus-visible {
  outline: 2px solid var(--ds-accent); outline-offset: 2px;
}
.ds-breakdown__tip {
  position: fixed; z-index: 60; display: none; min-width: 140px; max-width: 260px;
  background: var(--ds-bg); border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-md); padding: 9px 11px; pointer-events: none;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
}
.ds-breakdown__tip.is-visible { display: block; }
.ds-breakdown__tip-head { display: block; font-size: var(--ds-fs-meta); font-weight: 700; color: var(--ds-text); }
.ds-breakdown__tip-line { display: block; font-size: var(--ds-fs-meta); color: var(--ds-text-dim); margin-top: 3px; }

/* Hide stale segments/rows during a header refetch (mirrors .ds-metric.is-loading
   above) — the AJAX period-refresh flow replaces the whole fragment anyway, but
   this avoids a flash of the pre-fetch breakdown while the request is in flight. */
.ds-metric.is-loading .ds-breakdown { visibility: hidden; }
/* Org Overview-only footer line ("Startup & shutdown tracked for X of Y
   sites" + an optional "add business hours for N more" action) rendered
   below the breakdown_bar by client/includes/_org_avoidable_card.html — not
   part of the breakdown_bar tag itself (the site-level widget has no
   equivalent footer, since coverage there is a per-site binary shown via
   the prompt instead). Reuses .ds-breakdown__prompt-cta for the action link
   so both CTA styles match. */
.ds-breakdown__footer {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin-top: 2px; padding-top: 8px; border-top: 1px solid var(--ds-border);
  font-size: var(--ds-fs-meta); color: var(--ds-text-muted);
}
.ds-breakdown__footer .ds-breakdown__prompt-cta { flex-shrink: 0; }

/* InsightCard (translucent; lightbulb + impact header row, no accent bar) */
.ds-insight-card { position: relative; border: 1px solid var(--ds-border); border-radius: var(--ds-radius-lg); overflow: hidden; width: 100%; }
.ds-insight-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--ds-accent), transparent); opacity: 0.3; z-index: 1; }
.ds-insight-card__body { background: var(--ds-surface); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.ds-insight-card__top { display: flex; align-items: center; gap: 8px; min-height: 22px; }
.ds-insight-card__icon { color: var(--ds-accent); flex: none; line-height: 0; }
.ds-insight-card__kicker { font-size: 12px; font-weight: 600; letter-spacing: 0.2px; color: var(--ds-accent); }
.ds-insight-card__impact { margin-left: auto; font-size: 18px; font-weight: 700; letter-spacing: -0.4px; color: var(--ds-accent); }
.ds-insight-card__unit { font-size: 12px; color: var(--ds-text-muted); margin-left: 3px; }
.ds-insight-card__headline { font-size: 14px; font-weight: 600; line-height: 1.35; }
.ds-insight-card__text { font-size: 12.5px; color: var(--ds-text-dim); line-height: 1.45; }
.ds-insight-card__action { font-size: 13px; font-weight: 500; color: var(--ds-positive); }

/* EventCard (translucent, left status accent) */
.ds-event-card { border-radius: var(--ds-radius-md); overflow: hidden; width: 340px; }
.ds-event-card__body { background: var(--ds-surface); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border: 1px solid var(--ds-border); border-left: 3px solid var(--ds-alert); padding: 14px 16px 12px; display: flex; flex-direction: column; gap: 10px; }
.ds-event-card--alert .ds-event-card__body { border-left-color: var(--ds-alert); }
.ds-event-card--warning .ds-event-card__body { border-left-color: var(--ds-warning); }
.ds-event-card--positive .ds-event-card__body { border-left-color: var(--ds-positive); }
.ds-event-card__top { display: flex; align-items: center; justify-content: space-between; }
.ds-event-card__badge { font-size: 10px; font-weight: 600; letter-spacing: 1.3px; text-transform: uppercase; }
.ds-event-card--alert .ds-event-card__badge { color: var(--ds-alert-soft); }
.ds-event-card--warning .ds-event-card__badge { color: var(--ds-warning-soft); }
.ds-event-card__time { font-size: 11px; color: var(--ds-text-muted); }
.ds-event-card__headline { font-size: 13.5px; font-weight: 500; line-height: 1.35; }
.ds-event-card__bottom { display: flex; align-items: flex-end; justify-content: space-between; gap: 10px; }
.ds-event-card__stats { display: flex; gap: 12px; }
.ds-stat { display: flex; flex-direction: column; font-size: 11px; color: var(--ds-text-dim); }

/* NoticeBanner (inline informational strip; icon + title + body, left-accent tint by tone) */
.ds-notice-banner { display: flex; align-items: flex-start; gap: var(--ds-space-3); padding: var(--ds-space-3) var(--ds-space-4); border-radius: var(--ds-radius-md); border: 1px solid var(--ds-border); }
.ds-notice-banner__icon { flex: none; line-height: 0; margin-top: 2px; font-size: 15px; }
.ds-notice-banner__body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ds-notice-banner__title { font-size: var(--ds-fs-secondary); font-weight: 600; color: var(--ds-text); }
.ds-notice-banner__text { font-size: var(--ds-fs-secondary); color: var(--ds-text-dim); line-height: 1.45; }
.ds-notice-banner--warning { background: rgba(243, 168, 38, 0.10); border-color: rgba(243, 168, 38, 0.3); }
.ds-notice-banner--warning .ds-notice-banner__icon { color: var(--ds-warning); }
.ds-notice-banner--alert { background: rgba(255, 106, 97, 0.10); border-color: rgba(255, 106, 97, 0.3); }
.ds-notice-banner--alert .ds-notice-banner__icon { color: var(--ds-alert); }
.ds-notice-banner--positive { background: rgba(108, 212, 116, 0.10); border-color: rgba(108, 212, 116, 0.3); }
.ds-notice-banner--positive .ds-notice-banner__icon { color: var(--ds-positive); }
.ds-stat strong { font-size: 12.5px; font-weight: 600; color: var(--ds-text); }
.ds-stat--cost strong { color: var(--ds-accent); }
.ds-label-pill { display: inline-flex; align-items: center; gap: 5px; padding: 4px 9px; border-radius: var(--ds-radius-pill); border: 1px solid var(--ds-border-strong); font-size: 10.5px; font-weight: 500; color: var(--ds-text-muted); white-space: nowrap; }
.ds-label-pill__dot { width: 6px; height: 6px; border-radius: 50%; }
.ds-label-pill--empty { border-style: dashed; }

/* Bars (shared by equipment & device rows) */
.ds-bar-track { height: 10px; background: rgba(255, 255, 255, 0.05); border-radius: 5px; overflow: hidden; }
.ds-bar-track--sm { height: 6px; border-radius: 3px; }
.ds-bar-fill { height: 100%; background: var(--ds-voltage); border-radius: inherit; }

/* SiteTableRow */
.ds-site-row { display: grid; grid-template-columns: 96px minmax(180px, 1fr) repeat(5, 130px); gap: 24px; align-items: center; padding: 18px 28px; background: var(--ds-surface); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border: 1px solid var(--ds-border); border-radius: var(--ds-radius-md); }
.ds-site-row__name { font-size: 14px; font-weight: 500; }
.ds-site-row__addr { font-size: 12px; color: var(--ds-text-muted); margin-top: 3px; }
.ds-site-row__num { text-align: right; font-size: 13.5px; font-weight: 500; }

/* EquipmentGroupRow */
.ds-equip-row { display: grid; grid-template-columns: 220px 1fr 160px 22px; gap: 24px; align-items: center; padding: 18px 24px; background: var(--ds-surface); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border: 1px solid var(--ds-border); border-radius: var(--ds-radius-md); }
.ds-equip-row__name { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; letter-spacing: -0.2px; }
.ds-equip-row__bar { display: flex; flex-direction: column; gap: 8px; }
.ds-equip-row__pct { font-size: 11px; font-weight: 500; color: var(--ds-text-dim); }
.ds-equip-row__vals { text-align: right; }
.ds-equip-row__cost { font-size: 19px; font-weight: 700; letter-spacing: -0.4px; }
.ds-equip-row__kwh { font-size: 12px; color: var(--ds-text-muted); }
.ds-equip-row__chev { color: var(--ds-text-muted); }

/* DeviceRow */
.ds-device-row { display: grid; grid-template-columns: 188px 1fr 160px 22px; gap: 24px; align-items: center; padding: 11px 12px; border-radius: var(--ds-radius-sm); background: rgba(0, 0, 0, 0.14); }
.ds-device-row__name { display: flex; align-items: center; gap: 9px; font-size: 13.5px; font-weight: 500; }
.ds-device-row__dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(74, 82, 242, 0.65); }
.ds-device-row__vals { text-align: right; }
.ds-device-row__cost { font-size: 13.5px; font-weight: 600; }
.ds-device-row__kwh { font-size: 11px; color: var(--ds-text-muted); margin-left: 7px; }
.ds-device-row__chev { color: var(--ds-text-muted); text-align: center; }

/* =============================================================================
   Interaction states — CSS only. These define how interactive components look
   on hover / focus / active / selected / disabled. Actual behaviour (opening
   menus, filtering, selecting) lives in the product, not in this reference.
   Paired `.is-hover` / `.is-selected` / `.is-disabled` classes let the style
   guide show each state statically. `:focus-visible` styles apply once the app
   makes the element focusable.
   ============================================================================= */
.ds-filter-pill,
.ds-dropdown,
.ds-insight-card,
.ds-event-card,
.ds-site-row,
.ds-equip-row,
.ds-device-row {
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, transform 0.05s;
}

.ds-filter-pill:hover,
.ds-filter-pill.is-hover { background: rgba(255, 255, 255, 0.07); border-color: var(--ds-border-strong); color: var(--ds-text); }
.ds-filter-pill.is-selected { background: rgba(234, 246, 114, 0.08); border-color: rgba(234, 246, 114, 0.3); color: var(--ds-accent); }

.ds-dropdown:hover,
.ds-dropdown.is-hover { background: rgba(255, 255, 255, 0.07); }

.ds-insight-card:hover .ds-insight-card__body,
.ds-insight-card.is-hover .ds-insight-card__body,
.ds-event-card:hover .ds-event-card__body,
.ds-event-card.is-hover .ds-event-card__body { background: var(--ds-surface-hover); }
.ds-insight-card__action:hover { text-decoration: underline; }

.ds-site-row:hover,
.ds-site-row.is-hover,
.ds-equip-row:hover,
.ds-equip-row.is-hover,
.ds-device-row:hover,
.ds-device-row.is-hover { background: var(--ds-surface-hover); }

.ds-site-row.is-selected,
.ds-equip-row.is-selected,
.ds-device-row.is-selected { border-color: rgba(234, 246, 114, 0.4); box-shadow: inset 0 0 0 1px rgba(234, 246, 114, 0.25); }

.ds-filter-pill:active,
.ds-dropdown:active,
.ds-site-row:active,
.ds-equip-row:active,
.ds-device-row:active { transform: translateY(1px); }

.ds-filter-pill:focus-visible,
.ds-dropdown:focus-visible,
.ds-insight-card:focus-visible,
.ds-event-card:focus-visible,
.ds-site-row:focus-visible,
.ds-equip-row:focus-visible,
.ds-device-row:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 2px; }

.is-disabled,
[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; }

/* Portal — Site Performance table + EPC band.
   Shared across portal pages (Overview, Carbon, Site Overview); lifted here from
   the page <style> blocks so the "Site Performance" table is a single reusable
   component. Token-driven; not ds-prefixed (legacy portal class names kept so
   existing markup/JS is unchanged). */
.perf-card { position: relative; overflow: hidden; background: var(--ds-surface-card); border: 1px solid var(--ds-border); border-radius: var(--ds-radius-xl); }
.perf-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, var(--ds-accent), transparent); opacity: 0.3; z-index: 1; }
.perf-card__header { display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-4); padding: 20px 28px; border-bottom: 1px solid var(--ds-border); flex-wrap: wrap; }
.perf-card__title { font-size: var(--ds-fs-panel); font-weight: var(--ds-fw-panel); letter-spacing: -0.3px; color: var(--ds-text); }
.perf-card__sub { font-size: var(--ds-fs-secondary); color: var(--ds-text-muted); margin-top: 4px; }
/* Four controls now (search, status, period) rather than two. They stay on one
   line and move to a line of their own, as a block, when the heading beside
   them leaves too little room — breaking the group apart puts the divider at
   the end of a line, separating nothing. The search box is the one that gives,
   because the two selects carry a chosen value that must not truncate. */
.perf-card__controls { display: flex; align-items: center; gap: var(--ds-space-2); flex-wrap: nowrap; justify-content: flex-end; }
.perf-card__controls .ds-search { flex: 1 1 220px; min-width: 150px; max-width: 300px; }
/* The selects never shrink: squeezing them wraps the value somebody has chosen
   onto a second line and makes the control taller than everything beside it.
   All the give comes from the search box, and when even that runs out the
   whole group wraps under the heading. */
.perf-card__controls > .ds-dropdown,
.perf-card__controls > .ds-multiselect { flex: none; }
.perf-card__controls .ds-dropdown__value,
.perf-card__controls .ds-multiselect__value { white-space: nowrap; }
.perf-filters { display: flex; align-items: center; gap: var(--ds-space-2); flex-wrap: wrap; }
.perf-card__divider { width: 1px; height: 22px; background: var(--ds-border-strong); margin: 0 4px; }
.perf-card__divider { flex: none; }
/* Results scroll within the table rather than running the page down forever.
   The cap is expressed in whole rows, not a round pixel number, so the last
   visible row is never cut in half — change --perf-visible-rows, not the
   height. --perf-row-h is the real rendered row: two lines in the Site cell
   and a change pill under each metric. */
.perf-card__table-wrap {
    overflow-x: auto;
    overflow-y: auto;
    /* Measured, not guessed: a row is 74.3px (two lines in the Site cell, a
       change pill under each metric) and the sticky header is 41.2px. Rounding
       the row UP by a fraction leaves a few pixels of the next row showing,
       which is the honest signal that there is more below — better than a cap
       that lands flush and looks like the end of the list. */
    --perf-row-h: 75px;
    --perf-head-h: 41px;
    --perf-visible-rows: 9;
    max-height: calc(var(--perf-head-h) + var(--perf-row-h) * var(--perf-visible-rows));
}
/* The header has to stay put while the body scrolls under it, or a long
   portfolio becomes a column of numbers with no column names. It must also be
   opaque: .perf-th's 1.5% tint alone let every row scrolled beneath show
   through, printing "SITE" over a site name. The tint stays, layered over the
   solid card navy, so the header looks the same at rest. */
.perf-table thead th { position: sticky; top: 0; z-index: 2; background: linear-gradient(rgba(255, 255, 255, 0.015), rgba(255, 255, 255, 0.015)), var(--ds-surface-card-solid); }

.perf-table { width: 100%; border-collapse: collapse; min-width: 920px; }
.perf-th { text-align: left; padding: 12px 12px; font-size: var(--ds-fs-eyebrow); font-weight: 500; text-transform: uppercase; letter-spacing: 0.6px; color: var(--ds-text-muted); background: rgba(255, 255, 255, 0.015); border-bottom: 1px solid var(--ds-border); white-space: nowrap; cursor: pointer; user-select: none; }
.perf-th:first-child { padding-left: 20px; }
.perf-th:last-child { padding-right: 20px; }
.perf-th--num { text-align: right; }
.perf-th:hover { color: var(--ds-text-dim); }
.perf-th.sort-active { color: var(--ds-text); }
.perf-th.sort-active .sort-indicator { color: var(--ds-accent); }

.perf-table tbody tr { border-bottom: 1px solid var(--ds-border); transition: background 0.12s; cursor: pointer; }
.perf-table tbody tr:last-child { border-bottom: none; }
.perf-table tbody tr:hover { background: rgba(255, 255, 255, 0.025); }
.perf-table td { padding: 16px 12px; font-size: 14px; vertical-align: middle; color: var(--ds-text); }
.perf-table td:first-child { padding-left: 20px; }
.perf-table td:last-child { padding-right: 20px; }
.perf-table td.num { text-align: right; }

.perf-site__name { color: var(--ds-text); font-weight: 500; font-size: 13.5px; }
.perf-site__addr { color: var(--ds-text-muted); font-size: 12px; margin-top: 3px; }
.perf-site__reason { font-style: italic; }
.perf-cell__primary--empty { color: var(--ds-text-muted); font-weight: 400; }
/* Shown in place of rows when a status filter matches no sites for the month.
   JS sets an inline height equal to three normal rows; min-height is a fallback. */
.perf-empty-cell { text-align: center; vertical-align: middle; min-height: 198px; color: var(--ds-text-dim); font-size: 13px; }
/* Text, not a button: it is an escape hatch beside a sentence, and a box would
   make it compete with the four controls it undoes. Same reasoning as
   .ds-filterbar__clear. */
.perf-empty-clear { margin-left: 6px; padding: 0; border: 0; background: none; cursor: pointer; color: var(--ds-accent); font-family: var(--ds-font); font-size: 13px; font-weight: 600; text-decoration: underline; }
.perf-empty-clear:hover { color: var(--ds-text); }

.perf-cell { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 3px; font-variant-numeric: tabular-nums; }
.perf-cell__primary { color: var(--ds-text); font-weight: 500; font-size: 13.5px; }
.perf-cell__sub { color: var(--ds-text-muted); font-size: 11px; }
.perf-change { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; color: var(--ds-text-muted); font-variant-numeric: tabular-nums; }
.perf-change--up { color: var(--ds-alert); }
.perf-change--down { color: var(--ds-positive); }
.perf-change--flat { color: var(--ds-text-muted); }

.perf-card__footer { display: flex; justify-content: space-between; align-items: center; padding: 16px 28px; border-top: 1px solid var(--ds-border); font-size: 12px; color: var(--ds-text-muted); }
/* Pager — only rendered above PERF_PAGE_SIZE results, so most portfolios never
   see it. Sits in the footer beside the count it is paging through. */
.perf-pager { display: flex; align-items: center; gap: var(--ds-space-3); }
.perf-pager__label { font-size: 12px; color: var(--ds-text-dim); font-variant-numeric: tabular-nums; }
.perf-pager__btn { padding: 6px 12px; border: 1px solid var(--ds-border-strong); border-radius: var(--ds-radius-sm); background: transparent; color: var(--ds-text-dim); font-family: var(--ds-font); font-size: 12px; font-weight: 600; cursor: pointer; }
.perf-pager__btn:hover:not(:disabled) { border-color: var(--ds-accent); color: var(--ds-text); }
.perf-pager__btn:disabled { opacity: 0.4; cursor: default; }

/* EPC asset rating band pill — used via templates/client/_epc_band_pill.html and
   rendered inline by the Overview / Carbon site tables. Colours follow GOV.UK. */
.epc-band { display: inline-block; border-radius: 4px; font-weight: 700; line-height: 1; text-align: center; letter-spacing: 0.5px; }
.epc-band-sm { padding: 2px 8px; font-size: 11px; min-width: 24px; }
.epc-band-lg { padding: 8px 16px; font-size: 22px; min-width: 44px; margin-top: 2px; }

/* TopBar — full-bleed, sharp-cornered portal header (logo · tenant · nav). Mirrors the
   prototype's nav.topbar. Used by the client portal base (templates/client/base_client.html)
   and showcased via the portal_topbar inclusion tag on /design-guide/. */
.ds-topbar { position: relative; z-index: 10; display: flex; align-items: center; padding: 20px 48px; background: var(--ds-topbar-bg); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px); border-bottom: 1px solid var(--ds-border); border-radius: 0; }
.ds-topbar__brand { display: flex; align-items: center; gap: 20px; }
.ds-topbar__logo { height: 32px; width: auto; object-fit: contain; display: block; }
.ds-topbar__wordmark { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; color: var(--ds-energy); }
.ds-topbar__divider { width: 1px; height: 22px; background: var(--ds-border-strong); }
.ds-topbar__tenant { font-size: 15px; font-weight: 500; color: var(--ds-text); letter-spacing: -0.1px; }
.ds-topbar__nav { margin-left: auto; display: flex; gap: 32px; align-items: center; }
.ds-topbar__link { font-size: 14px; color: var(--ds-text-dim); text-decoration: none; padding: 4px 0; position: relative; white-space: nowrap; transition: color 0.2s; cursor: pointer; }
.ds-topbar__link:hover { color: var(--ds-text); }
.ds-topbar__link--active { color: var(--ds-text); font-weight: 500; }
.ds-topbar__link--active::after { content: ''; position: absolute; bottom: -6px; left: 0; right: 0; height: 2px; background: var(--ds-energy); border-radius: 1px; box-shadow: 0 0 8px rgba(234, 246, 114, 0.35); }
@media (max-width: 900px) {
  .ds-topbar { padding: 18px 20px; flex-wrap: wrap; }
  .ds-topbar__nav { gap: 16px; margin-top: 12px; margin-left: 0; width: 100%; overflow-x: auto; }
}

/* ── Filter select ────────────────────────────────────────────────────────
   A URL-driven multi-select for a list page's filter bar. Unlike
   .ds-multiselect (client-side, all-selected by default), this renders its
   selected state from the server and submits through the page's own GET form.

   Surfaces follow the host form controls, NOT the client portal's purple
   cards: the sales forms paint <select>/<input> on --ds-bg with a --mid-gray
   border and a yellow focus ring, so the trigger matches them exactly (42px,
   14px type, 4px radius) and the menu sits one step up on the neutral
   --ds-surface-card-solid. --ds-bg-card (#160F3A) is the purple client-portal
   surface and is deliberately not used here.

   Two states carry the whole design, because there is no chip row under the
   bar to summarise what is on:
     - inactive: the filter's name and a caret, deliberately quiet, so an
       untouched bar never looks like a filtered one.
     - active: the values spelled out in --ds-energy with a count, because the
       trigger is then the only thing on the page that says what is on.
   Inactive controls share the free space (flex) while active ones size to
   their content — a long selection must not be truncated. */
.ds-filterbar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px;
    background: var(--ds-surface-card);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md);
}
.ds-filterbar__spacer { flex: 1 1 auto; }
/* Text and an ✕, not a button — it is an escape hatch, and giving it a box
   would make it compete with the four controls it undoes. Full control height
   so it sits centred against them, but no border or background of its own.
   Always rendered, inert until there is something to clear, so the bar never
   changes width and the control is discoverable before the first pick. */
.ds-filterbar__clear {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    flex: 0 0 auto;
    height: 42px;
    font-size: 14px;
    color: var(--text-white);
    text-decoration: none;
    white-space: nowrap;
    transition: color 0.15s ease;
}
.ds-filterbar__clear-x { font-size: 12px; line-height: 1; }
.ds-filterbar__clear:hover { color: var(--ds-alert); }
.ds-filterbar__clear.is-disabled {
    color: var(--ds-text-muted);
    cursor: default;
    pointer-events: none;
}

.ds-filterselect { position: relative; flex: 1 1 0; min-width: 0; max-width: 220px; }
.ds-filterselect.is-active { flex: 0 0 auto; max-width: none; }

.ds-filterselect__trigger {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    height: 42px;
    padding: 10px 14px;
    background: var(--ds-bg);
    border: 1px solid var(--mid-gray);
    border-radius: 4px;
    color: var(--text-white);
    font-family: inherit;
    font-size: 14px;
    line-height: 1.4;
    cursor: pointer;
    text-align: left;
    transition: border-color 0.15s ease, background 0.15s ease;
}
.ds-filterselect__trigger:hover { border-color: var(--ds-text-dim); }
.ds-filterselect__trigger:focus-visible {
    outline: none;
    border-color: var(--electric-yellow);
    box-shadow: 0 0 0 2px rgba(234, 246, 114, 0.1);
}
.ds-filterselect.is-active .ds-filterselect__trigger {
    background: rgba(234, 246, 114, 0.08);
    border-color: var(--ds-energy);
    color: var(--ds-energy);
    font-weight: 700;
}
.ds-filterselect__value {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ds-filterselect.is-active .ds-filterselect__value { overflow: visible; }
.ds-filterselect__count {
    flex: 0 0 auto;
    padding: 1px 7px;
    background: var(--ds-energy);
    color: var(--ds-bg);
    border-radius: 9px;
    font-family: 'Roboto Mono', ui-monospace, monospace;
    font-size: 10px;
    font-weight: 700;
}
.ds-filterselect__caret { flex: 0 0 auto; font-size: 10px; color: var(--ds-text-muted); }
.ds-filterselect.is-active .ds-filterselect__caret { color: var(--ds-energy); }

.ds-filterselect__menu {
    display: none;
    position: absolute;
    z-index: 40;
    top: calc(100% + 4px);
    left: 0;
    min-width: 270px;
    max-width: 380px;
    background: var(--ds-surface-card-solid);
    border: 1px solid var(--mid-gray);
    border-radius: 4px;
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
}
.ds-filterselect.is-open .ds-filterselect__menu { display: block; }
/* A control near the right-hand edge opens leftwards so the menu stays on
   screen; the bar runs the full width of the page. */
.ds-filterselect.opens-left .ds-filterselect__menu { left: auto; right: 0; }

.ds-filterselect__search { padding: 10px 12px 6px; }
.ds-filterselect__search-input {
    width: 100%;
    padding: 7px 10px;
    background: var(--ds-bg);
    border: 1px solid var(--mid-gray);
    border-radius: 4px;
    color: var(--text-white);
    font-family: inherit;
    font-size: 13.5px;
}
.ds-filterselect__search-input:focus {
    outline: none;
    border-color: var(--electric-yellow);
    box-shadow: 0 0 0 2px rgba(234, 246, 114, 0.1);
}
.ds-filterselect__list { max-height: 320px; overflow-y: auto; padding: 6px 0; }

.ds-filterselect__item {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 14px;
    cursor: pointer;
    font-size: 13.5px;
    color: var(--text-white);
}
.ds-filterselect__item:hover { background: rgba(255, 255, 255, 0.05); }
.ds-filterselect__item.is-selected { background: rgba(255, 255, 255, 0.04); font-weight: 600; }
/* The native checkbox, tinted the way every other checkbox in the sales portal
   is (base_sales.html: accent-color: var(--electric-yellow)). A hand-built box
   was a flat yellow square with no tick in it — it read as a colour swatch
   rather than a checked box, and it broke label-less clicks. */
.ds-filterselect__item input[type="checkbox"] {
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    margin: 0;
    accent-color: var(--electric-yellow);
    cursor: pointer;
}
.ds-filterselect__text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-filterselect__empty { padding: 10px 14px; font-size: 12.5px; color: var(--ds-text-muted); margin: 0; }

.ds-filterselect__foot {
    display: flex;
    justify-content: flex-end;
    padding: 10px 12px;
    border-top: 1px solid var(--ds-border);
}
/* The portal's own primary button, scaled for a menu: same cut-corner radius,
   uppercase weight and hover lift as .btn-primary in base_sales.html. */
.ds-filterselect__apply {
    padding: 9px 22px;
    background: var(--electric-yellow);
    border: none;
    border-radius: 8px 0 8px 0;
    color: var(--black);
    font-family: inherit;
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    cursor: pointer;
    transition: all 0.3s ease;
}
.ds-filterselect__apply:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(234, 246, 114, 0.3);
}

@media (max-width: 900px) {
    .ds-filterbar { flex-wrap: wrap; }
    .ds-filterselect { max-width: none; flex: 1 1 200px; }
}

/* =============================================================================
   Tab rail (.ds-rail) — the per-page section switcher under the top navigation.

   Width is a custom property so the rail and anything sized against it animate
   from one value. The collapsed rules are scoped to the wide breakpoint on
   purpose: below it the rail becomes a horizontal strip, where "collapsed" has
   no meaning and the un-hiding would otherwise need !important to undo.
   ============================================================================= */
/* 260px — set by the longest label the rail carries, which is the Organisation
   page's "Site Performance", alongside a count pill wide enough for a
   four-figure portfolio. At the old 225px that label ellipsised, and a tab
   whose name you cannot read is not a tab. The Site page's shorter labels
   simply gain whitespace.
   The collapsed width below is deliberately NOT scaled with it: that one is
   derived from the square-highlight constraint (12 + 44 + 12 + 1), so widening
   it would just make the highlight a landscape rectangle instead. */
:root {
    --ds-rail-w: 260px;
    /* One clock for the box and everything inside it. The jag this fixes was
       a label that left on frame 0 while its box took 200ms to follow, so the
       two must not be able to drift apart. */
    --ds-rail-anim: 0.2s;
    --ds-rail-ease: cubic-bezier(0.4, 0, 0.2, 1);
}
/* 69px, not 56. An entry fills the rail's content box, and every entry has a
   44px min-height, so a square highlight needs a 44px content box:
   12px padding + 44 + 12px padding + the 1px right border = 69. The odd number
   is the border — border-box sizing counts it inside the width. */
html.ds-rail-collapsed { --ds-rail-w: 69px; }

.ds-rail {
    width: var(--ds-rail-w);
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
    align-self: stretch;
    /* One inset for everything in the rail, so the caption, the entries and the
       collapse control line up by construction rather than by three numbers
       that agree today. */
    --ds-rail-inset: 14px;
    /* The horizontal padding is what sets the active entry's yellow marker off
       the rail's own edge; the inset above then carries the icon and label in
       from there. Together they keep the label at 26px. */
    padding: var(--ds-space-3);
    background: var(--ds-topbar-bg);
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
    /* Chrome, not a card: square corners, flush to the header above and the
       viewport edge to its left, with a single right edge against the content. */
    border-right: 1px solid var(--ds-border);
    transition: width var(--ds-rail-anim) var(--ds-rail-ease);
}
/* The blur is what made the collapse jagged, not the width. An animating width
   makes the browser re-sample and re-blur the backdrop over a changing region
   on every frame: measured in Chrome, a 50ms frame (three dropped) at the head
   of every toggle, gone the moment `backdrop-filter: none` was set. So it is
   suspended for the ~200ms the width is moving and restored on transitionend.
   The background is 85% opaque over a flat page, so the missing blur is not
   visible; the dropped frames were. */
html.ds-rail-animating .ds-rail {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
}
/* Nothing here conveys meaning by moving — it is the same rail at either
   width — so there is no reason to animate it for somebody who asked not to. */
@media (prefers-reduced-motion: reduce) {
    .ds-rail, .ds-rail__item, .ds-rail__label, .ds-rail__caption,
    .ds-rail__caption-text, .ds-rail__count { transition: none; }
}
/* Caption row owns the collapse control. At the foot it read as another
   navigation entry; top-right is where a sidebar's own chrome belongs. */
.ds-rail__caption {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--ds-space-2);
    min-height: 28px; padding: 0 4px 0 var(--ds-rail-inset); margin-bottom: 6px;
    font-size: var(--ds-fs-eyebrow); font-weight: 600; letter-spacing: 1.2px;
    color: var(--ds-text-muted);
    transition: padding var(--ds-rail-anim) var(--ds-rail-ease),
                gap var(--ds-rail-anim) var(--ds-rail-ease);
}
/* Same trick as the entries: the caption grows to push the toggle to the right
   edge, and shrinking it to nothing leaves `space-between` with one item, so
   the toggle lands centred. The zero basis is what lets it reach nothing --
   at its intrinsic 29px, "SITE" plus a 28px button does not fit the 44px the
   collapsed rail has, and the button was pushed through the rail's border. */
.ds-rail__caption-text {
    flex: 1 1 0; min-width: 0; overflow: hidden; white-space: nowrap;
    transition: flex-grow var(--ds-rail-anim) var(--ds-rail-ease),
                opacity calc(var(--ds-rail-anim) * 0.6) linear;
}
.ds-rail__caption-dots { display: none; }

.ds-rail__item {
    position: relative;
    display: flex; align-items: center; gap: 10px;
    /* Centred at BOTH widths. `justify-content` is not animatable, so a rule
       that switched it on collapse landed as a jump; here it is a no-op while
       the label fills the row and takes over as the label shrinks to nothing.
       Expanded and collapsed put the icon at the same x (26px), so it should
       barely move at all. */
    justify-content: center;
    min-height: 44px; padding: 0 12px 0 var(--ds-rail-inset);
    border-radius: var(--ds-radius-md);
    color: var(--ds-text-dim);
    font-size: var(--ds-fs-body);
    text-decoration: none;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s,
                padding var(--ds-rail-anim) var(--ds-rail-ease),
                gap var(--ds-rail-anim) var(--ds-rail-ease);
}
.ds-rail__item:hover { background: rgba(255, 255, 255, 0.04); color: var(--ds-text); }
.ds-rail__item:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: -2px; }
.ds-rail__item--active {
    background: rgba(234, 246, 114, 0.08);
    color: var(--ds-accent);
    font-weight: 600;
    box-shadow: inset 3px 0 0 var(--ds-accent);
}
.ds-rail__item--active:hover { background: rgba(234, 246, 114, 0.12); color: var(--ds-accent); }
/* Locked entries stay clickable — they open the upsell rather than doing
   nothing, so they must not look or behave disabled, only quieter.
   Not the one you are standing on, though: this rule is declared after
   --active and would otherwise win on an entry that is both, leaving a grey
   label beside a yellow marker while its unlocked neighbour lit up properly.
   The padlock already says it is gated; the accent says where you are, which
   is true on either tier. Worth revisiting once the rail has more entries. */
.ds-rail__item--locked:not(.ds-rail__item--active) { color: var(--ds-text-muted); }
.ds-rail__icon { flex: none; width: 16px; text-align: center; font-size: 14px; }
/* `flex-grow` is what shrinks the label, not width or max-width: with a grow
   factor below 1 the item takes only that fraction of the free space and the
   rest falls to `justify-content`, so 1 -> 0 slides the label shut and centres
   the icon in one continuous motion. The fade is shorter than the shrink --
   text compressed to nothing looks worse than text that has already gone. */
.ds-rail__label {
    flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis;
    transition: flex-grow var(--ds-rail-anim) var(--ds-rail-ease),
                opacity calc(var(--ds-rail-anim) * 0.6) linear;
}
/* The count animates on the same clock as the label beside it. It used to be
   `display: none` when collapsed, which is not animatable: the pill vanished on
   the frame the class landed while the label next to it took 160ms to fade, and
   on the way back it reappeared at full size inside a rail that had only just
   started to grow. max-width carries the width change (the pill is a couple of
   characters, so 48px is past any real count) and the padding goes with it so
   nothing is left holding space. */
.ds-rail__count {
    flex: none; font-size: var(--ds-fs-meta); font-weight: 600;
    background: rgba(255, 255, 255, 0.07); color: var(--ds-text-dim);
    border-radius: var(--ds-radius-pill); padding: 2px 8px;
    max-width: 48px; overflow: hidden; white-space: nowrap;
    transition: opacity calc(var(--ds-rail-anim) * 0.6) linear,
                max-width var(--ds-rail-anim) var(--ds-rail-ease),
                padding var(--ds-rail-anim) var(--ds-rail-ease);
}
.ds-rail__item--active .ds-rail__count { background: var(--ds-accent); color: var(--ds-concrete); }
.ds-rail__lock { flex: none; font-size: 11px; }
/* Kept for assistive tech — a padlock glyph alone says nothing out loud. */
.ds-rail__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.ds-rail__tip { display: none; }

.ds-rail__foot { margin-top: auto; padding-top: var(--ds-space-3); }
.ds-rail__toggle {
    position: relative;
    flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; padding: 0;
    border: 0; border-radius: var(--ds-radius-sm);
    background: transparent; color: var(--ds-text-muted);
    font-family: inherit; cursor: pointer;
}
.ds-rail__toggle:hover { background: rgba(255, 255, 255, 0.06); color: var(--ds-text); }
.ds-rail__toggle:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: -2px; }
.ds-rail__toggle-icon { font-size: 13px; }
/* The label stays in the document for assistive tech; the shortcut shows on
   hover, so the control is one glyph without becoming a mystery. */
.ds-rail__toggle-label {
    position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.ds-rail__toggle .ds-rail__tip { left: auto; right: calc(100% + 8px); }
.ds-rail__toggle:hover .ds-rail__tip,
.ds-rail__toggle:focus-visible .ds-rail__tip {
    display: block;
    position: absolute; top: 50%; transform: translateY(-50%);
    z-index: 30;
    background: var(--ds-surface-card-solid);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-sm);
    padding: 5px 9px;
    font-size: var(--ds-fs-meta); font-weight: 400; letter-spacing: 0;
    color: var(--ds-text); white-space: nowrap;
    box-shadow: 0 10px 28px -12px #000;
}
.ds-rail__kbd {
    font-family: inherit; font-size: var(--ds-fs-micro);
    border: 1px solid var(--ds-border-strong); border-radius: 4px;
    padding: 1px 5px; margin-left: 6px;
}
.ds-rail__note {
    padding: var(--ds-space-3) 12px 0;
    margin-top: var(--ds-space-3);
    border-top: 1px solid var(--ds-border);
    font-size: var(--ds-fs-eyebrow); line-height: 1.6; color: var(--ds-text-muted);
}

@media (min-width: 1101px) {
    /* Collapsed: the label shrinks to nothing and fades, but stays in flow and
       in the document, so screen readers and find-in-page still reach it. It
       used to be pulled out of flow and clipped, which did reach them — but it
       happened on the frame the class landed, a whole animation ahead of the
       box it belongs to, and that was the jag. A `title` attribute would be no
       better: it cannot be styled and appears too slowly. */
    html.ds-rail-collapsed .ds-rail__label,
    html.ds-rail-collapsed .ds-rail__caption-text {
        flex-grow: 0;
        opacity: 0;
    }
    html.ds-rail-collapsed .ds-rail__note {
        position: absolute; width: 1px; height: 1px;
        overflow: hidden; clip-path: inset(50%); white-space: nowrap;
    }
    html.ds-rail-collapsed .ds-rail__item { padding: 0; gap: 0; }
    html.ds-rail-collapsed .ds-rail__caption { padding: 0; gap: 0; }
    html.ds-rail-collapsed .ds-rail__toggle-icon { transform: scaleX(-1); }
    html.ds-rail-collapsed .ds-rail__count {
        opacity: 0; max-width: 0; padding-left: 0; padding-right: 0;
    }
    html.ds-rail-collapsed .ds-rail__lock {
        position: absolute; right: 7px; bottom: 6px; font-size: 9px;
    }
    /* The toggle's tip is pinned to its left for the expanded rail, where the
       button sits at the far right. At 69px that points off the edge of the
       screen, so collapsed it opens rightwards like every entry's. */
    html.ds-rail-collapsed .ds-rail__toggle .ds-rail__tip {
        right: auto; left: calc(100% + 10px);
    }
    html.ds-rail-collapsed .ds-rail__item:hover .ds-rail__tip,
    html.ds-rail-collapsed .ds-rail__item:focus-visible .ds-rail__tip {
        display: block;
        position: absolute; left: calc(100% + 10px); top: 50%; transform: translateY(-50%);
        z-index: 30;
        background: var(--ds-surface-card-solid);
        border: 1px solid var(--ds-border-strong);
        border-radius: var(--ds-radius-sm);
        padding: 6px 10px;
        font-size: var(--ds-fs-meta); color: var(--ds-text);
        white-space: nowrap;
        box-shadow: 0 10px 28px -12px #000;
    }
}

@media (max-width: 1100px) {
    /* The rail becomes a tab strip above the content and the page gets its
       width back. Charts need it more than the rail does. */
    .ds-rail {
        width: auto;
        flex-direction: row;
        align-self: auto;
        gap: 4px;
        padding: 0 0 2px;
        border-right: 0;
        border-bottom: 2px solid var(--ds-border);
        overflow-x: auto;
    }
    .ds-rail__caption, .ds-rail__foot { display: none; }
    .ds-rail__item { min-height: 40px; }
    .ds-rail__item--active { box-shadow: none; }
    .ds-rail__label { flex: none; }
}

/* ---------------------------------------------------------------------------
   Floating chart tooltip
   ---------------------------------------------------------------------------
   The hover panel for a chart. Out of the flow, so showing it cannot reflow
   the page, and fixed rather than absolute so no scroll container can clip it.
   pointer-events:none matters: the panel follows the cursor, so anything else
   would put it under the pointer and swallow the hover it is describing.
   Positioned by dsFloatTip.place() (design_system/js/float_tip.js) -- which
   writes `transform`, so nothing else may.
   --------------------------------------------------------------------------- */
.ds-float-tip {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 60;
    pointer-events: none;
    width: fit-content;
    max-width: 340px;
    background: var(--ds-surface-card-solid);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-md);
    padding: var(--ds-space-3) var(--ds-space-4);
    box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
}

/* Anchored variant: the panel does not move with the cursor, so its width may
   not move either. A panel over a dense chart changes its numbers on nearly
   every pixel of travel; `width: fit-content` turns that into the panel
   resizing on every pixel. Measured on Site Consumption by Area: 181px of
   width swing and 293px of height swing in one sweep, on a chart rendering at
   8.3ms/frame. It was never frame rate -- it was the panel. */
.ds-float-tip--steady {
    width: 300px;
    max-width: 300px;
    transition: transform .12s ease-out;
}

/* Pinned variant (dsFloatTip.pin): positioned inside its chart instead of
   the viewport, so it scrolls with the chart rather than staying put on
   screen while the chart moves away. The chart must be the positioning
   context (position: relative). Declared after .ds-float-tip so it wins. */
.ds-float-tip--pinned {
    position: absolute;
}

/* One row of a tooltip. The value column is held at a width and set in
   tabular figures, so "0.01" and "12.39" occupy the same space and nothing
   reflows as the cursor moves. */
.ds-tip-row {
    display: flex;
    align-items: center;
    gap: var(--ds-space-2);
    font-size: 12.5px;
    color: var(--ds-text-dim);
}
.ds-tip-row__sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.ds-tip-row__nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-tip-row__v {
    width: 86px;
    flex: none;
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--ds-text);
}
/* A series with nothing at this index keeps its row — an area that has
   dropped to nothing is information, and dropping the row would change the
   panel's height under the cursor. */
.ds-tip-row--none .ds-tip-row__nm,
.ds-tip-row--none .ds-tip-row__v { color: var(--ds-text-muted); }
.ds-tip-row--none .ds-tip-row__sw { opacity: .35; }

/* --------------------------------------------------------------------------
   Pick select (.ds-pick) — one choice from a searchable list, as a pill.
   The single-choice sibling of .ds-filterselect: every option is a link, the
   × beside the pill is a link that clears it. Behaviour: pick_select.js.
   -------------------------------------------------------------------------- */
.ds-pick { position: relative; display: inline-flex; align-items: center; gap: 4px; }
.ds-pick__trigger {
    display: inline-flex; align-items: center; gap: 8px;
    max-width: 280px; height: 28px; padding: 0 11px;
    background: var(--ds-surface-card-solid);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-pill);
    color: var(--ds-text-dim);
    font-family: inherit; font-size: 12.5px; font-weight: 500;
    cursor: pointer;
}
.ds-pick__trigger:hover { border-color: var(--ds-text-dim); }
.ds-pick__trigger:focus-visible { outline: 2px solid var(--ds-accent); outline-offset: 1px; }
.ds-pick.is-active .ds-pick__trigger {
    background: rgba(234, 246, 114, 0.08); border-color: var(--ds-energy); color: var(--ds-energy);
}
.ds-pick__value { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-pick__caret { font-size: 9px; color: var(--ds-text-muted); }
.ds-pick__clear {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; border-radius: var(--ds-radius-pill);
    color: var(--ds-text-dim); font-size: 16px; line-height: 1;
}
.ds-pick__clear:hover { background: rgba(255, 255, 255, 0.06); color: var(--ds-text); }
.ds-pick__menu {
    display: none; position: absolute; z-index: 40; top: calc(100% + 6px); left: 0;
    width: 340px; padding: 12px 8px 6px;
    background: var(--ds-surface-card-solid);
    border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-lg);
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}
.ds-pick.is-open .ds-pick__menu { display: block; }
.ds-pick__head {
    padding: 0 6px 8px; font-size: var(--ds-fs-eyebrow); font-weight: 600;
    letter-spacing: 1.3px; color: var(--ds-text-muted);
}
.ds-pick__search-wrap { padding: 0 2px 6px; }
.ds-pick__search {
    width: 100%; height: 36px; padding: 0 11px;
    background: var(--ds-bg); border: 1px solid var(--ds-border-strong);
    border-radius: var(--ds-radius-md); color: var(--ds-text);
    font-family: inherit; font-size: 13.5px;
}
.ds-pick__search:focus { outline: none; border-color: var(--ds-energy); }
.ds-pick__list { max-height: 320px; overflow-y: auto; }
.ds-pick__item {
    display: flex; align-items: flex-start; gap: 11px;
    padding: 8px; border-radius: var(--ds-radius-md); color: var(--ds-text);
}
.ds-pick__item:hover { background: rgba(255, 255, 255, 0.04); }
.ds-pick__item.is-selected { background: rgba(255, 255, 255, 0.07); }
.ds-pick__item[hidden] { display: none; }
.ds-pick__text { min-width: 0; }
.ds-pick__text b { display: block; font-size: 13.5px; font-weight: 600; }
.ds-pick__text small { display: block; margin-top: 2px; font-size: 12px; color: var(--ds-text-muted); }
.ds-pick__dot { flex: none; width: 8px; height: 8px; margin-top: 6px; border-radius: var(--ds-radius-pill); }
.ds-pick__dot--positive { background: var(--ds-positive); }
.ds-pick__dot--warning { background: var(--ds-warning); }
.ds-pick__dot--alert { background: var(--ds-alert); }
.ds-pick__dot--voltage { background: var(--ds-voltage); }
.ds-pick__dot--led { background: var(--ds-led); }
.ds-pick__dot--muted { background: var(--ds-text-muted); }
.ds-pick__empty { padding: 10px 8px; margin: 0; font-size: 12.5px; color: var(--ds-text-muted); }

/* ── Portal footer ────────────────────────────────────────────────────────
   {% portal_footer "client" | "partner" %}. Content follows the
   heliotec.energy footer (Footer.astro); content lives in
   design_system/footer.py. The look is each portal's own chrome, so the
   footer reads as the bottom of the bar at the top of the page:
     --client   mirrors .ds-topbar (this file)
     --partner  mirrors .partner-topbar / .partner-tab (base_partner.html)
   --compact (signed-out pages) keeps only the copyright and legal bar. */
.ds-footer { margin-top: auto; color: var(--ds-text); font-family: var(--ds-font); border-top: 1px solid var(--ds-border); }
.ds-footer__inner { padding: var(--ds-space-10) 48px var(--ds-space-6); }
.ds-footer__main { display: grid; grid-template-columns: 3fr 2fr 2fr 2fr 3fr; gap: var(--ds-space-7); align-items: start; padding-bottom: var(--ds-space-7); border-bottom: 1px solid var(--ds-border); margin-bottom: var(--ds-space-4); }
.ds-footer__brand { display: flex; align-items: center; gap: 20px; }
.ds-footer__logo { height: 32px; width: auto; object-fit: contain; display: block; }
.ds-footer__divider { width: 1px; height: 22px; background: var(--ds-border-strong); }
.ds-footer__label { font-size: 15px; font-weight: 500; letter-spacing: -0.1px; color: var(--ds-text); }
.ds-footer__col { display: flex; flex-direction: column; gap: var(--ds-space-2); }
.ds-footer__heading { margin: 0 0 var(--ds-space-1); font-size: 15px; font-weight: 500; color: var(--ds-text); }
.ds-footer__link { font-size: 14px; color: var(--ds-text-dim); text-decoration: none; transition: color 0.2s; }
.ds-footer__link:hover, .ds-footer__link:focus-visible { color: var(--ds-text); }
.ds-footer__company { display: flex; flex-direction: column; align-items: flex-end; gap: var(--ds-space-1); font-style: normal; font-size: var(--ds-fs-secondary); color: var(--ds-text-dim); text-align: right; }
.ds-footer__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--ds-space-4); }
.ds-footer__legal { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--ds-space-2) var(--ds-space-6); font-size: var(--ds-fs-meta); color: var(--ds-text-muted); }
.ds-footer__legal .ds-footer__link { font-size: var(--ds-fs-meta); }
.ds-footer__follow { display: inline-flex; align-items: center; gap: var(--ds-space-3); font-size: var(--ds-fs-meta); font-weight: 600; color: var(--ds-text-dim); text-decoration: none; transition: color 0.2s; }
.ds-footer__follow:hover { color: var(--ds-text); }
.ds-footer__linkedin { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: var(--ds-radius-sm); background: var(--ds-energy); color: var(--ds-concrete); }

/* Client: the .ds-topbar surface, brand lockup and link type. */
.ds-footer--client { background: var(--ds-topbar-bg); -webkit-backdrop-filter: blur(20px); backdrop-filter: blur(20px); }

/* Partner: the .partner-topbar surface and .partner-tab type, 32px gutters (px-8). */
.ds-footer--partner { background: var(--ds-surface); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.ds-footer--partner .ds-footer__inner { padding-left: 32px; padding-right: 32px; }
.ds-footer--partner .ds-footer__brand { flex-direction: column; align-items: flex-start; gap: var(--ds-space-2); }
.ds-footer--partner .ds-footer__logo { height: 36px; }
.ds-footer--partner .ds-footer__divider { display: none; }
.ds-footer--partner .ds-footer__label { font-size: var(--ds-fs-eyebrow); font-weight: 600; letter-spacing: 1px; text-transform: uppercase; color: var(--ds-text-dim); }
.ds-footer--partner .ds-footer__heading { font-size: 13px; font-weight: 600; }
.ds-footer--partner .ds-footer__link { font-size: 13px; font-weight: 500; }
.ds-footer--partner .ds-footer__legal .ds-footer__link { font-size: var(--ds-fs-meta); font-weight: 400; }

/* Signed out: no chrome above it either, so just the line, on the page. */
.ds-footer--compact { background: transparent; border-top: 0; -webkit-backdrop-filter: none; backdrop-filter: none; }
.ds-footer--compact .ds-footer__inner { padding-top: var(--ds-space-4); padding-bottom: var(--ds-space-4); }
.ds-footer--compact .ds-footer__bar { justify-content: center; }

@media (max-width: 900px) {
  .ds-footer__inner, .ds-footer--partner .ds-footer__inner { padding: var(--ds-space-7) 20px var(--ds-space-4); }
  .ds-footer__main { grid-template-columns: 1fr 1fr; }
  .ds-footer__brand, .ds-footer__company { grid-column: 1 / -1; }
  .ds-footer__company { align-items: flex-start; text-align: left; }
  .ds-footer__bar { flex-direction: column; align-items: flex-start; }
  .ds-footer__legal { width: 100%; }
  /* Signed-out / auth pages: stay centred under the card, as on desktop. */
  .ds-footer--compact .ds-footer__bar { align-items: center; text-align: center; }
  .ds-footer--compact .ds-footer__legal { justify-content: center; }
}
