/* ============================================================================
   TMD Partner Portal — design system
   ----------------------------------------------------------------------------
   Written from first principles for a data-dense B2B dashboard, in the spirit
   of Apple's own apps (Music, Mail, Settings) rather than apple.com marketing.

   Three rules everything here obeys:
     1. Deference — the client's data is the interface. Chrome recedes.
     2. Clarity   — nothing below 13px, nothing below AA contrast, numbers align.
     3. Restraint — one accent. Hairlines and whitespace, not shadows and fills.

   Light is the default. Dark follows the system, and the header toggle can
   override it. No external fonts, no CDN — this runs offline from a file://.
   ========================================================================= */

/* ---------------------------------------------------------------- tokens -- */
:root {
  /* Surfaces. Apple greys: never pure white on pure black. */
  --ground:      #F5F5F7;
  --surface:     #FFFFFF;
  --surface-2:   #F5F5F7;   /* wells, pressed rows, table zebra */
  --surface-3:   #EBEBED;   /* tracks, skeletons */
  --sidebar:     rgba(250,250,252,0.72);

  /* Text. #1D1D1F is Apple's near-black — softer than #000 on white. */
  --text:        #1D1D1F;
  --text-2:      #58585C;   /* 7.1:1 on white */
  --text-3:      #6E6E73;   /* 5.1:1 on white, 4.7:1 on the ground — Apple's
                               own grey is lighter, but it fails AA at 13px and
                               unreadable meta text is the complaint we're fixing */

  --hairline:    rgba(0,0,0,0.10);
  --hairline-2:  rgba(0,0,0,0.06);

  /* One accent. Everything interactive is this colour; nothing else is. */
  /* Apple's #0071E3 is 4.31:1 on the grey ground — a hair under AA. This is
     one shade darker so links and buttons pass on white AND on the ground. */
  --accent:      #006CDB;
  --accent-press:#0059B2;
  --accent-soft: rgba(0,113,227,0.10);
  --accent-ring: rgba(0,113,227,0.32);
  --on-accent:   #FFFFFF;

  /* Semantic — status only, never decoration. Each is dark enough to pass AA
     as 13px text on its OWN 10% tint, which is how pills use it (4.9–5.4:1). */
  --good:        #177734;
  --good-soft:   rgba(23,119,52,0.10);
  --warn:        #9C5200;
  --warn-soft:   rgba(156,82,0,0.10);
  --bad:         #B32E24;
  --bad-soft:    rgba(179,46,36,0.10);
  --on-bad:      #FFFFFF;   /* 6.3:1 on --bad */

  /* Products. Identity dots and chart series only — never a background fill. */
  --p-solar:     #C77700;
  --p-ashp:      #0F7C8C;
  --p-battery:   #5A4BD6;
  --p-heating:   #C0405E;
  --p-commercial:#7A4FC0;

  /* Radii — Apple's card corner is generous, its controls are not. */
  --r-card: 18px;
  --r-ctrl: 12px;
  --r-pill: 980px;

  /* Motion — the iOS sheet curve. Short, and only ever to confirm. */
  --ease: cubic-bezier(.32,.72,0,1);
  --dur: 220ms;
  --dur-fast: 140ms;

  --shadow-card: 0 1px 2px rgba(0,0,0,.04);
  --shadow-pop:  0 12px 32px rgba(0,0,0,.14), 0 2px 8px rgba(0,0,0,.08);

  --sidebar-w: 264px;
  --gutter: 40px;
  --page-max: 1320px;

  /* The TMD wordmark PNG is white-on-transparent (drawn for the dark stats
     panel). brightness(0) repaints it for the light sidebar; opacity matches
     --text's near-black. Dark themes clear the filter and use it as shipped. */
  --logo-filter: brightness(0) opacity(0.92);

  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
          "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
}

:root[data-theme="dark"], html.dark {
  --ground:      #000000;
  --surface:     #1C1C1E;
  --surface-2:   #2C2C2E;
  --surface-3:   #3A3A3C;
  --sidebar:     rgba(28,28,30,0.72);

  --text:        #F5F5F7;
  --text-2:      #ABABB0;
  --text-3:      #8A8A8F;

  --hairline:    rgba(255,255,255,0.14);
  --hairline-2:  rgba(255,255,255,0.08);

  --accent:      #0A84FF;
  --accent-press:#409CFF;
  --accent-soft: rgba(10,132,255,0.16);
  --accent-ring: rgba(10,132,255,0.40);
  /* The dark accents are the bright iOS ones, which is right for text on a
     black ground but leaves white text on the *fill* at 3.65:1 — below AA, on
     the primary button of every screen. So on dark the text on an accent or
     danger fill is near-black instead: 5.2:1 on the accent, 6.7:1 on its
     press state, 5.4:1 on the danger fill. */
  --on-accent:   #06121F;

  --good:        #30D158;  --good-soft: rgba(48,209,88,0.16);
  --warn:        #FF9F0A;  --warn-soft: rgba(255,159,10,0.16);
  --bad:         #FF453A;  --bad-soft:  rgba(255,69,58,0.16);
  --on-bad:      #2B0704;

  --p-solar:     #FF9F0A;
  --p-ashp:      #40C8E0;
  --p-battery:   #8E86FF;
  --p-heating:   #FF6482;
  --p-commercial:#BF5AF2;

  --shadow-card: none;
  --shadow-pop:  0 12px 40px rgba(0,0,0,.6);

  --logo-filter: none;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ground:#000000; --surface:#1C1C1E; --surface-2:#2C2C2E; --surface-3:#3A3A3C;
    --sidebar:rgba(28,28,30,0.72);
    --text:#F5F5F7; --text-2:#ABABB0; --text-3:#8A8A8F;
    --hairline:rgba(255,255,255,0.14); --hairline-2:rgba(255,255,255,0.08);
    --accent:#0A84FF; --accent-press:#409CFF; --accent-soft:rgba(10,132,255,0.16);
    --accent-ring:rgba(10,132,255,0.40); --on-accent:#06121F;
    --good:#30D158; --good-soft:rgba(48,209,88,0.16);
    --warn:#FF9F0A; --warn-soft:rgba(255,159,10,0.16);
    --bad:#FF453A;  --bad-soft:rgba(255,69,58,0.16); --on-bad:#2B0704;
    --p-solar:#FF9F0A; --p-ashp:#40C8E0; --p-battery:#8E86FF;
    --p-heating:#FF6482; --p-commercial:#BF5AF2;
    --shadow-card:none; --shadow-pop:0 12px 40px rgba(0,0,0,.6);
    --logo-filter:none;
  }
}

/* ------------------------------------------------------------------ base -- */
*, *::before, *::after { box-sizing: border-box; }

/* Alpine is deferred, so x-cloak markup (sheets, scrims, popovers) exists in
   the DOM before it boots. Without this rule an invisible fixed scrim sits
   over the page swallowing every click until Alpine initialises. Global,
   because head.php's account menu needs it on every view. */
[x-cloak] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 17px;              /* iOS body. The floor for anything readable. */
  line-height: 1.47;
  letter-spacing: -0.012em;
  color: var(--text);
  background: var(--ground);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1,h2,h3,h4,p,figure,ul,ol { margin: 0; }
ul,ol { padding: 0; list-style: none; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: var(--accent); text-decoration: none; }
/* Colour alone can't say "link" (WCAG 1.4.1). Anchors dressed as buttons are
   recognisable by their shape, so those stay clean; an anchor sitting inside a
   sentence is underlined, because nothing else marks it out. */
a:not(.btn):not(.list-row-btn):not(.nav-item) {
  text-decoration: underline;
  text-underline-offset: 0.16em;
  text-decoration-thickness: 1px;
}

/* Focus: visible, on-brand, and only for keyboards.
   An outline, not a box-shadow. A shadow ring is painted outside the box and
   is therefore sheared off by any scrolling or clipping ancestor — which is
   where the most-used controls in this product live (.nav scrolls, .list and
   .card-pad-0 clip, chip strips scroll sideways). Full-bleed rows have no room
   for an outside ring either, so theirs is drawn just inside the row. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.nav-item:focus-visible,
.account:focus-visible,
.list-row-btn:focus-visible { outline-offset: -2px; }
/* An accent ring is invisible on an accent fill. */
.nav-item[aria-current="page"]:focus-visible { outline-color: var(--on-accent); }
/* The switch input is an invisible box the size of the control; the track
   draws the ring for it. */
.switch input:focus-visible { outline: none; }

/* Landing spots for programmatic focus. Being moved somewhere is not the same
   as tabbing to it, and shouldn't draw a ring. */
#view:focus, .page-head h1:focus, .sheet:focus { outline: none; box-shadow: none; }

::selection { background: var(--accent-soft); }

[hidden] { display: none !important; }

/* Every number in this product is tabular. Columns must align or they lie. */
.num, td.num, .stat-value, .metric, table td, table th {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* -------------------------------------------------------------- typography */
/* The scale is closed: seven steps, 34 / 28 / 22 / 20 / 17 / 15 / 13, plus the
   two display sizes on .stat-value. Nothing in the product may invent a size
   between them — round to the nearest step instead. For reference, the
   one-offs still living in views map like this:
     30px .ov-ring-n, 28px .perf-feedback-pct  -> .t-title   (28)
     24px .perf-feedback-pct sm, 24px headlines -> .t-title-2 (22)
     22px .ov-fact-v                            -> .t-title-2 (22)
     20px .ar-count, .ar-group-title            -> .t-title-3 (20)
     18px .ar-count sm, 17px .cov-n             -> .t-body    (17)
   Each is a one-word change at the call site. */
.t-large   { font-size: 34px; font-weight: 700; letter-spacing: -0.022em; line-height: 1.12; }
.t-title   { font-size: 28px; font-weight: 700; letter-spacing: -0.021em; line-height: 1.16; }
.t-title-2 { font-size: 22px; font-weight: 600; letter-spacing: -0.018em; line-height: 1.22; }
.t-title-3 { font-size: 20px; font-weight: 600; letter-spacing: -0.016em; line-height: 1.26; }
.t-body    { font-size: 17px; font-weight: 400; }
.t-sub     { font-size: 15px; font-weight: 400; color: var(--text-2); }
.t-foot    { font-size: 13px; font-weight: 400; color: var(--text-2); }
.t-strong  { font-weight: 600; }
.t-mut     { color: var(--text-2); }
.t-mut-3   { color: var(--text-3); }
.t-good { color: var(--good); } .t-warn { color: var(--warn); } .t-bad { color: var(--bad); }

/* Section label — the one place small caps type is allowed. */
.t-section {
  font-size: 13px; font-weight: 600; letter-spacing: 0.03em;
  text-transform: uppercase; color: var(--text-2);
}

/* ---------------------------------------------------------------- layout -- */
.app { display: flex; min-height: 100vh; }

/* Sidebar — the macOS/iPadOS pattern. Scales to any number of screens,
   unlike a top tab bar, and keeps the client's place visible at all times. */
.sidebar {
  position: fixed; inset: 0 auto 0 0; width: var(--sidebar-w);
  display: flex; flex-direction: column;
  padding: 16px 12px 12px;
  background: var(--sidebar);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border-right: 1px solid var(--hairline);
  z-index: 40;
}

/* Logo on top, then the client's name, then a first-name greeting — the same
   lockup as the tmdstats.com admin sidebar. */
.brand { display: flex; flex-direction: column; align-items: flex-start; padding: 10px 10px 18px; }
.brand-logo {
  height: 52px; width: auto; display: block; margin-bottom: 12px;
  filter: var(--logo-filter, none);
}
/* The "T" tile still opens the auth screens — see .p-auth-brand. */
.brand-mark {
  width: 28px; height: 28px; border-radius: 8px; flex: none;
  background: var(--text); color: var(--ground);
  display: grid; place-items: center;
  font-size: 13px; font-weight: 700; letter-spacing: -0.02em;
}
.brand-name { display: block; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.brand-sub  { display: block; font-size: 12px; color: var(--text-3); margin-top: 1px; }

.nav { display: flex; flex-direction: column; gap: 2px; flex: 1; overflow-y: auto; }
.nav-label { padding: 14px 10px 6px; }

.nav-item {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 10px; border-radius: 8px;
  font-size: 15px; font-weight: 500; color: var(--text);
  background: none; border: 0; width: 100%; text-align: left;
  cursor: pointer;
  /* Colour must travel with the background: transitioning one and not the
     other leaves white text on a near-transparent fill mid-swap. */
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.nav-item:hover { background: var(--hairline-2); }
.nav-item[aria-current="page"] { background: var(--accent); color: var(--on-accent); }
.nav-item[aria-current="page"] .nav-icon { opacity: 1; }
.nav-item[aria-current="page"] .nav-count { color: var(--on-accent); opacity: .75; }
.nav-icon { width: 20px; height: 20px; flex: none; opacity: .82; }
.nav-count { margin-left: auto; font-size: 13px; color: var(--text-3); font-variant-numeric: tabular-nums; }

.sidebar-foot { border-top: 1px solid var(--hairline); padding-top: 10px; margin-top: 10px; }

.account {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: 10px; background: none;
  cursor: pointer; text-align: left; transition: background var(--dur-fast) ease;
}
.account:hover { background: var(--hairline-2); }
.avatar {
  width: 30px; height: 30px; border-radius: 50%; flex: none;
  background: var(--accent); color: var(--on-accent);
  display: grid; place-items: center; font-size: 12px; font-weight: 600;
}
.account-name { font-size: 14px; font-weight: 500; line-height: 1.2; }
.account-role { font-size: 12px; color: var(--text-3); }

/* Content column */
.main { flex: 1; min-width: 0; margin-left: var(--sidebar-w); }

/* Sticky header that condenses on scroll — the iOS large-title collapse. */
.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 16px;
  /* The bar itself is full-bleed so the blur runs edge to edge, but its
     contents sit in the same centred column as the page — otherwise the
     appearance toggle drifts hundreds of pixels away from everything else. */
  padding: 0 max(var(--gutter), calc((100% - var(--page-max)) / 2 + var(--gutter)));
  height: 52px;
  background: color-mix(in srgb, var(--ground) 72%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur) ease;
}
.topbar.is-stuck { border-bottom-color: var(--hairline); }
.topbar-title {
  font-size: 17px; font-weight: 600; letter-spacing: -0.014em;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.topbar.is-stuck .topbar-title { opacity: 1; transform: none; }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }

/* The column is centred, not left-aligned: on a 27" display an uncentred
   1320px column leaves the whole product hanging off the left edge. */
.page { padding: 8px var(--gutter) 96px; max-width: var(--page-max); margin-inline: auto; }
.page-head { padding: 20px 0 28px; display: flex; align-items: flex-start; gap: 24px; }
.page-head .t-sub { margin-top: 6px; max-width: 60ch; }
.page-head-actions { margin-left: auto; display: flex; gap: 10px; flex: none; padding-top: 4px; }

.section { margin-top: 44px; }
.section:first-child { margin-top: 0; }
.section-head {
  display: flex; align-items: baseline; gap: 14px;
  margin-bottom: 14px; padding-bottom: 0;
}
.section-head .t-sub { margin-left: auto; }
/* A section heading is one size everywhere — 20px/600, the .t-title-3 step.
   The rule lives on the component so the page is consistent whichever token a
   screen reached for; screens still carrying .t-title-2 here should drop to
   .t-title-3 so the markup says what it renders. */
.section-head > h2, .section-head > h3 {
  font-size: 20px; font-weight: 600; letter-spacing: -0.016em; line-height: 1.26;
}

/* Hero — the one-sentence answer a screen opens with. Every screen states its
   position the same way: one padding, one measure, one size. Screens with a
   bespoke hero (.cov-hero, .perf-answer, .set-lede, .ov-hero) should use
   `card hero` instead, and the two that put their sentence in the 15px page
   subtitle (Billing, Leads) should promote it to one. */
.hero { padding: 26px 28px; }
.hero > h1, .hero > h2, .hero-line { max-width: 32ch; }
.hero > p, .hero-sub { margin-top: 10px; max-width: 62ch; }

.stack { display: flex; flex-direction: column; gap: 12px; }
.row-x { display: flex; align-items: center; gap: 12px; }
.spacer { flex: 1; }

.grid { display: grid; gap: 20px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0,1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); }
.grid-auto { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* ----------------------------------------------------------------- cards -- */
.card {
  background: var(--surface);
  border: 1px solid var(--hairline-2);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  padding: 24px;
}
/* clip, not hidden: overflow:hidden makes the card a sticky containing block,
   which pins a sticky table header 52px down INSIDE the card and hides the
   first row under it. clip rounds the corners without becoming one. */
.card-pad-0 { padding: 0; overflow: hidden; overflow: clip; }
.card-head {
  display: flex; align-items: center; gap: 14px;
  padding: 20px 24px; border-bottom: 1px solid var(--hairline-2);
}
.card-head .spacer { flex: 1; }
.card-body { padding: 24px; }

/* Stat — the headline number. Big, tabular, with its label above it. */
.stat { display: flex; flex-direction: column; gap: 6px; }
.stat-label { font-size: 15px; color: var(--text-2); font-weight: 500; }
.stat-value { font-size: 40px; font-weight: 600; letter-spacing: -0.028em; line-height: 1; }
.stat-value.sm { font-size: 28px; }
.stat-hint { font-size: 13px; color: var(--text-3); }
.stat-delta { font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; gap: 3px; }

/* --------------------------------------------------------------- buttons -- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 38px; padding: 0 18px; border-radius: var(--r-pill);
  font-size: 15px; font-weight: 500; letter-spacing: -0.01em;
  /* The hairline is load-bearing: --surface-2 is the same colour as the page
     ground, so without it a secondary button on the page is invisible. */
  border: 1px solid var(--hairline); background: var(--surface-2); color: var(--text);
  cursor: pointer; white-space: nowrap;
  transition: background var(--dur-fast) ease, transform var(--dur-fast) ease, opacity var(--dur-fast) ease;
}
.btn:hover { background: var(--surface-3); }
.btn:active { transform: scale(.97); }
.btn:disabled { opacity: .4; cursor: not-allowed; transform: none; }

.btn-primary { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.btn-primary:hover { background: var(--accent-press); }

.btn-plain { background: none; color: var(--accent); padding: 0 10px; border-color: transparent; }
.btn-plain:hover { background: var(--accent-soft); }

.btn-danger { background: var(--bad-soft); color: var(--bad); border-color: transparent; }
.btn-danger:hover { background: var(--bad); color: var(--on-bad); }

.btn-sm { height: 30px; padding: 0 13px; font-size: 13px; }
.btn-lg { height: 46px; padding: 0 24px; font-size: 17px; }
.btn-icon { width: 34px; height: 34px; padding: 0; border-radius: 50%; background: none; border-color: transparent; }
.btn-icon:hover { background: var(--hairline-2); }
.btn svg { width: 16px; height: 16px; flex: none; }

/* ---------------------------------------------------------------- inputs -- */
.field { display: flex; flex-direction: column; gap: 7px; }
.label { font-size: 15px; font-weight: 500; }
.help { font-size: 13px; color: var(--text-2); }

.input, .select, textarea.input {
  height: 40px; padding: 0 13px; width: 100%;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--hairline); border-radius: var(--r-ctrl);
  font-size: 16px;                    /* 16px stops iOS zooming on focus */
  transition: border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
textarea.input { height: auto; padding: 11px 13px; line-height: 1.5; resize: vertical; }
.input:focus, .select:focus, textarea.input:focus {
  outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-ring);
}
.input::placeholder { color: var(--text-3); }
.input[aria-invalid="true"] { border-color: var(--bad); }

.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='%2386868B' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 13px center;
  padding-right: 34px; cursor: pointer;
}

.search { position: relative; }
.search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--text-3); pointer-events: none; }
.search .input { padding-left: 36px; }

/* iOS switch — the real geometry (51×31), because a fake one always shows. */
.switch { position: relative; display: inline-block; width: 51px; height: 31px; flex: none; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch .track {
  position: absolute; inset: 0; border-radius: var(--r-pill);
  background: var(--surface-3); transition: background var(--dur) var(--ease);
  pointer-events: none;
}
.switch .thumb {
  position: absolute; top: 2px; left: 2px; width: 27px; height: 27px;
  border-radius: 50%; background: #fff;
  box-shadow: 0 3px 8px rgba(0,0,0,.15), 0 1px 1px rgba(0,0,0,.16);
  transition: transform var(--dur) var(--ease);
  pointer-events: none;
}
.switch input:checked ~ .track { background: var(--good); }
.switch input:checked ~ .thumb { transform: translateX(20px); }
.switch input:focus-visible ~ .track { box-shadow: 0 0 0 4px var(--accent-ring); }

/* Segmented control with a sliding indicator. */
.seg {
  position: relative; display: inline-flex; padding: 2px; gap: 0;
  background: var(--surface-3); border-radius: 10px;
}
.seg button {
  position: relative; z-index: 1; border: 0; background: none; cursor: pointer;
  height: 30px; padding: 0 14px; border-radius: 8px;
  font-size: 14px; font-weight: 500; color: var(--text);
  transition: color var(--dur-fast) ease;
}
.seg button[aria-selected="true"] { font-weight: 600; }
.seg-thumb {
  position: absolute; top: 2px; bottom: 2px; z-index: 0;
  background: var(--surface); border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,.12);
  transition: transform var(--dur) var(--ease), width var(--dur) var(--ease);
}

/* ------------------------------------------------------- pills & badges -- */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  height: 24px; padding: 0 10px; border-radius: var(--r-pill);
  font-size: 13px; font-weight: 500; white-space: nowrap;
  background: var(--surface-2); color: var(--text-2);
}
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.pill-good { background: var(--good-soft); color: var(--good); }
.pill-warn { background: var(--warn-soft); color: var(--warn); }
.pill-bad  { background: var(--bad-soft);  color: var(--bad); }
.pill-accent { background: var(--accent-soft); color: var(--accent); }

/* Product identity: a coloured dot and a name. Never a coloured background. */
.product { display: inline-flex; align-items: center; gap: 7px; font-size: 15px; white-space: nowrap; }
.product .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--_c, var(--text-3)); flex: none; }
.p-solar      { --_c: var(--p-solar); }
.p-ashp       { --_c: var(--p-ashp); }
.p-battery    { --_c: var(--p-battery); }
.p-heating    { --_c: var(--p-heating); }
.p-commercial { --_c: var(--p-commercial); }

/* ---------------------------------------------------------------- tables -- */
/* No overflow here by default: an overflow container becomes the sticky
   context, which would pin the header inside the table instead of below the
   topbar and hide the first row. Tables re-flow to cards on small screens;
   add .scroll-x only where horizontal scrolling is genuinely the answer. */
.table-wrap { width: 100%; }
.table-wrap.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.table-wrap.scroll-x table.data th { top: 0; }
table.data { width: 100%; border-collapse: collapse; font-size: 15px; }
table.data th {
  position: sticky; top: 52px; z-index: 2;
  text-align: left; font-size: 13px; font-weight: 600; color: var(--text-2);
  padding: 10px 14px; white-space: nowrap;
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
}
table.data td {
  padding: 13px 14px; border-bottom: 1px solid var(--hairline-2);
  vertical-align: middle;
}
table.data tbody tr { transition: background var(--dur-fast) ease; }
table.data tbody tr:hover { background: var(--surface-2); }
table.data tbody tr:last-child td { border-bottom: 0; }
table.data .num, table.data th.num { text-align: right; }
table.data .strong { font-weight: 600; }
.col-tight { width: 1%; white-space: nowrap; }

/* Grouped inset list — iOS Settings. Ideal for anything configurable. */
.list { background: var(--surface); border: 1px solid var(--hairline-2); border-radius: var(--r-card); overflow: hidden; }
.list-row {
  display: flex; align-items: center; gap: 14px;
  min-height: 56px; padding: 12px 20px;
  border-bottom: 1px solid var(--hairline-2);
}
.list-row:last-child { border-bottom: 0; }
.list-row-btn { width: 100%; background: none; border: 0; cursor: pointer; text-align: left; font: inherit; }
.list-row-btn:hover { background: var(--surface-2); }
.list-row .body { min-width: 0; flex: 1; }
.list-row .title { font-size: 17px; }
.list-row .meta { font-size: 13px; color: var(--text-2); margin-top: 1px; }
.list-row .trail { margin-left: auto; display: flex; align-items: center; gap: 10px; flex: none; }
.list-head { padding: 0 4px 8px; }

/* -------------------------------------------------------------- progress -- */
.bar { height: 8px; border-radius: var(--r-pill); background: var(--surface-3); overflow: hidden; }
.bar > span { display: block; height: 100%; border-radius: inherit; background: var(--accent); transition: width var(--dur) var(--ease); }
.bar.good > span { background: var(--good); }
.bar.warn > span { background: var(--warn); }
.bar.full > span { background: var(--text-3); }

.ring { transform: rotate(-90deg); }
.ring circle { fill: none; stroke-linecap: round; }
.ring .track { stroke: var(--surface-3); }
.ring .value { stroke: var(--accent); transition: stroke-dashoffset 600ms var(--ease); }

/* ----------------------------------------------------------------- sheet -- */
/* The page holds still behind the scrim. Without this a flick anywhere outside
   the sheet scrolls the screen underneath it, and cancelling hands the user
   back a page scrolled somewhere they never chose. --scrollbar-w is measured
   in sheet() so removing the scrollbar doesn't shift the layout sideways. */
body.is-modal { overflow: hidden; padding-right: var(--scrollbar-w, 0px); }

.scrim {
  position: fixed; inset: 0; z-index: 90;
  background: rgba(0,0,0,.32);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
  opacity: 0; transition: opacity var(--dur) ease;
}
.scrim.open { opacity: 1; }

.sheet {
  position: fixed; z-index: 91; left: 50%; top: 50%;
  transform: translate(-50%, -46%) scale(.98); opacity: 0;
  width: min(560px, calc(100vw - 40px)); max-height: min(84vh, 760px);
  display: flex; flex-direction: column;
  background: var(--surface); border-radius: 20px; box-shadow: var(--shadow-pop);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.sheet.open { opacity: 1; transform: translate(-50%, -50%) scale(1); }
.sheet.wide { width: min(760px, calc(100vw - 40px)); }
.sheet-head { padding: 22px 24px 0; }
/* overscroll-behavior: scrolling past the end of the sheet's own content must
   not start scrolling the page behind it. */
.sheet-body { padding: 18px 24px; overflow-y: auto; overscroll-behavior: contain; }
.sheet-foot {
  display: flex; gap: 10px; justify-content: flex-end;
  padding: 16px 24px 22px; border-top: 1px solid var(--hairline-2);
}
.sheet-grabber { display: none; }

/* -------------------------------------------------------------- toasts --- */
.toasts {
  position: fixed; z-index: 120; left: 50%; bottom: 28px;
  transform: translateX(-50%);
  display: flex; flex-direction: column-reverse; gap: 8px; align-items: center;
  pointer-events: none;
}
.toast {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 18px; border-radius: var(--r-pill);
  background: var(--text); color: var(--ground);
  font-size: 15px; font-weight: 500; box-shadow: var(--shadow-pop);
  animation: toast-in var(--dur) var(--ease);
  max-width: min(560px, calc(100vw - 32px));
}
.toast svg { width: 17px; height: 17px; flex: none; }
.toast.out { animation: toast-out var(--dur-fast) ease forwards; }
@keyframes toast-in  { from { opacity: 0; transform: translateY(12px) scale(.96); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(6px) scale(.98); } }

/* --------------------------------------------------------------- states -- */
.empty { padding: 56px 24px; text-align: center; }
.empty-icon {
  width: 46px; height: 46px; margin: 0 auto 14px; border-radius: 50%;
  background: var(--surface-2); color: var(--text-3);
  display: grid; place-items: center;
}
/* TMD.icon() emits an <svg> with no intrinsic size, so without this the glyph
   fills the circle edge to edge and the circle disappears behind it. Two
   screens patched this locally; it belongs here, and those can drop it. */
.empty-icon svg { width: 22px; height: 22px; }
.empty h3 { font-size: 17px; font-weight: 600; margin-bottom: 4px; }
.empty p { font-size: 15px; color: var(--text-2); max-width: 40ch; margin: 0 auto; }

.skel { background: var(--surface-3); border-radius: 8px; animation: pulse 1.4s ease-in-out infinite; }
@keyframes pulse { 50% { opacity: .5; } }

/* ---------------------------------------------------------------- charts -- */
.chart { width: 100%; display: block; overflow: visible; }
.chart .axis { stroke: var(--hairline); stroke-width: 1; }
.chart .grid-line { stroke: var(--hairline-2); stroke-width: 1; }
.chart .tick { font-size: 11px; fill: var(--text-3); font-variant-numeric: tabular-nums; }
.chart .bar-r { transition: opacity var(--dur-fast) ease; }
.chart .bar-r:hover { opacity: .72; }

.legend { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.legend-item { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--text-2); }
.legend-swatch { width: 9px; height: 9px; border-radius: 3px; flex: none; }

/* Map */
.map-svg { width: 100%; height: auto; display: block; }
.map-svg path {
  fill: var(--surface-3); stroke: var(--surface); stroke-width: 1.2;
  transition: fill var(--dur-fast) ease;
}
.map-svg path.on { fill: var(--_c, var(--accent)); }
.map-svg path.hit { cursor: pointer; }
.map-svg path.hit:hover { fill: var(--accent-press); }

.map-tip {
  position: absolute; z-index: 5; pointer-events: none;
  padding: 8px 12px; border-radius: 10px;
  background: var(--text); color: var(--ground);
  font-size: 13px; font-weight: 500; white-space: nowrap;
  box-shadow: var(--shadow-pop); transform: translate(-50%, -130%);
}

/* ------------------------------------------------------------ responsive -- */
@media (max-width: 1100px) {
  :root { --gutter: 28px; }
  .grid-4 { grid-template-columns: repeat(2, minmax(0,1fr)); }
  .grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); }
}

/* Below 900px the sidebar becomes an iOS tab bar. */
@media (max-width: 900px) {
  :root { --gutter: 20px; }
  .app { display: block; }
  .main { margin-left: 0; padding-bottom: calc(58px + env(safe-area-inset-bottom)); }

  .sidebar {
    inset: auto 0 0 0; width: auto; flex-direction: row;
    padding: 6px 8px calc(6px + env(safe-area-inset-bottom));
    border-right: 0; border-top: 1px solid var(--hairline);
  }
  .brand, .sidebar-foot, .nav-label, .nav-count { display: none; }
  .nav {
    flex-direction: row; justify-content: space-around; gap: 0;
    overflow-x: auto; scrollbar-width: none;
  }
  .nav::-webkit-scrollbar { display: none; }
  .nav-item {
    flex-direction: column; gap: 3px; padding: 6px 10px; min-width: 62px;
    font-size: 10px; font-weight: 500; border-radius: 10px; text-align: center;
  }
  .nav-item[aria-current="page"] { background: none; color: var(--accent); }
  .nav-icon { width: 23px; height: 23px; }

  .page-head { flex-direction: column; gap: 16px; }
  .page-head-actions { margin-left: 0; width: 100%; }
  .page-head-actions .btn { flex: 1; }
  .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0,1fr); }
  .t-large { font-size: 28px; }
  .stat-value { font-size: 34px; }

  /* Any table that hasn't re-flowed into cards must stay reachable here, so
     the wrap scrolls again. Safe at this width: the header pins to 0 rather
     than below the topbar, so making this a scroll container costs nothing. */
  .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  table.data th { top: 0; }

  /* Sheets become bottom sheets, as they should on a phone. */
  .sheet {
    top: auto; bottom: 0; left: 0; transform: translateY(100%);
    width: 100%; max-width: none; max-height: 88vh;
    border-radius: 18px 18px 0 0;
    padding-bottom: env(safe-area-inset-bottom);
  }
  .sheet.open { transform: translateY(0); }
  .sheet.wide { width: 100%; }
  .sheet-grabber {
    display: block; width: 36px; height: 5px; border-radius: 3px;
    background: var(--surface-3); margin: 8px auto 0;
  }
  .toasts { bottom: calc(70px + env(safe-area-inset-bottom)); }
}

@media (max-width: 560px) {
  .card { padding: 18px; border-radius: 14px; }
  .card-head, .card-body { padding: 16px 18px; }
  .hero { padding: 20px; }
}

/* Motion is a confirmation, not a feature. Honour the system preference. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}

@media print {
  .sidebar, .topbar, .page-head-actions, .toasts { display: none !important; }
  .main { margin: 0; }
  .card { break-inside: avoid; border-color: #ddd; }
}

/* ============================================================================
   Live-portal additions — pieces the offline prototype had no need for.
   ========================================================================= */

/* Admin "view as client". Deliberately the loudest thing on the page: staff
   must never forget whose account they are looking at. */
.p-adminbar {
  position: sticky; top: 52px; z-index: 29;
  display: flex; align-items: center; gap: 10px;
  padding: 10px var(--gutter);
  font-size: 14px; font-weight: 500;
  color: var(--warn); background: var(--warn-soft);
  border-bottom: 1px solid color-mix(in srgb, var(--warn) 26%, transparent);
}
.p-adminbar svg { width: 16px; height: 16px; flex: none; }
.p-adminbar a { margin-left: auto; color: inherit; text-decoration: underline; font-weight: 600; }

/* Account menu in the sidebar foot. */
.account-menu {
  position: absolute; bottom: calc(100% + 8px); left: 12px; right: 12px; z-index: 50;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 14px; box-shadow: var(--shadow-pop); overflow: hidden;
  padding: 6px;
}
.account-menu a {
  display: block; padding: 9px 12px; border-radius: 8px;
  font-size: 15px; color: var(--text); text-decoration: none;
}
.account-menu a:hover { background: var(--surface-2); }
.account-menu a.danger { color: var(--bad); }
.account-menu-head { padding: 10px 12px 8px; border-bottom: 1px solid var(--hairline-2); margin-bottom: 6px; }

/* ---------------------------------------------------------- auth screens -- */
/* Signed-out pages have no shell: one centred card on the page ground. */
.p-auth {
  min-height: 100vh; display: grid; place-items: center;
  padding: 32px 20px calc(32px + env(safe-area-inset-bottom));
}
.p-auth-card { width: 100%; max-width: 420px; }
.p-auth-brand {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-bottom: 26px;
}
.p-auth-brand .brand-mark { width: 32px; height: 32px; border-radius: 9px; font-size: 15px; }
.p-auth-brand-name { font-size: 17px; font-weight: 600; letter-spacing: -0.014em; }
.p-auth-box {
  background: var(--surface); border: 1px solid var(--hairline-2);
  border-radius: var(--r-card); box-shadow: var(--shadow-card);
  padding: 28px;
}
.p-auth-box h1 { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 6px; }
.p-auth-foot { text-align: center; margin-top: 20px; font-size: 15px; color: var(--text-2); }
.p-auth-foot a { font-weight: 500; }

.p-alert {
  display: flex; align-items: flex-start; gap: 9px;
  padding: 12px 14px; border-radius: var(--r-ctrl);
  font-size: 15px; margin-bottom: 20px;
}
.p-alert svg { width: 17px; height: 17px; flex: none; margin-top: 1px; }
.p-alert-error { background: var(--bad-soft); color: var(--bad); }
.p-alert-ok { background: var(--good-soft); color: var(--good); }

/* Legacy aliases: a handful of live views still reach for these names. */
.p-hint { font-size: 13px; color: var(--text-2); }
.p-num  { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }

/* Footer sits outside .main, so it needs the sidebar offset itself. In a
   stylesheet rather than inline: an inline style would beat the media query
   below and leave the footer indented 264px on a phone. */
.p-foot {
  margin-left: var(--sidebar-w);
  padding: 0 var(--gutter) 40px;
  display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  color: var(--text-3); font-size: 13px;
}
@media (max-width: 900px) {
  .p-foot { margin-left: 0; padding-bottom: calc(70px + env(safe-area-inset-bottom)); }
}
