@import "tailwindcss";
@plugin "@tailwindcss/forms";

@theme {
  /* Prose/controls default to Inter; MONO stays the explicit opt-in for data
     (money, hours, dates, table columns, eyebrow labels) — see tokens.js. */
  --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
  --font-mono: 'Fira Code', monospace;
  --font-serif: 'Source Serif 4', Georgia, serif; /* invoice document body only */
}

/* Color palette (--tb-*) is injected at runtime from the JS token object C
   (shared/ui/tokens.js) in each app's main.jsx — single source of truth.
   Fallbacks below cover the first paint before that runs. */
* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-sans);
  color: var(--tb-ink, #1C1E22);
  background: var(--tb-page-bg, #F8F7F4);
}

input, select, textarea, button { font-family: var(--font-sans); }
input[type=date]::-webkit-calendar-picker-indicator { opacity: 0.6; }
/* Number inputs (rate fields, etc.) use the same soft-fill styling as every
   other control — the browser's default spin-button clashes with it. */
input[type=number] { -moz-appearance: textfield; appearance: textfield; }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

/* Soft-fill controls (DESIGN.md §6): calm wells, gold focus ring, no hard boxes.
   Compact by default so fields/buttons don't feel oversized. */
input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea {
  background-color: var(--tb-control-fill);
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 10px;
  font-size: 12.5px;
  color: var(--tb-ink);
  height: 32px;
  box-sizing: border-box;
  outline: none;
  transition: box-shadow .15s ease, background-color .15s ease;
}
textarea { height: auto; min-height: 58px; }
select {
  -webkit-appearance: none; appearance: none; cursor: pointer; padding-right: 26px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239CA1AA' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center;
}
/* Data-URI SVG can't reference CSS vars, so the dropdown chevron gets an
   explicit dark-mode override using DARK.muted's hex directly. */
[data-theme="dark"] select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236B6E76' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}
input:not([type=checkbox]):not([type=radio]):focus, select:focus, textarea:focus {
  background-color: var(--tb-white);
  box-shadow: var(--tb-focus-ring);
}
/* Ghost select (e.g. dashboard range picker): stays transparent/muted through
   hover + focus instead of flashing into the standard filled-field look. */
select.tb-ghost-select,
select.tb-ghost-select:focus,
select.tb-ghost-select:hover {
  background-color: transparent;
  box-shadow: none;
  color: var(--tb-on-navy-muted);
}
select.tb-ghost-select:hover, select.tb-ghost-select:focus {
  color: var(--tb-steel);
}
@media (prefers-reduced-motion: reduce) {
  input, select, textarea { transition: none !important; }
}

/* Ghost toggle (DESIGN.md §6): muted off, soft-gold on, knob slides */
.tb-toggle { position: relative; width: 34px; height: 19px; border-radius: 20px; background: var(--tb-control-fill); border: 1px solid var(--tb-input); cursor: pointer; box-sizing: border-box; padding: 0; flex-shrink: 0; transition: background .18s ease, border-color .18s ease; }
.tb-toggle .tb-knob { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--tb-muted); transition: left .2s cubic-bezier(.2,.7,.3,1), background .18s ease; }
.tb-toggle.on { background: var(--tb-steel-faint); border-color: var(--tb-steel); }
.tb-toggle.on .tb-knob { left: 17px; background: var(--tb-steel-dk); }

/* Open transitions (DESIGN.md §9): things ease open, never pop */
@keyframes tb-modal-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes tb-card-in { from { opacity: 0; transform: scale(.93); } to { opacity: 1; transform: none; } }
@keyframes tb-drawer-in { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
.tb-modal-backdrop-anim { animation: tb-modal-in .18s ease; }
.tb-card-anim { animation: tb-card-in .22s cubic-bezier(.2,.7,.3,1); }
.tb-drawer-anim { animation: tb-drawer-in .22s cubic-bezier(.2,.7,.3,1); }

/* NOTE: `.tb-anim` (dropdowns/popovers — DatePicker, DescriptionAutocomplete,
   SelectOrCreate, TimeRangePicker) intentionally has NO animation here. Those
   popovers stay mounted and drive their open/close motion with an inline
   `transition` on opacity/transform, which is what lets them ease CLOSED as well
   as open — an animation can't do that, since React would unmount the node first.
   `.tb-anim` exists purely as the hook for the reduced-motion guard below, hence
   its place on the `transition` line. Do NOT give it an `animation`: the element
   is in the DOM while closed, so a mount animation flashes every popover open for
   its duration before the inline `opacity: open ? 1 : 0` snaps it back. */
@media (prefers-reduced-motion: reduce) {
  .tb-toggle, .tb-toggle .tb-knob, .tb-anim { transition: none !important; }
  .tb-modal-backdrop-anim, .tb-card-anim, .tb-drawer-anim { animation: none !important; }
}

/* Calendar day cells: soft hover (screen only; print never triggers :hover) */
.tb-cal-day { border-radius: 6px; transition: background-color .15s ease; }
.tb-cal-day:hover { background-color: var(--tb-gold-tint) !important; }
@media (prefers-reduced-motion: reduce) { .tb-cal-day { transition: none !important; } }

/* Onboarding checklist rows: soft gold-wash hover on the actionable (undone) steps */
.tb-onboard-step > div { transition: background-color .15s ease; }
.tb-onboard-step:hover > div { background-color: var(--tb-gold-tint); }

/* Collapsible nav groups: smooth height via grid-template-rows 1fr<->0fr (no
   JS measuring). Inner wrapper must clip + allow 0 min-height to fully collapse. */
.tb-collapse { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .24s cubic-bezier(.2,.7,.3,1); }
.tb-collapse.collapsed { grid-template-rows: 0fr; }
.tb-collapse > div { overflow: hidden; min-height: 0; }
/* grid-template-rows: 0fr clips PAINT and nothing else — a collapsed group's
   buttons stayed in the sequential focus order at 0px height, so tabbing past a
   collapsed group walked through invisible controls and Enter swapped the panel
   with no affordance under the caret (and Chrome scroll-anchored to the
   zero-height element). visibility:hidden takes them out of the focus order;
   the 0s delay holds it off until the collapse animation has finished, and it
   is only declared on .collapsed so EXPANDING reveals them immediately. */
.tb-collapse.collapsed > div { visibility: hidden; transition: visibility 0s .24s; }
@media (prefers-reduced-motion: reduce) { .tb-collapse.collapsed > div { transition: none !important; } }
.tb-caret { transition: transform .2s cubic-bezier(.2,.7,.3,1); }
@media (prefers-reduced-motion: reduce) { .tb-collapse, .tb-caret { transition: none !important; } }

/* Clickable analytics panels (Card with `to`) + KPI link tiles: lift + gold hairline on hover */
.tb-card-link > section, .tb-card-link > div { transition: border-color .15s ease, box-shadow .15s ease, transform .12s ease; }
.tb-card-link:hover > section, .tb-card-link:hover > div { border-color: var(--tb-gold-faint); box-shadow: 0 6px 18px rgba(0,0,0,0.08); transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) { .tb-card-link:hover > section, .tb-card-link:hover > div { transform: none; } }

/* --- View transitions: page navigation, and the theme crossfade -------------
   Both go through the SAME ::view-transition-old/new(root) pseudo-elements, so
   theme.js marks the root with .tb-vt-theme for the duration of a theme swap and
   the two are styled apart. Without that, changing pages would inherit the
   theme's 280ms crossfade.

   PAGE CHANGE (default): the outgoing content wipes away along the same diagonal
   the incoming content arrives on, then the new page fades in and slides up —
   DESIGN.md §9's "content fades + slides up", now on the way OUT as well as in.
   The two snapshots are stacked, so the exit is deliberately shorter and leads
   the enter, rather than the two cross-dissolving into mush.

   Requires the DATA router — <BrowserRouter> ignores `viewTransition` entirely,
   which is why main.jsx uses createBrowserRouter.

   THE FRAME STAYS PUT; ONLY THE CONTENT MOVES. A view transition snapshots the
   whole document, so by default the nav rail, the page header and the Preferences
   tab bar all faded out and back in on every navigation — which reads as the
   entire viewport flashing, since between them they frame the screen. None of
   them are going anywhere.

   Giving an element its own `view-transition-name` lifts it OUT of the root
   snapshot and captures it separately, so the wipe below applies to the content
   alone. Names must be UNIQUE per document — two elements sharing one makes the
   browser skip the ENTIRE transition — so each gets its own.

   These aren't all static in content: the header's title changes per page and the
   tab bar's active pill moves. So this is a SNAP, not a freeze — the old snapshot
   is hidden outright and the new one shows at full opacity from frame one. Hiding
   `old` explicitly matters: with `animation: none` the UA's fade never runs, so
   both snapshots would otherwise sit at opacity 1 — double-printing two different
   titles, or two active pills. (The UA also blends the pair with plus-lighter,
   which would blow out the overlap; normal keeps the new one honest.) */
.tb-vt-nav    { view-transition-name: tb-nav; }
.tb-vt-drawer { view-transition-name: tb-drawer; }
.tb-vt-percy  { view-transition-name: tb-percy; }
.tb-vt-header { view-transition-name: tb-header; }
.tb-vt-tabs   { view-transition-name: tb-tabs; }

/* No position/size morph — the group snaps straight to the new geometry. */
::view-transition-group(tb-nav),
::view-transition-group(tb-drawer),
::view-transition-group(tb-percy),
::view-transition-group(tb-header),
::view-transition-group(tb-tabs) { animation: none; }

::view-transition-old(tb-nav),
::view-transition-old(tb-drawer),
::view-transition-old(tb-percy),
::view-transition-old(tb-header),
::view-transition-old(tb-tabs) { animation: none; opacity: 0; }

::view-transition-new(tb-nav),
::view-transition-new(tb-drawer),
::view-transition-new(tb-percy),
::view-transition-new(tb-header),
::view-transition-new(tb-tabs) { animation: none; opacity: 1; mix-blend-mode: normal; }

/* The content leaves the way it arrived. The enter reads as a diagonal —
   top-left to bottom-right — because .tb-flow-group staggers the cards 40ms
   apart; the page fade itself is flat. The outgoing snapshot is a single
   flattened image, so its cards CAN'T be staggered individually. A diagonal mask
   sweeping across it gets the same read: the fade-out begins where the enter
   begins (top-left) and finishes where the enter finishes (bottom-right).

   The mask is 300% of the element with a soft transparent->opaque ramp along
   135deg. At 100% 100% the window sits on the opaque end (page fully visible);
   animating to 0% 0% drags the transparent leading edge in from the top-left. */
@keyframes tb-vt-page-out { from { mask-position: 100% 100%; } to { mask-position: 0% 0%; } }
@keyframes tb-vt-page-in  { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

::view-transition-old(root) {
  mask-image: linear-gradient(135deg, transparent 0 30%, #000 55% 100%);
  mask-size: 300% 300%;
  mask-repeat: no-repeat;
  animation: tb-vt-page-out 180ms cubic-bezier(.2,.7,.3,1) both;
}
::view-transition-new(root) {
  animation: tb-vt-page-in 220ms cubic-bezier(.2,.7,.3,1) 60ms both;
}

/* THEME CROSSFADE: a pure dissolve between palettes — no movement, since nothing
   is arriving or leaving, only recoloring. Longer, symmetric, and both snapshots
   run at once. Own keyframes rather than the browser's `-ua-view-transition-*`
   internals, which aren't ours to depend on. */
@keyframes tb-vt-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes tb-vt-fade-in  { from { opacity: 0; } to { opacity: 1; } }
html.tb-vt-theme::view-transition-old(root) { animation: tb-vt-fade-out 280ms cubic-bezier(.2,.7,.3,1) both; }
html.tb-vt-theme::view-transition-new(root) { animation: tb-vt-fade-in 280ms cubic-bezier(.2,.7,.3,1) both; }

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root),
  html.tb-vt-theme::view-transition-old(root), html.tb-vt-theme::view-transition-new(root) { animation: none !important; }
}

/* --- Motion (DESIGN.md §9): click-triggered only, reduced-motion safe --- */

/* First paint only, and only on a COLD one. Route changes are handled by the view
   transitions above — this element is deliberately NOT keyed on the pathname (it
   used to be), because replaying it on every navigation would double up with the
   transition. It covers the case a view transition can't: arriving fresh, where
   there's no outgoing page to snapshot.

   `data-tb-warm` (set before first paint by warmStart.js) means the app was
   already open in this tab — a refresh, not an arrival. Then this doesn't run at
   all. The reason is the header band: it lives INSIDE this frame, so it rides
   this animation and re-flows on every hard refresh even though you never left,
   and a descendant can't opt out of an ancestor's animation — the only lever is
   whether the frame animates. Nothing is lost on a warm start: the cards still
   flow themselves in via .tb-flow as their data lands, which is the part that
   actually just arrived. */
@keyframes tb-page-enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.tb-page { animation: tb-page-enter 220ms cubic-bezier(.2,.7,.3,1); }
html[data-tb-warm] .tb-page { animation: none; }
@media (prefers-reduced-motion: reduce) { .tb-page { animation: none !important; } }

/* Card flow (DESIGN.md §9 "content fades + slides up on route/section change").
   .tb-page only animates the page frame — which on a hard refresh means it flows
   in the "Loading…" placeholder, and then the real cards POP in when the query
   resolves. These classes animate the CARDS themselves, so they flow whenever
   they mount: on navigation, and on async data arriving.

   .tb-flow       — one card.
   .tb-flow-group — a container whose direct children are cards; they stagger.

   `backwards` fill-mode matters: without it a delayed card paints at full opacity
   for a frame before animating (a visible flash). Stagger is capped at 200ms from
   the 5th card on, so a long page still settles in ~420ms and never feels sluggish.

   It must be `backwards`, NOT `forwards`/`both`. A fill that persists after the
   animation ends keeps the animation permanently "in effect", and since this one
   animates transform+opacity the card then creates a STACKING CONTEXT forever —
   which traps any popover inside it (z-index:50) below later cards on the page.
   `forwards` also buys nothing: the `to` state is identical to a card's natural
   resting state, so dropping the fill changes nothing visually. (.tb-page sets no
   fill at all, which is why it never had this problem.) */
@keyframes tb-flow-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.tb-flow,
.tb-flow-group > * { animation: tb-flow-in 220ms cubic-bezier(.2,.7,.3,1) backwards; }
.tb-flow-group > *:nth-child(2) { animation-delay: 40ms; }
.tb-flow-group > *:nth-child(3) { animation-delay: 80ms; }
.tb-flow-group > *:nth-child(4) { animation-delay: 120ms; }
.tb-flow-group > *:nth-child(5) { animation-delay: 160ms; }
.tb-flow-group > *:nth-child(n+6) { animation-delay: 200ms; }
@media (prefers-reduced-motion: reduce) {
  .tb-flow, .tb-flow-group > * { animation: none !important; }
}
.tb-press { transition: transform 120ms ease; }
.tb-press:active { transform: scale(.96); }

/* Spinner for in-flight buttons (loader icons) */
@keyframes tb-spin { to { transform: rotate(360deg); } }
.tb-spin { animation: tb-spin .8s linear infinite; }
@media (prefers-reduced-motion: reduce) { .tb-spin { animation-duration: 2s; } }

/* Percy chat: messages ease in (never pop), the thinking logo fades+grows in,
   and the response avatar settles from the animated state. Matches §9 motion. */
@keyframes tb-percy-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@keyframes tb-percy-think-in { from { opacity: 0; transform: scale(.7); } to { opacity: 1; transform: none; } }
@keyframes tb-percy-avatar-in { from { opacity: 0; transform: scale(1.6); } to { opacity: 1; transform: none; } }
.tb-percy-in { animation: tb-percy-in .24s cubic-bezier(.2,.7,.3,1); }
.tb-percy-think { animation: tb-percy-think-in .28s cubic-bezier(.2,.7,.3,1); }
.tb-percy-avatar-in { animation: tb-percy-avatar-in .3s cubic-bezier(.2,.7,.3,1); }
@media (prefers-reduced-motion: reduce) {
  .tb-percy-in, .tb-percy-think, .tb-percy-avatar-in { animation: none !important; }
}
/* Audio-reactive Percy glow. --percy-amp (0..1) is set per-frame by usePercyGlow
   from the live TTS amplitude; the whole steel glow scales with it — no ring, no
   bars. At rest (amp 0) it's a faint steel presence ring; peaks bloom it out. */
.tb-percy-live {
  --percy-amp: 0;
  display: inline-flex; border-radius: 50%; flex: none; line-height: 0;
  box-shadow:
    0 0 0 1px rgba(111, 139, 171, calc(0.28 + var(--percy-amp) * 0.5)),
    0 0 calc(3px + var(--percy-amp) * 20px) calc(var(--percy-amp) * 5px) rgba(111, 139, 171, calc(0.10 + var(--percy-amp) * 0.5));
}
.tb-percy-live img { border-radius: 50%; display: block; }
/* ==========================================================================
   Percy chat surface — SINGLE SOURCE OF TRUTH for Percy's look, shared by the
   in-app PercyPanel (frontend) AND the marketing demo (marketing/). Each host
   supplies its own data/behaviour; these classes define the chrome. Every
   colour is a --tb-* token, so both themes and both surfaces stay in lockstep:
   change it here and the app + the website both move. (memory:
   marketing-inherits-shared-ui — never re-hand-roll this in marketing.)
   ========================================================================== */
.tb-percy-surface         { display: flex; flex-direction: column; font-family: 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.tb-percy-surface--float  { background: transparent; }                /* desktop overlay */
.tb-percy-surface--framed { background: var(--tb-page-bg); }          /* pinned sidebar / mobile */

/* Slim strip carried by framed modes (unpin / close live here). */
.tb-percy-strip      { display: flex; align-items: center; gap: 8px; padding: 10px 13px; border-bottom: 1px solid var(--tb-line-lt); flex-shrink: 0; }
.tb-percy-strip-name { font-size: 11px; font-weight: 700; letter-spacing: .06em; color: var(--tb-muted); }

/* Message column — newest at the bottom, older fading up into the top edge. */
.tb-percy-thread { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding: 20px 6px 12px;
  -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 54px); mask-image: linear-gradient(180deg, transparent 0, #000 54px); }

/* Speech bubbles — a colour-matched triangle tail points back at the speaker
   (Percy on the left, you on the right). */
.tb-percy-bubble { position: relative; align-self: flex-start; max-width: 85%;
  background: var(--tb-surface-alt); color: var(--tb-ink);
  padding: 8px 11px; border-radius: 10px; font-size: 12.5px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.tb-percy-bubble::before { content: ''; position: absolute; left: -5px; bottom: 9px; width: 11px; height: 11px;
  background: var(--tb-surface-alt); transform: rotate(45deg); border-radius: 2px; z-index: -1; }
.tb-percy-bubble--markdown { white-space: normal; }
.tb-percy-bubble--user { align-self: flex-end; background: var(--tb-navy); color: var(--tb-cream); }
.tb-percy-bubble--user::before { left: auto; right: -5px; background: var(--tb-navy); }
/* A bubble carrying a chart / PDF inset steps darker (navy) so the inset reads
   as a distinct object floating inside Percy's speech. */
.tb-percy-bubble--rich, .tb-percy-bubble--rich::before { background: var(--tb-navy); }

/* Composer bubble — mic sits inside, no send button (Enter sends). Slim: one line
   tall, growing with content, so it reads as a compact ask field (not a chunky
   textarea). The `min-height: 0` overrides the global `textarea { min-height }`. */
.tb-percy-input { display: flex; align-items: flex-end; gap: 8px; background: var(--tb-surface-up); border-radius: 12px; padding: 6px 12px;
  box-shadow: 0 6px 22px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.04); }
.tb-percy-input textarea, .tb-percy-input .tb-percy-input-field { flex: 1; min-width: 0; min-height: 0; height: auto; background: none; border: none; outline: none; resize: none;
  max-height: 120px; padding: 0; color: var(--tb-ink); font-family: inherit; font-size: 12.5px; line-height: 1.4; }
.tb-percy-input textarea::placeholder, .tb-percy-input .tb-percy-input-field::placeholder { color: var(--tb-muted); }

/* Percy companion — the octopus stands BESIDE one speech bubble that holds the
   greeting + the conversation and grows downward (the thread scrolls inside), with
   a slim ask line at the bottom. SINGLE SOURCE, shared by the app's standard float
   (PercyPanel soloBox) and the marketing hero, so a visual change flows to both.
   The guide keeps its own inline variant (heavier companion shadow, no growth cap). */
.tb-percy-companion { flex-shrink: 0; display: flex; align-items: flex-start; gap: 2px; padding: 12px 10px 6px; }
.tb-percy-companion-octo { flex: none; }
.tb-percy-companion-octo img { display: block; width: 72px; height: 72px; object-fit: contain; filter: drop-shadow(0 5px 12px rgba(0,0,0,.55)); }
/* The bubble = a shared base + composable modifiers, so every surface renders the
   SAME box: the guide/dock/mobile companion (base + --heavy), the standard float
   (base + --soft + --scroll), and the marketing hero (base + --heavy + --scroll).
     · base    — surface, border, radius, padding, position + the ::before beak
     · --heavy — the guide's 3-layer companion shadow (the reference look)
     · --soft  — the float's single soft functional shadow (DESIGN §11)
     · --scroll — grows with content then scrolls: the thread lives INSIDE and the
                  box caps, instead of the guide's grow-only bubble with the thread
                  rendered below it. */
.tb-percy-box { position: relative; flex: 1; min-width: 0; margin-left: -2px; margin-top: 16px;
  background: var(--tb-surface-up); border: 1px solid var(--tb-line-on-navy); border-radius: 16px; padding: 13px 16px 14px; }
.tb-percy-box::before { content: ''; position: absolute; left: -7px; top: 24px; width: 13px; height: 13px;
  background: var(--tb-surface-up); border-left: 1px solid var(--tb-line-on-navy); border-bottom: 1px solid var(--tb-line-on-navy); transform: rotate(45deg); }
.tb-percy-box--heavy { box-shadow: 0 24px 54px -14px rgba(0,0,0,0.72), 0 2px 10px rgba(0,0,0,0.35), 0 0 34px -6px rgba(111,139,171,0.28); }
.tb-percy-box--soft { box-shadow: 0 16px 40px -14px rgba(0,0,0,.30); }
.tb-percy-box--scroll { display: flex; flex-direction: column; max-height: min(520px, calc(100vh - 96px)); }
.tb-percy-box-name { font-size: 12.5px; font-weight: 700; color: var(--tb-gold); letter-spacing: .01em; }

/* Rich cards Percy returns inside a bubble — shared so the app and the marketing
   demo stay in parity. Chart mirrors PercyChart.jsx; PDF mirrors the
   generate_invoice_pdf download card. Series colours are set inline on the SVG
   (they vary per series); everything else is token-driven here. */
.tb-percy-chart { background: var(--tb-white); border: 1px solid var(--tb-line); border-radius: 10px; padding: 10px 12px; margin-top: 6px; }
.tb-percy-chart-title { font-size: 10.5px; font-weight: 700; letter-spacing: .08em; color: var(--tb-ink); margin-bottom: 8px; text-transform: uppercase; }
.tb-percy-chart svg { display: block; width: 100%; height: 150px; }
.tb-percy-chart-x { display: flex; justify-content: space-between; font-size: 9px; color: var(--tb-muted); margin-top: 5px; padding: 0 4px; }
.tb-percy-chart-legend { display: flex; flex-wrap: wrap; gap: 12px; font-size: 10px; color: var(--tb-gray); margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--tb-line-td); }
.tb-percy-chart-legend span { display: inline-flex; align-items: center; gap: 5px; }
.tb-percy-chart-legend i { width: 10px; height: 3px; border-radius: 1px; display: inline-block; }

.tb-percy-pdf { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; margin-top: 6px; max-width: 100%;
  background: var(--tb-gold-tint); border: 1px solid var(--tb-gold-wash); border-radius: 10px; padding: 9px 12px; color: var(--tb-gold-dk); font-size: 12px; transition: border-color .15s; }
.tb-percy-pdf:hover { border-color: var(--tb-gold); }
.tb-percy-pdf-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tb-percy-pdf-dl { margin-left: auto; padding-left: 6px; font-size: 10.5px; color: var(--tb-muted); flex-shrink: 0; }

/* --- Percy Guide (PERCY-GUIDE-PLAN.md §4.2, §6) ---
   Percy SLIDES between the controls it points at — it never fades out and back
   in, and it never drifts on its own. The transition is on `transform` only, so
   the move is one GPU-composited property; the beak slides along the panel edge
   with it. This is interaction-triggered (a step advanced), which is what keeps
   it inside §9's "motion is a response to interaction, never ambient". */
.tb-guide-anchored { transition: transform .5s cubic-bezier(.2,.7,.3,1); }
.tb-guide-beak { transition: top .5s cubic-bezier(.2,.7,.3,1), left .5s cubic-bezier(.2,.7,.3,1); }

/* The spotlight: a one-shot pull of attention on step-enter. The scrim fades
   in, holds, and fades back out — it is NOT a persistent dim and NOT a loop
   (§9 bans both). The ring stays quietly for the rest of the step so the target
   is still obvious once the scrim clears. Percy sits above the scrim and is
   never dimmed; the scrim is click-through so the highlighted control stays
   usable while it's up. */
.tb-guide-scrim { transition: opacity 2.2s ease; pointer-events: none; }
.tb-guide-ring { transition: box-shadow .2s ease; }

@media (prefers-reduced-motion: reduce) {
  /* Snap, don't slide; and no dim pulse at all — the ring alone marks the spot. */
  .tb-guide-anchored, .tb-guide-beak, .tb-guide-ring { transition: none !important; }
  .tb-guide-scrim { transition: none !important; opacity: 0 !important; }
}

/* Themed scrollbar (opt-in via .tb-scroll) — replaces the browser default. */
.tb-scroll { scrollbar-width: thin; scrollbar-color: var(--tb-input) transparent; }
.tb-scroll::-webkit-scrollbar { width: 10px; height: 10px; }
.tb-scroll::-webkit-scrollbar-track { background: transparent; }
.tb-scroll::-webkit-scrollbar-thumb { background: var(--tb-input); border-radius: 6px; border: 2px solid transparent; background-clip: padding-box; }
.tb-scroll::-webkit-scrollbar-thumb:hover { background: var(--tb-muted); border-radius: 6px; border: 2px solid transparent; background-clip: padding-box; }

/* Resizable table columns (shared/ui/useResizableColumns): an invisible grab
   strip on each column boundary that reveals a gold hairline on hover/drag. */
.tb-col-resize { background: transparent; transition: background-color .12s ease; }
.tb-col-resize::after {
  content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px;
  transform: translateX(-50%); background: transparent; transition: background-color .12s ease;
}
.tb-col-resize:hover::after, .tb-col-resize:active::after { background: var(--tb-steel); }
@media (prefers-reduced-motion: reduce) { .tb-col-resize, .tb-col-resize::after { transition: none !important; } }

/* Sidebar nav items (DESIGN.md §10): no box — gold bar + gold text when active */
.tb-nav { position: relative; display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 12px; border: none; background: none; text-align: left; cursor: pointer; font-family: var(--font-sans); font-size: 13.5px; font-weight: 600; color: var(--tb-nav-idle); transition: color .18s ease, padding-left .18s ease; }
.tb-nav:hover { color: var(--tb-cream); padding-left: 16px; }
.tb-nav.on { color: var(--tb-steel); }
.tb-nav .tb-bar { position: absolute; left: 0; top: 6px; bottom: 6px; width: 4px; background: var(--tb-steel); border-radius: 0 3px 3px 0; transform: scaleY(0); transform-origin: center; transition: transform .2s ease; }
.tb-nav.on .tb-bar { transform: scaleY(1); }
/* The same nav item rendered on the PAGE CANVAS instead of the navy rail (the
   Preferences section rail). .tb-nav's colours are on-navy roles: against
   pageBg in light theme idle --tb-nav-idle is 2.4:1, and the --tb-cream hover
   IS pageBg — the label disappeared under the cursor. Re-role both to canvas
   text tokens (gray 5.6:1 idle, ink on hover); active stays steel, restated
   after :hover so hovering the active item doesn't drop its state colour. */
.tb-nav-canvas .tb-nav { color: var(--tb-gray); }
.tb-nav-canvas .tb-nav:hover { color: var(--tb-ink); }
.tb-nav-canvas .tb-nav.on, .tb-nav-canvas .tb-nav.on:hover { color: var(--tb-steel); }
@media (prefers-reduced-motion: reduce) { .tb-nav, .tb-nav .tb-bar { transition: none !important; } }

@media (prefers-reduced-motion: reduce) {
  .tb-page, .tb-press { animation: none !important; transition: none !important; }
}

/* Print/PDF output must always render as a normal light printed page,
   regardless of the app's current theme — an invoice mailed to a client
   should never come out inverted because the sender has dark mode on.
   Re-declaring the light palette's literal values here means every
   var(--tb-*) reference inside this subtree (InvoiceHeader, SummaryBlock's
   print variants, TimeEntriesTable/CalendarGrid's printMode branches)
   resolves to light values regardless of :root's current theme — no
   component changes needed. Keep in sync with LIGHT in shared/ui/tokens.js. */
.tb-print-scope {
  --tb-navy: #1B2A45;
  --tb-navy-deep: #16233B;
  --tb-gold: #B8934F;
  --tb-gold-dk: #9C7830;
  --tb-steel: #4E6B88;
  --tb-steel-dk: #3C5670;
  --tb-cream: #F8F7F4;
  --tb-page-bg: #F8F7F4;
  --tb-ink: #1C1E22;
  --tb-gray: #5B6572;
  --tb-muted: #7C828E;
  --tb-faint: #C7CBD1;
  --tb-line: #E3E1DB;
  --tb-line-lt: #EDEBE5;
  --tb-line-td: #F2F0EA;
  --tb-input: #D9D6CE;
  --tb-white: #FFFFFF;
  --tb-coral: #9B4238;
  --tb-green: #3F6B4A;
  --tb-blue: #2B4C7E;
  --tb-control-fill: #F1EFEA;
  --tb-surface-alt: #FAFAF8;
  --tb-gold-tint: #FBF7EF;
  --tb-gold-wash: #EFE6D6;
  --tb-bg-error: #F5E7E5;
  --tb-border-error: #E8CFC9;
  --tb-bg-neutral: #EDEEF0;
  --tb-bg-info: #E7EDF5;
  --tb-bg-success: #E8F0E9;
  --tb-skeleton: #F0EEE8;
  --tb-scrim: rgba(20,22,26,0.35);
  --tb-on-navy-muted: #AEB6C2;
  --tb-nav-idle: #9AA3B2;
  --tb-line-on-navy: rgba(255,255,255,0.16);
  --tb-gold-faint: rgba(184,147,79,0.35);
  --tb-steel-faint: rgba(78,107,136,0.35);
  --tb-focus-ring: 0 0 0 2px rgba(78,107,136,.45);
}

@page { size: letter; margin: 0.5in; }

@media print {
  .no-print { display: none !important; }

  /* body's cream background is otherwise printed (printBackground: true),
     bleeding through any page area past the end of the content. */
  html, body { background: #fff !important; }

  /* --- Inline print-preview modal path (in-app "Print") --- */
  .print-modal-backdrop {
    position: static !important;
    background: #fff !important;
    padding: 0 !important;
    display: block !important;
    overflow: visible !important;
  }
  .print-surface {
    box-shadow: none !important;
    border: none !important;
    margin: 0 !important;
    max-width: none !important;
    border-radius: 0 !important;
  }
  /* When the print-preview modal is open, only the print surface prints.
     The modal renders in a body-level portal (sibling of #root). */
  body.modal-printing #root { display: none !important; }

  /* --- Puppeteer PDF path (/invoice/:id?print=1) --- */
  thead            { display: table-header-group; }
  tr.avoid-break,
  .avoid-break     { page-break-inside: avoid; }
  [data-pdf-section] { break-inside: avoid; }
}

/* --- Mobile layout (shared by the tenant app + operator console) ---
   At <=820px the SidebarShell renders a fixed top bar + off-canvas drawer, so
   <main> goes full-width and needs top padding to clear the 54px bar. The apps
   set <main> padding inline, so these overrides use !important. Keep the 820px
   breakpoint in sync with MOBILE_BREAKPOINT in shared/ui/useIsMobile.js. */
@media (max-width: 820px) {
  main { padding: 68px 16px 56px !important; max-width: none !important; }
}
@media (max-width: 480px) {
  main { padding: 66px 12px 52px !important; }
}

/* Responsive content grids. The app lays out multi-column form/page grids with
   inline `grid-template-columns`, which can't hold a media query — so tagged
   containers collapse here, and !important beats the inline value. Calendars and
   compact per-row grids are deliberately left untagged so they keep their shape.
   Print (Puppeteer renders at ~816px) never matches, so invoices stay multi-col. */
@media (max-width: 640px) {
  .tb-grid-stack { grid-template-columns: 1fr !important; }
  .tb-stat-row { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (max-width: 420px) {
  .tb-stat-row { grid-template-columns: 1fr !important; }
}

/* Section tab strip (Preferences): a wrapped pill row on desktop, but a single
   swipeable row on mobile — the modern native pattern. The last visible pill is
   cut off (a "peek") to signal there's more to scroll; the scrollbar is hidden
   and the active tab is auto-centered in JS. Mobile-only, so desktop is untouched. */
.tb-tabs-strip { display: flex; flex-wrap: wrap; max-width: 100%; }
@media (max-width: 640px) {
  .tb-tabs-strip {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;             /* Firefox */
    -webkit-overflow-scrolling: touch; /* momentum scroll on iOS */
    scroll-behavior: smooth;
  }
  .tb-tabs-strip::-webkit-scrollbar { display: none; } /* WebKit */
  .tb-tabs-strip > button { flex-shrink: 0; }
}

/* Time entries: a dense 8-column table on desktop; on mobile each row becomes a
   compact card. The row is a flex-wrap container so cells can be re-ordered:
   row 1 is the day (title) with the billable dot + edit/delete pinned right; then
   Project and Notes; then Start + End share one "Start / End  t / t" line; then
   Hours. Labels are injected per column via nth-child, so the JSX rows are
   untouched (just `.tb-entries` on the table). */
@media (max-width: 640px) {
  .tb-entries, .tb-entries tbody { display: block; width: 100%; }
  .tb-entries thead, .tb-entries colgroup { display: none; }
  .tb-entries tr {
    display: flex; flex-wrap: wrap; align-items: center; column-gap: 10px;
    border: 1px solid var(--tb-line); border-radius: 10px; margin: 0 0 10px; padding: 8px 12px; background: var(--tb-white);
  }
  .tb-entries td {
    flex: 0 0 100%;                    /* each field on its own line by default */
    display: flex !important; align-items: baseline; justify-content: space-between; gap: 12px;
    width: auto !important; max-width: none !important; border: 0 !important;
    padding: 3px 0 !important; text-align: right !important; white-space: normal !important;
  }
  .tb-entries td::before { color: var(--tb-muted); font-weight: 600; text-align: left; white-space: nowrap; flex-shrink: 0; }

  /* Row 1 — title: day grows and pushes the billable dot + edit/delete to the right. */
  .tb-entries td:nth-child(1) { flex: 1 1 auto; order: 0; justify-content: flex-start; font-weight: 700; color: var(--tb-ink); font-size: 13px; }
  .tb-entries td:nth-child(7) { flex: 0 0 auto; order: 1; }   /* billable status dot */
  .tb-entries td:nth-child(8) { flex: 0 0 auto; order: 2; }   /* edit / delete actions */
  .tb-entries td:nth-child(1), .tb-entries td:nth-child(7), .tb-entries td:nth-child(8) {
    padding-bottom: 7px !important; margin-bottom: 5px; border-bottom: 1px solid var(--tb-line-td) !important;
  }
  .tb-entries td:nth-child(1)::before, .tb-entries td:nth-child(7)::before, .tb-entries td:nth-child(8)::before { content: none !important; }

  /* Body lines. */
  .tb-entries td:nth-child(2) { order: 3; } .tb-entries td:nth-child(2)::before { content: 'Project'; }
  .tb-entries td:nth-child(3) { order: 4; } .tb-entries td:nth-child(3)::before { content: 'Notes'; }
  .tb-entries td:nth-child(6) { order: 7; } .tb-entries td:nth-child(6)::before { content: 'Hours'; }

  /* Start + End on one line with the times right-aligned:
     "Start / End                 8:45 AM / 12:00 PM". The Start cell grows and
     uses space-between so its label sits left and the start time is pushed right,
     next to the End cell (which follows immediately). */
  .tb-entries td:nth-child(4) { flex: 1 1 auto; order: 5; justify-content: space-between; gap: 8px; }
  .tb-entries td:nth-child(4)::before { content: 'Start / End'; }
  .tb-entries td:nth-child(5) { flex: 0 0 auto; order: 6; justify-content: flex-start; gap: 6px; }
  .tb-entries td:nth-child(5)::before { content: '/'; color: var(--tb-muted); font-weight: 700; }

  .tb-entries td:empty { display: none !important; }
  /* The inline edit form / totals span all columns — full-width line, no label. */
  .tb-entries td[colspan] { flex: 0 0 100%; order: 8; display: block !important; text-align: left !important; padding: 8px 0 !important; }
  .tb-entries td[colspan]::before { content: none !important; }
  .tb-entries tfoot td { text-align: right !important; }
}

/* A two-column flex row that stacks to one column on mobile — e.g. the invoice
   canvas (entries / calendar / notes beside a fixed-width sticky summary), which
   otherwise crushes the left column to ~100px on a phone. The summary's inline
   `width:250` is overridden to full width. Print renders at ~816px so this never
   matches — invoices stay two-column in the PDF. */
@media (max-width: 640px) {
  .tb-stack-mobile { flex-direction: column !important; }
  .tb-stack-mobile > * { width: 100% !important; }
}

/* Percy chat panel goes full-screen on mobile (docked or floating both fill the
   viewport). The anchored guide card is excluded — it stays a small contextual
   popover. Resize handles are hidden (nothing to resize full-screen). */
@media (max-width: 640px) {
  .tb-percy-panel {
    inset: 0 !important;
    width: 100vw !important; max-width: 100vw !important;
    height: 100dvh !important; max-height: 100dvh !important;
    border: 0 !important; border-radius: 0 !important; box-shadow: none !important;
  }
  .tb-percy-panel [style*="resize"] { display: none !important; }
}

/* Generic "table → cards on mobile": any wide table tagged `.tb-cardify` (with the
   useCardLabels hook copying its headers onto the cells as data-label) collapses
   to a stacked card per row ≤640px. First cell is the card title; the rest become
   labeled key/value lines. Desktop renders the normal table. */
@media (max-width: 640px) {
  .tb-cardify, .tb-cardify tbody { display: block; width: 100%; }
  .tb-cardify thead, .tb-cardify colgroup { display: none; }
  .tb-cardify tr { display: block; border: 1px solid var(--tb-line); border-radius: 10px; margin: 0 0 10px; padding: 6px 12px; background: var(--tb-white); }
  .tb-cardify td {
    display: flex !important; align-items: baseline; justify-content: space-between; gap: 12px;
    width: auto !important; max-width: none !important; border: 0 !important;
    padding: 4px 0 !important; text-align: right !important; white-space: normal !important;
  }
  .tb-cardify td[data-label]::before { content: attr(data-label); color: var(--tb-muted); font-weight: 600; text-align: left; white-space: nowrap; flex-shrink: 0; }
  .tb-cardify td:first-child { justify-content: flex-start; font-weight: 700; color: var(--tb-ink); border-bottom: 1px solid var(--tb-line-td) !important; padding-bottom: 6px !important; margin-bottom: 2px; }
  .tb-cardify td:first-child::before { content: none !important; }
  .tb-cardify td:empty { display: none !important; }
  .tb-cardify td[colspan] { display: block !important; text-align: left !important; }
  .tb-cardify td[colspan]::before { content: none !important; }
}
