/* ============================================================
   assets/public.css — client-facing design system
   ============================================================

   FE-1 (Stage 4, 2026-05-23). Single stylesheet shared by every page
   the customer sees: portal_login, portal_forgot, portal_reset,
   portal_set_password, public_portal, public_doc, public_profile,
   public_statement, public_approve, public_comment, public_sign,
   public_verify.

   Brand reference (Araneae Communication Networks Installation L.L.C):
     - Spider-web mark, cyan + navy palette (assets/branding/logo*.png)
     - Cyan = energy, primary accent, links, CTAs
     - Navy = trust, headers, primary text

   Customers see this on phones, tablets, work laptops. Constraints:
     - Light theme (prints clean, works in webmail previews, familiar)
     - Mobile-first responsive — many customers open from a phone
     - Self-contained (no admin app styles, no sidebar, no auth)
     - Brand-forward at the top (logo strip), unobtrusive everywhere else
*/

/* ── Brand tokens ────────────────────────────────────────────── */
:root {
  /* ONE portal content width (2026-07-03, owner request "all screens same
     width, one design token"). Every portal surface — the client document
     shell AND the task-app screens (home / list / detail / calendar / timeline)
     — caps its content at this token so navigating between them never shifts
     the content edges. */
  --portal-content:    980px;
  /* Type scale — the DESIGN's exact scale (styles.css), not the admin's. The
     portal is the redesigned surface, so its .fs-* utilities resolve to the
     mock's values (this :root only governs the portal; the admin keeps its own). */
  --fs-2xs:10px; --fs-xs:11.5px; --fs-sm:13px; --fs-base:13px; --fs-md:14px;
  --fs-lg:16px; --fs-xl:20px; --fs-2xl:26px; --fs-3xl:34px; --fs-4xl:48px;
  --lh-tight:1.25; --lh-snug:1.4; --lh-base:1.5;

  /* ── DESIGN TOKENS (lifted from the handoff styles.css :root) ──────────────
     The canonical .ax- design system. The portal's legacy names below (--brand-*,
     --surface-alt, --border, --radius, --cta …) alias to THESE, so there's one
     source of truth and every value matches the mock exactly. */
  --accent:        #1AB7EA;              /* brand cyan — fills, with dark ink */
  --accent-bright: #0D84B6;              /* links / hover text (AA on white) */
  --accent-dim:    rgba(26,183,234,.14);
  --accent-line:   rgba(26,183,234,.42);
  --line:  rgba(20,32,58,.10);
  --line2: rgba(20,32,58,.17);
  --text2: #52607A;
  --text3: #8591A8;
  --green: #15A34A; --green-dim: rgba(21,163,74,.13);
  --amber: #C2740A; --amber-dim: rgba(194,116,10,.13);
  --red:   #DC2626; --red-dim:   rgba(220,38,38,.11);
  /* Spacing (4px base) */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px;
  /* Radius */
  --r-sm:8px; --r:12px; --r-lg:18px; --r-pill:999px;
  /* Shadows (floating layers only — cards rest flat) */
  --sh-md:  0 6px 20px rgba(20,32,58,.10);
  --sh-lg:  0 20px 50px rgba(20,32,58,.16);
  --sh-cyan:0 0 0 4px var(--accent-dim);
  --ff: 'IBM Plex Sans', 'Segoe UI', system-ui, -apple-system, sans-serif;
  --fm: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Primary brand pair — from logo.png (navy) and logo_light.png (cyan).
     If different deployments need re-skinning, this block is the only
     thing to change. Everything below references these vars.

     V2 (2026-06-09, portal redesign): fintech-grade "Trust & Authority"
     refresh. Brand hues kept; neutrals moved to the slate scale, CTA color
     moved to the DARK cyan (white-on-#1DB6F0 was ~2.2:1 — failed WCAG AA;
     white-on-#0676A1 passes), IBM Plex Sans, deeper layered shadows, and a
     softer radius scale. */
  /* ── LIGHT REBUILD (2026-07-06) ───────────────────────────────────────
     my.araneae.net portal redesign — LIGHT theme: white chrome, a navy-tinted
     grey page (#eef2f9), navy ink (#14203a), the brand's own web-emblem cyan
     (#1ab7ea) used ONLY as a fill (with dark ink); links/hover text use the
     darkened cyan #0d84b6 which passes AA on white. Compact spacing. Documents
     render as elevated white sheets. The whole portal is token-driven, so these
     :root values re-skin every client + field screen coherently.

     Contrast: --brand-cyan is the raw emblem cyan for FILLS only (dark ink on
     top, --accent-ink). --brand-cyan-dark is the AA-safe link/badge TEXT colour
     on white (#0d84b6). */
  --brand-cyan:        #1AB7EA;   /* accent — emblem cyan, fills only */
  --brand-cyan-hover:  #0D84B6;   /* darker cyan for hover/links on white */
  --brand-cyan-dark:   #0D84B6;   /* AA link/badge text on white */
  --brand-cyan-deep:   #0A6F99;
  --brand-cyan-tint:   rgba(26,183,234,.14);  /* accent-dim — tint bg */
  --brand-cyan-soft:   rgba(26,183,234,.42);  /* accent-line */
  --brand-navy:        #0F1B3F;
  --brand-navy-soft:   #1E2952;
  --brand-navy-deep:   #0A1330;
  --brand-navy-tint:   #F2F4FB;

  --accent-ink:        #04263A;   /* text/icon ON cyan fills */

  /* CTA — emblem cyan fill + dark ink (AA: #04263a on #1ab7ea ≈ 8:1). */
  --cta:               #1AB7EA;
  --cta-hover:         #0D84B6;

  /* Surfaces — navy-tinted greys, white chrome. */
  --bg:                #EEF2F9;
  --surface:           #FFFFFF;
  --surface-alt:       #EEF1F7;
  --surface2:          #EEF1F7;   /* inputs, insets, row hover */
  --surface3:          #E1E7F1;   /* hovers, chips, active */
  --surface-strong:    #FFFFFF;
  --paper:             #FFFFFF;   /* elevated white document paper */
  --paper-ink:         #16203A;

  /* Text — navy ink scale. */
  --text:              #14203A;
  --text-1:            #14203A;
  --text-2:            #52607A;
  --text-3:            #8591A8;
  --text-on-brand:     #FFFFFF;
  /* X6: admin-vocabulary aliases so a SHARED component (e.g. form_field) that
     uses --text2/--text3 resolves correctly here too (admin uses non-hyphen,
     portal uses hyphen — both names now work in both themes). */
  --text2:             var(--text-2);
  --text3:             var(--text-3);

  /* Borders — hairlines on navy. */
  --border:            rgba(20,32,58,.10);
  --border-strong:     rgba(20,32,58,.17);

  /* Semantic — AA-tuned signal text on a ~13% tinted fill. Status is always
     paired with a text label in the pill, never colour alone (WCAG). */
  --ok:                #15A34A;
  --ok-bg:             rgba(21,163,74,.13);
  --ok-text:           #0F7A37;
  --warn:              #C2740A;
  --warn-bg:           rgba(194,116,10,.13);
  --warn-text:         #9A5B08;
  --danger:            #DC2626;
  --danger-bg:         rgba(220,38,38,.11);
  --danger-text:       #B91C1C;
  --info-bg:           rgba(26,183,234,.14);
  --info-text:         #0D84B6;

  /* Effects — radii aliased to the design's --r/--r-sm (12/8), not 14/10. */
  --radius:            var(--r);       /* 12px, per the mock's .ax-card */
  --radius-sm:         var(--r-sm);    /* 8px, per the mock's buttons/inputs */
  --radius-xs:         6px;
  --shadow-sm:         0 1px 2px rgba(20,32,58,.05), 0 1px 3px rgba(20,32,58,.05);
  --shadow-md:         var(--sh-md);
  --shadow-lg:         var(--sh-lg);

  /* Motion */
  --ease:              cubic-bezier(.2, .7, .3, 1);
  --speed:             .16s;

  /* Type — alias to the design's --ff/--fm. */
  --font:              var(--ff);
  --font-mono:         var(--fm);
}

/* ── Reset + base ────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
html { -webkit-text-size-adjust: 100% }
body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  font-size: 14.5px;
  line-height: 1.55;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
img { max-width: 100%; height: auto; display: block }
a { color: var(--brand-cyan-dark); text-decoration: none }
a:hover { color: var(--brand-cyan-hover); text-decoration: underline }
button, input, select, textarea { font-family: inherit; font-size: inherit; color: inherit }
:focus-visible { outline: 2px solid var(--brand-cyan); outline-offset: 2px; border-radius: 4px }

/* ── Page shell ──────────────────────────────────────────────── */

/* Top brand strip — every page leads with this. Navy bar, cyan logo,
   company name. Anchors the brand on every screen the customer sees. */
.brand-bar {
  background: var(--surface);
  color: var(--text);
  padding: 12px 24px;
  display: flex;
  align-items: center;
  gap: 14px;
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  z-index: 10;
  border-bottom: 1px solid var(--border);
}
.brand-bar-logo {
  height: 36px;
  width: auto;
  display: block;
}
.brand-bar-text {
  flex: 1;
  min-width: 0;
}
.brand-bar-name {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.2px;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.brand-bar-sub {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 2px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.brand-bar-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-shrink: 0;
  font-size: 12.5px;
}
.brand-bar-actions a {
  color: var(--text-2);
  opacity: 1;
  padding: 6px 10px;
  border-radius: var(--radius-xs);
}
.brand-bar-actions a:hover {
  background: var(--surface2);
  color: var(--brand-cyan-dark);
  text-decoration: none;
}

/* ── Brand-bar account menu (2026-06-11 redesign) — fills the navy bar's
   previously-empty right side with a company chip + a CSS-only dropdown
   (Statement · Update details · Sign out). <details>/<summary>, no JS. ── */
.acct-menu { position: relative }
.acct-menu > summary { list-style: none; cursor: pointer }
.acct-menu > summary::-webkit-details-marker { display: none }
.acct-menu-btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 12px 5px 6px; border-radius: 999px;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--text); max-width: 230px;
  transition: background 0.14s, border-color 0.14s;
}
.acct-menu-btn:hover { background: var(--surface3) }
.acct-menu[open] .acct-menu-btn { background: var(--surface3); border-color: var(--border-strong) }
.acct-menu-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--brand-cyan); color: var(--accent-ink);
  font-size: 11px; font-weight: 700; letter-spacing: 0.3px; flex-shrink: 0;
}
.acct-menu-caret { font-size: 9px; opacity: 0.8; flex-shrink: 0 }
.acct-menu-pop {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 30;
  min-width: 210px; padding: 6px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-lg);
}
.acct-menu-item {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: var(--radius-xs);
  font-size: 13px; color: var(--text-2) !important; opacity: 1 !important;
  text-decoration: none;
}
.acct-menu-item:hover { background: var(--brand-cyan-tint); color: var(--brand-cyan-dark) !important; text-decoration: none }
.acct-menu-item--signout { color: var(--danger) !important }
.acct-menu-item--signout:hover { background: var(--danger-bg); color: var(--danger-text) !important }
/* SVG glyphs in the account menu — muted, sized to a 16px box, and inheriting
   the row's hover/danger tone via currentColor (phase93 emoji → icon swap). */
.acct-menu-ic { display: inline-flex; flex: 0 0 auto; font-size: 15px; color: var(--text-3) }
.acct-menu-item:hover .acct-menu-ic { color: var(--brand-cyan-dark) }
.acct-menu-item--signout .acct-menu-ic { color: var(--danger) }

/* Main content shell */
.shell {
  flex: 1;
  max-width: var(--portal-content);
  width: 100%;
  margin: 0 auto;
  padding: 16px 20px 28px;   /* DENSITY (2026-06-12): less top gap → docs higher */
}
.shell-narrow {
  max-width: 460px;
}

/* Footer strip */
.brand-footer {
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: 18px 20px;
  text-align: center;
  font-size: 11.5px;
  color: var(--text-3);
}
.brand-footer-line {
  margin: 2px 0;
}
.brand-footer strong {
  color: var(--text-2);
  font-weight: 500;
}
.brand-footer a {
  color: var(--text-2);
}

/* ── Typography helpers ──────────────────────────────────────── */
.h1 { font-size: 22px; font-weight: 700; color: var(--text); letter-spacing: -0.3px; line-height: 1.25 }
.h1 small { font-weight: 400; color: var(--text-2); font-size: 13px; display: block; margin-top: 4px; letter-spacing: 0 }
.h2 { font-size: 17px; font-weight: 600; color: var(--text); letter-spacing: -0.1px }
.h3 { font-size: 14px; font-weight: 600; color: var(--text) }
.muted { color: var(--text-3) }
.label-up {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-weight: 600;
  color: var(--text-2);
}
.mono { font-family: var(--font-mono) }
.tabular { font-variant-numeric: tabular-nums }
.center { text-align: center }

/* ── Card ────────────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* Cards rest FLAT on the light canvas (border + a barely-there ambient
     shadow), matching the mock's .ax-card — elevation comes from the surface
     step, not a drop shadow. */
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}
.card + .card { margin-top: 16px }
.card-pad { padding: 24px }
/* Card header. NOTE: the portal loads ONLY this stylesheet (not core.css), so
   .card-header / .card-title — used by some portal cards (Projects, Outstanding
   by age) — MUST be styled here too, or those titles render unstyled and sit
   flush against the card edge. Keep both class names visually identical. */
.card-head,
.card-header {
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-alt);
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.card-title { font-size: 13px; font-weight: 600; color: var(--text) }
/* The right-hand note in a header (e.g. "1,234 total · hide") stays light so it
   reads as secondary against the 600-weight title. */
.card-head .dim,
.card-header .dim { font-weight: 400; font-size: 12px }

/* ── KPI row (portal summary) ────────────────────────────────── */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}
/* V2: hero stat cards — top accent bar, larger tabular figures, gentle
   lift on hover. The accent doubles as the semantic signal (ok/warn/danger)
   and each card keeps a text-colour echo so colour isn't the only cue. */
.kpi {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 18px 20px 16px;
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition: box-shadow var(--speed) var(--ease), transform var(--speed) var(--ease);
}
.kpi::before {
  content: '';
  position: absolute;
  inset: 0 auto auto 0;
  width: 100%;
  height: 3px;
  background: linear-gradient(90deg, var(--brand-cyan), var(--brand-cyan-dark));
}
.kpi:hover { box-shadow: var(--shadow-md); transform: translateY(-1px) }
.kpi-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 8px;
}
.kpi-value {
  font-size: 26px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.5px;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.kpi--ok::before     { background: linear-gradient(90deg, var(--ok), #34D399) }
.kpi--ok .kpi-value  { color: var(--ok-text) }
.kpi--warn::before   { background: linear-gradient(90deg, var(--warn), #F59E0B) }
.kpi--warn .kpi-value{ color: var(--warn-text) }
.kpi--danger::before { background: linear-gradient(90deg, var(--danger), #F87171) }
.kpi--danger.kpi-value,
.kpi--danger .kpi-value { color: var(--danger-text) }
.kpi--accent::before    { background: linear-gradient(90deg, var(--brand-cyan), var(--brand-cyan-soft)) }
.kpi--accent .kpi-value { color: var(--brand-cyan-dark) }

/* ── Buttons ────────────────────────────────────────────────── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 18px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
  color: var(--text);
  background: var(--surface);
  font-size: 13.5px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  min-height: 40px;
  line-height: 1.2;
  transition: background var(--speed) var(--ease), border-color var(--speed) var(--ease),
              color var(--speed) var(--ease), box-shadow var(--speed) var(--ease),
              transform var(--speed) var(--ease);
}
.btn:hover {
  background: var(--surface-alt);
  border-color: var(--cta);
  color: var(--cta);
  text-decoration: none;
}
.btn:active { transform: scale(.985) }
/* V2: primary CTA uses the AA-contrast dark cyan (white on #1DB6F0 was
   ~2.2:1 — illegible in sunlight, failed WCAG; white on --cta passes). */
.btn--primary {
  background: var(--cta);
  color: var(--accent-ink);
  border-color: var(--cta);
  box-shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 12px rgba(26, 183, 234, .18);
}
.btn--primary:hover {
  background: var(--cta-hover);
  border-color: var(--cta-hover);
  color: var(--accent-ink);
  box-shadow: 0 2px 4px rgba(0, 0, 0, .35), 0 6px 16px rgba(26, 183, 234, .25);
}
.btn--ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
}
.btn--ghost:hover {
  background: var(--brand-cyan-tint);
  color: var(--brand-cyan-dark);
  border-color: var(--brand-cyan-soft);
}
.btn--danger {
  background: var(--danger);
  color: #fff;
  border-color: var(--danger);
}
.btn--danger:hover {
  background: #B91C1C;
  border-color: #B91C1C;
  color: #fff;
}
.btn--block {
  width: 100%;
}
.btn--sm {
  padding: 6px 12px;
  font-size: 12px;
  min-height: 30px;
}
.btn--lg {
  padding: 14px 22px;
  font-size: 15px;
  min-height: 48px;
}
.btn:disabled, .btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Submit-in-progress feedback — assets/public.js adds .is-loading on submit.
   Spinner prepends the label (currentColor → white on primary, navy elsewhere);
   pointer-events:none + the form-level guard block a double-submit. (FE polish 2026-06-07) */
@keyframes portal-spin { to { transform: rotate(360deg); } }
.btn.is-loading {
  position: relative;
  pointer-events: none;
}
.btn.is-loading::before {
  content: "";
  display: inline-block;
  width: 14px;
  height: 14px;
  margin-right: 8px;
  vertical-align: -2px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: portal-spin 0.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .btn.is-loading::before { animation-duration: 1.4s; }
}

/* ── Forms ───────────────────────────────────────────────────── */
.form-row {
  margin-bottom: 14px;
}
.form-row + .form-row { margin-top: 0 }
.form-label {
  display: block;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-2);
  margin-bottom: 5px;
}
.form-input,
.form-textarea,
.form-select {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 11px 13px;
  font-size: 15px;   /* >=16px prevents iOS auto-zoom; 15px is borderline safe */
  color: var(--text);
  outline: none;
  min-height: 44px;
  font-family: var(--font);
  transition: border-color .12s, box-shadow .12s;
}
.form-input:focus,
.form-textarea:focus,
.form-select:focus {
  border-color: var(--brand-cyan);
  box-shadow: 0 0 0 3px rgba(29, 182, 240, 0.18);
}
.form-input[readonly] {
  background: var(--surface-alt);
  color: var(--text-2);
  cursor: not-allowed;
}
.form-help {
  font-size: 11.5px;
  color: var(--text-3);
  margin-top: 4px;
}
.pw-row { position: relative }
.pw-row .form-input { padding-right: 56px }
.pw-show {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: 0;
  color: var(--text-3);
  font-size: 12px;
  cursor: pointer;
  padding: 8px 10px;
}
.pw-show:hover { color: var(--text) }

/* ── Notices / alerts ────────────────────────────────────────── */
.notice {
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  font-size: 13px;
  margin-bottom: 14px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  line-height: 1.5;
  border: 1px solid transparent;
}
.notice-icon {
  font-size: 16px;
  line-height: 1.2;
  flex-shrink: 0;
}
.notice--ok      { background: var(--ok-bg);     color: var(--ok-text);     border-color: rgba(16, 185, 129, 0.3) }
.notice--warn    { background: var(--warn-bg);   color: var(--warn-text);   border-color: rgba(217, 119, 6, 0.3) }
.notice--danger  { background: var(--danger-bg); color: var(--danger-text); border-color: rgba(220, 38, 38, 0.3) }
.notice--info    { background: var(--info-bg);   color: var(--info-text);   border-color: var(--brand-cyan-soft) }

/* ── Tables (documents, payments, statement lines) ──────────── */
.tbl {
  width: 100%;
  border-collapse: collapse;
}
.tbl thead th {
  background: var(--surface-alt);
  color: var(--text-2);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  padding: 11px 16px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.tbl tbody td {
  padding: 14px 16px;
  font-size: 13.5px;
  border-bottom: 1px solid var(--border);
  color: var(--text);
  vertical-align: top;
  font-variant-numeric: tabular-nums;   /* V2: amounts/dates align cleanly */
}
.tbl tbody tr:last-child td { border-bottom: none }
.tbl tbody tr { transition: background var(--speed) var(--ease) }
.tbl tbody tr:hover td { background: var(--brand-cyan-tint) }
.tbl .r { text-align: right }
.tbl .nw { white-space: nowrap }
.tbl .dim { color: var(--text-3) }
.tbl .strong { font-weight: 600; color: var(--text) }

/* ── Status badges (re-uses semantic colours) ──────────────── */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  line-height: 1.5;
  white-space: nowrap;
}
/* V2: leading status dot so state isn't conveyed by colour alone. */
.badge::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: currentColor;
  opacity: .75;
  flex-shrink: 0;
}
.b-draft      { background: var(--surface3); color: var(--text-2) }
.b-sent,
.b-confirmed  { background: var(--brand-cyan-tint); color: var(--brand-cyan-dark) }
.b-paid,
.b-done,
.b-completed  { background: var(--ok-bg); color: var(--ok-text) }
.b-overdue    { background: var(--danger-bg); color: var(--danger-text) }
.b-not_paid,
.b-partial    { background: var(--warn-bg); color: var(--warn-text) }
.b-cancelled,
.b-void,
.b-voided     { background: var(--surface3); color: var(--text-3) }

/* Generic gray/blue/amber/red/green modifiers — the vocabulary returned by
   UiHelpers::statusBadgeVariant() / WorkStatus::priorityBadgeVariant(), so
   the same PHP helpers used office-side render correctly here too (phase85
   Phase 2, 2026-07-01 — subcontractor "My Tasks" status/priority pills). */
.badge-gray   { background: var(--surface3); color: var(--text-2) }
.badge-blue   { background: var(--brand-cyan-tint); color: var(--brand-cyan-dark) }
.badge-amber  { background: var(--warn-bg); color: var(--warn-text) }
.badge-red    { background: var(--danger-bg); color: var(--danger-text) }
.badge-green  { background: var(--ok-bg); color: var(--ok-text) }

/* ── Tabs (used on public_portal Documents/Payments switcher) ─ */
.tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
  padding: 0 8px;
  background: var(--surface);
}
.tab {
  appearance: none;
  background: none;
  border: none;
  padding: 14px 18px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
.tab:hover { color: var(--text) }
.tab.is-active {
  color: var(--brand-cyan-dark);
  border-bottom-color: var(--brand-cyan);
}
.tab:focus-visible {
  outline: 2px solid var(--brand-cyan);
  outline-offset: -2px;
  border-radius: 4px;
}
.tab-count {
  background: var(--surface-alt);
  color: var(--text-2);
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.4;
}
.tab.is-active .tab-count {
  background: var(--brand-cyan-tint);
  color: var(--brand-cyan-dark);
}
.panel.is-hidden { display: none }

/* Filter form container (the facet rows inside use .pf-bar / .pf-facet). */
.portal-filters {
  padding: 14px 16px;
}

/* ════════════════════════════════════════════════════════════════════════
   PORTAL HOME v3 (2026-06-12) — brand-forward, dense, single-line filters.
   ════════════════════════════════════════════════════════════════════════ */

/* Slim one-line password nudge (was a full banner card). */
.notify-strip {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 14px; margin-bottom: 12px;
  background: var(--brand-cyan-tint); border: 1px solid var(--brand-cyan-soft);
  border-radius: var(--radius-sm); font-size: 12.5px; color: var(--brand-cyan-deep);
}
.notify-strip-ic { display: inline-flex; align-items: center; font-size: 15px; flex-shrink: 0; color: var(--brand-cyan-dark) }
.notify-strip-txt { flex: 1 1 auto; min-width: 0 }
.notify-strip-cta {
  font-weight: 600; color: var(--brand-cyan-dark);
  padding: 3px 10px; border-radius: 999px; background: var(--surface);
  border: 1px solid var(--brand-cyan-soft); text-decoration: none; white-space: nowrap;
}
.notify-strip-cta:hover { background: var(--brand-cyan); color: var(--accent-ink); text-decoration: none }
.notify-strip-x {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; cursor: pointer; color: var(--brand-cyan-dark);
  font-size: 15px; padding: 2px 4px; line-height: 1; flex-shrink: 0;
}

/* Brand hero — full-width navy band flush under the brand bar so the two read
   as ONE continuous header. The brand bar drops its own bottom border on the
   portal home; the cyan accent moves to the bottom of the hero. */
body.portal-home .brand-bar { border-bottom: none }
.phead--full {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  padding: 14px 0 17px;
  margin-bottom: 0;   /* the shell's top padding provides the gap to the content */
}
.phead-inner {
  max-width: var(--portal-content); width: 100%; margin: 0 auto; padding: 0 20px;
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px 24px; flex-wrap: wrap;
}
.phead-id { min-width: 0 }
.phead-name {
  color: var(--text); font-size: 18px; font-weight: 700; line-height: 1.2; letter-spacing: -0.2px;
}
.phead-sub {
  color: var(--text-3); font-size: 10px; margin-top: 3px;
  text-transform: uppercase; letter-spacing: 0.6px;
}
.phead-kpis { display: flex; gap: 9px; flex-wrap: wrap }
.phead-kpi {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 14px; border-radius: var(--radius-sm); min-width: 118px;
  background: var(--surface2); border: 1px solid var(--border);
}
.phead-kpi-l {
  font-size: 9px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase;
  color: var(--text-3); white-space: nowrap;
}
.phead-kpi-v {
  font-size: 17px; font-weight: 700; color: var(--text); line-height: 1.15;
  font-variant-numeric: tabular-nums; display: inline-flex; align-items: baseline;
}
/* Overdue KPI reads in danger red (value only), per the mock's .ax-kpi.hot. */
.phead-kpi--hot .phead-kpi-v { color: var(--danger) }
/* Outstanding popped in brand cyan (fill + dark ink). */
.phead-kpi--due { background: var(--brand-cyan); border-color: var(--brand-cyan) }
.phead-kpi--due .phead-kpi-l { color: rgba(4, 38, 58, 0.72) }
/* Clickable Outstanding KPI — toggles the unpaid-invoice view + aging panel. */
a.phead-kpi-link { text-decoration: none; cursor: pointer; transition: transform .12s ease, box-shadow .12s ease }
a.phead-kpi-link:hover { transform: translateY(-1px); box-shadow: var(--shadow-md) }
a.phead-kpi-link:focus-visible { outline: 2px solid var(--brand-cyan); outline-offset: 2px }
a.phead-kpi-link.is-active { box-shadow: 0 0 0 2px var(--brand-cyan-soft) inset }
.phead-kpi-caret { font-size: .85em; opacity: .8; margin-left: 2px }
.phead-kpi--due .phead-kpi-v { color: var(--accent-ink) }

/* ── Filter toolbar (2026-06-12 "blend"): a segmented PERIOD control + inline
   FACET-PILL selects. One wrapping row, no disclosure panel, no Apply. ── */
.pf-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap }
/* Search takes the slack but is capped so it never balloons and shoves the
   pills onto a second line on wide screens. */
.pf-bar .pf-search { flex: 1 1 200px; max-width: 320px }

/* Segmented period control — connected buttons in one rounded shell. */
.pf-period {
  display: inline-flex; align-items: stretch; flex: 0 0 auto; height: 38px;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: 8px; overflow: hidden;
}
.pf-seg {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 11px; border: 0; border-left: 1px solid var(--border-strong);
  background: transparent; color: var(--text-2); font: inherit; font-size: 12.5px;
  line-height: 1; white-space: nowrap; cursor: pointer;
  transition: background .12s, color .12s;
}
.pf-seg:first-child { border-left: 0 }
.pf-seg:hover { background: var(--brand-cyan-tint); color: var(--brand-cyan-dark) }
.pf-seg.is-active { background: var(--brand-navy); color: #fff; font-weight: 500 }
.pf-seg-custom { padding: 0 12px; font-size: 14px }
.pf-seg-ic { display: inline-flex; align-items: center }

/* Facet pills — native <select>s dressed as rounded pills that SHOW their
   value; .is-set highlights the active facet in brand cyan. The chevron is an
   inline SVG (muted by default, cyan when set). */
.pf-facet {
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  flex: 0 0 auto; max-width: 150px; height: 38px;
  padding: 0 28px 0 12px; border-radius: 999px;
  border: 1px solid var(--border-strong); background-color: var(--surface);
  color: var(--text-2); font: inherit; font-size: 13px; cursor: pointer;
  background-repeat: no-repeat; background-position: right 11px center; background-size: 13px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%237C8AA0' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  transition: background-color .12s, border-color .12s, color .12s;
}
.pf-facet:hover { border-color: var(--brand-cyan); color: var(--brand-cyan-dark) }
.pf-facet:focus { outline: none; border-color: var(--brand-cyan); box-shadow: 0 0 0 2px var(--brand-cyan-tint) }
.pf-facet.is-set {
  background-color: var(--brand-cyan-tint); border-color: var(--brand-cyan);
  color: var(--brand-navy); font-weight: 500;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='13' height='13' viewBox='0 0 24 24' fill='none' stroke='%230676A1' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
}

/* Custom-range panel — revealed by the Custom pill (toggle JS in public.js). */
.pf-customdates { margin-top: 10px }
.pf-customdates.is-hidden { display: none }

/* Compact active-filter bar — a count + one Clear-all reset (the pills now
   carry their own state, so the old per-filter ×-pills row is gone). */
.pf-activebar {
  display: flex; align-items: center; gap: 10px;
  margin-top: 11px; padding-top: 10px; border-top: 1px dashed var(--border);
}
/* In the redesigned docs-card header the activebar is a direct child of the
   flex .pdocs-head — force it onto its own full-width row (below the tabs +
   filter + export row) so the count sits at the far left and Clear all at the
   far right, spanning the whole card instead of hugging the search column. */
.pdocs-head .pf-activebar { flex: 0 0 100%; width: 100%; order: 3; margin-top: 2px }
.pf-activebar-label { font-size: 12px; color: var(--text-3) }
.pf-clear {
  margin-left: auto; display: inline-flex; align-items: center; gap: 5px;
  font-size: 12.5px; font-weight: 500; color: var(--brand-cyan-dark);
}
.pf-clear:hover { color: var(--brand-cyan-deep); text-decoration: none }
.pf-clear span { font-size: 15px; line-height: 1 }

@media (max-width: 560px) {
  .phead-kpis { width: 100% }
  .phead-kpi { flex: 1 1 calc(50% - 5px); min-width: 0 }
  .pf-bar .pf-search { flex: 1 1 100% }
  /* Full-width segmented control: the text segments share the width equally so
     they connect edge-to-edge (no white shell gaps) and the active segment
     fills its whole slot. The Custom (📅) pill stays compact. */
  .pf-period { flex: 1 1 100% }
  .pf-period .pf-seg { flex: 1 1 0; padding-left: 0; padding-right: 0 }
  .pf-period .pf-seg-custom { flex: 0 0 46px }
  .pf-facet { flex: 1 1 calc(50% - 4px); max-width: none }
}

/* ── Signage (2026-06-11) — captured-signature record + delivery sign-off ── */
.signed-block { background: var(--surface-alt); border-top: 1px solid var(--border) }
.cust-sig-img {
  display: block;
  max-height: 96px;
  margin-top: 10px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-xs);
  padding: 6px 8px;
}
.req { color: var(--danger) }
/* X6: form_field hint — classed (was an inline style on the shared component). */
.form-hint { font-size: 11.5px; color: var(--text-3); margin-top: 3px; display: block }

/* ── Document action bar (2026-06-11) — Print / Ask / Approve as clear
   buttons; each form reveals in a panel (toggled by public.js) instead of
   sitting open as a scattered box. ── */
.doc-actions-wrap { border-top: 1px solid var(--border) }
.doc-actions { display: flex; flex-wrap: wrap; gap: 10px }
/* Action STACK (redesign): one primary action, a 2-up ghost row, quiet PDF. */
.doc-actions-stack { display: flex; flex-direction: column; gap: 10px }
.doc-actions-2up { display: grid; grid-template-columns: 1fr 1fr; gap: 10px }
@media (max-width: 440px) { .doc-actions-2up { grid-template-columns: 1fr } }
.doc-actions .btn { font-weight: 600 }
.doc-panel { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border) }
.doc-panel.is-hidden { display: none }
.btn--ok { background: var(--ok); border-color: var(--ok); color: #04231a }
.btn--ok:hover { background: #34d399; border-color: #34d399; color: #04231a }
/* active (open) action button gets a clear pressed ring */
.doc-actions .btn.is-active { box-shadow: inset 0 0 0 2px rgba(15, 27, 63, .18) }
.stack-sm { display: flex; flex-direction: column; gap: 8px }
/* Previous-messages list — flat divided rows (no box-in-box). */
.prior-comments { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border) }
.prior-comment { padding: 10px 0; border-top: 1px solid var(--border) }
.prior-comment-head {
  font-size: 11.5px; color: var(--text-3); margin-bottom: 4px;
  display: flex; justify-content: space-between; gap: 8px; flex-wrap: wrap;
}
.prior-comment-body { font-size: 13px; color: var(--text); white-space: pre-wrap }

/* ── FE-10: KPI right-alignment so magnitudes line up visually ── */
.kpi-value { text-align: right; font-variant-numeric: tabular-nums }
.kpi-label { text-align: right }

/* ── Pay-status strip (2026-07-06, per the redesign — ".ax-paystrip") ──────
   A tinted strip above a priced document: state label, % settled, a progress
   bar, and the remaining amount. Colour-coded but always paired with the text
   label + a percentage so meaning never rests on colour alone. */
.paystrip { display: flex; align-items: center; gap: 12px; padding: 13px 16px;
            border-radius: var(--radius); border: 1px solid var(--border); margin-bottom: 14px; }
.paystrip-body { flex: 1; min-width: 0 }
.paystrip-row { display: flex; align-items: center; justify-content: space-between; gap: 10px }
.paystrip-lab { font-family: var(--font-mono); font-size: 10px; font-weight: 600;
                letter-spacing: .1em; text-transform: uppercase }
.paystrip-pct { font-family: var(--font-mono); font-size: 10px; color: var(--text-3) }
.paystrip-bar { height: 7px; border-radius: 999px; background: rgba(20,32,58,.10);
                overflow: hidden; margin: 7px 0 8px }
.paystrip-bar i { display: block; height: 100%; border-radius: 999px }
.paystrip-foot { font-size: 12px; color: var(--text-3) }
.paystrip-amt { font-family: var(--font-mono); font-weight: 600; font-size: 16px; color: var(--text) }
/* variants — fill + label tone */
.paystrip--paid    { background: var(--ok-bg);     border-color: rgba(21,163,74,.30) }
.paystrip--paid    .paystrip-lab, .paystrip--paid .paystrip-bar i    { color: var(--ok-text) }
.paystrip--paid    .paystrip-bar i    { background: var(--ok) }
.paystrip--partial { background: var(--warn-bg);   border-color: rgba(194,116,10,.30) }
.paystrip--partial .paystrip-lab      { color: var(--warn-text) }
.paystrip--partial .paystrip-bar i    { background: var(--warn) }
.paystrip--overdue { background: var(--danger-bg); border-color: rgba(220,38,38,.30) }
.paystrip--overdue .paystrip-lab      { color: var(--danger-text) }
.paystrip--overdue .paystrip-bar i    { background: var(--danger) }
.paystrip--unpaid  { background: var(--surface-alt); border-color: var(--border) }
.paystrip--unpaid  .paystrip-lab      { color: var(--text-2) }
.paystrip--unpaid  .paystrip-bar i    { background: var(--text-3) }

/* ── Documents card (2026-07-06, per the mock) — the filter bar, the
   Documents/Payments switch and the table read as ONE clean card. The filter
   controls are compacted into the card header; nothing is removed. */
/* Single clean header row like the mock (no period segments — the list defaults
   to All; date range folds away). Every row shares ONE 16px gutter so the filter
   controls, tabs, column heads and cells all line up on the same edge. */
.portal-docs-card { padding: 0; margin-bottom: 16px; overflow: visible }
.portal-docs-card .pf-period,
.portal-docs-card .pf-customdates { display: none }
/* ONE header row (like the mock): tabs on the left, the filter controls + export
   on the right, all on a single line sharing the 16px gutter. */
.pdocs-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px;
  padding: 9px 16px; border-bottom: 1px solid var(--border) }
.pdocs-head .tabs { order: -1; flex: 0 0 auto; padding: 0; margin: 0; background: transparent; border: 0 }
.pdocs-head .tab { padding: 5px 0; margin-right: 18px; font-size: 13px }
/* The redesigned docs card carries the filter form inline in .pdocs-head, so it
   must NOT keep the standalone form's grey surface-alt fill — that band showed
   through the empty "N filters applied" row as a stray grey strip (owner report
   2026-07-07). Flatten it to match the white card. */
.pdocs-head .portal-filters { flex: 1 1 auto; padding: 0; margin: 0; border: 0; min-width: 0; background: transparent }
.pdocs-head .pf-bar { gap: 8px; justify-content: flex-end; flex-wrap: nowrap }
.pdocs-head .tab-export { order: 2; display: flex; gap: 6px; flex: 0 0 auto }
/* Compact controls, ALL the same 34px pill height so search + Type + Status +
   export line up (the base .pf-facet hard-codes 38px — override it here). */
.portal-docs-card .pf-search { flex: 1 1 150px; max-width: 240px; min-width: 130px }
.portal-docs-card .filter-search-inline { height: 34px; min-height: 34px; font-size: 12.5px }
.portal-docs-card .pf-facet { height: 34px; min-height: 34px; max-width: 150px; font-size: 12.5px; flex: 0 0 auto }
.portal-docs-card .export-icon-btn { height: 34px }
.portal-docs-card .panel { padding: 0 }
/* On a phone the header controls stack below the tabs. */
@media (max-width: 640px) {
  .pdocs-head .portal-filters { flex-basis: 100% }
  .pdocs-head .pf-bar { justify-content: flex-start; flex-wrap: wrap }
}
/* Column heads: 9px mono uppercase, wide tracking, NO grey band (just a hairline
   under, like the mock). Cells 12px, middle-aligned. */
.portal-docs-card .tbl thead th { background: transparent; font-family: var(--font-mono);
  font-size: 9px; font-weight: 600; letter-spacing: .12em; color: var(--text-3); padding: 10px 16px }
.portal-docs-card .tbl tbody td { padding: 12px 16px; font-size: 13px; vertical-align: middle }
.portal-docs-card .pag { padding: 12px 16px }

/* ── Client-overview KPI grid (2026-07-06, per the mock's .ax-kpi) ─────────
   4-up plain tiles below the Needs band: mono uppercase label, big mono value,
   a mono sub. Account balance is a link that toggles the aging breakdown. */
.pkpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px }
@media (max-width: 820px) { .pkpi-grid { grid-template-columns: 1fr 1fr } }
/* Phones keep the compact 2×2 KPI grid (2026-07-06) — collapsing to one column
   made four tall full-width tiles the user had to scroll past before any
   content. 2×2 shows the whole snapshot above the fold. */
@media (max-width: 460px) { .pkpi-grid { gap: 10px } }
/* Mock .ax-card .head — used by the Home cards (Due this week / Project
   progress). Compact 13px/16px header with a hairline under it. */
.card > .head { display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 13px 16px; border-bottom: 1px solid var(--border) }
.card > .head h5 { font-size: 13px; font-weight: 600; color: var(--text) }

/* KPI tiles rest FLAT (elevation via the surface step), like the mock — no
   drop shadow. */
.pkpi { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
        padding: 15px 16px }
.pkpi-l { display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
          text-transform: uppercase; color: var(--text-3) }
.pkpi-v { display: block; font-family: var(--font-mono); font-size: 26px; font-weight: 600;
          letter-spacing: -.02em; margin-top: 8px; line-height: 1; color: var(--text); font-variant-numeric: tabular-nums }
.pkpi-sub { display: block; font-family: var(--font-mono); font-size: 10px; color: var(--text-3); margin-top: 7px }
.pkpi--hot .pkpi-v { color: var(--danger) }
.pkpi--warn .pkpi-v { color: var(--warn) }

/* Coordinator Home — 2-col Due-this-week + Project-progress (2026-07-06, mock) */
.phome-cols { display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; margin-top: 4px }
/* The global stacked-card spacing (.card + .card) would push the second grid
   card down inside its cell — the grid's own gap handles spacing here. */
.phome-cols > .card { margin-top: 0 }
@media (max-width: 820px) { .phome-cols { grid-template-columns: 1fr } }
.phome-cols .card .head h5 { display: inline-flex; align-items: center; gap: 8px }
.phome-cols .card .head h5 .icon, .phome-cols .card .head h5 svg { width: 16px; height: 16px; color: var(--text-3) }
.phome-link { font-size: 12.5px; font-weight: 600; color: var(--brand-cyan-dark);
  display: inline-flex; align-items: center; padding: 8px 2px; margin: -8px 0 }
/* Due-this-week list — flush rows inside the card (mock), NOT floating cards:
   no per-row border/radius/shadow/margin; a hairline divides each row; the
   priority rail hugs the left edge and the status pill sits on the right. */
.phome-cols .card .ptask-row {
  border: 0; border-radius: 0; margin: 0; box-shadow: none;
  padding: 12px 16px 12px 0; min-height: 62px;
  border-top: 1px solid var(--border); background: var(--surface);
}
.phome-cols .card .ptask-row:first-of-type { border-top: 0 }
.phome-cols .card .ptask-row:hover { transform: none; box-shadow: none;
  border-color: var(--border); background: var(--surface-alt) }
.phome-cols .card .ptask-accent { margin: 0; flex-basis: 3px; border-radius: 0 }
.phome-cols .card .ptask-main { gap: 4px }
/* Status pill parked at the row's right edge (mock). */
.ptask-status { align-self: center; margin-left: auto; flex: 0 0 auto }
.phome-cols .card .ptask-chev { display: none }

/* Project progress — padded list, even compact rows, at-risk shows a red bar. */
.phome-projlist { padding: 2px 16px 6px }
.phome-proj { padding: 12px 0 }
.phome-proj + .phome-proj { margin-top: 0; padding-top: 12px; border-top: 1px solid var(--border) }
.phome-proj-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 9px }
.phome-proj-name { font-size: 14px; font-weight: 600; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.phome-proj-meta { font-size: 11px; color: var(--text-3); margin-top: 8px; font-family: var(--font-mono); letter-spacing: .02em }
/* Compact empty state inside a Home card (CSP-clean: class, not inline style). */
.pt-empty--card { padding: 28px 16px }
/* Progress bar (mock .ax-progress) */
.ax-progress { height: 6px; border-radius: 999px; background: var(--surface2); overflow: hidden }
.ax-progress i { display: block; height: 100%; background: var(--brand-cyan); border-radius: 999px; min-width: 3px }
.ax-progress--risk i { background: var(--danger) }
a.pkpi-link { text-decoration: none; cursor: pointer; transition: border-color var(--speed) var(--ease), box-shadow var(--speed) var(--ease) }
a.pkpi-link:hover { border-color: var(--brand-cyan-soft); box-shadow: var(--shadow-md) }
a.pkpi-link.is-active { border-color: var(--brand-cyan); box-shadow: var(--sh-cyan) }
.pkpi-caret { font-size: .82em; color: var(--text-3); margin-left: 2px }
/* Document-number link in the overview table (navy mono, hover cyan) — the row
   opens the document; no separate "Open" column, per the mock. */
.doc-num-link { color: var(--text) }
.doc-num-link:hover { color: var(--brand-cyan-dark); text-decoration: underline }

/* Whole-row click target for the portal documents list. The doc-number link
   stretches a transparent ::after over the entire row so a tap anywhere on the
   row opens the document — no JS, so a no-JS client still uses the plain link.
   (position:relative on the <tr> is the containing block for the overlay.) */
.pp-open { position: relative; cursor: pointer }
.pp-open:hover { background: var(--surface2) }
.pp-open .doc-num-link::after { content: ""; position: absolute; inset: 0; z-index: 1 }
/* Anything else genuinely interactive in the row must sit above the overlay. */
.pp-open a:not(.doc-num-link), .pp-open button { position: relative; z-index: 2 }

/* ── "Needs attention" band (2026-07-06, per the redesign) ────────────────
   Accent-bordered card at the very top of the client overview listing the
   documents that need the client to act (approve / sign / overdue). Caps at 3
   rows; extras reveal via the Show-more toggle (public.js). */
.needs-band { border-color: var(--brand-cyan-soft); margin-bottom: 16px; }
.needs-band .needs-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
                          padding: 12px 16px; border-bottom: 1px solid var(--border); }
.needs-sub { color: var(--text-3); font-size: 12.5px }
.nb-row { display: flex; align-items: center; justify-content: space-between; gap: 14px;
          padding: 12px 16px; border-bottom: 1px solid var(--border); flex-wrap: wrap }
.nb-row:last-of-type { border-bottom: 0 }
.nb-row-l { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap }
.nb-row-r { display: flex; align-items: center; gap: 10px; flex-shrink: 0 }
.nb-site { color: var(--text-3); font-size: 12.5px }
.nb-amt { font-weight: 600 }
.nb-row--extra { display: none }
.needs-band.is-open .nb-row--extra { display: flex }
.needs-more { width: 100%; border: 0; border-top: 1px solid var(--border); background: transparent;
              color: var(--brand-cyan-dark); font-family: var(--font); font-weight: 600;
              font-size: 13px; padding: 11px; cursor: pointer }
.needs-more:hover { background: var(--surface2) }

/* ── Single-row filter inside the activity card ──────── */
/* Everything fits in one flex row that wraps gracefully on narrow screens. */
.portal-activity { padding: 0 }
.portal-filters {
  padding: 14px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-alt);
}
.filter-search-inline {
  flex: 1 1 220px;
  min-width: 170px;
  min-height: 38px;
  padding: 8px 12px;
  font-size: 13px;
}

/* ── FILTER-REDESIGN (2026-06-11): search-first top row + a collapsible
   "Filters" panel for the rest. Declutters the old cramped single row;
   stacks cleanly on mobile. ── */
/* Search box with an inline 🔎 submit (no Apply button) — guarantees Enter
   submits and gives a click target. */
.pf-search { position: relative; flex: 1 1 240px; display: flex; align-items: center }
.pf-search .filter-search-inline { width: 100%; padding-right: 38px }
.pf-search-btn {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; border-radius: 6px; cursor: pointer;
  font-size: 14px; color: var(--text-3); line-height: 1;
}
.pf-search-btn:hover { background: var(--brand-cyan-tint) }
/* Joined from→to date range — two native inputs in one pill (native pickers,
   mobile-friendly, no JS date library). */
.pf-daterange {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: 8px; padding: 2px 8px; min-height: 38px;
}
.pf-daterange:focus-within { border-color: var(--brand-cyan); box-shadow: 0 0 0 2px var(--brand-cyan-tint) }
.pf-date {
  border: 0; background: transparent; font: inherit; font-size: 12.5px;
  color: var(--text); padding: 6px 2px; min-width: 0; width: 120px;
}
.pf-date:focus { outline: none }
.pf-daterange-sep { color: var(--text-3); font-size: 12px; flex-shrink: 0 }
@media (max-width: 560px) {
  /* The custom range spans the row and its two inputs share the width. */
  .pf-customdates .pf-daterange { display: flex; width: 100%; justify-content: space-between }
  .pf-date { width: auto; flex: 1 1 0 }
}
/* ── CUR-1: Dirham currency icon ──────────────────────────────
   The official UAE Dirham symbol (Central Bank, March 2025): a
   stylized "D" with two horizontal strokes. Rendered as inline SVG
   via Format::dh() — uses currentColor so it inherits the surrounding
   text color, and sizes via em so it scales with the parent font. */
/* CUR-3 (2026-05-24): width:auto + fixed height so the official UAE CB
   Dirham mark (1000x870 viewBox, wider than tall) fills the box at the
   right aspect ratio instead of being letterboxed in a square. */
.dh-icon {
  display: inline-block;
  width: auto;
  height: 0.95em;
  vertical-align: -0.13em;
  margin-right: 0.22em;
  flex-shrink: 0;
}
/* Slightly smaller in dense tables, slightly larger in KPI tiles —
   pages can opt into these by adding the extra class to the helper. */
.dh-icon.dh-sm { height: 0.85em; margin-right: 0.18em }
.dh-icon.dh-lg { height: 1.05em; margin-right: 0.25em }

/* CUR-4 (2026-05-26): .money / .money-val wrapper — see core.css note.
   Keeps dirham marks vertically aligned in customer-facing totals
   boxes on the public portal / public_doc views. */
.money { display: inline-flex; align-items: baseline; white-space: nowrap }
.money-val { font-variant-numeric: tabular-nums }
.totals-box .money,
.totals-box .mono { display: inline-flex; align-items: baseline; justify-content: flex-end }
.totals-box .money-val { display: inline-block; min-width: 5.5em; text-align: right }


@media (max-width: 880px) {
  .filter-search-inline { flex: 1 1 100% }
}

/* ── Sortable headers (FE-8) ────────────────────────────────── */
.tbl thead th .sort-link {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}
.tbl thead th .sort-link:hover { color: var(--brand-cyan-dark) }
.tbl thead th .sort-link.is-sorted { color: var(--brand-cyan-dark) }
.tbl thead th .sort-arrow {
  font-size: 10px;
  opacity: 0.75;
}

/* ── Payment-type badges (FE-8) ────────────────────────────── */
/* Branded equivalents of plain "Advance"/"Credit applied"/"Invoice payment".
   Reuses the same .badge base from the document status badges. The class is
   built as 'b-pt-' . [a-z-stripped type] (public_portal.php), e.g.
   b-pt-invoicepayment — the selector must keep that hyphen after `pt` or the
   badge renders unstyled. */
.b-pt-invoicepayment { background: var(--brand-cyan-tint); color: var(--brand-cyan-dark) }
.b-pt-advance        { background: var(--info-bg); color: var(--info-text); border: 1px solid var(--brand-cyan-soft) }
.b-pt-creditapplied  { background: var(--warn-bg); color: var(--warn-text) }

/* ── Filtered/total row in payments tfoot (FE-8) ───────────── */
.tbl tfoot tr.tbl-total td {
  background: var(--surface-alt);
  border-top: 2px solid var(--brand-cyan);
  padding-top: 14px;
  padding-bottom: 14px;
}



/* ── FE-9: discreet footer link ─────────────────────────────── */
.portal-footnote {
  margin-top: 18px;
  font-size: 12px;
  color: var(--text-3);
  text-align: center;
}
.portal-footnote a { color: var(--brand-cyan-dark); margin-left: 4px }
.portal-footnote a:hover { color: var(--brand-cyan-hover) }

/* ── FE-10: Tabs with inline export icons ──────────────────── */
/* Tab strip pulls the PDF/CSV export icons to the right edge.
   Saves the entire "Export this view:" toolbar row from FE-9. */
.tabs-with-export { display: flex; align-items: center }
.tabs-with-export .tab { flex: 0 0 auto }
.tab-export {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 4px;
  padding-right: 10px;
}
.export-icon-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background .12s, border-color .12s, color .12s;
}
.export-icon-btn:hover {
  background: var(--brand-cyan-tint);
  border-color: var(--brand-cyan);
  color: var(--brand-cyan-dark);
  text-decoration: none;
}

/* ── FE-9: Page-number pagination ───────────────────────────── */
.pag {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 18px;
  border-top: 1px solid var(--border);
  background: var(--surface-alt);
  gap: 12px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--text-2);
}
.pag-count strong { color: var(--text); font-weight: 600 }
.pag-nav {
  display: flex;
  align-items: center;
  gap: 4px;
}
.pag-btn,
.pag-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 34px;
  padding: 0 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-2);
  font-size: 12.5px;
  font-weight: 500;
  border-radius: var(--radius-sm);
  text-decoration: none;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}
.pag-btn:hover,
.pag-num:hover {
  background: var(--brand-cyan-tint);
  border-color: var(--brand-cyan);
  color: var(--brand-cyan-dark);
  text-decoration: none;
}
.pag-num.is-current {
  background: var(--brand-cyan);
  border-color: var(--brand-cyan);
  color: #fff;
  cursor: default;
}
.pag-num.is-current:hover {
  background: var(--brand-cyan);
  color: #fff;
}
.pag-gap {
  color: var(--text-3);
  padding: 0 4px;
}
.pag-btn.is-disabled,
.pag-num.is-disabled,
.btn.is-disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* ── Empty state ─────────────────────────────────────────────── */
.empty {
  padding: 48px 20px;
  text-align: center;
  color: var(--text-3);
}
.empty-icon {
  font-size: 36px;
  margin-bottom: 8px;
  opacity: 0.7;
}

/* ── Spotlight panel (single-purpose pages: login, set-pw, etc.) ── */
/* V2: the auth pages get a soft radial brand wash behind a floating card —
   the "front door" should feel considered, not like an error page. */
.spotlight {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(100dvh - 64px);  /* full height minus brand-bar */
  padding: 32px 16px;
  background:
    radial-gradient(48rem 30rem at 85% -10%, rgba(29, 182, 240, 0.08), transparent 60%),
    radial-gradient(40rem 26rem at 8% 110%, rgba(15, 27, 63, 0.05), transparent 55%);
}
.spotlight-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) + 2px);
  width: 100%;
  max-width: 420px;
  padding: 36px 32px;
  box-shadow: var(--shadow-lg);
}
.spotlight-mark {
  width: 60px;
  height: 60px;
  background: linear-gradient(135deg, var(--brand-cyan-tint), var(--brand-cyan-soft));
  border: 1px solid var(--brand-cyan-soft);
  border-radius: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 16px;
  color: var(--brand-cyan-dark);
  font-size: 26px;
}
/* Login leads with the bare brand emblem (2026-07-06, matching the mock — a
   64px web-mark centred on the surface, no tile, no chrome). */
.spotlight-mark--brand { background: transparent; border: 0; box-shadow: none; padding: 0;
  width: 64px; height: 64px; border-radius: 0 }
.spotlight-mark--brand img { display: block; width: 64px; height: 64px }
.spotlight-card { position: relative; overflow: hidden }
.spotlight-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px;
  background: linear-gradient(90deg, var(--brand-cyan), var(--cta)) }
/* Sign-in screen matches the mock: no marketing brand bar — just the centred
   card on the clean surface, full-height. */
body.portal-signin .brand-bar { display: none }
body.portal-signin .spotlight { min-height: 100dvh }
/* FLAT, SIMPLE sign-in (2026-07-07, owner request "flat simple login page").
   The login is a single-purpose auth screen, not a marketing page — so we strip
   every decorative layer down to just the form:
     • no radial gradient wash behind the card → one flat page tone
     • no drop shadow / no top accent stripe on the card → it rests FLAT
     • no site footer, no floating quick-contact buttons → nothing but sign-in
   Everything is scoped to body.portal-signin so the other auth surfaces
   (set-password, forgot, reset) keep their existing elevated card. */
body.portal-signin { background: var(--bg) }
body.portal-signin .spotlight { background: none; padding: 24px 16px }
body.portal-signin .spotlight-card {
  box-shadow: none;                 /* flat — no elevation */
  border: 1px solid var(--border);
}
body.portal-signin .spotlight-card::before { display: none }  /* drop accent stripe */
body.portal-signin .brand-footer,
body.portal-signin .fab-stack { display: none }               /* no marketing chrome */
/* Hairline + subcontractor link pinned under the card, per the mock. */
.signin-alt { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border);
  text-align: center; font-size: 13px }

/* ── Chip / company tag (used on set-password "for: COMPANY") ── */
.company-chip {
  display: inline-block;
  background: var(--brand-cyan-tint);
  color: var(--brand-cyan-dark);
  border-radius: 999px;
  padding: 4px 12px;
  font-size: 12px;
  font-weight: 500;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}



/* ── Signature pad (public_sign) ─────────────────────────────── */
.sigpad-wrap {
  border: 2px dashed var(--brand-cyan-soft);
  border-radius: var(--radius-sm);
  background: var(--surface);
  position: relative;
  overflow: hidden;
}
.sigpad-canvas {
  display: block;
  width: 100%;
  height: 200px;
  cursor: crosshair;
  touch-action: none;
  position: relative;
  z-index: 1;
  background: transparent;
}
/* .ax-sign guides (2026-07-06): a centred "Sign with your finger" hint and a
   baseline, drawn behind the canvas; the hint clears on the first stroke. */
.sigpad-wrap::before {
  content: 'Sign with your finger';
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  display: grid; place-items: center; color: var(--text-3); font-size: 13px;
}
.sigpad-wrap::after {
  content: '✕'; position: absolute; left: 16px; right: 16px; bottom: 30px; z-index: 0;
  color: var(--text-3); font-family: var(--font-mono); font-size: 12px; line-height: 0;
  border-bottom: 1px solid var(--border-strong);
}
.sigpad-wrap.is-signing::before { display: none }

/* Sign ceremony (2026-07-06, per the mock): binding statement + Document/Total
   facts above the pad. */
.sign-intro { margin-bottom: 12px }
.sign-intro-lead { font-size: 12.5px; color: var(--text-3) }
.sign-intro-doc { font-size: 16px; font-weight: 600; color: var(--text); margin-top: 2px }
.sign-intro-bind { font-size: 13px; color: var(--text-2); margin-top: 6px }
.sign-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; margin-bottom: 14px }
.sign-facts > div { background: var(--surface); padding: 11px 13px }
.sign-facts .l { display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3) }
.sign-facts .v { display: block; font-size: 14px; font-weight: 600; color: var(--text); margin-top: 4px }
.sigpad-actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 10px;
  gap: 8px;
}

/* ── Verify status panel (public_verify) ──────────────────── */
.verify-result {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px 20px;
}
.verify-icon {
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
}
.verify-icon--ok    { background: var(--ok-bg);     color: var(--ok-text) }
.verify-icon--bad   { background: var(--danger-bg); color: var(--danger-text) }
.verify-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 2px;
}
.verify-sub {
  font-size: 12.5px;
  color: var(--text-2);
}

/* ── Utility ─────────────────────────────────────────────────── */
.mt-1 { margin-top: 8px }
.mt-2 { margin-top: 12px }
.mt-3 { margin-top: 16px }
.mt-4 { margin-top: 24px }
.mb-1 { margin-bottom: 8px }
.mb-2 { margin-bottom: 12px }
.mb-3 { margin-bottom: 16px }
.row-end { display: flex; justify-content: flex-end; gap: 8px; align-items: center; flex-wrap: wrap }
.dim { color: var(--text-3) }
.link-quiet { color: var(--text-2) }
.link-quiet:hover { color: var(--brand-cyan-dark) }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0 }

/* ── Responsive tweaks ───────────────────────────────────────── */
@media (max-width: 640px) {
  .brand-bar { padding: 10px 14px; gap: 10px }
  .brand-bar-logo { height: 28px }
  .brand-bar-name { font-size: 12px }
  .brand-bar-sub { display: none }   /* mobile: keep header compact */
  .acct-menu-btn { padding: 5px 8px 5px 6px }
  .shell { padding: 16px 14px }
  .h1 { font-size: 18px }
  .card-pad { padding: 16px }
  .tab { padding: 10px 12px; font-size: 12.5px }
  .kpi-grid { grid-template-columns: 1fr }
  /* Tables: horizontal scroll inside the card rather than blowing past the viewport */
  .card .tbl-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch }
}

/* ── STAGE-2 (2026-06-10): table → stacked cards on phones ──────────
   Tables marked .tbl-cards drop the column grid under 720px: each row
   becomes a card, each cell a "Label  Value" line fed by data-label.
   The action cell (.tbl-cards-action) spans full width as a tap-friendly
   button. Sort headers disappear with the thead (sorting stays available
   on wider screens; date-desc default is the right mobile order). */
@media (max-width: 720px) {
  .tbl-cards thead { display: none }
  .tbl-cards, .tbl-cards tbody, .tbl-cards tr, .tbl-cards td,
  .tbl-cards tfoot { display: block }
  .tbl-cards tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    margin: 10px 12px;
    padding: 6px 0;
    box-shadow: var(--shadow-sm);
    background: var(--surface);
  }
  .tbl-cards tbody tr:hover td { background: transparent }
  .tbl-cards tbody td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 14px;
    border: none;
    padding: 7px 14px;
    text-align: right;
  }
  .tbl-cards tbody td::before {
    content: attr(data-label);
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    color: var(--text-3);
    text-align: left;
    flex-shrink: 0;
  }
  .tbl-cards tbody td:not([data-label])::before { content: none }
  .tbl-cards tbody td.tbl-cards-action {
    display: block;
    padding: 8px 14px 10px;
    text-align: center;
    border-top: 1px solid var(--border);
    margin-top: 4px;
  }
  .tbl-cards tbody td.tbl-cards-action .btn { width: 100%; justify-content: center }
  .tbl-cards tfoot tr { border: none; box-shadow: none; margin: 0 12px }
  .tbl-cards tfoot td { display: flex; justify-content: space-between; border: none; padding: 8px 14px }
  /* Task cards: give the title row room + weight so it reads as the tap target. */
  .tbl-cards tbody td[data-label="Task"] { padding-top: 11px; padding-bottom: 11px }
  .tbl-cards tbody td[data-label="Task"] a { font-weight: 600; font-size: 15px }
}

/* ── Portal task pages — inline icons + touch-first actions (phase90) ──────
   The office app sizes .icon in app.css; the portal has its own stylesheet,
   so mirror the rule here or an icon_svg() renders at its intrinsic 24px. */
.icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; flex: 0 0 auto }

/* My Tasks group-header icon (Overdue / Today / Upcoming / No date) — replaces
   the emoji glyphs that rendered inconsistently across phones. */

/* A subcontractor's core job on a phone is to advance the status or post a
   comment — make those thumb-sized. On a phone they go full-width and stack. */
@media (max-width: 560px) {
  .ptask-comment .btn { width: 100%; min-height: 50px; font-size: 15px }
  .ptask-comment textarea { font-size: 16px }   /* ≥16px stops iOS zoom-on-focus */
}
.spotlight-mark svg { width: 26px; height: 26px }

/* ── Portal task list — mobile-first (phase90) ─────────────────────────
   A native-feeling task list for subcontractors + team on a phone: a summary
   line, horizontally-scrollable filter chips, and large tappable task rows. */

/* View switcher (List · Calendar · Timeline). The coordinator hub bottom nav
   (Home/Tasks/Team/Projects) carries no Calendar/Timeline tab, so admins flip
   between the three task views with this segmented control (owner report
   2026-07-07: "can't see timeline or calendar on the portal"). Subcontractors
   already get the three as bottom-nav tabs, so callers render it only for admins. */
.pt-viewswitch { display: flex; gap: 4px; margin: 0 0 14px; padding: 4px;
  background: var(--surface2); border: 1px solid var(--border); border-radius: 11px }
.pt-vs { flex: 1 1 0; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 36px; border-radius: 8px; font-size: 13px; font-weight: 500; color: var(--text-2);
  text-decoration: none; white-space: nowrap }
.pt-vs svg { width: 16px; height: 16px }
.pt-vs:hover { color: var(--text-1); text-decoration: none }
.pt-vs.is-active { background: var(--surface); color: var(--brand-cyan-dark); font-weight: 600;
  box-shadow: 0 1px 3px rgba(20,32,58,.14) }
@media (max-width: 380px) { .pt-vs span { font-size: 12px } }

/* Two clean rows (2026-07-13): a full-width search field on top, the facet
   chips on their own row below. The earlier one-row layout tucked the search
   inside the chip strip (collapsed to a magnifier), which read as "search is
   one of the tabs". Separating them makes search a first-class control and
   frees the whole width for the chips to scroll. */
.pt-filterbar { display: flex; flex-direction: column; align-items: stretch; gap: 10px; margin: 0 0 14px }
.pt-filterbar .pt-search { width: 100%; margin: 0 }
.pt-filterbar .pt-segs { width: 100%; min-width: 0; padding: 0 }
.pt-filterbar .pt-search-in { min-height: 44px }
.pt-filterbar .pt-seg { min-height: 40px }

.pt-segs { display: flex; gap: 8px; overflow-x: auto; padding: 3px 0 12px; -webkit-overflow-scrolling: touch; scrollbar-width: none;
           -webkit-mask-image: linear-gradient(90deg, #000 92%, transparent); mask-image: linear-gradient(90deg, #000 92%, transparent) }
.pt-segs::-webkit-scrollbar { display: none }
.pt-seg { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px; min-height: 40px; padding: 0 15px; border-radius: 999px;
          border: 1px solid var(--border); background: var(--surface); color: var(--text-2); font-size: 13.5px; font-weight: 600;
          text-decoration: none; white-space: nowrap; transition: background .15s, border-color .15s, color .15s }
.pt-seg:hover { border-color: var(--border-strong); text-decoration: none }
.pt-seg.is-active { background: var(--brand-cyan); border-color: var(--brand-cyan); color: var(--accent-ink); box-shadow: 0 3px 10px rgba(26,183,234,.30) }
.pt-seg--od.is-active { background: var(--danger); border-color: var(--danger); color: #fff }
.pt-seg-n { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px;
            border-radius: 999px; background: var(--surface2, #EEF2F7); color: var(--text-2); font-size: 11.5px; font-weight: 700 }
.pt-seg.is-active .pt-seg-n { background: rgba(255,255,255,0.25); color: #fff }

/* Portal task search — a rounded input matching the seg chips (phase90b). */
.pt-search { position: relative; display: flex; align-items: center; margin: 0 0 12px }
.pt-search-ic { position: absolute; left: 13px; display: inline-flex; pointer-events: none; color: var(--text-3) }
.pt-search-ic .icon { width: 17px; height: 17px }
.pt-search-in { flex: 1 1 auto; min-height: 44px; padding: 0 42px 0 40px; border-radius: 999px;
                border: 1px solid var(--border); background: var(--surface); color: var(--text-1);
                font-size: 15px; -webkit-appearance: none; appearance: none }
.pt-search-in:focus { outline: none; border-color: var(--brand-cyan); box-shadow: 0 0 0 3px rgba(0,169,224,.15) }
.pt-search-in::-webkit-search-cancel-button { display: none }
.pt-search-x { position: absolute; right: 8px; display: inline-flex; align-items: center; justify-content: center;
               width: 30px; height: 30px; border-radius: 999px; color: var(--text-3); text-decoration: none }
.pt-search-x:hover { background: var(--surface2, #EEF2F7); color: var(--text-1) }
.pt-search-x .icon { width: 17px; height: 17px }

.pt-group-title { font-size: 11px; font-weight: 700; letter-spacing: .8px; text-transform: uppercase; color: var(--text-3);
                  margin: 16px 2px 8px; display: flex; align-items: center; gap: 7px }
.pt-group-title:first-child { margin-top: 4px }
.pt-group-title .icon { width: 14px; height: 14px }
.pt-group-title--od { color: var(--danger) }
/* "Show more completed" — no-JS pager at the foot of the Done history. */
.pt-loadmore { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%;
               margin-top: 10px; padding: 11px; border: 1px solid var(--border); border-radius: 12px;
               background: var(--surface); color: var(--accent); font-size: 13px; font-weight: 600;
               text-decoration: none; -webkit-tap-highlight-color: transparent }
.pt-loadmore:hover { background: var(--accent-dim); border-color: var(--accent-line) }
.pt-loadmore .icon { width: 15px; height: 15px }

/* ── My Tasks list — denser, flatter rhythm (2026-07-13) ──────────────────
   Scoped to body.is-app-list so the home hub (.phome-cols) and the task-view
   rows keep their own styling. A tighter vertical rhythm + a single soft
   shadow + a slim colour cue on the group header make the list scan like a
   modern task app rather than a stack of floating cards. */
body.is-app-list .ptask-row { margin-bottom: 6px; border-radius: 12px; min-height: 52px;
  box-shadow: 0 1px 2px rgba(15,27,63,.05) }
body.is-app-list .ptask-row:hover { transform: translateY(-1px);
  box-shadow: 0 4px 10px rgba(15,27,63,.07), 0 10px 22px rgba(15,27,63,.06) }
body.is-app-list .ptask-swipe { margin-bottom: 6px }
body.is-app-list .ptask-swipe > .ptask-row { margin-bottom: 0 }
body.is-app-list .pt-group-title { margin: 20px 4px 10px; gap: 8px; align-items: center }
body.is-app-list .pt-group-title:first-child { margin-top: 4px }
/* A short accent bar leads the section header — cyan by default, red for the
   overdue group — so each section reads at a glance. */
body.is-app-list .pt-group-title::before { content: ""; flex: 0 0 auto; width: 3px; height: 13px;
  border-radius: 2px; background: var(--brand-cyan) }
body.is-app-list .pt-group-title--od::before { background: var(--danger) }

/* Task row — the whole card is one big tap target. */
/* Task row — flat + dense (phase94 UX pass). No per-row drop shadow; a crisp
   hairline + a slim rounded priority rail. Denser than the old 62px card so
   more tasks fit the viewport, matching modern task apps. */
.ptask-row { display: flex; align-items: stretch; gap: 11px; background: var(--surface); border: 1px solid var(--border);
             border-radius: 14px; padding: 9px 12px 9px 0; margin-bottom: 8px; text-decoration: none;
             min-height: 54px; overflow: hidden; box-shadow: 0 1px 2px rgba(15,27,63,.04), 0 4px 14px rgba(15,27,63,.045);
             transition: transform .15s ease, border-color .15s, box-shadow .2s ease }
.ptask-row:hover { border-color: var(--border-strong); text-decoration: none; transform: translateY(-2px);
             box-shadow: 0 6px 14px rgba(15,27,63,.08), 0 16px 32px rgba(15,27,63,.09) }
.ptask-row:active { transform: translateY(0) scale(.995) }
.ptask-accent { flex: 0 0 4px; align-self: stretch; margin: 8px 0; border-radius: 0 4px 4px 0; background: var(--border-strong) }
.ptask-accent--urgent { background: var(--danger) }
.ptask-accent--high   { background: var(--warn) }
.ptask-accent--normal { background: var(--brand-cyan) }
.ptask-accent--low    { background: var(--border-strong) }
/* Done-check on each task row (per the mock's .ax-task .check) — a rounded
   square that fills green with a tick when the task is done. */
.ptask-check { flex: 0 0 22px; width: 22px; height: 22px; border-radius: 7px; align-self: center;
  border: 1.5px solid var(--border-strong); display: grid; place-items: center; color: transparent }
.ptask-check.is-done { background: var(--ok); border-color: var(--ok); color: #04231a }
.ptask-check .icon { width: 14px; height: 14px }
.ptask-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center; gap: 5px; padding: 1px 0 }
.ptask-title { font-size: 14.5px; font-weight: 600; color: var(--text); line-height: 1.32;
               overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical }
.ptask-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 9px; font-size: 12px; color: var(--text-3) }
.ptask-due { font-weight: 600; color: var(--text-2) }
.ptask-due--od { color: var(--danger) }
/* Decodable priority — a small labelled flag for high/urgent only, so triage
   doesn't depend on reading the colour rail alone (phase94c). */
.ptask-prio { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-weight: 700;
              letter-spacing: .2px; text-transform: uppercase }
.ptask-prio svg { width: 12px; height: 12px }
.ptask-prio--urgent { color: var(--danger) }
.ptask-prio--high   { color: var(--warn) }
.ptask-proj { display: inline-flex; align-items: center; gap: 4px; color: var(--text-3); min-width: 0 }
.ptask-proj svg { width: 12px; height: 12px; flex: 0 0 auto }
.ptask-chev { flex: 0 0 auto; align-self: center; color: var(--border-strong); width: 18px; height: 18px }
.ptask-row.is-done { opacity: .72 }
.ptask-row.is-done .ptask-title { text-decoration: line-through; color: var(--text-3); font-weight: 500 }

/* ── Slide-to-complete (mobile task list, 2026-07-06) ─────────────────────
   Open rows are wrapped in .ptask-swipe with a green "Done" layer behind the
   card. assets/public.js slides the row left on a touch drag; past ~⅖ it arms,
   and releasing submits the complete form. A tap still opens the task, and with
   no JS the card is a plain link (the layer never shows). */
.ptask-swipe { position: relative; margin-bottom: 8px }
.ptask-swipe > .ptask-row { margin-bottom: 0; position: relative; z-index: 1; touch-action: pan-y }
.ptask-swipe.is-swiping > .ptask-row { transition: none }
.ptask-swipe:not(.is-swiping) > .ptask-row { transition: transform .22s var(--ease), opacity .22s var(--ease) }
.ptask-swipe.is-swiping, .ptask-swipe.is-completing { overflow: hidden; border-radius: var(--radius) }
.ptask-swipe-fill { position: absolute; inset: 0; z-index: 0; border-radius: var(--radius);
  background: var(--ok); color: #fff; display: flex; align-items: center; justify-content: flex-end;
  gap: 8px; padding-right: 22px; font-weight: 700; font-size: 14px; opacity: 0; pointer-events: none }
.ptask-swipe-fill svg { width: 20px; height: 20px }
.ptask-swipe.is-swiping .ptask-swipe-fill, .ptask-swipe.is-armed .ptask-swipe-fill { opacity: 1 }
.ptask-swipe.is-completing > .ptask-row { transform: translateX(-100%); opacity: 0 }

/* ── Desktop task rows (2026-07-06) ───────────────────────────────────────
   The stacked title-over-meta card is right for a phone, but on a wide screen
   it leaves ~60% of each row empty and reads as a mobile layout stretched out.
   From the app desktop breakpoint (matches .app-main at 900px), reflow the row
   onto ONE line — title grows and truncates on the left, the status/priority/
   due/project cluster right-aligns beside the chevron — so the row uses its
   width and scans like a proper desktop list. Also caps pushed detail screens
   (task view / new task) to a comfortable reading column instead of one very
   wide 980px stripe. Mobile is untouched (below the query). */
@media (min-width: 900px) {
  .ptask-row { padding: 12px 16px 12px 0; gap: 14px; }
  .ptask-main { flex-direction: row; align-items: center; gap: 20px; }
  .ptask-title { flex: 1 1 auto; display: block; white-space: nowrap; overflow: hidden;
                 text-overflow: ellipsis; font-size: 15px; }
  .ptask-meta { flex: 0 0 auto; flex-wrap: nowrap; justify-content: flex-end; gap: 8px 16px; font-size: 12.5px; }
  .ptask-meta > * { white-space: nowrap; }
  .ptask-proj { max-width: 220px; }
  .ptask-proj span, .ptask-proj { overflow: hidden; text-overflow: ellipsis; }

  /* Home hub "Due this week" list lives in a NARROW two-column card, not the
     full-width task list — the single-line reflow squeezes its titles to
     "Terminate patc…". Keep those rows stacked (title over meta) so the name
     stays readable; the single-line layout is only for the wide list contexts. */
  .phome-cols .ptask-main { flex-direction: column; align-items: stretch; gap: 4px; }
  .phome-cols .ptask-title { white-space: normal; overflow: visible; }
  .phome-cols .ptask-meta { justify-content: flex-start; }

  /* Pushed detail screens read as a centred reading column, not a full-width
     stripe — the wide "Mark complete" button + chat bubbles feel intentional. */
  body.is-app-detail .app-main { max-width: 760px; }
  /* …except list-shaped detail screens (Notifications), which should match the
     full width of the other top-level lists rather than sit narrower. */
  body.is-app-detail.is-app-wide .app-main { max-width: var(--portal-content); }
}
.pt-empty { text-align: center; color: var(--text-3); padding: 52px 22px 44px; font-size: 14px; line-height: 1.5 }
.pt-empty .icon { width: 30px; height: 30px; color: var(--border-strong); margin-bottom: 8px }
/* Designed empty state (2026-07-03): the leading icon sits in a soft brand
   gradient disc so "nothing here" reads as intentional, not bare. */
.pt-empty > div:first-child { width: 66px; height: 66px; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center;
   border-radius: 50%; background: linear-gradient(135deg, var(--brand-cyan-tint), var(--brand-cyan-soft)) }
.pt-empty > div:first-child .icon { width: 30px; height: 30px; color: var(--brand-cyan-dark); margin: 0 }

/* ── Portal task detail — chat-style comment thread + sticky composer ── */
.pt-thread { display: flex; flex-direction: column; gap: 12px; margin-bottom: 14px }
.pt-bubble { max-width: 84%; display: flex; flex-direction: column; gap: 3px }
.pt-bubble-body { padding: 10px 13px; border-radius: 14px; font-size: 14px; line-height: 1.45; word-break: break-word }
.pt-bubble-meta { font-size: 11px; color: var(--text-3); padding: 0 4px }
.pt-bubble--me { align-self: flex-end; align-items: flex-end }
.pt-bubble--me .pt-bubble-body { background: var(--brand-cyan); color: var(--accent-ink); border-bottom-right-radius: 5px }
.pt-bubble--them { align-self: flex-start; align-items: flex-start }
.pt-bubble--them .pt-bubble-body { background: var(--surface2, #EEF2F7); color: var(--text); border-bottom-left-radius: 5px }
.pt-thread-empty { color: var(--text-3); font-size: 14px; text-align: center; padding: 22px 8px }
.pt-composer { position: sticky; bottom: 0; background: var(--surface); border-top: 1px solid var(--border);
               padding: 12px 0 4px; margin-top: 6px; display: flex; flex-direction: column; gap: 8px }
.pt-composer textarea { width: 100%; font-size: 16px; resize: vertical; min-height: 48px }
/* Photo attach on the task chat (phase96). */
.pt-composer-row { display: flex; align-items: stretch; justify-content: space-between; gap: 8px }
/* Camera matches the Send button's height (both stretch to the row). */
.pt-composer-cam { position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 48px; min-height: 40px; border-radius: 12px; color: var(--brand-cyan-dark); background: var(--surface-alt);
  border: 1px solid var(--border); cursor: pointer; transition: background var(--speed) var(--ease), color var(--speed) var(--ease) }
.pt-composer-cam:hover { background: var(--brand-cyan-tint) }
.pt-composer-cam.has-photo { color: #fff; background: var(--ok); border-color: var(--ok) }
.pt-composer-cam svg { width: 22px; height: 22px }
.pt-composer-cam input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer }
.pt-photo-preview { position: relative; display: inline-block; align-self: flex-start }
.pt-photo-preview[hidden] { display: none }
.pt-photo-preview img { display: block; max-width: 130px; max-height: 130px; border-radius: 10px; border: 1px solid var(--border) }
.pt-photo-preview-x { position: absolute; top: -8px; right: -8px; width: 24px; height: 24px; border-radius: 999px;
  border: 0; background: var(--brand-navy); color: #fff; font-size: 12px; line-height: 1; cursor: pointer; box-shadow: var(--shadow-sm) }
/* Photo bubbles in the thread. */
.pt-bubble-photo { display: block; margin-bottom: 4px }
.pt-bubble-photo img { display: block; max-width: 240px; max-height: 280px; width: auto; height: auto;
  border-radius: 12px; border: 1px solid var(--border); background: var(--surface-alt) }
.pt-detail-title { font-size: 20px; font-weight: 700; color: var(--text); line-height: 1.25; letter-spacing: -0.2px; margin-bottom: 10px }
.pt-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 14px }
/* Priority as a quiet outline chip with a colour-coded dot (2026-07-13). The
   solid status badge is the headline state; priority sits beside it as lighter
   secondary metadata rather than a second competing solid pill. */
.pt-prio-chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 11px 3px 9px;
  border-radius: 999px; border: 1px solid var(--border); background: var(--surface);
  font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px;
  line-height: 1.5; color: var(--text-2); white-space: nowrap }
.pt-prio-chip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
  background: var(--text-3) }
.pt-prio-chip--low::before    { background: var(--ok) }
.pt-prio-chip--normal::before { background: var(--brand-cyan) }
.pt-prio-chip--high    { color: var(--warn-text, #B45309); border-color: var(--warn-bg, #FCE6C4) }
.pt-prio-chip--high::before    { background: var(--warn) }
.pt-prio-chip--urgent  { color: var(--danger-text, #C0362C); border-color: var(--danger-bg, #FBD5D2) }
.pt-prio-chip--urgent::before  { background: var(--danger) }

/* "Waiting on" — the still-open tasks that block this one (2026-07-13; the
   section had no styles before and rendered as loose unstyled text). An amber
   "on hold" callout listing each open blocker with its status pill. Done
   dependencies are filtered out in PHP, so this only ever shows real holds. */
.pt-blockers { margin: 0 0 14px; padding: 11px 13px 12px; border-radius: 12px;
  background: var(--warn-bg, #FEF3E2); border: 1px solid var(--warn-bg, #FCE6C4) }
.pt-blockers-h { display: flex; align-items: center; gap: 7px; font-size: 11.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .5px; color: var(--warn-text, #B45309); margin-bottom: 2px }
.pt-blockers-h .icon { width: 15px; height: 15px }
/* Folds the old "Blocked — finish the tasks above" line into the panel, so the
   blocked state is stated once, here, instead of three times. */
.pt-blockers-sub { font-size: 12.5px; line-height: 1.4; color: var(--warn-text, #B45309); opacity: .85; margin: 0 0 9px }
.pt-blocker { display: flex; align-items: center; gap: 9px; padding: 6px 0; min-width: 0 }
.pt-blocker + .pt-blocker { border-top: 1px solid rgba(180,83,9,.14) }
.pt-blocker-t { font-size: 14px; font-weight: 600; color: var(--text); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap }

/* ── Task-detail sections (2026-07-13 rework) ─────────────────────────────
   A small uppercase header labels each region (Brief / Details / Checklist)
   so nothing floats unlabelled, and the regions get an even vertical rhythm. */
.pt-sec { margin: 18px 0 0 }
.pt-sec-h { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 700;
  letter-spacing: .6px; text-transform: uppercase; color: var(--text-3); margin: 0 0 9px }
.pt-sec-h .icon { width: 14px; height: 14px; color: var(--text-3) }
.pt-sec-h-n { margin-left: auto; font-family: var(--font-mono); font-size: 11px; font-weight: 700; color: var(--text-3) }
.pt-sec .ax-facts { margin: 0 }

/* Brief — a framed reading block instead of a bare paragraph. */
.pt-desc { color: var(--text-1); font-size: 14.5px; line-height: 1.6; margin: 0;
  padding: 12px 14px; background: var(--surface2, #F4F7FB); border: 1px solid var(--border); border-radius: 11px }

/* Action bar — the complete control. When a task is blocked by an open
   dependency NO action is rendered at all: the "Waiting on" panel is the sole
   notice that it can't be completed yet, so there's no locked-button repeat. */
.pt-actionbar { margin: 18px 0 0 }
.pt-actionbar .pt-statusbar { margin: 0 }
/* Facts grid (2026-07-06, per the mock's .ax-facts) — 2×2 key facts on the task
   detail: Due / Priority / Project / Assignee. */
.ax-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border);
  border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; margin: 14px 0 }
.ax-facts .ax-f { background: var(--surface); padding: 12px 14px }
.ax-f-l { display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3) }
.ax-f-v { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600;
  color: var(--text); margin-top: 5px }
.ax-f--od .ax-f-v { color: var(--danger) }
.ax-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--text-3) }
.ax-avatar { width: 22px; height: 22px; border-radius: 50%; background: var(--surface3);
  border: 1px solid var(--border-strong); display: inline-grid; place-items: center;
  font-family: var(--font-mono); font-size: 9px; font-weight: 600; color: var(--text) }

/* Key facts list (phase94c) — due / project / dates, surfaced up top. */
.pt-facts { display: flex; flex-direction: column; gap: 9px; margin: 14px 0 2px;
            padding: 12px 0; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border) }
.pt-fact { display: flex; align-items: center; gap: 9px; font-size: 14px; min-width: 0 }
.pt-fact .icon { width: 16px; height: 16px; color: var(--text-3); flex: 0 0 auto }
.pt-fact-l { color: var(--text-3); flex: 0 0 auto; min-width: 52px }
.pt-fact-v { color: var(--text); font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.pt-fact--od .icon, .pt-fact--od .pt-fact-v { color: var(--danger) }
/* Status update: a dropdown + Update button. */
/* One-tap status (phase91): a single toggle tick — tap to complete, tap again
   to bring the task back to pending. Replaces the old dropdown + Update button. */
.pt-statusbar { margin: 16px 0 6px }
.pt-complete { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 52px; padding: 12px 15px;
               border: 1.5px solid var(--border-strong); border-radius: 12px; background: var(--surface);
               font-size: 16px; font-weight: 600; color: var(--text); cursor: pointer; text-align: left }
.pt-complete:hover { border-color: var(--ok); color: var(--ok-text) }
.pt-complete-box { flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%; border: 2px solid var(--border-strong);
                   display: inline-flex; align-items: center; justify-content: center; color: transparent }
.pt-complete:hover .pt-complete-box { border-color: var(--ok); color: var(--ok) }
.pt-complete-box .icon { width: 16px; height: 16px }
.pt-complete.is-done { border-color: var(--ok); color: var(--ok-text); background: var(--ok-bg) }
.pt-complete.is-done .pt-complete-box, .pt-complete-box.is-checked { background: var(--ok); border-color: var(--ok); color: #04231a }
.pt-complete-hint { margin-left: auto; font-size: 12.5px; font-weight: 600; color: var(--ok-text); opacity: .75 }

/* Slide-to-complete track (phase91). The knob (green, with a › affordance)
   drags from the left; assets/public.js submits past ~⅔. A plain tap still
   completes (the button is a real submit) so it degrades gracefully. */
.pt-slide { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; min-height: 56px;
            padding: 4px; border: 1.5px solid var(--border-strong); border-radius: 14px; background: var(--surface2);
            font-size: 15px; font-weight: 600; color: var(--text-3); cursor: pointer; overflow: hidden;
            touch-action: pan-y; user-select: none; -webkit-user-select: none }
.pt-slide-label { position: relative; z-index: 1; pointer-events: none }
.pt-slide-knob { position: absolute; left: 4px; top: 4px; bottom: 4px; width: 50px; z-index: 2; border-radius: 11px;
                 background: var(--brand-cyan); color: var(--accent-ink); display: inline-flex; align-items: center; justify-content: center;
                 box-shadow: 0 1px 5px rgba(0,0,0,.3); transition: transform .18s ease }
.pt-slide-knob .icon { width: 22px; height: 22px }
.pt-slide.is-dragging .pt-slide-knob { transition: none }
.pt-slide.is-armed { border-color: var(--ok); background: var(--ok-bg); color: var(--ok-text) }
.pt-slide:hover { border-color: var(--brand-cyan) }
/* Touch phones: the slide IS the control — dragged with a finger. Desktop /
   mouse (2026-07-13): a slide gesture makes no sense with a cursor, and a
   click should match the label — so on a fine pointer present a plain "Mark
   complete" CTA button (the slide JS is skipped there). This restores the
   affordance/behaviour match: on desktop you click a button that says click. */
.pt-slide-label-desk { display: none }
@media (pointer: fine) {
  .pt-slide { justify-content: center; gap: 8px; border-color: var(--cta); background: var(--cta);
              color: var(--accent-ink); box-shadow: 0 1px 2px rgba(0,0,0,.3), 0 4px 12px rgba(26,183,234,.18);
              min-height: 52px; cursor: pointer }
  .pt-slide:hover { border-color: var(--cta-hover); background: var(--cta-hover) }
  .pt-slide-knob { display: none }
  .pt-slide-label { display: none }
  .pt-slide-label-desk { display: inline-flex; align-items: center; gap: 8px; position: relative; z-index: 1;
                         pointer-events: none }
  .pt-slide-label-desk .icon { width: 18px; height: 18px }
}

/* Checklist / subtasks on the portal (phase91). */
.pt-subs { margin: 4px 0 14px }
/* Progress bar IS the checklist header (phase96b) — the bar fills the row, the
   fraction sits at its end; no redundant "Checklist" label. */
.pt-subs-progress { display: flex; align-items: center; gap: 10px; margin-bottom: 12px }
.pt-subs-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px }
.pt-subs-title { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 700; color: var(--text-2);
                 text-transform: uppercase; letter-spacing: .4px }
.pt-subs-title .icon { width: 16px; height: 16px; color: var(--text-3) }
.pt-subs-count { flex: 0 0 auto; font-size: 12.5px; font-weight: 700; color: var(--text-3); font-variant-numeric: tabular-nums }
.pt-subs-bar { flex: 1 1 auto; height: 6px; border-radius: 4px; background: var(--surface2, #EEF2F7); overflow: hidden; margin: 0 }
.pt-subs-fill { display: block; height: 100%; border-radius: 4px; background: var(--ok); min-width: 0 }
.pt-sub { display: block; margin: 0 0 6px }
.pt-sub-btn { display: flex; align-items: center; gap: 11px; width: 100%; min-height: 48px; padding: 9px 12px;
              border: 1px solid var(--border); border-radius: 10px; background: var(--surface); color: var(--text);
              font-size: 15px; font-weight: 500; text-align: left; cursor: pointer }
.pt-sub-btn:hover { border-color: var(--border-strong) }
.pt-sub-box { flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--border-strong);
              display: inline-flex; align-items: center; justify-content: center; color: transparent }
.pt-sub-box.is-checked { background: var(--ok); border-color: var(--ok); color: #04231a }
.pt-sub-box .icon { width: 15px; height: 15px }
.pt-sub-title { min-width: 0; flex: 1 1 auto }
.pt-sub.is-done .pt-sub-title { color: var(--text-3); text-decoration: line-through }
/* Subcontractor adds a checklist step (phase91b). */
.pt-subs-hint { font-size: 13px; color: var(--text-3); margin: 0 0 8px }
.pt-sub-add { position: relative; display: flex; align-items: center; margin-top: 6px }
.pt-sub-add-ic { position: absolute; left: 11px; display: inline-flex; pointer-events: none; color: var(--text-3) }
.pt-sub-add-ic .icon { width: 16px; height: 16px }
.pt-sub-add-in { flex: 1 1 auto; min-height: 46px; padding: 0 68px 0 36px; border: 1px dashed var(--border-strong);
                 border-radius: 10px; background: var(--surface); color: var(--text); font-size: 15px }
.pt-sub-add-in:focus { outline: none; border-style: solid; border-color: var(--brand-cyan); box-shadow: 0 0 0 3px rgba(0,169,224,.15) }
/* Secondary/ghost (2026-07-06) — "Add step" is a minor inline action; keeping it
   a solid cyan button made it compete with the primary actions. Cyan text on a
   transparent chip reads clearly without shouting. */
.pt-sub-add-btn { position: absolute; right: 6px; min-height: 40px; padding: 0 14px; border: 0; border-radius: 8px;
                  background: transparent; color: var(--brand-cyan-dark); font-size: 14px; font-weight: 700; cursor: pointer }
.pt-sub-add-btn:hover { background: var(--brand-cyan-tint) }

/* Canned quick-update chips above the composer (phase91b). */
.pt-canned { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px }
.pt-canned-f { margin: 0 }
.pt-canned-chip { min-height: 38px; padding: 0 14px; border: 1px solid var(--border); border-radius: 999px;
                  background: var(--surface2, #EEF2F7); color: var(--text-2); font-size: 13.5px; font-weight: 600; cursor: pointer }
.pt-canned-chip:hover { border-color: var(--brand-cyan); color: var(--brand-cyan-dark) }

/* ── PWA app shell (phase90) — portal task screens read like a native app ──
   A compact top app bar (navy, matches theme-color so the status-bar area
   blends), an optional bottom tab bar on list screens, safe-area insets, and
   no marketing header/footer. Opt-in per page via $portal_app_shell. */
body.is-app { background: var(--bg); min-height: 100dvh }
/* z-index 70 (was 60) so the header's stacking context — and the account/
   Sign-out dropdown inside it — sits ABOVE the desktop nav bar (.appnav, z-60).
   At 60 the nav tied on z and won by DOM order, painting over the open menu so
   "Sign out" couldn't be clicked on desktop. */
.appbar { position: sticky; top: 0; z-index: 70; background: var(--surface); color: var(--text);
          padding-top: env(safe-area-inset-top, 0px); box-shadow: var(--shadow-sm); border-bottom: 1px solid var(--border) }
.appbar-inner { display: flex; align-items: center; gap: 2px; min-height: 54px; padding: 0 8px 0 4px; max-width: var(--portal-content); margin: 0 auto }
.appbar-back { flex: 0 0 auto; width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center;
               color: var(--text-2); background: none; border: 0; border-radius: 10px; cursor: pointer; text-decoration: none }
.appbar-back:hover { background: var(--surface2); color: var(--text); text-decoration: none }
.appbar-back svg { width: 23px; height: 23px }
.appbar-titles { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; justify-content: center; padding: 0 6px }
.appbar-title { font-size: 17px; font-weight: 700; line-height: 1.2; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.appbar-sub { font-size: 12px; color: var(--text-3); line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
/* Top-level app bar: Araneae mark + name on the left (native app-bar branding). */
.appbar-brand { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 9px; padding-left: 8px }
.appbar-brand-mark { flex: 0 0 auto; width: 28px; height: 28px; border-radius: 6px; display: block }
/* Full navy wordmark logo in the app bar (per the mock). Capped in height so the
   emblem + name lockup sits neatly in the bar. */
.appbar-brand-logo { flex: 0 0 auto; height: 34px; width: auto; max-width: 210px; display: block; object-fit: contain }
.appbar-brand-name { min-width: 0; font-size: 16px; font-weight: 700; letter-spacing: .1px; color: var(--text);
                     white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.appbar-acct { flex: 0 0 auto }
.appbar-right { display: flex; align-items: center; gap: 4px; flex: 0 0 auto }
.appbar-bell { position: relative; display: inline-flex; align-items: center; justify-content: center;
               width: 40px; height: 40px; border-radius: 50%; color: var(--text-2); text-decoration: none }
.appbar-bell:hover { background: var(--surface2) }
.appbar-bell svg { width: 22px; height: 22px }
.appbar-bell-dot { position: absolute; top: 3px; right: 2px; min-width: 17px; height: 17px; padding: 0 4px;
                   display: inline-flex; align-items: center; justify-content: center; border-radius: 999px;
                   background: var(--danger, #e5484d); color: #fff; font-size: 10.5px; font-weight: 700;
                   border: 2px solid var(--surface); box-sizing: content-box }

/* Portal notifications: mark-all bar (phase92). */
.pt-notif-allbar { display: flex; align-items: center; justify-content: space-between; margin: 0 0 12px }
.pt-notif-allbar-n { font-size: 13px; font-weight: 600; color: var(--text-2) }
.pt-notif-allbtn { min-height: 38px; padding: 0 14px; border: 1px solid var(--border); border-radius: 999px;
                   background: var(--surface); color: var(--brand-cyan-dark); font-size: 13.5px; font-weight: 600; cursor: pointer }
.pt-notif-allbtn:hover { border-color: var(--brand-cyan) }
/* Portal notification list — mirrors the office .nt-* list (phase92). */
.nt-list { padding: 0; overflow: hidden }
.nt-item { display: flex; align-items: flex-start; gap: 12px; padding: 13px 14px; border-bottom: 1px solid var(--border);
           text-decoration: none; color: inherit }
.nt-item:last-child { border-bottom: 0 }
.nt-item:hover { background: var(--surface2, #f4f7fb); text-decoration: none }
.nt-item.is-unread { background: rgba(0,169,224,.08) }
.nt-item-ic { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
              border-radius: 50%; background: var(--surface2, #eef2f7); color: var(--text-3) }
.nt-item.is-unread .nt-item-ic { background: var(--brand-cyan); color: var(--accent-ink) }
.nt-item-ic .icon { width: 18px; height: 18px }
.nt-item-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px }
.nt-item-title { font-size: 14px; color: var(--text-1); font-weight: 500; overflow-wrap: anywhere }
.nt-item.is-unread .nt-item-title { font-weight: 650 }
.nt-item-body { font-size: 13px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.nt-item-time { font-size: 11.5px; color: var(--text-3); margin-top: 1px }
.nt-item-dot { flex: 0 0 auto; width: 9px; height: 9px; border-radius: 50%; background: var(--brand-cyan); margin-top: 7px }

/* ── Portal polish (phase93c) ────────────────────────────────────────────── */
.phub-greet { margin: 0 0 16px }
.phub-greet-hi { font-size: 21px; font-weight: 700; color: var(--text-1); letter-spacing: -0.2px; line-height: 1.2;
                 overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical }
.phub-greet-sub { font-size: 13px; color: var(--text-3); margin-top: 2px }
/* Native tap feel: kill the blue flash + a subtle press-down on interactive
   surfaces so the PWA feels responsive to touch. */
body.is-app { -webkit-tap-highlight-color: transparent }
.ptask-row, .phub-action, .phub-kpi, .pt-seg, .pt-canned-chip, .nt-item,
.appbar-bell, .pt-sub-btn, .pt-complete, .phub-proj {
  transition: transform .08s ease, background-color .12s ease, border-color .12s ease;
  -webkit-tap-highlight-color: transparent;
}
.ptask-row:active, .phub-action:active, .phub-kpi:active, .nt-item:active,
.phub-proj:active { transform: scale(.985) }
.pt-seg:active, .pt-canned-chip:active, .appbar-bell:active,
.pt-sub-btn:active, .pt-complete:active { transform: scale(.96) }
@media (prefers-reduced-motion: reduce) {
  .ptask-row, .phub-action, .phub-kpi, .pt-seg, .pt-canned-chip, .nt-item,
  .appbar-bell, .pt-sub-btn, .pt-complete, .phub-proj { transition: none }
  .ptask-row:active, .phub-action:active, .phub-kpi:active, .nt-item:active,
  .phub-proj:active, .pt-seg:active, .pt-canned-chip:active, .appbar-bell:active,
  .pt-sub-btn:active, .pt-complete:active { transform: none }
}

.app-main { width: 100%; max-width: var(--portal-content); margin: 0 auto; min-width: 0;
            padding: 14px 14px calc(20px + env(safe-area-inset-bottom, 0px)) }
body.has-appnav .app-main { padding-bottom: calc(74px + env(safe-area-inset-bottom, 0px)) }
/* The "New task" FAB (portal_my_tasks admins) floats above the tab bar, so it
   overhangs the default 74px nav clearance — reserve extra room so the last
   list row is never hidden behind it. */
body.has-pt-fab.has-appnav .app-main { padding-bottom: calc(150px + env(safe-area-inset-bottom, 0px)) }
body.is-app .shell, body.is-app .shell-narrow { max-width: none; margin: 0; padding: 0; min-width: 0 }
/* Keep the horizontally-scrollable chip row from stretching the page (flexbox
   min-content blowout): let it shrink below its content so overflow-x engages. */
body.is-app .pt-segs { min-width: 0; max-width: 100% }

.appnav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 60; display: flex; justify-content: center;
          background: var(--surface); border-top: 1px solid var(--border); box-shadow: 0 -1px 10px rgba(15,27,63,.06);
          padding-bottom: env(safe-area-inset-bottom, 0px) }
.appnav-inner { display: flex; width: 100%; max-width: var(--portal-content); margin: 0 auto }
.appnav-item { flex: 1 1 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
               min-height: 58px; text-decoration: none; color: var(--text-3); font-size: 11px; font-weight: 600 }
.appnav-item:hover { text-decoration: none; color: var(--brand-cyan-dark) }
.appnav-item.is-active { color: var(--brand-cyan-dark) }
.appnav-item svg { width: 23px; height: 23px }
/* On the app shell the sticky composer clears the home indicator. */
body.is-app .pt-composer { padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px)) }

/* Client overview + coordinator home: the header BARS (app bar / hero band) stay
   full-bleed, but their CONTENT caps at the SAME --portal-content column as every
   other portal screen so the content edges never shift as you navigate between
   screens (owner request: one grid for all elements). */
body.portal-home.is-app .app-main { max-width: none; margin: 0; padding: 0 }
body.portal-home.is-app .shell,
body.portal-home.is-app .shell-narrow {
  max-width: var(--portal-content); margin: 0 auto; padding: 20px 20px 32px; min-width: 0;
}
/* Account menu (app-bar action) on the white app bar — matches the client
   brand-bar chip: light surface, navy ink, cyan avatar with dark ink. */
body.is-app .appbar .acct-menu-btn { color: var(--text) }
body.is-app .appbar .acct-menu-avatar { background: var(--brand-cyan); color: var(--accent-ink) }
/* Client name beside the avatar in the app bar (per the mock's "Marina Khoury ·
   Meraas FitOut"). Hidden on narrow screens where the avatar alone suffices. */
.appbar-acct-name { font-size: 13px; font-weight: 600; color: var(--text); max-width: 220px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 4px }
@media (max-width: 560px) { .appbar-acct-name { display: none } }

/* ── Desktop web layout (2026-07-03) ─────────────────────────────────────────
   The portal is phone-first, but on a wide screen it must read as a real web
   app instead of a 720px phone column stranded in the middle. Widen the shell
   and turn the fixed bottom tab bar into a horizontal top nav under the app bar
   (the tab bar now renders at the top of the DOM, so it flows there naturally).
   The app bar + tabs stay full-bleed; only their inner rows cap width. */
@media (min-width: 900px) {
  /* ONE width for every portal screen (owner request): app bar, content and the
     top tab bar all cap at --portal-content and center, so the content edges
     never shift as you move between Home / Tasks / a task / Calendar / Team. */
  .appbar-inner { max-width: var(--portal-content); padding-left: 20px; padding-right: 20px }
  .app-main { max-width: var(--portal-content); padding: 22px 20px 36px }
  body.has-appnav .app-main { padding-bottom: 36px }   /* no bottom bar to clear */
  body.has-pt-fab.has-appnav .app-main { padding-bottom: 96px }  /* clear the fixed FAB */

  .appnav { position: static; justify-content: center; background: var(--surface);
            border-top: 0; border-bottom: 1px solid var(--border); box-shadow: none;
            padding-bottom: 0 }
  .appnav-inner { max-width: var(--portal-content); margin: 0 auto; padding: 0 20px; justify-content: flex-start }
  .appnav-item { flex: 0 0 auto; flex-direction: row; gap: 8px; min-height: 50px; padding: 0 16px;
                 font-size: 13.5px; border-bottom: 2px solid transparent }
  .appnav-item svg { width: 19px; height: 19px }
  .appnav-item.is-active { color: var(--brand-cyan-dark); border-bottom-color: var(--brand-cyan) }

  /* Desktop uses the width: the four Home KPIs sit in ONE row (like the
     quick-actions), not two sparse columns; both grids gain a little more gap.
     body.is-app raises specificity above the base .phub-kpis (which is defined
     later in the file) so this override actually wins. */
  body.is-app .phub-kpis { grid-template-columns: repeat(4, 1fr); gap: 14px }
  body.is-app .phub-actions { gap: 12px }
}

/* ── Portal calendar (mobile month grid) + timeline (phase90) ───────────── */
.pcal-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 14px }
.pcal-month { font-size: 15px; font-weight: 700; color: var(--text) }
.pcal-navbtn { flex: 0 0 auto; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center;
               border-radius: 11px; border: 1px solid var(--border); background: var(--surface); color: var(--text-2);
               font-size: 18px; text-decoration: none; box-shadow: 0 1px 2px rgba(15,27,63,.04);
               transition: transform .12s ease, border-color .15s, color .15s, box-shadow .15s }
.pcal-navbtn:hover { border-color: var(--brand-cyan-soft); text-decoration: none; color: var(--brand-cyan-dark);
               box-shadow: 0 3px 9px rgba(15,27,63,.09) }
.pcal-navbtn:active { transform: scale(.94) }
.pcal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px }
.pcal-head { margin-bottom: 4px }
.pcal-dow { text-align: center; font-size: 10.5px; font-weight: 700; color: var(--text-3); letter-spacing: .5px; padding: 2px 0 }
.pcal-cell { min-height: 56px; background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
             padding: 5px 3px 4px; display: flex; flex-direction: column; align-items: center; gap: 4px;
             box-shadow: 0 1px 2px rgba(15,27,63,.035);
             color: inherit; text-decoration: none; cursor: pointer;
             transition: border-color .12s ease, box-shadow .12s ease, transform .06s ease, background .12s ease }
.pcal-cell:hover { border-color: var(--brand-cyan-soft); text-decoration: none;
                   box-shadow: 0 3px 10px rgba(29,182,240,.14) }
.pcal-cell:active { transform: scale(.96) }
.pcal-cell:focus-visible { outline: 2px solid var(--brand-cyan); outline-offset: 2px }
.pcal-cell--empty { background: transparent; border: none; box-shadow: none; cursor: default; pointer-events: none }
.pcal-cell.is-today { border-color: var(--brand-cyan); box-shadow: inset 0 0 0 1px var(--brand-cyan), 0 3px 10px rgba(29,182,240,.18) }
/* Selected day (tapped): filled cyan wash so it reads as the active filter,
   and it wins over is-today. */
.pcal-cell.is-selected { border-color: var(--brand-cyan); background: var(--brand-cyan);
                         box-shadow: 0 4px 14px rgba(29,182,240,.32) }
.pcal-cell.is-selected .pcal-day { color: #fff }
.pcal-cell.is-selected .pcal-dot { background: rgba(255,255,255,.9) }
/* Header above the schedule list: title on the left, "whole month" reset on
   the right when a single day is filtered. */
.pcal-agenda-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 16px 4px 8px }
.pcal-agenda-head .pt-group-title { margin: 0 }
/* The day filter hides non-matching rows via the `hidden` attribute; a class
   rule on .ptask-row (display:flex) would otherwise beat the UA hidden rule. */
.pcal-agenda [hidden] { display: none }
.pcal-agenda-clear { flex: 0 0 auto; font-size: 12px; font-weight: 600; color: var(--brand-cyan-dark);
                     padding: 5px 11px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface) }
.pcal-agenda-clear:hover { border-color: var(--brand-cyan-soft); text-decoration: none; background: var(--surface-2, var(--surface)) }
.pcal-day { font-size: 12px; font-weight: 600; color: var(--text-2); line-height: 1 }
.pcal-cell.is-today .pcal-day { color: var(--brand-cyan-dark) }
.pcal-dots { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center }
.pcal-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border-strong) }
.pcal-dot--urgent { background: var(--danger) }
.pcal-dot--high   { background: var(--warn) }
.pcal-dot--normal { background: var(--brand-cyan) }
.pcal-dot--low    { background: var(--border-strong) }

.ptl-row { display: block; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
           padding: 12px 14px; margin-bottom: 9px; text-decoration: none; box-shadow: var(--shadow-sm) }
.ptl-row:hover { border-color: var(--border-strong); text-decoration: none }
.ptl-head { display: flex; align-items: center; gap: 10px; margin-bottom: 9px }
.ptl-title { flex: 1 1 auto; min-width: 0; font-size: 14.5px; font-weight: 600; color: var(--text);
             white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.ptl-track { position: relative; height: 12px; border-radius: 6px; background: var(--surface2, #EEF2F7); overflow: hidden }
.ptl-bar { position: absolute; top: 0; height: 12px; border-radius: 6px; background: var(--brand-cyan); min-width: 8px }
.ptl-bar--urgent { background: var(--danger) }
.ptl-bar--high   { background: var(--warn) }
.ptl-bar--normal { background: var(--brand-cyan) }
.ptl-bar--low    { background: var(--border-strong) }
.ptl-bar.is-od   { background: var(--danger) }
.ptl-dates { display: flex; justify-content: space-between; gap: 10px; margin-top: 6px; font-size: 11.5px; color: var(--text-3) }

/* Add-a-task FAB + the new-task form (phase90). */
/* Extended FAB (phase94b) — a LABELLED primary action ("New task") rather than a
   bare "+", so the main create action is unmistakable (Material extended-FAB). */
.pt-fab { position: fixed; right: 16px; z-index: 61; bottom: calc(78px + env(safe-area-inset-bottom, 0px));
          min-height: 52px; padding: 0 20px 0 15px; border-radius: 999px; display: inline-flex; align-items: center; gap: 7px;
          background: var(--cta); color: #fff; font-size: 15px; font-weight: 700; letter-spacing: .1px;
          box-shadow: 0 8px 22px rgba(6,118,161,.30), 0 2px 6px rgba(6,118,161,.26); text-decoration: none;
          transition: transform .15s ease, box-shadow .2s ease, background .15s }
.pt-fab:hover { background: var(--cta-hover); color: #fff; text-decoration: none; transform: translateY(-2px);
          box-shadow: 0 14px 30px rgba(6,118,161,.36), 0 3px 8px rgba(6,118,161,.3) }
.pt-fab:active { transform: translateY(0) scale(.97) }
.pt-fab svg { width: 21px; height: 21px }
.pt-fab-l { white-space: nowrap }
.pt-newform { display: flex; flex-direction: column; gap: 16px }
.pt-field { display: flex; flex-direction: column; gap: 6px }
.pt-field-label { font-size: 13px; font-weight: 600; color: var(--text-2) }
.pt-newform .form-input { font-size: 16px }   /* ≥16px stops iOS zoom-on-focus */

/* New-task form redesign (2026-07-03) — richer + matches the office task form:
   a prominent title, a responsive 2-column meta grid, and a segmented priority
   picker (radio pills, no JS). */
.pt-new-title { width: 100%; border: 0; border-bottom: 2px solid var(--border); background: transparent;
                padding: 6px 2px 10px; font-size: 22px; font-weight: 700; color: var(--text);
                line-height: 1.25; outline: none }
.pt-new-title::placeholder { color: var(--text-3); font-weight: 600 }
.pt-new-title:focus { border-bottom-color: var(--brand-cyan) }
.pt-new-grid { display: grid; grid-template-columns: 1fr; gap: 16px }
@media (min-width: 560px) { .pt-new-grid { grid-template-columns: 1fr 1fr } }
.pt-field--full { grid-column: 1 / -1 }
.pt-prio-seg { display: flex; gap: 6px; flex-wrap: wrap }
.pt-prio-opt { flex: 1 1 0; min-width: 74px; position: relative }
.pt-prio-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; pointer-events: none }
.pt-prio-opt > span { display: flex; align-items: center; justify-content: center; gap: 5px; padding: 10px 8px;
                      border: 1.5px solid var(--border); border-radius: 10px; background: var(--surface);
                      font-size: 13px; font-weight: 600; color: var(--text-2); cursor: pointer;
                      transition: border-color .12s, background .12s, color .12s }
.pt-prio-opt > span .icon { width: 15px; height: 15px }
.pt-prio-opt:hover > span { border-color: var(--border-strong) }
.pt-prio-opt input:focus-visible + span { outline: 2px solid var(--brand-cyan); outline-offset: 1px }
.pt-prio-opt input:checked + span { border-color: var(--brand-cyan); background: var(--brand-cyan-weak, #E7F6FE); color: var(--brand-cyan-dark) }
.pt-prio-opt--high input:checked + span   { border-color: var(--warn-text, #B45309); background: var(--warn-bg, #FEF3E2); color: var(--warn-text, #B45309) }
.pt-prio-opt--urgent input:checked + span { border-color: var(--danger-text, #C0362C); background: var(--danger-bg, #FDECEA); color: var(--danger-text, #C0362C) }

/* ── New task form — redesigned (2026-07-13) ──────────────────────────────
   A single elevated card: a hero title row (the task name reads as the
   headline), then a padded body of labelled fields with leading icons, and a
   sticky action bar so "Add task" is always in reach on a phone. Scoped to
   .ntf so the shared .pt-newform (portal pay/bill forms) is untouched. */
.ntf { display: flex; flex-direction: column; gap: 16px }
.ntf-card { background: var(--surface); border: 1px solid var(--border); border-radius: 18px; overflow: hidden;
            box-shadow: 0 1px 2px rgba(15,27,63,.04), 0 10px 30px rgba(15,27,63,.06) }

/* Hero title band — a tinted strip that frames the one input everything else
   hangs off. The eyebrow is a quiet uppercase label; the input is large and
   chrome-free so typing feels like naming, not filling a field. */
.ntf-hero { padding: 20px 22px 18px; background: linear-gradient(180deg, var(--brand-cyan-tint, #F0FAFE), var(--surface));
            border-bottom: 1px solid var(--border) }
.ntf-hero-eyebrow { display: block; font-size: 11px; font-weight: 700; letter-spacing: .8px; text-transform: uppercase;
                    color: var(--brand-cyan-dark); margin-bottom: 6px }
.ntf-title { width: 100%; border: 0; background: transparent; padding: 2px 0; color: var(--text);
             font-size: 24px; font-weight: 700; line-height: 1.2; letter-spacing: -.01em }
.ntf-title::placeholder { color: var(--text-3); font-weight: 700 }
.ntf-title:focus { outline: none }

.ntf-body { display: flex; flex-direction: column; gap: 18px; padding: 20px 22px 22px }
.ntf-grid { display: grid; grid-template-columns: 1fr; gap: 16px }
@media (min-width: 560px) { .ntf-grid { grid-template-columns: 1fr 1fr } }
.ntf-field { display: flex; flex-direction: column; gap: 7px; min-width: 0 }
.ntf-label { font-size: 12px; font-weight: 700; letter-spacing: .3px; text-transform: uppercase; color: var(--text-3) }
.ntf-req { color: var(--danger) }
.ntf-optional { text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--text-3); font-size: 11px;
                margin-left: 4px; opacity: .8 }

/* Control shell — a bordered box with a leading icon; the real <input>/<select>
   sits transparently on top so the icon reads as part of the field. */
.ntf-control { position: relative; display: flex; align-items: center; gap: 9px; padding: 0 12px;
               border: 1.5px solid var(--border); border-radius: 11px; background: var(--surface);
               min-height: 48px; transition: border-color .12s, box-shadow .12s }
.ntf-control > .icon:first-child { flex: 0 0 auto; width: 17px; height: 17px; color: var(--text-3) }
.ntf-control:focus-within { border-color: var(--brand-cyan); box-shadow: 0 0 0 3px rgba(0,169,224,.14) }
.ntf-control .ntf-input { border: 0; background: transparent; padding: 0; min-height: 46px; flex: 1 1 auto; min-width: 0 }
.ntf-control .ntf-input:focus { outline: none; box-shadow: none }
.ntf-control--select .ntf-input { appearance: none; -webkit-appearance: none; padding-right: 4px; cursor: pointer }
.ntf-control--select > .icon:last-child { flex: 0 0 auto; width: 16px; height: 16px; color: var(--text-3); pointer-events: none }

/* Bare inputs (the title + textarea live outside .ntf-control). ≥16px font
   stops iOS zoom-on-focus. */
.ntf-input { width: 100%; font-size: 16px; color: var(--text); font-family: inherit }
.ntf-textarea { border: 1.5px solid var(--border); border-radius: 11px; background: var(--surface);
                padding: 12px 13px; line-height: 1.5; resize: vertical; min-height: 108px }
.ntf-textarea:focus { outline: none; border-color: var(--brand-cyan); box-shadow: 0 0 0 3px rgba(0,169,224,.14) }
.ntf-textarea::placeholder { color: var(--text-3) }

/* Priority pills fill the row on the form (they read as one segmented band). */
.ntf-field .pt-prio-seg { gap: 8px }

/* Action bar — Cancel + Add task. On a phone it sticks to the bottom of the
   viewport (over a translucent backdrop) so the primary action never scrolls
   away; on desktop it just sits under the card, right-aligned. */
.ntf-actions { display: flex; gap: 10px; align-items: center }
.ntf-cancel { flex: 0 0 auto }
.ntf-submit { flex: 1 1 auto; display: inline-flex; align-items: center; justify-content: center; gap: 8px }
.ntf-submit .icon { width: 18px; height: 18px }
@media (max-width: 640px) {
  .ntf-actions { position: sticky; bottom: 0; z-index: 5; margin: 4px -4px 0; padding: 12px 4px calc(12px + env(safe-area-inset-bottom, 0px));
                 background: linear-gradient(180deg, rgba(244,247,251,0), var(--canvas, #F4F7FB) 42%) }
}
@media (min-width: 641px) {
  .ntf-actions { justify-content: flex-end }
  .ntf-submit { flex: 0 0 auto; min-width: 168px }
}

/* ── Portal ADMIN hub (phase90): Home KPIs, quick actions, projects ─────── */
.phub-kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin: 2px 0 14px }
.phub-kpi { display: flex; flex-direction: column; gap: 2px; padding: 15px 15px 13px; border-radius: 14px;
            background: var(--surface); border: 1px solid var(--border);
            box-shadow: 0 1px 2px rgba(15,27,63,.04), 0 4px 14px rgba(15,27,63,.05); text-decoration: none;
            transition: transform .15s ease, box-shadow .2s ease, border-color .15s }
.phub-kpi:hover { border-color: var(--border-strong); text-decoration: none; transform: translateY(-2px);
            box-shadow: 0 6px 14px rgba(15,27,63,.08), 0 16px 32px rgba(15,27,63,.09) }
.phub-kpi-n { font-size: 26px; font-weight: 700; line-height: 1.05; color: var(--text) }
.phub-kpi-l { font-size: 12.5px; font-weight: 600; color: var(--text-3) }
.phub-kpi--brand  .phub-kpi-n { color: var(--brand-cyan-dark) }
.phub-kpi--danger .phub-kpi-n { color: var(--danger) }
.phub-kpi--warn   .phub-kpi-n { color: var(--warn) }
.phub-kpi--muted  .phub-kpi-n { color: var(--text-2) }

/* Quick create actions — a visible grid, not a clipping scroll strip, so every
   action is obvious at a glance (phase94b). Navigation destinations live in the
   bottom tab bar; this row is CREATE-only. */
.phub-actions { display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 200px)); justify-content: start; gap: 8px; margin: 0 0 18px }
.phub-action { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 14px 6px;
               border-radius: 14px; background: var(--surface); border: 1px solid var(--border); text-decoration: none;
               color: var(--text-2); font-size: 11.5px; font-weight: 600;
               box-shadow: 0 1px 2px rgba(15,27,63,.04), 0 3px 10px rgba(15,27,63,.045);
               transition: transform .15s ease, box-shadow .2s ease, border-color .15s, color .15s }
.phub-action:hover { border-color: var(--brand-cyan-soft); color: var(--brand-cyan-dark); text-decoration: none;
               transform: translateY(-2px); box-shadow: 0 6px 14px rgba(15,27,63,.08), 0 14px 28px rgba(15,27,63,.09) }
.phub-action-ic { display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px;
                  border-radius: 12px; background: linear-gradient(135deg, var(--brand-cyan-tint), var(--brand-cyan-soft)); color: var(--brand-cyan-dark) }
.phub-action-ic .icon { width: 20px; height: 20px }

.phub-group-n { margin-left: 6px; font-size: 12px; font-weight: 700; color: var(--text-3) }
.pt-group-title--person { color: var(--text) }

.phub-proj { display: block; text-decoration: none; color: inherit; cursor: pointer;
             background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm);
             padding: 13px 14px; margin-bottom: 9px; box-shadow: var(--shadow-sm) }
.phub-proj:hover { border-color: var(--border2) }
.phub-proj-head { display: flex; align-items: center; gap: 10px; margin-bottom: 9px }
.phub-proj-name { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 7px; font-size: 14.5px; font-weight: 600;
                  color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.phub-proj-name .icon { flex: 0 0 auto; width: 17px; height: 17px; color: var(--text-3) }
.phub-proj-pct { flex: 0 0 auto; font-size: 13px; font-weight: 700; color: var(--brand-cyan-dark) }
.phub-bar { height: 7px; border-radius: 4px; background: var(--surface2, #EEF2F7); overflow: hidden }
.phub-bar-fill { display: block; height: 100%; border-radius: 4px; background: var(--brand-cyan); min-width: 2px }
.phub-proj-meta { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 12.5px; color: var(--text-3) }
.phub-proj-od { color: var(--danger); font-weight: 600 }
.phub-dot { color: var(--border-strong) }

/* ── Portal ADMIN billing (phase91): quote/invoice + payment forms ──────── */
.pbill-typeseg { margin-bottom: 12px }
/* Light, numbered line rows (phase94d) — no heavy bordered box per line, so a
   quick 1–2 line invoice no longer shows a wall of empty boxes. */
.pbill-lines { display: flex; flex-direction: column; gap: 12px }
.pbill-line { display: flex; gap: 9px; align-items: flex-start }
.pbill-line-n { flex: 0 0 auto; width: 20px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
                color: var(--text-3); font-size: 13px; font-weight: 700 }
.pbill-line-fields { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 6px }
.pbill-line-qp { display: grid; grid-template-columns: 1fr 1fr; gap: 6px }
/* Description is a catalog type-ahead (list="pbill-item-list"): a matching item
   auto-fills the unit price. A faint downward chevron hints the suggestions,
   mirroring a native select without overriding the datalist's own picker. */
.pbill-line-fields input[data-item-desc] {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center; padding-right: 34px;
}
.pbill-line-fields input[data-item-desc]:focus { background-image: none; padding-right: 12px }
/* "Add more lines" disclosure — extra rows on demand (no-JS <details>). */
.pbill-more { margin-top: 10px }
.pbill-more > summary { cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
                        color: var(--brand-cyan-dark); font-size: 14px; font-weight: 600; padding: 4px 0 }
.pbill-more > summary::-webkit-details-marker { display: none }
.pbill-more > summary .icon { width: 16px; height: 16px }
.pbill-hint { margin: 12px 0 0; font-size: 12px; color: var(--text-3) }
.pbill-done { text-align: center; padding: 22px 16px }
.pbill-done-ic { display: inline-flex; align-items: center; justify-content: center; width: 56px; height: 56px;
                 border-radius: 50%; background: var(--brand-cyan-tint); color: var(--brand-cyan-dark); margin-bottom: 12px }
.pbill-done-ic .icon { width: 30px; height: 30px }
.pbill-done-h { margin: 0 0 6px; font-size: 18px; font-weight: 700; color: var(--text) }
.pbill-done-p { margin: 0 0 18px; font-size: 13.5px; color: var(--text-2); line-height: 1.5 }
.pbill-done-actions { display: block }

/* ── Reduced motion ──────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ── Print (so customers can print invoices/statements from portal) ── */
@media print {
  .brand-bar, .brand-footer, .brand-bar-actions, .btn, .fab-stack { display: none !important }
  body { background: #fff }
  .card { box-shadow: none; border-color: #999 }
  .tbl thead th { background: #f0f0f0 }
}

/* ── Floating quick-contact + footer site links (2026-06-15) ─────────────
   Mirrors the FabStack on araneae.net: circular floating buttons, bottom-right,
   so a customer on the portal is one tap from WhatsApp / our contact page. */
.fab-stack {
  position: fixed; right: 22px; bottom: 22px;
  display: flex; flex-direction: column; gap: 12px; z-index: 120;
}
/* Scroll-reveal: JS adds .fab-reveal so the FAB starts hidden and slides/fades
   in once the customer scrolls down a little. Without JS the class is never
   added → the FAB is simply always visible (progressive enhancement). */
.fab-stack.fab-reveal {
  opacity: 0; transform: translateY(20px) scale(.94); pointer-events: none;
  transition: opacity .4s ease, transform .5s cubic-bezier(.22, 1, .36, 1);
}
.fab-stack.fab-reveal.is-visible {
  opacity: 1; transform: translateY(0) scale(1); pointer-events: auto;
}
.fab-btn {
  width: 52px; height: 52px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #fff; text-decoration: none; border: none; cursor: pointer;
  box-shadow: 0 4px 12px rgba(15,27,63,.26);
  transition: transform .15s ease, box-shadow .15s ease;
}
.fab-btn:hover, .fab-btn:focus-visible { transform: translateY(-3px) scale(1.06) }
.fab-btn:active { transform: translateY(-1px) scale(.97) }
.fab-btn svg { width: 26px; height: 26px; display: block }
.fab-whatsapp { background: #25D366 }
.fab-contact  { background: var(--brand-cyan-dark, #0676A1) }
/* Coloured glow on hover, per button. A SYMMETRIC halo (0-offset) + a small
   ambient drop — deliberately compact so it stays inside the viewport margin
   and never clips into a hard rectangle at the bottom-right corner. */
.fab-whatsapp:hover, .fab-whatsapp:focus-visible {
  box-shadow: 0 0 16px 1px rgba(37, 211, 102, .6), 0 4px 12px rgba(15, 27, 63, .22);
}
.fab-contact:hover, .fab-contact:focus-visible {
  box-shadow: 0 0 16px 1px rgba(29, 182, 240, .6), 0 4px 12px rgba(15, 27, 63, .22);
}
/* Idle attention pulse on the WhatsApp button — stops on hover / reduced motion.
   Ring max 11px so its outer edge stays clear of the viewport corner. */
.fab-whatsapp::before {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  pointer-events: none; box-shadow: 0 0 0 0 rgba(37, 211, 102, .5);
  animation: fabPulse 2.4s ease-out infinite;
}
.fab-whatsapp:hover::before, .fab-whatsapp:focus-visible::before { animation: none }
@keyframes fabPulse {
  0%   { box-shadow: 0 0 0 0 rgba(37, 211, 102, .45) }
  70%  { box-shadow: 0 0 0 11px rgba(37, 211, 102, 0) }
  100% { box-shadow: 0 0 0 0 rgba(37, 211, 102, 0) }
}
@media (prefers-reduced-motion: reduce) { .fab-whatsapp::before { animation: none } }
.fab-btn[data-tooltip] { position: relative }
.fab-btn[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute; right: calc(100% + 12px); top: 50%;
  transform: translateY(-50%) translateX(8px);
  background: #0F1B3F; color: #fff; font-size: 12px; line-height: 1; white-space: nowrap;
  padding: 7px 10px; border-radius: 6px; opacity: 0; pointer-events: none;
  box-shadow: 0 6px 16px rgba(15, 27, 63, .25);
  transition: opacity .15s ease, transform .15s ease;
}
.fab-btn[data-tooltip]:hover::after, .fab-btn[data-tooltip]:focus-visible::after {
  opacity: 1; transform: translateY(-50%) translateX(0);
}
@media (max-width: 560px) {
  .fab-stack { right: 16px; bottom: 16px }
  .fab-btn { width: 48px; height: 48px }
}

.brand-footer-links { margin-top: 4px }
.brand-footer-links a { color: var(--brand-cyan, #1DB6F0); text-decoration: none; white-space: nowrap }
.brand-footer-links a:hover { text-decoration: underline }

/* ── Outstanding-by-age panel (2026-06-15 redesign) ─────────────────────
   A stacked proportion bar + a grid of severity-coloured buckets. Replaces the
   flat 5-column table; overdue tiers escalate green → amber → orange → red and
   a non-zero 90+ bucket is emphasised. */

/* Smooth in-place reveal (2026-06-18): the Outstanding KPI expands this panel
   with NO page reload and NO scroll jump. The grid-template-rows 0fr→1fr trick
   animates the panel's real content height (no magic max-height); opacity +
   margin fade it in. Collapsed by default; .is-open is toggled by public.js
   (and set server-side for the no-JS fallback). */
.portal-aging-wrap {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  margin: 0;
  transition: grid-template-rows .38s cubic-bezier(.4, 0, .2, 1),
              opacity .3s ease, margin .38s ease;
}
.portal-aging-wrap.is-open {
  grid-template-rows: 1fr;
  opacity: 1;
  margin: 16px 0;
}
.portal-aging-wrap > .portal-aging { overflow: hidden; min-height: 0 }
@media (prefers-reduced-motion: reduce) {
  .portal-aging-wrap { transition: none }
}

.portal-aging .aging-body { padding: 16px 20px 18px }
.aging-bar {
  display: flex; height: 10px; border-radius: 999px; overflow: hidden;
  background: var(--surface-alt); border: 1px solid var(--border);
}
.aging-seg { height: 100% }
.aging-seg + .aging-seg { box-shadow: -1px 0 0 rgba(255, 255, 255, .7) }
.aging-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-top: 14px }
.aging-bucket {
  display: flex; flex-direction: column; gap: 3px; padding: 10px 12px;
  border-radius: var(--radius-xs); background: var(--surface-alt);
  border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
}
.aging-bucket-label { font-size: 10px; font-weight: 600; letter-spacing: .5px; text-transform: uppercase; color: var(--text-3); white-space: nowrap }
.aging-bucket-amt { font-size: 15px; font-weight: 700; color: var(--text); font-variant-numeric: tabular-nums }
.aging-bucket-pct { font-size: 11px; color: var(--text-3); font-variant-numeric: tabular-nums }
.aging-bucket.is-zero { opacity: .5 }
.aging-seg--current { background: #0E9F6E } .aging-bucket--current { border-left-color: #0E9F6E }
.aging-seg--d1_30   { background: #D9A406 } .aging-bucket--d1_30   { border-left-color: #D9A406 }
.aging-seg--d31_60  { background: #E07C02 } .aging-bucket--d31_60  { border-left-color: #E07C02 }
.aging-seg--d61_90  { background: #EA580C } .aging-bucket--d61_90  { border-left-color: #EA580C }
.aging-seg--d90     { background: #DC2626 } .aging-bucket--d90     { border-left-color: #DC2626 }
.aging-bucket--d90:not(.is-zero) { background: var(--danger-bg); border-color: #F8B4B4 }
.aging-bucket--d90:not(.is-zero) .aging-bucket-amt { color: var(--danger-text) }
@media (max-width: 640px) { .aging-grid { grid-template-columns: repeat(2, 1fr) } }
