/* ==========================================================================
   shoku-ui — the shared APPLICATION layer
   "Industrial Gallerist", continued past the marketing page.

   CASCADE ORDER, always:   styles.css  ->  app.css  ->  <site>.css
   styles.css is owned by shoku-web and holds every design TOKEN and every
   marketing primitive. This file owns everything a marketing page never needed
   — an app shell, forms, tables, status pills, modals, states — and it NEVER
   redeclares a token styles.css already defines. A site sheet may only add
   layout for one of its own features.

   The DESIGN.md rules that shape everything below:
     R1  no 1px sectioning lines; a boundary is a tonal step (ghost border, as
         an inset shadow, only where a surface must read as an edge)
     R2  surface -> container-low bands -> container cards -> highest floating
     R6  shadows are tinted rgba(var(--shadow-tint), ...), never black
     R7  radius >= --radius-sm; nothing is ever square
     R8  inputs are surface-container-lowest + ghost border; an error is text,
         an underline and an icon — never a red box
     R13 tokens only: no literal hex/rgb outside :root
   ========================================================================== */

/* ---- Tokens ---------------------------------------------------------------
   ONLY values styles.css does not define. Two real colours (the audit found
   both invented independently on two sites), the rgb triplets the tint recipes
   need, one float shadow, and the semantic status aliases every site maps its
   own vocabulary onto.
   -------------------------------------------------------------------------- */
:root {
  /* The two hues the product needs and the marketing page never did. */
  --success: #5dd28d;
  --info: #a8b4c4;

  /* Triplets for color-mix()-free alpha work (rgba(var(--x-rgb), .18)). */
  --primary-rgb: 255, 145, 83;
  --tertiary-rgb: 255, 205, 114;
  --error-rgb: 255, 115, 81;
  --success-rgb: 93, 210, 141;
  --info-rgb: 168, 180, 196;

  /* The floating tier's shadow, tinted per R6. */
  --shadow-float: 0 32px 64px -20px rgba(var(--shadow-tint), 0.35);

  /* The nav glass, lifted verbatim from styles.css's `.site-header.scrolled`
     so the app chrome and the marketing chrome are literally the same material.
     Named here because two rules need it (the header and the bottom tab bar)
     and a literal colour outside :root is exactly what R13 forbids. */
  --glass: rgba(31, 28, 26, 0.6);
  --glass-opaque: rgba(19, 19, 19, 0.96);

  /* Semantic status aliases — the ONLY names a status may be painted with.
     Six tones, six hue families, so colour still carries information at 12px. */
  --status-pending: var(--tertiary);
  --status-progress: var(--primary);
  --status-success: var(--success);
  --status-info: var(--info);
  --status-neutral: var(--on-surface-variant);
  --status-danger: var(--error);

  /* Chrome metrics, shared so a feature can compute against them instead of
     hard-coding the shell's height (the trap contractor.css fell into with
     `calc(100dvh - 12rem)`).

     4.5rem, not the 3.75rem this said while nothing consumed it: a destination
     is 2.75rem tall and the avatar button 3rem, so an app header has never been
     able to be 60px. `.site-header--app .nav` now DECLARES this height for
     every route, which is what makes the number true of the auth variant too. */
  --appbar-h: 4.5rem;
  /* Tall enough for the longest label to take TWO lines rather than be
     truncated: at 412px five columns give "Booking setup" ~80px, and a tab bar
     that ellipsises its own destinations is a tab bar with unlabelled tabs. */
  --tabbar-h: 4.5rem;

  /* Everything the shell puts ABOVE a screen's own content: the glass header,
     .app-main's top padding and the page head. A feature that has to fit the
     viewport exactly — the chat thread, whose composer must stay put while the
     log scrolls — subtracts this instead of hard-coding a guess. */
  --app-chrome-h: 15rem;
}

/* Below the shell's breakpoint the tab bar takes its own band out of the
   viewport, so a viewport-fitting feature owes it that much again — plus the
   home indicator the bar itself pads for. The inset belongs HERE and not on
   `--tabbar-h`: four consumers already append `env(safe-area-inset-bottom)` to
   that token themselves and would count it twice. The 15rem desktop value gets
   none of it, because there is no fixed bottom bar up there to clear. */
@media (max-width: 860px) {
  :root {
    --app-chrome-h: calc(13rem + var(--tabbar-h) + env(safe-area-inset-bottom));
  }
}

/* The header is `position: sticky; top: 0` and permanently opaque glass, so
   every scroll that lands on a fragment lands UNDERNEATH it. "Skip to content"
   is the case that matters: it targets #main, the browser scrolled main's top
   edge to y=0, and the eyebrow — the first thing a keyboard user was sent to —
   came to rest 54px down a 72px bar. The one control on the page whose entire
   job is to get past the chrome was putting people behind it.

   `scroll-padding-top` on the scrollport fixes it for anchors, `focus()`,
   `scrollIntoView()` and browser find-in-page alike, which is why it is here
   rather than an offset hard-coded into one skip-link handler. It is the bar
   plus a gap, because content flush against glass reads as clipped. */
html {
  scroll-padding-top: calc(var(--appbar-h) + var(--space-4));
}

/* An element carrying `hidden` must actually be hidden. The attribute's rule
   lives in the UA sheet as a plain `display: none`, which ANY author `display`
   beats whatever the specificity — and this layer hands out `display: grid` and
   `display: flex` by the dozen (`.card-grid`, `.stack`, `.table-wrap`, `.menu`
   …). Every consumer that toggles `hidden` on one of them hit the same thing
   and patched it locally: the contractor site pairs the attribute with an
   inline `display: none`, and the partner dashboard shipped its own copy of
   this exact rule. It belongs to the layer that creates the problem. */
[hidden] {
  display: none !important;
}

/* ==========================================================================
   App scope
   Every product document sets `class="app"` on <body>. That class is what
   makes the marketing rhythm stop at the door.
   ========================================================================== */

/* styles.css gives every <section> 6rem of vertical padding, which is right for
   a landing page and catastrophic inside a chat pane or a settings card. Real
   specificity, deliberately — a :where() version of this rule loses to the
   site's own `section` selectors and was found inert on two sites. */
.app section {
  padding-block: 0;
}

body.app {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* A React site mounts into a wrapper the Shell neither renders nor can see.
   Without this, the flex column stops at that wrapper and a short page floats
   its footer in the middle of the viewport instead of pinning it to the bottom.
   The static documents that consume this layer put the shell markup straight
   into <body> and are unaffected either way. */
body.app > #root {
  display: contents;
}

body.app > .app-main,
body.app > #root > .app-main,
body.app > main,
body.app > #root > main {
  flex: 1;
}

/* ==========================================================================
   Shell — one header, one footer, one tab bar for all three product sites
   ========================================================================== */

/* The reference header, permanently in its `.scrolled` state: an app has no
   pre-scroll hero to sit transparently over, and a transparent sticky bar means
   the title collides with the content scrolling under it.

   The glass lives on a PSEUDO-ELEMENT, not on the header itself, and that is
   load-bearing: `backdrop-filter` makes an element a containing block for
   fixed-position descendants, so with the filter on the header the bottom tab
   bar below 860px — which is the same `.nav-links` element, re-positioned —
   pinned itself to the bottom of the 60px header instead of the viewport, and
   landed on top of the brand. */
.site-header--app {
  background: transparent;
  padding-block: var(--space-3);
}

/* One row height, whatever the variant puts in it.

   Left to its content the row is as tall as its tallest child, and the two
   variants have different tallest children: an app route's account cluster is a
   3rem avatar button (2rem disc + the padding that makes it a real touch
   target), while an auth route has nothing but the 2.25rem brand. So the header
   was 72px on the dashboard and 60px on sign-in and the 404, and the brand mark
   sat 6px lower on one than on the other — the same jump PV-05 reported when
   the account cluster was still a full-height Sign out button, just smaller.
   Declared here so `--appbar-h` is a fact rather than an estimate. */
.site-header--app .nav {
  min-height: calc(var(--appbar-h) - 2 * var(--space-3));
}

.site-header--app::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--glass);
  backdrop-filter: blur(24px) saturate(140%);
  -webkit-backdrop-filter: blur(24px) saturate(140%);
}

@supports not (backdrop-filter: blur(1px)) {
  .site-header--app::before {
    background: var(--glass-opaque);
  }
}

/* Glass needs something to blur (R3): every app page puts one warm wash behind
   its page head, which is what the header picks up. */
.app-glow {
  /* Percent of <main>, not vw: 100vw counts the scrollbar, and a glow one
     scrollbar wider than the page gives every route a horizontal scroll. */
  width: min(48rem, 100%);
  height: 28rem;
  /* Deliberately negative, and <main> is deliberately NOT clipped: the wash has
     to reach up BEHIND the header, because that is the whole point of it —
     glass with nothing behind it is just a lighter band of page (R3). The
     header's z-index keeps it painted over the glow rather than under it. */
  top: -10rem;
  left: 50%;
  transform: translateX(-50%);
  opacity: 0.7;
  /* Its own gradient, sized to its own box. styles.css's `.glow--primary` is a
     `circle` (farthest-corner = 445px here) that is still opaque at 72% — past
     this box's 224px half-height — so the box CLIPPED the wash and cut a hard
     horizontal line across the page head. `closest-side` reaches transparent
     exactly at the edge, which is the fade DESIGN.md asks for. */
  background: radial-gradient(
    closest-side,
    rgba(var(--primary-rgb), 0.32) 0%,
    rgba(var(--primary-rgb), 0.12) 45%,
    transparent 100%
  );
}

.app-main {
  position: relative;
  padding-block: var(--space-12) var(--space-24);
  /* An email address has no soft-break opportunity, so it sets the min-content
     width of whatever contains it — and `html { overflow-x: clip }` in
     styles.css means the overflow is unrecoverable, with no scrollbar to get it
     back. Measured at 390px: a 61-character address clipped 122px off the
     verify screen, taking the primary CTA with it. `anywhere`, not
     `break-word`: only `anywhere` also shrinks min-content, which is the half
     that stops the page widening. */
  overflow-wrap: anywhere;
}

/* The glow is `position: absolute; z-index: 0` inside this element, and a
   positioned element paints above in-flow content — so without a position of
   its own the page's own content would sit UNDER the wash rather than in it.
   `position` and NO `z-index`, deliberately: `z-index: 1` here makes this a
   stacking context, and every modal inside it is then trapped at layer 1 —
   under the header, whose tab bar quietly swallowed the clicks on a bottom
   sheet's own buttons. */
.app-main > .container {
  position: relative;
}

/* `fit` — a screen that has to FIT the viewport rather than flow down the page.
   There is one shape of it: a chat, whose composer must stay put while the log
   scrolls.

   The measurement is done by the flex column, not by arithmetic. <body> is
   `min-height: 100dvh` with the header, the footer and the tab-bar spacer as
   `flex: none` siblings, so a `flex: 1` <main> is ALREADY exactly the space
   they leave — the only thing stopping a screen using it is `min-height: auto`,
   the flex default, which lets an item grow past its flex size to fit its
   content. That growth is the page scrolling. `min-height: 0` here, and
   `min-height: 0` again on the container, is what hands the leftover down to
   the screen's own root, whatever the header, the page head and the footer
   happen to measure.

   Subtracting a guess instead is how the homeowner chat ended up with a 128px
   thread on a phone: `--app-chrome-h` is documented as including the page head,
   the screen rendered its page head INSIDE the pane as well, and the head was
   charged to the layout twice. Nothing here counts anything twice because
   nothing here counts. */
.app-main--fit {
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The band of page under a viewport-fitting screen is 96px the thread could
     have had; a screen that ends AT the fold has nothing to separate itself
     from. The 96px ABOVE it is the same money: a screen with a fixed budget
     spends it on the content, not on air under the glass. */
  padding-block: var(--space-4) 0;
}

.app-main--fit > .container {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

/* The head's usual 48px of air is a luxury a screen with a fixed budget cannot
   afford: on a 839px phone it is a sixth of everything the thread has. */
.app-main--fit .page-head {
  margin-block-end: var(--space-4);
}

/* Trim the head to what identifies the screen — the h1, the brand and the
   footer the chrome contract requires all stay.

   At EVERY width, not below 860px as this once was: what a fit screen is short
   of is HEIGHT, and a landscape phone is 844px wide and 390px tall, so it took
   none of this trimming and gave the thread a 32px pane with a 0px content box.
   The eyebrow repeats the section the tab bar already lights, and the display
   h1 is a page-flow size, so neither is worth 130px of thread at any width. */
.app-main--fit .page-head .eyebrow {
  display: none;
}

.app-main--fit .page-head h1.display {
  font-size: clamp(1.25rem, 1vw + 1rem, 1.5rem);
}

@media (max-width: 860px) {
  .app-main--fit .page-head {
    margin-block-end: var(--space-2);
  }

  .app-main--fit .page-head__text > .back-link {
    margin-block-end: var(--space-2);
  }

  .app-main--fit + .site-footer--app {
    padding-block: var(--space-3);
  }
}

/* The last thing a fit screen can give back. Gated on HEIGHT and nothing else,
   deliberately: both chrome-contract viewports (1440x900 and 412x839) are
   taller than 700px, so `expectChrome`'s "a footer on every route" requirement
   still holds everywhere it is asserted, and no spec needs editing for this.
   Do not "tidy" it into an unconditional rule — that deletes the footer from
   the chat route and fails all three design-contract suites. */
@media (max-height: 700px) {
  .app-main--fit + .site-footer--app {
    display: none;
  }
}

/* And the one after that. On both sites a fit screen is a CHAT screen, and the
   thread renders its own header — the job title as an h2, the customer under
   it, the back link beside it — so the head above the pane says "Messages" over
   a screen that is obviously messages, and carries no back link and no action
   the thread does not already have. On a 640x360 landscape phone that costs
   47px of a 169px pane.

   Same gate and the same reasoning as the footer rule above: HEIGHT only, at
   600px, which no contract viewport reaches (1280x720, 1440x900, 412x915,
   412x839), so `expectChrome`'s "the page head owns the only h1" still holds
   everywhere it is asserted — that case runs /jobs, /jobs/:id, /earnings and
   /profile, none of them fit routes. Do not widen this to a width query or
   drop the gate: the h1 is the route's only heading, and deleting it anywhere
   a spec looks fails all three design-contract suites. */
@media (max-height: 600px) {
  .app-main--fit .page-head {
    display: none;
  }
}

/* variant="auth" — sign-in, register, the funnel, the 404. One card centred
   over the glow, no links and no account cluster, because on these routes there
   is exactly one thing to do and every other target is a way to not do it. */
.app-main--auth {
  display: flex;
  /* Centred, the card re-centres on every status change — the validation
     message, "Sending your code…", the 429 — and the heading and the field
     being typed into move by half the delta. Anchored, it only grows down. */
  align-items: flex-start;
  justify-content: center;
  padding-block: var(--space-12);
}

.app-main--auth > .container {
  max-width: 34rem;
}

/* The "New to Shoku? Create an account" line under the auth card: the one
   thing on these screens that is deliberately NOT in the card. */
.auth-note {
  margin-block-start: var(--space-6);
  text-align: center;
}

/* Nav destinations. The reference `.nav-links` gives 0.9375rem/600 links; the
   app adds a real target size and the tonal current-page pill. */
.site-header--app .nav-links a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  min-height: 2.75rem;
  /* The anchor for a tab's unread badge. `.icon-btn__dot` is `position:
     absolute` and ShellLink now renders it inside the link, so without a
     positioned tab it would hang off whatever distant ancestor happens to be
     positioned — `.app-main > .container`, most of a page away. */
  position: relative;
  /* A two-word destination is one destination. Without this, "Booking setup"
     broke across two lines everywhere between the 861px the tab bar hands over
     at and the ~940px the row finally fits in: one tab twice the height of its
     four neighbours, and the header band growing 17px to hold it. */
  white-space: nowrap;
}

/* The wrap was the browser fitting a row that does not fit; forbidding it only
   moves the symptom, and at 861px the account cluster went 30px off the right
   edge instead. This is the actual fix for the same band: the reference gap is
   2rem, which is right once there is a laptop's width to spend and is 128px of
   it here, immediately after the tab bar has handed five destinations back to
   the header. Each link carries 0.75rem of padding either side, so the row
   still reads as 2rem between the words. */
@media (min-width: 861px) and (max-width: 1023px) {
  .site-header--app .nav-links {
    gap: var(--space-2);
  }
}

/* The icon is the phone tab bar's affordance; beside a label in a 60px
   header it is just noise. One markup, two treatments. */
.site-header--app .nav-links a svg {
  display: none;
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
}

/* Current page is a tonal lift, never colour alone and never a drawn bar. */
.site-header--app .nav-links a[aria-current="page"] {
  background: var(--surface-container-high);
  color: var(--on-surface);
}

/* The CTA is a button that happens to sit in the nav row. `.site-header--app
   .nav-links a` out-specifies `.btn`, so restore the `.btn-sm` geometry — the
   right density for chrome — instead of inheriting nav-link geometry.

   `.btn-sm` geometry INCLUDES its `min-height: 2.75rem`, which is the 44px tap
   target every other primary gets and which chrome-contract measures on this
   very link. Zeroing it here left the one global CTA at 38.6px — smaller than
   the ~36px this rule was written to fix was meant to become — so the
   min-height inherited from `.site-header--app .nav-links a` is left to stand.
   The `@media (max-width: 860px)` tab-bar branch below is where the CTA is
   deliberately denser. */
.site-header--app .nav-links a.btn {
  padding: 0.55rem var(--space-4);
  border-radius: var(--radius-md);
  font-weight: 700;
}

.nav-account {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* The anchor for `.menu--end`. Anchored to the 40px bell instead, a 22rem
     panel hung 31px off the left of a 412px phone: its padding and the first
     characters of every line were outside the viewport. */
  position: relative;
}

.nav-account > .combobox {
  position: static;
}

.nav-account .menu--end {
  width: min(22rem, calc(100vw - 2 * var(--space-4)));
}

.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The bell sits beside a 48px avatar and was 40px. The glyph stays 1.25rem —
     only the target grows. */
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-md);
  color: var(--on-surface-variant);
  transition: background 0.2s var(--ease), color 0.2s var(--ease);
}

/* The header's glass composites to about rgb(24,22,21), so surface-container
   hovers are 1.03:1 against it and DARKER than rest — the inverted hover this
   sheet already fixed for `.menu__item`. Same recipe, same reason. */
.icon-btn:hover {
  background: color-mix(in srgb, var(--primary) 14%, transparent);
  color: var(--on-surface);
}

/* The account cluster is part of the navigation, so it answers "where am I" the
   same way `.nav-links` does — the same tonal lift, on whichever of the bell or
   the avatar owns the current route.

   Without this the header goes dark on every route reached from here (alerts,
   settings, refer): four destinations that say "you are here" and three that,
   from the reader's side, look exactly like every other page. Tonal, never
   colour alone, so it survives a forced-colours mode and a monochrome print. */
.icon-btn[aria-current="page"],
.avatar-btn[aria-current="page"] {
  background: var(--surface-container-high);
  color: var(--on-surface);
}

.icon-btn svg {
  width: 1.25rem;
  height: 1.25rem;
}

/* Inside a chip or a table cell, where a 2.5rem square would dwarf its row. */
.icon-btn--sm {
  width: 1.75rem;
  height: 1.75rem;
}
.icon-btn--sm svg {
  width: 1rem;
  height: 1rem;
}

/* The account menu's trigger: a button whose entire visual is the avatar, with
   the padding that makes a 2rem disc a real touch target. */
.avatar-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-2);
  border-radius: var(--radius-pill);
  background: transparent;
}

.avatar-btn:hover {
  background: color-mix(in srgb, var(--primary) 14%, transparent);
}

/* Name and email above the menu's choices: the one place in an app shell that
   answers "who am I signed in as" without a round trip to a settings page. */
.menu__head .menu__name {
  display: block;
  font-weight: 700;
  color: var(--on-surface);
}

.menu__head .menu__email {
  display: block;
  font-size: 0.8125rem;
  color: var(--on-surface-variant);
  overflow-wrap: anywhere;
}

/* Unread count. Caps at "9+" in the component so the pill never grows wider
   than the button it sits on. */
.icon-btn__dot {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  min-width: 1rem;
  height: 1rem;
  padding-inline: 0.25rem;
  border-radius: var(--radius-pill);
  background: var(--primary);
  color: var(--on-primary);
  font-size: 0.625rem;
  font-weight: 800;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* The bar is `position: fixed`, so it takes no space and would otherwise cover
   the last of the footer. A spacer rather than `body { padding-bottom }`
   because the Shell renders one only when there IS a bar — an auth route has no
   tab bar and must not carry a hole at the bottom of the page.

   Declared BEFORE the media query, deliberately: at equal specificity the last
   rule wins, and a `display: none` written after the block that turns it on is
   a spacer that never spaces anything. */
.tabbar-spacer {
  display: none;
  /* It is a flex item of `body.app` with no content, so the default
     `flex-shrink: 1` collapsed it to nothing the moment a page was taller than
     the viewport — which is exactly when it is needed. */
  flex: none;
}

/* Below the reference's own 860px breakpoint the SAME .nav-links element
   becomes a glass bottom tab bar. One DOM, two positions, every link labelled
   at every width — a hamburger would hide both the current-page indicator and
   the unread badge, which an app needs visible. */
@media (max-width: 860px) {
  .site-header--app .nav-links {
    position: fixed;
    inset-inline: 0;
    top: auto;
    bottom: 0;
    z-index: 90;
    flex-direction: row;
    align-items: stretch;
    gap: var(--space-1);
    /* A DECLARED height, so `--tabbar-h` is the truth rather than an estimate
       of it: the spacer below the footer, the toast region and any feature
       that fits the viewport all measure against this one number. */
    box-sizing: border-box;
    height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    padding: var(--space-2) var(--space-2) calc(var(--space-2) + env(safe-area-inset-bottom));
    border-radius: 0;
    background: var(--glass);
    backdrop-filter: blur(24px) saturate(140%);
    -webkit-backdrop-filter: blur(24px) saturate(140%);
    box-shadow: none;
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  @supports not (backdrop-filter: blur(1px)) {
    .site-header--app .nav-links {
      background: var(--glass-opaque);
    }
  }

  .site-header--app .nav-links a {
    flex: 1;
    min-width: 0;
    flex-direction: column;
    /* Top-aligned, not centred: a two-line label ("Booking setup" in bookable
       mode) makes a taller column, and centring it lifted that tab's icon 6px
       above the four beside it. The second line hangs below instead. */
    justify-content: flex-start;
    padding-block-start: var(--space-1);
    gap: var(--space-1);
    padding-inline: var(--space-1);
    border-radius: var(--radius-md);
    font-size: 0.6875rem;
    font-weight: 700;
    /* The body line-height would put two 11px lines over the bar's height. */
    line-height: 1.2;
    text-align: center;
    /* A two-WORD label may take the two lines `--tabbar-h` is sized for. The
       header row above forbids exactly this — five labels on one line there,
       wrapped labels in five columns here — so the tab bar takes the permission
       back, but only at the space.

       `normal`, not the `anywhere` this said: `anywhere` splits a single word
       at any character it likes, and at 320-360px it printed "Message / s"
       under the chat icon. A destination labelled "Message" is a mislabelled
       destination — worse than one that overhangs its column by a few pixels.
       `.app-main`'s `overflow-wrap: anywhere` is not inherited here (the bar is
       in the header), but the declaration is kept explicit so nobody restores
       the old value thinking it was load-bearing. */
    white-space: normal;
    overflow-wrap: normal;
  }

  .site-header--app .nav-links a svg {
    display: block;
  }

  /* The CTA is a BUTTON in the bar, not a fifth tab: it has no icon to stack
     under and no current state to show, and at the tab's 0.6875rem column
     layout "Post a job" broke across two lines beside four one-line labels. */
  .site-header--app .nav-links a.btn {
    flex: 0 0 auto;
    flex-direction: row;
    /* The tabs' top-aligning pad outranks `.btn`'s own padding, and the CTA has
       no stacked icon to align — it stretches to the bar, so symmetric is the
       only thing that keeps its label centred. */
    padding-block: 0;
    font-size: 0.8125rem;
    white-space: nowrap;
  }

  .site-header--app .nav-links a.btn svg {
    display: none;
  }

  .site-header--app .nav-links a[aria-current="page"] {
    color: var(--primary);
  }

  .tabbar-spacer {
    display: block;
    height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  }

  /* A site with ONE destination has no tab bar to be: a full-width glass band
     holding a single tab that is always the current page navigates nowhere and
     covers the bottom of the page on every phone screen. It keeps the header
     row's pill instead — visible and named at both widths, so the link itself
     is never hidden. Both React consumers have ≥2 links, so this never matches
     them. */
  .site-header--app .nav-links:has(> a:only-child) {
    position: static;
    height: auto;
    padding: 0;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    flex-direction: row;
    gap: var(--space-2);
    border-radius: var(--radius-pill);
  }

  .site-header--app .nav-links:has(> a:only-child) a {
    flex: 0 0 auto;
    flex-direction: row;
    gap: var(--space-2);
    padding-inline: var(--space-3);
    font-size: 0.8125rem;
    white-space: nowrap;
    text-align: start;
  }

  /* The icon is the tab bar's affordance; beside a label in a 60px header row
     it is noise — the same call the header makes at every other width. */
  .site-header--app .nav-links:has(> a:only-child) a svg {
    display: none;
  }

  /* --primary is the TAB BAR's current treatment. Back in the header row the
     current page is the tonal pill, so the label goes back to on-surface. */
  .site-header--app .nav-links:has(> a:only-child) a[aria-current="page"] {
    color: var(--on-surface);
  }

  /* The reference 2rem row gap is 48px of a 272px column at 320px, where the
     brand, the pill and the account button now share one line. */
  .site-header--app:has(.nav-links > a:only-child) .nav {
    gap: var(--space-4);
  }

  body:has(.site-header--app .nav-links > a:only-child) .tabbar-spacer {
    display: none;
  }
}

/* Four destinations and a ~102px button in 320-360px of bar is ~56px a tab,
   which is under what "Messages" or "Bookings" needs on one line. The CTA is
   the only item in the row that is not a destination, so it is the one that
   gives the width back: at the tab's own type size and half the horizontal
   padding it costs ~75px instead, and the tabs get the difference.
   It is NOT hidden — chrome-contract asserts this link is visible in the
   header at 412px and jobs.spec clicks it there. */
@media (max-width: 599px) {
  .site-header--app .nav-links a.btn {
    padding-inline: var(--space-2);
    font-size: 0.6875rem;
  }
}

/* The compact footer: the reference's tonal bar only — a three-column sitemap
   inside a workspace is noise. Rendered on EVERY route, auth and 404 included,
   so terms, privacy and support are always one click away. */
.site-footer--app {
  padding-block: var(--space-8);
  /* Same reason as .tabbar-spacer: a shrinking flex item squashes its own
     padding on a long page. */
  flex: none;
}

/* Privacy, Terms and Support all LOOK like links, and the sheet is what says
   so — not each document.

   styles.css paints `.footer-bottom a` in the footer's own dim 13px and the
   global reset takes the underline off, so on the marketing page these three
   are told apart from the copyright beside them by nothing at all. Each site
   then patched its own: the partner dashboard hung `.link-inline` on the
   mailto and left Privacy and Terms as text, while the React <AppFooter/>
   hung it on none of them — two renderers of ONE shell disagreeing about
   whether the footer has links in it, with a test asserting the underline the
   shared component would have failed.

   Settled here, once, for every consumer. Underlined rather than orange: R4
   keeps orange for the action to take, and three legal links in a footer bar
   are not it. The line is a 40% primary so it reads as a link at 13px without
   competing, and firms to full primary on hover. */
.site-footer--app .footer-bottom a {
  color: var(--on-surface-variant);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: rgba(var(--primary-rgb), 0.4);
  /* 13px of text with no padding is a 20px target, on the three links that are
     on EVERY page of all three sites. The `.back-link` idiom: grow to 44px and
     spend the growth on the neighbours' whitespace, so the footer bar's own
     rhythm does not move. */
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  margin-block: calc(-1 * var(--space-2));
}

.site-footer--app .footer-bottom a:hover {
  color: var(--on-surface);
  text-decoration-color: var(--primary);
}

/* ==========================================================================
   Page head
   eyebrow -> h1.display -> p.lead, with an actions slot. Exactly one <h1> per
   document; section headings are h2.headline-sm.
   ========================================================================== */

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-4);
  margin-block-end: var(--space-12);
}

.page-head__text {
  flex: 1 1 22rem;
  min-width: 0;
}

/* Its own line above the eyebrow — both are inline-flex, so without this they
   sit side by side and the breadcrumb reads as part of the section label. */
.page-head__text > .back-link {
  display: flex;
  width: fit-content;
  margin-block-end: var(--space-4);
}

.page-head__text .lead {
  margin-block-start: var(--space-3);
}

.page-head__actions {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* The head-to-first-block distance is 48px on every route. Inside a gap stack
   the stack's own gap adds to the margin, which is why nine routes started
   their content 80px under the head and three started it at 48px. */
.stack > .page-head {
  margin-block-end: calc(var(--space-12) - var(--space-4));
}
.stack--loose > .page-head {
  margin-block-end: calc(var(--space-12) - var(--space-8));
}

/* Back / breadcrumb. Orange is the ACTION colour (R4) — going back is not an
   action, so it reads as secondary text that underlines on hover. */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--on-surface-variant);
  font-size: 0.8125rem;
  font-weight: 600;
  padding-block: var(--space-1);
  /* 13px text in 4px of padding is a 29px target on the control that heads
     every detail page. The negative margin spends the growth on the two
     neighbours' whitespace, so the target reaches 44px and the rhythm the
     page head was drawn to does not move. */
  min-height: 2.75rem;
  margin-block: calc(-1 * var(--space-2));
}

.back-link:hover {
  color: var(--on-surface);
  text-decoration: underline;
  text-underline-offset: 0.25em;
}

.back-link svg {
  width: 1rem;
  height: 1rem;
  flex: none;
}

/* The global reset zeroes heading margins, so a section title sat on the card
   below it with 4px of air while the page head above kept 48. The ELEMENT is
   the subject, not `.headline-sm` — that class belongs to styles.css and this
   sheet never becomes its styler. A heading inside `.card .stack` is not a
   direct child of a <section> and keeps the stack's own gap. */
.app section > h2 {
  margin-block-end: var(--space-4);
}

/* ==========================================================================
   Buttons — ADDITIONS ONLY. styles.css owns .btn/.btn-primary/-secondary/
   -tertiary/-lg and is never touched here.
   ========================================================================== */

/* An 0.55-opacity gradient reads as a muddy burnt-orange fourth button style,
   not as "nothing to send yet". Disabled is a tonal state, like everything
   else in this system. */
.btn:disabled,
.btn[aria-disabled="true"] {
  background: var(--surface-container-highest);
  background-image: none;
  color: var(--on-surface-dim);
  box-shadow: none;
  cursor: not-allowed;
  filter: none;
}

.btn:disabled:hover,
.btn[aria-disabled="true"]:hover {
  transform: none;
  filter: none;
  background: var(--surface-container-highest);
  box-shadow: none;
}

/* …except a tertiary, whose whole visual is its label: filled, a disabled
   "Resend code (60s)" is a grey slab beside a bare orange sibling for the
   whole countdown. The :hover pair is needed because the rule above repaints
   the fill on hover. */
.btn-tertiary:disabled,
.btn-tertiary[aria-disabled="true"],
.btn-tertiary:disabled:hover,
.btn-tertiary[aria-disabled="true"]:hover {
  background: none;
  color: var(--on-surface-dim);
}

.btn-sm {
  padding: 0.55rem var(--space-4);
  font-size: 0.875rem;
  /* "Retry", "Close" and the chat composer's siblings are the small controls a
     phone uses most; 36px was under the target size. */
  min-height: 2.75rem;
}

/* Full-width buttons are a PHONE idiom. Unscoped, it produced 690px slabs on a
   desktop form. */
@media (max-width: 480px) {
  .btn-block {
    width: 100%;
    justify-content: center;
  }
}

/* Destructive confirmation. Tonal, and NOT the gradient, so the one-primary
   guardrail is untouched — but the error tone says which of the two buttons is
   the irreversible one, which two identical secondaries never did. */
.btn-danger {
  background: var(--surface-container-highest);
  color: var(--error);
  box-shadow: inset 0 0 0 1px rgba(var(--error-rgb), 0.35);
}

.btn-danger:hover {
  background: color-mix(in srgb, var(--error) 12%, var(--surface-container-highest));
}

/* A primary-tinted ring on the primary gradient is invisible; every "Sign in",
   "Send quote" and "Start work" showed no progress at all while loading. */
.btn .spinner {
  border-color: color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
}

/* ==========================================================================
   Rhythm + layout utilities (no colour, no chrome — spacing only)
   ========================================================================== */

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.stack--tight {
  gap: var(--space-2);
}
.stack--loose {
  gap: var(--space-8);
}

/* A status region stays in the DOM while empty so it can announce — but a
   zero-height flex child still spends its parent's gap, which is why the
   dashboard started its stats 86px under the head and the sign-in form put
   29px between a field and its button. A negative margin equal to the gap
   collapses the slot without touching `display`, so nothing about the
   announcement changes. */
.stack > [role="status"]:empty {
  margin-block-start: calc(-1 * var(--space-4));
}
.stack--tight > [role="status"]:empty {
  margin-block-start: calc(-1 * var(--space-2));
}
.stack--loose > [role="status"]:empty {
  margin-block-start: calc(-1 * var(--space-8));
}

/* A .stack stretches its children, so a bare .btn dropped into one became a
   full-width bar with its label floating in the middle of the panel. Buttons
   size to their label unless a form explicitly opts into stretching. */
.stack > .btn {
  align-self: flex-start;
}
.stack--stretch > .btn {
  align-self: stretch;
}

/* A tertiary's whole visual is its label, so the 8px it keeps for a target
   stood "Use a different email" and "Sign out" a few pixels right of everything
   above them. Pull the box back by exactly that padding: the label lands on the
   column edge and the hit area is untouched. */
.stack > .btn-tertiary,
.cluster > .btn-tertiary:first-child,
.card > .btn-tertiary {
  margin-inline-start: calc(-1 * var(--space-2));
}

/* The horizontal counterpart: an inline group of related controls or metadata. */
.cluster {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.cluster--between {
  justify-content: space-between;
}
.cluster--nowrap {
  flex-wrap: nowrap;
  min-width: 0;
}
.cluster--start {
  align-items: flex-start;
}
.cluster--end {
  align-items: flex-end;
}
.cluster--tight {
  gap: var(--space-2);
}
/* Text inside a nowrap cluster must be the part that shrinks, otherwise a long
   address wraps as one flex item and orphans the icon on its own line. */
.cluster--nowrap > .cluster__text {
  flex: 1;
  min-width: 0;
}

.flow > * + * {
  margin-block-start: var(--space-4);
}

.card-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
}

/* Uncaptioned photo tiles: a fixed ratio so a portrait phone shot and a
   landscape one do not make a ragged grid. */
.photo-tile {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--radius-md);
}

.field-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}

/* ---- Text utilities ------------------------------------------------------ */

.muted {
  color: var(--on-surface-variant);
}
.dim {
  color: var(--on-surface-dim);
}

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.nowrap {
  white-space: nowrap;
}

.pre-wrap {
  white-space: pre-wrap;
}

/* A three-step type scale for the small print that is NOT a spec-sheet label.
   These exist so a screen never hand-rolls `style={{ fontSize: "0.8125rem" }}`,
   which drifts from the sheet and cannot be audited or themed. */
.text-xs {
  font-size: 0.75rem;
}
.text-sm {
  font-size: 0.8125rem;
}
.text-md {
  font-size: 0.875rem;
}

/* The flex slack-taker. Without `min-width: 0` a long word inside a flex child
   refuses to shrink and pushes its siblings off the row. */
.fill {
  flex: 1;
  min-width: 0;
}

.push-end {
  margin-inline-start: auto;
}

/* Inline glyph beside a line of text (a pin before an address, a clock before
   a time). Sized here so it never inherits an icon's default 1.25em in a
   context that has no rule of its own. */
.icon-sm {
  width: 1rem;
  height: 1rem;
  flex: none;
}

/* `.cluster--start` keeps the glyph on the FIRST line of a wrapping address,
   which also parks it on the top of a 22.5px line box. Drop it onto the
   x-height without giving up the first-line anchoring. */
.cluster--start > .icon-sm {
  margin-block-start: 0.2em;
}

/* The shared reset strips list markers everywhere, which is right for nav and
   card lists and wrong for prose. This puts them back where the bullets ARE
   the content. */
.bullet-list {
  list-style: disc;
  padding-inline-start: var(--space-5);
}

/* Money and counts: display face, tabular figures so columns align. */
.amount {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.amount--md {
  font-size: 1.5rem;
}
.amount--lg {
  font-size: clamp(1.75rem, 1.4rem + 1.6vw, 2.5rem);
}

/* ==========================================================================
   Surfaces — styles.css .card is the one bounded panel. These are modifiers.
   ========================================================================== */

/* App density: a workspace card carries a quarter of the copy a marketing card
   does, and 2rem of padding around three lines reads as an empty box. */
.card--dense {
  padding: var(--space-5);
}

.card--raised {
  background: var(--surface-container-high);
}

.card--flush {
  padding: 0;
  overflow: hidden;
}

/* Auth cards and other floating single-purpose panels. */
.card--hero {
  border-radius: var(--radius-xl);
  padding: var(--space-10);
  box-shadow: var(--shadow-float);
}

@media (max-width: 480px) {
  .card--hero {
    padding: var(--space-6);
  }
}

/* A card that leads with a glyph: the tile is a fixed slot and the body takes
   the slack, so a two-line and a five-line card keep the same left edge. */
.card--lead {
  display: flex;
  gap: var(--space-4);
  align-items: flex-start;
}

/* The trade tile the apps draw beside a job. A tint, not a border — the glyph
   is identification, not state, so it stays the one tinted square and never
   grows a ring. */
.card__glyph {
  width: 2.5rem;
  height: 2.5rem;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--primary) 14%, transparent);
  color: var(--primary);
}

/* ---- List rows -----------------------------------------------------------
   Rows are separated by gap and told apart by a tonal hover, never by a rule.
   ------------------------------------------------------------------------- */

.row {
  display: block;
  width: 100%;
  text-align: left;
  background: var(--surface-container);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}

/* Scoped to rows you can actually click: the quote rows on Active work and the
   service/add-on panels are plain divs holding their own buttons, and lifting
   them promised a click nothing answers. The ghost edge is there because the
   tonal step alone is 1.07:1 — invisible through the header glass and grain. */
a.row:hover,
button.row:hover {
  background: var(--surface-container-high);
  transform: translateY(-1px);
  box-shadow: inset 0 0 0 1px var(--ghost-border-strong);
}

/* A row that is only PART of a panel does not lift on its own — the linked body
   would rise while the action footer stayed put and the card would split into
   two tones. The whole card answers instead. */
.card--flush > a.row:hover {
  transform: none;
}
.card--flush:has(> a.row:hover) {
  background: var(--surface-container-high);
}

/* Selected row: the same tonal lift plus the orange laser pointer on its title.
   The 3px inset bar this replaces was a drawn line for state. */
.row[aria-current="true"],
.row[aria-current="page"] {
  background: var(--surface-container-high);
}
.row[aria-current="true"] .row__title,
.row[aria-current="page"] .row__title {
  color: var(--primary);
}

/* The title line: title takes the slack, the trailing slot never wraps. Long
   titles must truncate, not push the status pill onto its own line — which is
   why a list of three rows had two different layouts. */
.row__top {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: nowrap;
  min-width: 0;
}

.row__title {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.row__when {
  flex: none;
  white-space: nowrap;
  color: var(--on-surface-dim);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

/* The kind label — "QUOTE", "PAYMENT", "APPOINTMENT" — heading a card in a
   thread. `.app-main`'s `overflow-wrap: anywhere` is inherited, and a 12px
   uppercase single word with letter-spacing is exactly what it splits: the
   appointment card read "APPOINTMEN / T". It is a label, not prose, so it gets
   the same `flex: none; white-space: nowrap` the trailing slot has. */
.row__top > .label-sm {
  flex: none;
  white-space: nowrap;
  overflow-wrap: normal;
}

/* Unread: one primary dot beside the timestamp. The laser pointer earns its
   keep here — in a list of identical rows this is the only thing that is
   genuinely new. */
.row__unread {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-pill);
  background: var(--primary);
  flex: none;
}

.row__sub {
  color: var(--on-surface-variant);
  font-size: 0.9375rem;
}

.row__preview {
  color: var(--on-surface-variant);
  font-size: 0.9375rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* A list whose rows can be ticked: the box sits BESIDE the row, never inside
   its link. */
.select-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}
.select-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: var(--space-3);
}

/* ---- Spec list — the one key/value block -------------------------------- */

.spec-list {
  display: grid;
  gap: var(--space-5);
}

.spec-list > div {
  min-width: 0;
}

.spec-list dt {
  font-family: var(--font-body);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--on-surface-variant);
}

.spec-list dd {
  margin-block-start: var(--space-2);
  font-size: 1.0625rem;
  overflow-wrap: anywhere;
}

@media (min-width: 600px) {
  .spec-list--columns {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  }
}

/* ---- Stat tile ----------------------------------------------------------- */

.stat {
  background: var(--surface-container);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
}

.stat__value {
  display: block;
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.stat__label {
  display: block;
  margin-block-start: var(--space-2);
}

/* A right-aligned block only ever makes sense beside something. When the row
   wraps on a phone the block lands under a left-aligned name and the card ends
   with two ragged edges. */
.stat-block {
  text-align: left;
}

@media (min-width: 600px) {
  .stat-block {
    margin-left: auto;
    text-align: right;
  }
}

/* ==========================================================================
   Status pills
   .trade-tag's geometry, six semantic tones, sentence case, always with text.
   ========================================================================== */

.status-pill {
  /* Never left undefined: an unknown status still renders a readable pill. */
  --status-tone: var(--status-neutral);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-pill);
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1.25;
  background: color-mix(in srgb, var(--status-tone) 16%, transparent);
  color: var(--status-tone);
}

.status-pill--pending {
  --status-tone: var(--status-pending);
}
.status-pill--progress {
  --status-tone: var(--status-progress);
}
.status-pill--success {
  --status-tone: var(--status-success);
}
.status-pill--info {
  --status-tone: var(--status-info);
}
.status-pill--neutral {
  --status-tone: var(--status-neutral);
}
.status-pill--danger {
  --status-tone: var(--status-danger);
}

/* ==========================================================================
   Forms
   ========================================================================== */

.form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* A radio group is a real <fieldset>, and a fieldset's default border is
   exactly the 1px sectioning line R1 forbids. */
fieldset.field {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

/* A rendered <legend> is not a flex item, so the fieldset's gap never reaches
   it: every radio and chip group hugged its own label with 4px while the text
   fields beside it had 8. A group heading gets more, because it titles the
   fields under it rather than labelling one control. */
fieldset.field > legend {
  margin-block-end: var(--space-2);
}
fieldset.field > legend.spec-title {
  margin-block-end: var(--space-3);
}

/* The label IS .label-sm — the spec-sheet pairing, at the reference's size.
   Three sites had three different label sizes reaching for the same thing. */
.field__label {
  font-family: var(--font-body);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--on-surface-variant);
}

.field__label .required {
  color: var(--primary);
}

.field__hint {
  font-size: 0.8125rem;
  color: var(--on-surface-dim);
}

/* R8: never turn the whole box red. An error is text, an underline and (in the
   banner case) an icon, so the form keeps its composure. */
.field__error {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.8125rem;
  color: var(--error);
  font-weight: 600;
}

.field__error svg {
  width: 1rem;
  height: 1rem;
  flex: none;
}

.input,
.textarea,
.select {
  width: 100%;
  background: var(--surface-container-lowest);
  color: var(--on-surface);
  border: none;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--ghost-border);
  padding: 0.75rem var(--space-4);
  font-size: 0.9375rem;
  transition: box-shadow 0.2s var(--ease);
}

.textarea {
  min-height: 7rem;
  /* The fields size by `rows` off a generous min-height, and the browser's grip
     was the one un-designed glyph left on a machined field — a stray light dot
     beside the send button on the two-row composer, which must never be
     user-resizable anyway. */
  resize: none;
}

.input:focus,
.textarea:focus,
.select:focus {
  outline: none;
  box-shadow: inset 0 0 0 1px var(--primary);
}

.input[aria-invalid="true"],
.textarea[aria-invalid="true"],
.select[aria-invalid="true"] {
  box-shadow: inset 0 -2px 0 0 var(--error);
}

.input::placeholder,
.textarea::placeholder {
  color: var(--on-surface-dim);
}

.input:disabled,
.textarea:disabled,
.select:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* The OS chevron and the browser's white clock/calendar glyphs were the only
   un-designed marks on every form. */
.select {
  appearance: none;
  -webkit-appearance: none;
  padding-inline-end: var(--space-10);
  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='%23adaaaa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-4) center;
}

.input::-webkit-calendar-picker-indicator {
  /* The indicator ships as a black glyph; invert it to sit on a dark field at
     roughly on-surface-variant weight. */
  filter: invert(1) opacity(0.6);
  cursor: pointer;
}

/* One-time code: wide tracking so six digits read as six discrete glyphs
   without six separate inputs, which are a paste-and-autofill trap on mobile. */
.input--otp {
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  font-size: clamp(1.75rem, 6vw, 2.125rem);
  font-weight: 800;
  letter-spacing: 0.5em;
  text-indent: 0.5em; /* re-centres text that trailing letter-spacing shifts left */
  text-align: center;
  padding-block: var(--space-4);
}

/* The reveal toggle is anchored to the INPUT ROW, not to the field: measuring
   from a wrapper that also contains the label floats the button up into the gap
   above the input as soon as the label wraps. */
.input-row {
  position: relative;
}

.input-row .input {
  /* Reserve real space, or a revealed password runs under the word "Show" and
     clicking the tail of the field toggles visibility instead of placing the
     caret. */
  padding-inline-end: 4.5rem;
}

.password-toggle {
  position: absolute;
  inset-block: 0;
  inset-inline-end: var(--space-2);
  padding: var(--space-2);
  background: none;
  color: var(--on-surface-variant);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
}

.password-toggle:hover {
  color: var(--on-surface);
}

/* ---- Message composer ----------------------------------------------------
   A field whose send control sits INSIDE the input row rather than beside the
   whole field. Beside the field, the button lines itself up with whichever
   edge the field ends on — which on a phone is the second line of the hint,
   so the send button floated below the box it belonged to and the box itself
   shrank to about 60% of the row. The label stays above and the hint below,
   both spanning the full width, which is where a person expects to find them.
   ------------------------------------------------------------------------- */

.composer__row {
  display: flex;
  align-items: flex-end;
  gap: var(--space-3);
}

.composer__row .textarea {
  flex: 1;
  min-width: 0;
  /* A composer is two lines that grow, not the 7rem block a form field is. */
  min-height: 0;
}

.composer__send {
  flex: none;
}

@media (max-width: 599px) {
  /* The icon plus the button's accessible name carry it; the word "Send" costs
     a third of the row on a 412px screen, and the field is the part that has
     to be usable. */
  .composer__send-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ---- Drawn checkbox / radio ---------------------------------------------
   The native control was the one browser-default glyph on a form full of
   machined surfaces. `appearance: none` on the real input keeps every bit of
   native semantics, focus and keyboard behaviour and repaints only the box.
   ------------------------------------------------------------------------- */

.checkbox,
.radio {
  display: flex;
  /* Centred, not flex-start: the label is the target and a one-line label in a
     44px row reads as floating if it stays pinned to the top. */
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  font-size: 0.9375rem;
  /* A 20px box beside one line of 15px text is a 23px row — the contractor's
     seven weekday on/off toggles, stacked, were seven 23px targets. */
  min-height: 2.75rem;
  padding-block: var(--space-1);
}

.checkbox input[type="checkbox"],
.radio input[type="radio"],
.category-chip > input[type="radio"],
.category-chip > input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  flex: none;
  width: 1.25rem;
  height: 1.25rem;
  margin: 0.125rem 0 0;
  display: grid;
  place-items: center;
  background: var(--surface-container-lowest);
  /* A control's boundary owes 3:1 (WCAG 1.4.11). The ghost border composited
     over this fill measured 1.01:1 — an unticked box was invisible on a card. */
  box-shadow: inset 0 0 0 1.5px var(--on-surface-dim);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.15s var(--ease), box-shadow 0.15s var(--ease);
}

.radio input[type="radio"],
.category-chip > input[type="radio"] {
  border-radius: 50%;
}

.checkbox input[type="checkbox"]:checked,
.radio input[type="radio"]:checked,
.category-chip > input:checked {
  background: var(--primary);
  box-shadow: none;
}

.checkbox input[type="checkbox"]::after,
.category-chip > input[type="checkbox"]::after {
  content: "";
  width: 0.35rem;
  height: 0.65rem;
  margin-block-start: -0.15rem;
  border: solid var(--on-primary);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0);
  transition: transform 0.15s var(--ease);
}

.checkbox input[type="checkbox"]:checked::after,
.category-chip > input[type="checkbox"]:checked::after {
  transform: rotate(45deg) scale(1);
}

.radio input[type="radio"]::after,
.category-chip > input[type="radio"]::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--on-primary);
  transform: scale(0);
  transition: transform 0.15s var(--ease);
}

.radio input[type="radio"]:checked::after,
.category-chip > input[type="radio"]:checked::after {
  transform: scale(1);
}

.checkbox input:disabled,
.radio input:disabled,
.category-chip > input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

@media (forced-colors: active) {
  /* Windows High Contrast discards author backgrounds and box-shadow, which
     leaves every drawn control identical. `outline` survives. */
  .checkbox input[type="checkbox"],
  .radio input[type="radio"] {
    outline: 1px solid CanvasText;
  }
  .checkbox input[type="checkbox"]:checked,
  .radio input[type="radio"]:checked {
    outline: 2px solid Highlight;
  }
}

/* ---- Selection chips ----------------------------------------------------
   styles.css owns .category-chip and documents WHY the spec's literal active
   pair (#924c00 on #4b2400, ~2.1:1) is not used: the warm-amber "selected"
   feeling is carried by a primary tint and ring, with readable text throughout.
   This is that state, written once, for every site.
   ------------------------------------------------------------------------- */

.category-chip[aria-pressed="true"],
.category-chip:has(> input:checked) {
  background: color-mix(in srgb, var(--primary) 18%, transparent);
  color: var(--on-surface);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 45%, transparent);
}

/* Filter rows and multi-select pickers carry many more chips than a landing
   page's twelve trades, so they get the same chip one density step down. */
.category-chip--sm {
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  font-size: 0.8125rem;
}
.category-chip--sm svg {
  width: 1rem;
  height: 1rem;
}

.category-chip:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
.category-chip:disabled:hover {
  transform: none;
  background: var(--surface-container-high);
  box-shadow: inset 0 0 0 1px transparent;
}

@media (forced-colors: active) {
  .category-chip[aria-pressed="true"],
  .category-chip:has(> input:checked) {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }
}

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Horizontal filter rail on narrow screens.

   A rail that overflows silently is a rail nobody scrolls: the twelve trades
   clipped mid-word at the container edge with the scrollbar hidden, and four of
   the twelve were all a phone ever showed. Two rules together, and they are a
   pair on purpose:

   · From 860px — the shell's own breakpoint, so the system has one — the strip
     WRAPS. There is room for every chip on two lines, so there is nothing left
     to scroll and nothing to indicate.
   · Below it the strip stays a strip, because a wrapped twelve-chip block would
     push the content it filters off a phone screen — and it takes a mask fade
     at the trailing edge so the last chip visibly runs out of the container
     rather than ending at it.

   The fade lives ONLY in the narrow branch for that reason: a permanent mask
   would dim the final chip of a four-chip filter that fits perfectly well —
   and, for the same reason, it ends when the scrolling does (below). */
.chip-scroller {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding-bottom: var(--space-2);
}
.chip-scroller > * {
  flex: none;
}

@media (max-width: 859.98px) {
  .chip-scroller {
    flex-wrap: nowrap;
    overflow-x: auto;
    /* A scroller clips on BOTH axes, so the 2px ring at 3px offset was cut off
       the top and the ends of the strip. Padding makes room for it and the
       matching negative margin keeps the visual shift at zero. */
    padding: var(--space-2);
    margin: calc(-1 * var(--space-2));
    scrollbar-width: thin;
    scroll-padding-inline: var(--space-4);
    scroll-snap-type: x proximity;
    /* `currentColor`, not a literal black: a mask reads only the ALPHA of its
       gradient, so any opaque colour does, and spelling one out would be the
       one literal colour in a sheet whose whole contract is that it has none.

       Both spellings — -webkit- for the WebKit that still needs it, unprefixed
       for everything else. A lone unprefixed declaration masks nothing on older
       Safari, which is a large share of this product's phones. */
    -webkit-mask-image: linear-gradient(90deg, currentColor calc(100% - 2.5rem), transparent);
    mask-image: linear-gradient(90deg, currentColor calc(100% - 2.5rem), transparent);
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
  }
  .chip-scroller > * {
    scroll-snap-align: start;
  }

  /* The state where there is nothing further to scroll to is the state where
     the fade is a lie: scrolled to the end, the last of the twelve trades sat
     permanently dimmed, and the affordance said "there is more" about the one
     position where there is not.

     Driven by the strip's own scroll position rather than by a listener, so it
     costs no JavaScript and works in a static document as well as in React.
     The mask IMAGE never changes — only its box does: 2.5rem wider than the
     element puts the gradient's transparent tail past the trailing edge, which
     is a fade that has run out rather than a fade switched off. It moves over
     the last tenth of the travel, so a strip mid-scroll still shows it.

     Everything without scroll-driven animations keeps the static fade, which is
     the behaviour this replaced and is still correct for every position but
     one. */
  @supports (animation-timeline: scroll()) {
    .chip-scroller {
      animation: chip-strip-end linear both;
      animation-timeline: scroll(self inline);
    }
  }

  @keyframes chip-strip-end {
    0%,
    90% {
      -webkit-mask-size: 100% 100%;
      mask-size: 100% 100%;
    }
    100% {
      -webkit-mask-size: calc(100% + 2.5rem) 100%;
      mask-size: calc(100% + 2.5rem) 100%;
    }
  }
}

/* ---- Combobox + the one floating menu ------------------------------------
   An autocomplete listbox, a notification dropdown and an account menu are the
   same object: a floating panel of choices on the highest surface. One recipe,
   so restyling the picker cannot silently restyle the bell.
   ------------------------------------------------------------------------- */

.combobox {
  position: relative;
}

.menu {
  position: absolute;
  /* 120, above the header's 100 and the tab bar's 90, below the modal's 150.
     Both populations needed it: a menu opened from the header sits inside
     `.site-header`'s layer-100 stacking context, where 70 lost to `.nav-links`
     at 90, and a menu opened from page content lost to the whole header at the
     root. One number clears both. */
  z-index: 120;
  inset-inline: 0;
  margin-block-start: var(--space-2);
  padding: var(--space-2);
  background: var(--surface-container-highest);
  border-radius: var(--radius-md);
  /* 16rem is the panel's own budget; the viewport's is what is actually left
     between the header and the tab bar, which on a short phone is less. The
     notification panel was the case: a 256px panel with 180px of room scrolled
     its last rows under the bar rather than sizing itself to the gap. */
  max-height: min(
    16rem,
    calc(100dvh - var(--appbar-h) - var(--tabbar-h) - var(--space-6))
  );
  overflow-y: auto;
  /* rgba(), not rgb(... / ...): --shadow-tint is the comma-separated triple
     "20, 12, 6", so the slash form is invalid syntax and the whole declaration
     is dropped. */
  box-shadow: 0 32px 48px -12px rgba(var(--shadow-tint), 0.32);
  /* The marketing dropdown's motion, on the product's highest-tier panel.
     `allow-discrete` transitions the computed value, so the global
     `[hidden] { display: none !important }` is no obstacle and stays. */
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease),
    display 0.2s allow-discrete;
}

.menu[hidden] {
  opacity: 0;
  transform: translateY(-0.25rem);
}

@starting-style {
  .menu {
    opacity: 0;
    transform: translateY(-0.25rem);
  }
}

/* Anchored to the trailing edge (account menu, notification bell) rather than
   stretched across its trigger. */
.menu--end {
  inset-inline: auto 0;
  width: min(22rem, calc(100vw - 2rem));
}

.menu__item {
  display: grid;
  gap: var(--space-1);
  width: 100%;
  text-align: left;
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  color: var(--on-surface);
  cursor: pointer;
}

/* Hover goes to a primary tint: surface-container-high on a highest-surface
   panel measures 1.08:1 — imperceptible, and DARKER, which inverts what every
   other hover in the system does. */
.menu__item:hover,
.menu__item[aria-selected="true"],
.menu__item.is-active {
  background: color-mix(in srgb, var(--primary) 14%, transparent);
}

/* The item is a stack, so a two- or three-line option (a notification's title,
   body and time) needs no extra class — its spans are its rows. This is the
   second line of a two-line option: an address under a place name. */
.menu__item .menu__detail {
  display: block;
  font-size: 0.8125rem;
  color: var(--on-surface-dim);
}

.menu__head {
  padding: var(--space-3);
  border-radius: var(--radius-sm);
  background: var(--surface-container-high);
  margin-block-end: var(--space-2);
}

/* The bell's hover preview: five rows plus "View all" must fit without
   scrolling, so each row is ONE line of title and one of body. Unclamped, a
   three-line title pushed the fifth row under the footer on a 900px desktop.
   No rem cap: five clamped rows ARE the bound, and they measure ~32rem — a
   32rem cap left 3px of slack on macOS and scrolled on Linux, whose text sets
   a hair taller. Only the viewport limits it now. */
.menu--preview {
  max-height: calc(100dvh - var(--appbar-h) - var(--tabbar-h) - var(--space-6));
}
/* `minmax(0, 1fr)`: a grid item's min-width is its content, and a no-wrap
   title's content is the whole title — without this the ellipsis never
   engages and the row runs off the panel, "New" pill and all. */
.menu--preview .menu__item {
  grid-template-columns: minmax(0, 1fr);
}
.menu--preview .row__title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.menu--preview .row__preview {
  -webkit-line-clamp: 1;
  line-clamp: 1;
}

/* "View all" stays pinned to the bottom of a panel that scrolls (a short
   phone still can), and spans it: `.btn-block` is phone-only by design. */
.menu__foot {
  position: sticky;
  bottom: calc(-1 * var(--space-2));
  margin: var(--space-2) calc(-1 * var(--space-2)) calc(-1 * var(--space-2));
  padding: var(--space-2);
  background: inherit;
}
.menu__foot .btn {
  width: 100%;
  justify-content: center;
}

/* ==========================================================================
   Tables — tonal bars, not a ruled grid
   ========================================================================== */

.table-wrap {
  overflow-x: auto;
  scrollbar-width: thin;
}

.table {
  width: 100%;
  /* A collapsed table cannot round its cells, and rounded tonal bars are what
     stands in for the rules R1 forbids. */
  border-collapse: separate;
  border-spacing: 0 var(--space-2);
  font-size: 0.9375rem;
  /* Below this the date column wrapped to three lines and the status column
     fell off the edge mid-word; with it the region scrolls, which is at least
     a thing a person can do something about. */
  min-width: 32rem;
}

.table th {
  text-align: left;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 600;
  color: var(--on-surface-variant);
  padding: 0 var(--space-4) var(--space-2);
  white-space: nowrap;
}

.table td {
  background: var(--surface-container);
  padding: var(--space-4);
  vertical-align: middle;
}

.table tbody tr:hover td {
  background: var(--surface-container-high);
}

.table tbody tr td:first-child {
  border-radius: var(--radius-md) 0 0 var(--radius-md);
}
.table tbody tr td:last-child {
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
}

.table .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ---- Below 600px a table stops being a table ------------------------------
   A 32rem grid inside a 327px column puts the last column 185px past the right
   edge, and a sideways scroll with no visible edge is a column a phone user
   never learns exists — the payout list looked like it had three columns and
   no status at all. Rather than fade the overflow and hope, each row becomes
   the panel it is already painted as, one label/value line per cell.

   The rows keep their markup, so the desktop table and the phone list are the
   same DOM: a cell's column name comes from its own `data-label`, which is one
   of the two things a consumer has to add. A cell without one prints just its
   value — right for a cell whose content names itself.

   The other is `role="table"/"rowgroup"/"row"/"cell"` spelled out in the
   markup. `display: block` on a <table> drops the IMPLICIT roles, so without
   them this rule would hand a screen reader a pile of text on a phone and a
   table on a laptop. Stated explicitly they survive the layout change.
   -------------------------------------------------------------------------- */

@media (max-width: 599px) {
  .table-wrap {
    /* There is nothing left to scroll sideways. */
    overflow-x: visible;
  }

  .table,
  .table tbody {
    display: block;
    min-width: 0;
  }

  /* The column names move into the rows, so the header row has nothing left to
     say — and it is `display: none` rather than hidden text because the labels
     below would otherwise be read out twice. */
  .table thead {
    display: none;
  }

  .table tbody tr {
    display: grid;
    gap: var(--space-3);
    padding: var(--space-4);
    background: var(--surface-container);
    border-radius: var(--radius-md);
  }

  .table tbody tr + tr {
    /* `border-spacing` does not survive `display: block` on the table. */
    margin-block-start: var(--space-2);
  }

  .table tbody tr:hover {
    background: var(--surface-container-high);
  }

  .table td,
  .table tbody tr:hover td,
  .table tbody tr td:first-child,
  .table tbody tr td:last-child {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
    padding: 0;
    background: none;
    border-radius: 0;
  }

  /* The column name, in the same type as the <th> it replaces. */
  .table td[data-label]::before {
    content: attr(data-label);
    flex: none;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 600;
    color: var(--on-surface-variant);
  }

  /* Numbers keep their tabular figures but not their column alignment: there
     is no column left to align to. */
  .table .num {
    text-align: left;
  }
}

/* ==========================================================================
   States: empty, error, loading, notice, transient
   ========================================================================== */

/* An empty state is a CARD, on the same surface every other block of content
   on the page gets (R2).

   Painted on nothing it was a centred column of dim text floating in the page
   background — and on a phone, where it arrives below a screenful of totals,
   "No commission yet" read as a rendering failure rather than as the answer.
   The surface is what makes it look deliberate: a card of the same size sits
   exactly where the list would have been, which is the shape of "there is
   nothing here yet" rather than the shape of "something did not load".

   space-12 rather than the old space-16: 4rem of dead band above the icon was
   affordable when nothing was drawn around it and is not once there is an edge
   to measure it against. */
.empty-state {
  display: grid;
  justify-items: center;
  gap: var(--space-4);
  text-align: center;
  padding: var(--space-12) var(--space-6);
  color: var(--on-surface-variant);
  background: var(--surface-container);
  border-radius: var(--radius-lg);
}

/* …unless something already gave it one. R2's stack only reads if each step is
   a step: surface-container inside a .card of the same tone is an invisible
   box, and inside the account/notification .menu (surface-container-highest)
   it is a dark well punched into a floating panel. In both the container IS
   the card and the empty state is simply its content. */
.card .empty-state,
.menu .empty-state {
  background: none;
  border-radius: 0;
  padding-inline: 0;
}

.empty-state svg {
  width: 2.5rem;
  height: 2.5rem;
  color: var(--on-surface-dim);
}

.empty-state__title {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 1vw + 1rem, 1.5rem);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--on-surface);
}

/* A .lead is left-anchored by default (max-width: 42rem); inside centred text
   that reads as a mis-alignment rather than a measure. */
.empty-state .lead {
  margin-inline: auto;
}

.spinner {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--primary) 25%, transparent);
  border-top-color: var(--primary);
  animation: spin 0.7s linear infinite;
}
.spinner--lg {
  width: 2.25rem;
  height: 2.25rem;
  border-width: 3px;
}
.spinner--page {
  min-height: 60dvh;
  display: grid;
  place-items: center;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

.skeleton {
  border-radius: var(--radius-md);
  background: linear-gradient(
    90deg,
    var(--surface-container) 25%,
    var(--surface-container-high) 37%,
    var(--surface-container) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
}

@keyframes shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: 0 0;
  }
}

/* ---- Banner -------------------------------------------------------------
   One neutral surface for every tone; the tone is carried by the icon and the
   text, never by a tinted box (R8 again — a warning is not a red rectangle).
   ------------------------------------------------------------------------- */

.banner {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md);
  background: var(--surface-container-high);
}

.banner svg {
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
  margin-top: 0.1rem;
}

.banner__title {
  font-weight: 700;
}

.banner--info svg {
  color: var(--info);
}
.banner--warn svg,
.banner--warn .banner__title {
  color: var(--tertiary);
}
.banner--danger svg,
.banner--danger .banner__title {
  color: var(--error);
}
.banner--success svg,
.banner--success .banner__title {
  color: var(--success);
}

.banner__action {
  margin-left: auto;
  flex: none;
}

/* ---- Toast — the transient channel -------------------------------------- */

.toast-region {
  position: fixed;
  z-index: 200;
  bottom: calc(var(--space-6) + env(safe-area-inset-bottom));
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  width: min(100% - 2rem, 28rem);
  pointer-events: none;
}

/* Above the tab bar, not on top of it: a confirmation that covers navigation is
   worse than no confirmation. `.tabbar-spacer` is the Shell's own signal that a
   bar exists — auth routes render neither, and reserved 88px there put the
   sign-out toast across the sign-in card's "Forgot your password?".

   The spacer being PRESENT is not the same as the bar being SHOWN: a
   one-destination site renders the spacer and then hides it, because its single
   link stays a pill in the header row. The partner dashboard is that site, and
   its toasts floated 4.5rem off the bottom with nothing underneath them. Gate
   on the same condition that hides the spacer. */
@media (max-width: 860px) {
  body.app:has(.tabbar-spacer):not(:has(.site-header--app .nav-links > a:only-child))
    .toast-region {
    bottom: calc(var(--tabbar-h) + var(--space-4) + env(safe-area-inset-bottom));
  }
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-container-highest);
  box-shadow: 0 1rem 2rem rgba(var(--shadow-tint), 0.4);
  font-size: 0.9375rem;
  /* A toast is a message, not a surface: the region above is already
     `pointer-events: none` and the toast itself was handing the hit back. On a
     phone it lands on the chat composer, and `elementFromPoint` at the Send
     button's centre returned DIV.toast — the tap that confirmed the message
     was eaten by the confirmation of the last one. Only the two CONTROLS take
     pointer events back, below. */
  pointer-events: none;
}

.toast__action,
.toast .icon-btn {
  pointer-events: auto;
}

.toast svg {
  width: 1.25rem;
  height: 1.25rem;
  flex: none;
  margin-top: 0.1rem;
  color: var(--success);
}

.toast--error svg {
  color: var(--error);
}

/* The dismiss control sits at the trailing edge, like `.toast__action`. */
.toast .icon-btn {
  margin-inline-start: auto;
  flex: none;
}

.toast__action {
  margin-left: auto;
  color: var(--primary);
  font-weight: 700;
  white-space: nowrap;
}

/* Centred status notice inside a thread — the anti-divider rule applies to
   conversations too. */
.msg-system {
  align-self: center;
  justify-self: center;
  max-width: 34rem;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  background: var(--surface-container-low);
  color: var(--on-surface-variant);
  font-size: 0.875rem;
  text-align: center;
}

/* ==========================================================================
   Floating tier — modal / bottom sheet
   ========================================================================== */

/* The layer ladder, in one place: menu 120 · tab bar 90 · header 100 ·
   modal 150 · toast 200. A modal must clear the chrome (a bottom sheet whose
   buttons sit under the tab bar is unusable), and a toast must clear the modal
   (a confirmation nobody can see is not a confirmation). */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 150;
  /* Tinted, never pure black: the one thing the elevation rules say never to do. */
  background: rgba(var(--shadow-tint), 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  display: flex;
  align-items: flex-end;
  justify-content: center;
}

@media (min-width: 600px) {
  .modal-backdrop {
    align-items: center;
    padding: var(--space-6);
  }
}

.modal {
  width: 100%;
  max-width: 34rem;
  max-height: 92dvh;
  overflow-y: auto;
  background: var(--surface-container-highest);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  padding: var(--space-6);
  box-shadow: var(--shadow-float);
}

@media (min-width: 600px) {
  .modal {
    border-radius: var(--radius-xl);
  }
}

.modal--wide {
  max-width: 46rem;
}

/* 34rem/46rem of content cannot hold 3 x 14rem plus two gaps, so a three-field
   terms row laid out 2 + 1 and orphaned the last field at half width. */
.modal .field-grid {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr));
}

.modal__head {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
}

.modal__title {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-right: auto;
}

.modal__actions {
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  margin-top: var(--space-6);
  flex-wrap: wrap;
}

/* As a bottom sheet the panel is its own scroll container (92dvh, overflow-y),
   so a tall form put its only action below the fold with nothing to say so —
   and on an iPhone under the home indicator. Sticky, edge to edge: the negative
   margins undo the sheet's own padding.

   Height as well as width: a landscape phone is 640-844px WIDE and ~390px tall,
   so it missed this entirely and got the quote form with its Send button 285px
   below the fold. What makes the actions need pinning is the panel scrolling,
   and that is a question about height. */
@media (max-width: 599px), (max-height: 600px) {
  .modal {
    padding-bottom: calc(var(--space-6) + env(safe-area-inset-bottom));
  }

  .modal__actions {
    position: sticky;
    bottom: calc(-1 * (var(--space-6) + env(safe-area-inset-bottom)));
    margin-inline: calc(-1 * var(--space-6));
    margin-block-end: calc(-1 * (var(--space-6) + env(safe-area-inset-bottom)));
    padding: var(--space-4) var(--space-6) calc(var(--space-4) + env(safe-area-inset-bottom));
    background: var(--surface-container-highest);
  }
}

/* ==========================================================================
   Avatar — styles.css owns .avatar and its tertiary availability halo.
   The halo is the AVAILABILITY signal, so it has to be gated, not decorative.
   ========================================================================== */

/* Opt-IN: Avatar.tsx emits no attribute at all when a caller has no
   availability concept, so gating on "false" glowed every job avatar gold. */
.avatar:not([data-available="true"])::after {
  display: none;
}

.avatar--sm {
  width: 2rem;
  height: 2rem;
}
.avatar--sm .avatar__initials {
  font-size: 0.75rem;
}

.avatar--lg {
  width: 4rem;
  height: 4rem;
}
.avatar--lg .avatar__initials {
  font-size: 1.25rem;
}

.avatar__initials {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.875rem;
  color: var(--on-surface);
  letter-spacing: 0.02em;
}

.avatar img {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
}

/* ==========================================================================
   Focus — the reference ring is canonical and untouched. These are the ONLY
   three sanctioned exceptions in the system.
   ========================================================================== */

/* 1. A field's ring is drawn INSIDE it, because an outset ring on a full-width
      input collides with the field beside it in a two-column grid. */
.input:focus-visible,
.textarea:focus-visible,
.select:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 1px var(--primary);
}

/* An invalid field is focused BY the site on every validation failure, and a
   single box-shadow at equal specificity later in the sheet meant the red
   underline was gone in exactly the state it was drawn for. :focus-visible,
   not :focus — a text input matches it even after a programmatic .focus(). */
.input[aria-invalid="true"]:focus-visible,
.textarea[aria-invalid="true"]:focus-visible,
.select[aria-invalid="true"]:focus-visible {
  box-shadow: inset 0 0 0 1px var(--primary), inset 0 -2px 0 0 var(--error);
}

/* 2. A drawn control's ring belongs to the whole label, not to the 20px box. */
.checkbox:has(input:focus-visible),
.radio:has(input:focus-visible),
.category-chip:has(> input:focus-visible) {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
.checkbox input:focus-visible,
.radio input:focus-visible,
.category-chip > input:focus-visible {
  outline: none;
}

/* 3. A link that fills a clipped panel draws its ring INSIDE the panel, because
      the panel's overflow:hidden would otherwise erase an outset one. */
.card--flush > a.row:focus-visible {
  outline-offset: -3px;
}

/* ==========================================================================
   Motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .spinner,
  .skeleton {
    animation-duration: 0.01ms;
    animation-iteration-count: 1;
  }
  * {
    transition-duration: 0.01ms !important;
  }
}
