/* ── Design tokens ─────────────────────────────────────────────── */
/* One palette for both platforms: Apple's dark system colours. This used to be
   a saturated navy with translucent "glass" panels, overridden to iOS colours
   only inside body.mobile-mode — two designs to keep in step, and the desktop
   one read as a monitoring dashboard rather than an app. The phone deltas that
   remain are listed under "Phone presentation" at the bottom of this file. */
:root{
  --bg:#0a0a0c;              /* near-black: pure #000 is harsh across a large
                                bright monitor for a full working day. The phone
                                overrides this to true black. */
  --surface:#141416;
  --card:#1c1c1e;            /* secondarySystemBackground */
  --card2:#2c2c2e;           /* tertiarySystemBackground */
  --glass:#1c1c1e;
  --glass2:#2c2c2e;

  --border:rgba(255,255,255,.09);
  --border2:rgba(255,255,255,.16);
  --border-accent:rgba(10,132,255,.45);

  /* iOS system colours (dark). */
  --blue:#0A84FF;--blue2:#0A84FF;--blue3:#0060df;
  --blue-glow:rgba(10,132,255,.28);--blue-glow2:rgba(10,132,255,.14);
  --green:#30D158;--green2:#248A3D;--green-glow:rgba(48,209,88,.22);
  --amber:#FF9F0A;--amber-glow:rgba(255,159,10,.22);
  --red:#FF453A;--red-glow:rgba(255,69,58,.22);
  --purple:#BF5AF2;--purple-glow:rgba(191,90,242,.22);
  --cyan:#64D2FF;

  --text:#ffffff;--text2:#98989f;--text3:#636366;   /* label / secondary / tertiary */

  --radius:14px;--radius-sm:10px;--radius-lg:16px;--radius-xl:20px;
  --shadow:none;
  --shadow-lg:0 12px 40px rgba(0,0,0,.6);
  --shadow-glow:none;

  /* SF on Apple hardware, Inter everywhere else. */
  --font:-apple-system,BlinkMacSystemFont,'SF Pro Text','Inter',system-ui,sans-serif;

  /* Chrome heights. Four fixed/sticky elements are positioned relative to the
     topbar and two to the bottom tab bar; these were hardcoded 64px in each
     place, so changing the bar height on phones detached the project popover,
     the overflow sheet and the menu backdrop from it. */
  --topbar-h:60px;
  --tabbar-h:64px;
}

/* ── Reset ─────────────────────────────────────────────────────── */
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
/* Suppress native page rubber-band so the fixed topbar can never shift during
   the pull-to-refresh gesture — only the in-flow .ptr-container below moves. */
html,body{overscroll-behavior-y:none}
img,svg{max-width:100%;height:auto}
body{
  font-family:var(--font);
  background:var(--bg);
  color:var(--text);
  min-height:100vh;
  font-size:14px;
  -webkit-font-smoothing:antialiased;
  /* The two decorative radial blobs are gone — a plain surface is what lets the
     cards read as raised, which is the whole basis of this look. */
}
::-webkit-scrollbar{width:8px;height:8px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16);border-radius:4px;border:2px solid transparent;background-clip:padding-box}
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.28);background-clip:padding-box}

/* Sentence case is set on each rule below rather than as a blanket override
   here — an override placed this early loses on source order to every rule it
   names, which is exactly how the tab-bar labels stayed uppercase. */

/* ── Topbar ────────────────────────────────────────────────────── */
.topbar{
  position:sticky;top:0;z-index:100;
  background:rgba(20,20,22,.72);
  backdrop-filter:blur(28px) saturate(180%);
  -webkit-backdrop-filter:blur(28px) saturate(180%);
  border-bottom:1px solid var(--border);
  padding:0 clamp(16px,4vw,32px);
  padding-top:env(safe-area-inset-top);
  padding-left:max(clamp(16px,4vw,32px),env(safe-area-inset-left));
  padding-right:max(clamp(16px,4vw,32px),env(safe-area-inset-right));
  min-height:calc(var(--topbar-h) + env(safe-area-inset-top));
  display:flex;align-items:center;gap:clamp(10px,2vw,16px);
}
.nav-toggle{
  display:none;flex-shrink:0;margin-left:auto;
  width:44px;height:44px;align-items:center;justify-content:center;
  background:transparent;border:none;
  border-radius:var(--radius-sm);color:var(--blue);font-size:20px;
  cursor:pointer;font-family:inherit;
}
/* The shimmering accent rule under the bar was the loudest ornament on
   screen; the hairline border does the job. */
.topbar::after{display:none}
.logo{
  padding:4px 8px;background:transparent;box-shadow:none;
  display:flex;align-items:center;flex-shrink:0;
}
.logo img{height:34px;width:auto;max-width:none;display:block}
.topbar-title{flex:1;min-width:0}
.topbar-title p{font-size:11px;color:var(--text2);margin-top:1px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Project switcher: the page heading doubles as the control that changes
   project. Tapping it opens an iOS-style popover listing every project with its
   client and completion, one tap to switch. It replaced a native <select>, which
   on iOS meant opening the full-height picker wheel, scrolling and tapping Done
   just to move between two projects — and could only ever show the bare name. */
.proj-switcher{position:relative;display:inline-flex;max-width:100%}
.proj-switcher-face{
  display:inline-flex;align-items:center;gap:7px;min-width:0;
  font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--text);
  padding:3px 8px 3px 0;border-radius:var(--radius-sm);
  background:none;border:none;font-family:inherit;cursor:pointer;
  transition:color .15s;text-align:left;
}
.proj-switcher-face > span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.proj-switcher-face svg{flex-shrink:0;color:var(--text2);transition:transform .2s ease,color .15s}
.proj-switcher.open .proj-switcher-face svg{transform:rotate(180deg);color:var(--blue)}
.proj-switcher-face:focus-visible{outline:2px solid var(--blue);outline-offset:3px}
@media(hover:hover){
  .proj-switcher:not(.is-static):hover .proj-switcher-face{color:var(--blue)}
  .proj-switcher:not(.is-static):hover .proj-switcher-face svg{color:var(--blue)}
}
/* Only one project: behave as a plain heading, no affordance, not clickable. */
.proj-switcher.is-static .proj-switcher-face{cursor:default;pointer-events:none}
.proj-switcher.is-static .proj-switcher-face svg{display:none}

/* The popover itself. Scales up from the title it's anchored to, the way an iOS
   context menu grows out of the control that summoned it. */
.proj-menu{
  position:absolute;top:calc(100% + 10px);left:-4px;
  min-width:min(310px,calc(100vw - 32px));max-width:calc(100vw - 32px);
  max-height:min(60vh,420px);overflow-y:auto;-webkit-overflow-scrolling:touch;
  background:var(--card2);
  border:1px solid var(--border2);
  border-radius:14px;
  box-shadow:0 18px 50px rgba(0,0,0,.55),0 2px 6px rgba(0,0,0,.35);
  padding:5px;
  z-index:140;
  opacity:0;transform:scale(.94) translateY(-6px);transform-origin:top left;
  pointer-events:none;visibility:hidden;
  transition:opacity .16s ease,transform .2s cubic-bezier(.32,.9,.36,1.06),visibility 0s linear .2s;
}
.proj-switcher.open .proj-menu{
  opacity:1;transform:scale(1) translateY(0);
  pointer-events:auto;visibility:visible;
  transition:opacity .16s ease,transform .24s cubic-bezier(.32,.9,.36,1.06),visibility 0s;
}
.proj-menu-item{
  width:100%;display:flex;align-items:center;gap:10px;
  padding:9px 10px;min-height:46px;
  background:none;border:none;border-radius:9px;
  font-family:inherit;color:var(--text);cursor:pointer;text-align:left;
  transition:background .12s;
}
.proj-menu-item + .proj-menu-item{
  /* Hairline separators inset past the checkmark gutter, as iOS lists do. */
  box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
}
.proj-menu-check{
  width:16px;flex-shrink:0;display:flex;align-items:center;justify-content:center;
  color:var(--blue);
}
.proj-menu-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.proj-menu-name{
  font-size:13px;font-weight:500;line-height:1.3;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.proj-menu-item.is-active .proj-menu-name{font-weight:650;color:var(--blue)}
.proj-menu-sub{
  font-size:11px;color:var(--text2);line-height:1.3;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.proj-menu-pct{
  font-size:11.5px;font-weight:600;color:var(--text2);
  flex-shrink:0;font-variant-numeric:tabular-nums;
}
/* Reorder arrows. Stacked into the height of one row so they add a column
   rather than a second line, and dimmed until the row is hovered or something
   inside it has focus: on a list of five projects, ten always-lit arrows read as
   the point of the menu instead of a thing you can do in it. Kept fully visible
   on touch, where there is no hover to reveal them. */
.proj-reorder{
  display:flex;flex-direction:column;flex-shrink:0;margin-left:2px;
  opacity:.55;transition:opacity .12s;
}
.proj-menu-item:hover .proj-reorder,
.proj-menu-item:focus-within .proj-reorder{opacity:1}
@media(hover:none){ .proj-reorder{opacity:.8} }
/* 16px each, so the pair stacks to the same 32px as the name-plus-client text
   beside it and no row gets taller for having them. */
.proj-reorder-btn{
  display:flex;align-items:center;justify-content:center;
  width:24px;height:16px;padding:0;
  background:none;border:none;border-radius:5px;
  color:var(--text2);cursor:pointer;
  transition:background .12s,color .12s;
}
.proj-reorder-btn:hover:not(:disabled){background:rgba(255,255,255,.10);color:var(--text)}
.proj-reorder-btn:active:not(:disabled){background:rgba(255,255,255,.16)}
.proj-reorder-btn:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}
.proj-reorder-btn:disabled{opacity:.25;cursor:default}
/* A phone has no hover, so these have to be big enough to hit with a thumb, but
   every pixel here is taken off the project name beside them. 26x22 is the
   smallest that still reads as two separate targets. */
body.mobile-mode .proj-reorder-btn{width:26px;height:22px}
body.mobile-mode .proj-reorder{gap:1px;margin-left:0}

.proj-menu-action{color:var(--blue)}
.proj-menu-action .proj-menu-name{color:var(--blue);font-weight:500}
.proj-menu-action .proj-menu-check{color:var(--blue)}
/* On a phone the switcher sits well right of the screen edge (after the logo),
   so anchoring the popover to it pushed the card off-screen. Pin it to the
   viewport instead: a full-width card under the topbar, which is also the more
   iOS-like presentation at this size. */
body.mobile-mode .proj-menu{
  position:fixed;
  top:calc(var(--topbar-h) + env(safe-area-inset-top) + 8px);
  left:max(12px,env(safe-area-inset-left));
  right:max(12px,env(safe-area-inset-right));
  min-width:0;max-width:none;
  transform-origin:top center;
}
.proj-menu-item:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}
.proj-menu-item:active{background:rgba(255,255,255,.09)}
@media(hover:hover){
  .proj-menu-item:hover{background:rgba(255,255,255,.06)}
}

.topbar-right{display:flex;align-items:center;gap:8px;flex-shrink:0}
/* The one action that stays out of the overflow menu at every width. */
.topbar-primary{flex-shrink:0}

/* ── Notification bell & feed ──────────────────────────────────── */
/* Modelled on .proj-menu above rather than being a third kind of popover: a
   card anchored under its control on desktop, pinned to the viewport under the
   topbar on a phone (where anchoring to a control sitting near the right edge
   would push the card off-screen). */
.notif{position:relative;flex-shrink:0}
.notif-bell{
  position:relative;display:flex;align-items:center;justify-content:center;
  width:38px;height:38px;
  background:transparent;border:none;border-radius:var(--radius-sm);
  color:var(--text2);cursor:pointer;font-family:inherit;
  transition:color .15s,background .15s;
}
.notif-bell:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
.notif.open .notif-bell{color:var(--blue);background:rgba(10,132,255,.12)}
@media(hover:hover){.notif-bell:hover{color:var(--text)}}

/* The badge is the only thing on the topbar allowed to be red — it is the one
   element whose whole job is to be noticed. */
.notif-badge{
  position:absolute;top:3px;right:2px;
  min-width:17px;height:17px;padding:0 4px;
  display:flex;align-items:center;justify-content:center;
  background:var(--red);color:#fff;
  font-size:10px;font-weight:700;line-height:1;
  border-radius:999px;border:2px solid var(--surface);
  font-variant-numeric:tabular-nums;pointer-events:none;
}
.notif-badge[hidden]{display:none}

.notif-panel{
  position:absolute;top:calc(100% + 10px);right:-4px;
  width:min(380px,calc(100vw - 32px));
  background:var(--card2);
  border:1px solid var(--border2);
  border-radius:14px;
  box-shadow:0 18px 50px rgba(0,0,0,.55),0 2px 6px rgba(0,0,0,.35);
  z-index:140;overflow:hidden;
  display:flex;flex-direction:column;max-height:min(72vh,560px);
  opacity:0;transform:scale(.94) translateY(-6px);transform-origin:top right;
  pointer-events:none;visibility:hidden;
  transition:opacity .16s ease,transform .2s cubic-bezier(.32,.9,.36,1.06),visibility 0s linear .2s;
}
.notif.open .notif-panel{
  opacity:1;transform:scale(1) translateY(0);
  pointer-events:auto;visibility:visible;
  transition:opacity .16s ease,transform .24s cubic-bezier(.32,.9,.36,1.06),visibility 0s;
}
.notif-panel-head{
  display:flex;align-items:center;gap:4px;
  padding:12px 10px 10px 14px;border-bottom:1px solid var(--border);
  flex-shrink:0;
}
.notif-panel-title{flex:1;font-size:14px;font-weight:650;letter-spacing:-.01em}
.notif-icon-btn{
  width:30px;height:30px;display:flex;align-items:center;justify-content:center;
  background:none;border:none;border-radius:8px;color:var(--text2);cursor:pointer;
  transition:background .12s,color .12s;
}
.notif-icon-btn:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}
@media(hover:hover){.notif-icon-btn:hover{background:rgba(255,255,255,.07);color:var(--text)}}

.notif-chips{display:flex;gap:6px;padding:10px 12px;flex-wrap:wrap;flex-shrink:0}
.notif-chip{
  padding:5px 11px;border-radius:999px;
  background:rgba(255,255,255,.06);border:1px solid transparent;
  color:var(--text2);font-family:inherit;font-size:11.5px;font-weight:550;
  cursor:pointer;transition:background .12s,color .12s;
}
.notif-chip.is-on{background:rgba(10,132,255,.16);color:var(--blue);border-color:var(--border-accent)}
.notif-chip:focus-visible{outline:2px solid var(--blue);outline-offset:1px}

/* The two scrolling regions carry their own background rather than inheriting
   the panel's. They are composited as separate layers (a scroll container with
   -webkit-overflow-scrolling:touch), and .topbar's backdrop-filter makes it a
   backdrop root — so a child layer with no background of its own paints against
   the blurred page instead of against the panel, and the whole list renders
   see-through. .proj-menu never hit this because it IS the scroller and holds
   the background itself. */
/* #notifBody is what makes the list scrollable, and it is easy to miss because
   it has no visual job at all — it only groups the chips and the feed so the
   settings pane can replace both at once. Being a plain block, it sized itself
   to its content, so .notif-list inherited no height limit: overflow-y:auto had
   nothing to scroll (scrollHeight === clientHeight) while .notif-panel's
   overflow:hidden quietly cut the list off. With 40 notifications only the
   first 6 could be reached, and no amount of scrolling or dragging revealed the
   rest. flex:1 + min-height:0 is the pair that actually bounds a flex child —
   flex:1 alone still floors it at min-height:auto, i.e. its content height. */
#notifBody{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
.notif-list{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:0 6px 8px;background:var(--card2)}
.notif-row{
  width:100%;display:flex;align-items:flex-start;gap:10px;
  padding:10px;border:none;border-radius:10px;background:none;
  font-family:inherit;text-align:left;color:var(--text);cursor:pointer;
  transition:background .12s;position:relative;
}
.notif-row + .notif-row{box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
.notif-row:focus-visible{outline:2px solid var(--blue);outline-offset:-2px}
.notif-row:active{background:rgba(255,255,255,.09)}
@media(hover:hover){.notif-row:hover{background:rgba(255,255,255,.06)}}
/* Unread marker: a dot rather than a filled row, so a long unread list doesn't
   turn into a wall of highlight. */
.notif-row.is-new::after{
  content:'';position:absolute;top:14px;right:8px;
  width:7px;height:7px;border-radius:50%;background:var(--blue);
}
.notif-row-icon{flex-shrink:0;display:flex;margin-top:1px}
.notif-row-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1;padding-right:10px}
/* The project name leads every row — a feed spanning five sites is unreadable
   without knowing which one each line is about. */
.notif-row-project{
  font-size:10.5px;font-weight:650;letter-spacing:.03em;text-transform:uppercase;
  color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.notif-row-title{font-size:12.5px;font-weight:550;line-height:1.35}
.notif-row.is-high .notif-row-title{font-weight:650}
.notif-row-body{font-size:11.5px;color:var(--text2);line-height:1.4}
.notif-row-meta{font-size:10.5px;color:var(--text3);margin-top:1px}
.notif-empty{padding:26px 18px;text-align:center;font-size:12px;color:var(--text2);line-height:1.6}
.notif-link{background:none;border:none;color:var(--blue);font:inherit;font-size:12px;cursor:pointer;text-decoration:underline}

.notif-settings{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:4px 14px 14px;background:var(--card2)}
.notif-settings[hidden]{display:none}
.notif-set-group{padding:12px 0}
.notif-set-group + .notif-set-group{border-top:1px solid var(--border)}
.notif-set-label{
  font-size:10.5px;font-weight:650;letter-spacing:.05em;text-transform:uppercase;
  color:var(--text2);margin-bottom:9px;
}
.notif-radio,.notif-mute{display:flex;align-items:flex-start;gap:9px;padding:6px 0;font-size:12.5px;cursor:pointer}
.notif-radio input,.notif-mute input{margin-top:2px;flex-shrink:0;accent-color:var(--blue)}
.notif-radio strong{display:block;font-weight:550}
.notif-radio em{display:block;font-style:normal;font-size:11px;color:var(--text2);margin-top:2px;line-height:1.45}
.notif-note{font-size:11px;color:var(--text2);line-height:1.55;margin-top:8px}
.notif-push-btn{width:100%;justify-content:center}

/* Phone: pin the card to the viewport under the topbar, exactly as .proj-menu
   does, instead of anchoring it to a bell sitting near the right edge. */
body.mobile-mode .notif-panel{
  position:fixed;
  top:calc(var(--topbar-h) + env(safe-area-inset-top) + 8px);
  left:max(12px,env(safe-area-inset-left));
  right:max(12px,env(safe-area-inset-right));
  width:auto;max-height:min(76vh,620px);
  transform-origin:top center;
}

/* ── Destructive-action summary ────────────────────────────────── */
/* Used by the delete-all-work-logs confirmation. Its job is to make the cost
   legible at a glance, so the decision is made on the numbers rather than on
   the word "sure". */
.danger-box{
  background:rgba(255,69,58,.08);
  border:1px solid rgba(255,69,58,.28);
  border-radius:var(--radius-sm);
  padding:12px 14px;
}
.danger-row{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  font-size:12.5px;padding:5px 0;
}
.danger-row + .danger-row{box-shadow:inset 0 1px 0 rgba(255,255,255,.06)}
.danger-row span{color:var(--text2)}
.danger-row b{font-weight:650;font-variant-numeric:tabular-nums}
.danger-note{
  font-size:11px;color:var(--text2);line-height:1.55;
  margin-top:10px;padding-top:10px;border-top:1px solid rgba(255,69,58,.22);
}

/* ── View tab bar ──────────────────────────────────────────────── */
.view-tabbar{
  position:sticky;top:calc(var(--topbar-h) + env(safe-area-inset-top));z-index:90;
  background:rgba(20,20,22,.72);
  backdrop-filter:blur(28px) saturate(180%);
  -webkit-backdrop-filter:blur(28px) saturate(180%);
  border-bottom:1px solid var(--border);
  padding:9px clamp(16px,4vw,32px);
  padding-left:max(clamp(16px,4vw,32px),env(safe-area-inset-left));
  padding-right:max(clamp(16px,4vw,32px),env(safe-area-inset-right));
  display:flex;align-items:center;gap:2px;
  overflow-x:auto;scrollbar-width:none;
}
.view-tabbar::-webkit-scrollbar{display:none}
/* An iOS segmented control rather than an underlined tab strip. */
.view-tab{
  padding:8px 16px;
  font-size:13.5px;font-weight:500;cursor:pointer;
  color:var(--text2);
  transition:background .18s,color .18s;
  border:none;background:transparent;font-family:inherit;
  border-radius:9px;
  display:flex;align-items:center;gap:7px;
  position:relative;
  flex-shrink:0;white-space:nowrap;
}
@media(hover:hover){.view-tab:hover{color:var(--text);background:rgba(255,255,255,.06)}}
.view-tab.active{
  color:var(--text);
  background:rgba(255,255,255,.12);
  font-weight:600;
}
.view-tab.active svg{color:var(--blue)}

/* ── Buttons ───────────────────────────────────────────────────── */
/* Flat tinted fills — no gradients, no glow, no lift. Hover is a slight
   brightening rather than movement, and the press scales and dims. */
.btn{
  display:inline-flex;align-items:center;gap:6px;
  padding:8px 15px;
  border-radius:var(--radius-sm);
  font-size:13px;font-family:inherit;font-weight:600;letter-spacing:-.01em;
  cursor:pointer;border:none;box-shadow:none;
  transition:background .15s,color .15s,opacity .15s,transform .12s;
  white-space:nowrap;position:relative;
  /* Anchors styled as buttons were rendering underlined (Manage Users) */
  text-decoration:none;
}
.btn:active{transform:scale(.97);opacity:.75}
.btn-blue{background:var(--blue);color:#fff}
.btn-green{background:var(--green);color:#04240f}
.btn-outline{background:rgba(255,255,255,.10);border:none;color:var(--text)}
.btn-red{background:rgba(255,69,58,.17);border:none;color:var(--red)}
.btn-amber{background:rgba(255,159,10,.17);border:none;color:var(--amber)}
@media(hover:hover){
  .btn-blue:hover{background:#3a9dff}
  .btn-green:hover{background:#4ade6b}
  .btn-outline:hover{background:rgba(255,255,255,.16)}
  .btn-red:hover{background:rgba(255,69,58,.28)}
  .btn-amber:hover{background:rgba(255,159,10,.28)}
}
.btn-sm{padding:7px 13px;font-size:12.5px}
.btn-xs{padding:5px 10px;font-size:11.5px;border-radius:8px}
.btn-icon{padding:7px 9px}
/* Ghost danger variant for inline edit/delete row actions — replaces the
   old ad-hoc style="color:var(--red)" sprinkled on outline buttons. */
.btn-outline.btn-danger{color:var(--red)}
@media(hover:hover){
  .btn-outline.btn-danger:hover{background:rgba(255,69,58,.22);color:var(--red)}
}

/* Inline SVG icons align optically with the text baseline */
.btn svg,.view-tab svg,.mtab-icon svg,.import-banner-label svg{flex-shrink:0;display:block}
.import-banner-label{display:inline-flex;align-items:center;gap:6px}
.chev-icon{display:inline-flex;align-items:center;justify-content:center}
.chev-icon svg{display:block}

/* Keyboard focus rings (invisible for mouse users, clear for keyboard) */
.btn:focus-visible,.view-tab:focus-visible,.mtab:focus-visible,
.nav-toggle:focus-visible,.tab:focus-visible{
  outline:2px solid var(--blue);outline-offset:2px;
}

/* ── Main layout ───────────────────────────────────────────────── */
.main{
  padding:clamp(16px,3vw,28px) clamp(16px,4vw,32px);
  padding-left:max(clamp(16px,4vw,32px),env(safe-area-inset-left));
  padding-right:max(clamp(16px,4vw,32px),env(safe-area-inset-right));
  padding-bottom:max(clamp(16px,3vw,28px),env(safe-area-inset-bottom));
  display:flex;flex-direction:column;gap:clamp(16px,2.4vw,22px);
  max-width:2100px;margin-inline:auto;width:100%;
}

/* ── Section label ─────────────────────────────────────────────── */
.section-label{font-size:13px;font-weight:600;color:var(--text2);padding-left:2px}

/* ── Panel ─────────────────────────────────────────────────────── */
/* Grouped-list presentation, as Settings/Health do it: the section title sits
   OUTSIDE the container in muted text and the content is one rounded card.
   Replaces a bordered box with its own header bar, a divider and a glowing
   accent tick — three pieces of chrome doing the work of one line of type. */
.panel{
  background:none;
  border:none;
  box-shadow:none;
  overflow:visible;
}
.panel-header{
  padding:0 4px 9px;
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  background:none;border-bottom:none;min-height:0;
}
.panel-header h2{
  font-size:13px;font-weight:600;
  color:var(--text2);
  display:flex;align-items:center;gap:8px;
}
.panel-body{
  background:var(--card);
  border-radius:var(--radius);
  padding:6px 18px;
}
/* Panels that hand their body a full-bleed child (a table, a card list) set
   padding:0 inline; the card background still applies. */

/* ── KPI row ───────────────────────────────────────────────────── */
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px}
.kpi-card{
  background:var(--card);
  border:none;
  border-radius:var(--radius);
  padding:17px 18px 16px;
  position:relative;overflow:hidden;
  box-shadow:none;
  cursor:default;
}
/* The pulsing gradient bar, the radial sheen and the hover lift are gone. The
   accent moves onto the number itself, the way Health tints its metrics — same
   information, three fewer ornaments. */
.kpi-label{
  font-size:12.5px;color:var(--text2);
  margin-bottom:8px;font-weight:500;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.kpi-value{
  font-size:clamp(26px,2.1vw,34px);font-weight:650;line-height:1.05;
  color:var(--accent,var(--text));
  letter-spacing:-.03em;font-variant-numeric:tabular-nums;
}
.kpi-sub{font-size:12px;color:var(--text2);margin-top:6px;line-height:1.4}
.kpi-icon{display:none}

/* ── Grid layouts ──────────────────────────────────────────────── */
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:20px;align-items:start}
.grid-3{display:grid;grid-template-columns:2fr 1fr 1fr;gap:20px;align-items:start}
.grid-4{display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;align-items:start}

/* Tiles sitting side by side match heights, and the list inside scrolls rather
   than the tile growing to fit it. Previously align-items:start let each take
   its natural height: the bar list was capped so it stopped at ~12 rows while
   the value list beside it had no cap at all, so a 13-row panel ran hundreds of
   pixels past its neighbour and left the row visibly lopsided.
   --tile-list-h caps how tall a row may get before its lists scroll. */
/* Scoped away from the phone as well as by width: on a phone a capped,
   independently-scrolling list inside a scrolling page is the anti-pattern that
   made a swipe stop dead mid-page, so there the tiles keep their natural height
   and the page does the scrolling. (?mobile=1 on a wide window would otherwise
   hit this block.) */
@media(min-width:900px){
  body:not(.mobile-mode) .grid-2,
  body:not(.mobile-mode) .grid-3{align-items:stretch;--tile-list-h:440px}
  body:not(.mobile-mode) .grid-2 > .panel,
  body:not(.mobile-mode) .grid-3 > .panel{display:flex;flex-direction:column;min-height:0}
  body:not(.mobile-mode) .grid-2 > .panel > .panel-body,
  body:not(.mobile-mode) .grid-3 > .panel > .panel-body{flex:1;min-height:0;display:flex;flex-direction:column}
  /* min-height:0 is load-bearing: a flex item defaults to min-height:auto and
     would refuse to shrink below its content, so the list would never scroll. */
  body:not(.mobile-mode) .grid-2 > .panel > .panel-body > .sbar-list,
  body:not(.mobile-mode) .grid-3 > .panel > .panel-body > .sbar-list,
  body:not(.mobile-mode) .grid-2 > .panel > .panel-body > .ios-list,
  body:not(.mobile-mode) .grid-3 > .panel > .panel-body > .ios-list{
    flex:1;min-height:0;max-height:var(--tile-list-h);
    overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;
  }
  /* A ring should sit in the middle of whatever height it's given, not hug the
     top of a tile that stretched to match a long list beside it. */
  body:not(.mobile-mode) .grid-2 > .panel > .panel-body > .ios-rings,
  body:not(.mobile-mode) .grid-3 > .panel > .panel-body > .ios-rings{flex:1;align-items:center}
}
/* ── Responsive breakpoint system ──────────────────────────────── */
@media(max-width:1100px){
  .grid-3{grid-template-columns:1fr 1fr}
}
@media(max-width:900px){
  .grid-3,.grid-4{grid-template-columns:1fr 1fr}
  .grid-2{grid-template-columns:1fr}
}
/* Tablets & phones: collapse the busy dashboard topbar into a drawer */
/* Secondary actions (New Project, Manage, Setup, Manage Users, Log out) live in
   an overflow menu at EVERY width, not just below 1024px. Showing six equally
   weighted buttons across the topbar was the bulk of its clutter; the switcher
   and the primary Daily Log action are enough on their own. */
.has-drawer .nav-toggle{display:inline-flex}
.has-drawer .topbar-right{
  position:absolute;top:calc(100% + 6px);right:clamp(16px,4vw,32px);left:auto;
  flex-direction:column;align-items:stretch;gap:6px;
  width:min(240px,calc(100vw - 32px));
  padding:8px;
  background:var(--card2);
  border:1px solid var(--border2);
  border-radius:var(--radius);
  box-shadow:var(--shadow-lg);
  display:none;max-height:80dvh;overflow-y:auto;
  z-index:120;
}
.has-drawer .topbar-right.open{display:flex}
.has-drawer .topbar-right .btn,
.has-drawer .topbar-right form{width:100%;max-width:none}
/* Menu items read as a list, not a row of pills: left-aligned, flat until hover. */
.has-drawer .topbar-right .btn{
  justify-content:flex-start;min-height:38px;
  background:transparent;border-color:transparent;
  color:var(--text);box-shadow:none;
}
.has-drawer .topbar-right .btn svg{color:var(--text2)}
@media(hover:hover){
  .has-drawer .topbar-right .btn:hover{background:rgba(255,255,255,.06);border-color:transparent;transform:none;box-shadow:none}
  .has-drawer .topbar-right .btn:hover svg{color:var(--blue)}
}
/* Log out sits below a divider — destructive-ish and shouldn't be a mis-tap. */
.has-drawer .topbar-right form{margin-top:4px;padding-top:6px;border-top:1px solid var(--border)}
.has-drawer .topbar-right form .btn{color:var(--text2)}

@media(max-width:1024px){
  .has-drawer .topbar-title p{display:none}
}
@media(max-width:560px){
  /* Collapse the primary button to its icon before anything is allowed to wrap. */
  .topbar-primary span{display:none}
  .topbar-primary{padding:8px 10px}
}
@media(max-width:600px){
  .grid-2,.grid-3,.grid-4{grid-template-columns:1fr}
  .form-row{grid-template-columns:1fr}
  .modal-overlay{padding:10px}
  .modal-header{padding:16px 18px 14px}
  .modal-footer{padding:14px 16px;flex-wrap:wrap}
  .modal-footer .btn{flex:1 1 auto;justify-content:center}
  .kpi-card{padding:16px 16px 14px}
  /* Non-drawer topbars (e.g. Manage Users) wrap gracefully */
  .topbar:not(.has-drawer){flex-wrap:wrap;padding-bottom:10px}
  .topbar:not(.has-drawer) .topbar-right{width:100%;flex-wrap:wrap}
  /* Wide admin table becomes horizontally scrollable */
  table.mu-table{display:block;overflow-x:auto;white-space:nowrap}
  .mu-wrap{padding:20px 14px}
}
@media(max-width:400px){
  .btn-sm{padding:6px 10px}
  .topbar-title h1{font-size:13px}
}
/* Ultra-wide monitors: nudge base type up for readability */
@media(min-width:1920px){
  body{font-size:14px}
}
/* Touch devices: enforce accessible tap-target sizes */
@media(pointer:coarse){
  .btn{min-height:44px}
  .btn-xs{min-height:36px}
  .tab{min-height:40px}
  .form-group input,.form-group select,.form-group textarea{min-height:44px}
  .edit-tbl input,.edit-tbl select{min-height:44px}
  /* iOS Safari zooms the whole viewport when a focused input's text is under
     16px — and doesn't zoom back out, so every subsequent tap lands off-target.
     Only tap-target *height* was handled before; this covers font size. */
  .form-group input,.form-group select,.form-group textarea,
  .edit-tbl input,.edit-tbl select,
  input[type="search"]{font-size:16px}
}

/* ── Chart wraps ───────────────────────────────────────────────── */
.chart-wrap{position:relative;height:280px}
.chart-wrap-tall{position:relative;height:360px}
.chart-wrap-sm{position:relative;height:220px}

/* Scrollable chart tiles: for charts with many categories/points, the
   canvas is sized to its natural (uncompressed) content size by JS and this
   viewport clips + scrolls it, instead of Chart.js squeezing everything to
   fit the tile. -y scrolls vertically (many-row horizontal bar charts),
   -x scrolls horizontally (many-point time series). */
.chart-scroll-y{
  position:relative;height:360px;overflow-y:auto;overflow-x:hidden;
  -webkit-overflow-scrolling:touch;overscroll-behavior:contain;
  scrollbar-width:thin;
}
.chart-scroll-y-inner{position:relative;width:100%}
.chart-scroll-x{
  position:relative;height:360px;overflow-x:auto;overflow-y:hidden;
  -webkit-overflow-scrolling:touch;overscroll-behavior:contain;
  scrollbar-width:thin;
}
.chart-scroll-x-inner{position:relative;height:100%}

/* ── Tables ────────────────────────────────────────────────────── */
.tbl-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse}
th{
  background:var(--surface);
  color:var(--text2);
  font-size:12px;font-weight:600;
  padding:12px 14px;text-align:left;
  border-bottom:1px solid var(--border);
  white-space:nowrap;
  position:sticky;top:0;z-index:1;
}
td{
  padding:12px 14px;
  border-bottom:1px solid var(--border);
  font-size:13px;vertical-align:middle;
  transition:background .12s;
  font-variant-numeric:tabular-nums;
}
tr:last-child td{border-bottom:none}
@media(hover:hover){
  tbody tr:hover td{background:rgba(255,255,255,.04)!important}
}

/* ── Badges ────────────────────────────────────────────────────── */
.badge{
  display:inline-flex;align-items:center;
  padding:3px 8px;border-radius:7px;border:none;
  font-size:12px;font-weight:600;
}
.badge-green{background:rgba(48,209,88,.16);color:var(--green)}
.badge-amber{background:rgba(255,159,10,.16);color:var(--amber)}
.badge-red{background:rgba(255,69,58,.16);color:var(--red)}
.badge-blue{background:rgba(10,132,255,.16);color:var(--blue)}

/* ── Progress bar ──────────────────────────────────────────────── */
.prog-bar{height:6px;background:rgba(255,255,255,.09);border-radius:3px;overflow:hidden}
.prog-fill{height:100%;border-radius:4px;transition:width .5s cubic-bezier(.4,0,.2,1)}

/* ── Import banner ─────────────────────────────────────────────── */
.import-banner{
  background:var(--card);
  border:none;
  border-radius:var(--radius);
  padding:13px 16px;
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  box-shadow:none;
}
.import-banner-actions{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto}
/* Mobile: label above one full-width row of equal buttons — no ragged wrap */
body.mobile-mode .import-banner{flex-direction:column;align-items:stretch;gap:10px}
body.mobile-mode .import-banner-actions{margin-left:0}
body.mobile-mode .import-banner-actions .btn{flex:1;justify-content:center}
.import-banner-label{font-size:13px;color:var(--text2);flex:1;min-width:140px}

/* ── Drop zone ─────────────────────────────────────────────────── */
/* Drag-and-drop: the banner is the target. While a file is over it, the normal
   contents fade out and a single "Drop to import" overlay takes their place. */
.import-banner{position:relative;transition:border-color .18s,background .18s}
.import-banner-hint{font-style:normal;color:var(--text3);margin-left:2px}
.import-banner-drop{
  position:absolute;inset:0;
  display:none;align-items:center;justify-content:center;gap:9px;
  border-radius:var(--radius);
  background:var(--blue-glow2);
  color:var(--blue);font-size:12.5px;font-weight:600;
  pointer-events:none;
}
.import-banner.is-dropping{
  border-color:var(--blue);
  background:rgba(36,120,232,.08);
}
.import-banner.is-dropping > *:not(.import-banner-drop){opacity:0}
.import-banner.is-dropping .import-banner-drop{display:flex}
/* Dragging a file is a pointer-device gesture; on touch the hint is noise.
   Keyed on both the media query and the app's own phone mode so the forced
   ?mobile=1 preview matches what a real device shows. */
@media(pointer:coarse){
  .import-banner-hint{display:none}
}
body.mobile-mode .import-banner-hint{display:none}

/* ── Modal ─────────────────────────────────────────────────────── */
.modal-overlay{
  display:none;position:fixed;inset:0;
  background:rgba(0,0,0,.55);
  z-index:200;
  align-items:center;justify-content:center;
  padding:clamp(12px,3vw,20px);
  backdrop-filter:blur(10px);
  -webkit-backdrop-filter:blur(10px);
}
.modal-overlay.open{display:flex}
.modal{
  background:var(--surface);
  border:none;
  border-radius:var(--radius-lg);
  width:100%;max-width:700px;
  max-height:92vh;
  max-height:92dvh;
  display:flex;flex-direction:column;
  box-shadow:0 24px 70px rgba(0,0,0,.65);
}
.modal-wide{max-width:min(1240px,97vw)}
.modal-header{
  padding:20px 24px 16px;
  border-bottom:1px solid var(--border);
  flex-shrink:0;
  background:none;
}
.modal-header h3{font-size:18px;font-weight:650;letter-spacing:-.022em}
.modal-header p{font-size:12.5px;color:var(--text2);margin-top:3px}
.modal-body{padding:clamp(16px,3vw,24px);overflow:auto;flex:1}
.modal-footer{
  padding:14px 24px;
  border-top:1px solid var(--border);
  display:flex;justify-content:flex-end;gap:10px;
  flex-shrink:0;
  background:none;
}
.modal-footer-left{margin-right:auto;display:flex;gap:8px}

/* ── Forms ─────────────────────────────────────────────────────── */
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
.form-group{display:flex;flex-direction:column;gap:6px}
.form-group label{font-size:12.5px;color:var(--text2);font-weight:500}
.form-group input,.form-group select,.form-group textarea{
  background:var(--card2);
  border:1px solid transparent;
  color:var(--text);
  padding:11px 13px;
  border-radius:var(--radius-sm);
  font-size:13.5px;font-family:inherit;width:100%;
  transition:border-color .15s,box-shadow .15s;
}
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{
  outline:none;
  border-color:var(--blue);
  box-shadow:0 0 0 3px var(--blue-glow2);
  background:var(--card2);
}

/* ── Date / time inputs ─────────────────────────────────────────
   iOS Safari renders these as a native control whose intrinsic minimum width
   comes from the formatted value ("Aug 4, 2026"). min-width:auto resolves to
   that intrinsic size, which beats width:100% — so the field could not shrink
   to its container and ran off the right edge of the modal while every other
   input stopped at the margin. Chromium sizes them normally, which is why this
   only ever showed up on a phone. */
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"]{
  min-width:0;
  max-width:100%;
}
/* iOS also centres the value inside the field; every other input in the app is
   left-aligned, so the date row read as a different kind of control. */
input[type="date"]::-webkit-date-and-time-value,
input[type="time"]::-webkit-date-and-time-value,
input[type="datetime-local"]::-webkit-date-and-time-value{
  text-align:left;
  min-width:0;
  margin:0;
}
@media (pointer:coarse){
  /* Dropping the native appearance removes the intrinsic sizing at its source.
     Scoped to touch because on a desktop browser it would also remove the
     calendar picker button, which is the only way to open the picker there. */
  input[type="date"],
  input[type="time"],
  input[type="datetime-local"],
  input[type="month"]{
    -webkit-appearance:none;
    appearance:none;
  }
}

/* ── Type-to-filter dropdown ────────────────────────────────────
   Wraps a native <select> (see combobox.js) once its list is long enough to be
   worth searching. The <select> itself stays in the DOM holding the value, so
   it's hidden rather than removed — and not with display:none, which would take
   it out of the form and break label association. */
/* No `flex` here on purpose. The wrapper stands in different containers — a
   column .form-group, a row of controls, a table cell — and a blanket flex
   value stretches it vertically in the column case. enhanceSelect() copies the
   select's own inline flex/width across instead, so the markup's layout intent
   travels with it. */
.cbx{position:relative;width:100%;min-width:0}
.cbx-native{
  position:absolute;width:1px;height:1px;
  opacity:0;pointer-events:none;overflow:hidden;clip-path:inset(50%);
}
.cbx-face{
  display:flex;align-items:center;gap:8px;width:100%;
  background:var(--card2);border:1px solid transparent;
  color:var(--text);padding:11px 13px;
  border-radius:var(--radius-sm);
  font-size:13.5px;font-family:inherit;text-align:left;cursor:pointer;
  transition:border-color .15s,box-shadow .15s;
}
.cbx-face:focus-visible{outline:none;border-color:var(--blue);box-shadow:0 0 0 3px var(--blue-glow2)}
.cbx.is-open .cbx-face{border-color:var(--blue)}
.cbx-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cbx-label.is-placeholder{color:var(--text3)}
.cbx-face svg{flex-shrink:0;color:var(--text2);transition:transform .2s}
.cbx.is-open .cbx-face svg{transform:rotate(180deg)}

/* Portalled to <body> and positioned by combobox.js, so it can't be clipped by
   the scrolling modal body its field sits in. z-index clears the modal (200). */
.cbx-pop{
  position:fixed;z-index:260;
  display:flex;flex-direction:column;
  background:var(--card2);
  border:1px solid var(--border2);
  border-radius:12px;
  box-shadow:0 16px 44px rgba(0,0,0,.55);
  padding:6px;
  opacity:0;transform:translateY(-6px) scale(.98);transform-origin:top center;
  visibility:hidden;pointer-events:none;
  transition:opacity .14s ease,transform .18s cubic-bezier(.32,.9,.36,1.06),visibility 0s .18s;
}
.cbx-pop.is-open{
  opacity:1;transform:none;visibility:visible;pointer-events:auto;
  transition:opacity .14s ease,transform .18s cubic-bezier(.32,.9,.36,1.06),visibility 0s;
}
.cbx-search-wrap{display:flex;align-items:center;gap:7px;padding:0 8px 6px}
.cbx-search-wrap svg{flex-shrink:0;color:var(--text3)}
.cbx-search{
  flex:1;min-width:0;background:none;border:none;outline:none;
  color:var(--text);font-family:inherit;font-size:13.5px;padding:6px 0;
}
.cbx-search::placeholder{color:var(--text3)}
/* The popup's own max-height is set by positionCombobox from the space
   available, so the list just takes what's left of it. */
.cbx-list{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}
.cbx-opt{
  display:flex;align-items:center;gap:8px;width:100%;
  padding:9px 10px;min-height:40px;
  background:none;border:none;border-radius:8px;
  font-family:inherit;font-size:13.5px;color:var(--text);
  text-align:left;cursor:pointer;
}
.cbx-opt span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cbx-opt svg{flex-shrink:0;color:var(--blue)}
.cbx-opt.is-active{background:rgba(255,255,255,.07)}
.cbx-opt.is-on{color:var(--blue);font-weight:600}
.cbx-opt[disabled]{opacity:.4;cursor:default}
.cbx-empty{padding:14px 10px;text-align:center;font-size:12px;color:var(--text2)}

/* Inside the dense BOQ / bulk-entry grids the control has to match the row
   height it sits in, not the roomier modal form fields. */
.edit-tbl .cbx-face{padding:6px 7px;font-size:12.5px;border-radius:7px}

@media (max-width:760px){
  /* A bottom sheet rather than an anchored popover: at 390px a dropdown pinned
     to its field ends up nearly the full width anyway, and a sheet is both the
     native idiom and out of the keyboard's way. !important because
     positionCombobox writes inline top/left/width for the desktop case — it
     bails early in mobile-mode, but a control enhanced before the mode was
     detected can still carry stale inline values. */
  .cbx-pop{
    top:auto!important;bottom:0!important;left:0!important;
    width:100%!important;max-height:70dvh!important;
    border-radius:16px 16px 0 0;
    padding:10px 10px calc(10px + env(safe-area-inset-bottom));
    transform:translateY(100%);transform-origin:bottom center;
  }
  .cbx-pop.is-open{transform:translateY(0)}
  .cbx-opt{min-height:46px;font-size:15px}
  .cbx-search{font-size:16px}   /* under 16px and iOS zooms the viewport in */
}

/* ── Edit table inside modal ───────────────────────────────────── */
.edit-tbl{width:100%;border-collapse:collapse;font-size:12.5px}
.edit-tbl th{
  background:var(--surface);
  color:var(--text2);
  padding:10px 7px;
  font-size:11.5px;font-weight:600;
  border-bottom:1px solid var(--border);
  white-space:nowrap;
  position:sticky;top:0;z-index:1;
}
.edit-tbl td{padding:4px 3px;border-bottom:1px solid var(--border)}
.edit-tbl input{
  width:100%;
  background:transparent;
  border:1px solid transparent;
  color:var(--text);
  padding:7px 8px;
  border-radius:7px;
  font-size:12.5px;font-family:inherit;
  transition:border-color .12s,background .12s;
}
.edit-tbl input:focus{
  outline:none;
  border-color:var(--blue);
  background:var(--blue-glow2);
}
.edit-tbl .boq-computed{
  display:block;
  padding:6px 7px;
  color:var(--text2);
  white-space:nowrap;
}

/* ── BOQ pricing settings panel (Edit BOQ modal) ─────────────────── */
.boq-pricing-settings{
  background:var(--card);
  border:none;
  border-radius:var(--radius-sm);
  padding:12px 14px;
  margin-bottom:16px;
}
.boq-pricing-settings-title{
  font-size:13px;font-weight:600;color:var(--text);
  margin-bottom:10px;
}
.boq-pricing-settings-title span{
  font-weight:400;text-transform:none;letter-spacing:0;color:var(--text2);
}
.boq-pricing-settings-grid{
  display:grid;
  /* The two groups no longer carry the same number of settings — foreign has 7,
     local has 8 (VAT and AIT instead of a duty rate). Fitting to the available
     width beats a hardcoded count that has to be revisited every time the
     pricing chain gains a stage. */
  grid-template-columns:repeat(auto-fit,minmax(124px,1fr));
  gap:12px;
}
.boq-pricing-settings-grid .form-group{gap:4px}
.boq-pricing-settings-grid label{font-size:12px}
@media (max-width:760px){
  .boq-pricing-settings-grid{grid-template-columns:repeat(2,1fr)}
}
/* Local rows keep the Duty column so both tables stay aligned, but there is
   nothing to type in it — they reach landed cost through VAT + AIT. */
.boq-na{color:var(--text3);font-size:11px;cursor:default}

/* ── BOQ foreign / local groups ─────────────────────────────────── */
/* Customs duty only applies to imported material, so a BOQ is priced in two
   groups with independent percentages. Both the editor and the read-only table
   report them separately: one merged list would hide that two different rate
   sets produced the numbers in it. */
.boq-group + .boq-group{margin-top:28px;padding-top:24px;border-top:1px solid var(--border2)}
.boq-group-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:12px}
.boq-group-head h4{font-size:14px;font-weight:650;letter-spacing:-.01em;display:flex;align-items:center;gap:8px}
.boq-group-hint{font-size:11.5px;color:var(--text2)}
/* Add-row sits with the group heading rather than under the table, so a long
   BOQ can't push it off the bottom of the section while it's being clicked. */
.boq-group-add{margin-left:auto;align-self:center;flex-shrink:0}
.boq-group .tbl-wrap{margin-bottom:12px}
.boq-group .edit-tbl tfoot td{
  padding:9px 7px;font-size:11px;font-weight:700;
  border-top:1px solid var(--border2);
  background:rgba(255,255,255,.03);
  text-align:right;white-space:nowrap;
}
.boq-group .edit-tbl tfoot td:first-child{text-align:left;color:var(--text2);font-weight:600}
/* Deliberately NOT display:flex. A <td> set to flex drops out of the table
   layout algorithm: it stops forming a column with its header and stops sharing
   the row's border box, so the row hairlines stop short of it and the buttons
   float outside the grid. The buttons are laid out inline instead, which leaves
   the cell a real table-cell. */
.edit-tbl-actions{white-space:nowrap;text-align:right;vertical-align:middle}
.edit-tbl-actions .btn{display:inline-flex;vertical-align:middle}
.edit-tbl-actions .btn + .btn{margin-left:4px}
.boq-grand{font-size:11.5px;color:var(--text2);margin-right:14px;white-space:nowrap}
.boq-grand b{font-size:13px;font-weight:700;color:var(--text);margin-left:5px;font-variant-numeric:tabular-nums}

/* Section and subtotal rows inside the read-only BOQ table. */
.boq-sec-row td{
  background:rgba(255,255,255,.045);
  font-size:12.5px;font-weight:600;
  color:var(--text);padding:11px 14px;
}
.boq-sec-row td span{font-weight:400;color:var(--text2);margin-left:6px}
.boq-sec-row td:last-child{text-align:right;font-weight:400;color:var(--text2)}
.boq-sec-row svg{vertical-align:-2px;margin-right:4px;color:var(--blue)}
.boq-sub-row td{
  font-weight:600;font-size:12.5px;
  border-top:1px solid var(--border2);
  background:rgba(255,255,255,.02);
}
.boq-sub-row td:first-child{color:var(--text2)}
.boq-grand-row td{
  background:var(--surface);font-weight:700;
  border-top:2px solid var(--border2);
}
.boq-grand-row td:first-child{color:var(--text2);font-size:12.5px}

/* Phone: a group heading above each card list. */
.boq-sec-head{
  display:flex;align-items:center;gap:8px;
  padding:16px 16px 8px;font-size:13px;font-weight:600;color:var(--text);
}
.boq-sec-head svg{color:var(--blue);flex-shrink:0}
.boq-sec-head b{margin-left:auto;font-size:13px;font-variant-numeric:tabular-nums;color:var(--blue)}
.boq-sec-head + .mobile-card-list{padding-top:0}

/* ── Toast ─────────────────────────────────────────────────────── */
.toast{
  position:fixed;
  bottom:max(20px,env(safe-area-inset-bottom));
  right:max(16px,env(safe-area-inset-right));
  background:rgba(44,44,46,.97);
  border:none;
  border-radius:var(--radius);
  padding:14px 18px;
  display:flex;align-items:center;gap:12px;
  z-index:300;
  transform:translateY(80px) scale(.97);opacity:0;
  transition:all .3s cubic-bezier(.34,1.56,.64,1);
  font-size:13.5px;
  box-shadow:var(--shadow-lg);
  max-width:min(340px,calc(100vw - 32px));
  backdrop-filter:blur(16px);
  /* The toast is always in the DOM and only hidden by opacity, so without this
     it stayed hit-testable while invisible — a ~174x56 dead zone above the
     bottom-right of the screen that swallowed taps on the Menu tab. Only
     interactive while actually shown. */
  pointer-events:none;
  cursor:pointer;
}
.toast.show{transform:translateY(0) scale(1);opacity:1;pointer-events:auto}
.toast-icon{
  display:flex;align-items:center;justify-content:center;
  width:28px;height:28px;flex-shrink:0;
  border-radius:50%;background:rgba(255,255,255,.06);
}
.toast-icon svg{display:block}

/* ── Stat rows ─────────────────────────────────────────────────── */
.stat-row{
  display:flex;justify-content:space-between;align-items:center;
  padding:12px 0;border-bottom:1px solid var(--border);
}
.stat-row:last-child{border-bottom:none}
.stat-label{font-size:13px;color:var(--text)}
.stat-value{font-size:13.5px;font-weight:600;color:var(--text);font-variant-numeric:tabular-nums}

/* ── Donut center ──────────────────────────────────────────────── */
.donut-wrap{position:relative}
/* The tall cost-share donut sits in a wide 1fr grid column on large
   screens; without a cap it stretches into an oddly wide, mostly-empty
   box instead of a proportionate circle. */
.donut-wrap.chart-wrap-tall{max-width:480px;margin:0 auto}
.donut-center{
  position:absolute;top:50%;left:50%;
  transform:translate(-50%,-50%);
  text-align:center;pointer-events:none;
}
.donut-center-val{font-size:28px;font-weight:650;line-height:1;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.donut-center-sub{font-size:12.5px;color:var(--text2);margin-top:4px}

/* ── Tabs (inline) ─────────────────────────────────────────────── */
.tabs{display:flex;gap:4px;padding:4px;background:rgba(255,255,255,.04);border-radius:var(--radius-sm);width:fit-content;border:1px solid var(--border)}
.tab{padding:5px 14px;border-radius:6px;font-size:11px;font-weight:500;cursor:pointer;color:var(--text2);transition:all .15s;border:none;background:transparent;font-family:inherit}
.tab.active{background:var(--blue2);color:#fff;box-shadow:0 2px 8px rgba(36,120,232,.4)}

/* ── Highlighted rows ──────────────────────────────────────────── */
/* An activity with no progress at all, in the detail table. */
.row-hl0 td{background:rgba(255,69,58,.07)!important}

/* ── Divider ───────────────────────────────────────────────────── */
.divider{border:none;border-top:1px solid var(--border);margin:0}

/* The save-state chrome (.btn-save, .save-reminder, .unsaved-dot, .save-wrap)
   and the .live-badge pulse were removed along with their last callers — saving
   is automatic and reported by the toast now. .row-hl had no caller either. */

/* ── Button ripple ─────────────────────────────────────────────── */
.btn{overflow:hidden}
.btn-ripple{
  position:absolute;
  width:6px;height:6px;
  border-radius:50%;
  background:rgba(255,255,255,.35);
  transform:translate(-50%,-50%) scale(0);
  animation:ripple .6s ease-out forwards;
  pointer-events:none;
}
@keyframes ripple{
  to{transform:translate(-50%,-50%) scale(30);opacity:0}
}

/* ── Page-load skeleton shimmer ────────────────────────────────── */
@keyframes shimmer{
  0%{background-position:-200% 0}
  100%{background-position:200% 0}
}

/* The pulsing KPI accent bar, the shimmering rule under the topbar and the
   glowing tab underline are gone along with the elements they animated. They
   were the clearest "dashboard, not an app" tell, and three infinite loops kept
   the compositor awake for decoration. */

/* ── Chart panel fade-in ───────────────────────────────────────── */
.chart-wrap canvas,.chart-wrap-tall canvas,.chart-wrap-sm canvas,
.chart-scroll-y-inner canvas,.chart-scroll-x-inner canvas{
  animation:fadeIn .6s ease both;
}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}

/* ── Pull-to-refresh ───────────────────────────────────────────── */
/* Driven by a finger on touch and by trackpad/wheel overscroll on desktop —
   same indicator, same thresholds, same code path from release onward. */
/* Lives in normal flow directly above #mainContent (inserted there by
   initPullToRefresh in animations.js) — its height is driven by JS during
   the drag, so it pushes content down rather than floating over it. Colors
   are fixed per spec rather than the app's --blue/--green tokens, matching
   a dedicated dark surface (#141926) distinct from the page background. */
.ptr-container{
  height:0;position:relative;overflow:visible;
  flex-shrink:0;pointer-events:none;z-index:40;
}
.ptr-circle{
  position:absolute;left:50%;top:0;
  transform:translate(-50%,-20px);opacity:0;
  width:40px;height:40px;border-radius:50%;
  background:#141926;border:1px solid #252d3f;
  box-shadow:0 4px 14px rgba(0,0,0,.35);
  display:flex;align-items:center;justify-content:center;
  will-change:transform,opacity;
}
.ptr-ring{width:24px;height:24px;display:block;transform:rotate(-90deg)}
.ptr-ring-track{stroke:#252d3f}
.ptr-ring-arc{stroke:#16a34a;transition:stroke .15s ease-out}
.ptr-ring-arc.ptr-ready{stroke:#4ade80}
.ptr-container.ptr-spinning .ptr-ring{animation:ptrSpin .75s linear infinite}
@keyframes ptrSpin{to{transform:rotate(270deg)}}
@media(prefers-reduced-motion:reduce){
  /* Respect reduced-motion by slowing the spin, not disabling it — it's the
     only signal that a refresh is still in progress. */
  .ptr-container.ptr-spinning .ptr-ring{animation-duration:2s}
}

/* ── Stat bar list ─────────────────────────────────────────────── */
/* Narrow (phone): name and value on one line, bar underneath.
   Wide (desktop): one line per item with the bar in its own fixed column —
   see the media query below for why. */
.sbar-list{
  display:flex;flex-direction:column;
  max-height:460px;overflow-y:auto;
  -webkit-overflow-scrolling:touch;overscroll-behavior:contain;
  scrollbar-width:thin;
}
.sbar-row{padding:8px 0;flex-shrink:0;position:relative}
.sbar-row:first-child{padding-top:2px}
.sbar-row:last-child{padding-bottom:2px}
.sbar-top{
  display:flex;justify-content:space-between;align-items:baseline;
  gap:14px;margin-bottom:6px;
}
.sbar-name{
  font-size:13px;font-weight:400;color:var(--text);line-height:1.35;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden;min-width:0;
}
.sbar-val{
  font-size:13px;font-weight:600;color:var(--text);
  white-space:nowrap;flex-shrink:0;letter-spacing:-.01em;
  font-variant-numeric:tabular-nums;
}
.sbar-val em{font-style:normal;font-weight:400;color:var(--text2);font-size:12px;margin-left:6px}
.sbar-track{height:8px;border-radius:4px;background:rgba(255,255,255,.07);overflow:hidden}
.sbar-fill{
  position:relative;height:100%;border-radius:4px;min-width:4px;overflow:hidden;
  width:var(--w,0%);
  background:var(--c,var(--blue));
  animation:sbarGrow .65s cubic-bezier(.22,1,.36,1) var(--d,0ms) backwards;
}
@keyframes sbarGrow{from{width:0}}

/* Wide screens: the bar moves into a column of its own.
   Stacked full-width bars meant a 1,150px band per row — comparing two values
   was a long horizontal scan, the top few all read as "full", and each row cost
   ~104px of height. In a fixed column the bars line up directly under one
   another, so relative size is legible at a glance, and a row is 40px. */
@media(min-width:900px){
  .sbar-row{
    display:grid;
    /* Columns 2 and 3 must be deterministic, not `auto`: every row is its own
       grid, so a content-sized column would resolve differently per row and the
       bars would stop lining up — which is the entire point of this layout.
       Percentages so a panel in a two-up grid and a full-width one both work. */
    grid-template-columns:minmax(0,1fr) clamp(110px,22%,300px) clamp(120px,15%,175px);
    align-items:center;gap:18px;
    padding:12px 0;
  }
  .sbar-row + .sbar-row::before{
    content:'';position:absolute;top:0;left:0;right:0;height:1px;
    background:var(--border);
  }
  /* Hoists name and value out of .sbar-top so all three become grid items. */
  .sbar-top{display:contents}
  /* Real ellipsis rather than a clamped box, and the full text stays available
     on hover via the title attribute. */
  .sbar-name{
    order:1;margin:0;display:block;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  }
  .sbar-track{order:2;margin:0}
  .sbar-val{order:3;text-align:right}
}

/* ── iOS primitives ────────────────────────────────────────────── */
/* Three shapes that the phone layout is built out of — a grouped list, a
   progress ring and a segmented control. They're defined outside the
   mobile-mode block because desktop reuses them too (the at-risk panel, the
   table date filters), and because keeping one definition means the two
   layouts can't drift apart.

   Grouped list: label left, value right, hairline separators drawn as an
   absolutely-positioned ::before so they sit between rows without adding to
   the row box (a border-bottom on the last row leaves a stray hairline). */
.ios-list{display:flex;flex-direction:column}
.ios-list-inset{padding:2px 18px 12px}
.ios-row{
  display:flex;align-items:center;gap:14px;
  padding:11px 0;min-height:44px;position:relative;
}
.ios-row + .ios-row::before{
  content:'';position:absolute;top:0;left:0;right:0;height:1px;
  background:var(--border);
}
.ios-row-label{
  flex:1;min-width:0;
  font-size:13px;color:var(--text);line-height:1.35;
}
.ios-row-label small{display:block;margin-top:2px;font-size:11.5px;color:var(--text2)}
.ios-row-value{
  flex-shrink:0;text-align:right;white-space:nowrap;
  font-size:13.5px;font-weight:600;color:var(--text);
  font-variant-numeric:tabular-nums;letter-spacing:-.01em;
}
.ios-row-value em{
  font-style:normal;font-weight:400;color:var(--text2);
  font-size:11.5px;margin-left:5px;
}
.ios-dot{
  display:inline-block;vertical-align:middle;
  width:9px;height:9px;border-radius:50%;margin-right:9px;flex-shrink:0;
}
/* Semantic value tints, shared by lists, cards and totals. */
.tone-good{color:var(--green)}
.tone-warn{color:var(--amber)}
.tone-bad{color:var(--red)}
.tone-info{color:var(--blue)}

/* A list row that also carries a paragraph body and row actions (expenses). */
.ios-row-stacked{
  display:grid;grid-template-columns:1fr auto auto;
  grid-template-areas:"label value btns" "body body body";
  align-items:center;gap:6px 12px;padding:12px 0;
}
.ios-row-stacked .ios-row-label{grid-area:label}
.ios-row-stacked .ios-row-value{grid-area:value}
.ios-row-body{grid-area:body}
.ios-row-body p{font-size:12.5px;color:var(--text2);line-height:1.45}
.ios-row-body p.is-muted{color:var(--text3);font-size:11.5px;margin-top:3px}
.ios-row-stacked .ios-row-btns{grid-area:btns;margin-right:-8px}
.ios-row-btns{display:flex;gap:2px;flex-shrink:0}

/* Bare icon button for row actions — replaces .btn-xs, which carried a border
   and a 28px-tall hit box under every destructive action in the app. */
.icon-btn{
  width:34px;height:34px;flex-shrink:0;
  display:inline-flex;align-items:center;justify-content:center;
  border:none;background:transparent;border-radius:9px;
  color:var(--text2);cursor:pointer;font-family:inherit;
  transition:background .15s,color .15s,transform .12s;
}
.icon-btn svg{display:block}
.icon-btn.is-danger{color:var(--red)}
.icon-btn:active{transform:scale(.88);background:rgba(255,255,255,.09)}
.icon-btn:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
@media(hover:hover){.icon-btn:hover{background:rgba(255,255,255,.07);color:var(--text)}}
@media(pointer:coarse){.icon-btn{width:42px;height:42px}}

/* Progress ring. An inline SVG arc animated via stroke-dashoffset — no canvas,
   no Chart instance, no per-frame JS, and it stays crisp at any DPR. */
.ios-rings{display:flex;justify-content:center;gap:12px;padding:10px 0 16px}
.ios-rings.is-single .ios-ring{flex:0 0 auto}
.ios-ring{flex:1;position:relative;display:flex;flex-direction:column;align-items:center}
.ios-ring svg{display:block;transform:rotate(-90deg)}
.ios-ring-track{stroke:rgba(255,255,255,.09)}
.ios-ring-arc{
  stroke:var(--ring-color,var(--blue));
  stroke-dashoffset:var(--off);
  animation:ringDraw .95s cubic-bezier(.22,1,.36,1) both;
}
@keyframes ringDraw{from{stroke-dashoffset:var(--dash)}}
.ios-ring-value{
  position:absolute;top:calc(var(--ring-size)/2);left:0;right:0;
  transform:translateY(-50%);text-align:center;pointer-events:none;
  font-size:25px;font-weight:650;letter-spacing:-.03em;
  color:var(--text);font-variant-numeric:tabular-nums;
}
.ios-ring-value span{font-size:14px;font-weight:500;color:var(--text2);margin-left:1px}
.ios-ring-label{margin-top:11px;font-size:12.5px;color:var(--text2);text-align:center}

/* Segmented control. Replaces three outline buttons whose selected state was
   written as inline style and therefore silently reset on every re-render. */
.seg{display:inline-flex;gap:2px;padding:2px;background:rgba(255,255,255,.07);border-radius:9px}
.seg-item{
  flex:1;min-width:0;padding:6px 14px;
  border:none;background:transparent;border-radius:7px;
  font-family:inherit;font-size:12px;font-weight:500;color:var(--text2);
  cursor:pointer;white-space:nowrap;
  transition:color .18s,background .18s;
}
.seg-item.is-on{background:var(--card2);color:var(--text);font-weight:600;box-shadow:0 1px 3px rgba(0,0,0,.35)}
.seg-item:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
@media(pointer:coarse){.seg-item{min-height:34px;font-size:13px}}

/* Panel header laid out as a toolbar: title, actions, filters. Replaces four
   anonymous inline-styled divs per header, which wrapped into three ragged
   rows on a phone. */
.ph-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:10px}
.ph-title{display:flex;align-items:center;gap:8px;min-width:0}
.ph-title h2{margin:0;min-width:0}
.ph-title.is-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}
.ph-title .chev-icon{color:var(--text2);flex-shrink:0;transition:color .15s}
@media(hover:hover){.ph-title.is-toggle:hover .chev-icon{color:var(--blue)}}
.ph-count{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:18px;height:18px;padding:0 6px;margin-left:6px;
  border-radius:9px;background:rgba(255,255,255,.09);
  font-size:10px;font-weight:600;color:var(--text2);letter-spacing:0;
}
.ph-filters{margin-left:auto;order:1}
.ph-actions{display:flex;gap:6px;flex-shrink:0;order:2}
/* A panel header carrying a toggle is the control that opens the panel, so it
   needs to look like one — otherwise a collapsed section is a line of text
   floating above nothing. */
.panel-header.ph-toolbar{
  background:var(--card);border-radius:var(--radius);
  padding:12px 16px;margin-bottom:8px;
}
.panel-header.ph-toolbar .ph-title h2{font-size:14px;color:var(--text);font-weight:600}
@media(hover:hover){.panel-header.ph-toolbar .ph-title.is-toggle:hover h2{color:var(--blue)}}

/* The panel wrapper is transparent now, so a left border would draw a stray
   rule beside the title. Tint the card itself instead. */
.panel-danger .panel-body{background:rgba(255,69,58,.10)}
.panel-danger .panel-header h2{color:var(--red)}
.panel-danger .chev-icon{color:var(--red)}

/* Disclosure card — an activity or a BOQ line. Headline figure and a bar are
   always visible; the planning detail expands on tap. grid-template-rows
   animates the reveal without any height measurement in JS, and collapses to
   a true zero so the hidden rows aren't tabbable. */
.act-card{background:var(--card2);border-radius:12px;padding:12px 14px 2px;overflow:hidden}
.act-card-head{
  display:flex;align-items:center;gap:10px;width:100%;min-height:32px;
  padding:0;margin:0;border:none;background:none;
  font-family:inherit;color:inherit;text-align:left;cursor:pointer;
}
.act-card-head:focus-visible{outline:2px solid var(--blue);outline-offset:3px;border-radius:6px}
.act-card-name{flex:1;min-width:0;font-size:14px;font-weight:600;line-height:1.3;letter-spacing:-.01em}
.act-card-pct{
  flex-shrink:0;font-size:14px;font-weight:700;
  letter-spacing:-.02em;font-variant-numeric:tabular-nums;
}
.act-card-chev{display:flex;flex-shrink:0;color:var(--text3);transition:transform .28s cubic-bezier(.32,.72,0,1)}
.act-card.is-open .act-card-chev{transform:rotate(90deg)}
.act-card-bar{margin-top:11px}
.act-card-quick{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:12px 0}
.act-card-quick div{display:flex;flex-direction:column;gap:3px;min-width:0}
.act-card-quick span{font-size:11px;color:var(--text2)}
.act-card-quick b{
  font-size:13.5px;font-weight:600;color:var(--text);
  letter-spacing:-.01em;font-variant-numeric:tabular-nums;
}
.act-card-more{display:grid;grid-template-rows:0fr;transition:grid-template-rows .3s cubic-bezier(.32,.72,0,1)}
.act-card.is-open .act-card-more{grid-template-rows:1fr}
.act-card-more-inner{overflow:hidden;min-height:0}
.act-card-more .ios-list{border-top:1px solid var(--border);padding-bottom:6px}
.act-card-actions{display:flex;justify-content:flex-end;padding:0 0 12px}

/* ── Per-activity schedule ──────────────────────────────────────
   Shown wherever the numbers it drives are read; edited only in Setup, next to
   the quantity and weight it belongs with. A date inherited from the project is
   muted, so which activities carry their own schedule is clear at a glance. */
.act-date{white-space:nowrap}
.act-date.is-inherited{color:var(--text3)}

/* ── Inventory ──────────────────────────────────────────────────
   Stock is read as a direction (came in / went out) before it's read as a
   number, so the type carries a tinted pill rather than a bare word. */
.inv-pill{
  display:inline-flex;align-items:center;gap:4px;
  padding:3px 8px;border-radius:99px;
  font-size:10.5px;font-weight:600;letter-spacing:.01em;white-space:nowrap;
}
.inv-pill.is-in{background:rgba(48,209,88,.14);color:var(--green)}
.inv-pill.is-out{background:rgba(255,159,10,.14);color:var(--amber)}

/* The activity an item's usage is derived from. Deliberately quiet — it's
   provenance, not a headline. */
.inv-link{
  display:flex;align-items:center;gap:4px;margin-top:3px;
  font-size:10.5px;font-weight:500;color:var(--blue);
}
.inv-link svg{flex-shrink:0}

/* Negative stock means more was issued than was ever logged as received. It's a
   real site condition, not an error to clamp away, so it stays visible. */
.inv-neg,.inv-neg b{color:var(--red)!important}

.panel-note{
  padding:10px 18px 0;font-size:11px;line-height:1.5;color:var(--text3);
}
.ph-sub{display:block;font-size:11px;font-weight:400;color:var(--text2);margin-top:2px}

/* Item manager rows */
.inv-edit-row{
  background:var(--card2);border-radius:12px;
  padding:14px 14px 12px;margin-bottom:12px;
}
.inv-edit-actions{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding-top:10px;margin-top:4px;border-top:1px solid var(--border);
}
.inv-edit-meta{margin-right:auto;font-size:11px;color:var(--text2);font-variant-numeric:tabular-nums}
.inv-edit-meta b{color:var(--text)}

.inv-boq-import{
  background:var(--card2);border-radius:12px;padding:14px;margin-top:4px;
}
.inv-boq-head{display:flex;align-items:center;gap:10px;margin-bottom:4px}
.inv-boq-head h4{margin:0;font-size:13px;font-weight:600;margin-right:auto}
.inv-boq-head > div{display:flex;gap:6px}
.inv-boq-note{margin:0 0 10px;font-size:11px;color:var(--text3);line-height:1.5}
.inv-boq-list{max-height:260px;overflow-y:auto;overscroll-behavior:contain;margin-bottom:12px}
.inv-boq-pick{
  display:flex;align-items:center;gap:10px;
  padding:9px 4px;font-size:12.5px;cursor:pointer;
  border-bottom:1px solid var(--border);
}
.inv-boq-pick:last-child{border-bottom:none}
.inv-boq-pick span{flex:1;min-width:0}
.inv-boq-pick em{
  font-style:normal;font-size:11px;color:var(--text2);
  white-space:nowrap;font-variant-numeric:tabular-nums;
}
.inv-boq-pick input{flex-shrink:0;width:16px;height:16px;accent-color:var(--blue)}

.modal-hint{font-size:11.5px;line-height:1.5;color:var(--text2);margin-top:8px}
.modal-hint.is-warn{color:var(--amber)}
.modal-hint b{color:var(--text)}
.modal-hint .btn{margin-left:6px;vertical-align:middle}

/* Grand totals under the BOQ card list — the phone equivalent of the table's
   TOTAL row, which was off-screen to the right of a nine-column grid. */
.boq-totals{
  display:grid;grid-template-columns:repeat(3,1fr);gap:10px;
  margin:0 14px 14px;padding:14px;
  background:var(--card2);border-radius:12px;
}
.boq-totals div{display:flex;flex-direction:column;gap:3px;min-width:0}
.boq-totals span{font-size:11px;color:var(--text2)}
.boq-totals b{font-size:14px;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}

/* ── Mobile mode ───────────────────────────────────────────────── */
/* Everything below only activates once JS (mobile.js) determines this is
   an actual touch device at phone width (body.mobile-mode) — a real phone
   gets a native-app-style shell (bottom tab bar, bottom-sheet modals,
   stacked cards instead of wide tables); a resized desktop window doesn't. */

.mobile-tabbar{display:none}
.mobile-menu-backdrop{display:none}

/* The menu opens from the topbar on phones too, so the bottom bar is left to
   the three destinations (Progress / Costing / Log). The sheet it opens still
   slides up from the bottom, which keeps it in thumb reach. */
body.mobile-mode .nav-toggle{display:inline-flex;margin-left:0}
/* Daily Log lives in the bottom bar; no need to repeat it up top. */
body.mobile-mode .topbar-primary{display:none}
body.mobile-mode .view-tabbar{display:none}
body.mobile-mode .main{padding-bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 20px)}

/* Bottom tab bar (replaces the top Progress/Costing tabs + hamburger).
   z-index must stay BELOW .modal-overlay (200): bottom-sheet modals sit flush
   against the bottom edge, so at 220 the tab bar covered their Save/Cancel
   footer and stayed tappable through the scrim. */
body.mobile-mode .mobile-tabbar{
  display:flex;position:fixed;left:0;right:0;bottom:0;z-index:180;
  background:rgba(7,15,32,.92);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  border-top:1px solid var(--border2);
  padding:6px max(8px,env(safe-area-inset-left)) max(6px,env(safe-area-inset-bottom));
  box-shadow:0 -8px 24px rgba(0,0,0,.35);
}
.mtab{
  flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:7px 4px;background:none;border:none;color:var(--text2);
  font-family:inherit;cursor:pointer;border-radius:var(--radius-sm);
  transition:color .15s;min-height:44px;
}
.mtab-icon{display:flex;line-height:1;transition:transform .2s cubic-bezier(.34,1.56,.64,1)}
.mtab-label{font-size:10px;font-weight:500;letter-spacing:0}
.mtab.active{color:var(--blue)}
.mtab.active .mtab-icon{transform:translateY(-1px) scale(1.1)}
.mtab:active .mtab-icon{transform:scale(.88)}

/* Slide-up menu sheet — reuses the existing #topbarRight drawer /
   toggleNav(), just presented as a bottom sheet instead of a dropdown.
   #topbarRight is a flex item of .topbar (align-items:center); some
   browsers resolve position:fixed's auto top/bottom offsets using the
   item's hypothetical flex static-position instead of the viewport, which
   put a naively-positioned sheet in the wrong place. To sidestep that
   entirely, the outer #topbarRight becomes an unambiguous full-viewport
   shell (inset:0 — no auto offsets anywhere) that's click-through except
   where the actual sheet is; the new inner .topbar-right-sheet div is the
   visually-styled, bottom-anchored sheet, positioned by plain flex
   justify-content and animated with its own transform. */
/* Full-width panel that drops DOWN from under the topbar, matching where the
   hamburger that opens it lives. The shell is anchored just below the bar and
   clips its child, so the panel slides in from behind the topbar rather than
   appearing over it. width/border-radius must be reset explicitly: the desktop
   dropdown rule above sets a fixed 240px width and rounded card styling, and
   anchoring alone doesn't undo them. */
body.mobile-mode .has-drawer .topbar-right{
  position:fixed;
  top:calc(var(--topbar-h) + env(safe-area-inset-top));
  left:0;right:0;bottom:auto;
  z-index:230;max-height:none;
  width:auto;border-radius:0;
  overflow:hidden;                    /* clips the panel while it's off-screen */
  display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;
  pointer-events:none;
  background:none;border:none;box-shadow:none;padding:0;gap:0;
}
/* .topbar's backdrop-filter creates a new containing block for this
   position:fixed shell in some browser engines (treating backdrop-filter
   like filter, which is spec'd to do this) — that pinned the "fixed" sheet
   to the ~64px topbar instead of the viewport. Mobile-mode's topbar is
   compact anyway, so drop the blur there and use a solid backdrop instead. */
body.mobile-mode .topbar{
  backdrop-filter:none;-webkit-backdrop-filter:none;
  /* Fully opaque: without blur, even 98% alpha lets bright content (large
     amber % values) ghost through the bar while scrolling.
     Must be the token, not a literal — this was hardcoded to the desktop navy,
     so the bar stayed blue while the rest of the phone UI went black. */
  background:var(--bg);
}
/* Live backdrop-filter blur is by far the most expensive thing to composite on
   a phone: every blurred element re-samples the pixels behind it each frame, so
   a page of blurred panels/cards over a scrolling viewport (plus the sticky
   blurred tab bar) repaints the whole screen on every scroll tick. Phones don't
   benefit from the frosted-glass look enough to justify that — swap the
   translucent + blur combo for solid fills in mobile-mode so scrolling composites
   cheaply. (Desktop keeps the glass effect.) */
body.mobile-mode .panel,
body.mobile-mode .kpi-card,
body.mobile-mode .view-tabbar,
body.mobile-mode .btn-outline,
body.mobile-mode .toast,
body.mobile-mode .modal,
body.mobile-mode .import-banner{
  backdrop-filter:none;-webkit-backdrop-filter:none;
}
/* .panel is deliberately NOT in this list. It's a transparent wrapper — the
   rounded card is .panel-body, which is already a solid var(--card) and carries
   no backdrop-filter, so there's nothing here to swap out. This rule dates from
   when the panel itself was the glass card; after the grouped-list restructure
   it kept painting an un-rounded block behind the rounded body, which is where
   the sharp corners on every mobile tile came from. */
body.mobile-mode .kpi-card,
body.mobile-mode .import-banner{background:var(--card)}
body.mobile-mode .toast{background:var(--card2)}
/* Table headers only appear inside sheets on a phone (Edit BOQ, the bulk
   grids). Both were pinned to the desktop navy. */
body.mobile-mode th,
body.mobile-mode .edit-tbl th{background:var(--surface);backdrop-filter:none}
body.mobile-mode .has-drawer .topbar-right-sheet{
  pointer-events:auto;
  display:flex;flex-direction:column;align-items:stretch;gap:8px;
  max-height:75dvh;overflow-y:auto;
  background:var(--surface);
  border-bottom:1px solid var(--border2);
  border-radius:0 0 20px 20px;
  box-shadow:0 16px 48px rgba(0,0,0,.5);
  padding:10px clamp(16px,4vw,24px) 14px;
  transform:translateY(-100%);
  transition:transform .3s cubic-bezier(.32,.72,0,1);
}
body.mobile-mode .has-drawer .topbar-right.open .topbar-right-sheet{transform:translateY(0)}
/* Starts below the topbar so the bar — and the hamburger that toggles the menu —
   stay crisp and tappable while it's open, as a dropdown should. */
.mobile-menu-backdrop{
  position:fixed;
  top:calc(var(--topbar-h) + env(safe-area-inset-top));
  left:0;right:0;bottom:0;z-index:225;
  background:rgba(0,0,0,.5);
  opacity:0;pointer-events:none;
  transition:opacity .25s ease;
}
.mobile-menu-backdrop.show{opacity:1;pointer-events:auto}

/* Full-screen bottom-sheet modals with a drag handle (see
   initMobileSheetDrag() in animations.js for the drag-to-dismiss gesture) */
body.mobile-mode .modal-overlay{align-items:flex-end;padding:0}
body.mobile-mode .modal{
  max-width:100%;width:100%;
  max-height:92dvh;
  border-radius:20px 20px 0 0;
}
body.mobile-mode .modal::before{
  content:'';display:block;flex-shrink:0;
  width:36px;height:4px;margin:8px auto 0;
  background:rgba(255,255,255,.22);border-radius:3px;
}
body.mobile-mode .modal-header{cursor:grab;touch-action:none}

/* Container for the phone's stacked lists (Daily Log, Expense History,
   Activity Detail, BOQ). The rows themselves are .ios-list / .act-card now —
   the old .mobile-card-* set (bordered box, uppercase micro-labels, a 2×N
   field grid) has no remaining callers and was removed with them. */
.mobile-card-list{display:flex;flex-direction:column;gap:10px;padding:14px}
.mobile-card-empty{text-align:center;color:var(--text2);padding:24px 12px;font-size:12px}

/* Taller chart tiles on phones — more room per bar/point so scrolling inside
   the tile (chart-scroll-y/-x) is comfortable instead of everything being
   squeezed into a short viewport. */
body.mobile-mode .chart-scroll-y,
body.mobile-mode .chart-scroll-x{height:460px}
body.mobile-mode .chart-wrap-tall{height:420px}

/* Hide the bottom tab bar entirely while a modal is open — belt and braces
   alongside the z-index fix, and it stops the tabs stealing taps meant for a
   bottom sheet's footer. Applied via a body class set in animations.js so it
   works without :has() support. */
body.mobile-mode.modal-open .mobile-tabbar{display:none}
/* The overflow menu drops down from the topbar and doesn't reach the tab bar, so
   the bar stays put (dimmed under the backdrop) rather than disappearing. */

/* Keep toasts clear of the bottom tab bar on phones */
body.mobile-mode .toast{bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 12px)}

/* ══ Phone presentation ═════════════════════════════════════════ */
/* The palette, typography, grouped panels and flat controls are now shared
   base styles — both platforms use one design system. What remains here is
   only what genuinely differs on a phone: a true-black background, tighter
   chrome, the bottom tab bar, bottom-sheet modals and the slide-down menu. */

body.mobile-mode{
  /* True black rather than the desktop's near-black: on an OLED phone held
     close it reads cleaner, and there's no large bright surface to fatigue. */
  --bg:#000000;
  --surface:#0c0c0e;
  --topbar-h:54px;
  --tabbar-h:60px;
  font-size:15px;
}

/* ── Grouped sections ──────────────────────────────────────────── */
/* The grouped-list presentation itself is now shared; only the phone's tighter
   insets remain here. */
body.mobile-mode .main{gap:22px;padding-top:18px}
body.mobile-mode .panel-header{padding:0 6px 8px}
body.mobile-mode .panel-body{padding:2px 16px}

/* Toolbar headers: title and actions share a line, the segmented filter takes
   its own full-width line underneath. Previously these wrapped into three
   ragged rows of pills.
   These three headers are also the collapsible ones, so they become their own
   tappable card — a bare title floating above a collapsed (display:none) body
   read as a stray line of text with no affordance. */
body.mobile-mode .ph-toolbar{gap:8px;padding:11px 14px}
body.mobile-mode .ph-toolbar:active{background:var(--card2)}
/* Title + two labelled buttons don't fit one 343pt line, and letting them wrap
   put the actions on a half-empty row of their own. Toolbar icons instead —
   both keep their title/aria-label, and both actions are reachable elsewhere
   (the Log tab, the bulk sheet). */
body.mobile-mode .ph-actions .btn span{display:none}
body.mobile-mode .ph-actions .btn{padding:0;width:38px;height:38px;min-height:38px;justify-content:center}
body.mobile-mode .ph-title{flex:1 1 auto;order:0}
body.mobile-mode .ph-actions{order:1;margin-left:auto}
body.mobile-mode .ph-filters{order:2;flex:1 0 100%;margin-left:0}
body.mobile-mode .seg{width:100%}

/* ── KPI grid ──────────────────────────────────────────────────── */
/* Five full-width cards stacked was ~1,000px of scrolling before any content.
   Two columns with the headline metric spanning both is the iOS widget grid. */
body.mobile-mode .kpi-row{grid-template-columns:1fr 1fr;gap:10px}
body.mobile-mode .kpi-card{
  background:var(--card);border:none;box-shadow:none;
  border-radius:var(--radius);padding:14px 15px 13px;
}
body.mobile-mode .kpi-card:hover{transform:none;box-shadow:none}
body.mobile-mode .kpi-icon{display:none}
body.mobile-mode .kpi-label{
  font-size:12px;font-weight:500;color:var(--text2);margin-bottom:7px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* The accent moves from a decorative top bar onto the number itself, the way
   Health tints its metrics — same information, one less ornament. */
body.mobile-mode .kpi-value{
  font-size:24px;font-weight:650;letter-spacing:-.03em;
  color:var(--accent,var(--text));font-variant-numeric:tabular-nums;
}
body.mobile-mode .kpi-sub{
  font-size:11.5px;margin-top:5px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
body.mobile-mode .kpi-row > .kpi-card:first-child{grid-column:1/-1;padding:17px 18px 16px}
body.mobile-mode .kpi-row > .kpi-card:first-child .kpi-value{font-size:40px}
body.mobile-mode .kpi-row > .kpi-card:first-child .kpi-sub{font-size:12.5px}
/* The hero spans both columns, so an even number of cards leaves the last one
   orphaned beside a hole. Let it span instead of stranding it. */
body.mobile-mode .kpi-row > .kpi-card:last-child:nth-child(even){grid-column:1/-1}

/* ── Bars & lists ──────────────────────────────────────────────── */
/* A capped, independently-scrolling list inside a scrolling page is a phone
   anti-pattern — with overscroll-behavior:contain, a swipe that began on a bar
   list stopped dead at the end of the list instead of carrying on down the
   page. Let the list run its natural height and let the page do the scrolling. */
body.mobile-mode .sbar-list{max-height:none;overflow:visible}
body.mobile-mode .sbar-row{padding:9px 0}
body.mobile-mode .sbar-name{font-size:13.5px}
body.mobile-mode .sbar-val{font-size:13.5px}
body.mobile-mode .sbar-track{height:10px;border-radius:5px}
body.mobile-mode .sbar-fill{border-radius:5px}
body.mobile-mode .mobile-card-list{padding:14px;gap:10px}
body.mobile-mode .mobile-card-empty{padding:28px 16px;font-size:13.5px;color:var(--text2)}
body.mobile-mode .ios-list-inset{padding:2px 16px 10px}

/* ── Chrome ────────────────────────────────────────────────────── */
body.mobile-mode .topbar{gap:10px;border-bottom:1px solid var(--border)}
body.mobile-mode .logo{padding:0}
body.mobile-mode .logo img{height:24px}
body.mobile-mode .proj-switcher-face{font-size:17px;font-weight:650;letter-spacing:-.022em;padding:2px 6px 2px 0}
body.mobile-mode .topbar-title p{display:none}
body.mobile-mode .nav-toggle{
  width:40px;height:40px;background:transparent;border:none;color:var(--blue);
}
body.mobile-mode .nav-toggle:active{opacity:.5}

/* Project popover: a translucent iOS context menu. Safe to blur here — it's a
   small transient surface, unlike the full-page panels blurred on desktop. */
body.mobile-mode .proj-menu{
  background:rgba(44,44,46,.94);
  backdrop-filter:blur(30px) saturate(180%);
  -webkit-backdrop-filter:blur(30px) saturate(180%);
  border:none;border-radius:14px;padding:6px;
  box-shadow:0 16px 44px rgba(0,0,0,.6);
}
body.mobile-mode .proj-menu-item{min-height:50px;border-radius:10px}
body.mobile-mode .proj-menu-name{font-size:15px}
body.mobile-mode .proj-menu-sub{font-size:12.5px}
body.mobile-mode .proj-menu-pct{font-size:13px}

/* Bottom tab bar — the one place translucency is worth its cost, and the most
   recognisable iOS element in the app.

   Worth its cost, but the cost is real and it is paid per frame: this is a
   fixed bar, so the content behind it changes on every scroll and the blur is
   recomputed each time. The radius came down from 28px to 14px and the
   background went more opaque to compensate — blur expense scales with radius,
   and at this size nobody can tell 14 from 28 through an 88%-opaque bar. The
   effect stays; roughly half the per-frame work goes.
   (The topbar took the harder version of this trade and dropped blur entirely —
   see body.mobile-mode .topbar above.) */
body.mobile-mode .mobile-tabbar{
  background:rgba(22,22,24,.88);
  backdrop-filter:blur(14px) saturate(160%);
  -webkit-backdrop-filter:blur(14px) saturate(160%);
  border-top:1px solid rgba(255,255,255,.13);
  box-shadow:none;
  padding:5px max(8px,env(safe-area-inset-left)) max(4px,env(safe-area-inset-bottom));
}
body.mobile-mode .mtab{gap:3px;padding:6px 4px;transition:opacity .15s}
body.mobile-mode .mtab.active .mtab-label{font-weight:600}
/* iOS tints the selected tab, it doesn't grow it. */
body.mobile-mode .mtab.active .mtab-icon{transform:none}
body.mobile-mode .mtab:active{opacity:.45}
body.mobile-mode .mtab:active .mtab-icon{transform:scale(.92)}

/* Overflow sheet: a list of destinations, not a stack of pills. */
body.mobile-mode .has-drawer .topbar-right-sheet{
  background:var(--surface);border-radius:0 0 16px 16px;
  padding:8px 12px 14px;gap:1px;
}
body.mobile-mode .has-drawer .topbar-right .btn{
  min-height:48px;padding:0 12px;gap:13px;
  font-size:15px;font-weight:400;border-radius:10px;
  background:transparent;color:var(--text);justify-content:flex-start;
}
body.mobile-mode .has-drawer .topbar-right .btn:active{background:rgba(255,255,255,.09);transform:none}
body.mobile-mode .has-drawer .topbar-right .btn svg{width:19px;height:19px;color:var(--blue)}
body.mobile-mode .has-drawer .topbar-right form .btn{color:var(--red)}
body.mobile-mode .has-drawer .topbar-right form .btn svg{color:var(--red)}

/* ── Sheets ────────────────────────────────────────────────────── */
/* Buttons, badges, the import banner and form fields are shared base styles
   now; only the bottom-sheet presentation is phone-specific. */
body.mobile-mode .modal{border-radius:14px 14px 0 0;box-shadow:0 -10px 44px rgba(0,0,0,.65)}
body.mobile-mode .modal::before{background:rgba(255,255,255,.28);margin-top:7px}
body.mobile-mode .modal-header{padding:8px 20px 14px}
body.mobile-mode .modal-header h3{font-size:17px}
body.mobile-mode .modal-footer{padding:12px 16px calc(12px + env(safe-area-inset-bottom))}
body.mobile-mode .modal-footer .btn{flex:1;justify-content:center;min-height:47px}

/* ── Remaining canvas ──────────────────────────────────────────── */
/* Expense-vs-progress is the only chart still drawn on a canvas here: a time
   series genuinely needs one. Everything else is HTML/SVG now. */
body.mobile-mode .chart-scroll-x{height:300px}

/* Scrubbable chart (phone). The readout sits above the plot rather than
   floating over it as a tooltip: it's always visible, never covers the data,
   and needs no aiming — drag anywhere and it follows your finger. */
.chart-scrub{padding-top:4px}
.chart-scrub-readout{min-height:64px;padding:2px 2px 10px}
.chart-scrub-date{font-size:12px;color:var(--text2);margin-bottom:7px}
.chart-scrub-date em{
  font-style:normal;font-size:10px;font-weight:600;color:var(--text3);
  border:1px solid var(--border2);border-radius:5px;padding:1px 5px;margin-left:6px;
  vertical-align:1px;
}
.chart-scrub-vals{display:flex;flex-wrap:wrap;gap:6px 20px}
.chart-scrub-vals span{
  display:inline-flex;align-items:baseline;gap:7px;
  font-size:12.5px;color:var(--text2);
}
.chart-scrub-vals i{
  width:9px;height:9px;border-radius:50%;flex-shrink:0;
  align-self:center;
}
.chart-scrub-vals b{
  font-size:17px;font-weight:650;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums;
}
.chart-scrub-vals em{font-style:normal;font-size:11.5px;color:var(--text3)}
.chart-scrub-plot{position:relative;height:196px}
/* pan-y is load-bearing: vertical page scroll passes straight through, while
   horizontal drags reach the canvas as pointer events and drive the scrub. */
.chart-scrub-plot canvas{touch-action:pan-y}
.chart-scrub-range{padding:12px 0 2px}
.chart-scrub-range .seg{width:100%}

/* The one line most people actually read. */
.chart-verdict{
  display:flex;align-items:center;gap:8px;
  margin-top:10px;padding:12px 14px;
  background:var(--card);border-radius:var(--radius);
  font-size:12.5px;line-height:1.4;color:var(--text2);
}
.chart-verdict svg{flex-shrink:0}
.chart-verdict.tone-good{color:var(--green)}
.chart-verdict.tone-warn{color:var(--amber)}
.chart-verdict.tone-bad{color:var(--red)}
.chart-verdict.tone-info{color:var(--blue)}

/* ── Toast ─────────────────────────────────────────────────────── */
body.mobile-mode .toast{
  left:max(12px,env(safe-area-inset-left));
  right:max(12px,env(safe-area-inset-right));
  max-width:none;border:none;border-radius:14px;
  background:rgba(44,44,46,.97);
  font-size:14px;padding:14px 16px;
}

/* ── Busy / invalid states ─────────────────────────────────────── */
/* A button running an async action: dimmed, non-interactive, with a spinner in
   place of its icon. Prevents the double-tap-creates-duplicates problem. */
.btn.is-busy{opacity:.65;pointer-events:none;position:relative}
.btn.is-busy > *{visibility:hidden}
.btn.is-busy::after{
  content:'';position:absolute;top:50%;left:50%;
  width:14px;height:14px;margin:-7px 0 0 -7px;
  border:2px solid currentColor;border-right-color:transparent;
  border-radius:50%;
  animation:btnSpin .6s linear infinite;
}
@keyframes btnSpin{to{transform:rotate(360deg)}}
button:disabled{cursor:not-allowed}

/* A started-but-incomplete row in a bulk-entry grid — makes it obvious which
   rows will be skipped on save instead of silently dropping them. */
.edit-tbl tr.row-invalid td{background:rgba(255,69,58,.08)}
.edit-tbl tr.row-invalid td:first-child{box-shadow:inset 2px 0 0 var(--red)}

/* ── Reduced motion ────────────────────────────────────────────── */
/* Respect the OS "reduce motion" accessibility setting: kill decorative
   animation and transition movement while keeping instant state changes. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.01ms !important;
  }
  /* The busy spinner is the only signal that work is in flight — slow it
     rather than freezing it. */
  .btn.is-busy::after{animation-duration:1.6s !important;animation-iteration-count:infinite !important}
}

/* An activity with more logged against it than its target quantity. Called out
   on the row rather than left to be inferred from a percentage that has been
   quietly clamped to 100 — the clamp keeps the maths honest, this says why. */
.act-overlogged{
  display:inline-block;vertical-align:middle;
  margin-left:6px;padding:1px 6px;
  border:1px solid rgba(255,159,10,.42);
  border-radius:999px;
  background:rgba(255,159,10,.14);
  color:var(--amber);
  font-size:9.5px;font-weight:650;letter-spacing:.02em;text-transform:uppercase;
  white-space:nowrap;cursor:help;
}

/* ── Log picker ────────────────────────────────────────────────── */
/* The Daily Log button opens a choice of work / expense / stock rather than
   going straight to the work-log form. Reuses .proj-menu wholesale — same card,
   same rows, same open/close transition — so the topbar has one popover design
   rather than two that drift apart.

   position:fixed in both layouts. The menu is anchored to whichever trigger is
   on screen, and on a desktop that trigger's position is only knowable at open
   time, so toggleLogMenu() sets top/right from its rect. */
.log-picker-chev{margin-left:1px;opacity:.85}
.log-menu{
  position:fixed;top:auto;left:auto;right:auto;
  min-width:min(272px,calc(100vw - 32px));
  transform-origin:top right;
}
.log-menu.open{
  opacity:1;transform:scale(1) translateY(0);
  pointer-events:auto;visibility:visible;
  transition:opacity .16s ease,transform .24s cubic-bezier(.32,.9,.36,1.06),visibility 0s;
}
/* The icon sits in the checkmark gutter .proj-menu-item already reserves, which
   is why these rows line up with the project list's. */
.log-menu-icon{width:22px;color:var(--green)}

/* On a phone the trigger is the Log tab at the BOTTOM of the screen, so the menu
   rises from there instead of dropping from the topbar. */
body.mobile-mode .log-menu{
  top:auto;
  bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom) + 10px);
  left:max(12px,env(safe-area-inset-left));
  right:max(12px,env(safe-area-inset-right));
  min-width:0;max-width:none;
  transform-origin:bottom center;
  transform:scale(.94) translateY(6px);
}
body.mobile-mode .log-menu.open{transform:scale(1) translateY(0)}

/* ── BOQ: dollar pricing for imported lines ────────────────────── */
/* The rate is not one of the percentages and is not styled as one. Those
   compound a taka figure; this decides what the taka figure is, and moving it
   re-prices every dollar-priced line at once. */
.boq-rate{
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;
  padding:10px 12px;margin-bottom:10px;
  background:rgba(10,132,255,.07);
  border:1px solid var(--border-accent);
  border-radius:var(--radius-sm);
}
.boq-rate > label{font-size:11.5px;font-weight:650;color:var(--text);white-space:nowrap}
.boq-rate-input{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--text2)}
.boq-rate-input input{
  width:110px;padding:7px 9px;
  background:var(--surface);border:1px solid var(--border2);border-radius:7px;
  color:var(--text);font-family:inherit;font-size:12.5px;font-variant-numeric:tabular-nums;
}
.boq-rate-input input:focus{outline:none;border-color:var(--blue);background:var(--blue-glow2)}
.boq-rate-note{
  flex:1 1 220px;min-width:0;margin:0;
  font-size:11px;color:var(--text2);line-height:1.45;
}
/* The dollar figure sits under the taka one in the same cell. A second set of
   five money columns would not fit on a laptop, and reading a row would mean
   scrolling sideways past the numbers being compared. */
.boq-usd{
  display:block;
  font-size:10.5px;color:var(--text3);
  font-variant-numeric:tabular-nums;line-height:1.3;
}

/* ── Costing: BOQ line items, one table per pricing chain ──────── */
/* Foreign and local are priced differently now, so they are reported in
   separate tables rather than as sections of one — a shared header row would
   have to name stages that only one of them has. */
.boq-view-group + .boq-view-group{margin-top:6px;border-top:1px solid var(--border)}
.boq-view-head{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  padding:11px 14px 9px;
  font-size:12px;font-weight:650;color:var(--text);
}
.boq-view-head svg{opacity:.75;vertical-align:-2px}
.boq-view-share{margin-left:auto;font-size:11px;font-weight:500;color:var(--text2)}
.boq-totals-wide{flex-wrap:wrap}

/* ── Edit BOQ table: legible values and usable spinners ─────────── */
/* The row inputs were pinned to fixed pixel widths, so a real BOQ truncated:
   a 6-digit quantity in a 65px box read "334360" as "33436(", and a 7-digit
   taka price lost its last digit. Widths become MINIMUMS, and field-sizing lets
   a box grow to whatever it is holding where the browser supports it — the
   table already scrolls sideways, so growing costs nothing but a wider scroll.
   The minimums alone are enough on browsers without field-sizing. */
.edit-tbl .bq-sl,
.edit-tbl .bq-desc,
.edit-tbl .bq-unit,
.edit-tbl .bq-qty,
.edit-tbl .bq-pct,
.edit-tbl .bq-money{
  width:100%;
  field-sizing:content;
}
.edit-tbl .bq-sl   {min-width:46px}
.edit-tbl .bq-desc {min-width:210px}
.edit-tbl .bq-unit {min-width:58px}
.edit-tbl .bq-qty  {min-width:92px}
.edit-tbl .bq-pct  {min-width:78px}
.edit-tbl .bq-money{min-width:118px}

/* A computed cell can be longer than the input above it — 44,592,967.25 with a
   dollar figure under it — and was the other half of the truncation. */
.edit-tbl .boq-computed{min-width:104px}

/* Chrome and Safari hide the number spinner until the pointer is over the box,
   and draw it at about 12px of low-contrast grey when they do. These are the
   controls the request was about, so they are always on, full height, and
   carry a border that makes them read as a control rather than a smudge. */
.edit-tbl input[type=number]{padding-right:2px}
.edit-tbl input[type=number]::-webkit-inner-spin-button{
  -webkit-appearance:inner-spin-button;
  opacity:1;
  height:26px;
  width:16px;
  margin-left:4px;
  cursor:pointer;
  border-left:1px solid var(--border2);
  background:var(--surface);
  border-radius:0 5px 5px 0;
}
.edit-tbl input[type=number]:hover::-webkit-inner-spin-button,
.edit-tbl input[type=number]:focus::-webkit-inner-spin-button{
  background:var(--blue-glow2);
  border-left-color:var(--border-accent);
}
/* Firefox hides them entirely unless asked. */
.edit-tbl input[type=number]{-moz-appearance:number-input}

/* Money columns read down the page, so the figures line up on the right and the
   digits are the same width. The subtotal row was already right-aligned while
   the rows above it were left-aligned, which is what made a column of numbers
   look like it did not belong to the total under it. */
.edit-tbl .bq-num{text-align:right}
.edit-tbl .boq-computed{text-align:right;font-variant-numeric:tabular-nums}
.edit-tbl .boq-usd{text-align:right}
/* A body figure sits inside a padded span inside a padded cell — 3px + 7px. The
   subtotal is bare text in a cell padded 7px, so every total sat 3px left of the
   column it belonged to. Match the inset so they cannot drift apart again. */
.boq-group .edit-tbl tfoot td{padding-left:10px;padding-right:10px;font-variant-numeric:tabular-nums}
