/* VertiServices — modern, mobile-first stylesheet */

/* Sprint 474 — the icon CSS is NOT imported here any more.
   These three @import rules used to sit on this line, and an @import cannot
   start downloading until its parent stylesheet has been fetched AND parsed.
   On this host (~1s time-to-first-byte on every request, HTTP/1.1) that was a
   guaranteed extra serial round trip in front of first paint. The parts are
   now concatenated into assets/css/icons.css (built by
   scripts/build-icons-css.php) and every page links it in PARALLEL with this
   file. Brands went with them: fa-brands is used nowhere in the app.
   Do not reintroduce an @import here — add to the build script instead. */

:root {
  /* Sprint 199: brand navy = the VERTI logo colour (#1D3360) so menus,
     headings, buttons, links and default table text all match the logo. */
  --verti-navy: #1D3360;
  /* The same navy as CHANNELS. rgba() takes numbers, not a hex, so a tint of
     the brand could not use --verti-navy and 43 of them stayed literal until
     this token existed (sprint ux2-alpha, 6 Sep 2026, the operator's C12).
     Keep the two in step: a brand move edits both, and D1 now FAILS a tint
     that spells the channels out. color-mix() would read better and is younger
     than the field phones' WebViews, so it is not used here. */
  --verti-navy-rgb: 29, 51, 96;
  --verti-navy-700: #142848;
  --verti-navy-50: #EEF2F8;
  --tke-orange: #E85A2A;
  --tke-purple: #7A2E6B;
  /* Sprint 200: the two-brand accent — balanced. VERTI navy holds only a
     short lead (~20%), then an even transition through TKE purple (55%) to
     TKE orange (100%), so the three brand colours read in similar proportion
     with navy just a touch ahead. Used everywhere: topbar strip, page-header
     underline, KPI cards, buttons, table top-line, dividers, helpers. */
  --brand-gradient: linear-gradient(90deg, #1D3360 0%, #1D3360 20%, #7A2E6B 55%, #E85A2A 100%);
  /* Two recurring SHAPES the fleet writes beside the one above, named here so
     they move with the brand (sprint ux3-gradient-tokens, 6 Sep 2026, the
     operator's C13). Both are written with the colour tokens, not hexes, so a
     brand move edits one line: the two-stop rule that heads and foots every
     offer document (9 sites, 4 of them reachable - the other 5 are inside the
     A4 sheet units-view.js prints into its own window, which loads no
     stylesheet), and the 120 degree purple->orange pill behind every "AI fix"
     button (5 sites). Adding tokens is what the design system says it does not
     want; it is the lesser of the two evils against a hand-written copy of the
     brand in ten files. */
  --brand-gradient-2stop: linear-gradient(90deg, var(--verti-navy), var(--tke-orange));
  --brand-gradient-diag: linear-gradient(120deg, var(--tke-purple), var(--tke-orange));
  --bg: #F4F6F9;
  --surface: #FFFFFF;
  --text: #1A2333;
  --muted: #6B7588;
  --border: #E2E6EE;
  --danger: #D94B4B;
  --success: #2E9E6B;
  --warning: #E0A93A;
  --radius: 12px;
  --radius-sm: 8px;
  --shadow-sm: 0 1px 2px rgba(20,30,60,0.06);
  --shadow-md: 0 6px 24px rgba(20,30,60,0.08);
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;

  /* Sprint 474 (C4) — MOTION TOKENS. One curve for everything that travels,
     one set of durations, defined once. Before this the navigation curve was
     the literal string cubic-bezier(0.32, 0.72, 0, 1) copied into four rules;
     a timing that lives in several places stops being a design decision and
     becomes four numbers that drift apart. Use these, never a raw curve.
       --ease-spring : the iOS navigation curve — decelerates like a physical
                       object. Anything that MOVES uses it.
       --ease-exit   : leaving the screen; starts fast, no lingering.
       --ease-enter  : arriving; eases into place.
     Durations are deliberately short: a transition the user waits for is a
     cost, not a delight. */
  --ease-spring: cubic-bezier(0.32, 0.72, 0, 1);
  --ease-exit:   cubic-bezier(0.4, 0, 1, 1);
  --ease-enter:  cubic-bezier(0, 0, 0.2, 1);
  --dur-nav:      430ms;   /* page-to-page directional push */
  --dur-out:      190ms;   /* content leaving */
  --dur-in:       400ms;   /* content arriving */
  --dur-pill:     320ms;   /* the active-menu highlight morph */
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--verti-navy); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 16px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--brand-gradient);
  color: #fff;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
  transition: filter 0.15s, transform 0.05s, box-shadow 0.15s;
  white-space: nowrap;
}
/* Sprint 718 — THE [hidden] TRAP, canonical statement (see also .row,
   .mobile-h1-action, .msh-ttabs below and .offline-chip in shell.js).
   The `hidden` attribute contributes exactly one thing: the USER-AGENT rule
   `[hidden] { display: none }`. Origin is resolved BEFORE specificity, so the
   author-origin `display: inline-flex` above beats it outright and a
   `<button class="btn" hidden>` stays fully visible. Every permission-gated
   create button in this app is written that way, which is how view-only users
   were being shown ecNew / sqNew / esNew / esImport. */
.btn[hidden] { display: none; }
.btn:hover { filter: brightness(1.08); }
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn.btn-block { width: 100%; padding: 12px; font-size: 15px; }
.btn.btn-ghost {
  background: transparent;
  color: var(--verti-navy);
  border-color: var(--border);
}
.btn.btn-ghost:hover { background: var(--verti-navy-50); }
.btn.btn-danger { background: var(--danger); }
.btn.btn-danger:hover { background: #B83C3C; }
.btn.btn-accent { background: var(--tke-orange); }
.btn.btn-accent:hover { background: #C9491E; }
.btn.btn-sm { padding: 6px 10px; font-size: 13px; }

/* ---------- Inputs ---------- */
.field { margin-bottom: 14px; }
.field label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 6px;
  color: var(--text);
}
.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 11px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: inherit;
  background: #fff;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--verti-navy);
  box-shadow: 0 0 0 3px rgba(27, 58, 107, 0.15);
}
.field .hint { font-size: 12px; color: var(--muted); margin-top: 4px; }

/* ---------- Alerts ---------- */
.alert {
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  margin-bottom: 12px;
}
.alert.error  { background: #FBE9E9; color: #8C2A2A; border: 1px solid #F2C9C9; }
.alert.success { background: #E5F4EC; color: #235E45; border: 1px solid #BFE2D0; }
.alert.warning { background: #FBF2DA; color: #6E5318; border: 1px solid #EFD89A; }

/* ---------- Login layout ---------- */
.login-shell {
  min-height: 100vh;
  display: grid;
  grid-template-columns: 1fr;
}
.login-hero { display: none; }
.login-panel {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.login-card {
  width: 100%;
  max-width: 400px;
  background: var(--surface);
  border-radius: var(--radius);
  padding: 28px 24px;
  box-shadow: var(--shadow-md);
  position: relative;
  overflow: hidden;
}
/* Sprint 200: two-brand gradient cap on the login card. */
.login-card::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: var(--brand-gradient);
}
/* Sprint 200: real combined VERTI + TKE lockup replaces the old "V" chip. */
.login-brand { margin-bottom: 24px; }
.login-logo { height: 34px; width: auto; max-width: 280px; display: block; }
.brand-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 22px;
}
.brand-logo {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--verti-navy);
  color: #fff;
  display: grid; place-items: center;
  font-weight: 800; font-size: 22px;
  letter-spacing: 0.5px;
}
.brand-name { font-size: 20px; font-weight: 800; color: var(--verti-navy); }
.brand-sub  { font-size: 12px; color: var(--muted); }
.login-title { font-size: 22px; font-weight: 700; margin: 0 0 4px; }
.login-subtitle { color: var(--muted); margin: 0 0 22px; font-size: 14px; }
.login-foot { margin-top: 18px; font-size: 12px; color: var(--muted); text-align: center; }

@media (min-width: 880px) {
  .login-shell { grid-template-columns: 1.1fr 1fr; }
  .login-hero {
    display: flex;
    align-items: center;
    justify-content: center;
    background:
      radial-gradient(circle at 20% 20%, rgba(232,90,42,0.25), transparent 45%),
      radial-gradient(circle at 80% 80%, rgba(122,46,107,0.25), transparent 45%),
      linear-gradient(135deg, var(--verti-navy), var(--verti-navy-700));
    color: #fff;
    padding: 48px;
  }
  .hero-inner { max-width: 480px; }
  .hero-inner h1 { font-size: 36px; line-height: 1.15; margin: 0 0 12px; }
  .hero-inner p  { font-size: 16px; opacity: 0.85; }
  .hero-tag {
    display: inline-block; padding: 4px 10px;
    background: rgba(255,255,255,0.15);
    border: 1px solid rgba(255,255,255,0.25);
    border-radius: 999px; font-size: 12px; margin-bottom: 16px;
    letter-spacing: 0.4px; text-transform: uppercase;
  }
}

/* ---------- Branded print header / footer (Sprint 200) ---------- */
.print-brand, .print-foot { display: none; }
@media print {
  /* Hide the app chrome so a report prints clean. */
  .topbar, .sidebar, .menu-btn, .offline-chip, .lang-switch, .user-chip,
  .ai-chip, .notif-bubble, .brand-program-name, .fab, [data-no-print],
  .page-header .btn, .page-header button,
  /* Sprint 473: the back chip is navigation chrome — never printed. */
  .ph-back,
  /* Sprint 466: on the merged tabbed pages the action controls no longer sit
     inside .page-header (there is no title row any more) — they live in a
     .pane-toolbar under the tab strip. Hide both, plus the strip itself: a
     printout of one tab should not carry the other tabs' names. */
  .pb-tabs, .pane-toolbar, .ptab-toolbar { display: none !important; }
  .layout { display: block !important; }
  main, .content { padding: 0 !important; }
  .page-header { position: static !important; }
  /* Sprint 473: the house-standard rule is screen chrome; a printout keeps
     the plain navy title only. (Sprint 475 removed the second, title-width
     rule, so there is just the one to hide here now.) */
  .page-header.ph-std::after { display: none !important; }
  /* Same reason the header is un-stickied: a sticky strip repeats on every
     printed page. */
  .pb-tabs, .pane-toolbar, .ptab-toolbar, table.data thead th { position: static !important; }
  /* Reveal the branded header + footer (visibility:visible covers report
     pages that print via a body * { visibility:hidden } rule). */
  .print-brand {
    display: flex !important; visibility: visible !important;
    align-items: center; justify-content: space-between;
    border-bottom: 2px solid var(--verti-navy); padding-bottom: 8px; margin-bottom: 14px;
  }
  .print-brand * { visibility: visible !important; }
  .print-brand img { height: 34px; width: auto; }
  .print-brand .pb-meta { text-align: right; font-size: 11px; color: #374151; }
  .print-foot {
    display: block !important; visibility: visible !important;
    position: fixed; bottom: 0; left: 0; right: 0; text-align: center;
    padding-top: 6px; border-top: 3px solid; border-image: var(--brand-gradient) 1;
  }
  .print-foot * { visibility: visible !important; }
  .print-foot .pf-www  { font-weight: 800; color: var(--verti-navy); font-size: 13px; }
  .print-foot .pf-line { font-weight: 700; color: var(--verti-navy); font-size: 10px; }
}

/* ---------- App shell ---------- */
.app {
  min-height: 100vh;
  display: grid;
  grid-template-rows: auto 1fr;
}
.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 10px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  position: sticky; top: 0; z-index: 10;
}
/* Two-brand accent strip along the bottom of the topbar (every page). */
.topbar::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: -3px; height: 3px;
  background: var(--brand-gradient);
}
.topbar .brand-row { margin: 0; gap: 10px; align-items: center; }
.brand-program-name {
  /* Sprint 95: the title now lives in the normal flex flow as a
     flex:1 centered block between the logo and the right-side controls,
     so it can never overlap them (the old absolute+center could ride
     over the language switcher on mid widths). The flexible width
     absorbs the slack; text truncates with an ellipsis rather than
     pushing neighbours. */
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  font-size: 24px; font-weight: 700;
  color: var(--verti-navy); letter-spacing: 0.3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
}
/* Hide once the topbar gets crowded so it never squeezes the controls. */
@media (max-width: 1100px) { .brand-program-name { display: none; } }
.topbar .brand-logo-img {
  height: 32px;
  width: auto;
  max-width: 270px;
  display: block;
}
@media (max-width: 879px) {
  .topbar .brand-logo-img { height: 26px; max-width: 210px; }
}
/* The flexible title (.brand-program-name) absorbs the slack on wide
   screens, so the spacer only needs to grow once the title is hidden. */
.topbar .spacer { flex: 0 0 0; }
@media (max-width: 1100px) { .topbar .spacer { flex: 1; } }
.topbar .user-chip {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 6px 10px; border-radius: 999px;
  background: var(--verti-navy-50); color: var(--verti-navy);
  font-size: 13px; font-weight: 600;
  text-decoration: none;
  transition: background 0.15s;
}
.topbar a.user-chip:hover { background: #DDE5F1; text-decoration: none; }
.topbar .ai-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px; border-radius: 999px; cursor: pointer;
  background: linear-gradient(120deg, #d65f1f 0%, #b8430f 100%);
  color: #fff; border: 0; font-size: 13px; font-weight: 700;
  margin-right: 8px;
  transition: filter 0.15s;
}
.topbar .ai-chip:hover { filter: brightness(1.08); }
.avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--verti-navy); color: #fff;
  display: grid; place-items: center; font-size: 12px; font-weight: 700;
}
.menu-btn {
  border: 1px solid var(--border);
  background: #fff; border-radius: var(--radius-sm);
  padding: 6px 10px; cursor: pointer; font-size: 18px;
}
/* Sprint 124: the hamburger is now shown on desktop too, where it collapses
   / expands the pinned sidebar (it kept its mobile overlay role as well). */

.layout { display: grid; grid-template-columns: 1fr; min-height: 0; }
@media (min-width: 880px) { .layout { grid-template-columns: 240px 1fr; } }

.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 16px 10px;
  display: none;
}
@media (min-width: 880px) {
  .sidebar {
    display: block;
    /* Pin the sidebar to the viewport on desktop so the nav stays
       reachable while the content area scrolls. top:56px lands it
       just under the global .topbar (sticky at top:0). The fixed
       height + overflow-y:auto let the nav itself scroll internally
       if more links are added than fit the viewport. align-self:start
       keeps the grid item its natural height (otherwise grid would
       stretch it and sticky would have nothing to slide against). */
    position: sticky;
    top: 56px;
    align-self: start;
    height: calc(100vh - 56px);
    overflow-y: auto;
  }
}
.sidebar.open { display: block; position: fixed; inset: 56px 0 0 0; z-index: 20; }

/* Sprint 124: desktop collapsible sidebar (default expanded). The #menuToggle
   in the topbar flips .sidebar-collapsed on .app; the state is persisted in
   localStorage so it survives page navigation. */
@media (min-width: 880px) {
  .app.sidebar-collapsed .layout { grid-template-columns: 1fr; }
  .app.sidebar-collapsed .sidebar { display: none; }
}

.nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  color: var(--verti-navy);
  font-weight: 600; font-size: 13px;
}
.nav a:hover { background: var(--verti-navy-50); text-decoration: none; }
.nav a.active { background: var(--verti-navy); color: #fff; }
.nav-section { font-size: 13px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--verti-navy); padding: 10px 12px 4px; }
/* Sprint 138: collapsible nav sections (accordion). The heading is a
   full-width button with a chevron; its links live in .nav-group-body
   which is hidden when the group has .collapsed. */
button.nav-section.nav-toggle {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; background: none; border: 0; cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 400; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--verti-navy); text-align: left;
}
button.nav-section.nav-toggle:hover { color: var(--verti-navy); }
.nav-chevron { font-size: 12px; transition: transform .15s ease; opacity: .7; }
.nav-group.collapsed .nav-chevron { transform: rotate(-90deg); }
.nav-group.collapsed .nav-group-body { display: none; }

main.content { padding: 20px 16px 40px; min-width: 0; }
@media (min-width: 880px) { main.content { padding: 28px 32px; } }

.page-header {
  display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 20px;
  /* Sprint 30: pin the page title + button row to the top of the
     content area on every page. top:56px lands it just under the
     global .topbar (sticky at top:0 / z-index:10). z-index:5 stacks
     below the topbar but above the page content. Solid bg so
     scrolling content doesn't bleed through. The negative margin +
     padding pair extends the bg upward to cover the gap the original
     20px content padding leaves -- otherwise a strip of background
     shows above the header when scrolled. */
  position: sticky;
  top: 56px;
  z-index: 5;
  background: var(--bg);
  padding: 10px 0;
  margin: -10px 0 12px;
}
.page-header h1 { margin: 0; font-size: 22px; position: relative; padding-bottom: 9px; }
/* Sprint 475 (operator): the old 64px gradient underline that used to sit
   under every page title is GONE. Every real page title is a `.ph-titlerow h1`
   and had this cancelled already, so nothing changes on screen today — but
   leaving the rule in place meant a future page-header whose h1 was not
   wrapped in a title row would silently sprout a SECOND brand line under the
   full-width one, which is exactly what this sprint removed. One gradient
   rule per page, and now the CSS cannot produce a second one. */
.page-header .spacer { flex: 1; }

/* ── Page header — the HOUSE STANDARD (Sprints 467-473; full spec + the
   measured font-size/line-height pair table live in CLAUDE.md). Lifted out of
   map.html's inline <style> the moment a second page adopted it, per the
   CLAUDE.md rule — no page carries a private copy.
   .ph-std marks a page's TOP header only: .page-header is also reused as a
   section h2 row on some pages (Sprint 30 pattern) and the closing rule must
   never appear there. */
.page-header.ph-std { padding: 10px 0 12px; }
.page-header.ph-std::after { content: ''; position: absolute; left: 0; right: 0;
  bottom: 0; height: 3px; background: var(--brand-gradient); pointer-events: none; }
/* The title set row: back chip + title (+ optional .ph-link shortcut pills).

   Sprint 475 (operator): this row NO LONGER draws its own gradient rule.
   Sprints 471-473 gave the header two of them — a short one tracing the title
   set, and the full-width one closing the header — and side by side they read
   as a mistake rather than a hierarchy, especially on a page like
   installation-projects where the title is short and the two lines sit a few
   pixels apart. There is now exactly ONE brand rule per page: the full-width
   `.page-header.ph-std::after` above. Do not reintroduce a second one.
   `position: relative` stays — it is the containing block for the row. */
.ph-titlerow { position: relative; display: flex; align-items: center; gap: 8px;
  flex-wrap: nowrap; min-width: 0; }
/* 22px pairs with line-height 28px — a SOLVED PAIR, never move one alone.
   Sprint 571 (operator): the title sits 2px lower than it did. line-height is
   the lever that positions the cap — half the leading goes above it, so +4px of
   leading moves the cap down 2px; the font size is untouched and the 28px box
   is unchanged, which is why the row still lines up and nothing reflows.
   MEASURED on map.html before and after, not eyeballed: cap top was 90 against
   a pill top of 91 (1px HIGH of the documented "cap = pill top"), and is now
   92. So this is deliberately 1px below the old standard, at the operator's
   request — they are the one looking at the live screen.
   Hotfix ut-review-findings (operator, 24 Sep 2026, "5 px lower", on every
   page): 18px -> 28px of line-height moves the cap down 5px, the same lever.
   MEASURED on unit-task.html: the capitals' centre sat 4px ABOVE the pills'
   centre and now sits 1px below it; with the pills at 31px the cap top is
   7.5px below the pill top (scripts/measure-page-title.mjs SOLVED_DELTA). The
   box stays 28px, so a page with nothing but a title keeps its header height. */
.ph-titlerow h1 { flex: 0 0 auto; white-space: nowrap; height: 28px; margin: 0;
  font-size: 22px; line-height: 28px; padding-bottom: 0; color: var(--verti-navy); }
/* Data-driven titles (a project name, a person) may be arbitrarily long — the
   .ph-fit variant lets ONLY the title give way (ellipsis) so the row never
   wraps and the page never scrolls sideways. Static titles keep flex:0 0 auto. */
.ph-titlerow h1.ph-fit { flex: 0 1 auto; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; }
/* Shortcut pills — the SAME Apple-glass material as the mobile bar's active
   pill (.msh-tab.on, Sprint 462). One material, one source.
   Hotfix ut-review-findings (operator, 24 Sep 2026: "the same as the Reports
   tab", on every page): 31px tall, 13px text, 16px sides - the page tab pill's
   size (unit-task.html .fr-tab). Icon-only below 1500px, so never narrower
   than it is tall. */
.ph-links { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; min-width: 0; }
.ph-link { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  flex: 0 0 auto; white-space: nowrap; height: 31px; min-width: 31px; padding: 0 16px; border: 0;
  border-radius: 999px; background: var(--brand-gradient); color: #fff;
  font-size: 13px; font-weight: 700; text-decoration: none;
  box-shadow: 0 5px 14px rgba(122,46,107,.32),
              inset 0 1px 0 rgba(255,255,255,.4),
              inset 0 -4px 9px rgba(13,21,38,.14),
              inset 0 0 0 1px rgba(255,255,255,.2); }
.ph-link::before { content: ''; position: absolute; left: 6%; right: 6%; top: 2px;
  height: 44%; border-radius: 999px 999px 40% 40%; pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,.28), rgba(255,255,255,.03)); }
.ph-link > * { position: relative; z-index: 1; }
.ph-link i { font-size: 12px; opacity: .9; }
.ph-link:hover { filter: brightness(1.08); text-decoration: none; }
.ph-link:active { transform: scale(.97); }
.ph-link:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* Back chip — same 31px geometry, deliberately QUIET (white ground, navy ink):
   forward shortcuts are glass and sit RIGHT of the title; back is quiet and
   sits LEFT (the Sprint 209 back-link rule). Icon pair "← + destination
   glyph"; .ph-lbl names the destination and drops with the shared breakpoint.
   It grew with the shortcut pills (hotfix ut-review-findings), so a row that
   carries both is one height. */
.ph-back { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  white-space: nowrap; height: 31px; padding: 0 12px; border: 1px solid #CBD5E4;
  border-radius: 999px; background: #fff; color: var(--verti-navy);
  font-size: 13px; font-weight: 700; text-decoration: none;
  box-shadow: 0 1px 3px rgba(var(--verti-navy-rgb), .10); }
.ph-back i { font-size: 12px; }
.ph-back i.fa-arrow-left { font-size: 11px; opacity: .85; }
.ph-back:hover { background: var(--verti-navy-50); border-color: #AFBDD4; text-decoration: none; }
.ph-back:active { transform: scale(.97); }
.ph-back:focus-visible { outline: 2px solid var(--verti-navy); outline-offset: 2px; }
/* ── the house page-level tab strip ──────────────────────────────────────
   Sprint 399 drew it, and it lived in finance-report.css until sprint
   install-tabs moved it HERE. It was never a finance rule: six pages wear
   .pb-tabs and only three of them are financial, so every other one had to
   link a 2,000-line finance stylesheet to borrow four selectors. The rest of
   that file — .pb-tabacts, .pb-warnbox, the FBE split — stays where it is and
   is still linked by the pages that actually use it.
   The stickiness is NOT here: how far down a strip pins depends on what sits
   above it, so each host declares its own `position:sticky; top:…` block. */
.pb-tabs { display:flex; gap:4px; border-bottom:1px solid var(--border); margin:0 0 16px; flex-wrap:wrap; }
.pb-tab { padding:8px 16px 10px; font-weight:700; font-size:13px; color:var(--muted); cursor:pointer;
          border-radius:8px 8px 0 0; position:relative; background:transparent; border:0; }
.pb-tab.active { color:var(--verti-navy); background:#fff; border:1px solid var(--border); border-bottom-color:#fff; }
.pb-tab.active::after { content:""; position:absolute; left:12px; right:12px; bottom:5px; height:3px;
          border-radius:2px; background:var(--brand-gradient); }
/* On the Sprint 466 tabbed pages there is no title row — the chip docks at the
   left edge of the sticky .pb-tabs strip instead. */
.pb-tabs .ph-back { margin-right: 6px; }
/* Labels drop below 1500px — measured on the map's worst-case row (CLAUDE.md:
   the breakpoint tracks the title width). One shared breakpoint so chips
   behave identically on every page. */
@media (max-width: 1500px) {
  .ph-lbl { display: none; }
  .ph-link { padding: 0 9px; }
  .ph-back { padding: 0 9px; }
}
@media (max-width: 980px) {
  .ph-links { gap: 4px; }
  .ph-link { padding: 0 7px; }
}

/* Reusable accent helpers — a full-width strip and a vertical section tick. */
.brand-accent-bar { height: 3px; border-radius: 2px; background: var(--brand-gradient); border: 0; }
.section-tick { display: inline-block; width: 5px; height: 1em; border-radius: 2px;
  background: var(--brand-gradient); vertical-align: -2px; margin-right: 9px; }

/* Per-column filter row in data tables -- used by Projects, Service
   Records (Sprint 30), and any future list page that wants Projects-
   style narrowing. Selects + text inputs share the same look so a
   "Filter…" field next to dropdowns stays visually consistent. */
table.data thead tr.col-filters th { background:#fff; padding:6px 8px; }
table.data thead tr.col-filters select,
table.data thead tr.col-filters input {
  width:100%; min-width:0; box-sizing:border-box;
  font-size:12px; padding:4px 6px;
  border:1px solid var(--border); border-radius:6px; background:#fff;
}

/* Sprint 507 (fixed 508): shared column-resize handle (assets/js/col-resize.js)
   — a drag strip pinned to a header cell's right edge, added by the module at
   mount time, not baked into markup. It draws a faint separator AT REST: the
   first version was invisible until hover, which made the feature impossible
   to discover — people dragged the header text instead and concluded nothing
   was resizable. Colour is `currentColor`, so it inherits the header's own ink
   (white on a gradient banner, navy on a plain header) and needs no per-table
   override. */
.col-resize-handle { position:absolute; top:0; right:0; height:100%; width:8px;
  cursor:col-resize; z-index:5; touch-action:none;
  display:flex; align-items:center; justify-content:center; }
/* Sprint 863: a 1px line at .34 over the brand-gradient caption row is below
   the threshold of perception — measured, not guessed: the operator reported
   the columns as "not adjustable" while the drag worked perfectly. The skill's
   own rule is that a handle invisible until hover is undiscoverable, and this
   one met the letter of it while failing the point. 2px at .55 and full height
   reads as a divider between columns, which is what it is. Hover/drag states
   below are unchanged, so the affordance still answers to the pointer. */
.col-resize-handle::before { content:''; width:2px; height:100%; border-radius:1px;
  background:currentColor; opacity:.55; transition:opacity .12s, width .12s; }
.col-resize-handle:hover::before,
.col-resize-handle.dragging::before { width:3px; height:100%; opacity:.95; }
/* Under fixed layout a caption longer than its column would spill into the
   next one, so a resized table ellipsises its headers (the handle sits at
   right:0, INSIDE the cell, so this never clips it). Body cells stay each
   table's own business — table.rv already ellipsises its name column. */
table.col-resized thead th { overflow:hidden; text-overflow:ellipsis; }

/* Sprint 508: the filter row closes with a rule across the full table width,
   so the header block (captions + filters) reads as one unit separated from
   the data. Same 2px navy the grand-total row uses to close a table. */
table.data thead tr.col-filters th { border-bottom:2px solid var(--verti-navy); }

/* ==========================================================================
   THE TABLE STANDARD — the shared header block (assets/js/data-table.js)
   Sprint 512. Operator-approved from the interactive mockup. Every rule here
   animates ONLY transform/opacity (plus colour on single elements) — the
   compositor does those for free, and animating layout across a list is
   exactly where a table stutters. `prefers-reduced-motion` kills all of it.
   ========================================================================== */

/* --- caption row: label rail + a FIXED glyph rail ------------------------- */
/* The caption never wraps. A two-word caption used to push the sort glyph onto
   its own line, which is what made the old header read ragged; columns are
   resizable now, so widening is the answer, not wrapping. */
/* The module PAINTS ITS OWN caption row. Relying on `table.data thead
   tr:first-child` for the banner only works on tables that wear `.data` —
   items.html's catalog is `table.itt`, so its captions came out dark-on-white
   with no banner at all. A shared component cannot assume the host's class.
   The gradient goes on the ROW (Sprint 394: per-cell restarts the ramp at every
   column); a page whose header is STICKY overrides this with a flat opaque
   cell, because a sticky TH leaves the row's background behind (see §6). */
table thead tr.dt-caps { background-image: var(--brand-gradient); }
table thead tr.dt-caps th { background: transparent; color: #fff; }

/* NO `position` here on purpose. col-resize.js sets `relative` on a header
   only when its computed position is `static`, which is the correct, targeted
   behaviour — the handle needs a containing block, nothing more. Declaring it
   in CSS looked equivalent and was not: `table thead tr.dt-caps th` is FOUR
   elements + a class (0,0,1,4) and beat a page's own
   `table.data thead th { position:sticky }` (0,0,1,3). The page's `top:106px`
   then applied to a `relative` box and shoved the whole caption row 106px DOWN,
   onto the data. Same specificity trap as Sprint 513, from the other side: the
   shared component must not out-specify the page it is a guest in. */
table thead tr.dt-caps th { overflow:hidden; cursor:pointer;
  user-select:none; transition:opacity 150ms var(--ease-enter); }
.dt-th { display:grid; grid-template-columns:minmax(0,1fr) 12px; align-items:center;
  gap:7px; height:100%; }
table thead tr.dt-caps th.num .dt-cap { text-align:right; }
.dt-cap { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* the "this column is filtered" dot — the quiet half of the two-place signal */
.dt-cap .dt-dot { display:inline-block; width:5px; height:5px; border-radius:50%;
  background:currentColor; margin-left:5px; vertical-align:middle;
  animation:dt-pop var(--dur-pill) var(--ease-spring) both; }
@keyframes dt-pop { from { transform:scale(0); opacity:0 } to { transform:scale(1); opacity:1 } }

.dt-sort { position:relative; width:12px; height:14px; flex:none; opacity:.4;
  transition:opacity var(--dur-pill) var(--ease-spring); }
.dt-sort i { position:absolute; left:50%; width:0; height:0; margin-left:-4px;
  border-left:4px solid transparent; border-right:4px solid transparent;
  transition:transform var(--dur-pill) var(--ease-spring),
             opacity var(--dur-pill) var(--ease-spring); }
.dt-sort i.up   { top:1px;    border-bottom:4.5px solid currentColor; }
.dt-sort i.down { bottom:1px; border-top:4.5px solid currentColor; }
.dt-sort.on { opacity:1; }
tr.dt-caps th:hover .dt-sort:not(.on) { opacity:.8; }
th.dt-asc  .dt-sort i.down { opacity:0; transform:translateY(4px) scale(.6); }
th.dt-asc  .dt-sort i.up   { transform:translateY(4px) scale(1.18); }
th.dt-desc .dt-sort i.up   { opacity:0; transform:translateY(-4px) scale(.6); }
th.dt-desc .dt-sort i.down { transform:translateY(-4px) scale(1.18); }
/* the sorted column reads at a glance — the arrow alone is too quiet */
table thead tr.dt-caps th.dt-sorted::after { content:''; position:absolute; inset:0;
  background:rgba(255,255,255,.17); pointer-events:none;
  animation:dt-wash var(--dur-pill) var(--ease-spring) both; }
@keyframes dt-wash { from { opacity:0 } to { opacity:1 } }
th.dt-lift { opacity:.28; }   /* the column being dragged to a new position */

/* --- group row (option A, operator 14 Sep 2026) --------------------------- */
/* A matrix whose columns come in families — users.html's roles by department,
   sales-offer-pricing's Factory / Costs / Calc, installation-followup's phases —
   carries a GROUP row above its captions (data-table.js, a column's `group`).
   The module emits the row only when a visible column names a group, so no
   other table ever matches a rule here. It wears the caption row's ribbon, so an
   unstyled grouped header reads as one block; each of the three matrices paints
   its own flat header on top, as it always has (design-system.md: dense
   cross-tab matrices are deliberate flat-header exceptions).
   A caption whose column is in no group is LIFTED into this row with rowspan=2
   (users.html's Page corner). It is still a caption, so it gets a caption's
   behaviour — the clip, the cursor, the numeric alignment and the sorted wash
   the tr.dt-caps rules above give every other caption. */
table thead tr.dt-groups { background-image: var(--brand-gradient); }
table thead tr.dt-groups th { background: transparent; color: #fff; white-space:nowrap; }
table thead tr.dt-groups th[data-ck] { overflow:hidden; cursor:pointer;
  user-select:none; transition:opacity 150ms var(--ease-enter); }
table thead tr.dt-groups th.num .dt-cap { text-align:right; }
tr.dt-groups th:hover .dt-sort:not(.on) { opacity:.8; }
table thead tr.dt-groups th.dt-sorted::after { content:''; position:absolute; inset:0;
  background:rgba(255,255,255,.17); pointer-events:none;
  animation:dt-wash var(--dur-pill) var(--ease-spring) both; }

/* --- filter row ----------------------------------------------------------- */
/* Self-contained on purpose. This row deliberately does NOT also wear
   `col-filters`: that older class is styled by `table.data thead tr.col-filters
   input`, which outranks `.dt-fx input` on specificity, and wearing both let the
   old `padding:4px 6px` win — which slid the placeholder underneath the
   magnifier. A shared component styles itself through its own class only. */
table thead tr.dt-filters th { background:#fff; padding:6px 7px;
  border-bottom:2px solid var(--verti-navy); }
/* Sprint table-filter-fit (operator, 14 Sep 2026): the filter row SIZES NOTHING.
   col-resize.js takes it out of every width probe, so a column is as wide as its
   caption and its cells, and every control here FILLS that column — `min-width:0`
   on each level, or a flex item's automatic minimum (an input's ~20-character
   default, a chip's label) would push back out of the cell it was given.
   The search box is a size CONTAINER so it can answer its own width: below 64px
   the magnifier goes and the side padding shrinks (the @container rules after
   .dt-x), which keeps a text area to type in at every width a column can have. */
.dt-fx { position:relative; display:flex; align-items:center; height:28px;
  min-width:0; container-type:inline-size; }
.dt-fx input { width:100%; min-width:0; box-sizing:border-box; font:inherit; font-size:11.5px;
  height:28px; padding:0 24px 0 26px; color:var(--text); text-overflow:ellipsis;
  border:1px solid var(--border); border-radius:8px; background:#F7F9FC;
  transition:border-color 170ms var(--ease-enter), background 170ms var(--ease-enter),
             box-shadow 170ms var(--ease-enter); }
.dt-fx input::placeholder { color:var(--muted); }
.dt-fx input:focus { outline:none; border-color:var(--verti-navy); background:#fff;
  box-shadow:0 0 0 3px rgba(var(--verti-navy-rgb), .15); }
.dt-mag { position:absolute; left:8px; width:12px; height:12px; color:var(--muted);
  pointer-events:none; transition:color 170ms var(--ease-enter),
                                  transform 260ms var(--ease-spring); }
.dt-fx input:focus ~ .dt-mag { color:var(--verti-navy); transform:scale(1.12); }
.dt-fx.has input { border-color:var(--verti-navy); background:#fff; font-weight:650; }
.dt-x { position:absolute; right:6px; width:15px; height:15px; padding:0; border:0;
  border-radius:999px; background:var(--muted); color:#fff; cursor:pointer;
  display:none; place-items:center; font-size:9px; line-height:1;
  animation:dt-pop 200ms var(--ease-spring) both;
  transition:background 140ms var(--ease-enter), transform 160ms var(--ease-spring); }
.dt-fx.has .dt-x { display:grid; }
.dt-x:hover { background:#B91C1C; transform:rotate(90deg); }
/* A NARROW search box stays typeable (sprint table-filter-fit). The resting
   padding (26px left for the magnifier, 24px right for the clear button) plus
   the border leaves NO text area in a box under ~52px, and a column may be as
   narrow as its caption now — or 40px if the user drags it there. Measured from
   the box itself, so a dragged column, a phone and a tr/az caption all get the
   same answer. The two steps: under 64px the magnifier goes and the left side
   shrinks to 6px; under 44px the clear button goes too (the badge above the
   table still clears it) and both sides are 6px. */
@container (max-width: 64px) {
  .dt-fx .dt-mag { display:none; }
  .dt-fx input { padding:0 6px; }
  .dt-fx.has input { padding:0 20px 0 6px; }
}
@container (max-width: 44px) {
  .dt-fx.has .dt-x { display:none; }
  .dt-fx.has input { padding:0 6px; }
}

/* a chip stands in for a filter that needs more than a text box (range, list) */
.dt-chip { width:100%; min-width:0; height:28px; box-sizing:border-box; display:flex;
  align-items:center; gap:6px; padding:0 8px; text-align:left; cursor:pointer;
  font:inherit; font-size:11.5px; color:var(--muted); background:#F7F9FC;
  border:1px solid var(--border); border-radius:8px; overflow:hidden;
  font-variant-numeric:tabular-nums;
  transition:border-color 170ms var(--ease-enter), background 170ms var(--ease-enter),
             transform 160ms var(--ease-spring); }
.dt-chip:hover { border-color:var(--verti-navy); background:#fff; }
.dt-chip:active { transform:scale(.97); }
/* Keyboard focus had no ring at all (only :hover was styled). INSET, because
   the filter-row th clips its overflow under a resized table (the
   `table.col-resized thead th` rule above) and an outside ring is cut away. */
.dt-chip:focus-visible { outline:2px solid var(--verti-navy); outline-offset:-2px;
  border-color:var(--verti-navy); background:#fff; }
.dt-chip .dt-lbl { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The icon-only chip (sprint table-filter-fit, operator 14 Sep 2026): a chip
   whose label cannot fit its column — "Min – Max" under "Qty", "Başlangıç –
   Bitiş" under a date — shows a funnel and the chevron instead of a cut word,
   and its title carries the label. data-table.js fitChips() decides it from a
   measurement, for list chips too; an ACTIVE chip first tries its value cut at
   a whole token ("≥ …") and only then falls back to this. `.dt-ico-min` is the
   last step, for a column dragged to the 40px floor: the chevron goes so the
   funnel is not clipped. */
.dt-chip .dt-fun { display:none; flex:1; min-width:0; align-items:center; }
.dt-chip .dt-fun svg { width:12px; height:12px; flex:none; }
.dt-chip.dt-ico .dt-lbl { display:none; }
.dt-chip.dt-ico .dt-fun { display:flex; }
.dt-chip.dt-ico-min { padding:0; justify-content:center; }
.dt-chip.dt-ico-min .dt-fun { flex:none; }
.dt-chip.dt-ico-min .dt-cv { display:none; }
.dt-cv { width:11px; height:11px; flex:none; opacity:.7;
  transition:transform var(--dur-pill) var(--ease-spring); }
.dt-chip.open .dt-cv { transform:rotate(180deg); }
.dt-chip.has { border-color:var(--verti-navy); background:#fff; color:var(--verti-navy); font-weight:650; }
/* an unfilterable column keeps an EMPTY cell — an honest signal, not a dead box */
.dt-nofilter { height:28px; display:flex; align-items:center; justify-content:center; }
.dt-nofilter span { width:14px; height:1.5px; border-radius:1px; background:var(--border); }

/* --- toolbar: Columns button + active-filter badges ----------------------- */
/* One row shared with whatever the page already prints there (a row count, a
   note). A toolbar on a line of its own spends a whole row of vertical space on
   one button — on a dense list that is a row of data the user cannot see. */
.dt-bar { display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin:0 0 8px; }
.dt-bar .dt-left { flex:1 1 auto; min-width:0; }
/* Sprint 529 — badges on the SAME row as the Columns button, for a page whose
   table sits under a KPI card with room to spare. The left content no longer
   grows to fill the bar (that would shove the badges to the right edge, away
   from the summary they qualify); the button is pinned right instead. */
.dt-bar-inline .dt-left { flex:0 1 auto; }
.dt-bar-inline > [data-dtcols] { margin-left:auto; }
/* In a row, the badges' reveal must not change the bar's height — a badge is
   shorter than the button beside it, so the strip grows inside the line and
   nothing below it moves. Its vertical padding would have undone exactly that. */
.dt-bar-inline .dt-badges.on { padding:0; }
.dt-btn { position:relative; display:inline-flex; align-items:center; gap:7px;
  padding:7px 13px; border-radius:9px; cursor:pointer; font:inherit;
  font-size:12.5px; font-weight:650; color:var(--verti-navy);
  background:#fff; border:1px solid var(--border);
  transition:border-color 160ms var(--ease-enter), background 160ms var(--ease-enter),
             transform 160ms var(--ease-spring); }
.dt-btn:hover { border-color:var(--verti-navy); background:var(--verti-navy-50); }
.dt-btn:active { transform:scale(.965); }
.dt-ico { width:14px; height:14px; flex:none; display:block; }
/* how many columns the user has hidden — absent means "table as shipped" */
.dt-dotn { position:absolute; top:-4px; right:-4px; min-width:17px; height:17px;
  padding:0 4px; box-sizing:border-box; border-radius:999px; background:var(--tke-orange);
  color:#fff; font-size:10px; font-weight:800; display:grid; place-items:center;
  animation:dt-pop var(--dur-pill) var(--ease-spring) both; }

/* The loud half of the two-place filter signal. The field failure is always the
   same — a forgotten filter read as an empty table — so an active filter is
   never only a dot on a header. */
.dt-badges { display:flex; align-items:center; gap:7px; flex-wrap:wrap;
  max-height:0; opacity:0; overflow:hidden; padding:0;
  transition:max-height var(--dur-pill) var(--ease-spring),
             opacity var(--dur-out) var(--ease-enter),
             padding var(--dur-pill) var(--ease-spring); }
.dt-badges.on { max-height:140px; opacity:1; padding:10px 0 2px; }
.dt-badge { display:inline-flex; align-items:center; gap:7px; padding:4px 5px 4px 11px;
  border-radius:999px; font-size:11.5px; font-weight:650; color:var(--verti-navy);
  background:var(--verti-navy-50); border:1px solid var(--border);
  animation:dt-badge-in var(--dur-pill) var(--ease-spring) both; }
@keyframes dt-badge-in { from { opacity:0; transform:translateY(4px) scale(.92) }
                         to   { opacity:1; transform:none } }
.dt-badge .k { color:var(--muted); font-weight:600; }
.dt-badge button { width:17px; height:17px; padding:0; border:0; cursor:pointer;
  border-radius:999px; background:none; color:var(--muted);
  display:grid; place-items:center;
  transition:background 140ms var(--ease-enter), color 140ms var(--ease-enter),
             transform 140ms var(--ease-spring); }
.dt-badge button:hover { background:#B91C1C; color:#fff; transform:rotate(90deg); }
.dt-badge.clear { background:none; border-style:dashed; cursor:pointer;
  padding:4px 11px; color:var(--muted); }
.dt-badge.clear:hover { color:#B91C1C; border-color:#B91C1C; }

/* --- popovers ------------------------------------------------------------- */
.dt-veil { position:fixed; inset:0; z-index:79; }
.dt-pop { position:fixed; z-index:80; min-width:212px; padding:9px;
  background:#fff; border:1px solid var(--border); border-radius:13px;
  box-shadow:0 2px 6px rgba(var(--verti-navy-rgb), .10), 0 18px 44px rgba(var(--verti-navy-rgb), .20);
  transform-origin:top left; animation:dt-pop-in 230ms var(--ease-spring) both; }
@keyframes dt-pop-in { from { opacity:0; transform:translateY(-7px) scale(.95) }
                       to   { opacity:1; transform:none } }
.dt-ph { padding:3px 7px 8px; font-size:10px; font-weight:700; letter-spacing:.13em;
  text-transform:uppercase; color:var(--muted); }
.dt-row2 { display:flex; gap:7px; padding:0 4px 4px; }
.dt-pop input { width:100%; box-sizing:border-box; font:inherit; font-size:12px;
  padding:7px 9px; border:1px solid var(--border); border-radius:8px;
  background:#F7F9FC; color:var(--text);
  transition:border-color 160ms var(--ease-enter), box-shadow 160ms var(--ease-enter); }
.dt-pop input:focus { outline:none; border-color:var(--verti-navy); background:#fff;
  box-shadow:0 0 0 3px rgba(var(--verti-navy-rgb), .15); }
.dt-opts { display:grid; gap:1px; max-height:260px; overflow-y:auto; }
.dt-opt { display:flex; align-items:center; gap:9px; width:100%; padding:7px 8px;
  border:0; border-radius:8px; background:none; cursor:pointer; text-align:left;
  font-family:inherit; font-size:12.5px; color:var(--text);
  transition:background 130ms var(--ease-enter); }
.dt-opt:hover { background:var(--verti-navy-50); }
.dt-box { width:16px; height:16px; flex:none; border-radius:5px; background:#fff;
  border:1.5px solid var(--border); display:grid; place-items:center;
  transition:background var(--dur-pill) var(--ease-spring),
             border-color var(--dur-pill) var(--ease-spring); }
.dt-opt.on .dt-box { background:var(--verti-navy); border-color:var(--verti-navy); }
/* the tick DRAWS itself rather than blinking on — the one flourish worth its cost */
.dt-box svg { width:10px; height:10px; color:#fff; stroke-dasharray:16;
  stroke-dashoffset:16; transition:stroke-dashoffset 240ms var(--ease-spring); }
.dt-opt.on .dt-box svg { stroke-dashoffset:0; }
/* sprint catalog-compare — the Columns popover's SECTION headings and the
   search that filters the list. `.dt-opt` declares `display:flex`, and an
   author declaration beats the UA sheet's `[hidden] { display:none }` whatever
   its specificity (the Sprint 718 trap), so hiding a filtered-out entry has to
   be said out loud here. The heading reuses `.dt-ph` and only adds the rule
   above it. */
.dt-ph.grp { margin-top:6px; padding-top:8px; border-top:1px solid var(--border);
  color:var(--verti-navy); }
.dt-opt[hidden], .dt-ph[hidden] { display:none !important; }
.dt-nm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dt-ct { font-size:10.5px; color:var(--muted); }
.dt-opt.locked { opacity:.45; cursor:not-allowed; }
.dt-pf { display:flex; gap:7px; margin-top:5px; padding:8px 4px 2px;
  border-top:1px solid var(--border); }
.dt-pf button { flex:1; padding:7px; border-radius:8px; cursor:pointer; font:inherit;
  font-size:11.5px; font-weight:650; color:var(--verti-navy);
  background:#fff; border:1px solid var(--border);
  transition:background 140ms var(--ease-enter), transform 140ms var(--ease-spring); }
.dt-pf button:active { transform:scale(.97); }
.dt-pf button.pri { background:var(--verti-navy); border-color:var(--verti-navy); color:#fff; }

/* --- column reorder chrome ------------------------------------------------ */
.dt-ghost { position:fixed; top:0; left:0; z-index:90; display:none;
  padding:8px 14px; border-radius:10px; background:var(--verti-navy); color:#fff;
  font-size:10.5px; font-weight:700; letter-spacing:.055em; text-transform:uppercase;
  pointer-events:none; box-shadow:0 12px 30px rgba(16,24,52,.4);
  animation:dt-ghost-in 180ms var(--ease-spring) both; }
@keyframes dt-ghost-in { from { opacity:0; transform:scale(.9) } to { opacity:1 } }
/* deliberately un-transitioned: in direct manipulation, lag reads as a bug */
.dt-drop { position:absolute; top:0; bottom:0; width:3px; border-radius:2px;
  background:var(--tke-orange); display:none; pointer-events:none; z-index:10;
  box-shadow:0 0 0 4px rgba(232,90,42,.18); }

@media print { .dt-bar, .dt-badges, .dt-filters, .col-resize-handle { display:none !important; } }
/* Sprint table-fit: and the WIDTHS go with them. A resized table carries an
   explicit px total and a <colgroup> measured for the screen it is on — on paper
   that total is simply wrong, and it is what clipped an 11-column list off the
   right edge of an A4 landscape sheet. The sheet is a different container, so
   hand the question back to the browser: 100% of it, automatic layout, and <col>
   widths that state nothing. The handles are already hidden by the rule above,
   so nothing here has to allow for them. */
@media print {
  table.col-resized { width:100% !important; table-layout:auto !important; }
  table.col-resized colgroup col { width:auto !important; }
}

/* ---------- Card ---------- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}
.card-pad { padding: 18px; }

/* ---------- Toolbar ---------- */
.toolbar {
  display: flex; gap: 10px; flex-wrap: wrap;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.toolbar .search,
.toolbar select {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  font: inherit;
}
.toolbar .search { flex: 1; min-width: 200px; }
.toolbar select  { min-width: 140px; }
.toolbar .search:focus,
.toolbar select:focus { outline: none; border-color: var(--verti-navy); }

/* ---------- Tables (responsive: cards on mobile) ---------- */
/* Sprint 199: two-brand gradient line across the top of every data table. */
.table-wrap { overflow-x: auto; border-top: 3px solid; border-image: var(--brand-gradient) 1; }
table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}
table.data th, table.data td {
  text-align: left;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
table.data th {
  font-size: 12px; text-transform: uppercase;
  letter-spacing: 0.4px; color: var(--muted);
  background: #FAFBFD;
}
/* Sprint 200: the main header row carries the two-brand gradient itself
   (continuous across the row — the gradient sits on the <tr>, cells are
   transparent so it shows through). White labels. The per-column filter
   row keeps its light style. */
/* Sprint 200: every table's caption header is a continuous two-brand gradient
   banner (gradient on the <tr>, cells transparent, white labels). Pages with
   an in-header column-filter row keep that filter row on a clean WHITE
   background just below, so it reads as part of the table. */
table.data thead tr:first-child { background-image: var(--brand-gradient); }
table.data thead tr:first-child th { background: transparent; color: #fff; }
/* The top gradient line above the table is redundant under a gradient header. */
.table-wrap { border-top: 0; }
/* Sprint 200 follow-up: default table cell text in a calm dark grey — the
   navy reading colour was tiring in dense lists. Coloured badges / pills
   inside cells still override via higher specificity. */
table.data td { color: #374151; }
table.data tbody tr:hover { background: #FBFCFE; }
/* Contract-end alerts: warn = ends within 30 days, danger = expired. */
table.data tbody tr.row-warn,
table.data tbody tr.row-warn:hover   { background: #FBF2DA; }
table.data tbody tr.row-danger,
table.data tbody tr.row-danger:hover { background: #FBE9E9; }
.actions-cell { display: flex; gap: 6px; flex-wrap: wrap; }

.badge {
  display: inline-block;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 11px; font-weight: 700;
  letter-spacing: 0.3px; text-transform: uppercase;
}
.badge-admin      { background: #FCE3D9; color: #9C3712; }
.badge-manager    { background: #E7E0F2; color: #4F2D6F; }
.badge-technician { background: #DCEAF7; color: #214E7E; }
.badge-viewer     { background: #E8ECF2; color: #495568; }
.badge-status-on      { background: #D9F1E4; color: #1E6C49; }
.badge-status-off     { background: #F0E2E2; color: #7A2828; }
.badge-status-install { background: #DCEAF7; color: var(--verti-navy); }
.badge-agreement-R { background: #DCEAF7; color: #214E7E; }
.badge-agreement-G { background: #FCE3D9; color: #9C3712; }
/* Sprint 748 — official like R, but 0% VAT. Kept in the R family's blue and set
   apart by an outline rather than a new hue: it IS an official contract. */
.badge-agreement-R0 { background: #DCEAF7; color: #214E7E; box-shadow: inset 0 0 0 1px #7FA8E6; }
.badge-type-elevator     { background: #DCEAF7; color: #214E7E; }
.badge-type-escalator    { background: #E7E0F2; color: #4F2D6F; }
.badge-type-mww          { background: #FCE3D9; color: #9C3712; }
.badge-type-other        { background: #E8ECF2; color: #495568; }

/* Service-record work types */
.badge-work-maintenance         { background: #DCEAF7; color: #214E7E; }
.badge-work-maintenance_support { background: #E1ECF7; color: #2F5B85; }
.badge-work-fault               { background: #F7E7EB; color: #A6394F; }
.badge-work-fault_support       { background: #FCE3D9; color: #9C3712; }
.badge-work-correction          { background: #F7E9E1; color: #B8623F; }
.badge-work-part_replacement    { background: #F3E7F1; color: #7D3A72; }
.badge-work-inspection          { background: #EEEAF7; color: #5B4A8A; }
.badge-work-meeting             { background: #E8ECF2; color: #495568; }

/* ---------- Modal ---------- */
/* Sprint 248: shared message box (showMessage in api.js) — a centered,
   blocking, localized result/error dialog that stays until the user taps OK.
   Replaces transient toasts for messages the user must actually read. */
.msgbox-overlay {
  position: fixed; inset: 0; z-index: 10050;
  background: rgba(15, 27, 51, 0.50);
  display: flex; align-items: center; justify-content: center; padding: 22px;
  animation: msgboxIn 0.12s ease-out;
}
@keyframes msgboxIn { from { opacity: 0; } to { opacity: 1; } }
.msgbox {
  width: 100%; max-width: 340px; background: #fff; border-radius: 16px;
  overflow: hidden; box-shadow: 0 18px 44px rgba(15, 27, 51, 0.38);
}
.msgbox-cap { height: 6px; background: var(--brand-gradient); }
.msgbox-body { padding: 22px 22px 18px; text-align: center; }
.msgbox-ic {
  width: 54px; height: 54px; border-radius: 50%; margin: 2px auto 13px;
  display: flex; align-items: center; justify-content: center;
  font-size: 26px; font-weight: 800;
}
.msgbox-ic.success { background: #E6F4EA; color: #15803D; }
.msgbox-ic.error   { background: #FEECEC; color: #B91C1C; }
.msgbox-ic.info    { background: #EFF6FF; color: #1D4ED8; }
.msgbox-title { font-size: 16px; font-weight: 800; color: var(--verti-navy); margin-bottom: 6px; }
.msgbox-msg { font-size: 13.5px; color: #475569; line-height: 1.5; }
.msgbox-ok {
  margin-top: 17px; width: 100%; border: 0; cursor: pointer;
  background: var(--brand-gradient); color: #fff;
  font-size: 14px; font-weight: 800; padding: 12px; border-radius: 11px;
}
.msgbox-ok:active { transform: scale(.98); }

/* Sprint 255: hidden manager exit PIN pad (triple-tap the mobile-home V mark).
   Built by assets/js/pin-exit.js; mirrors the .msgbox modal look + the home-
   screen widget's PIN screen. */
.pinx-overlay {
  position: fixed; inset: 0; z-index: 10060;
  background: rgba(15, 27, 51, 0.55);
  display: flex; align-items: center; justify-content: center; padding: 22px;
  animation: msgboxIn 0.12s ease-out;
}
.pinx {
  width: 100%; max-width: 300px; background: #fff; border-radius: 18px;
  overflow: hidden; box-shadow: 0 20px 50px rgba(15, 27, 51, 0.42);
}
.pinx-cap { height: 5px; background: var(--brand-gradient); }
.pinx-head { display: flex; align-items: center; gap: 9px; padding: 15px 16px 4px; }
.pinx-lock {
  width: 34px; height: 34px; border-radius: 10px; flex: 0 0 auto;
  background: #FDECE5; color: var(--tke-orange);
  display: grid; place-items: center; font-size: 16px;
}
.pinx-title { font-size: 17px; font-weight: 800; color: var(--verti-navy); }
.pinx-sub { font-size: 12px; color: var(--muted); padding: 0 16px 10px; }
.pinx-sub.err { color: #B91C1C; font-weight: 700; }
.pinx-dots { display: flex; justify-content: center; gap: 14px; margin: 4px 0 14px; }
.pinx-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #C3CDDC; }
.pinx-dot.on { background: var(--verti-navy); border-color: var(--verti-navy); }
.pinx-pad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 0 16px 12px; }
.pinx-key {
  height: 46px; border-radius: 11px; background: #F1F4F9; border: 1px solid var(--border);
  font-size: 20px; font-weight: 800; color: var(--verti-navy); cursor: pointer;
}
.pinx-key:active { transform: scale(.96); }
.pinx-key.muted { background: transparent; border: 0; color: var(--muted); font-size: 17px; font-weight: 700; }
.pinx-key.muted:disabled { cursor: default; }
.pinx-foot { display: flex; padding: 4px 16px 16px; }
.pinx-cancel {
  flex: 1; height: 42px; border-radius: 11px; cursor: pointer;
  background: #fff; border: 1px solid var(--border); color: var(--muted);
  font-size: 14px; font-weight: 800;
}
.pinx-cancel:active { transform: scale(.98); }
.pinx.shake { animation: pinxShake 0.34s ease; }
@keyframes pinxShake {
  0%, 100% { transform: translateX(0); }
  20%, 60% { transform: translateX(-7px); }
  40%, 80% { transform: translateX(7px); }
}

/* Sprint 290 — impersonation ("act as user"): top banner + user picker. */
.imp-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100000;
  height: 34px; display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 0 12px; background: var(--verti-navy); color: #fff;
  box-shadow: 0 2px 8px rgba(15, 27, 51, 0.3);
}
.imp-banner::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: var(--brand-gradient); }
.imp-b-txt { flex: 0 0 auto; font-size: 12px; font-weight: 700; white-space: nowrap; }
.imp-b-txt i { color: #FFD9C7; margin-right: 5px; }
.imp-b-sel {
  flex: 1 1 auto; min-width: 0; max-width: 52%; margin: 0 auto 0 2px;
  height: 24px; padding: 0 8px; border-radius: 8px; cursor: pointer;
  background: rgba(255, 255, 255, 0.16); color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.4);
  font-size: 12px; font-weight: 700;
}
.imp-b-sel option { color: var(--verti-navy); background: #fff; }
.imp-b-back {
  flex: 0 0 auto; background: var(--tke-orange); color: #fff; border: 0; cursor: pointer;
  font-size: 11px; font-weight: 800; padding: 5px 11px; border-radius: 8px;
}
.imp-b-back:active { transform: scale(.97); }
/* make room so the fixed banner never covers the topbar (sticky at top:0) */
body.imp-on { padding-top: 34px; }
body.imp-on .topbar, body.imp-on .mobile-topbar { top: 34px; }

/* Sprint 445 (operator design; supersedes the Sprint-443 mobile strip): on
   mobile NOTHING is added to the layout while impersonating — the topbar
   avatar becomes the switch button (orange ring + ⇄ corner badge, single tap
   opens the quick-switch panel) and a small ↩ return button appears to its
   right. Zero layout shift = zero overlap, whatever the page structure. */
.mobile-user-chip .avatar.imp-av-switch {
  box-shadow: 0 0 0 2.5px var(--tke-orange);
  position: relative; cursor: pointer;
}
.mobile-user-chip .avatar.imp-av-switch::after {
  content: "\21c4";                       /* ⇄ — no icon-font dependency */
  position: absolute; right: -6px; bottom: -6px;
  width: 16px; height: 16px; border-radius: 50%;
  background: var(--tke-orange); color: #fff;
  font-size: 10px; line-height: 16px; text-align: center; font-weight: 800;
}
.mobile-user-chip .imp-ret-ic {
  width: 30px; height: 30px; border-radius: 50%; border: 0; cursor: pointer;
  background: #fff; color: var(--verti-navy); font-size: 13px;
  margin-left: 8px; flex: 0 0 auto;
  box-shadow: 0 1px 5px rgba(15, 27, 51, 0.35);
}

/* Sprint 443 — the return / quick-switch panel (triple-tap while acting). */
.imp-home {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: calc(100% - 32px); margin: 10px 16px 4px; padding: 11px;
  border: 0; border-radius: 12px; background-image: var(--brand-gradient);
  color: #fff; font-size: 14px; font-weight: 800; cursor: pointer;
}
.imp-qs-lbl {
  margin: 10px 18px 4px; font-size: 10.5px; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.05em;
}

.imp-card.imp-pick { max-width: 360px; }
.imp-card .imp-x {
  flex: 0 0 auto; margin-left: auto; width: 30px; height: 30px; border-radius: 8px;
  background: #F1F4F9; border: 1px solid var(--border); color: var(--muted);
  font-size: 14px; font-weight: 800; cursor: pointer;
}
.imp-search {
  width: calc(100% - 32px); margin: 4px 16px 8px; padding: 9px 11px;
  border: 1px solid var(--border); border-radius: 10px; background: #F8FAFC;
  font-size: 13px; color: var(--text);
}
.imp-list { max-height: 46vh; overflow-y: auto; padding: 0 8px 12px; }
.imp-row {
  width: 100%; display: flex; align-items: center; gap: 10px; text-align: left;
  padding: 9px 8px; background: none; border: 0; border-radius: 10px; cursor: pointer;
}
.imp-row + .imp-row { border-top: 1px solid #EEF1F6; }
.imp-row:hover { background: #F4F6FA; }
.imp-av {
  flex: 0 0 auto; width: 32px; height: 32px; border-radius: 50%;
  background: var(--verti-navy); color: #fff; font-weight: 800; font-size: 12px;
  display: grid; place-items: center;
}
.imp-ut { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.imp-un { font-size: 13.5px; font-weight: 700; color: var(--verti-navy); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.imp-ur { font-size: 10.5px; color: var(--muted); text-transform: capitalize; }
.imp-go { flex: 0 0 auto; font-size: 11px; font-weight: 800; color: #fff; background: var(--verti-navy); padding: 6px 12px; border-radius: 8px; }
.imp-row.imp-going { opacity: .5; pointer-events: none; }
.imp-empty { padding: 22px 16px; text-align: center; color: var(--muted); font-size: 12.5px; }

.modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(15, 23, 42, 0.45);
  display: none; align-items: center; justify-content: center;
  /* Sprint 90: bumped to 10000 so the help modal (and every other modal)
     overlays Leaflet, which paints internal panes up to z-index 700. The
     old value (50) left the ? help modal hidden under the map on map.html. */
  z-index: 10000; padding: 16px;
}
.modal-backdrop.open { display: flex; }
.modal {
  background: #fff;
  border-radius: var(--radius);
  width: 100%;
  max-width: 480px;
  box-shadow: var(--shadow-md);
  /* Sprint 138: footer actions (Save / Close / Cancel) moved to the TOP
     of every modal so the user never has to scroll to reach them. The
     modal is a flex column — the title stays, the .modal-foot is ordered
     right under it (fixed), and the .modal-body scrolls underneath. */
  max-height: 90vh;
  display: flex; flex-direction: column; overflow: hidden;
  position: relative;
}
/* Sprint 200: two-brand gradient cap along the top of every modal (clipped
   to the rounded corners by the modal's overflow:hidden). */
.modal::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: var(--brand-gradient); z-index: 2;
}
.modal h2 {
  margin: 0;
  padding: 16px 20px;
  font-size: 18px;
  color: var(--verti-navy);
  border-bottom: 1px solid var(--border);
  order: 0; flex: 0 0 auto;
}
.modal .modal-body {
  padding: 18px 20px;
  order: 1; flex: 1 1 auto; overflow-y: auto; min-height: 0;
}
.modal .modal-foot {
  padding: 12px 20px;
  display: flex; gap: 10px; justify-content: flex-end;
  border-bottom: 1px solid var(--border);
  order: 0; flex: 0 0 auto;
}

/* ---------- Toast ---------- */
.toasts {
  /* Sprint 224: above everything — Leaflet panes/controls climb to ~700-1000
     and the map grid/panels sit at 560-601, so the old z-index:100 buried
     every toast behind the map. 100000 keeps notifications on top app-wide. */
  position: fixed; right: 16px; bottom: 16px; z-index: 100000;
  display: flex; flex-direction: column; gap: 8px;
}
.toast {
  background: #1F2A40; color: #fff;
  padding: 10px 14px; border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  font-size: 14px; max-width: 320px;
  display: flex; align-items: center; gap: 10px;
}
.toast.success { background: var(--success); }
.toast.error   { background: var(--danger); }
/* Sprint unit-duplicate: the ONE inline action a toast may carry (Undo). A real
   button, so it is reachable from the keyboard; underlined as well as boxed, so
   it does not read as an action by colour alone. */
.toast .toast-act {
  flex: 0 0 auto; margin-left: auto;
  background: rgba(255, 255, 255, .16); color: #fff;
  border: 1px solid rgba(255, 255, 255, .5); border-radius: 999px;
  padding: 2px 12px; font-size: 12.5px; font-weight: 700;
  text-decoration: underline; cursor: pointer;
}
.toast .toast-act:hover { background: rgba(255, 255, 255, .3); }

/* ---------- Empty state ---------- */
.empty {
  text-align: center; padding: 40px 16px; color: var(--muted);
}

/* ---------- Utilities ---------- */
.row { display: flex; gap: 12px; flex-wrap: wrap; }
/* Sprint 718 — the [hidden] trap (see .btn). This one was not cosmetic: the
   only `.row` in the tree toggled with [hidden] is projects.html #incRow, the
   Sprint 575 "Installation Not Completed" control, which must appear only on a
   SERVICE project. The select inside it is in the form and not disabled, so it
   always serialised, and the server gated the key on PERMISSION only — so the
   `type === 'service'` half of Sprint 575 was enforced by this dead CSS alone.
   Projects::update() now gates on type too; this rule is the other half.
   !important because .row is a shared utility that pages routinely re-declare
   through a descendant selector (`.modal-body .row`, `.sales-form .row`) in a
   page <style> parsed AFTER this file — such a rule ties on specificity and
   wins on source order. No page does that to a hidden .row today; the
   !important is what keeps that from silently becoming a defect again. */
.row[hidden] { display: none !important; }
.row .col { flex: 1; min-width: 180px; }
/* Sprint 138: inside a modal, let row columns shrink below 180px so 3-4
   fields stay on a single line instead of wrapping a field to the next
   row (the 180px floor above made narrow modals wrap). */
.modal .row .col { min-width: 0; }
.text-muted { color: var(--muted); }
.text-right { text-align: right; }
.mt-2 { margin-top: 14px; }
/* `display: revert` restores the user-agent default for the element type
   (table-cell for <th>/<td>, inline for <span>, etc.) — `display: initial`
   would force everything to `inline`, which broke the user list table. */
.hide-on-mobile { display: none; }
@media (min-width: 720px) { .hide-on-mobile { display: revert; } }

/* ---------- Inline filter bar (Reports / Raw Records) ---------- */
.filter-bar {
  display: flex; flex-wrap: wrap; gap: 8px;
  align-items: center;
}
.filter-bar .filter-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.4px;
  color: var(--muted); font-weight: 700;
  margin: 0 4px 0 4px;
}
.filter-bar input[type="date"],
.filter-bar input[type="text"],
.filter-bar select {
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  font: inherit;
}
.filter-bar input[type="date"]:focus,
.filter-bar select:focus { outline: none; border-color: var(--verti-navy); }
.filter-bar .btn,
.filter-bar .btn-sm {
  height: 36px;
  padding: 0 14px;
  font-size: 13px;
}
.filter-sep {
  width: 1px; align-self: stretch;
  background: var(--border);
  margin: 0 6px;
}

/* Icon-only buttons (Raw Records action column) */
.btn-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  cursor: pointer;
  font-size: 16px;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
  color: var(--text);
}
.btn-icon:hover { background: var(--verti-navy-50); border-color: var(--verti-navy); }
.btn-icon.btn-icon-danger { color: var(--danger); }
.btn-icon.btn-icon-danger:hover { background: #FBE9E9; border-color: var(--danger); }
.btn-icon:disabled { opacity: 0.5; cursor: not-allowed; }

/* ---------- Summary page ---------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 14px;
  margin-bottom: 20px;
}
.stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 18px 16px;
  box-shadow: var(--shadow-sm);
  position: relative;
  overflow: hidden;
}
/* Sprint 199: KPI cards carry a two-brand gradient strip across the top.
   Toned variants keep a solid colour strip to stay meaningful (orange /
   purple / green KPIs). */
.stat-card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 4px;
  background: var(--brand-gradient);
}
.stat-card.accent::before { background: var(--tke-orange); }
.stat-card.purple::before { background: var(--tke-purple); }
.stat-card.green::before  { background: var(--success); }
/* Sprint 199 — gradient "hero" KPI card (full brand gradient, white text). */
.stat-card.hero { background: var(--brand-gradient); border-color: transparent; }
.stat-card.hero::before { display: none; }
.stat-card.hero .stat-value,
.stat-card.hero .stat-label,
.stat-card.hero .stat-sub { color: #fff; }

/* Sprint 199 — brand design-system utilities (opt-in per page).
   Gradient progress meter: <div class="brand-progress"><span style="width:72%"></span></div> */
.brand-progress { height: 14px; border-radius: 7px; background: #E8ECF3; overflow: hidden; }
.brand-progress > span { display: block; height: 100%; border-radius: 7px; background: var(--brand-gradient); }
/* Gradient ring (for a % gauge or an avatar): <div class="brand-ring"><span>52%</span></div> */
.brand-ring { width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center;
  background: conic-gradient(from -90deg, #1D3360, #7A2E6B, #E85A2A); }
.brand-ring::before { content: ''; grid-area: 1 / 1; width: 44px; height: 44px; border-radius: 50%; background: var(--surface); }
.brand-ring > * { grid-area: 1 / 1; z-index: 1; font-weight: 800; font-size: 14px; color: var(--verti-navy); }
/* Active tab/pill in the brand gradient. */
.brand-tab.active { background: var(--brand-gradient); color: #fff; border-color: transparent; }
.stat-card .stat-value {
  font-size: 30px; font-weight: 800; color: var(--text);
  line-height: 1.1; margin-bottom: 4px;
}
.stat-card .stat-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--muted); font-weight: 600;
}
.stat-card .stat-sub {
  font-size: 12px; color: var(--muted); margin-top: 6px;
}

.breakdown-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}
@media (min-width: 880px) {
  .breakdown-grid { grid-template-columns: repeat(2, 1fr); }
}

.breakdown {
  display: flex; flex-direction: column; gap: 8px;
}
.bar-row {
  display: grid;
  grid-template-columns: minmax(110px, 30%) 1fr 50px;
  align-items: center;
  gap: 10px;
  font-size: 13px;
}
.bar-row .bar-label {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--text);
}
.bar-row .bar-track {
  background: var(--verti-navy-50);
  border-radius: 999px;
  height: 10px; overflow: hidden;
}
.bar-row .bar-fill {
  background: var(--verti-navy);
  height: 100%; border-radius: 999px;
  transition: width 0.4s ease;
}
.bar-row.accent .bar-fill { background: var(--tke-orange); }
.bar-row.purple .bar-fill { background: var(--tke-purple); }
.bar-row.green  .bar-fill { background: var(--success); }
.bar-row .bar-count {
  text-align: right; font-weight: 700; color: var(--text);
}

/* Spinner */
.spinner {
  width: 18px; height: 18px;
  border: 2px solid rgba(255,255,255,0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---------------- Map page ---------------- */
/* Sprint 124: the map fills the full width; the Coordinates / Missing
 * Coordinates panel is a collapsible drawer overlaying its right edge. */
.map-layout {
  position: relative;
  overflow: hidden;            /* clip the drawer when it slides off-right */
}
.map-canvas {
  height: calc(100vh - 180px);
  min-height: 420px;
  width: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--border);
  background: var(--surface);
}
.map-side {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 600;
  width: 320px;
  max-width: 86vw;
  max-height: calc(100% - 20px);
  display: flex;
  flex-direction: column;
  overflow: auto;
  transition: transform .25s ease, opacity .2s ease;
}
.map-side.collapsed {
  transform: translateX(calc(100% + 16px));
  opacity: 0;
  pointer-events: none;
}
/* Drawer handle: sits at the panel's left edge; when the panel is collapsed
 * it stays at the map's right edge (sibling combinator keys off .collapsed). */
.map-side-toggle {
  position: absolute;
  top: 10px;
  right: 338px;
  z-index: 601;
  width: 30px;
  height: 40px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #fff;
  color: var(--verti-navy);
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: right .25s ease;
}
.map-side.collapsed ~ .map-side-toggle { right: 10px; }
@media (max-width: 880px) {
  .map-side { width: 300px; }
  .map-side-toggle { right: 318px; }
  .map-side.collapsed ~ .map-side-toggle { right: 10px; }
}

/* Sprint 124: header toolbar that carries the three map controls on the
 * title row (Pick user | Live tracking | GPS events). */
.map-headrow {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}
/* Sprint 161: drag-to-reposition visual cue. The marker the user armed
   via its popup's "Edit position" link gets .vpin-edit — an amber halo
   + grab cursor — so it's obvious which pin is the one being moved. */
.leaflet-marker-icon.vpin-edit { cursor: grab; }
.leaflet-marker-icon.vpin-edit.leaflet-marker-dragging { cursor: grabbing; }
.leaflet-marker-icon.vpin-edit .vpin {
  box-shadow: 0 0 0 2px #fff, 0 0 0 4px #B45309, 0 2px 6px rgba(0,0,0,.25);
}
/* Sprint 144: GPS event marker — small circle + action glyph. Stays
   smaller than the project pin (30mm teardrop) and the live user marker
   (30mm circle) so an overlapping cluster reads as "events on top of the
   project / user". Moved out of map.html's inline <style> in Sprint 934:
   gps-timeline.html draws the same markers through
   assets/js/gps-events-layer.js, and a second copy of the rule is exactly
   the "one implementation" trap. */
.vevent { width:22px; height:22px; border-radius:50%;
  border:2px solid #fff; box-shadow:0 1px 3px rgba(0,0,0,.45);
  display:flex; align-items:center; justify-content:center; color:#fff; }
.vevent i { font-size:10px; line-height:1; }
/* Sprint 164: the focused marker (clicked on the map, in the Coordinates
   list, or as a GPS event) gets a navy halo — distinct from the amber
   edit halo above. Works for both project pins (.vpin) and event dots
   (.vevent). */
.leaflet-marker-icon.marker-selected .vpin,
.leaflet-marker-icon.marker-selected .vevent {
  box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--verti-navy), 0 2px 8px rgba(27,58,107,.4);
}
.leaflet-marker-icon.marker-selected { z-index: 10000 !important; }
/* Sprint 164: the no-location badge — GPS events that happened but carry no
   fix, so nothing can be drawn for them. Sprint 939 retired the header's Map
   Filters card and the badge moved INTO the Leaflet layer control's own overlay
   label, beside the checkbox that asked for the events; it is sized for that
   line now (the toolbar it used to sit in was 13px and roomy). The rule and its
   #gpsTool wrapper were the last two users of the .map-tool family, which went
   with the card. */
.gps-noloc-badge {
  color: #92400E; font-size: 10px; font-weight: 700; white-space: nowrap;
}
/* Sprint 164: project + unit rows inside the GPS-event popup. */
.leaflet-popup-content .gps-pop-meta {
  margin: 6px 0; padding: 6px 0; border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border); font-size: 12px; line-height: 1.6;
}
.leaflet-popup-content .gps-pop-meta i { color: var(--verti-navy); margin-right: 4px; }
.map-edit-banner {
  position: fixed;
  top: 88px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1500;
  background: #FFF7ED;
  color: #7C2D12;
  border: 1px solid #FDBA74;
  border-radius: 999px;
  padding: 6px 14px 6px 12px;
  font-size: 13px;
  display: flex;
  gap: 10px;
  align-items: center;
  box-shadow: 0 6px 18px rgba(180,83,9,.18);
}
.map-edit-banner i { color: #B45309; }
.map-edit-banner .btn { padding: 2px 10px; }
/* Sprint 161: click-to-place. While armed, the whole Leaflet pane gets
   a crosshair cursor so the user knows the next click drops the pin. */
body.map-set-location .leaflet-container,
body.map-set-location .leaflet-grab,
body.map-set-location .leaflet-clickable { cursor: crosshair !important; }
.missing-list .set-loc-link {
  display: inline-block;
  margin-top: 4px;
  font-size: 12px;
  font-weight: 600;
  color: #B45309;
  text-decoration: none;
}
.missing-list .set-loc-link:hover { text-decoration: underline; }
.missing-list .set-loc-link i { margin-right: 3px; }
.map-side .missing-list {
  list-style: none; padding: 0; margin: 0;
}
.map-side .missing-list li {
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.map-side .missing-list li:last-child { border-bottom: 0; }
.map-side .missing-list a { color: var(--verti-navy); font-weight: 600; }
.map-side .missing-list .region { color: var(--muted); font-size: 12px; }

/* Marker popup contents */
.leaflet-popup-content {
  margin: 12px 14px;
  font-family: inherit;
  font-size: 13px;
  line-height: 1.45;
  color: var(--text);
  max-width: 280px;
}
.leaflet-popup-content h3 {
  margin: 0 0 6px;
  font-size: 15px;
  color: var(--verti-navy);
}
.leaflet-popup-content .popup-region {
  color: var(--muted); font-size: 12px; margin-bottom: 8px;
}
.leaflet-popup-content .popup-notes { margin: 8px 0; }
.leaflet-popup-content .popup-notes p:first-child { margin-top: 0; }
.leaflet-popup-content .popup-notes p:last-child  { margin-bottom: 0; }
.leaflet-popup-content .popup-actions {
  margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border);
}
.leaflet-popup-content .popup-actions a {
  color: var(--verti-navy); font-weight: 600;
}
/* Sprint 161: popup actions collapsed to a row of compact icon buttons —
   tooltip carries the label so the popup stays small. */
.leaflet-popup-content .popup-icon-actions {
  display: flex; gap: 8px; align-items: center;
}
.leaflet-popup-content .popup-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; border-radius: 6px;
  background: var(--verti-navy-50); color: var(--verti-navy);
  text-decoration: none; transition: background .12s, color .12s;
}
.leaflet-popup-content .popup-icon-btn:hover {
  background: var(--verti-navy); color: #fff;
}
.leaflet-popup-content .popup-icon-btn.popup-icon-danger {
  background: #FEE2E2; color: #B91C1C;
}
.leaflet-popup-content .popup-icon-btn.popup-icon-danger:hover {
  background: #B91C1C; color: #fff;
}

/* ---------------- Chart cards (Summary / Reports / Dashboard) ------- */
.chart-box {
  min-height: 300px;
}
.chart-box .empty { text-align: center; }
/* ApexCharts inherits the page font; keep its text on-brand. */
.apexcharts-tooltip {
  border: 1px solid var(--border) !important;
  box-shadow: var(--shadow-md) !important;
}
.apexcharts-tooltip-title {
  background: var(--verti-navy-50) !important;
  border-bottom: 1px solid var(--border) !important;
  font-weight: 600;
}

/* ============================================================== */
/* Mobile actions shell (Sprint 88) — four big buttons + scanner   */
/* ============================================================== */
.mobile-body { background: var(--verti-navy-50); margin: 0; }
.mobile-shell {
  min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column;
  max-width: 480px; margin: 0 auto;
  padding: 14px 8px 24px;                            /* Sprint 278: side gap 16→8px */
  position: relative;
}
.mobile-topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding-bottom: 12px; border-bottom: 1px solid var(--border);
}
/* Sprint 459: the Sprint-89 wide-banner topbar (.mobile-brand /
   .mobile-brand-logo / .mobile-brand-tagline) was dead CSS — the topbar was
   rebuilt around .mobile-brand-mark + .mobile-wordmark long ago. Removed. */
/* Sprint 129: chip just hosts the round avatar now (the full name is
 * gone from the topbar — see mobile.html). The .avatar class handles the
 * round navy circle + initials, so this wrapper only needs to keep its
 * place in the topbar's flex row. */
.mobile-user-chip { flex-shrink: 0; display: flex; align-items: center; }

.mobile-prefill {
  display: flex; align-items: center; gap: 10px;
  margin: 14px 0 4px;
  padding: 10px 12px;
  background: #E5F4EC; color: #235E45;
  border: 1px solid #B7E1CC; border-radius: 10px;
}
.mobile-prefill > i { font-size: 20px; }
.mobile-prefill-text { flex: 1; min-width: 0; }
.mobile-prefill-title { font-weight: 700; font-size: 15px; }
.mobile-prefill-sub { font-size: 12px; color: var(--text-muted); }
.mobile-prefill-clear {
  background: none; border: 0; font-size: 22px;
  color: #235E45; cursor: pointer; line-height: 1; padding: 0 4px;
}

.mobile-actions {
  flex: 1; display: flex; flex-direction: column; gap: 14px;
  padding: 18px 0 0;
}
.mobile-action-btn {
  position: relative;
  background: #fff; border: 1px solid var(--border); border-radius: 14px;
  padding: 18px 18px 18px 22px;
  display: flex; align-items: center; gap: 16px;
  min-height: 78px;
  font-size: 18px; font-weight: 600; line-height: 1.2; font-family: inherit; color: var(--verti-navy);
  text-align: left; cursor: pointer;
  box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  transition: transform .08s ease, box-shadow .15s ease;
}
.mobile-action-btn:active { transform: scale(0.985); box-shadow: 0 1px 1px rgba(0,0,0,0.06); }
.mobile-action-btn[disabled] { opacity: 0.5; cursor: progress; }
.mobile-action-icon { font-size: 28px; color: var(--verti-navy); width: 32px; text-align: center; }
.mobile-action-label { flex: 1; }
.mobile-action-scan {
  font-size: 22px; color: #E85A2A;
  background: #FFF4EE; border-radius: 8px; padding: 6px 8px;
}
/* Sprint 127: FORM badge (pen-to-square) — sits to the LEFT of the QR
 * badge on each data-entry button. Opens the matching APK form page on
 * tap; its handler stops propagation so the parent button's QR scan
 * does not fire. */
.mobile-action-form {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 20px; color: var(--verti-navy);
  background: #DCE6F4; border-radius: 8px; padding: 6px 8px;
  cursor: pointer;
  margin-left: auto;
}
.mobile-action-form:active { transform: scale(0.95); }
.mobile-action-btn .mobile-action-form + .mobile-action-scan { margin-left: 6px; }
/* When the active-service card is on screen, S/F/P tiles are dimmed
 * but still tappable (we intercept the click to show a toast so the
 * user knows why). Photo, Map and Manual stay live. */
.mobile-action-btn.disabled-by-active { opacity: 0.45; }
.mobile-action-btn.disabled-by-active .mobile-action-icon,
.mobile-action-btn.disabled-by-active .mobile-action-label,
.mobile-action-btn.disabled-by-active .mobile-action-form,
.mobile-action-btn.disabled-by-active .mobile-action-scan {
  color: #9CA3AF !important; background: #F3F4F6 !important;
}
/* Sprint 127: notification bubbles in the mobile topbar (replacing the
 * brand logo + tagline). One per category — S/F/P — sized to be
 * thumb-friendly. Hidden via [hidden] until the user has unread items
 * in that category. */
/* Sprint 129 (revised): the V-shaped Verti mark on the far-left of the
 * mobile topbar — a navy rounded square with a white V, mirroring the APK
 * launcher icon.  Rendered as inline SVG inside this span, so size +
 * shrink-resist live here. */
.mobile-brand-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px;
  flex-shrink: 0;
  /* Sprint 283: the real VERTI V badge (circular — navy V + a thin white ring)
     replaces the inline chevron SVG. One CSS swap applies it to every mobile
     page; the original topbar size/layout is unchanged. */
  background: url('../img/verti-v-badge.png?v=977') center / contain no-repeat;
}
/* Sprint 459: the inline chevron SVG left TOPBAR_HTML (it had been display:none
   since the Sprint-283 PNG badge), and the Sprint-129 floating home FAB left
   the pages (it flashed on every load until mountMobileChrome removed it) —
   both rule sets removed. */
/* Sprint 129: common form-page H1 — coloured swatch + bold title. */
.mobile-page-h1 {
  display: flex; align-items: center; gap: 10px;
  margin: 14px 0 10px;
}
.mobile-page-swatch { width: 4px; height: 22px; border-radius: 2px; }
/* Sprint 234: the old `font: 800 18px/1.1 inherit` was an INVALID shorthand
   (inherit is not a valid family there) → the whole rule was dropped and the
   title fell back to the browser's huge default h1. Match the VERTI AIM
   wordmark (16px / 900), navy. */
.mobile-page-h1 h1 { font-size: 16px; font-weight: 900; line-height: 1.1; color: var(--verti-navy); margin: 0; }
/* Sprint 299: "new record" action in the page-title row — sits at the far
   right of the h1, title-height, brand gradient (operator's placement rule:
   every mobile page's create button lives top-right beside the title). */
.mobile-h1-action { margin-left: auto; border: 0; cursor: pointer; border-radius: 9px;
  padding: 9px 13px; font-size: 12px; font-weight: 800; color: #fff;
  /* Hotfix unit-task (operator): a <button> does not inherit the page font,
     so this one wore the browser default (Arial), whose 800 weight is Arial
     Black - a cut with no Azerbaijani Ə / ə; Chrome drew those two letters
     from Times mid-word ("Yeni İş Əlavə Et"). Measured with CDP
     getPlatformFontsForNode: Arial Black 14 glyphs + Times New Roman 2. */
  font-family: inherit;
  background: var(--brand-gradient); display: inline-flex; align-items: center;
  gap: 6px; text-decoration: none; white-space: nowrap; }
/* Sprint 718 — the [hidden] trap (see .btn). Two victims, and they fail
   differently: mobile-part.html #mpNewBtn was a ROLE leak (a non-privileged
   tech was shown the create button; the button retired in hotfix
   mp-collapse-delete), while mobile-inspection.html #miNewBtn is
   a MODE leak — its toggle sits inside `if (!id)`, so the "New" link painted
   over the detail and form views as well as the list. */
.mobile-h1-action[hidden] { display: none; }
/* Sprint 547: the DISCLOSURE variant of the same slot — a toggle rather than a
   link to another page. Only the GLYPH rotates: the "+" turns 135° into an "×",
   so the rotation says "this closes the thing it opened" while the label beside
   it names the action in words. Sprint 548 (operator): the icon-only circle
   left the action unnamed, so the button carries its label and the round
   modifier is gone. */
.mobile-h1-action.mh1-toggle { box-shadow: 0 4px 12px rgba(122,46,107,.30),
  inset 0 1px 0 rgba(255,255,255,.35); transition: filter 120ms linear; }
.mobile-h1-action.mh1-toggle:active { filter: brightness(1.08); }
.mh1-plus { display: inline-block; font-size: 17px; font-weight: 400; line-height: 1;
  transition: transform var(--dur-in) var(--ease-spring); }
.mobile-h1-action.mh1-toggle[aria-expanded="true"] .mh1-plus { transform: rotate(135deg); }

/* Sprint 547 — the shared mobile DISCLOSURE fold.
   `grid-template-rows: 0fr → 1fr` is the only way to animate to a height the
   content decides, without measuring it in JS and without the jump a fixed
   max-height gives on a list whose length changes. The child owns the
   `overflow: hidden` (a grid row cannot clip on its own) and carries a small
   downward slide so the panel reads as arriving rather than appearing.
   A CLOSED fold is still in the DOM, so the caller sets `.inert` on it —
   otherwise its buttons stay tabbable and a screen reader walks through a
   panel nobody can see. */
.msh-fold { display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows var(--dur-in) var(--ease-spring),
              opacity var(--dur-in) var(--ease-enter); }
.msh-fold > * { overflow: hidden; min-height: 0; transform: translateY(-6px);
  transition: transform var(--dur-in) var(--ease-spring); }
.msh-fold.open { grid-template-rows: 1fr; opacity: 1; }
.msh-fold.open > * { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .msh-fold, .msh-fold > *, .mh1-plus { transition: none; }
}
/* Common select / input bits used inside the form pages. */
.mobile-field { margin-bottom: 12px; }
.mobile-field label { display: block; font-size: 11px; font-weight: 700;
  color: #374151; margin-bottom: 5px; }
/* hotfix mp-collapse-delete (25 Sep 2026): a phone number is `type="tel"` -
   the phone's number keypad - and wears the same house input. */
.mobile-field select, .mobile-field input[type="text"], .mobile-field input[type="tel"],
.mobile-field input[type="date"], .mobile-field input[type="datetime-local"],
.mobile-field textarea {
  width: 100%; padding: 11px 12px; border: 1px solid var(--border);
  border-radius: 10px; font-size: 14px; background: #fff; box-sizing: border-box;
  font-family: inherit;
}
.mobile-field textarea { min-height: 80px; resize: vertical; }
/* Status filter chips row reused across list pages. */
.mobile-chips { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 12px; }
.mobile-chip {
  padding: 6px 12px; border-radius: 999px; font-size: 11px; font-weight: 700;
  background: #fff; color: #374151; border: 1px solid var(--border); cursor: pointer;
}
.mobile-chip.active { background: var(--verti-navy); color: #fff; border-color: var(--verti-navy); }
/* Card row used by the list-style pages (Service / Part / Fault open list). */
.mobile-card {
  background: #fff; border: 1px solid var(--border); border-radius: 10px;
  padding: 12px 14px; margin-bottom: 10px;
  display: flex; align-items: center; gap: 10px;
  border-left-width: 4px;
}
.mobile-card .body { flex: 1; min-width: 0; }
.mobile-card .title { font-size: 13px; font-weight: 700; line-height: 1.2; color: var(--verti-navy); }
.mobile-card .sub   { font-size: 11px; font-weight: 500; line-height: 1.3; color: #6B7280; margin-top: 3px; }
.mobile-card .meta  { font-size: 10px; font-weight: 600; line-height: 1; margin-top: 5px; }
.mobile-card.s-open    { border-left-color: #C0392B; }
.mobile-card.s-prog    { border-left-color: #FFB300; }
.mobile-card.s-done    { border-left-color: #2E7D32; }
.mobile-card.s-pending { border-left-color: var(--verti-navy); }
.mobile-card .meta.s-open    { color: #991B1B; }
.mobile-card .meta.s-prog    { color: #B7791F; }
.mobile-card .meta.s-done    { color: #15803D; }
.mobile-card .meta.s-pending { color: #1E40AF; }
.mobile-card .card-action {
  /* Sprint 239: bigger tap target for gloved / dirty field hands (was 42px). */
  width: 52px; height: 52px; border-radius: 12px; border: 0;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  font-size: 20px; flex-shrink: 0;
}
.mobile-card .card-action.start    { background: #FFB300; color: #fff; }
.mobile-card .card-action.complete { background: #16A34A; color: #fff; }
.mobile-card .card-action.view     { background: #fff; color: #374151; border: 1px solid var(--border); }
.mobile-topbar { gap: 10px; }
/* Sprint 459: the legacy S/F/P .mobile-notif-bubble cluster was removed from
   TOPBAR_HTML (never filled by any code, permanently hidden under .msh-app) —
   its CSS went with it. */
/* Sprint 127: live "active operation" card. Lives between the topbar
 * and the action grid; visible only while the user has an in-progress
 * service (initially) or fault / part-replacement (later). The clock
 * ticks in JS (mobile-actions.js). */
.mobile-active-card {
  margin: 14px 0 0;
  padding: 14px 16px;
  background: #E6F4EA;
  border: 1.6px solid #2E7D32;
  border-radius: 14px;
  display: flex; flex-direction: column; gap: 6px;
}
.mobile-active-card[hidden] { display: none !important; }
.mobile-active-head {
  display: flex; align-items: center; justify-content: space-between;
}
.mobile-active-type {
  font-size: 11px; font-weight: 800; letter-spacing: 0.6px;
  color: #15803D;
}
.mobile-active-live {
  display: flex; align-items: center; gap: 5px;
  font-size: 9px; font-weight: 800; color: #15803D;
}
.mobile-active-dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: #10B981;
  /* Sprint 243: brighter, ringed "LED" glow so the live state reads at a
     glance on a dirty field phone. */
  box-shadow: 0 0 0 3px rgba(16,185,129,.28), 0 0 8px 2px rgba(16,185,129,.80);
  animation: mActiveDotPulse 1.5s ease-in-out infinite;
}
@keyframes mActiveDotPulse { 0%,100% { opacity: .35; } 50% { opacity: 1; } }
.mobile-active-title { font-size: 14px; font-weight: 700; color: var(--verti-navy); }
.mobile-active-sub   { font-size: 11px; font-weight: 500; color: #475569; }
.mobile-active-foot {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 4px;
}
.mobile-active-clock {
  font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
  font-size: 18px; font-weight: 800; color: #15803D;
}
.mobile-active-complete {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 16px; border-radius: 10px;
  background: #2E7D32; color: #fff;
  border: 0; cursor: pointer;
  font-size: 13px; font-weight: 700;
}
.mobile-active-complete:active { transform: scale(.97); }
.mobile-active-complete[disabled] { opacity: 0.6; cursor: progress; }

/* Sprint 252: the close-action pair — QR "Complete" + a "Close without QR"
   ghost fallback for a technician who can't / forgot to scan. */
.mobile-active-btns { display: flex; align-items: center; gap: 8px; }
/* Sprint 432 hotfix: `hidden` attr loses to display:flex without this
   (the classic display-vs-[hidden] pattern). Without it, the
   Finish buttons (service-only, live-card.js hides them for fault/part/
   inspection/correction ops) stayed fully visible on a non-service active
   card; tapping them silently no-op'd (the click handler already guards on
   activeOp.type !== 'service'), reading as "the close button doesn't work". */
.mobile-active-btns[hidden] { display: none !important; }
.mobile-active-manualclose {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 12px; border-radius: 10px;
  background: #fff; color: var(--verti-navy);
  border: 1.4px solid #C7D2E6; cursor: pointer;
  font-size: 12px; font-weight: 700;
}
.mobile-active-manualclose:active { transform: scale(.97); }
.mobile-active-manualclose[disabled] { opacity: 0.6; cursor: progress; }
.mobile-active-card.is-warn .mobile-active-manualclose { border-color: #F3C0C0; color: #B91C1C; }

/* Sprint 246: tiered "warn" state — a service open ≥ 2h, or one whose device
   left the project geofence. The whole card flips to a red treatment, the LED
   blinks red, and a reminder line appears so the technician closes it. */
.mobile-active-card.is-warn {
  background: #FEECEC;
  border-color: #DC2626;
}
.mobile-active-card.is-warn .mobile-active-type,
.mobile-active-card.is-warn .mobile-active-live,
.mobile-active-card.is-warn .mobile-active-clock { color: #B91C1C; }
.mobile-active-card.is-warn .mobile-active-dot {
  background: #EF4444;
  box-shadow: 0 0 0 3px rgba(239,68,68,.30), 0 0 8px 2px rgba(239,68,68,.85);
}
.mobile-active-card.is-warn .mobile-active-complete { background: #DC2626; }
.mobile-active-warn {
  display: flex; align-items: center; gap: 7px;
  font-size: 11.5px; font-weight: 700; color: #B91C1C;
  background: #fff; border: 1px solid #F3C0C0; border-radius: 8px;
  padding: 6px 10px;
}
.mobile-active-warn[hidden] { display: none !important; }
.mobile-active-warn::before { content: "\26A0"; font-size: 12px; } /* ⚠ */

/* participant-live-card: the role layer (assets/js/live-card-role.js). The
   chip heads the title row of a helper's ("Helper") or an extra work's
   ("Extra work") card; the ONE role button stands where an owner's Finish pair
   would - outline navy "End my part" for a helper, filled green "Finish"
   (.is-fin) for an extra work. */
.mobile-active-chip {
  display: inline-block; vertical-align: 1px; margin-right: 6px;
  padding: 2px 7px; border-radius: 999px;
  background: var(--verti-navy); color: #fff;
  font-size: 9.5px; font-weight: 800; letter-spacing: .3px; line-height: 1.3;
}
.mobile-active-rolebtn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 8px 11px; border-radius: 10px; cursor: pointer; white-space: nowrap;
  background: #fff; border: 1.5px solid var(--verti-navy); color: var(--verti-navy);
  font-size: 12px; font-weight: 800;
}
.mobile-active-rolebtn.is-fin {
  padding: 9px 16px; border-color: #15803D; background: #15803D; color: #fff;
  font-size: 13px; font-weight: 700;
}
.mobile-active-rolebtn:active { transform: scale(.97); }
.mobile-active-rolebtn[disabled] { opacity: 0.6; cursor: progress; }
/* the [hidden] trap: the display above beats the UA [hidden] rule without this */
.mobile-active-rolebtn[hidden] { display: none !important; }

/* Sprint 127: pages opened from the APK launcher (`?apk=1` -> body.is-apk)
 * strip the desktop chrome: the sidebar, the brand banner, the language
 * switcher, the help button and the hamburger all disappear; the content
 * grid drops to a single column; only the small notification chips and
 * the user/logout cluster remain in the topbar. The page-level scripts
 * keep working unchanged. */
@media (min-width: 0) {
  body.is-apk .sidebar,
  body.is-apk .menu-btn,
  body.is-apk .brand-row,
  body.is-apk .lang-switch,
  body.is-apk .brand-program-name,
  body.is-apk #helpBtn { display: none !important; }
  body.is-apk .layout { grid-template-columns: 1fr !important; }
  body.is-apk .topbar { padding: 6px 12px; gap: 6px; }
  body.is-apk main.content { padding: 12px 12px 24px; }
}
/* Sprint 459: dead .mobile-manual / .mobile-logout-btn rules removed (no
   matching markup anywhere). */

/* Make the [hidden] attribute decisively win over .mobile-prefill,
   .scan-overlay and .modal-backdrop, all of which set display:flex.
   Author single-class selectors otherwise out-rank the UA's [hidden]
   single-attr rule (same specificity, later in cascade) and the modal
   pops open on first render. */
.mobile-prefill[hidden],
#confirmModal[hidden] { display: none !important; }

/* Scanner overlay — fullscreen camera view with framed center */
.scan-overlay {
  position: fixed; inset: 0; z-index: 9999;
  background: #000; display: flex; align-items: center; justify-content: center;
}
.scan-overlay[hidden] { display: none !important; }
.scan-overlay video {
  width: 100%; height: 100%; object-fit: cover;
}
.scan-frame {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(70vw, 320px); height: min(70vw, 320px);
  border: 3px solid #FFF; border-radius: 16px;
  box-shadow: 0 0 0 9999px rgba(0,0,0,0.45);
}
.scan-help {
  position: absolute; bottom: 14%; left: 0; right: 0;
  text-align: center; color: #fff; font-weight: 600; font-size: 15px;
  text-shadow: 0 1px 3px rgba(0,0,0,0.6);
  padding: 0 16px;
}
.scan-cancel {
  position: absolute; top: 14px; right: 14px;
  width: 44px; height: 44px; border-radius: 50%;
  border: 0; background: rgba(255,255,255,0.18); color: #fff;
  font-size: 28px; line-height: 1; cursor: pointer;
}

/* Sprint 123: branding line above the unit-barcode QR. Same font
 * character as the label lines below it (Arial, weight 700) — just a
 * slightly larger point size. One line (white-space:nowrap), solid
 * black, overflow:hidden as a hard backstop against an unexpected spill. */
.qr-url {
  width: 100%;
  margin: 0 auto 2px;
  text-align: center;
  font-weight: 700;
  font-size: 7pt;
  line-height: 1.2;
  color: var(--verti-navy);
  white-space: nowrap;
  overflow: hidden;
}


/* ===== Sprint 136: GPS Reports ===== */
.gpsr-filters {
  display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
}
.gpsr-filters .pf { display: flex; flex-direction: column; gap: 4px; }
.gpsr-filters label { font-size: 12px; font-weight: 600; color: var(--muted); }
.gpsr-filters input, .gpsr-filters select {
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); font: inherit;
}
.gpsr-gran { display: inline-flex; border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
.gpsr-gran button {
  padding: 8px 14px; border: 0; background: #fff; cursor: pointer;
  font: inherit; font-weight: 700; font-size: 12px; color: var(--verti-navy);
}
.gpsr-gran button.active { background: var(--verti-navy); color: #fff; }

.gpsr-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; padding: 16px; }
.gpsr-kpi {
  position: relative; background: #fff; border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 12px 14px; overflow: hidden;
}
/* Sprint 200: GPS report KPI accent moved to a top strip — the two-brand
   gradient by default, or a meaningful solid colour when one is passed. */
.gpsr-kpi::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 4px; background: var(--accent, var(--brand-gradient)); }
.gpsr-kpi .k-label { font-size: 10px; font-weight: 700; letter-spacing: .5px; color: var(--muted); text-transform: uppercase; }
.gpsr-kpi .k-value { font-size: 22px; font-weight: 800; color: var(--text); margin-top: 4px; }
.gpsr-kpi .k-sub { font-size: 10px; color: var(--muted); margin-top: 2px; }

/* Sprint presence-one-model — Project Hours.
   The definition line sits under the page title, the honest footnote under it
   (the day strip counts an unknown-return signal gap; this page and the
   customer portal do not), and the row count under the table. Small, quiet,
   and never a toast: all three are part of the READING, not an event. */
.gpsr-def      { margin: 6px 0 0; font-size: 13px; color: var(--muted); max-width: 90ch; }
.gpsr-def-note { margin: 4px 0 0; font-size: 12px; color: var(--muted); max-width: 90ch; font-style: italic; }
.gpsr-foot     { padding: 10px 0 4px; font-size: 11.5px; color: var(--muted); }
/* The stationed star, and the "+1" on an Out that landed after midnight — the
   house rule counts a day on the shift that STARTED it, and 02:13 with no
   marker reads as an early morning. */
.stationed     { color: #F59E0B; font-weight: 700; }
.nextday       { font-size: 10px; font-weight: 700; color: var(--muted); vertical-align: super; }

/* Timeline bands — Sprint 144 unified palette (shared with the live map
 * via Gps::liveStateFor: at_project=green, travel=blue, idle=red,
 * offline=grey, lunch=white pill with "LUNCH" label). */
.gpsr-timeline { position: relative; height: 30px; background: #F3F4F6; border-radius: 6px; overflow: hidden; }
.gpsr-band { position: absolute; top: 0; bottom: 0; }
.gpsr-band.at_project { background: #16A34A; }
.gpsr-band.travel     { background: #2563EB; }
.gpsr-band.idle       { background: #DC2626; }
.gpsr-band.offline    { background: #9CA3AF; }
.gpsr-band.lunch      { background: #FFFFFF; border-top:1px solid #9CA3AF; border-bottom:1px solid #9CA3AF;
  color:#28344A; font:800 9px/30px Arial, sans-serif; text-align:center; letter-spacing:.4px; overflow:hidden; }
.gpsr-band.lunch::before { content:'LUNCH'; }
.gpsr-band.off_clock  { background: #E5E7EB; }
.gpsr-axis { position: relative; height: 16px; margin-top: 2px; }
.gpsr-axis span { position: absolute; font-size: 9px; color: var(--muted); transform: translateX(-50%); }
.gpsr-legend { display: flex; gap: 14px; flex-wrap: wrap; margin: 10px 0; font-size: 11px; }
.gpsr-legend i { display: inline-block; width: 12px; height: 10px; border-radius: 2px; margin-right: 4px; vertical-align: middle; }

/* Diverging balance bar */
.gpsr-bal { position: relative; height: 16px; }
.gpsr-bal .deficit { position: absolute; right: 50%; height: 16px; background: #DC2626; border-radius: 2px 0 0 2px; }
.gpsr-bal .overtime { position: absolute; left: 50%; height: 16px; background: #059669; border-radius: 0 2px 2px 0; }
.gpsr-bal .axis { position: absolute; left: 50%; top: -2px; bottom: -2px; width: 1px; background: var(--muted); }

.gpsr-net-pos { color: #059669; font-weight: 800; }
.gpsr-net-neg { color: #DC2626; font-weight: 800; }
.gpsr-flag { color: #DC2626; font-weight: 700; }
.gpsr-bar { height: 12px; border-radius: 6px; background: #F3F4F6; overflow: hidden; }
.gpsr-bar > span { display: block; height: 100%; border-radius: 6px; }

/* ---- Reusable file-upload field (Sprint 181, assets/js/file-upload.js) ---- */
.upl-field { margin-bottom: 12px; }
.upl-label { font-weight: 600; font-size: 12px; color: var(--muted); margin-bottom: 5px; }
.upl-zone {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; border: 1.5px dashed #A9BEDC; border-radius: 7px;
  background: #F5F9FE; color: var(--verti-navy); font-size: 12px; font-weight: 600;
  cursor: pointer; transition: background .12s, border-color .12s;
}
.upl-zone:hover, .upl-zone:focus { background: #ECF3FC; border-color: var(--verti-navy); outline: none; }
.upl-zone.drag { background: #FBE6DD; border-color: var(--tke-orange); color: var(--tke-orange); }
.upl-zone i { font-size: 14px; }
.upl-list { margin-top: 6px; display: flex; flex-direction: column; gap: 5px; }
.upl-empty { font-size: 11px; color: var(--muted); padding: 2px 2px; }
.upl-item {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px;
  background: var(--surface); font-size: 12px;
}
.upl-item.busy { opacity: .7; }
.upl-item.editing { border-color: var(--tke-orange); }
.upl-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text); text-decoration: none; }
a.upl-name { color: var(--verti-navy); }
a.upl-name:hover { text-decoration: underline; }
.upl-busy { font-size: 11px; color: var(--muted); font-style: italic; }
/* Sprint 771: queue-mode progress + failed-with-retry row */
.upl-pbar { width: 56px; height: 4px; background: #E4E9F1; border-radius: 99px; overflow: hidden; flex: none; display: inline-block; }
.upl-pbar i { display: block; height: 100%; background: #2F6FED; border-radius: 99px; }
.upl-item.failed { border-color: #E6B0AC; background: #FDF4F3; }
/* ---- The invoice text block on the project card (Sprint 749) ---- */
#invoiceFields { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
/* .field carries no display of its own today, but this block is toggled with
   `hidden` on every project open — so the override is written next to it and
   cannot be lost to a later rule (the trap this house has hit six times). */
#invoiceFields[hidden] { display: none !important; }
.inv-chip {
  display: inline-block; font-size: 9.5px; font-weight: 800; letter-spacing: .03em;
  border-radius: 20px; padding: 1px 7px; color: #fff; margin-left: 6px; vertical-align: middle;
}
/* The house [hidden] rule: `display: inline-block` above is author-origin and
   beats the user-agent `[hidden] { display: none }` outright, so a chip hidden
   by the attribute would stay fully visible. Written next to the rule that
   causes it, per CLAUDE.md. */
.inv-chip[hidden] { display: none; }
.inv-chip.auto { background: var(--success, #177A4C); }
.inv-chip.man  { background: #C2670F; }
/* .sm variant retired with the Additional note field (Sprint 817). */
/* Sprint 816: marks the field whose value prints on the Zoho invoice. */
.inv-chip.prn  { background: #7A2E6B; }
.inv-mini {
  float: right; border: 1px solid #C9D6EA; background: var(--surface); color: var(--verti-navy);
  border-radius: 5px; font-size: 10.5px; font-weight: 700; padding: 2px 8px; cursor: pointer;
}
.inv-mini:hover { background: var(--bg); }
.inv-warn { color: #C2670F !important; }
/* ---- Unit prices panel on the project card (Sprint 750) ----
   A project's unit prices must sum to its contract price. The running total
   below the table is the only place that rule is visible while it is being
   typed, so it is styled to be read at a glance rather than hunted for. */
.up-wrap { border: 1px solid var(--border, #DFE5EF); border-radius: 7px; overflow: auto; max-height: 320px; }
.up-tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.up-tbl th {
  position: sticky; top: 0; z-index: 1; background: var(--verti-navy); color: #fff;
  text-align: left; padding: 6px 9px; font-size: 10px; letter-spacing: .05em;
  text-transform: uppercase; font-weight: 700; white-space: nowrap;
}
.up-tbl th.num, .up-tbl td.num { text-align: right; }
.up-tbl td { padding: 5px 9px; border-top: 1px solid #EDF1F7; color: var(--verti-navy); }
.up-tbl td:first-child { font-weight: 700; white-space: nowrap; }
.up-tbl input.up-price {
  width: 108px; text-align: right; padding: 3px 7px; font-size: 13px;
  border: 1px solid var(--border, #DFE5EF); border-radius: 5px; color: var(--verti-navy);
  font-variant-numeric: tabular-nums;
}
.up-tbl input.up-price:focus { border-color: #7A2E6B; outline: none; box-shadow: 0 0 0 2px rgba(122, 46, 107, .18); }
/* The stationed-staff row (Sprint 760): count × rate, shown only where the
   project actually bills a fixed person. [hidden] override written next to the
   display rule, per the house rule. */
.up-staff { display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center;
  margin-top: 8px; font-size: 13px; color: var(--verti-navy); }
.up-staff[hidden] { display: none; }
.up-staff input { width: 92px; text-align: right; padding: 3px 7px; font-size: 13px;
  border: 1px solid var(--border, #DFE5EF); border-radius: 5px; color: var(--verti-navy);
  font-variant-numeric: tabular-nums; }
.up-staff input:focus { border-color: #7A2E6B; outline: none;
  box-shadow: 0 0 0 2px rgba(122, 46, 107, .18); }
.up-staff .up-x { color: var(--muted, #6B7793); }
.up-staff b { font-variant-numeric: tabular-nums; }

.up-foot {
  display: flex; flex-wrap: wrap; gap: 8px 22px; align-items: center;
  margin-top: 8px; font-size: 13px; color: var(--verti-navy);
}
.up-foot b { font-variant-numeric: tabular-nums; }
.up-chip { border-radius: 20px; padding: 2px 10px; font-size: 12px; font-weight: 700; }
.up-chip.ok  { background: rgba(23, 122, 76, .14); color: #177A4C; }
.up-chip.bad { background: rgba(179, 38, 30, .13); color: #B3261E; }
.up-chip[hidden] { display: none; }
.up-fill { margin-left: auto; display: flex; gap: 6px; align-items: center; }
.up-fill[hidden] { display: none; }
.up-fill input { width: 104px; text-align: right; padding: 3px 7px; font-size: 13px;
  border: 1px solid var(--border, #DFE5EF); border-radius: 5px; font-variant-numeric: tabular-nums; }
.up-diff { color: #B3261E; font-weight: 700; }

/* ---- Missing invoice data KPI + drill-down (Sprint 748) ---- */
.sb-miss-card {
  border-color: var(--tke-orange); cursor: pointer; text-align: inherit;
  font-family: inherit; box-shadow: 0 0 0 2px rgba(232, 90, 42, .13);
}
.sb-miss-card .stat-value { color: var(--tke-orange); }
.sb-miss-card:hover { background: var(--bg); }
.sb-miss-card.open { box-shadow: 0 0 0 2px var(--tke-orange); }
.sb-miss-sub { display: block; font-weight: 400; color: var(--muted); font-size: .85em; }
.sb-missing { margin: 10px 0 0; }
/* The panel is a block, and a plain author `display` beats the user-agent
   [hidden] rule outright — so the override is written here, next to it. */
.sb-missing[hidden] { display: none !important; }
.sb-miss-chip {
  display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: .02em;
  border-radius: 20px; padding: 1px 8px; margin: 1px 4px 1px 0; color: #fff;
  background: var(--danger);
}
.sb-miss-chip.name      { background: #6B7793; }
.sb-miss-chip.contract  { background: var(--tke-orange); }
.sb-miss-chip.agreement { background: #7A2E6B; }
.sb-miss-chip.zoho      { background: #7A2E6B; }

/* ---- Free maintenance (Sprint 744) ---- */
.pj-free {
  display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: .02em;
  border-radius: 20px; padding: 1px 8px; margin-right: 5px;
  background: #7A2E6B; color: #fff;
}
.pj-free.over { background: #C2670F; }
.fm-sw { display: inline-flex; align-items: center; gap: 7px; font-weight: 600; }
.fm-sw input { width: auto; margin: 0; }

/* ---- Unit document scope: unit vs whole project (Sprint 742) ---- */
.uf-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.uf-head label { margin: 0; }
.uf-scope { display: inline-flex; border: 1px solid #C9D6EA; border-radius: 7px; overflow: hidden; }
.uf-scope button {
  border: 0; padding: 3px 10px; font-size: 11px; font-weight: 700; cursor: pointer;
  background: var(--surface); color: var(--muted); font-family: inherit;
}
.uf-scope button.on { background: var(--verti-navy); color: #fff; }
.uf-scope button[data-scope="project"].on { background: #3F6DB5; }

.uf-shared {
  border: 1px solid #BFD3EE; background: #EEF4FD; border-radius: 8px;
  padding: 9px 11px; margin-bottom: 8px;
}
.uf-shared-lbl {
  display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 700;
  color: #3F6DB5; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px;
}
.uf-chip {
  background: #3F6DB5; color: #fff; border-radius: 20px; padding: 1px 8px;
  font-size: 10px; font-weight: 700; text-transform: none; letter-spacing: .02em;
}
/* The switch moves the DROP ZONE, it never hides a document. `hidden` is not
   usable here: .upl-zone carries display:flex and a plain author rule beats the
   user-agent [hidden] rule, so the zone would stay fully visible (Sprint 742). */
.uf-zone-off .upl-zone { display: none; }
/* A band with no document and no drop zone in it is an empty blue box. */
.uf-shared.uf-zone-off[data-empty="1"] { display: none; }

/* ---- Unit pick-list field: a select plus "add to the list" (Sprint 741) ---- */
.uo-field { display: flex; align-items: center; gap: 6px; }
.uo-field > select { flex: 1 1 auto; min-width: 0; }
.uo-new { display: flex; align-items: center; gap: 4px; flex: 1 1 auto; min-width: 0; }
.uo-new input { flex: 1 1 auto; min-width: 0; }
.uo-ico {
  flex: 0 0 auto; width: 28px; height: 28px; display: inline-flex; align-items: center;
  justify-content: center; border: 1px solid var(--border); border-radius: 5px;
  background: var(--surface); color: var(--muted); cursor: pointer; font-size: 11px;
}
.uo-ico:hover { background: var(--bg); color: var(--verti-navy); border-color: var(--verti-navy); }
.uo-ico.ok { color: var(--success); border-color: var(--success); }
.uo-ico:disabled { opacity: .5; cursor: default; }

/* Opt-in image preview (Sprint 741) — only rendered when a mount passes thumbs:true. */
.upl-thumb { flex: 0 0 auto; width: 46px; height: 34px; object-fit: cover; border-radius: 4px;
  border: 1px solid var(--border); cursor: pointer; background: var(--bg); }
.upl-edit { flex: 1; min-width: 0; padding: 3px 6px; border: 1px solid var(--tke-orange);
  border-radius: 5px; font-size: 12px; }
.upl-ico {
  flex: 0 0 auto; width: 24px; height: 24px; display: inline-flex; align-items: center;
  justify-content: center; border: 1px solid var(--border); border-radius: 5px;
  background: var(--surface); color: var(--muted); cursor: pointer; font-size: 11px;
}
.upl-ico:hover { background: var(--bg); color: var(--text); }
.upl-ico.danger:hover { color: var(--danger); border-color: var(--danger); }
.upl-ico.ok { color: var(--success); border-color: var(--success); }

/* ===== Puantaj (Sprint 183) ===== */
.btn.ghost { background: var(--surface); color: var(--verti-navy); border-color: var(--border); }
.btn.ghost:hover { background: var(--bg); }
.btn.small { padding: 6px 10px; font-size: 13px; }
.btn.icon { padding: 8px 10px; }
.btn.small.icon { padding: 5px 8px; }
.btn.danger { background: var(--danger); }
.btn.danger.ghost { background: var(--surface); color: var(--danger); border-color: var(--border); }
.btn.danger.ghost:hover { border-color: var(--danger); }

.pnt-wrap { padding: 0 0 24px; }
.pnt-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 14px 16px 4px; }
.pnt-datenav { display: flex; align-items: center; gap: 6px; }
.pnt-datenav input[type=date] { padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.pnt-userwrap { display: flex; align-items: center; gap: 6px; }
.pnt-userwrap label { font-size: 12px; color: var(--muted); }
.pnt-userwrap select { padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); max-width: 60vw; }
.pnt-head { padding: 2px 16px; }
.pnt-shift { font-size: 12px; color: var(--muted); }

.pnt-kpis { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; padding: 10px 16px; }
.pnt-kpi { border-radius: var(--radius-sm); padding: 12px 8px; text-align: center; }
.pnt-kpi b { display: block; font-size: 18px; }
.pnt-kpi span { font-size: 11px; }
.pnt-kpi.a { background: #DEF1E8; color: #2E9E6B; }
.pnt-kpi.b { background: var(--verti-navy-50); color: var(--verti-navy); }
.pnt-kpi.c { background: #FEF3C7; color: #B45309; }

.pnt-flow-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 16px 2px; }
.pnt-flow-head span { font-size: 11px; font-weight: 700; letter-spacing: .04em; color: var(--muted); }
.pnt-bar { padding: 4px 16px 8px; }

.pnt-flow { padding: 4px 12px 0; position: relative; }
.pnt-flow::before { content: ''; position: absolute; left: 60px; top: 6px; bottom: 6px; width: 2px; background: var(--border); }
.pnt-item { display: flex; align-items: flex-start; gap: 8px; position: relative; margin-bottom: 8px; }
.pnt-time { width: 42px; flex: none; text-align: right; font-variant-numeric: tabular-nums; font-size: 11px; color: var(--muted); padding-top: 6px; }
.pnt-time span { display: block; }
.pnt-dot { width: 10px; height: 10px; border-radius: 50%; margin: 9px 0 0 4px; flex: none; z-index: 1; box-shadow: 0 0 0 2px var(--surface); }
.pnt-card { flex: 1; min-width: 0; background: var(--surface); border: 1px solid var(--border); border-left-width: 3px; border-radius: var(--radius-sm); padding: 8px 10px; cursor: pointer; }
.pnt-card:hover { box-shadow: var(--shadow-sm); }
.pnt-card-head { display: flex; align-items: center; gap: 8px; }
.pnt-name { font-weight: 600; font-size: 14px; flex: 1; min-width: 0; }
.pnt-src { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 9px; flex: none; }
.pnt-src.gps { background: var(--verti-navy-50); color: var(--verti-navy); }
.pnt-src.manual { background: #EDEFF3; color: var(--muted); }
.pnt-cmeta { font-size: 11px; color: var(--muted); margin-top: 2px; }
.pnt-note { font-size: 12px; color: var(--text); margin-top: 5px; }
.pnt-hint { font-size: 11px; color: var(--muted); margin-top: 4px; font-style: italic; }
.pnt-empty { padding: 26px 16px; text-align: center; color: var(--muted); }

/* modal bits */
.pnt-lbl { display: block; font-size: 11px; font-weight: 700; color: var(--muted); margin: 12px 0 5px; }
.pnt-subhead { font-size: 13px; color: var(--text); display: flex; align-items: center; gap: 8px; }
.pnt-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.pnt-chip { border: 1px solid var(--border); background: var(--surface); color: var(--text); border-radius: 16px; padding: 7px 12px; font-size: 13px; cursor: pointer; }
.pnt-chip.on { background: var(--verti-navy-50); border-color: var(--verti-navy); color: var(--verti-navy); font-weight: 600; }
.pnt-newcat { display: flex; gap: 8px; margin-bottom: 10px; }
.pnt-newcat input { flex: 1; padding: 9px 11px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.pnt-muted { font-size: 12px; color: var(--muted); margin: 0 0 10px; }
.pnt-catlist { display: flex; flex-direction: column; gap: 6px; }
.pnt-catrow { display: flex; align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 6px 8px; }
.pnt-catrow.off { background: #F7F7F9; }
.pnt-catrow .cat-id { width: 22px; text-align: center; font-size: 11px; color: var(--muted); background: var(--bg); border-radius: 8px; padding: 2px 0; }
.pnt-catrow .cat-input { flex: 1; min-width: 0; padding: 7px 9px; border: 1px solid var(--border); border-radius: var(--radius-sm); }
.pnt-catrow.off .cat-input { border-color: transparent; background: transparent; color: var(--muted); text-decoration: line-through; }
.pnt-retiredtag { font-size: 10px; color: var(--danger); }

@media (min-width: 720px) {
  .pnt-kpis { max-width: 520px; }
}

/* Sales-activity bridge — read-only cards in puantaj timeline. */
.pnt-src.sales { background: #FEE8DC; color: var(--tke-orange); }
.pnt-link { text-decoration: none; color: inherit; }
.pnt-link .pnt-card::after { content: '↗'; position: absolute; top: 6px; right: 8px; color: var(--tke-orange); font-size: 12px; opacity: .6; }
.pnt-link .pnt-card { position: relative; }
.pnt-untimed-head { padding: 14px 6px 6px 60px; font-size: 11px; font-weight: 700; letter-spacing: .04em; color: var(--muted); }

/* Sales puantaj accordion (Sprint 183) — embedded day timeline on sales.html */
.sp-acc { padding: 0; overflow: hidden; }
.sp-acc-head { width: 100%; display: flex; align-items: center; gap: 10px; padding: 14px 18px;
  background: #F8FAFC; border: none; cursor: pointer; text-align: left; font: inherit; }
.sp-acc-head:hover { background: #F1F5F9; }
.sp-chev { color: var(--tke-navy, var(--verti-navy)); width: 14px; }
.sp-acc-title { font-weight: 700; color: var(--tke-navy, var(--verti-navy)); font-size: 15px; }
.sp-acc-sub { color: var(--muted, #6B7280); font-size: 12px; }
.sp-acc-body { padding: 14px 18px 18px; border-top: 1px solid var(--border, #E5E7EB); }
.sp-loading, .sp-empty { padding: 18px; color: var(--muted, #6B7280); }
.sp-controls { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-bottom: 8px; }
.sp-datenav { display: flex; align-items: center; gap: 6px; }
.sp-datenav input[type=date] { padding: 6px 8px; border: 1px solid var(--border, #D1D5DB); border-radius: 7px; }
.sp-userwrap { display: flex; align-items: center; gap: 8px; }
.sp-userwrap select { padding: 6px 8px; border: 1px solid var(--border, #D1D5DB); border-radius: 7px; }
.sp-lbl { font-size: 12px; color: var(--muted, #6B7280); }
.sp-head { font-size: 13px; color: var(--muted, #6B7280); margin: 6px 0 10px; }
.sp-bar { margin-bottom: 12px; }
.sp-composer { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding: 10px; background: #FAFBFD; border: 1px solid var(--border, #E5E7EB); border-radius: 10px; }
.sp-composer select, .sp-composer input { padding: 7px 9px; border: 1px solid var(--border, #D1D5DB);
  border-radius: 7px; font-size: 13px; }
.sp-c-dept { color: var(--tke-orange, #E85A2A); font-weight: 700; }
.sp-c-kind { min-width: 180px; }
.sp-c-note { flex: 1; min-width: 140px; }
.sp-c-time { width: 92px; }
.sp-dash { color: var(--muted, #6B7280); }
.sp-c-hint { font-size: 12px; color: var(--tke-orange, #E85A2A); margin: 6px 2px 0; min-height: 14px; }
.sp-flow { margin-top: 8px; }
.sp-lunch { background: #FFFBF0; }

/* Sprint 183: clickable puantaj bar — tap a free gap to fill the composer. */
.sp-bar .gpsr-timeline.sp-bar-click { cursor: pointer; }
.sp-bar .gpsr-timeline.sp-bar-click:hover { outline: 2px solid #2563EB55; outline-offset: 1px; }

/* Sprint 183: per-row edit/delete on the puantaj flow cards. */
.sp-actions { display: inline-flex; gap: 4px; margin-left: 8px; }
.sp-icon { border: none; background: transparent; cursor: pointer; padding: 3px 5px; border-radius: 5px;
  color: #9CA3AF; font-size: 12px; line-height: 1; }
.sp-icon:hover { background: #F1F5F9; color: #374151; }
.sp-icon.sp-del:hover { background: #FEE2E2; color: #B91C1C; }

/* Sprint 210: Daily Work Log MATRIX — the day's records as one grid (replaces
   the vertical flow cards). GPS gap rows carry an inline category + note + log
   ✓ so a location segment is logged in one tap, no time typing. */
.wl-matrix { width: 100%; border-collapse: collapse; font-size: 13px; }
.wl-matrix thead tr { background: var(--brand-gradient); }
.wl-matrix thead th { color: #fff; font-weight: 800; font-size: 11px; text-align: left;
  padding: 8px 10px; white-space: nowrap; letter-spacing: .02em; }
.wl-matrix thead th:first-child { border-radius: 6px 0 0 6px; }
.wl-matrix thead th:last-child  { border-radius: 0 6px 6px 0; text-align: right; }
.wl-matrix tbody td { border-bottom: 1px solid var(--border); padding: 7px 10px;
  vertical-align: middle; color: #374151; }
.wl-matrix tbody tr:hover { background: #F8FAFC; }
.wl-matrix tbody tr.wl-hl { background: #EBF4FF; box-shadow: inset 3px 0 0 #2563EB; }
/* Sprint 211: highlight the folded composer ("+ Add" block) when the bar
   click landed on a free gap. Fades after a couple of seconds. */
.wl-addrow { border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px 10px; margin-top: 8px; background: var(--surface); transition: background .3s, box-shadow .3s; }
.wl-addrow.wl-hl { background: #EBF4FF; box-shadow: inset 3px 0 0 #2563EB; }
.wl-addrow .sp-composer { margin: 0; }
.wl-place { border: 1px solid var(--tke-orange); background: #FFF1E7; color: var(--tke-orange);
  border-radius: 6px; padding: 4px 9px; font-weight: 700; font-size: 12px; cursor: pointer; white-space: nowrap; }
.wl-place:hover { background: var(--tke-orange); color: #fff; }
/* While "place mode" is armed, the bar shows a crosshair so the user knows
   the next click drops the activity. */
.wl-placing .sp-bar .gpsr-timeline { cursor: crosshair !important; outline: 2px dashed var(--tke-orange); outline-offset: 1px; }
.wl-time { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--verti-navy); white-space: nowrap; }
.wl-time small { display: block; font-weight: 400; color: var(--muted); }
.wl-kind { font-weight: 600; color: var(--verti-navy); }
.wl-dur { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.wl-detail { color: var(--muted); }
.wl-src { font-size: 10px; font-weight: 800; padding: 2px 8px; border-radius: 9px; white-space: nowrap; }
.wl-src.gps { background: var(--verti-navy-50); color: var(--verti-navy); }
.wl-src.sales { background: #FEE8DC; color: var(--tke-orange); }
.wl-src.manual { background: #EDEFF3; color: var(--muted); }
.wl-src.lunch { background: #FFF7E0; color: #B45309; }
.wl-matrix tbody tr.wl-gap td { background: #F7FAFF; }
.wl-matrix tbody tr.wl-gap:hover td { background: #EEF5FF; }
.wl-logcell { display: flex; gap: 6px; align-items: center; }
.wl-logcell select, .wl-logcell input { padding: 5px 7px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; }
.wl-logcell select { flex: 0 0 auto; max-width: 160px; }
.wl-logcell input { flex: 1; min-width: 80px; }
.wl-log { border: 0; background: #047857; color: #fff; border-radius: 6px; padding: 5px 10px;
  cursor: pointer; font-weight: 800; font-size: 12px; white-space: nowrap; }
.wl-log:hover { filter: brightness(1.08); }
.wl-log[disabled] { opacity: .5; cursor: default; }
.wl-act { white-space: nowrap; text-align: right; }
@media (max-width: 720px) {
  /* On a phone the grid collapses to the source + kind + note essentials. */
  .wl-matrix .wl-hide-sm { display: none; }
  .wl-matrix { font-size: 12px; }
}

/* ============================================================
   Sprint 232 — Mobile VERTI/TKE brand layer + department-aware home.
   Most of the brand signal lives here (shared) so every mobile-*.html
   page inherits the gradient identity; mobile.html adds the wordmark,
   the department pill and the per-department action sections.
   ============================================================ */
/* Topbar: brand-gradient strip under the bar (mirrors the web .topbar::after). */
.mobile-topbar { position: relative; gap: 10px; }
.mobile-topbar::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; border-radius: 2px; background: var(--brand-gradient);
}
/* V + VERTI AIM lockup, sitting right of the V mark. */
.mobile-wordmark { display: flex; flex-direction: column; line-height: 1.04; flex: 1; min-width: 0; }
.mobile-wordmark b { font-size: 16px; font-weight: 900; color: var(--verti-navy); letter-spacing: .3px; }
.mobile-wordmark small { font-size: 7px; font-weight: 700; color: var(--muted); letter-spacing: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Sprint 459: dead .mobile-dept-pill rules removed (no matching markup). */

/* Sprint 232: the department wrappers must keep their cards full-width.
   A <button> is a form control, so with width:auto it shrinks to fit its
   content instead of filling — when the buttons stopped being direct flex
   items of .mobile-actions (now nested in these wrappers) the cards
   staircased to content width. Making each wrapper a flex column restores
   align-items:stretch (full-width cards) + the same 14px gap. */
#mServiceSection, #mSalesSection { display: flex; flex-direction: column; gap: 14px; }

/* Action cards: left gradient accent + the icon becomes a gradient chip. */
/* sprint start-a-job-now — THE [hidden] TRAP. The rule above sets
   display:flex on this class, which is author-origin and beats the UA's
   [hidden] rule: a .mobile-action-btn carrying the attribute NEVER hid. It has
   not mattered until now because every button on the home was unconditional;
   the field door is drawn only when its migration is in, and without this line
   it would have shown to everyone from the deploy. Gate: check-hidden-collisions. */
.mobile-action-btn[hidden] { display: none; }
.mobile-action-btn { overflow: hidden; }
.mobile-action-btn::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: var(--brand-gradient);
}
.mobile-action-icon {
  width: 44px; height: 44px; border-radius: 12px; display: grid; place-items: center;
  font-size: 21px; color: #fff !important; background: var(--brand-gradient);
  box-shadow: 0 3px 8px rgba(122, 46, 107, .25); flex: 0 0 auto;
}
/* Sprint 459: dead .mobile-actions-head rules removed (the assigned-list
   heading renders via #mAssignedHead + .mobile-atabs now). */

/* Form pages: gradient page-title underline + gradient save + gradient FAB. */
.mobile-page-h1 h1 { position: relative; padding-bottom: 9px; }
.mobile-page-h1 h1::after {
  content: ''; position: absolute; left: 0; bottom: 0; width: 56px; height: 3px;
  border-radius: 2px; background: var(--brand-gradient);
}
.mobile-save {
  width: 100%; border: none; border-radius: 12px; padding: 15px; font-size: 16px;
  /* hotfix unit-task: same trap as .mobile-h1-action - a <button> at weight
     800 in the browser's Arial is Arial Black, which has no Ə / ə. */
  font-family: inherit;
  font-weight: 800; color: #fff; background: var(--brand-gradient); cursor: pointer;
  box-shadow: 0 4px 12px rgba(122, 46, 107, .3);
}
.mobile-save:active { transform: scale(0.99); }

/* Department empty-state note (modules with no mobile actions yet). */
.mobile-dept-empty {
  margin: 22px 4px; padding: 16px; border: 1px dashed var(--border); border-radius: 14px;
  background: #fff; color: var(--muted); font-size: 13px; text-align: center;
}

/* Sprint 232: bring the mobile sub-pages' primary surfaces on-brand.
   The page-header swatch becomes a vertical brand gradient (matching the
   title's gradient underline); the collapsible "add" toggle on the list
   pages (service / part) becomes a gradient pill. Replaces the old
   per-page semantic inline colours (green / navy / red / purple). */
.mobile-page-swatch {
  background: linear-gradient(180deg, var(--verti-navy), var(--tke-purple), var(--tke-orange));
}
/* Sprint 459: dead .mobile-add-toggle rules removed (no matching <details>
   markup anywhere). */

/* ===== Sprint 233 — GPS record-location mismatch flag ===== */

/* Pre-commit warning modal (field-entry guard). */
.gps-warn-overlay {
  position: fixed; inset: 0; z-index: 4000; background: rgba(15, 23, 42, .55);
  display: flex; align-items: flex-start; justify-content: center; padding: 80px 18px;
}
.gps-warn {
  width: 100%; max-width: 380px; background: #fff; border-radius: 18px; overflow: hidden;
  box-shadow: 0 18px 50px rgba(0, 0, 0, .4);
}
.gps-warn-top {
  background: linear-gradient(135deg, #DC2626, #991B1B); color: #fff; padding: 16px 18px;
  display: flex; align-items: center; gap: 12px;
}
.gps-warn-top i { font-size: 24px; }
.gps-warn-top b { font-size: 16px; font-weight: 800; line-height: 1.2; }
.gps-warn-body { padding: 16px 18px; }
.gps-warn-body p { margin: 0 0 12px; font-size: 13.5px; font-weight: 600; line-height: 1.5; color: #374151; }
.gps-warn-dist {
  display: flex; align-items: center; gap: 8px; background: #FEF2F2;
  border: 1px solid #FCA5A5; border-radius: 10px; padding: 10px 12px; margin-bottom: 14px;
  font-size: 14px; font-weight: 800; line-height: 1; color: #991B1B;
}
.gps-warn-btns { display: flex; gap: 10px; }
.gps-warn-btns button { flex: 1; border: 0; border-radius: 11px; padding: 13px; font-size: 14px; font-weight: 800; line-height: 1; font-family: inherit; cursor: pointer; }
.gps-warn-go { background: var(--brand-gradient); color: #fff; box-shadow: 0 4px 12px rgba(122, 46, 107, .3); }
.gps-warn-cancel { background: #F3F4F6; color: #374151; border: 1px solid var(--border); }
/* Sprint 697 — reported accuracy under the distance chip + the re-check button. */
.gps-warn-acc { margin: -8px 0 12px; font-size: 12px; font-weight: 600; line-height: 1.4; color: #6B7280; text-align: center; }
.gps-warn-acc:empty { display: none; }
.gps-warn-retry {
  display: block; width: 100%; margin-top: 10px; border: 0; background: none;
  color: #7A2E6B; font-size: 13px; font-weight: 700; line-height: 1; font-family: inherit; cursor: pointer; padding: 9px;
}
.gps-warn-retry:disabled { opacity: .6; cursor: default; }

/* List / detail badge — red mismatch, green verified, grey unverified. */
.gps-badge {
  display: inline-flex; align-items: center; gap: 5px; border-radius: 999px;
  padding: 2px 9px; font-size: 11px; font-weight: 800; line-height: 1.6; white-space: nowrap;
}
.gps-badge-bad { background: #FEE2E2; color: #991B1B; border: 1px solid #FCA5A5; }
.gps-badge-ok  { background: #D1FAE5; color: #065F46; border: 1px solid #6EE7B7; }
/* Sprint 697 — fix too coarse to judge: "could not verify" ≠ "far away". */
.gps-badge-unv { background: #F3F4F6; color: #4B5563; border: 1px solid #D1D5DB; }
/* Sprint 702 — QR presence proof: the strongest ✓, and the QR-vs-GPS
   contradiction (sticker scanned, device far — possible photographed QR). */
.gps-badge-qr     { background: #E0EAFF; color: #1D4ED8; border: 1px solid #93B4FD; }
.gps-badge-qrconf { background: #FEF3C7; color: #92400E; border: 1px solid #FCD34D; }
/* Sprint 268 — open/close phase tag prefixing the badge (e.g. "Açılış ✓"). */
.gps-badge-ph { font-weight: 800; opacity: .8; letter-spacing: .2px; }

/* Personal-dashboard "your flagged records" card (red left accent). */
.gps-flag-card { position: relative; overflow: hidden; }
.gps-flag-card::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 5px;
  background: linear-gradient(180deg, #DC2626, #991B1B);
}
.gps-flag-row {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 8px 0; border-top: 1px solid #f0f2f5; font-size: 12.5px; color: #374151;
}
.gps-flag-row:first-of-type { border-top: 0; }

/* ===== Sprint 234 — technician mobile: full-bleed gradient topbar, safe-area,
   A56-fit compact cards, header home button + topbar minimize button ===== */

/* The topbar spans edge-to-edge and sits BELOW the phone status bar
   (safe-area). Propagates to every mobile page via the shared .mobile-topbar. */
.mobile-shell { padding-top: 0; }
.mobile-topbar {
  margin: 0 -8px;                                    /* Sprint 278: break out of the shell's 8px side padding */
  /* Sprint 273: hug the phone status bar — keep the safe-area inset (so the
     wordmark never collides with the clock/battery) but drop the extra 12px
     band that left a big empty gap above VERTI AIM. */
  padding: calc(env(safe-area-inset-top, 0px) + 4px) 8px 10px;
  background: var(--brand-gradient);
  border-bottom: none;
  box-shadow: 0 3px 12px rgba(var(--verti-navy-rgb), .22);
}
.mobile-topbar::after { display: none; }             /* whole bar is gradient now — drop the strip */
/* Invert the topbar elements so they read on the gradient. (Sprint 459: the
   dead brand-mark SVG recolors and .mobile-dept-pill overrides are gone.) */
.mobile-wordmark b     { color: #fff; }
.mobile-wordmark small { color: rgba(255, 255, 255, .85); }
/* Sprint 278/280: the user avatar matches the home button EXACTLY (same 32px
   outer diameter — both box-sizing:border-box; Sprint 280 +15% from 28→32px). */
.mobile-topbar .mobile-user-chip .avatar { background: #fff; color: var(--verti-navy); width: 32px; height: 32px; font-size: 15px; }

/* Sprint 459: the injected .mobile-homebtn is gone (it was inject-then-removed
   waste on app-shell pages). The wordmark home affordance stays (Sprint 278):
   the wordmark + the empty header space to its right trigger home. */
.mobile-wordmark { cursor: pointer; }

/* Page-head row: section title + a right-aligned HOME button (on every page). */
.mobile-headrow { display: flex; align-items: center; gap: 10px; margin: 12px 0 9px; }
.mobile-headrow-title {
  flex: 1; font-size: 16px; font-weight: 800; line-height: 1.1; color: var(--verti-navy);
  position: relative; padding-left: 11px;
}
.mobile-headrow-title::before {
  content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 4px; height: 16px; border-radius: 2px; background: var(--brand-gradient);
}

/* Sprint 459: the Sprint-254 .svc-bubbles / .svc-bub / .svc-ring stat-bubble
   family was dead CSS — its renderer (service-stats.js) and its last mount
   point (#mSvcStats) are both gone. Removed. */

/* ===== Sprint 273 — mobile home: grouped white cards + inline KPIs + tabs +
   one VERTI+TKE-branded 1-year fault badge (operator layout request). =====
   (Sprint 459: the .mobile-action-row white-card variant was removed with the
   dead #mActionRow markup.) */
/* the whole service section sits in ONE white card. */
.mobile-svccard {
  background: #fff; border: 1px solid var(--border); border-radius: 15px;
  box-shadow: 0 4px 14px rgba(20, 30, 60, .05); padding: 2px 12px 12px; margin-bottom: 14px;
  min-width: 0;   /* Sprint 280: flex-column child — don't grow to content min-content (let the KPI grid clip/fit) */
}
/* 3) heading row inside the card: title at natural width, tabs at the end. */
.mobile-svccard > .mobile-headrow { margin: 9px 0 4px; gap: 8px; }
.mobile-svccard > .mobile-headrow .mobile-headrow-title { flex: 0 1 auto; font-size: 15px; }
/* 4) "Atanmamış Servis Başlat" — bold, title-ish font, one line (Sprint 274). */
.mobile-action-btn.mobile-start { padding: 13px 12px; margin: 4px 0 0; }
.mobile-action-btn.mobile-start .mobile-action-label {
  /* Sprint 280: was `font: 800 13px/1.1 inherit` — an INVALID shorthand (a
     global keyword can't be the font-family), so the 13px was dropped and the
     label inherited the button's 16px (rendering BIGGER than the "Servis İşləri"
     heading). Use valid longhands so the intended 13px actually applies. */
  flex: 1; font-weight: 800; font-size: 13px; line-height: 1.1; color: var(--verti-navy); text-align: center; white-space: nowrap;
}
/* Sprint 274: the manual-start + QR actions match the assigned-row buttons
   EXACTLY (same function → same look): a brand-gradient ▶ play + an orange ▦ QR. */
.mobile-action-btn.mobile-start .mobile-action-form,
.mobile-action-btn.mobile-start .mobile-action-scan {
  width: 38px; height: 38px; border-radius: 10px; padding: 0; margin: 0;
  display: grid; place-items: center; font-size: 15px; border: 0;
}
.mobile-action-btn.mobile-start .mobile-action-form { background: var(--brand-gradient); color: #fff; }
.mobile-action-btn.mobile-start .mobile-action-scan { background: #FFF4EE; color: var(--tke-orange); }
.mobile-action-btn.mobile-start .mobile-action-form + .mobile-action-scan { margin-left: 7px; }
/* 5) Sprint 280: the two tabs are VERTI-navy BUTTONS (active = filled navy,
   inactive = navy outline) with an orange total-count badge — replaces the old
   underline tabs. */
.mobile-atabs { display: flex; gap: 8px; margin: 6px 0 2px; }  /* Sprint 278 gap kept */
.mobile-atab {
  position: relative; flex: 1; cursor: pointer; border-radius: 11px;
  padding: 10px 8px; font-size: 12.5px; font-weight: 800; line-height: 1.1; font-family: inherit;
  background: #fff; color: var(--verti-navy); border: 1.6px solid var(--verti-navy);
}
.mobile-atab.on { background: var(--verti-navy); color: #fff; }
.mobile-atab:active { transform: scale(0.97); }
.mobile-atab-badge {
  position: absolute; top: -7px; right: -7px; background: var(--tke-orange);
  color: #fff; font-size: 10px; font-weight: 900; border-radius: 10px;
  padding: 0 6px; border: 2px solid #F2F5FA; line-height: 1.5;
}
/* 6) ONE fault badge — last-year count, VERTI+TKE brand gradient. */
.maa-fbs { display: inline-flex; }
.maa-fb1 {
  display: inline-flex; align-items: center; gap: 3px; background: #A6394F;   /* Sprint 304: Arıza colour */
  color: #fff; font-size: 9.5px; font-weight: 800; padding: 2px 8px; border-radius: 7px; cursor: pointer;
}
.maa-fb1 i { font-size: 9px; }
.maa-fb1 em { font-style: normal; opacity: .9; margin-left: 1px; }

/* 7) Sprint unit-photo-slots — the n/3 unit-photo chip, at the END of line 2.
   THREE states and they are three different answers:
     .part  amber  — some of the three are missing, and that is a to-do
     .full  green  — all three are on file
     .unk   grey   — the batch count has not arrived (or failed); the chip is
                     still tappable because the sheet fetches per unit
   Amber is #B45309 on white (contrast 4.87:1) and green #15803D (5.24:1), both
   above 4.5:1 — a status told by colour alone would be no status at all.

   THE TAP TARGET IS 40px AND THE ROW IS NOT. Measured: padding alone to a 40px
   box grew .maa-l2 and with it every agenda row, 60.75px -> 84px, which costs a
   whole row of screen on an A56 for a chip that only has to be easy to hit. So
   the BOX stays badge-sized next to the fault badge it sits beside, and a
   transparent ::after band extends the hit area to 40px around it — clicks on
   the band land on the button because the band is the button's own child. It
   can only ever overlap the title line's right end, which is text and not a
   control, so it steals nothing. */
.maa-ph {
  position: relative;
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 9.5px; font-weight: 800; line-height: 1;
  /* Padding matched to .maa-fb1 above, deliberately: the box must be no taller
     than the fault badge it sits beside or line 2 grows and the row with it. */
  padding: 2px 9px; margin-left: 6px; flex: 0 0 auto;
  border: 0; border-radius: 7px;
  background: #FEF3E2; color: #B45309; cursor: pointer; font-family: inherit;
}
/* The band grows UPWARD, not symmetrically. Line 2 is the LAST line of the
   row, so a band centred on the chip spilled 6px past the row's bottom edge
   and the next row — a later sibling, painted on top — took those taps.
   Upward it only ever covers the title line, which is text. */
.maa-ph::after {
  content: ''; position: absolute; left: 0; right: 0;
  top: -21px; height: 40px;
}
.maa-ph[hidden] { display: none; }
.maa-ph i { font-size: 9.5px; }
.maa-ph.full { background: #E7F6EC; color: #15803D; }
.maa-ph.unk  { background: #EEF1F6; color: #5A6B85; }
.maa-ph:active { transform: scale(0.94); }
.maa-ph:focus-visible { outline: 2px solid var(--verti-navy); outline-offset: 2px; }

/* Sprint 459: dead .mobile-headbtn rules removed (superseded by the msh-nav /
   msh-badges header redesign). */

/* Compact action cards — a SERVICE home (6 actions + map) fits an A56 with no
   scroll. */
.mobile-actions { gap: 9px; padding-top: 2px; }
/* Sprint 239: bigger tap targets for gloved / dirty field hands. */
.mobile-action-btn { min-height: 70px; padding: 11px 14px 11px 17px; gap: 14px; border-radius: 15px; }
.mobile-action-icon { width: 54px; height: 54px; border-radius: 14px; font-size: 26px; }
.mobile-action-scan,
.mobile-action-form { font-size: 22px; padding: 11px 13px; border-radius: 10px; }
/* Sprint 234 tweak: tighten the per-department wrapper gap to match the
   compact cards. */
#mServiceSection, #mSalesSection { gap: 9px; }
/* Sprint 234 fix: the per-department wrappers set display:flex, which defeats
   the [hidden] attribute (a service user kept seeing the gated Sales section).
   Restore [hidden] = display:none for them. */
#mServiceSection[hidden], #mSalesSection[hidden] { display: none !important; }

/* Sprint 459: dead headrow-button era removed — .mobile-mapbtn (S234),
   .mobile-puantajbtn (S265), .mobile-otherbtn + its badge (S259), the
   .mobile-action-row / .mact quick-icon strip (S265) and the compact
   .svc-bubbles overrides. All superseded by the Sprint-303 bottom nav +
   Diğer sheet; none had matching markup anymore. */

/* Sprint 259: assigned-maintenance list (under Servis Ekle). Left accent
   encodes the due state — red overdue, green TODAY (light-green card so it
   stands out), navy future. Each row carries manual (▶) + QR (▦) start. */
.mobile-assigned { margin: 2px 0 4px; }
.mobile-assigned[hidden] { display: none; }
.mobile-assigned-row {
  background: #fff; border: 1px solid var(--border); border-radius: 12px;
  padding: 9px 11px; display: flex; align-items: center; gap: 10px;
  margin-bottom: 8px; position: relative; overflow: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
.mobile-assigned-row::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--verti-navy);
}
.mobile-assigned-row.over::before  { background: #C0392B; }
.mobile-assigned-row.today::before { background: #16A34A; }
.mobile-assigned-row.today { background: #F0FBF4; border-color: #BFE6CD; }
.maa-tx { flex: 1; min-width: 0; }
.maa-tx b {
  display: block; font-size: 13px; font-weight: 800; color: var(--verti-navy);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.maa-tx small { font-size: 11px; font-weight: 700; color: var(--muted); }
.mobile-assigned-row.over  .maa-tx small { color: #C0392B; }
.mobile-assigned-row.today .maa-tx small { color: #15803D; font-weight: 800; }
.maa-acts { display: flex; gap: 7px; flex: 0 0 auto; }
.maa-st {
  width: 38px; height: 38px; border-radius: 10px; border: 0; cursor: pointer;
  display: grid; place-items: center; font-size: 15px;
}
.maa-st.maa-man { background: var(--brand-gradient); color: #fff; }
.maa-st.maa-qr  { background: #FFF4EE; color: var(--tke-orange); }
.maa-st:active { transform: scale(0.94); }
.maa-st.disabled-by-active { opacity: 0.4; }

.mobile-assigned-empty { font-size: 12px; color: var(--muted); padding: 4px 2px 10px; }

/* Sprint 264: each assigned row now stacks title + date + the fault badges,
   so align the start/✓ actions to the top of that taller block. */
.mobile-assigned-row { align-items: flex-start; }
.maa-acts { padding-top: 1px; }
/* Completed maintenances: grey accent + a green ✓ instead of start buttons. */
.mobile-assigned-row.done::before { background: #94A3B8; }
.mobile-assigned-row.done .maa-tx small { color: #64748B; }
.maa-doneic {
  width: 38px; height: 38px; border-radius: 10px; display: grid; place-items: center;
  background: #E8F5EC; color: #15803D; font-size: 15px;
}

/* Sprint 264: VERTI-branded per-unit fault badges inside each assigned row
   (last 3 months / last year / all-time). Recency drives the colour. */
.maa-fbs { display: flex; gap: 5px; margin-top: 6px; flex-wrap: wrap; }
.maa-fb {
  display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
  user-select: none; font-size: 10px; font-weight: 800; line-height: 1.3;
  padding: 3px 8px; border-radius: 999px;
}
.maa-fb i { font-size: 9px; }
.maa-fb em {
  font-style: normal; font-weight: 700; font-size: 8px; letter-spacing: .4px;
  text-transform: uppercase; opacity: .9;
}
.maa-fb.fb3 { background: #C0392B; color: #fff; box-shadow: 0 1px 3px rgba(192, 57, 43, .3); }
.maa-fb.fb1 { background: #FCE4E4; color: #B91C1C; }
.maa-fb.fba { background: #FEF3C7; color: #92400E; }
.maa-fb:active { transform: scale(.95); }

/* ============================================================================
   Sprint 269 — mobile HOME layout tightening (reduce empty space, gain area).
   Everything is scoped to .mobile-actions so other mobile pages are untouched.
   Hard rule (operator): nothing crushes / overflows — cells flex + clamp,
   titles + dates ellipsize, fault badges never clip.
   ============================================================================ */
/* (1,3,4) one tight ~8px vertical rhythm — the flex `gap` was being doubled by
   each child's own margin; zero those so spacing is the gap alone. */
.mobile-actions { gap: 8px; padding-top: 9px; }
.mobile-actions > .mp-tlcard,
.mobile-actions > .mobile-headrow { margin: 0; }
/* (2) puantaj card — tighter top/bottom inner padding. */
.mobile-actions .mp-tlcard { padding: 7px 9px; }
/* (1) shorter "Servis Ekle" button (78px → ~52px), icon/label intact. */
.mobile-actions .mobile-action-btn { padding: 12px 14px; min-height: 0; font-size: 16px; }
.mobile-actions .mobile-action-icon { font-size: 23px; width: 28px; }
/* (Sprint 459: the .svc-bubbles.compact KPI-slot overrides and the
   .mobile-assigned-head / .mobile-afilters filter-icon era were dead — the
   heading renders via #mAssignedHead + .mobile-atabs since Sprint 273/303.) */
/* gap between the Add-Service button and the assigned list. */
.mobile-actions .mobile-assigned { margin: 4px 0 0; }

/* (7) assigned row — title on line 1 (ellipsis); date + fault badges share
   line 2, badges right-aligned at the row's height, no clip. */
.mobile-actions .mobile-assigned-row { align-items: center; }
.mobile-actions .maa-acts { padding-top: 0; }
.maa-tx { display: flex; flex-direction: column; gap: 3px; }
.maa-l2 { display: flex; align-items: center; gap: 6px; min-width: 0; }
.maa-l2 small {
  flex: 1 1 auto; min-width: 40px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.maa-l2 .maa-fbs { margin: 0 0 0 auto; flex: 0 0 auto; flex-wrap: nowrap; gap: 3px; }
/* compact the inline fault badges so the full date + all three fit one line
   without crushing the date (recency colour stays; caption + bolt shrink). */
.maa-l2 .maa-fb { font-size: 9px; padding: 2px 5px; gap: 2px; }
.maa-l2 .maa-fb i { font-size: 7.5px; }
.maa-l2 .maa-fb em { font-size: 7px; }

/* Sprint 264: fault-history sheet rows (reuses the shared bottom-sheet). */
.mfh-row { padding: 8px 4px; border-bottom: 1px solid #EEF1F6; font-size: 12px; color: var(--text); }
.mfh-row:last-child { border-bottom: 0; }
.mfh-meta { color: var(--muted); font-size: 11px; margin-top: 2px; }
.mfh-loading, .mfh-empty { color: var(--muted); font-size: 12px; text-align: center; padding: 14px; }
.mfh-err { color: #C0392B; font-size: 12px; padding: 8px; }

/* Sprint 264: the Add-Service page is now a single direct form card. */
.mobile-form-card {
  background: #fff; border: 1px solid var(--border); border-radius: 12px;
  padding: 14px; margin-bottom: 14px;
}

/* ===== Sprint 265: shared puantaj timeline (puantaj-timeline.js) =====
   ONE design used everywhere (mobile home, mobile puantaj, sales, desktop).
   Filled bands + striped GAP bands (unfilled time → tap to fill). */
/* Sprint 272: compact card — title + "?" help on the left, "doldur" on the right;
   the bar sits flush under the header (no gap). */
.pt-host { position: relative; }
/* Sprint 380: title + "?" + ✓/⚠ badge all cluster on the LEFT (the live clock
   chip owns the strip's right end now). */
.pt-head { display: flex; justify-content: flex-start; flex-wrap: wrap; align-items: center; gap: 5px 8px; margin-bottom: 3px; }
.pt-htitle { display: flex; align-items: center; gap: 6px; min-width: 0; }
.pt-lab  { color: var(--verti-navy); font-weight: 800; font-size: 11px; }
.pt-help {
  flex: 0 0 auto; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid #B9C2D4;
  background: #fff; color: #7B859B; font-size: 10px; font-weight: 900; line-height: 13px;
  text-align: center; cursor: pointer; padding: 0;
}
.pt-help:hover { border-color: var(--verti-navy); color: var(--verti-navy); }
.pt-miss { color: #B91C1C; font-weight: 800; font-size: 10px; white-space: nowrap; }
.pt-ok   { color: #15803D; font-weight: 800; font-size: 10px; white-space: nowrap; }
/* Sprint 690 — the DAY-OFF face: a quiet badge instead of a false "complete",
   and the bar says the day off in words instead of drawing a blank grey box
   (the operator read that blank as a broken card, on every surface). */
.pt-ok.pt-offday-b { color: #7B859B; }
.pt-bar.pt-bar-off { display: flex; align-items: center; justify-content: center;
  background: repeating-linear-gradient(45deg, #F4F6FA 0 8px, #EDF0F6 8px 16px); }
.pt-bar.pt-bar-off span { font-size: 10.5px; font-weight: 800; color: #94A3B8;
  letter-spacing: .04em; text-transform: uppercase; }
.pt-bar  {
  position: relative; height: 38px; background: #F4F0EA; border-radius: 6px;
  overflow: hidden; border: 1px solid #E1E6EF;                /* Sprint 278: +25% (30→38px) */
  display: flex; align-items: stretch;                        /* Sprint 439: segmented axis */
}
/* Sprint 439 — one .pt-seg per axis segment; a ⫽ break column between far
   off-shift blocks. The pad / off-shift zone is tinted; the dashed lines mark
   the real mesai edges. */
.pt-seg  { position: relative; min-width: 0; height: 100%; }
.pt-brk  { display: flex; align-items: center; justify-content: center; color: #8A94A8;
  font-weight: 900; font-size: 13px; letter-spacing: -2px; user-select: none;
  background: #FBFAF7; border-left: 1px solid #E1E6EF; border-right: 1px solid #E1E6EF; }
.pt-padzone { position: absolute; top: 0; bottom: 0; background: #EAEDF4; z-index: 0; }
.pt-mesln { position: absolute; top: -2px; bottom: -2px; width: 0; z-index: 1;
  border-left: 1.5px dashed rgba(var(--verti-navy-rgb), .42); }
.pt-offnote { margin-top: 2px; text-align: right; font-size: 10px; font-weight: 700;
  color: #7B859B; font-variant-numeric: tabular-nums; }
.pt-band { position: absolute; top: 0; bottom: 0; display: flex; align-items: center; justify-content: center; z-index: 2; }
/* Sprint 272: "No Signal" gap — GREY stripes (was orange). */
.pt-gap  {
  position: absolute; top: 0; bottom: 0; cursor: pointer; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  background: repeating-linear-gradient(45deg, #C3CAD6 0 5px, #AEB6C4 5px 10px);
}
/* Sprint 272: the "+" fill marker INSIDE a fill segment (idle red / gap grey). */
.pt-plus {
  width: 15px; height: 15px; border-radius: 50%; font-size: 12px; font-weight: 900;
  line-height: 13px; text-align: center; flex: 0 0 auto; pointer-events: none;
}
.pt-plus-idle { background: rgba(255,255,255,.92); color: #DC2626; border: 1px solid #fff; }
.pt-plus-gap  { background: var(--verti-navy); color: #fff; }
/* Sprint 272: the NOW marker line at the current time (today only). */
.pt-now { position: absolute; top: -2px; bottom: -2px; width: 2.5px; background: var(--verti-navy); z-index: 6; }
/* Sprint 380: a pulsing dot (colour = current activity, set inline) rides the NOW line. */
.pt-now-mk { position: absolute; top: -6px; left: 50%; transform: translateX(-50%); width: 11px; height: 11px;
  border-radius: 50%; border: 1.5px solid #fff; background: var(--verti-navy);
  box-shadow: 0 0 0 2px rgba(var(--verti-navy-rgb), .16), 0 1px 3px rgba(0,0,0,.25); animation: pt-beat 1.7s ease-in-out infinite; }
@keyframes pt-beat { 0%,100% { box-shadow: 0 0 0 2px rgba(var(--verti-navy-rgb), .16), 0 1px 3px rgba(0,0,0,.25); }
  50% { box-shadow: 0 0 0 5px rgba(var(--verti-navy-rgb), .03), 0 1px 3px rgba(0,0,0,.25); } }
/* Sprint 382: axis is a SINGLE row — each segment/op START gets a tick; a label
   that would collide is dropped. The NOW wall-clock sits at the right end in its
   OWN format (navy/bold), replacing the old top-right live-clock time. */
.pt-axis { position: relative; height: 16px; margin-top: 4px; display: flex; align-items: stretch; } /* Sprint 439: mirrors the bar's segments */
.pt-axseg { position: relative; min-width: 0; height: 100%; }
.pt-axbrk { height: 100%; }
.pt-axtk { position: absolute; top: 0; width: 1px; height: 4px; background: #C7CEDA; transform: translateX(-50%); }
.pt-axlb { position: absolute; top: 6px; transform: translateX(-50%); font-size: 8.5px; font-weight: 700; color: var(--muted);
  white-space: nowrap; font-variant-numeric: tabular-nums; }
.pt-axnow { color: var(--verti-navy); font-weight: 800; transform: translateX(-100%); }

/* Sprint 265/272: the work bar + the connectivity strip are ONE merged component
   (the connectivity strip — with a leading signal glyph — sits flush under it). */
/* Sprint 380: overflow VISIBLE so the live clock chip sitting ABOVE the strip is
   not clipped; the inner work bar + GPS strip re-round their own outer corners. */
.pt-bars { position: relative; border: 1px solid #E1E6EF; border-radius: 7px; overflow: visible; }
.pt-bars .pt-bar { border: 0; border-radius: 6px 6px 0 0; }
.pt-bars .pt-gpsrow { border-radius: 0 0 6px 6px; overflow: hidden; }
/* Sprint 278: the work-bar state blocks are now SEPARATED rounded blocks like
   the GPS connectivity strip — a 2px vertical inset, rounded corners, and a 2px
   gap (a right border in the bar's own background colour) between consecutive
   blocks. Applies to the state bands AND the No-Signal gap block. */
.pt-bars .pt-band, .pt-bars .pt-gap {
  top: 2px; bottom: 2px; border-radius: 3px;
  border-right: 2px solid #F4F0EA; box-sizing: border-box;
}
.pt-fill { cursor: pointer; }                       /* idle band = a fill target */
/* Sprint 283: the GPS connectivity strip is FULL-WIDTH (the leading signal glyph
   moved onto the first hour-axis label — see .pt-ax0 below). */
/* Sprint 304: thin, elegant GPS strip glued directly under the work bar
   (operator request; was a chunky 20px). Same width as the bar (both fill
   .pt-bars), so the two align exactly. */
.pt-gpsrow { display: flex; align-items: center; background: #fff; border-top: 1px solid #EEF1F6; }
.pt-conn { position: relative; flex: 1; height: 4px; background: #E7ECF3;
  display: flex; align-items: stretch; }                        /* Sprint 439: mirrors the bar's segments */
.pt-cseg { position: relative; min-width: 0; height: 100%; }
.pt-cbrk { height: 100%; background: #fff; }
/* Sprint 283: the signal-bars glyph rides the first axis label ("09"), to its
   right with its BOTTOM level with the digits (shrunk; baseline-aligned). */
.pt-axis .pt-ax0 { transform: none; color: var(--verti-navy); font-weight: 800; white-space: nowrap; }
.pt-axis .pt-axsig { margin-left: 3px; }
.pt-axis .pt-axsig svg { width: 9px !important; height: 7px !important; fill: var(--verti-navy); vertical-align: baseline; }
.pt-cband { position: absolute; top: 0; bottom: 0; border-radius: 0; }

/* ===== Sprint 380: live clock chip (right end) + hover tooltip ===== */
.pt-nowclock {
  position: absolute; top: -21px; right: -1px; z-index: 9;
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 2px 7px; border-radius: 7px; font-weight: 800; font-size: 11px; line-height: 1.35;
  box-shadow: 0 3px 10px rgba(20,30,60,.22); font-variant-numeric: tabular-nums;
  animation: pt-pop .34s ease;
}
.pt-nowclock::after { content: ""; position: absolute; right: 7px; bottom: -3px; width: 7px; height: 7px;
  background: inherit; transform: rotate(45deg); border-radius: 1px; }
.pt-nowlive { width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto; animation: pt-live 1.7s ease-out infinite; }
.pt-nowcolon { animation: pt-blink 1s steps(1) infinite; }
@keyframes pt-pop  { 0% { transform: scale(1); } 42% { transform: scale(1.09); } 100% { transform: scale(1); } }
@keyframes pt-blink { 0%,50% { opacity: 1; } 51%,100% { opacity: .3; } }
@keyframes pt-live  { 0% { opacity: 1; transform: scale(1); } 60% { opacity: .45; transform: scale(.7); } 100% { opacity: 1; transform: scale(1); } }

.pt-tip {
  position: fixed; z-index: 4000; pointer-events: none; min-width: 148px; max-width: 232px;
  background: #fff; border: 1px solid #D7DEEA; border-radius: 11px; box-shadow: 0 14px 34px rgba(20,30,60,.22);
  padding: 9px 11px; font-size: 12px; color: #37415A;
}
.pt-tip::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  border-radius: 11px 0 0 11px; background: var(--brand-gradient); }
.pt-tip .tt-t { display: flex; align-items: center; gap: 7px; font-weight: 800; color: var(--verti-navy); font-size: 12.5px; margin-bottom: 2px; }
.pt-tip .tt-badge { width: 18px; height: 18px; border-radius: 5px; color: #fff; font-weight: 900; font-size: 11px;
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; }
.pt-tip .tt-dot { width: 11px; height: 11px; border-radius: 3px; flex: 0 0 auto; }
.pt-tip .tt-dot.tt-nosig { background: repeating-linear-gradient(45deg, #C3CAD6 0 3px, #AEB6C4 3px 6px); }
/* Sprint 384: the title carries the project's FULL name + the unit. The unit
   never shrinks; the project ellipsises when the two don't fit (operator rule). */
.pt-tip .tt-proj { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pt-tip .tt-unit { flex: 0 0 auto; white-space: nowrap; }
.pt-tip .tt-unit::before { content: "·"; margin-right: 5px; color: #9AA4B7; }   /* separator lives here, not in the ellipsised text */
.pt-tip .tt-row { font-variant-numeric: tabular-nums; font-weight: 700; color: #37415A; }
.pt-tip .tt-dur { margin-top: 3px; font-weight: 800; font-size: 11.5px; }
.pt-tip .tt-dur.done { color: #15803D; }
.pt-tip .tt-dur.live { color: #B26A00; }
.pt-tip .tt-dur.gapc { color: #B91C1C; }

@media (prefers-reduced-motion: reduce) {
  .pt-now-mk, .pt-nowclock, .pt-nowlive, .pt-nowcolon { animation: none; }
  .pt-nowcolon { opacity: 1; }
}

/* Sprint 272: the state-key legend popover, opened by the "?" head icon. */
.pt-pop {
  position: absolute; top: 20px; left: 0; z-index: 30; width: min(252px, 94%);
  background: #fff; border: 1px solid #D7DEEA; border-radius: 11px;
  box-shadow: 0 12px 30px rgba(20,30,60,.18); padding: 10px 12px;
}
.pt-leg-title { font-size: 10px; font-weight: 800; color: var(--verti-navy); text-transform: uppercase; letter-spacing: .3px; margin-bottom: 6px; }
.pt-legrow { display: flex; align-items: center; gap: 7px; font-size: 11px; color: #42506b; font-weight: 600; padding: 2px 0; }
.pt-legsw { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
.pt-legsw-nosig { background: repeating-linear-gradient(45deg, #C3CAD6 0 3px, #AEB6C4 3px 6px); }
.pt-legsw-pad { background: #EAEDF4; border: 1px dashed rgba(var(--verti-navy-rgb), .42); }   /* Sprint 439: view-pad zone */
.pt-legtx { flex: 1; }
.pt-legplus { width: 13px; height: 13px; border-radius: 50%; background: var(--verti-navy); color: #fff; font-size: 8px; font-weight: 900; line-height: 13px; text-align: center; flex: 0 0 auto; }
/* state % + connectivity % breakdown pills (full surfaces). */
.pt-bd { margin-top: 9px; display: flex; flex-direction: column; gap: 5px; }
.pt-bd-row { display: flex; flex-wrap: wrap; gap: 5px; }
.pt-pill {
  display: inline-flex; align-items: center; gap: 5px; font-size: 10px; font-weight: 700;
  color: var(--text); background: #F6F8FB; border: 1px solid #E7ECF3; border-radius: 999px; padding: 3px 8px;
}
.pt-pill b { color: var(--verti-navy); font-weight: 800; }
.pt-pill-lite { background: #FBFAF6; border-style: dashed; color: var(--muted); }   /* Sprint 439: lunch-allowance pill */
.pt-pdur { color: var(--muted); font-weight: 600; } /* Sprint 322: total duration next to the % */
.pt-pdot { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }
.pt-pdot.pt-gapdot { background: repeating-linear-gradient(45deg, #FCE2C7 0 3px, #FFF1E0 3px 6px); border: 1px dashed #E8924A; }

/* Sprint 266: stacked-bar + legend for the admin/manager puantaj reports. */
.pt-sbar { display: flex; height: 12px; min-width: 110px; border-radius: 4px; overflow: hidden; border: 1px solid #E1E6EF; }
.pt-sbar i { display: block; height: 100%; }
.pt-leg { display: flex; flex-wrap: wrap; gap: 10px; padding: 11px 4px 2px; }
.pt-leg-i { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--text); }
.pt-leg-d { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }
table.data td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }
table.data td.barcell, table.data th.barcell { min-width: 120px; }

/* ===== Sprint 265: mobile puantaj page (mobile-puantaj.html) ===== */
.mp-datenav {
  display: flex; align-items: center; justify-content: space-between;
  background: #fff; border-radius: 11px; padding: 7px 11px; margin-bottom: 10px;
  box-shadow: 0 2px 6px rgba(var(--verti-navy-rgb), .06);
}
.mp-datenav button {
  border: 0; background: #EEF1F8; color: var(--verti-navy); width: 32px; height: 32px;
  border-radius: 8px; font-weight: 800; font-size: 15px; cursor: pointer;
}
.mp-datenav .d { color: var(--verti-navy); font-weight: 800; font-size: 12.5px; }
.mp-tlcard { background: #fff; border-radius: 12px; padding: 11px; margin-bottom: 10px; box-shadow: 0 2px 8px rgba(var(--verti-navy-rgb), .07); }
.mp-tlcard[hidden] { display: none; }
.mp-row {
  display: flex; gap: 8px; align-items: flex-start; background: #fff;
  border-radius: 11px; padding: 8px 10px; margin-bottom: 7px;
  box-shadow: 0 1px 3px rgba(var(--verti-navy-rgb), .05);
}
.mp-row .dot { width: 9px; height: 9px; border-radius: 50%; margin-top: 3px; flex: 0 0 auto; }
.mp-row .tx { flex: 1; min-width: 0; }
.mp-row .tt { color: var(--verti-navy); font-weight: 800; font-size: 11.5px; }
.mp-row .mt { color: var(--muted); font-size: 9.5px; font-weight: 700; margin-top: 1px; }
.mp-cat {
  margin-top: 5px; display: inline-flex; align-items: center; gap: 4px;
  font-size: 9.5px; font-weight: 800; border-radius: 999px; padding: 3px 9px; cursor: pointer;
}
.mp-cat.set { background: #EAF0FB; color: var(--verti-navy); }
.mp-cat.add { background: #FFF1E0; color: #C2410C; border: 1px dashed #E8924A; }
.mp-row.gap { background: #FFFBF4; border: 1px dashed #E8924A; }
.mp-row.gap .dot { background: #E8924A; }
.mp-row.gap .tt { color: #C2410C; }
.mp-acts { display: flex; gap: 5px; flex: 0 0 auto; }
.mp-eb {
  width: 26px; height: 26px; border-radius: 7px; background: #EEF1F8; color: var(--verti-navy);
  display: flex; align-items: center; justify-content: center; font-size: 10px; border: 0; cursor: pointer;
}
.mp-empty { color: var(--muted); font-size: 12px; text-align: center; padding: 14px; }
.mp-fab {
  background: var(--brand-gradient); color: #fff; font-weight: 800; font-size: 13px;
  border: 0; border-radius: 11px; padding: 12px; width: 100%; cursor: pointer; margin-top: 2px;
}
/* composer fields inside the shared bottom-sheet */
.mp-comp .mobile-field { margin-bottom: 9px; }
.mp-comp .two { display: flex; gap: 8px; }
.mp-comp .two .mobile-field { flex: 1; }
.mp-catpick { display: flex; flex-wrap: wrap; gap: 6px; }
/* Sprint 845: the NINTH fall. puantaj-fill.js hides the sales-lane mode row
   with `modeRow.hidden = !canSales`, so without this the picker was on screen
   for every technician who has no offers to file against. */
.mp-catpick[hidden] { display: none; }
/* Sprint 459: the Sprint-279 lunch-window editor chips (.mp-lunchdur /
   .mp-durchips / .mp-lunchhint) were dead since the Sprint-439 allowance
   model removed the lunch band — rules deleted. */
/* Sprint 280: an operation (service_records) window in the day list reads as a
   dark-green WORK row, matching the summary bar. */
.mp-row.mp-oprow { background: #EAF7EE; border-color: #BFE6CB; }
.mp-catchip {
  border: 1.5px solid var(--border); background: #fff; border-radius: 999px;
  padding: 6px 11px; font-size: 11px; font-weight: 800; color: var(--verti-navy); cursor: pointer;
}
.mp-catchip.on { background: var(--verti-navy); color: #fff; border-color: var(--verti-navy); }

/* ── Sprint 636: the shared puantaj FILL layer (puantaj-fill.js) ───────────
   One panel (strip + day list + composer) for mobile-puantaj, the sales-map
   full-day modal and mobile-sales-team's sheet. It reuses the .mp-* row/chip
   vocabulary above; these rules add only what the shared panel itself needs. */
.ptf-host .ptf-add { margin: 8px 0 2px; }
.ptf-untimed-h {
  margin: 10px 2px 4px; font-size: 10px; font-weight: 800; letter-spacing: .05em;
  color: #6B7280; text-transform: uppercase;
}
.ptf-gear {
  border: 0; background: none; color: var(--verti-navy); cursor: pointer;
  font-size: 11px; padding: 0 2px; vertical-align: middle;
}
.ptf-mgr-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.ptf-mgr-row .ptf-mgr-lbl {
  flex: 1; padding: 6px 8px; border: 1px solid #D1D5DB; border-radius: 6px; font-size: 13px;
}
.ptf-mgr-add { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
.ptf-mgr-add .ptf-mgr-new {
  flex: 1; padding: 6px 8px; border: 1px solid #D1D5DB; border-radius: 6px; font-size: 13px;
}
.ptf-mgr-add .mp-fab { width: auto; padding: 9px 14px; margin: 0; }
/* On a wide screen the composer centers as a compact modal instead of a
   full-width bottom sheet (the desktop sales-map modal hosts the panel). */
@media (min-width: 900px) {
  .mobile-sheet-backdrop.ptf-wide { align-items: center; padding: 24px; }
  .mobile-sheet-backdrop.ptf-wide .mobile-sheet { max-width: 460px; border-radius: 18px; }
}

/* Sprint 259: Other Work bottom-sheet (the 5 non-service actions). */
.mobile-sheet-backdrop {
  position: fixed; inset: 0; z-index: 10040;
  background: rgba(15, 27, 51, .5); display: flex; align-items: flex-end;
}
.mobile-sheet-backdrop[hidden] { display: none; }
.mobile-sheet {
  width: 100%; background: #F4F6FA; border-radius: 18px 18px 0 0;
  max-height: 82vh; overflow: auto; box-shadow: 0 -8px 30px rgba(0, 0, 0, .3);
  animation: sheetUp .18s ease-out;
  overscroll-behavior: contain;   /* the page behind never scrolls with it */
}
@keyframes sheetUp { from { transform: translateY(28px); } to { transform: translateY(0); } }
/* z-index: a positioned control in the body (a photo thumbnail, a switch)
   otherwise paints OVER the sticky header as it scrolls under it. */
.mobile-sheet-head {
  position: sticky; top: 0; z-index: 2; background: var(--brand-gradient); color: #fff;
  font-size: 15px; font-weight: 800; padding: 13px 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  /* A title that needs two lines splits them evenly: "Nasazlığı Redaktə Et"
     left "Et" alone on the second line of a Samsung A56 (360 px). */
  text-wrap: balance;
}
.mobile-sheet-close {
  background: rgba(255, 255, 255, .2); border: 0; color: #fff;
  width: 30px; height: 30px; border-radius: 50%; font-size: 20px;
  cursor: pointer; line-height: 1;
}
.mobile-sheet-body { padding: 12px 14px 18px; display: flex; flex-direction: column; gap: 9px; }

/* ============================================================================
   THE HOUSE MOBILE ELEMENTS - operator, 24 Sep 2026 (hotfix ut-review-findings):
   every phone page speaks ONE UI, the same element for the same job, working
   the same way. The sheet above is the house modal and what follows completes
   it; the segmented choice is the house control for a pick among 2-5 options.
   The rule: .claude/rules/pages.md "Mobile". A page never draws its own copy.
   ========================================================================== */
/* The header's Save + Cancel pair: Save LEFT of Cancel, in the header, so a
   long body never hides the confirm (docs/design-system.md, Modals). */
.mobile-sheet-act { display: flex; gap: 8px; flex: 0 0 auto; }
.mobile-sheet-act[hidden] { display: none; }
.mobile-sheet-act button {
  border: 0; border-radius: 8px; padding: 6px 13px; font: inherit;
  font-size: 12.5px; font-weight: 800; cursor: pointer;
}
.mobile-sheet-act .sv { background: #fff; color: var(--verti-navy); }
.mobile-sheet-act .cn { background: rgba(255, 255, 255, .2); color: #fff; }
.mobile-sheet-act button:disabled { opacity: .5; cursor: default; }
/* A long form opens to 92% of the screen - dvh, the height left while the
   browser bar shows; vh first for a browser without it. */
.mobile-sheet.tall { max-height: 92vh; max-height: 92dvh; }
/* On a tablet or a desk screen a sheet keeps a phone's reading width,
   centred, instead of stretching a two-segment row across 2000 px (the
   Start-a-free-job sheet was capped at 480 px on its own; now every sheet is).
   ...and it stands in the MIDDLE of the screen, a dialog of its own (operator,
   24 Sep 2026, hotfix ut-review-findings: "masaüstü bilgisayar olduğu için
   alttan çıkması kullanışsız"). Rising from the bottom edge is a phone's
   gesture; on a desk the eye is not there, and a long form ran off it. So:
   four round corners, a shadow on every side, 24 px clear of each edge, and
   a long form scrolls inside its own frame under the sticky header. */
@media (min-width: 700px) {
  .mobile-sheet-backdrop { justify-content: center; align-items: center; padding: 24px; }
  .mobile-sheet { max-width: 560px; border-radius: 18px; box-shadow: 0 18px 48px rgba(0, 0, 0, .32); }
  .mobile-sheet, .mobile-sheet.tall { max-height: calc(100vh - 48px); max-height: calc(100dvh - 48px); }
}
/* A sheet that other layers open OVER. The frame's 10040 sits above the QR
   scanner (9999), the part picker (96) and the GPS warning (4000): each,
   opened from inside it, drew behind it unseen. 94 keeps the page's own
   shell under it and all of them over it. Two `.low` sheets tie on 94 and
   the one later in the DOM wins - the Start-a-free-job sheet is appended to
   <body> on first use, so it always opens over the new-job form it can be
   opened from. */
.mobile-sheet-backdrop.low { z-index: 94; }
/* The caption above a control inside a sheet. On a .mobile-field label it
   out-ranks the field's own label look, so one caption looks one way. */
.mobile-sheet-cap,
.mobile-field label.mobile-sheet-cap {
  display: block; margin: 0 0 4px; font-size: 11px; font-weight: 800;
  letter-spacing: .3px; color: #6B7280; text-transform: uppercase;
}
.mobile-sheet-cap[hidden] { display: none; }
/* A group of fields inside a sheet: one white card per group. */
.mobile-sheet-box {
  background: #fff; border: 1px solid #DDE3EE; border-radius: 12px;
  padding: 10px 12px; margin: 0 0 10px;
}
.mobile-sheet-box[hidden] { display: none; }
.mobile-sheet-box .mobile-field { margin-bottom: 8px; }
.mobile-sheet-box .mobile-field:last-child { margin-bottom: 0; }
/* Label and control on ONE line (operator, Sprint 450: vertical space is
   precious on the phone) - the unit picker's Project and Unit rows. */
.mobile-field.inline { display: flex; align-items: center; gap: 8px; }
.mobile-field.inline[hidden] { display: none; }
.mobile-field.inline > label { flex: 0 0 auto; margin: 0; min-width: 52px; }
.mobile-field.inline > select { flex: 1; min-width: 0; }
/* The button that picks the unit by its QR code - one look on every sheet.
   Its label WRAPS inside the button rather than being cut: "QR ilə Avadanlıq
   Seç" needed 140 px of the 125-131 a Samsung A56 row leaves it. */
.mobile-qr-btn {
  flex: 1; min-width: 0; min-height: 42px; border: 0; border-radius: 10px;
  color: #fff; background: var(--brand-gradient); font: inherit;
  font-weight: 800; font-size: 12px; line-height: 1.15;
  display: flex; align-items: center; justify-content: center; gap: 6px;
  padding: 8px 6px; white-space: normal; text-align: center; overflow: hidden; cursor: pointer;
}
.mobile-qr-btn[hidden] { display: none; }

/* The segmented choice - ONE row of 2-5 options (operator: "ikili, uclu,
   dortlu"), the chosen one navy; the look is finance-dashboard's .cm-seg.
   Equal shares, and the automatic minimum of a flex item keeps the longest
   word whole: a long label wraps inside its own segment while the short ones
   give way. The font steps down by option count so five fit a Samsung A56
   (360 px). NO default where the choice is the user's: an unanswered row
   wears .need until it is answered. The state is the ARIA attribute, so every
   page reads and styles the same one. */
.seg-choice {
  display: flex; width: 100%; box-sizing: border-box; margin: 0;
  border: 1px solid var(--border); border-radius: 10px; overflow: hidden; background: #fff;
}
.seg-choice[hidden] { display: none; }
.seg-choice > button {
  flex: 1 1 0; min-height: 42px; margin: 0; padding: 8px 6px;
  border: 0; border-right: 1px solid var(--border); border-radius: 0;
  background: #fff; color: var(--verti-navy); font: inherit;
  font-size: 12.5px; font-weight: 700; line-height: 1.2; text-align: center;
  white-space: normal; cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.seg-choice > button:last-child { border-right: 0; }
.seg-choice > button:is([aria-checked="true"], [aria-pressed="true"]) { background: var(--verti-navy); color: #fff; }
.seg-choice > button:focus-visible { outline: 2px solid #7A2E6B; outline-offset: -2px; }
.seg-choice:has(> button:nth-child(4)) > button { font-size: 11.5px; padding: 8px 4px; }
.seg-choice:has(> button:nth-child(5)) > button { font-size: 10.5px; padding: 7px 3px; }
.seg-choice.need { border: 1.4px dashed #B45309; }
/* hotfix mp-collapse-delete: the same orange on a missing text box or photo
   area after a save check (house-sheet.js sheetMissing), cleared on touch. */
.need-hl { outline: 1.4px dashed #B45309; outline-offset: 3px; border-radius: 10px; }
/* The New job sheet's closing hint line - assets/js/house-sheet.js draws the
   sheet for the phone (mobile-report.html) and the desk (unit-task.html). */
.fr-sheet-hint { font-size: 11px; color: #6B7280; }

/* ── THE UNIT TASK TAB PILLS ────────────────────────────────────────────────
   Sprint 533 put the house pill material on the Unit Task desk's tabs
   (unit-task.html); hotfix billable-jobs-ui (operator, 25 Sep 2026) put the
   same tabs on the Billable Jobs desk (part-replacements.html), so they live
   here, once, moved from unit-task.html unchanged.
   Hotfix ut-review-findings (operator, 24 Sep 2026): the header's shortcut
   pills took THIS pill's size (31px, 13px, 16px sides; .ph-link), so the two
   must stay one pill in SIZE. Its text wears the app's font, not the browser's
   button font (Arial), and its height is pinned (line-height 29 + the 1px
   rim), not left to a font's metrics.
   THE HOUSE RULE ON COLOUR (operator, 25 Sep 2026, hotfix billable-jobs-ui):
   a FILTER - a tab pill, a scope toggle, a chip that narrows a list - is
   VERTI NAVY when pressed, always; the VERTI+TKE brand gradient (and its glass
   sheen) is for BUTTONS only, the actions (.btn, .ph-link, the sheet header).
   The active pill wore the header link's glass material from 24 Sep to
   25 Sep 2026; the operator turned it back ("filtreler her zaman verti navy,
   sadece butonlar gradient"). Same rule, same element, on every page.
   The page's primary New button stands at the RIGHT END of the tab row, not
   on a row of its own (same hotfix, operator): a .btn in the row goes right. */
.fr-tabs { display:flex; gap:6px; margin:0 0 14px; flex-wrap:wrap; align-items:center; }
.fr-tabs > .btn { margin-left:auto; }
.fr-tab { border:1px solid var(--border); border-radius:999px; padding:0 16px; line-height:29px;
          font-family:inherit; font-size:13px;
          color:#374151; background:#fff; cursor:pointer; font-weight:700; position:relative;
          transition:transform var(--dur-pill) var(--ease-spring), filter .15s; }
.fr-tab:hover { filter:brightness(1.04); }
.fr-tab:active { transform:scale(.96); }
.fr-tab.on { background:var(--verti-navy); border-color:var(--verti-navy); color:#fff; }
.fr-tab b { color:#E85A2A; } .fr-tab.on b { color:#fff; }
/* ── THE UNIT TASK ROW BUTTON ───────────────────────────────────────────────
   Unit Task's action button (unit-task.html, Sprint 442/533): white, a 1px
   rim, 8px corners, navy bold text; .pri = navy fill, .ok = green fill. Moved
   here unchanged by hotfix billable-jobs-ui (operator, 25 Sep 2026), which put
   the same button on the Billable Jobs desk's rows and added two variants that
   page's actions needed: .go, the amber of a thing to do NOW (Start; #B45309
   measures 5.02:1 under white, the gate wants >= 3:1), and .bad, the red text
   of an undo, a cancel or a delete (outlined, never a red fill). The house
   micro-motion (Sprint 533) rides along; the reduced-motion guard disables it. */
.fr-btn { border:1px solid var(--border); background:#fff; border-radius:8px; padding:7px 13px;
          font-size:12.5px; font-weight:700; cursor:pointer; color:var(--verti-navy); }
.fr-btn.pri { background:var(--verti-navy); color:#fff; border-color:var(--verti-navy); }
.fr-btn.ok  { background:#15803D; color:#fff; border-color:#15803D; }
.fr-btn.go  { background:#B45309; color:#fff; border-color:#B45309; }
.fr-btn.bad { color:#B91C1C; }
.fr-btn:disabled { opacity:.45; cursor:default; }
/* A row button that is a TOGGLE - the Before-photo exit (`<photo> not
   available`) on the Billable Jobs phone's Finish composer and on its New
   Billable Job sheet - shows its pressed state in the amber it always wore.
   hotfix mp-collapse-delete moved it here from mobile-part.html, where it was
   scoped to `.mp-acts` and so never reached the exit button it was written
   for (that one sits in the composer, outside the action row). */
.fr-btn[aria-pressed="true"] { background:#B45309; color:#fff; border-color:#B45309; }
.fr-btn { transition:transform var(--dur-pill) var(--ease-spring), filter .15s; }
.fr-btn:not(:disabled):hover { filter:brightness(1.05); }
.fr-btn:not(:disabled):active { transform:scale(.96); }
/* ── THE UNIT TASK COMPLETE BUTTON ──────────────────────────────────────────
   The full-width green button that closes a job from its card - the Unit
   Task phone's `Mark fixed` (mobile-report.html, Sprint 442). Moved here
   unchanged by hotfix billable-jobs-ui (operator, 25 Sep 2026), which gave the
   Billable Jobs phone's Finish composer the same button; it is never greyed
   for a missing field (the house save check names them), :disabled is a save
   in flight. */
.fr-mark { width:100%; border:0; border-radius:10px; padding:9px; font-size:12.5px; font-weight:800;
           color:#fff; background:#15803D; margin-top:6px; }
.fr-mark:disabled { background:#C3CAD6; }

/* ── THE UNIT TASK STATUS TILES ─────────────────────────────────────────────
   Sprint unit-task-intake drew them on the phone (mobile-report.html); hotfix
   ut-review-findings (operator, 24 Sep 2026: "one to one with the phone") put
   the same tiles on the desk (unit-task.html), so they live here, once, moved
   from the phone page unchanged. The two pages lay them out differently (two
   across on the phone, one row on the desk), which is each page's own grid
   rule; everything a tile IS is this block.
   The same component mobile-part.html has carried since sprint
   billable-jobs-phone, deliberately: the two pages are two tabs of one bar
   and a second look would read as a second product. Buttons with
   aria-pressed, so the open one is announced and not merely coloured. */
.fr-tiles { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:2px 0 10px; }
.fr-tile { text-align:left; border:1px solid var(--border); border-left-width:4px;
  background:#fff; border-radius:12px; padding:5px 8px; cursor:pointer;
  display:grid; grid-template-columns:auto 1fr; grid-template-rows:auto auto;
  column-gap:6px; row-gap:1px; align-items:center; align-content:center;
  min-height:0; font:inherit; }
/* display:grid above out-ranks the UA's [hidden] rule (the house trap the
   static gate catches): a tile a page hides must really hide. */
.fr-tile[hidden] { display:none; }
/* Hotfix unit-task (operator): two lines per tile - a one-line name and
   a one-line hint beside the count - and less air above and below.
   Measured at 412 CSS px (Samsung A56) with THREE-digit counts in az,
   tr and en: 30 / 10.5 / 9.5 px is the largest set where every tile
   stays 1+1 and the name keeps 16 px clear of the check mark. */
/* Hotfix unit-task (operator): the count stands FULL-HEIGHT at the start
   of the tile and the name + hint sit to its right (was: count above). */
/* hotfix unit-task-speed: longhands. `font:800 19px/1 inherit` is an
   INVALID shorthand (a CSS-wide keyword cannot stand in for the family),
   so the browser dropped all three rules and the tiles read as a button's
   13px default text instead of a count over a name (Sprint 234's trap). */
.fr-tile .n  { grid-column:1; grid-row:1 / span 2; font-size:30px; font-weight:800; line-height:1;
               color:var(--verti-navy); font-variant-numeric:tabular-nums; }
.fr-tile .nm, .fr-tile .hint { grid-column:2; }
.fr-tile .nm { font-size:10.5px; font-weight:700; line-height:1.2; color:var(--verti-navy); }
.fr-tile .hint { font-size:9.5px; font-weight:500; line-height:1.25; color:#6B7280; }
.fr-tile[data-tile="decide"] { border-left-color:#B45309; }
.fr-tile[data-tile="decide"] .n { color:#B45309; }
.fr-tile[data-tile="open"]   { border-left-color:var(--verti-navy); }
.fr-tile[data-tile="check"]  { border-left-color:#1D6FB8; }
.fr-tile[data-tile="check"] .n { color:#1D6FB8; }
.fr-tile[data-tile="done"]   { border-left-color:#15803D; }
.fr-tile[data-tile="done"] .n { color:#15803D; }
/* The desk's fifth tile (same hotfix): Out of service, in the red its card
   wore. It opens its own tab rather than filtering, so it is never pressed. */
.fr-tile[data-tile="oos"]    { border-left-color:#DC2626; }
.fr-tile[data-tile="oos"] .n { color:#DC2626; }
/* Hotfix billable-jobs-ui (operator, 25 Sep 2026: "the Unit Task look"): the
   Billable Jobs desk (part-replacements.html) wears the same tile for its five
   money counters - the keys and colours below, one per money state, in the
   house state colours (to quote = amber, waiting = blue, to invoice = navy,
   money due = green, declined = red). There Out of service FILTERS the list
   (its working principle), so that tile can be pressed and takes a --tc too;
   on Unit Task it is never pressed and nothing there changes.
   These stand ABOVE the pressed block on purpose: a key's `.n` colour and the
   pressed white have the same specificity, so the later one wins - the
   operator saw an amber count on an amber tile when they stood below it. */
.fr-tile[data-tile="not_quoted"]          { border-left-color:#B45309; --tc:#B45309; }
.fr-tile[data-tile="not_quoted"] .n       { color:#B45309; }
.fr-tile[data-tile="quoted_no_answer"]    { border-left-color:#1D6FB8; --tc:#1D6FB8; }
.fr-tile[data-tile="quoted_no_answer"] .n { color:#1D6FB8; }
.fr-tile[data-tile="done_not_invoiced"]   { border-left-color:var(--verti-navy); }
.fr-tile[data-tile="invoiced_not_paid"]   { border-left-color:#15803D; --tc:#15803D; }
.fr-tile[data-tile="invoiced_not_paid"] .n { color:#15803D; }
.fr-tile[data-tile="declined"]            { border-left-color:#DC2626; --tc:#DC2626; }
.fr-tile[data-tile="declined"] .n         { color:#DC2626; }
.fr-tile[data-tile="oos"]                 { --tc:#DC2626; }
/* ...and the Billable Jobs PHONE's four keys (mobile-part.html, sprint
   billable-jobs-phone): the colours its own copy of this component wore,
   now here beside the rest - amber = anyone can start, navy = waiting,
   blue = being worked on, green = done in the last 7 days. */
.fr-tile[data-tile="unassigned"]          { border-left-color:#B45309; --tc:#B45309; }
.fr-tile[data-tile="unassigned"] .n       { color:#B45309; }
.fr-tile[data-tile="waiting"]             { border-left-color:var(--verti-navy); }
.fr-tile[data-tile="in_progress"]         { border-left-color:#1D6FB8; --tc:#1D6FB8; }
.fr-tile[data-tile="in_progress"] .n      { color:#1D6FB8; }
.fr-tile[data-tile="done7"]               { border-left-color:#15803D; --tc:#15803D; }
.fr-tile[data-tile="done7"] .n            { color:#15803D; }
/* hotfix unit-task-speed (operator: "the user must SEE which tile is
   selected"): the open tile FILLS with its own colour and turns its text
   white — the page's own selected look, the one the Everyone / Mine chips
   wear — and carries a check mark, so the state is not colour alone.
   It was a 2 px ring at 16 % opacity. */
.fr-tile { position:relative; --tc:var(--verti-navy);
  transition:background-color var(--dur-pill, .2s) var(--ease-spring, ease),
             box-shadow var(--dur-pill, .2s) var(--ease-spring, ease); }
.fr-tile[data-tile="decide"] { --tc:#B45309; }
.fr-tile[data-tile="check"]  { --tc:#1D6FB8; }
.fr-tile[data-tile="done"]   { --tc:#15803D; }
.fr-tile[aria-pressed="true"] { background:var(--tc); border-color:var(--tc);
  box-shadow:0 6px 16px rgba(13,21,38,.24); }
.fr-tile[aria-pressed="true"] .n,
.fr-tile[aria-pressed="true"] .nm { color:#fff; }
.fr-tile[aria-pressed="true"] .hint { color:rgba(255,255,255,.86); }
.fr-tile[aria-pressed="true"]::after { content:'✓'; position:absolute; top:4px; right:4px;
  width:15px; height:15px; border-radius:50%; background:#fff; color:var(--tc);
  font-size:10px; font-weight:800; line-height:15px; text-align:center; }

/* ── THE UNIT TASK REPORT FORM ──────────────────────────────────────────────
   Sprint 442 drew the item composer on the phone (mobile-report.html), and
   hotfix ut-review-findings put it in the house sheet - then opened the
   desk's New report and ✎ Edit (unit-task.html) in the same sheet (operator,
   24 Sep 2026: "the same format and layout as the phone"). So the card lives
   here, once, moved from the phone page unchanged, bar `cursor:pointer` on
   its five controls: a desk mouse needs it, a phone never draws it. Each
   choice is the shared .seg-choice under its caption, in the operator's
   order - kind, suggestion, severity, then the description - and nothing is
   preselected, so an unanswered row wears .need and the save names it.
   The CARD is scoped to the `.fr-form` it sits in, because unit-task.html
   calls its unit detail rows `.fr-item` too. The phone's own extras (the job
   line, the photo nudge, the sent badge) stay on the phone page; the
   fixed-on-the-spot switch row moved here (below) when the Billable Jobs
   phone's New Billable Job sheet drew the same switch. */
.fr-form .fr-item { background:#fff; border:1px solid #DDE3EE; border-left:4px solid #7A2E6B;
  border-radius:12px; padding:10px 12px; margin-bottom:10px; }
.fr-item-h { display:flex; align-items:center; gap:7px; margin:0 0 4px; }
.fr-item-h .mobile-sheet-cap { margin:0; }
.fr-form .fr-item .seg-choice { margin:0 0 10px; }
.fr-del { border:0; background:none; color:#A6394F; font-size:16px; padding:2px 8px; margin-left:auto; cursor:pointer; }
/* Hotfix unit-task (operator): the ✕ shows only when there is a second item
   to remove - on a lone item the handler refuses anyway, so it was dead. */
.fr-form .fr-item:only-child .fr-del { display:none; }
.fr-form .fr-item textarea { width:100%; min-height:56px; }
.fr-photos { display:flex; gap:8px; align-items:center; margin:8px 0 2px; flex-wrap:wrap; }
.fr-ph { width:52px; height:52px; border-radius:10px; object-fit:cover; border:1px solid #DDE3EE; position:relative; }
.fr-phwrap { position:relative; }
.fr-phx { position:absolute; top:-7px; right:-7px; width:20px; height:20px; border-radius:50%;
  border:0; background:#A6394F; color:#fff; font-size:11px; line-height:20px; padding:0; cursor:pointer; }
.fr-phbtn { width:52px; height:52px; border-radius:10px; border:1.4px dashed #C3CAD6; background:#F3F5F9;
  color:var(--verti-navy); font-size:17px; cursor:pointer; }
/* A photo already on the server (sprint unit-task-fixes S8): a green rim, no
   ✕, and a ✓-only badge - "✓ Gönderildi" was 73 px under a 52 px thumbnail and
   two badges overlapped in tr and az; the word stays in aria-label. The Complete
   panel's photos wear it on the phone, the Complete sheet's on the desk. */
.fr-phwrap.sent .fr-ph { border:2px solid #15803D; }
.fr-phsent { position:absolute; left:50%; bottom:-8px; transform:translateX(-50%); background:#15803D; color:#fff;
  width:20px; height:20px; box-sizing:border-box; display:flex; align-items:center; justify-content:center;
  font-size:11px; font-weight:800; line-height:1; border-radius:50%; border:1.5px solid #fff; }
/* The Out of Service row - Sprint 450: a tight frame, an even WIDER switch. */
.fr-oos { display:flex; justify-content:space-between; align-items:center; gap:10px;
  background:#fff; border:1px solid #DDE3EE; border-radius:12px; padding:5px 12px; margin:8px 0; }
.fr-oos .tx b { display:block; color:#DC2626; font-size:14px; line-height:1.2; }
.fr-tgl { width:72px; height:32px; border-radius:999px; background:#DDE3EE; border:0; position:relative;
  flex:0 0 auto; transition:background .15s; cursor:pointer; }
.fr-tgl::after { content:""; position:absolute; width:26px; height:26px; border-radius:50%; background:#fff;
  top:3px; left:3px; box-shadow:0 1px 4px rgba(0,0,0,.3); transition:left .15s; }
.fr-tgl.on { background:#DC2626; }
.fr-tgl.on::after { left:43px; }
/* The small switch (Sprint 532): the phone's unit group headers and its
   fixed-on-the-spot row. */
.fr-tgl.sm { width:44px; height:24px; }
.fr-tgl.sm::after { width:18px; height:18px; }
.fr-tgl.sm.on::after { left:23px; }
/* THE FIXED-ON-THE-SPOT ROW - the small switch in its green frame, its label
   and its hint, and the completion block it opens (.fr-spotmore). Sprint
   job-photos drew it on the Unit Task phone's filing form (mobile-report.html);
   hotfix mp-collapse-delete (operator, 25 Sep 2026) moved it here unchanged;
   the Billable Jobs phone's start-without-approval sheet (mobile-actions.js,
   openStartJobNow) shares .fr-spot-off - one element, not a copy (its
   .fr-spotmore finish block retired, 25 Sep 2026: a start closes that sheet
   and the job is finished from its card). .fr-spot-off is the house offline
   line by a control the server has to
   answer (it is disabled with no signal). Each is toggled with `hidden`, so each carries its
   own display:none beside the rule that sets display (the house trap). */
.fr-spot { display:flex; align-items:center; gap:9px; flex-wrap:wrap;
           background:#F0FAF4; border:1px solid #BFE2D0; border-radius:11px;
           padding:8px 10px; margin-top:8px; }
.fr-spot-lbl { font-size:12.5px; font-weight:800; color:#15803D; }
.fr-spot-hint { flex:1 1 100%; font-size:10.5px; color:#3F6B53; line-height:1.4; }
.fr-spot-off { flex:1 1 100%; font-size:11.5px; font-weight:700; color:#B45309; line-height:1.4; }
.fr-spot .fr-tgl:disabled { opacity:.5; cursor:default; }
.fr-spotmore { display:block; margin-top:8px; }
.fr-spot[hidden], .fr-spot-off[hidden], .fr-spotmore[hidden] { display:none !important; }
.fr-add { width:100%; border:1.6px solid var(--verti-navy); background:#fff; color:var(--verti-navy); font-weight:700;
  border-radius:12px; padding:10px; font-size:13px; margin:2px 0 10px; cursor:pointer; }
/* A decided item its reporter may no longer change, in an edit (Sprint 452). */
.fr-lockbox { background:#EAEDF4; border-radius:11px; padding:8px 11px; margin-bottom:9px; font-size:11px; color:#6B7280; }
.fr-lockbox b { color:var(--verti-navy); }

/* Sprint 459: dead Sprint-261 .mobile-alert-chip rules removed (no matching
   markup — the counts moved to the header badges + bottom-nav badges). */

/* The header title is VERTI navy + bold. */
.mobile-headrow-title { color: var(--verti-navy); font-weight: 800; }

/* ============================================================================
   Sprint 303 — mobile home app-shell (technician + supervisor)
   Fixed top (header + puantaj + connectivity strip + always-on live strip),
   scrollable middle, fixed bottom nav. Applied only when the shell carries
   .msh-app (added in JS for the field roles); every other role keeps the
   original single-column layout untouched.
   ========================================================================== */
.mobile-shell.msh-app {
  height: 100vh; height: 100dvh;
  max-width: 480px; margin: 0 auto; padding: 0;
  display: flex; flex-direction: column; overflow: hidden;
}
.msh-app .msh-top {
  flex: 0 0 auto; position: relative; z-index: 5;
  background: var(--verti-navy-50);
  box-shadow: 0 4px 10px rgba(20, 30, 60, .10);
}
.msh-app .msh-mid {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  -webkit-overflow-scrolling: touch; padding: 4px 8px 10px;
}

/* Sprint 444 — pull-to-refresh indicator (VERTI orange ring, approved mockup).
   Sticky inside the scroll container so it rides the top edge; --ptr is the
   live pull distance the JS sets while the finger drags. */
.msh-ptr {
  position: sticky; top: 0; height: 0; z-index: 40;
  display: flex; justify-content: center; overflow: visible;
  pointer-events: none;
}
.msh-ptr-win { position: fixed; left: 0; right: 0; top: 0; }
.msh-ptr .msh-ptr-spin {
  width: 34px; height: 34px; border-radius: 50%; background: #fff;
  box-shadow: 0 3px 10px rgba(var(--verti-navy-rgb), 0.28);
  display: flex; align-items: center; justify-content: center;
  transform: translateY(calc(var(--ptr, 0px) - 44px));
  opacity: 0; transition: opacity 0.12s;
}
.msh-ptr.show .msh-ptr-spin { opacity: 1; }
.msh-ptr.spin .msh-ptr-spin { transition: transform 0.15s; }
.msh-ptr .msh-ptr-spin i {
  width: 18px; height: 18px; border-radius: 50%;
  border: 2.5px solid var(--border, #DDE3EE); border-top-color: var(--tke-orange);
  transform: rotate(calc(var(--ptr, 0px) * 3.4deg));
}
.msh-ptr.spin .msh-ptr-spin i { animation: msh-ptr-rot 0.9s linear infinite; }
@keyframes msh-ptr-rot { to { transform: rotate(360deg); } }
.msh-app .msh-nav { flex: 0 0 auto; }

/* Header — full-bleed gradient, edge-to-edge into the status bar (native side
   makes the status bar transparent; env(safe-area-inset-top) pads the notch). */
.msh-app .mobile-topbar {
  margin: 0; border-radius: 0; gap: 7px;
  padding: calc(env(safe-area-inset-top, 0px) + 3px) 11px 9px;
}
.msh-app .mobile-brand-mark { width: 32px; height: 32px; }

/* Wordmark lockup: VERTI AIM over TKE DISTRIBUTOR, two lines the SAME width,
   block height = the V-badge ring (32px), lines close but not touching. */
.msh-app .mobile-wordmark {
  height: 32px; justify-content: center; line-height: 1;
  margin-left: -3px; flex: 1; min-width: 0;
}
.msh-app .mobile-wordmark b {
  font-weight: 900; font-size: 17.6px; letter-spacing: .3px; color: #fff;
}
.msh-app .mobile-wordmark small {
  font-weight: 800; font-size: 7.7px; letter-spacing: 1.43px;
  color: rgba(255, 255, 255, .9); margin-top: 1.5px;
  white-space: nowrap; overflow: hidden; text-overflow: clip;
}

/* Header badges (conditional — shown only when their count > 0):
   sync queue · notifications · GPS mismatch (this month's total). */
.msh-badges { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; }
.msh-badge {
  position: relative; width: 25px; height: 25px; border-radius: 50%;
  background: rgba(255, 255, 255, .16); border: 1px solid rgba(255, 255, 255, .5);
  color: #fff; display: grid; place-items: center; font-size: 11px;
  padding: 0; cursor: pointer;
}
.msh-badge[hidden] { display: none !important; }
.msh-badge-n {
  position: absolute; top: -4px; right: -4px;
  background: var(--tke-orange); color: #fff;
  font-size: 8px; font-weight: 800; line-height: 1;
  border-radius: 7px; padding: 1px 3px; min-width: 12px; text-align: center;
}

/* Avatar carries the 2-state GPS dot (green reporting / grey offline). */
.msh-app .mobile-user-chip { position: relative; }
.msh-gps-dot {
  position: absolute; bottom: -1px; right: -1px;
  width: 10px; height: 10px; border-radius: 50%;
  background: #22C55E; border: 2px solid #fff;
}
.msh-gps-dot.off { background: #94A3B8; }

/* Puantaj timeline sits flush under the header. */
.msh-app .msh-top .mp-tlcard { margin: 6px 11px 2px; padding: 5px 9px; }

/* ── Always-visible live-status strip ────────────────────────────────────
   Active op → the existing green active card (relocated, compact). No active
   op → the current puantaj state (colour-coded, no button). */
.msh-app .mobile-active-card { margin: 8px 11px; padding: 9px 12px; border-radius: 13px; }
.msh-live-idle {
  margin: 8px 11px; border-radius: 13px; background: #fff;
  display: flex; align-items: center; gap: 9px; padding: 9px 11px;
  border-left: 4px solid var(--msh-c, #86C99A);
  box-shadow: 0 1px 3px rgba(20, 30, 60, .06);
}
.msh-live-idle[hidden] { display: none !important; }
.msh-live-idle .dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--msh-c, #86C99A); flex: 0 0 auto;
}
.msh-live-idle .tx { flex: 1; min-width: 0; }
.msh-live-idle .tx b {
  display: block; font-size: 12.5px; font-weight: 800; color: var(--verti-navy);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.msh-live-idle .tx small { font-size: 9.5px; color: var(--muted); }
.msh-live-idle .clk {
  font-size: 10px; font-weight: 800; color: var(--muted);
  flex: 0 0 auto; font-family: ui-monospace, Menlo, monospace;
}

/* Sprint 325: THE shared live-status strip (team-card.js `liveStripHtml`) — the
   read-only "canlı kart" on the map.html team cards + the mobile-team selected
   person; one class so a change lands on every live strip. Accent colour comes
   from --ls-c (the active operation, else the puantaj state). */
.live-strip {
  display: flex; align-items: center; gap: 8px;
  background: #fff; border-radius: 10px; padding: 7px 10px;
  border-left: 4px solid var(--ls-c, #94A3B8);
  box-shadow: 0 1px 4px rgba(13, 26, 43, .08);
}
.live-strip .ls-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--ls-c, #94A3B8); flex: 0 0 auto; }
.live-strip .ls-tx { flex: 1; min-width: 0; }
.live-strip .ls-tx b {
  display: block; font-size: 12px; font-weight: 800; color: var(--ls-c, var(--verti-navy));
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.live-strip .ls-tx small {
  display: block; font-size: 9.5px; color: var(--muted); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.live-strip .ls-clk {
  font-size: 11px; font-weight: 800; color: var(--ls-c); flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}
/* Sprint 337: project·unit split — project ellipsizes, unit is protected. */
.live-strip .ls-tx small.ls-sub { display: flex; align-items: baseline; gap: 3px; overflow: visible; }
.live-strip .ls-sub .ls-p { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-strip .ls-sub .ls-u { flex: 0 0 auto; white-space: nowrap; }
.live-strip .ls-sub .ls-sep { flex: 0 0 auto; }
.live-strip .ls-seen { font-size: 10px; font-weight: 700; color: var(--muted); flex: 0 0 auto; }
.live-strip .ls-live {
  display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto;
  font-size: 8.5px; font-weight: 800; color: var(--ls-c); letter-spacing: .4px;
}
.live-strip .ls-live i {
  width: 7px; height: 7px; border-radius: 50%; background: var(--ls-c);
  animation: ls-blink 1.4s infinite;
}
@keyframes ls-blink { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ── Fixed bottom navigation ──────────────────────────────────────────── */
.msh-nav {
  display: flex; gap: 1px; background: #fff;
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 10px rgba(20, 30, 60, .08);
  padding: 7px 4px calc(env(safe-area-inset-bottom, 0px) + 8px);
}
/* Sprint 460: instant navigation feedback — the tapped tab dims and a thin
   indeterminate brand-gradient line sweeps along the top edge until the next
   document paints (full-page navs over a ~220ms-RTT link read as dead taps
   without it). mobile-shell.js wireNavFeedback() drives both. */
.msh-navprog {
  position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 12000;
  background: rgba(var(--verti-navy-rgb), .12); overflow: hidden; pointer-events: none;
}
.msh-navprog[hidden] { display: none; }
.msh-navprog i {
  position: absolute; top: 0; bottom: 0; left: -40%; width: 40%;
  background: var(--brand-gradient); border-radius: 2px;
  animation: msh-navprog-sweep 0.9s ease-in-out infinite;
}
@keyframes msh-navprog-sweep { to { left: 100%; } }
.msh-tab.nav-going, .msh-more-item.nav-going { opacity: .55; }
.msh-tab {
  flex: 1 1 0; min-width: 0; text-decoration: none; position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.msh-tab .ic {
  width: 30px; height: 22px; border-radius: 8px;
  display: grid; place-items: center; font-size: 14px; color: #8A94A8;
}
.msh-tab .lb {
  font-size: 7px; font-weight: 700; color: #8A94A8;
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.msh-tab.on .ic { background: var(--brand-gradient); color: #fff; }
.msh-tab.on .lb { color: var(--verti-navy); font-weight: 900; }
.msh-tab:active .ic { transform: scale(.94); }
.msh-tab-badge {
  position: absolute; top: -3px; right: 16%;
  background: var(--tke-orange); color: #fff;
  font-size: 8px; font-weight: 800; line-height: 1;
  border-radius: 7px; padding: 1px 4px;
}
.msh-tab-badge[hidden] { display: none !important; }
/* sprint billable-jobs-phone — the "there is something behind this door" dot.
   It sits where a badge would, minus the number: the Diğer tab counts nothing
   of its own, it only says that an entry inside its sheet is asking for
   attention (today: the supervisor's puantaj, which left the bar for it).
   [hidden] from JS needs its own display rule (.claude/rules/pages.md). */
.msh-tab-dot {
  position: absolute; top: -1px; right: 16%;
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--tke-orange);
  box-shadow: 0 0 0 1.5px #fff;
}
.msh-tab:not(.on) .msh-tab-dot { top: -5px; right: -5px; }
.msh-tab-dot[hidden] { display: none !important; }

/* "Diğer" (more) bottom sheet — the operations not on the nav bar. */
.msh-more-backdrop {
  position: fixed; inset: 0; z-index: 1200;
  background: rgba(15, 23, 42, .45);
  display: flex; align-items: flex-end;
}
.msh-more-backdrop[hidden] { display: none !important; }
.msh-more-sheet {
  width: 100%; max-width: 480px; margin: 0 auto;
  background: #fff; border-radius: 18px 18px 0 0;
  padding: 14px 16px calc(env(safe-area-inset-bottom, 0px) + 18px);
  box-shadow: 0 -8px 26px rgba(15, 23, 42, .28);
}
.msh-more-head {
  display: flex; align-items: center; justify-content: space-between;
  font-weight: 900; color: var(--verti-navy); font-size: 14px; margin-bottom: 12px;
}
.msh-more-close {
  border: 0; background: none; font-size: 26px; line-height: 1;
  color: var(--muted); cursor: pointer; padding: 0 4px;
}
/* Sprint 427: the columns are set inline in the render = the item count, so the
   icons auto-fit ONE row whatever the count. minmax(0,1fr) lets them shrink. */
.msh-more-grid { display: grid; gap: 12px 6px; }
.msh-more-item {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-decoration: none; min-width: 0;
}
.msh-more-ic {
  width: 46px; height: 46px; border-radius: 14px;
  display: grid; place-items: center; font-size: 18px; color: #fff;
  background: var(--brand-gradient); flex: 0 0 auto;
}
.msh-more-lb {
  font-size: 10px; font-weight: 700; color: var(--verti-navy);
  text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}

/* The header badge cluster shows only in app-shell mode. (Sprint 459: the old
   .mobile-action-row / .mobile-notif-bubbles hide rule went with their markup.) */
.msh-badges { display: none; }
.msh-app .msh-badges { display: flex; }

/* ── Supervisor "Ekip" (team) page ────────────────────────────────────── */
/* (Sprint 459: dead .msh-teammap / .tm-open / .msh-stchip rules removed — the
   Sprint-324 redesign replaced them with the tsel mini-map + trow-dur.) */
.msh-teamhead { display: flex; align-items: center; gap: 8px; margin: 12px 13px 8px; }
.msh-teamhead .mobile-headrow-title { flex: 1; }
.msh-ttabs { display: flex; background: #E4E8F0; border-radius: 10px; padding: 2px; gap: 2px; flex: 0 0 auto; }
/* Sprint 718 — the [hidden] trap (see .btn). mobile-sales-team.html #mTabs. */
.msh-ttabs[hidden] { display: none; }
.msh-ttab { font-weight: 800; font-size: 9.5px; padding: 6px 9px; border-radius: 8px; color: var(--muted); cursor: pointer; }
.msh-ttab.on { background: #fff; color: var(--verti-navy); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
.msh-trow {
  display: flex; align-items: center; gap: 10px; margin: 0 11px 7px;
  background: #fff; border-radius: 13px; padding: 9px 10px; text-decoration: none;
  box-shadow: 0 1px 2px rgba(20, 30, 60, .05);
}
.msh-tav {
  width: 36px; height: 36px; border-radius: 11px; flex: 0 0 auto;
  display: grid; place-items: center; font-weight: 900; font-size: 12px; color: #fff;
}
.msh-trow .tt { flex: 1; min-width: 0; }
.msh-trow .tt b {
  display: block; font-weight: 800; font-size: 12.5px; color: var(--verti-navy);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.msh-trow .tt small {
  display: block; font-size: 9.5px; color: var(--muted); margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.msh-trow .rt { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex: 0 0 auto; }
.msh-trow .seen { font-weight: 700; font-size: 8px; color: var(--muted); font-family: ui-monospace, monospace; }
.msh-trow .chev { color: #B9C2D4; font-size: 15px; flex: 0 0 auto; }
.msh-team-empty { margin: 22px 13px; text-align: center; color: var(--muted); font-size: 13px; }

/* ── App-shell home agenda (Sprint 303) ──────────────────────────────── */
/* "Bu Ayın Bakımları" on ONE line (shrinks/ellipsizes, never overflows) with
   the Bekleyen/Tamam tabs inline to its right. */
.msh-app .mobile-headrow { align-items: center; margin: 10px 0 8px; }
.msh-app #mHeadTitle {
  flex: 1 1 auto; min-width: 0; font-size: 15px; line-height: 1.15;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.msh-atabs-slot { flex: 0 0 auto; }
.msh-atabs-slot .mobile-atabs { margin: 0; }
/* Maintenance rows: project (shrinks with …) + unit (ALWAYS visible) on one
   VERTI-navy line; the fault badge sits right after the date (away from the
   button, no misclick); a wide dark-green QR-start button (QR icon + Başlat). */
.msh-app .mobile-assigned-row .maa-tx b {
  display: flex; align-items: baseline; min-width: 0; color: var(--verti-navy);
}
.msh-app .maa-proj { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msh-app .maa-sep,
.msh-app .maa-unit { flex: 0 0 auto; white-space: nowrap; }
.msh-app .maa-sep { padding: 0 5px; }
.msh-app .maa-l2 small { flex: 0 1 auto; }   /* don't grow → badge hugs the date */
.msh-app .maa-l2 .maa-fbs { margin-left: 0; }
/* In the app-shell home the fault badge hugs the date, so the photo chip is
   the one that carries the auto margin and holds the right edge of line 2. */
.msh-app .maa-l2 .maa-ph { margin-left: auto; }
.msh-app .maa-st.maa-qr {
  width: auto; min-width: 82px; height: 38px; padding: 0 12px;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: #15803D; color: #fff; font-size: 14px;
}
.msh-app .maa-qr-lb { font-weight: 800; font-size: 12.5px; }

/* Compact live (active) card — "Bitir" + manual-close icon, the clock rides
   the sub line (Sprint 365: was a single "Bitir" button until the manual
   close was un-hidden, see below). */
.msh-app .mobile-active-card {
  display: flex; flex-direction: row; align-items: center; gap: 10px;
  border: 1.5px solid #BBF0CB; border-left: 4px solid #15803D;
}
.msh-app .mobile-active-card.is-warn { border-color: #F3C0C0; border-left-color: #DC2626; }
.msh-app .mobile-active-head { display: none; }
.msh-app .mobile-active-main {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px;
}
.msh-app .mobile-active-title {
  font-size: 13.5px; font-weight: 800; color: var(--verti-navy);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.msh-app .mobile-active-card.is-warn .mobile-active-title { color: #B91C1C; }
.msh-app .mobile-active-sub { font-size: 10.5px; font-weight: 600; color: var(--muted); }
.msh-app .mobile-active-sub .msh-clk {
  font-family: ui-monospace, Menlo, monospace; font-weight: 800; color: #15803D;
}
/* hotfix mp-collapse-delete (operator, 25 Sep 2026): the card names WHICH
   record (#id · name, navy, ahead of the type) and - for a part job or an
   inspection - OPENS it: the whole card takes the tap (live-card-role.js
   paintCardLink), the chevron says so, and the keyboard's link is the text
   block. */
.mobile-active-sub .msh-opid { font-weight: 800; color: var(--verti-navy); }
.mobile-active-card.is-link { cursor: pointer; }
.mobile-active-card.is-link .mobile-active-main:focus-visible {
  outline: 2px solid var(--verti-navy); outline-offset: 2px; border-radius: 6px;
}
.mobile-active-go { flex: 0 0 auto; align-self: center; color: #15803D; font-size: 13px; line-height: 1; }
.mobile-active-go[hidden] { display: none; }
.msh-app .mobile-active-foot { margin-top: 0; flex: 0 0 auto; }
.msh-app .mobile-active-clock { display: none; }         /* clock now rides the sub */
/* Sprint 365 (operator request): the manual close is no longer hidden — it
   sits as a small icon-only button to the LEFT of "Bitir" (order:-1), same
   confirm + GPS-proximity flow, just no camera step. Icon-only relies on
   data-i18n-title for its accessible name (see mobile.html / live-card.js). */
.msh-app .mobile-active-manualclose {
  display: inline-flex; align-items: center; justify-content: center;
  order: -1; width: 34px; height: 34px; padding: 0; flex: 0 0 auto;
  font-size: 15px;
}
.msh-app .mobile-active-complete {
  background: #15803D; color: #fff; gap: 6px;
  padding: 9px 16px; border-radius: 10px; font-size: 13px;
}
.msh-app .mobile-active-complete i { display: inline; }  /* QR icon + "Bitir" */

/* ============================================================================
   Sprint 304 — mobile home field-fix follow-up (operator real-phone feedback)
   ========================================================================== */
/* item 4: header badges = avatar size (32px), all 3 always shown; only the
   orange count bubble is conditional. */
.msh-app .msh-badge { width: 32px; height: 32px; font-size: 13px; }
.msh-app .msh-badge .msh-badge-n { top: -4px; right: -4px; }
.msh-badge-n[hidden] { display: none !important; }

/* item 2: compact single-row "Açık / Biten" segmented tabs (heading never
   crushed — the tabs sit inline, short, low). */
.msh-app .mobile-atabs.msh-segs { background: #E4E8F0; border-radius: 9px; padding: 2px; gap: 2px; margin: 0; }
.msh-app .msh-segs .mobile-atab {
  height: 26px; padding: 0 9px; border-radius: 7px; font-size: 11px; font-weight: 800;
  background: transparent; color: var(--muted); border: 0;
  display: inline-flex; align-items: center; gap: 5px; box-shadow: none;
}
.msh-app .msh-segs .mobile-atab.on { background: var(--verti-navy); color: #fff; }
.msh-app .msh-segs .mobile-atab-badge {
  position: static; top: auto; right: auto;      /* inline (base rule is absolute) */
  background: #D3D9E4; color: var(--muted);
  font-size: 10px; font-weight: 900; border-radius: 6px; padding: 0 5px; min-width: 0;
}
.msh-app .msh-segs .mobile-atab.on .mobile-atab-badge { background: var(--tke-orange); color: #fff; }

/* item 3: "Servis Başlat" row looks like a service-record card — a left label
   + a Manuel button + a green QR+Başlat button (row height = a maintenance
   card). The button text labels show only in app-shell. */
.msh-sbtn-lb { display: none; }
.msh-app .msh-sbtn-lb { display: inline; }
.msh-app .mobile-start {
  display: flex; align-items: center; gap: 7px; width: 100%; box-sizing: border-box;
  background: #fff; border: 1px solid var(--border); border-radius: 12px;
  padding: 9px 11px; margin: 0 0 8px; position: relative; overflow: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
/* sprint start-a-job-now — THE [hidden] TRAP, one specificity level up. The
   .msh-app descendant rule above out-orders the plain .mobile-action-btn[hidden]
   override, so the field door's button stayed visible inside the app shell even
   while its migration was pending. Gate: check-hidden-collisions. */
.msh-app .mobile-start[hidden] { display: none; }
/* Sprint 304: the navy left bar — identical to a maintenance card's ::before. */
.msh-app .mobile-start::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--verti-navy);
}
.msh-app .mobile-start .mobile-action-label {
  flex: 1 1 auto; min-width: 0; text-align: left; font-weight: 900; font-size: 13px;
  color: var(--verti-navy); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.msh-app .mobile-start .mobile-action-form {
  position: static; flex: 0 0 auto; width: auto; height: 38px; padding: 0 11px; border-radius: 10px;
  background: #EAF0FB; color: var(--verti-navy); font-weight: 800; font-size: 12px;
  display: inline-flex; align-items: center; gap: 5px;
}
.msh-app .mobile-start .mobile-action-scan {
  position: static; flex: 0 0 auto; width: auto; height: 38px; padding: 0 11px; border-radius: 10px;
  background: #15803D; color: #fff; font-weight: 800; font-size: 12.5px;
  display: inline-flex; align-items: center; gap: 5px;
}

/* item 6: bottom nav — Option A "pill-active" (active tab = full gradient pill,
   icon + label side by side; inactive = compact icon+label).
   ------------------------------------------------------------------------
   Sprint 915 (operator, Option B) — the INACTIVE tabs go icon-only so the
   active pill has room for its label in every language on every phone. The
   7px inactive label was half the iOS/Material minimum and unreadable anyway;
   its width was the only thing the pill could not have. Measured with the
   design harness (six role bars x en/tr/az x 360/390/412 = 54 bars, 351 active
   labels, real browser): before, 111 active labels clipped in 41 of the 54
   bars; after, 0 in 0 — see scripts/test-mobile-nav-labels.mjs, which is the
   standing proof.

   The geometry is arithmetic, not taste:
   - `gap: 0` + `flex: 1 1 39px` puts the old 3px gap INSIDE the hit box, so an
     inactive tab is 39px wide instead of 36 + a 3px dead strip.
   - the bar's own 7px/4px vertical padding moves onto the tab as
     `min-height: 47px`, which makes the whole bar height tappable while the
     BAR stays 47px — `.msh-app .msh-mid`'s 76px padding-bottom depends on that
     number, and a naive `min-height: 44px` on `.msh-tab` would also hit the
     pill (36 -> 44) and grow the bar to ~55px.
   - `justify-content: center` is not optional: `.msh-tab` is a COLUMN flex
     with the default `flex-start`, so once the label leaves the flow the icon
     sticks to the top of the 47px box and floats ~8px above the pill. And the
     vertical padding must be ZERO, not the bar's old asymmetric 7/4: the tab
     centres its icon in whatever the padding leaves, so 7-top/4-bottom pushes
     the icon 1.5px BELOW the pill's centre line — measured, by assertion (h)
     of test-mobile-nav-labels.mjs, which is why that assertion exists.
   - the pill FILLS what is left (`flex-grow: 99`) but never SHRINKS
     (`flex-shrink: 0`), so an Android system font scale of 1.3x widens it and
     squeezes the icons to their 30px floor instead of clipping the word.
   - `max-width: 160px` stops the five-tab sales bar growing a 244px pill at
     412px (176 today) and a 554px one in landscape; the slack goes back to the
     inactive tabs, never to margins.
   Sprint 623's `[data-tabs="7"]` pair is retired — one rule serves every bar
   now. `mountBottomNav` still STAMPS data-tabs (test-mobile-items-browser
   asserts it); only the CSS that keyed off it is gone. */
.msh-nav { padding: 0 6px env(safe-area-inset-bottom, 0px); gap: 0; align-items: center; }
.msh-tab:not(.on) {
  flex: 1 1 39px; min-width: 30px; padding: 0;
  min-height: 47px; box-sizing: border-box; justify-content: center;
}
/* Hidden VISUALLY, never `display: none` — the <a>'s accessible name is the
   label, and test-admin-mobile-nav / test-mobile-today read every `.lb`'s
   textContent. Side effect worth knowing: every inactive label now reports
   `scrollWidth > clientWidth` by construction, so a clip test must judge
   `.on .lb` only. */
.msh-tab:not(.on) .lb {
  position: absolute; width: 1px; height: 1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
/* The count badge is anchored to the TAB box (`top:-3px`, :3315 above) and its
   markup sits inside `.ic`. An inactive tab is now a 47px box holding a ~17px
   icon on its centre line, so that corner is ~15px above the icon's corner and
   the badge floats off the glyph entirely — re-anchor it to the ICON, in the
   INACTIVE state only. Do NOT write `.msh-tab .ic { position: relative }`: on
   the pill `.ic` is unpositioned, which is what puts the badge on the PILL's
   top-right; positioning it there would drag the badge onto the left-hand
   icon. Both states are asserted separately by the gate. */
.msh-tab:not(.on) .ic { position: relative; }
.msh-tab:not(.on) .msh-tab-badge { top: -7px; right: -9px; }
.msh-tab .ic { width: auto; height: auto; background: none; }
.msh-tab.on {
  flex: 99 0 auto; min-width: 0; max-width: 160px; width: auto;
  flex-direction: row; justify-content: center; gap: 5px;
  background: var(--brand-gradient); border-radius: 12px; padding: 9px 8px;
}
.msh-tab.on .ic { background: none; color: #fff; font-size: 15px; }
.msh-tab.on .lb { color: #fff; font-weight: 900; font-size: 12px; }

/* Sprint 548 — the ACTIVE Unit Task pill's three counts (jobs / free fix /
   quote). Only the active tab is a wide horizontal pill, so this rides inside
   it to the right of the label; the light pill grounds match the page's own
   fr-p-n / fr-p-done / fr-p-est so the same colours mean the same things at
   both zoom levels. Numbers only — three words would not survive the bar. */
/* On the active Unit Task pill the inline split already carries the job count,
   so the corner count-badge would be a second, identical "3" — hide it there.
   Inactive tabs keep the single badge. */
.msh-tab.on .msh-tab-badge[data-badge="report"] { display: none !important; }
/* Sprint 580 put the ACTIVE Unit Task tab's three counts on the button's top
   edge (.msh-tab-split / .msh-tsc).
   sprint unit-task-intake RETIRED THEM with the page's own header pills. The
   page is four status tiles now, and "open jobs / of them free / of them
   quoted" is one slice of what those four say; a 390px bar pill cannot carry
   four numbers, and choosing three would be choosing which to leave out. The
   tab keeps the single .msh-tab-badge, whose meaning now depends on the
   reader (FieldReports::tabBadge). The rules are deleted rather than left
   dormant: dead CSS is how a later sprint comes to believe a control exists. */

/* Sprint 459: the Sprint-304 .msh-greet rules went with the dormant #mGreet
   markup (greeting retired from the home in Sprint 312). */

/* ============================================================================
   Sprint 425 — shared photo / document upload FIELD (assets/js/photo-upload.js)
   ONE look for every "photo of the part / defect" area (part-replacements +
   inspections), so a field user learns a single interaction.
   ========================================================================== */
.pf{ border:1.5px dashed var(--border); border-radius:11px; padding:12px 13px 13px;
  background:#F6F8FB; margin-bottom:13px; }
.pf-h{ display:flex; align-items:center; gap:9px; }
.pf-ic{ width:30px; height:30px; border-radius:8px; display:flex; align-items:center; justify-content:center;
  font-size:15px; color:#fff; flex:0 0 auto; }
.pf-t{ font-size:13.5px; font-weight:800; color:var(--text); line-height:1.2; }
.pf-t small{ display:block; font-weight:600; color:var(--muted); font-size:10.5px; margin-top:1px; }
.pf-badge{ margin-left:auto; min-width:20px; text-align:center; font-size:10px; font-weight:800;
  padding:2px 8px; border-radius:999px; background:var(--verti-navy); color:#fff; }
.pf-actions{ display:flex; gap:8px; margin:10px 0 11px; flex-wrap:wrap; }
.pf-btn{ display:inline-flex; align-items:center; gap:7px; border:1px solid var(--border); background:var(--surface);
  color:var(--verti-navy); font-weight:700; font-size:12px; padding:9px 14px; border-radius:9px; cursor:pointer; font-family:inherit; }
.pf-btn.solid{ background:var(--verti-navy); color:#fff; border-color:var(--verti-navy); }
.pf-btn:disabled{ opacity:.55; cursor:not-allowed; }
.pf-grid{ display:flex; flex-wrap:wrap; gap:9px; }
.pf-grid.pf-busy{ opacity:.5; pointer-events:none; }
.pf-empty{ color:var(--muted); font-size:12px; padding:2px 0; }
.pf-thumb{ position:relative; width:64px; height:64px; border-radius:9px; border:1px solid var(--border); overflow:hidden; background:#E1E8F0; }
.pf-thumb img{ width:100%; height:100%; object-fit:cover; display:block; }
.pf-thumb a{ position:absolute; inset:0; }
.pf-docchip{ position:relative; display:inline-flex; align-items:center; }
.pf-docchip a{ display:inline-flex; align-items:center; gap:6px; padding:8px 28px 8px 11px; background:#FEF2F2;
  border:1px solid #FCC8C8; border-radius:8px; color:#7A2828; font-size:11.5px; text-decoration:none; max-width:210px; }
.pf-x{ position:absolute; top:2px; right:2px; width:18px; height:18px; border-radius:50%; background:rgba(220,38,38,.94);
  color:#fff; font-size:11px; line-height:1; display:flex; align-items:center; justify-content:center; cursor:pointer; border:0; }
.pf-docchip .pf-x{ top:50%; transform:translateY(-50%); right:5px; }

/* sprint job-photos — a photo that is on this PHONE and not yet on the server.
   It reads as a real thumbnail, deliberately: the picture exists and the
   technician's job is done. The strip along the bottom is the only thing that
   says it is still travelling, and it turns red with a Retry when a route
   refused it — a photo is never deleted because the server said no. */
.pf-thumb.pf-queued{ border-style:dashed; border-color:#B45309; }
.pf-thumb.pf-queued img{ opacity:.72; }
.pf-qmark{ position:absolute; left:0; right:0; bottom:0; background:rgba(180,83,9,.92); color:#fff;
  font-size:8.5px; font-weight:800; line-height:1.25; padding:2px 3px; text-align:center;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.pf-qmark.bad{ background:rgba(185,28,28,.94); }
.pf-qretry{ position:absolute; left:2px; top:2px; border:0; border-radius:6px; background:rgba(255,255,255,.94);
  color:#B91C1C; font-size:9.5px; font-weight:800; padding:2px 5px; cursor:pointer; }

/* Sprint unit-photo-slots — the "Unit photos" bottom sheet: three slot cards,
   each one a createPhotoField, inside the shared .mobile-sheet chrome. The card
   is a surface rather than a bare stack so the three read as three THINGS to
   do, not one long form; the hint line under them is where "a new photo
   replaces this one" is said once instead of three times. */
.uslot-cards { display: flex; flex-direction: column; gap: 10px; }
.uslot-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; padding: 11px 12px;
}
.uslot-card .pf-actions { margin-bottom: 4px; }
.uslot-card .pf-thumb { width: 84px; height: 84px; }
.uslot-load { color: var(--muted); font-size: 12px; padding: 6px 2px; }
.uslot-hint { color: var(--muted); font-size: 11px; margin: 2px 0 0; }
.uslot-off {
  background: #FEF3E2; color: #B45309; border: 1px solid #F5D8AE;
  border-radius: 9px; padding: 8px 10px; font-size: 11.5px; font-weight: 700; margin: 2px 0 0;
}
.uslot-load[hidden], .uslot-hint[hidden], .uslot-off[hidden] { display: none; }

/* Sprint unit-photo-slots — the DESKTOP surfaces (projects.html?tab=units and
   the three other pages units-view.js drives): the "Unit photos" KPI frame, the
   keyed Photos column and the edit/detail modal slots. The frame borrows the
   "Active Units" frame's radius and padding so the KPI row reads as one row of
   equals, and takes a navy tint instead of that frame's green — green there
   means "running", and a photo count is neither good nor bad news. */
.uslot-frame {
  background: #EDF2FA; border: 1px solid #C3D2E8; border-radius: 8px;
  padding: 10px; display: flex; flex-direction: column; gap: 6px; min-width: 0;
}
.uslot-cap {
  font-size: 10.5px; font-weight: 800; letter-spacing: .04em;
  text-transform: uppercase; color: #4A5F80;
}
.uslot-hero {
  display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap;
  border-radius: 7px; padding: 2px 4px; margin: -2px -4px 0;
}
.uslot-hero .uh-v { font-size: 21px; font-weight: 800; color: var(--verti-navy); line-height: 1.1; }
.uslot-hero .uh-l { font-size: 11.5px; font-weight: 700; color: #4A5F80; }
.uslot-frame .brand-progress { height: 8px; border-radius: 4px; }
.uslot-frame .brand-progress > span { border-radius: 4px; }
.uslot-legend {
  display: flex; align-items: center; gap: 8px; border-radius: 7px;
  padding: 3px 6px; margin: 0 -6px; font-size: 11.5px; color: #35486A;
}
.uslot-legend .ul-l { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uslot-legend .ul-n { font-weight: 800; color: var(--verti-navy); font-variant-numeric: tabular-nums; }
/* The pressable pair. .ustat-go carries the press affordance on projects.html
   only — units-view.js also drives three other pages, so the cursor, the hover
   and the focus ring are stated here rather than borrowed. */
.uslot-hero, .uslot-legend { cursor: pointer; transition: background .12s ease; }
.uslot-hero:hover, .uslot-legend:hover { background: rgba(var(--verti-navy-rgb), .07); }
.uslot-hero:focus-visible, .uslot-legend:focus-visible {
  outline: 2px solid var(--verti-navy); outline-offset: 1px;
}
.uslot-legend[aria-pressed="true"], .uslot-hero[aria-pressed="true"] {
  background: var(--verti-navy); color: #fff;
}
.uslot-legend[aria-pressed="true"] .ul-n, .uslot-hero[aria-pressed="true"] .uh-v,
.uslot-hero[aria-pressed="true"] .uh-l { color: #fff; }
.uslot-pending { font-size: 11.5px; color: #6B7B95; line-height: 1.45; }
.uslot-frame[hidden], .uslot-legend[hidden], .uslot-pending[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .uslot-hero, .uslot-legend { transition: none; }
}

/* The keyed "Photos" column: three fixed icons, present in navy and missing as
   a light outline. Three OUTLINES say "none of the three" at a glance where a
   blank cell says only "nothing to show". */
.uslot-col { white-space: nowrap; }
.usc {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 5px; margin-right: 2px;
  background: #EEF1F6; color: #B9C3D3; font-size: 10.5px;
}
.usc.on { background: var(--verti-navy); color: #fff; }
.usc[hidden] { display: none; }

/* The keyed "QR evidence" column (sprint unit-qr-evidence): the strongest
   evidence the row has that its sticker is on the lift — a technician's scan,
   else a photograph of the sticker, else nothing.

   NO RED ALARM, and that is the design rather than a palette choice. A scan is
   a successful TECHNICIAN action, so it proves the sticker EXISTS while its
   absence proves nothing at all; a cell that shouted would be claiming the one
   thing this column may never claim. The dash therefore wears the page's own
   quiet ground — the #F4F6FA + navy-bold pair td.zc-cell already uses two
   columns to the left (projects.html) — which reads as "start here" on a
   worklist instead of "something is wrong". */
.uqr-scan { color: var(--verti-navy); font-variant-numeric: tabular-nums; white-space: nowrap; }
.uqr-photo {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  background: #EDF2FA; color: #35486A; font-size: 11.5px; font-weight: 600;
}
.uqr-photo[hidden] { display: none; }
td.uqr-none { background: #F4F6FA; }
td.uqr-none .d { color: var(--verti-navy); font-weight: 700; font-size: 16px; line-height: 1; }
tbody tr:hover td.uqr-none { background: #EEF2F9; }

/* Edit modal, fourth area: the three slots side by side — a desk has the width
   the phone sheet does not, and three abreast keeps the area one glance tall. */
.uslot-edit {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px;
}
.uslot-edit[hidden] { display: none; }
/* Detail modal: the empty slot is drawn, not omitted. */
.uslot-miss {
  display: flex; align-items: center; justify-content: center; text-align: center;
  border: 1px dashed var(--border); border-radius: 6px; background: #F7F9FC;
  color: var(--muted); font-size: 10.5px; padding: 4px; box-sizing: border-box;
}
.uslot-miss[hidden] { display: none; }
/* Sprint unit-photos-tech-info — the slot sheet's way to the unit's details,
   and the "old photo" nudge on a slot card (both surfaces of the sheet). */
.uslot-details {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 44px; margin: 8px 0 0; border-radius: 10px;
  border: 1.6px solid var(--verti-navy); color: var(--verti-navy);
  font-size: 13px; font-weight: 800; text-decoration: none; background: #fff;
}
.uslot-age { font-size: 11px; font-weight: 700; color: #B45309; margin: 4px 0 0; }
.uslot-age[hidden] { display: none; }

/* Sprint unit-photos-tech-info — the Unit Register's technical form, change
   rows and pills. ONE copy for both surfaces: unit-detail-modal.js builds the
   same markup on the desk modal and on the phone's unit screen. */
.utd-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px 14px; }
.utd-field label, .utd-field .utd-lbl { display: block; font-weight: 600; font-size: 12.5px; margin-bottom: 4px; }
.utd-field input, .utd-field select { width: 100%; }
.utd-field input.invalid { border-color: var(--danger); }
.utd-ro { font-size: 13.5px; padding: 6px 0; }
.utd-hint { font-size: 11px; color: var(--muted); margin: 3px 0 0; }
.utd-err { font-size: 11.5px; color: var(--danger); }
.utd-note { border-radius: 9px; padding: 8px 10px; margin: 0 0 10px; }
.utd-note-h { font-size: 12.5px; font-weight: 600; margin: 0 0 4px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.utd-pend { background: #F8F1F7; border: 1px solid #E4CDE0; }
.utd-rej { background: #FEF3E2; border: 1px solid #F5D8AE; color: #8A4A0B; }
.utc-ch { font-size: 12.5px; line-height: 1.45; }
.utc-f { font-weight: 700; color: #35486A; margin-right: 4px; }
.utc-arr { color: var(--muted); }
.utc-stale { font-size: 11.5px; color: #B45309; font-weight: 600; }
.utr-muted { color: var(--muted); }
.utr-pill {
  display: inline-block; font-size: 10.5px; font-weight: 700; line-height: 1.5;
  padding: 0 7px; border-radius: 999px; white-space: nowrap; vertical-align: 1px;
}
.utr-pill-pend { background: #F1E6F0; color: #6A2560; }
/* Hollow amber: "incomplete" is a to-do, not an error. */
.utr-pill-spec { background: transparent; color: #B45309; border: 1px solid #F5C58A; }

/* A count on a More-sheet entry (the approver's pending Units requests). The
   bar-tab badge, re-anchored on the entry's icon. */
.msh-more-item { position: relative; }
.msh-more-item .msh-tab-badge { top: -4px; right: calc(50% - 30px); font-size: 9px; padding: 2px 5px; }

/* Sprint 426 — "AI fix text" button (moved from the per-page inline styles to the
   shared sheet so the mobile field pages can reuse it). */
.btn-ai-fix{ display:inline-flex; align-items:center; gap:4px;
  background:var(--brand-gradient-diag); color:#fff; border:none;
  border-radius:999px; padding:3px 11px; font-size:11px; font-weight:700;
  cursor:pointer; margin-left:8px; vertical-align:middle; transition:filter .15s; }
.btn-ai-fix:hover{ filter:brightness(1.1); }
.btn-ai-fix:disabled{ opacity:.5; cursor:wait; }

/* Sprint 428 — field tools toolbar (assets/js/ai-fix.js): AI fix · clear · speak,
   auto-added under every textarea on the service/fault/part/inspection pages. */
.ft-bar { display: flex; flex-wrap: wrap; gap: 6px; margin: 5px 0 2px; }
.ft-btn { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--border);
  background: var(--surface); color: var(--verti-navy); font-weight: 700; font-size: 11px;
  padding: 6px 11px; border-radius: 999px; cursor: pointer; font-family: inherit; line-height: 1; }
.ft-btn.btn-ai-fix { background: var(--brand-gradient-diag); color: #fff; border: 0; margin-left: 0; }
.ft-btn:disabled { opacity: .5; cursor: wait; }

/* ============================================================================
   Sprint 462 — fluid transitions ("Secenek B", operator-approved rev4).
   Cross-document View Transitions + light Liquid-Glass bottom bar + skeleton
   first-load. Sprint 463 (operator: "mobildeki apple gorsel tasarimi webe de
   uygula") made the @view-transition OPT-IN global here — the DESKTOP joined,
   so the per-page inline blocks 462 put in the mobile heads were removed.
   Every same-origin navigation (mobile, desktop, login) now animates; the
   named chrome groups below keep each surface's shell visually FIXED while
   only the content transitions. Unsupported browsers/WebViews ignore all of
   it and keep the old hard-cut behaviour.
   ========================================================================= */
@view-transition { navigation: auto; }

/* Named groups: the header + bottom bar snapshot separately, so they read as
   FIXED across the navigation; the ACTIVE nav pill morphs from its old tab to
   its new tab — the shared-element move (the "Apple" feel's centrepiece). */
.msh-app .mobile-topbar       { view-transition-name: msh-topbar; }
.msh-app .msh-nav             { view-transition-name: msh-nav; }
.msh-app .msh-nav .msh-tab.on { view-transition-name: msh-navpill; }

/* Sprint 463 — the DESKTOP's own named chrome: sidebar + topbar stay visually
   FIXED across a menu navigation (the MPA reads like an SPA), and the ACTIVE
   sidebar highlight morphs from its old menu item to the new one — the
   desktop twin of the mobile pill move. Pages without the shell (login)
   simply have no names and take the plain root fade. */
.layout .sidebar { view-transition-name: shell-sidebar; }
.topbar          { view-transition-name: shell-topbar; }
.nav a.active    { view-transition-name: shell-menupill; }

/* Sprint 463 — light glass on the (already sticky) desktop topbar, the same
   "hafif" material as the mobile bar: content scrolls beneath it; the brand
   gradient strip (.topbar::after) stays. Solid navy sidebar deliberately
   keeps its identity — no glass there. */
.topbar {
  background: rgba(255, 255, 255, .78);
  backdrop-filter: blur(12px) saturate(1.5);
  -webkit-backdrop-filter: blur(12px) saturate(1.5);
}

/* Root content — default: gentle rise+fade (used when no direction is known,
   e.g. a Diger-sheet hop or a deep link). */
::view-transition-old(root) { animation: var(--dur-out) var(--ease-exit) both vt-fade-out; }
::view-transition-new(root) { animation: var(--dur-in) var(--ease-spring) both vt-fade-in; }
@keyframes vt-fade-out { to   { opacity: 0; transform: translateY(-6px); } }
@keyframes vt-fade-in  { from { opacity: 0; transform: translateY(8px); } }

/* Directional push (Apple nav curve): the tapped direction is stored by
   mobile-shell.js on the outgoing tap and applied as a transition TYPE on
   pagereveal in the incoming document. Old page parallaxes back and dims;
   the incoming page slides over it. */
html:active-view-transition-type(vt-fwd)::view-transition-old(root)  {
  animation: var(--dur-nav) var(--ease-spring) both vt-push-out-l; }
html:active-view-transition-type(vt-fwd)::view-transition-new(root)  {
  animation: var(--dur-nav) var(--ease-spring) both vt-push-in-r; }
html:active-view-transition-type(vt-back)::view-transition-old(root) {
  animation: var(--dur-nav) var(--ease-spring) both vt-push-out-r; }
html:active-view-transition-type(vt-back)::view-transition-new(root) {
  animation: var(--dur-nav) var(--ease-spring) both vt-push-in-l; }
@keyframes vt-push-in-r  { from { transform: translateX(102%); } }
@keyframes vt-push-in-l  { from { transform: translateX(-102%); } }
@keyframes vt-push-out-l { to { transform: translateX(-26%) scale(.985); filter: brightness(.85); } }
@keyframes vt-push-out-r { to { transform: translateX(26%) scale(.985); filter: brightness(.85); } }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

/* Light Liquid-Glass bottom bar (rev4 values — "cami daha hafif"): the bar
   floats over the content, which now scrolls beneath the glass. */
.mobile-shell.msh-app { position: relative; }
.msh-app .msh-nav {
  position: absolute; bottom: 0; left: 0; right: 0;
  background: rgba(255, 255, 255, .74); border-top: 0;
  backdrop-filter: blur(12px) saturate(1.5);
  -webkit-backdrop-filter: blur(12px) saturate(1.5);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7),   /* ust spekuler cizgi */
              inset 0 -1px 0 rgba(var(--verti-navy-rgb), .05),    /* alt cam kenari */
              0 -6px 18px rgba(13, 21, 38, .07);       /* camin zemine golgesi */
}
/* Content clears the floating bar (padding inside the scroll container). */
.msh-app .msh-mid { padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 76px); }

/* The active pill as light glass over the brand gradient: top sheen + 1px
   rim + soft inner shade. NO overflow:hidden — the Ariza count badge pokes
   above the pill and must not be clipped. */
.msh-app .msh-tab.on {
  box-shadow: 0 5px 14px rgba(122, 46, 107, .32),
              inset 0 1px 0 rgba(255, 255, 255, .4),
              inset 0 -4px 9px rgba(13, 21, 38, .14),
              inset 0 0 0 1px rgba(255, 255, 255, .2);
}
.msh-app .msh-tab.on::before {
  content: ''; position: absolute; left: 6%; right: 6%; top: 2px; height: 44%;
  border-radius: 10px 10px 40% 40%; pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, .28), rgba(255, 255, 255, .03));
}

/* Skeleton first-load (only when swrGet has NO snapshot — see skeletonHtml in
   mobile-shell.js) + the staggered "born in sequence" entry. */
.msh-sk { display: flex; align-items: center; gap: 8px; background: #fff;
  border: 1px solid #EEF1F6; border-left: 4px solid #E3E8F1;
  border-radius: 0 11px 11px 0; padding: 10px; margin-bottom: 8px; }
.msh-sk i { display: block; border-radius: 6px; background: #E9EDF5;
  animation: msh-sk-breathe 1.3s ease-in-out infinite; }
.msh-sk .skx { flex: 1; display: flex; flex-direction: column; gap: 6px; }
.msh-sk .l1 { height: 10px; width: 72%; }
.msh-sk .l2 { height: 8px; width: 48%; }
.msh-sk .btn { width: 64px; height: 34px; border-radius: 9px; flex: 0 0 auto; }
@keyframes msh-sk-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
/* Sprint 474 (C3) — desktop skeleton rows (shell.js skeletonRows). Same
   breathing placeholder idea as the mobile .msh-sk above, shaped like a
   desktop list row so the real data lands without moving anything. Only ever
   rendered when swrGet has no snapshot, i.e. a first-ever visit. */
.sk-list { display: flex; flex-direction: column; gap: 8px; }
.sk-row {
  display: flex; align-items: center; gap: 14px;
  background: #fff; border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 14px 16px;
}
.sk-row i {
  display: block; height: 11px; border-radius: 5px; background: #E9EDF5;
  animation: msh-sk-breathe 1.3s ease-in-out infinite;
}
.sk-row i:nth-child(2) { animation-delay: .15s; }
.sk-row i:nth-child(3) { animation-delay: .3s; }

.msh-cas { opacity: 0; animation: msh-cas-in 480ms var(--ease-spring) forwards; }
@keyframes msh-cas-in { from { opacity: 0; transform: translateY(16px) scale(.97); }
                        to   { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  .msh-sk i { animation: none; }
  .msh-cas { animation-duration: 1ms; }
}

/* ── Sprint 555: the SHARED mobile revenue-stream view (assets/js/mobile-stream.js)
   ── Drawn identically by mobile-pl.html (all four streams) and mobile-svcfin.html
   (the two service ones). The classes live HERE, not in either page, because a
   shared component must style itself through its own class: borrowing a page's
   prefix borrows its whole cascade, and the more specific selector wins whatever
   the file order (the Sprint 513 placeholder-under-its-own-magnifier lesson). */
.mst-kpis { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 8px; }
.mst-k { background: #fff; border: 1px solid var(--border); border-radius: 10px;
         padding: 8px 10px; position: relative; overflow: hidden; min-width: 0; }
.mst-k::before { content: ''; position: absolute; top: 0; left: 0; right: 0;
                 height: 3px; background: var(--brand-gradient); }
.mst-k .l { font-size: 8px; font-weight: 800; color: var(--muted);
            text-transform: uppercase; letter-spacing: .4px; margin-top: 2px; line-height: 1.3; }
.mst-k .n { font-size: 15px; font-weight: 900; color: var(--verti-navy); margin-top: 2px;
            line-height: 1.15; font-variant-numeric: tabular-nums; }
.mst-k .n.pos { color: #15803D; }
.mst-k .n.neg { color: #DC2626; }

.mst-lr { background: #fff; border: 1px solid var(--border); border-radius: 9px;
          padding: 7px 9px; display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.mst-lr .nm { min-width: 0; flex: 1; font-size: 10.5px; color: var(--verti-navy); font-weight: 700; }
.mst-lr .nm b { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mst-lr .nm .sub { display: block; font-weight: 500; font-size: 9px; color: var(--muted); }
.mst-lr .v { font-size: 11.5px; font-weight: 900; color: var(--verti-navy);
             font-variant-numeric: tabular-nums; white-space: nowrap; }
.mst-lr .v.pos { color: #15803D; }
.mst-lr .v.neg { color: #DC2626; }
.mst-lr.tot-row { background: var(--verti-navy); border-color: var(--verti-navy); margin-bottom: 0; }
.mst-lr.tot-row .nm, .mst-lr.tot-row .v { color: #fff; }

.mst-empty { font-size: 11.5px; color: var(--muted); padding: 6px 2px; }
.mst-notice { background: #EAEDF4; border-radius: 11px; padding: 12px 14px;
              font-size: 12.5px; color: #6B7280; }

/* ── Sprint 560: the two-page SWITCH in a mobile page header ────────────────
   Service Finance and Receivables Aging are two halves of one job, so they
   share a header: two pills where the ACTIVE one names the page you are on and
   the other navigates. Used by mobile-svcfin.html and mobile-receivables.html —
   the CSS lives HERE and not in either page, because a header duplicated into
   two files is exactly what drifted in Sprint 557 (the QR sticker's stylesheet
   existed twice and only one copy got fixed).

   The active pill is the SAME glass material as the bottom bar's active tab
   (Sprint 462) — brand gradient, inner highlight, soft shadow — so the app has
   one "this is selected" look rather than a second one invented here. */
.mpsw { display: flex; align-items: center; gap: 6px; min-width: 0; }
.mpsw a {
  flex: 0 1 auto; min-width: 0; position: relative; display: inline-flex;
  align-items: center; height: 30px; padding: 0 12px; border-radius: 999px;
  font-size: 11.5px; font-weight: 800; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; text-decoration: none;
  border: 1px solid var(--border); background: #fff; color: var(--muted);
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--dur-pill) var(--ease-spring);
}
.mpsw a:active { transform: scale(.97); }
.mpsw a:focus-visible { outline: 2px solid var(--tke-purple); outline-offset: 2px; }
.mpsw a.on {
  border-color: transparent; color: #fff; background: var(--brand-gradient);
  box-shadow: 0 5px 14px rgba(122, 46, 107, .32),
              inset 0 1px 0 rgba(255, 255, 255, .4),
              inset 0 -4px 9px rgba(13, 21, 38, .14),
              inset 0 0 0 1px rgba(255, 255, 255, .2);
}
.mpsw a.on::before {
  content: ''; position: absolute; left: 6%; right: 6%; top: 2px; height: 44%;
  border-radius: 999px 999px 40% 40%; pointer-events: none;
  background: linear-gradient(180deg, rgba(255,255,255,.28), rgba(255,255,255,.03));
}

/* THE page rule — full width, closing the header. Exactly ONE per page: the
   56px title underline (.mobile-page-h1 h1::after) is suppressed on a page that
   carries this switch, or the screen would show two brand lines side by side —
   the mistake Sprint 475 removed from every desktop header. */
.mobile-page-h1.mph-sw h1::after { display: none; }
.mph-rule {
  height: 3px; border-radius: 2px; background: var(--brand-gradient);
  margin: 0 -8px 10px;                 /* break out of .mobile-shell's 8px side padding */
}
@media (prefers-reduced-motion: reduce) {
  .mpsw a { transition: none; }
  .mpsw a:active { transform: none; }
}

/* ── Sprint 628 — the Leaflet layers-control icon, self-healed ──────────────
   The vendored leaflet.css points its base-layer chooser button at
   images/layers.png, which was never vendored — prod has answered 404 for it
   since the control first shipped, so map.html's chooser has been a BLANK
   square all along (found by test-sales-map-browser's no-page-errors gate).
   One inline-SVG override heals every page that mounts the control; the
   glyph is the classic layers stack in the brand navy. */
.leaflet-control-layers-toggle {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%231D3360' d='M12 3 2 8l10 5 10-5-10-5zm-7.5 8.75L2 13l10 5 10-5-2.5-1.25L12 15.5l-7.5-3.75zm0 4.5L2 17.5l10 5 10-5-2.5-1.25L12 20l-7.5-3.75z'/%3E%3C/svg%3E") !important;
  background-size: 22px 22px;
}

/* ── Sprint 734/778 — the project contact card ──────────────────────────────
   Own `pcc-` class set, never borrowing .modal's or .contact-row's cascade
   (the Sprint 513 lesson). Lived inside projects.html's <style> until Sprint
   778 gave the card a SECOND mount (installation-projects.html); a page-local
   copy would have been the "one implementation, never a second copy" rule
   broken in the same commit that shared the JS, so the whole block moved here
   verbatim. The classes are unique to this card, so nothing else on any page
   ties with them and the move cannot change a cascade.

   EVERY element here that the code can hide carries its own [hidden] rule.
   `display:flex` outranks the UA's `[hidden] { display:none }`, which is how
   a "hidden" control kept serialising in Sprint 718 and how a flex overlay
   stayed visible in Sprint 454 — six times in this codebase now. */
.pcc-sec { border-top:1px solid #E3E8F0; margin:18px 0 0; padding:16px 0 0; }
.pcc-sec[hidden] { display:none; }
.pcc-hd { display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin:0 0 10px; }
.pcc-hd[hidden] { display:none; }
.pcc-hd h3 { margin:0; font-size:12px; font-weight:800; letter-spacing:.07em;
             text-transform:uppercase; color:var(--verti-navy); }
.pcc-hd .pcc-sp { flex:1; }
.pcc-sub { font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
           color:#8A93A6; margin:12px 0 7px; }
.pcc-grid { display:flex; flex-wrap:wrap; gap:10px; }
.pcc-grid[hidden] { display:none; }
.pcc-grid .field { flex:1 1 150px; min-width:0; margin:0; }
.pcc-grid .field.wide { flex:2 1 230px; }
.pcc-grid .field label { font-size:11.5px; }
.pcc-note { font-size:11.5px; color:#6B7688; margin:8px 0 0; }
.pcc-note[hidden] { display:none; }
.pcc-btn { background:#fff; border:1px solid #D8DEE9; border-radius:7px; cursor:pointer;
           font-size:12px; font-weight:600; color:var(--verti-navy); padding:5px 11px; }
.pcc-btn:hover { background:var(--verti-navy-50); border-color:var(--verti-navy); }

.pcc-card { position:relative; background:#F8FAFD; border:1px solid #E3E8F0;
            border-radius:10px; padding:12px 14px 13px; margin:0 0 10px; }
.pcc-card-hd { display:flex; align-items:center; gap:9px; flex-wrap:wrap; margin:0 0 10px; }
.pcc-card-hd[hidden] { display:none; }
.pcc-no { width:23px; height:23px; border-radius:6px; flex:none; display:grid; place-items:center;
          background:var(--verti-navy); color:#fff; font-size:11.5px; font-weight:700; }
.pcc-name { font-weight:700; font-size:13.5px; color:#1A2333; }
.pcc-tag { font-size:10px; font-weight:700; padding:2px 7px; border-radius:99px;
           background:#E7F3EB; color:#15803D; }
.pcc-tag.warn { background:#FBEAEA; color:#8C2A2A; }
.pcc-del { background:#FEE2E2; color:#991B1B; border:1px solid #FCA5A5; border-radius:5px;
           cursor:pointer; font-size:13px; line-height:1; width:24px; height:24px;
           display:inline-flex; align-items:center; justify-content:center; }
.pcc-del[hidden] { display:none; }

.pcc-sw-row { display:flex; gap:9px; flex-wrap:wrap; align-items:center;
              margin:11px 0 0; padding:10px 0 0; border-top:1px dashed #E3E8F0; }
.pcc-sw-row[hidden] { display:none; }
.pcc-sw { display:inline-flex; align-items:center; gap:8px; cursor:pointer; font-size:12px;
          font-weight:600; color:#6B7688; background:#fff; border:1px solid #D8DEE9;
          border-radius:99px; padding:5px 12px 5px 7px; user-select:none; }
.pcc-sw[aria-disabled="true"] { cursor:not-allowed; opacity:.55; }
.pcc-sw[hidden] { display:none; }
.pcc-sw .pcc-track { width:31px; height:17px; border-radius:99px; background:#D8DEE9;
                     position:relative; flex:none; transition:background .25s ease; }
.pcc-sw .pcc-knob { position:absolute; top:2px; left:2px; width:13px; height:13px;
                    border-radius:50%; background:#fff; box-shadow:0 1px 2px rgba(20,30,60,.35);
                    transition:transform .25s cubic-bezier(.32,.72,0,1); }
.pcc-sw[aria-pressed="true"] .pcc-knob { transform:translateX(14px); }
/* ⚠ ONE RULE, NOT ONE PER KIND — operator decision, 19 Aug 2026.
   "bütün switch'leri açık iken VERTI lacivert yap, kapalı iken soluk gri."

   Until Sprint 805 the pressed colour was keyed on data-kind: navy for
   invoice, TKE orange for fault, plum for docs, the signed-green for sign, and
   Sprint 804 added three more for the channels. Every one of those was a real
   distinction — but the operator had just spent two sprints unable to tell ON
   from OFF, and the answer to that is not a richer vocabulary. ON now looks
   IDENTICAL everywhere, so it is learned once.

   ⚠ IT ALSO MAKES SPRINT 800's DEFECT UNREACHABLE. That bug was "a switch with
   no data-kind gets no colour". With the selector kind-agnostic, a switch
   cannot fail to have a pressed appearance -- there is nothing left to forget.
   check-switch-state.mjs was rewritten to check the INVARIANT (every switch has
   a visible pressed state) rather than the old mechanism (every kind is
   coloured), because a gate that guards a mechanism nobody uses any more is a
   gate that will be deleted by the next person who reads it.

   ONE EXCEPTION SURVIVES, and it is the one that carries meaning rather than
   decoration -- see below. */
.pcc-sw[aria-pressed="true"] { border-color:var(--verti-navy); color:var(--verti-navy); }
.pcc-sw[aria-pressed="true"] .pcc-track { background:var(--verti-navy); }

/* ...except `May sign`, which stays the signed-green (operator, 19 Aug 2026,
   after the exception was raised and accepted).
   It is not a delivery switch. Every other one answers "will this person
   RECEIVE X"; this one answers "may this person PUT THEIR NAME under a
   document" -- a different kind of permission, and the signature badges
   elsewhere in the app are already this green, so the card and the badge agree
   at a glance (Sprint 778's reason, unchanged).
   ⚠ It wins on SPECIFICITY, not source order: [data-kind][aria-pressed] beats
   the bare [aria-pressed] above wherever it sits. Kept here anyway so a reader
   meets the general rule first and the exception second. */
.pcc-sw[data-kind="sign"][aria-pressed="true"] { border-color:#177245; color:#177245; }
.pcc-sw[data-kind="sign"][aria-pressed="true"] .pcc-track { background:#177245; }

/* ⚠ THERE IS NO `data-inert` DIMMING, AND THAT IS DELIBERATE (Sprint 806).
   Sprint 804 added `opacity:.45` to the channel switches while the global lane
   was off, reasoning that a control which cannot act must not look like one
   that can. It was wrong twice over:
     * the row ALREADY says it, in words -- "Not sent yet, the lane is off".
       Opacity repeated that and added nothing.
     * and it destroyed the very signal two sprints had just been spent
       restoring. Navy at 45% is a washed-out blue, so ON and OFF became hard
       to tell apart again -- the operator's screenshot showed exactly that.
   OPACITY IS FOR "YOU MAY NOT CHANGE THIS" (aria-disabled), NEVER FOR "THIS IS
   CORRECTLY SET BUT HAS NO EFFECT YET". The first is about the reader's
   permission; the second is about the world, and the world is what the verdict
   sentence is for. check-switch-state.mjs now refuses any opacity on .pcc-sw
   that is not keyed on aria-disabled. */

/* The verdict. Three toggles and a grey chip are a state dump; this is the
   sentence that answers "will it get a mail or not". */
.pch-verdict { display:inline-block; margin-left:10px; padding:3px 10px; border-radius:99px;
               font-size:11.5px; font-weight:700; white-space:nowrap; }
.pch-verdict.pch-go   { background:#E4F4EC; color:#177245; }
.pch-verdict.pch-none { background:#EDF0F5; color:#6B7688; }
.pcc-lang { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:#6B7688; }
.pcc-lang[hidden] { display:none; }
.pcc-lang select { padding:4px 7px; border:1px solid #D8DEE9; border-radius:6px;
                   font-size:12px; background:#fff; }
.pcc-locked { font-size:11px; color:#8A93A6; }
.pcc-locked[hidden] { display:none; }

/* ---- Deep-link row flash (shared, Sprint 931) ----
   Was sales.html's own @keyframes salesRowFlash + .row-flash pair. This sprint
   is the SECOND user (part-replacements.html's ?id= entry lands on a row and
   has to say WHICH one), and a second copy of an animation is exactly the
   "one implementation, never a second copy" rule. The end colour is the table
   standard's own row tint, so the flash fades INTO the row rather than off it. */
@keyframes rowFlash { 0% { background:#FEF3C7; } 100% { background:#EFF6FF; } }
table.data tbody tr.row-flash { animation: rowFlash 1.6s ease-out; }

/* ---- A job reference chip (hotfix billable-jobs-page) ----
   "#214" linking to part-replacements.html?id=214, where that job's row
   flashes. ONE class for every page that names the jobs a document covers:
   the defect act row on project.html and the estimate on estimates.html. */
.job-ref { display:inline-block; padding:1px 7px; margin:1px 0; border:1px solid #C3CAD6; border-radius:999px;
           font-size:11.5px; font-weight:700; line-height:1.5; color:var(--verti-navy); text-decoration:none;
           white-space:nowrap; background:#fff; }
.job-ref:hover { background:#EAEDF4; text-decoration:none; }

/* ---- The scope KPI card (.pt-kpi) — sprint portal-tasks-tab -------------
   ONE card shape, used by the portal's Ongoing work tab (a card per site) and
   by customer-tasks.html (a card per scope group). It is tasks.html's inline
   shape lifted into the house sheet rather than pasted into a second page: the
   two surfaces show the same customer's work to the two sides of it, and a
   drifted card is how they start disagreeing about what a bar means.

   ONE NAVY ACCENT, not tasks.html's five department colours. Nothing here is
   department-coded - a site is a site - and a colour that carries no meaning is
   a colour a reader spends attention on for nothing.

   ⚠ THE `.pt-` PREFIX IS SHARED IN THIS FILE. Everything else called .pt-*
   here (.pt-bar, .pt-seg, .pt-axis, .pt-gap …) belongs to the PUANTAJ
   timeline widget. That is why every part of this card is spelled .pt-kpi-*
   and not .pt-bar: a card wearing .pt-bar would inherit a 22px puantaj row on
   every page that loads both. Do not add a bare .pt-<word> under this heading. */
.pt-kpi-row { display: flex; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
.pt-kpi {
  flex: 1 1 190px; min-width: 0; background: var(--surface);
  border: 1px solid var(--border); border-radius: 10px;
  padding: 9px 12px 9px 14px; position: relative; overflow: hidden;
  text-align: left; font: inherit; color: inherit;
  transition: box-shadow .15s, transform .12s;
}
/* A REAL <button>, because the card selects a scope. A div with a click
   handler is unreachable by keyboard, and this row is the tab's only filter. */
button.pt-kpi { cursor: pointer; }
.pt-kpi::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0;
                  width: 3px; background: var(--verti-navy); }
button.pt-kpi:hover { box-shadow: 0 4px 14px rgba(0,0,0,.1); transform: translateY(-1px); }
.pt-kpi.active { outline: 2px solid var(--verti-navy); outline-offset: 1px; }
.pt-kpi:focus-visible { outline: 2px solid var(--verti-navy); outline-offset: 2px; }
/* display:block on every part, because the card is a <button> and its children
   are <span>s - a button cannot legally contain a <div>, and inline spans would
   run the five lines together on one row. */
.pt-kpi-k, .pt-kpi-v, .pt-kpi-meter, .pt-kpi-p, .pt-kpi-sub, .pt-kpi-sub2 { display: block; }
.pt-kpi-k { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .3px;
            color: var(--verti-navy); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pt-kpi-v { font-size: 26px; font-weight: 800; color: var(--verti-navy); line-height: 1.05; }
.pt-kpi-v .u { display: inline; }
.pt-kpi-v .u { font-size: 12px; font-weight: 600; color: var(--muted); }
.pt-kpi-meter { height: 7px; border-radius: 4px; background: #EEF1F5; overflow: hidden; margin: 5px 0 2px; }
.pt-kpi-meter > span { display: block; height: 100%; background: var(--verti-navy); }
.pt-kpi-p { font-size: 11px; font-weight: 700; color: #374151; }
.pt-kpi-sub { font-size: 11px; color: var(--muted); margin-top: 3px; white-space: nowrap; }
/* The overdue sub-line earns weight and colour; it is the one figure on the
   card a customer may have to act on. Never colour ALONE - it carries the "!"
   glyph too, so it survives a monochrome print and a colour-blind reader. */
.pt-kpi-sub.warn { color: #B91C1C; font-weight: 700; }
.pt-kpi-sub2 { font-size: 10.5px; color: #64748B; margin-top: 1px;
               white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---- Priority stars and the progress bar (shared by both work surfaces) ----
   tasks.html's SHAPE, repeated - not imported from it. The stars carry an
   aria-label ("Priority 4 of 5"), written by the page, because five glyphs are
   not a number to a screen reader. */
.ct-stars { color: #D97706; font-size: 13px; white-space: nowrap; }
.ct-bar { display: inline-block; width: 84px; height: 6px; border-radius: 4px;
          background: #EEF1F5; vertical-align: middle; overflow: hidden; }
.ct-bar > span { display: block; height: 100%; background: var(--brand-gradient); }
.ct-bar > span.done { background: var(--success); }
.ct-pctnum { font-size: 11px; color: var(--muted); font-weight: 700; }
.ct-overdue { color: #B91C1C; font-weight: 700; }
.ct-pill { display: inline-block; padding: 2px 8px; border-radius: 9px; font-size: 10px; font-weight: 700; }
.ct-pill-open   { background: #FEF3C7; color: #B45309; }
.ct-pill-closed { background: #D1FAE5; color: #047857; }
