/* ============================================================
   CYGM card viewer — the app's own sheet, inside the site chrome.

   What went first was the duplication. Gone are its private palette, its own
   nav, hero and footer, and the global resets that fought the shared sheets.
   Everything that is left is confined to .viewer-app, the wrapper the page puts
   around the app, so no rule here can reach the header, the menu or the footer.

   Three families are the exception and are deliberately NOT scoped: the live
   bar (.live-bar/.lb-*) and the full-screen readout (.lf-*) are appended to
   document.body by the script, so they sit outside the wrapper. They carry
   unique prefixes and cannot collide, and they take their colours from the
   same mapping below, which lists them alongside .viewer-app.
   ============================================================ */

.viewer-app,
.live-bar,
.lf-wrap {
  /* The site tokens, under the names the app already uses. A change to the
     shared palette carries here; there is no second set of colours. */
  --bg-primary:      var(--c-ground, #0a0e17);
  /* What the page itself is painted, for the one surface that has to occlude
     whatever scrolls under it. The site took its ground a few stops below
     --c-ground and never moved that token with it, so a sticky bar painted
     --bg-primary renders as a lighter strip across the page. Measured: the
     body is 7,9,14 and --bg-primary is 10,14,23. */
  --bg-page:         var(--ink-900, var(--c-ground, #0a0e17));
  --bg-secondary:    var(--c-band, #0f1420);
  --bg-tertiary:     var(--c-panel, #151b2b);
  --bg-card:         var(--c-card, #141a2a);
  --bg-card-hover:   var(--color-surface-hover, #1a2236);

  --text-primary:    var(--color-text, #e8ecf4);
  --text-secondary:  var(--color-text-muted, #8b95a8);
  --text-muted:      var(--color-text-faint, #74839d);

  --accent:             var(--c-accent, #00d4ff);
  --accent-dim:         var(--c-accent-dim, #00a8cc);
  --accent-glow:        var(--color-glow, rgba(0, 212, 255, 0.15));
  --accent-glow-strong: var(--color-glow-strong, rgba(0, 212, 255, 0.3));

  --glucose-green:  var(--c-green, #22c55e);
  --glucose-yellow: var(--c-yellow, #eab308);
  --glucose-red:    var(--c-red, #ef4444);

  --border-subtle: var(--color-border, rgba(255, 255, 255, 0.08));
  --border-accent: var(--color-border-strong, rgba(0, 212, 255, 0.2));
  --line:          var(--color-border, rgba(255, 255, 255, 0.1));

  --container-max: 1200px;

  /* Flat, like every other surface on the site. The pills keep their shape
     because they ask for 100px directly; everything that asked for --radius
     is now a hairline panel. */
  --radius: 0;
  --radius-sm: 0;
  --radius-xs: 0;

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* Zone colours stay literal: these are data, read off a chart, and they
     have to stay distinguishable from each other rather than track a brand. */
  --z-vlow: #dc2626;
  --z-low: #f59e0b;
  --z-range: #22c55e;
  --z-high: #eab308;
  --z-vhigh: #f97316;
}

/* The body rule that set these is gone with the rest of the globals. */
.viewer-app {
  font-family: var(--font-body, 'DM Sans', system-ui, sans-serif);
  line-height: 1.6;
  color: var(--text-primary);
}

/* The wrapper is the band itself on this page, so the band's own top padding
   would put a whole section of air between the hero and the one question the
   page asks. On a phone that air was the difference between seeing the choice
   and seeing nothing but the headline. The hero's bottom padding already
   separates the two. */
.viewer-app.section { padding-top: 0; }

.viewer-app .sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* The shared sheet gives .card and .btn a display of their own, and a class
   beats the browser's own [hidden] rule, so anything the script hid that was
   one of those stayed on the page regardless. It has to be !important: the
   class rules it overrides are written at the same specificity further down
   this very file, where they would otherwise win on order. */
.viewer-app [hidden] { display: none !important; }

.viewer-app .mono { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.viewer-app .privacy-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 1.5rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border-subtle);
    background: none;
    border-radius: 0;
    font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-secondary);
}
.viewer-app .privacy-badge svg { width: 16px; height: 16px; flex-shrink: 0; stroke: var(--glucose-green); }

/* ============================================
   CARDS / BUTTONS
   ============================================ */
.viewer-app .card {
    background: var(--bg-card);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius);
    padding: clamp(1.1rem, 3vw, 1.75rem);
}
.viewer-app .card + .card { margin-top: 1.25rem; }

.viewer-app .card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}
/* Every card title is a real h3 now, so the size and the weight here are the
   whole of its treatment and it must not inherit a heading's own scale. */
.viewer-app .card-title {
    font-family: 'Outfit', sans-serif;
    font-size: 1.15rem;
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: -0.01em;
    margin: 0;
}
.viewer-app .card-note {
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.viewer-app .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.8rem 1.5rem;
    border-radius: 100px;
    font-family: 'DM Sans', sans-serif;
    font-size: 0.9375rem;
    font-weight: 600;
    cursor: pointer;
    border: 1px solid transparent;
    background: transparent;
    color: var(--text-primary);
    transition: transform 0.3s var(--ease-spring), background 0.25s, border-color 0.25s, box-shadow 0.25s;
    min-height: 44px;
}
.viewer-app .btn svg { width: 18px; height: 18px; flex-shrink: 0; }
.viewer-app .btn-primary { background: var(--accent); color: #06121a; }
.viewer-app .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 30px var(--accent-glow-strong); }
.viewer-app .btn-secondary { border-color: var(--border-subtle); background: rgba(255,255,255,0.02); }
.viewer-app .btn-secondary:hover { border-color: var(--text-muted); background: rgba(255,255,255,0.06); }
.viewer-app .btn-small { padding: 0.5rem 0.95rem; font-size: 0.8125rem; min-height: 44px; }
.viewer-app .btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }
/* The tabs are switched off until a day is loaded, so they need the same
   "not yet" reading the buttons have; without it a dead tab still invites a
   click and the hover lift fires on something that cannot respond. */
.viewer-app .tab:disabled { opacity: 0.45; cursor: not-allowed; }
.viewer-app .tab:disabled:hover { background: none; transform: none; }

/* ============================================
   LOADER
   ============================================ */
.viewer-app .loader { padding-bottom: 2rem; }

/* The loader leads the workspace, so it needs a heading a sighted reader can
   see: two unlabelled panels side by side do not say they are a choice. */
.viewer-app .loader-heading {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.25rem, 3.5vw, 1.6rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    margin-bottom: 1rem;
}

.viewer-app .dropzone {
    border: 2px dashed rgba(255,255,255,0.14);
    border-radius: var(--radius);
    background: var(--bg-secondary);
    padding: clamp(1.75rem, 6vw, 3.25rem) 1.5rem;
    text-align: center;
    transition: border-color 0.25s, background 0.25s, transform 0.25s var(--ease-out);
}
.viewer-app .dropzone.over {
    border-color: var(--accent);
    background: rgba(0, 212, 255, 0.06);
    transform: scale(1.005);
}
.viewer-app .dropzone-icon {
    width: 56px; height: 56px;
    margin: 0 auto 1rem;
    color: var(--accent);
    opacity: 0.9;
}
.viewer-app .dropzone-icon svg { width: 100%; height: 100%; }
.viewer-app .dropzone h3 {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.15rem, 3vw, 1.4rem);
    font-weight: 700;
    margin-bottom: 0.5rem;
}
.viewer-app .dropzone p {
    color: var(--text-secondary);
    font-size: 0.9375rem;
    max-width: 48ch;
    margin: 0 auto;
}
.viewer-app .dropzone-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    justify-content: center;
    margin-top: 1.5rem;
}
/* How to get hold of the files at all. It has to beat .dropzone p on
   specificity, which is why it names the box as well as the note. */
.viewer-app .dropzone .dropzone-note {
    margin: 1.1rem auto 0;
    max-width: 52ch;
    font-size: 0.8125rem;
    color: var(--text-muted);
}
.viewer-app .dropzone .dropzone-note a { color: var(--accent); }

/* Two lanes, not two equal halves. The drop box is the front door and takes
   the wider one, and neither lane stretches to the height of the other: the
   two have different amounts to say and a shared height would pad the shorter
   one with empty card. */
.viewer-app .route-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: 1.25rem;
}
/* Two lanes only where both are wide enough to read. At 768 the device lane
   was a 330px column of prose beside a drop box that ran out of content half
   way down it, so the band from 48rem to 60rem stacks them instead. */
@media (min-width: 60rem) {
    .viewer-app .route-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
}
.viewer-app .route-card h3 {
    font-family: 'Outfit', sans-serif;
    font-size: 1.05rem;
    font-weight: 700;
    margin-bottom: 0.5rem;
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.viewer-app .route-card h3 svg { width: 20px; height: 20px; color: var(--accent); }
.viewer-app .route-card p { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 1rem; }
.viewer-app .route-card ol,
.viewer-app .route-card ul { margin: 0 0 1rem 1.1rem; font-size: 0.9rem; color: var(--text-secondary); }
.viewer-app .route-card li { margin-bottom: 0.4rem; }
.viewer-app .route-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
}
/* Marked when the other route is the one worth trying next. */
.viewer-app .btn.suggested { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }

/* ── one button, and the spare one ────────────────────────────────────────────
   Two buttons side by side both named a way of talking to a cable, and nobody
   could tell which one was theirs, so the lane offers one and keeps the other
   back. Three things bring it out, and they are written as separate rules on
   purpose: the first two are plain sibling selectors that work everywhere, and
   only the third asks for :has(), so a browser without it drops that rule
   alone and still reveals the button on the two paths that matter.

     1. the script marks it, which is what it does the moment the first route
        comes up empty or the browser has no port chooser;
     2. the first route cannot run here at all, so the button for it is
        disabled and the spare is the only way through;
     3. the reader opened the disclosure looking for help. A session that dies
        part way through never marks anything, and that is the case this one
        covers.

   #usbBtn has to stay a sibling of #serialBtn inside .route-actions, and the
   note a sibling after it, or two of the three stop matching. */
.viewer-app #usbBtn { display: none; }
.viewer-app #serialBtn:disabled ~ #usbBtn,
.viewer-app #usbBtn.suggested { display: inline-flex; }
.viewer-app .route-card:has(.route-detail[open]) #usbBtn { display: inline-flex; }

.viewer-app .route-fallback-note {
    display: none;
    flex: 1 1 100%;
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--text-secondary);
}
.viewer-app #usbBtn.suggested ~ .route-fallback-note,
.viewer-app #serialBtn:disabled ~ #usbBtn ~ .route-fallback-note { display: block; }
.viewer-app .route-card:has(.route-detail[open]) .route-fallback-note { display: block; }

/* ── the two answers, in one row ──────────────────────────────────────────────
   Where the lanes stand side by side both answers are already on screen and
   this row is nothing but noise. Where they stack, the second lane is off the
   bottom of the screen and nothing on the first screen says it exists, so the
   question is followed immediately by its two answers and a tap on either
   puts that lane at the top of the screen. */
.viewer-app .route-chips {
    display: none;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0 0 1.25rem;
}
.viewer-app .route-chips a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0.35rem 0.9rem;
    border: 1px solid var(--border-accent);
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--accent);
    text-decoration: none;
}
.viewer-app .route-chips a:hover { border-color: var(--accent); background: var(--accent-glow); }

/* The word between the two lanes, so a stacked pair still reads as a choice
   and not as two steps. It is a real element and not a pseudo on the card:
   .card already owns its ::before for the hover hairline and clips it with
   overflow, so a word hung above the card's top edge never paints. Above the
   breakpoint the lanes are side by side, the word has no gap to sit in, and a
   third grid item would drop into a row of its own. */
.viewer-app .route-or { display: none; }

@media (max-width: 59.999rem) {
    .viewer-app .route-chips { display: flex; }

    .viewer-app .route-or {
        display: block;
        margin: 0;
        text-align: center;
        font-family: 'JetBrains Mono', ui-monospace, monospace;
        font-size: 0.75rem;
        font-weight: 600;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: var(--text-muted);
    }
}

/* ── the walkthrough at each route ────────────────────────────────────────────
   The step-by-step for each way in, folded away at the route it belongs to
   rather than kept as a card two screens further down. Folded is right here
   and wrong for the privacy note above: this is an answer to a question most
   readers will not have, and that is a notice everyone has to be able to read
   without pressing anything. */
.viewer-app .route-detail {
    margin-top: 1.1rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--border-subtle);
    text-align: left;
}
.viewer-app .route-detail > summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 44px;
    cursor: pointer;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--text-secondary);
    list-style: none;
}
.viewer-app .route-detail > summary::-webkit-details-marker { display: none; }
.viewer-app .route-detail > summary::before {
    content: "+";
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border-subtle);
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85rem;
    line-height: 1;
}
.viewer-app .route-detail[open] > summary::before { content: "\2212"; }
.viewer-app .route-detail > summary:hover { color: var(--text-primary); }
.viewer-app .route-detail > summary:hover::before { border-color: var(--text-muted); }

/* Both of these have to beat .dropzone p and .route-card ol/ul, which name an
   element as well as a class and would otherwise win. */
.viewer-app .route-detail ol,
.viewer-app .route-detail ul {
    margin: 0.9rem 0 0 1.15rem;
    font-size: 0.875rem;
    color: var(--text-secondary);
}
.viewer-app .route-detail li { margin-bottom: 0.45rem; }
.viewer-app .route-detail p {
    max-width: none;
    margin: 0.75rem 0 0;
    font-size: 0.8125rem;
    color: var(--text-muted);
    text-align: left;
}
.viewer-app .route-detail code {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85em;
    color: var(--text-primary);
}

/* The drop box sits beside the cable route rather than above it, and is the
   page's own furniture rather than a gate, so it gives back the height it was
   carrying when it was the only thing on the screen. */
.viewer-app .route-grid .dropzone { padding: clamp(1.25rem, 3vw, 1.9rem) 1.25rem; }
.viewer-app .route-grid .dropzone-icon { width: 40px; height: 40px; margin-bottom: 0.75rem; }
/* Tighter again where the lanes stack. Everything above the first button on a
   phone is height the second lane never gets, and the first button is the one
   thing on this screen that has to be reachable without a scroll. */
@media (max-width: 59.999rem) {
    .viewer-app .route-grid .dropzone-icon { width: 30px; height: 30px; margin-bottom: 0.5rem; }
    .viewer-app .dropzone-actions { margin-top: 1rem; }
}

/* ============================================
   EMPTY STATE
   What stands in for the panels before any readings exist. It has to say what
   the page will do, because on arrival there is nothing else to judge it by.
   ============================================ */
.viewer-app .empty-state { margin-bottom: 1.25rem; }
.viewer-app .empty-state h2 {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.25rem, 3.5vw, 1.6rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    margin-bottom: 0.5rem;
}
.viewer-app .empty-state p {
    color: var(--text-secondary);
    font-size: 0.9375rem;
    max-width: 68ch;
}
.viewer-app .empty-ways {
    list-style: none;
    margin: 1.1rem 0 0;
    padding: 0;
    display: grid;
    gap: 0.6rem;
}
.viewer-app .empty-ways li {
    position: relative;
    padding-left: 1.35rem;
    font-size: 0.9375rem;
    line-height: 1.55;
    color: var(--text-secondary);
    max-width: 68ch;
}
.viewer-app .empty-ways li::before {
    content: "";
    position: absolute;
    left: 0.15rem;
    top: 0.62em;
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--accent);
}
.viewer-app .empty-ways strong { color: var(--text-primary); font-weight: 650; }
.viewer-app .empty-ways em { font-style: normal; color: var(--text-primary); font-weight: 600; }

/* The way in for someone with no card to hand. One action on this block, and
   it sits directly under the drop box, so nothing is lost by taking it out of
   the row of three buttons it used to make three deep on a phone. */
.viewer-app .empty-action {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 1.4rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--border-subtle);
    font-size: 0.9375rem;
    color: var(--text-secondary);
}

/* A sketch of the layout that arrives, in hairlines only. It is decoration
   standing in for content, so it is out of the accessibility tree and out of
   the tab order; the list above is what carries the meaning. */
.viewer-app .preview {
    max-width: 44rem;
    margin-top: 1.4rem;
    color: var(--text-muted);
    opacity: 0.5;
}
.viewer-app .preview-tiles {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.6rem;
}
.viewer-app .preview-tiles span {
    display: block;
    height: 46px;
    border: 1px solid var(--line);
}
.viewer-app .preview-chart {
    margin-top: 0.6rem;
    border: 1px solid var(--line);
    height: 84px;
}
.viewer-app .preview-chart svg { display: block; width: 100%; height: 100%; }
.viewer-app .preview-rows { margin-top: 0.6rem; }
.viewer-app .preview-rows span {
    display: block;
    height: 22px;
    border-bottom: 1px solid var(--line);
}

.viewer-app .status-line {
    margin-top: 1rem;
    font-size: 0.875rem;
    color: var(--text-secondary);
    min-height: 1.4em;
}
.viewer-app .status-line.error { color: #ffb4b4; }
.viewer-app .status-line.ok { color: #a6e9c1; }

/* The two lines inside the choice have nothing to report until something has
   been pressed, and a reserved line each is a real slice of a phone screen
   spent on nothing, directly above the fold. They take their space when they
   have something to say. */
.viewer-app #loadStatus:empty,
.viewer-app #serialStatus:empty { display: none; }
/* .dropzone p sets its own margins, and carries an element as well as a class,
   so the status line inside the box needs the box named to keep its gap. */
.viewer-app .dropzone .status-line { margin-top: 1rem; }

/* A browser with no USB API is a fact about the browser. Amber said the reader
   had done something wrong, so this is the page's own hairline panel and the
   copy hands over to the route that works everywhere. */
.viewer-app .note-box {
    margin-top: 1rem;
    padding: 0.85rem 1rem;
    border: 1px solid var(--border-subtle);
    border-left: 3px solid var(--text-muted);
    font-size: 0.875rem;
    color: var(--text-secondary);
}

/* ============================================
   WORKSPACE TOOLBAR + TABS
   ============================================ */
.viewer-app .toolbar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding: 1rem 0 1.25rem;
}

/* The results heading and the controls under it stand for the panels, so while
   the workspace is empty there is nothing for them to name or to control. The
   row of five switched-off controls was the first thing on the page, and the
   remembered tab rendered highlighted and disabled at once, which reads as
   broken. syncEmptyState() already toggles #emptyState.hidden, so this needs
   no script.

   It has to fail safe: where :has() does not match, all three simply stay on
   the page exactly as they did before. */
/* With nothing loaded, hide exactly the controls that need data - the same
   set syncEmptyState disables - rather than the whole bar. The live reading
   and the unit switch both work with an empty workspace, and the live button
   is the only way in for somebody who has no card at all, so hiding the bar
   wholesale removed the page's answer to its own second promise. #liveStatus
   stays for the same reason: it is where the live route writes its result. */
.viewer-app #workspace:has(#emptyState:not([hidden])) > .results-head,
.viewer-app #workspace:has(#emptyState:not([hidden])) > .toolbar > .tabs,
.viewer-app #workspace:has(#emptyState:not([hidden])) > .toolbar > #exportBtn,
.viewer-app #workspace:has(#emptyState:not([hidden])) > .toolbar > #addMoreBtn,
.viewer-app #workspace:has(#emptyState:not([hidden])) > .toolbar > #clearBtn {
    display: none;
}

/* The panels carry a headline, a day strip and a canvas that are all empty
   until data parses, and the script that hides them does not run until the end
   of the body. Without this a cold load paints the empty shell first. */
.viewer-app #workspace:has(#emptyState:not([hidden])) > #panelSimple,
.viewer-app #workspace:has(#emptyState:not([hidden])) > #panelDetail {
    display: none;
}

.viewer-app .results-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
    margin-top: 0.5rem;
}
.viewer-app .results-heading {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.25rem, 3.5vw, 1.6rem);
    font-weight: 700;
    letter-spacing: -0.01em;
    margin: 0;
}
.viewer-app .results-heading:focus { outline: none; }
.viewer-app .results-heading:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* Units, the view switch, Export and Ranges used to scroll away for good the
   moment anyone read their own data. The masthead is z-index 70 and the live
   bar 200, so 30 sits under both and over the cards. It needs its own ground:
   a chart scrolling under a transparent bar looks like a rendering fault.

   Pinned only from 48rem up. Measured at 390px the bar wraps to four rows and
   224px, a quarter of the phone screen given over to chrome for the whole
   scroll, which costs more than scrolling back up. From 48rem it is two rows,
   and one row on a wide screen. */
@media (min-width: 48rem) {
    .viewer-app #workspace > .toolbar {
        position: sticky;
        /* The masthead condenses to 3.25rem once the page is scrolled, and
           redefines --nav-height on itself where :root cannot see it, so
           reading the token here leaves an 8px strip of page sliding between
           the two. Scrolled is the only state where this bar is stuck, so the
           condensed height is the right one; the masthead outranks it on
           z-index and covers the overlap before the page moves. */
        top: 3.25rem;
        z-index: 30;
        padding: 0.7rem 0 0.8rem;
        background: var(--bg-page);
        border-bottom: 1px solid var(--border-subtle);
        margin-bottom: 1.25rem;
    }
}

/* Why the live button is switched off, kept beside it once the empty state
   that used to explain it is gone. A disabled control cannot raise a tooltip,
   so the reason has to be on the page. */
.viewer-app .toolbar-note {
    display: none;
    flex: 1 1 100%;
    order: 11;
    margin: 0;
    font-size: 0.8125rem;
    color: var(--text-muted);
}
.viewer-app #workspace:has(#liveBtn:disabled) .toolbar-note { display: block; }

.viewer-app .toolbar-label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}
.viewer-app .tabs {
    display: inline-flex;
    padding: 4px;
    gap: 4px;
    background: var(--bg-secondary);
    border: 1px solid var(--border-subtle);
    border-radius: 100px;
}
.viewer-app .tab {
    appearance: none;
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font-family: 'DM Sans', sans-serif;
    font-size: 0.9375rem;
    font-weight: 600;
    padding: 0.6rem 1.4rem;
    border-radius: 100px;
    cursor: pointer;
    min-height: 44px;
    transition: background 0.25s, color 0.25s;
}
.viewer-app .tab[aria-selected="true"] {
    background: var(--accent);
    color: #06121a;
}
.viewer-app .tab:hover:not([aria-selected="true"]) { color: var(--text-primary); background: rgba(255,255,255,0.05); }

.viewer-app .toolbar-spacer { flex: 1 1 auto; }

.viewer-app .seg {
    display: inline-flex;
    padding: 3px;
    gap: 3px;
    background: var(--bg-secondary);
    border: 1px solid var(--border-subtle);
    border-radius: 100px;
}
.viewer-app .seg button {
    appearance: none;
    border: 0;
    background: transparent;
    color: var(--text-secondary);
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.78rem;
    font-weight: 600;
    padding: 0.45rem 0.85rem;
    border-radius: 100px;
    cursor: pointer;
    min-height: 44px;
    transition: background 0.2s, color 0.2s;
}
.viewer-app .seg button[aria-pressed="true"] { background: var(--accent); color: #06121a; }
.viewer-app .seg button:hover:not([aria-pressed="true"]) { color: var(--text-primary); }

.viewer-app .dataset-summary {
    font-size: 0.8125rem;
    color: var(--text-muted);
    font-family: 'JetBrains Mono', monospace;
}

/* Nothing sits between the tabs and their panel in the ordinary case, so the
   live region gives its line back until it has something to say. */
.viewer-app #workspace > #liveStatus:empty { display: none; }

/* ============================================
   DETAIL: THE RANGE BAR AND THE JUMP ROW
   The five range buttons govern every card in the panel, so they lead it. The
   readout sits with the control that sets it.
   ============================================ */
.viewer-app .range-bar { padding-top: 0; }
.viewer-app .range-readout {
    flex: 1 1 24ch;
    margin: 0;
    text-align: right;
}

.viewer-app .jump-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
}
.viewer-app .jump-chips a {
    display: inline-flex;
    align-items: center;
    min-height: 34px;
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--border-subtle);
    font-size: 0.8125rem;
    color: var(--text-secondary);
    text-decoration: none;
}
.viewer-app .jump-chips a:hover { border-color: var(--accent); color: var(--text-primary); }

/* The device card is only there for a card written by a newer release, so its
   chip waits for it. Hidden is the safe direction: where :has() does not
   match, the chip stays away rather than pointing at nothing. */
.viewer-app .jump-chips .jump-device { display: none; }
.viewer-app #panelDetail:has(#deviceCard:not([hidden])) .jump-chips .jump-device { display: inline-flex; }

/* Clear of both the masthead and the sticky toolbar, so a jump does not land
   the card's own heading underneath them. */
.viewer-app #panelDetail > .card[id] { scroll-margin-top: calc(var(--nav-height, 3.75rem) + 5.5rem); }

/* Every reading that is loaded, whatever the range is set to. It belongs on
   this card and not under the note promising the range is followed. */
.viewer-app .table-export { margin-left: auto; }

/* ============================================
   SIMPLE VIEW
   ============================================ */
.viewer-app .day-strip {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}
.viewer-app .day-scroll {
    display: flex;
    gap: 0.5rem;
    overflow-x: auto;
    scroll-behavior: smooth;
    padding: 0.25rem;
    flex: 1;
    scrollbar-width: thin;
}
.viewer-app .day-chip {
    flex: 0 0 auto;
    appearance: none;
    border: 1px solid var(--border-subtle);
    background: var(--bg-secondary);
    color: var(--text-secondary);
    border-radius: 12px;
    padding: 0.55rem 0.9rem;
    min-height: 52px;
    min-width: 84px;
    cursor: pointer;
    text-align: center;
    font-family: 'DM Sans', sans-serif;
    transition: border-color 0.2s, background 0.2s, color 0.2s;
}
.viewer-app .day-chip .dow { display: block; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); }
.viewer-app .day-chip .dnum { display: block; font-size: 1rem; font-weight: 700; color: inherit; }
.viewer-app .day-chip .bar { display: block; height: 4px; border-radius: 2px; margin-top: 0.35rem; background: var(--z-range); opacity: 0.75; }
.viewer-app .day-chip[aria-pressed="true"] {
    border-color: var(--accent);
    background: rgba(0,212,255,0.1);
    color: var(--text-primary);
}
.viewer-app .day-chip:hover { border-color: var(--text-muted); color: var(--text-primary); }
.viewer-app .day-nav {
    appearance: none;
    border: 1px solid var(--border-subtle);
    background: var(--bg-secondary);
    color: var(--text-primary);
    width: 44px; height: 44px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 1.1rem;
    flex: 0 0 auto;
}
.viewer-app .day-nav:hover { border-color: var(--accent); }
.viewer-app .day-nav:disabled { opacity: 0.35; cursor: not-allowed; }

.viewer-app .headline {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.5rem, 4.5vw, 2.35rem);
    font-weight: 700;
    line-height: 1.2;
    letter-spacing: -0.02em;
    margin-bottom: 0.5rem;
}
.viewer-app .headline-sub {
    color: var(--text-secondary);
    font-size: 1rem;
}

.viewer-app .zone-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 0.9rem;
    margin-top: 1.5rem;
}
.viewer-app .zone-tile {
    border-radius: var(--radius-sm);
    padding: 1.1rem;
    border: 1px solid var(--tint-border, var(--border-subtle));
    background: var(--tint-bg, var(--bg-secondary));
}
.viewer-app .zone-tile .pct {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.75rem, 5vw, 2.4rem);
    font-weight: 800;
    line-height: 1;
    color: var(--tint, var(--text-primary));
}
.viewer-app .zone-tile .name {
    font-weight: 700;
    font-size: 0.95rem;
    margin-top: 0.4rem;
}
.viewer-app .zone-tile .meta {
    font-size: 0.8125rem;
    color: var(--text-secondary);
    margin-top: 0.2rem;
}

.viewer-app .chart-wrap {
    position: relative;
    width: 100%;
}
.viewer-app canvas.chart {
    display: block;
    width: 100%;
    border-radius: var(--radius-sm);
    background: var(--bg-secondary);
    touch-action: none;
}
.viewer-app canvas#dayChart { height: 300px; }
.viewer-app canvas#timelineChart { height: 340px; cursor: crosshair; }
.viewer-app canvas#patternChart { height: 300px; }

.viewer-app .tooltip {
    position: absolute;
    pointer-events: none;
    z-index: 20;
    background: rgba(10, 14, 23, 0.96);
    border: 1px solid var(--border-accent);
    border-radius: var(--radius-xs);
    padding: 0.5rem 0.7rem;
    font-size: 0.8125rem;
    line-height: 1.45;
    white-space: nowrap;
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
    opacity: 0;
    transition: opacity 0.12s;
}
.viewer-app .tooltip.show { opacity: 1; }
.viewer-app .tooltip .tv { font-family: 'Outfit', sans-serif; font-weight: 700; font-size: 1rem; }
.viewer-app .tooltip .tt { color: var(--text-muted); font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; }

.viewer-app .legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 1.1rem;
    margin-top: 0.85rem;
    font-size: 0.78rem;
    color: var(--text-secondary);
}
.viewer-app .legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
.viewer-app .legend i { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }

.viewer-app .explain { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.25rem; }
.viewer-app .explain h4 {
    font-family: 'Outfit', sans-serif;
    font-size: 1rem;
    font-weight: 700;
    margin-bottom: 0.35rem;
    display: flex; align-items: center; gap: 0.5rem;
}
.viewer-app .explain h4 i { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
.viewer-app .explain p { font-size: 0.9rem; color: var(--text-secondary); }

/* ============================================
   DETAIL VIEW
   ============================================ */
.viewer-app .stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 1px;
    background: var(--border-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    overflow: hidden;
}
.viewer-app .stat {
    background: var(--bg-card);
    padding: 1rem 1.1rem;
}
.viewer-app .stat .label {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.68rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
    display: flex; align-items: center; gap: 0.35rem;
}
.viewer-app .stat .value {
    font-family: 'Outfit', sans-serif;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    margin-top: 0.3rem;
}
.viewer-app .stat .sub { font-size: 0.78rem; color: var(--text-secondary); }

.viewer-app .info-dot {
    display: inline-flex;
    align-items: center; justify-content: center;
    width: 15px; height: 15px;
    border-radius: 50%;
    border: 1px solid var(--text-muted);
    color: var(--text-muted);
    font-size: 0.6rem;
    font-family: 'DM Sans', sans-serif;
    cursor: help;
    flex: 0 0 auto;
}

.viewer-app .tir-row {
    display: grid;
    grid-template-columns: 92px 1fr auto 62px;
    align-items: center;
    gap: 0.75rem;
    padding: 0.3rem 0;
}
.viewer-app .tir-row .alarms { min-width: 0; }
.viewer-app .tir-row .lbl { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-secondary); }
.viewer-app .tir-row .pct { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; text-align: right; color: var(--text-primary); }
.viewer-app .tir-bar {
    display: flex;
    height: 20px;
    border-radius: 5px;
    overflow: hidden;
    background: rgba(255,255,255,0.05);
}
.viewer-app .tir-bar span { display: block; height: 100%; }
.viewer-app .tir-overall .tir-bar { height: 34px; border-radius: 8px; }
.viewer-app .tir-list { max-height: 340px; overflow-y: auto; padding-right: 0.4rem; margin-top: 0.75rem; }

.viewer-app .table-controls {
    display: flex;
    gap: 0.6rem;
    flex-wrap: wrap;
    align-items: center;
    margin-bottom: 0.9rem;
}
.viewer-app input[type="search"],
.viewer-app input[type="number"],
.viewer-app select {
    font-family: 'DM Sans', sans-serif;
    font-size: 0.9rem;
    color: var(--text-primary);
    background: var(--bg-secondary);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xs);
    padding: 0.6rem 0.8rem;
    min-height: 42px;
}
.viewer-app input[type="search"] { min-width: 200px; flex: 1 1 220px; }
.viewer-app select { min-width: 150px; }
.viewer-app input:focus,
.viewer-app select:focus { border-color: var(--accent); outline: none; }

.viewer-app .table-scroll { overflow-x: auto; max-height: 520px; overflow-y: auto; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); }
.viewer-app table { border-collapse: collapse; width: 100%; font-size: 0.875rem; }
.viewer-app thead th {
    position: sticky; top: 0;
    background: var(--bg-tertiary);
    text-align: left;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    padding: 0.6rem 0.9rem;
    border-bottom: 1px solid var(--border-subtle);
    z-index: 2;
}
.viewer-app tbody td { padding: 0.5rem 0.9rem; border-bottom: 1px solid rgba(255,255,255,0.03); }
.viewer-app tbody tr:hover { background: var(--bg-card-hover); }
.viewer-app td.num { font-family: 'JetBrains Mono', monospace; text-align: right; font-weight: 600; }
.viewer-app td.tm { font-family: 'JetBrains Mono', monospace; color: var(--text-secondary); white-space: nowrap; }
.viewer-app .arrow { font-size: 1rem; display: inline-block; width: 1.3em; text-align: center; }

.viewer-app .chip-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; margin-right: 0.45rem; vertical-align: -1px; }

/* the extra columns newer cards carry */
.viewer-app td.trend { white-space: nowrap; }
.viewer-app td.soft { color: var(--text-secondary); white-space: nowrap; }
.viewer-app th[data-extra],
.viewer-app td.soft,
.viewer-app tbody tr td:nth-child(n+4) { padding-left: 0.65rem; padding-right: 0.65rem; }
.viewer-app td.num.soft { font-weight: 500; }
.viewer-app .muted { color: var(--text-muted); }
.viewer-app .tag {
    display: inline-block;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.68rem;
    letter-spacing: 0.04em;
    padding: 0.15rem 0.45rem;
    border-radius: var(--radius-xs);
    white-space: nowrap;
}
.viewer-app .tag-warn { background: rgba(234,179,8,0.14); color: #eab308; border: 1px solid rgba(234,179,8,0.3); }
.viewer-app .tag-alarm { background: rgba(239,68,68,0.14); color: #f87171; border: 1px solid rgba(239,68,68,0.32); }
.viewer-app tbody tr.row-flag td:first-child { box-shadow: inset 3px 0 0 #eab308; }
.viewer-app tbody tr.row-alarm td:first-child { box-shadow: inset 3px 0 0 #ef4444; }
.viewer-app tbody tr.row-flag { background: rgba(234,179,8,0.05); }
.viewer-app tbody tr.row-alarm { background: rgba(239,68,68,0.06); }
.viewer-app .dev-grid .stat .value { font-size: 1.15rem; }

/* ============================================
   DIALOGS
   ============================================ */
.viewer-app dialog {
    margin: auto;                       /* the reset above clears the centring a modal gets for free */
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius);
    background: var(--bg-card);
    color: var(--text-primary);
    padding: 0;
    max-width: min(560px, calc(100vw - 2rem));
    width: 100%;
}
.viewer-app dialog::backdrop { background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); }
.viewer-app .dlg-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 1rem;
    padding: 1.1rem 1.35rem;
    border-bottom: 1px solid var(--border-subtle);
}
.viewer-app .dlg-head h3 { font-family: 'Outfit', sans-serif; font-size: 1.1rem; font-weight: 700; }
.viewer-app .dlg-body { padding: 1.35rem; max-height: 60vh; overflow-y: auto; }
.viewer-app .dlg-foot {
    display: flex; gap: 0.6rem; justify-content: flex-end;
    padding: 1rem 1.35rem;
    border-top: 1px solid var(--border-subtle);
    flex-wrap: wrap;
}
.viewer-app .icon-btn {
    appearance: none;
    border: 1px solid var(--border-subtle);
    background: transparent;
    color: var(--text-secondary);
    width: 36px; height: 36px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 1.05rem;
    line-height: 1;
}
.viewer-app .icon-btn:hover { color: var(--text-primary); border-color: var(--text-muted); }

.viewer-app .field-row {
    display: grid;
    grid-template-columns: 1fr 130px;
    gap: 0.75rem;
    align-items: center;
    padding: 0.5rem 0;
}
.viewer-app .field-row label { font-size: 0.9rem; }
.viewer-app .field-row .hint { font-size: 0.78rem; color: var(--text-muted); }

.viewer-app .file-list { list-style: none; }
.viewer-app .file-list li {
    display: flex; align-items: center; gap: 0.7rem;
    padding: 0.5rem 0.2rem;
    border-bottom: 1px solid rgba(255,255,255,0.04);
    font-size: 0.875rem;
}
.viewer-app .file-list input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--accent); flex: 0 0 auto; }
.viewer-app .file-list .fname { font-family: 'JetBrains Mono', monospace; }
.viewer-app .file-list .fsize { margin-left: auto; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; }
.viewer-app .file-list .fstate { font-size: 0.75rem; color: var(--text-muted); min-width: 5.5em; text-align: right; }
.viewer-app .file-list .fstate.ok { color: #a6e9c1; }
.viewer-app .file-list .fstate.bad { color: #ffb4b4; }

.viewer-app .progress {
    height: 6px;
    border-radius: 3px;
    background: rgba(255,255,255,0.07);
    overflow: hidden;
    margin-top: 0.85rem;
}
.viewer-app .progress i { display: block; height: 100%; width: 0%; background: var(--accent); transition: width 0.15s linear; }

/* ============================================
   HELP + FOOTER
   ============================================ */
/* The two cards have very different amounts to say, and a shared height pads
   the shorter one with most of a card's worth of nothing. */
.viewer-app .help-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    align-items: start;
    gap: 1.25rem;
}
.viewer-app .help-grid h3 {
    font-family: 'Outfit', sans-serif;
    font-size: 1.05rem;
    font-weight: 700;
    margin-bottom: 0.6rem;
}
.viewer-app .help-grid ol,
.viewer-app .help-grid ul { margin-left: 1.15rem; font-size: 0.9rem; color: var(--text-secondary); }
.viewer-app .help-grid li { margin-bottom: 0.45rem; }
.viewer-app .help-grid code {
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.85em;
    background: rgba(255,255,255,0.06);
    padding: 0.1em 0.4em;
    border-radius: 4px;
    color: var(--text-primary);
}

.viewer-app section.block { padding: 2rem 0; }
.viewer-app .section-title {
    font-family: 'Outfit', sans-serif;
    font-size: clamp(1.4rem, 3.5vw, 2rem);
    font-weight: 700;
    letter-spacing: -0.02em;
    margin-bottom: 1.25rem;
}

/* ============================================
   RESPONSIVE
   ============================================ */
@media (max-width: 760px) {
    .viewer-app .toolbar { gap: 0.5rem; }
    .viewer-app .tab { padding: 0.6rem 1rem; font-size: 0.875rem; }
    .viewer-app .tir-row { grid-template-columns: 74px 1fr auto 52px; gap: 0.5rem; }
    .viewer-app canvas#dayChart { height: 240px; }
    .viewer-app canvas#timelineChart { height: 260px; }
    .viewer-app canvas#patternChart { height: 250px; }
    .viewer-app .dataset-summary { width: 100%; order: 10; }
    /* A sentence has no business being squeezed into the end of a wrapped row,
       so the scope readout takes its own line under the buttons that set it. */
    .viewer-app .range-readout { flex: 1 1 100%; order: 10; text-align: left; }
    .viewer-app .table-export { margin-left: 0; }
}
@media (max-width: 480px) {
    .viewer-app .dropzone-actions .btn,
.viewer-app .route-actions .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
    .viewer-app *,
.viewer-app *::before,
.viewer-app *::after {
        animation-duration: 0.001ms !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
}

/* ============================================
   LIVE BAR
   Pinned along the bottom while a device is connected. The script builds it and
   removes it again, so it is simply absent for dropped files and the sample day.
   The page's bottom padding is set to match its height from the script, which is
   what lets the last of the content still scroll clear of it.
   ============================================ */
.live-bar {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 200;                       /* over the sticky nav; modal dialogs sit above both */
    background: rgba(15, 20, 32, 0.97);
    border-top: 1px solid var(--border-accent);
    box-shadow: 0 -8px 26px rgba(0, 0, 0, 0.45);
}
@supports (backdrop-filter: blur(8px)) {
    .live-bar { background: rgba(15, 20, 32, 0.9); backdrop-filter: blur(8px); }
}

.live-inner {
    max-width: var(--container-max);
    margin: 0 auto;
    padding: 0.5rem 1.25rem;
    display: flex;
    align-items: center;
    gap: 0.9rem;
    white-space: nowrap;
}

.lb-read { display: flex; align-items: baseline; gap: 0.3rem; }
.lb-value {
    font-family: 'Outfit', sans-serif;
    font-size: 1.75rem;
    font-weight: 800;
    line-height: 1.1;
    letter-spacing: -0.02em;
}
.lb-value.waiting {
    font-family: 'DM Sans', sans-serif;
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: 0;
    color: var(--text-secondary);
}
.lb-unit { font-size: 0.75rem; color: var(--text-secondary); }
.lb-arrow { font-size: 1.125rem; margin-left: 0.15rem; }

.lb-age,
.lb-batt,
.lb-link,
.lb-src { font-size: 0.8125rem; color: var(--text-secondary); }
.lb-batt { margin-left: auto; }         /* the reading holds the left, the device state the right */
.lb-batt.low { color: var(--z-vlow); }
.lb-link.off { color: var(--text-muted); }
.lb-src { color: var(--text-muted); }

.lb-stop { flex: 0 0 auto; }

/* Narrow screens drop the least useful items outright rather than shrinking
   everything into something nobody can read at a glance. */
@media (max-width: 760px) {
    .live-inner { padding: 0.45rem 1rem; gap: 0.7rem; }
    .lb-rssi,
.lb-src { display: none; }
}
@media (max-width: 460px) {
    .live-inner { gap: 0.55rem; }
    .lb-value { font-size: 1.5rem; }
    .lb-link { display: none; }
    .lb-stop { padding: 0.4rem 0.6rem; min-height: 34px; }
    .lb-stop span { display: none; }
    .lb-stop::before { content: '\2715'; font-size: 0.875rem; }
}

/* ============================================
   EXPORT SHEET
   ============================================ */
.viewer-app #exportDialog { max-width: min(720px, calc(100vw - 2rem)); }
.viewer-app #exportDialog .dlg-body { max-height: 74vh; }
.viewer-app .export-intro {
    font-size: 0.875rem;
    color: var(--text-secondary);
    margin-bottom: 0.9rem;
}
.viewer-app .export-scope {
    padding: 0.85rem 0.95rem;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background: rgba(255,255,255,0.02);
    margin-bottom: 1.1rem;
}
.viewer-app .export-scope .seg { margin-bottom: 0.6rem; }
.viewer-app .export-scope .card-note { line-height: 1.45; }

.viewer-app .export-list { list-style: none; }
.viewer-app .export-list li {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 0.7rem 0;
    border-bottom: 1px solid var(--border-subtle);
}
.viewer-app .export-list li:last-child { border-bottom: 0; }
.viewer-app .export-list .ex-text { flex: 1 1 260px; min-width: 0; }
.viewer-app .export-list .ex-text strong {
    display: block;
    font-family: 'Outfit', sans-serif;
    font-size: 0.9375rem;
    font-weight: 700;
    margin-bottom: 0.1rem;
}
.viewer-app .export-list .ex-text span {
    display: block;
    font-size: 0.8125rem;
    color: var(--text-secondary);
    line-height: 1.4;
}
.viewer-app .export-list .btn { flex: 0 0 auto; margin-left: auto; }

/* The printable report lives outside the app shell and is built on demand. */
.viewer-app #printReport { display: none; }

/* The unit switch on the live surfaces. Smaller than the toolbar's, because the
   chrome it sits in has to leave the reading the rest of the window. */
.lf-units button { padding: 0.35rem 0.7rem; min-height: 32px; font-size: 0.72rem; }
@media (max-width: 480px) {
    .lf-units button { padding: 0.3rem 0.55rem; font-size: 0.68rem; }
    .lf-chrome { gap: 0.45rem; padding: 0.55rem 0.7rem; }
}

/* The bar's unit doubles as the switch: pressing the unit to change the unit
   needs no label, and the bar has no width to give one on a phone. */
.lb-unit-btn {
    appearance: none;
    border: 0;
    background: transparent;
    padding: 0.1rem 0.25rem;
    margin: -0.1rem -0.05rem;
    border-radius: var(--radius-xs);
    cursor: pointer;
    font: inherit;
    color: var(--text-secondary);
    text-decoration: underline dotted;
    text-underline-offset: 0.2em;
}
.lb-unit-btn:hover { color: var(--text-primary); }

/* ── where the readings go ────────────────────────────────────────────────────
   Every claim in here is checkable in the source of this page, which is the
   point: it is worth nothing if it is only a promise.

   It used to sit inside the device lane, open, between what that route does
   and the buttons that open a port, which put a five-point notice in front of
   the second half of the one question the page asks. It is a section of its
   own below the choice now, with a heading, and the one-line badge under the
   two lanes carries the promise itself. The five points are still short and
   still unfolded: a notice that has to be opened first is not a notice.

   Nothing here names .route-card any more, so a bare .privacy-list is enough
   to beat the element-plus-class rules that used to outrank it. */
.viewer-app .privacy-list { list-style: none; margin: 0 0 0.9rem; padding: 0; display: grid; gap: 0.65rem; }
/* Two columns from the width where a bullet still has room to read. The block
   runs the full band now rather than one lane of it, so that width arrives far
   sooner than it used to. */
@media (min-width: 48rem) {
    .viewer-app .privacy-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.65rem 1.25rem; }
}
.viewer-app .privacy-list li {
    position: relative;
    padding-left: 1.35rem;
    font-size: 0.9375rem;
    line-height: 1.55;
    color: var(--text-secondary);
}
.viewer-app .privacy-list li::before {
    content: "";
    position: absolute;
    left: 0.15rem;
    top: 0.62em;
    width: 0.4rem;
    height: 0.4rem;
    border-radius: 50%;
    background: var(--glucose-green);
}
.viewer-app .privacy-list strong { color: var(--text-primary); font-weight: 650; }
.viewer-app .privacy-next {
    font-size: 0.875rem;
    color: var(--text-muted);
    border-top: 1px solid var(--border-subtle);
    padding-top: 0.8rem;
    margin: 0;
}

/* ── full-screen live view ────────────────────────────────────────────────────
   The whole window given over to the one number, for a display sitting across
   the room. Opened from the bar's button, and by itself for a device with no
   card, where there is nothing else on the page to look at.

   Sized in viewport units rather than measured in script: the number has to
   survive a phone turning over and a window being dragged between monitors,
   and a clamp does that without a resize handler that can lag a frame behind. */
.lf-wrap {
    position: fixed;
    inset: 0;
    /* Above the sticky nav (100) and the live bar (200): this covers the page,
       so anything left painted over it would look like a rendering fault. A
       modal dialog still wins, being in the browser's own top layer. */
    z-index: 300;
    display: flex;
    flex-direction: column;
    background: var(--bg-primary);
}
.lf-chrome {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--border-subtle);
}
.lf-brand { font-weight: 800; letter-spacing: -0.02em; font-size: 1.05rem; white-space: nowrap; }
.lf-brand i { color: var(--accent); font-style: normal; }
.lf-brand span {
    font-weight: 600;
    color: var(--text-secondary);
    font-size: 0.72em;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin-left: 0.35rem;
}
.lf-spacer { flex: 1 1 auto; }

.lf-stage {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.lf-readout {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 0;
    padding: 0.4rem 0.75rem;
    overflow: hidden;
}
/* Never allowed to shrink: it is the box the number is measured against. */
.lf-row {
    display: inline-flex;
    align-items: center;
    gap: 0.08em;
    flex: 0 0 auto;
    font-size: clamp(64px, min(34vw, 44vh), 400px);
}
.lf-numcol { display: flex; flex-direction: column; align-items: center; }
.lf-value {
    font-size: 1em;
    line-height: 0.92;
    font-weight: 700;
    letter-spacing: -0.035em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.lf-value.waiting {
    font-size: 0.26em;
    line-height: 1.2;
    font-weight: 650;
    color: var(--text-secondary);
    letter-spacing: 0;
    text-align: center;
    white-space: normal;
    max-width: 8em;
}
.lf-unit {
    font-size: 0.12em;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-secondary);
    margin-top: 0.5em;
}
.lf-arrow { font-size: 0.5em; line-height: 1; color: var(--text-secondary); }

.lf-age {
    flex: 0 0 auto;
    text-align: center;
    color: var(--text-secondary);
    font-weight: 600;
    font-size: 1.05rem;
    padding: 0 0.75rem 0.3rem;
}
.lf-age.old { color: var(--z-low); }

.lf-facts {
    flex: 0 0 auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.45rem 0.55rem;
    padding: 0.65rem 0.75rem calc(0.75rem + env(safe-area-inset-bottom));
    border-top: 1px solid var(--border-subtle);
}
.lf-fact {
    font-size: 0.8125rem;
    color: var(--text-secondary);
    background: var(--bg-secondary);
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    padding: 0.3rem 0.7rem;
    white-space: nowrap;
}

/* A reading nobody should trust is dimmed and stripped of its colour rather
   than left sitting there looking current. */
.lf-stage.lost .lf-row { opacity: 0.32; }
.lf-stage.lost .lf-value { color: var(--text-secondary) !important; }

/* Portrait on a phone: the arrow goes above the number, which buys the number
   about a third more width. Landscape keeps them side by side. */
@media (orientation: portrait) and (max-width: 540px) {
    .lf-row { flex-direction: column-reverse; gap: 0.02em; }
}
@media (max-height: 460px) and (orientation: landscape) {
    .lf-chrome { padding: 0.35rem 0.75rem; }
    .lf-age { padding-bottom: 0.1rem; font-size: 0.9rem; }
    .lf-facts { padding: 0.35rem 0.6rem calc(0.4rem + env(safe-area-inset-bottom)); }
}

/* ============================================
   SITE TREATMENT
   The plates the app shipped with were moulded: a lifted fill, a 16px radius
   and a shadow. The rest of the site draws a hairline and stops, so these do
   too. Only the frame changes — the charts, tiles and tables inside keep the
   contrast they were drawn for.
   ============================================ */
.viewer-app .card {
  background: none;
  border: 1px solid var(--line);
}
.viewer-app .dropzone { background: none; }
.viewer-app .btn-primary:hover { transform: none; box-shadow: none; background: var(--accent-dim); }

@media print {
  /* With a report laid out, only the report prints. The app no longer sits at
     the top of <body> — it is inside the shared <main> — so the rule keeps
     every ancestor of the report and hides everything else, wherever it sits. */
  body.printing-report *:not(:has(#printReport)):not(#printReport):not(#printReport *) {
    display: none !important;
  }
  body.printing-report #printReport { display: block !important; }

  .viewer-app .toolbar, .viewer-app .loader, .viewer-app .table-controls,
  .viewer-app dialog, .live-bar, .lf-wrap { display: none !important; }

  body { padding-bottom: 0 !important; }
  html, body {
    background: #fff !important;
    color: #111827 !important;
    overflow: visible !important;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  @page { size: A4; margin: 14mm; }
}

/* The two day-nav buttons ate 88px of a 320px row and squeezed the strip down
   to a chip and a half. Below 25rem they sit under it instead. */
@media (max-width: 25rem) {
    .viewer-app .day-strip { flex-wrap: wrap; justify-content: flex-end; }
    .viewer-app .day-scroll { flex: 1 0 100%; order: -1; }
    /* Three chips have to fit the strip at 320, so they lose the width they
       were carrying for a wider row. */
    .viewer-app .day-chip { min-width: 62px; padding: 0.5rem 0.4rem; }
}

/* ============================================
   THE OTHER TOOLS

   The card shape is the site's own primitive, and the shared sheet supplies
   its look and its --tool colour. What it does not supply on this page is the
   layout, because the two sheets that carry it are loaded only for the home
   page and the guide pages. So the layout is here, scoped, and the appearance
   stays where it belongs.

   All four tools are one shape. One of them spent a while in a smaller
   treatment of its own, and being the odd one out made it the one nobody
   noticed. A rule above the band sets it off from the workspace, so it reads
   as somewhere to go next rather than as another panel of the tool.
   ============================================ */
.viewer-app #toolsSection {
    border-top: 1px solid var(--line);
    margin-top: 1rem;
}
.viewer-app #toolsSection .tool-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 1.25rem;
}
.viewer-app #toolsSection .tool-head {
    font-family: 'Outfit', sans-serif;
    font-size: 1.15rem;
    font-weight: 700;
    line-height: 1.25;
    margin: 0;
}
.viewer-app #toolsSection .tool-body {
    margin: 0;
    font-size: 0.9rem;
    color: var(--text-secondary);
    max-width: 52ch;
}
.viewer-app #toolsSection .tool-actions { margin: auto 0 0; padding-top: 0.9rem; }

/* Four cards, so the wide layout is two rows of two rather than a row of
   three with one left over on its own. */
@media (min-width: 48rem) {
    .viewer-app #toolsSection .tool-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 75rem) {
    .viewer-app #toolsSection .tool-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
