/* ===================================================================
   Moby — customer/tenant mobile app's own design language,
   loaded on every page via _layout.html, after common/_tokens.css
   (the single source for values shared with the admin dashboards too).
   ink/white/border/text-tertiary/font-body/teal come from there —
   this file only holds what's deliberately different here: a rounder
   radius scale, Poppins headlines, and this family's own neutral shades.
   =================================================================== */

/* Colours this sheet alone uses, named so none of them is a bare hex.
   Same values as before — this changed no pixel. */
:root {
  --form-control-ink    : #A9B6BE;
  --btn-primary-bg      : #B7C2C8;
  --customer-footer-line: #E0E7EC;
}

:root {
  /* --navy aliases to --ink (Primary Navy) — see common/_shell.css for why. */
  --navy:       var(--ink);
  /* Generic accent name, matching org/contractor/platform's convention —
     buttons reference this rather than a hardcoded color so all portals'
     primary actions share one brand teal. */
  --accent:      var(--teal);
  --accent-deep: var(--teal-deep);
  --mist:       #E6EEF2;
  --paper:      #F7F9FA;
  /* The page ground, which is NOT --mist even though it is the same
     colour in light. --mist has 51 background uses in this app and 50 of
     them are component fills — a row hover, a divider, a progress track,
     a disabled button — so in dark it has to become the raised panel
     (#1B1F2E) or every one of those stops reading as raised. The 51st
     was `body`, which needs the opposite: the DARKEST value, under the
     cards rather than on them. One token cannot be both, and the split
     is free in light because it points straight back at --mist, theme
     overrides and all. contractor/_layout.css already had --body-bg for
     exactly this reason; this is the same name for the same job. */
  --body-bg:    var(--mist);
  /* --green used to be kept as its own distinct vibrant shade, deliberately
     different from --success, back when there was no authoritative brand
     spec. docs/brand-palette.md defines exactly one Success green, so
     keeping two is itself an "additional brand color" — now a real alias. */
  --green:      var(--success);
  --amber:      var(--warning);

  /* --text-secondary comes from common/_tokens.css, which always loads
     before this file — it is deliberately not redeclared here. */
  --text-primary:   var(--ink);
  /* --text-on-dark used to carry its own #C9D7DE here. It is now the
     muted member of the hero family in common/_tokens.css, alongside
     --hero-ground and --hero-ink, because the sidebar and topbar this
     sheet paints are the same permanently-dark surface the membership
     card and the tenant hero are. Same value, one declaration. */

  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 10px;

  --font-display: "Poppins", system-ui, sans-serif;

  --shadow-card:        0 1px 2px color-mix(in srgb, var(--ink) 4%, transparent), 0 8px 24px color-mix(in srgb, var(--ink) 6%, transparent);
  --shadow-card-lifted: 0 4px 12px color-mix(in srgb, var(--ink) 8%, transparent), 0 16px 40px color-mix(in srgb, var(--ink) 10%, transparent);
}

/* moby theme — see common/_tokens.css for the shared color/font block this
   layers on top of. Shared by tenant too (tenant/_layout.html loads this
   same stylesheet, no separate tenant CSS). --mist (page background) warms
   to the theme's paper-100 instead of a cool gray; radius comes down some
   from the default's rounder mobile-app language but stays gentler than
   the admin dashboards' since this is a touch surface. */
:root[data-theme="moby"] {
  --font-display: "Big Shoulders Display", sans-serif;
  --mist:         #EDEAE0;
  --paper:        var(--white);
  --radius-lg:    14px;
  --radius-md:    10px;
  --radius-sm:    8px;
}

:root[data-theme="slate"] {
  --font-display: "Space Grotesk", sans-serif;
  --mist:         #E8ECEF;
  --paper:        var(--white);
  --radius-lg:    16px;
  --radius-md:    11px;
  --radius-sm:    8px;
}

:root[data-theme="meadow"] {
  --font-display: "Lora", serif;
  --mist:         #EDF1E7;
  --paper:        var(--white);
  --radius-lg:    24px;
  --radius-md:    17px;
  --radius-sm:    12px;
}

:root[data-theme="clay"] {
  --font-display: "DM Serif Display", serif;
  --mist:         #F2E9DD;
  --paper:        var(--white);
  --radius-lg:    22px;
  --radius-md:    15px;
  --radius-sm:    11px;
}

/* ── The same palette, for a system-set dark scheme ───────────────────
   "System" writes no attribute, so `prefers-color-scheme` is the only
   thing that says dark — and it is the DEFAULT: the state of every
   dark-set device whose owner never opened More → Appearance. This sheet
   had only the block below, so those readers got this family's light
   grounds under common/_tokens.css's dark ink.

   Declared twice because CSS cannot say "either condition", which is what
   common/_tokens.css does too; `dark-half` checks the two say the same
   thing. The `:not([data-appearance="light"])` guard is load-bearing —
   without it an explicit Light pick loses to this block on a dark-set OS. */
@media (prefers-color-scheme: dark) {
  :root:not([data-appearance="light"]) {
    --body-bg: var(--bg-subtle);
    --paper:   var(--surface);
    /* Not because the shared dark --mist is wrong — it is right, and it is
       what the 50 component fills want — but because the four preset
       blocks above tie with this one and would otherwise keep their warm
       literal. Re-asserting the shared value is the whole point. */
    --mist:    var(--surface-2);

    /* A placeholder is decoration, not reading — --text-tertiary is the
       rung this app keeps for that, and it has a dark value already. */
    --form-control-ink: var(--text-tertiary);
    /* A disabled primary reads as "a button, switched off", so it wants
       the raised-panel grey rather than a dimmed accent. */
    --btn-primary-bg: var(--mist);
    --customer-footer-line: var(--border);

    /* Both shadows are mixed from --ink, and --ink flips to near-white —
       so every card in the portal came out of the first dark pass wearing
       a soft white glow where its shadow used to be. Rebuilt on
       --bg-subtle, the near-black page ground, at the heavier alphas a
       dark UI needs before depth reads at all. */
    --shadow-card:        0 1px 2px color-mix(in srgb, var(--bg-subtle) 45%, transparent), 0 8px 24px color-mix(in srgb, var(--bg-subtle) 60%, transparent);
    --shadow-card-lifted: 0 4px 12px color-mix(in srgb, var(--bg-subtle) 55%, transparent), 0 16px 40px color-mix(in srgb, var(--bg-subtle) 70%, transparent);
  }
}

/* ── This family's dark half ──────────────────────────────────────────
   common/_tokens.css owns the dark palette, but it cannot own THIS
   sheet's four private colours, and --paper is the one that matters:
   161 rules across the app read it as a card ground, it is declared
   here and in contractor/_layout.css and common/_shell.css and nowhere
   else, and through the first dark pass it stayed #F7F9FA while --ink
   flipped to near-white text on top of it. Customer Home's category
   grid, quick-action grid and score-attention rows all rendered label-
   on-nothing because of this one line.

   It has to sit AFTER the five [data-theme] blocks above, not up with
   the other :root rules. Those are [data-theme="clay"] at (0,2,0) and
   this is [data-appearance="dark"] at (0,2,0) — they tie, and a tie is
   settled by source order. Today the four preset blocks say
   `--paper: var(--white)`, which in dark resolves to #13161F and would
   have been right by luck; placing this block last is what makes it
   right on purpose, and it is what a sixth preset with a literal in it
   would need. Scheme beats brand — the P1 rule the app settled, where
   the shell is fixed per scheme and only the accent follows the org.

   Same reason the radius and font-display lines above are NOT repeated
   here — a dark clay org is still clay-shaped and still set in DM
   Serif. Only the colours move. */
:root[data-appearance="dark"] {
  --body-bg: var(--bg-subtle);
  --paper:   var(--surface);
  /* Not because the shared dark --mist is wrong — it is right, and it is
     what the 50 component fills want — but because the four preset
     blocks above tie with this one and would otherwise keep their warm
     literal. Re-asserting the shared value is the whole point. */
  --mist:    var(--surface-2);

  /* A placeholder is decoration, not reading — --text-tertiary is the
     rung this app keeps for that, and it has a dark value already. */
  --form-control-ink: var(--text-tertiary);
  /* A disabled primary reads as "a button, switched off", so it wants
     the raised-panel grey rather than a dimmed accent. */
  --btn-primary-bg: var(--mist);
  --customer-footer-line: var(--border);

  /* Both shadows are mixed from --ink, and --ink flips to near-white —
     so every card in the portal came out of the first dark pass wearing
     a soft white glow where its shadow used to be. Rebuilt on
     --bg-subtle, the near-black page ground, at the heavier alphas a
     dark UI needs before depth reads at all. */
  --shadow-card:        0 1px 2px color-mix(in srgb, var(--bg-subtle) 45%, transparent), 0 8px 24px color-mix(in srgb, var(--bg-subtle) 60%, transparent);
  --shadow-card-lifted: 0 4px 12px color-mix(in srgb, var(--bg-subtle) 55%, transparent), 0 16px 40px color-mix(in srgb, var(--bg-subtle) 70%, transparent);
}

* { box-sizing: border-box; margin: 0; padding: 0; }
a { color: inherit; text-decoration: none; }

html, body { overflow-x: hidden; }

body {
  font-family: var(--font-body);
  background: var(--body-bg);
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
}

/* ===================================================================
   Shared form-input error state — used by both login and signup pages.
   Defined once here instead of duplicated in login.css and signup.css.
   =================================================================== */
.input-error {
  border-color: var(--danger) !important;
}

/* ===================================================================
   Outer frame
   =================================================================== */
.phone-frame {
  display: flex;
  justify-content: center;
  padding: 32px 0;
  /* Less the non-production strip's row — see .screen's phone rule. */
  min-height: calc(100vh - var(--env-banner-h, 0px));
  background: var(--body-bg);
}

.screen {
  width: 390px;
  max-width: 100%;
  /* A flex item's min-width defaults to auto, which refuses to go below its
     min-content width and OVERRIDES the max-width above — so one wide
     descendant stretched the whole frame past the viewport and the page
     scrolled sideways, on desktop and phone alike. The frame's own
     overflow:hidden then clipped the content, which is why it read as
     "cut off" rather than as a scrollbar. */
  min-width: 0;
  background: var(--paper);
  border-radius: 36px;
  overflow: hidden;
  box-shadow: 0 30px 60px -20px color-mix(in srgb, var(--ink) 35%, transparent);
  display: flex;
  flex-direction: column;
  height: 844px;
}

@media (max-width: 480px) {
  .phone-frame { padding: 0; }
  /* 100vh is the viewport height with Safari's address bar/toolbar chrome
     collapsed — taller than what's actually visible while that chrome is
     showing. .screen has overflow:hidden (only .content scrolls inside
     it), so a .screen sized to the larger 100vh pushes the tabbar below
     the real visible fold with no page-level scroll to trigger Safari's
     chrome to collapse and reveal it. 100dvh tracks the true visible
     viewport as the chrome shows/hides instead. */
  /* Less the non-production strip's row (0px in production): sized to
     the full viewport, the strip pushed the frame 20px past the fold, the
     page scrolled by exactly that, and the strip — sticky at 0 — covered
     the top of the topbar. */
  .screen { border-radius: 0; height: calc(100dvh - var(--env-banner-h, 0px)); width: 100%; }
}

/* ===================================================================
   Topbar (mobile only)
   =================================================================== */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* black-translucent status bar: in the installed app the page draws
     under the clock/reception icons, so the navy bar keeps its colour up
     there but the brand row starts below the inset. */
  padding: calc(18px + env(safe-area-inset-top, 0px)) 20px 14px;
  background: var(--hero-ground);
  flex-shrink: 0;
}

.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark { width: 35px; height: 30px; color: var(--teal); flex-shrink: 0; }
.brand-text { display: flex; flex-direction: column; line-height: 1.05; }

.brand-word {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.3px;
  color: var(--hero-ink);
}
.brand-word b { font-weight: 800; color: var(--teal); }

.brand-tagline {
  font-size: 7.5px;
  font-weight: 600;
  letter-spacing: 0.9px;
  color: var(--text-on-dark);
  margin-top: 2px;
}

.bell-btn {
  position: relative;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  flex-shrink: 0;
  background: rgba(255, 255, 255, 0.08);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
/* One optical size for whatever glyph the button holds — bell, close, or
   the tenant sidebar's download. Each used to carry its own inline
   font-size, which is how one round button ended up with icons at 17, 18
   and 20px depending on the page. */
.bell-btn > .ti { font-size: 19px; line-height: 1; }
.bell-btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

/* .bell-dot (and its duplicate under .sidebar-footer) is gone with the
   markup that drew it: an amber unread dot rendered in the else-branch of
   the unread count, i.e. only ever when there was nothing unread. The
   count's own .bell-badge is the whole indicator. */

/* ===================================================================
   Content scroll area
   =================================================================== */
.content {
  flex: 1;
  overflow-y: auto;
  padding: 20px 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* This scroller is a flex column, so everything a page puts in it is a flex
   item — and a flex item's default flex-shrink is 1. Once a page is taller
   than the viewport, which is the normal state of a scroll area, the
   children are squeezed to fit rather than overflowing and scrolling.

   A child with overflow:visible doesn't visibly suffer: its automatic
   min-height (min-content) refuses the squeeze. A child with overflow
   hidden or clip has an automatic minimum of ZERO by spec — flexbox §4.5 —
   so it collapses to padding and borders and silently clips everything
   inside it. That is what happened to the report's Photo History card
   (.hhr-photo-history is a .report-section, and .report-section clips):
   card and title drew, eleven rows did not. Measured in a browser — box
   42px tall, scrollHeight 500px, rows laid out below their own container —
   after three wrong guesses from screenshots.

   Only phone widths broke, because .report-section's 860px min-height was
   holding the box open above 640px. That is an accident, not a defence,
   and it protects nothing else on the portal. Shrinking is never what a
   scroll container wants: the content is meant to overflow — that is what
   it scrolls. */
.content > * { flex-shrink: 0; }

/* A live region is ONE child of `.content`, so its own cards fell outside
   the 16px gap above and stacked flush against each other — the visit card
   and the payment card on a request page touched, while every card around
   them was spaced. It carries the same column and the same gap so the seam
   is invisible: the reader cannot tell which cards happen to live inside a
   swapped region and which do not, which is the point of the region. */
.content > [data-live-region] {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* The unsettled-bill banner at the top of a request page. An accent edge
   rather than a filled panel: it sits directly under the page title on a
   screen that is already a stack of white cards, and a coloured block
   there reads as an error the reader has to clear rather than a balance
   they have to pay. */
.sr-pay-banner {
  border-inline-start: 4px solid var(--accent);
}

.sr-pay-banner__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.sr-pay-banner__title {
  display: block;
  color: var(--ink);
}

/* ===================================================================
   Flash messages
   =================================================================== */
.flash-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.flash-message {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-radius: var(--radius-md);
  font-size: 13.5px;
  font-weight: 500;
  opacity: 1;
  transition: opacity 250ms, transform 250ms;
}
.flash-message--dismissing {
  opacity: 0;
  transform: translateX(12px);
}

.flash-message--success { background: color-mix(in srgb, var(--success) 10%, transparent); color: var(--green); }
.flash-message--danger  { background: color-mix(in srgb, var(--danger) 10%, transparent); color: var(--danger); }
.flash-message--warning { background: color-mix(in srgb, var(--warning) 12%, transparent); color: var(--amber); }
.flash-message--info    { background: color-mix(in srgb, var(--teal) 10%, transparent); color: var(--teal-deep); }

.flash-close {
  background: none;
  border: none;
  cursor: pointer;
  color: inherit;
  opacity: 0.7;
  display: flex;
  align-items: center;
  padding: 0;
}
.flash-close:hover { opacity: 1; }

/* ===================================================================
   Shared card shell
   =================================================================== */
.card {
  background: var(--white);
  border-radius: var(--radius-lg);
  padding: 18px;
  box-shadow: var(--shadow-card);
}

.card-heading h3 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
}

.card-eyebrow {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.3px;
  color: var(--text-secondary);
  text-transform: uppercase;
}

/* Single "locked" indicator, shared across every customer page (quick
   action tiles, the credits card, the report-issue property picker) —
   previously three near-duplicate one-off badges with different shapes,
   sizes, and one hardcoding uppercase text instead of using
   text-transform like the others. */
.lock-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 20px;
  background: color-mix(in srgb, var(--ink) 7%, transparent);
  color: var(--text-secondary);
  font-size: 10.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.2px;
  flex-shrink: 0;
}
.lock-badge i { font-size: 11px; }

/* ===================================================================
   Nav links — shared between sidebar and tab bar
   =================================================================== */
.nav-link {
  display: flex;
  align-items: center;
  text-decoration: none;
  color: var(--text-secondary);
}
.nav-link--active { color: var(--teal-deep); }

/* ===================================================================
   Tab bar (mobile only)
   =================================================================== */
.tabbar {
  flex-shrink: 0;
  display: flex;
  background: var(--white);
  border-top: 1px solid var(--mist);
  padding: 8px 4px calc(8px + env(safe-area-inset-bottom, 0px));
}

/* With org context the legal footer renders below the tabbar, so the
   footer — not the tabbar — borders the home indicator and absorbs the
   safe-area inset. Both here would leave a dead white band between the
   tab icons and the footer in the installed app. */
.tabbar--with-footer {
  padding-bottom: 8px;
}

.tabbar .nav-link {
  flex: 1;
  flex-direction: column;
  gap: 3px;
  font-size: 10.5px;
  font-weight: 500;
  padding: 4px 0;
}
.tabbar .nav-link--active { color: var(--teal-deep); }
.tabbar .nav-link:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; border-radius: 6px; }

/* CTA bubble — tabbar */
.tabbar .nav-link--cta {
  color: var(--teal-deep);
  position: relative;
}
.tabbar .nav-link--cta .nav-cta-bubble {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--teal);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 4px 14px color-mix(in srgb, var(--teal) 45%, transparent);
  margin-top: -20px;
}
.tabbar .nav-link--cta span:not(.nav-cta-bubble) {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--teal-deep);
  margin-top: 2px;
}

/* ===================================================================
   Sidebar (desktop only)
   =================================================================== */
.sidebar { display: none; }

.sidebar-brand { display: flex; align-items: center; gap: 10px; padding: 0 10px 28px; }
/* Uploaded logos keep their real colors — no brightness(0) invert(1),
   which used to flatten every logo to an illegible white silhouette
   (see common/_shell.css .admin-brand .brand-logo for the same fix).
   Fixed height/width (not max-*) so object-fit scales small SVGs UP
   to fill the slot instead of rendering at their tiny intrinsic size. */
.brand .brand-logo { height: 36px; width: 160px; object-fit: contain; object-position: left center; }
.sidebar-brand .brand-logo { height: 40px; width: 168px; object-fit: contain; object-position: left center; }
.sidebar-brand .brand-mark { width: 33px; height: 28px; color: var(--teal); flex-shrink: 0; }
.sidebar-brand .brand-text { display: flex; flex-direction: column; line-height: 1.05; }

.sidebar-brand .brand-word {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.3px;
  color: var(--hero-ink);
}
.sidebar-brand .brand-word b { font-weight: 800; color: var(--teal); }

.sidebar-brand .brand-tagline {
  font-size: 7.5px;
  font-weight: 600;
  letter-spacing: 0.9px;
  color: var(--text-on-dark);
  margin-top: 2px;
}

.sidebar-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; }

.sidebar-nav .nav-link {
  gap: 12px;
  padding: 11px 12px;
  border-radius: var(--radius-sm);
  font-size: 14px;
  font-weight: 500;
  color: var(--text-on-dark);
  transition: background 120ms ease, color 120ms ease;
}
.sidebar-nav .nav-link:hover { background: rgba(255, 255, 255, 0.06); color: var(--hero-ink); }
.sidebar-nav .nav-link:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.sidebar-nav .nav-link svg { flex-shrink: 0; }

.sidebar-nav .nav-link--active { background: color-mix(in srgb, var(--teal) 16%, transparent); color: var(--teal); }
.sidebar-nav .nav-link--active:hover { background: color-mix(in srgb, var(--teal) 20%, transparent); color: var(--teal); }

/* CTA row — sidebar */
.sidebar-nav .nav-link--cta {
  background: var(--teal);
  color: var(--white);
  border-radius: 10px;
  margin-top: 8px;
}
.sidebar-nav .nav-link--cta:hover { background: var(--teal-deep); color: var(--white); }
.sidebar-nav .nav-link--cta .nav-cta-bubble { display: none; }

.sidebar-footer {
  padding-top: 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  display: flex;
  align-items: center;
}

/* .sidebar-footer .bell-btn used to restate .bell-btn's thirteen
   declarations verbatim, at higher specificity — so a fix to the button
   (the missing text-decoration:none, the icon sizing) landed on the topbar
   copy and not on this one. The plain rule above covers both placements. */

/* Signing out is a POST now (app/routes/index/auth.py), so this dresses a
   <button> inside a .logout-form rather than the <a> it used to. That
   wrapper is display:contents (common/_components.css), so the button —
   not the form — stays .sidebar-footer's flex item and the footer lays out
   exactly as before. */
.logout-btn {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-on-dark);
  text-decoration: none;
  padding: 7px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(255, 255, 255, 0.12);
  transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
  /* Button chrome reset — a <button> otherwise arrives with the platform's
     own grey fill and font, which on this dark sidebar reads as a broken
     control rather than a quiet one. */
  background: none;
  font-family: inherit;
  cursor: pointer;
}

.logout-btn:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--white);
  border-color: rgba(255, 255, 255, 0.2);
}

.logout-btn:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
}

/* ===================================================================
   Desktop breakpoint — sidebar replaces topbar/tabbar
   Page-specific grid-template-areas (e.g. home.css) extend .content
   inside their own @media (min-width: 768px) block — this block only
   handles the chrome shared by every page: sidebar, topbar, tabbar.
   =================================================================== */
@media (min-width: 768px) {

  .phone-frame {
    justify-content: center;
    align-items: flex-start;
    padding: 0;
    min-height: calc(100vh - var(--env-banner-h, 0px));
  }

  .sidebar {
    display: flex;
    flex-direction: column;
    width: 248px;
    flex-shrink: 0;
    height: calc(100vh - var(--env-banner-h, 0px));
    position: sticky;
    /* Sticks under the non-production strip, not beneath it — see
       .topbar in contractor/_layout.css. 0px in production. */
    top: var(--env-banner-h, 0px);
    background: var(--hero-ground);
    padding: 24px 18px;
  }

  .sidebar-footer {
    justify-content: space-between;
  }

  .screen > .topbar,
  .tabbar {
    display: none;
  }

  .screen {
    flex: 1;
    width: auto;
    max-width: none;
    height: calc(100vh - var(--env-banner-h, 0px));
    border-radius: 0;
    box-shadow: none;
    margin: 0;
  }
}

/* ===================================================================
   Shared form elements — used across wizard, properties, profile, support
   =================================================================== */
.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.form-group label {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  /* .form-group is a flex column with its own gap here — the shared rule's
     margin-bottom would stack on top of it. */
  margin-bottom: 0;
}



.form-control {
  width: 100%;
  height: 46px;
  padding: 0 14px;
  font-size: 14px;
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--paper);
  border: 1.5px solid var(--mist);
  border-radius: var(--radius-md);
  appearance: none;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}

.form-control::placeholder { color: var(--form-control-ink); }

.form-control:focus {
  outline: none;
  border-color: var(--teal);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--teal) 15%, transparent);
}

/* Baseline chrome for any text-like input/textarea/select that ISN'T given
   .form-control — the fallback so a plain <input> on a customer/tenant
   page can never render as raw unstyled browser chrome. No width/height
   here on purpose, so it doesn't fight fields sized with their own
   width/max-width. A more specific rule (.form-control) still wins. */
input:not([type]),
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="date"],
input[type="month"],
input[type="week"],
input[type="time"],
input[type="datetime-local"],
input[type="password"],
input[type="search"],
input[type="url"],
input[type="color"],
textarea,
select {
  padding: 12px 14px;
  font-size: 14px;
  font-family: var(--font-body);
  color: var(--ink);
  background: var(--paper);
  border: 1.5px solid var(--mist);
  border-radius: var(--radius-md);
}
input:not([type]):focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="tel"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
input[type="month"]:focus,
input[type="week"]:focus,
input[type="time"]:focus,
input[type="datetime-local"]:focus,
input[type="password"]:focus,
input[type="search"]:focus,
input[type="url"]:focus,
input[type="color"]:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--teal);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--teal) 15%, transparent);
}

textarea.form-control {
  height: auto;
  padding: 12px 14px;
  resize: vertical;
  min-height: 100px;
  line-height: 1.5;
}

/* Native file inputs render as the bare browser default - style just the
   button part via ::file-selector-button so it matches btn-secondary.
   :not([hidden]) matters — a plain type+attribute selector otherwise
   outranks the browser's own `[hidden] { display: none }` rule and pops
   the native control back into view behind a custom attach button/label
   (chat composers, the profile photo picker, ...). */
input[type="file"]::file-selector-button {
  margin-inline-end: 10px;
  padding: 9px 14px;
  border: 1.5px solid var(--mist);
  border-radius: var(--radius-md);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { background: var(--mist); }

/* Primary button */
.btn { font: inherit; }

.btn-primary {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 48px;
  background: var(--accent);
  color: var(--white);
  border: none;
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease;
  text-decoration: none;
}

.btn-primary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-primary:disabled { background: var(--btn-primary-bg); cursor: not-allowed; }

/* Compact inline variant — e.g. a header action next to a page title,
   where the full-width block button doesn't fit. */
.btn-primary--sm {
  display: inline-flex;
  width: auto;
  height: 34px;
  padding: 0 14px;
  gap: 5px;
  font-size: 13px;
  white-space: nowrap;
  flex-shrink: 0;
}
.btn-primary:disabled:hover { background: var(--btn-primary-bg); }

/* Outline companion to .btn-primary — secondary inline actions (e.g. the
   "Enable" browser-push button on notification preferences, used by both
   the customer and tenant portals, which share this stylesheet). */
.btn-outline {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-weight: 600;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
  text-decoration: none;
}
.btn-outline:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, transparent); }
.btn-outline:disabled { color: var(--btn-primary-bg); border-color: var(--border); cursor: not-allowed; background: none; }

.btn-sm {
  height: 34px;
  padding: 0 14px;
  font-size: 13px;
  white-space: nowrap;
  flex-shrink: 0;
}

/* ===================================================================
   Subpage chrome — shared by properties, support, profile, etc.
   =================================================================== */
.subpage {
  display: flex;
  flex-direction: column;
  gap: 14px;
}




/* ===================================================================
   Footer — legal and support links
   =================================================================== */
/* Bottom padding and max-height grow by the safe-area inset: the footer
   is the last element in .screen, so it clears the home indicator in the
   installed app (see .tabbar--with-footer). */
.customer-footer {
  max-height: calc(32px + env(safe-area-inset-bottom, 0px));
  padding: 4px 16px calc(4px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--customer-footer-line);
  background: var(--white);
  overflow: hidden;
  transition: max-height 300ms ease, padding 300ms ease;
  position: relative;
}

.customer-footer.expanded {
  max-height: calc(400px + env(safe-area-inset-bottom, 0px));
  padding: 24px 16px calc(24px + env(safe-area-inset-bottom, 0px));
}

.footer-collapsed-label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  opacity: 1;
  transition: opacity 300ms ease;
  white-space: nowrap;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  user-select: none;
  margin-bottom: 12px;
}

.footer-chevron {
  display: inline-block;
  transition: transform 300ms ease;
  font-size: 10px;
}

.customer-footer.expanded .footer-chevron {
  transform: rotate(180deg);
}

.footer-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  opacity: 0;
  transition: opacity 300ms ease 150ms;
}

.customer-footer.expanded .footer-grid {
  opacity: 1;
}

.footer-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.footer-section h4 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 6px;
}

.footer-section a {
  color: var(--text-secondary);
  text-decoration: none;
  transition: color 120ms ease;
}

.footer-section a:hover {
  color: var(--teal);
}

@media (max-width: 640px) {
  .footer-grid {
    grid-template-columns: 1fr;
    gap: 16px;
  }
}

/* ===================================================================
   Cookie consent banner
   =================================================================== */
.cookie-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  background: var(--hero-ground);
  border-top: 1px solid rgba(255,255,255,0.08);
  flex-shrink: 0;
  flex-wrap: wrap;
}

.cookie-banner-text {
  font-size: 12.5px;
  color: rgba(255,255,255,0.75);
  margin: 0;
  line-height: 1.4;
}

.cookie-banner-text a {
  color: var(--teal);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.cookie-banner-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.cookie-btn {
  height: 32px;
  padding: 0 14px;
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  border: none;
  transition: opacity 120ms ease;
}
.cookie-btn:hover { opacity: 0.85; }

.cookie-btn--accept {
  background: var(--teal);
  color: var(--white);
}

.cookie-btn--decline {
  background: rgba(255,255,255,0.1);
  color: rgba(255,255,255,0.75);
}

/* Add to Home Screen banner styles moved to common/_a2hs.css — shared
   with the contractor and tenant layouts. */

/* ===================================================================
   Reduced motion
   =================================================================== */
@media (prefers-reduced-motion: reduce) {
  .shield-fill { transition: none; }
}

/* Subpage heading used by requests, approvals and chats. */
.requests-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  color: var(--ink);
  margin: 0;
}

/* Page header shared by activity.html and insights.html. */
.activity-page-header {
  margin-bottom: 4px;
}

.activity-page-title {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 22px;
  color: var(--ink);
  margin: 0 0 4px;
}

.activity-page-sub {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0;
}

/* ── Pending-approval banner ─────────────────────────────────────────
   partials/_pending_approval_banner.html. Deliberately the amber "waiting
   on you" bucket rather than the live visit's teal, so two banners
   stacked on the same page read as two different facts rather than one
   repeated. Geometry matches .live-visit-banner (common/_visit_gauge.css)
   because they sit in the same slot and stacking two shapes there looks
   like a mistake. */
.approval-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  text-decoration: none;
  background: var(--status-wait-bg);
  border-bottom: 1px solid var(--warning);
}
.approval-banner__icon {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--warning);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 19px;
}
.approval-banner__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.approval-banner__body strong { font-size: 14px; color: var(--ink); line-height: 1.3; }
.approval-banner__body span {
  font-size: 12px;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.approval-banner__go { color: var(--status-wait-fg); font-size: 18px; flex-shrink: 0; }

/* ── Outstanding-payment banner ──────────────────────────────────────
   partials/_unpaid_invoice_banner.html. The approval banner's shape, in
   the bad bucket instead of the wait one — §4's map, not a new colour.
   Reusing the shape is the point: the two sit in the same slot and can
   stack on one page, and a second geometry there reads as a mistake. The
   BUCKET is what has to differ, or two stacked banners look like one
   fact repeated. */
.approval-banner--payment {
  background: var(--status-bad-bg);
  border-bottom-color: var(--danger);
}
.approval-banner--payment .approval-banner__icon { background: var(--danger); }
.approval-banner--payment .approval-banner__go { color: var(--status-bad-fg); }

/* ── The approval card ────────────────────────────────────────────────────
   One definition, not two. These rules lived in the <style> blocks of
   customer/approvals.html AND customer/service_request_detail.html, whose
   own comment said it "mirrors customer/approvals.html so approval cards
   look identical there and here" — which they did not, because the two
   copies had drifted apart and each was missing the other's half.

   The detail page had no `:target` flash, while the pending-approval
   banner deep-links `#approval-N` straight at it — so the card the reader
   was sent to looked like every other card on the page. And it had no
   `.approval-photos img` sizing, so a part's photos rendered at whatever
   size they were uploaded.

   Both pages are the customer portal, so §2 puts this in the portal's
   _layout.css rather than in _components.css. Card chrome still comes from
   the shared .card; only the approval-specific bits are here. */
.approval-card h3 { margin: 0 0 8px; font-size: 16px; }
.approval-card p { margin: 4px 0; color: var(--text-secondary); font-size: 14px; }

/* Opposite ends, not neighbours — Approve and Decline are irreversible
   opposites, and 10px was one thumb-width of separation. */
.approval-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: space-between; }
.approval-actions button { padding: 9px 18px; border-radius: var(--radius-sm); border: none; font-size: 14px; cursor: pointer; }
.approval-approve { background: var(--teal); color: var(--white); }
.approval-decline { background: var(--paper); color: var(--ink); }

/* The card a deep link landed on says so, briefly. */
.approval-card:target { outline: 2px solid var(--teal); outline-offset: 2px; animation: approval-card-flash 2s ease-out; }
@keyframes approval-card-flash { 0% { background: var(--teal-pale); } 100% { background: var(--white); } }

.approval-photos { display: flex; gap: 8px; margin: 8px 0; }
.approval-photos img { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); }

/* An approval is the one thing on the detail page that STOPS until the
   reader acts, so it leads. It used to sit below the timeline, where a
   customer scrolling for status never reached it and the job sat waiting
   on them without either side knowing why. */
.approval-card--pending { border-inline-start: 3px solid var(--teal); }
.approval-card--pending .sr-approval-lead {
    display: block;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.4px;
    text-transform: uppercase;
    color: var(--teal-deep);
    margin-bottom: 6px;
}
