/* styles-tools.css -- one of 2 files split out of the former styles.css
   (2026-08-20, structural item #43). See the header of styles.css itself
   for the full explanation of why and how this split was done safely.

   This file: everything the internal Workspace tool suite needs beyond
   the shared tokens/resets in styles.css (which this file still depends
   on -- loaded AFTER styles.css on every tool page, exactly matching
   this content's original position at the end of the single combined
   file). Not used by the public-facing pages.
*/

/* Item #65: cross-document view transitions (2026-08-26), requested
   directly as part of a broader "make it feel like an app" pass --
   navigating between tool pages is a series of real, separate page
   loads (this isn't an SPA), which is exactly the case this CSS
   at-rule was built for. Pure CSS, no JavaScript needed for the
   default cross-fade -- both the source and destination page need
   this same rule for a transition to actually happen between them,
   which is automatically true here since every tool page loads this
   shared file. Genuinely safe regardless of browser support gaps: a
   browser that doesn't understand this at-rule just ignores it and
   falls back to a normal, instant navigation -- true progressive
   enhancement, not something that could break anything anywhere. */
@view-transition {
  navigation: auto;
}

/* ==========================================================================
   INTERNAL TOOLS SUITE — shared styles for /workspace.html and its 7 tools.
   Not used by the public-facing pages (index.html + 5 landing pages).
   Scoped under distinct classnames (tool-header, help-*) so nothing here
   overlaps or overrides public-site styles above.
   ========================================================================== */

/* Bug fix (2026-09-06): .tool-title/.tool-sub are used as the page
   intro (and, on several pages, the role-blocked-overlay message) on
   13 tool pages, but had never once been defined here -- 7 pages each
   carried their own identical copy in a local <style> block, and 6
   more used the classes with no CSS backing them at all, rendering as
   bare unstyled text. On pos.html and clients.html specifically that
   meant the actual page heading, and on clients.html the ~20 dynamic
   loading/empty/error rows that reuse .tool-sub throughout the page.
   Defined once, here, so every current and future tool page gets it
   for free. A handful of pages still carry their own local override
   for a taller .tool-sub margin (30px instead of 24px) -- that's a
   deliberate per-page choice, not a duplicate to clean up, and it
   still wins by normal cascade order (their <style> block loads after
   this stylesheet). */
.tool-title{
  font-family:var(--font-ui);
  font-weight:700;
  font-size:28px;
  color:var(--white);
  margin-bottom:6px;
}
.tool-sub{
  color:var(--text-dim);
  margin-bottom:24px;
}

/* Inline hints nested inside a <label>.

   styles.css (the public-site stylesheet, which every tool page also
   loads) carries a bare `label{ text-transform:uppercase; letter-spacing:1px; }`
   written for the contact form. Both of those properties are inherited,
   so any <span> nested inside a label picks them up -- which turned the
   explanatory parentheticals here into shouty, letter-spaced all-caps
   sentences up to ~100 characters long ("(OPTIONAL -- LETS THIS CLIENT
   VIEW AND PAY THIS INVOICE ONLINE)").

   The labels themselves staying uppercase is a deliberate, consistent
   design choice across the suite, so this reset is scoped to the nested
   hint spans only: they're body copy and belong in sentence case. */
label .tool-sub,
label .col-hint,
.label-hint{
  text-transform:none;
  letter-spacing:normal;
}

/* Canonical inline label hint -- the parenthetical that trails a field
   label. Previously hand-rolled as an inline style on each occurrence. */
.label-hint{
  font-weight:400;
  color:var(--text-dim);
}

.tool-header{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:16px;
  margin-bottom:18px;
}
.tool-header-brand{
  display:flex;
  align-items:center;
  gap:10px;
  text-decoration:none;
  color:var(--blue-light);
}
.tool-header-brand img{
  height:32px;
  width:32px;
  object-fit:contain;
  flex-shrink:0;
}
.tool-header-brand span{
  font-family:var(--font-ui);
  font-size:14px;
  font-weight:600;
}
.tool-header-brand:hover span{ color:var(--orange-light); }

.help-btn{
  flex-shrink:0;
  width:30px;
  height:30px;
  border-radius:50%;
  border:1px solid var(--border);
  background:var(--bg-panel-2);
  color:var(--text-dim);
  font-family:var(--font-ui);
  font-weight:700;
  font-size:14px;
  cursor:pointer;
  display:flex;
  align-items:center;
  justify-content:center;
  transition:border-color .15s ease, color .15s ease;
}
.help-btn:hover{ border-color:var(--orange-light); color:var(--orange-light); }
/* 2026-08-16: header row (help / notifications / sign-out) had zero
   visual hierarchy -- three identical circles with no signal for which
   is used most. Sign-out is the rarest of the three, so it steps back
   rather than competing evenly with help and notifications. */
.help-btn.is-muted { opacity: .55; }
.help-btn.is-muted:hover { opacity: 1; }
.help-btn.is-active{ border-color:var(--blue-light); color:var(--blue-light); background:rgba(58,160,255,0.12); }
.help-btn.is-syncing { animation: helpBtnSpin 0.8s linear infinite; }
@keyframes helpBtnSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .help-btn.is-syncing { animation: none; opacity: 0.5; } }

.help-modal-overlay{
  display:none;
  position:fixed;
  inset:0;
  background:rgba(0,0,0,0.6);
  z-index:200;
  align-items:center;
  justify-content:center;
  padding:20px;
}
.help-modal-overlay.is-open{ display:flex; animation: helpOverlayFadeIn .15s ease; }
@keyframes helpOverlayFadeIn{ from{ background:rgba(0,0,0,0); } to{ background:rgba(0,0,0,0.6); } }
.help-modal-overlay.is-open .help-modal{ animation: helpModalPopIn .18s ease; }
@keyframes helpModalPopIn{ from{ opacity:0; transform:scale(0.95) translateY(8px); } to{ opacity:1; transform:scale(1) translateY(0); } }
@media (prefers-reduced-motion: reduce){ .help-modal-overlay.is-open, .help-modal-overlay.is-open .help-modal{ animation:none; } }
.help-modal{
  position:relative;
  background:var(--bg-panel);
  border:1px solid var(--border);
  border-radius:var(--radius);
  max-width:520px;
  width:100%;
  max-height:80vh;
  overflow-y:auto;
  padding:26px 28px;
}
/* Grab handle -- touch devices only. Swipe-to-dismiss is invisible
   without an affordance, and on a pointer device it would just be a
   decoration for a gesture that can't be performed. See
   attachSwipeToDismiss() in tools-common.js for the physics. */
@media (hover: none) and (pointer: coarse){
  .help-modal{ padding-top:20px; }
  .help-modal::before{
    content:'';
    position:sticky; top:0; left:50%;
    display:block; width:38px; height:4px; margin:0 auto 14px;
    border-radius:999px; background:var(--border);
  }
}
.help-modal h3{
  font-family:var(--font-ui);
  color:var(--orange-light);
  font-size:19px;
  margin:0 0 14px;
  padding-right:24px;
}
.help-modal-body{ color:var(--text); font-size:14.5px; line-height:1.65; }
.help-modal-body p{ margin:0 0 12px; }
.help-modal-body ul{ margin:0 0 12px; padding-left:20px; }
.help-modal-body li{ margin-bottom:8px; }
.help-modal-body strong{ color:var(--white); }
.help-modal-close{
  position:absolute;
  top:16px;
  right:16px;
  background:none;
  border:none;
  color:var(--text-dim);
  font-size:22px;
  line-height:1;
  cursor:pointer;
  padding:4px 8px;
}
.help-modal-close:hover{ color:var(--white); }

/* --- Notes tab (multi-note) --- */
.notes-layout{ display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap; }
.notes-sidebar{ flex:0 0 260px; min-width:220px; }
.notes-editor{ flex:1; min-width:280px; }
.note-new-btn{ width:100%; margin-bottom: 10px; }
.note-list-item{
  display:flex; justify-content:space-between; align-items:center; gap:8px;
  padding:12px 14px; border-radius:8px; border:1px solid var(--border); background:var(--bg-panel);
  margin-bottom:8px; cursor:pointer;
}
.note-list-item.is-active{ border-color:var(--orange-light); background:var(--bg-panel-2); }
.note-list-item-text{ overflow:hidden; }
.note-list-item-text h4{ font-family:var(--font-ui); font-size:14px; color:var(--white); margin:0 0 3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.note-list-item-text p{ font-size:12px; color:var(--text-dim); margin:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.note-list-item .small-btn{ flex-shrink:0; }
#noteTitle{
  display:block; width:100%; box-sizing:border-box;
  padding:10px 12px; background:var(--bg-panel-2); border:1px solid var(--border); border-radius:6px;
  color:var(--white); font-family:var(--font-ui); font-size:18px; font-weight:600; margin-bottom:12px;
}
#noteBody{
  display:block; width:100%; box-sizing:border-box;
  padding:14px 16px; background:var(--bg-panel-2); border:1px solid var(--border); border-radius:var(--radius);
  color:var(--white); font-size:15px; font-family:inherit; min-height:340px; resize:vertical;
}

/* --- Checkbox rows (Cost Lookup first-time discount, etc) --- */
.checkbox-row{ display:flex; align-items:center; gap:8px; margin-bottom:14px; color:var(--white); font-size:14px; }
.checkbox-row input[type="checkbox"]{ width:16px; height:16px; accent-color:var(--orange); }

/* --- Price reference chips (Cost Lookup) --- */
.price-ref-list{ display:flex; flex-wrap:wrap; gap:8px; margin-top: 4px; }
.price-ref-chip{
  display:flex; align-items:center; gap:8px; background:var(--bg-panel-2); border:1px solid var(--border);
  border-radius:8px; padding:8px 10px 8px 14px; cursor:pointer; font-size:13px; color:var(--white);
}
.price-ref-chip:hover{ border-color:var(--orange-light); }
.price-ref-chip .chip-price{ color:var(--orange-light); font-weight:600; }
.price-ref-chip .chip-remove{
  background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:14px; padding:2px 4px;
}
.price-ref-chip .chip-remove:hover{ color:#e05252; }

/* --- Responsive table wrapper (prevents page-wide horizontal scroll) --- */
.table-scroll{ overflow-x:auto; -webkit-overflow-scrolling:touch; }
.table-scroll table{ min-width:640px; }

/* Bug fix (2026-09-06), confirmed with real seeded job data rather than
   assumed: Job Tracker's Jobs tab has carried TWO separate containers for
   the same list since the table was added -- #jobsList (touch-friendly
   cards, swipe actions) and #jobsTableWrap (dense sortable table, added
   in a later "dense-table pass"). renderJobs() fills both, unconditionally,
   on every render. Nothing anywhere -- no media query, no JS toggle --
   ever hid either one. The result: every single job appeared twice on any
   desktop screen, stacked one under the other, confirmed by rendering the
   page with two seeded jobs at 1440px and finding both containers visible
   with matching data-job-id attributes. This exact possibility was flagged
   as a suspicion, unconfirmed, when the table was first added ("could not
   reproduce it without real job data") and sat live ever since.
   Fixed with the same 1024px breakpoint this file already uses everywhere
   else for a mobile/desktop split (see .hub-header, .jump-nav above):
   cards below it, the table at and above it -- matching the table's own
   column set (Phone, Address, a Margin column), which needs the room a
   phone screen doesn't have anyway. */
#jobsTableWrap { display: none; }
@media (min-width: 1024px) {
  #jobsList { display: none; }
  #jobsTableWrap { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  #jobsTableWrap table { min-width: 760px; }
}

/* --- Inline helper text --- */
.inline-note{ color:var(--text-dim); font-size:12px; margin-top:4px; }

/* --- Sync section (workspace.html) --- */
.sync-section{
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 18px 20px; margin: 24px 0; font-size: 13.5px;
}
.sync-section h4{ font-family:var(--font-ui); color: var(--orange-light); font-size: 13px; margin: 0 0 10px; text-transform: uppercase; letter-spacing: 1px; }
.sync-row{ display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:8px; }
.sync-row input{
  flex:1; min-width:160px; padding:9px 11px; background: var(--bg-panel-2); border: 1px solid var(--border);
  border-radius: 6px; color: var(--white); font-size: 14px; font-family: inherit; box-sizing: border-box;
}
.sync-status{ font-size:12.5px; color:var(--text-dim); margin-top:6px; }
.sync-status.is-ok{ color:var(--orange-light); }
.sync-status.is-error{ color:#e05252; }

/* --- Shared tab component (used by job-tracker.html and invoice-generator.html) --- */
.tabs {
  display:flex; gap:4px; margin-bottom:22px; flex-wrap:wrap;
  background:var(--bg-panel); border:1px solid var(--border); border-radius:12px; padding:5px;
}
.tab-btn {
  background:none; border:none; color:var(--text-dim); font-family:var(--font-ui);
  font-size:14px; font-weight:600; padding:10px 18px; cursor:pointer;
  border-radius:8px; transition:color .18s ease, background .18s ease, box-shadow .18s ease;
}
.tab-btn:hover{ color:var(--text); }
.tab-btn:active{ transform: translateY(1px); }
.tab-btn.is-active {
  /* 2026-08-16: previously a solid gradient-filled pill with a heavy
     glow. When a scrolled screenshot cropped this button so only its
     bottom edge showed, it read as a random floating orange bar rather
     than a tab. The underline indicator added in the v2 redesign layer
     (.tab-btn::after) already signals "active" clearly and can never be
     mistaken for debris even when cropped -- this is now the only
     active-tab treatment, just a color change plus that thin rail. */
  color: var(--orange-light);
}
.tab-panel { display:none; }
.tab-panel.is-active { display:block; animation: tabPanelFadeIn .18s ease; }
@keyframes tabPanelFadeIn{ from{ opacity:0; transform:translateY(4px); } to{ opacity:1; transform:translateY(0); } }
@media (prefers-reduced-motion: reduce){ .tab-panel.is-active{ animation:none; } }

/* Sticky tabs (2026-08-20) -- opt-in via this modifier, not the base
   .tabs class, so workspace.html's Business Health tabs (the same base
   class, but a small embedded sub-widget partway down the page, not
   primary navigation) are unaffected. Matches Runway Dashboard's own
   tab bar, which already does this -- direct fix for pages where a
   long tab's content pushed the tab row itself off-screen while
   scrolling, meaning switching tabs required scrolling all the way
   back up first. top/z-index match the existing .jump-nav pattern
   exactly, since both are a secondary bar riding just under the
   already-sticky .hub-header/.tool-header. */
/* App tour coach-mark card (2026-08-20: expanded from a 3-step dashboard-
   only tour into a full cross-page walkthrough, moved here from
   workspace.html's own local styles since every tool page needs it now). */
.onboarding-card {
  position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 800;
  max-width: 420px; margin: 0 auto;
  background: linear-gradient(180deg, var(--bg-panel-3), var(--bg-panel-2));
  border: 1px solid var(--orange-tint-border); border-radius: 16px;
  padding: 16px 18px; box-shadow: 0 -4px 30px rgba(0,0,0,.5), 0 0 30px -8px var(--orange-tint-glow);
  animation: onboardingSlideUp .3s cubic-bezier(.2,.8,.2,1);
}
@keyframes onboardingSlideUp { from { transform: translateY(16px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .onboarding-card { animation: none; } }
body.th-has-bottomnav .onboarding-card { bottom: calc(76px + env(safe-area-inset-bottom, 0px) + 10px); }
.onboarding-dots { display: flex; gap: 5px; margin-bottom: 10px; }
.onboarding-dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--border); }
.onboarding-dots span.is-active { background: var(--orange); width: 16px; border-radius: 3px; }
.onboarding-title { font-family:var(--font-ui); color: var(--white); font-size: 16px; margin-bottom: 4px; }
.onboarding-body { color: var(--text-dim); font-size: 13.5px; line-height: 1.5; margin-bottom: 14px; }
.onboarding-actions { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.onboarding-skip, .onboarding-back { background: none; border: none; color: var(--text-dim); font-size: 13px; cursor: pointer; padding: 8px 4px; }
.onboarding-skip:hover, .onboarding-back:hover { color: var(--white); }
.onboarding-actions-left { display: flex; gap: 4px; align-items: center; }
/* Tour highlight (2026-08-20): applied to whatever real element a step
   is actually talking about, per direct feedback that the tour
   described things without ever visually pointing at them. A pulsing
   glow rather than a static border, so it's noticeable even if the
   element is partway down a busy page. */
.th-tour-highlight {
  outline: 2px solid var(--orange); outline-offset: 3px; border-radius: 8px;
  animation: tourHighlightPulse 1.6s ease-in-out infinite;
  position: relative; z-index: 700;
}
@keyframes tourHighlightPulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--orange-tint-glow); }
  50% { box-shadow: 0 0 0 8px var(--orange-tint-glow); }
}
@media (prefers-reduced-motion: reduce) { .th-tour-highlight { animation: none; } }

.tabs.tabs-sticky {
  position: sticky; top: calc(61px + max(env(safe-area-inset-top, 0px), 44px)); z-index: 55;
}
/* Same WebKit ghosting fix as .hub-header/.tool-header above -- see
   that comment for the full explanation. border-radius: inherit
   matches .tabs' own rounded corners (12px), since this pseudo-element
   would otherwise show square corners peeking out from behind the
   rounded pill shape. */
.tabs.tabs-sticky::before {
  content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background: color-mix(in srgb, var(--bg-panel) 92%, transparent);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
}

.upload-progress-bar { height: 3px; background: var(--bg-panel-2); border-radius: 2px; overflow: hidden; margin-top: 10px; position: relative; }
.upload-progress-bar::after {
  content: ''; position: absolute; top: 0; left: -40%; height: 100%; width: 40%;
  background: var(--orange-light); border-radius: 2px;
  animation: uploadProgressSlide 1.1s ease-in-out infinite;
}
@keyframes uploadProgressSlide { 0% { left: -40%; } 100% { left: 100%; } }
@media (prefers-reduced-motion: reduce) { .upload-progress-bar::after { animation: none; left: 0; width: 100%; } }

/* Row entrance/exit -- deliberately scoped to ONE specific row at a
   time, never the whole list. Entrance is a keyframe animation applied
   only to the row matching whatever ID was just added, cleared
   immediately after one render so a later re-render (searching,
   filtering) never replays it. Exit is driven entirely from JS
   (animateRowExit in tools-common.js), which measures the row's real
   height before collapsing it, rather than guessing a fixed value that
   wouldn't fit every row's actual content. */
@keyframes rowEnterFade{ from{ opacity:0; transform:translateY(-6px); } to{ opacity:1; transform:translateY(0); } }
.list-row-enter{ animation: rowEnterFade .22s ease; }
@media (prefers-reduced-motion: reduce){ .list-row-enter{ animation:none; } }

.icon-search { display: inline-flex; align-items: center; position: relative; }
.icon-search-toggle {
  width: 36px; height: 36px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--bg-panel-2); color: var(--text-dim); cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 16px;
  transition: border-color .15s ease, color .15s ease;
}
.icon-search-toggle:hover, .icon-search.is-expanded .icon-search-toggle { border-color: var(--orange-light); color: var(--orange-light); }
.icon-search-input {
  width: 0; opacity: 0; padding: 0; margin-left: 0; border: none; background: var(--bg-panel-2);
  color: var(--white); font-size: 14px; font-family: inherit; border-radius: 8px; box-sizing: border-box;
  transition: width .25s ease, opacity .2s ease, padding .25s ease, margin-left .25s ease;
  overflow: hidden;
}
.icon-search.is-expanded .icon-search-input { width: 240px; opacity: 1; padding: 9px 28px 9px 11px; margin-left: 8px; border: 1px solid var(--border); }
/* Item #54: clear (x) button -- shown only once there's text to clear,
   positioned at the input's right edge (the input is the rightmost
   child of .icon-search, so right:8px on the wrapper naturally lands
   there without needing to know the toggle button's own width). */
.icon-search-clear {
  display: none; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  width: 24px; height: 24px; border-radius: 50%; border: none; background: none;
  color: var(--text-dim); font-size: 17px; line-height: 1; cursor: pointer; padding: 0;
  align-items: center; justify-content: center;
}
.icon-search-clear:hover { color: var(--orange-light); }
.icon-search.has-text .icon-search-clear { display: flex; }
@media (max-width: 480px) {
  .icon-search.is-expanded .icon-search-input { width: 55vw; }
}
@media (prefers-reduced-motion: reduce){ .icon-search-input{ transition:none; } }

/* --- Custom confirm/alert dialog (replaces native browser confirm()/alert()
   with something styled to match the app instead of the browser's default
   system dialog look) --- */
#customDialogOverlay .help-modal { max-width: 400px; text-align: center; padding: 30px 26px; }
.dialog-message { font-size: 15px; color: var(--white); margin: 4px 0 22px; line-height: 1.5; white-space: pre-line; }
.dialog-buttons { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.dialog-btn {
  border: none; border-radius: 8px; padding: 11px 22px; cursor: pointer;
  font-family:var(--font-ui); font-size: 14.5px; font-weight: 600;
}
.dialog-btn-primary {
  background: linear-gradient(135deg, var(--orange-light), var(--orange) 60%, var(--orange-dark));
  color: #1a0d02;
}
.dialog-btn-danger { background: #e05252; color: #fff; }
.dialog-btn-cancel { background: var(--bg-panel-2); border: 1px solid var(--border); color: var(--white); }
.dialog-textarea {
  width: 100%; resize: vertical; margin-bottom: 18px;
  background: var(--bg-panel-2); border: 1px solid var(--border); border-radius: 8px;
  color: var(--white); font-family:var(--font-ui); font-size: 14px; padding: 10px 12px;
}
.dialog-textarea:focus { outline: none; border-color: var(--orange-light); }

/* --- Toast notifications (tools-media-sharing.js showToast()) --- */
.th-toast-container{
  position:fixed; left:50%; bottom:24px; transform:translateX(-50%);
  z-index:400; display:flex; flex-direction:column; gap:8px; align-items:center;
  pointer-events:none; width:100%; padding:0 20px; box-sizing:border-box;
  transition: bottom .2s ease;
}
/* Bug fix (2026-08-16): the mobile bottom app nav added in the last
   redesign pass is fixed at the viewport bottom with its own height
   (~60px) plus safe-area inset -- a toast sitting at a flat bottom:24px
   would render underneath or flush against it on every phone. Raised to
   clear the bar with real breathing room whenever it's present. */
body.th-has-bottomnav .th-toast-container{
  bottom: calc(76px + env(safe-area-inset-bottom, 0px) + 12px);
}
.th-toast{
  pointer-events:auto; cursor:pointer;
  display:flex; align-items:center; gap:10px;
  background:var(--bg-panel-2); border:1px solid var(--border); border-left:3px solid var(--orange);
  color:var(--white); font-size:14px; font-family:inherit; line-height:1.4;
  padding:12px 18px; border-radius:8px; max-width:420px; width:max-content;
  box-shadow:0 6px 20px rgba(0,0,0,0.35);
  opacity:0; transform:translateY(8px); transition:opacity .18s ease, transform .18s ease;
}
.th-toast .th-icon{ width:17px; height:17px; flex-shrink:0; color:#6fcf97; }
.th-toast.is-error .th-icon{ color:#e0807a; }
.th-toast.is-shown{ opacity:1; transform:translateY(0); }
.th-toast.is-error{ border-left-color:#e05252; }
@media (prefers-reduced-motion: reduce){
  .th-toast{ transition:none; }
}
.th-toast-undo-btn {
  flex-shrink: 0; background: none; border: none; color: var(--orange-light);
  font-weight: 700; font-size: 13.5px; font-family: inherit; cursor: pointer;
  padding: 2px 4px; margin-left: 2px;
}
.th-toast-undo-btn:hover { color: var(--orange); }

/* ==========================================================================
   INTERNAL TOOLS — PROFESSIONAL POLISH PASS
   Subtle glow + smoother interaction states, scoped deliberately to classes
   used only by the Workspace tool suite (never the public marketing site's
   chat bubble / copy-email buttons, which stay exactly as they were).
   Respects the existing global prefers-reduced-motion rule above (already
   disables all transitions/animations site-wide when requested) and the
   existing blue focus-visible glow (already enhanced above) rather than
   introducing a second, conflicting focus treatment.
   ========================================================================== */

/* --- Primary / call-to-action buttons: soft orange glow + slight lift --- */
.primary-btn, .generate-btn, .note-new-btn, .dialog-btn-primary {
  transition: box-shadow .18s ease, transform .12s ease, filter .18s ease;
}
.primary-btn:hover, .generate-btn:hover, .note-new-btn:hover, .dialog-btn-primary:hover {
  box-shadow: 0 0 16px rgba(255,128,0,0.4);
  transform: translateY(-1px);
  filter: brightness(1.04);
}
.primary-btn:active, .generate-btn:active, .note-new-btn:active, .dialog-btn-primary:active {
  transform: translateY(0);
  filter: brightness(0.97);
}

/* --- Secondary / neutral buttons: understated lift, no color glow --- */
.secondary-btn, .backup-btn, .add-stop-btn, .add-contact-btn, .dialog-btn-cancel {
  transition: border-color .18s ease, box-shadow .18s ease, transform .12s ease;
}
.secondary-btn:hover, .backup-btn:hover, .add-stop-btn:hover, .add-contact-btn:hover, .dialog-btn-cancel:hover {
  box-shadow: 0 0 10px rgba(255,255,255,0.05);
  transform: translateY(-1px);
}

/* --- Small utility buttons: quiet, just smoother transitions --- */
.small-btn, .sort-btn, .period-btn, .help-btn, .icon-btn,
.remove-contact-btn, .remove-row-btn, .lead-toggle-btn, .action-btn {
  transition: border-color .15s ease, color .15s ease, background .15s ease;
}

/* --- Interactive cards: tool links, metrics, compliance cards --- */
.tool-card, .metric-card, .compliance-card, .lead-card, .job-card {
  transition: border-color .18s ease, box-shadow .18s ease, transform .15s ease;
}
.tool-card:hover {
  box-shadow: 0 0 20px rgba(255,128,0,0.12);
  transform: translateY(-2px);
}
.metric-card:hover {
  border-color: var(--border);
  box-shadow: 0 0 14px rgba(255,255,255,0.04);
}
.job-card:hover {
  box-shadow: 0 0 14px rgba(255,255,255,0.05);
  transform: translateY(-1px);
}

/* --- Live-sync indicator: a gentle pulse so "active" reads as alive,
   not just a static dot. Already covered by the global reduced-motion
   rule above, which disables this automatically when requested. --- */
.realtime-dot.is-live {
  box-shadow: 0 0 5px rgba(76,175,80,0.7);
  animation: th-pulse-glow 2.2s ease-in-out infinite;
}
@keyframes th-pulse-glow {
  0%, 100% { box-shadow: 0 0 4px rgba(76,175,80,0.55); }
  50% { box-shadow: 0 0 10px rgba(76,175,80,0.9); }
}
/* Retry affordance (2026-09-06): only appears once the badge is
   actually in a failed state (is-retryable is toggled by each page's
   own updateRealtimeBadge(), never present alongside "connecting" or
   "active") -- a static status badge suddenly looking clickable all the
   time, whether or not clicking it would do anything, would be worse
   than not signaling it at all. */
.realtime-badge.is-retryable {
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
  text-underline-offset: 3px;
  transition: text-decoration-color .15s ease;
}
.realtime-badge.is-retryable:hover { text-decoration-color: currentColor; }

/* --- Help "?" button: small glow on hover so it reads as tappable --- */
.help-btn:hover {
  box-shadow: 0 0 10px rgba(255,128,0,0.25);
}

/* --- Shared button system ---
   Consolidated from what used to be separately defined, near-duplicate
   rules in workspace.html (.backup-btn), job-tracker.html (.primary-btn,
   .secondary-btn), route-planner.html (.primary-btn), and
   invoice-generator.html (.generate-btn, plus an undefined .secondary-btn
   that was silently relying on browser defaults). Base classes now live
   here once; page-specific sizing/layout needs are modifier classes. */
.primary-btn {
  background: linear-gradient(135deg, var(--orange-light), var(--orange) 60%, var(--orange-dark));
  color: #1a0d02; font-weight: 700; font-family:var(--font-ui); font-size: 14.5px;
  border: none; border-radius: 8px; padding: 11px 22px; cursor: pointer;
}
.primary-btn.is-disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.primary-btn.block {
  width: 100%; margin-top: 16px; text-decoration: none; display: block;
  text-align: center; box-sizing: border-box; font-size: 15px; padding: 13px 24px;
}
.primary-btn.wide {
  width: 100%; font-size: 16px; padding: 14px 28px;
  text-transform: uppercase; letter-spacing: 1px;
}
.primary-btn.wide:hover { opacity: 0.92; }

.secondary-btn {
  background: var(--bg-panel-2); border: 1px solid var(--border); color: var(--white);
  border-radius: 8px; padding: 11px 18px; cursor: pointer; font-size: 14px; font-family: inherit;
}
.secondary-btn:hover { border-color: var(--blue-light); }
.secondary-btn:active { transform: translateY(1px); filter: brightness(0.95); }

/* --- Shared "tool card" link row (Dashboard's Tools section, and
   available now for any future page that needs the same pattern) --- */
.tool-card {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  background: var(--bg-panel); border: 1px solid var(--border); border-left: 3px solid var(--orange);
  border-radius: var(--radius); padding: 22px 24px; margin-bottom: 14px; text-decoration: none;
  box-shadow: 0 3px 12px rgba(0,0,0,0.18), 0 1px 3px rgba(0,0,0,0.12);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.tool-card:hover { border-color: var(--orange-light); box-shadow: 0 8px 22px rgba(0,0,0,0.28); }
.tool-card-text h3 { font-family:var(--font-ui); color: var(--white); font-size: 17px; margin: 0 0 6px; }
.tool-card-text p { color: var(--text-dim); font-size: 14px; margin: 0; }
.tool-card-arrow { color: var(--orange-light); font-size: 22px; flex-shrink: 0; }

/* --- Shared small-btn (Delete/History/etc. secondary actions) ---
   Was only ever defined locally in job-tracker.html, hover-only for the
   .danger variant. workspace.html and contract-generator.html both use
   these exact classes in markup without any definition at all -- meaning
   workspace.html's delete buttons (Leads, etc.) have been rendering
   completely unstyled on the live site. Moved here once, and added a
   resting-state red tint for .danger so it reads as destructive even
   without hovering (matters more on touch devices, which don't hover). */
.small-btn {
  background: none; border: 1px solid var(--border); color: var(--text-dim); border-radius: 6px;
  padding: 6px 12px; cursor: pointer; font-size: 12.5px; font-family: inherit;
}
.small-btn:hover { border-color: var(--orange-light); color: var(--orange-light); }
.small-btn:active { transform: translateY(1px); filter: brightness(0.95); }
.small-btn.is-active { border-color: var(--blue-light); color: var(--blue-light); background: rgba(58,160,255,0.1); }
/* 2026-09-06: resting state was border+text tint only, no fill -- read as
   barely different from a normal small-btn at a glance. A faint red wash
   behind it means "destructive" registers before you've even read the
   label, matching how .primary-btn already reads as "go" from color alone. */
.small-btn.danger { background: rgba(224,82,82,0.09); border-color: rgba(224,82,82,0.4); color: #e0a0a0; }
.small-btn.danger:hover { background: rgba(224,82,82,0.16); border-color: #e05252; color: #e05252; }

/* --- Shared dash-list-item (used for Leads, Invoices, Job Profitability,
   and any other "row of items with a right-aligned value" list). Was only
   ever defined locally in workspace.html; job-tracker.html now needs it
   too for the new Job Profitability tab. --- */
.dash-list-item {
  display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 0; border-bottom: 1px solid var(--border); font-size: 14px;
}
.dash-list-item:last-child { border-bottom: none; }
.dash-list-item-title { color: var(--white); }
.dash-list-item-meta { color: var(--text-dim); font-size: 12.5px; }
.dash-list-item-amount { color: var(--orange-light); font-weight: 600; flex-shrink: 0; }
.dash-list-item-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* --- Shared form-section / form-field system ---
   Was copy-pasted across invoice-generator.html, job-tracker.html, and
   contract-generator.html with real drift: invoice-generator's version
   was missing box-sizing:border-box (a genuine minor bug -- a width:100%
   input with padding+border but no border-box renders wider than its
   container), and textarea min-height varied (60px vs 70px). Standardized
   on the more defensive version here. route-planner.html only used the
   .form-section base (not .form-row/.form-field), so only that piece was
   removed there. review-request.html's form-field is a genuinely
   different single-column layout by design and was left untouched --
   its own local <style> block still wins the cascade for anything it
   overrides here, so nothing changes on that page. */

/* Generic table styling -- was byte-for-byte duplicated in finance.html
   and job-tracker.html before 2026-08-20; consolidated here since both
   pages already load this file. invoice-generator.html's own
   .line-items-table is intentionally separate -- a genuinely different,
   more specific style for editable line-item rows with inputs, not
   this same pattern. */
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
th { text-align: left; font-size: 11.5px; color: var(--text-dim); padding: 8px; text-transform: uppercase; border-bottom: 1px solid var(--border); white-space: nowrap; }
td { padding: 10px 8px; font-size: 14px; color: var(--white); border-bottom: 1px solid var(--border); }

/* Dense table pass (2026-09-06), requested directly: Finance's income and
   expense tables (#incomeTable, #entriesTable) can run to hundreds of
   rows over months of use, unlike every other page's short reference
   tables sharing these same bare table/th/td rules -- so this is scoped
   to those two containers specifically, rather than changed on the
   generic selectors above, which invoice-generator.html's line-items
   table (always a handful of rows, edited in place, no scrolling
   benefit) and other pages also rely on unchanged.
   Right-aligned amount column: numbers align on their ones place down
   a long list, which is how every real accounting UI does it and how
   job-tracker.html's own dense table already treats its own
   numeric-ish columns.
   Real overlap bug, reported directly with a screenshot (2026-09-07):
   sticking the header to the PAGE (position:sticky measured against
   the whole scrolling page, top offset tuned to clear the fixed app
   bar) put arbitrary scrolled-page content directly above the header
   at any given scroll position -- and since the header (a single
   line, ~28px tall) is shorter than a real data row once a long
   description or job name wraps to multiple lines, whatever content
   was passing behind the header showed through flush against its top
   edge, an abrupt, glyph-clipping overlap that reads as broken.
   Switched the header to stick to its OWN bounded container instead
   (#incomeTable/#entriesTable, given a fixed max-height and its own
   scrollbar) rather than the page. This isn't just a smaller-scoped
   version of the same fix -- it changes what can ever be directly
   above the header at all: nothing but this container's own clipped
   edge, never real page content like the "Log" heading a few pixels
   above it. That makes it safe to give the header a generous opaque
   ::before extending upward past its own box (see below) to actually
   hide a taller row's excess height -- doing the same thing against
   page-level sticky positioning was tried first and rejected: any
   size big enough to cover a real wrapped row was also big enough to
   paint over the heading above the table on ordinary page load,
   before any scrolling had even happened. */
#incomeTable, #entriesTable {
  max-height: 60vh;
  overflow-y: auto;
}
#incomeTable table, #entriesTable table { margin-top: 0; }
#incomeTable thead th, #entriesTable thead th {
  position: sticky; top: 0; z-index: 2;
  background: var(--bg-panel-2);
}
/* Extends the header's own opaque background upward past its own box,
   so a taller row's excess height (beyond the header's ~28px) is
   already hidden before it can become visible, rather than being
   clipped right at the header's boundary. Safe to size generously now
   that the header sticks within its own clipped, bounded container
   (see above) instead of the page -- there is nothing above it in that
   container to accidentally cover.
   250px, not a round guess: checked this business's real income log
   directly (via its live Supabase data) for the actual tallest real
   row rather than an arbitrary string length -- the Job column's
   longest real entry ("Baseboards, side cabinet, wire hider behind
   TV, remove satellite dish", 69 characters) wraps to 5 lines at this
   column's narrow mobile width, measuring 117px tall in a real
   rendered row (excess over the ~28px header: ~89px). 250px comfortably
   covers that with real margin to spare for a longer future entry --
   generous is free here specifically because the container above is
   now bounded, so there's nothing real for an oversized cover to ever
   paint over by mistake. A fixed-height cover still can't guarantee
   full coverage for an arbitrarily long future entry without capping
   the column's own height instead, which would clip real content the
   user typed. */
#incomeTable thead th::before, #entriesTable thead th::before {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 100%;
  height: 250px;
  background: var(--bg-panel-2);
}
#incomeTable .col-amount, #entriesTable .col-amount { text-align: right; font-variant-numeric: tabular-nums; }
.totals-row-money { display: flex; gap: 24px; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); flex-wrap: wrap; }

.form-section {
  background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 24px; margin-bottom: 22px;
  box-shadow: 0 3px 12px rgba(0,0,0,0.18), 0 1px 3px rgba(0,0,0,0.12);
}
.form-section h3 { font-family:var(--font-ui); color: var(--orange-light); font-size: 15px; margin: 0 0 15px; text-transform: uppercase; letter-spacing: 1px; }
.form-section-header {
  display: flex; justify-content: space-between; align-items: center;
  cursor: pointer; user-select: none;
}
.form-section-header h3 { margin: 0; }
.form-section-header .collapse-chevron {
  color: var(--text-dim); font-size: 13px; transition: transform 0.2s ease; flex-shrink: 0; margin-left: 10px;
}
.form-section.is-collapsed .collapse-chevron { transform: rotate(-90deg); }
.form-section-body { margin-top: 14px; max-height: 4000px; opacity: 1; overflow: hidden; transition: max-height .3s ease, opacity .2s ease, margin-top .3s ease; }
.form-section.is-collapsed .form-section-body { max-height: 0; opacity: 0; margin-top: 0; overflow: hidden; }
@media (prefers-reduced-motion: reduce){ .form-section-body{ transition:none; } }
.tool-form-row { display: flex; gap: 15px; margin-bottom: 13px; flex-wrap: wrap; }
.form-field { flex: 1; min-width: 170px; }
.form-field label { display: block; font-size: 13px; color: var(--text-dim); margin-bottom: 5px; }
.form-field input, .form-field select, .form-field textarea {
  width: 100%; padding: 9px 11px; background: var(--bg-panel-2); border: 1px solid var(--border);
  border-radius: 6px; color: var(--white); font-size: 14.5px; font-family: inherit; box-sizing: border-box;
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
  transition: border-color .15s ease, box-shadow .15s ease;
}
/* A native date input's internal shadow-DOM content (the day/month/year
   segments) has its own intrinsic minimum width on iOS Safari that
   ignores width:100%/box-sizing:border-box above, so it can visibly
   overflow its own flex container even though every other input type
   respects that same rule. min-width:0 is what actually lets a flex
   item shrink below its content's natural size; without it, flexbox
   defaults to min-width:auto, which is the real cause of the overflow. */
.form-field input[type="date"] { min-width: 0; max-width: 100%; }
/* :focus-visible above already gives keyboard users a blue outline: this
   is deliberately separate -- :focus-visible's own browser heuristics
   often don't fire for a plain tap/click, which on a touch-primary tool
   like this one would mean NO visible feedback at all when tapping into
   a field. This fires for every focus method, tap included, and uses
   orange instead of blue to actually read as this app's own state
   rather than a generic browser default. */
/* :focus-visible above already gives keyboard users a blue outline: this
   is deliberately separate and scoped to :not(:focus-visible), since
   :focus-visible's own browser heuristics often don't fire for a plain
   tap/click, which on a touch-primary tool like this one would mean NO
   visible feedback at all when tapping into a field. This covers that
   gap without double-applying alongside the keyboard outline, and uses
   orange instead of blue to read as this app's own state rather than a
   generic browser default. */
.form-field input:focus:not(:focus-visible),
.form-field select:focus:not(:focus-visible),
.form-field textarea:focus:not(:focus-visible) {
  border-color: var(--orange-light);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.3), 0 0 0 3px rgba(255,128,0,0.25);
}
.form-field textarea { resize: vertical; min-height: 70px; }

/* --- Scroll-reveal: progressive enhancement only ---
   Elements are fully visible by default. The "start hidden, fade/slide
   in on scroll" behavior ONLY applies once JS confirms IntersectionObserver
   support and adds .reveal-ready to <html> -- so no-JS visitors and old
   browsers always see full content immediately, nothing is ever
   permanently hidden behind a JS dependency. Wrapped in
   prefers-reduced-motion: no-preference so reduced-motion visitors never
   get the hidden starting state at all -- not just a faster animation,
   genuinely no scroll-gating, matching what reduced-motion actually asks for. */
@media (prefers-reduced-motion: no-preference) {
  .reveal-ready [data-reveal] {
    opacity: 0;
    transform: translateY(28px);
    transition: opacity 0.7s ease, transform 0.7s ease;
  }
  .reveal-ready [data-reveal].is-visible {
    opacity: 1;
    transform: translateY(0);
  }
  /* Slight stagger within a grid, so cards cascade in rather than
     popping in as one flat block */
  .reveal-ready [data-reveal]:nth-child(2) { transition-delay: 0.08s; }
  .reveal-ready [data-reveal]:nth-child(3) { transition-delay: 0.16s; }
  .reveal-ready [data-reveal]:nth-child(4) { transition-delay: 0.24s; }
  .reveal-ready [data-reveal]:nth-child(5) { transition-delay: 0.32s; }
  .reveal-ready [data-reveal]:nth-child(6) { transition-delay: 0.4s; }
  .reveal-ready [data-reveal]:nth-child(7) { transition-delay: 0.48s; }
}

/* Bug fix (2026-09-06): this exact @view-transition{navigation:auto;}
   rule was already declared once, near the top of this file (Item #65,
   2026-08-26) -- this second copy is a stray duplicate, its own comment
   even misdescribes itself as living in "the shared styles.css" (the
   public site's stylesheet, which has never actually had this rule at
   all -- confirmed by checking directly). Removed rather than kept:
   repeating the same at-rule twice does nothing browsers don't already
   do once, so this was dead weight, not a second, different behavior. */

/* Slightly faster and punchier than the browser default cross-fade */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.28s;
  animation-timing-function: ease-out;
}

/* --- Light mode (theme toggle) ---
   Overrides the same CSS custom properties the dark theme already uses,
   rather than duplicating every rule that references them. Every color
   here was chosen and verified against WCAG AA contrast requirements
   against the --bg value below (not just eyeballed) -- --orange-light
   and --blue-light specifically needed real recoloring, not just a
   lighter/darker flip, since they're used as TEXT color in 19 and 17
   places respectively (eyebrow labels, links) and the original dark-mode
   pastel values would fail contrast on a light background. Toggled via
   [data-theme="light"] on <html>, set by theme.js. Defaults to the
   visitor's OS-level preference on first visit (prefers-color-scheme),
   then remembers their explicit choice in localStorage after that. */
[data-theme="light"] {
  --bg: #f3f1ec;
  --bg-panel: #ffffff;
  --bg-panel-2: #f2f0ec;
  --bg-panel-3: #e9e6e0;
  --border: #ddd8d0;
  --text: #2b2b28;
  --text-dim: #544f49;
  --white: #14120f;
  --steel: #55524c;
  --orange: #d85f0a;
  --orange-dark: #b04808;
  --orange-light: #a84408;
  --orange-tint-soft: rgba(216,95,10,0.08);
  --orange-tint-border: rgba(216,95,10,0.3);
  --orange-tint-glow: rgba(216,95,10,0.12);
  --blue: #0d6efd;
  --blue-dark: #0a52c2;
  --blue-light: #0650bd;
}

/* Note: a few subtle ambient effects (the soft orange/blue radial glow
   behind the hero section, and some box-shadow opacities) were tuned
   for a dark background and weren't individually re-tuned for light
   mode -- they'll still work, just possibly read as a bit more washed
   out or subtle than intended. Worth a look once this is live and
   visible for real, rather than guessing at values now. */

/* --- Light-mode shadow & glow corrections ---
   The dark theme uses colored "glows" around buttons and soft ambient
   light behind the hero -- a visual metaphor of light emanating into
   darkness that doesn't have a sensible light-mode equivalent (there's
   no "darkness" for a glow to emanate into on a white background, so it
   either disappears or reads as a faint stain). Button glows are
   replaced with a standard neutral elevation shadow instead. Heavy
   black box-shadows (0.5-0.6 opacity, tuned to read clearly against
   near-black) are reduced substantially, since the same opacity on a
   white background looks muddy and dated rather than crisp -- modern
   light UI shadows are usually soft and in the 0.10-0.20 range.
   Deliberately NOT touched: .gallery-caption's dark gradient and the
   lightbox overlay's dark scrim, since both sit on top of a photograph
   or dim the whole viewport respectively -- both are correct dark
   regardless of site theme, not something tied to the page's own
   light/dark mode. */
[data-theme="light"] .hero::before,
[data-theme="light"] .hero::after {
  opacity: 0.85;
}
[data-theme="light"] .hero-badge img {
  filter: drop-shadow(0 16px 28px rgba(20, 18, 15, 0.14));
}
[data-theme="light"] .modal {
  box-shadow: 0 20px 48px rgba(20, 18, 15, 0.18);
}
[data-theme="light"] .chat-panel {
  box-shadow: 0 10px 30px rgba(20, 18, 15, 0.16);
}

/* F34 fix (2026-09-07): the theme-toggle/theme-switch/theme-icon-light/
   mobile-theme-row rules that used to live here were an exact-name
   collision with styles.css's own (the public site's real, live theme
   toggle) -- but grepped clean across every tool page's HTML and JS:
   none of these classes are ever used anywhere in the tools app. Removed
   as dead code rather than renamed, since there was nothing here to
   disambiguate from -- the tools app has no theme-toggle feature of its
   own to keep. */

/* --- Internal tools: mobile touch-target improvements ---
   These tools are used primarily on a phone, often on an actual job
   site rather than at a desk -- reliable tapping matters more here than
   on the marketing site. .small-btn in particular sits multiple-in-a-row
   on every job card (Advance Status, Photos, Show on Calendar, Delete)
   with only 8px between them at its previous ~28px height, well under
   the 44px minimum both Apple and Google recommend for a reliable tap
   target. Scoped to a phone-width breakpoint rather than changing
   desktop, which already works fine as-is. */
@media (max-width: 768px) {
  .small-btn {
    padding: 11px 15px;
    font-size: 13.5px;
    min-height: 42px;
  }
  .job-card-actions { gap: 10px; row-gap: 10px; }
  .tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tab-btn {
    padding: 13px 18px;
    flex-shrink: 0;
  }
  .checkbox-row { padding: 4px 0; }
  .checkbox-row input[type="checkbox"] {
    width: 20px;
    height: 20px;
    flex-shrink: 0;
  }
}

/* Editable public-site announcement banners -- content comes from
   Supabase (see /tools/dev-tools.html to edit), hidden by default and
   only shown by index.html's own script if a message is actually set.
   Falls back to simply not showing at all if that fetch ever fails,
   never breaks the page underneath it. */
.site-banner {
  background: var(--orange); color: #1a1200; text-align: center;
  font-weight: 600; font-size: 14px; padding: 10px 16px;
  position: relative; z-index: 500;
}
.site-banner:nth-of-type(2) { background: var(--blue); color: #071b2e; }

/* F34 fix (2026-09-07): the .hours-grid/.hours-row/.hours-day/
   .hours-value rules that used to live here collided by exact name with
   styles.css's own (the public site's real, live per-day hours display)
   -- but grepped clean across every tool page's HTML and JS: none of
   these classes are ever used anywhere in the tools app. Removed as dead
   code rather than renamed, since there was no actual tools-app feature
   here to disambiguate. */

/* ==========================================================================
   TOOL SUITE REDESIGN v2 -- 2026-08-16
   Replaces the too-subtle "machined edge" layer, which read as a stray
   orange line. This one is a full visual system: app-bar header, layered
   ambient background, elevated gradient surfaces, an app-icon tool grid,
   a segmented jump-nav, big confident numbers, and (from tools-common.js)
   a native-style bottom navigation bar on mobile.

   Every selector is prefixed with `body` so this layer BEATS the pages'
   own inline <style> blocks (link comes before inline, so bare equal-
   specificity selectors here would lose). Built on the shared :root
   tokens, so [data-theme="light"] still adapts automatically.
   ========================================================================== */

/* ---- global feel: rounder, deeper, ambient ---- */
/* Bug fix (2026-08-20): this used to live on body.th-tool-page itself
   -- the same element the page's own max-width constraint applies to.
   Widening that container for the desktop layout work shifted where
   this gradient (fixed rem-based size, percentage-based position)
   landed relative to the visible content, and left the margin area
   outside max-width with no background at all (visible black bars in
   a screenshot). Moved to html, which is never width-constrained, so
   this now always spans the full window consistently regardless of
   how wide the content column itself is. body's own background below
   is now transparent so this shows through everywhere, including
   within the content column, not just in the margins. */
html {
  background-image:
    radial-gradient(90rem 42rem at 8% -12%, rgba(255,128,0,.07), transparent 60%),
    radial-gradient(70rem 40rem at 108% 8%, rgba(58,160,255,.045), transparent 55%),
    linear-gradient(var(--bg), var(--bg));
  background-attachment: fixed;
}
body.th-tool-page {
  background: transparent;
  /* The tool suite is scanned and operated at speed, not read, so its
     running text -- table cells, form values, and the ten controls that
     say font-family:inherit -- takes the app face rather than the public
     site's body serif. Newsreader is right for a blog post and wrong for
     a Finance table. */
  font-family: var(--font-app);
  /* Item #63: stops scroll chaining to the browser chrome (its own
     pull-to-refresh and address-bar-reveal behavior on mobile Chrome),
     which would otherwise fight the custom pull-to-refresh gesture
     built for this page. The page's own content can still bounce
     naturally at its edges -- only the escape to browser-level
     gestures is contained. */
  overscroll-behavior-y: contain;
}
/* Item #1: barely-there grain texture -- an inline SVG feTurbulence
   noise tile, alpha reduced to ~3% via feComponentTransfer so it reads
   as a subtle material texture rather than visible static. Scoped to
   .th-tool-page only (not the public site) via a separate ::before
   layer rather than folding into the background-image stack above, so
   it can repeat/tile independently of those fixed-position gradients. */
body.th-tool-page::before {
  content: '';
  position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.03 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}
body.th-tool-page { --radius: 14px; }
/* F15 fix (2026-09-07): the desktop (min-width:1024px) content column
   width was seven different ad-hoc pixel values across the suite (950,
   1000, 1050, 1100, 1300, 1400, 1500), so the column visibly jumped
   left/right and changed width on every navigation. Two tiers instead:
   narrow for form/detail-focused pages (was 950-1100), wide for
   data-table/dashboard-heavy pages (was 1300-1500). workspace.html
   keeps its own already-unique 1400 untouched -- it has other
   genuinely special chrome (the live-sync status row, a taller header)
   the audit didn't ask to fold in here. */
body.th-tool-page { --tool-maxw-narrow: 1050px; --tool-maxw-wide: 1500px; }
/* F16/F17 fix (2026-09-07): the base (tablet-width, 721-1023px) body
   padding was 90px top on 6 pages (the former .tool-header pages),
   44px top on 6 others, and completely absent on 4 (finance, clients,
   pos, parts-reference) -- those 4 had no side padding at all in this
   band, sitting flush against both window edges with the header
   overflowing past them (a horizontal scrollbar). One shared rule
   instead of 16 per-page copies (or, for those 4, no rule at all);
   90px was only ever the old, now-retired .tool-header pattern's own
   value, so 44px (the majority, already-correct value) is what every
   page converges on. */
body.th-tool-page { padding: 44px 20px 60px; }

/* Visual pass (2026-09-06): the elevation system directly above ("real
   elevation, gradient faces") was always sound in its logic -- gradient
   surfaces, a 3-tier shadow scale, brighter panel tiers for raised
   content -- but the four surface tokens it's built on (--bg through
   --bg-panel-3, defined in styles.css) span only about 8 points of
   lightness (#0a0a0a to #1f1f1f). On an actual screen that gap all but
   disappears: every card, table, and tool tile read as one flat mass
   regardless of the gradients/shadows applied to it, confirmed by
   screenshotting real pages rather than reading the CSS and assuming
   the depth was visible. Overriding the same four tokens here, scoped
   to .th-tool-page, roughly doubles that spread -- verified against
   WCAG contrast for --text/--text-dim/--orange-light against every
   tier before picking these values (worst case, --text-dim on the
   brightest tier, is still 4.82:1, comfortably above the 4.5:1 AA
   floor for normal text). Deliberately scoped here rather than changed
   in styles.css itself, which the public marketing site also loads --
   that page's own dark theme was separately tuned and verified in an
   earlier session and must not shift because of tool-suite work.

   Bug fix (2026-09-06): scoped to body.th-tool-page alone, with no
   check for [data-theme="light"], this unconditionally overrode these
   same four tokens back to their dark-mode values even when the user
   had explicitly switched to light mode -- data-theme lives on <html>,
   custom properties are inherited from the nearest ancestor that sets
   them, and body sits between html and every sidebar/card/table in the
   tool suite, so every one of them silently ignored [data-theme="light"]
   (tools/styles-tools.css line ~806) and rendered dark panels on a
   light page. Confirmed visually (a real light-mode screenshot showed
   a correctly light page background with a fully dark sidebar and
   cards floating on it). html:not([data-theme="light"]) restores the
   original dark-mode-only scope without touching this rule's actual
   values. */
html:not([data-theme="light"]) body.th-tool-page {
  --bg: #07080a;
  --bg-panel: #18191d;
  --bg-panel-2: #212327;
  --bg-panel-3: #2c2e34;
  --border: #34363c;
}

::selection { background: var(--orange); color: #0a0a0a; }
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--orange-dark); background-clip: padding-box; }
:focus-visible {
  outline: 2px solid var(--orange);
  outline-offset: 2px;
  border-radius: 4px;
  /* matches the glow language already used on button hovers and card
     hovers elsewhere, instead of being a plain, generic flat outline */
  box-shadow: 0 0 0 5px var(--orange-tint-soft);
}
/* Bug fix (2026-08-21), reported directly with a screenshot: a stray
   blue box -- the browser's own native focus outline, not this app's
   deliberate orange :focus-visible styling above -- showed around a
   button right after a click. Nothing suppressed the plain :focus
   state for a mouse click; only :focus-visible (keyboard-style focus)
   was styled. This explicitly clears the outline whenever focus
   wasn't keyboard-triggered, leaving the intended orange ring above
   completely untouched for actual keyboard navigation. */
:focus:not(:focus-visible) { outline: none; }

/* ---- surfaces: real elevation, gradient faces ---- */
body .section-block, body .dev-panel, body .form-section,
body .card, body .metric-card, body .stat-box, body .job-card,
body .compliance-card, body .lead-card {
  background: linear-gradient(180deg, var(--bg-panel-2), var(--bg-panel) 55%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 1px 0 rgba(255,255,255,.03) inset, var(--shadow-resting);
}

/* section headings: white + orange kicker bar, not tiny orange whisper */
body .section-heading, body .dev-panel-heading {
  color: var(--white);
  font-size: 15px;
  letter-spacing: .8px;
  /* 2026-08-16: the base border-bottom (var(--border), #2a2a2a) reads
     as barely-there next to the bold orange kicker bar to its left --
     a gradient fade reads as an intentional divider instead of an
     afterthought, brightest under the text and tapering to nothing. */
  border-bottom: 1px solid transparent;
  border-image: linear-gradient(90deg, rgba(255,255,255,.16), rgba(255,255,255,.03) 70%) 1;
}
body .section-heading::before, body .dev-panel-heading::before {
  content: '';
  width: 4px; height: 18px; border-radius: 3px; flex-shrink: 0;
  margin-right: 10px;
  background: linear-gradient(180deg, var(--orange-light), var(--orange));
  display: inline-block;
}
body .section-heading { display: flex; align-items: center; }
body .section-heading > span:first-child,
body .section-heading > *:first-child { margin-right: auto; }

/* Redesign (2026-09-06), requested directly: the dashboard's collapsible
   sections (Today, Business Snapshot, Action Items, Gallery Queue,
   Compliance, Analytics, Backup) were visually identical regardless of
   content -- same bar, same all-caps label, same chevron, no way to
   tell one apart from another at a glance or find a section by shape.
   A small icon per heading fixes that without touching the collapse
   behavior or layout rhythm. .section-heading-title wraps the icon and
   label together as ONE first-child, so the existing margin-right:auto
   rule above still pushes the pair left as a unit -- an icon placed as
   its own sibling span would have become the new first-child instead
   and been shoved away from its own label. */
body .section-heading-title {
  display: inline-flex; align-items: center; gap: 8px;
}
body .section-heading-icon {
  width: 15px; height: 15px; flex-shrink: 0;
  color: var(--orange-light);
  opacity: .85;
}
/* Action Items is the one section people actually need to act on every
   day (leads, bookings, follow-ups); everything else is reference or
   review. Same icon language, but filled and glowing instead of a
   plain outline, so it reads as "this one matters most" without any
   text saying so. */
body .section-heading-icon.is-priority {
  width: 26px; height: 26px; padding: 5px; box-sizing: border-box;
  border-radius: 50%; opacity: 1; color: #1a0d02;
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.5), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, var(--orange-light), var(--orange) 55%, var(--orange-dark) 130%);
  box-shadow: 0 2px 6px -2px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3) inset;
}
body #section-actionitems { border-color: var(--orange-tint-border); }

/* ---- the numbers are the product: make them read across the room ---- */
body .metric-card .metric-value, body .stat-box .value {
  font-size: 30px;
  letter-spacing: .3px;
  background: linear-gradient(180deg, var(--white), var(--steel));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
body .metric-card .metric-value.is-orange {
  background: linear-gradient(180deg, var(--orange-light), var(--orange));
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
body .metric-card .metric-label, body .stat-box .label { letter-spacing: 1.2px; }

/* ---- app bar: the header becomes real chrome ---- */
body .hub-header, body .tool-header {
  /* Sticks to the viewport top once scrolled; before that it sits in
     normal flow, so this works for every page's own top padding (44px
     and 90px variants both exist). Horizontal padding is 20px on every
     tool page, so the full-bleed x-margins are safe.
     Bug fix (2026-08-20): top:0 had zero accounting for the iOS
     safe-area-inset-top (the notch/Dynamic Island region) despite this
     app already using that exact pattern elsewhere and setting
     viewport-fit=cover, which means content genuinely extends under
     that area unless explicitly pushed clear of it. On a large-notch
     device this pushed the header's own right-side buttons (including
     Settings) up under the status bar -- unreachable, not just
     visually cramped. Fixed the header's own top padding below (which
     alone already solves this, by pushing the header's inner content
     down clear of the notch).
     Bug fix (2026-08-21), reported directly with a screenshot: also
     changing the sticky top value itself (to env(safe-area-inset-top)
     instead of 0) was an unnecessary over-correction from the fix
     above -- on a notched device, that meant the header only started
     sticking partway down the screen, leaving the true top of the
     viewport completely uncovered. Scrolled-past content became
     visible through that gap. Reverted to top:0 -- the padding fix
     alone already keeps buttons clear of the notch, so the sticky
     position itself never needed to change too. */
  position: sticky; top: 0; z-index: 60;
  margin: 0 -20px 14px; padding: calc(12px + max(env(safe-area-inset-top, 0px), 44px)) 20px 12px;
  border-bottom: 1px solid var(--border);
  /* This was missing entirely -- with no display:flex here, any page
     relying on this shared rule (dev-tools.html, settings.html; unlike
     workspace.html, which has always had its own local override) had
     its logo, title, and every header button stacking vertically as
     plain block elements instead of sitting in a row. */
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
}
/* Desktop (2026-08-20), requested directly from a screenshot: the
   header was still constrained to the same narrow, centered column as
   the rest of the page's content, with visible dark space on either
   side -- it should span the full remaining width next to the
   sidebar instead, like a real app toolbar. Switches from sticky
   (which stays within its own containing block's width) to fixed
   (independent of it), spanning from right after the sidebar
   (left:240px) to the browser's right edge (right:0). Each page's own
   desktop media query below adds matching padding-top to its body, to
   compensate for the header no longer contributing to normal page
   flow. */
@media (min-width: 1024px) {
  body .hub-header, body .tool-header {
    position: fixed; top: 0; left: 240px; right: 0; z-index: 90;
    margin: 0; padding: 12px 24px; border-radius: 0;
  }
}
/* Bug fix (2026-08-20), found from a direct report with a screenshot
   showing the header rendering twice while scrolling: iOS Safari
   (confirmed via research to be a known, documented issue, especially
   on iOS 26) can visually duplicate/ghost a sticky or fixed element
   that carries backdrop-filter directly on itself. The documented fix
   is to keep the sticky positioning on the element itself, but move
   the actual blur/background onto an absolutely-positioned child
   instead -- position: sticky already provides a valid containing
   block for this, no extra positioning context needed. */
body .hub-header::before, body .tool-header::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
/* Bug fix (2026-08-20), found from a direct report: "Skip to main
   content" (and any other #mainContent anchor jump) positioned the
   page's own title at the very top of the viewport with no awareness
   that the sticky header above also occupies that space -- the header
   rendered directly on top of the title instead of the title sitting
   below it. scroll-margin-top tells the browser to leave room for the
   header's real height (including the safe-area-inset-top it also
   needs to clear) when jumping to this target. Applies identically
   across all 8 pages sharing this exact #mainContent skip-link pattern. */
#mainContent { scroll-margin-top: calc(61px + max(env(safe-area-inset-top, 0px), 44px)); }
/* F08 fix (2026-09-07): rendered every hub page rather than trusting a
   read -- font-size and logo size were already consistent (22px/36px
   everywhere), but workspace.html's own local .hub-title override set
   font-family:var(--font-display) (Anton) while every other hub page
   fell back to the ambient body font (Archivo), since this shared rule
   never declared font-family at all. Promoted workspace's choice here
   (Anton reads better for a page titled "Dashboard" than the body
   font) and removed its now-redundant local copy, rather than the
   reverse. */
body .hub-title { font-family: var(--font-display); font-size: 22px; letter-spacing: .5px; }
body .hub-header img { height: 36px; width: 36px; }
body .hub-header-left { display: flex; align-items: center; gap: 14px; }
/* Groups multiple header buttons (help, settings, back-arrow) so
   space-between above distributes between the left content and this
   ONE right-hand group -- not evenly across every individual button,
   which used to spread them apart from each other instead of keeping
   them together at the right edge. */
body .hub-header-right { display: flex; align-items: center; gap: 8px; }

/* jump-nav: segmented pill control riding just under the app bar */
body .jump-nav {
  position: sticky;
  top: calc(61px + max(env(safe-area-inset-top, 0px), 44px)); z-index: 55;
  margin-left: -20px; margin-right: -20px; padding: 8px 20px;
  border-bottom: 1px solid var(--border);
}
/* Desktop (2026-08-20), requested directly from a screenshot: this bar
   still spanned only the narrow content column beneath the now-fixed
   header, looking like a separate floating box -- same fix as
   .hub-header/.tool-header above, same reasoning. Positioned right
   below the fixed header (top:61px, that header's own real height on
   desktop, no safe-area-inset needed there). */
@media (min-width: 1024px) {
  body .jump-nav { position: fixed; top: 61px; left: 240px; right: 0; margin: 0; padding: 8px 24px; }
}
/* Same WebKit ghosting fix as .hub-header/.tool-header and
   .tabs-sticky above -- see the first of those comments for the full
   explanation. Also fixes a real, separate bug found in the process:
   position: sticky was missing from this rule entirely, so this
   element was never actually sticky at all despite the comment above
   describing it as riding just under the app bar. */
body .jump-nav::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
body .jump-nav a {
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 7px 14px;
  background: var(--bg-panel);
}
body .jump-nav a:hover {
  border-color: var(--orange-tint-border);
  color: var(--orange-light);
  background: var(--bg-panel-2);
}
/* 2026-08-16: the mobile bottom nav and tab buttons both show a clear
   "you are here" state; the jump-nav -- despite being the thing you
   scroll past most -- had none. Reuses the same orange-glow chip
   language as the bottom nav's active icon, driven by the scroll-spy
   script below. */
body .jump-nav a.is-active {
  border-color: var(--orange-tint-border);
  color: var(--orange-light);
  background: var(--orange-tint-soft);
  box-shadow: 0 0 0 1px var(--orange-tint-border), 0 0 14px -4px var(--orange-tint-glow);
}

/* ---- tool grid: app icons, not gray squares ---- */
body .tool-tile-link {
  gap: 9px; padding: 18px 6px 14px;
  /* 2026-08-16: was --bg-panel-2 to --bg-panel, both very close in value
     to the page's own near-black background -- on an OLED phone screen
     the whole tool grid read as one flat mass until you looked closely.
     Bumped the top stop to --bg-panel-3 (one step brighter) and paired
     it with a slightly stronger border below, so each tile visibly
     separates at a glance instead of only on close inspection. */
  background: linear-gradient(180deg, var(--bg-panel-3), var(--bg-panel-2));
  border-color: rgba(255,255,255,0.08);
  border-radius: 16px;
}
/* Redesign (2026-09-06), requested directly ("logos... make it amazing"):
   these hex tiles are the closest thing this app has to a mark for each
   tool, and a flat 16%-alpha tint (this session's first pass) still read
   as a swatch, not a badge. Now filled solid by default -- a radial
   gloss highlight over the same orange ramp used elsewhere (primary
   button, active nav state), so it reads as an enamel pin rather than a
   tinted outline. Dark icon glyph on the vivid fill for contrast, same
   pairing .primary-btn already uses (var(--bg) on solid orange). Because
   the resting state is now already the "filled" look, hover no longer
   needs a color swap to signal interactivity -- it only needs to lift,
   exactly like .tool-tile-link's own hover below. This is deliberately
   NOT applied to .th-hex-icon (the sidebar/bottom-nav icons further
   down): those use fill specifically to mark the CURRENT page, and
   making every icon vivid by default would erase that signal. The tool
   grid has no such state to protect -- every tile is just a link. */
body .tool-tile-icon {
  font-size: 24px;
  width: 52px; height: 52px;
  display: flex; align-items: center; justify-content: center;
  clip-path: var(--hex);
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.55), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, var(--orange-light), var(--orange) 55%, var(--orange-dark) 130%);
  border: 1px solid var(--orange-dark);
  box-shadow: 0 3px 8px -3px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.3) inset;
  transition: transform .18s ease, box-shadow .18s ease;
}
body .tool-tile-icon .th-icon { color: #1a0d02; }
body .tool-tile-link:hover {
  transform: translateY(-3px);
  border-color: var(--orange-tint-border);
  box-shadow: var(--shadow-hover), 0 0 24px -6px var(--orange-tint-glow);
}
body .tool-tile-link:hover .tool-tile-icon {
  transform: scale(1.06);
  box-shadow: 0 5px 14px -4px var(--orange-tint-glow), 0 1px 0 rgba(255,255,255,.35) inset;
}
body .tool-tile-label { font-size: 13px; letter-spacing: .4px; }

/* Category color-coding (2026-09-06), requested directly: the grid was
   uniform orange regardless of what a tile actually does, so scanning
   it meant reading every label. Colors follow the dashboard's own
   existing group headings (Jobs/Money/Grow/Admin) -- Jobs keeps the
   default orange (it's the primary brand color and the largest, most-
   used group, so it stays the unmarked default rather than being
   recolored for its own sake). Money gets green (the universal
   financial-software convention). Grow reuses --blue/--blue-light/
   --blue-dark, already this app's established secondary accent, rather
   than inventing a fifth color the palette doesn't otherwise use. Admin
   gets a neutral steel gradient -- deliberately desaturated, since
   Settings/Dev Tools are utility, not a "warm" category like the other
   three. Each icon's dark glyph color was picked and contrast-checked
   against that category's actual gradient (not reused from orange's
   #1a0d02), same method as the rest of this icon system. */
body .tool-tile[data-cat="money"] .tool-tile-icon {
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.55), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, #6ee7a8, #22c55e 55%, #0f6b32 130%);
  border-color: #0f6b32;
}
body .tool-tile[data-cat="money"] .tool-tile-icon .th-icon { color: #04160c; }
body .tool-tile[data-cat="grow"] .tool-tile-icon {
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.55), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, var(--blue-light), var(--blue) 55%, var(--blue-dark) 130%);
  border-color: var(--blue-dark);
}
body .tool-tile[data-cat="grow"] .tool-tile-icon .th-icon { color: #04101c; }
body .tool-tile[data-cat="admin"] .tool-tile-icon {
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.55), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, #e6e8ec, #a8adb6 55%, #6b6f78 130%);
  border-color: #6b6f78;
}
body .tool-tile[data-cat="admin"] .tool-tile-icon .th-icon { color: #161616; }
body .tool-tile[data-cat="money"] .tool-tile-link:hover .tool-tile-icon { box-shadow: 0 5px 14px -4px rgba(34,197,94,.45), 0 1px 0 rgba(255,255,255,.35) inset; }
body .tool-tile[data-cat="grow"] .tool-tile-link:hover .tool-tile-icon { box-shadow: 0 5px 14px -4px rgba(58,160,255,.45), 0 1px 0 rgba(255,255,255,.35) inset; }
body .tool-tile[data-cat="admin"] .tool-tile-link:hover .tool-tile-icon { box-shadow: 0 5px 14px -4px rgba(168,173,182,.45), 0 1px 0 rgba(255,255,255,.35) inset; }

/* ---- rows and lists: tappable cards with direction ---- */
body .dash-list-item {
  border: 1px solid transparent;
  border-radius: 10px;
  padding: 10px 12px;
  margin: 0 -12px;
  transition: background .15s ease, border-color .15s ease, transform .15s ease;
}
body a.dash-list-item:hover {
  background: var(--bg-panel-2);
  border-color: var(--orange-tint-border);
  transform: translateX(3px);
}
body .dev-check-row:hover { background: var(--orange-tint-soft); }
body table tbody tr:hover { background: var(--orange-tint-soft); }
/* Zebra striping for dense multi-column tables (Job Tracker's income/
   expense logs, 8-9 columns each and growing over months of use) --
   subtle enough on a dark theme to help scan a row across many columns
   without competing with the existing hover tint above. */
body table tbody tr:nth-child(even) { background: rgba(255,255,255,0.02); }
body table tbody tr:nth-child(even):hover { background: var(--orange-tint-soft); }

/* ---- controls: pill buttons with real states ----
   Redesign (2026-09-06), requested directly ("cleaner... make it
   amazing"): the gradient itself was already good, but it was the only
   depth cue -- a single flat light-to-dark ramp reads as a colored
   rectangle, not a pressable object. Adds a second background layer (a
   glass sheen, brightest top-left, fading out well before halfway) over
   the same orange ramp, plus a real top/bottom edge highlight in the
   shadow stack -- the two things that make a physical pill button read
   as lit from above rather than painted flat. Same technique applied to
   .secondary-btn below, at much lower intensity, so the visual language
   is shared but primary still reads as the one button meant to be
   pressed. */
body .primary-btn, body .dialog-btn-primary, body .note-new-btn {
  position: relative;
  border-radius: 999px;
  background-image:
    linear-gradient(165deg, rgba(255,255,255,.38), rgba(255,255,255,0) 44%),
    linear-gradient(180deg, var(--orange-light) -35%, var(--orange) 50%, var(--orange-dark) 135%);
  box-shadow:
    0 1px 0 rgba(255,255,255,.32) inset,
    0 -1px 0 rgba(0,0,0,.18) inset,
    0 2px 6px -2px rgba(0,0,0,.5),
    0 12px 22px -16px var(--orange-tint-glow);
  font-weight: 600; letter-spacing: .3px;
}
body .secondary-btn, body .small-btn, body .dialog-btn-cancel { border-radius: 999px; }
body .secondary-btn {
  background-image: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,0) 55%),
    linear-gradient(180deg, var(--bg-panel-2), var(--bg-panel) 130%);
  box-shadow: 0 1px 0 rgba(255,255,255,.05) inset, 0 1px 2px -1px rgba(0,0,0,.3);
}
body .primary-btn:hover, body .dialog-btn-primary:hover, body .note-new-btn:hover {
  transform: translateY(-1px);
  box-shadow:
    0 1px 0 rgba(255,255,255,.36) inset,
    0 -1px 0 rgba(0,0,0,.18) inset,
    0 3px 10px -3px rgba(0,0,0,.55),
    0 16px 30px -14px var(--orange-tint-glow);
  filter: brightness(1.06);
}
body .secondary-btn:hover, body .small-btn:hover {
  transform: translateY(-1px);
  border-color: var(--orange-tint-border);
  box-shadow: 0 1px 0 rgba(255,255,255,.06) inset, 0 5px 14px -6px rgba(0,0,0,.4);
}
body .primary-btn:active, body .secondary-btn:active, body .small-btn:active,
body .dialog-btn:active, body .note-new-btn:active {
  transform: translateY(1px) scale(.98);
  filter: brightness(.95);
  box-shadow: inset var(--shadow-active);
}
body .tab-btn { position: relative; border-radius: 10px 10px 0 0; }
body .tab-btn::after {
  content: ''; position: absolute; left: 12%; right: 12%; bottom: -1px; height: 3px;
  border-radius: 3px 3px 0 0;
  background: linear-gradient(90deg, var(--orange), var(--orange-light));
  transform: scaleX(0); transition: transform .2s cubic-bezier(.2,.8,.2,1);
}
body .tab-btn.is-active::after { transform: scaleX(1); }
body .tab-btn.is-active { color: var(--orange-light); }

body .form-field input:focus, body .form-field select:focus, body .form-field textarea:focus,
body input[type="text"]:focus, body input[type="email"]:focus, body input[type="tel"]:focus,
body input[type="number"]:focus, body input[type="date"]:focus, body input[type="search"]:focus,
body select:focus, body textarea:focus {
  border-color: var(--orange);
  box-shadow: 0 0 0 3px var(--orange-tint-soft);
  outline: none;
}

/* ==========================================================================
   MOBILE: this is where it has to feel like a different product
   ========================================================================== */
@media (max-width: 720px) {
  /* full-bleed cards: stop wasting 40px of a 390px screen on gutters */
  body.th-tool-page { padding-left: 12px; padding-right: 12px; padding-top: 12px; }
  body .hub-header, body .tool-header { margin: 0 -12px 12px; padding: calc(10px + max(env(safe-area-inset-top, 0px), 44px)) 14px 10px; }
  body .jump-nav { margin-left: -12px; margin-right: -12px; padding: 8px 12px; top: calc(53px + max(env(safe-area-inset-top, 0px), 44px)); }
  body .section-block, body .dev-panel, body .form-section { padding-left: 14px; padding-right: 14px; }

  /* type: comfortable, confident */
  body .hub-title { font-size: 20px; }
  body .metric-card .metric-value, body .stat-box .value { font-size: 26px; }
  body .section-heading { font-size: 14px; }

  /* touch targets */
  body .primary-btn, body .secondary-btn, body .dialog-btn, body .note-new-btn, body .tab-btn { min-height: 46px; }
  body .small-btn { min-height: 40px; }
  /* F22 fix (2026-09-07): 36px still fell short of a real touch
     target -- this is the shared circular icon button used for help
     modals, back-to-workspace, and settings links across every tool
     page, so this one change reaches all of them. */
  body .help-btn { width: 44px; height: 44px; }
  body .dash-list-item { padding: 13px 12px; }

  /* the bottom nav (injected by tools-common.js) needs runway */
  body.th-has-bottomnav { padding-bottom: calc(76px + env(safe-area-inset-bottom, 0px)); }

  /* hover-lift is meaningless on touch */
  body .tool-tile-link:hover, body a.dash-list-item:hover,
  body .primary-btn:hover, body .secondary-btn:hover { transform: none; }
}

/* ---- bottom app nav: injected markup, styled here ---- */
.th-bottom-nav {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 900;
  display: none;
  padding: 8px max(10px, env(safe-area-inset-left)) calc(8px + env(safe-area-inset-bottom, 0px)) max(10px, env(safe-area-inset-right));
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  -webkit-backdrop-filter: blur(18px); backdrop-filter: blur(18px);
  border-top: 1px solid var(--border);
}
@media (max-width: 720px) { .th-bottom-nav { display: flex; justify-content: space-around; gap: 4px; } }
.th-bottom-nav a {
  flex: 1; max-width: 88px;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  text-decoration: none; color: var(--text-dim);
  font-family:var(--font-ui); font-size: 10.5px; letter-spacing: .4px;
  padding: 5px 2px; border-radius: 12px;
  transition: color .15s ease, background .15s ease;
}
.th-bottom-nav a .th-bn-icon {
  width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
  transition: background .18s ease, border-color .18s ease;
}
.th-bottom-nav a .th-bn-icon .th-icon { width: 20px; height: 20px; margin-right: 0; }
.th-bottom-nav a.is-active { color: var(--orange-light); }
.th-bottom-nav a.is-active .th-bn-icon {
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.5), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, var(--orange-light), var(--orange) 55%, var(--orange-dark) 130%);
  border-color: var(--orange-dark);
  box-shadow: 0 2px 6px -2px rgba(0,0,0,.5), 0 1px 0 rgba(255,255,255,.3) inset;
}
.th-bottom-nav a.is-active .th-bn-icon .th-icon { color: #1a0d02; }
.th-bottom-nav a:active .th-bn-icon { background: var(--bg-panel-2); }

/* ---- desktop sidebar: injected markup, styled here ---- */
/* Requested directly (2026-08-20): a persistent left sidebar on
   desktop, replacing top-tab-only navigation. Hidden by default,
   shown only at min-width:1024px -- mobile is completely unaffected,
   the bottom nav above still does the job there exactly as before. */
/* ---- "flag this page" button (2026-08-21), requested directly:
   a quick way to flag something to come back to later without
   writing a full message. Small and muted -- a utility, not a
   primary action -- fixed in the bottom-right corner. On mobile,
   sits above the bottom nav (its own height plus the safe-area
   inset); on desktop, there's no bottom nav there, so it sits at
   the true bottom-right instead. ---- */
/* Redesign (2026-09-06): matches the same glass-highlight language now
   used on .primary-btn/.secondary-btn, so this stops reading as a plain
   leftover gray circle disconnected from the rest of the button system. */
.th-flag-btn {
  position: fixed; right: 16px; z-index: 70;
  bottom: calc(76px + env(safe-area-inset-bottom, 0px) + 16px);
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background-image: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,0) 55%),
    linear-gradient(180deg, var(--bg-panel-2), var(--bg-panel) 130%);
  border: 1px solid var(--border); color: var(--text-dim);
  box-shadow: 0 1px 0 rgba(255,255,255,.05) inset, 0 3px 10px rgba(0,0,0,0.3);
  cursor: pointer; transition: color .15s ease, border-color .15s ease, transform .15s ease, box-shadow .15s ease;
}
.th-flag-btn:active { transform: translateY(1px) scale(.96); }
.th-flag-btn .th-icon { width: 20px; height: 20px; }
.th-flag-btn:hover {
  color: var(--orange-light); border-color: var(--orange-light);
  transform: translateY(-1px);
  box-shadow: 0 1px 0 rgba(255,255,255,.06) inset, 0 6px 16px -6px var(--orange-tint-glow);
}
@media (min-width: 1024px) { .th-flag-btn { bottom: 16px; } }
@media (prefers-reduced-motion: reduce) { .th-flag-btn, .th-flag-btn:hover { transition: none; transform: none; } }

/* Resting state stays a quiet tint deliberately -- this icon's whole job
   in the sidebar and bottom nav is to show which page is CURRENT, so
   only .is-active (below, and in the sidebar/bottom-nav rules) gets the
   vivid filled treatment .tool-tile-icon uses unconditionally. Refined
   here with a faint inner sheen for a touch more craft at rest, without
   closing the gap to the active state. */
.th-hex-icon {
  clip-path: var(--hex);
  background: linear-gradient(165deg, rgba(255,255,255,.08), rgba(255,255,255,0) 50%), var(--orange-tint-soft);
  border: 1px solid var(--orange-tint-border);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  transition: background .15s ease, border-color .15s ease;
}
.th-hex-icon .th-icon { color: var(--orange-light); }
@media (prefers-reduced-motion: reduce) { .th-hex-icon { transition: none; } }

.th-desktop-sidebar {
  display: none;
  position: fixed; left: 0; top: 0; bottom: 0; z-index: 100;
  width: 240px; overflow-y: auto;
  background: var(--bg-panel); border-right: 1px solid var(--border);
  padding: 20px 12px;
}
@media (min-width: 1024px) { .th-desktop-sidebar { display: flex; flex-direction: column; gap: 4px; } }
/* Offsets the main content column to make room for the fixed sidebar
   above -- same class-boosted-specificity technique already proven
   for body.th-has-bottomnav's padding offset, so this correctly wins
   over each page's own bare body{margin:0 auto} rule. */
/* The flat 240px offset that used to live here was removed (2026-08-20):
   found, from a direct screenshot, that a fixed offset combined with
   max-width and a leftover auto margin piled all the extra space onto
   one side instead of centering it. Every real page now computes its
   own correct margin-left directly (a real centering calc using that
   page's own max-width), which this rule's higher specificity would
   otherwise have silently overridden regardless of load order. */
.th-sidebar-brand {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 20px; margin-bottom: 8px; border-bottom: 1px solid var(--border);
  text-decoration: none; color: var(--white);
  font-family:var(--font-display); font-size: 16px; letter-spacing: .5px;
}
.th-sidebar-brand img { width: 32px; height: 32px; object-fit: contain; border-radius: 6px; }
.th-sidebar-links { display: flex; flex-direction: column; gap: 2px; }
.th-sidebar-link {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px; border-radius: 10px;
  text-decoration: none; color: var(--text-dim);
  font-family:var(--font-ui); font-size: 13.5px; font-weight: 500; letter-spacing: .2px;
  transition: color .15s ease, background .15s ease;
}
.th-sidebar-link .th-hex-icon { width: 30px; height: 30px; }
.th-sidebar-link .th-hex-icon .th-icon { width: 16px; height: 16px; }
.th-sidebar-link.is-active .th-hex-icon {
  background:
    radial-gradient(120% 130% at 30% 18%, rgba(255,255,255,.5), rgba(255,255,255,0) 46%),
    linear-gradient(165deg, var(--orange-light), var(--orange) 55%, var(--orange-dark) 130%);
  border-color: var(--orange-dark);
  box-shadow: 0 2px 6px -2px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3) inset;
}
.th-sidebar-link.is-active .th-hex-icon .th-icon { color: #1a0d02; }
.th-sidebar-link:hover .th-hex-icon { border-color: var(--orange-light); }
.th-sidebar-link:hover { background: var(--bg-panel-2); color: var(--text); }
.th-sidebar-link.is-active {
  background: var(--orange-tint-soft); color: var(--orange-light);
  box-shadow: inset 0 0 0 1px var(--orange-tint-border);
}
@media (prefers-reduced-motion: reduce) { .th-sidebar-link { transition: none; } }

/* ---- reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
  body .tool-tile-link, body .tool-tile-icon, body .dash-list-item, body .tab-btn::after,
  body .primary-btn, body .secondary-btn, body .small-btn, body .note-new-btn,
  .th-bottom-nav a, .th-bottom-nav a .th-bn-icon { transition: none; }
  body .tool-tile-link:hover, body .tool-tile-link:hover .tool-tile-icon, body a.dash-list-item:hover,
  body .primary-btn:hover, body .secondary-btn:hover { transform: none; }
}

/* ---- th-icon: shared sizing for the SVG icon sprite ---- */
.th-icon {
  display: inline-block;
  width: 1em; height: 1em;
  vertical-align: -0.15em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex-shrink: 0;
}
.icon-star-filled, svg.th-icon use[href="#icon-star-filled"] { fill: currentColor; }

/* 2026-08-16: icon at 26px inside a 52px tile squircle left almost no
   breathing room (icon nearly touched the container's rounded edge).
   Dropped to 21px (~40% of the container instead of ~50%) -- refined
   icon sets read premium specifically because of generous internal
   padding, not because the glyph fills the available space. */
body .tool-tile-icon .th-icon { width: 21px; height: 21px; margin-right: 0; }
body .jump-nav-icon .th-icon, body .jump-nav .th-icon { width: 15px; height: 15px; }
body .section-heading .th-icon, body .dev-panel-heading .th-icon { width: 15px; height: 15px; margin-right: 2px; }
body a .th-icon { margin-right: 5px; }
/* Icon-only circular buttons (.help-btn, used for Settings/back-arrow/
   refresh links) never pair an icon with trailing text -- the margin
   above was pushing their icon 5px left of the button's actual center
   with nothing on the right to balance it. */
body a.help-btn .th-icon { margin-right: 0; }
/* F34 fix (2026-09-07): `.lightbox-nav .th-icon` used to live here,
   colliding by exact name with styles.css's own .lightbox-nav (the
   public site's real image-lightbox prev/next controls) -- but no tool
   page has a .lightbox-nav element at all (the tools' own photo lightbox
   uses .photo-lightbox-nav, a different name). Removed as dead code. */

/* ---- empty states: one shared definition, icon + message row ----
   Previously duplicated identically across calendar.html, contract-
   generator.html, and workspace.html -- and never defined at all for
   invoice-generator.html, whose "No invoices generated yet" /
   "No quotes match..." states have been rendering completely
   unstyled (no color, no size, no padding) since that page existed. */
.empty-state-small {
  /* 2026-08-16: align-items:center on a 2-line wrapped message centers
     the icon against the whole paragraph block, so it visually floats
     above where the eye actually starts reading. flex-start anchors it
     to the first line instead; the icon's own small top margin nudges
     it down to sit on that line's baseline rather than its very top. */
  display: flex; align-items: flex-start; gap: 10px;
  color: var(--text-dim); font-size: 13.5px; line-height: 1.5;
  padding: 16px 4px;
}
.empty-state-small .th-icon {
  width: 20px; height: 20px; flex-shrink: 0; margin-top: 2px;
  color: var(--text-dim); opacity: .8;
}
.empty-state-small.is-good .th-icon { color: #4caf50; }
.empty-state-small.is-warning .th-icon { color: #e0a552; }

/* ---- shared log-item row (contracts, invoices, quotes) ----
   Previously defined only inside contract-generator.html's own <style>
   block. invoice-generator.html uses this exact same markup for its
   Invoice and Quote logs but never defined the class anywhere -- its
   lists have been rendering completely unstyled (no padding, no border,
   no badge shape) since that page existed. Moved here once so both
   pages -- and any future page using the same pattern -- get it. */
.contract-log-item {
  display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 12px 0; border-bottom: 1px solid var(--border);
}
.contract-log-item:last-child { border-bottom: none; }
.contract-log-title { font-family:var(--font-ui); color: var(--white); font-size: 14.5px; }
.contract-log-meta { color: var(--text-dim); font-size: 12.5px; margin-top: 2px; }
.badge-pending-signature {
  display: inline-block; background: rgba(255,128,0,0.12); border: 1px solid rgba(255,128,0,0.4);
  color: var(--orange-light); font-size: 11px; padding: 2px 8px; border-radius: 20px; margin-left: 6px; vertical-align: middle;
}
body.is-compact-density .contract-log-item { padding: 6px 0; }

/* ---- skeleton loading shimmer -- shared ----
   Previously only defined locally inside workspace.html. Moved here so
   Job Tracker and Invoice Generator can use the same treatment during
   their own real async gap (the initial cloud-sync pull), instead of
   sitting fully blank until that resolves. */
.skeleton-line{height:14px;border-radius:6px;margin-bottom:10px;background:linear-gradient(90deg, var(--bg-panel-2) 25%, var(--bg-panel) 50%, var(--bg-panel-2) 75%);background-size:200% 100%;animation:shimmer 1.4s ease-in-out infinite;}
.skeleton-line.w-40{width:40%;}
.skeleton-line.w-60{width:60%;}
.skeleton-line.w-100{width:100%;}
@keyframes shimmer{0%{background-position:200% 0;} 100%{background-position:-200% 0;}}
@media (prefers-reduced-motion: reduce){ .skeleton-line{animation:none; opacity:0.6;} }

/* ---- long-press quick actions (item #9) ---- */
.is-long-pressing {
  transform: scale(0.97);
  transition: transform .15s ease;
  box-shadow: 0 0 0 2px var(--orange-tint-border), 0 4px 16px -4px var(--orange-tint-glow);
}
@media (prefers-reduced-motion: reduce) { .is-long-pressing { transition: none; transform: none; } }

.quick-actions-overlay {
  position: fixed; inset: 0; z-index: 950;
  background: rgba(0,0,0,0);
  display: flex; align-items: flex-end; justify-content: center;
  transition: background .2s ease;
}
.quick-actions-overlay.is-shown { background: rgba(0,0,0,0.55); }
.quick-actions-sheet {
  width: 100%; max-width: 480px;
  background: linear-gradient(180deg, var(--bg-panel-3), var(--bg-panel-2));
  border: 1px solid var(--border); border-radius: 18px 18px 0 0;
  padding: 10px 16px calc(16px + env(safe-area-inset-bottom, 0px));
  transform: translateY(100%);
  transition: transform .22s cubic-bezier(.2,.8,.2,1);
}
.quick-actions-overlay.is-shown .quick-actions-sheet { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .quick-actions-overlay, .quick-actions-sheet { transition: none; }
}
.quick-actions-title {
  text-align: center; color: var(--text-dim); font-size: 12.5px; font-weight: 600;
  text-transform: uppercase; letter-spacing: .6px; padding: 10px 0 6px;
}
.quick-actions-btn {
  display: block; width: 100%; text-align: center; background: none; border: none;
  border-top: 1px solid var(--border); color: var(--white); font-size: 16px;
  font-family: inherit; padding: 15px 0; cursor: pointer;
}
.quick-actions-btn:active { background: var(--bg-panel); }
.quick-actions-btn.is-danger { color: #e0807a; }
.quick-actions-cancel { color: var(--orange-light); font-weight: 600; margin-top: 6px; border-top: none; }

/* ---- Item #5: staggered list-reveal ---- */
/* Reusable utility: add .list-stagger to a container right before
   setting its innerHTML, only on that list's very first real render
   (page load) -- NOT on every subsequent re-render from a search
   keystroke or filter click, which would make the list visibly
   re-cascade every time someone types, reading as janky rather than
   considered. nth-child delays cap at 10 items; anything beyond that
   shares the last delay rather than making someone wait through an
   ever-longer tail for a big list. */
.list-stagger > * {
  opacity: 0;
  animation: listStaggerIn .32s ease forwards;
}
.list-stagger > *:nth-child(1) { animation-delay: 0ms; }
.list-stagger > *:nth-child(2) { animation-delay: 30ms; }
.list-stagger > *:nth-child(3) { animation-delay: 60ms; }
.list-stagger > *:nth-child(4) { animation-delay: 90ms; }
.list-stagger > *:nth-child(5) { animation-delay: 120ms; }
.list-stagger > *:nth-child(6) { animation-delay: 150ms; }
.list-stagger > *:nth-child(7) { animation-delay: 180ms; }
.list-stagger > *:nth-child(8) { animation-delay: 210ms; }
.list-stagger > *:nth-child(9) { animation-delay: 240ms; }
.list-stagger > *:nth-child(n+10) { animation-delay: 270ms; }
@keyframes listStaggerIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .list-stagger > * { animation: none; opacity: 1; }
}

/* ---- Item #5: offline banner ---- */
.th-offline-banner {
  position: fixed; top: 0; left: 0; right: 0; z-index: 990;
  display: flex; align-items: center; justify-content: center; gap: 8px;
  background: linear-gradient(90deg, #b34a15, #d9601a);
  color: #fff; font-size: 13px; font-weight: 600;
  padding: 10px 16px; padding-top: calc(10px + max(env(safe-area-inset-top, 0px), 44px));
  transform: translateY(-100%);
  transition: transform .25s cubic-bezier(.2,.8,.2,1);
}
.th-offline-banner.is-shown { transform: translateY(0); }
.th-offline-banner .th-icon { width: 15px; height: 15px; flex-shrink: 0; }
/* Push page content down while the banner is visible, on tool pages
   whose header is sticky to the very top -- otherwise the banner covers
   the first ~40px of the header. */
body.th-is-offline .hub-header, body.th-is-offline .tool-header {
  margin-top: 40px;
}
@media (prefers-reduced-motion: reduce) { .th-offline-banner { transition: none; } }

/* ---- shared bar-row component (status/vendor/client/weekday bars) ----
   Previously only defined inside workspace.html's own <style> block.
   Job Density by Weekday (calendar.html) needs this same pattern, so
   moved here once rather than duplicated a second time. */
.status-bar-row, .vendor-bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; font-size: 13px; }
.status-bar-label, .vendor-bar-label { width: 110px; flex-shrink: 0; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.status-bar-track, .vendor-bar-track { flex: 1; height: 8px; background: var(--bg-panel-2); border-radius: 4px; overflow: hidden; }
.status-bar-fill { height: 100%; background: var(--orange); }
.vendor-bar-fill { height: 100%; background: var(--blue); }
.status-bar-count, .vendor-bar-count { width: 60px; text-align: right; color: var(--white); flex-shrink: 0; font-size: 12.5px; }

/* ---- Item #1: photo lightbox ---- */
.photo-lightbox-overlay {
  position: fixed; inset: 0; z-index: 970;
  background: rgba(0,0,0,0);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  transition: background .2s ease;
  padding: max(env(safe-area-inset-top, 0px), 44px) env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
}
.photo-lightbox-overlay.is-shown { background: rgba(0,0,0,0.92); }
.photo-lightbox-stage { position: relative; width: 100%; flex: 1; display: flex; align-items: center; justify-content: center; touch-action: none; }
.photo-lightbox-img { max-width: 92vw; max-height: 78vh; object-fit: contain; border-radius: 6px; opacity: 0; transition: opacity .15s ease; }
.photo-lightbox-overlay.is-shown .photo-lightbox-img { opacity: 1; }
.photo-lightbox-close {
  position: absolute; top: max(14px, env(safe-area-inset-top, 0px), 44px); right: 16px; z-index: 5;
  width: 38px; height: 38px; border-radius: 50%; border: none;
  background: rgba(255,255,255,0.1); color: #fff; font-size: 22px; line-height: 1; cursor: pointer;
}
.photo-lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 42px; height: 42px; border-radius: 50%; border: none;
  background: rgba(255,255,255,0.1); color: #fff; font-size: 18px; cursor: pointer;
}
.photo-lightbox-prev { left: 10px; }
.photo-lightbox-next { right: 10px; }
.photo-lightbox-caption { color: var(--text-dim); font-size: 13px; padding: 4px 0; text-align: center; }
.photo-lightbox-counter { color: var(--text-dim); font-size: 12px; padding-bottom: max(12px, env(safe-area-inset-bottom, 0px)); }
@media (prefers-reduced-motion: reduce) {
  .photo-lightbox-overlay, .photo-lightbox-img { transition: none; }
}

/* ---- Item #2: voice dictation button ---- */
.voice-dictation-btn {
  position: absolute; bottom: 8px; right: 8px; z-index: 2;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--bg-panel-3); border: 1px solid var(--border); color: var(--text-dim);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.voice-dictation-btn:hover { color: var(--orange-light); border-color: var(--orange-tint-border); }
.voice-dictation-btn.is-listening {
  color: #fff; background: #e05252; border-color: #e05252;
  animation: micListeningPulse 1.2s ease-in-out infinite;
}
@keyframes micListeningPulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(224,82,82,.5); } 50% { box-shadow: 0 0 0 6px rgba(224,82,82,0); } }
@media (prefers-reduced-motion: reduce) { .voice-dictation-btn.is-listening { animation: none; } }

/* ---- Item #6: generic print stylesheet for the tool suite ----
   Distinct from runway-dashboard.html's own bespoke print-only report
   builder (a purpose-built financial report, different problem). This
   is the fix for every OTHER tool page: hitting Ctrl+P on Parts
   Reference, a SOP, or Dev Tools currently printed exactly what was on
   screen -- dark background, sticky nav, buttons and all. Hides chrome,
   flips to a light/readable palette, and lets the actual content print.
   Scoped to .th-tool-page so the public marketing site is unaffected. */
@media print {
  body.th-tool-page {
    background: #fff !important; color: #111 !important;
  }
  body.th-tool-page::before { display: none !important; } /* grain texture -- pure ink waste on paper */
  .hub-header, .tool-header, .jump-nav, .th-bottom-nav,
  .th-offline-banner, .th-toast-container, .onboarding-card,
  .photo-lightbox-overlay, .quick-actions-overlay,
  .primary-btn, .secondary-btn, .small-btn, .help-btn, .dev-info-bubble,
  .job-card-swipe-action, .icon-search-toggle, .voice-dictation-btn,
  input[type="file"], .backup-row {
    display: none !important;
  }
  body.th-tool-page .section-block, body.th-tool-page .card,
  body.th-tool-page .job-card-swipe-inner, body.th-tool-page .form-section,
  body.th-tool-page .dev-panel {
    background: #fff !important; border-color: #ccc !important;
    box-shadow: none !important; color: #111 !important;
  }
  body.th-tool-page .text-dim, body.th-tool-page [class*="-dim"] { color: #555 !important; }
  a { color: #111 !important; text-decoration: underline; }
}

/* ---- Item #3: PWA install banner ---- */
.th-install-banner {
  position: fixed; left: 12px; right: 12px; z-index: 985;
  bottom: 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  background: linear-gradient(180deg, var(--bg-panel-3), var(--bg-panel-2));
  border: 1px solid var(--orange-tint-border); border-radius: 14px;
  padding: 12px 14px; color: var(--white); font-size: 13px;
  box-shadow: 0 8px 30px rgba(0,0,0,.5), 0 0 24px -8px var(--orange-tint-glow);
  transform: translateY(120%);
  transition: transform .25s cubic-bezier(.2,.8,.2,1);
}
body.th-has-bottomnav .th-install-banner { bottom: calc(76px + env(safe-area-inset-bottom, 0px) + 10px); }
.th-install-banner.is-shown { transform: translateY(0); }
.th-install-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.th-install-action {
  background: linear-gradient(180deg, var(--orange-light), var(--orange) 60%, var(--orange-dark));
  color: #1a0d02; border: none; border-radius: 999px; padding: 7px 14px;
  font-weight: 700; font-size: 12.5px; cursor: pointer;
}
.th-install-dismiss { background: none; border: none; color: var(--text-dim); font-size: 18px; line-height: 1; cursor: pointer; padding: 4px; }
@media (prefers-reduced-motion: reduce) { .th-install-banner { transition: none; } }

/* Pull-to-refresh indicator (2026-08-27) -- moved here from workspace.html's
   own inline style once setupPullToRefresh() (tools-effects.js) became a
   shared function every synced tool page uses, not just workspace.html.
   No transition on --pull-progress itself while actively dragging (set
   inline by JS) so it tracks the finger exactly; a transition applies
   only for the release/snap-back.
   top uses env(safe-area-inset-top) with the same max(...,44px)
   floor used everywhere else in this file (real bug found via
   on-device debugging, 2026-08-27) -- a plain top:10px landed
   directly behind the Dynamic Island on newer iPhones (confirmed via
   research: ~59px safe-area-inset-top on those devices vs ~0-20px on
   older iPhones and most Android). The 44px floor guards a separate,
   known iOS Safari bug (already documented elsewhere in this file)
   where the dynamic env() value can unexpectedly return 0px even on
   a real notched device -- caught by this project's own existing
   test for exactly that, not something noticed independently. */
.pull-refresh-indicator {
  position: fixed; top: calc(max(env(safe-area-inset-top, 0px), 44px) + 10px); left: 50%; margin-left: -18px; z-index: 200;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--bg-panel-2); border: 1px solid var(--border); color: var(--orange-light);
  display: flex; align-items: center; justify-content: center; font-size: 18px;
  opacity: 0; transform: scale(0) rotate(calc(var(--pull-progress, 0) * 180deg));
  pointer-events: none;
}
.pull-refresh-indicator.is-visible { opacity: 1; transform: scale(var(--pull-progress, 0)) rotate(calc(var(--pull-progress, 0) * 180deg)); }
.pull-refresh-indicator.is-refreshing { opacity: 1; transform: scale(1); animation: pullRefreshSpin 0.7s linear infinite; }
@keyframes pullRefreshSpin { from { transform: scale(1) rotate(0deg); } to { transform: scale(1) rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .pull-refresh-indicator.is-refreshing { animation: none; } }

/* Role-restricted page overlay (2026-08-27) -- shared across
   finance.html, runway-dashboard.html, invoice-generator.html,
   contract-generator.html, and review-request.html, all newly gated
   behind canManageBusinessFinances() alongside the new Employee role.
   A true full-viewport overlay (not a "swap which view is displayed"
   div like site-content.html/dev-tools.html use) specifically so a
   blocked page never needs its whole existing body wrapped in a new
   container -- lower-risk than restructuring 5 already-complex pages'
   DOM just for this. Paired with gating the actual init/render logic
   itself (not just this visual overlay), so a blocked account's data
   never actually populates the DOM underneath it either. */
.role-blocked-overlay {
  position: fixed; inset: 0; z-index: 9999; background: var(--bg);
  display: none; flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 24px;
}
.role-blocked-overlay.is-shown { display: flex; }

/* =====================================================================
   Tools polish pass (2026-09-06) -- presentation only.

   Deliberately modest, per instruction: no new components, no layout
   restructuring, nothing that touches how any tool works. This is
   consistency work only -- one radius scale, one elevation language,
   readable tables, real touch targets, and a single focus treatment --
   appended at the end of this file so it lands last and needs no
   !important. Every selector below already exists in the tools markup.
===================================================================== */

/* one radius + elevation language across panels */
.tool-card,
.metric-card,
.section-block,
.dash-list-item,
.form-section {
  border-radius: 12px;
}
.tool-card,
.metric-card,
.section-block {
  box-shadow: var(--shadow-resting);
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}

/* tables: make long operational lists scannable */
.table-scroll table th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg-panel-2);
  font-family:var(--font-ui);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
}
.table-scroll table tbody tr {
  transition: background .12s ease;
}
.table-scroll table tbody tr:nth-child(even) {
  background: rgba(255, 255, 255, .018);
}
.table-scroll table tbody tr:hover {
  background: rgba(58, 160, 255, .07);
}

/* F09 fix (2026-09-07): promoted from job-tracker.html, the only place
   this was ever defined -- finance.html used the class 3x with zero
   styling, rendering full-size/full-brightness text flush to the
   panel edge, unlike Job Tracker's own centered, dim, padded block.
   Also folds in parts-reference.html's .pr-empty, which was
   byte-for-byte identical to this rule's own base under another name
   (renamed at each of its 3 call sites instead of kept as a duplicate).
   runway-dashboard.html keeps its own separate, deliberately
   self-contained .empty-state copy (a different, independently-built
   treatment) since it never loads this file. */
.empty-state { text-align: center; color: var(--text-dim); padding: 40px 20px; font-size: 14px; }
.empty-state.empty-state-first-run { padding: 48px 24px; font-size: 14.5px; }
.empty-state.empty-state-first-run .primary-btn { margin: 0 auto; }
.empty-state .th-icon { display: block; width: 26px; height: 26px; margin: 0 auto 10px; opacity: .7; }

/* F11 fix (2026-09-07): load-error messages (clients.html's "Could not
   load bug reports:", "Could not search:", etc.) were rendered in
   class="tool-sub", the same dim grey as an ordinary page caption --
   a failure read as a caption, with no colour, icon, or way to tell it
   apart from normal text. Red matches the app's already-established
   error convention (.sync-status.is-error, .th-toast.is-error). */
.error-state { color: #e05252; font-weight: 500; }
.error-state::before { content: '\26a0  '; }

/* touch targets: these tools are used on a phone on a job site */
.small-btn { min-height: 40px; }
.tab-btn { min-height: 42px; }
@media (max-width: 760px) {
  .small-btn { min-height: 44px; }
  .form-field input,
  .form-field select,
  .form-field textarea { min-height: 46px; }
}

/* one focus treatment everywhere, matching the rest of the project */
.tool-card:focus-visible,
.small-btn:focus-visible,
.tab-btn:focus-visible,
.help-btn:focus-visible {
  outline: 2px solid var(--blue-light);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px rgba(58, 160, 255, .16);
}

/* an empty list should read as empty, not as a failed load */
.empty-note,
.storage-note {
  border-radius: 10px;
}

@media (prefers-reduced-motion: reduce) {
  .tool-card,
  .metric-card,
  .section-block { transition: none; }
}
