/* portal-polish.css -- presentation-only refinement pass for the client
   portal (2026-09-06).

   WHY A SEPARATE FILE, LOADED LAST
   The portal's cascade is already four layers deep: /styles.css, then
   portal-app.css, then a per-page <style> block, then styles-tools.css,
   then a second per-page <style> block. Editing any of those in place
   would mean reasoning about what each later layer overrides, on a
   surface that handles real invoices and payments. Adding one sheet at
   the very end of <head> instead means every rule here lands last, wins
   ties without !important, changes nothing structurally, and can be
   removed by deleting a single <link> if anything looks wrong.

   Strictly visual. No layout is restructured, no element is hidden or
   moved, no behaviour is touched -- only colour, depth, spacing rhythm,
   typography, focus and touch-target size. Every selector below already
   exists in the portal's markup. */

/* ---------- 1. a calmer branded ground ---------- */
/* The portal is a utility people open to settle a bill, not a marketing
   page, so this is a much quieter version of the public site's blueprint:
   a single soft warm wash rather than a full drafting grid. */
/* login.html and set-password.html carry no body class, so they are
   matched structurally instead. :has() is progressively enhanced -- a
   browser without it simply keeps the plain background. */
body.portal-page,
body:has(.login-box) {
  background-image:
    radial-gradient(ellipse 70% 40% at 50% -6%, rgba(255, 128, 0, .07), transparent 62%),
    radial-gradient(ellipse 60% 36% at 90% 102%, rgba(58, 160, 255, .05), transparent 66%);
  background-attachment: fixed;
}
/* login.html/set-password.html only (2026-09-06, requested directly --
   "should we add the background to tools and portal like the main
   site"): faint drafting-grid lines, roughly a third of the public
   site's own opacity (styles.css section 1, "the blueprint world").
   These two are the one truly sparse screen in the whole app -- a
   single centered card on an otherwise empty page -- so the grid has
   room to read as atmosphere rather than competing with content. NOT
   extended to .portal-page's real screens (invoices, quotes, jobs) or
   the tool suite: those stay content-dense enough that the same grid
   would fight the tables and cards sitting on top of it, which is the
   whole reason this stayed a quiet wash instead of the full blueprint
   in the first place (see 1b below). background-image can't be layered
   across two rules -- this repeats the two washes above rather than
   only adding the grid, since the later, more specific declaration
   fully replaces the property rather than extending it. */
body:has(.login-box) {
  background-image:
    radial-gradient(ellipse 70% 40% at 50% -6%, rgba(255, 128, 0, .07), transparent 62%),
    radial-gradient(ellipse 60% 36% at 90% 102%, rgba(58, 160, 255, .05), transparent 66%),
    repeating-linear-gradient(0deg, rgba(255,255,255,.009) 0 1px, transparent 1px 96px),
    repeating-linear-gradient(90deg, rgba(255,255,255,.009) 0 1px, transparent 1px 96px),
    repeating-linear-gradient(0deg, rgba(255,255,255,.004) 0 1px, transparent 1px 24px),
    repeating-linear-gradient(90deg, rgba(255,255,255,.004) 0 1px, transparent 1px 24px);
}

/* ---------- 1b. elevation range widened ----------
   The four surface tokens this cascade inherits from styles.css
   (--bg through --bg-panel-3) span only ~8 points of lightness --
   confirmed by screenshotting real portal pages, not by reading the
   gradient/shadow rules and assuming they were visible. Every card
   (invoice, quote, work order) read as barely distinguishable from the
   page background regardless of the elevation CSS already applied to
   it. Same fix and same verified values as the internal tool suite's
   equivalent override (styles-tools.css, body.th-tool-page) -- contrast
   against --text/--text-dim/--orange-light was checked against all four
   tiers before picking these; the worst case stays above 4.5:1. Scoped
   to .portal-page only, so the public marketing site's separately-tuned
   dark theme is untouched. login.html/set-password.html (no body class,
   matched via :has() above) intentionally keep the unmodified tokens --
   they're a single centered card on an otherwise empty page, where the
   original tighter range was never the problem. */
/* login.html and set-password.html deliberately carry a bare <body> --
   no bottom nav, no elevation overrides -- so they miss the
   .portal-page rule below. They are still the app rather than
   something anyone reads, so they take the app face too. Same
   :has(.login-box) hook the drafting-grid background already uses. */
body:has(.login-box) { font-family: var(--font-app); }

body.portal-page {
  /* Same reasoning as the tool suite: the portal is an app a client
     operates, so its running text takes the app face, not the public
     site's body serif. */
  font-family: var(--font-app);
  --bg: #07080a;
  --bg-panel: #18191d;
  --bg-panel-2: #212327;
  --bg-panel-3: #2c2e34;
  --border: #34363c;
}

/* ---------- 2. header ---------- */
.portal-header {
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
.portal-title {
  font-family:var(--font-display);
  letter-spacing: .6px;
}
.portal-sub {
  color: var(--text-dim);
  font-size: 13px;
  letter-spacing: .2px;
}
.portal-icon-btn {
  border-radius: 10px;
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.portal-icon-btn:hover {
  border-color: var(--blue-light);
  color: var(--blue-light);
}

/* ---------- 3. navigation ---------- */
.portal-nav a {
  min-height: 46px;
  border-radius: 10px;
  transition: color .15s ease, background .15s ease;
}
.portal-nav a:hover { color: var(--white); }
.portal-nav a.is-active { font-weight: 600; }

/* ---------- 4. cards ---------- */
/* One consistent elevation language instead of each card type choosing
   its own. Matches the 3-tier shadow scale already defined in styles.css. */
.attention-card,
.help-card,
.home-card,
.invoice-card,
.job-card,
.quote-card,
.set-card,
.wo-card,
.wo-form-card {
  border-radius: 12px;
  box-shadow: var(--shadow-resting);
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}
.invoice-card:hover,
.job-card:hover,
.quote-card:hover,
.wo-card:hover {
  box-shadow: var(--shadow-hover);
  border-color: var(--blue-dark);
}
.attention-card {
  border-left: 3px solid var(--orange);
}

/* ---------- 5. status badges ---------- */
/* Colour alone should not carry the meaning, so each state keeps its own
   text; this just makes the states instantly separable at a glance. */
.invoice-status,
.quote-status,
.wo-status,
.set-card-badge,
.wo-scheduled-tag,
.wo-urgency-tag,
.checkup-due-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px;
  border-radius: 999px;
  font-family:var(--font-ui);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  border: 1px solid transparent;
  line-height: 1.7;
}
.invoice-status.is-paid,
.quote-status.is-paid,
.paid {
  background: rgba(58, 214, 107, .12);
  border-color: rgba(58, 214, 107, .38);
  color: #63e08d;
}
.invoice-status.is-unpaid,
.quote-status.is-unpaid,
.due {
  background: var(--orange-tint-soft);
  border-color: var(--orange-tint-border);
  color: var(--orange-light);
}
.invoice-status.is-not-due,
.is-not-due {
  background: rgba(58, 160, 255, .10);
  border-color: rgba(58, 160, 255, .32);
  color: var(--blue-light);
}

/* ---------- 6. buttons ---------- */
/* Boldness spent in one place: the primary action glows, everything else
   stays quiet, so the paying action is never ambiguous. */
.btn,
.small-btn,
.secondary-btn,
.login-btn {
  border-radius: 9px;
  transition: transform .15s ease, box-shadow .15s ease, background .15s ease, border-color .15s ease;
}
.btn:hover,
.login-btn:hover { transform: translateY(-1px); }
.btn.orange:hover { box-shadow: 0 8px 22px -6px rgba(255, 128, 0, .55); }
.btn.blue:hover,
.login-btn:hover { box-shadow: 0 8px 22px -6px rgba(58, 160, 255, .5); }
.secondary-btn:hover { border-color: var(--steel); color: var(--white); }
.small-btn { min-height: 38px; }
.date-btn,
.slot-btn,
.wo-urgency-btn {
  min-height: 44px;
  border-radius: 10px;
  transition: border-color .15s ease, background .15s ease, transform .12s ease;
}
.date-btn:hover,
.slot-btn:hover,
.wo-urgency-btn:hover { border-color: var(--blue-light); }
.date-btn.is-selected,
.slot-btn.is-selected,
.wo-urgency-btn.is-selected {
  border-color: var(--orange);
  background: var(--orange-tint-soft);
}

/* ---------- 6b. Redesigned buttons (2026-09-06) ----------
   Matches the glass-highlight language just added to the tool suite's
   .primary-btn/.secondary-btn (styles-tools.css). The portal's own real
   CTAs -- Pay now, Request Work, Call, Text Us, Sign In -- are .btn.orange
   / .btn.blue (defined in styles.css for the public marketing site, a
   different class from the tool suite's .primary-btn), so they'd
   otherwise be left out of a "cleaner, amazing buttons" pass entirely.
   Scoped to .portal-page (and :has(.login-box), for login.html/
   set-password.html, which carry no body class) so the public site's
   own buttons are untouched -- same scoping already used for section 1b
   above, for the same reason. */
body.portal-page .btn.orange,
body:has(.login-box) .btn.orange {
  background-image: linear-gradient(165deg, rgba(255,255,255,.34), rgba(255,255,255,0) 44%),
    linear-gradient(135deg, #ffb347, #ff8000 60%, #c96400);
  box-shadow: 0 1px 0 rgba(255,255,255,.3) inset, 0 -1px 0 rgba(0,0,0,.16) inset, 0 8px 20px -6px rgba(255,128,0,.5);
}
body.portal-page .btn.blue,
body:has(.login-box) .btn.blue {
  background-image: linear-gradient(165deg, rgba(255,255,255,.34), rgba(255,255,255,0) 44%),
    linear-gradient(135deg, #7ec4ff, #3aa0ff 60%, #1f5f9e);
  box-shadow: 0 1px 0 rgba(255,255,255,.3) inset, 0 -1px 0 rgba(0,0,0,.16) inset, 0 8px 20px -6px rgba(58,160,255,.5);
}
body.portal-page .secondary-btn,
body.portal-page .small-btn,
body:has(.login-box) .secondary-btn,
body:has(.login-box) .small-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.portal-page .portal-icon-btn {
  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%);
}
/* home.html's "Need Something?" card (Request Work / Call / Text Us) is a
   fourth, page-local button pattern -- plain <a> tags styled only by
   .help-actions a, inline in home.html, not any of the shared classes
   above. Same treatment, so it doesn't look like the one card that
   missed this pass. */
body.portal-page .help-actions a {
  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);
  transition: border-color .15s ease, color .15s ease, transform .15s ease, box-shadow .15s ease;
}
body.portal-page .help-actions a:hover {
  transform: translateY(-1px);
  box-shadow: 0 1px 0 rgba(255,255,255,.06) inset, 0 5px 14px -6px rgba(0,0,0,.4);
}
@media (prefers-reduced-motion: reduce) { body.portal-page .help-actions a { transition: none; } body.portal-page .help-actions a:hover { transform: none; } }
/* That card offered three visually identical actions, so it had no
   default -- nothing told a client which one to take. "Request Work" is
   the one the card's first paragraph is actually about; calling is the
   urgent-only exception named in the second. So it gets the portal's
   established primary treatment, the same blue as Pay Now, Approve,
   Send Request and Sign In, and Call/Text stay secondary.
   It lives here rather than in home.html's own <style> because the rule
   just above already owns this button pattern at the same specificity
   and loads later -- a page-local .help-actions a.primary would tie and
   lose, which is exactly what happened on the first attempt. */
body.portal-page .help-actions a.primary {
  background-image: linear-gradient(165deg, rgba(255,255,255,.34), rgba(255,255,255,0) 44%),
    linear-gradient(135deg, #7ec4ff, #3aa0ff 60%, #1f5f9e);
  border-color: transparent;
  color: #04101c;
  box-shadow: 0 1px 0 rgba(255,255,255,.3) inset, 0 -1px 0 rgba(0,0,0,.16) inset, 0 8px 20px -6px rgba(58,160,255,.5);
}
body.portal-page .help-actions a.primary:hover {
  border-color: transparent;
  color: #04101c;
  box-shadow: 0 1px 0 rgba(255,255,255,.3) inset, 0 -1px 0 rgba(0,0,0,.16) inset, 0 10px 24px -6px rgba(58,160,255,.55);
}

/* Note: these message boxes are styled only via :not(:empty). They exist
   in the markup at all times and are filled in by script, so giving them
   a background unconditionally renders an empty coloured strip on a clean
   form -- which is exactly what happened on the first pass here. */

/* ---------- 7. the login screen ---------- */
/* First impression for a client, and it was plain text floating on black.
   Now a real card, which is what people expect of anything asking for a
   password. */
.login-box {
  background: linear-gradient(168deg, var(--bg-panel-2), var(--bg-panel));
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 38px 34px;
  box-shadow: 0 24px 60px -28px rgba(0, 0, 0, .8), var(--shadow-resting);
}
.login-logo { filter: drop-shadow(0 8px 20px rgba(0, 0, 0, .5)); }
.login-title {
  font-family:var(--font-display);
  letter-spacing: .6px;
}
.login-sub { color: var(--text-dim); }
.login-field input {
  min-height: 46px;
  border-radius: 9px;
}
.login-btn { min-height: 48px; }
.login-error:not(:empty) {
  border-left: 3px solid var(--orange);
  background: var(--orange-tint-soft);
  border-radius: 0 6px 6px 0;
  padding: 10px 13px;
}
.login-success:not(:empty) {
  border-left: 3px solid #3ad66b;
  background: rgba(58, 214, 107, .1);
  border-radius: 0 6px 6px 0;
  padding: 10px 13px;
}
@media (max-width: 480px) {
  .login-box { padding: 28px 22px; }
}

/* ---------- 8. forms ---------- */
.set-field input,
.set-field select,
.set-field textarea,
.wo-field input,
.wo-field select,
.wo-field textarea {
  min-height: 46px;
  border-radius: 9px;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.wo-field textarea,
.set-field textarea { min-height: 104px; }
.wo-hint,
.set-card-sub,
.wo-urgency-sub { color: var(--text-dim); line-height: 1.55; }
/* Bug fix (2026-09-07), found by rendering the real form rather than
   reading the CSS: every .wo-hint on this page was displaying in ALL
   CAPS with 1px letter-spacing -- including two full sentences of 104
   and 121 characters ("What's happening, how long it's been going on,
   anything you've already tried...").
   Cause: styles.css carries a bare, unscoped `label { text-transform:
   uppercase; letter-spacing: 1px; }` written for the public site's
   short contact-form labels. The portal loads that same stylesheet,
   each .wo-hint is a <span> nested INSIDE its <label>, and both of
   those properties inherit -- so the hints picked up casing meant for
   two-word labels. work-orders.html's own rule resets font-weight on
   these spans (so the author knew they inherit label styling) but
   never reset casing.
   Deliberately narrow: the labels themselves stay uppercase, which is
   a real, consistent design choice across this app. Only the nested
   explanatory sentences -- which are body copy, not labels -- go back
   to sentence case, where word shapes are legible again. */
label .wo-hint,
label .login-hint {
  text-transform: none;
  letter-spacing: normal;
}
/* Requirement text under a login/set-password field label. Nested in the
   label, so it inherits that rule's weight as well as its casing -- both
   have to be reset for it to read as the body copy it is. */
.login-hint {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--text-dim);
  margin-top: 3px;
}
.wo-section-title,
.set-card-title {
  font-family:var(--font-ui);
  font-weight: 600;
  letter-spacing: .4px;
}

/* ---------- 9. empty states ---------- */
/* An empty list should read as "nothing here yet", not as a page that
   failed to load. */
.empty-state,
.slots-empty {
  border: 1px dashed var(--border);
  border-radius: 12px;
  padding: 30px 24px;
  text-align: center;
  color: var(--text-dim);
  background: rgba(255, 255, 255, .015);
}

/* ---------- 10. collapsible cards ---------- */
/* The invoice and quote cards' "View details" is a bare <details>/<summary>
   carrying only an inline cursor/size/colour, so it rendered with the
   browser's own black disclosure triangle -- the one raw platform widget
   left anywhere in this app, sitting a few hundred pixels from Settings'
   custom chevron doing the identical job. Same affordance, same rotation,
   same easing, so a client learns one gesture rather than two.
   Scoped to .portal-page so nothing on the public site is touched. */
body.portal-page details > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 32px;
}
body.portal-page details > summary::-webkit-details-marker { display: none; }
body.portal-page details > summary::before {
  content: "";
  width: 6px;
  height: 6px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform .22s ease;
  flex-shrink: 0;
  /* The glyph is drawn from two borders, so it sits optically low inside
     its own box; nudge it back onto the text baseline. */
  margin-top: -3px;
}
body.portal-page details[open] > summary::before {
  transform: rotate(45deg);
  margin-top: -1px;
}
body.portal-page details > summary:hover { color: var(--orange-light); }
@media (prefers-reduced-motion: reduce) {
  body.portal-page details > summary::before { transition: none; }
}

.set-card-header { min-height: 52px; }
/* The collapsed rows were doing the opposite of what collapsing them was
   for. Settings was made collapsible to cut the scroll length ("it still
   looks a little much"), but a collapsed card still measured ~106px --
   18px card padding + the 52px header tap target + 18px padding + 18px
   margin -- to show one line of 15px text. Seven of those is ~740px of
   almost entirely empty panel before you have opened anything.
   The 52px min-height on the header already guarantees a comfortable tap
   target on its own, so the card's own vertical padding is doing nothing
   for reachability while collapsed. Trim it (and the gap) only in that
   state: ~76px per row instead of ~106px, about 210px less scrolling on
   the default view, with the tap target and the expanded layout both
   completely unchanged. */
.set-card.is-collapsed {
  padding-top: 6px;
  padding-bottom: 6px;
  margin-bottom: 12px;
}
.set-card-chevron { transition: transform .22s ease; }
.set-card.is-collapsed .set-card-chevron { transform: rotate(-90deg); }

/* ---------- 11. modals ---------- */
.report-bug-modal,
.payment-modal {
  border-radius: 14px;
  box-shadow: 0 30px 70px -24px rgba(0, 0, 0, .8);
}
.report-bug-title { font-family:var(--font-display); letter-spacing: .5px; }
.report-bug-sub { color: var(--text-dim); }
.report-bug-error:not(:empty) {
  border-left: 3px solid var(--orange);
  background: var(--orange-tint-soft);
  border-radius: 0 6px 6px 0;
  padding: 10px 13px;
}
.report-bug-success:not(:empty) {
  border-left: 3px solid #3ad66b;
  background: rgba(58, 214, 107, .1);
  border-radius: 0 6px 6px 0;
  padding: 10px 13px;
}
.report-bug-link {
  color: var(--text-dim);
  transition: color .15s ease;
  min-height: 44px;
}
.report-bug-link:hover { color: var(--blue-light); }

/* ---------- 12. loading skeletons ---------- */
.skeleton-line { border-radius: 6px; }
.skeleton-card { border-radius: 12px; }

/* ---------- 13. focus ---------- */
/* One consistent focus treatment across every interactive thing here,
   matching the public site's blue ring. */
.portal-page a:focus-visible,
.portal-page button:focus-visible,
.portal-page input:focus-visible,
.portal-page select:focus-visible,
.portal-page textarea:focus-visible {
  outline: 2px solid var(--blue-light);
  outline-offset: 2px;
  box-shadow: 0 0 0 5px rgba(58, 160, 255, .16);
}

/* ---------- 14. reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  .btn:hover,
  .login-btn:hover,
  .invoice-card:hover,
  .job-card:hover,
  .quote-card:hover,
  .wo-card:hover { transform: none; }
}

/* ---------- 15. invoice hierarchy ---------- */
/* The amount owed is the single most important number on this screen, so
   it should read first. Everything else on the card is supporting detail. */
.invoice-total {
  font-family:var(--font-display);
  font-size: 21px;
  letter-spacing: .4px;
  color: var(--white);
  line-height: 1.15;
}
.invoice-number {
  font-family:var(--font-ui);
  font-weight: 600;
  letter-spacing: .8px;
  color: var(--white);
}
.invoice-desc { color: var(--text-dim); line-height: 1.5; }
.invoice-status { margin-top: 6px; }

/* Something that still needs paying gets a standing accent, so a client
   scanning a long list sees what needs action without reading every card.
   :has() is progressive enhancement -- without it the card simply keeps
   its normal border, which is the current behaviour anyway. */
.invoice-card:has(.invoice-status.is-unpaid) {
  border-left: 3px solid var(--orange);
}
.invoice-card:has(.invoice-status.is-paid) {
  opacity: .92;
}
.invoice-card:has(.invoice-status.is-paid):hover { opacity: 1; }

/* ---------- 16. printing ---------- */
/* People do print and save invoices. Left alone, a dark-themed app prints
   as a wall of ink with navigation and buttons in the middle of it. */
@media print {
  body.portal-page {
    background: #fff !important;
    background-image: none !important;
    color: #000 !important;
  }
  .portal-nav,
  .portal-header-actions,
  .report-bug-link,
  .ptr-indicator,
  .offline-banner,
  .btn,
  .small-btn,
  .secondary-btn,
  .portal-icon-btn { display: none !important; }
  .invoice-card,
  .quote-card,
  .job-card,
  .wo-card {
    border: 1px solid #bbb !important;
    box-shadow: none !important;
    background: #fff !important;
    color: #000 !important;
    break-inside: avoid;
    page-break-inside: avoid;
    margin-bottom: 12px;
  }
  .invoice-number,
  .invoice-total,
  .portal-title { color: #000 !important; }
  .invoice-desc { color: #333 !important; }
  .invoice-status {
    border: 1px solid #666 !important;
    background: transparent !important;
    color: #000 !important;
  }
}

/* ---------- 17. small screens ---------- */
/* Most clients open this on a phone, usually to settle one bill. */
@media (max-width: 560px) {
  .invoice-card,
  .quote-card,
  .job-card,
  .wo-card { padding: 16px 14px; }
  .invoice-total { font-size: 19px; }
  .portal-title { font-size: 19px; }
  .btn,
  .secondary-btn { min-height: 46px; }
}

/* ---------- 18. portal home ---------- */
/* The home screen is a set of counts. Counts should be the thing you see,
   with the label underneath, rather than both competing at one size. */
.home-greeting {
  font-family:var(--font-display);
  letter-spacing: .5px;
  color: var(--white);
}
.home-intro { color: var(--text-dim); line-height: 1.6; }
.home-card {
  transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}
.home-card:hover {
  border-color: var(--blue-dark);
  box-shadow: var(--shadow-hover);
}
.home-card-count {
  font-family:var(--font-display);
  font-size: 30px;
  line-height: 1.05;
  color: var(--orange-light);
  letter-spacing: .5px;
}
.home-card-label {
  font-family:var(--font-ui);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--white);
}
.home-card-desc { color: var(--text-dim); font-size: 13.5px; line-height: 1.5; }

/* Anything needing the client's attention should look like it does. */
.attention-card { border-left-width: 3px; }
.attention-title {
  font-family:var(--font-ui);
  font-weight: 600;
  letter-spacing: .4px;
  color: var(--white);
}
.attention-row { border-radius: 10px; }
.attention-row-text { color: var(--text-dim); line-height: 1.5; }

.section-title {
  font-family:var(--font-ui);
  font-weight: 600;
  font-size: 12.5px;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: var(--text-dim);
}

.help-card { border-left: 3px solid var(--blue); }
.help-actions { display: flex; flex-wrap: wrap; gap: 10px; }

/* ---------- 19. loading feels intentional ---------- */
/* A static grey block reads as broken; a slow sweep reads as loading. */
.skeleton-line,
.skeleton-card {
  position: relative;
  overflow: hidden;
}
.skeleton-line::after,
.skeleton-card::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .06), transparent);
  animation: portalShimmer 1.4s ease-in-out infinite;
}
@keyframes portalShimmer {
  100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .skeleton-line::after,
  .skeleton-card::after { animation: none; display: none; }
}

/* ---------- 20. the update notice ---------- */
/* Shown only when a genuinely newer version is waiting. Sits above the
   bottom navigation and stays clear of the home indicator on a phone. */
.portal-update-banner {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  z-index: 90;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 12px 12px 16px;
  border-radius: 12px;
  background: linear-gradient(160deg, var(--bg-panel-2), var(--bg-panel));
  border: 1px solid var(--blue-dark);
  box-shadow: 0 18px 44px -18px rgba(0, 0, 0, .85);
  animation: portalUpdateIn .28s ease-out;
}
@keyframes portalUpdateIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.portal-update-text {
  flex: 1;
  font-size: 14px;
  line-height: 1.45;
  color: var(--text);
}
.portal-update-btn {
  flex-shrink: 0;
  min-height: 40px;
  padding: 9px 16px;
  font-size: 13px;
}
.portal-update-dismiss {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.portal-update-dismiss:hover { color: var(--white); border-color: var(--steel); }
@media (max-width: 420px) {
  .portal-update-banner { flex-wrap: wrap; }
  .portal-update-text { flex: 1 1 100%; }
  .portal-update-btn { flex: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .portal-update-banner { animation: none; }
}

/* ---------- 21. installed-app treatment ---------- */
/* When it is launched from a home screen there is no browser chrome, so
   the app owns the whole screen including the notch and the home
   indicator. Without this the header sits under the status bar and the
   bottom nav sits under the home bar on an iPhone. */
@media (display-mode: standalone) {
  body.portal-page {
    padding-top: env(safe-area-inset-top, 0px);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .portal-nav {
    padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .portal-header {
    padding-left: max(16px, env(safe-area-inset-left, 0px));
    padding-right: max(16px, env(safe-area-inset-right, 0px));
  }
  /* Nothing to go "back" to in a standalone app, so links that only make
     sense in a browser tab should not be the loudest thing on screen. */
  .report-bug-link { opacity: .75; }
}

/* Long-press on a control in an installed app should feel like an app,
   not like a web page offering to select text. */
.portal-page .btn,
.portal-page .small-btn,
.portal-page .secondary-btn,
.portal-page .portal-nav a,
.portal-page .portal-icon-btn {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  user-select: none;
}

/* Anchor jumps should not tuck the target under the sticky header. */
.portal-page :target { scroll-margin-top: 84px; }

/* ---------- 22. quote + work-order status colours ---------- */
/* The badge SHAPE was defined earlier for every status type, but only the
   invoice states got colours -- so a quote badge inherited the pill and
   the transparent border with nothing behind it. Each page does define
   its own colours inline, but relying on that meant the two lists looked
   like different products. These make every status read the same way
   wherever it appears. */
.quote-status.is-pending {
  background: var(--orange-tint-soft);
  border-color: var(--orange-tint-border);
  color: var(--orange-light);
}
.quote-status.is-approved {
  background: rgba(58, 214, 107, .12);
  border-color: rgba(58, 214, 107, .38);
  color: #63e08d;
}
.quote-status.is-declined,
.wo-status.is-declined {
  background: rgba(255, 255, 255, .05);
  border-color: var(--border);
  color: var(--text-dim);
}
.wo-status.is-submitted {
  background: var(--orange-tint-soft);
  border-color: var(--orange-tint-border);
  color: var(--orange-light);
}
.wo-status.is-reviewing,
.wo-status.is-quoted {
  background: rgba(58, 160, 255, .12);
  border-color: rgba(58, 160, 255, .36);
  color: var(--blue-light);
}
.wo-status.is-scheduled,
.wo-status.is-completed {
  background: rgba(58, 214, 107, .12);
  border-color: rgba(58, 214, 107, .38);
  color: #63e08d;
}

/* ---------- 23. where is my request? ---------- */
/* "What's happening with the thing I asked for" is the question this page
   exists to answer, and a badge alone only says the current word. A thin
   track along the bottom of each card shows how far along the request is
   at a glance: submitted, reviewing, quoted, scheduled, done.

   Driven entirely off the status badge the page already renders, via
   :has(), so no markup changes and no JS. Browsers without :has() simply
   see no track, which is exactly today's behaviour. */
.wo-card {
  position: relative;
  overflow: hidden;
}
.wo-card::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 0;
  background: linear-gradient(90deg, var(--orange-dark), var(--orange-light));
  transition: width .5s cubic-bezier(.22, .61, .36, 1);
}
.wo-card:has(.wo-status.is-submitted)::after { width: 20%; }
.wo-card:has(.wo-status.is-reviewing)::after { width: 40%; }
.wo-card:has(.wo-status.is-quoted)::after    { width: 60%; }
.wo-card:has(.wo-status.is-scheduled)::after {
  width: 80%;
  background: linear-gradient(90deg, var(--blue-dark), var(--blue-light));
}
.wo-card:has(.wo-status.is-completed)::after {
  width: 100%;
  background: linear-gradient(90deg, #2e8b57, #63e08d);
}
/* A declined request is finished, but not an achievement -- full width,
   deliberately grey, so it never reads as success. */
.wo-card:has(.wo-status.is-declined)::after {
  width: 100%;
  background: var(--border);
}
@media (prefers-reduced-motion: reduce) {
  .wo-card::after { transition: none; }
}

/* ---------- 24. printing quotes and work orders ---------- */
/* Invoices already printed cleanly; these did not, and a quote is exactly
   the kind of thing someone prints to think about or show a spouse. */
@media print {
  .quote-status,
  .wo-status {
    border: 1px solid #666 !important;
    background: transparent !important;
    color: #000 !important;
  }
  .wo-card::after { display: none !important; }
  .wo-section-title,
  .wo-hint,
  .set-card-title { color: #000 !important; }
  .schedule-panel,
  .schedule-toggle,
  .slots-grid { display: none !important; }
}
