/*
 * Torvi — the app shell: the navigation panel that slides in behind the hamburger.
 *
 * WHY THIS IS A SHARED FILE
 *
 * Every page carries its own copy of `.sidebar { ... height:100vh }`, and the mobile media
 * query on every page repeats it a second time. That is twenty-odd copies of one rule, which
 * is why Sign out was unreachable on all of them at once rather than on one. Same argument as
 * form-inputs.css: a fix that has to hold everywhere belongs in one file.
 *
 * WHAT WAS WRONG
 *
 * `100vh` is not the height of what you can see. Measured inside the iOS app on an iPhone 17
 * Pro, where the visible web view is 812px tall:
 *
 *     100vh = 874    100svh = 874    100lvh = 874    100dvh = 812
 *     window.innerHeight = 812        visualViewport.height = 812
 *
 * 874 is the FULL DEVICE height. The web view is inset from it by 62px, because
 * capacitor.config.json sets `contentInset: "always"` so the page clears the status bar and
 * the home indicator. vh, svh and lvh all ignore that inset; only dvh tracks it.
 *
 * So the panel was 874px tall inside an 812px window, and its bottom 62px hung off the screen
 * with no way to reach it. Sign out is 37px tall and sat at y=811..848: one pixel of it was
 * visible at the very bottom edge and the rest was past it.
 *
 * SCROLLING COULD NOT SAVE IT, which is the part that makes this read as "the button is
 * missing" rather than "the panel is too tall". `.sidebar` has overflow-y:auto, so it does
 * scroll -- but scrolling moves content WITHIN a box whose own bottom edge is off-screen.
 * The last 62px of that box can never be brought into view no matter how far you scroll, so
 * the button stays hidden and the panel looks like it simply ends above it.
 *
 * Applied at every width, not just phone widths: the app ships to iPad
 * (TARGETED_DEVICE_FAMILY "1,2"), where the layout is wide enough to use the desktop sticky
 * sidebar and the inset applies just the same.
 */

/*
 * !important, for the same reason form-inputs.css needs it: this rule loses on SPECIFICITY,
 * not on source order. index.html writes `#dashboard .sidebar` for its mobile panel, and an
 * id beats a bare class however late this file is linked -- so the fix landed on nineteen
 * pages and skipped the Dashboard, which is the page you are most likely to open the menu on.
 * A panel whose bottom is unreachable is broken regardless of what a page meant it to be.
 */
.sidebar {
  /* The plain vh first, so a browser without dvh still gets something sensible. */
  height: 100vh !important;
  height: 100dvh !important;
}

/* ---- ONE PAGE WIDTH, FOR EVERY PAGE IN THE PRODUCT (5 Oct) -------------------------------
 *
 * THIRTY PAGES HAD THIRTY OPINIONS. Each copied `.main { padding:40px 48px; max-width:Npx }`
 * into its own <style> block and then drifted: 680 on My profile, 820 on Availability, 920 on
 * the Training pages and the contractor's, 1000, 1080, 1100, 1180, and 1300 on Schedule. On a
 * wide monitor that reads as a different product per page -- Settings using a third of the
 * screen while the roster beside it uses all of it -- and no amount of looking at one page
 * tells you it is the narrow one.
 *
 * SCHEDULE IS THE STANDARD, because it is the page the product is really used on and the one
 * width nobody complained about. 1300px, stated ONCE, here. The number is deliberately a
 * literal rather than a custom property: palettecheck owns var(--token) resolution and a
 * layout token living outside the palette is a second place to look for the same kind of
 * answer. One number, one file, one line.
 *
 * WHY IT CAN LIVE HERE AND BEAT THE PAGES. Every app page links this file AFTER its own
 * <style>, so an equal-specificity rule wins on source order -- the argument the block below
 * makes at length. The pages' own declarations are gone all the same: an override that is only
 * beaten by load order is one that comes back the day somebody moves a <link>, and
 * tools/pagewidthcheck.py refuses a page that writes a width for .main at all.
 *
 * NOT ON PHONES. The @media (max-width: 900px) block further down writes
 * `body[data-torvi-shell] .main` -- an attribute, a class and a type -- which outranks this
 * bare class at any source position, so the phone's own padding and max-width:100% still hold.
 * That is checked rather than assumed: tools/mobilecheck.py renders every page at 390.
 *
 * READING WIDTH IS A DIFFERENT QUESTION. Lesson prose in the learner's player keeps a
 * comfortable measure (~72 characters) INSIDE this full-width page -- see .t-lesson-prose in
 * training-2026-10-01.css. A page is as wide as the screen; a paragraph is as wide as an eye
 * can track.
 */
.main {
  padding: 40px 48px;
  max-width: 1300px;
  /*
   * AND THE RULE THAT STOPS A WIDE TABLE SCROLLING THE WHOLE PAGE. #app is a grid and .main is one
   * of its items; a grid item defaults to min-width:auto, which means "never narrower than my
   * content" -- so one table wider than the column pushed the entire page sideways, sidebar and
   * all, however well the table's own scroller behaved.
   *
   * This lived in training-2026-10-01.css, where its comment said a layout change reaching forty
   * other pages was not one to make on the way past. This pass IS that change: every page gets it,
   * because every page has the same grid and the same problem.
   */
  min-width: 0;
}

/* ---- The safe area, and the height that tracks it ---------------------------------------
 *
 * WHY THESE ARE HERE AND NOT ON THE PAGES. The same argument as the .sidebar rule above: one
 * rule that has to hold on twenty-four pages, where twenty-four copies is how twenty-three
 * get fixed. Every app page links this file AFTER its own <style>, so an equal-specificity
 * rule here wins on source order -- except where it does not, which is why !important appears
 * below with a reason each time.
 *
 * TWO DIFFERENT BUGS, PULLING OPPOSITE WAYS. Getting one right breaks the other unless both
 * are held in mind at once:
 *
 *   IN THE NATIVE APP, capacitor.config.json sets contentInset:"always". Without cover, that
 *   insets the WebView and env(safe-area-inset-*) reads 0 -- and what was wrong was vh:
 *   measured on an iPhone 17 Pro, 100vh = 874 while the visible view was 812, so every page
 *   was 62px taller than its viewport, with dead scroll at the bottom. dvh tracks the view.
 *
 *   WITH viewport-fit=cover THE APP GOES EDGE TO EDGE TOO, and env() reports the real inset
 *   (about 62pt at the top of that phone). The first version of this block assumed the app
 *   would stay at 0 and inset only the left, right and bottom; the simulator showed the
 *   drawer's wordmark printed over the clock and, on every page still carrying its own
 *   `padding-top: 70px`, content behind a hamburger that had correctly moved down to clear
 *   the status bar. So everything anchored to the top of the screen reads the top inset here:
 *   the fixed chrome, the drawer, and the 70px strip the pages reserve for that chrome.
 *
 *   IN A HOME-SCREEN PWA ON iOS, the page runs UNDER the status bar -- 25 pages declare
 *   apple-mobile-web-app-status-bar-style=black-translucent and manifest.json is display:
 *   standalone -- and env() returns 0 unless viewport-fit=cover is declared. So the inset is
 *   real, and nothing could read it. That is why the floating hamburger at top:14px and the
 *   bell at top:18px sit under the clock.
 *
 * viewport-fit=cover is what enables env(), and it is also why the HORIZONTAL insets below
 * are not optional. In landscape on a notched iPhone, cover lets content under the notch;
 * without cover Safari was insetting it for us. Fixing the top alone would have traded a
 * standalone bug for a landscape one.
 *
 * max() rather than a bare env() for positions and padding: env() is 0 on the desktop and on
 * Android, and a bare env() would collapse spacing these layouts need. The literal is the floor
 * and the inset only ever raises it. The one exception is the 70px strip, which is ADDED to the
 * inset rather than maxed with it -- the strip exists to hold the chrome, and the chrome itself
 * has moved down by the inset.
 */

/* The page containers. !important because a page writes these as #authGate / #app -- one id,
   which this file's bare selectors tie on, and a tie is decided by whichever stylesheet the
   page happened to link last. */
#authGate, #app, #dashboard {       /* #dashboard: the signed-in half of the sign-in page */
  min-height: 100vh !important;      /* a browser without dvh still gets something sensible */
  min-height: 100dvh !important;
}

/* The fixed chrome clears the status bar and the notch.
   !important on #notifBell because its rule is injected into <head> BY THE PAGE AT RUNTIME,
   after this file is linked -- so on source order the injected copy wins, and only weight
   beats it. Nineteen pages inject an identical copy; this is the one place that can correct
   all of them at once. */
.hamburger-btn {
  top: max(14px, env(safe-area-inset-top)) !important;
  left: max(14px, env(safe-area-inset-left)) !important;
}
#notifBell {
  top: max(18px, env(safe-area-inset-top)) !important;
  right: max(24px, env(safe-area-inset-right)) !important;
}
/* The panel hangs off the bell, so it has to move by the same amount. 48px is the bell's
   42px plus the 6px gap the original 66px-under-18px left. */
#notifPanel {
  top: max(66px, calc(env(safe-area-inset-top) + 48px)) !important;
  right: max(24px, env(safe-area-inset-right)) !important;
}

/* The drawer covers the whole screen on a phone, so its own padding is what keeps the wordmark
   out from under the clock, Sign out off the home indicator, and the nav out from under the
   notch. The base rule is a `padding: 26px 20px` shorthand; these longhands sit in a later
   sheet and win. */
.sidebar {
  padding-top: max(26px, env(safe-area-inset-top)) !important;
  padding-left: max(20px, env(safe-area-inset-left)) !important;
  padding-right: max(20px, env(safe-area-inset-right)) !important;
  padding-bottom: max(26px, env(safe-area-inset-bottom)) !important;
}
.sidebar-close {
  top: max(12px, env(safe-area-inset-top)) !important;
  right: max(12px, env(safe-area-inset-right)) !important;
}

/* Page content clears the chrome, and the side insets in landscape. Only at phone widths: the
   desktop layout has a 250px sidebar in the way of the left inset and 48px of padding of its own.
   !important because the page's own mobile rule is `padding: 70px 16px 28px !important` -- a
   shorthand carrying weight, so a longhand has to carry it too to win.

   The top is 70px PLUS the inset, not the larger of them: the 70px is the strip that holds the
   hamburger and the bell, and they have moved down by the inset themselves. Messages has no
   .main -- its layout reserves the same strip on #msgTabLayout -- so it is named here too. */
@media (max-width: 900px) {
  .main {
    padding-top: calc(70px + env(safe-area-inset-top)) !important;
    padding-left: max(16px, env(safe-area-inset-left)) !important;
    padding-right: max(16px, env(safe-area-inset-right)) !important;
  }
  #msgTabLayout { padding-top: calc(70px + env(safe-area-inset-top)) !important; }
}

/* ---- The segmented control -------------------------------------------------------------
 *
 * One pill holding a set of mutually exclusive views, with the current one filled. It was
 * written once in schedule.html for List / Week / Month, and the two dashboards had a
 * different thing entirely -- a ghost <a> beside a filled <span>, two elements of different
 * heights with different baselines that happened to sit next to each other.
 *
 * Here rather than in a page so "the same control" is literally the same rule. Buttons and
 * anchors both, because one of these navigates and the other switches a view in place, and
 * they have to be indistinguishable to look at.
 */
.view-toggle { display:inline-flex; background:var(--surface-2); border-radius:9px; padding:3px; }
.view-toggle-btn {
  border:none; background:none; padding:7px 14px;
  font-family:'Sora',sans-serif; font-weight:600; font-size:12px;
  color:var(--muted); border-radius:7px; cursor:pointer;
  /* An <a> is not a button: it needs the line-height pinned or it sits a pixel high, and
     text-decoration removed or it underlines inside a filled pill. */
  text-decoration:none; line-height:20px; display:inline-flex; align-items:center;
}
.view-toggle-btn.active { background:var(--card); color:var(--heading); box-shadow:0 1px 3px rgba(11,22,66,0.1); }

/* ---- Dashboard tiles and chart captions ------------------------------------------------
 *
 * Both dashboards draw the same shapes: a row of KPI tiles with a number, a delta and a
 * sparkline, and a one-line caption under every chart saying what it was drawn from. Written
 * once for the Financial pass and kept in that page; here now, because the Operational
 * dashboard needs the identical thing and a second copy is two pages drifting apart.
 */
  .row-4 { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:14px; }
  /* A KPI tile: the number, how it moved, and the shape of the last six periods. */
  .kpi { background:var(--panel); border:1px solid var(--surface-2); border-radius:var(--radius);
         padding:16px 18px; }
  .kpi-label { font-size:12px; font-weight:600; color:var(--muted); }
  .kpi-value { font-family:'Sora',sans-serif; font-size:24px; font-weight:700; margin:4px 0 2px;
               font-variant-numeric:tabular-nums; }
  .kpi-sub { font-size:11.5px; color:var(--muted); }
  .kpi-delta { font-size:12px; font-weight:600; }
  .kpi-spark { margin-top:8px; height:28px; }
  /* A caption under every chart, saying what it was drawn from. A chart whose source is not
     stated is a number somebody will quote without knowing what it counts. */
  .chart-caption { font-size:11.5px; color:var(--muted); margin:8px 0 0; }
  @media (max-width: 900px) {
    .row-4 { grid-template-columns:repeat(2, minmax(0,1fr)); }
  }
  @media (max-width: 560px) {
    .row-4 { grid-template-columns:minmax(0,1fr); }
  }

  /* ── Searchable select ──────────────────────────────────────────────────────────────
   *
   * A <select> you can type into. The native control stays in the DOM as the value holder;
   * what follows dresses a button and a floating panel over the top of it.
   *
   * THE NATIVE SELECT IS NOT display:none. Three of these are `required`, and a form whose
   * invalid control is display:none gives Chrome "an invalid form control is not focusable"
   * -- the submit silently does nothing, with no message anywhere on the page, because the
   * browser cannot scroll to a control that is not rendered. Clipped to one transparent
   * pixel instead: laid out, focusable, validated, and invisible.
   */
  .ts-wrap { position:relative; display:block; max-width:100%; }
  .ts-native {
    position:absolute; left:12px; bottom:0; width:1px; height:1px;
    opacity:0; padding:0; margin:0; border:0; pointer-events:none;
  }
  /* The wrap carries the minimum width the script computes from the widest option label; the
     field fills it. max-width keeps a stretched dialog field from being pushed past its
     container, since a minimum set in pixels would otherwise win over the layout. */
  .ts-field {
    display:flex; align-items:center; justify-content:space-between; gap:8px;
    width:100%; max-width:100%; box-sizing:border-box; min-height:42px; padding:9px 12px;
    font:inherit; font-size:14px; text-align:left; color:var(--ink);
    background:var(--card); border:1px solid var(--control-edge);
    border-radius:10px; cursor:pointer;
  }
  .ts-field:focus-visible { outline:2px solid var(--teal); outline-offset:1px; }
  .ts-value { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .ts-caret { color:var(--muted); font-size:11px; flex:0 0 auto; }
  .ts-disabled .ts-field { background:var(--surface-2); color:var(--muted); cursor:not-allowed; }

  /* position:fixed, and that is the point. These selects sit inside dialogs and inside
   * scrolling cards, both of which clip their overflow -- an absolutely positioned list of
   * three hundred staff would be cut off at the dialog's edge. Fixed escapes every ancestor's
   * clipping; the script keeps it under the field as the page scrolls. */
  .ts-panel {
    position:fixed; z-index:1200; display:flex; flex-direction:column;
    background:var(--card); border:1px solid var(--control-edge); border-radius:10px;
    box-shadow:0 12px 32px rgba(11,22,66,0.18); overflow:hidden;
  }
  .ts-search { padding:8px; border-bottom:1px solid var(--line); flex:0 0 auto; }
  .ts-input {
    width:100%; box-sizing:border-box; padding:8px 10px; font:inherit; font-size:14px;
    color:var(--ink); background:var(--paper); border:1px solid var(--control-edge);
    border-radius:8px;
  }
  .ts-input:focus { outline:2px solid var(--teal); outline-offset:-1px; }
  .ts-list { overflow-y:auto; overscroll-behavior:contain; padding:4px; flex:1 1 auto; }
  .ts-option {
    padding:9px 10px; font-size:14px; color:var(--ink); border-radius:8px;
    cursor:pointer; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .ts-option.ts-active { background:var(--teal-tint); color:var(--teal-text); }
  .ts-option.ts-selected { font-weight:600; }
  .ts-group {
    padding:8px 10px 4px; font-size:11px; font-weight:700; letter-spacing:0.04em;
    text-transform:uppercase; color:var(--muted);
  }
  .ts-empty { padding:14px 10px; font-size:13px; color:var(--muted); text-align:center; }

  /* On a phone the panel is the full width of the screen and the rows are thumb-sized. A
   * roster is filled in a car park between visits, one-handed. */
  @media (max-width: 560px) {
    .ts-panel { left:8px !important; right:8px; width:auto !important; }
    .ts-option { padding:13px 12px; font-size:15px; }
    .ts-input { padding:11px 12px; font-size:16px; }
  }

/* ---- The shell's chrome ----------------------------------------------------------------
 *
 * Drawn by app-shell.js on any page whose <body> carries data-torvi-shell, and styled here so
 * it is styled once. Every rule below is scoped to that marker: a page that has not migrated
 * still carries its own copy of all of this in its inline <style>, and must not have it
 * restyled from under it by a file it happens to link.
 *
 * THE LAYER ORDER, which is the reason this block exists as much as the deduplication is:
 *
 *     page content                 < 900
 *     chrome: hamburger, bell      900
 *     scrim behind the drawer      940
 *     drawer                       950
 *     modal dialogs               1100
 *     searchable-select panel     1200   (opens inside dialogs, so it must clear them)
 *
 * Before this, the hamburger was 1001 and the bell 1000 against .modal-backdrop at 10, so both
 * painted over every dialog in the app; and the bell tied the open drawer at 1000 and won on DOM
 * order, so it floated on top of the navigation it should have been hidden behind.
 * tools/shellcheck.py holds this order on every shell-managed page.
 */

body[data-torvi-shell] .hamburger-btn {
  display: none; position: fixed; z-index: 900;
  top: max(14px, env(safe-area-inset-top)); left: max(14px, env(safe-area-inset-left));
  width: 42px; height: 42px; border: none; border-radius: 10px; cursor: pointer;
  background: var(--navy); color: var(--card); font-size: 19px;
  align-items: center; justify-content: center;
  box-shadow: 0 2px 10px rgba(11, 22, 66, 0.18);
}
body[data-torvi-shell] .sidebar-close { display: none; }
body[data-torvi-shell] .sidebar-overlay {
  display: none; position: fixed; inset: 0; z-index: 940; background: rgba(11, 22, 66, 0.82);
}
body[data-torvi-shell] .sidebar-overlay.mobile-open { display: block; }

/* A dialog is above the chrome, whatever number the page gave it. Specificity, not source
   order: body[attr] .class outranks the page's bare .modal-backdrop wherever it is written. */
body[data-torvi-shell] .modal-backdrop { z-index: 1100; }

@media (max-width: 900px) {
  /* While the drawer is open: the page behind it does not scroll, the hamburger does not sit on
     the panel it opened, and the page content is hidden outright -- a translucent scrim left
     dark-on-dark text legible through it, which read as the menu bleeding into the page.
     Scoped to this width so a rotate to a wider viewport can never leave .main hidden. */
  body[data-torvi-shell].menu-open { overflow: hidden; }
  body[data-torvi-shell].menu-open .hamburger-btn { display: none; }
  body[data-torvi-shell].menu-open .main { visibility: hidden; }

  /* minmax(0, ...) and min-width:0 together: a bare 1fr is minmax(auto, 1fr), and a grid item
     defaults to min-width:auto, so one child wider than the phone widens the column and the
     whole page scrolls sideways. tools/gridcheck.py. */
  body[data-torvi-shell] #app { grid-template-columns: minmax(0, 1fr); }
  body[data-torvi-shell] .main {
    min-width: 0; max-width: 100%;
    padding: 70px max(16px, env(safe-area-inset-right)) 28px max(16px, env(safe-area-inset-left));
    /* The strip at the top is what keeps the fixed hamburger and bell off the page's own
       content -- and it has to grow with the status bar in a home-screen app, or the chrome
       moves down and the content does not. */
    padding-top: calc(70px + env(safe-area-inset-top));
  }

  body[data-torvi-shell] .hamburger-btn { display: flex; }
  body[data-torvi-shell] .sidebar {
    position: fixed; top: 0; left: -100%; width: 100%; z-index: 950;
    transition: left 0.25s ease; box-shadow: 2px 0 24px rgba(0, 0, 0, 0.2);
  }
  body[data-torvi-shell] .sidebar.mobile-open { left: 0; }
  /* The drawer covers the whole screen, so there is no scrim left to tap: this is the only way
     back out besides choosing a destination. */
  body[data-torvi-shell] .sidebar-close {
    display: flex; position: absolute;
    top: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right));
    width: 42px; height: 42px; align-items: center; justify-content: center;
    background: rgba(255, 255, 255, 0.12); color: var(--card);
    border: none; border-radius: 10px; font-size: 24px; line-height: 1; cursor: pointer;
  }
}

/* ---- The bell ----------------------------------------------------------------------------
 * The palette's own tokens where the nineteen injected copies wrote hexes out. Three are exact
 * (navy, the badge red, the link blue); the border, secondary text and the two row tints move
 * to their nearest tokens, which is a deliberate and very small shift toward the palette.
 */
body[data-torvi-shell] #notifBell {
  position: fixed; z-index: 900;
  top: max(18px, env(safe-area-inset-top)); right: max(24px, env(safe-area-inset-right));
  width: 42px; height: 42px; padding: 0; border-radius: 50%; cursor: pointer;
  background: var(--card); border: 1px solid var(--line);
  box-shadow: 0 2px 8px rgba(11, 22, 66, 0.08);
  display: flex; align-items: center; justify-content: center;
  font: inherit; font-size: 18px; line-height: 1;   /* a <button> now, so undo its own font */
}
body[data-torvi-shell] #notifBellBadge {
  position: absolute; top: -3px; right: -3px; display: none;
  min-width: 17px; height: 17px; padding: 0 4px; border-radius: 20px;
  background: var(--danger); color: var(--card); font-size: 10px; font-weight: 700;
  align-items: center; justify-content: center;
}
body[data-torvi-shell] #notifPanel {
  position: fixed; z-index: 900; display: none;
  top: max(66px, calc(env(safe-area-inset-top) + 48px)); right: max(24px, env(safe-area-inset-right));
  width: 340px; max-width: calc(100vw - 32px); max-height: 420px; overflow-y: auto;
  background: var(--card); border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 8px 24px rgba(11, 22, 66, 0.14);
}
body[data-torvi-shell] #notifPanel.active { display: block; }
body[data-torvi-shell] .notif-header {
  padding: 14px 16px; border-bottom: 1px solid var(--surface-2);
  display: flex; align-items: center; justify-content: space-between;
  font-family: 'Sora', sans-serif; font-weight: 700; font-size: 14px; color: var(--heading);
}
body[data-torvi-shell] .notif-header button {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-family: 'Inter', system-ui, sans-serif;
  font-size: 11.5px; font-weight: 600; color: var(--status-progress);
}
body[data-torvi-shell] .notif-item {
  padding: 12px 16px; border-bottom: 1px solid var(--surface-2); cursor: pointer; font-size: 12.5px;
}
body[data-torvi-shell] .notif-item:hover,
body[data-torvi-shell] .notif-item:focus-visible { background: var(--paper); outline: 2px solid var(--teal); outline-offset: -2px; }
body[data-torvi-shell] .notif-item.unread { background: var(--status-progress-bg); }
body[data-torvi-shell] .notif-item b { display: block; font-size: 13px; color: var(--heading); margin-bottom: 2px; }
body[data-torvi-shell] .notif-item .notif-body { color: var(--muted); }
body[data-torvi-shell] .notif-item .notif-time { color: var(--muted); font-size: 11px; margin-top: 4px; }
body[data-torvi-shell] .notif-empty { padding: 30px 16px; text-align: center; color: var(--muted); font-size: 12.5px; }

/* ---- The phone shell: tab bar and the shared pieces every phone screen is made of ---------
 *
 * Eight components, one copy each, per the mobile spec (§3): the tab bar, screen header, card,
 * list row, chip, sheet, button and empty state, plus skeleton and toast. Every rule is scoped
 * to a shell page at phone width -- desktop is out of scope and untouched.
 *
 * LAYERS, extended from the chrome block above:
 *     content < chrome and tab bar 900 < scrim 940 < drawer 950 < sheet scrim 990 < sheet 1000
 *             < dialog 1100 < toast 1150 < select panel 1200
 * The drawer sits over the tab bar because, until the More screen exists, More IS the drawer.
 */
@media (max-width: 900px) {
  /* The tab bar replaces the hamburger on a phone; More reaches everything the drawer did. */
  html[data-tabs] body[data-torvi-shell] .hamburger-btn { display: none; }
  /* Nothing is ever behind the tab bar: the page ends above it. */
  html[data-tabs] body[data-torvi-shell] .main {
    padding-bottom: calc(80px + max(18px, env(safe-area-inset-bottom)));
  }

  body[data-torvi-shell] .shell-tabs {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 900;
    display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
    padding: 10px max(6px, env(safe-area-inset-right)) max(18px, env(safe-area-inset-bottom)) max(6px, env(safe-area-inset-left));
    background: var(--navy); box-shadow: 0 -6px 18px rgba(11, 22, 66, 0.12);
  }
  /* Out of the way while typing, and while a sheet holds the bottom of the screen. */
  /* data-keyboard-open is Messages' own measurement of the same thing; honoured too, so the two
     can never disagree about whether the keyboard is up on the one page that has both. */
  html[data-keyboard="up"] body[data-torvi-shell] .shell-tabs,
  html[data-keyboard-open] body[data-torvi-shell] .shell-tabs,
  html[data-sheet-open] body[data-torvi-shell] .shell-tabs { display: none; }

  /*
   * ── A DIALOG STAYS ABOVE THE KEYBOARD (7 Oct) ───────────────────────────────────────────
   *
   * Reported from an iPhone: tapping somebody to message opens New message, and the screen is
   * behind the keyboard -- recipient list, text box and Send all under it.
   *
   * iOS does not resize the web view when the keyboard opens. `position: fixed; inset: 0` is the
   * LAYOUT viewport, which is still the whole screen, so a dialog centred in it has its lower
   * half behind the keyboard, and `max-height: 85vh` measures the same full height. The visual
   * viewport is the part still visible; app-shell.js publishes it as --vv-height.
   *
   * So while the keyboard is up, every dialog is sized and placed in the visible part instead,
   * and pinned to the top of it rather than centred -- centring inside 380px of space puts a tall
   * dialog's buttons off both ends. Written once, here, because every page in the product has
   * dialogs with fields in them and the next one would have had the same bug.
   *
   * Specificity beats a page's own `.modal-backdrop` (0,1,0) without !important: these are (0,2,1).
   */
  html[data-keyboard="up"] .modal-backdrop,
  html[data-keyboard-open] .modal-backdrop {
    height: var(--vv-height, 100%);
    align-items: flex-start;
    padding-top: 10px; padding-bottom: 10px;
    overflow-y: auto;
  }
  html[data-keyboard="up"] .modal-backdrop > .modal,
  html[data-keyboard-open] .modal-backdrop > .modal {
    max-height: calc(var(--vv-height, 100vh) - 20px);
    margin: 0 auto;
  }
  /*
   * ── AND A DIALOG YOU TYPE INTO KEEPS THE TYPING IN VIEW (8 Oct, item 6) ─────────────────
   *
   * The rule above gets a dialog into the visible rectangle and lets it scroll, which is right
   * for a form. It is not enough for a COMPOSER: New SMS and New group broadcast are a picker
   * with a text box under it, so with the keyboard up the box and Send were below the fold --
   * present, correct, and reached by scrolling, which is exactly how both were reported.
   *
   * A composer is three parts instead: the To row and title stay put, the picker takes whatever
   * height is left and scrolls inside itself, and the composer is pinned beneath it. The backdrop
   * stops scrolling so there is only ever one thing that moves.
   *
   * minmax/min-height:0 on the scrolling part, for the reason messages.html already documents one
   * axis over: a flex item's default min-height is auto, which refuses to shrink below its
   * content -- so the middle would keep its full height and push the composer back off the
   * bottom, which reads as the rule never having applied.
   */
  html[data-keyboard="up"] .modal-backdrop:has(> .modal-compose),
  html[data-keyboard-open] .modal-backdrop:has(> .modal-compose) { overflow-y: hidden; }
  html[data-keyboard="up"] .modal-backdrop > .modal-compose,
  html[data-keyboard-open] .modal-backdrop > .modal-compose {
    display: flex; flex-direction: column;
    height: calc(var(--vv-height, 100vh) - 20px);
  }
  html[data-keyboard="up"] .modal-compose > .modal-scroll,
  html[data-keyboard-open] .modal-compose > .modal-scroll {
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
  }
  html[data-keyboard="up"] .modal-compose > .modal-foot,
  html[data-keyboard-open] .modal-compose > .modal-foot { flex: 0 0 auto; }
  html[data-keyboard="up"] .modal-compose > .to-chips,
  html[data-keyboard-open] .modal-compose > .to-chips { flex: 0 0 auto; }
  /* The composer's own box stays a sensible size inside 380px of screen. */
  html[data-keyboard="up"] .modal-compose > .modal-foot textarea,
  html[data-keyboard-open] .modal-compose > .modal-foot textarea { min-height: 56px; max-height: 26vh; }

  body[data-torvi-shell] .shell-tab {
    position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; min-height: 44px; padding: 0; border: 0; background: none; cursor: pointer;
    color: var(--navy-text); text-decoration: none; font: inherit;
    -webkit-tap-highlight-color: transparent;
  }
  body[data-torvi-shell] .shell-tab svg {
    width: 23px; height: 23px; fill: none; stroke: currentColor; stroke-width: 1.8;
    stroke-linecap: round; stroke-linejoin: round;
  }
  body[data-torvi-shell] .shell-tab-label { font-size: 10px; font-weight: 600; letter-spacing: 0.01em; }
  body[data-torvi-shell] .shell-tab.active { color: var(--teal-bright); }
  body[data-torvi-shell] .shell-tab.active svg { filter: drop-shadow(0 0 6px rgba(45, 212, 191, 0.55)); }
  body[data-torvi-shell] .shell-tab:focus-visible { outline: 2px solid var(--teal-bright); outline-offset: -2px; border-radius: 12px; }
  /* Locked by plan: present, dimmed, and a small lock -- never absent. */
  body[data-torvi-shell] .shell-tab.locked { opacity: 0.5; }
  body[data-torvi-shell] .shell-tab-lock {
    position: absolute; top: 2px; right: calc(50% - 20px); width: 9px; height: 7px;
    border-radius: 2px; background: var(--navy-text);
  }
  body[data-torvi-shell] .shell-tab-lock::before {
    content: ''; position: absolute; left: 1.5px; top: -5px; width: 6px; height: 6px;
    border: 1.5px solid var(--navy-text); border-bottom: 0; border-radius: 4px 4px 0 0; box-sizing: border-box;
  }
  /* A count is text, so it takes the red that white is legible on: --danger carries white at
     3.91:1, --danger-button at 5.73. */
  body[data-torvi-shell] .shell-tab-badge {
    position: absolute; top: 0; left: calc(50% + 6px); min-width: 17px; height: 17px; padding: 0 5px;
    border-radius: 999px; background: var(--danger-button); color: var(--on-fill);
    font-size: 10px; font-weight: 700; line-height: 17px; text-align: center; box-sizing: border-box;
  }
  body[data-torvi-shell] .shell-tab-badge[hidden] { display: none; }
}
@media (min-width: 901px) {
  body[data-torvi-shell] .shell-tabs { display: none; }
}
body[data-torvi-shell] #notifBellBadge { background: var(--danger-button); color: var(--on-fill); }

/* ---- A product you have not got says so, and says nothing else ---------------------------- */
/*
 * ── ONE ANSWER, NOT TWO (8 Oct, item 9) ──────────────────────────────────────────────────────
 *
 * nav-guard paints the plan panel and hides .main's children. The page's own script keeps running
 * though -- it has to, because hiding is not removing and its elements must survive -- so its
 * fetch comes back 404 (requireTraining answers 404 for an organisation without Training, on
 * purpose: a product you have not bought should not announce itself) and it reports that. On
 * Staff > Training that meant the neutral plan message AND "Not found" on the same screen, which
 * reads as "and it is also broken".
 *
 * So while a page is plan-gated, nothing but the panel is shown: children added AFTER the panel
 * was painted are covered too, which an inline hidden attribute set once could not do, and so is
 * the toast, which lives on <body> outside .main entirely.
 */
:root[data-plan-gated] .main > *:not(.plan-gate),
:root[data-plan-gated] main > *:not(.plan-gate) { display: none !important; }
:root[data-plan-gated] #torviNotice,
:root[data-plan-gated] .torvi-notice { display: none !important; }

/* ---- A signature is on paper ------------------------------------------------------------- */
/*
 * SIGNATURES STORED BEFORE 8 Oct ARE TRANSPARENT PNGs with dark ink, so wherever one is shown on
 * a dark surface it is invisible. signature-pad.js paints white paper into the canvas now, which
 * fixes everything signed from here on; this is what makes the ones already on file readable.
 *
 * --on-fill rather than #fff because it is white in every palette and is already the token the
 * phone's own .ts-sig reached for, so there is one answer to "what colour is paper".
 */
body .sig-shown { background: var(--on-fill); }

/* ---- A phone number or an email you can act on ------------------------------------------- */
/*
 * The control is the number itself, so it has to read as a number and still be visibly tappable:
 * the glyph does that, not a button's outline. Inline, so it drops into a table cell, a key/value
 * row or a line of detail text without changing the line's height.
 */
body .contact-link {
  display: inline-flex; align-items: center; gap: 5px; vertical-align: baseline;
  background: none; border: 0; padding: 1px 2px; margin: 0 -2px; font: inherit; color: var(--teal);
  text-align: left; cursor: pointer; border-radius: 5px; max-width: 100%;
}
body .contact-link > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body .contact-link:hover > span, body .contact-link:focus-visible > span { text-decoration: underline; }
/* 44px of thumb on a phone, without 44px of line height: the padding grows, the box does not. */
@media (max-width: 900px) {
  body .contact-link { padding: 9px 4px; margin: -9px -4px; }
}
/* Beside an editable field (staff profile): the field still edits, this still acts. */
body .contact-beside { margin-top: 5px; min-height: 0; }
body .contact-beside:empty { display: none; }
body .contact-beside .contact-link { font-size: 12.5px; font-weight: 600; }
body .contact-ic { width: 13px; height: 13px; flex: 0 0 auto; fill: currentColor; opacity: 0.75; }

body .contact-sheet { position: fixed; inset: 0; z-index: 1200; display: flex;
                      align-items: flex-end; justify-content: center; }
body .contact-scrim { position: absolute; inset: 0; background: rgba(11, 22, 66, 0.35); }
body .contact-panel {
  position: relative; width: 100%; max-width: 420px; margin: 0 12px 12px;
  background: var(--card); border: 1px solid var(--line); border-radius: 16px;
  padding: 8px; box-shadow: 0 -10px 30px rgba(11, 22, 66, 0.22);
  padding-bottom: calc(8px + env(safe-area-inset-bottom));
}
body .contact-head { padding: 10px 12px 12px; }
body .contact-head b { display: block; font-size: 16px; font-weight: 700; color: var(--heading);
                       word-break: break-word; }
body .contact-head span { display: block; font-size: 12.5px; color: var(--muted); margin-top: 2px; }
body .contact-act {
  display: block; width: 100%; box-sizing: border-box; padding: 13px 12px; margin-top: 4px;
  background: var(--surface-2); border: 0; border-radius: 10px;
  font: inherit; font-size: 15px; font-weight: 600; color: var(--heading);
  text-align: center; text-decoration: none; cursor: pointer;
}
body .contact-act:hover { background: var(--line); }
body .contact-act.is-done { color: var(--ok-text); background: var(--ok-bg); }
body .contact-cancel { background: none; color: var(--muted); font-weight: 500; }
/* On a desktop it is a small dialog in the middle, not a sheet stuck to the bottom edge. */
@media (min-width: 901px) {
  body .contact-sheet { align-items: center; }
  body .contact-panel { margin: 0; max-width: 360px; }
}

/* ---- Sheet -------------------------------------------------------------------------------- */
body[data-torvi-shell] .shell-scrim {
  position: fixed; inset: 0; z-index: 1140; background: rgba(11, 22, 66, 0.25);
  opacity: 0; transition: opacity 0.2s ease;
}
body[data-torvi-shell] .shell-scrim.open { opacity: 1; }
body[data-torvi-shell] .shell-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1150; max-height: 88dvh; overflow: hidden;
  display: flex; flex-direction: column; background: var(--card); color: var(--ink);
  border-radius: 22px 22px 0 0; box-shadow: 0 -10px 30px rgba(11, 22, 66, 0.18);
  padding: 8px max(16px, env(safe-area-inset-right)) max(18px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  transform: translateY(100%); transition: transform 0.2s ease-out; outline: none;
}
body[data-torvi-shell] .shell-sheet.open { transform: translateY(0); }
/* With the keyboard up the sheet sits on it, and fits what is left above it: its Save is never
   under the keyboard. --kb-inset and --vv-height come from the visual viewport (app-shell.js). */
html[data-keyboard="up"] body[data-torvi-shell] .shell-sheet { bottom: var(--kb-inset, 0px); max-height: calc(var(--vv-height, 88dvh) - 8px); padding-bottom: 12px; }
body[data-torvi-shell] .shell-sheet-grab { width: 38px; height: 5px; border-radius: 999px; background: var(--line); margin: 2px auto 12px; flex: 0 0 auto; }
body[data-torvi-shell] .shell-sheet-title { font-family: 'Sora', sans-serif; font-size: 17px; font-weight: 700; margin: 0 0 10px; color: var(--ink); }
body[data-torvi-shell] .shell-sheet-body { overflow-y: auto; overscroll-behavior: contain; flex: 1 1 auto; min-height: 0; }
body[data-torvi-shell] .shell-sheet-text { font-size: 14px; line-height: 1.5; color: var(--ink); margin: 0 0 14px; }
body[data-torvi-shell] .shell-sheet-note { font-size: 13px; line-height: 1.5; color: var(--muted); margin: 0 0 6px; }
@media (min-width: 901px) {
  body[data-torvi-shell] .shell-sheet { left: 50%; right: auto; width: 440px; margin-left: -220px; bottom: 24px; border-radius: 22px; }
}

/* ---- Button, card, row, chip, skeleton, empty state ---------------------------------------
 * One filled button per screen (§1.3); the rest are tiles, chips and rows. */
body[data-torvi-shell] .shell-button {
  display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 48px;
  padding: 0 18px; border: 0; border-radius: 12px; box-sizing: border-box; cursor: pointer;
  background: var(--teal); color: var(--on-teal); font: inherit; font-size: 13px; font-weight: 600;
  text-decoration: none;
}
body[data-torvi-shell] .shell-button:active { background: var(--teal-dark); }
body[data-torvi-shell] .shell-button.danger { background: var(--danger-button); color: var(--on-fill); }
body[data-torvi-shell] .shell-button:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

body[data-torvi-shell] .shell-card {
  background: var(--card); color: var(--ink); border-radius: 18px; padding: 14px;
  box-shadow: 0 6px 18px rgba(11, 22, 66, 0.06);
}
body[data-torvi-shell] .shell-card.hero { background: var(--navy); color: var(--on-fill); box-shadow: none; }

body[data-torvi-shell] .shell-row {
  display: flex; align-items: center; gap: 12px; min-height: 56px; padding: 10px 12px;
  background: var(--card); color: var(--ink); border-radius: 14px; text-decoration: none;
  box-sizing: border-box; min-width: 0;
}
body[data-torvi-shell] .shell-row + .shell-row { margin-top: 8px; }
body[data-torvi-shell] .shell-avatar {
  flex: 0 0 34px; width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--teal-tint); color: var(--teal-text); font-size: 12px; font-weight: 700; overflow: hidden; position: relative;
}
body[data-torvi-shell] .shell-avatar img { width: 100%; height: 100%; object-fit: cover; }
body[data-torvi-shell] .shell-row-main { flex: 1 1 auto; min-width: 0; }
/* display:block -- they are spans, and an inline box ignores text-overflow, so a long line ran past
   the screen instead of ending in an ellipsis. */
body[data-torvi-shell] .shell-row-title, body[data-torvi-shell] .shell-row-meta { display: block; }
body[data-torvi-shell] .shell-row-title { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
body[data-torvi-shell] .shell-row-meta { font-size: 11px; color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

body[data-torvi-shell] .shell-chip {
  display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; padding: 3px 9px; border-radius: 999px;
  font-size: 10px; font-weight: 600; white-space: nowrap; background: var(--sand); color: var(--sand-text);
}
body[data-torvi-shell] .shell-chip.ok { background: var(--ok-bg); color: var(--ok-text); }
body[data-torvi-shell] .shell-chip.warn { background: var(--warn-bg); color: var(--warn-text); }
body[data-torvi-shell] .shell-chip.danger { background: var(--danger-bg); color: var(--danger-text); }
body[data-torvi-shell] .shell-chip.progress { background: var(--status-progress-bg); color: var(--status-progress-text); }

/* ---- Tables: the shared default, and a cell you can click ----------------------------------
 *
 * WHY THIS IS HERE AT ALL. .data-table is used by eight pages and DEFINED by six of them, so
 * suppliers.html, location-profile.html and staff-profile.html have been rendering tables with no
 * rule behind the class -- browser-default borders, no padding, and a plain <a> in a cell showing as
 * raw underlined blue. That is not a theme problem, it is a missing stylesheet, and it is why the
 * jobs list looked like a web page from 1998.
 *
 * WRAPPED IN :where() SO A PAGE STILL WINS. :where() zeroes the specificity of everything inside it,
 * so these are a floor rather than a ceiling: the six pages that define .data-table themselves keep
 * exactly the look they have, and the three that never did get a sensible one. Without :where(),
 * body[data-torvi-shell] .data-table would outrank a page's own bare .data-table and silently
 * restyle six screens that nobody asked me to touch.
 */
:where(body[data-torvi-shell] .data-table) { width: 100%; border-collapse: collapse; font-size: 13px; }
/*
 * NO white-space: nowrap IN THE SHARED FLOOR. The six pages that define .data-table themselves set it
 * and every one of them wraps the table in .table-scroll, so a wide header scrolls. The three pages
 * that never defined the class do NOT wrap their tables -- suppliers.html's jobs list among them --
 * so nowrap here pushed that table 155px past a phone, which a 390px render caught. A page that wants
 * unbreakable headers asks for them, and takes the scroller with it.
 */
:where(body[data-torvi-shell] .data-table th) {
  text-align: left; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--muted); padding: 8px 10px; border-bottom: 1px solid var(--line);
}
:where(body[data-torvi-shell] .data-table td) {
  padding: 10px; border-bottom: 1px solid var(--surface-2); vertical-align: middle;
}

/*
 * A CLICKABLE CELL IS NOT A WEB LINK. This one is NOT in :where(): it has to win, because what it is
 * correcting is the browser's own default for <a>, which every page inherits and no page overrode.
 *
 * The text stays the row's text -- same colour, same weight, no underline -- and the cursor is what
 * says it can be pressed. A row of navy text with one blue underlined cell reads as "this cell is a
 * link to somewhere else"; it is not, it opens the thing the row is about, which is what clicking
 * anywhere on the row already does.
 *
 * AN EXPLICIT COLOUR STILL WINS. The handful of anchors that are deliberately teal carry it inline,
 * which outranks this, so nothing that was meant to look like a link stops looking like one.
 */
body[data-torvi-shell] .data-table td a,
body[data-torvi-shell] .rate-table td a {
  color: inherit;
  font: inherit;
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
}
/*
 * And the hover belongs to the ROW, not the cell -- the whole row is the target, so highlighting one
 * cell would be telling the truth about the markup and a lie about the behaviour. Only on tables that
 * say they are clickable: a hover on a table nobody can press is a promise the page does not keep.
 */
body[data-torvi-shell] .data-table tbody tr[data-row-click]:hover,
body[data-torvi-shell] .rate-table tbody tr[data-row-click]:hover {
  background: var(--surface-2);
}
body[data-torvi-shell] .data-table tbody tr[data-row-click],
body[data-torvi-shell] .rate-table tbody tr[data-row-click] { cursor: pointer; }

/*
 * A BUTTON THAT IS THE ROW'S OWN TEXT. Where the clickable thing is one cell rather than the row, a
 * button is the honest element -- it is pressed, it is reachable by keyboard, and a screen reader
 * announces it as something that does rather than somewhere that goes. What it must not do is look
 * like a button, so every default a browser puts on one is undone here and the type comes from the
 * row. text-align:left because a button centres its text and a table column does not.
 */
body[data-torvi-shell] .cell-open {
  appearance: none; background: none; border: 0; padding: 0; margin: 0;
  font: inherit; font-weight: 500; color: inherit; text-align: left; cursor: pointer;
}
body[data-torvi-shell] .cell-open:hover,
body[data-torvi-shell] .data-table td a.cell-link:hover,
body[data-torvi-shell] .rate-table td a.cell-link:hover { text-decoration: none; color: var(--teal-text); }
/* Keyboard focus stays visible: it is the only thing telling somebody tabbing through where they are. */
body[data-torvi-shell] .cell-open:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 3px; }

body[data-torvi-shell] .shell-skeleton {
  border-radius: 14px; background: linear-gradient(90deg, var(--sand) 0%, var(--surface-2) 50%, var(--sand) 100%);
  background-size: 200% 100%; animation: shell-shimmer 0.6s linear infinite;
}
@keyframes shell-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) { body[data-torvi-shell] .shell-skeleton { animation: none; } }

body[data-torvi-shell] .shell-empty { text-align: center; padding: 22px 16px; color: var(--muted); font-size: 13px; line-height: 1.5; }
body[data-torvi-shell] .shell-empty svg { width: 64px; height: 64px; display: block; margin: 0 auto 10px; fill: none; stroke: var(--muted); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }

/* ---- Phone screens (today.js, phone-kit.js) --------------------------------------------------
 * Only at phone width, only while it is showing: the page's own desktop content steps aside for
 * it (data-phone-screen), and comes back the moment the window is wide again.
 */
@media (max-width: 900px) {
  html[data-phone-screen] body[data-torvi-shell] .main > :not(.phone-screen) { display: none !important; }
  /* A screen built for both palettes reads its text colour from --ink. The pages set body text to
     --navy, which stays navy in the dark palette and would be navy on near-black. */
  html[data-dark-ready] body { color: var(--ink); background: var(--paper); }
  /* The spec's screen padding: the status bar's inset, and 12px under it. The bell floats in the
     top-right corner, so the header keeps clear of it rather than the whole page moving down. */
  html[data-phone-screen] body[data-torvi-shell] .main { padding-top: max(12px, env(safe-area-inset-top)) !important; }
  /* And something BEHIND the status bar (21 Sep): the padding keeps the first row out from under the
     clock, but a scrolled screen sends every later row up through it -- "Fluid balance" printed behind
     08:03 on a real phone. A fixed strip in the page colour, exactly the inset tall, under every sheet
     and dialog (they dim it) and above the page. It takes hits (no pointer-events: none): the band is the status
     bar's, and elementFromPoint answering body there is how mobilecheck knows a scrolled row is covered. */
  html[data-phone-screen] body[data-torvi-shell]::before { content: ''; position: fixed; top: 0; left: 0; right: 0; height: env(safe-area-inset-top); background: var(--paper); z-index: 5; }

  body[data-torvi-shell] .phone-screen { display: block; color: var(--ink); }
  body[data-torvi-shell] .phone-screen svg, body[data-torvi-shell] .shell-sheet .today-tile svg, body[data-torvi-shell] .shell-sheet .shell-row svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex: 0 0 auto; }
  body[data-torvi-shell] .today-head { padding: 6px 60px 0 2px; margin-bottom: 14px; min-height: 48px; }
  body[data-torvi-shell] .today-title { font-family: 'Sora', sans-serif; font-size: 20px; font-weight: 700; margin: 0; color: var(--ink); line-height: 1.25; }
  body[data-torvi-shell] .today-sub { font-size: 13px; color: var(--muted); margin: 4px 0 0; }
  /* Which organisation this is, for somebody who belongs to more than one (8 Oct). A control,
     not a label: it reads as tappable and it is the way out of this company. Sits between the
     greeting and the date, where the eye already is. Colours are tokens, so it is right in both
     palettes without a second rule. */
  body[data-torvi-shell] .today-org {
    display: inline-flex; align-items: center; gap: 4px;
    margin: 6px 0 0; padding: 4px 10px 4px 11px;
    background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px;
    font-family: inherit; font-size: 12px; font-weight: 600; color: var(--ink);
    cursor: pointer; max-width: 100%;
  }
  body[data-torvi-shell] .today-org-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  body[data-torvi-shell] .today-org-chev { color: var(--muted); flex: none; }
  body[data-torvi-shell] .today-org:active { border-color: var(--muted); }
  body[data-torvi-shell] .today-section { margin-top: 20px; }
  body[data-torvi-shell] .today-h2 { font-family: 'Sora', sans-serif; font-size: 15px; font-weight: 600; margin: 0 0 8px 2px; color: var(--ink); }
  body[data-torvi-shell] .phone-screen .shell-row { color: var(--ink); }
  body[data-torvi-shell] .today-chev { color: var(--muted); display: flex; }
  body[data-torvi-shell] .today-chev svg { width: 18px; height: 18px; }

  /* The shift card: the clock, the figure beside it, and everything one tap from it. */
  body[data-torvi-shell] .today-card { padding: 16px; }
  body[data-torvi-shell] .today-clockline { display: flex; align-items: center; gap: 14px; }
  body[data-torvi-shell] .today-dial { flex: 0 0 68px; width: 68px; height: 68px; }
  /* The mark arrives as an <img> and live-mark.js swaps it for the same drawing as inline SVG, so
     both are sized -- and above the generic icon rule, which would otherwise shrink the clock to
     the size of a tab icon. */
  body[data-torvi-shell] .phone-screen .today-dial img,
  body[data-torvi-shell] .phone-screen .today-dial svg { width: 68px; height: 68px; display: block; stroke: none; }
  body[data-torvi-shell] .today-figure { font-family: 'Sora', sans-serif; font-size: 28px; font-weight: 700; line-height: 1.1; font-variant-numeric: tabular-nums; color: var(--ink); }
  body[data-torvi-shell] .today-figure-label { font-size: 11px; color: var(--muted); margin-top: 2px; }
  body[data-torvi-shell] .today-meta { font-size: 13px; color: var(--muted); margin: 12px 0 10px; }
  body[data-torvi-shell] .today-none { font-size: 14px; font-weight: 600; margin-top: 12px; color: var(--ink); }
  body[data-torvi-shell] .today-card .shell-row { background: var(--paper); }
  body[data-torvi-shell] .today-card .shell-button { margin-top: 12px; }
  body[data-torvi-shell] .today-place, body[data-torvi-shell] .today-thing { background: var(--sand); color: var(--sand-text); }
  body[data-torvi-shell] .today-place svg { width: 18px; height: 18px; }
  /* Start shift breathes until it is pressed -- once every couple of seconds, never bouncing. */
  body[data-torvi-shell] .today-start { animation: today-breathe 2.4s ease-in-out infinite; }
  @keyframes today-breathe {
    0%, 100% { box-shadow: 0 0 0 0 rgba(15, 118, 110, 0.35); }
    50% { box-shadow: 0 0 0 8px rgba(15, 118, 110, 0); }
  }
  @media (prefers-reduced-motion: reduce) { body[data-torvi-shell] .today-start { animation: none; } }

  body[data-torvi-shell] .today-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
  body[data-torvi-shell] .today-tiles.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  body[data-torvi-shell] .today-tile {
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
    min-height: 64px; padding: 8px 4px; border: 0; border-radius: 14px; box-sizing: border-box;
    background: var(--sand); color: var(--ink); font: inherit; font-size: 12px; font-weight: 600;
    text-decoration: none; cursor: pointer; text-align: center;
  }
  body[data-torvi-shell] .today-tile:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

  /* The safety line: amber, under the card, never behind a tap. */
  body[data-torvi-shell] .today-safety {
    display: flex; gap: 10px; align-items: flex-start; margin-top: 10px; padding: 12px 14px;
    border-radius: 14px; border-left: 3px solid var(--warn); background: var(--warn-bg); color: var(--sand-text);
    font-size: 13px; line-height: 1.45;
  }
  body[data-torvi-shell] .today-safety svg { color: var(--warn); margin-top: 1px; }
  body[data-torvi-shell] .today-attrib { font-size: 10px; color: var(--muted); margin-top: 6px; }

  /* Manager: the site line, four counters coloured by meaning, and the one decision. */
  /* One rhythm down the screen (7 Oct): the clock card, the counters and the one decision were
     12px, nothing and whatever followed, so the cards read as unevenly spaced on a phone. */
  body[data-torvi-shell] .today-site-card { margin-bottom: 16px; }
  body[data-torvi-shell] .today-site-line { font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.4; }
  body[data-torvi-shell] .today-site-line [data-figure] { font-family: 'Sora', sans-serif; font-variant-numeric: tabular-nums; }
  body[data-torvi-shell] .today-site {
    margin-top: 8px; min-height: 36px; max-width: 100%; padding: 6px 10px; border-radius: 10px;
    border: 1px solid var(--control-edge); background: var(--sand); color: var(--sand-text); font: inherit; font-size: 13px;
  }
  body[data-torvi-shell] .today-counters { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 16px; }
  /*
   * A FIGURE THAT OPENS ITS OWN LIST (7 Oct, item 3) is a <button>, so it needs the reset every
   * button in this shell needs -- the width of its grid cell, no browser chrome, the page's font.
   * A nought stays a <div>: nothing to open, so nothing that looks pressable.
   */
  body[data-torvi-shell] .today-counter { border-radius: 14px; padding: 10px 6px; text-align: center; background: var(--sand);
    display: block; width: 100%; border: 0; font: inherit; color: inherit; -webkit-appearance: none; appearance: none; }
  body[data-torvi-shell] button.today-counter { cursor: pointer; }
  body[data-torvi-shell] button.today-counter:active { transform: scale(0.97); }
  body[data-torvi-shell] .today-counter-n { font-family: 'Sora', sans-serif; font-size: 22px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
  body[data-torvi-shell] .today-counter-l { font-size: 11px; color: var(--muted); margin-top: 2px; }
  body[data-torvi-shell] .today-counter.ok { background: var(--ok-bg); }
  body[data-torvi-shell] .today-counter.ok .today-counter-n { color: var(--ok-text); }
  body[data-torvi-shell] .today-counter.warn { background: var(--warn-bg); }
  body[data-torvi-shell] .today-counter.warn .today-counter-n { color: var(--warn-text); }
  body[data-torvi-shell] .today-counter.danger { background: var(--danger-bg); }
  body[data-torvi-shell] .today-counter.danger .today-counter-n { color: var(--danger-text); }
  /* Today's roster band (22 Sep): the four groups as sub-headings; the week strip as seven taps. Tokens
     only, so Dark follows the palette. */
  body[data-torvi-shell] .today-h3 { font-family: 'Sora', sans-serif; font-size: 13px; font-weight: 600; margin: 0 0 6px 2px; color: var(--muted); }
  body[data-torvi-shell] .today-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
  body[data-torvi-shell] .today-day {
    display: flex; flex-direction: column; align-items: center; gap: 2px; min-height: 48px; padding: 7px 0;
    border-radius: 12px; background: var(--sand); color: var(--sand-text); text-decoration: none;
  }
  body[data-torvi-shell] .today-day-l { font-size: 11px; font-weight: 600; }
  body[data-torvi-shell] .today-day-n { font-family: 'Sora', sans-serif; font-size: 18px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
  body[data-torvi-shell] .today-day.has { background: var(--danger-bg); }
  body[data-torvi-shell] .today-day.has .today-day-n { color: var(--danger-text); }
  body[data-torvi-shell] .today-day.is-today { box-shadow: inset 0 0 0 2px var(--teal); }
  body[data-torvi-shell] .today-hero { margin-top: 12px; }
  body[data-torvi-shell] .today-hero-t { font-family: 'Sora', sans-serif; font-size: 17px; font-weight: 700; color: var(--on-fill); }
  body[data-torvi-shell] .today-rowacts { display: flex; gap: 6px; flex: 0 0 auto; }
  body[data-torvi-shell] .today-ic {
    width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
    background: var(--sand); color: var(--ink);
  }
  body[data-torvi-shell] .today-present::after {
    content: ''; position: absolute; right: -1px; bottom: -1px; width: 10px; height: 10px; border-radius: 50%;
    background: var(--ok); box-shadow: 0 0 0 2px var(--card);
  }
  body[data-torvi-shell] .today-request { width: 100%; border: 0; font: inherit; text-align: left; cursor: pointer; }
  body[data-torvi-shell] .today-sheet-actions { display: grid; gap: 10px; margin-top: 6px; }
}

/* ---- Shared phone pieces (phone-kit.js) and Roster (roster-phone.js) --------------------------- */
@media (max-width: 900px) {
  body[data-torvi-shell] button.shell-row { width: 100%; border: 0; font: inherit; text-align: left; cursor: pointer; }
  body[data-torvi-shell] .shell-row:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
  body[data-torvi-shell] .shell-row.is-selected { box-shadow: 0 0 0 2px var(--teal); }
  body[data-torvi-shell] .today-section > * + * { margin-top: 8px; }
  body[data-torvi-shell] .phone-count { font-family: 'Inter', sans-serif; font-size: 12px; font-weight: 600; color: var(--muted); margin-left: 4px; }
  /* A header with one trailing control; the bell floats top-right, so the header keeps clear of it. */
  body[data-torvi-shell] .phone-head { display: flex; align-items: flex-start; gap: 10px; }
  body[data-torvi-shell] .phone-head-text { flex: 1 1 auto; min-width: 0; }
  body[data-torvi-shell] .phone-filter {
    flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 14px;
    border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--ink);
    font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  }
  body[data-torvi-shell] .phone-filter svg { width: 16px; height: 16px; }
  body[data-torvi-shell] .phone-filter.on { background: var(--teal); border-color: var(--teal); color: var(--on-teal); }
  body[data-torvi-shell] .today-tile.danger svg { color: var(--danger-button); }
  body[data-torvi-shell] .today-sheet-actions.two { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: stretch; }
  body[data-torvi-shell] .today-sheet-actions.two .shell-button { min-height: 64px; }

  /* The floating action: above the tab bar and its safe area, clear of the right edge's inset. */
  body[data-torvi-shell] .phone-fab {
    position: fixed; z-index: 800; right: max(16px, env(safe-area-inset-right));
    bottom: calc(64px + max(18px, env(safe-area-inset-bottom)) + 16px);
    width: 56px; height: 56px; border-radius: 50%; border: 0; display: flex; align-items: center; justify-content: center;
    background: var(--teal); color: var(--on-teal); box-shadow: 0 8px 20px rgba(11, 22, 66, 0.22); cursor: pointer;
  }
  body[data-torvi-shell] .phone-fab.danger { background: var(--danger-button); color: var(--on-fill); }
  body[data-torvi-shell] .phone-fab svg { width: 26px; height: 26px; }
  body[data-torvi-shell] .phone-fab:focus-visible { outline: 3px solid var(--teal-bright); outline-offset: 2px; }
  html[data-keyboard="up"] body[data-torvi-shell] .phone-fab,
  html[data-sheet-open] body[data-torvi-shell] .phone-fab { display: none; }

  /* Facts on a sheet: label over value, never a table. */
  body[data-torvi-shell] .phone-facts { margin: 10px 2px 4px; display: grid; gap: 8px; }
  body[data-torvi-shell] .phone-facts dt { font-size: 11px; color: var(--muted); }
  body[data-torvi-shell] .phone-facts dd { margin: 1px 0 0; font-size: 13px; color: var(--ink); overflow-wrap: anywhere; }
  body[data-torvi-shell] .phone-summary .shell-row { background: var(--paper); }

  /* Forms inside a sheet: single column, 44px fields. */
  body[data-torvi-shell] .phone-form { display: grid; gap: 12px; }
  body[data-torvi-shell] .phone-field { display: grid; gap: 4px; font-size: 12px; font-weight: 600; color: var(--muted); }
  body[data-torvi-shell] .phone-field select,
  body[data-torvi-shell] .phone-field input,
  body[data-torvi-shell] .phone-field textarea {
    min-height: 44px; width: 100%; box-sizing: border-box; padding: 8px 12px; border-radius: 12px;
    border: 1px solid var(--control-edge); background: var(--card); color: var(--ink); font: inherit; font-size: 15px; font-weight: 400;
  }

  /* The week strip. Today is navy; the chosen day is outlined in teal; a day needing attention
     carries an amber dot (not coral -- coral is the mark's alone). */
  body[data-torvi-shell] .roster-strip { display: flex; align-items: center; gap: 2px; margin-bottom: 4px; }
  body[data-torvi-shell] .roster-days { flex: 1 1 auto; min-width: 0; display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
  body[data-torvi-shell] .roster-step {
    flex: 0 0 28px; width: 28px; height: 44px; border: 0; background: none; color: var(--muted);
    display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
  }
  body[data-torvi-shell] .roster-step svg { width: 18px; height: 18px; }
  body[data-torvi-shell] .roster-day {
    position: relative; min-height: 56px; border: 0; border-radius: 14px; background: var(--card); color: var(--ink);
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; font: inherit; cursor: pointer; padding: 0;
  }
  body[data-torvi-shell] .roster-dow { font-size: 10px; font-weight: 600; color: var(--muted); }
  body[data-torvi-shell] .roster-date { font-family: 'Sora', sans-serif; font-size: 15px; font-weight: 700; }
  body[data-torvi-shell] .roster-day.today { background: var(--navy); color: var(--on-fill); }
  body[data-torvi-shell] .roster-day.today .roster-dow { color: var(--navy-text); }
  body[data-torvi-shell] .roster-day.chosen { box-shadow: 0 0 0 2px var(--teal); }
  body[data-torvi-shell] .roster-dot { position: absolute; top: 6px; right: 6px; width: 6px; height: 6px; border-radius: 50%; background: var(--warn); }
  body[data-torvi-shell] .roster-list { padding-bottom: 72px; }
  body[data-torvi-shell] .roster-line { display: flex; align-items: center; gap: 8px; }
  body[data-torvi-shell] .roster-line .shell-row { flex: 1 1 auto; min-width: 0; }
  body[data-torvi-shell] .roster-offer {
    flex: 0 0 auto; min-height: 44px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--teal);
    background: var(--card); color: var(--teal); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  }
}

/* ---- Wide tables, on a phone (responsive-table.js) -------------------------------------------- */
@media (max-width: 560px) {
  /*
   * A ROW IS A CARD. Reported from a phone about Leave and Reports: a nine-column table pinched
   * into 390px is unreadable whichever columns are hidden, so below this width the table stops
   * being a table. The markup does not change -- it is still a <table>, still sorted and exported
   * the same -- only how it draws.
   */
  .rt-table.rt-cards, .rt-table.rt-cards tbody, .rt-table.rt-cards tr, .rt-table.rt-cards td { display: block; width: auto; }
  .rt-table.rt-cards thead { display: none; }
  .rt-table.rt-cards tr {
    background: var(--card); border: 1px solid var(--surface-2); border-radius: 12px;
    padding: 12px 14px; margin: 0 0 10px;
  }
  .rt-table.rt-cards td { border: 0; padding: 2px 0; display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }
  /* The first cell is the card's title: bigger, no label, and on its own line. */
  .rt-table.rt-cards td:first-child { display: block; font-weight: 700; font-size: 14px; margin-bottom: 4px; }
  .rt-table.rt-cards td[data-rt-label]::before {
    content: attr(data-rt-label); color: var(--muted); font-size: 12px; font-weight: 600;
    flex: 0 0 auto; text-align: left;
  }
  /* A cell with nothing in it is a line of nothing: no label, no gap. */
  .rt-table.rt-cards td.rt-empty { display: none; }
  /* The actions column has no heading, so it gets the foot of the card and the whole width. */
  .rt-table.rt-cards td[data-rt-actions] {
    display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap;
    margin-top: 8px; padding-top: 10px; border-top: 1px solid var(--surface-2);
  }
  .rt-table.rt-cards .rt-hidden { display: none; }
  /* No sideways scroll to shadow when there are no columns. */
  .rt-cards-host .rt-scroll::after { display: none; }
}

/* ---- Timesheets (timesheets-phone.js) -------------------------------------------------------- */
@media (max-width: 900px) {
  /*
   * Filter chips: card with a line, active teal; counts inside.
   *
   * THEY WRAP (7 Oct). They used to scroll sideways inside their own row, which on a 390px screen
   * cut the fourth one in half with nothing to say there were more -- reported from a phone as
   * "Declined is clipped". A filter nobody can see is a filter nobody uses, and a hidden sideways
   * scroll is the least discoverable control there is. Wrapped, every filter is on the screen; two
   * short rows cost less than one lost one.
   */
  body[data-torvi-shell] .phone-chips { display: flex; flex-wrap: wrap; gap: 8px; padding: 2px 2px 6px; margin: 0 -2px 6px; }
  body[data-torvi-shell] .phone-chips::-webkit-scrollbar { display: none; }
  /*
   * The filter sheet's preset row (item 4). It wraps rather than scrolls sideways: five fixed
   * words, and a row that scrolls hides the last one on the narrowest phone -- which would be
   * Custom, the one somebody is looking for when none of the four fit.
   */
  body[data-torvi-shell] .ts-presets { margin-bottom: 0; padding-bottom: 0; }
  body[data-torvi-shell] .ts-dates { display: flex; gap: 8px; }
  body[data-torvi-shell] .ts-dates input { flex: 1 1 0; min-width: 0; }
  body[data-torvi-shell] .phone-field[hidden] { display: none; }
  body[data-torvi-shell] .phone-chipbtn {
    flex: 0 0 auto; min-height: 36px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--line);
    background: var(--card); color: var(--ink); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
  }
  body[data-torvi-shell] .phone-chipbtn span { color: var(--muted); margin-left: 2px; }
  body[data-torvi-shell] .phone-chipbtn.on { background: var(--teal); border-color: var(--teal); color: var(--on-teal); }
  body[data-torvi-shell] .phone-chipbtn.on span { color: var(--on-teal); }
  body[data-torvi-shell] .ts-list { padding-bottom: 88px; }

  /* The swipe: the card slides over a green (approve) or red (decline) layer. */
  body[data-torvi-shell] .ts-swipe { position: relative; border-radius: 14px; overflow: hidden; }
  body[data-torvi-shell] .ts-swipe .shell-row { position: relative; transition: transform .2s ease; }
  body[data-torvi-shell] .ts-under { position: absolute; inset: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 18px; font-size: 13px; font-weight: 700; }
  body[data-torvi-shell] .ts-under span { display: inline-flex; align-items: center; gap: 6px; opacity: 0; }
  body[data-torvi-shell] .ts-swipe[data-dir="ok"] .ts-under { background: var(--ok-bg); color: var(--ok-text); }
  body[data-torvi-shell] .ts-swipe[data-dir="ok"] .ts-under .ok,
  body[data-torvi-shell] .ts-swipe[data-dir="no"] .ts-under .no { opacity: 1; }
  body[data-torvi-shell] .ts-swipe[data-dir="no"] .ts-under { background: var(--danger-bg); color: var(--danger-text); }
  @media (prefers-reduced-motion: reduce) { body[data-torvi-shell] .ts-swipe .shell-row { transition: none; } }

  body[data-torvi-shell] .ts-check {
    flex: 0 0 34px; width: 34px; height: 34px; border-radius: 50%; border: 2px solid var(--control-edge);
    display: flex; align-items: center; justify-content: center; box-sizing: border-box; color: var(--on-teal);
  }
  body[data-torvi-shell] .ts-check.on { background: var(--teal); border-color: var(--teal); }

  /* The bulk bar: navy, above the tab bar. */
  body[data-torvi-shell] .ts-bulk {
    position: fixed; z-index: 800; left: 12px; right: 12px; bottom: calc(64px + max(18px, env(safe-area-inset-bottom)) + 10px);
    display: flex; align-items: center; gap: 12px; padding: 10px 10px 10px 16px; border-radius: 16px;
    background: var(--navy); color: var(--on-fill); font-size: 13px; font-weight: 600; box-shadow: 0 8px 20px rgba(11, 22, 66, 0.22);
  }
  body[data-torvi-shell] .ts-bulk span { flex: 1 1 auto; min-width: 0; }
  body[data-torvi-shell] .ts-bulk .shell-button { flex: 0 0 auto; width: auto; padding: 0 20px; }
  body[data-torvi-shell] .ts-bulk .shell-button[disabled] { opacity: .5; }

  body[data-torvi-shell] .ts-sig { display: block; max-width: 220px; margin: 8px 2px; border: 1px solid var(--line); border-radius: 10px; background: var(--on-fill); } /* dark ink on transparency: white behind it in both palettes */
  body[data-torvi-shell] .ts-history { margin-top: 12px; display: grid; gap: 8px; }
  body[data-torvi-shell] .ts-history-row { font-size: 12px; color: var(--muted); line-height: 1.45; }
  body[data-torvi-shell] .ts-history-row b { color: var(--ink); text-transform: capitalize; }
  body[data-torvi-shell] .ts-was { font-style: normal; font-weight: 400; margin-left: 6px; }
  body[data-torvi-shell] .ts-none { font-weight: 400; color: var(--muted); }
  body[data-torvi-shell] .ts-break { display: flex; align-items: center; gap: 8px; color: var(--ink); font-weight: 400; }
  body[data-torvi-shell] .ts-break span { flex: 1 1 auto; min-width: 0; }
  body[data-torvi-shell] .ts-break .today-tile, body[data-torvi-shell] .ts-addbreak .today-tile { min-height: 44px; flex: 0 0 auto; padding: 0 14px; }
  body[data-torvi-shell] .ts-addbreak { display: flex; gap: 8px; align-items: stretch; }
  body[data-torvi-shell] .ts-addbreak select { flex: 1 1 auto; min-width: 0; }
  body[data-torvi-shell] .ts-addbreak input { flex: 0 0 76px; width: 76px; }
  body[data-torvi-shell] .shell-sheet .shell-button + .today-tiles { margin-top: 10px; }
}

/* ---- End shift (shift-clock.js) ---------------------------------------------------------------
 *
 * The sheet had no styles of its own at all: the supervisor's name came out as a browser-default
 * input a third of the width, the signature canvas had no border -- an invisible pad -- and the ink
 * is navy on a transparent canvas, which in dark mode is navy on navy.
 *
 * THE PAD IS PAPER, IN BOTH SCHEMES. Not a dark surface with light ink: the signature is exported
 * as a PNG and read later by whoever approves the timesheet, on their screen and not this one, so
 * the ink has to be dark on white wherever it is drawn. Only the pad is held light; the sheet
 * around it follows the theme like everything else.
 */
.shift-end-form { display: flex; flex-direction: column; }
.shift-end-label { font-size: 12.5px; font-weight: 600; color: var(--ink); margin: 0 0 5px; }
.shift-end-label span { font-weight: 500; color: var(--muted); }
.shift-end-input {
  width: 100%; box-sizing: border-box; min-height: 44px; margin: 0 0 14px;
  padding: 10px 12px; border-radius: 10px; border: 1.5px solid var(--line);
  background: var(--card); color: var(--ink);
  font: inherit; font-size: 16px;  /* 16px or iOS zooms the page when it is focused */
}
.shift-end-sig {
  display: block; width: 100%; height: 130px; box-sizing: border-box;
  /* --on-fill is white in every scheme: the one token that means "paper", not "the surface". */
  border: 1.5px solid var(--control-edge); border-radius: 10px; background: var(--on-fill);
  touch-action: none; cursor: crosshair;
}
.shift-end-clear {
  align-self: flex-start; margin-top: 8px; min-height: 40px; padding: 8px 14px;
  border-radius: 9px; border: 1px solid var(--line); background: transparent;
  color: var(--ink); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
}
.shift-end-submit { margin-top: 16px; }
.shift-end-error { margin-top: 12px; color: var(--danger-text); font-size: 13px; font-weight: 600; }
.shift-end-nobody { display: flex; align-items: center; gap: 10px; margin-top: 14px; font-size: 13.5px; font-weight: 600; color: var(--ink); min-height: 44px; }
.shift-end-nobody input { width: 20px; height: 20px; margin: 0; }
.shift-end-reason-wrap { margin-top: 6px; }
.shift-end-reason-wrap textarea.shift-end-input { min-height: 84px; resize: vertical; }

/* ---- Care (care-phone.js) -------------------------------------------------------------------- */
@media (max-width: 900px) {
  body[data-torvi-shell] .phone-seg { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 4px; border-radius: 14px; background: var(--sand); margin-bottom: 6px; }
  body[data-torvi-shell] .phone-seg button {
    min-height: 40px; border: 0; border-radius: 10px; background: transparent; color: var(--sand-text);
    font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  }
  body[data-torvi-shell] .phone-screen .roster-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 4px; border-radius: 14px; background: var(--sand); margin: 0 0 10px; }
  body[data-torvi-shell] .phone-screen .roster-switch a { display: flex; padding: 0; font-weight: 600; box-shadow: none; background: transparent; align-items: center; justify-content: center; min-height: 40px; border-radius: 10px; color: var(--sand-text); font-size: 13px; }
  body[data-torvi-shell] .phone-screen .roster-switch a.active { background: var(--card); color: var(--ink); box-shadow: 0 1px 3px rgba(11, 22, 66, 0.10); }
  body[data-torvi-shell] .phone-seg button[aria-pressed="true"] { background: var(--card); color: var(--ink); box-shadow: 0 1px 3px rgba(11, 22, 66, 0.10); }
  body[data-torvi-shell] .care-head { align-items: center; justify-content: space-between; min-height: 48px; }
  body[data-torvi-shell] .care-back {
    display: inline-flex; align-items: center; gap: 4px; min-height: 44px; padding: 0 12px 0 4px; margin-left: -4px;
    color: var(--teal); font-size: 15px; font-weight: 600; text-decoration: none;
  }
  body[data-torvi-shell] .phone-call { text-decoration: none; margin-right: 56px; }
  body[data-torvi-shell] .care-id { padding: 6px; }
  body[data-torvi-shell] .care-id .shell-row { background: transparent; }
  body[data-torvi-shell] .care-id .shell-row-title { font-family: 'Sora', sans-serif; font-size: 17px; white-space: normal; }
  body[data-torvi-shell] .care-id .shell-row-meta { white-space: normal; }
  body[data-torvi-shell] .care-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 12px 10px; }
  body[data-torvi-shell] .care-sentence { margin: 0; padding: 12px 14px; border-radius: 14px; background: var(--card); color: var(--ink); font-size: 13px; line-height: 1.5; }
  body[data-torvi-shell] .care-id .care-sentence { background: transparent; padding-top: 0; }
  body[data-torvi-shell] .care-add {
    width: 100%; min-height: 56px; display: flex; align-items: center; justify-content: center; gap: 8px;
    border: 1.5px dashed var(--control-edge); border-radius: 14px; background: transparent; color: var(--teal);
    font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  }
  body[data-torvi-shell] .care-note { display: flex; gap: 12px; padding: 12px; border-radius: 14px; background: var(--card); }
  body[data-torvi-shell] .care-note-main { flex: 1 1 auto; min-width: 0; }
  body[data-torvi-shell] .care-note-head { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--muted); }
  body[data-torvi-shell] .care-note-head b { color: var(--ink); font-size: 13px; }
  body[data-torvi-shell] .care-note-text { margin-top: 4px; font-size: 13px; line-height: 1.5; color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
  body[data-torvi-shell] .care-foot { height: 84px; }
}

/* ---- More (app-shell.js moreSheet) ------------------------------------------------------------ */
body[data-torvi-shell] .shell-sheet.tall { height: 88dvh; }
body[data-torvi-shell] .more { display: grid; gap: 4px; padding-bottom: 8px; }
body[data-torvi-shell] .more-h { font-family: 'Sora', sans-serif; font-size: 13px; font-weight: 600; color: var(--muted); margin: 14px 2px 6px; }
body[data-torvi-shell] .more-group { display: grid; gap: 1px; border-radius: 14px; overflow: hidden; background: var(--line); }
body[data-torvi-shell] .more-group .shell-row { border-radius: 0; margin: 0; background: var(--paper); color: var(--ink); }
body[data-torvi-shell] button.more-row { width: 100%; border: 0; font: inherit; text-align: left; cursor: pointer; }
body[data-torvi-shell] .more-row.locked .shell-row-title { color: var(--muted); }
body[data-torvi-shell] .more-lock { color: var(--muted); display: flex; }
body[data-torvi-shell] .more-lock svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
body[data-torvi-shell] .more-tick { color: var(--teal); font-weight: 700; }
body[data-torvi-shell] .more-account { background: var(--paper); }

/* ---- Invoices (invoices-phone.js) ------------------------------------------------------------ */
body[data-torvi-shell] .inv-lines { display: grid; gap: 1px; margin: 10px 0; border-radius: 14px; overflow: hidden; background: var(--line); }
body[data-torvi-shell] .inv-line { display: flex; gap: 10px; align-items: flex-start; padding: 10px 12px; background: var(--paper); font-size: 13px; color: var(--ink); }
body[data-torvi-shell] .inv-line-main { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
body[data-torvi-shell] .inv-line-meta { font-size: 11px; color: var(--muted); margin-top: 2px; }
body[data-torvi-shell] .inv-line-amt { flex: 0 0 auto; font-variant-numeric: tabular-nums; font-weight: 600; }
body[data-torvi-shell] .inv-totals { display: grid; gap: 4px; margin: 0 2px 14px; font-size: 13px; }
body[data-torvi-shell] .inv-totals div { display: flex; justify-content: space-between; color: var(--muted); }
body[data-torvi-shell] .inv-totals dd { margin: 0; color: var(--ink); font-variant-numeric: tabular-nums; }
body[data-torvi-shell] .inv-totals .total { font-family: 'Sora', sans-serif; font-size: 16px; font-weight: 700; color: var(--ink); }
body[data-torvi-shell] .inv-full { margin-top: 10px; background: var(--paper); }

/* In the dark palette, cards stand off the page by a border rather than a shadow (spec §2). */
@media (max-width: 900px) and (prefers-color-scheme: dark) {
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] .shell-card:not(.hero) { box-shadow: none; border: 1px solid var(--line); }
}
/* THE DARK SLICE (22 Sep). Headings, labels and the notification panel's titles are --navy on light --
   and navy stays navy in the dark palette (the tab bar is navy in both), so on a dark card they vanish.
   In Dark they read in --ink. Inputs inside a dialog that says it is dark-ready take the palette's
   surface, not the browser's white. Written twice, once per way of being dark, like the palette. */
@media (max-width: 900px) and (prefers-color-scheme: dark) {
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] :is(.notif-header, .notif-item b, h1, h2, h3, h4, th, label, .section-title, .modal h2) { color: var(--ink); }
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] .btn-ghost { color: var(--sand-text); }
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] .btn:not(.btn-primary):not(.btn-danger):not(.btn-ghost) { color: var(--ink); }
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] .broadcast-tag { color: var(--navy-text); }
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] :is(input, select, textarea):not([data-force-light] *) { background-color: var(--surface-2); color: var(--ink); border-color: var(--control-edge); color-scheme: dark; }
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] :is(.btn-primary:not(:disabled), .p-avatar) { color: var(--on-teal); }
  html[data-dark-ready]:not([data-theme="light"]) body[data-torvi-shell] :is(.p-card, .p-name, .loc-card, .loc-name, b, strong) { color: var(--ink); }
}
@media (max-width: 900px) {
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] :is(.notif-header, .notif-item b, h1, h2, h3, h4, th, label, .section-title, .modal h2) { color: var(--ink); }
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] .btn-ghost { color: var(--sand-text); }
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] .btn:not(.btn-primary):not(.btn-danger):not(.btn-ghost) { color: var(--ink); }
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] .broadcast-tag { color: var(--navy-text); }
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] :is(input, select, textarea):not([data-force-light] *) { background-color: var(--surface-2); color: var(--ink); border-color: var(--control-edge); color-scheme: dark; }
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] :is(.btn-primary:not(:disabled), .p-avatar) { color: var(--on-teal); }
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] :is(.p-card, .p-name, .loc-card, .loc-name, b, strong) { color: var(--ink); }
}
@media (max-width: 900px) {
  html[data-dark-ready][data-theme="dark"] body[data-torvi-shell] .shell-card:not(.hero) { box-shadow: none; border: 1px solid var(--line); }
}

/* ---- The phone's page frame (S2 and S3, 19 Sep) -----------------------------------------------
 *
 * NO FLOATING BELL ON A PHONE (S3). With the tab bar, notifications are a count on More and a row
 * in it that opens a sheet (app-shell.js notificationsSheet); the bell and its dropdown sat over
 * every page's own header. The web, with no tab bar, keeps its bell.
 *
 * ONE FRAME (S2). The 70px strip at the top of every page was there to clear the hamburger and the
 * bell, and the tab bar took both away: the page starts 12px under the status bar, with the same
 * 12px -- or the curve of the screen, if that is more -- at each side, like the phone screens.
 * One page background under every page, the paper; headers keep no gap for a bell that is gone.
 */
body[data-torvi-shell] .shell-button.ghost { background: var(--card); color: var(--ink); border: 1px solid var(--line); }
body[data-torvi-shell] .notif-sheet { display: flex; flex-direction: column; gap: 10px; }
body[data-torvi-shell] .notif-sheet .notif-item { border: 1px solid var(--line); border-radius: 12px; margin: 0; }
@media (max-width: 900px) {
  html[data-tabs] body[data-torvi-shell] #notifBell,
  html[data-tabs] body[data-torvi-shell] #notifPanel { display: none !important; }
  html[data-tabs] body[data-torvi-shell] { background: var(--paper); }
  /* !important, as the 70px rule above is: the pages' own mobile shorthands carry it too. */
  html[data-tabs] body[data-torvi-shell] .main {
    padding-top: max(12px, env(safe-area-inset-top)) !important;
    padding-left: max(12px, env(safe-area-inset-left)) !important;
    padding-right: max(12px, env(safe-area-inset-right)) !important;
  }
  html[data-tabs] body[data-torvi-shell] .today-head { padding-right: 2px; }
}

/* ---- Dialogs are sheets on a phone (S5, 19 Sep) -----------------------------------------------
 *
 * Every page dialog is a .modal-backdrop holding one card. Centred with a margin, a long form was a
 * box floating mid-screen whose Save sat below the fold -- scroll the card to find it, and the
 * keyboard covered it again. At phone width the card is a full-width sheet from the bottom, like
 * the shell's own, and its last row of buttons is PINNED to the bottom of it while the rest
 * scrolls. !important because the cards carry their widths, radii and padding inline.
 *
 * LIGHT, for now (decision 2): the palette's forced-light scope names .modal-backdrop too.
 */
@media (max-width: 900px) {
  html[data-tabs] body[data-torvi-shell] .modal-backdrop,
  html[data-tabs] body[data-torvi-shell] #endShiftBackdrop {
    align-items: flex-end !important; justify-content: stretch !important; padding: 0 !important; overflow: hidden !important;
  }
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div,
  html[data-tabs] body[data-torvi-shell] #endShiftBackdrop > div {
    width: 100% !important; max-width: none !important; margin: 0 !important; box-sizing: border-box !important;
    border-radius: 18px 18px 0 0 !important; max-height: 92vh !important; max-height: 92dvh !important;
    overflow-y: auto !important; overscroll-behavior: contain;
    padding: 20px max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)) !important;
    /* A field scrolled into view lands above the sticky footer, not under it (21 Sep). */
    scroll-padding-bottom: 96px;
  }
  /* The footer: the card's actions row wherever it sits (a form's often holds it), or the card's or
     its form's last child when that is a row of buttons. Sticky inside a form still pins to the
     card's bottom edge: the form runs to the end of the card, and the row is its end. */
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div .modal-actions,
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div > div:last-child:has(> button),
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div > form > div:last-child:has(> button),
  html[data-tabs] body[data-torvi-shell] #endShiftBackdrop > div > div:last-child:has(> button) {
    /* The card's colour, not `inherit`: inside a <form> that inherits the form's transparent
       background, and the buttons sat see-through over the Incident details box (21 Sep). */
    position: sticky; bottom: 0; z-index: 1; background: var(--card);
    margin: 16px calc(-1 * max(16px, env(safe-area-inset-left))) 0 !important;
    padding: 12px max(16px, env(safe-area-inset-right)) max(14px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left)) !important;
    border-top: 1px solid var(--line);
  }
  /* A card that never set its own background leaves the footer see-through; it gets the card colour. */
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div:not([style*="background"]) { background: var(--card); }

  /*
   * ── AND THE SHEET SITS ON THE KEYBOARD, NOT UNDER IT (8 Oct, item 6) ────────────────────
   *
   * THE 7 OCT KEYBOARD RULE WAS LOSING TO THE 19 SEP SHEET RULE, and that is the whole bug.
   *
   *   19 Sep (above):  align-items: flex-end !important   max-height: 92dvh !important
   *   7 Oct (earlier): align-items: flex-start            max-height: calc(--vv-height - 20px)
   *
   * The sheet rule carries !important and the keyboard rule does not, so on every phone dialog
   * the card stayed anchored to the bottom of the FULL screen at 92% of it. Put a keyboard over
   * the bottom 336px and the message box and Send are behind it — reachable by scrolling the
   * card, which is exactly how it was reported for New message and for SMS. One-person chat was
   * fine because its composer is #msgTabLayout, not a dialog, and nothing competes for that.
   *
   * FLEX-END IS KEPT ON PURPOSE. The backdrop is now only as tall as the VISIBLE area, so
   * bottom-aligned means the sheet rests on top of the keyboard — which is where a bottom sheet
   * belongs. Pinning it to the top instead would leave a gap under it and move the whole dialog
   * away from the thumb.
   *
   * These carry !important and two attribute selectors, so they beat the sheet rule on both
   * specificity and order rather than relying on either alone.
   */
  html[data-tabs][data-keyboard="up"] body[data-torvi-shell] .modal-backdrop,
  html[data-tabs][data-keyboard-open] body[data-torvi-shell] .modal-backdrop,
  html[data-tabs][data-keyboard="up"] body[data-torvi-shell] #endShiftBackdrop,
  html[data-tabs][data-keyboard-open] body[data-torvi-shell] #endShiftBackdrop {
    height: var(--vv-height, 100%) !important;
    align-items: flex-end !important;
  }
  html[data-tabs][data-keyboard="up"] body[data-torvi-shell] .modal-backdrop > div,
  html[data-tabs][data-keyboard-open] body[data-torvi-shell] .modal-backdrop > div,
  html[data-tabs][data-keyboard="up"] body[data-torvi-shell] #endShiftBackdrop > div,
  html[data-tabs][data-keyboard-open] body[data-torvi-shell] #endShiftBackdrop > div {
    max-height: calc(var(--vv-height, 100vh) - 8px) !important;
  }

  /*
   * ── A PHONE FOOTER HOLDS TWO BUTTONS (7 Oct, item 15) ───────────────────────────────────────
   *
   * Edit shift's footer carries four: Delete shift, Offer to more…, Cancel and Save changes. Four
   * on a 390px line is 70px each, so "Offer to more…" wrapped onto three lines, "Save changes" ran
   * off the edge, and the two a person presses most were the smallest things on the screen.
   *
   * The footer keeps Cancel and the primary action -- the two that end the job. Everything else
   * wraps above them, full width, in the order the markup already has: a destructive button and a
   * secondary one each on their own line, where a thumb can tell them apart. No markup moves, so
   * the desktop row is untouched and nothing re-binds.
   */
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div .modal-actions { flex-wrap: wrap; row-gap: 8px; }
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div .modal-actions > *:not(.modal-actions-right) { flex: 1 0 100%; margin-left: 0 !important; }
  /*
   * ...ON ITS OWN LAYER, and that is not decoration. WebKit repaints only the region it thinks
   * changed, and this button's label is REPLACED after it is shown ("Offer shift…" becomes "Offer
   * to more…" once the shift's offers have been read). On the phone it then painted the new label
   * with the first and last glyph missing -- "ffer to more" -- because the dirty rectangle was the
   * old, narrower text. Visible only in the real WebView: Chrome, and every gate that renders in
   * it, paints the whole button and shows nothing wrong. translateZ(0) gives the button its own
   * layer, so a change to it repaints all of it.
   */
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div .modal-actions > *:not(.modal-actions-right) .btn { width: 100%; transform: translateZ(0); }
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div .modal-actions-right { flex: 1 0 100%; margin-left: 0 !important; }
  html[data-tabs] body[data-torvi-shell] .modal-backdrop > div .modal-actions-right .btn { flex: 1; white-space: nowrap; }
}

/*
 * ── A CONTROL BELONGING TO A PRODUCT NOBODY BOUGHT (7 Oct, item 21) ──────────────────────────
 *
 * nav-guard writes the organisation's products onto <html> as data-features; anything that
 * belongs to one says so with data-feature, and this hides it when the list does not hold it.
 * One rule per product rather than one line of JavaScript per button -- the next button then
 * only has to carry the attribute.
 *
 * The attribute is written from the cache before first paint, so a control somebody DOES have
 * does not appear and then vanish on every page load.
 */
html:not([data-features~="kiosk"]) [data-feature="kiosk"],
html:not([data-features~="incidents"]) [data-feature="incidents"],
html:not([data-features~="reports"]) [data-feature="reports"],
html:not([data-features~="messages"]) [data-feature="messages"],
html:not([data-features~="sms"]) [data-feature="sms"],
html:not([data-features~="participants"]) [data-feature="participants"],
html:not([data-features~="staff_documents"]) [data-feature="staff_documents"],
html:not([data-features~="contractor_pro"]) [data-feature="contractor_pro"] { display: none !important; }

/*
 * ── A PAGE OF A PRODUCT NOBODY BOUGHT (7 Oct, item 21) ───────────────────────────────────────
 * One panel, the same on every page, because the answer is the same on every page.
 */
.plan-gate { max-width: 460px; margin: 40px auto; padding: 26px 24px; background: var(--card);
             border: 1px solid var(--line); border-radius: var(--radius); text-align: center; }
.plan-gate h1 { font-family: 'Sora', sans-serif; font-size: 20px; margin: 0 0 6px; color: var(--heading); }
.plan-gate-what { color: var(--muted); font-size: 13.5px; margin: 0 0 14px; }
.plan-gate-line { color: var(--ink); font-size: 14px; margin: 0 0 16px; }

/* ---- The sidebar's locked items (1.3.1 item B) ------------------------------------------------
 * An item the plan does not include, shown only to somebody who can change the plan: greyed, in its
 * own place in its group, and saying why. It opens Subscription (nav-guard.js). Everybody else
 * never sees it -- the same one rule as the phone's More.
 */
body[data-torvi-shell] .nav-item.locked { opacity: 0.6; }
body[data-torvi-shell] .nav-item.locked:hover { opacity: 0.85; }
body[data-torvi-shell] .nav-item .nav-lock {
  margin-left: auto; padding-left: 8px; font-size: 10.5px; font-weight: 600; letter-spacing: 0.01em;
  color: var(--navy-text); white-space: nowrap;
}

/* ---- A select is never wider than its container (1.3.1 item A, 19 Sep) -----------------------
 *
 * A <select> is as wide as its longest option, and on a real account an option is a site name like
 * "Elora Aged Care — Elora Aged Care Wollongong…". On the manager's Today the site switcher sits in
 * a flex item, whose default min-width:auto grows to fit it: the card, and the whole screen, slid
 * sideways. The review data had short names, so no gate saw it.
 *
 * Shell-wide, not just there: whatever directly holds a select may shrink, and the select stays
 * inside it and cuts its label short with an ellipsis. The option list itself still shows names in
 * full when opened.
 */
body[data-torvi-shell] :is(div, label, span, p, li, section, header, form, fieldset):has(> select) { min-width: 0; }
body[data-torvi-shell] select { max-width: 100%; min-width: 0; text-overflow: ellipsis; }
@media (max-width: 900px) {
  body[data-torvi-shell] .phone-screen select,
  body[data-torvi-shell] .shell-sheet select { overflow: hidden; white-space: nowrap; }
}

/* ---- A place's office record is for location.view (hotfix, 20 Sep) ----------------------------
 * nav-guard.js marks the location page for anybody without it: Details only, read-only.
 */
html[data-location-limited] .tabs .tab:not([data-tab="details"]),
html[data-location-limited] .tab-panel:not(#panel-details),
html[data-location-limited] #editToggleBtn,
html[data-location-limited] #editBar,
html[data-location-limited] #addStaffBtn,
html[data-location-limited] #addStaffBackdrop { display: none !important; }

/*
 * ── THE PRODUCT SWITCHER ─────────────────────────────────────────────────────────────────────
 *
 * HERE RATHER THAN IN shell-chrome, and that is the fix rather than the tidy-up: shell-chrome is
 * linked by ten pages and this file by all forty-one. The switcher is drawn by nav-guard into
 * every sidebar it draws, so on a roster page -- Dashboard, Schedule, Staff -- it arrived with no
 * rules at all and rendered as the words "RosterTraining" underlined above OVERVIEW.
 *
 * Two words in a pill above the menu. It has to read as "which product am I in", not as another
 * menu item, so it is a segmented control: the chosen half is filled, the other is not, and
 * neither carries an icon. Inside the navy sidebar, so the colours are the sidebar's own tokens
 * (palette-2026-09-11.css, which every page also links) and it follows light and dark with them.
 */
.nav-switch { display:flex; gap:3px; padding:3px; margin:0 0 20px; border-radius:10px;
              background:rgba(255,255,255,0.07); }
.nav-switch-item { flex:1; text-align:center; padding:7px 8px; border-radius:8px;
                   font-size:12.5px; font-weight:600; color:var(--navy-text);
                   text-decoration:none; }
.nav-switch-item:hover { background:rgba(255,255,255,0.06); }
.nav-switch-item.on { background:var(--card); color:var(--heading); font-weight:700; }

/* ── BUTTONS, FOR THE PAGES THAT NEVER DEFINED THEM ───────────────────────────────────────────
 *
 * There was no base .btn rule in ANY shared stylesheet -- this file carried .btn only inside its
 * dark-mode overrides -- so a page that wrote class="btn" and did not also define .btn in its own
 * inline <style> got nothing at all. On My Roster that made "Submit timesheet" a default blue
 * underlined link, and the same on My Documents ("View") and Contractor Billing ("Open in
 * Finance"), which had already been patched once with an inline text-decoration:none.
 *
 * It is NOT a regression: rendered against a1549c5's stylesheets the markup gives the identical
 * blue underlined link. It has been live as long as those pages have.
 *
 * THE VALUES ARE THE ONES THIRTEEN PAGES ALREADY AGREE ON, copied rather than invented, so this
 * is the same button the rest of the product draws. A page with its own .btn still wins: its
 * inline <style> is parsed after this file is linked, so every property it sets overrides this
 * one. What those pages gain is only what none of them set -- text-decoration:none and
 * display:inline-block -- which is the half an <a class="btn"> was always missing.
 *
 * NO line-height HERE, deliberately. The first draft carried line-height:1.2, and the render pass
 * showed it was the one property that reached <button class="btn"> on all twenty-five pages that
 * define their own .btn -- normal -> 16.2px, a real change to buttons that have been fine for
 * months. Dropped, so what those pages gain is only what an anchor was missing and a button
 * already had.
 */
.btn { font-family:'Sora',sans-serif; font-weight:600; font-size:13.5px; padding:10px 18px;
       border-radius:9px; border:none; cursor:pointer; text-decoration:none;
       display:inline-block; text-align:center; }
.btn-sm { padding:6px 12px; font-size:12px; }
/* Teal, not blue: blue is the signup and invite pages, and both define their own. */
.btn-primary { background:var(--teal); color:var(--card); }
.btn-quiet { background:none; border:1px solid var(--line); color:var(--heading); text-decoration:none; }
.btn-ghost { background:var(--sand); border:1px solid var(--control-edge); color:var(--heading); }
/*
 * ...AND THE DESTRUCTIVE ONE, for the same reason (7 Oct, item 22). Thirteen pages define
 * .btn-danger in their own <style> and the rest -- the location and participant records among them
 * -- never did, so "Decline timesheet" came out as the browser's default grey button: a light
 * surface sitting in a dark dialog, and the one button on it that should look like it does
 * something irreversible looking like nothing at all.
 */
.btn-danger { background:none; border:1px solid var(--danger); color:var(--danger-text); }
.btn-danger:hover { background:var(--danger-bg); }

/* How many courses are still to do, beside "My training" in a learner's menu. Drawn only when
   there is a number to draw, so a menu with nothing outstanding is the menu it was before. */
.nav-count { margin-left:auto; min-width:20px; padding:1px 6px; border-radius:20px;
             background:var(--teal); color:#fff; font-size:11px; font-weight:700;
             text-align:center; line-height:1.6; }
