/* ============================================================
   TameLink design system — hand-authored, no build step.
   Order: TOKENS → UTILITIES → COMPONENTS. Do not reorder.

   The Tailwind CDN has been removed (Phase 43 final sweep). Utility
   classes below reuse Tailwind's names and exact v3.4.17 values.
   The global reset is css/reset.css, linked before this file in
   every layout that loads it.

   BRAND COLOURS: --brand-50..900 are emitted per-request as
   space-separated RGB channels by BrandSetting::cssVariables()
   (e.g. --brand-600: 22 101 52). Always consume them as
   rgb(var(--brand-N)) or rgb(var(--brand-N) / 0.2).
   HOVERS: use --brand-hover / -hover-deep (fills) and --brand-hover-text /
   -hover-strong (text), not a raw shade: on a black brand every dark shade
   is black, so these go lighter instead (BrandSetting::hoverShades, TameLink-9hag).
   ============================================================ */

/* ---------- TOKENS ---------- */
:root {
  /* Grays — Tailwind v3.4.17 values, verbatim */
  --gray-50:#f9fafb; --gray-100:#f3f4f6; --gray-200:#e5e7eb;
  --gray-300:#d1d5db; --gray-400:#9ca3af; --gray-500:#6b7280;
  --gray-600:#4b5563; --gray-700:#374151; --gray-800:#1f2937;
  --gray-900:#111827;

  /* Semantic colour ladders (only the shades in use) */
  --red-50:#fef2f2;   --red-100:#fee2e2;   --red-200:#fecaca;
  --red-300:#fca5a5;  --red-400:#f87171;   --red-500:#ef4444;
  --red-600:#dc2626;  --red-700:#b91c1c;   --red-800:#991b1b;
  --green-50:#f0fdf4; --green-100:#dcfce7; --green-200:#bbf7d0;
  --green-300:#86efac;--green-500:#22c55e; /* Batch 7b — roles legend dot */
  --green-600:#16a34a; --green-700:#15803d;
  --green-800:#166534;
  --green-900:#14532d;
  --yellow-50:#fefce8;--yellow-100:#fef9c3;--yellow-200:#fef08a;
  --yellow-300:#fde047;--yellow-400:#facc15;--yellow-500:#eab308;
  --yellow-600:#ca8a04;  /* Batch 6a — schedules.php's partial-time-off warning */
  --yellow-700:#a16207;--yellow-800:#854d0e;--yellow-900:#713f12;
  --blue-50:#eff6ff;  --blue-100:#dbeafe;  --blue-200:#bfdbfe;
  --blue-500:#3b82f6; /* Batch 7b — roles legend dot */
  --blue-600:#2563eb; --blue-700:#1d4ed8; --blue-800:#1e40af; --blue-900:#1e3a8a;
  --sky-100:#e0f2fe;    --sky-700:#0369a1;
  --teal-50:#f0fdfa;    --teal-100:#ccfbf1;   --teal-200:#99f6e4;
  --teal-600:#0d9488;   --teal-700:#0f766e;   --teal-800:#115e59;
  --cyan-100:#cffafe;   --cyan-700:#0e7490;
  --indigo-100:#e0e7ff; --indigo-700:#4338ca;
  --violet-100:#ede9fe; --violet-700:#6d28d9;
  --pink-100:#fce7f3;   --pink-700:#be185d;
  --purple-50:#faf5ff;  --purple-100:#f3e8ff; --purple-200:#e9d5ff;
  --purple-300:#d8b4fe; --purple-500:#a855f7; --purple-700:#7e22ce;
  --orange-50:#fff7ed;  --orange-100:#ffedd5; --orange-200:#fed7aa;
  --orange-300:#fdba74; --orange-400:#fb923c; --orange-500:#f97316;
  --orange-600:#ea580c; --orange-700:#c2410c; --orange-800:#9a3412;
  /* Amber — OD-N (Nix, 2026-08-17). settings/tax.php's ValorPay surcharge
     disclosure is a WARM caution note and stays warm; the existing
     .alert-warning is lemon (yellow-50/200/700) and recolouring a billing
     disclosure to it was rejected. Three shades, one consumer. */
  --amber-50:#fffbeb;   --amber-200:#fde68a;
  --amber-600:#d97706;  --amber-700:#b45309;  /* t1o6: amber text on white (5.0:1) */
  --amber-800:#92400e;

  /* Shape */
  --radius-xs:.25rem;   /* = rounded — OD-A, calendar blocks keep 4px corners */
  --radius-sm:.375rem;  /* = rounded-md  */
  --radius:.5rem;       /* = rounded-lg  */
  --radius-lg:.75rem;   /* = rounded-xl  */

  /* Elevation — Tailwind v3.4.17 values, verbatim */
  --shadow-sm:0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow:0 1px 3px 0 rgb(0 0 0 / 0.1),0 1px 2px -1px rgb(0 0 0 / 0.1);
  --shadow-md:0 4px 6px -1px rgb(0 0 0 / 0.1),0 2px 4px -2px rgb(0 0 0 / 0.1);
  --shadow-lg:0 10px 15px -3px rgb(0 0 0 / 0.1),0 4px 6px -4px rgb(0 0 0 / 0.1);
  --shadow-xl:0 20px 25px -5px rgb(0 0 0 / 0.1),0 8px 10px -6px rgb(0 0 0 / 0.1);

  /* THE focus ring — one treatment app-wide (unifies the 3-way drift,
     inventory §2a/§hover-focus). 2px brand ring, no border swap.
     A 2px white gap sits between the element and the ring, so focus shows
     on a brand-coloured (even black) button too (TameLink-9hag F70).
     --ring-flat is the old gap-less ring, kept for selected states. */
  --ring:0 0 0 2px #fff,0 0 0 4px rgb(var(--brand-500));
  --ring-flat:0 0 0 2px rgb(var(--brand-500));
}

/* Alpine flash-of-unstyled-content guard — global infra, kept from
   views/layouts/app.php's former inline rule. */
[x-cloak]{display:none!important}

/* ---------- UTILITIES (Tailwind-name whitelist) ----------
   Values transcribed verbatim from Tailwind v3.4.17. A class that is
   not in this section and not a component class below MUST NOT appear
   in converted markup. */

/* Display */
.block{display:block}.inline-block{display:inline-block}.inline{display:inline}
.flex{display:flex}.inline-flex{display:inline-flex}
.grid{display:grid}.hidden{display:none}

/* Flex / grid */
.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}
.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}
.items-start{align-items:flex-start}.items-center{align-items:center}
.justify-center{justify-content:center}
.justify-between{justify-content:space-between}
.justify-end{justify-content:flex-end}
.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}
.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}   /* Batch 6b: profile city/state/zip row */
.col-span-2{grid-column:span 2/span 2}                /* Batch 6b: profile two-thirds column */
.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}
.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}

/* Spacing — padding */
.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}
.p-6{padding:1.5rem}.p-10{padding:2.5rem}.p-12{padding:3rem}
.px-2{padding-left:.5rem;padding-right:.5rem}
.px-3{padding-left:.75rem;padding-right:.75rem}
.px-4{padding-left:1rem;padding-right:1rem}
.px-6{padding-left:1.5rem;padding-right:1.5rem}
.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}
.py-1{padding-top:.25rem;padding-bottom:.25rem}
.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}
.py-2{padding-top:.5rem;padding-bottom:.5rem}
.py-3{padding-top:.75rem;padding-bottom:.75rem}
.pt-2{padding-top:.5rem}

/* Spacing — margin */
.mx-auto{margin-left:auto;margin-right:auto}
.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}
.mt-6{margin-top:1.5rem}.mb-1{margin-bottom:.25rem}
.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}
.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}

/* Stack spacing */
.space-y-1>:not([hidden])~:not([hidden]){margin-top:.25rem}
.space-y-2>:not([hidden])~:not([hidden]){margin-top:.5rem}
.space-y-3>:not([hidden])~:not([hidden]){margin-top:.75rem}
.space-y-4>:not([hidden])~:not([hidden]){margin-top:1rem}
.space-y-5>:not([hidden])~:not([hidden]){margin-top:1.25rem}
.space-y-6>:not([hidden])~:not([hidden]){margin-top:1.5rem}
.divide-y>:not([hidden])~:not([hidden]){border-top-width:1px;border-top-style:solid}
.divide-gray-100>:not([hidden])~:not([hidden]){border-color:var(--gray-100)}

/* Sizing */
.w-full{width:100%}.min-w-full{min-width:100%}
.max-w-lg{max-width:32rem}.max-w-xs{max-width:20rem}
.max-w-7xl{max-width:80rem}
.h-16{height:4rem}
.w-4{width:1rem}.h-4{height:1rem}.w-5{width:1.25rem}.h-5{height:1.25rem}
.w-6{width:1.5rem}.h-6{height:1.5rem}

/* Typography */
.text-xs{font-size:.75rem;line-height:1rem}
.text-sm{font-size:.875rem;line-height:1.25rem}
.text-base{font-size:1rem;line-height:1.5rem}
.text-lg{font-size:1.125rem;line-height:1.75rem}
.text-xl{font-size:1.25rem;line-height:1.75rem}
.text-2xl{font-size:1.5rem;line-height:2rem}
.text-3xl{font-size:1.875rem;line-height:2.25rem}
.font-medium{font-weight:500}.font-semibold{font-weight:600}
.font-bold{font-weight:700}
.text-left{text-align:left}.text-center{text-align:center}
.text-right{text-align:right}
.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}
.tracking-wide{letter-spacing:.025em}
.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.whitespace-nowrap{white-space:nowrap}
.tabular-nums{font-variant-numeric:tabular-nums}
.underline{text-decoration-line:underline}

/* Text colours (prose ladder only — component colours live in components) */
.text-white{color:#fff}
/* text-gray-400 uses gray-500: #9ca3af was 2.5:1 on white, under the 4.5:1 minimum (QA F74, Nix 2026-10-23) */
.text-gray-400{color:var(--gray-500)}.text-gray-500{color:var(--gray-500)}
.text-gray-600{color:var(--gray-600)}.text-gray-700{color:var(--gray-700)}
.text-gray-900{color:var(--gray-900)}
.text-red-600{color:var(--red-600)}
.text-red-700{color:var(--red-700)}
.text-brand-600{color:rgb(var(--brand-600))}
.text-brand-700{color:rgb(var(--brand-700))}

/* Background / border / shape */
.bg-white{background-color:#fff}.bg-gray-50{background-color:var(--gray-50)}
.border{border-width:1px;border-style:solid;border-color:var(--gray-200)}
.border-t{border-top-width:1px;border-top-style:solid}
.border-b{border-bottom-width:1px;border-bottom-style:solid}
.border-gray-100{border-color:var(--gray-100)}
.border-gray-200{border-color:var(--gray-200)}
.border-gray-300{border-color:var(--gray-300)}
.rounded{border-radius:var(--radius-xs)}.rounded-md{border-radius:var(--radius-sm)}
.rounded-lg{border-radius:var(--radius)}.rounded-xl{border-radius:var(--radius-lg)}
.rounded-full{border-radius:9999px}
.shadow-sm{box-shadow:var(--shadow-sm)}
.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-auto{overflow:auto}
/* A centred row that scrolls (the client and pet tab bars): plain centring
   pushes the first items off the left edge where scrolling cannot reach them
   on a phone. `safe center` centres only while everything fits; browsers
   without `safe` keep the flex-start line (TameLink-0ovg F71). */
.overflow-x-auto.justify-center{justify-content:flex-start;justify-content:safe center}

/* Position / stacking */
.relative{position:relative}.absolute{position:absolute}
.fixed{position:fixed}.inset-0{inset:0}
/* Text for screen readers only, e.g. a label on an icon or an empty table
   header (portal QA F-20). */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}

/* Interaction */
.cursor-pointer{cursor:pointer}.cursor-not-allowed{cursor:not-allowed}
.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}

/* ---- Batch 1 whitelist additions (2026-08-03) — Tailwind v3.4.17 values ---- */
.max-w-sm{max-width:24rem;white-space:normal}.max-w-2xl{max-width:42rem}.max-w-md{max-width:28rem}
.ml-4{margin-left:1rem}.text-blue-900{color:var(--blue-900)}
.mb-3{margin-bottom:.75rem}.mt-3{margin-top:.75rem}.mt-5{margin-top:1.25rem}
.mt-0\.5{margin-top:.125rem}.mb-0\.5{margin-bottom:.125rem}
.pt-3{padding-top:.75rem}.pb-2{padding-bottom:.5rem}
.pr-4{padding-right:1rem}.pl-4{padding-left:1rem}
.px-1{padding-left:.25rem;padding-right:.25rem}
.items-end{align-items:flex-end}
.font-normal{font-weight:400}.italic{font-style:italic}
.whitespace-pre-line{white-space:pre-line}
.text-gray-300{color:var(--gray-300)}.text-gray-800{color:var(--gray-800)}
.text-yellow-500{color:var(--yellow-500)}.text-yellow-700{color:var(--yellow-700)}
.text-yellow-800{color:var(--yellow-800)}
.w-3\.5{width:.875rem}.h-3\.5{height:.875rem}
/* ---- Batch 2 whitelist additions (2026-08-05) — Tailwind v3.4.17 values ---- */
.max-w-3xl{max-width:48rem}
.space-y-2\.5>:not([hidden])~:not([hidden]){margin-top:.625rem}
.space-y-8>:not([hidden])~:not([hidden]){margin-top:2rem}
.pt-1{padding-top:.25rem}
.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}
.ml-auto{margin-left:auto}
.gap-x-6{column-gap:1.5rem}.gap-y-1{row-gap:.25rem}
.w-10{width:2.5rem}.h-10{height:2.5rem}
.w-24{width:6rem}.h-24{height:6rem}
.h-full{height:100%}.min-w-0{min-width:0}
.object-cover{object-fit:cover}
.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}
.bg-gray-100{background-color:var(--gray-100)}
.bg-brand-50{background-color:rgb(var(--brand-50))}
.text-brand-300{color:rgb(var(--brand-300))}

/* ---- Batch 3 whitelist additions (2026-08-06) — Tailwind v3.4.17 values ---- */
.w-auto{width:auto}
.w-16{width:4rem}.w-28{width:7rem}
.min-w-48{min-width:12rem}
.leading-none{line-height:1}
.leading-relaxed{line-height:1.625}
.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}
.mr-4{margin-right:1rem}
.mt-1\.5{margin-top:.375rem}
.pb-5{padding-bottom:1.25rem}
.pl-3{padding-left:.75rem}
.pl-5{padding-left:1.25rem}
.py-4{padding-top:1rem;padding-bottom:1rem}
.list-disc{list-style-type:disc}
.list-inside{list-style-position:inside}
.space-y-0\.5>:not([hidden])~:not([hidden]){margin-top:.125rem}
.border-t-2{border-top-width:2px;border-top-style:solid}
.border-yellow-200{border-color:var(--yellow-200)}
.text-yellow-900{color:var(--yellow-900)}
/* text-green-600 uses green-700: #16a34a was 3.3:1 on white (QA F74) */
.text-green-600{color:var(--green-700)}
.text-green-700{color:var(--green-700)}
.text-green-800{color:var(--green-800)}
.text-green-900{color:var(--green-900)}
.text-orange-600{color:var(--orange-700)}   /* AA on white (t1o6) */
.text-amber-600{color:var(--amber-700)}   /* AA on white (t1o6) */

/* ---- Batch 4 whitelist additions (2026-08-11) — Tailwind v3.4.17 values ----
   NB the six space-y-* rules the Batch-4 plan listed here were ALREADY present
   (lines above, in Tailwind's real `>:not([hidden])~:not([hidden])` form). They
   were not re-added: the plan's drafted `>*+*` form is not equivalent — it also
   targets hidden elements — so duplicating them would have changed behaviour. */
.max-h-24{max-height:6rem}
.max-h-64{max-height:16rem}
.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}
.last\:border-b-0:last-child{border-bottom-width:0}
/* REQUIRED, not optional: services/create.php binds :class="{'rotate-180':open}"
   on the reference-panel chevron. Alpine writes that class at runtime, so it is
   invisible to any static scan of the markup and there is no CDN fallback once
   Tailwind is dropped — without this rule the chevron silently stops flipping.
   Same trap class as Batch 3's JS-injected .text-orange-600 / .mt-1\.5. */
.rotate-180{transform:rotate(180deg)}
/* Task 7 gaps found by the kept-token audit on services/create.php. */
.pt-4{padding-top:1rem}
.pb-1\.5{padding-bottom:.375rem}
.py-6{padding-top:1.5rem;padding-bottom:1.5rem}
.border-gray-50{border-color:var(--gray-50)}
/* .ref-panel-group carries only the 10px size (OD-C); the type badge above the
   reference list also needs the markup's tracking-wider. */
.tracking-wider{letter-spacing:.05em}

/* ---- Batch 5 whitelist additions (2026-08-12) — Tailwind v3.4.17 values ---- */
.max-w-xl{max-width:36rem}      /* invoices/remove.php page column */
.my-1{margin-top:.25rem;margin-bottom:.25rem}  /* the .menu divider rule */
.pt-5{padding-top:1.25rem}
.leading-snug{line-height:1.375}
.border-gray-900{border-color:var(--gray-900)}
.text-red-500{color:var(--red-600)}   /* AA on white (t1o6) */
.w-32{width:8rem}
.ml-7{margin-left:1.75rem}
/* billing/invoices.php wrote this as the arbitrary `min-w-[160px]`. 160px is
   exactly Tailwind's min-w-40 (10rem), so it is renamed to the scale utility
   rather than teaching app.css its first escaped-bracket selector — same call
   Batch 3 made when it added .min-w-48. Zero delta. */
.min-w-40{min-width:10rem}
.max-w-4xl{max-width:56rem}
.max-w-5xl{max-width:64rem}   /* Batch 6b: profile/index.php page column */
.ml-3{margin-left:.75rem}
.my-4{margin-top:1rem;margin-bottom:1rem}
/* checkout.php wrote this as the arbitrary `max-w-[12rem]`; 12rem is exactly
   Tailwind's max-w-48. Same rename call as .min-w-40 above. */
.max-w-48{max-width:12rem}
.w-8{width:2rem}.h-8{height:2rem}
.mb-8{margin-bottom:2rem}.mt-8{margin-top:2rem}
.mr-3{margin-right:.75rem}
.pb-4{padding-bottom:1rem}
.bg-green-100{background-color:var(--green-100)}
.text-blue-600{color:var(--blue-600)}
.opacity-25{opacity:.25}.opacity-75{opacity:.75}
.animate-spin{animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.opacity-50{opacity:.5}         /* the three "coming soon" panels, invoices/remove.php */

/* ---- Batch 6a whitelist additions (2026-08-13) — Tailwind v3.4.17 values ----
   On-scale renames (B6a-11): schedule.php wrote these as arbitrary values, but
   16rem and 3rem are exactly Tailwind's min-w-64 and min-h-12, so they become
   the scale utilities rather than app.css's first escaped-bracket selectors —
   the same call as .min-w-40 / .max-w-48 above. Zero delta. */
.min-w-64{min-width:16rem}
.min-h-12{min-height:3rem}
/* Off-scale sizes preserved EXACTLY per OD-B (Nix, 2026-08-13). The unit suffix
   marks these as literal values — scale-step names are unitless, so .text-9px
   can never be misread as a step number.
   .text-9px deliberately states NO line-height: both sites are the arbitrary
   `text-[9px] leading-none`, and an arbitrary size sets font-size only, leaving
   the inherited line-height (Batch 4's F1 rule — the opposite of the named-size
   rule that bit Batch 3's .text-link and Batch 5's .menu-item). Do NOT pair it
   with 1rem. */
.text-9px{font-size:9px}
.min-w-200px{min-width:200px}
/* Task 8 gaps found by the kept-token audit on schedule.php. `grid-cols-7` is
   the month grid (3 sites); `gap-px` is the 1px hairline between day cells that
   lets the wrapper's gray-100 show through as gridlines. */
.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}
.gap-px{gap:1px}
.gap-x-4{column-gap:1rem}
.gap-y-2{row-gap:.5rem}
.p-1\.5{padding:.375rem}
.select-none{user-select:none}
/* The read-view day tile's enabled border. This one stays a utility — unlike the
   EDIT view's row (see .sched-day below) it is chosen by a plain PHP ternary,
   not by Alpine, so there is no component for it to lose to on source order. */
.border-brand-100{border-color:rgb(var(--brand-100))}

/* ---- Batch 6b whitelist additions (2026-08-14) — Tailwind v3.4.17 values ----
   Added by the task that proved each one survives as a plain utility rather
   than being absorbed by a component (see the scope note in the Batch 6b
   vocabulary block near the end of this file).
   Task 3, my-schedule/index.php:
   - .text-brand-500 is the "·" today-marker dot beside the date in the grid
     header — a one-off inline colour with no component to belong to.
   - .py-8 is the time-off panel's empty state. Deliberately NOT mapped to
     .empty-state: that component is 2.5rem on all four sides, while this box
     is 2rem vertical / 1.5rem horizontal, so the component would be a real
     change to a state the baseline shots do not even cover (the panel has
     requests in them). Exact Tailwind value, zero delta.
   Task 5, dashboard/index.php:
   - .bg-orange-100 / .text-orange-700 are the small round count chip beside
     "Pending Requests". Deliberately NOT minted as a component: the chip is a
     single site app-wide, and every other part of it (inline-flex, h-5, w-5,
     rounded-full, text-xs, font-bold) is already a defined utility, so two
     colour utilities is the smaller and more reversible change. This is the
     first task where a colour token survived as a utility rather than being
     absorbed — the deferral recorded in Task 1's scope note. */
.text-brand-500{color:rgb(var(--brand-500))}
.py-8{padding-top:2rem;padding-bottom:2rem}
.bg-orange-100{background-color:var(--orange-100)}
.text-orange-700{color:var(--orange-700)}

/* ---- Batch 7a whitelist additions (2026-08-17) — Tailwind v3.4.17 values ----
   The eight core settings views. Notes on the three that are not routine:
   - .border-dashed is B7a-9. `border-dashed` was undefined, and .border already
     states `border-style:solid`, so settings/index.php's three "coming soon"
     placeholder tiles paint a SOLID edge under app.css alone. Defining it
     restores the dashed edge — a visible change back to the CDN's rendering,
     not damage. Same preflight family as Batch 6b's B6b-19.
   - .max-w-200px and .text-10px are B7a-12, both OD-B renames of arbitrary
     values in branding.php (`max-w-[200px]`, `text-[10px]`). Per Batch 4's F1
     an arbitrary text size INHERITS line-height rather than copying a named
     pair, so .text-10px states font-size only.
   - .flex-shrink-0 is Tailwind's v2 alias for .shrink-0, which this file
     already defines. Both names are live in the markup; keeping the alias is
     smaller and more reversible than rewriting five call sites. */
.border-dashed{border-style:dashed}
.flex-shrink-0{flex-shrink:0}
.gap-8{gap:2rem}.gap-10{gap:2.5rem}
.gap-x-3{column-gap:.75rem}
.space-y-10>:not([hidden])~:not([hidden]){margin-top:2.5rem}
.mx-1{margin-left:.25rem;margin-right:.25rem}
.p-0{padding:0}
.px-2\.5{padding-left:.625rem;padding-right:.625rem}
.resize-none{resize:none}
.shadow{box-shadow:var(--shadow)}
.w-9{width:2.25rem}.w-12{width:3rem}.w-64{width:16rem}
.inset-y-0{top:0;bottom:0}
.right-3{right:.75rem}
.tracking-widest{letter-spacing:.1em}
.text-purple-700{color:var(--purple-700)}
.bg-brand-700{background-color:rgb(var(--brand-700))}
.max-w-200px{max-width:200px}
.text-10px{font-size:10px}

/* ---- Batch 7b whitelist additions (2026-08-17) — Tailwind v3.4.17 values ----
   The twelve one-off admin views. Notes on the ones that are not routine:
   - .sticky/.bottom-0/.shadow-lg exist only for .save-bar's page (roles). They
     are kept as utilities rather than folded into the component because the
     component states the geometry and these state the positioning contract —
     see the .save-bar note for why that split was chosen.
   - .bg-gray-50\/50 and .hover\:bg-blue-50\/30 are the phase's FIRST
     slash-opacity utilities (roles' zebra + hover permission rows). Tailwind
     compiles the modifier to an alpha channel on the same colour, so these are
     literal `rgb(… / α)` copies, NOT a new token pair. Recon R3.
   - .hover\:bg-gray-700 and .bg-gray-800 serve vaccines-edit's dark "Add"
     button, which rides on .btn (which sets neither background nor colour, so
     there is no component-beats-utility conflict). Deliberately NOT minted as
     a .btn-dark arm: one site, and Batch 10 can name it if a second appears.
   - .h-12 and .pt-6 are also on the Batch 10 layouts-sweep list (progress.md);
     defining them here does not close that item, which is about auditing
     views/layouts/*.php, not about these names existing.
   - .w-48 as a BARE utility (added at Task 13). Only `.field-input.w-48`
     existed, and the token audit reads that compound as defining `w-48`, so
     the gap was invisible to the tool: tax.php's three rate boxes sit in a
     plain `relative w-48` wrapper that anchors the "%" glyph, and without a
     bare rule the wrapper would go full-width the moment the CDN is dropped,
     throwing the glyph to the far right. Value is Tailwind's own 12rem, so it
     is zero delta, and it does not weaken `.field-input.w-48` — that compound
     is (0,2,0) and still beats .field-input's width:100%.
     billing/deposits.php:106 carries the same wrapper and will need it too. */
.sticky{position:sticky}
.bottom-0{bottom:0}
.self-center{align-self:center}
.self-start{align-self:start}
.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
.p-5{padding:1.25rem}
.pt-6{padding-top:1.5rem}.pb-6{padding-bottom:1.5rem}.pl-6{padding-left:1.5rem}
.pb-8{padding-bottom:2rem}
.py-12{padding-top:3rem;padding-bottom:3rem}
.h-12{height:3rem}.h-48{height:12rem}
.w-48{width:12rem}
.bg-gray-800{background-color:var(--gray-800)}
.hover\:bg-gray-50:hover{background-color:var(--gray-50)}
.hover\:bg-gray-100:hover{background-color:var(--gray-100)}
.hover\:bg-gray-200:hover{background-color:var(--gray-200)}
.hover\:bg-gray-700:hover{background-color:var(--gray-700)}
.hover\:no-underline:hover{text-decoration-line:none}
.bg-gray-50\/50{background-color:rgb(249 250 251 / 0.5)}
.hover\:bg-blue-50\/30:hover{background-color:rgb(239 246 255 / 0.3)}

/* ---- Batch 9 whitelist additions (2026-08-18) — Tailwind v3.4.17 values ----
   Portal booking/shop views. Everything else the token audit flagged this
   batch maps onto an existing component (.btn-primary, .field-input,
   .alert-success/.alert-error, .avatar/.avatar-fallback/.avatar-lg) and
   needs no new CSS — see the plan's Recon section (R8-R11). These eight are
   the only genuinely new tokens. */
.aspect-square{aspect-ratio:1/1}   /* shop.php product image tile */
.aspect-square.shop-tile-empty{aspect-ratio:auto;height:8rem}   /* no photo: short placeholder, not a phone-tall square (CPv-10) */
.line-clamp-3{display:-webkit-box;-webkit-line-clamp:3;
  -webkit-box-orient:vertical;overflow:hidden}   /* shop.php product description */
.text-gray-200{color:var(--gray-200)}   /* shop.php empty-image placeholder icon */
/* Parallel to the existing .bg-green-100 utility; matches --red-100, the same
   variable .badge-red already uses. questionnaire.php's Yes/No answer chips
   are plain rounded (not pill) utility markup — every other class on those
   chips (px-2.5, py-1, rounded, text-sm, font-medium, text-red-700) already
   exists, so this one gap is filled as a bare utility rather than reaching
   for .badge-sq, whose padding is a different, smaller size (see R9). */
.bg-red-100{background-color:var(--red-100)}
/* bookings.php's "Clear" filter link is `rounded-lg px-4 py-2 text-sm
   font-medium text-gray-500 hover:text-gray-700` — a padded pill, not the
   plain-text .back-link shape (no padding/radius/weight there), so it stays
   raw utility markup and only this one missing hover leg needs filling. */
.hover\:text-gray-700:hover{color:var(--gray-700)}
/* shop.php's cart-count pill is `bg-brand-100 px-2 py-0.5 text-xs
   font-semibold text-brand-700` — NOT .badge-brand: that component was
   byte-copied from the dashboard's brand-800 "In Progress" pill (Batch 6b)
   and its .badge base padding is px-2.5, both a delta from this site.
   Filled as a bare utility instead, same reasoning as .bg-red-100 above. */
.bg-brand-100{background-color:rgb(var(--brand-100))}
/* Compound override for the Add-to-cart button's icon gap. .btn sets
   gap:.5rem and sits AFTER the utilities block, so a bare .gap-1 (.25rem)
   loses to it on source order — the same component-beats-utility hazard
   as the .field-input compounds above. Stated at (0,2,0) to win regardless
   of order. Byte-copied from the markup's own `gap-1`. */
.btn.gap-1{gap:.25rem}
/* Same idiom, .gap-3 leg — login.php's "Sign in with Microsoft" OAuth
   button wants gap:.75rem, not .btn's own gap:.5rem. Batch 10 Task 8. */
.btn.gap-3{gap:.75rem}

/* ---- Batch 10 whitelist additions (2026-08-19) — agreements/contracts admin
   + auth. Everything else the token audit flagged this batch maps onto an
   existing component (.field-input, .field-error, .field-check, .btn-primary,
   .btn-secondary, .btn-danger-soft, .max-h-24, .max-w-xs) or an existing
   hover idiom (.hover-brand, .hover-underline — see the B10-3-style
   correction below) and needs no new CSS. These ten are the genuinely new
   tokens. */
.pr-3{padding-right:.75rem}
.pr-10{padding-right:2.5rem}
.h-20{height:5rem}
.p-8{padding:2rem}
.border-2{border-width:2px}
.divide-gray-200>:not([hidden])~:not([hidden]){border-color:var(--gray-200)}
.hover\:border-brand-300:hover{border-color:rgb(var(--brand-300))}
.hover\:text-brand-700:hover{color:rgb(var(--brand-hover))}
.hover\:text-brand-800:hover{color:rgb(var(--brand-hover-strong))}
.hover\:text-red-700:hover{color:var(--red-700)}
.hover\:text-gray-600:hover{color:var(--gray-600)}
.placeholder-gray-400::placeholder{color:var(--gray-500)}   /* AA on white (t1o6) */
.text-5xl{font-size:3rem;line-height:1}
.bg-green-600{background-color:var(--green-700)}   /* white text contrast, as .text-green-600 already maps (CPv-05) */
.hover\:bg-green-700:hover{background-color:var(--green-800)}
/* Compound override so .field-input's own `padding:.5rem .75rem` shorthand
   doesn't beat the bare .pr-10 at equal specificity (component-beats-utility
   hazard, source order) — same idiom as .btn.gap-1 above. Password
   show/hide icon spacing on login.php/register.php/change-password.php. */
.field-input.pr-10{padding-right:2.5rem}

/* ---- Batch 10 Task 13 additions (2026-08-19) — shared layouts
   (views/layouts/app.php + auth.php). Closes the 12-token audit gap on the
   app's shared chrome (progress.md's Batch-10-carry-forward layouts sweep).
   app.php's portal-footer `hover:text-brand-600` reuses the existing
   .hover-brand idiom (Task 1's B10-3-style decision) instead of a new bare
   hover utility — markup changed, no CSS needed for that one. */
.min-h-full{min-height:100%}
.mt-12{margin-top:3rem}
.tracking-tight{letter-spacing:-.025em}
.max-w-180px{max-width:180px}
.max-w-260px{max-width:260px}

/* ---- Batch 10 Task 14 additions (2026-08-19) — views/cart/index.php, the
   last CDN-audit gap before the Tailwind CDN can be dropped. Notice-box
   blue reuses the .alert-info palette vars but not .alert itself (its
   padding is p-3 uniform, not .alert's .75rem 1rem asymmetric shorthand).
   .hover\:text-red-500 mints the hover leg for the existing bare
   .text-red-500 (line ~289). .btn.py-3 follows the .btn.gap-1/.btn.gap-3
   compound-override idiom above for the "Proceed to Checkout" button's
   taller padding. */
.bg-blue-50{background-color:var(--blue-50)}
.border-blue-200{border-color:var(--blue-200)}
.hover\:text-red-500:hover{color:var(--red-600)}   /* AA on white (t1o6) */
.top-6{top:1.5rem}
.btn.py-3{padding-top:.75rem;padding-bottom:.75rem}

/* admin/questionnaire/show.php's Pending badge is px-3 py-1 text-sm — a
   different size than the .badge/.badge-yellow component (.125rem .625rem
   padding, .75rem font), so it's a bare-utility gap, not a component
   match. .text-yellow-700 already exists (line ~213). */
.bg-yellow-100{background-color:var(--yellow-100)}

/* admin/billing/deposits.php — flash boxes are px-4 py-3 text-sm, which
   IS .alert's exact box model, but the source markup is a bare div, not
   using the .alert class; mapped as bare utilities to avoid a markup
   restructure for a single deferred page. .field-input.pl-7 is the "$"
   prefix-icon input (invoice_threshold_amount), same compound-override
   idiom as .field-input.pr-10 above. .btn.px-6 is the Save button's
   wider horizontal padding, same idiom as .btn.py-3/.btn.gap-3. Checkbox/
   radio inputs converted to the existing .field-check/.field-radio
   components instead of minting bare focus:ring-brand-500 (OD-established
   idiom, Batch 7b Task 1). */
.bg-green-50{background-color:var(--green-50)}
.border-green-200{border-color:var(--green-200)}
.bg-red-50{background-color:var(--red-50)}
.border-red-200{border-color:var(--red-200)}
.text-red-800{color:var(--red-800)}
.field-input.pl-7{padding-left:1.75rem}
.btn.px-6{padding-left:1.5rem;padding-right:1.5rem}

/* portal/booking-confirm.php — last file in the Task 14 CDN-removal
   audit. "Sign Contract"/"Complete Assessment" pill links are
   px-3 py-1.5 text-xs rounded (.25rem radius) — close to .btn-sm's box
   model but .btn's border-radius:var(--radius) (.5rem) would round the
   corners more than the source, so mapped as bare utilities rather than
   .btn.btn-sm to hold pixel parity. Status badge's completed/overridden
   PHP status strings are confirmed non-class literals (Task 14 audit
   triage), not real tokens. */
.bg-brand-600{background-color:rgb(var(--brand-600))}
.hover\:bg-brand-700:hover{background-color:rgb(var(--brand-hover))}
.bg-yellow-50{background-color:var(--yellow-50)}
.bg-blue-100{background-color:var(--blue-100)}
.text-blue-700{color:var(--blue-700)}

/* Responsive — the app's two informal breakpoints, kept verbatim.
   sm: = form-grid collapse idiom; md: = nav mobile/desktop swap. */
@media (min-width:640px){
  .sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
  .sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
  .sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}   /* F-50: profile city/state/zip 2/1/2 from 640px */
  .sm\:col-span-2{grid-column:span 2/span 2}
  .sm\:flex-row{flex-direction:row}
  .sm\:items-center{align-items:center}
  .sm\:justify-between{justify-content:space-between}
  .sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}
  .sm\:col-span-1{grid-column:span 1/span 1}   /* Batch 7a: blackouts create-form grid */
  .sm\:mx-auto{margin-left:auto;margin-right:auto}   /* Batch 10 Task 13: auth.php card wrap */
  .sm\:w-full{width:100%}                            /* Batch 10 Task 13: auth.php card wrap */
  .sm\:max-w-md{max-width:28rem}                     /* Batch 10 Task 13: auth.php card wrap */
  .sm\:px-10{padding-left:2.5rem;padding-right:2.5rem}   /* Batch 10 Task 14: register.php card */
}
@media (min-width:768px){
  .md\:hidden{display:none}
  .md\:flex{display:flex}
  .md\:block{display:block}
  .md\:inline-flex{display:inline-flex}
  .md\:items-center{align-items:center}   /* Batch 10 Task 13: nav desktop row */
  .md\:gap-1{gap:.25rem}                  /* Batch 10 Task 13: nav desktop links */
  .md\:gap-3{gap:.75rem}                  /* Batch 10 Task 13: nav user-menu row */
  /* TameLink-xb1s (Nix, 2026-10-08): page layouts hold two columns from 768px,
     not 1024px, so a half-screen window or a tablet does not stack. A sidebar
     page pairs `md:grid-cols-2 lg:grid-cols-3` — two equal columns here, its
     1/3 + 2/3 split from 1024px — so the narrow column's forms are not squeezed.
     Must stay above the lg: block: the lg: rules win at 1024px by source order. */
  .md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
  .md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}   /* invoices/detail top row */
  .md\:flex-row{flex-direction:row}        /* admin/settings two-column index */
  .md\:items-start{align-items:flex-start} /* admin/settings two-column index */
}
@media (min-width:1024px){
  .lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}
  .lg\:px-8{padding-left:2rem;padding-right:2rem}
  .lg\:col-span-2{grid-column:span 2/span 2}
  .lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}
  .lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}
  .lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}  /* Batch 6b: dashboard stat row */
  .lg\:col-span-3{grid-column:span 3/span 3}
  /* Batch 4 (2026-08-11) — services/create.php two-column + sticky sidebar */
  .lg\:flex-row{flex-direction:row}
  .lg\:max-w-2xl{max-width:42rem}
  .lg\:sticky{position:sticky}
  .lg\:top-4{top:1rem}
  .lg\:w-80{width:20rem}
  .lg\:items-start{align-items:flex-start}   /* Batch 7a: settings index two-column */
  .lg\:col-span-1{grid-column:span 1/span 1} /* Batch 7b: report_cards/edit sidebar */
}
/* First xl: tier in the project (Batch 9) — Tailwind's own 1280px step,
   needed only for shop.php's 4-up product grid. Mechanical extension of the
   sm:/md:/lg: pattern above, not a new breakpoint decision. */
@media (min-width:1340px){
  .xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}
}

/* ---------- COMPONENTS: buttons ---------- */
/* BOX-MODEL RULE — read before mapping any button, and before writing a register
   entry that claims "padding is an exact match".
   `.btn` states `border:1px solid transparent` so that a bordered `.btn-secondary`
   and an unbordered `.btn-primary` sitting in the same row are the same height.
   The consequence, which is easy to miss when comparing padding alone:
     - a button that had NO border pre-conversion grows exactly 2px in BOTH axes;
     - a button that already had a 1px border is zero-delta (ours replaces theirs).
   So `.btn` = 20px line-box + 8+8 padding + 1+1 border = 38px, and `.btn-sm` =
   16 + 6+6 + 2 = 30px. Matching padding is NOT the same as matching geometry.
   (Ruled by Nix 2026-08-10, Task 10 mobile-diff finding F5, after the Batch 3
   register mis-stated the height of every button entry by this 2px. Registered as
   B3-21; Batches 4-6 inherit this rule rather than rediscovering it.) */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
  border-radius:var(--radius);padding:.5rem 1rem;
  font-size:.875rem;line-height:1.25rem;font-weight:600;
  border:1px solid transparent;cursor:pointer;
  transition:color .15s,background-color .15s,border-color .15s}
.btn:focus-visible{outline:none;box-shadow:var(--ring)}
.btn:disabled{opacity:.5;cursor:not-allowed;pointer-events:none}

.btn-primary{background:rgb(var(--brand-600));color:#fff}
.btn-primary:hover{background:rgb(var(--brand-hover))}
.btn-secondary{background:#fff;color:var(--gray-700);border-color:var(--gray-300);font-weight:500}
.btn-secondary:hover{background:var(--gray-50);border-color:var(--gray-400)}
/* Dashed "add another" affordance (Time Clock Report's + Add Manual Punch).
   Sits on .btn, so it is zero-delta on geometry per the BOX-MODEL RULE above —
   the markup already carried a 1px border and ours replaces it. Resets
   font-weight to 400 because .btn is 600 and this control is deliberately
   quiet. Byte-copied incl. both hover legs. Batch 6b Task 6. */
.btn-dashed{background:none;border-style:dashed;border-color:var(--gray-300);
  color:var(--gray-500);font-weight:400}
.btn-dashed:hover{border-color:rgb(var(--brand-400));color:rgb(var(--brand-600))}
/* Borderless quiet button — profile/index.php's Cancel, which carries full
   .btn padding but no border and no fill, so it reads as text beside the
   solid Save. NOT .btn-link: that strips the padding to zero and would let
   Save shift left. Grows 2px in both axes per the BOX-MODEL RULE (it had no
   border to replace). Batch 6b Task 7. */
.btn-ghost{background:none;color:var(--gray-600);font-weight:500}
.btn-ghost:hover{color:var(--gray-800)}
.btn-danger{background:var(--red-600);color:#fff}
.btn-danger:hover{background:var(--red-700)}
.btn-danger-outline{background:#fff;color:var(--red-700);border-color:var(--red-300);font-weight:500}
.btn-danger-outline:hover{background:var(--red-50)}
/* Soft-filled danger button (bookings/show.php "Cancel Booking") — a bolder
   fill than .btn-danger-outline, byte-copied from the markup */
.btn-danger-soft{background:var(--red-50);color:var(--red-700);
  border-color:var(--red-200);font-weight:500}
.btn-danger-soft:hover{background:var(--red-100)}
/* Soft-filled brand button (products/index.php "Add to Cart") — the brand
   sibling of .btn-danger-soft above. Ruled by Nix 2026-08-11; the same idiom
   recurs in settings/hours*.php, which Batch 7 converts. Borderless by design:
   .btn's transparent border supplies the 1px, matching the markup's fill-only
   look (the +2px this costs is registered as B4-13). */
.btn-brand-soft{background:rgb(var(--brand-50));color:rgb(var(--brand-700))}
.btn-brand-soft:hover{background:rgb(var(--brand-100))}
/* Brand twin of .btn-danger-outline above (invoices/show.php "Email Invoice").
   Byte-copied from `border border-brand-300 bg-white px-4 py-2 text-sm
   font-medium text-brand-700 hover:bg-brand-50` — zero delta, and already
   bordered, so B5-6's +2px does not apply here. */
.btn-brand-outline{background:#fff;color:rgb(var(--brand-700));
  border-color:rgb(var(--brand-300));font-weight:500}
.btn-brand-outline:hover{background:rgb(var(--brand-50))}
/* Muted twin of .btn-brand-soft — the disabled "Add to Cart" shown when no
   customer is selected. The gray fill IS the disabled affordance, and the
   markup carries no opacity, so .btn:disabled's generic dimming is switched
   off here rather than stacking on top of it. */
.btn-muted{background:var(--gray-100);color:var(--gray-600)}
/* Solid orange / solid green / soft red action buttons — employees/timeoff.php
   (Reassign, Approve, Deny). .btn-danger-soft already exists but is red-50 on
   rest and red-100 on hover; Deny is one step darker (red-100 -> red-200), so it
   gets its own modifier rather than shifting a shared one. Byte-copied. */
.btn-orange{background:var(--orange-700);color:#fff}   /* white on orange-600 was 3.6:1 (t1o6) */
.btn-orange:hover{background:var(--orange-800)}
.btn-approve{background:var(--green-700);color:#fff}   /* white on green-600 was 3.3:1 (EPv-04) */
.btn-approve:hover{background:var(--green-800)}
.btn-deny{background:var(--red-100);color:var(--red-700)}
.btn-deny:hover{background:var(--red-200)}
.btn.btn-muted:disabled{opacity:1}
/* Bare "×" dismiss control (chip clear, modal close). Owns colour, reset,
   line-height AND size — pair it with one of the two size modifiers below.
   Do NOT size it with a Tailwind text-* utility (Task 10 diff finding F4,
   ruled by Nix 2026-08-10): while the CDN still loads, a utility's
   line-height BEATS this rule. Both are (0,1,0), and the CDN injects its
   copy after app.css, so `.text-base` re-imposed a 1.5rem line box and
   silently undid the `leading-none` the pre-conversion markup carried —
   growing the chip 4px and the modal close 8px. Keeping the size inside the
   component leaves no utility on the element for the CDN to win with. */
.btn-close{background:none;border:none;padding:0;line-height:1;
  color:var(--gray-500);cursor:pointer}
.btn-close:hover{color:var(--gray-600)}
.btn-close-base{font-size:1rem}
.btn-close-lg{font-size:1.25rem}

.btn-sm{padding:.375rem .75rem;font-size:.75rem;line-height:1rem}
.btn-lg{padding:.625rem 1.5rem}
.btn-xs{padding:.25rem .5rem;font-size:.75rem;line-height:1rem;border-radius:var(--radius-sm)}

/* Text-only action links (row actions, modal cancel) */
.btn-link{background:none;border:none;padding:0;font-size:.875rem;line-height:1.25rem;
  font-weight:500;color:rgb(var(--brand-600));cursor:pointer}
.btn-link:hover{color:rgb(var(--brand-hover));text-decoration-thickness:2px}
.btn-link:focus-visible{outline:none;box-shadow:var(--ring);border-radius:.125rem}
/* Underline text links so they read as links, not labels (Nix 2026-09-30, TameLink-gyb1).
   Dismiss and quiet variants stay plain. */
.btn-link:not(.btn-link-dismiss):not(.btn-link-quiet){text-decoration:underline;text-underline-offset:2px}
.btn-link-danger{color:var(--red-600)}
.btn-link-danger:hover{color:var(--red-700)}
.btn-link-muted{color:var(--gray-500)}
.btn-link-muted:hover{color:var(--gray-700)}
/* Action link sitting inside an .alert-info panel (products/index.php
   "Change"), where brand-600 would fight the blue surround. Ruled by Nix
   2026-08-11. Pair with .btn-link-sm — the markup is 12px. */
.btn-link-info{color:var(--blue-600)}
.btn-link-info:hover{color:var(--blue-800)}
.btn-link-sm{font-size:.75rem;line-height:1rem}
/* OD-2 (ruled by Nix 2026-08-06): muted text link whose hover reads as a
   destructive action — the discount "Remove" link (create.php:509). Used as
   .btn-link.btn-link-sm.btn-link-dismiss. font-weight:400 because the markup
   has no weight class today and .btn-link's own 500 would otherwise apply.
   line-height:1rem is redundant since .btn-link-sm gained text-xs's line-height
   (Task 10 diff finding F1, 2026-08-07); kept so the modifier stands alone. */
.btn-link-dismiss{color:var(--gray-500);font-weight:400;line-height:1rem}
.btn-link-dismiss:hover{color:var(--red-600)}
/* Quiet gray text link (invoices/show.php "Print ↗"). Same shape as
   .btn-link-dismiss above — including font-weight:400, because the markup
   carries no font-weight class and .btn-link's 500 would otherwise thicken it. */
/* gray-500, not 400: gray-400 is 2.5:1 on white, under the 4.5:1 text minimum (TameLink-9hag F74). */
.btn-link-quiet{color:var(--gray-500);font-weight:400}
.btn-link-quiet:hover{color:var(--gray-600)}
/* Faint row-action link that warms to brand on hover — settings/services.php's
   "Edit →". NOT .btn-link-quiet above: that is font-weight 400 and hovers to
   gray-600; this one keeps .btn-link's 500 and hovers to brand-600. Used as
   .btn-link.btn-link-sm.btn-link-faint. Byte-copied from `text-xs font-medium
   text-gray-400 hover:text-brand-600 transition-colors` — colours and size are
   zero delta; the markup's `transition-colors` is dropped because no member of
   the .btn-link family carries one, so the hover colour change is instant
   instead of fading over 150ms. Batch 7a Task 8. */
.btn-link-faint{color:var(--gray-500)}
.btn-link-faint:hover{color:rgb(var(--brand-hover-text))}
/* The destructive sibling of .btn-link-faint above, and the exact parallel of
   it: a quiet 12px row action that only commits to red on hover. Byte-copied
   from settings/vaccines-edit.php:94 (`text-xs text-red-400 hover:text-red-600`)
   — zero delta. NOT .btn-link-danger: that is red-600 at rest and would make
   the "Remove" link shout beside the gray-400 "Set default" it sits next to.
   Use as .btn-link.btn-link-sm.btn-link-danger-faint.btn-link-normal — the
   fourth class is NOT optional here (corrected at Batch 7b Task 12): the
   markup carries no weight class, so it renders at 400, and .btn-link's own
   500 would thicken it. The same applies to the "Set default" link beside it,
   which is .btn-link-faint without a `font-medium` (unlike services.php's
   "Edit →", the arm that rule was byte-copied from). Batch 7b Task 1. */
/* t1o6: red-400 was 2.8:1 on white; red-600 is the lightest red that passes AA. */
.btn-link-danger-faint{color:var(--red-600)}
.btn-link-danger-faint:hover{color:var(--red-700)}
/* Weight-only modifier, Batch 6b Task 2. The breadcrumb/back-link idiom
   (`text-sm text-gray-500 hover:text-gray-700` with NO weight class) is
   .btn-link-muted's colours at font-weight 400 — .btn-link's own 500 would
   otherwise thicken it, the same trap .btn-link-dismiss and .btn-link-quiet
   each solved by baking 400 into their colour rule. A third near-synonym
   colour modifier (quiet/muted/subtle) would be unreadable, so the weight is
   split out instead: use `.btn-link .btn-link-muted .btn-link-normal`.
   ~10 more sites app-wide in unconverted views (agreements, contracts,
   settings/*, questionnaire, report_cards) will want this too. */
.btn-link-normal{font-weight:400}

/* Inline link inside body copy — the sibling of .btn-link above, and NOT
   interchangeable with it. .btn-link is a standalone action control and
   states its own size and weight; .text-link deliberately states neither, so
   it inherits the sentence it sits in. Use .btn-link for row actions and
   modal cancels, .text-link for a link mid-paragraph.
   (Ruled by Nix 2026-08-07, Task 10 diff finding F3: settings.php:39 had been
   mapped to .btn-link, which forced 14px/20px text inside a 12px/16px
   paragraph and grew the card 3px.) */
.text-link{color:rgb(var(--brand-600));text-decoration-line:none}
.text-link:hover{text-decoration-line:underline}
.text-link:focus-visible{outline:none;box-shadow:var(--ring);border-radius:.125rem}

/* ---------- COMPONENTS: form fields ---------- */
.field-label{display:block;font-size:.875rem;line-height:1.25rem;
  font-weight:500;color:var(--gray-700);margin-bottom:.25rem}
/* Small uppercase field label — the Time Clock Report filter bar. Same
   typography as .data-table th (12px/600/gray-500/uppercase/tracking-wide) but
   as a form label, so it keeps .field-label's bottom margin. Byte-copied from
   `block text-xs font-semibold text-gray-500 uppercase tracking-wide mb-1` —
   zero delta. Batch 6b Task 6. */
.field-label-xs{display:block;font-size:.75rem;line-height:1rem;font-weight:600;
  color:var(--gray-500);text-transform:uppercase;letter-spacing:.025em;
  margin-bottom:.25rem}
.field-input{display:block;width:100%;border-radius:var(--radius);
  border:1px solid var(--gray-300);padding:.5rem .75rem;
  font-size:.875rem;line-height:1.25rem;background:#fff;
  box-shadow:var(--shadow-sm)}
.field-input:focus{outline:none;box-shadow:var(--shadow-sm),var(--ring)}
.field-input.is-invalid{border-color:var(--red-400)}
.field-input:read-only:not(select),.field-input.is-locked{
  background:var(--gray-50);border-color:var(--gray-200);color:var(--gray-600)}
textarea.field-input{resize:vertical}
/* Resize escape hatch — the OD-15/OD-16 compound-class remedy, the same shape
   as `.data-table th.text-right` above and for the same reason: the rule on the
   line above is (0,1,1), so a plain `.resize-none` (0,1,0) is silently ignored
   and the textarea stays draggable. Found at Batch 7a Task 4 on
   settings/blackouts.php, whose Notes box is written `resize-none` and whose
   baseline asserts `resize:none`; converting it to .field-input without this
   would have made it resizable. blackouts is the only site pairing the two
   today, checked repo-wide. */
textarea.field-input.resize-none{resize:none}
.field-input-price{padding-left:1.75rem}  /* room for the absolute $ glyph */
.field-input-affix{padding-right:2.5rem}   /* room for a right-side affix button */
/* Room for a right-side GLYPH (settings/discounts.php's "%" on the Amount box),
   as .field-input-price above is for a left-side one. Distinct from
   .field-input-affix: that one clears a whole button at 2.5rem, this clears a
   single character at 2rem, byte-copied from the markup's `pr-8`. Minted rather
   than left as that utility because .field-input's `padding` shorthand is
   (0,1,0) in COMPONENTS and would swallow it once the Tailwind CDN is dropped —
   the same trap as .card-placeholder (OD-J), and ruled the same way by Nix
   2026-08-17. Batch 7a Task 5. Zero delta. settings/tax.php carries the same
   `pr-8` on three rate boxes and should reuse this at Batch 7b — which is also
   why `.pr-8` stays in the whitelist rather than being retired here. */
.field-input-suffix{padding-right:2rem}
/* ---- Batch 7b: three compound-selector remedies, recon R6 ----
   The component-beats-utility hazard, 6th-10th occurrence. `.field-input` lives
   in COMPONENTS and the utilities block sits EARLIER in this file, so at equal
   (0,1,0) specificity the component wins every tie. These three pairings are
   live in the markup today and work ONLY because the Tailwind CDN injects after
   app.css and re-wins — each breaks silently the moment the CDN is dropped at
   Batch 10. Stating them as (0,2,0) compounds makes today's rendering survive
   that removal. ZERO DELTA, now and after. Never weaken .field-input, never
   reach for !important. Same idiom as `textarea.field-input.resize-none` above
   and `.data-table th.text-right`. */
.field-input.w-32{width:8rem}    /* vaccines-edit: the "Label" mini-form box  */
.field-input.w-24{width:6rem}    /* vaccines-edit: the "Months" mini-form box */
.field-input.text-xs{font-size:.75rem;line-height:1rem}  /* email-templates/edit: the two code textareas */
/* Third-level compound: agreements create/edit's section-body textarea wants
   text-xs's font-size but leading-relaxed's line-height (1.625), not text-xs's
   own 1rem — the same component-beats-utility hazard one level deeper.
   (0,3,0) to win over .field-input.text-xs's (0,2,0). Batch 10 Task 3. */
.field-input.text-xs.leading-relaxed{line-height:1.625}
.field-input.border-brand-300{border-color:rgb(var(--brand-300))}  /* agreements-sign: the per-section initials box */
.field-input-inline{display:inline-block;width:auto;padding:.25rem .5rem;
  font-size:.75rem;line-height:1rem;border-radius:var(--radius-sm)}
/* Shrink-to-fit .field-input at the FULL size — schedule.php's "Effective from"
   date picker, which is a normal 14px control that simply is not full-bleed.
   .field-input-inline is not the match: it would also drop it to 12px and 6px
   corners. Width has to live in a component, not a `w-auto` utility, because
   .field-input's width:100% would win on source order (the OD-15 trap). */
.field-input-auto{display:inline-block;width:auto}
/* 12px size step for .field-input — settings/hours.php's "Select service"
   dropdown, and the same control on hours-edit and services. Byte-copied from
   `px-3 py-1.5 text-xs`; it deliberately keeps .field-input's radius, border,
   shadow and 2px focus ring, which is the whole reason it is a modifier and not
   a standalone rule. NOT .field-input-mini: that is a different control (4px
   radius, .25rem/.5rem padding, 1px focus ring). Pair with .field-input-auto
   where the select must not stretch. Batch 7a Task 6. Zero delta. */
.field-input-sm{padding:.375rem .75rem;font-size:.75rem;line-height:1rem}
/* Tighter padding at the FULL 14px size — settings/hours-edit.php's 14 time
   pickers, byte-copied from `px-2 py-1.5`. Nix ruled 2026-08-17 that these keep
   their current size rather than dropping to .field-input-inline, which is what
   the two employee schedule pages use (12px text, 4px radius): matching those
   would visibly shrink every picker on the batch's most-photographed page.
   So the app knowingly carries two time-picker sizes. Pair with
   .field-input-auto — the markup sets no width. Batch 7a Task 7. Zero delta. */
.field-input-tight{padding:.375rem .5rem}
/* Standalone compact control (employees/timeoff.php's review-notes textarea).
   NOT a .field-input modifier: .field-input-inline sets display:inline-block and
   width:auto, which would stop this textarea stretching in its flex column, and
   .field-input would add a shadow and a 2px focus ring the markup does not have.
   Deliberately states no display or width, so the flex stretch is preserved
   exactly. Byte-copied, including the 1px focus ring. */
.field-input-mini{border-radius:var(--radius-xs);border:1px solid var(--gray-300);
  padding:.25rem .5rem;font-size:.75rem;line-height:1rem;background:#fff}
.field-input-mini:focus{outline:none;box-shadow:0 0 0 1px rgb(var(--brand-500))}

/* Right-side in-input button (password reveal etc.) — wrap input in .relative */
.field-affix{position:absolute;top:0;bottom:0;right:0;display:flex;align-items:center;
  padding-right:.75rem;color:var(--gray-500);background:none;border:none;cursor:pointer}
.field-affix:hover{color:var(--gray-600)}
/* Left-side in-input affix (the "$" on money inputs) — wrap input in .relative.
   Mirror of .field-affix, which is right-side only. */
.field-affix-left{position:absolute;top:0;bottom:0;left:0;display:flex;
  align-items:center;padding-left:.625rem;color:var(--gray-500);
  font-size:.875rem;line-height:1.25rem}
/* Compact right-aligned money input with room for .field-affix-left (booking
   discount). Width stays a utility (.w-28). */
.field-amount{border-radius:var(--radius);border:1px solid var(--gray-300);
  padding:.375rem .5rem .375rem 1.5rem;font-size:.875rem;line-height:1.25rem;
  text-align:right;background:#fff;box-shadow:var(--shadow-sm)}
.field-amount:focus{outline:none;box-shadow:var(--shadow-sm),var(--ring)}
/* Small quantity input (products modal). Width stays a utility (.w-16). */
.field-qty{border-radius:.25rem;border:1px solid var(--gray-300);
  padding:.25rem .5rem;font-size:.875rem;line-height:1.25rem;background:#fff}
.field-qty:focus{outline:none;box-shadow:var(--ring)}
/* Width overrides for .field-input in inline contexts (filter bars, small
   numeric fields). COMPOUND selectors deliberately: .field-input sets
   width:100% and lives after the utilities block, so a bare .w-auto/.w-24
   utility can never win on source order. Specificity decides instead. */
.field-input.w-auto{display:inline-block;width:auto}
.field-input.w-24{display:inline-block;width:6rem}
/* Batch 4: services create/edit use `block w-48` on the minutes/length numeric
   inputs — display stays block here, unlike the two inline cases above. */
.field-input.w-48{width:12rem}
/* Batch 5: billing/invoices.php's "cancel after N days" input. Width only, as
   with .w-48 — it is a flex item, so display is blockified anyway. */
.field-input.w-20{width:5rem}
/* Batch 5: the invoices index search box is `w-72`. Compound for the same reason
   as the two above — a bare .w-72 utility sits before this block and would lose
   to .field-input's width:100%. Display stays block, as with .w-48; the input is
   a flex item either way, so nothing moves. */
.field-input.w-72{width:18rem}
/* Batch 7a: settings/hours.php's "Gap Between Appointments" (w-28) and "New
   Schedule" name box (w-64). Same reason as every rule above — the bare
   utilities sit in the utilities block and lose to .field-input's width:100%.
   Display stays block, as with .w-48 and .w-72. */
.field-input.w-28{width:7rem}
.field-input.w-64{width:16rem}
/* OD-1 (ruled by Nix 2026-08-06): compact input/select for tight rows — the
   booking status <select> (bookings/show.php). Compound selectors deliberately:
   the base .field-input rules sit earlier in this same block, and
   .field-input:focus is (0,2,0) — a bare .field-input-compact:focus would lose.
   Revised 2026-08-11 (ruled by Nix): the shadow reset moved out to
   .field-input-flat below. OD-1 originally folded box-shadow:none in here
   because its single site had no shadow, but repo-wide 9 of the 10 px-2 py-1.5
   form controls DO carry shadow-sm — the component was encoding the minority.
   Padding and shadow are now separate concerns; bookings/show.php takes both
   classes and is pixel-identical. */
.field-input.field-input-compact{padding:.375rem .5rem}
/* Compact money input — the Amount field in invoices/show.php's Record Payment
   box. Needed because .field-input-price is (0,1,0) and loses its padding-left to
   .field-input.field-input-compact's (0,2,0); this (0,3,0) rule puts the room for
   the "$" back. OD-14 (Nix, 2026-08-12): that box's four fields were three
   different sizes, and the ruling was to make them all match the compact size —
   so Amount and Notes give up 4px of side padding. Pair with .field-affix-left. */
.field-input.field-input-compact.field-input-price{padding-left:1.75rem}
/* Clears the base .field-input shadow, restoring only the focus ring. Same
   (0,2,0) compounding reason as above. */
.field-input.field-input-flat{box-shadow:none}
.field-input.field-input-flat:focus{box-shadow:var(--ring)}
/* OD-3(ii) (ruled by Nix 2026-08-06): a field that is BOTH locked and failing
   validation must keep its red error border. Without this, the read-only rule
   above — (0,2,1) with :not(select), vs .field-input.is-invalid's (0,2,0) —
   wins and silently hides the error. Pre-existing bug; closed here. */
.field-input:read-only:not(select).is-invalid,
.field-input.is-locked.is-invalid{border-color:var(--red-400)}
.field-help{margin-top:.25rem;font-size:.75rem;line-height:1rem;color:var(--gray-500)}
.field-error{margin-top:.25rem;font-size:.75rem;line-height:1rem;color:var(--red-600)}
.req{color:var(--red-700, #b91c1c)}  /* the “*” marker — §2h, already standardized */
.field-check{border-radius:.25rem;border-color:var(--gray-300);color:rgb(var(--brand-600))}
.field-check:focus-visible{outline:none;box-shadow:var(--ring)}
/* The RADIO sibling of .field-check. Identical but for the corner radius, which
   is the whole reason it exists: .field-check states border-radius:.25rem, and
   wearing that on a radio squares it off. Byte-copied from the vaccine forms'
   `text-brand-600 focus:ring-brand-500` (they carry no `rounded`) — zero delta.
   Sites: settings/vaccines-create.php and vaccines-edit.php, 3 radios each.
   Batch 7b Task 1. */
.field-radio{border-color:var(--gray-300);color:rgb(var(--brand-600))}
.field-radio:focus-visible{outline:none;box-shadow:var(--ring)}
/* Styled file input (pet photo upload; report_cards/edit + settings/branding
   convert to this in later batches) — values byte-copied from the Tailwind
   file:* markup via ::file-selector-button */
.field-file{display:block;width:100%;font-size:.875rem;line-height:1.25rem;
  color:var(--gray-600)}
.field-file::file-selector-button{margin-right:.75rem;border-radius:var(--radius);
  border:0;background:rgb(var(--brand-50));padding:.375rem .75rem;
  font-size:.875rem;line-height:1.25rem;font-weight:500;
  color:rgb(var(--brand-700));cursor:pointer}
.field-file::file-selector-button:hover{background:rgb(var(--brand-100))}
/* OD-F (Nix, 2026-08-17): settings/branding.php's logo picker is a SOLID brand
   button, not the pale one .field-file paints. Nix ruled both looks are kept
   rather than forcing one, so nothing already shipped changes — this is the
   solid variant, worn ALONGSIDE .field-file, byte-copied from branding.php:102
   (`file:mr-3 file:rounded-md file:border-0 file:bg-brand-600 file:px-3
   file:py-2 file:text-sm file:font-semibold file:text-white
   hover:file:bg-brand-700`, and the field itself is text-gray-700 not -600).
   Do NOT fold the two together, and do NOT "fix" the pale one to match — that
   would regress pets/edit and the styleguide, which are already approved.
   PROVED in-browser against the live input 2026-08-17: identical 468x36 box,
   colour, radius, padding, size and weight — zero delta on every visual
   property. The ONE difference is the cursor. This variant is worn ALONGSIDE
   .field-file, so it inherits that component's cursor:pointer, where the
   Tailwind markup carried no cursor utility and showed the default arrow.
   Accepted rather than overridden: it is invisible in every screenshot, it
   makes the app's two file buttons behave alike, and a hand cursor on a
   clickable button is the better behaviour. Registered as B7a-15.
   Batch 7b's report-cards editor inherits this variant — do not re-decide it. */
.field-file-solid{color:var(--gray-700)}
.field-file-solid::file-selector-button{margin-right:.75rem;
  border-radius:var(--radius-sm);border:0;background:rgb(var(--brand-600));
  padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem;font-weight:600;
  color:#fff}
.field-file-solid::file-selector-button:hover{background:rgb(var(--brand-hover))}
/* Card-style option row (radio/checkbox picker) — §2d */
.option-card{display:flex;align-items:flex-start;padding:1rem;
  border:1px solid var(--gray-300);border-radius:var(--radius);cursor:pointer}
.option-card:hover{background:var(--gray-50)}
/* Horizontal option row (booking add-on and product pickers). Distinct from
   .option-card: center-aligned, gap-bearing, lighter border, tighter padding —
   byte-copied from bookings/create.php. */
.option-row{display:flex;align-items:center;gap:.75rem;padding:.75rem;
  border:1px solid var(--gray-200);border-radius:var(--radius);
  transition:background-color .15s,border-color .15s}
.option-row:hover{background:var(--gray-50)}
.option-row.is-selected{border-color:rgb(var(--brand-400));background:rgb(var(--brand-50))}
/* Checkout payment-method picker. A SIBLING of .option-card, not a variant of
   it: .option-card is an inline flex+gap checkbox row, this is a BLOCK
   container holding a header row plus an x-show detail panel that must sit
   BENEATH it (the cash amount field, the Passage card widget). Under
   .option-card's display:flex those two children would render side by side.
   OD-9 (Nix, 2026-08-12) — supersedes OD-4, which assumed .option-card fit.
   Byte-copied from `block p-4 border border-gray-300 rounded cursor-pointer
   hover:bg-gray-50` — zero delta. Radius is a literal .25rem: `rounded` is 4px
   and the --radius scale starts at .375rem, so no token matches. */
.option-panel{display:block;padding:1rem;border:1px solid var(--gray-300);
  border-radius:.25rem;cursor:pointer}
.option-panel:hover{background:var(--gray-50)}
/* `.is-selected` follows the convention set by .option-row.is-selected above.
   A component, not `ring-*` utilities — Batch 4 kept that door shut. Byte-copied
   from `border-brand-500 ring-1 ring-brand-500`; the 1px ring is a spread-only
   shadow, which is what Tailwind's ring-1 compiles to. Measured live 2026-08-12:
   both resolve to rgb(41,41,41) on this facility's brand palette. */
.option-panel.is-selected{border-color:rgb(var(--brand-500));
  box-shadow:0 0 0 1px rgb(var(--brand-500))}
/* Checkbox/radio row: box on the left, bold label + grey explainer stacked
   right (22 sites across services/products create+edit). Byte-copied from
   `flex items-start gap-3 cursor-pointer` — zero delta. */
.check-row{display:flex;align-items:flex-start;gap:.75rem;cursor:pointer}
/* Selected-record chip (booking client/pet search) */
.chip{display:flex;align-items:center;gap:.5rem;border-radius:var(--radius);
  border:1px solid var(--gray-300);background:var(--gray-50);
  padding:.5rem .75rem;font-size:.875rem;line-height:1.25rem}

/* ---------- COMPONENTS: badges ---------- */
.badge{display:inline-flex;align-items:center;border-radius:9999px;
  padding:.125rem .625rem;font-size:.75rem;line-height:1rem;font-weight:600}
.badge-gray{background:var(--gray-100);color:var(--gray-600)}
.badge-green{background:var(--green-100);color:var(--green-700)}
.badge-yellow{background:var(--yellow-100);color:var(--yellow-700)}
.badge-red{background:var(--red-100);color:var(--red-700)}
.badge-blue{background:var(--blue-100);color:var(--blue-700)}
.badge-sky{background:var(--sky-100);color:var(--sky-700)}
.badge-teal{background:var(--teal-100);color:var(--teal-700)}
.badge-cyan{background:var(--cyan-100);color:var(--cyan-700)}
.badge-indigo{background:var(--indigo-100);color:var(--indigo-700)}
.badge-violet{background:var(--violet-100);color:var(--violet-700)}
.badge-pink{background:var(--pink-100);color:var(--pink-700)}
.badge-purple{background:var(--purple-100);color:var(--purple-700)}
/* B7a-10 — settings/services.php's "Add-On" pill. OUTLINED, not filled: a
   purple-50 ground with a purple-200 hairline, and font-medium where .badge is
   semibold, so it restates the weight. Byte-copied from services.php:73
   (`rounded-full bg-purple-50 border border-purple-200 px-2.5 py-0.5 text-xs
   font-medium text-purple-700`) — the padding is already .badge's, and the
   border is in the source, so this is zero delta. .badge-purple above is a
   DIFFERENT pill (filled purple-100, no border) and stays exactly as it is. */
.badge-purple-outline{background:var(--purple-50);
  border:1px solid var(--purple-200);color:var(--purple-700);font-weight:500}
.badge-orange{background:var(--orange-50);color:var(--orange-700)}
/* Brand-tinted badge — the dashboard's "In Progress" booking pill. The only
   badge colour that follows the client's palette rather than a fixed hue, so
   it is stated in rgb(var(--brand-N)) like every other brand surface.
   Byte-copy of `bg-brand-100 text-brand-800`. Batch 6b Task 5. */
.badge-brand{background:rgb(var(--brand-100));color:rgb(var(--brand-800))}
/* Square-cornered badge geometry (services scheduling badge, product status +
   tax badges). Distinct from .badge: 4px radius not a pill, tighter padding,
   medium not semibold. Byte-copied from the markup's
   `inline-flex rounded px-2 py-0.5 text-xs font-medium` — zero delta. */
.badge-sq{border-radius:.25rem;padding:.125rem .5rem;font-weight:500}
/* Thick-border alert pill (“vaccine overdue”, “no emergency contact”) — §5 */
.badge-alert{background:#fff;border:2px solid var(--yellow-400);
  color:var(--yellow-700);font-weight:700;padding:.125rem .75rem}
/* Larger status pill for standalone status panels (bookings/show.php) */
.badge-lg{padding:.25rem .75rem;font-size:.875rem;line-height:1.25rem}
/* Clickable .badge-alert variant (pet-header tag buttons that jump to a tab) —
   hover byte-copied from the markup's hover:bg-yellow-50 */
.badge-alert-btn{cursor:pointer;transition:background-color .15s}
.badge-alert-btn:hover{background:var(--yellow-50)}
/* Legend / calendar dots — §5 */
.dot{display:inline-block;width:.5rem;height:.5rem;border-radius:9999px}
.dot-brand{background:rgb(var(--brand-500))}
.dot-gray{background:var(--gray-300)}
.dot-purple{background:var(--purple-500)}
/* OD-R (Nix, 2026-08-17) — the roles legend's four role colours. Only purple
   existed; these three complete the set. Byte-copied from roles/index.php:40-42
   (`bg-blue-500` / `bg-green-500` / `bg-orange-500`) — zero delta.
   NOT related to OD-C's role BADGE ramp (admin purple / manager blue / lead
   teal / staff cyan): that governs the pill worn beside a person's name on the
   employees pages. This is the standalone legend on the permissions matrix,
   which has always run blue/green/ORANGE and is not being re-hued here. If the
   two are ever unified, that is a Batch 10 decision, not a Batch 7b one. */
.dot-blue{background:var(--blue-500)}
.dot-green{background:var(--green-500)}
.dot-orange{background:var(--orange-500)}
/* 10px size step — the roles legend runs `h-2.5 w-2.5` where .dot is 8px.
   Stated as a size modifier rather than a second dot rule so the colour arms
   above serve both sizes. */
.dot-lg{width:.625rem;height:.625rem}
/* Small inline label — OD-T (Nix, 2026-08-17). The grey "system" marker on the
   vaccine list and the brand "default" marker in the vaccine editor: 4px
   corners, tight padding, 12px, NORMAL weight. Byte-copied from
   settings/vaccines.php:55 (`text-xs font-normal text-gray-400 bg-gray-100
   rounded px-1.5 py-0.5`) — zero delta at both sites.
   NOT .badge-sq: that is semibold-ish (500), wider (px-2) and pill-family.
   NOT .chip: that name is taken by the booking client/pet search's selected
   record, which is a bordered 14px box. Two consumers today. */
.tag{display:inline-block;border-radius:var(--radius-xs);padding:.125rem .375rem;
  font-size:.75rem;line-height:1rem;font-weight:400;
  background:var(--gray-100);color:var(--gray-600)}   /* gray-500 on gray-100 was 4.4:1 (t1o6) */
.tag-brand{background:rgb(var(--brand-50));color:rgb(var(--brand-600));font-weight:500}

/* ---------- COMPONENTS: cards ---------- */
/* gray-200 edge: gray-100 on the gray-50 page was 1.02:1, invisible (TameLink-9hag F75). */
.card{background:#fff;border:1px solid var(--gray-200);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:1.5rem}
.card-flush{padding:0;overflow:hidden}      /* table-wrapper card */
.card-warning{border:2px solid var(--yellow-300)}
/* Neutral emphasis outline — the "Payment Info" / due-date card on
   invoices/show.php. Same shape as .card-warning above, in gray. OD-13 (Nix,
   2026-08-12): kept rather than flattened to a plain .card, because the heavy
   outline is the deliberate "this is when it's due" cue. Byte-copied from
   `border-2 border-gray-700` — zero delta. */
.card-emphasis{border:2px solid var(--gray-700)}
/* TameLink-p056.10: Settings > Business Hours — one day per line. */
.hours-note{padding:.75rem 1.5rem 0;font-size:.8125rem;color:var(--gray-600)}
.hours-list{padding:.75rem 1.5rem;border-bottom:1px solid var(--gray-100);font-size:.8125rem}
.hours-row{display:flex;gap:1rem;padding:.125rem 0}
.hours-row dt{width:6.5rem;font-weight:600;color:var(--gray-700)}
.hours-row dd{margin:0;color:var(--gray-700)}
.hours-row.is-off dd{color:var(--gray-500)}
.card-danger{border-color:var(--red-100)}
/* Border-colour-only card modifier — client_requests/index.php tints an
   unresolved request's card yellow-200 and leaves a resolved one on .card's
   gray-100. Written as a component, not the `border-yellow-200` utility the
   markup carries, because .card's `border` SHORTHAND (0,1,0) sits later in
   this file and swallows the utility's border-color outright — recon R6, the
   same trap as .card-placeholder (OD-J). Exactly the shape of .card-danger
   above. Byte-copied — zero delta. Batch 7b Task 1. */
.card-pending{border-color:var(--yellow-200)}
/* Booking page Billing card: the client can see a bill that is still owed
   (issued, open, balance above $0). Same border-colour-only shape as above. */
.card-due{border-color:var(--red-300)}
/* Mirrors `.panel-danger .card-heading` below, but at red-500 rather than
   red-700: .panel-danger is a red-50 FILLED panel that needs the darker shade
   for contrast, whereas .card-danger is a white card with a red hairline where
   red-500 already reads. Byte-copied from employees/edit.php's Danger Zone
   heading — zero delta. */
.card-danger .card-heading{color:var(--red-600)}   /* red-500 was 3.8:1 on white (t1o6) */
.card-tight{padding:1rem}                   /* p-4 card variant (utilities cannot override .card) */
.card-snug{padding:1.25rem}                 /* p-5 card variant — the dashboard stat tiles. Sits
                                               between .card-tight and .card's own 1.5rem; same
                                               reason as the other two, a utility cannot win
                                               against .card. Batch 6b Task 5. Zero delta. */
.card-row{padding:1rem 1.5rem}              /* py-4 px-6 card variant — settings/index.php's 20
                                               tiles. The FIRST asymmetric member of this family:
                                               tight/snug/roomy name a single uniform size, which
                                               does not extend to a row shape, so this one is named
                                               for what it is instead. Same reason as the others —
                                               .card's own padding is (0,1,0) on the element, so
                                               .px-6/.py-4 utilities could never win. Batch 7a
                                               Task 1, one consumer today (email-templates in 7b
                                               is the likely second). Zero delta. */
.card-roomy{padding:2rem}                   /* p-8 card variant — same reason: .card's own
                                               padding is (0,1,0) on the element itself, so a
                                               .p-8 utility could never win. Batch 6b Task 4,
                                               timeclock/index.php's clock-in card; one site
                                               app-wide today. Zero delta. */
/* A whole card that is itself a link (dashboard's Unpaid Invoices tile): the
   border tints and the shadow lifts one step on hover. Deliberately NOT
   .link-card — that is the bordered row on profile/index.php, which tints its
   BACKGROUND and restyles children. This one only raises the card. Byte-copy
   of `hover:border-brand-300 hover:shadow transition-all`. Batch 6b Task 5. */
.card-link{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);
  transition-duration:.15s}
.card-link:hover{border-color:rgb(var(--brand-300));box-shadow:var(--shadow)}
/* OD-G (Nix, 2026-08-17): settings/index.php's 20 tiles EXTEND this card rather
   than becoming a third unrelated card name. .card-link already owned the
   border-and-shadow half at the exact values; these two parts replace the
   `group` / `group-hover:text-brand-700` / `group-hover:text-brand-500` trio,
   which app.css has no `group` mechanism for. Byte-copied from index.php:12-24.
   The title's RESTING colour stays a utility because it branches by tile
   (text-gray-900 normally, text-gray-400 on a "coming soon" placeholder); the
   hover rule is (0,3,0) so it beats either one whatever the source order.
   Batch 7b's email-templates page reuses these — do not re-decide them. */
.card-link-title{font-size:.875rem;line-height:1.25rem;font-weight:600}
.card-link-arrow{height:1rem;width:1rem;flex-shrink:0;margin-left:1rem;
  color:var(--gray-300)}
.card-link:hover .card-link-title{color:rgb(var(--brand-hover))}
.card-link:hover .card-link-arrow{color:rgb(var(--brand-500))}
/* OD-J (Nix, 2026-08-17): a "coming soon" tile — dashed, one shade darker.
   This exists because .card's `border:1px solid var(--gray-100)` SHORTHAND
   resets style AND colour, and it lives in COMPONENTS, after the utilities.
   `border-dashed border-gray-200` on the element therefore loses on source
   order: proved in-browser with the CDN disabled, the tile came back
   `solid rgb(243,244,246)` — indistinguishable from a real tile. The Tailwind
   CDN is currently masking that (its copies of those two utilities are
   injected after app.css), so it is invisible TODAY and bites the moment the
   CDN is dropped. Hence a component-level rule, which beats .card the same way
   .card-danger's border-color does. `.card-link:hover` is (0,2,0) and still
   tints the border on hover, exactly as `hover:border-brand-300` does now.
   GENERAL RULE this sets: a card that needs a different border must say so in
   a component, never with a `border-*` utility. settings/index.php's 3
   placeholder tiles are the first consumers app-wide; there were zero others,
   checked. Batch 7a Task 3. Zero delta today. */
.card-placeholder{border-style:dashed;border-color:var(--gray-200)}
.card-heading{font-size:.875rem;line-height:1.25rem;font-weight:600;color:var(--gray-500);
  text-transform:uppercase;letter-spacing:.025em}
/* Group label above a table (services index type groups, products index status
   groups). OD-E (Nix, 2026-08-10): deliberately NOT .card-heading — that is a
   heading inside a card and is 14px/gray-500/tracking-wide. This is 12px/
   gray-400/tracking-widest. Byte-copied from the markup — zero delta for the
   9 headings on services/products index, invoices/show and the styleguide.
   NOT zero delta for profile/index.php's 4 CONTACT n sub-headings (Batch 6b):
   those asked for tracking-wide (.025em), so adopting this rule widened them
   to .1em — measured 62 -> 69px on "CONTACT 1". B6b-32, ruled by Nix
   2026-08-17: the shared rule wins, profile takes the wider tracking. Do NOT
   "fix" this to .025em — that would regress the 9 already-approved headings. */
.group-heading{font-size:.75rem;line-height:1rem;font-weight:600;
  color:var(--gray-500);text-transform:uppercase;letter-spacing:.1em}  /* gray-500: F74 contrast */
/* 14px sibling of .group-heading (billing settings section labels). OD-2 (Nix,
   2026-08-12): minted rather than folded into .group-heading or .card-heading —
   OD-E already ruled that distinct heading sizes stay distinct, and this differs
   from .card-heading on colour (gray-400 vs gray-500) as well as tracking
   (.1em vs .025em). Byte-copied from `text-sm font-semibold uppercase
   tracking-widest text-gray-400` — zero delta. Batch 7 reuses it for
   settings/contact.php. */
.group-heading-lg{font-size:.875rem;line-height:1.25rem;font-weight:600;
  color:var(--gray-500);text-transform:uppercase;letter-spacing:.1em}
/* Accented note card (client notes page) + inline note list item (client detail) */
.card-note{background:#fff;border:1px solid var(--gray-100);
  border-left:4px solid rgb(var(--brand-300));border-radius:var(--radius-lg);
  box-shadow:var(--shadow-sm);padding:1.25rem}
.note-item{border-left:2px solid rgb(var(--brand-200));padding-left:1rem}
/* Danger Zone panel (client settings) */
.panel-danger{border-radius:var(--radius-lg);border:1px solid var(--red-200);
  background:var(--red-50);padding:1.5rem}
.panel-danger hr{border-color:var(--red-200)}
.panel-danger .card-heading{color:var(--red-700)}
.panel-danger-title{font-size:.875rem;line-height:1.25rem;font-weight:500;color:var(--red-800)}
.panel-danger-desc{margin-top:.125rem;font-size:.75rem;line-height:1rem;color:var(--red-600)}
/* Yellow caution banner (client detail "needs attention") */
.panel-warning{border-radius:var(--radius-lg);border:1px solid var(--yellow-300);
  background:var(--yellow-50);padding:1rem}
/* Red twin of .panel-warning: a banner whose items block something, e.g. the
   "Complete before check-in" box (TameLink-40cu: red blocks, yellow warns, blue informs). */
.panel-error{border-radius:var(--radius-lg);border:1px solid var(--red-200);
  background:var(--red-50);padding:1rem}
/* Link inside a coloured banner (yellow "Complete before check-in" box, red/yellow
   vaccine banner). Takes the banner's own text colour and size, so one class fits
   every banner colour; hover thickens the underline instead of changing colour.
   Replaces .panel-warning-link (Nix 2026-10-21). */
.banner-link{font-weight:500;text-decoration-line:underline;text-underline-offset:2px;
  cursor:pointer;background:none;border:none;padding:0;color:inherit;font-size:inherit}
.banner-link:hover{text-decoration-thickness:2px}
.banner-link:focus-visible{outline:none;box-shadow:var(--ring);border-radius:.125rem}
/* Roomier .panel-warning (booking create pre-flight warnings, p-6 not p-4) */
.panel-warning-lg{padding:1.5rem}
/* TameLink-x5jf: a sign-off checkbox inside a .panel-warning (Edit Booking closure). */
.panel-warning-check{display:flex;gap:.5rem;align-items:flex-start;margin-top:.75rem}
/* Pending-review card (employees/timeoff.php). Deliberately NOT .panel-warning:
   that is a page-level caution banner at 12px radius / yellow-300, while this is
   a list item at 8px radius / yellow-200. Same call as OD-A — small stacked
   items keep their tighter corners. Byte-copied from
   `rounded-lg border border-yellow-200 bg-yellow-50 p-4`. */
.panel-pending{border-radius:var(--radius);border:1px solid var(--yellow-200);
  background:var(--yellow-50);padding:1rem}
/* Roomier .panel-pending — schedule.php's "New Time Off Request" form panel,
   byte-copied from `rounded-xl bg-yellow-50 border border-yellow-200 p-6`. It
   restates the radius as well as the padding (unlike .panel-warning-lg, whose
   base is already at 12px), because .panel-pending's base is the 8px list item. */
.panel-pending-lg{border-radius:var(--radius-lg);padding:1.5rem}
/* Orange conflict panel — employees/timeoff.php's blocked-approval list. The
   third member of the .panel-danger / .panel-warning family, byte-copied from
   `rounded-xl border border-orange-200 bg-orange-50 p-6`. Its heading rule
   mirrors `.panel-danger .card-heading`; the two table rules absorb the
   `border-b border-orange-200` / `divide-orange-100` the markup carried, so the
   conflict table needs no orange classes of its own. */
.panel-orange{border-radius:var(--radius-lg);border:1px solid var(--orange-200);
  background:var(--orange-50);padding:1.5rem}
.panel-orange .card-heading{color:var(--orange-700)}
.panel-orange-desc{font-size:.75rem;line-height:1rem;color:var(--orange-700)}
.panel-orange thead tr{border-bottom:1px solid var(--orange-200);
  color:var(--orange-700)}
.panel-orange tbody tr+tr{border-top:1px solid var(--orange-100)}
/* Compact white select inside .panel-orange (the reassign picker). Deliberately
   NOT .field-input — that is a full-width 14px control with a shadow; this is an
   inline 12px picker. Byte-copied, including the 1px orange focus ring. */
.panel-orange-select{border-radius:var(--radius-xs);border:1px solid var(--orange-300);
  background:#fff;padding:.25rem .5rem;font-size:.75rem;line-height:1rem}
.panel-orange-select:focus{outline:none;box-shadow:0 0 0 1px var(--orange-400)}

/* Brand-tinted inline add/edit form panel (pets med/allergy/vaccination forms,
   4 sites) — byte-copied from the markup; mb-6 stays a separate utility */
.panel-inset{border-radius:var(--radius);border:1px solid rgb(var(--brand-100));
  background:rgb(var(--brand-50));padding:1rem}
/* Neutral 2px-bordered panel (Vet on Record) — byte-copied from pets/show.php
   (rounded-xl bg-gray-50 border-2 border-gray-400 px-5 py-4) */
.panel-neutral{border-radius:var(--radius-lg);border:2px solid var(--gray-400);
  background:var(--gray-50);padding:1rem 1.25rem}

/* ---------- COMPONENTS: tables ---------- */
/* renamed .table → .data-table 2026-08-03 (checkpoint ruling): `table` is a real
   Tailwind utility (display:table), the vocabulary's only name collision with the CDN */
.data-table{min-width:100%;font-size:.875rem;line-height:1.25rem}
.data-table thead{background:var(--gray-50)}
.data-table th{padding:.75rem 1.5rem;text-align:left;font-size:.75rem;
  line-height:1rem;font-weight:600;color:var(--gray-500);
  text-transform:uppercase;letter-spacing:.025em}
.data-table td{padding:.75rem 1.5rem}
.data-table tbody tr{border-top:1px solid var(--gray-100)}
/* Row hovers are gray-100: gray-50 on white was too faint to see (TameLink-9hag F75). */
.data-table tbody tr:hover{background:var(--gray-100);
  transition:background-color .15s cubic-bezier(.4,0,.2,1)}
/* Alignment escape hatch for header cells — the OD-15/OD-16 compound-class
   remedy, NOT an !important and NOT a :where() rewrite of the rule above.
   `.data-table th` is (0,1,1) and `.text-right` is (0,1,0), so a right-aligned
   <th> was silently forced back to left no matter the source order; <td> was
   never affected, since no rule here sets td alignment. Confirmed in-browser
   2026-08-14, Batch 6b Task 2.
   This was a LATENT BUG, not a new problem: views/admin/pets/show.php:724 has
   carried `<th class="text-right">Total</th>` under a plain .data-table since
   Batch 2 and has been rendering left-aligned ever since. Ruled by Nix
   2026-08-14 — fix the shared rule rather than write a narrower one, which
   also restores that heading. Registered as B6b-15.
   .data-table-flush / -plain do not need this: their th selectors sit inside
   :where(), which is exactly why that was done (see the note below). */
.data-table th.text-right{text-align:right}
.data-table th.text-center{text-align:center}
.data-table tfoot tr:hover{background:transparent}
/* Open-punch row — the shift someone is clocked into RIGHT NOW (timeclock
   my-report.php and admin/timeclock/report.php).
   Green survives hover, deliberately. RULED BY NIX 2026-08-14: the two pages
   disagreed — my-report kept the row green under the pointer, the manager's
   report replaced it with the hover grey — and green wins on both, because
   green means "on the clock now" and hiding it under the mouse loses the
   information exactly when someone is looking for it. The report.php half of
   that is a real visual change, registered as B6b-13.
   The :hover arm is stated explicitly rather than relying on source order:
   `.data-table tbody tr.row-open` and `.data-table tbody tr:hover` are both
   (0,2,2), so a tie decided by position would silently flip if either rule
   ever moved. */
.data-table tbody tr.row-open,
.data-table tbody tr.row-open:hover{background:var(--green-50)}
/* Expanded configuration row — settings/services.php's "Allowed On" panel,
   which slides out under an add-on service. Same idiom as .row-open above and
   for the same two reasons: .data-table tbody tr sets a gray-100 top border
   that a `border-purple-100` utility cannot beat, and .data-table tbody
   tr:hover would otherwise wash the panel gray on hover, which it does not do
   today (the markup puts no hover class on this row). Batch 7a Task 8. */
.data-table tbody tr.row-config,
.data-table tbody tr.row-config:hover{border-top-color:var(--purple-100);
  background:var(--purple-50)}
/* In-card borderless mini-table (§4 pb-2 family). The WHOLE selector sits inside
   :where() so th/td really are zero-specificity and alignment/padding utilities
   (text-right, pl-4, whitespace-nowrap) can override per-cell.
   OD-15 (Nix, 2026-08-12): these two components previously wrote
   `.data-table-flush :where(th)`, which is still (0,1,0) — :where() zeroes only
   what is INSIDE it, not the class in front. The utilities block sits earlier in
   this file, so the component won every tie and the documented override never
   worked. It LOOKED fine only because the Tailwind CDN injects after app.css and
   its `text-right` beat both; the bug would have surfaced the moment the CDN was
   dropped at Batch 10. Live sites that were silently affected: clients/show.php
   (Total/Balance/Status headers), pets/show.php, styleguide.php ×3. This fix is
   zero-delta today and prevents the regression later. */
.data-table-flush{min-width:100%;font-size:.875rem;line-height:1.25rem}
:where(.data-table-flush th){padding:0 1rem .5rem 0;text-align:left;
  font-size:.75rem;line-height:1rem;font-weight:600;color:var(--gray-500)}
:where(.data-table-flush td){padding:.5rem 1rem .5rem 0}
.data-table-flush tbody tr+tr{border-top:1px solid var(--gray-50)}
/* Padding-free mini table (bookings/show.php report cards, invoices/show.php
   payment history) — columns space by content, header sits directly above the
   rule. Same zero-specificity fix as above (OD-15). Composes with
   .data-table-hover. */
:where(.data-table-plain th){padding:0 0 .5rem 0;text-align:left;font-size:.75rem;
  line-height:1rem;font-weight:600;color:var(--gray-500);
  text-transform:uppercase;letter-spacing:.025em}
:where(.data-table-plain td){padding:.625rem 0}
.data-table-plain tbody tr+tr{border-top:1px solid var(--gray-100)}
/* A voided payment (TameLink-t47k Run 6): kept in the history, struck
   through; the note under it (who, when, why) is not. */
.data-table-plain tbody tr.row-voided td{color:var(--gray-500);text-decoration:line-through}
.data-table-plain tbody tr.row-void-note{border-top:0}
.data-table-plain tbody tr.row-void-note td{padding-top:0}
/* Opt-in row hover, composable with .data-table-flush (Report Cards table).
   Deliberately NOT baked into .data-table-flush — that would restyle the
   Batch-1 clients mini-tables. */
.data-table-hover tbody tr:hover{background:var(--gray-100)}

/* ---------- COMPONENTS: alerts (global flash + inline boxes) ---------- */
.alert{border-radius:var(--radius);border:1px solid;padding:.75rem 1rem;
  font-size:.875rem;line-height:1.25rem}
.alert-success{background:var(--green-50);border-color:var(--green-200);color:var(--green-700)}
.alert-error{background:var(--red-50);border-color:var(--red-200);color:var(--red-700)}
.alert-warning{background:var(--yellow-50);border-color:var(--yellow-200);color:var(--yellow-700)}
.alert-info{background:var(--blue-50);border-color:var(--blue-200);color:var(--blue-700)}
/* Staff dashboard "client help requests are waiting" banner (QA F-26). A
   solid teal-600 left edge so it reads as a call to act, a step stronger than
   the passive .alert-info. */
.alert-teal{background:var(--teal-50);border-color:var(--teal-200);color:var(--teal-800);
  border-left:4px solid var(--teal-600)}
/* OD-N (Nix, 2026-08-17) — settings/tax.php's ValorPay surcharge disclosure.
   A WARM caution note, deliberately distinct from .alert-warning's lemon: the
   two mean different things, and recolouring a billing disclosure to match an
   ordinary warning was rejected. Byte-copied from tax.php:156-157
   (`bg-amber-50 border border-amber-200` + `text-xs text-amber-800`). One
   consumer today; do not reach for it as a general "warning" — that is
   .alert-warning. */
.alert-amber{background:var(--amber-50);border-color:var(--amber-200);color:var(--amber-800)}
/* Booking page "New pet" bar: blue-600 on blue-100, a step stronger than
   .alert-info. Its border is the fill colour, so the bar reads borderless. */
.alert-blue{background:var(--blue-100);border-color:var(--blue-100);color:var(--blue-700)}
/* Size modifier for .alert — the amber note above is written px-3 py-2.5 at
   12px, where .alert is px-4 py-3 at 14px. Stated as a modifier so the note
   keeps its exact current size (zero delta) rather than growing to the standard
   flash-message box, which is what reusing .alert alone would have done. */
.alert-tight{padding:.625rem .75rem;font-size:.75rem;line-height:1rem}

/* ---------- COMPONENTS: page furniture ---------- */
/* Title row: stacks on mobile, row on ≥sm — §9’s 7× responsive idiom */
.page-header{margin-bottom:1.5rem;display:flex;flex-direction:column;gap:1rem}
@media (min-width:640px){
  .page-header{flex-direction:row;align-items:center;justify-content:space-between}}
.page-header h1,.page-title{font-size:1.5rem;line-height:2rem;
  font-weight:700;color:var(--gray-900)}

/* GET filter row — admin/reports.php's report picker + per-report filter
   controls. Named component version of the `flex flex-wrap gap-4 items-end`
   combo already carried inline on admin/timeclock-report.php's filter form
   (that page is untouched here; this is only for new markup). Deliberately
   just layout — the controls inside stay .field-input/.field-label-xs/.btn,
   nothing about this component is form-field-specific. .filter-bar-group is
   the optional label+control stack; a control that needs no label (a lone
   button) can sit directly in .filter-bar. Batch 8 (TameLink-pmr). */
.filter-bar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:1rem}
.filter-bar-group{display:flex;flex-direction:column;gap:.25rem}

.back-link{font-size:.875rem;line-height:1.25rem;color:var(--gray-500)}
.back-link:hover{color:var(--gray-700)}
.back-link + .text-gray-300{color:var(--gray-500)}   /* breadcrumb "/" separator contrast (CPv-06) */

.empty-state{padding:2.5rem;text-align:center;
  font-size:.875rem;line-height:1.25rem;color:var(--gray-500)}
/* 32px round avatar (pet photos; fallback shows an initial) */
.avatar{height:2rem;width:2rem;border-radius:9999px;object-fit:cover;
  border:1px solid var(--gray-200)}
.avatar-fallback{display:flex;height:2rem;width:2rem;align-items:center;
  justify-content:center;border-radius:9999px;background:rgb(var(--brand-100));
  color:rgb(var(--brand-600));font-size:.75rem;line-height:1rem;font-weight:700}
/* 36px size modifier for .avatar/.avatar-fallback (pets index: h-9 w-9 photos,
   text-sm fallback initial — byte-copied so nothing shrinks) */
.avatar-lg{height:2.25rem;width:2.25rem;font-size:.875rem;line-height:1.25rem}
/* 40px size step — portal/dashboard.php's "My Pets" sidebar (h-10 w-10). The
   markup states no text-size utility on the fallback initial, so this keeps
   the default 16px rather than borrowing a neighbouring step's size.
   Byte-copied. Batch 8 Task 1. */
.avatar-md{height:2.5rem;width:2.5rem;font-size:1rem}
/* 48px size step — report_cards/edit.php's pet banner photo and its initials
   fallback (`h-12 w-12` with a `text-lg` initial). Sits between .avatar-lg
   (36px) and .avatar-hero (64px), and unlike .avatar-hero it is a plain size
   modifier: the hero inverts the colours as well, which is why that one is
   standalone. Byte-copied — the only delta is .avatar's 1px gray-200 hairline
   on the photo arm, registered as B7b-28/B7b-29. Batch 7b Task 1. */
.avatar-xl{height:3rem;width:3rem;font-size:1.125rem;line-height:1.75rem}
/* 56px size step — portal/pets.php's pet-card header (h-14 w-14, text-xl
   initial). Sits between .avatar-xl (48px) and .avatar-hero (64px); a plain
   size modifier like .avatar-xl, not standalone like .avatar-hero (no colour
   inversion here). Byte-copied, save for the brand-700 -> brand-600 text
   drift every .avatar-fallback consumer takes (B7b-29 precedent). Batch 8
   Task 1. */
.avatar-2xl{height:3.5rem;width:3.5rem;font-size:1.25rem;line-height:1.75rem}
/* Blue colour arm of .avatar-fallback — admin/search's PET initial discs, which
   are blue where the CLIENT discs beside them are brand. Written as an arm
   rather than `bg-blue-100 text-blue-700` utilities because .avatar-fallback
   states both background and color in COMPONENTS and would swallow them
   (recon R6). Text lands on blue-700 here where the brand arm uses brand-600 —
   that is the markup's own choice at both sites, not a drift. Batch 7b Task 1. */
.avatar-fallback-blue{background:var(--blue-100);color:var(--blue-700)}
/* 64px initials disc at the top of profile/index.php. Deliberately standalone
   rather than an .avatar-fallback size modifier: the colours are inverted
   (a SOLID brand-600 fill with white text, against .avatar-fallback's brand-100
   tint with brand-600 text), so a size modifier would have to undo two colours
   as well. States its own flex-shrink so the header's flex row cannot squash
   it. Byte-copied from `flex h-16 w-16 items-center justify-center rounded-full
   bg-brand-600 text-white text-xl font-bold flex-shrink-0` — zero delta.
   Batch 6b Task 7. */
.avatar-hero{display:flex;height:4rem;width:4rem;align-items:center;
  justify-content:center;border-radius:9999px;background:rgb(var(--brand-600));
  color:#fff;font-size:1.25rem;line-height:1.75rem;font-weight:700;flex-shrink:0}
/* Primary in-row record link (tables/lists): dark, brand on hover */
.row-link{font-weight:500;color:var(--gray-900)}
/* Underline on hover too: a colour-only hover is invisible on a black brand (TameLink-9hag F72). */
.row-link:hover{color:rgb(var(--brand-hover-text));text-decoration:underline;text-underline-offset:2px}
/* Hover-only brand tint for quiet inline links — rest state inherits.
   Byte-copy of the bare hover:text-brand-600 idiom (pets owner links). */
.hover-brand:hover{color:rgb(var(--brand-hover-text))}
/* Underline name links too, like .btn-link (Nix 2026-09-30, TameLink-gyb1). The
   brand hover tint is invisible while the brand colour is black. */
a.hover-brand{text-decoration:underline;text-underline-offset:2px}
/* OD-U (Nix, 2026-08-17) — a tappable row inside a .card.card-flush list, as
   used by admin/search's three result groups. The rows are separated by the
   markup's own `divide-y divide-gray-100` on the wrapper, so this rule states
   NO border: adding one would double the hairline. Byte-copied from
   search/index.php:48 (`flex items-center gap-3 px-5 py-3 hover:bg-gray-50
   transition-colors`) — zero delta.
   NOT .card-row (px-6 py-4, and a card's own padding rather than a list row's)
   and NOT .data-table's tr, which brings table geometry with it. */
.list-row{display:flex;align-items:center;gap:.75rem;padding:.75rem 1.25rem;
  transition:background-color .15s}
.list-row:hover{background:var(--gray-50)}
/* OD-S (Nix, 2026-08-17) — the action bar pinned to the bottom of the viewport
   on admin/roles. The app's first sticky element. The POSITIONING contract
   (.sticky + .bottom-0) stays in the markup as utilities and only the box is
   named here, so the same bar can be un-pinned on a page that does not want it
   without editing a shared rule. Byte-copied from roles/index.php:130
   (`mt-6 flex items-center justify-between rounded-xl border border-gray-200
   bg-white px-6 py-4 shadow-lg`) — zero delta. One consumer. */
.save-bar{margin-top:1.5rem;display:flex;align-items:center;
  justify-content:space-between;border:1px solid var(--gray-200);
  border-radius:var(--radius-lg);background:#fff;padding:1rem 1.5rem;
  box-shadow:var(--shadow-lg)}
/* Hover-only underline for quiet inline links, rest state inherited — the
   sibling of .hover-brand above. Byte-copy of the bare hover:underline idiom
   (dashboard pet-name links). */
.hover-underline:hover{text-decoration-line:underline}

/* Stat tiles — §13; text-3xl standard incl. currency (the text-2xl outlier
   normalizes at conversion).
   .stat-label was written in Phase 1 from §13 as 14px sentence case, BEFORE
   anyone looked at the only page that renders stat tiles. Batch 6b Task 5
   found dashboard/index.php — still the sole site app-wide — renders 12px
   uppercase medium with tracking-wide. Nix ruled 2026-08-14: the live page
   wins, so the rule below is now a byte-copy of it and the dashboard has zero
   delta. The three demo tiles on the styleguide restyle to match; that page
   documents reality rather than setting it. */
.stat-label{font-size:.75rem;line-height:1rem;font-weight:500;color:var(--gray-500);
  text-transform:uppercase;letter-spacing:.025em}
.stat-value{margin-top:.25rem;font-size:1.875rem;line-height:2.25rem;
  font-weight:700;color:var(--gray-900)}
.stat-value-brand{color:rgb(var(--brand-700))}
.stat-value-alert{color:var(--orange-600)}
.stat-value-danger{color:var(--red-600)}

/* ---------- COMPONENTS: navigation (single source: layouts/app.php) ---------- */
.nav{background:rgb(var(--brand-700));box-shadow:var(--shadow-sm)}
.nav-inner{margin:0 auto;max-width:80rem;padding:0 1rem}
@media (min-width:640px){.nav-inner{padding:0 1.5rem}}
@media (min-width:1024px){.nav-inner{padding:0 2rem}}
.nav-row{display:flex;height:4rem;align-items:center;justify-content:space-between}
.nav-link{display:inline-block;border-radius:var(--radius-sm);padding:.5rem .75rem;
  font-size:.875rem;line-height:1.25rem;font-weight:500;color:rgb(var(--brand-100));
  transition:color .15s,background-color .15s}
/* White tints, not brand-600: on a black brand 600 and 700 are both black (TameLink-9hag F69). */
.nav-link:hover{background:rgb(255 255 255 / .10);color:#fff}
.nav-icon-btn{border-radius:var(--radius-sm);padding:.5rem;
  color:rgb(var(--brand-100));transition:color .15s,background-color .15s}
.nav-icon-btn:hover{background:rgb(255 255 255 / .10);color:#fff}
.nav-count{position:absolute;top:-.25rem;right:-.25rem;display:inline-flex;
  align-items:center;justify-content:center;width:1.25rem;height:1.25rem;
  border-radius:9999px;background:#fff;color:rgb(var(--brand-700));
  font-size:.75rem;line-height:1rem;font-weight:700;box-shadow:0 0 0 2px rgb(var(--brand-700))}
/* Search box: a white-tint fill plus a white hairline and white placeholder, so the box
   stands out on any brand bar, a black one included (TameLink-t1o6). Focus turns it white
   with a white ring. */
.nav-search{width:10rem;border:1px solid rgb(255 255 255 / .35);border-radius:var(--radius-sm);
  background:rgb(255 255 255 / .18);padding:calc(.375rem - 1px) calc(.75rem - 1px);
  font-size:.875rem;line-height:1.25rem;color:#fff;transition:all .15s}
.nav-search::placeholder{color:rgb(255 255 255 / .80)}
.nav-search:focus{outline:none;width:14rem;background:#fff;color:var(--gray-900);
  border-color:#fff;box-shadow:0 0 0 2px rgb(255 255 255 / .55)}
.nav-search:focus::placeholder{color:var(--gray-500)}
/* Initials button for the user menu, shown at every width (Nix, 2026-10-20). White tints, not
   brand shades, so it reads on a black brand too (same reason as .nav-link:hover). */
.nav-user{display:flex;align-items:center}
.nav-avatar{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;
  border-radius:9999px;background:rgb(255 255 255 / .16);color:#fff;font-size:.8125rem;line-height:1;
  font-weight:600;letter-spacing:.03em;transition:background-color .15s}
.nav-avatar:hover,.nav-avatar[aria-expanded="true"]{background:rgb(255 255 255 / .28)}
.menu-head{padding:.5rem 1rem;border-bottom:1px solid var(--gray-100);margin-bottom:.25rem;
  font-size:.875rem;line-height:1.25rem;font-weight:600;color:var(--gray-900);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* Bar layout — Nix, 2026-10-21 (replaces the 1100/1180px switch points of 2026-10-20).
     1280px and up   bar links, full staff search box
     900–1279px      same bar links, staff search is an icon that opens a panel under the bar
     under 900px     ☰ opens the drawer; the bar keeps logo, (search icon,) cart, avatar
   Left to right: ☰ (phones), logo, links 32px after it, then search, cart and avatar
   pushed to the far right, 8px apart. .nav-inner has main's max-width and padding, so
   the logo and avatar line up with the page content's edges. No current-page highlight.
   Under 900px the avatar's menu is off: the avatar links to the person's own page
   and the menu's links sit in the drawer under a divider. */
.nav-brand{display:flex;align-items:center;gap:.75rem;min-width:0}
.nav-logo{display:flex;align-items:center;flex-shrink:0;color:#fff}
.nav-link-inner{display:flex;align-items:center;gap:.25rem}
.nav-actions{display:flex;align-items:center;gap:.5rem;margin-left:auto;padding-left:.5rem;flex-shrink:0}
/* The count badge hangs 4px past the cart's edge; the extra 4px keeps it clear of the avatar. */
.nav-cart{position:relative;display:inline-flex;align-items:center;margin-right:.25rem}
.nav-search-btn{display:inline-flex;align-items:center}
.nav-desktop{display:none;align-items:center}
.nav-desktop-links{gap:.25rem;margin-left:2rem}
.nav-search-form,.nav-avatar-menu{display:none}
.nav-avatar-link{display:inline-flex}
/* Keyboard focus on the brand bar: a white ring reads on every brand colour. */
.nav-link:focus-visible,.nav-icon-btn:focus-visible,.nav-avatar:focus-visible,.nav-logo:focus-visible{
  outline:2px solid #fff;outline-offset:2px}
.menu-item:focus-visible{outline:2px solid rgb(var(--brand-600));outline-offset:-2px;background:var(--gray-50)}
@media (min-width:900px){
  .nav-desktop{display:flex}
  .nav-toggle,.nav-avatar-link,.nav-drawer,.nav-drawer-backdrop{display:none !important}
  .nav-avatar-menu{display:block}
}
/* Under 1280px: tighter links so the staff bar's five items fit beside the search icon.
   The logo keeps its full size (32px tall, up to 180px wide) at every width. */
@media (min-width:900px) and (max-width:1279px){
  .nav-desktop-links{gap:0;margin-left:1rem}   /* EPv-02: 8px overflow at 768px */
  .nav-desktop-links .nav-link{padding:.5rem .5rem}
}
@media (min-width:1280px){
  .nav-search-form{display:block}
  .nav-search-btn,.nav-search-panel{display:none !important}
}
/* Search panel under the bar (staff, under 1280px). */
.nav-search-panel{background:rgb(var(--brand-700));border-top:1px solid rgb(255 255 255 / .12)}
.nav-search-panel-form{display:flex;align-items:center;gap:.5rem;padding-top:.75rem;padding-bottom:.75rem}
.nav-search.nav-search-wide,.nav-search.nav-search-wide:focus{flex:1;min-width:0;width:auto}

/* Drawer (under 900px): a left panel over a dimmed page. Restyles the old ☰ list. */
/* z-index 30 / 40 = tokens z-drawer-backdrop / z-drawer (docs/design-system/tokens.json),
   the same layers as the .drawer-* panel. */
.nav-drawer-backdrop{position:fixed;inset:0;z-index:30;background:rgb(0 0 0 / .4)}
.nav-drawer{position:fixed;top:0;left:0;bottom:0;z-index:40;
  display:flex;flex-direction:column;width:300px;max-width:85vw;
  background:rgb(var(--brand-700));box-shadow:var(--shadow-xl)}
.nav-drawer-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;
  height:4rem;flex-shrink:0;padding:0 .5rem 0 1rem;border-bottom:1px solid rgb(255 255 255 / .12)}
.nav-drawer-body{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:.5rem;
  scrollbar-color:rgb(255 255 255 / .3) transparent}
.nav-drawer-heading{padding:1rem .75rem .25rem;color:rgb(var(--brand-300))}
.nav-drawer-item{display:flex;align-items:center;gap:.75rem;width:100%;min-height:44px;
  padding:0 .75rem;border-radius:var(--radius-sm);text-align:left;
  font-size:.875rem;line-height:1.25rem;font-weight:500;color:rgb(var(--brand-100));
  transition:color .15s,background-color .15s}
.nav-drawer-item:hover{background:rgb(255 255 255 / .10);color:#fff}
.nav-drawer-item:focus-visible{outline:2px solid #fff;outline-offset:-2px}
.nav-drawer-item svg{flex-shrink:0}
.nav-drawer-divider{margin:.5rem .75rem;border:0;border-top:1px solid rgb(255 255 255 / .16)}
body.nav-drawer-lock{overflow:hidden}

/* ---------- My Account (client): views/profile/index.php ---------- */
/* Label / value row: label left (gray-500), value right-aligned (gray-900), long values wrap.
   Used in Account Info, Additional Contacts and Emergency Contact. */
.info-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;
  font-size:.875rem;line-height:1.25rem}
.info-row dt{flex-shrink:0;color:var(--gray-500)}
.info-row dd{min-width:0;text-align:right;overflow-wrap:anywhere;color:var(--gray-900);font-weight:400}
/* Address lines under the tag badges, indented 10px: in line with the first tag's text (.badge padding). */
.address-lines{padding-left:.625rem}

/* Dropdown (user menu — the app’s only dropdown, §6) */
.menu{position:absolute;right:0;z-index:10;margin-top:.25rem;width:12rem;
  border-radius:var(--radius-sm);background:#fff;padding:.25rem 0;
  box-shadow:var(--shadow-lg);border:1px solid rgb(0 0 0 / 0.05)}
/* width/text-align added 2026-08-12 (B5-15, OD-8) so <button> rows fill the
   panel — invoices/show.php's actions menu is four form-wrapped buttons, and a
   button does not stretch or left-align by default the way the existing <a>
   rows do. ZERO DELTA at the pre-existing site (views/layouts/app.php:136+),
   which uses <a class="menu-item">: already display:block, so already
   full-width and left-aligned. */
/* line-height restored 2026-08-12 (Task 9 finding F1, Nix ruled). The rule
   copied text-sm's font-size but not its paired line-height:1.25rem, so rows
   fell back to `normal` and grew 36px -> 37.5px, making the Options panel 4px
   taller than baseline. Both sites were `text-sm` before conversion
   (invoices/show.php buttons and layouts/app.php:136+ links), so this restores
   the original at both rather than setting a new size. Same defect class as
   Batch 3's F1 (.text-link, 52e2229): copy BOTH halves of a text-* utility. */
.menu-item{display:block;width:100%;text-align:left;padding:.5rem 1rem;
  font-size:.875rem;line-height:1.25rem;color:var(--gray-700)}
.menu-item:hover{background:var(--gray-50)}
/* Destructive row in a .menu (invoices/show.php "Remove Invoice"). Follows the
   -danger convention of .btn-danger / .btn-link-danger. Byte-copied from
   `text-red-600 hover:bg-red-50` — zero delta. Declared after .menu-item:hover
   so the red hover wins the same-specificity tie. */
.menu-item-danger{color:var(--red-600)}
.menu-item-danger:hover{background:var(--red-50)}

/* ---------- COMPONENTS: modal ---------- */
.modal-overlay{position:fixed;inset:0;z-index:50;display:flex;
  align-items:center;justify-content:center;
  background:rgb(0 0 0 / 0.4);padding:0 1rem}
/* OD-16 (Nix, 2026-08-12). booking-checkout.php's processing modal predates
   Alpine and is toggled by hand-written JS doing classList.add/remove('hidden').
   .hidden lives in the utilities block near the top of this file, so against
   .modal-overlay's display:flex — both (0,1,0) — the component won on source
   order and the modal would have been stuck permanently open. This compound
   (0,2,0) lets the existing toggle keep working, so no JavaScript in a live
   payment path had to be touched. Harmless to Alpine modals: x-show writes an
   inline display:none, which outranks everything here either way. */
.modal-overlay.hidden{display:none}
.modal-panel{width:100%;max-width:32rem;border-radius:var(--radius-lg);
  background:#fff;box-shadow:var(--shadow-xl);padding:1.5rem}
.modal-title{font-size:1rem;line-height:1.5rem;font-weight:600;color:var(--gray-900);margin-bottom:1rem}
.modal-footer{margin-top:1rem;display:flex;align-items:center;
  justify-content:flex-end;gap:.75rem}
/* Tall scrolling modal with fixed header/footer (products picker).
   .modal-panel is single-padded and non-scrolling; this is its split sibling.
   Reuses .modal-overlay (registered delta B3-15). */
.modal-panel-tall{display:flex;flex-direction:column;width:100%;max-width:32rem;
  max-height:85vh;border-radius:var(--radius);background:#fff;
  box-shadow:var(--shadow-xl)}
.modal-header{display:flex;align-items:center;justify-content:space-between;
  padding:1rem 1.5rem;border-bottom:1px solid var(--gray-100)}
.modal-body-scroll{flex:1;overflow-y:auto;padding:1rem 1.5rem}
/* Split footer: bordered, space-between. Distinct from .modal-footer above,
   which is borderless and right-aligned for the single-padded .modal-panel. */
.modal-footer-split{display:flex;align-items:center;justify-content:space-between;
  padding:1rem 1.5rem;border-top:1px solid var(--gray-100)}

/* ---------- COMPONENTS: reference panel (services/create.php sidebar) ----------
   The sticky "Existing services — for reference" panel. OD-C (Nix, 2026-08-10):
   its three arbitrary-value sizes are absorbed here rather than minted as
   global utilities — they exist for this panel only.
     max-h-[32rem] -> .ref-panel-scroll max-height
     text-[10px]   -> .ref-panel-group   (the type caps)
     text-[11px]   -> .ref-panel-meta    (the scheduling line)

   Task 9 finding F1 (ruled by Nix 2026-08-12: FIX, not accept). Both classes
   replace a Tailwind ARBITRARY font-size — `text-[10px]` / `text-[11px]` set
   font-size ONLY and never pair a line-height, so the markup kept the inherited
   `html{line-height:1.5}` number: 15px and 16.5px. Stating `line-height:1rem`
   silently changed both. `line-height:1.5` restores them exactly and says why.
   Same family as the Task-11 `.nav-link` finding and Batch 3's F3 — the rule is:
   a component replacing an arbitrary-value text size inherits its line-height,
   it does not get the paired value of the nearest named size.

   .ref-panel-group also needs its own font-weight: it replaces `font-semibold`
   (600), but `.badge-sq` above states 500 and would otherwise win on source
   order. This is the only `.badge-sq` site in Batch 4 whose markup was not
   `text-xs font-medium` — the other five are exact matches and untouched. */
.ref-panel-scroll{max-height:32rem;overflow-y:auto;border-top:1px solid var(--gray-100)}
.ref-panel-group{font-size:10px;line-height:1.5;font-weight:600}
/* The service rows inside the panel: `px-2 py-1.5 rounded hover:bg-gray-50`,
   byte-copied. Added Task 7 — the hover was the only token the kept-token audit
   could not account for, and app.css states no `hover:` utility variants at all
   (every hover in the file is a component's own :hover). Folding it in here
   keeps that true rather than opening the door to a hover: utility block. */
.ref-panel-item{padding:.375rem .5rem;border-radius:.25rem}
.ref-panel-item:hover{background:var(--gray-50)}
.ref-panel-meta{font-size:11px;line-height:1.5;color:var(--gray-500);margin-top:.125rem}

/* ---------- COMPONENTS: toggle switch (§1g — both sizes kept, named) ----------
   Batch 7a found this component ALREADY MATCHES the two real switch pages
   geometrically and had only been wired to the styleguide demo:
     .toggle          = tax.php's switch      (h-6 w-11, knob h-5 w-5, +1.25rem)
     .toggle .toggle-sm = hours-edit's switch (h-5 w-9,  knob h-4 w-4, +1rem)
   Two mismatches were found against the real markup and Nix ruled both on
   2026-08-17. Do not "restore" either — the markup is what changes, not this.
   - OD-H: the knob's shadow was --shadow-sm; both real pages write Tailwind
     `shadow`. The real pages won (13 live switches against one demo), so the
     knob now takes --shadow. The styleguide's demo switch gains a very
     slightly stronger knob shadow as a result. That is B7a-13, registered.
   - OD-I: keyboard focus was inconsistent in the markup — hours-edit showed
     NOTHING (`focus:outline-none` alone) and tax.php showed a ring with a
     white offset gap. One treatment wins app-wide: the :focus-visible rule
     below, which is the file's single --ring. hours-edit GAINS a focus
     highlight it did not have (an accessibility gain); tax.php loses the gap.
     Mouse users see no change on either. That is B7a-14, registered. */
.toggle{position:relative;display:inline-flex;height:1.5rem;width:2.75rem;
  flex-shrink:0;cursor:pointer;border-radius:9999px;border:none;
  background:var(--gray-200);transition:background-color .15s}
.toggle.is-on{background:rgb(var(--brand-600))}
.toggle:focus-visible{outline:none;box-shadow:var(--ring)}
.toggle-knob{display:inline-block;height:1.25rem;width:1.25rem;
  margin:.125rem;border-radius:9999px;background:#fff;
  box-shadow:var(--shadow);transition:transform .15s}   /* OD-H — see block note */
.toggle.is-on .toggle-knob{transform:translateX(1.25rem)}
.toggle-sm{height:1.25rem;width:2.25rem}
.toggle-sm .toggle-knob{height:1rem;width:1rem}
.toggle-sm.is-on .toggle-knob{transform:translateX(1rem)}

/* ---------- COMPONENTS: tabs (single instance today — clients/show) ---------- */
.tab{border-radius:var(--radius-sm);padding:.5rem .75rem;font-size:.875rem;
  line-height:1.25rem;font-weight:500;color:var(--gray-600);cursor:pointer;background:none;border:none}
.tab:hover{color:var(--gray-800)}
.tab.is-active{background:rgb(var(--brand-600));color:#fff}
/* Phone width: tighter tab pills so five tabs fit without a scroll strip (Batch 1 diff ruling 2026-08-05) */
@media (max-width:639px){.tab{padding-left:.5rem;padding-right:.5rem}}
/* Client pet page tab bar (portal QA F-50). Centred with wide gaps on a wide
   screen; on a phone the gaps close up and the row starts at the left, so if
   it still overflows it scrolls instead of cutting off the first tab. */
.pet-tabs{justify-content:center;gap:1.5rem}
@media (max-width:639px){.pet-tabs{justify-content:flex-start;gap:0}}

/* OD-V (Nix, 2026-08-17) — UNDERLINE tabs, a second and genuinely different tab
   idiom from the filled pill above. admin/client-requests' Pending/Resolved/All
   filter strip: a transparent 2px bottom edge that colours in when active,
   sitting on a gray-200 rule the markup draws on the wrapper and pulls up with
   `-mb-px`. Restyling it into .tab was rejected — it is a page-level filter,
   not an in-record section switch.
   The wrapper's border and the -mb-px overlap stay in the markup, for the same
   reason .save-bar leaves .sticky there: this rule owns the tab, not its
   container. Byte-copied from client_requests/index.php:18-21 — zero delta.
   `.is-active` matches .tab's own state-class convention (and .toggle.is-on,
   .option-panel.is-selected); do NOT introduce a fourth state-word here. */
.tab-underline{white-space:nowrap;border-bottom:2px solid transparent;
  padding-bottom:.75rem;font-size:.875rem;line-height:1.25rem;font-weight:500;
  color:var(--gray-500);transition:color .15s,border-color .15s}
.tab-underline:hover{color:var(--gray-700);border-bottom-color:var(--gray-300)}
.tab-underline.is-active{border-bottom-color:rgb(var(--brand-500));
  color:rgb(var(--brand-600))}
/* The active tab must not pick up the hover greys when moused over. Declared
   after the hover rule and at (0,2,0) so it wins both ties. */
.tab-underline.is-active:hover{color:rgb(var(--brand-600));
  border-bottom-color:rgb(var(--brand-500))}
/* ---------- COMPONENTS: segmented control (vaccination path toggle) ----------
   Values byte-copied from _vaccinations.php's two-button toggle; the wrapper
   absorbs its old w-fit/p-1/gap-1 (not whitelisted). */
.segment{display:inline-flex;align-items:center;gap:.25rem;border-radius:var(--radius);
  background:#fff;border:1px solid var(--gray-200);padding:.25rem}
.segment-btn{border-radius:var(--radius-sm);padding:.375rem .75rem;font-size:.75rem;
  line-height:1rem;font-weight:500;color:var(--gray-600);background:none;border:none;
  cursor:pointer;transition:color .15s,background-color .15s}
.segment-btn:hover{color:var(--gray-800)}
.segment-btn.is-active{background:rgb(var(--brand-600));color:#fff}
/* TameLink-9mc.1 — handoff variant: gray track, white active pill (used by the
   service form's Type / Scheduling / Cadence / Deposit-mode controls). */
.segment-track{background:var(--gray-100);border:none;padding:3px;gap:0;border-radius:var(--radius)}
.segment-track .segment-btn{padding:6px 11px;font-size:12.5px;font-weight:600;color:var(--gray-500);border-radius:6px}
.segment-track .segment-btn.is-active{background:#fff;color:var(--gray-900);box-shadow:0 1px 2px rgb(17 24 39 / .12)}
.segment-track .segment-btn:focus-visible{outline:none;box-shadow:var(--ring)}
.segment-track.segment-wrap{flex-wrap:wrap;display:inline-flex}

/* ---------- COMPONENTS: search dropdown (booking client/pet picker) ----------
   Byte-copied from bookings/create.php; absorbs z-20, shadow-lg, the first/last
   corner rounding and the row hover, none of which are bare utilities here. */
.dropdown-panel{position:absolute;z-index:20;margin-top:.25rem;width:100%;
  border-radius:var(--radius);border:1px solid var(--gray-200);
  background:#fff;box-shadow:var(--shadow-lg)}
.dropdown-item{display:flex;width:100%;align-items:center;gap:.75rem;
  padding:.625rem 1rem;text-align:left;font-size:.875rem;line-height:1.25rem;
  background:none;border:none;cursor:pointer}
.dropdown-item:hover{background:var(--gray-50)}
.dropdown-item:first-child{border-top-left-radius:var(--radius);
  border-top-right-radius:var(--radius)}
.dropdown-item:last-child{border-bottom-left-radius:var(--radius);
  border-bottom-right-radius:var(--radius)}
.dropdown-empty{position:absolute;z-index:20;margin-top:.25rem;width:100%;
  border-radius:var(--radius);border:1px solid var(--gray-200);background:#fff;
  padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem;
  color:var(--gray-500);box-shadow:var(--shadow-lg)}

/* ---------- COMPONENTS: month calendar (employees/schedule.php) ----------
   Only the parts that CANNOT survive as utilities live here: every rule below
   either carries a :hover (app.css has no variant utilities — five batches of
   precedent) or is a state Alpine toggles. The day cell's layout deliberately
   stays on the element as `p-1.5 min-h-12 flex flex-col items-center pt-2
   select-none`, so the markup still reads as a calendar cell. */

/* Month back/forward arrows. */
.cal-nav{border-radius:var(--radius-sm);padding:.5rem .75rem;
  color:var(--gray-500);font-size:1.125rem;line-height:1;
  transition:color .15s,background-color .15s}
.cal-nav:hover{color:var(--gray-700);background:var(--gray-100)}

/* Legend key, shared by BOTH calendars — the month calendar's availability
   legend and the week planner's event legend are the same 12px square. Minted
   as .cal-swatch in Task 8 and renamed here the moment it gained a second
   caller. The 2px corner is Tailwind's `rounded-sm` (.125rem) — off the app's
   radius scale, so it is stated literally rather than snapped to --radius-xs
   (4px), same call as .text-9px (OD-B). */
.legend-swatch{display:inline-block;width:.75rem;height:.75rem;
  border-radius:.125rem;border:1px solid}
.legend-swatch-sched{background:rgb(var(--brand-100));border-color:rgb(var(--brand-200))}
.legend-swatch-open{background:#fff;border-color:var(--gray-300)}
.legend-swatch-closed{background:var(--gray-200);border-color:var(--gray-300)}
.legend-swatch-pending{background:var(--yellow-200);border-color:var(--yellow-300)}
.legend-swatch-off{background:var(--red-200);border-color:var(--red-300)}
/* The legend's appointment-count pill, and the same pill on a cell once
   appointments ship. Not a .badge: that is a 12px pill-shaped status chip,
   this is a 9px square-cornered counter. */
.cal-count{display:inline-flex;align-items:center;justify-content:center;
  padding:.125rem .375rem;border-radius:var(--radius-xs);
  background:var(--blue-600);color:#fff;
  font-size:9px;font-weight:700;line-height:1}

/* Day cells — one class per state, replacing the $bgClass/$hoverClass pair.
   cursor:pointer is folded into the two clickable states because those are
   exactly the states that carry the @click. */
.cal-cell-off{background:var(--red-50);cursor:pointer}
.cal-cell-off:hover{background:var(--red-100)}
.cal-cell-pending{background:var(--yellow-50);cursor:pointer}
.cal-cell-pending:hover{background:var(--yellow-100)}
.cal-cell-work{background:#fff}
.cal-cell-nonwork{background:var(--gray-100)}
.cal-cell-outside{background:var(--gray-50)}
/* Selected day. A compound on the cell, so it beats the base on specificity —
   and an INSET box-shadow rather than a ring-* utility (OD-4/OD-9), which keeps
   the cell's box exactly 0px bigger and so cannot shift the grid. */
.cal-cell-off.is-selected,
.cal-cell-pending.is-selected{box-shadow:inset 0 0 0 2px rgb(var(--brand-500))}
/* Today's date number, shown as a filled disc instead of bare text. */
.cal-today{display:flex;align-items:center;justify-content:center;
  width:1.5rem;height:1.5rem;border-radius:9999px;
  background:rgb(var(--brand-600));color:#fff;
  font-size:.75rem;line-height:1rem;font-weight:600}

/* Recurring-availability EDIT row. The enabled state has to be a compound state
   class rather than the `bg-brand-50 border-brand-200` utilities Alpine used to
   write: the utilities block sits EARLIER in this file than the components, so
   a border-colour utility would LOSE to this component's own border-color and
   an enabled row would have kept its gray border the moment the CDN was
   dropped. That is the OD-16 trap from Batch 5, caught here before it shipped.
   It states ONLY the box and the two colours: schedule.php's row is a flex row
   at px-4 py-3 and schedules.php's is a block at px-3 py-2, so layout and
   padding stay on the element as utilities, where they cannot collide. */
.sched-day{border-radius:var(--radius);border:1px solid var(--gray-200);
  background:var(--gray-50)}
.sched-day.is-enabled{background:rgb(var(--brand-50));
  border-color:rgb(var(--brand-200))}

/* ---- Batch 6a Task 9 whitelist additions (2026-08-14) ---- */
/* THE ALPINE-TRANSITION GAP. Every rule in this block was MISSING and is
   invisible to the kept-token audit, which only reads class="" — these are
   written in `x-transition:enter="…"` / `:leave-start="…"` attributes and
   applied by Alpine at runtime. Same trap class as Batch 3's .rotate-180.
   Four views depend on them (bookings/show, clients/show, pets/show,
   employees/schedules), three of which were converted in EARLIER batches and
   would have lost their fade/slide the moment the CDN is dropped at Batch 10.
   Values are Tailwind v3.4.17's, so this is zero delta today.
   NB `duration-250`, used once on the schedules.php drawer, is NOT a Tailwind
   class (the scale is 75/100/150/200/300/500/700/1000) — it does nothing today,
   so it is deliberately NOT defined here. Adding it would SPEED UP nothing and
   SLOW the drawer from .transition's default 150ms to 250ms, i.e. a behaviour
   change. Logged as a finding instead. */
.transition{transition-property:color,background-color,border-color,
  text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;
  transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}
.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}
.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}
.duration-100{transition-duration:.1s}
.duration-150{transition-duration:.15s}
.duration-200{transition-duration:.2s}
.opacity-0{opacity:0}
.opacity-100{opacity:1}
/* No other transform utility lands on the two elements that use these, so the
   plain transform is exact — no need for Tailwind's --tw-translate-x plumbing. */
.translate-x-0{transform:translateX(0)}
.translate-x-full{transform:translateX(100%)}
.-translate-x-full{transform:translateX(-100%)}   /* nav drawer slides in from the left */

/* Scale gaps the audit did find on schedules.php. */
.gap-0\.5{gap:.125rem}
.gap-x-2{column-gap:.5rem}
.gap-y-0\.5{row-gap:.125rem}
.gap-y-1\.5{row-gap:.375rem}
.border-l{border-left-width:1px;border-left-style:solid}
.border-r{border-right-width:1px;border-right-style:solid}
.border-collapse{border-collapse:collapse}
.border-current{border-color:currentColor}
.border-gray-400{border-color:var(--gray-400)}
.divide-gray-50>:not([hidden])~:not([hidden]){border-color:var(--gray-50)}
.w-1\.5{width:.375rem}.h-1\.5{height:.375rem}
.w-2{width:.5rem}.h-2{height:.5rem}
.w-36{width:9rem}
.left-0{left:0}.right-0{right:0}
.right-2{right:.5rem}
.opacity-30{opacity:.3}.opacity-60{opacity:.6}.opacity-70{opacity:.7}
.overflow-y-auto{overflow-y:auto}
.leading-tight{line-height:1.25}
.align-middle{vertical-align:middle}
.pt-0\.5{padding-top:.125rem}
.px-1\.5{padding-left:.375rem;padding-right:.375rem}
.py-10{padding-top:2.5rem;padding-bottom:2.5rem}
.bg-brand-500{background-color:rgb(var(--brand-500))}
.text-orange-500{color:var(--orange-700)}   /* AA on white (t1o6) */
.text-yellow-600{color:var(--yellow-700)}   /* AA on white (t1o6) */
/* the availability grid dot for a staff member with no colour assigned */
.bg-gray-200{background-color:var(--gray-200)}

/* ---------- COMPONENTS: week planner (employees/schedules.php) ----------
   The planner positions EVERYTHING with inline style="" — px offsets computed
   from minutes in PHP, and staff colours read from the database. None of that
   is touched here; these rules carry only the chrome around it. */

/* Today's column gets a 20%-strength brand tint. The day HEADER above it keeps
   the full-strength .bg-brand-50 utility — the two are deliberately different. */
.planner-day-today{background:rgb(var(--brand-50) / .2)}

/* A staff member's scheduled-hours bar. Opacity lives here (not inline) so
   both the hover-to-full-opacity and the drag-dim (.staff-bar-dragging)
   states actually take effect. */
.staff-bar{position:absolute;cursor:grab;opacity:.6;transition:opacity .15s}
.staff-bar:active{cursor:grabbing}
.staff-bar:hover{opacity:1}

/* Appointment blocks. --radius-xs per OD-A: these are small stacked blocks and
   they keep their 4px corners. `border:1px solid` with no colour matches the
   markup's bare `border` + a separate colour class; the per-event inline
   `border-left:3px solid <staff colour>` and the pending `border-style:dashed`
   still override it, which is why the shorthand is safe here. */
.event-block{position:absolute;right:.125rem;overflow:hidden;
  border-radius:var(--radius-xs);border:1px solid}
.event-block-arrival{background:var(--green-50);border-color:var(--green-300);color:var(--green-800)}
.event-block-departure{background:var(--orange-50);border-color:var(--orange-300);color:var(--orange-800)}
.event-block-appt{background:var(--blue-50);border-color:var(--blue-200);color:var(--blue-800)}
.event-block-group{background:var(--gray-50);border-color:var(--gray-300);color:var(--gray-600)}

/* Employee schedule planner — shift drag-and-drop
   (admin/employees-schedules.php). Drag mechanics are plain HTML5
   drag-and-drop (no library), matching the booking calendar's pattern; these
   classes are only ever toggled from views/admin/employees/schedules.php's
   inline script. */
.planner-day.drag-over{background:rgb(var(--brand-50));outline:2px dashed rgb(var(--brand-300));outline-offset:-2px}
.staff-bar-dragging{opacity:.15}
.staff-bar-flash{animation:staff-bar-flash .7s ease-out}
@keyframes staff-bar-flash{0%{box-shadow:0 0 0 3px var(--green-300)}100%{box-shadow:0 0 0 0 transparent}}

/* Legend swatch colours used by the planner (the month calendar's five live
   with .legend-swatch above). */
.legend-swatch-appt{background:var(--blue-100);border-color:var(--blue-200)}
.legend-swatch-arrival{background:var(--green-50);border-color:var(--green-300)}
.legend-swatch-departure{background:var(--orange-50);border-color:var(--orange-300)}
.legend-swatch-tentative{background:var(--blue-50);border-color:var(--blue-200)}

/* The 10px round dot that carries a staff member's colour. The colour itself
   stays inline — it comes from the database, per staff member. */
.staff-dot{width:.625rem;height:.625rem;border-radius:9999px;flex-shrink:0}

/* Week availability grid. Deliberately NOT .data-table: that pads cells to
   .75rem 1.5rem, and this table has SEVEN day columns inside a horizontal
   scroller, so it would have grown ~168px wider and fought the inline
   min-width. Only the frame lives here; per-cell padding and alignment stay as
   utilities, which is also what lets the Employee column keep its wider px-4. */
.week-grid{width:100%;font-size:.875rem;line-height:1.25rem;border-collapse:collapse}
.week-grid thead tr{border-bottom:1px solid var(--gray-100)}
.week-grid tbody tr+tr{border-top:1px solid var(--gray-50)}
.week-grid tbody tr:hover{background:var(--gray-100);transition:background-color .15s}
/* Full-day time-off pill. Not .badge: this one is OUTLINED (gray-200 hairline),
   and dropping that border to reuse .badge would visibly change its shape. */
.week-off-pill{display:inline-flex;align-items:center;gap:.25rem;
  border-radius:9999px;background:var(--gray-100);border:1px solid var(--gray-200);
  padding:.125rem .5rem;color:var(--gray-600);font-weight:500}

/* Assigned-service pill on settings/hours.php — a submit button shaped as a
   pill, with an "×" that darkens on its own hover. Standalone rather than
   .btn + .btn-brand-soft on the .week-off-pill precedent above: .btn's
   transparent 1px border would grow it 2px in both axes and its radius and
   padding are both wrong here, so extending it would cost a delta this way
   round costs nothing. Byte-copied from hours.php's `inline-flex items-center
   gap-1.5 rounded-full bg-brand-50 px-3 py-1 text-xs font-medium text-brand-700
   hover:bg-brand-100 transition-colors`. The × keeps its OWN :hover rather than
   inheriting the pill's, exactly as the markup has it. Batch 7a Task 6. Zero
   delta. */
.service-pill{display:inline-flex;align-items:center;gap:.375rem;
  border-radius:9999px;border:none;background:rgb(var(--brand-50));
  padding:.25rem .75rem;font-size:.75rem;line-height:1rem;font-weight:500;
  color:rgb(var(--brand-700));cursor:pointer;transition:background-color .15s}
.service-pill:hover{background:rgb(var(--brand-100))}
.service-pill-x{color:rgb(var(--brand-400))}
.service-pill-x:hover{color:rgb(var(--brand-hover))}

/* On/off state pill with a leading dot — settings/services.php's "Public
   Booking" and "Add-On Only" columns, where each pill is itself the submit
   button that flips the value. Three colour arms because the two columns use
   different "on" hues (brand for public booking, purple for add-on) and share
   one "off". Byte-copied from services.php:90-120; OUTLINED, which is why it
   is not .service-pill above — that one is borderless and would come out 2px
   smaller. The leading dot needs NO new name: .dot / .dot-brand / .dot-gray /
   .dot-purple in the badges section above are already exactly `w-2 h-2
   rounded-full` in these three colours. Batch 7a Task 8, Nix approved the
   family 2026-08-17. Zero delta. */
.status-pill{display:inline-flex;align-items:center;gap:.375rem;
  border-radius:9999px;border:1px solid transparent;
  padding:.25rem .75rem;font-size:.75rem;line-height:1rem;font-weight:600;
  cursor:pointer;transition:color .15s,background-color .15s,border-color .15s}
.status-pill-on{background:rgb(var(--brand-50));
  border-color:rgb(var(--brand-300));color:rgb(var(--brand-700))}
.status-pill-on:hover{background:rgb(var(--brand-100))}
.status-pill-purple{background:var(--purple-50);
  border-color:var(--purple-300);color:var(--purple-700)}
.status-pill-purple:hover{background:var(--purple-100)}
.status-pill-off{background:var(--gray-50);
  border-color:var(--gray-200);color:var(--gray-500)}
.status-pill-off:hover{border-color:var(--gray-300);color:var(--gray-600)}

/* Collapsible section header (the "No Availability Schedule Completed" panel). */
.collapse-toggle{display:flex;width:100%;align-items:center;justify-content:space-between;
  padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:500;
  color:var(--gray-600);background:none;border:none;cursor:pointer;
  transition:background-color .15s}
.collapse-toggle:hover{background:var(--gray-50)}

/* ---------- COMPONENTS: slide-in drawer (employees/schedules.php) ----------
   A right-edge, full-height panel. Distinct from the .modal-* family above,
   which is a centred dialog: the backdrop is lighter (20% vs 40%) and sits at
   a lower z-index so the drawer can stack over it. Alpine slides it with the
   x-transition translate-x-* utilities added in this batch's whitelist. */
.drawer-backdrop{position:fixed;inset:0;background:rgb(0 0 0 / .2);z-index:30}
.drawer-panel{position:fixed;right:0;top:0;z-index:40;
  display:flex;flex-direction:column;height:100%;width:100%;max-width:28rem;
  background:#fff;box-shadow:var(--shadow-xl)}
.drawer-header{display:flex;align-items:center;justify-content:space-between;
  padding:1rem 1.25rem;border-bottom:1px solid var(--gray-100)}
.drawer-body{flex:1;overflow-y:auto;padding:1rem 1.25rem}

/* Compact inline alert (the drawer's three per-day warnings). Composes with the
   .alert-* colours; .alert-orange and .alert-neutral extend that family rather
   than starting a parallel one. */
.alert-xs{display:flex;align-items:flex-start;gap:.375rem;
  border-radius:var(--radius-xs);padding:.375rem .5rem;
  font-size:.75rem;line-height:1rem}
.alert-orange{background:var(--orange-50);border-color:var(--orange-200);color:var(--orange-800)}
.alert-neutral{background:var(--gray-50);border-color:var(--gray-200);color:var(--gray-600)}

/* ============================================================
   Batch 6b vocabulary (2026-08-14) — Task 1, css/app.css only.
   Minted BEFORE any view is converted; Tasks 2-7 consume these.

   SCOPE NOTE (why no colour utilities are added here): the Batch 6b
   audit lists 17 undefined colour tokens, but every one of them sits
   inside something that becomes a component — the two flash boxes
   (-> .alert-success/.alert-error), the two clock buttons (below),
   the dashboard/profile badges (-> .badge + model helpers), the
   avatar circles, and the field error state. Defining them as
   utilities up front would leave rules nothing ends up using, which
   is the opposite of what this phase is for. Colour utilities are
   therefore added by the task that PROVES one survives as a utility
   — the same way Batches 4, 5 and 6a did it (see the "Task 7 gaps"
   and "Task 8 gaps" notes in the whitelist blocks above).
   ============================================================ */

/* ---- B6b-11: off-scale value, OD-B (exact value, unit-suffixed) ----
   my-schedule's day cell is `min-h-[88px]`. The scale runs 80/96, so 88
   is genuinely off-scale and keeps its literal value rather than being
   rounded to a neighbour. Zero delta. */
.min-h-88px{min-height:88px}

/* ---- B6b-7: the oversized clock-in / clock-out buttons ----
   timeclock/index.php's two big touch targets. Byte-copied from
   `w-full max-w-xs rounded-xl px-6 py-5 text-xl font-bold text-white
   transition-colors shadow-md` plus the colour arms.

   STANDALONE — do NOT pair with .btn. Same shape as .btn-close and
   .btn-link above: the `btn-` prefix marks the family, not a required
   base class. Pairing with .btn would add its `border:1px solid
   transparent` and grow this button 2px in both axes (the BOX-MODEL
   RULE at the top of the button section), and the whole point of this
   component is that the box is preserved exactly.

   `display` is deliberately NOT set: the button is inline-block by
   default and its parent is text-center, which is what centres it.
   Forcing `display:block` would left-align it.

   No :focus-visible ring is declared, deliberately. The markup has no
   focus classes today, so adding one would be an unregistered visual
   change on the highest-risk element in the batch; the UA focus ring
   still applies. If a ring is ever wanted here it must be registered
   and shot, not slipped in.

   The :active (pressed) state is REQUIRED, not decorative — staff use
   this on a phone every shift, and a lost pressed state reads as
   "the tap didn't register". */
.btn-clock{width:100%;max-width:20rem;
  border-radius:var(--radius-lg);padding:1.25rem 1.5rem;
  font-size:1.25rem;line-height:1.75rem;font-weight:700;color:#fff;
  box-shadow:var(--shadow-md);cursor:pointer;
  transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;
  transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}
.btn-clock-in{background:var(--green-600)}
.btn-clock-in:hover{background:var(--green-700)}
.btn-clock-in:active{background:var(--green-800)}
.btn-clock-out{background:var(--red-600)}
.btn-clock-out:hover{background:var(--red-700)}
.btn-clock-out:active{background:var(--red-800)}

/* ---- B6b-12 / OD-D: link card (parent hover restyles its children) ----
   OD-D ruling (Nix, 2026-08-14): Tailwind's `group` / `group-hover:*` has
   no mechanism in app.css, and rebuilding a generic one was rejected. The
   parent is a named component that owns the WHOLE hover through descendant
   selectors; the children carry no hover instructions at all.

   Named parts rather than element selectors (`:hover p:first-child`) so the
   rules survive the markup being re-ordered or re-tagged.

   NOT a sibling of .group-heading / .group-heading-lg above — those are
   unrelated section labels. Nothing about this component is a "group".

   SMALL VARIANT ONLY, per the B6a-16 scope principle: this is the shell
   profile/index.php uses (rounded-lg, gray-200 border, no fill, no shadow).
   The three later sites (settings/index, email-templates/index) are
   card-shaped — rounded-xl, white, gray-100, shadow-sm, px-6 py-4, and a
   hover that adds shadow instead of a fill. They get a modifier in the
   batch that converts them; do not pre-build it here.
   report_cards/edit.php:198 is a DIFFERENT shape (a photo overlay revealed
   on hover) and is explicitly out of this family. */
.link-card{display:flex;align-items:center;justify-content:space-between;
  border:1px solid var(--gray-200);border-radius:var(--radius);
  padding:.75rem 1rem;
  transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);
  transition-duration:.15s}
.link-card-title{font-size:.875rem;line-height:1.25rem;font-weight:500;color:var(--gray-900)}
.link-card-sub{margin-top:.125rem;font-size:.75rem;line-height:1rem;color:var(--gray-500)}
.link-card-arrow{flex-shrink:0;width:1rem;height:1rem;color:var(--gray-300)}
.link-card:hover{border-color:rgb(var(--brand-300));background:rgb(var(--brand-50))}
.link-card:hover .link-card-title{color:rgb(var(--brand-hover))}
.link-card:hover .link-card-arrow{color:rgb(var(--brand-500))}

/* ============================================================
   Batch 7b vocabulary (2026-08-17) — Task 1, css/app.css only.
   Minted BEFORE any view is converted; Tasks 2-13 consume these.

   The batch's twelve views are mostly routine — existing homes already
   cover their buttons, links, form fields, flash boxes, tables, cards
   and status pills. Everything genuinely NEW belongs to two pages, the
   Report Cards editor and Reports, and is below. The batch's smaller
   additions (dots, tags, list rows, underline tabs, the save bar, the
   amber note, four in-family arms and three compound remedies) live
   WITH their families further up this file rather than here, so a
   reader looking at .badge or .alert sees the whole set at once.
   ============================================================ */

/* ---------- COMPONENTS: pick tiles (report card mood/appetite/activity) ------
   OD-O (Nix, 2026-08-17). Eleven "choose one" tiles across three groups on
   report_cards/edit.php. Alpine keeps ownership of the CHOICE; this component
   owns the LOOK, and the binding narrows to writing a state class:

     :class="mood === 'happy' ? 'is-selected' : ''"

   The @click handler and the Alpine expression itself are NOT touched — same
   discipline as .toggle.is-on (OD-H/OD-I) and for the same reason: a :class
   that toggles a UTILITY against a component's own background loses, because
   the utilities block sits earlier in this file. `.is-selected` matches
   .option-panel.is-selected, the app's existing selectable-card state word.

   The markup's `ring-2 ring-brand-500` IS this file's --ring (a 2px brand-500
   box-shadow), so the selected state reuses the token rather than restating the
   value — which also means the tiles follow the client's palette for free.
   Byte-copied from report_cards/edit.php:79-82 — zero delta. */
.pick-tile{border:1px solid var(--gray-200);border-radius:var(--radius-lg);
  padding:1rem;text-align:center;background:var(--gray-50);cursor:pointer;
  transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);
  transition-duration:.15s}
.pick-tile:hover{background:var(--gray-100);border-color:var(--gray-400)}
.pick-tile.is-selected{background:rgb(var(--brand-50));box-shadow:var(--ring-flat)}
/* Selected wins over hover — declared after, and (0,2,0) against (0,1,0). */
.pick-tile.is-selected:hover{background:rgb(var(--brand-50))}
.pick-tile-emoji{font-size:1.5rem;line-height:2rem;margin-bottom:.25rem}
.pick-tile-label{font-size:.75rem;line-height:1rem;font-weight:500;color:var(--gray-700)}

/* ---------- COMPONENTS: photo tile (report card photo grid) ------------------
   OD-P (Nix, 2026-08-17). A thumbnail whose "Remove" control is hidden until
   the tile is hovered. The TILE owns the whole behaviour through a descendant
   selector and the overlay carries no hover instruction of its own — OD-D's
   shape, replacing Tailwind's `group` / `group-hover:*`, for which this file
   has no mechanism and a generic one was rejected.

   Explicitly NOT a member of the .link-card family: that note already says so
   ("report_cards/edit.php:198 is a DIFFERENT shape ... explicitly out of this
   family"). This one is a photo with a veil, not a bordered row that tints.

   Introduces the app's first half-transparent black. It is stated HERE rather
   than as a `bg-black/50` utility because it exists for this veil only; the two
   slash-opacity utilities that DID stay utilities (roles' zebra rows) are in
   the whitelist block. Byte-copied from report_cards/edit.php:198-212 — zero
   delta. NOTE for anyone testing this page: the Remove button fires an
   onsubmit confirm() dialog, which blocks browser automation. Do not click it. */
.photo-tile{position:relative;border-radius:var(--radius);overflow:hidden;
  background:var(--gray-100)}
.photo-tile-img{height:7rem;width:100%;object-fit:cover}
.photo-tile-overlay{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;background:rgb(0 0 0 / 0.5);opacity:0;
  transition:opacity .15s}
.photo-tile:hover .photo-tile-overlay{opacity:1}
/* The veil's own action button — a white pill, not a .btn: .btn's 8/16 padding
   and 8px corners would fill most of a 112px-tall thumbnail. Byte-copied from
   `rounded-full bg-white px-3 py-1 text-xs font-semibold text-red-600
   hover:bg-red-50`. */
.photo-tile-btn{border-radius:9999px;border:none;background:#fff;
  padding:.25rem .75rem;font-size:.75rem;line-height:1rem;font-weight:600;
  color:var(--red-600);cursor:pointer;transition:background-color .15s}
.photo-tile-btn:hover{background:var(--red-50)}

/* ---------- COMPONENTS: chart bar + meter (admin/reports) --------------------
   OD-Q (Nix, 2026-08-17). Two unrelated data shapes that happen to share a
   page: the 12 vertical revenue columns, and the horizontal occupancy meters
   in the table below them.

   THE BARS. Height is data and stays where it is — an Alpine `:style` binding
   the view computes. This component owns everything that is NOT data. The
   current month is highlighted by an arm rather than a :class toggling a
   `bg-brand-*` utility, for the usual reason: the component's own background
   would win the tie. Byte-copied from reports/index.php:19-21. */
.chart-bar{width:100%;
  border-top-left-radius:var(--radius-xs);border-top-right-radius:var(--radius-xs);
  transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);
  transition-duration:.3s}
.chart-bar-current{background:rgb(var(--brand-500))}
.chart-bar-past{background:rgb(var(--brand-200))}

/* THE METERS. Track and fill. Width is data and stays as the inline style the
   view already writes; the three colour arms replace the `$barColor` map at
   reports/index.php:101-105, which stays in that view's own PHP — it is a
   page-level presentation rule, not a domain fact, so it does NOT move to a
   Model the way statusBadgeClass() did. Thresholds unchanged: >=90% high,
   >=70% warn, otherwise the brand fill. Byte-copied — zero delta. */
.meter{flex:1;height:.5rem;border-radius:9999px;background:var(--gray-100)}
.meter-fill{height:.5rem;border-radius:9999px;background:rgb(var(--brand-400))}
.meter-fill-warn{background:var(--orange-400)}
.meter-fill-high{background:var(--red-500)}

/* ---------- COMPONENTS: print (portal/agreements-view.php) -------------------
   OD-AA (Nix, 2026-08-18). The rework's first print stylesheet, promoted from
   a page-local inline <style> block so later print-heavy views (report_cards,
   invoices, contracts) share it instead of re-inventing print CSS per page.
   Byte-copied from the inline block — zero delta. Batch 8 Task 1. */
@media print {
  nav, .no-print{display:none!important}
  main{padding:0!important;max-width:100%!important}
  .print-card{box-shadow:none!important;border:none!important}
}

/* ---------- COMPONENTS: signature pad (portal/agreements-sign.php) -----------
   One consumer, no existing analog to extend. Byte-copied from
   `relative border-2 border-gray-300 rounded-lg bg-white overflow-hidden`
   (wrapper, inline max-width:480px) / `w-full cursor-crosshair touch-none
   block` (canvas) / `absolute bottom-2 left-0 right-0 border-t border-dashed
   border-gray-300 mx-4 pointer-events-none` (guideline). Batch 8 Task 1. */
.signature-pad{position:relative;border:2px solid var(--gray-300);
  border-radius:var(--radius);background:#fff;overflow:hidden;max-width:30rem}
.signature-pad-canvas{width:100%;cursor:crosshair;touch-action:none;display:block}
.signature-pad-guideline{position:absolute;bottom:.5rem;left:0;right:0;
  border-top:1px dashed var(--gray-300);margin:0 1rem;pointer-events:none}

/* ---------- COMPONENTS: pet profile grid (admin/pets-show.php, portal/pet.php)
   TameLink-xb1s (Nix, 2026-10-08). Replaces `grid grid-cols-1 gap-6
   lg:grid-cols-5` + `lg:col-span-2|3 space-y-5` on the Profile tab.
     >=1024px  the same 2/5 + 3/5 split as before — zero delta.
     768-1023  two equal columns (was one): a half-screen window or a tablet.
     <768px    one column. The column wrappers drop out (display:contents) so
               the cards of both columns can interleave, and .pet-stack-N puts
               them in Nix's order: Details, Bookings, Medications, Allergies,
               Feeding, Medical Notes, Personality Notes, Veterinarian.
   .pet-stack-N is stated only inside the <768 query on purpose: `order` also
   applies to flex children, so outside it would reshuffle the desktop columns. */
.pet-profile-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:1.25rem}
.pet-profile-col{display:contents}
@media (max-width:767px){
  .pet-stack-1{order:1} .pet-stack-2{order:2} .pet-stack-3{order:3} .pet-stack-4{order:4}
  .pet-stack-5{order:5} .pet-stack-6{order:6} .pet-stack-7{order:7} .pet-stack-8{order:8}
}
@media (min-width:768px){
  .pet-profile-grid{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:1.5rem}
  .pet-profile-col{display:flex;flex-direction:column;gap:1.25rem;min-width:0}
}
@media (min-width:1024px){
  .pet-profile-grid{grid-template-columns:repeat(5,minmax(0,1fr))}
  .pet-profile-col-side{grid-column:span 2/span 2}
  .pet-profile-col-main{grid-column:span 3/span 3}
}

/* ---------- COMPONENTS: dashboard (dashboard.php) ----------------------------
   TameLink-b7r.3 (Nix, 2026-09-09). Handoff docs/design_handoff_staff_dashboard
   §1 (option 1a) translated from Tailwind utilities into one component family.
   Greens are the runtime brand ramp; neutrals are the --gray-N tokens; the
   Ready pill uses the handoff's blue-100/800.
   Sizes are the handoff's px values verbatim (22/16/14.5/13.5/12.5/11.5). */
.dash-header{display:flex;align-items:flex-end;justify-content:space-between;
  gap:1rem;margin-bottom:1.25rem}
.dash-greeting{font-size:22px;font-weight:700;color:var(--gray-900);
  letter-spacing:-.3px;line-height:1.2}
.dash-subline{margin-top:.25rem;font-size:13px;color:var(--gray-500)}
.dash-header-actions{display:flex;gap:.5rem;flex-shrink:0}
.dash-btn{display:inline-flex;align-items:center;padding:.5rem .875rem;
  border-radius:var(--radius);font-size:13px;font-weight:600;line-height:1.25;
  border:1px solid transparent;white-space:nowrap;
  transition:background-color .15s,border-color .15s,color .15s}
.dash-btn-clock{background:#fff;border-color:var(--gray-300);color:var(--gray-700)}
.dash-btn-clock:hover{background:var(--gray-50);border-color:var(--gray-400)}
.dash-btn-new{background:rgb(var(--brand-600));color:#fff}
.dash-btn-new:hover{background:rgb(var(--brand-hover))}

/* Every track is minmax(0,…): a bare fr track will not shrink below its
   content, so a long booking line pushed the right-hand column off the page
   on a narrow or portrait screen. min-width:0 lets the children shrink with
   their track instead of setting the floor. */
.dash-grid-top{display:grid;
  grid-template-columns:minmax(0,1.6fr) minmax(0,.8fr) minmax(0,.8fr) minmax(0,.8fr);
  gap:1rem;margin-bottom:1rem}
.dash-grid-main{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);
  gap:1rem;align-items:start}
.dash-col{display:flex;flex-direction:column;gap:1rem;min-width:0}
/* Left column: In House Today with Needs Attention under it (Nix, 2026-10-24). */
.dash-stack{display:flex;flex-direction:column;gap:1rem;min-width:0}
.dash-grid-top>*,.dash-grid-main>*{min-width:0}
/* Four summary cards across stop being readable well before the two-column
   body does, so they pair up first. */
@media (max-width:1199px){
  .dash-grid-top{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
}
/* TameLink-xb1s (Nix, 2026-10-08). The body used to stack below 1024px, which
   a half-screen window on a 1920 monitor (960px) already is. It now holds two
   columns down to 900px; 900-1023 drops the staff column so the pet name keeps
   the room it had at 1024. */
@media (min-width:900px) and (max-width:1023px){
  .dash-row-staff{display:none}
}
/* Portrait tablet / small window: one column — In House Today and Needs
   Attention first, then revenue and the To Do list, each full width. (The
   right column used to pair its three cards; with Needs Attention moved left
   it has only two, Nix 2026-10-24.) */
@media (max-width:899px){
  .dash-grid-main{grid-template-columns:minmax(0,1fr)}
}
/* Phone: one column. A booking row wraps — pet and client on the first line,
   status pill and action button on the second, indented to the name. */
@media (max-width:639px){
  .dash-grid-top{grid-template-columns:minmax(0,1fr)}
  .dash-header{flex-direction:column;align-items:flex-start}
  .dash-row{flex-wrap:wrap}
  .dash-row .dash-row-body{flex-basis:calc(100% - 34px - .75rem)}  /* beats the later flex:1 */
  .dash-row-time,.dash-row-staff,.dash-action-spacer{display:none}
  .dash-row .dash-pill{margin-left:calc(34px + .75rem)}
}

.dash-card{background:#fff;border:1px solid var(--gray-100);
  border-radius:var(--radius-lg);box-shadow:var(--shadow-sm)}
.dash-card-pad{padding:18px 20px}
.dash-card-header{display:flex;align-items:center;justify-content:space-between;
  gap:.75rem;padding:.875rem 1.25rem;border-bottom:1px solid var(--gray-100)}
.dash-card-title{font-size:14.5px;font-weight:700;color:var(--gray-900)}
.dash-card-sub{font-size:12px;color:var(--gray-500);margin-top:.125rem}  /* F74 contrast */
.dash-card-footer{padding:.75rem 1.25rem;border-top:1px solid var(--gray-100)}
.dash-label{font-size:11px;font-weight:600;text-transform:uppercase;
  letter-spacing:.6px;color:var(--gray-500);margin-bottom:.75rem}
.dash-label-row{display:flex;align-items:center;justify-content:space-between}
.dash-label-count{font-size:11.5px;font-weight:400;text-transform:none;
  letter-spacing:0;color:var(--gray-500)}

.dash-big{font-size:30px;font-weight:700;line-height:1;color:rgb(var(--brand-700))}
.dash-big-alert{color:var(--orange-700)}
.dash-big-muted{font-size:14px;font-weight:500;color:var(--gray-500);margin-left:.25rem}
.dash-meta{font-size:12px;color:var(--gray-500);margin-top:.5rem}
.dash-total{display:flex;align-items:baseline;gap:.5rem;
  font-size:13px;font-weight:600;color:var(--gray-700);margin-top:.35rem}
.dash-kv{display:flex;justify-content:space-between;font-size:12px;
  color:var(--gray-500);margin-top:.375rem}
.dash-kv b{font-weight:600;color:var(--gray-700)}
.dash-kv-link{color:inherit;text-decoration:underline;text-underline-offset:2px}
.dash-kv-link:hover{color:rgb(var(--brand-hover))}

.dash-next{display:flex;align-items:center;gap:.75rem}
.dash-next-body{flex:1;min-width:0}
.dash-next-title{font-size:16px;font-weight:700;color:var(--gray-900);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dash-next-meta{font-size:12.5px;color:var(--gray-500);margin-top:.125rem;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dash-chip{flex-shrink:0;font-size:12px;font-weight:600;
  color:rgb(var(--brand-600));background:rgb(var(--brand-50));
  border-radius:var(--radius);padding:.375rem .625rem;white-space:nowrap}
.dash-empty{font-size:13px;color:var(--gray-500)}

.dash-avatar{display:flex;align-items:center;justify-content:center;
  flex-shrink:0;width:34px;height:34px;border-radius:9999px;
  background:rgb(var(--brand-50));color:rgb(var(--brand-700));
  font-size:13px;font-weight:700;object-fit:cover;overflow:hidden}
.dash-avatar-lg{width:40px;height:40px;font-size:15px;
  background:rgb(var(--brand-100));color:rgb(var(--brand-800))}

.dash-shift-row{display:flex;align-items:baseline;gap:.5rem;padding:.3rem 0}
.dash-shift-name{font-size:12.5px;font-weight:600;color:var(--gray-900)}
.dash-shift-hours{font-size:12px;color:var(--gray-500)}  /* F74 contrast */

.dash-row{display:flex;align-items:center;gap:.75rem;padding:.625rem 1.25rem;
  border-bottom:1px solid var(--gray-50)}
.dash-row:last-child{border-bottom:0}
.dash-row-body{flex:1;min-width:0}
.dash-row-name{font-size:13.5px;font-weight:600;color:var(--gray-900);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dash-row-name a:hover{color:rgb(var(--brand-hover))}
/* Dashboard text links underlined like the rest of the app (TameLink-gyb1).
   Buttons and whole-row links (.dash-btn-floor, .dash-list-row) stay plain. */
.dash-next-title a,.dash-row-name a,.dash-footer-link{text-decoration:underline;text-underline-offset:2px}
.dash-row-meta{font-size:12px;color:var(--gray-500);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.dash-row-time{width:52px;text-align:right;font-size:12px;color:var(--gray-500);
  flex-shrink:0}
.dash-row-staff{width:64px;text-align:right;font-size:12px;color:var(--gray-500);
  flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

.dash-pill{display:inline-flex;align-items:center;justify-content:center;
  border-radius:9999px;padding:.125rem .625rem;font-size:11px;font-weight:600;
  white-space:nowrap;flex-shrink:0;min-width:84px}
.dash-pill-expected{background:var(--gray-100);color:var(--gray-600)}
.dash-pill-in{background:rgb(var(--brand-100));color:rgb(var(--brand-800))}
.dash-pill-ready{background:var(--blue-100);color:var(--blue-800)}
.dash-pill-out{background:var(--gray-100);color:var(--gray-600)}

.dash-action{width:96px;flex-shrink:0;background:#fff;border:1px solid var(--gray-300);
  color:var(--gray-700);border-radius:var(--radius-sm);padding:.375rem 0;
  font-size:11.5px;font-weight:600;text-align:center;cursor:pointer;
  transition:background-color .15s,border-color .15s,color .15s}
.dash-action:hover{background:rgb(var(--brand-50));
  border-color:rgb(var(--brand-200));color:rgb(var(--brand-700))}
.dash-action:disabled{opacity:.5;cursor:default}
.dash-action-spacer{width:96px;flex-shrink:0}

.dash-footer-link{font-size:12px;font-weight:600;color:rgb(var(--brand-600))}
.dash-footer-link:hover{color:rgb(var(--brand-hover))}
.dash-btn-floor{display:inline-flex;align-items:center;background:rgb(var(--brand-600));
  color:#fff;font-size:12px;font-weight:600;padding:.5rem .75rem;
  border-radius:var(--radius);white-space:nowrap}
.dash-btn-floor:hover{background:rgb(var(--brand-hover))}

.dash-revenue{display:block;background:rgb(var(--brand-800));border-radius:var(--radius-lg);
  padding:1rem;color:#fff;transition:background-color .15s}
.dash-revenue:hover{background:rgb(var(--brand-hover-deep))}
.dash-revenue-label{font-size:11px;font-weight:700;text-transform:uppercase;
  letter-spacing:.6px;color:rgb(var(--brand-300))}
.dash-revenue-amount{font-size:26px;font-weight:700;margin-top:.25rem}
.dash-revenue-sub{font-size:12px;color:rgb(var(--brand-200));margin-top:.125rem}
.dash-revenue-link{font-size:12px;font-weight:600;margin-top:.75rem}

.dash-todo-row{display:flex;align-items:center;gap:.75rem;padding:.625rem 1.25rem;
  border-bottom:1px solid var(--gray-50)}
.dash-todo-text{flex:1;min-width:0;font-size:13px;color:var(--gray-700)}
.dash-todo-done .dash-todo-text{text-decoration:line-through;color:var(--gray-500)}
.dash-todo-btn{flex-shrink:0;background:rgb(var(--brand-50));
  border:1px solid rgb(var(--brand-200));color:rgb(var(--brand-700));
  border-radius:var(--radius-sm);padding:.25rem .625rem;font-size:11.5px;
  font-weight:600;cursor:pointer;white-space:nowrap}
.dash-todo-btn:hover{background:rgb(var(--brand-100))}
.dash-todo-btn:disabled{opacity:.5;cursor:default}
.dash-todo-add{display:flex;gap:.5rem;padding:.75rem 1.25rem;
  border-top:1px solid var(--gray-100)}
.dash-todo-input{flex:1;min-width:0;font-size:12.5px;padding:.375rem .625rem;
  border:1px solid var(--gray-200);border-radius:var(--radius-sm);background:#fff}
.dash-todo-input:focus{outline:none;border-color:rgb(var(--brand-200));box-shadow:var(--ring)}
.dash-todo-submit{background:rgb(var(--brand-600));color:#fff;font-size:12px;border:0;
  font-weight:600;padding:.375rem .75rem;border-radius:var(--radius-sm);cursor:pointer}
.dash-todo-submit:hover{background:rgb(var(--brand-hover))}
.dash-todo-submit:disabled{opacity:.5;cursor:default}

.dash-tabs{display:flex;gap:1rem;padding:0 1.25rem;border-bottom:1px solid var(--gray-100);overflow-x:auto;padding-bottom:1px;scrollbar-width:thin}   /* scrolls in its own box when narrow (EPv-01) */
.dash-tab:focus-visible{outline:2px solid rgb(var(--brand-600));outline-offset:-2px}
.dash-tab{padding:.625rem 0;font-size:12.5px;font-weight:600;color:var(--gray-500);
  border:0;border-bottom:2px solid transparent;margin-bottom:-1px;cursor:pointer;
  background:none;white-space:nowrap}
.dash-tab:hover{color:var(--gray-700)}
.dash-tab.is-active{color:rgb(var(--brand-700));border-bottom-color:rgb(var(--brand-700))}
.dash-list-row{display:flex;align-items:center;justify-content:space-between;gap:.75rem;
  padding:.625rem 1.25rem;border-bottom:1px solid var(--gray-50);
  transition:background-color .15s}
.dash-list-row:hover{background:var(--gray-100)}
.dash-list-row:last-child{border-bottom:0}
.dash-list-body{flex:1;min-width:0}
.dash-list-title{font-size:13px;font-weight:600;color:var(--gray-900);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dash-list-meta{font-size:12px;color:var(--gray-500);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.dash-list-meta-red{color:var(--red-700)}
.dash-list-amount{flex-shrink:0;font-size:13px;font-weight:600;color:var(--gray-900)}
.dash-list-empty{padding:1.5rem 1.25rem;text-align:center;font-size:13px;color:var(--gray-500)}
.dash-list-error{padding:1rem 1.25rem;font-size:12.5px;color:var(--red-700)}

.dash-toast{position:fixed;right:1.25rem;bottom:1.25rem;z-index:50;max-width:24rem;
  background:var(--gray-900);color:#fff;font-size:12.5px;padding:.75rem 1rem;
  border-radius:var(--radius);box-shadow:var(--shadow-lg)}

/* ---------- COMPONENTS: floor board (floor.php) ---------- */
.floor-page{display:flex;flex-direction:column;gap:1rem}
/* Wraps so the chips drop to a second line on a phone (TameLink-0ovg F73). */
.floor-bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.5rem 1rem;
  background:#fff;border:1px solid var(--gray-100);border-radius:var(--radius-lg);
  box-shadow:var(--shadow-sm);padding:.875rem 1.25rem}
.floor-nav{display:flex;align-items:center;gap:.75rem;min-width:0}
.floor-nav-btn{display:inline-flex;align-items:center;justify-content:center;
  width:32px;height:32px;border-radius:var(--radius);border:1px solid var(--gray-300);
  background:#fff;color:var(--gray-700);font-size:16px;line-height:1;cursor:pointer;
  transition:background-color .15s,border-color .15s}
.floor-nav-btn:hover{background:var(--gray-50);border-color:var(--gray-400)}
.floor-nav-btn:disabled{opacity:.5;cursor:default}
.floor-date{min-width:0}
.floor-date-label{font-size:16px;font-weight:700;color:var(--gray-900);line-height:1.2;
  display:flex;align-items:baseline;gap:.5rem}
.floor-today{font-size:12px;font-weight:600;color:var(--gray-500)}
.floor-today-link{font-size:12px;font-weight:600;color:rgb(var(--brand-600));
  text-decoration:underline;background:none;border:0;padding:0;cursor:pointer}
.floor-today-link:hover{color:rgb(var(--brand-hover))}
.floor-summary{font-size:12.5px;color:var(--gray-500);margin-top:.125rem;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.floor-chips{display:flex;align-items:center;gap:.5rem;flex-shrink:0;flex-wrap:wrap;
  justify-content:flex-end}
/* On a phone the chips may shrink, so they wrap inside the bar (F73). */
@media (max-width:639px){.floor-chips{flex-shrink:1;min-width:0;justify-content:flex-start}}
.floor-chip{display:inline-flex;align-items:center;border-radius:9999px;
  padding:.25rem .625rem;font-size:11.5px;font-weight:600;white-space:nowrap}
/* -700 text: -600 failed 4.5:1 at this size (TameLink-9hag F74). */
.floor-chip-red{background:var(--red-50);color:var(--red-700)}
.floor-chip-orange{background:var(--orange-50);color:var(--orange-700)}
.floor-chip-gray{background:var(--gray-100);color:var(--gray-600)}

.floor-undo{display:flex;align-items:center;justify-content:space-between;gap:1rem;
  background:var(--gray-900);color:#fff;border-radius:9999px;padding:.5rem .625rem .5rem 1rem;
  font-size:12.5px}
.floor-undo-btn{background:#fff;color:var(--gray-900);border:0;border-radius:9999px;
  padding:.3rem .75rem;font-size:12px;font-weight:600;cursor:pointer}
.floor-undo-btn:hover{background:var(--gray-100)}
.floor-undo-btn:disabled{opacity:.5;cursor:default}

.floor-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.875rem;
  background:var(--gray-50);border:1px solid var(--gray-100);border-radius:var(--radius-lg);
  padding:1.25rem;align-items:start}
@media (max-width:1099px){.floor-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:639px){.floor-grid{grid-template-columns:1fr}}
.floor-col{display:flex;flex-direction:column;gap:.625rem;min-width:0}
.floor-col-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;
  padding:0 .25rem}
.floor-col-label{font-size:11px;font-weight:700;text-transform:uppercase;
  letter-spacing:.6px;color:var(--gray-500)}
.floor-count{display:inline-flex;align-items:center;justify-content:center;min-width:22px;
  height:20px;padding:0 .4rem;border-radius:9999px;background:#fff;
  border:1px solid var(--gray-200);font-size:11px;font-weight:700;color:var(--gray-700)}
.floor-col-body{display:flex;flex-direction:column;gap:.625rem;min-height:4rem}

.floor-card{background:#fff;border:1px solid var(--gray-100);border-radius:var(--radius-lg);
  box-shadow:var(--shadow-sm);padding:.75rem .875rem;display:flex;flex-direction:column;gap:.625rem}
.floor-card-top{display:flex;align-items:center;gap:.625rem;min-width:0}
.floor-avatar{display:flex;align-items:center;justify-content:center;flex-shrink:0;
  width:32px;height:32px;border-radius:9999px;background:rgb(var(--brand-50));
  color:rgb(var(--brand-700));font-size:13px;font-weight:700;object-fit:cover;overflow:hidden}
.floor-card-body{flex:1;min-width:0}
.floor-pet{font-size:13.5px;font-weight:700;color:var(--gray-900);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.floor-pet a:hover{color:rgb(var(--brand-hover-text));text-decoration:underline}
.floor-meta{font-size:12px;color:var(--gray-500);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
.floor-flag{font-size:11.5px;font-weight:600;color:var(--red-700);background:var(--red-50);
  border:1px solid var(--red-300);border-radius:var(--radius-sm);padding:.3rem .5rem}
.floor-meeting{font-size:11.5px;font-weight:600;color:var(--purple-700);background:var(--purple-50);
  border:1px solid var(--purple-200);border-radius:var(--radius-sm);padding:.3rem .5rem}
.floor-card-foot{display:flex;align-items:center;gap:.5rem}
.floor-staff{flex:1;min-width:0;font-size:12px;color:var(--gray-500);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.floor-back{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:30px;height:28px;border-radius:var(--radius-sm);
  border:1px solid var(--gray-300);background:#fff;color:var(--gray-600);font-size:13px;
  line-height:1;cursor:pointer;transition:background-color .15s,border-color .15s}
.floor-back:hover{background:var(--gray-50);border-color:var(--gray-400)}
.floor-back:disabled{opacity:.5;cursor:default}
.floor-action{flex-shrink:0;display:inline-block;text-decoration:none;background:rgb(var(--brand-600));color:#fff;border:0;
  border-radius:var(--radius-sm);padding:.375rem .625rem;font-size:11.5px;font-weight:600;
  white-space:nowrap;cursor:pointer;transition:background-color .15s}
.floor-action:hover{background:rgb(var(--brand-hover))}
.floor-action:disabled{opacity:.5;cursor:default}
.floor-empty{font-size:12.5px;color:var(--gray-500);text-align:center;padding:1rem .5rem;
  border:1px dashed var(--gray-200);border-radius:var(--radius-lg)}
.floor-error{padding:2rem 1rem;text-align:center;font-size:13px;color:var(--red-600);
  background:#fff;border:1px solid var(--gray-100);border-radius:var(--radius-lg)}
.floor-toast{position:fixed;right:1.25rem;bottom:1.25rem;z-index:50;max-width:24rem;
  background:var(--gray-900);color:#fff;font-size:12.5px;padding:.75rem 1rem;
  border-radius:var(--radius);box-shadow:var(--shadow-lg)}
/* A warning staff must dismiss (TameLink-iad9): the check-out invoice warning
   on the floor board and dashboard. It does not time out. */
.dash-toast.is-warning,.floor-toast.is-warning{background:var(--red-700);display:flex;align-items:flex-start;gap:.75rem}
.toast-dismiss{flex:none;background:none;border:1px solid rgba(255,255,255,.6);color:#fff;
  font:inherit;font-weight:600;padding:.15rem .5rem;border-radius:var(--radius);cursor:pointer}

/* ---------- COMPONENTS: service form (TameLink-9mc.1, handoff Screen 2) ----------
   Two-column Edit/Create Service. Greens are rgb(var(--brand-N)); neutrals are
   --gray-N tokens; amber/red are the handoff's warn/error hex mapped to tokens.
   Sizes are the handoff's px values verbatim. */
.svc-cols{display:flex;flex-wrap:wrap;gap:20px;align-items:flex-start}
.svc-col-main{flex:1 1 420px;min-width:0;display:flex;flex-direction:column;gap:16px}
.svc-col-side{flex:1 1 340px;min-width:0;display:flex;flex-direction:column;gap:16px}
.svc-card{background:#fff;border:1px solid var(--gray-200);border-radius:10px;padding:18px 20px}
.svc-card-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:4px}
.svc-card-title{font-size:15px;font-weight:700;color:var(--gray-900)}
.svc-card-sub{font-size:12px;color:var(--gray-500)}
.svc-card-hint{font-size:12.5px;color:var(--gray-500);margin-bottom:12px}
.svc-row{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start}
.svc-row > *{flex:1 1 140px;min-width:0}
.svc-field{margin-bottom:12px}
.svc-label{display:block;font-size:12px;font-weight:600;color:var(--gray-700);margin-bottom:6px}
.svc-label-opt{font-weight:400;color:var(--gray-500)}
.svc-pill{display:inline-flex;align-items:center;border-radius:9999px;padding:3px 8px;
  font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;
  background:rgb(var(--brand-50));border:1px solid rgb(var(--brand-200));color:rgb(var(--brand-800))}
.svc-pill-muted{background:var(--gray-100);border-color:var(--gray-200);color:var(--gray-600)}

/* Program-structure blocks */
.svc-block{border:1px solid var(--gray-200);border-radius:8px;padding:12px 14px;margin-top:12px;background:#fff}
.svc-block-off{border-style:dashed;background:var(--gray-50);opacity:.7}
.svc-block-off .svc-block-title{color:var(--gray-500)}
.svc-block-title{font-size:13.5px;font-weight:600;color:var(--gray-900)}
.svc-block-desc{font-size:12px;color:var(--gray-500);margin-top:2px}
.svc-toggle-row{display:flex;align-items:flex-start;gap:12px;padding:11px 2px;border-bottom:1px solid var(--gray-100)}
.svc-toggle-row:last-child{border-bottom:none}
.svc-toggle-row .toggle{margin-top:2px}
.svc-toggle-text{flex:1 1 auto;min-width:0}
.svc-toggle-dep{margin-top:8px}
.svc-preview{margin-top:10px;font-size:12.5px;color:rgb(var(--brand-800));background:rgb(var(--brand-50));
  border:1px solid rgb(var(--brand-200));border-radius:8px;padding:8px 12px}

/* Weekday chips (single-select) */
.svc-chips{display:flex;flex-wrap:wrap;gap:6px}
.svc-chip{border-radius:9999px;padding:6px 12px;font-size:12.5px;font-weight:600;cursor:pointer;
  border:1px solid var(--gray-200);background:#fff;color:var(--gray-700);transition:border-color .12s}
.svc-chip:hover{border-color:var(--gray-300)}
.svc-chip.is-selected{border-color:rgb(var(--brand-700));background:rgb(var(--brand-50));color:rgb(var(--brand-800))}
.svc-chip:focus-visible{outline:none;box-shadow:var(--ring)}

/* Meeting repeater */
.svc-meeting{border:1px solid var(--gray-200);border-radius:8px;padding:12px 14px;margin-top:10px;background:var(--gray-50)}
.svc-meeting-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.svc-meeting-name{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--gray-500)}
.svc-stepper{display:inline-flex;align-items:center;gap:6px}
.svc-stepper-btn{width:32px;height:36px;border:1px solid var(--gray-300);border-radius:8px;background:#fff;
  color:var(--gray-700);font-size:16px;cursor:pointer}
.svc-stepper-btn:disabled{opacity:.4;cursor:not-allowed}
.svc-stepper-btn:focus-visible{outline:none;box-shadow:var(--ring)}
.svc-stepper-val{min-width:72px;text-align:center;font-size:14px;font-weight:600;color:var(--gray-900);
  border:1px solid var(--gray-300);border-radius:8px;padding:8px 10px;background:#fff}
.svc-stepper-input{width:64px;height:36px;text-align:center;font-size:14px;font-weight:600;color:var(--gray-900);
  border:1px solid var(--gray-300);border-radius:8px;padding:0 6px;background:#fff;-moz-appearance:textfield;appearance:textfield}
.svc-stepper-input::-webkit-outer-spin-button,.svc-stepper-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.svc-stepper-input:focus-visible{outline:none;box-shadow:var(--ring)}
.svc-stepper-of{font-size:12px;color:var(--gray-500)}
.svc-add-btn{margin-top:10px;border:1px solid rgb(var(--brand-200));background:#fff;color:rgb(var(--brand-800));
  border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer}
.svc-add-btn:hover{background:rgb(var(--brand-50))}
.svc-add-btn:focus-visible{outline:none;box-shadow:var(--ring)}
.svc-meeting-foot{display:flex;justify-content:space-between;align-items:center;margin-top:10px;font-size:12px;color:var(--gray-500)}

/* Deposits */
.svc-rules{border:1px solid var(--gray-200);border-radius:8px;padding:10px 14px;background:var(--gray-50);margin-bottom:14px}
.svc-rules-head{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;color:var(--gray-500);margin-bottom:4px}
.svc-rule{display:flex;justify-content:space-between;gap:12px;padding:7px 0;border-bottom:1px solid var(--gray-100);font-size:12.5px}
.svc-rule:last-child{border-bottom:none}
.svc-rule-k{color:var(--gray-500)}
.svc-rule-v{color:var(--gray-900);font-weight:600;text-align:right}
.svc-rule-v.is-on{color:rgb(var(--brand-800))}
.svc-result{margin-top:12px;background:rgb(var(--brand-50));border:1px solid rgb(var(--brand-200));border-radius:8px;padding:10px 14px}
.svc-result-k{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:rgb(var(--brand-800));margin-bottom:2px}
.svc-result-v{font-size:12.5px;color:rgb(var(--brand-800))}

/* Save bar */
.svc-savebar{position:sticky;bottom:0;z-index:10;margin-top:16px;display:flex;align-items:center;
  justify-content:space-between;gap:12px;background:#fff;border:1px solid var(--gray-200);
  border-radius:10px;padding:12px 20px;box-shadow:0 -4px 16px rgb(17 24 39 / .06)}
.svc-savebar-count{font-size:12.5px;color:var(--gray-500)}
.svc-savebar-actions{display:flex;align-items:center;gap:14px}
.svc-save-btn{background:var(--gray-900);color:#fff;border:none;border-radius:8px;padding:10px 18px;
  font-size:13.5px;font-weight:700;cursor:pointer}
.svc-save-btn:hover{background:#000}
.svc-save-btn:focus-visible{outline:none;box-shadow:var(--ring)}
.svc-readonly{font-size:14px;color:var(--gray-900);padding:9px 12px;border:1px solid var(--gray-200);
  border-radius:8px;background:var(--gray-50)}
/* A colour picker beside its '#RRGGBB' text box (calendar colours,
   TameLink-44fg). The text box carries the name and is what posts. */
.color-field{display:flex;align-items:center;gap:8px}
.color-field-swatch{flex:none;width:44px;height:38px;padding:2px;border:1px solid var(--gray-300);
  border-radius:8px;background:#fff;cursor:pointer}
.color-field-hex{max-width:130px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.svc-colors{margin-bottom:24px}
.svc-colors-row{align-items:flex-end}
.color-dot{display:inline-block;margin-right:6px;width:10px;height:10px;border-radius:9999px;vertical-align:middle;
  box-shadow:inset 0 0 0 1px rgb(0 0 0 / .15)}

/* ---------- COMPONENTS: task drawer + composer (dashboard.php) ---------- */
/* Sub-project 5 of the staff dashboard redesign (TameLink-b7r.5). The drawer
   reuses .drawer-backdrop/.drawer-panel/.drawer-header/.drawer-body from the
   schedules page; only the width modifier and the inner furniture are new.
   The composer reuses .modal-overlay/.modal-panel/.modal-footer. */

/* The shared .drawer-panel is 28rem; the task drawer needs the handoff's 35rem. */
.drawer-panel-wide{max-width:35rem}

/* — To Do row, made clickable — */
.dash-todo-open{flex:1;min-width:0;text-align:left;background:none;border:none;
  padding:0;font:inherit;color:inherit;cursor:pointer}
.dash-todo-open:hover{color:rgb(var(--brand-hover));text-decoration:underline;text-underline-offset:2px}
.dash-todo-removed{display:none}

/* — Drawer header — */
.task-badge{display:inline-block;font-size:10.5px;font-weight:700;
  color:rgb(var(--brand-700));background:rgb(var(--brand-50));
  padding:3px 8px;border-radius:9999px;letter-spacing:.5px;text-transform:uppercase}
.task-drawer-title{margin:.5rem 0 0;font-size:17px;font-weight:700;color:var(--gray-900)}
.task-prov{margin:3px 0 0;font-size:12.5px;color:var(--gray-500)}

/* — Meta rows — */
.task-meta{display:flex;flex-direction:column;gap:9px;padding-bottom:.875rem;
  border-bottom:1px solid var(--gray-100)}
.task-meta-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.task-meta-label{width:74px;flex:none;font-size:11.5px;font-weight:600;
  color:var(--gray-500);text-transform:uppercase;letter-spacing:.5px}
.task-meta-value{font-size:13px;color:var(--gray-700)}
.task-link{font-size:13px;font-weight:600;color:rgb(var(--brand-700));text-decoration:underline;text-underline-offset:2px}
.task-sep{font-size:12.5px;color:var(--gray-300)}

/* — Due chips — */
.task-chip{font-size:11.5px;font-weight:600;padding:3px 8px;border-radius:var(--radius)}
.task-chip.is-today{color:var(--orange-700);background:var(--orange-50)}
.task-chip.is-overdue{color:var(--red-700);background:var(--red-50)}

/* — Priority segmented control — */
.task-seg{display:flex;gap:6px}
.task-seg-btn{font-size:12px;font-weight:600;padding:4px 11px;border-radius:7px;
  cursor:pointer;background:#fff;color:var(--gray-600);border:1px solid var(--gray-300)}
.task-seg-btn:hover:not(:disabled){background:var(--gray-50)}
.task-seg-btn.is-active{background:rgb(var(--brand-50));color:rgb(var(--brand-700));
  border-color:rgb(var(--brand-200))}
.task-seg-btn:disabled{opacity:.5;cursor:not-allowed}

/* — Assignee pills — */
.task-section{padding:.875rem 0;border-bottom:1px solid var(--gray-100)}
.task-section-label{margin:0 0 9px;font-size:11.5px;font-weight:600;
  color:var(--gray-500);text-transform:uppercase;letter-spacing:.5px}
.task-pills{display:flex;gap:8px;flex-wrap:wrap}
.task-pill{display:flex;align-items:center;gap:7px;font-size:12.5px;font-weight:600;
  padding:5px 11px 5px 6px;border-radius:9999px;cursor:pointer;background:#fff;
  color:var(--gray-700);border:1px solid var(--gray-300)}
.task-pill:hover:not(:disabled){background:var(--gray-50)}
.task-pill.is-active{background:rgb(var(--brand-700));color:#fff;
  border-color:rgb(var(--brand-700))}
.task-pill:disabled{opacity:.5;cursor:not-allowed}
.task-pill-avatar{width:22px;height:22px;border-radius:9999px;
  background:rgb(var(--brand-50));color:rgb(var(--brand-700));font-size:11px;
  font-weight:700;display:flex;align-items:center;justify-content:center;flex:none}
.task-pill.is-active .task-pill-avatar{background:rgb(var(--brand-600));color:#fff}

/* — Notes — */
.task-note{width:100%;box-sizing:border-box;border:1px solid var(--gray-200);
  border-radius:9px;padding:10px 12px;font-size:13px;color:var(--gray-700);
  line-height:1.55;font-family:inherit;resize:vertical;min-height:5rem}
.task-note:focus{outline:none;border-color:rgb(var(--brand-200))}

/* — Activity — */
.task-activity{display:flex;flex-direction:column;gap:4px}
.task-activity-line{margin:0;font-size:12.5px;color:var(--gray-500);line-height:1.6}

/* — Drawer footer — */
.task-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding-top:.875rem}
.task-foot-spacer{flex:1}
.task-danger{border:none;background:none;color:var(--gray-500);font-size:13px;
  font-weight:600;padding:9px 6px;cursor:pointer}
.task-danger:hover:not(:disabled){color:var(--red-700)}
.task-danger:disabled{opacity:.5;cursor:not-allowed}
.task-hint{margin:6px 0 0;font-size:11.5px;color:var(--gray-500)}
.task-state{font-size:13px;color:var(--gray-500);padding:1rem 0}

/* — Composer — */
.task-form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.task-field{display:block;margin-bottom:.875rem}
.task-field-label{display:block;font-size:11.5px;font-weight:600;color:var(--gray-500);
  text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
.task-link-wrap{position:relative}
/* Drawer "Linked to" picker (TameLink-nyga): fill the row so the results list isn't input-narrow. */
.task-meta-row>.task-link-wrap{flex:1;min-width:0}
.task-link-input{width:100%;box-sizing:border-box;border:1px solid var(--gray-200);
  border-radius:8px;padding:9px 11px;font-size:13px;color:var(--gray-900)}
.task-link-input:focus{outline:none;border-color:rgb(var(--brand-200))}
.task-link-chip{display:flex;align-items:center;gap:8px;border:1px solid var(--gray-200);
  border-radius:8px;padding:8px 11px;font-size:13px;color:var(--gray-900)}
.task-link-name{font-weight:600}
.task-link-type{font-size:12px;color:var(--gray-500)}
.task-link-x{margin-left:auto;background:none;border:none;font-size:13px;
  color:var(--gray-500);cursor:pointer;padding:0}
.task-link-x:hover{color:var(--gray-600)}
.task-dd-type{margin-left:auto;font-size:11.5px;color:var(--gray-500);
  text-transform:capitalize}
.task-dd-sub{font-size:12px;color:var(--gray-500)}

/* ---------- COMPONENTS: booking form (views/bookings/_form.php) ----------------
   TameLink-9mc.3. One block for the shared New Booking screen: card stack +
   sticky summary rail, service tabs/cards, day strip, slot grid, chips,
   staffing line, override panel. Handoff greens/ambers map to --brand-N /
   --amber-N tokens. Flex-wrap only, no media queries. */
.bk-layout{display:flex;flex-wrap:wrap;gap:20px;align-items:flex-start}
.bk-main{flex:1 1 560px;min-width:0;display:flex;flex-direction:column;gap:16px}
.bk-rail{flex:1 1 300px;max-width:360px;min-width:0;position:sticky;top:16px}
.bk-card{background:#fff;border:1px solid var(--gray-200);border-radius:10px;padding:18px 20px}
.bk-card-head{display:flex;align-items:flex-start;gap:12px;margin-bottom:14px}
.bk-card-head > div{flex:1 1 auto;min-width:0}
.bk-step{flex:0 0 22px;width:22px;height:22px;border-radius:9999px;display:inline-flex;align-items:center;
  justify-content:center;font-size:12px;font-weight:700;color:#fff;background:rgb(var(--brand-600))}
.bk-card-title{font-size:15px;font-weight:700;color:var(--gray-900);line-height:22px}
.bk-card-sub{font-size:12px;color:var(--gray-500);margin-top:1px}
.bk-sub-title{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--gray-600);margin:14px 0 8px}
.bk-sub-note{font-weight:400;text-transform:none;letter-spacing:0;color:var(--gray-500);margin-left:6px}
.bk-divider{border-top:1px solid var(--gray-100);margin-top:14px;padding-top:2px}
.bk-hint{font-size:12.5px;color:var(--gray-500);margin:6px 0 0}
.bk-hint-inline{font-size:12px;color:var(--gray-500)}
.bk-error{font-size:12.5px;color:var(--red-600);margin:6px 0 0}
.bk-link{background:none;border:0;padding:0;font:inherit;font-size:12.5px;font-weight:600;color:rgb(var(--brand-700));cursor:pointer;text-decoration:none}
.bk-link:hover{text-decoration:underline}
.bk-field{margin-top:12px}
.bk-two{display:flex;flex-wrap:wrap;gap:16px}
.bk-two > *{flex:1 1 220px;min-width:0}
.bk-k{font-size:12px;color:var(--gray-500)}
.bk-v{font-size:13px;font-weight:600;color:var(--gray-900)}

/* Owner card (picked client + dog) */
.bk-owner{display:flex;flex-wrap:wrap;gap:12px 24px;align-items:center;border:1px solid rgb(var(--brand-200));
  background:rgb(var(--brand-50));border-radius:8px;padding:10px 14px}
.bk-owner > div{min-width:0}
.bk-owner-name{font-size:13.5px;font-weight:700;color:var(--gray-900)}
.bk-owner-sub{font-size:12px;color:var(--gray-500)}
.bk-owner .bk-link{margin-left:auto}
.bk-search{position:relative}

/* Service tabs + cards */
.bk-tabs{display:flex;flex-wrap:wrap;gap:4px;padding:3px;border-radius:9px;background:var(--gray-100);margin-bottom:12px}
.bk-tab{flex:1 1 auto;border:0;background:transparent;border-radius:7px;padding:7px 10px;font:inherit;font-size:12.5px;
  font-weight:600;color:var(--gray-600);cursor:pointer}
.bk-tab:hover{color:var(--gray-900)}
.bk-tab.is-active{background:#fff;color:var(--gray-900);box-shadow:var(--shadow-sm)}
.bk-svc-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
.bk-svc{text-align:left;background:#fff;border:2px solid var(--gray-200);border-radius:10px;padding:12px 14px;
  font:inherit;cursor:pointer;display:flex;flex-direction:column;gap:4px;min-width:0}
.bk-svc:hover{border-color:rgb(var(--brand-300))}
.bk-svc.is-selected{border-color:rgb(var(--brand-600));background:rgb(var(--brand-50))}
.bk-svc:focus-visible{outline:none;box-shadow:var(--ring)}
.bk-svc-row{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
.bk-svc-name{font-size:14px;font-weight:700;color:var(--gray-900);min-width:0}
.bk-svc-price{font-size:13px;font-weight:700;color:rgb(var(--brand-700));white-space:nowrap}
.bk-svc-meta{font-size:12px;color:var(--gray-500)}
.bk-badges{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
.bk-badge{display:inline-flex;align-items:center;border-radius:9999px;padding:2px 8px;font-size:11px;font-weight:600;
  border:1px solid transparent}
.bk-badge-warn{background:var(--amber-50);border-color:var(--amber-200);color:var(--amber-800)}
.bk-badge-ok{background:rgb(var(--brand-50));border-color:rgb(var(--brand-200));color:rgb(var(--brand-800))}

/* Chips (add-ons, drop-off/pick-up/meeting times, trainers, note tags) */
.bk-chips{display:flex;flex-wrap:wrap;gap:8px}
.bk-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--gray-300);background:#fff;border-radius:9999px;
  padding:6px 12px;font:inherit;font-size:12.5px;font-weight:600;color:var(--gray-700);cursor:pointer;line-height:1.2}
.bk-chip:hover{border-color:rgb(var(--brand-300));color:var(--gray-900)}
.bk-chip.is-selected{border-color:rgb(var(--brand-600));background:rgb(var(--brand-600));color:#fff}
.bk-chip.is-selected .bk-chip-price,.bk-chip.is-selected .bk-chip-left{color:rgb(var(--brand-100))}
.bk-chip:disabled{opacity:.45;cursor:not-allowed}
.bk-chip:focus-visible{outline:none;box-shadow:var(--ring)}
.bk-chip-sm{padding:4px 10px;font-size:12px;font-weight:500}
.bk-chip-price,.bk-chip-left{font-weight:500;color:var(--gray-500);font-size:12px}

/* Products */
.bk-products{display:flex;flex-direction:column;gap:6px}
.bk-product{display:flex;align-items:center;gap:12px;font-size:13px}
.bk-product-name{flex:1 1 auto;min-width:0;color:var(--gray-900)}
.bk-product-price{color:var(--gray-500);white-space:nowrap}
/* Add product pop-up list (TameLink-fnkg): one row per product; the picked row is outlined. */
.bk-pick-list{display:flex;flex-direction:column;gap:6px;margin:0;padding:0;list-style:none}
.bk-pick{display:flex;align-items:center;gap:12px;width:100%;min-height:44px;padding:8px 12px;font:inherit;font-size:13px;
  text-align:left;background:#fff;border:1px solid var(--gray-200);border-radius:7px;cursor:pointer}
.bk-pick:hover{background:var(--gray-50)}
.bk-pick.is-on{border-color:rgb(var(--brand-500));box-shadow:var(--ring-flat)}
.bk-qty,.bk-stepper{display:inline-flex;align-items:center;border:1px solid var(--gray-300);border-radius:7px;overflow:hidden}
.bk-qty button,.bk-stepper button{border:0;background:var(--gray-50);width:28px;height:28px;font:inherit;font-size:15px;
  color:var(--gray-700);cursor:pointer}
.bk-qty button:hover,.bk-stepper button:hover{background:var(--gray-100)}
.bk-qty span,.bk-stepper span{min-width:28px;text-align:center;font-size:13px;font-weight:600;color:var(--gray-900)}

/* Legend + day strip */
.bk-legend{display:flex;flex-wrap:wrap;gap:12px;margin-left:auto;font-size:12px;color:var(--gray-500);align-self:center}
.bk-legend-item{display:inline-flex;align-items:center;gap:5px}
.bk-legend-dot{width:8px;height:8px;border-radius:9999px;display:inline-block;background:var(--gray-300)}
.bk-legend-dot.is-open{background:rgb(var(--brand-500))}
.bk-legend-dot.is-low{background:var(--amber-600)}
.bk-legend-dot.is-off{background:var(--gray-300)}
.bk-legend-dot.is-closure{background:var(--amber-50);outline:1px dashed var(--amber-600)}
.bk-strip-nav{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.bk-strip-btn{border:1px solid var(--gray-300);background:#fff;border-radius:7px;width:30px;height:30px;font:inherit;font-size:16px;
  line-height:1;color:var(--gray-700);cursor:pointer}
.bk-strip-btn:hover{background:var(--gray-50)}
.bk-strip-btn:disabled{opacity:.4;cursor:not-allowed}
.bk-strip-label{font-size:12.5px;font-weight:600;color:var(--gray-700)}
.bk-strip{display:grid;grid-template-columns:repeat(auto-fill,minmax(74px,1fr));gap:6px}
.bk-day{border:2px solid var(--gray-200);background:#fff;border-radius:9px;padding:8px 4px 7px;display:flex;flex-direction:column;
  align-items:center;gap:2px;font:inherit;cursor:pointer;min-width:0}
.bk-day:hover{border-color:rgb(var(--brand-300))}
.bk-day.is-selected{border-color:rgb(var(--brand-600));background:rgb(var(--brand-50))}
.bk-day:disabled{cursor:not-allowed;background:var(--gray-50);color:var(--gray-500)}
.bk-day:disabled .bk-day-num{color:var(--gray-500)}
.bk-day:focus-visible{outline:none;box-shadow:var(--ring)}
/* Small booking labels are floored at 12px (portal QA F87): .bk-day-pill,
   .bk-chip-price, .bk-legend, .bk-rail-head, .bk-pill, .res-k. .bk-day-pill.is-tight
   stays 10.5px so "Unavailable" still fits a 74px day cell. */
.bk-day-num{font-size:18px;font-weight:700;color:var(--gray-900);line-height:1.1}
.bk-day-pill{font-size:12px;font-weight:600;border-radius:9999px;padding:1px 7px;margin-top:2px;white-space:nowrap;max-width:100%;
  overflow:hidden;text-overflow:ellipsis}
.bk-day-pill.is-open{background:rgb(var(--brand-100));color:rgb(var(--brand-800))}
.bk-day-pill.is-low{background:var(--amber-50);color:var(--amber-800)}
/* TameLink-x5jf: a closed day that can still be picked, with sign-off. */
.bk-day-pill.is-closure{background:var(--amber-50);color:var(--amber-800);outline:1px dashed var(--amber-600)}
/* TameLink-p056.10: "Unavailable" is the longest pill; this keeps it whole. */
.bk-day-pill.is-tight{font-size:10.5px;padding:1px 3px}
.bk-closure-note{margin-top:.5rem;font-size:.875rem;color:var(--amber-800)}
.bk-day-pill.is-off{background:var(--gray-100);color:var(--gray-600)}

/* Slot grid (Mode A) */
.bk-slots{display:grid;grid-template-columns:repeat(auto-fill,minmax(112px,1fr));gap:8px}
.bk-slot{border:2px solid var(--gray-200);background:#fff;border-radius:9px;padding:9px 8px;display:flex;flex-direction:column;
  align-items:center;gap:2px;font:inherit;cursor:pointer}
.bk-slot:hover{border-color:rgb(var(--brand-300))}
.bk-slot.is-selected{border-color:rgb(var(--brand-600));background:rgb(var(--brand-50))}
.bk-slot:focus-visible{outline:none;box-shadow:var(--ring)}
.bk-slot-time{font-size:13.5px;font-weight:700;color:var(--gray-900)}
.bk-slot-left{font-size:11.5px;color:var(--gray-500)}

/* Stay (Mode B) + series (Mode C) */
.bk-block{margin-top:14px}
.bk-stay{border:1px solid var(--gray-200);border-radius:8px;padding:12px 14px;background:var(--gray-50)}
.bk-stay-row{display:flex;flex-wrap:wrap;gap:10px 24px;align-items:center}
.bk-stepper-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.bk-line-ok{font-size:13px;font-weight:600;color:rgb(var(--brand-700));margin:10px 0 0}
.bk-meetings{margin-top:14px}
.bk-meeting{border-top:1px solid var(--gray-100);padding:10px 0}
.bk-meeting-head{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline;margin-bottom:8px}
.bk-series{border:1px solid rgb(var(--brand-200));border-radius:8px;padding:12px 14px;background:rgb(var(--brand-50))}
.bk-series-head{display:flex;flex-wrap:wrap;gap:8px 12px;align-items:center}
.bk-weeks{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-top:10px}
.bk-week{background:#fff;border:1px solid var(--gray-200);border-radius:7px;padding:8px 10px;display:flex;flex-direction:column;gap:1px}
.bk-week-n{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--gray-500)}
.bk-week-d{font-size:13px;font-weight:600;color:var(--gray-900)}

/* Pills */
.bk-pill{display:inline-flex;align-items:center;border-radius:9999px;padding:2px 9px;font-size:12px;font-weight:700;
  text-transform:uppercase;letter-spacing:.06em;border:1px solid transparent}
.bk-pill-ok{background:rgb(var(--brand-50));border-color:rgb(var(--brand-200));color:rgb(var(--brand-800))}
.bk-pill-warn{background:var(--amber-50);border-color:var(--amber-200);color:var(--amber-800)}

/* Staffing */
.bk-staff-line{font-size:12.5px;font-weight:600;margin:8px 0 0}
.bk-staff-line.is-free{color:rgb(var(--brand-700))}
.bk-staff-line.is-conflict{color:var(--red-600)}
.bk-staff-line.is-outside{color:var(--amber-700)}
.bk-staff-line.is-unknown{color:var(--gray-500)}
.bk-note-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}
.bk-notes{min-height:76px;resize:vertical}

/* Override panel (staff, conditional) */
.bk-override{border:1px solid var(--amber-200);background:var(--amber-50);border-radius:10px;padding:16px 20px}
.bk-override.is-attention{border-color:var(--amber-600);box-shadow:0 0 0 3px var(--amber-200)} /* QA EPb-02: a save was refused for this sign-off */
.bk-override-title{font-size:14px;font-weight:700;color:var(--amber-800);margin-bottom:10px}
.bk-override-row{background:#fff;border:1px solid var(--amber-200);border-radius:8px;padding:12px 14px;margin-top:10px}
.bk-override-row p{font-size:13px;color:var(--gray-700);margin:0 0 8px}
.bk-override-row ul{font-size:13px;color:var(--gray-700);margin:0 0 8px 18px;list-style:disc}
.bk-override-row label{display:flex;align-items:flex-start;gap:8px;font-size:13px;font-weight:600;color:var(--amber-800);cursor:pointer}

/* Summary rail */
.bk-rail-card{background:#fff;border:1px solid var(--gray-200);border-radius:10px;padding:16px 18px}
.bk-rail-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;font-weight:700;
  text-transform:uppercase;letter-spacing:.08em;color:var(--gray-500);margin-bottom:10px}
.bk-rail-rows{margin:0;display:flex;flex-direction:column;gap:6px}
.bk-rail-row{display:flex;justify-content:space-between;gap:12px;font-size:13px}
.bk-rail-row dt{color:var(--gray-500);flex:0 0 auto}
.bk-rail-row dd{margin:0;color:var(--gray-900);font-weight:600;text-align:right;min-width:0;overflow-wrap:anywhere}
.bk-rail-foot{border-top:1px solid var(--gray-100);margin-top:12px;padding-top:10px;display:flex;flex-direction:column;gap:6px}
.bk-rail-line{display:flex;justify-content:space-between;align-items:center;gap:12px;font-size:13px;color:var(--gray-700)}
.bk-rail-line label{font-size:13px;color:var(--gray-700)}
.bk-rail-deposit{font-size:12.5px;color:rgb(var(--brand-700));font-weight:600}
.bk-rail-total{font-size:15px;font-weight:700;color:var(--gray-900);border-top:1px solid var(--gray-100);padding-top:8px;margin-top:2px}
.bk-discount{width:110px;text-align:right}
.bk-rail-errors{margin:12px 0 0;padding:10px 12px;border-radius:8px;background:var(--red-50);border:1px solid var(--red-200);
  font-size:12.5px;color:var(--red-700);list-style:disc;padding-left:26px}
.bk-cta{width:100%;margin-top:14px}
.bk-foot{display:flex;justify-content:space-between;align-items:center;margin-top:10px;font-size:12px;color:var(--gray-500)}
.bk-foot{gap:12px}
.bk-foot > .bk-link{flex-shrink:0}
.bk-foot > span{text-align:right}

/* ---------- COMPONENTS: agreement signing (SP4, TameLink-9mc.4) --------------
   views/portal/_agreement-signing.php, used by the signing page and by the New
   Booking signing modal. Replaces the Tailwind utilities agreements-sign.php
   carried. The pad itself keeps the .signature-pad classes above. */
.sign-header{display:flex;align-items:center;gap:.75rem;margin-bottom:1.5rem}
.sign-header-sep{color:var(--gray-300)}
.sign-page{max-width:48rem;margin:0 auto;display:flex;flex-direction:column;gap:1rem;padding-bottom:2rem}
.sign-errors{margin-bottom:1rem}
.sign-errors ul{margin:0;padding-left:1.25rem;list-style:disc;font-size:.875rem}
.sign-error{font-size:.8125rem;color:var(--red-600);margin:0}
.sign-actions{display:flex;align-items:center;justify-content:space-between;gap:1rem}
.sign-doc{display:flex;flex-direction:column;gap:1rem}
.sign-section{background:#fff;border:1px solid var(--gray-100);border-radius:var(--radius-lg);padding:1.25rem 1.5rem}
.sign-section-title{font-size:1rem;line-height:1.5rem;font-weight:600;color:var(--gray-900);margin:0 0 .75rem}
.sign-section-body{font-size:.875rem;line-height:1.625;color:var(--gray-700);white-space:pre-line;margin:0}
.sign-initial{margin-top:1rem;padding-top:1rem;border-top:1px solid var(--gray-100)}
.sign-initial-input{width:7rem;border-color:rgb(var(--brand-300));text-align:center;font-weight:600;
  text-transform:uppercase;letter-spacing:.1em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.sign-field{margin-top:1rem}
.sign-name-input{max-width:24rem;white-space:normal}
.sign-pad-actions{margin-top:.5rem;display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}
.sign-clear{background:none;border:0;padding:0;font:inherit;font-size:.8125rem;font-weight:600;
  color:var(--gray-600);text-decoration:underline;cursor:pointer}
.sign-clear:hover{color:var(--gray-900)}
.sign-hint{font-size:.75rem;color:var(--gray-500)}
.sign-modal{max-width:44rem}
.sign-modal .sign-section{border-color:var(--gray-200);padding:1rem 1.25rem}

/* ---------- COMPONENTS: booking form, client side (SP4, TameLink-9mc.4) ------
   Dog cards for Step 1, the vaccine note and the paperwork rows in Step 4, and
   the signing modal's panel size. Same tokens and flex-wrap-only rules as the
   TameLink-9mc.3 bk-* block above. */
.bk-pets{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px}
.bk-pet{display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center;background:#fff;
  border:2px solid var(--gray-200);border-radius:10px;padding:14px 10px;font:inherit;cursor:pointer;min-width:0}
.bk-pet:hover{border-color:rgb(var(--brand-300))}
.bk-pet.is-selected{border-color:rgb(var(--brand-600));background:rgb(var(--brand-50))}
.bk-pet:focus-visible{outline:none;box-shadow:var(--ring)}
.bk-pet-photo{width:56px;height:56px;border-radius:9999px;object-fit:cover}
.bk-pet-initials{width:56px;height:56px;border-radius:9999px;display:flex;align-items:center;justify-content:center;
  background:rgb(var(--brand-100));color:rgb(var(--brand-800));font-size:20px;font-weight:700}
.bk-pet-name{font-size:14px;font-weight:700;color:var(--gray-900);max-width:100%;overflow-wrap:anywhere}
.bk-pet-meta{font-size:12px;color:var(--gray-500);max-width:100%;overflow-wrap:anywhere}

.bk-vaccine{border:1px solid var(--amber-200);background:var(--amber-50);border-radius:8px;padding:12px 14px}
.bk-vaccine.is-blocking{border-color:var(--red-200);background:var(--red-50)}
.bk-vaccine-title{font-size:13.5px;font-weight:700;color:var(--amber-800);margin:0}
.bk-vaccine.is-blocking .bk-vaccine-title{color:var(--red-700)}
.bk-vaccine-list{margin:8px 0 0 18px;padding:0;list-style:disc}
.bk-vaccine-item{font-size:13px;color:var(--gray-700)}
.bk-vaccine-link{display:inline-block;margin-top:8px;font-size:12.5px;font-weight:600;color:rgb(var(--brand-700))}

.bk-approval{font-size:13px;color:var(--gray-700);background:var(--gray-50);border:1px solid var(--gray-200);
  border-radius:8px;padding:10px 12px;margin:12px 0 0}
.bk-doc{display:flex;flex-wrap:wrap;align-items:center;gap:10px;border-top:1px solid var(--gray-100);padding:10px 0}
.bk-doc-mark{flex:0 0 auto;width:20px;height:20px;border-radius:9999px;display:inline-flex;align-items:center;
  justify-content:center;font-size:12px;font-weight:700;background:var(--gray-100);color:var(--gray-600)}
.bk-doc.is-signed .bk-doc-mark{background:var(--green-100);color:var(--green-700)}
.bk-doc-title{flex:1 1 180px;min-width:0;font-size:13.5px;font-weight:600;color:var(--gray-900);overflow-wrap:anywhere}
.bk-override-reason{margin-top:8px}

/* ---------- COMPONENTS: booking result page (SP4, TameLink-9mc.4) -----------
   views/portal/booking-confirm.php. Same tokens as the bk-* blocks above. */
.res-head{margin-bottom:20px}
.res-title{font-size:24px;line-height:32px;font-weight:700;color:var(--gray-900);margin:0}
.res-title .badge{vertical-align:middle;margin-left:8px}
.res-sub{font-size:13px;color:var(--gray-600);margin:6px 0 0}
.res-card{background:#fff;border:1px solid var(--gray-200);border-radius:10px;padding:18px 20px;margin-bottom:16px}
.res-card-title{font-size:15px;font-weight:700;color:var(--gray-900);margin:0 0 12px}
.res-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin:0}
.res-k{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--gray-500)}
.res-v{font-size:14px;font-weight:600;color:var(--gray-900);margin:2px 0 0;overflow-wrap:anywhere}
.res-todo{border-color:var(--amber-200);background:var(--amber-50)}
.res-todo-row{display:flex;gap:10px;align-items:flex-start;padding:6px 0}
.res-todo-mark{flex:0 0 auto;color:var(--amber-800);font-weight:700;line-height:1.5}
.res-todo-text{font-size:13.5px;color:var(--gray-800);line-height:1.5}
.res-money{display:flex;flex-direction:column;gap:6px}
.res-money-row{display:flex;justify-content:space-between;gap:16px;font-size:13.5px;color:var(--gray-700)}
.res-money-total{border-top:1px solid var(--gray-100);padding-top:8px;margin-top:2px;font-size:15px;font-weight:700;color:var(--gray-900)}

/* ---------- COMPONENTS: booking holds panel (SP4, TameLink-9mc.4) -----------
   views/admin/bookings/show.php, "Waiting on". */
.hold-list{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}
.hold-item{display:flex;align-items:flex-start;gap:8px;font-size:13.5px;color:var(--gray-800);line-height:1.45}
.hold-dot{flex:0 0 auto;width:8px;height:8px;border-radius:9999px;margin-top:6px}
.hold-dot.is-staff{background:var(--amber-600)}
.hold-dot.is-client{background:var(--gray-300)}
.hold-dot.is-done{background:var(--green-600)}
.hold-dot.is-todo{background:var(--amber-600)}
.hold-note{font-size:12px;color:var(--gray-500);margin:8px 0 0}

/* ---------- COMPONENTS: "Finish Your Profile" strip (Phase 29) --------------
   views/partials/profile-checklist.php, staff dashboard + portal home. One row:
   heading, then the open items; wraps on a narrow screen (Nix, 2026-10-24). */
.card.profile-strip{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem 1.5rem;
  padding:.875rem 1.5rem;margin-bottom:1rem}
.profile-strip .card-heading{margin:0}
.profile-strip-list{margin:0;padding:0;list-style:none;display:flex;flex-wrap:wrap;
  align-items:center;gap:.5rem 1.25rem}
.profile-strip-item{display:flex;align-items:center;gap:8px;font-size:13.5px;color:var(--gray-800)}
.profile-strip-item .hold-dot{margin-top:0}
.profile-strip-detail{font-size:12px;color:var(--gray-500)}

/* ---------- COMPONENTS: Pet Booking Schedule (TameLink-sm04) ----------------
   views/admin/bookings/schedule.php — Day and Week views of
   admin/bookings-calendar.php. Design: docs/design_handoff_pet_booking_schedule.
   Each block takes its colour from --sched-c, so a per-service colour only has
   to set that one property on the block. */
.sched-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:20px}
.sched-eyebrow{margin:0 0 4px;font-size:12px;font-weight:500;letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
.sched-title{margin:0;font-size:28px;line-height:34px;font-weight:700;color:var(--gray-900)}
.sched-controls{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
.sched-controls .segment-btn{text-decoration:none}
.sched-nav{display:flex;gap:6px}

.sched-counts{padding:20px 24px;display:flex;flex-direction:column;gap:16px;margin-bottom:20px}
.sched-counts-head{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px}
.sched-label{font-size:12px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
.sched-note{font-size:13px;color:var(--gray-500)}
.sched-tiles{display:grid;grid-template-columns:minmax(0,1fr) 32px minmax(0,1fr) 32px minmax(0,1fr) 32px minmax(0,1fr);align-items:center}
.sched-tile{border:1px solid var(--gray-200);border-radius:10px;padding:16px 18px;display:flex;flex-direction:column;gap:2px;color:var(--gray-900)}
.sched-tile-label{font-weight:600}
.sched-tile-num{font-size:44px;line-height:52px;font-weight:700}
.sched-tile-in{background:var(--green-50);border-color:var(--green-200);color:var(--green-800)}
.sched-tile-out{background:var(--orange-50);border-color:var(--orange-200);color:var(--orange-800)}
.sched-tile-close{border:2px solid rgb(var(--brand-600));padding:15px 17px}
.sched-op{text-align:center;font-size:22px;color:var(--gray-500)}

.sched-day{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:20px;align-items:start}
.sched-side{display:flex;flex-direction:column;gap:20px}
.sched-scroll{overflow-x:auto}
.sched-week{min-width:980px}   /* wide enough that "Wed 28 TODAY" stays on one line (Nix 2026-10-28) */
.sched-grid{display:grid}
.sched-grid-day{grid-template-columns:64px minmax(0,1fr) minmax(0,1.5fr);min-width:520px}
.sched-grid-week{grid-template-columns:60px repeat(7,minmax(0,1fr))}
.sched-grid-head{border-bottom:1px solid var(--gray-200);background:#fff}
.sched-body{position:relative}
.sched-col-head{padding:12px;border-left:1px solid var(--gray-100);display:flex;align-items:baseline;gap:6px}
.sched-col-title{font-weight:600;color:var(--gray-900)}
.sched-col-count{font-size:12px;color:var(--gray-500)}
.sched-gutter{position:relative}
.sched-hour{position:absolute;right:8px;font-size:11px;color:var(--gray-500);white-space:nowrap}
/* Hour lines over quarter-hour lines; the row height follows the view. */
.sched-col{position:relative;border-left:1px solid var(--gray-100);
  background-image:linear-gradient(to bottom,var(--gray-200) 1px,transparent 1px),
                   linear-gradient(to bottom,var(--gray-100) 1px,transparent 1px)}
.sched-col-day{background-size:100% 64px,100% 16px}
.sched-col-week{background-size:100% 48px,100% 12px}
.sched-col-week.is-today{background-color:rgb(240 253 244 / .5)}

.sched-block{--sched-c:var(--blue-600);position:absolute;box-sizing:border-box;overflow:hidden;
  display:flex;flex-direction:column;padding:3px 6px;font-size:11.5px;line-height:14px;
  border-radius:6px;border-left:3px solid var(--sched-c);text-decoration:none;
  background:color-mix(in srgb,var(--sched-c) 13%,#fff);color:color-mix(in srgb,var(--sched-c) 62%,#000);
  transition:box-shadow .15s cubic-bezier(.4,0,.2,1)}
.sched-block:hover,.sched-block:focus-visible{outline:none;box-shadow:0 0 0 2px rgb(var(--brand-600));z-index:3}
.sched-block.is-short{padding:1px 6px;font-size:11px;line-height:13px}
.sched-block-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sched-block-sub{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.85}
.sched-block-check_in{background:var(--green-50);border-left-color:var(--green-500);color:var(--green-800)}
.sched-block-check_out{background:var(--orange-50);border-left-color:var(--orange-500);color:var(--orange-800)}
.sched-block-meeting{background:var(--purple-50);border-left-color:var(--purple-500);color:var(--purple-700)}
.sched-sum-meet{color:var(--purple-700)}
.sched-block.is-offsite{background:repeating-linear-gradient(135deg,color-mix(in srgb,var(--sched-c) 22%,#fff) 0 5px,#fff 5px 10px)}
.sched-block.is-pending{outline:1.5px dashed var(--gray-500);outline-offset:-1px;opacity:.7}
/* Colour fills (TameLink-44fg): the view sets --sched-c to the service, Arrival
   or Exit colour and adds is-ink-light / is-ink-dark, which the PHP picks by
   contrast. --sched-tone is the opposite of the text colour, so the off-site
   stripes move away from the text and never cost contrast. */
/* The preview drawer's band takes the same fill and ink (TameLink-4zx7.10). */
.sched-block.has-color,.sched-drawer-band.has-color{background:var(--sched-c);border-left-color:color-mix(in srgb,var(--sched-c) 70%,#000)}
.sched-block.is-ink-light,.sched-drawer-band.is-ink-light{color:#fff;--sched-tone:#000}
.sched-block.is-ink-dark,.sched-drawer-band.is-ink-dark{color:var(--gray-900);--sched-tone:#fff}
.sched-block.has-color.is-offsite{background:repeating-linear-gradient(135deg,var(--sched-c) 0 5px,color-mix(in srgb,var(--sched-c) 80%,var(--sched-tone)) 5px 10px)}
/* Drag to reschedule (js/booking-schedule.js). The server decides which blocks
   are .is-movable. A dragged block lets the pointer through so the column
   under it can be found, which is why the grabbing cursor sits on the grid. */
.sched-block.is-movable{cursor:grab;user-select:none;touch-action:none}
.sched-block.is-dragging{opacity:.92;z-index:20;pointer-events:none;box-shadow:var(--shadow-lg)}
.sched-body.is-dragging{cursor:grabbing}
.sched-col.is-drop{background-color:rgb(var(--brand-50))}

.sched-now{position:absolute;left:0;right:0;height:2px;background:rgb(var(--brand-600));z-index:4;pointer-events:none}
.sched-now-day{left:64px}
.sched-now-dot{position:absolute;left:-5px;top:-4px;width:9px;height:9px;border-radius:9999px;background:rgb(var(--brand-600))}
.sched-now-label{position:absolute;left:-60px;top:-8px;font-size:10.5px;font-weight:700;color:rgb(var(--brand-700))}

.sched-side-head{padding:16px 18px;border-bottom:1px solid var(--gray-100);font-size:17px;font-weight:600;color:var(--gray-900)}
.sched-empty{margin:0;padding:16px 18px;font-size:13px;color:var(--gray-500)}
.sched-roster-group{padding:12px 18px;display:flex;flex-direction:column;gap:6px;border-bottom:1px solid var(--gray-100)}
.sched-roster-group:last-child{border-bottom:0}
.sched-roster-top{display:flex;justify-content:space-between;align-items:center}
.sched-roster-range{font-size:13px;font-weight:600;color:var(--gray-700)}
.sched-roster-row{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:13px;
  white-space:nowrap;color:var(--gray-600);text-decoration:none}
.sched-roster-row:hover .sched-roster-who{text-decoration:underline}
.sched-roster-who{min-width:0;overflow:hidden;text-overflow:ellipsis}
.sched-roster-who strong{font-weight:600;color:var(--gray-900)}
.sched-roster-note{flex:none;font-size:12px;font-weight:500;color:var(--gray-500)}
.sched-roster-row.is-out .sched-roster-note{font-weight:600;color:var(--orange-700)}
.sched-roster-row.is-left .sched-roster-who,
.sched-roster-row.is-left .sched-roster-who strong{text-decoration:line-through;color:var(--gray-500)}
.sched-roster-row.is-left .sched-roster-note{font-weight:600;color:var(--gray-500)}
.sched-roster-row.is-in .sched-roster-note{font-weight:600;color:var(--green-700)}

.sched-summary{margin:-4px 0 16px;font-size:13px;color:var(--gray-600)}
.sched-summary > strong:first-child{color:var(--gray-900)}
.sched-sum-in{color:var(--green-700)}
.sched-sum-out{color:var(--orange-700)}
.sched-sum-appt{color:var(--blue-700)}
.sched-onsite-label{display:flex;align-items:flex-end;padding:0 8px 10px;font-size:10.5px;font-weight:600;letter-spacing:.05em;color:var(--gray-500)}
.sched-day-head{padding:10px;border-left:1px solid var(--gray-100);border-bottom:2px solid transparent;
  display:flex;flex-direction:column;gap:2px;text-decoration:none;color:inherit}
.sched-day-head:hover{background:var(--gray-50)}
.sched-day-head.is-today{background:rgb(var(--brand-50));border-bottom-color:rgb(var(--brand-600))}
.sched-day-top{display:flex;justify-content:space-between;align-items:center;gap:4px}
.sched-day-name{font-weight:600;color:var(--gray-900)}
.sched-today{font-size:10.5px;font-weight:700;color:rgb(var(--brand-700))}
.sched-day-counts{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;column-gap:4px;padding:2px 0}
.sched-day-num{display:flex;flex-direction:column;align-items:center}
.sched-day-num strong{font-size:20px;line-height:26px;font-weight:700;color:var(--gray-900)}
.sched-day-num small{font-size:10.5px;line-height:13px;color:var(--gray-500)}
.sched-day-moves{display:flex;flex-direction:column;align-items:center;gap:1px;font-size:12px;line-height:14px;font-weight:600}

/* Reschedule confirm and the save error. The dialog itself is .modal-overlay /
   .modal-panel; only its From/To rows and warning layout are new. */
.sched-alert{margin-bottom:16px}
.sched-move-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
.sched-move-when{display:grid;grid-template-columns:60px minmax(0,1fr);row-gap:6px;margin:0 0 14px;font-size:14px}
.sched-move-when dt{color:var(--gray-500)}
.sched-move-when dd{margin:0}
.sched-move-from{color:var(--gray-500);text-decoration:line-through}
.sched-move-to{font-weight:600;color:var(--gray-900)}
.sched-move-warn{display:flex;align-items:flex-start;gap:8px;margin-bottom:14px}
.sched-move-warn.hidden{display:none}
.sched-move-warn svg{flex:none;margin-top:1px}
.sched-move-notify{margin:0;font-size:13px;color:var(--gray-600)}
.sched-move-scope{margin:0 0 14px;padding:0;border:0;font-size:14px}
.sched-move-scope.hidden{display:none}
.sched-move-scope legend{padding:0;margin-bottom:6px;color:var(--gray-500)}
.sched-move-scope label{display:flex;align-items:center;gap:8px;padding:3px 0;color:var(--gray-900)}
.sched-move-scope label.is-blocked{color:var(--gray-500)}  /* F12: an option the save would refuse */
.sched-move-linked{margin:0 0 14px;font-size:14px}
.sched-move-linked.hidden{display:none}
.sched-move-linked p{margin:0 0 4px;color:var(--gray-500)}
.sched-move-linked ul{margin:0;padding-left:18px;color:var(--gray-900)}
.booking-meetings-heading{margin-bottom:12px}
.booking-meetings{list-style:none;margin:0;padding:0;font-size:14px;color:var(--gray-900)}
.booking-meetings li{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:12px;align-items:baseline;padding:6px 0}
.booking-meetings li + li{border-top:1px solid var(--gray-100)}
.booking-meetings-name{font-weight:500}
.booking-meetings-length{color:var(--gray-500)}
/* Stay changes (TameLink-zi3y): history card and the Change pick-up day box */
.stay-changes{list-style:none;margin:0;padding:0;font-size:14px;color:var(--gray-900)}
.stay-changes li{display:grid;gap:2px;padding:8px 0}
.stay-changes li + li{border-top:1px solid var(--gray-100)}
.stay-changes-what{font-weight:500}
.stay-changes-by{font-size:13px;color:var(--gray-500)}
.stay-changes-note{color:var(--gray-600);white-space:pre-line}
/* Booking page daily report cards: one tile per stay day, 5 to a row. */
.day-tiles{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px}
.day-tile{display:flex;flex-direction:column;gap:2px;min-height:56px;padding:10px;border-radius:var(--radius);
  font-size:13px;line-height:18px;color:var(--gray-900);text-decoration:none}
.day-tile strong{font-weight:600}
.day-tile.is-written{background:rgb(var(--brand-100))}
.day-tile.is-written .day-tile-state{color:rgb(var(--brand-800))}
.day-tile.is-draft{background:var(--yellow-100)}
.day-tile.is-draft .day-tile-state{color:var(--yellow-800)}
.day-tile.is-today{background:#fff;outline:2px solid rgb(var(--brand-600));outline-offset:-2px}
.day-tile.is-today .day-tile-state{color:rgb(var(--brand-700));font-weight:500}
.day-tile.is-empty{background:var(--gray-100)}
.day-tile.is-empty .day-tile-state{color:var(--gray-600)}
.day-tile:hover{box-shadow:var(--shadow)}
.day-tile:focus-visible{outline:none;box-shadow:var(--ring)}
.day-legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:12px;font-size:12px;line-height:16px;color:var(--gray-500)}
.day-legend > span{display:inline-flex;align-items:center;gap:6px}
.day-legend-swatch{width:12px;height:12px;border-radius:3px}
.day-legend-swatch.is-written{background:rgb(var(--brand-100))}
.day-legend-swatch.is-draft{background:var(--yellow-100)}
.day-legend-swatch.is-today{background:#fff;border:2px solid rgb(var(--brand-600))}
.day-legend-swatch.is-empty{background:var(--gray-100)}
@media (max-width:480px){.day-tiles{grid-template-columns:repeat(3,minmax(0,1fr))}}
.stay-change-panel{max-height:90vh;overflow-y:auto}
.stay-change-now{margin:0 0 14px;font-size:14px;color:var(--gray-500)}
.stay-change-when{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px}
.stay-change-days{margin:0 0 12px;font-size:14px;font-weight:600;color:var(--gray-900)}
.stay-change-block{display:block;border:0;margin:0 0 14px;padding:0;min-width:0;font-size:14px}
/* .hidden sits near the top of this file, so later display rules (.alert, .field-error) would win. */
#stay-change .hidden{display:none}
.stay-change-list{margin:4px 0 0;padding-left:18px;color:var(--gray-900)}
.stay-change-choice{display:inline-flex;align-items:center;gap:6px;margin-right:20px}
.stay-change-sum{margin:8px 0 0;font-weight:600;color:var(--gray-900)}
@media (max-width:480px){.stay-change-when{grid-template-columns:minmax(0,1fr)}}

@media (max-width:1023px){
  .sched-day{grid-template-columns:minmax(0,1fr)}
}
@media (max-width:767px){
  .sched-tiles{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}
  .sched-op{display:none}
  .sched-title{font-size:22px;line-height:28px}
}
/* Booking preview drawer. The panel and backdrop are .drawer-panel /
   .drawer-backdrop; only its width and inner layout are new. */
.sched-block.is-selected{box-shadow:0 0 0 2px rgb(var(--brand-600));z-index:3}
.sched-drawer{max-width:420px}
/* Header band: the event type centred on the same colour as its calendar block
   (TameLink-4zx7.10): the .sched-block default tint and kind tints below, or the
   block's own fill (.has-color + --sched-c + ink, copied by js/booking-schedule.js).
   The close button sits in the right-hand column; the empty left column the same
   width keeps the title truly centred. */
.sched-drawer-band{--sched-c:var(--blue-600);display:grid;grid-template-columns:40px minmax(0,1fr) 40px;align-items:center;padding:12px;
  background:color-mix(in srgb,var(--sched-c) 13%,#fff);color:color-mix(in srgb,var(--sched-c) 62%,#000)}
.sched-drawer-band-check_in{background:var(--green-50);color:var(--green-800)}
.sched-drawer-band-check_out{background:var(--orange-50);color:var(--orange-800)}
.sched-drawer-band-meeting{background:var(--purple-50);color:var(--purple-700)}
.sched-drawer-type{grid-column:2;margin:0;text-align:center;font-size:20px;line-height:28px;font-weight:700}
/* The close × takes the band's ink and a 40px touch target. */
.sched-drawer-band .btn-close{grid-column:3;width:40px;height:40px;color:inherit;opacity:.7}
.sched-drawer-band .btn-close:hover{color:inherit;opacity:1}
.sched-drawer-head{display:flex;flex-direction:column;gap:4px;padding:16px 24px;border-bottom:1px solid var(--gray-100)}
.sched-drawer-title{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px}
.sched-drawer-pet{margin:0;min-width:0;font-size:22px;line-height:28px;font-weight:700;color:var(--gray-900)}
/* Long words with no spaces wrap: the drawer never scrolls sideways. */
.sched-drawer-type,.sched-drawer-pet,.sched-drawer-sub,.sched-drawer-facts dd,.sched-drawer-notes{overflow-wrap:anywhere}
.sched-drawer-sub{margin:0;font-size:14px;line-height:20px;color:var(--gray-600)}
/* Footer pinned under the scrolling body: the booking link left, its number right. */
.sched-drawer-foot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;
  padding:16px 24px;border-top:1px solid var(--gray-100)}
.sched-drawer-body{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:18px;padding:16px 24px}
.sched-drawer-facts{display:grid;grid-template-columns:96px minmax(0,1fr);row-gap:10px;margin:0}
.sched-drawer-facts dt,.sched-drawer-label{font-size:12px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
.sched-drawer-facts dt{line-height:20px}
.sched-drawer-facts dd{margin:0;font-size:14px;color:var(--gray-900)}
.sched-drawer-label{margin:0 0 6px}
.sched-drawer-notes{margin:0;font-size:14px;line-height:20px;color:var(--gray-700);white-space:pre-line}
.sched-drawer-ref{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--gray-500)}

/* Client Invoices list on a phone (portal QA F-50). Seven columns at 390px
   scrolled sideways and put Pay now off-screen. Below 1024px the table keeps
   Invoice, Status, Balance and the actions, with tighter cell padding; the
   invoice page itself still shows the service, total and due date. */
@media (max-width:639px){
  /* .5rem, not .75rem, and View stacked over Pay now: at .75rem the table was
     still 37px wider than the phone's box and cut Pay now in half (F83). */
  .client-inv-table th,.client-inv-table td{padding-left:.5rem;padding-right:.5rem}
  .client-inv-table td:last-child>.flex{flex-direction:column;align-items:flex-end;gap:.25rem}
  .client-inv-table td:last-child .btn{white-space:nowrap}
  /* View was a 20px tap target (QA CPv-08): 44px tall, padded (TameLink-e8t.1). */
  .client-inv-table td:last-child .btn-link{display:inline-flex;align-items:center;min-height:2.75rem;padding:0 .5rem}
  /* Client invoice page, Payment History (QA CPv-02): each payment stacks,
     each value labelled from data-label, so the table never scrolls sideways. */
  .pay-hist-stack,.pay-hist-stack tbody,.pay-hist-stack tr,.pay-hist-stack td{display:block;width:100%}
  .pay-hist-stack thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
  .pay-hist-stack tbody td{padding:.125rem 0}
  .pay-hist-stack tbody tr{padding:.5rem 0}
  .pay-hist-stack td[data-label]{display:flex;justify-content:space-between;gap:1rem;text-align:right}
  .pay-hist-stack td[data-label]::before{content:attr(data-label);font-size:.75rem;font-weight:600;
    letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500);text-decoration:none}
  .pay-hist-stack tfoot{display:block}
  .pay-hist-stack tfoot tr{display:flex;justify-content:space-between}
  .pay-hist-stack tfoot td{width:auto}
  .pay-hist-stack tfoot td:empty{display:none}
}
/* Up to 1023px (tablet too, F84): all seven columns needed 800px at 768. */
@media (max-width:1023px){
  .client-inv-table .inv-col-wide{display:none}
}

/* Client Documents list on a phone (portal QA F-50), same approach as the
   Invoices list above: five columns put Review & Sign off-screen at 390px.
   Below 640px it keeps Document, Status and the action; the status badge
   already says whether it is signed. */
@media (max-width:639px){
  .client-agr-table th,.client-agr-table td{padding-left:.75rem;padding-right:.75rem}
  .client-agr-table .agr-col-wide{display:none}
}

/* Phone tap targets (portal QA F86). Below 640px buttons are at least 44px
   tall and the small controls 40px (Apple / Google guidance is 44 / 48px).
   Text links get an invisible ::after that reaches 10px above and below, so
   the tap area grows without changing the link's box or the layout. :where()
   keeps the specificity at zero so display / margin / position utilities on
   the same element still win (no display or margin is set here). */
@media (max-width:639px){
  .btn{min-height:44px}
  .btn-sm,.btn-xs,.bk-chip,.bk-tab,.tab{min-height:40px}
  .bk-strip-btn{min-width:40px;min-height:40px}
  :where(.btn-link,.banner-link,.back-link,a.hover-brand){position:relative}
  :where(.btn-link,.banner-link,.back-link,a.hover-brand)::after{content:"";position:absolute;inset:-10px 0}
}

/* ── Punch list #16: booking day picker, Sunday-to-Saturday weeks ──────────
   Seven equal columns at every width, one week per row, weekday names once
   in a header row. Days before today stay in the first row, greyed out.
   When the strip is narrow (phones, or a narrow form column) the pill turns
   into a coloured dot that matches the legend; the button's aria-label and
   title still give the full wording. */
.bk-strip-wrap{container-type:inline-size}
.bk-strip-head,.bk-strip{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px}
.bk-strip-head{margin-bottom:4px}
.bk-strip-head span{text-align:center;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--gray-500)}
.bk-day{min-height:44px;justify-content:center}
.bk-day.is-past{border-color:var(--gray-100)}
.bk-day.is-past .bk-day-num{color:var(--gray-500)}
@container (max-width:560px){
  .bk-strip-head,.bk-strip{gap:4px}
  .bk-strip-head span{letter-spacing:0}
  .bk-day{padding:6px 2px;border-radius:7px}
  .bk-day-num{font-size:16px}
  .bk-day-pill,.bk-day-pill.is-tight{font-size:0;width:8px;height:8px;padding:0;margin-top:3px;flex:none}
  .bk-day-pill.is-open{background:rgb(var(--brand-500))}
  .bk-day-pill.is-low{background:var(--amber-600)}
  .bk-day-pill.is-off{background:var(--gray-300)}
}

/* ── #27 floor board "Check add-ons" card line (L-FLOOR, 2026-10-22) ── */
.floor-addons{font-size:11.5px;font-weight:600;color:var(--teal-800);background:var(--teal-50);
  border:1px solid var(--teal-200);border-radius:var(--radius-sm);padding:.3rem .5rem}

/* ── #32 / #33 / #12 calendar: unsent changes, Changes not sent list (L-CAL, 2026-10-22) ── */
/* B33 (Nix, 2026-10-29): a change saved without notifying the client keeps
   the block's colours (no .is-unsent rule); a small mail-off icon ends the
   first line, so short blocks show it too. The title gives way, never the icon. */
.sched-block-line{display:flex;align-items:center;gap:3px;min-width:0}
.sched-block-line .sched-block-title{min-width:0}
.sched-block-icon{flex:none;width:12px;height:12px}
/* The move confirm has three choices; let them wrap on a narrow screen. */
#sched-move .modal-footer{flex-wrap:wrap}
/* Dashboard rows that hold a link and an action (Send, View on calendar). */
.dash-list-link{display:block;color:inherit;text-decoration:none}
.dash-list-action{flex-shrink:0;font-size:12.5px;white-space:nowrap}

/* ── #8 Edit Booking: shared booking form in edit mode (L-EDIT, 2026-10-22) ── */
/* The "When?" card is wrapped in a fieldset so it can be locked; no frame of its own. */
.bk-fieldset{border:0;padding:0;margin:0;min-width:0}
/* Where the change leaves the client: Paid in full / Balance due / Overpaid. */
.bk-rail-result{font-weight:700;justify-content:flex-end}
.bk-rail-result.is-ok{color:var(--green-700)}
.bk-rail-result.is-due{color:var(--orange-700)}
.bk-rail-result.is-over{color:var(--red-700)}
/* Save changes and Save Without Notifying stack; the second sits closer. */
.bk-cta + .bk-cta{margin-top:8px}

/* Employee time pages (Phase 28b) — docs/specs/2026-10-23-employee-time-pages-design.md §7.2 */
/* Week of shifts (admin/employees-schedules.php). Named .shift-week /
   .shift-day-head, not the spec's .sched-week / .sched-day-head: those names
   already belong to the Pet Booking Schedule above (Nix's coordinator,
   2026-10-24). */
.shift-week{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.75rem}
.shift-day{display:flex;flex-direction:column;gap:.5rem;min-width:0}
.shift-day-head{display:flex;flex-direction:column;gap:4px;padding-bottom:.5rem;border-bottom:1px solid var(--gray-200)}
/* 2px underline on today; 1px less padding so the cards below do not move. */
.shift-day-head.is-today{padding-bottom:calc(.5rem - 1px);border-bottom:2px solid rgb(var(--brand-600))}
.shift-day-top{display:flex;justify-content:space-between;align-items:center;gap:6px}
.shift-day-label{font-size:12px;line-height:16px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
.shift-day-head.is-today .shift-day-label{color:rgb(var(--brand-700))}
.shift-day-date{font-size:14px;line-height:20px;font-weight:600;color:var(--gray-900)}
.shift-day-counts{font-size:12px;line-height:16px;color:var(--gray-500)}

/* Shift card: row 1 = two time chips, row 2 = name ▾ and hours. */
.shift-card{position:relative;display:flex;flex-direction:column;gap:6px;padding:.5rem;
  background:#fff;border:1px solid var(--gray-200);border-radius:var(--radius);box-shadow:var(--shadow-sm)}
.shift-times{display:flex;align-items:center;gap:4px;font-size:13px;color:var(--gray-500)}
.shift-time{flex:1;min-width:0;height:26px;padding:0 6px;text-align:center;
  font:inherit;font-size:13px;line-height:24px;font-weight:600;color:var(--gray-900);white-space:nowrap;
  background:var(--gray-50);border:1px solid var(--gray-200);border-radius:var(--radius-sm);cursor:pointer}
.shift-time:focus-visible,.shift-time.is-editing{outline:none;background:#fff;box-shadow:var(--ring)}
.shift-time.is-invalid{border-color:var(--red-400)}
.shift-meta{display:flex;justify-content:space-between;align-items:center;gap:6px;font-size:13px;line-height:18px}
.shift-name{min-width:0;padding:0 0 0 7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  background:none;border:0;font:inherit;font-weight:500;color:var(--gray-900);text-align:left;cursor:pointer}
.shift-name:focus-visible{outline:none;border-radius:var(--radius-xs);box-shadow:var(--ring)}
.shift-hours{flex:none;padding-right:7px;font-size:12px;line-height:16px;color:var(--gray-500);white-space:nowrap}
/* Draft line ("◌ New" / "◌ Changed" / "◌ Removed"); the dashed-circle svg is 14px. */
.shift-draft{display:flex;align-items:center;gap:4px;font-size:12px;line-height:16px;font-weight:600;
  color:var(--blue-700);white-space:nowrap}
.shift-draft svg{flex:none;width:14px;height:14px}
/* A removed published shift: gray card, times and name struck through. */
.shift-card.is-removed{background:var(--gray-50);box-shadow:none}
.shift-card.is-removed .shift-times,
.shift-card.is-removed .shift-time,
.shift-card.is-removed .shift-name,
.shift-card.is-removed .shift-hours{color:var(--gray-500);text-decoration:line-through}
/* Corner trash. Hidden with opacity (not display) so it stays in the tab
   order: focusing it fires :focus-within and shows it. .is-touched is set by
   Alpine after a tap on a touch screen. */
.shift-delete{position:absolute;top:-10px;right:-10px;z-index:2;display:inline-flex;align-items:center;justify-content:center;
  width:26px;height:26px;padding:0;border-radius:9999px;cursor:pointer;
  background:var(--red-50);border:1px solid var(--red-200);color:var(--red-700);
  opacity:0;pointer-events:none;transition:opacity .15s,background-color .15s}
.shift-delete svg{width:14px;height:14px}
.shift-card:hover .shift-delete,
.shift-card:focus-within .shift-delete,
.shift-card.is-touched .shift-delete{opacity:1;pointer-events:auto}
.shift-delete:hover{background:var(--red-100)}
.shift-delete:focus-visible{outline:none;box-shadow:var(--ring)}

/* Time-off cards on the week: approved = amber, requested = white with a dashed amber border. */
.timeoff-card{display:flex;flex-direction:column;gap:4px;padding:.5rem;font-size:13px;line-height:18px;color:var(--gray-900);
  background:var(--amber-50);border:1px solid var(--amber-200);border-radius:var(--radius)}
.timeoff-card.is-requested{background:#fff;border:1px dashed var(--amber-600)}

/* Amber badges: "Time Off" in the Hours table, "Approved" on the amber card. */
.badge-amber{background:var(--amber-50);border:1px solid var(--amber-200);color:var(--amber-800)}
.badge-amber-outline{background:#fff;border:1px solid var(--amber-200);color:var(--amber-800)}

/* Red time-off count: sits in the text flow (not absolute like .nav-count),
   after "Employees" in the bar and at the end of "Time Off" in the menu and
   drawer. No margin: the parent row's gap spaces it. */
.nav-count-alert{display:inline-flex;align-items:center;justify-content:center;flex:none;
  min-width:1.25rem;height:1.25rem;padding:0 .375rem;border-radius:9999px;background:var(--red-600);
  font-size:.75rem;line-height:1rem;font-weight:700;color:#fff;white-space:nowrap}
/* Blue "new" count on the avatar: .nav-count's corner position and ring, blue
   fill. The avatar carries .relative. */
.nav-count-new{position:absolute;top:-.25rem;right:-.25rem;display:inline-flex;
  align-items:center;justify-content:center;min-width:1.25rem;height:1.25rem;padding:0 .25rem;
  border-radius:9999px;background:var(--blue-600);color:#fff;
  font-size:.75rem;line-height:1rem;font-weight:700;box-shadow:0 0 0 2px rgb(var(--brand-700))}

/* Header-size clock button (My Time and Dashboard headers): 56px tall. */
.btn-clock.btn-clock-header{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;
  width:auto;max-width:none;padding:.875rem 2rem;font-size:1.125rem;line-height:1.75rem}

/* Clock block (views/partials/clock-block.php, My Time and Dashboard headers):
   last punch ("Out at 6:52p") beside the button. On a phone the button goes
   full width (up to 20rem) and centred, with the last punch under it, and
   the Dashboard's + New Booking centred below (Nix, 2026-10-27). */
.clock-block{display:flex;align-items:center;gap:1rem}
.clock-block-last{font-size:1rem;line-height:1.5rem;font-weight:600;color:var(--gray-900);white-space:nowrap}
@media (max-width:639px){
  .dash-header-actions{width:100%;justify-content:center}
  .clock-block{flex-direction:column;gap:.5rem;width:100%}
  .clock-block-form{order:-1;display:flex;justify-content:center;width:100%}
  .clock-block .btn-clock.btn-clock-header{width:100%;max-width:20rem}
}

/* My Time, Schedule tab: two weeks of day boxes. A plain box is a shift day. */
.mytime-days{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px}
.mytime-day{display:flex;flex-direction:column;gap:2px;min-width:0;min-height:74px;padding:8px;
  font-size:13px;line-height:18px;color:var(--gray-900);
  background:#fff;border:1px solid var(--gray-200);border-radius:var(--radius)}
.mytime-day-label{font-size:11px;line-height:14px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
.mytime-day-time{font-weight:600}
.mytime-day-sub{font-size:12px;line-height:16px;color:var(--gray-500)}
.mytime-day-sub.is-amber{color:var(--amber-800);font-weight:600}
.mytime-day.is-off{background:var(--gray-100);color:var(--gray-600)}
.mytime-day.is-off .mytime-day-label{color:var(--gray-600)}
.mytime-day.is-timeoff{background:var(--amber-50);border-color:var(--amber-200)}
.mytime-day.is-timeoff .mytime-day-sub{color:var(--amber-800)}
.mytime-day.is-requested{border:1px dashed var(--amber-600)}
.mytime-day.is-today{background:rgb(var(--brand-50));border-color:rgb(var(--brand-600))}
.mytime-day.is-today .mytime-day-label,
.mytime-day.is-today .mytime-day-sub{color:rgb(var(--brand-700))}

/* Weekly availability: 7 columns (profile, availability edit), a list on a phone. */
.avail-week{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.5rem}
.avail-day{display:flex;flex-direction:column;gap:4px;min-width:0;padding:.5rem;text-align:center;
  font-size:13px;line-height:18px;color:var(--gray-900);border:1px solid var(--gray-200);border-radius:var(--radius)}
.avail-day-name{font-size:12px;line-height:16px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
.avail-day.is-off{color:var(--gray-600);background:var(--gray-50)}
.avail-day.is-off .avail-day-name{color:var(--gray-600)}

/* Timesheets / Hours tab: a voided punch stays on the list, grayed. Mark the
   Clock In and Clock Out cells .is-struck; the Voided badge is not struck. */
.data-table tbody tr.row-voided td{color:var(--gray-500)}
.data-table tbody tr.row-voided td.is-struck{text-decoration:line-through}

/* Employee profile, manager view (spec §5.2, option B; views/admin/employees/show.php).
   Sidebar card + main column; the sidebar goes full width on a tablet. */
.emp-profile{display:flex;flex-wrap:wrap;gap:1.5rem;align-items:flex-start}
.emp-profile-side{flex:1 1 280px;max-width:320px;display:flex;flex-direction:column;gap:1rem}
.emp-profile-main{flex:999 1 560px;min-width:0;display:flex;flex-direction:column;gap:1.25rem}
.emp-profile-head{display:flex;flex-direction:column;gap:4px}
.emp-profile-name{margin:0;font-size:22px;line-height:30px;font-weight:700;color:var(--gray-900)}
.emp-profile-details{display:flex;flex-direction:column;gap:1rem}
.break-words{overflow-wrap:anywhere}
.emp-profile-section{display:flex;flex-direction:column;gap:.5rem;padding-top:1rem;border-top:1px solid var(--gray-100)}
.emp-profile-section-head{display:flex;align-items:center;justify-content:space-between}
/* Phone-only "Address, emergency contact, password ▾" disclosure, link look. */
.emp-profile-more-btn{display:none;align-items:center;min-height:44px;padding:0;background:none;border:0;
  font-size:.875rem;line-height:1.25rem;font-weight:500;color:rgb(var(--brand-600));cursor:pointer}
.emp-profile-more-btn:hover{color:rgb(var(--brand-hover))}
.emp-profile-more-btn:focus-visible{outline:none;box-shadow:var(--ring);border-radius:.125rem}
/* Card header row and request rows (profile Time Off card, .card.card-flush). */
.card-head-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 24px;border-bottom:1px solid var(--gray-100)}
.timeoff-tools{display:flex;align-items:center;gap:12px}
.timeoff-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 16px;
  padding:12px 24px;border-top:1px solid var(--gray-100)}
.timeoff-row:first-child{border-top:0}
.timeoff-row-main{display:flex;flex-direction:column;min-width:0}
.timeoff-row-dates{font-weight:600;color:var(--gray-900)}
.timeoff-row-sub{font-size:.875rem;line-height:1.25rem;color:var(--gray-500)}
.timeoff-row-actions{display:flex;align-items:center;gap:16px}
.avail-day-time{font-weight:600;white-space:pre-line}

@media (max-width:1023px){
  .shift-week{grid-template-columns:repeat(2,minmax(0,1fr))}
  .emp-profile-side{flex-basis:100%;max-width:none}
  .emp-profile-head{flex-direction:row;align-items:center;justify-content:space-between}
  .emp-profile-details{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}
}
@media (max-width:767px){
  .mytime-days{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:639px){
  .shift-week{grid-template-columns:minmax(0,1fr)}
  .avail-week{grid-template-columns:minmax(0,1fr);gap:0}
  .avail-day{flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;
    min-height:44px;padding:.625rem 0;text-align:left;background:none;border:0;border-radius:0;border-top:1px solid var(--gray-100)}
  .avail-day:first-child{border-top:0}
  .avail-day.is-off{background:none}
  .emp-profile-details{display:flex;flex-direction:column}
  .emp-profile-more-btn{display:flex}
  .emp-profile-more:not(.is-open){display:none}
  .card-head-row{flex-direction:column;align-items:stretch;padding:14px 16px}
  .timeoff-tools{flex-direction:column;align-items:stretch}
  .timeoff-tools .segment{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}
  .timeoff-tools .segment-btn{min-height:40px}
  .timeoff-tools .btn{min-height:44px;width:100%;justify-content:center}
  .timeoff-row{flex-direction:column;align-items:stretch;padding:12px 16px}
  .timeoff-row-actions .btn-link{display:inline-flex;align-items:center;min-height:44px}
}

/* Schedule page states (Phase 28b schedule build, 2026-10-24). */
.shift-day.is-drop{border-radius:var(--radius);background:rgb(var(--brand-50));box-shadow:0 0 0 2px rgb(var(--brand-200))}
.shift-card.is-dragging{opacity:.5}
.shift-card.is-editing{border-color:rgb(var(--brand-400));box-shadow:var(--ring)}
.shift-card.is-invalid{border-color:var(--red-400)}
.shift-staff-list{min-width:14rem;max-height:16rem;overflow-y:auto}
.dropdown-item.is-active{background:var(--gray-50)}
.shift-head-divider{align-self:stretch;width:1px;background:var(--gray-200)}
.copy-day{min-height:44px;justify-content:center}
.shift-day > .btn-dashed{min-height:40px}

/* Products list (37a.6) ------------------------------------------------------
   Products page (views/admin/products/index.php) and the Add / Edit Product
   form (_form.php). Kept in one block so it can be committed on its own. */

/* Form: "Purchase price [$ __] per [unit]" on one line. */
.prod-cost-row{display:flex;align-items:center;gap:.5rem}
.prod-cost-amount{flex:1 1 0;min-width:0}
.prod-cost-per{flex-shrink:0;font-size:.875rem;color:var(--gray-500)}
.prod-cost-unit{flex:0 0 7.5rem;width:7.5rem}
/* Current image on Edit, with Remove image beside it; faded once ticked. */
.prod-image-current{display:flex;align-items:center;gap:1rem;margin-bottom:.75rem}
.prod-image-preview{width:4rem;height:4rem;object-fit:cover;border-radius:var(--radius);border:1px solid var(--gray-200)}
.prod-image-current.is-removed .prod-image-preview{opacity:.35}

/* Name search above the table. */
.prod-search{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem;max-width:32rem}
.prod-search .field-input{flex:1 1 0;min-width:0}
.prod-search .prod-sort-select{flex:0 0 auto;width:auto}

/* Name header sorts A–Z / Z–A; it keeps the header's own type. */
.prod-sort-link{display:inline-flex;align-items:center;gap:.25rem;color:inherit;
  font:inherit;letter-spacing:inherit;text-transform:inherit}
.prod-sort-link:hover{color:var(--gray-800)}

/* Name with its Out of stock / Low stock / Inactive tags. */
.prod-cell-name{overflow-wrap:anywhere}
/* Description wraps to two lines at most, so it never widens the table. */
.prod-desc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;max-width:24rem;white-space:normal}
/* Last resort only: if a row is still too wide, scroll the table, never clip the buttons. */
.prod-table-wrap{overflow-x:auto}
.prod-name-line{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem .5rem}
.prod-table td{vertical-align:middle}

/* Row actions: three small icon buttons (Add to Cart, Adjust stock, Edit). */
.prod-cell-actions{text-align:right;white-space:nowrap}
.prod-actions{display:inline-flex;align-items:center;justify-content:flex-end;gap:.375rem}
.prod-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;
  border-radius:var(--radius-sm);border:1px solid var(--gray-200);background:#fff;color:var(--gray-600);
  transition:background-color .15s,border-color .15s,color .15s}
.prod-icon-btn:hover{background:var(--gray-50);border-color:var(--gray-300);color:var(--gray-900)}
.prod-icon-btn:focus-visible{outline:2px solid rgb(var(--brand-600));outline-offset:2px}
.prod-icon-btn.is-brand{background:rgb(var(--brand-50));border-color:transparent;color:rgb(var(--brand-700))}
.prod-icon-btn.is-brand:hover{background:rgb(var(--brand-100))}
.prod-icon-btn:disabled{background:var(--gray-100);border-color:transparent;color:var(--gray-400);cursor:not-allowed}

/* Narrow windows: each product becomes a stacked card instead of a table
   row, so the page never scrolls sideways. Each cell after the name shows
   its column name from data-label. */
@media (max-width:1023px){
  .prod-table,.prod-table tbody,.prod-table tr,.prod-table td{display:block;width:100%}
  .prod-table thead{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
  .prod-table tbody{border-top:1px solid var(--gray-100)}
  .prod-table tbody:first-of-type{border-top:0}
  .prod-table tbody tr{border-top:0;padding:.75rem 1rem}
  .prod-table tbody tr.prod-row-override{padding-top:0}
  .prod-table td{padding:.125rem 0}
  .prod-table td.prod-cell-name{padding-bottom:.5rem}
  .prod-table td[data-label]{display:flex;justify-content:space-between;gap:1rem}
  .prod-table td[data-label]::before{content:attr(data-label);font-size:.75rem;font-weight:600;
    letter-spacing:.05em;text-transform:uppercase;color:var(--gray-500)}
  .prod-table td.prod-cell-actions{padding-top:.5rem;text-align:left}
  .prod-table .prod-actions{justify-content:flex-start}
  .prod-icon-btn{width:2.5rem;height:2.5rem}
  .prod-cost-unit{flex-basis:6rem;width:6rem}
}

/* ==== Mobile/tablet UI review fixes (Nix 2026-10-27, session tamelink-c6) ==== */
/* Booking form (views/bookings/_form.php), mobile/tablet review 2026-10-27.
   Append after the existing .bk-* block in css/app.css. */

/* D4: the price sits on its own line at the bottom right of a service card,
   so a long name has the full width. margin-top:auto keeps it at the bottom
   when cards in a row have different heights. */
.bk-svc-price{margin-top:auto;padding-top:4px;align-self:flex-end;text-align:right}

/* T5: tablet (768-1023px). Keep the Summary in the right-hand column, as on
   desktop, instead of wrapping under the steps. The main column shrinks
   (its grids are auto-fill, so they drop to fewer columns); the rail is a
   fixed 260px and stays sticky. 1024px and up is unchanged. */
@media (min-width:768px) and (max-width:1023px){
  .bk-layout{flex-wrap:nowrap}
  .bk-main{flex:1 1 0}
  .bk-rail{flex:0 0 260px;max-width:260px}
}

/* Dashboard header, tablet (T4, Nix 2026-10-27): beside the clock block and
   + New Booking the greeting wrapped onto two lines at 768px. From 640px to
   1023px the clock block and New Booking get their own row under the
   greeting. Phone (<640) already stacks; desktop unchanged. */
@media (min-width:640px) and (max-width:1023px){
  .dash-header{flex-direction:column;align-items:flex-start}
}

/* Floor board card (T6, Nix 2026-10-27): drop-off time and stay dates on
   their own line, in full (owner and service stay on the .floor-meta line).
   Wraps rather than cutting off, so the Overdue check-out badge (B8) fits. */
.floor-dates{display:flex;flex-wrap:wrap;align-items:center;gap:.25rem .5rem;
  margin-top:.125rem;font-size:12px;color:var(--gray-500)}

/* Dashboard In House row: keep the Overdue check-out badge (B8) on one line. */
.dash-row .badge{white-space:nowrap;flex-shrink:0}

/* Staff Invoices list (admin/invoices.php; Nix, 2026-10-27, mobile/tablet UI
   review T1 + D1). Service now sits as a line under Client / Pet, so the table
   has six columns. The client name stays on one line at every width (D1: it
   wrapped at desktop even with room); the other columns give way instead.
   Up to 1023px (tablet and phone) the Total column is hidden; Balance stays. */
.staff-inv-table .staff-inv-client{white-space:nowrap}
@media (max-width:1023px){
  .staff-inv-table .inv-col-total{display:none}
}

/* Email signing link buttons on the booking page (T11, Nix 2026-10-27): the
   "Before Checking In · Client" rows and the "Agreement to sign" notice.
   One button per owed agreement, right-aligned beside the item text; long
   agreement names wrap inside the card instead of running past it. Under
   768px the buttons drop below the item text, left-aligned. */
.signing-links{display:flex;flex-direction:column;align-items:flex-end;gap:.25rem;
  flex:0 1 auto;min-width:0;max-width:55%}
.signing-links form{max-width:100%}
.signing-links .btn-link{white-space:normal;overflow-wrap:anywhere;text-align:right}
@media (max-width:767px){
  .checkin-item{flex-direction:column;align-items:stretch;gap:.5rem}
  .checkin-item .signing-links{align-items:flex-start;max-width:none}
  .checkin-item .signing-links .btn-link{text-align:left}
}

/* Portal My Bookings header (D3/P3, 2026-10-27): the status filter is a short
   dropdown beside the "My Bookings" heading and applies when changed. The
   heading block and the Book button wrap onto two lines on a phone. */
.portal-bookings-head{flex-wrap:wrap;gap:.75rem}
.portal-bookings-title{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}
.portal-bookings-filter{display:flex;align-items:center;gap:.5rem}

/* Staff Time Off review history and Agreement Templates lists at tablet width
   (Nix, 2026-10-27, mobile/tablet UI review T1). Reason / Description now sit
   as a line under Employee / Title. Up to 1023px (tablet and phone) the
   columns that are not needed day to day are hidden: Reviewed By and Notes
   on Time Off history; Date Created and Created By on Agreement Templates.
   Desktop keeps every column. */
@media (max-width:1023px){
  .timeoff-history-table .col-wide,
  .agr-templates-table .col-wide{display:none}
}

/* Muted second line inside a table cell (e.g. Reason under Employee,
   Description under Title). Approved by the main session for Nix, 2026-10-27. */
.cell-sub{display:block;margin-top:.125rem;font-size:.75rem;line-height:1rem;
  font-weight:400;color:var(--gray-500)}

/* ---- Phone fixes (mobile UI review, Nix 2026-10-27, agent a-tables) ---- */
/* Roles page permission grid: keeps room for the Permission label plus the four
   role columns; on a phone the card's .overflow-x-auto wrapper scrolls it
   sideways (B4). No effect once the card is wider than 36rem. */
.roles-grid{min-width:36rem}
/* Phone tap targets (QA EPv-08, TameLink-4hxl; Nix FN1 call 2026-11-04): each
   Roles checkbox cell is a <label>, 44px tall, so the whole cell takes the tap;
   admin list row links (.row-link, .btn-link) are 44px tall too. */
@media (max-width:639px){
  .roles-cell{align-items:center;min-height:2.75rem}
  .data-table td .row-link,.data-table td .btn-link{display:inline-flex;align-items:center;min-height:2.75rem}
}
/* Edge fade on admin list tables that scroll sideways (QA EPv-07, TameLink-l3h1;
   Nix FN1 call 2026-11-04). CSS only, no card layout: a soft shadow shows at
   each edge that has more columns past it. The two white "local" layers move
   with the table and cover the shadow once that edge is reached; the two
   "scroll" layers stay put at the box edges. A table that fits shows nothing. */
@media (max-width:767px){
  .overflow-x-auto:has(> .data-table){
    background-color:#fff;
    background-image:
      linear-gradient(to right,#fff 30%,rgba(255,255,255,0)),
      linear-gradient(to left,#fff 30%,rgba(255,255,255,0)),
      linear-gradient(to right,rgba(17,24,39,.15),rgba(17,24,39,0)),
      linear-gradient(to left,rgba(17,24,39,.15),rgba(17,24,39,0));
    background-position:left center,right center,left center,right center;
    background-size:2.5rem 100%,2.5rem 100%,1.25rem 100%,1.25rem 100%;
    background-repeat:no-repeat;
    background-attachment:local,local,scroll,scroll}
}
/* Portal Account Settings, Account Details (QA CPv-07, TameLink-e8t.2; Nix FN1
   call 2026-11-04): on a phone a long email squeezed its label ("Account Holder
   Email / Login") onto 4 lines. Each label now sits above its value; a long
   email may break anywhere rather than run off the card. */
@media (max-width:639px){
  .acct-details>div{flex-direction:column;gap:.125rem}
  .acct-details dd{overflow-wrap:anywhere}
}
/* Opening Hours card, "Assign a service" row: the dropdown is sized by its
   longest service name, which pushed Assign off the card at 375px. Let the
   dropdown shrink and keep the button whole (B6). */
.hours-assign select{min-width:0;max-width:100%;flex:0 1 auto}
.hours-assign .btn{flex-shrink:0}
/* Edit Health Record Type, Duration Options "Add" row: Label, Months, Default
   and Add are wider than a phone, so the row wraps onto a second line instead
   of pushing the page sideways (B7). Fits on one line at desktop widths. */
.vacc-option-add{flex-wrap:wrap}
/* Summary rail header (Nix 2026-10-27): keep clear space between "Summary" and the
   status pill, and centre the pill text when the narrow tablet rail wraps it. */
.bk-rail-head{gap:.75rem}
.bk-rail-head .bk-pill{justify-content:center;text-align:center}
/* Floor board card (Nix 2026-10-28): the dog name is underlined so it reads as a link to the booking, the same style as the dashboard In House names. */
.floor-pet a{text-decoration:underline;text-underline-offset:2px}
/* ==== end mobile/tablet UI review fixes ==== */

/* Agreement template view (T12, Nix 2026-10-27): the two rules no existing
   class covers. Everything else on that page reuses existing components. */
.svc-rules-head-orange{color:var(--orange-800)}   /* "Client view" label, #9a3412 */
.svc-card-sub-center{text-align:center}           /* caption under "Edit contents" */
/* Staff sidebar card (Nix 2026-10-28): visible 2px dashed edge, 320px wide,
   and 14px text so its values match the agreement text beside it. Stacks full
   width below 1024px, the same point as .emp-profile-side. */
.agreement-staff-card{border:2px dashed var(--gray-300);flex-basis:320px;max-width:320px;
  font-size:.875rem;line-height:1.25rem}
@media (max-width:1023px){
  .agreement-staff-card{flex-basis:100%;max-width:none}
}
/* Floor board cards (Nix 2026-10-28): stacked info, overdue shown in red on the
   date line (plus a red left edge on the card), check-in block as plain red text. */
.floor-service{font-size:12px;color:var(--gray-500)}
.floor-dates-overdue{color:var(--red-600);font-weight:600}
.floor-card-overdue{border-left:3px solid var(--red-600)}
.floor-flag-text{font-size:11.5px;font-weight:600;color:var(--red-600)}
/* Services list (Nix 2026-10-28): fixed columns so every category table lines up,
   and the client-facing description is clamped to 3 lines. The table keeps a
   minimum width and scrolls sideways inside its card on narrow screens. */
.svc-list-table{table-layout:fixed;width:100%;min-width:740px}
.svc-list-table .svc-col-sched{width:130px}
.svc-list-table .svc-col-price{width:100px}
.svc-list-table .svc-col-unit{width:110px}
.svc-list-table .svc-col-cap{width:120px}
.svc-list-table .svc-col-act{width:80px}
.svc-list-desc{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;
  overflow-wrap:anywhere}
.svc-cap-stack{display:flex;flex-direction:column;align-items:flex-start;gap:.25rem}

/* ---------- COMPONENTS: Bookings header and floor board column tabs (Nix 2026-10-28) ----------
   _view-tabs.php: title + New Booking, then .tab-underline links to the three
   Bookings views (the rule under them is drawn here, as client-requests draws its own).
   Below 1100px the floor board shows two columns at a time: .floor-tab is .tab
   with a light gray fill so a hidden column still looks clickable, and the
   arrows reuse .floor-back. Narrow headers centre (floor bar, calendar head). */
.view-head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin-bottom:.5rem}
.view-tabs{display:flex;gap:1.5rem;border-bottom:1px solid var(--gray-200);margin-bottom:1.25rem}
.view-tabs .tab-underline{margin-bottom:-1px}
.floor-page > .view-header{margin-bottom:-1rem}   /* .floor-page already gaps 1rem between its blocks */
.floor-tabs{display:none;align-items:center;gap:.5rem}
.floor-tabs-row{flex:1;display:flex;gap:.375rem;min-width:0}
.floor-tab{flex:1;min-width:0;padding-left:.25rem;padding-right:.25rem;text-align:center;background:var(--gray-200)}
.floor-tabs .floor-back.is-off{visibility:hidden}
.floor-nav-today,.floor-tab-short{display:none}
.floor-tab-count{margin-left:.25rem}
/* Style 2 (Nix 2026-10-28): below 1100px the date stacks over a [‹ Today ›] row, then the chips.
   Calendar: the same [‹ Today ›] row first, then Day/Week and All/Pending together. */
@media (max-width:1099px){
  .floor-tabs{display:flex}
  .floor-col-off{display:none}
  .floor-bar{justify-content:center}
  .floor-nav{width:100%;display:grid;grid-template-columns:auto auto auto;justify-content:center;gap:.5rem .75rem}
  .floor-nav .floor-date{grid-column:1/-1;grid-row:1}
  .floor-nav-btn:first-child{grid-column:1;grid-row:2}
  .floor-nav-today{display:inline-flex;grid-column:2;grid-row:2}
  .floor-nav-btn:nth-of-type(2){grid-column:3;grid-row:2}
  .floor-date-label .floor-today,.floor-date-label .floor-today-link{display:none}
  .floor-date{text-align:center}
  .floor-date-label{justify-content:center}
  .floor-summary{white-space:normal}
  .floor-chips{width:100%;justify-content:center}
  .sched-head{justify-content:center;text-align:center}
  .sched-controls{justify-content:center}
  .sched-nav{order:-1;width:100%;justify-content:center}
}
/* Phone: four short tabs, the count under the name, one column at a time, no arrows. */
@media (max-width:639px){
  .floor-tab-long{display:none}
  .floor-tab-short{display:inline}
  .floor-tab{display:flex;flex-direction:column;align-items:center;font-size:.8125rem}
  .floor-tab-count{margin-left:0}
  .floor-tabs > .floor-back{display:none}
}

/* Cards on file (views/partials/cards-on-file.php): the Add-card form hosts the
   Passage widget. Same fixes as the inline block in partials/payment-methods.php:
   our own Save button drives tokenization, so Passage's submit stays hidden. */
.cof-add #passage-submit{display:none !important}
.cof-add #valor-fields img{display:inline-block}
.cof-add #valor-fields .d-flex{display:flex}
.cof-add #valor-checkout-form{width:100% !important}
.cof-add #valor-checkout-form,.cof-add #valor-checkout-form label{font-family:inherit !important}
