/* ============================================================
   Zeper Go HRM — Filament theme skin
   Maps the "Zeper Go" design system onto Filament's shell.
   Light + Dark palettes mirror the source HTML mockup.
   ============================================================ */
   @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@500;600;700&display=swap');

   :root {
       --zp-bg: #f1f4fd;
       --zp-panel: #ffffff;
       --zp-panel2: #f7f9ff;
       --zp-border: #e2e8f6;
       --zp-text: #0e0e44;
       --zp-sub: #61688c;
       --zp-accent: #381baf;
       --zp-accent2: #005bc9;
       --zp-cyan: #00ddd7;
       --zp-mint: #48f2ff;
       --zp-hover: #eef1fc;
       --zp-warn: #f7980c;
       --zp-danger: #ef4034;
       --zp-info: #2984fb;
       --zp-ok: #1ac689;
       --zp-grid: #e6eaf7;
       --zp-shadow: 0 1px 3px rgba(14, 14, 68, .06), 0 8px 24px rgba(14, 14, 68, .05);
       --zp-grad: linear-gradient(120deg, #4913b7, #2d99ea 55%, #6be2f2);
       --zp-grad2: linear-gradient(120deg, #381baf, #005bc9);
   }
   
   .dark {
       --zp-bg: #0e0e44;
       --zp-panel: #121556;
       --zp-panel2: #10134e;
       --zp-border: #1c1d68;
       --zp-text: #eaecfb;
       --zp-sub: #9aa1c8;
       --zp-accent: #6645dd;
       --zp-accent2: #2d99ea;
       --zp-cyan: #00ddd7;
       --zp-mint: #48f2ff;
       --zp-hover: #191c63;
       --zp-warn: #f7980c;
       --zp-danger: #ef4034;
       --zp-info: #2984fb;
       --zp-ok: #1ac689;
       --zp-grid: #1c1d68;
       --zp-shadow: 0 1px 3px rgba(0, 0, 0, .45), 0 10px 30px rgba(0, 0, 0, .4);
       --zp-grad2: linear-gradient(120deg, #4913b7, #2d99ea);
   }
   
   /* ---------- Typography ---------- */
   .fi-body,
   .fi-sidebar,
   .fi-main {
       font-family: 'DM Sans', 'Inter', system-ui, sans-serif !important;
       -webkit-font-smoothing: antialiased;
   }
   
   .fi-header-heading,
   .fi-section-header-heading,
   .fi-modal-heading,
   .fi-wi-stats-overview-stat-value,
   h1, h2, h3 {
       font-family: 'Space Grotesk', 'DM Sans', sans-serif !important;
       letter-spacing: -.01em;
   }
   
   /* ---------- App background ---------- */
   .fi-body {
       background-color: var(--zp-bg) !important;
       color: var(--zp-text);
   }
   
   .fi-main-ctn,
   .fi-main {
       background-color: var(--zp-bg) !important;
   }
   
   /* ---------- Main content area — full width, comfortable padding ----------
      Filament default: max-w-7xl (1280px) mx-auto px-4 md:px-6 lg:px-8.
      With maxContentWidth(Full) set in the panel, max-w-full removes the
      width cap; we keep a modest horizontal padding matching the mockup. */
   .fi-main {
       max-width: 100% !important;
       padding-left: 26px !important;
       padding-right: 26px !important;
       padding-top: 0 !important;
   }
   
   /* ---------- Sidebar ---------- */
   .fi-sidebar,
   .fi-sidebar-header,
   .fi-sidebar-nav {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
   }
   
   /* vertical divider separating the sidebar (logo + nav) from the main area,
      matching the mockup's `aside { border-right }` — appears right after the logo */
   .fi-sidebar {
       border-right: 1px solid var(--zp-border) !important;
   }
   
   /* Make the brand area flow as the top of the sidebar — no separate header bar.
      Filament's default is a rigid h-16 strip with ring + shadow + bg that reads
      like a top navbar; the mockup has the brand seamlessly atop the sidebar. */
   .fi-sidebar-header {
       background-color: var(--zp-panel) !important;
       box-shadow: none !important;
       border: none !important;
       --tw-ring-shadow: 0 0 #0000 !important;
       --tw-ring-offset-shadow: 0 0 #0000 !important;
       height: auto !important;
       min-height: 0 !important;
       align-items: flex-start !important;
       padding: 18px 14px 8px !important;
   }
   
   /* nav list spacing — tight gaps, minimal top padding so it hugs the brand */
   .fi-sidebar-nav {
       padding: 6px 10px 12px !important;
       gap: 2px !important;
   }
   
   .fi-sidebar-nav-groups,
   .fi-sidebar-group,
   .fi-sidebar-group-items {
       gap: 2px !important;
   }
   
   /* group header row (collapsible button holds the spacing) — compact */
   .fi-sidebar-group-button {
       padding: 12px 12px 5px !important;
   }
   
   /* group label — 10px / 700 / uppercase / .7px tracking / muted; no own padding */
   .fi-sidebar-group-label {
       font-size: 10px !important;
       font-weight: 700 !important;
       letter-spacing: .7px !important;
       line-height: 1.2 !important;
       text-transform: uppercase !important;
       color: var(--zp-sub) !important;
       opacity: .7 !important;
       padding: 0 !important;
   }
   
   /* collapse chevron — shrink so the row isn't tall */
   .fi-sidebar-group-collapse-button { width: 16px !important; height: 16px !important; }
   
   /* nav item — 13.5px / 600 / muted, 9px·12px padding, 11px gap, 10px radius */
   .fi-sidebar-item-button {
       border-radius: 10px !important;
       padding: 9px 12px !important;
       gap: 11px !important;
       justify-content: flex-start !important;
       font-family: 'DM Sans', sans-serif !important;
       font-size: 13.5px !important;
       font-weight: 600 !important;
       color: var(--zp-sub) !important;
       background: transparent !important;
       position: relative !important;
       transition: background .2s, color .2s, transform .15s !important;
   }
   
   .fi-sidebar-item-label {
       font-size: 13.5px !important;
       font-weight: 600 !important;
       color: inherit !important;
   }
   
   .fi-sidebar-item-icon {
       width: 17px !important;
       height: 17px !important;
       color: var(--zp-sub) !important;
       transition: transform .25s, color .2s !important;
   }
   
   /* hover */
   .fi-sidebar-item-button:hover {
       background: var(--zp-hover) !important;
       color: var(--zp-text) !important;
   }
   
   .fi-sidebar-item-button:hover .fi-sidebar-item-icon {
       color: var(--zp-text) !important;
       transform: scale(1.12);
   }
   
   /* active — accent-tinted bg, accent text/icon, 3px left bar */
   .fi-sidebar-item.fi-active > .fi-sidebar-item-button,
   .fi-sidebar-item-button.fi-active {
       background: color-mix(in srgb, var(--zp-accent) 14%, transparent) !important;
       color: var(--zp-accent) !important;
   }
   
   .fi-sidebar-item.fi-active .fi-sidebar-item-icon,
   .fi-sidebar-item-button.fi-active .fi-sidebar-item-icon {
       color: var(--zp-accent) !important;
   }
   
   .fi-sidebar-item.fi-active > .fi-sidebar-item-button::before,
   .fi-sidebar-item-button.fi-active::before {
       content: "";
       position: absolute;
       left: 0;
       top: 18%;
       height: 64%;
       width: 3px;
       border-radius: 3px;
       background: var(--zp-accent);
   }
   
   /* nav count badges keep the brand accent tone */
   .fi-sidebar-item-badge { font-weight: 700 !important; }
   
   /* ---------- Topbar ---------- */
   .fi-topbar > nav {
       background-color: var(--zp-panel) !important;
       border-bottom: 1px solid var(--zp-border) !important;
   }
   
   /* custom "Ask Zoono AI" search bar (TOPBAR_START render hook) */
   .zp-topbar-search {
       display: flex;
       align-items: center;
       gap: 10px;
       flex: 1;
       max-width: 460px;
       padding: 9px 14px;
       margin-right: 14px;
       background: var(--zp-bg);
       border: 1px solid var(--zp-border);
       border-radius: 10px;
       color: var(--zp-sub);
       cursor: text;
       text-align: left;
       transition: border-color .2s, box-shadow .2s;
   }
   .zp-topbar-search:hover { border-color: var(--zp-accent); }
   .zp-topbar-search:focus-visible {
       outline: none;
       border-color: var(--zp-accent);
       box-shadow: 0 0 0 3px color-mix(in srgb, var(--zp-accent) 18%, transparent);
   }
   .zp-topbar-search svg { flex-shrink: 0; color: var(--zp-sub); }
   .zp-topbar-search-text {
       font-size: 13.5px;
       font-family: 'DM Sans', sans-serif;
       white-space: nowrap;
       overflow: hidden;
       text-overflow: ellipsis;
   }
   @media (max-width: 1024px) {
       .zp-topbar-search { max-width: none; margin-right: 8px; }
       .zp-topbar-search-text { font-size: 12.5px; }
   }
   
   /* notification bell trigger — keep it tidy in the topbar */
   .fi-topbar .fi-icon-btn { color: var(--zp-sub); }
   
   /* "View as role" switcher (HR/Admin only) — mockup .role-switch */
   .zp-roles {
       display: flex;
       background: var(--zp-bg);
       border: 1px solid var(--zp-border);
       border-radius: 10px;
       padding: 3px;
       gap: 2px;
   }
   .zp-role {
       border: none;
       background: transparent;
       color: var(--zp-sub);
       font-size: 12px;
       font-weight: 600;
       padding: 6px 11px;
       border-radius: 8px;
       cursor: pointer;
       font-family: 'DM Sans', sans-serif;
       white-space: nowrap;
       transition: background .18s, color .18s;
   }
   .zp-role:hover { color: var(--zp-text); }
   .zp-role-active {
       background: var(--zp-grad2);
       color: #fff !important;
   }
   @media (max-width: 1100px) {
       .zp-roles { display: none; }
   }
   
   /* role line inside the user-menu dropdown */
   .zp-usermenu-role {
       display: flex;
       align-items: center;
       gap: 8px;
       padding: 8px 14px 4px;
   }
   .zp-usermenu-role-label {
       font-size: 10px;
       font-weight: 700;
       letter-spacing: .5px;
       text-transform: uppercase;
       color: var(--zp-sub);
   }
   .zp-usermenu-role-badge {
       font-size: 11.5px;
       font-weight: 700;
       color: #fff;
       background: var(--zp-grad2);
       padding: 2px 10px;
       border-radius: 20px;
   }
   .zp-usermenu-role-preview {
       font-size: 10px;
       font-weight: 600;
       color: var(--zp-warn);
       border: 1px solid color-mix(in srgb, var(--zp-warn) 40%, transparent);
       border-radius: 20px;
       padding: 1px 7px;
   }
   
   /* ---------- Brand block (custom logo view) ---------- */
   .fi-logo { width: 100%; }
   .zp-brand {
       display: flex;
       align-items: center;
       gap: 11px;
   }
   .zp-brand-mark {
       flex-shrink: 0;
       display: flex;
       align-items: center;
       justify-content: center;
   }
   .zp-brand-mark svg { width: 24px; height: 19px; }
   .zp-brand-text h1 {
       font-family: 'Space Grotesk', sans-serif;
       font-size: 16px;
       font-weight: 700;
       line-height: 1.1;
       color: var(--zp-text);
   }
   .zp-brand-text p {
       font-size: 11px;
       color: var(--zp-sub);
       margin-top: 2px;
   }
   
   /* ---------- Cards / sections ---------- */
   .fi-section,
   .fi-wi-stats-overview-stat,
   .fi-ta-ctn,
   .fi-fo-component-ctn .fi-section {
       border-radius: 16px !important;
       box-shadow: var(--zp-shadow);
   }
   
   /* ---------- Buttons keep brand radius ---------- */
   .fi-btn {
       border-radius: 10px !important;
   }
   
   /* ---------- Inputs ---------- */
   .fi-input,
   .fi-select-input,
   .fi-fo-field-wrp .fi-input {
       border-radius: 10px !important;
   }
   
   /* ---------- The phantom focus ring on the app's own fields ----------
      Clicking a hand-rolled search box drew a second rectangle inside it
      instead of lighting up the box itself.
   
      @tailwindcss/forms ships inside Filament's compiled stylesheet, and it
      carries a rule for every bare field:
   
          [type=text]:focus, ..., select:focus, textarea:focus {
              --tw-ring-color: #2563eb;
              --tw-ring-shadow: ... 0 0 0 1px var(--tw-ring-color);
              box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
          }
   
      That paints a 1px blue ring on the input element. Where the app styles
      the *wrapper* as the visible field - a pill with the magnifier in it,
      the input inside stripped to `border: none; background: transparent` -
      the ring is drawn around the stripped input, inset from the pill it
      sits in. So focus produced thin blue lines a few pixels inside the pill
      rather than the pill's own border turning blue. `outline: none` on the
      input never touched it: it is a box-shadow, not an outline.
   
      Filament's own inputs already carry `focus:ring-0` for exactly this
      reason. This gives the app's fields the same thing, once, instead of
      every screen having to remember to.
   
      Only the ring variables are neutralised, not `box-shadow` itself, so a
      field that deliberately draws its own focus shadow (the attendance
      correction textarea, for one) keeps it.
   
      Excluded: checkboxes, radios and file inputs, whose ring is their only
      focus indicator, and anything Filament renders (`fi-*`), which is not
      ours to restyle. */
   input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([class*="fi-"]):focus,
   select:not([class*="fi-"]):focus,
   textarea:not([class*="fi-"]):focus {
       --tw-ring-shadow: 0 0 #0000;
       --tw-ring-offset-shadow: 0 0 #0000;
   }
   
   /* ---------- The month / year jump picker ----------------------------
      Lifted out of the Calendar page so Revenue & Profitability can use the
      same control rather than a second one that looks nearly like it.
   
      The shape is the Calendar's and is deliberately unchanged: a run of
      years, then the months inside the year chosen, the period on screen
      filled and today's outlined - two different facts, and a period can be
      both. What is added here is only what a second consumer needs, and the
      Calendar is unaffected by any of it.
   
      Named `zcal-*` still: renaming it would have meant touching every line
      of the Calendar's markup to no visible end. */
   .zcal-jump { position: relative; flex: 0 1 auto; min-width: 0; }
   
   .zcal-jump-panel {
       position: absolute;
       z-index: 30;
       top: calc(100% + 6px);
       left: 0;
       width: 244px;
       padding: 10px;
       border-radius: 14px;
       background: var(--zp-panel, #fff);
       border: 1px solid var(--zp-border, #e2e8f6);
       box-shadow: 0 12px 32px rgba(56, 27, 175, .16);
   }
   .zcal-jump-head {
       display: flex; align-items: center; justify-content: space-between;
       gap: 6px; margin-bottom: 8px;
   }
   .zcal-jump-step {
       display: flex; align-items: center; justify-content: center;
       width: 26px; height: 26px; flex: none;
       border: 0; border-radius: 8px; cursor: pointer;
       background: var(--zp-hover, #eef2fb); color: var(--zp-sub, #61688c);
   }
   .zcal-jump-step:hover { color: var(--zp-text, #0e0e44); }
   .zcal-jump-level {
       flex: 1; padding: 4px 8px; border: 0; border-radius: 8px; cursor: pointer;
       background: none; font: inherit; font-size: 13px; font-weight: 700;
       color: var(--zp-text, #0e0e44);
   }
   .zcal-jump-level:hover { background: var(--zp-hover, #eef2fb); }
   
   .zcal-jump-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; }
   .zcal-jump-cell {
       padding: 8px 0; border: 0; border-radius: 9px; cursor: pointer;
       background: none; font: inherit; font-size: 12.5px; font-weight: 600;
       color: var(--zp-text, #0e0e44);
   }
   .zcal-jump-cell:hover { background: var(--zp-hover, #eef2fb); }
   .zcal-jump-cell:focus-visible { outline: 2px solid var(--zp-accent, #381baf); outline-offset: 1px; }
   /* The month on screen, filled. Today's month, outlined — two different
      facts, and a month can be both. */
   .zcal-jump-cell--on { background: var(--zp-accent, #381baf); color: #fff; }
   .zcal-jump-cell--on:hover { background: var(--zp-accent, #381baf); filter: brightness(1.08); }
   .zcal-jump-cell--now { box-shadow: inset 0 0 0 1.5px var(--zp-accent, #381baf); }
   
   .zcal-jump-today {
       width: 100%; margin-top: 8px; padding: 7px 0;
       border: 1px solid var(--zp-border, #e2e8f6); border-radius: 9px; cursor: pointer;
       background: none; font: inherit; font-size: 12px; font-weight: 600;
       color: var(--zp-accent, #381baf);
   }
   .zcal-jump-today:hover { background: var(--zp-hover, #eef2fb); }
   
   /* ---- What a second consumer needs ------------------------------------
   
      Revenue's copy sits at the right-hand end of a wide toolbar rather than
      at the left of a page, and it reports on months that have been and gone
      rather than on a diary. Hence: an alignment it can ask for, somewhere to
      go when the window is too short, and a period it is allowed to refuse.
   
      All three are opt-in modifiers. The Calendar asks for none of them and
      is drawn exactly as it was. */
   .zcal-jump-panel--right { left: auto; right: 0; }
   .zcal-jump-panel--up { top: auto; bottom: calc(100% + 6px); }
   .zcal-jump-panel { max-width: calc(100vw - 28px); }
   
   /* A period there is nothing to report on yet. Greyed rather than absent: a
      month missing from the grid reads as a bug, one visibly not here yet
      reads as what it is. */
   .zcal-jump-cell:disabled { opacity: .34; cursor: not-allowed; color: var(--zp-sub, #61688c); }
   .zcal-jump-cell:disabled:hover { background: none; }
   
   /* A year with something in it, in a grid of twelve that mostly has not.
      A dot rather than a colour, so it does not compete with "on" or "now". */
   .zcal-jump-cell--has { position: relative; }
   .zcal-jump-cell--has::after {
       content: ''; position: absolute; left: 50%; bottom: 3px;
       width: 3px; height: 3px; border-radius: 50%;
       transform: translateX(-50%);
       background: var(--zp-accent, #381baf);
   }
   .zcal-jump-cell--on.zcal-jump-cell--has::after { background: #fff; }
   
   /* ---------- Scrollbar ---------- */
   ::-webkit-scrollbar {
       width: 9px;
       height: 9px;
   }
   
   ::-webkit-scrollbar-thumb {
       background: var(--zp-border);
       border-radius: 6px;
   }
   
   ::-webkit-scrollbar-thumb:hover {
       background: var(--zp-sub);
   }
   
   /* ---------- Resource list filter tabs (mirror the mockup's `.filters` pills) ----------
      Filament renders list-page tabs as a single centered white card (`mx-auto rounded-xl
      bg-white p-2 shadow ring`), which leaves a wide empty gutter. The Zeper mockup uses a
      left-aligned, compact row of bordered pills (`.filters > .btn`). Reshape to match. */
   .fi-tabs {
       /* kill `mx-auto` centering → hug the left edge like `.filters` */
       margin-left: 0 !important;
       margin-right: auto !important;
       /* drop the wrapping card: transparent, no padding/shadow/ring */
       background: transparent !important;
       padding: 0 !important;
       box-shadow: none !important;
       --tw-ring-shadow: 0 0 #0000 !important;
       --tw-ring-offset-shadow: 0 0 #0000 !important;
       gap: 10px !important;
       flex-wrap: wrap;
       /* compact: snug under the header, tight above the table (mockup uses margin-top:20px) */
       margin-top: 2px !important;
       margin-bottom: 14px !important;
   }
   
   /* each tab = a `.btn` pill: bordered, white, muted label */
   .fi-tabs-item {
       padding: 8px 15px !important;
       border-radius: 10px !important;
       border: 1px solid var(--zp-border) !important;
       background: var(--zp-panel) !important;
       transition: transform .15s, background .2s, color .2s, border-color .2s !important;
   }
   
   .fi-tabs-item:hover {
       transform: translateY(-1px);
       background: var(--zp-hover) !important;
   }
   
   .fi-tabs-item .fi-tabs-item-label {
       color: var(--zp-sub) !important;
       font-size: 13px !important;
       font-weight: 600 !important;
   }
   
   .fi-tabs-item:hover .fi-tabs-item-label {
       color: var(--zp-text) !important;
   }
   
   /* active pill = filled accent + white label (mockup `.btn.active`) */
   .fi-tabs-item.fi-active,
   .fi-tabs-item.fi-tabs-item-active {
       background: var(--zp-accent) !important;
       border-color: var(--zp-accent) !important;
   }
   
   .fi-tabs-item.fi-active .fi-tabs-item-label,
   .fi-tabs-item.fi-tabs-item-active .fi-tabs-item-label {
       color: #fff !important;
   }
   
   /* ---------- Employees list: search + pills in one row (mockup `.filters`) ----------
      The custom list view (filament.resources.employees.list) lays out a free-growing
      search box beside the department pills, exactly like the mockup's `.filters` flex row. */
   .zp-filters {
       display: flex;
       align-items: center;
       gap: 10px;
       flex-wrap: wrap;
   }
   
   /* search grows to fill the row; pills hug the right (mockup `.filters .search{max-width:none}`) */
   .zp-search {
       position: relative;
       flex: 1 1 280px;
       min-width: 220px;
       display: block;
   }
   
   .zp-search svg {
       position: absolute;
       left: 12px;
       top: 50%;
       transform: translateY(-50%);
       width: 16px;
       height: 16px;
       color: var(--zp-sub);
       pointer-events: none;
   }
   
   .zp-search input {
       width: 100%;
       padding: 9px 12px 9px 38px;
       background: var(--zp-bg) !important;
       border: 1px solid var(--zp-border) !important;
       border-radius: 10px !important;
       color: var(--zp-text) !important;
       font-size: 13.5px;
       font-family: inherit;
       outline: none;
       transition: border-color .2s, box-shadow .2s;
       box-shadow: none !important;
   }
   
   .zp-search input::placeholder {
       color: var(--zp-sub);
   }
   
   .zp-search input:focus {
       border-color: var(--zp-accent) !important;
       box-shadow: 0 0 0 3px color-mix(in srgb, var(--zp-accent) 18%, transparent) !important;
   }
   
   /* inside the filters row the pills are no longer the full-width block: drop the
      block margins set above so they sit snug against the search box */
   .zp-filters .fi-tabs {
       margin: 0 !important;
   }
   
   /* the search + department filters now live in the `.zp-filters` row above the table,
      so the table's own search box is a redundant duplicate and goes.
    
      The toolbar itself must NOT go with it. Filament renders the bulk-actions button
      inside this toolbar, behind x-show="selectedRecords.length" — so hiding the whole
      strip meant ticking rows produced checkboxes, a selection count, and no way
      whatsoever to act on them. The toolbar now collapses to nothing while empty and
      appears only when there is a selection to act on. */
   .zp-emp-table .fi-ta-header-toolbar {
       padding: 0 !important;
       border: 0 !important;
   }
   
   .zp-emp-table .fi-ta-header-toolbar .fi-ta-search-field,
   .zp-emp-table .fi-ta-header-toolbar .fi-ta-col-toggle {
       display: none !important;
   }
   
   /* Only the bulk-actions group is left, and it brings its own spacing when shown. */
   .zp-emp-table .fi-ta-header-toolbar .fi-ta-actions {
       padding: 10px 16px;
   }
   
   /* ---------- Goals & Performance: Goal Workflow card (mockup `.card .wf-steps`) ----------
      The workflow stepper is rendered by GoalWorkflowOverview as a header widget. Classes are
      `zp-`prefixed copies of the mockup's `.card`, `.card-title`, `.wf-*` so they survive
      Tailwind's purge and don't collide with Filament's own utilities. */
   .zp-card {
       background: var(--zp-panel);
       border: 1px solid var(--zp-border);
       border-radius: 16px;
       padding: 18px;
       box-shadow: var(--zp-shadow);
   }
   
   .zp-card-title {
       font-size: 14.5px;
       font-weight: 700;
       color: var(--zp-text);
       margin-bottom: 14px;
   }
   
   .zp-ct-sub {
       display: block;
       font-size: 11.5px;
       font-weight: 500;
       color: var(--zp-sub);
       margin-top: 3px;
   }
   
   /* Four steps, one line, whatever the card is.
   
      They used to be four fixed 96px columns on a wrapping row, and the four of
      them plus their arrows come to 499px in the 478px this card gives them — so
      Year-end Review dropped onto a line of its own and the arrow that belonged
      in front of it stayed behind at the end of the first line, pointing at
      nothing. A workflow strip that wraps stops reading as a sequence, which is
      the only thing it is for.
   
      So the columns share the row instead of claiming a width from it: each step
      takes a quarter of whatever is there, and a label too long for its quarter
      wraps under its own dot rather than pushing the step after it onto the next
      line. The arrows keep their size and sit between them.
   
      Aligned to the top rather than the middle, so that a step whose label takes
      two lines does not sit its dot lower than the three beside it. The arrows
      are nudged down by hand to the dots' centre line for the same reason. */
   .zp-wf-steps {
       display: flex;
       align-items: flex-start;
       gap: 6px;
       flex-wrap: nowrap;
       padding: 6px 0;
   }
   
   .zp-wf-step {
       display: flex;
       flex-direction: column;
       align-items: center;
       gap: 6px;
       flex: 1 1 0;
       min-width: 0;
       /* For the count, which is pinned to the dot rather than set after the
          label — see the note on .zp-wf-count. */
       position: relative;
   }
   
   .zp-wf-dot {
       width: 34px;
       height: 34px;
       border-radius: 50%;
       background: var(--zp-grad2);
       color: #fff;
       display: flex;
       align-items: center;
       justify-content: center;
       font-weight: 700;
       font-family: 'Space Grotesk', 'DM Sans', sans-serif;
   }
   
   .zp-wf-lbl {
       font-size: 11.5px;
       color: var(--zp-sub);
       font-weight: 600;
       text-align: center;
   }
   
   .zp-wf-arrow {
       color: var(--zp-accent);
       font-size: 18px;
       flex: 0 0 auto;
       /* Half the difference between the dot and this line of text, so the arrow
          runs through the middle of the circles rather than the labels. */
       margin-top: 4px;
   }
   
   /* ---------- Goal workflow: done / here now / still to come ----------
      The strip keeps its shape; only the treatment of each step changes, so a
      glance says where the year has got to instead of implying step one. */
   .zp-wf-step--completed .zp-wf-dot {
       background: none;
       background-color: var(--zp-ok, #16a34a);
   }
   
   .zp-wf-step--current .zp-wf-dot {
       box-shadow: 0 0 0 4px color-mix(in srgb, var(--zp-accent) 22%, transparent);
       transform: scale(1.08);
   }
   
   .zp-wf-step--current .zp-wf-lbl {
       color: var(--zp-text);
       font-weight: 700;
   }
   
   .zp-wf-step--upcoming .zp-wf-dot {
       background: none;
       background-color: var(--zp-panel2, #eef1fc);
       color: var(--zp-sub);
       border: 1.5px dashed var(--zp-border);
   }
   
   .zp-wf-step--upcoming .zp-wf-lbl { opacity: .7; }

   /* The strip, on a phone.

      Four steps and three arrows share 343px once the page has its gutters,
      so a step gets about 63px. Nothing overflows — the steps are `flex: 1 1
      0` with a zero floor, so they give ground rather than push — but a 34px
      dot inside 63px leaves a label of two or three wrapped words at a size
      meant for a laptop. Smaller dot, smaller text, thinner arrows: the same
      strip, sized for the room it actually has.

      The strip still fits without scrolling, which is the point. A workflow
      that has to be dragged sideways to be read is a workflow nobody reads. */
   @media (max-width: 640px) {
       .zp-wf-steps { gap: 3px; }
       .zp-wf-step { gap: 4px; }
       .zp-wf-dot { width: 26px; height: 26px; font-size: 12px; }
       .zp-wf-dot svg { width: 12px; height: 12px; }
       .zp-wf-lbl { font-size: 9.5px; line-height: 1.25; }
       .zp-wf-arrow { font-size: 13px; margin-top: 2px; }
   }
   
   /* The count rides on the step's dot, not on the end of its label.
   
      Set after the label it was part of the same line of text, and a label as
      long as "Mid-year Review" fills a quarter of this card on its own — so the
      number was pushed onto a second line and sat under the words on its own,
      reading as a stray digit rather than as that step's count. Which label it
      happens to is a matter of how wide the card is, so no wording fixes it.
   
      On the dot it is out of the text altogether and every label has the whole
      column to itself. Placed from the middle of the step rather than by the
      dot's own corner, because the dot is a flex child that stays centred at any
      column width; half of 34px is 17, so 9 puts the badge over its top-right
      edge the way a count sits on a tab. */
   .zp-wf-count {
       position: absolute;
       top: -2px;
       left: calc(50% + 9px);
       min-width: 16px;
       padding: 0 4px;
       border-radius: 999px;
       background: var(--zp-panel2, #eef1fc);
       border: 1px solid var(--zp-border);
       font-size: 10px;
       font-weight: 700;
       color: var(--zp-sub);
   }
   
   .zp-wf-step--current .zp-wf-count {
       background: var(--zp-accent);
       border-color: transparent;
       color: #fff;
   }
   
   .zp-sr-only {
       position: absolute;
       width: 1px;
       height: 1px;
       padding: 0;
       margin: -1px;
       overflow: hidden;
       clip: rect(0, 0, 0, 0);
       white-space: nowrap;
       border: 0;
   }
   
   .zp-card-meta {
       font-size: 11px;
       color: var(--zp-sub);
       margin-top: 8px;
       display: flex;
       align-items: center;
       gap: 5px;
       opacity: .85;
   }
   
   .zp-card-meta svg {
       width: 12px;
       height: 12px;
       flex-shrink: 0;
   }
   
   /* ---------- Goal workflow: the strip arriving ----------
   
      The card says where the year has got to, and it now says it in the order the
      year goes in: the steps land left to right, sixty milliseconds apart, so the
      eye is walked from what is done to what is next rather than being handed all
      four at once. The arrows land with them, in their turn, which is what makes
      it read as a sequence being laid down instead of a row of dots fading in.
   
      It happens on arrival and then stops. The widget does not poll, so this runs
      once per page load and is never replayed underneath somebody who is reading
      it — an animation that restarts every few seconds is a distraction sitting
      permanently in the corner of the screen.
   
      The one exception is the step the goals are actually at, which keeps a slow
      ring — three seconds a breath, and the ring only, so the dot itself never
      moves. That one is worth a heartbeat: it is the answer to the question the
      card exists to answer, and it is the thing somebody scanning the screen
      should find first. */
   @keyframes zp-wf-appear {
       from {
           opacity: 0;
           transform: translateY(6px);
       }
   
       to {
           opacity: 1;
           transform: none;
       }
   }
   
   /* `backwards`, so each one is held invisible through its own delay rather than
      appearing and then starting. */
   .zp-wf-steps > * {
       animation: zp-wf-appear 380ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
   }
   
   /* Steps and arrows alternate, so this is simply the order along the row. */
   .zp-wf-steps > *:nth-child(1) { animation-delay: 0ms; }
   .zp-wf-steps > *:nth-child(2) { animation-delay: 60ms; }
   .zp-wf-steps > *:nth-child(3) { animation-delay: 120ms; }
   .zp-wf-steps > *:nth-child(4) { animation-delay: 180ms; }
   .zp-wf-steps > *:nth-child(5) { animation-delay: 240ms; }
   .zp-wf-steps > *:nth-child(6) { animation-delay: 300ms; }
   .zp-wf-steps > *:nth-child(7) { animation-delay: 360ms; }
   .zp-wf-steps > *:nth-child(n + 8) { animation-delay: 420ms; }
   
   /* The tick draws itself, once its own step has landed.
   
      26 is the length of the polyline in the widget's markup, near enough: the
      dash is the whole stroke, and the offset walks it on from the start. */
   @keyframes zp-wf-tick {
       from { stroke-dashoffset: 26; }
       to { stroke-dashoffset: 0; }
   }
   
   .zp-wf-step--completed .zp-wf-dot svg polyline {
       stroke-dasharray: 26;
       animation: zp-wf-tick 380ms ease-out 220ms backwards;
   }
   
   /* And the count arrives after the dot it sits on, with a little overshoot, so
      it reads as landing there rather than having always been there. */
   @keyframes zp-wf-count-in {
       from {
           opacity: 0;
           transform: scale(0.4);
       }
   
       to {
           opacity: 1;
           transform: none;
       }
   }
   
   .zp-wf-count {
       animation: zp-wf-count-in 300ms cubic-bezier(0.2, 1.4, 0.4, 1) 420ms backwards;
   }
   
   /* The step the goals are at, breathing. Ring only — the same ring the rule
      above gives it, widened and thinned and put back — so nothing moves and
      nothing reflows; and it starts once the strip has finished arriving. */
   @keyframes zp-wf-breathe {
       0%, 100% {
           box-shadow: 0 0 0 4px color-mix(in srgb, var(--zp-accent) 22%, transparent);
       }
   
       50% {
           box-shadow: 0 0 0 9px color-mix(in srgb, var(--zp-accent) 9%, transparent);
       }
   }
   
   .zp-wf-step--current .zp-wf-dot {
       animation: zp-wf-breathe 3.2s ease-in-out 900ms infinite;
   }
   
   /* Each step answers the pointer, since each one has something to say — the
      title on it names the stage, its state and how many goals are sitting
      there. */
   .zp-wf-dot {
       transition: transform 200ms cubic-bezier(0.2, 0.7, 0.3, 1);
   }
   
   .zp-wf-step:hover .zp-wf-dot {
       transform: scale(1.12);
   }
   
   .zp-wf-step--current:hover .zp-wf-dot {
       transform: scale(1.18);
   }
   
   /* Where movement is turned down at the system level the card is simply drawn
      as it ends up: every step present, the tick whole, the ring steady. */
   @media (prefers-reduced-motion: reduce) {
       .zp-wf-steps > *,
       .zp-wf-count,
       .zp-wf-step--current .zp-wf-dot,
       .zp-wf-step--completed .zp-wf-dot svg polyline {
           animation: none;
       }
   
       .zp-wf-step--completed .zp-wf-dot svg polyline {
           stroke-dasharray: none;
       }
   
       .zp-wf-dot {
           transition: none;
       }
   }
   
   /* On a narrow window the arrows go and the steps have the row to themselves;
      four dots in a line still read as an order without them. */
   @media (max-width: 980px) {
       .zp-wf-arrow {
           display: none;
       }
   }
   
   /* ---------- Acknowledged progress bar (Policies table) ---------- */
   .zp-ack {
       display: flex;
       align-items: center;
       gap: 8px;
       max-width: 240px;
   }
   
   .zp-ack-bar {
       height: 6px;
       background: var(--zp-hover);
       border-radius: 4px;
       overflow: hidden;
       min-width: 54px;
       flex: 1;
   }
   
   .zp-ack-bar span {
       display: block;
       height: 100%;
       border-radius: 4px;
       transition: width .8s cubic-bezier(.4, 0, .2, 1);
   }
   
   .zp-ack-val {
       font-size: 12px;
       color: var(--zp-sub);
       white-space: nowrap;
   }
   
   /* ---------- Dark mode: Filament table overrides ----------
      Filament tables use Tailwind dark: utilities (dark:bg-gray-900, etc.)
      which ignore our CSS variables. Force Zeper palette on all table
      elements when .dark is active. */
   .dark .fi-ta-ctn {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
   }
   
   .dark .fi-ta-header {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
   }
   
   .dark .fi-ta-header-toolbar {
       background-color: var(--zp-panel) !important;
   }
   
   .dark .fi-ta-filters-form {
       background-color: var(--zp-panel2) !important;
       border-color: var(--zp-border) !important;
   }
   
   /* column headers */
   .dark .fi-ta-col-header-cell {
       background-color: var(--zp-panel2) !important;
       border-color: var(--zp-border) !important;
       color: var(--zp-sub) !important;
   }
   
   /* table body rows */
   .dark .fi-ta-row {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
   }
   
   .dark .fi-ta-row:hover td,
   .dark .fi-ta-row:hover > * {
       background-color: var(--zp-hover) !important;
   }
   
   /* cells */
   .dark .fi-ta-cell,
   .dark .fi-ta-text-col {
       color: var(--zp-text) !important;
       border-color: var(--zp-border) !important;
   }
   
   /* table footer / pagination */
   .dark .fi-ta-footer,
   .dark .fi-pagination {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
   }
   
   .dark .fi-pagination-item-btn {
       color: var(--zp-sub) !important;
       background-color: transparent !important;
   }
   
   .dark .fi-pagination-item-btn:hover {
       background-color: var(--zp-hover) !important;
       color: var(--zp-text) !important;
   }
   
   .dark .fi-pagination-item-btn.fi-active {
       background-color: var(--zp-accent) !important;
       color: #fff !important;
   }
   
   /* checkboxes row */
   .dark .fi-ta-header-row,
   .dark thead tr {
       background-color: var(--zp-panel2) !important;
       border-color: var(--zp-border) !important;
   }
   
   /* empty state */
   .dark .fi-ta-empty-state {
       background-color: var(--zp-panel) !important;
       color: var(--zp-sub) !important;
   }
   
   /* search / filter inputs inside the table toolbar */
   .dark .fi-ta-search-field input,
   .dark .fi-input {
       background-color: var(--zp-panel2) !important;
       border-color: var(--zp-border) !important;
       color: var(--zp-text) !important;
   }
   
   .dark .fi-ta-search-field input::placeholder {
       color: var(--zp-sub) !important;
   }
   
   /* action buttons inside table rows */
   .dark .fi-ta-row-actions .fi-btn {
       background-color: transparent !important;
       border-color: var(--zp-border) !important;
       color: var(--zp-sub) !important;
   }
   
   .dark .fi-ta-row-actions .fi-btn:hover {
       background-color: var(--zp-hover) !important;
       color: var(--zp-accent) !important;
   }
   
   /* ---------- Dark mode: stats overview cards ----------
      Filament uses dark:bg-gray-900 (#111827) on stat cards, but the Zeper
      dark palette uses --zp-panel (#121556 navy). Override so all cards
      share the same panel colour across the entire app. */
   .dark .fi-wi-stats-overview-stat {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
       --tw-ring-color: var(--zp-border) !important;
   }
   
   .dark .fi-wi-stats-overview-stat-label {
       color: var(--zp-sub) !important;
   }
   
   .dark .fi-wi-stats-overview-stat-value {
       color: var(--zp-text) !important;
   }
   
   .dark .fi-wi-stats-overview-stat-description {
       color: var(--zp-sub) !important;
   }
   
   /* ---------- Dark mode: general form/section backgrounds ---------- */
   .dark .fi-section,
   .dark .fi-fo-component-ctn {
       background-color: var(--zp-panel) !important;
       border-color: var(--zp-border) !important;
   }
   
   .dark .fi-fo-field-wrp {
       background-color: transparent !important;
   }
   
   .dark .fi-input,
   .dark .fi-select-input,
   .dark textarea {
       background-color: var(--zp-panel2) !important;
       border-color: var(--zp-border) !important;
       color: var(--zp-text) !important;
   }
   
   /* ── Expense · Top Categories breakdown bars ───────────────────────────── */
   .zp-cat-row {
       margin-bottom: 12px;
   }
   
   .zp-cat-row:last-child {
       margin-bottom: 0;
   }
   
   .zp-cat-head {
       display: flex;
       justify-content: space-between;
       font-size: 13px;
       margin-bottom: 5px;
   }
   
   .zp-cat-name {
       color: var(--zp-text);
       font-weight: 600;
   }
   
   .zp-cat-pct {
       color: var(--zp-sub);
   }
   
   /* Named for what it is, after it turned out not to be the only `.zp-bar`.
      Three screens use that class: this progress bar, and the heading strips
      on Statutory Compliance and Escalations & Awards. This rule is global, so
      it was setting `height: 6px; overflow: hidden` on both of those toolbars
      — Compliance only escaped it by carrying a `min-height: 68px` of its own,
      and Escalations, which carries none, has been clipping its heading at
      every width since the day both were written. */
   .zp-cat-bar {
       height: 6px;
       background: var(--zp-hover);
       border-radius: 4px;
       overflow: hidden;
   }
   
   .zp-cat-bar span {
       display: block;
       height: 100%;
       border-radius: 4px;
       transition: width .8s cubic-bezier(.4, 0, .2, 1);
   }
   
   /* Livewire needs a root element even when there is no button to draw, so the
      wrapper is transparent to layout rather than an empty box adding a gap. */
   .zp-clock-slot { display: contents; }
   
   /* ---- Clock in / out, in the topbar beside the notification bell ----------
      Sized and spaced to sit with .zp-roles rather than beside it as a
      different-looking thing: same pill radius, same border, same 12px type. */
   .zp-clock {
       display: inline-flex;
       align-items: center;
       gap: 7px;
       background: var(--zp-bg);
       border: 1px solid var(--zp-border);
       border-radius: 10px;
       padding: 7px 13px;
       font-family: 'DM Sans', sans-serif;
       font-size: 12px;
       font-weight: 600;
       color: var(--zp-sub);
       cursor: pointer;
       white-space: nowrap;
       transition: background .18s, color .18s, border-color .18s;
   }
   .zp-clock:hover { color: var(--zp-text); border-color: var(--zp-accent); }
   .zp-clock:disabled { opacity: .55; cursor: progress; }
   .zp-clock:focus-visible { outline: 2px solid var(--zp-accent); outline-offset: 2px; }
   
   /* Clocked in: the button now offers the opposite action, so it reads as
      "running" rather than as a call to action. */
   .zp-clock-in {
       background: var(--zp-grad2);
       border-color: transparent;
       color: #fff;
   }
   .zp-clock-in:hover { color: #fff; border-color: transparent; filter: brightness(1.06); }
   
   .zp-clock-dot {
       width: 7px;
       height: 7px;
       border-radius: 50%;
       border: 1.5px solid currentColor;
       flex-shrink: 0;
   }
   .zp-clock-dot-live {
       background: #fff;
       border-color: #fff;
       animation: zp-clock-pulse 2s ease-in-out infinite;
   }
   @keyframes zp-clock-pulse {
       0%, 100% { opacity: 1; }
       50%      { opacity: .35; }
   }
   
   .zp-clock-since {
       font-variant-numeric: tabular-nums;
       opacity: .8;
       padding-left: 6px;
       border-left: 1px solid rgba(255, 255, 255, .35);
   }
   
   /* Below this the topbar is tight; the label goes and the dot carries the
      state, matching how .zp-roles bows out at the same width. */
   @media (max-width: 1100px) {
       .zp-clock { padding: 7px 10px; }
       .zp-clock-label, .zp-clock-since { display: none; }
   }
   @media (prefers-reduced-motion: reduce) {
       .zp-clock-dot-live { animation: none; }
   }
   
   /* ---- Settings screen ---------------------------------------------------- */
   .zp-settings { display: flex; flex-direction: column; gap: 14px; }
   
   .zp-setting {
       display: flex;
       align-items: flex-start;
       justify-content: space-between;
       gap: 28px;
       background: var(--zp-panel);
       border: 1px solid var(--zp-border);
       border-radius: 16px;
       padding: 20px 22px;
       box-shadow: var(--zp-shadow);
   }
   .zp-setting-body { min-width: 0; }
   .zp-setting-title {
       font-size: 14.5px;
       font-weight: 700;
       color: var(--zp-text);
       margin-bottom: 6px;
   }
   .zp-setting-desc {
       font-size: 13px;
       line-height: 1.65;
       color: var(--zp-sub);
       max-width: 62ch;
   }
   .zp-setting-note {
       margin-top: 10px;
       font-size: 12.5px;
       color: var(--zp-sub);
       opacity: .85;
   }
   
   /* The switch itself. A real button with role="switch" rather than a styled
      checkbox, so it is reachable by keyboard and announced as on or off. */
   .zp-switch {
       flex-shrink: 0;
       width: 46px;
       height: 26px;
       border-radius: 999px;
       border: 1px solid var(--zp-border);
       background: var(--zp-bg);
       padding: 2px;
       cursor: pointer;
       transition: background .2s, border-color .2s;
   }
   .zp-switch:disabled { opacity: .6; cursor: progress; }
   .zp-switch:focus-visible { outline: 2px solid var(--zp-accent); outline-offset: 2px; }
   .zp-switch-on { background: var(--zp-grad2); border-color: transparent; }
   
   .zp-switch-knob {
       display: block;
       width: 20px;
       height: 20px;
       border-radius: 50%;
       background: #fff;
       box-shadow: 0 1px 3px rgba(0, 0, 0, .28);
       transform: translateX(0);
       transition: transform .2s ease;
   }
   .zp-switch-on .zp-switch-knob { transform: translateX(20px); }
   
   @media (prefers-reduced-motion: reduce) {
       .zp-switch, .zp-switch-knob { transition: none; }
   }
   
   /* ---- A setting in a hand ----

      Stacking the row put the switch after the paragraph. Measured at 375px:
      on the clocking setting the title sat at 110px and its switch at 368 —
      258px of description and note between the name of the thing and the
      control that turns it on, and 346px on the one below it. Nothing
      overflowed and nothing scrolled; the control had simply been moved to
      the end of the reading.

      So the row becomes a small grid instead, and the control goes back level
      with the title where a phone expects it — the description and anything
      after it run the full width underneath. `display: contents` on the body
      lets the title out of its wrapper to sit beside a switch that is the
      wrapper's sibling, so none of this needs the markup changed.

      Written as "the last child" rather than as the switch, because not every
      setting has one: Shifts ends in a link to the screen that manages them. */
   @media (max-width: 640px) {
       /* the control beside the thing it controls */
       .zp-setting:not(.zp-setting-block) {
           display: grid;
           grid-template-columns: minmax(0, 1fr) auto;
           gap: 8px 14px;
           align-items: start;
           padding: 16px;
           border-radius: 13px;
       }
       .zp-setting-body { display: contents; }
       .zp-setting-title {
           grid-column: 1;
           grid-row: 1;
           margin-bottom: 0;
           font-size: 13.5px;
       }
       .zp-setting-body > :not(.zp-setting-title) { grid-column: 1 / -1; }
       .zp-setting:not(.zp-setting-block) > :last-child {
           grid-column: 2;
           grid-row: 1;
           align-self: center;
       }

       .zp-settings { gap: 10px; }
       .zp-setting-desc { font-size: 12.5px; line-height: 1.55; }
       .zp-setting-note { font-size: 11.5px; margin-top: 0; }
   }
   
   /* Break sits beside Clock In/Out as one control group, so the topbar reads as
      one thing rather than two unrelated buttons. */
   .zp-clock-group { display: inline-flex; align-items: center; gap: 6px; }
   
   /* Break is the quieter of the two: it is the more frequent press, but the
      less consequential one, so it does not compete with Clock Out. */
   .zp-clock-break { font-weight: 600; }
   .zp-clock-break:hover { border-color: var(--zp-warn); color: var(--zp-text); }
   
   /* Actively on a break — amber, matching the badge on the lifecycle row. */
   .zp-clock-onbreak {
       background: var(--zp-warn);
       border-color: transparent;
       color: #fff;
   }
   .zp-clock-onbreak:hover { background: var(--zp-warn); color: #fff; border-color: transparent; filter: brightness(1.06); }
   
   @media (max-width: 1100px) {
       .zp-clock-group { gap: 4px; }
   }
   
   /* ──────────────────────────────────────────────────────────────────────
      Phones
      ─────────────────────────────────────────────────────────────────────
      The topbar's search button was flex:1 with no maximum below 1024px, so
      on a 375px screen it took the whole bar and pushed the notification bell
      and the avatar off the right-hand edge. They were not missing; they were
      outside the screen.
   
      Below 640px it becomes the icon alone, which leaves room for everything
      that has to be reachable. */
   @media (max-width: 640px) {
       .zp-topbar-search {
           flex: 0 0 auto;
           width: 36px;
           height: 36px;
           justify-content: center;
           padding: 0;
           margin-right: 6px;
           gap: 0;
       }
       .zp-topbar-search-text { display: none; }
   
       /* The hamburger, which opens the side navigation, is rendered after the
          TOPBAR_START hook this search button lives in — so with the button at
          flex:1 it was pushed off the edge along with the bell and the avatar.
          Freeing the width brings it back; this puts it where a hamburger
          belongs, at the far left, ahead of everything else. */
       .fi-topbar-open-sidebar-btn,
       .fi-topbar-close-sidebar-btn { order: -1; flex: 0 0 auto; }
       .zp-topbar-search { order: 0; }
   
       /* Big enough to hit with a thumb. */
       .fi-topbar-open-sidebar-btn,
       .fi-topbar-close-sidebar-btn { width: 2.5rem; height: 2.5rem; }
   
       /* The drawer it opens should cover the screen it is on, not a desktop
          fraction of it. */
       .fi-sidebar.fi-sidebar-open { width: min(20rem, 86vw); }
   
       /* A shorter bar, because vertical space is what a phone is short of. */
       .fi-topbar > nav,
       .fi-topbar nav.fi-topbar-nav { min-height: 3.25rem; }
       .fi-topbar { padding-left: .5rem; padding-right: .5rem; }
   
       /* The right-hand group stays put and stays visible. */
       .fi-topbar-end { gap: .25rem; flex: 0 0 auto; margin-left: auto; }
       .fi-topbar-end .fi-icon-btn { width: 2.25rem; height: 2.25rem; }
   
       /* Clocking in is a topbar control on every screen; on a phone it is the
          icon and the state, without the wording that will not fit. */
       .zp-clock-group { gap: 3px; }
   
       /* Page padding on a phone was desktop padding, which cost a third of the
          width to whitespace. */
       .fi-main { padding-left: .75rem; padding-right: .75rem; }
       .fi-header { gap: .5rem; }
       .fi-header-heading { font-size: 1.375rem; line-height: 1.75rem; }
   
       /* Nothing anywhere in the panel may push the page sideways. A table or a
          chart that is genuinely wider than the screen scrolls inside its own
          box instead of moving the whole page.

          The clip belongs on .fi-main and nowhere closer in. A box clips at its
          own padding edge, and .fi-page is drawn exactly around the content —
          so clipping there cut every ring a field or a button draws just
          outside itself, which is a Tailwind box-shadow rather than a border
          and so falls outside the box. .fi-main has the page gutter between
          the content and its clip edge, which is the room those rings need. */
       .fi-main { max-width: 100%; overflow-x: clip; }
       .fi-page { max-width: 100%; }
       .fi-ta-ctn, .fi-ta-content { overflow-x: auto; }
   
       /* Filament's action groups and header buttons wrap rather than overflow. */
       .fi-header-actions, .fi-ac { flex-wrap: wrap; }
   
       /* A modal on a phone should use the screen it has. */
       .fi-modal-window { margin: .5rem; max-width: calc(100vw - 1rem); }
   }
   
   /* Landscape on a phone is short rather than narrow, so the bar shrinks
      further and the charts get the height back. */
   @media (max-width: 900px) and (orientation: landscape) {
       .fi-topbar > nav,
       .fi-topbar nav.fi-topbar-nav { min-height: 2.875rem; }
   }
   
   /* Department dropdown in the Employee Lifecycle filter row. Sized and
      bordered like the search box beside it so the row reads as one control
      group rather than two borrowed widgets. */
   .zp-dept-filter select {
       appearance: none;
       background: var(--zp-panel) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2361688c' stroke-width='3' stroke-linecap='round'><polyline points='6 9 12 15 18 9'/></svg>") no-repeat right 12px center;
       border: 1px solid var(--zp-border);
       border-radius: 10px;
       color: var(--zp-text);
       font-family: 'DM Sans', sans-serif;
       font-size: 13px;
       font-weight: 600;
       padding: 9px 34px 9px 13px;
       cursor: pointer;
       flex-shrink: 0;
   }
   .zp-dept-filter select:hover { border-color: var(--zp-accent); }
   .zp-dept-filter select:focus-visible { outline: 2px solid var(--zp-accent); outline-offset: 2px; }
   
   @media (max-width: 780px) {
       .zp-dept-filter select { width: 100%; }
   }
   
   /* ---- Requisitions: the Sign-off cell ------------------------------------
      Both signatures on one row — "Approved by Priya (Management), 2 Sep 2026 ·
      agreed by Diya (Manager), 2 Sep 2026" — ran to two full lines and set the
      height of every row on the page, including the ones with three words to
      say. Clamped to two lines; the whole sentence is on the tooltip, so nothing
      is lost by shortening what is drawn. */
   .fi-table-cell-approval-status p {
       display: -webkit-box;
       -webkit-line-clamp: 2;
       -webkit-box-orient: vertical;
       overflow: hidden;
       /* A ceiling as well as a clamp: -webkit-line-clamp is ignored by a few
          engines, and without this the row would grow again on those. */
       max-height: 2.6em;
       line-height: 1.3;
   }
   
   /* Stops the column claiming half the table when a sentence is long. */
   .fi-table-cell-approval-status .fi-ta-text {
       max-width: 22rem;
   }
   
   /* A select opens upwards when there is no room below it.
   
      Choices.js already works this out: `position: 'auto'` measures where the
      bottom of the list would land and, when that is past the bottom of the
      window, adds `is-flipped` to the wrapper. Filament ships no rule for that
      class, so the decision was made and then ignored — the list stayed pinned
      at `top: 100%` and ran off the screen.
   
      Where this bit was the Filters panel on Recruitment: the funnel sits well
      down the page, so the Status list opened below the fold and the options
      being picked could not be reached. Anything low on any screen had the same
      problem — a filter, a form field near the footer, a picker in a modal.
   
      Only the direction changes. The list keeps its own `max-height` and its own
      scrolling, so nothing new scrolls and the panel around it is untouched. */
   .choices.is-flipped .choices__list--dropdown,
   .choices.is-flipped .choices__list[aria-expanded] {
       top: auto;
       bottom: 100%;
       margin-top: 0;
       /* Mirrors the .5rem gap the un-flipped list leaves below the input. */
       margin-bottom: 0.5rem;
   }
   
   /* ---------------------------------------------------------------------------
      Modals: a dialog, not a second page.
      --------------------------------------------------------------------------- */
   
   /* Filament sizes a modal to its content and scrolls the full-screen overlay
      behind it, so the dialog itself has no height limit. On a long form — Add
      event carries eleven fields — it grew taller than the screen: the heading
      went off the top, the Create and Cancel buttons off the bottom, and there
      was no scrollbar on the dialog to say there was more of it.
   
      Capping the height and scrolling only the middle keeps the title and the
      buttons where they were put. The cap is `min()` of two things because either
      alone is wrong: a share of the viewport alone makes a dialog on a tall
      monitor almost as tall as the screen, and a fixed height alone overflows a
      short laptop.
   
      `.rounded-xl` is what tells an ordinary dialog apart from the two kinds that
      are supposed to fill the screen — slide-overs and full-screen modals both
      carry `h-[100dvh]` and are given square corners, so neither is touched here.
   
      `dvh` is declared after `vh` rather than instead of it: the second wins
      wherever it is understood, and on a phone `vh` counts the address bar, which
      would put the buttons underneath it. */
   .fi-modal-window.rounded-xl {
       max-height: 85vh;
       max-height: min(85dvh, 44rem);
   }
   
   /* The fields scroll; the heading and the buttons do not.
   
      min-height:0 because a flex child will not shrink below its own content
      without it, and nothing that cannot shrink can scroll. */
   .fi-modal-content {
       min-height: 0;
       overflow-y: auto;
       padding-top: 1rem;
       padding-bottom: 1rem;
   }
   
   /* The heading sat a clear 3rem above the first field — 1.5rem of padding under
      the header meeting 1.5rem over the content. */
   .fi-modal-header {
       padding-top: 1.25rem;
   }
   
   /* The buttons keep a little more room beneath them than above, so the row
      reads as the floor of the dialog rather than as another field. */
   .fi-modal-footer {
       padding-bottom: 1.25rem;
   }
   
   /* Centred rather than sitting high.
   
      The window lives in the middle row of a three-row grid, and above `sm` that
      grid is `1fr auto 3fr` — three times as much space below as above, which
      holds the dialog well up the screen. That reads as deliberate when the
      dialog is tall enough to nearly fill the space; now that it is capped it
      just looks misplaced. Below `sm` the ratio is already even. */
   @media (min-width: 640px) {
       [x-ref="modalContainer"] {
           grid-template-rows: 1fr auto 1fr;
       }
   }
   
   /* And the gaps are closed up.
   
      The height is nearly all gap rather than content: 1.5rem between each field
      and another 0.5rem between every label and its own input. Both read well
      down a full-page form with the room for them, but stacked eleven deep in a
      dialog they are most of why it does not fit — so without this, the scrolling
      added above is mostly scrolling past empty space.
   
      Scoped to modals, so full-page forms keep the airier spacing they were
      designed with. Only spacing changes: no field, button or behaviour is
      touched by any of this. */
   .fi-modal-content .fi-fo-component-ctn {
       gap: 0.75rem;
   }
   
   .fi-modal-content .fi-fo-field-wrp > .grid {
       row-gap: 0.25rem;
   }
   
   /* Helper text is a hint, not a paragraph — it does not need the field's
      leading. ("Only HR/Admin can add a company holiday", and so on.) */
   .fi-modal-content .fi-fo-field-wrp-helper-text,
   .fi-modal-content .fi-fo-field-wrp-hint {
       line-height: 1.3;
   }
   
   /* On a phone the screen is the constraint, not the ceiling above.
   
      44rem is taller than most phones, so the fixed half of the cap never binds
      and the dialog would be left at 85% of a screen that is already small. Here
      it takes what there is, less a margin, and the tighter padding buys back
      room that matters more at this size. */
   @media (max-width: 640px) {
       .fi-modal-window.rounded-xl {
           max-height: calc(100vh - 1.5rem);
           max-height: calc(100dvh - 1.5rem);
       }
   
       .fi-modal-header {
           padding-top: 1rem;
       }
   
       .fi-modal-content {
           padding-top: 0.75rem;
           padding-bottom: 0.75rem;
       }
   
       .fi-modal-footer {
           padding-bottom: 1rem;
       }
   }
   
   /* ── Description boxes ─────────────────────────────────────────────────────
      A textarea with a drag handle is one the reader can stretch to any height,
      which inside a modal pushes the buttons off the bottom of the screen. Every
      description and notes box in the app is now a fixed three lines that scrolls
      when the writing runs past them, rather than growing under the cursor. */
   .fi-fo-textarea textarea,
   textarea.fi-input {
       resize: none;
   }
   
   /* ── How far a box that can be dragged may be dragged ──────────────────────
      A drag handle with nothing behind it stretches for as long as the mouse
      moves. Inside a dialog that is not a big text box, it is a dialog whose
      Save button is somewhere below the bottom of the screen, with no way to
      scroll to it.
   
      A ceiling rather than no handle: making these boxes bigger is genuinely
      useful when somebody is writing a paragraph. What is not useful is being
      able to make one taller than the monitor. */
   textarea {
       max-width: 100%;
       max-height: 60vh;
   }
   
   /* Inside a dialog there is a heading and a row of buttons to fit as well, so
      a box may have less of the screen than one on a page of its own. */
   .fi-modal textarea,
   .zp-corr-modal textarea,
   .zeper-event-modal textarea {
       max-height: 40vh;
   }
   
   /* And a floor. A handle that drags both ways can be dragged shut, leaving a
      line too thin to read what you typed into it. */
   .zp-corr-textarea,
   .zp-ev-input {
       min-height: 76px;
   }
   
   /* A notification that leads somewhere looks like it does.
   
      Filament draws the card as a plain div and puts the only anchor underneath
      the body, so there was nothing to say the card was clickable — and until
      the script in filament/notification-click.blade.php it was not. The pointer
      is shown only on cards that actually carry a link, because a hand cursor
      over a notification that goes nowhere is its own small lie. */
   .fi-no-notification:has(.fi-no-notification-actions a[href]) {
       cursor: pointer;
   }
   
   /* A picker inside a dialog is capped by the dialog, not by the window.
   
      Choices.js sizes its list against the viewport, and .fi-modal-window is
      already capped at min(85dvh, 44rem) - so in a dialog the list could be
      taller than the box it opens in, and .fi-modal-content's own `overflow-y:
      auto` is not a clipping boundary it respects. The Filters panel on Policy
      Tracker showed it plainly: the Policy list left the dialog and lay across
      the Department field below it.
   
      The flip rule above decides which way it opens; this decides how far it can
      go. 14rem is about eight options, which is enough to choose from without
      the list becoming the dialog. It keeps its own internal scrolling, so
      nothing is unreachable, and the z-index only has to beat the fields beside
      it rather than the overlay. */
   .fi-modal-content .choices__list--dropdown,
   .fi-modal-content .choices__list[aria-expanded] {
       z-index: 30;
   }
   
   /* Capped on the inner list, which is the one that already scrolls.
   
      The cap was on the outer box, with its own overflow-y — and Filament caps
      the list *inside* it at 15rem with overflow of its own. Fifteen inside
      fourteen means both boxes overflow, so both drew a scrollbar: two bars a few
      pixels apart, one moving the list and one moving the box around it.
   
      Same 14rem, one layer down, so there is one bar and it is the one attached
      to the options. */
   .fi-modal-content .choices__list--dropdown .choices__list,
   .fi-modal-content .choices__list[aria-expanded] .choices__list {
       max-height: 14rem;
   }
   
   /* The dialog keeps its height when a list is opened.
   
      Two rules used to sit here: the window was allowed to grow to 54rem while a
      list was open, and the content was given a 22rem floor so the list had
      somewhere to sit. Both were answers to the same problem — the list being
      clipped by the scrolling content — and both solved it by making the box
      bigger, so a short dialog jumped taller the moment anybody touched a
      dropdown. That is a strange thing for a box to do while it is being read.
   
      Neither is needed now. The script in
      resources/views/filament/dropdowns-inside-dialogs.blade.php measures the room
      the dialog actually has on each side, opens the list towards the better one
      and caps it to what is there — so the list fits inside the height the dialog
      already had, and the dialog does not move. */
   
   /* And a dialog with nothing to scroll lets the list out altogether.
   
      The two rules above cap an in-dialog list to the room the dialog has, which
      is the right answer for a dialog tall enough to have any. A short one has
      none on either side: "Who has Monday off?" in Settings is one field, its
      helper text and two buttons, so there are about 40px below the field and
      about 90px above it. Capped to that, the list opened upward as the larger of
      two bad sides and drew a stub of itself over the field's own label.
   
      A dialog that short is not scrolling, and a box with no scroll position has
      none to lose by not clipping — so it stops. The list then hangs over the
      footer, which is what a dropdown does to whatever is beneath it everywhere
      else in the application, and the boundary goes back to being the window,
      where the room actually is. Nothing about the dialog moves; only the list
      is allowed past its edge.
   
      The class is put on by the script in
      resources/views/filament/dropdowns-inside-dialogs.blade.php, because "has
      nothing to scroll" is a measurement rather than a selector — and it is put
      on only there, so a dialog that *is* scrolling keeps its overflow. Taking
      the overflow off a scrolled element resets it to the top, and a nine-field
      panel jumping to its first field because somebody opened the dropdown on
      its last is worse than a capped list.
   
      `overflow: visible` rather than `overflow-y`: a lone `overflow-y: visible`
      against the inherited `overflow-x` computes back to `auto`, and the box goes
      on clipping. */
   .fi-modal-content.zp-lists-may-leave {
       overflow: visible;
   }
   
   /* Which way a list opens inside a dialog is decided against the dialog.
   
      The flip rule at the top of this file acts on `is-flipped`, and Choices.js
      works that class out by measuring the field against the *window*. Inside a
      dialog that is the wrong boundary in both directions:
   
       - The last field of a tall dialog has hundreds of pixels of window below
         it and none of dialog, so Choices says "room below", opens downward and
         the options are clipped by the bottom of the box. Priority on
         Requisitions - ninth of nine fields - was exactly this.
       - A field near the top of a short dialog has the opposite problem: flipping
         it upward puts the list past the top edge of a scroll container, where it
         cannot be reached at all.
   
      So `is-flipped` is recomputed against the dialog by the script in
      resources/views/filament/dropdowns-inside-dialogs.blade.php, which also caps
      the list to the room actually there. This file only says what the class
      means; nothing here forces a direction any more, because a fixed direction
      is wrong for one of the two cases whichever one is chosen. */
   
   /* ---------------------------------------------------------------------------
      Filters in the header strip.
      --------------------------------------------------------------------------- */
   
   /* A list of two or three filters lives in the header rather than behind the
      funnel — see App\Filament\Tables\FilterPanel for which lists those are and
      why. This is the one thing that needs saying in CSS about it.
   
      .fi-ta-ctn carries `overflow: hidden`, which is what keeps the table's rows
      inside its rounded corners. A filter's option list is absolutely positioned
      and cannot escape a clipping context, so in the header it was cut off at
      whichever edge it reached first: on a short list — Vendors has five rows, an
      empty one has none — the options ran straight into the bottom of the box and
      were gone.
   
      Unclipped only while a list is actually open, so the corners still clip the
      rows the rest of the time. Nothing else in the container scrolls: the
      horizontal scrolling for a wide table is on .fi-ta-content inside it, which
      keeps its own overflow either way. */
   .fi-ta-ctn:has(.fi-ta-filters-above-content-ctn .choices.is-open) {
       overflow: visible;
   }
   
   /* And with it unclipped, the list opens over the rows rather than under them.
   
      The rows are later in the document than the header, so without this they
      paint on top of an option list that is hanging over them — the options were
      there, and the first rows of the table were drawn across them. */
   .fi-ta-filters-above-content-ctn .choices__list--dropdown,
   .fi-ta-filters-above-content-ctn .choices__list[aria-expanded] {
       z-index: 30;
   }
   
   /* The flip-upward rule at the top of this file applies here and is wanted:
      this list is on the page, so upwards is more page, and a filter low on a
      long list opens towards the space there is. That is the opposite of the
      in-dialog rule, where upwards is the top edge of a scroll container — the
      difference is the point, and neither is a special case for one screen. */
   
   /* The filters sit on the search row, not on a row of their own above it.
   
      Filament renders an above-content filter form as its own band inside the
      table header, directly above the band holding Search. On a list with one
      filter that is a whole extra row of chrome to show one <select>, and it
      leaves the thing it was meant to fill — the empty half of the search row —
      still empty. Vendors showed both at once: an untouched strip beside Search,
      and a new line above it holding a single dropdown.
   
      So the two bands share a line: filters from the left, Search where it
      already was on the right.
   
      The guard is narrow on purpose. `> .fi-ta-filters-above-content-ctn:first-child`
      is only true when the filter band is the first thing in the header, which
      means the table has no heading, description or header actions of its own
      inside the container — if it had, they would be dragged onto this line too.
      All nineteen lists with header filters are in that state; the ones that are
      not keep the stacked layout, which is the right answer for them.
   
      flex-wrap, so a narrow window drops back to two rows rather than crushing
      both. Selecting rows on a list with bulk actions does the same: the toolbar
      grows leftward and the filters take their own line again. */
   .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child) {
       display: flex;
       flex-wrap: wrap;
       align-items: center;
   }
   
   .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
       > .fi-ta-filters-above-content-ctn {
       flex: 1 1 18rem;
       /* The toolbar beside it brings its own left padding. */
       padding-right: 0;
       /* And the same short padding as the toolbar it shares the line with.
          Filament's py-4 is for a band with a heading above the fields; the
          heading is gone, so 16rem of it was padding above nothing. Matching
          the toolbar's 0.5rem also stops the two halves of one row being
          different heights, which is what made the strip look like a section
          rather than a row. */
       padding-top: 0.5rem;
       padding-bottom: 0.5rem;
   }
   
   /* As many columns as the strip can actually hold, not as many as the window
      suggests.
   
      Filament sizes this grid off viewport breakpoints — two abreast at sm, three
      at lg, four at xl, five at 2xl — and the strip is not the viewport. It is
      whatever is left of the row after the search beside it, so on a 1400px
      window the grid was told to draw four columns into a box with room for six,
      and the Policy Tracker's fifth filter dropped to a line of its own with
      three-quarters of the row above it empty.
   
      auto-fit off the real width instead: the columns are as many as fit at
      11rem, so the fields fill the strip they are in and wrap only when they
      genuinely run out of room. One rule for every list rather than a column
      count per screen, and no dependency on which responsive variants survived
      the CSS build.
   
      Column gap down from Filament's gap-6. 1.5rem between two dropdowns is the
      spacing of a form somebody fills in top to bottom; this is a row of
      controls, and the gap was costing most of a column. The row gap stays
      generous for the narrow windows where these do wrap. */
   .fi-ta-filters-above-content-ctn .fi-ta-filters > .fi-fo-component-ctn {
       grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
       column-gap: 1rem;
       row-gap: 0.75rem;
   }
   
   .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
       > .fi-ta-header-toolbar {
       /* Was `0 1 auto` — sized to its content, which is what made the search
          the narrowest thing on the widest row. It grows now, with a basis wide
          enough that it takes its own line before it squeezes the filters.
          ...but only up to a point. Growing without a ceiling, it took half of
          every wide row: a 1610px header gave the search 850px and the filters
          the rest, so a one-line search box was wider than the five controls it
          sat beside. 28rem is a generous search field and nothing is typed into
          it that needs more; whatever it declines goes back to the filters,
          because flexbox redistributes what a maxed-out item gives up. */
       flex: 1 1 22rem;
       max-width: 28rem;
       min-width: 0;
       /* Shorter than Filament's py-3. With the heading row gone the strip was
          still padded as though something were above the fields. */
       padding-top: 0.5rem;
       padding-bottom: 0.5rem;
       /* divide-y puts a border-top between the two bands, which is a divider
          while they are stacked and a stray line across the middle of the row
          once they are not. It comes back when they wrap, because the rule below
          only lifts it at the width where they actually share a line. */
       border-top-width: 0;
   }
   
   /* And the band above the fields goes, so the fields are the row.
   
      Filament draws two rows inside that band: a heading reading "Filters"
      with a Reset link opposite it, and the fields underneath. Sharing the line
      with Search left the heading row on it too, which is why the dropdowns sat
      a row lower than the box they are level with.
   
      The heading is a label on something already obviously itself — a row of
      dropdowns named "In use" and "Employment type" does not need a word above
      it saying these are filters — and it cost that row on every list in the
      app.
   
      Reset went with it. It sat where the eye lands first, in danger red,
      offering to undo work rather than to do any; every field still clears on
      its own, which is the reset people actually reach for.
   
      `.fi-ta-filters` is a grid, so a hidden first row leaves no gap behind it.
   
      Scoped to the header strip. The filter *dialog* renders the same inner
      component and keeps both: in there the heading is the dialog's own title,
      and Reset is the only way out of a form somebody has half filled in. */
   .fi-ta-filters-above-content-ctn .fi-ta-filters > div:first-child {
       display: none;
   }
   
   /* With one row either side, the two bands line up on their centres rather
      than on their tops. */
   .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
       > .fi-ta-filters-above-content-ctn {
       align-content: center;
   }
   
   /* The search takes the room going spare.
   
      Matched by what the group holds rather than by the utility class that
      happens to align it, so it still finds the group if Filament stops using
      `ms-auto`. min-width: 0 throughout because a flex item will not shrink
      below its content otherwise, and an input's content is its placeholder.
   
      The toolbar's own width is set on the rule above that already governs it,
      rather than in a second rule with the same selector: two of those with
      contradictory `flex` values work only for as long as nobody reorders the
      file. */
   .fi-ta-header-toolbar > div:has(.fi-ta-search-field) {
       flex: 1 1 auto;
       min-width: 0;
       /* And Search and the column toggle sit at the end of the group rather than
          the start of it.
   
          Which matters on one kind of list and is invisible on every other. The
          search normally grows to fill this group, so where the group packs its
          children never comes up — except where a table has five or more filter
          fields: the rule in AppServiceProvider::narrowTheInlineFilterRow() takes
          the search's growth away there and pins it to 15rem, so that the filters
          can have the width it was using. The group still fills the toolbar
          though, which left a fixed-width search and the toggle beside it packed
          against the left of it, and a hundred and eight pixels of nothing
          between the toggle and the edge of the card — measured on the Policy
          Tracker, whose five fields are what turns that rule on, and the same on
          Invoicing for the same reason. */
       justify-content: flex-end;
   }
   
   .fi-ta-header-toolbar .fi-ta-search-field {
       flex: 1 1 auto;
       min-width: 0;
   }
   
   /* And the search box has an edge you can see.
   
      Filament draws it with `ring-gray-950/10` — the app's near-black at one
      tenth opacity — which on the panel white behind it comes out at about the
      same value as the hairline between two table rows. Now that the box is
      wide, that faint edge was outlining a large empty shape: the widening
      made the outline's weakness the thing you notice.
   
      Only the ring's colour is changed, not the box-shadow it is composed
      into. Filament's focus state swaps the same variable for the primary
      colour and thickens the ring to two pixels; writing a box-shadow here
      would win over that and leave the field with no focus state at all —
      which is the trade the phantom-ring rules further up this file exist to
      avoid making by accident.
   
      Dark separately, because the light value is an ink and the dark one has
      to be a light: Filament's own dark ring is `white/20`, and the same
      reasoning applies to it. */
   .fi-ta-header-toolbar .fi-ta-search-field .fi-input-wrp {
       /* Wide, but not the whole of a large monitor: past this the eye has to
          travel the length of the row to find where it was typing. */
       max-width: 34rem;
       margin-left: auto;
       /* And the same edge as the filters beside it. Filament rings its inputs
          with gray-950 at ten per cent, which on the panel's near-white ground
          is close to invisible - the box read as text floating on the card
          rather than as something to type in.
   
          One token for the whole header rather than a value chosen twice: the
          search had its own rgba and the filter band its own hex, picked
          independently and close enough to look like a mistake.
   
          Only the ring's colour, never the box-shadow it composes into:
          Filament's focus state swaps this same variable for the primary
          colour and thickens the ring, and a box-shadow written here would win
          over that and leave the field with no focus state at all. */
       --tw-ring-color: var(--zp-filter-edge);
   }
   
   /* Every field on this strip gets an edge you can see.
   
      Filament draws them with `ring-gray-950/10` — the app's near-black at one
      tenth opacity — which on the panel white behind them comes out at about
      the same value as the hairline between two table rows. The dropdowns read
      as text floating on the card rather than as boxes to open.
   
      One rule for the search and the filters together, so the edge is one
      decision rather than a value copied between two places that then drift.
   
      Only the ring's colour is changed, not the box-shadow it is composed
      into. Filament's focus state swaps the same variable for the primary
      colour and thickens the ring to two pixels; writing a box-shadow here
      would win over that and leave the field with no focus state at all —
      which is the trade the phantom-ring rules further up this file exist to
      avoid making by accident.
   
      Dark separately, because the light value is an ink and the dark one has
      to be a light: Filament's own dark ring is `white/20`. */
   
   /* Without :has(), a usable search rather than the narrow default. */
   @supports not selector(:has(*)) {
       .fi-ta-header-toolbar > .ms-auto { flex: 1 1 auto; min-width: 0; justify-content: flex-end; }
   }
   
   /* And the filter controls read as controls.
   
      With the heading gone, these two dropdowns are the only thing naming what
      the list is filtered by, and they were set in the same light weight as the
      table's body text — lighter still where the value shown is a placeholder,
      which "Every plan" and "All" both are. The one row that says what you are
      looking at was the quietest thing on the screen.
   
      Both selectors, because the same field is two different elements depending
      on when you look: Filament renders a bare <select> and Choices.js replaces
      it in the browser, putting the visible value in `.choices__item`. Styling
      only one of them leaves the field lighter for the moment before the script
      runs, and heavier after — a flicker on every page load.
   
      The placeholder keeps its own colour. It is greyer than a chosen value on
      purpose, and that difference is how you tell "not filtered" from "filtered
      by something"; weight makes it legible without flattening the two states
      into one. */
   .fi-ta-filters-above-content-ctn .fi-input-wrp select,
   .fi-ta-filters-above-content-ctn .fi-input-wrp input,
   .fi-ta-filters-above-content-ctn .choices__inner .choices__item {
       font-weight: 600;
   }
   
   /* The labels above them go up with the values, so the pair still reads as a
      label and its field rather than as two competing lines. */
   .fi-ta-filters-above-content-ctn .fi-fo-field-wrp-label,
   .fi-ta-filters-above-content-ctn label {
       font-weight: 600;
   }
   
   /* The strip's own layout — how the fields are laid out across it, how tall
      the band is, and where a toggle sits against the dropdown beside it — is
      further down this file, under "The strip, tightened" and the rules above
      it. There were two sets of answers here for a while, written from either
      end, and the later ones won every property they touched: what stood here
      was dead weight that read as though it were in force. */
   
   /* Stacked again on a narrow window, dividing line and all. */
   @media (max-width: 40rem) {
       .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child) {
           display: block;
       }
   
       .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
           > .fi-ta-filters-above-content-ctn {
           padding-right: 1rem;
       }
   
       .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
           > .fi-ta-header-toolbar {
           border-top-width: 1px;
       }
   }
   
   /* ---------------------------------------------------------------------------
      The date picker's calendar, which was leaving the dialog altogether.
      --------------------------------------------------------------------------- */
   
   /* One line, and it is the whole bug.
   
      Filament's calendar panel is `position: absolute` with an Alpine Floating UI
      `flip` middleware, so it is meant to open upwards when there is no room
      below. It never did, and worse, it hung outside the dialog: on Requisitions,
      opening Target from drew the September calendar across the fields beneath it
      and then past the bottom edge of the box, over the page.
   
      Neither the clipping nor the flipping worked because `.fi-fo-date-time-picker`
      is not positioned — not by Filament's stylesheet, not by ours. An absolutely
      positioned element is laid out in, and clipped by, its containing block, and
      with no positioned ancestor inside the dialog that containing block was
      something outside the scrolling content. So:
   
       - `.fi-modal-content`'s overflow did not clip the panel, because the panel
         was not in its containing-block chain. Hence a calendar over the page.
       - Floating UI works the boundary out the same way, decided the only
         constraint was the window, saw four hundred pixels of room below the
         dialog and opened downward into it. Hence no flip.
   
      Making the field itself the containing block fixes both at once: the dialog
      becomes the clipping ancestor, so `flip` starts measuring against the dialog
      and turns the calendar upwards when the room is above instead. Global rather
      than modal-only — the panel is supposed to be positioned against its own
      field wherever it is, and on a page there is nothing scrollable around it to
      clip against. */
   .fi-fo-date-time-picker {
       position: relative;
   }
   
   /* A calendar in a dialog is drawn against the window.
   
      Two answers were tried inside the dialog first. Room was made for it — the
      window let grow and the content given a floor — and the dialog jumped taller
      as the calendar opened, sliding the footer down the screen and moving filled
      fields under the pointer. Then the content was unclipped so the calendar
      could hang over the edge, which works only while the content has no scroll
      position to lose, and the dialogs where a calendar is most cramped are
      exactly the ones close enough to their 85dvh ceiling to be scrolling. On a
      zoomed browser the awards filters are one of those, and the month grid came
      back cut off along the top edge of the dialog.
   
      Both were attempts to fit a 300px panel inside a box that may be shorter than
      300px, which cannot be done. So the panel is taken out of the box instead:
      fixed puts it in the window's own coordinates, where no ancestor's overflow
      can reach it, and it is placed there by the script in
      resources/views/filament/calendars-inside-dialogs.blade.php — which is also
      what puts this class on, and takes it off again on close.
   
      The z-index clears the dialog's footer, the way an in-dialog select list's
      does. */
   .fi-fo-date-time-picker-panel.zp-calendar-afloat {
       position: fixed;
       z-index: 50;
   }
   
   /* And in the header strip, where the table container's own `overflow: hidden`
      would cut it off — the same unclipping the select lists there get. No list
      with four or fewer filters carries a date range today, so this is here so
      that the first one to do so does not have to rediscover it. */
   .fi-ta-ctn:has(.fi-ta-filters-above-content-ctn .fi-fo-date-time-picker-panel:not([style*="display: none"])) {
       overflow: visible;
   }
   
   .fi-ta-filters-above-content-ctn .fi-fo-date-time-picker-panel {
       z-index: 30;
   }
   
   /* Header filters take the width they need, not a fifth of the strip each.
   
      Filament lays the header form out at a fixed column count per breakpoint —
      two from 640px, three from 1024, four from 1280, five from 1536. That is
      sized for how many fields *could* be there rather than how many are, so on a
      wide screen a list with two filters still got two fifths of the strip and
      three empty fifths.
   
      The cost lands on the labels. "Approved but not on the record" is thirty
      characters; in a 195px cell beside its checkbox it broke over three lines,
      which made the filter twice the height of the select next to it and left the
      row looking like something had gone wrong.
   
      auto-fit with a floor instead: as many columns as fit at 15rem or more, and
      the ones in use stretch across the rest. Two filters become two wide
      columns and the label sits on one line; six filters still wrap onto a
      second row at a readable width rather than being squeezed onto one. Nothing
      here sets a fixed width, so it still collapses to a single column on a
      phone. */
   /* A row of controls, each the width it needs.
   
      Filament lays the header form out on a grid with a fixed column count per
      breakpoint - two from 640px, three from 1024, four from 1280, five from
      1536 - sized for how many filters *could* be there rather than how many
      are. Two filters on a wide screen therefore got a fifth of the strip each,
      and both were wrong in a different way: the Status select stretched to
      360px for a word like "Approved", while "Approved but not on the record"
      was squeezed into 195px and broke over three lines.
   
      A grid also aligns its cells to the top, so the checkbox sat level with the
      *label* above the select rather than with the select itself, leaving it
      hanging at the top of a taller cell.
   
      Flex instead: each filter takes its own width, and `align-items: center`
      lines the short checkbox up with the middle of the taller field beside it.
   
      Matched by class pattern rather than by name because Filament builds the
      columns from Tailwind arbitrary values - `sm:grid-cols-[--cols-sm]`, fed by
      custom properties it sets inline - across two nested grids, only one of
      which carries the responsive classes. The pattern catches both, and a
      two-class selector outranks Tailwind's single-class one at every
      breakpoint. */
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] {
       display: flex;
       flex-wrap: wrap;
       /* Bottom, not centre: see the checkbox rule below for why. */
       align-items: flex-end;
       gap: 0.5rem 1rem;
   }
   
   /* A dropdown or a date box is given a width, because Filament's inputs are
      `w-full` and would otherwise collapse to their shortest option.
   
      14rem, and the number comes from the content rather than from taste. The
      longest option any header dropdown has to show is Recruitment's "On Hold -
      Position Filled" at twenty-five characters: about 165px of text at the
      panel's 13.5px, plus the left padding, the caret and the border, which lands
      at roughly 207px. 13rem is 208px, so it fitted by a pixel, which is not a
      margin. 14rem leaves a comfortable one.
   
      Still a good deal less than the 360px the grid was handing out for a word
      like "Approved", which is what prompted this. */
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(select),
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(input[type="text"]),
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(input[type="search"]) {
       /* 14rem where there is room, and able to give ground where there is not.
          A fixed basis pushed HR Policies' third filter - "Compulsory, not read
          by everyone" - onto a line of its own, which is a whole extra row of
          header to avoid shortening two dropdowns by a few millimetres. 9.5rem
          still shows a dozen characters, which is enough to read a chosen value
          and correct a wrong one. */
       flex: 0 1 14rem;
       min-width: 9.5rem;
       max-width: 14rem;
   }
   
   /* A checkbox or a toggle is as wide as its label and no wider, so the label
      stays on one line and the control sits next to the field it qualifies
      rather than in a column of its own. */
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(input[type="checkbox"]),
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(button[role="switch"]) {
       flex: 0 0 auto;
       max-width: none;
       /* On one line, so the field asks for the width it actually needs. Allowed
          to wrap it, the browser reports a narrow minimum, takes the room, and
          then uses two lines of it. */
       white-space: nowrap;
   }
   
   
   /* The strip, tightened.
   
      Filament pads the filter band with 1rem top and bottom, on top of the
      toolbar's own 0.75rem, and stacks each label above its control with a 0.5rem
      gap. On a band holding one dropdown and one checkbox that added up to a
      110px-tall header above a three-row table - more height spent on the filters
      than on two of the rows they filter.
   
      Nothing is removed: the label still names the field and the controls keep the
      36px height every other control in the panel has. It is the space around them
      that comes down. */
   .fi-ta-filters-above-content-ctn {
       padding-top: 0.4rem;
       padding-bottom: 0.4rem;
   }
   
   /* Label tucked closer to the control it names, and set at the size the panel
      uses for secondary text rather than for body copy. */
   .fi-ta-filters-above-content-ctn .fi-fo-field-wrp-label {
       margin-bottom: 0.15rem;
       font-size: 0.75rem;
       line-height: 1.2;
   }
   
   .fi-ta-filters-above-content-ctn .fi-fo-field-wrp > * + * {
       margin-top: 0;
   }
   
   /* Row gap only where a second row of filters actually happens. Between the
      filter band and the toolbar beneath it, the two paddings already meet. */
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] {
       row-gap: 0.6rem;
   }
   
   /* And the band no longer draws a line under itself when it shares the row with
      the search box, which it does on every list with four filters or fewer - the
      divider belonged to the stacked layout. */
   .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
       > .fi-ta-filters-above-content-ctn {
       padding-top: 0.5rem;
       padding-bottom: 0.5rem;
   }
   
   /* The checkbox lines up with the control, not with the whole field.
   
      `align-items: center` on the row centres it against the *block* beside it -
      label, gap and control together - so it landed about eight pixels above the
      middle of the select itself and read as sitting slightly high. Nudged down
      by half the label's height so the two line up on the same axis.
   
      Done by geometry rather than by a guessed offset. The first attempt was
      `margin-top: 0.5rem`, worked out from a 12px label and its gap - which is
      right until somebody changes either, and was already a pixel or two out.
   
      Instead: the row bottom-aligns, and a checkbox is given the same 36px box
      every other control stands in, with the box itself centred inside it. The
      bottom of that box lines up with the bottom of the select, so their middles
      line up too - whatever the label above the select turns out to be. */
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] {
       align-items: flex-end;
   }
   
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(input[type="checkbox"]),
   .fi-ta-filters-above-content-ctn [class*="grid-cols-"] > *:has(button[role="switch"]) {
       display: flex;
       align-items: center;
       min-height: 36px;
   }
   
   /* A firmer edge on the controls in the strip.
   
      Filament rings its inputs with gray-950 at ten per cent, which on the panel's
      near-white ground is close to invisible - fine inside a dialog, where the
      dialog's own edge frames the fields, and too faint out on the page where the
      only thing saying "this is a control" is the ring itself. An unchecked
      checkbox suffered most: a pale square on white next to a label that looked
      like plain text.
   
      Only inside the strip. The same ring is right everywhere else, and darkening
      it globally would put a grey box around every field on every form. */
   .fi-ta-header-ctn,
   .fi-ta-filters-above-content-ctn {
       --zp-filter-edge: #a8adc8;
   }
   
   .dark .fi-ta-header-ctn,
   .dark .fi-ta-filters-above-content-ctn {
       --zp-filter-edge: #3f4194;
   }
   
   .fi-ta-filters-above-content-ctn .fi-input-wrp {
       --tw-ring-color: var(--zp-filter-edge);
   }
   
   .fi-ta-filters-above-content-ctn input[type="checkbox"],
   .fi-ta-filters-above-content-ctn input[type="radio"] {
       border-color: var(--zp-filter-edge);
   }
   
   /* Checked, the box is the accent and its own edge would fight it. */
   .fi-ta-filters-above-content-ctn input[type="checkbox"]:checked,
   .fi-ta-filters-above-content-ctn input[type="radio"]:checked {
       border-color: transparent;
   }
   
   /* A calendar in a dialog opens below its field, never over the dialog.
   
      Filament positions the panel with `x-float.placement.bottom-start...flip`,
      and `flip` measures the room below against the clipping ancestor — which
      inside a dialog is the scrolling content, not the window. On the Escalations
      filters, "Raised from" sits low enough that the space below it looked too
      small, so the calendar flipped upward and covered the heading and the three
      rows of selects above it. Turning it up there is worse than clipping it: the
      fields you were about to read are the ones it hides.
   
      Worse still, the measurement happens before the dialog has grown. The rule
      further up gives the content a floor while a calendar is open, but that only
      applies once the panel is displayed — so `flip` decides against the old,
      smaller box and never revisits it.
   
      `!important` because x-float writes `top` as an inline style, and a
      stylesheet declaration marked important is the one thing that outranks a
      plain inline one. Scoped to dialogs: on a page, flipping up near the bottom
      of the window is the correct behaviour and is left alone. And
      only for a panel this stylesheet is still positioning: Once the script
      above has taken one out of the dialog it is placed against the window, top
      and left, and forcing `top: 100%` on it would put it back under a field it is
      no longer positioned relative to. */
   .fi-modal-content .fi-fo-date-time-picker-panel:not(.zp-calendar-afloat) {
       top: 100% !important;
       bottom: auto !important;
       margin-top: 0.5rem;
   }
   
   
   /* ============================================================
      Dialogs hold the page
      ------------------------------------------------------------
      Two things a dialog on this app did not do, and every dialog
      on it now does. The behaviour is fitted by the script in
      resources/views/filament/dialogs-hold-the-page.blade.php,
      which only puts these two classes on and takes them off
      again; both halves are appearance, and neither of them
      decides whether a dialog opens, closes or saves anything.
      ============================================================ */
   
   /* 1. The page behind a dialog does not scroll.
   
      A dialog is a question with the rest of the screen dimmed out
      behind it, and a wheel over that dim used to scroll the board
      underneath — so the cards a scheduling dialog was opened from
      slid away while it was still being filled in, and closing it
      left you somewhere you did not choose to be.
   
      Held on the root element rather than by pinning the body:
      `position: fixed` on the body loses the scroll position and
      drops the page back to the top on close, which is a worse
      answer than the problem. Overflow hidden keeps it exactly
      where it was.
   
      The padding replaces the width the scrollbar had, so the
      layout does not jump sideways by that much as it goes; the
      script measures the bar and writes the value, and skips it
      where a scrollbar was never taking up room (an overlay one,
      a touch screen). This is the same trade Filament's own
      modals make through Alpine, so dialogs of both kinds behave
      alike. */
   html.zp-page-held,
   html.zp-page-held body {
       overflow: hidden !important;
   }
   
   /* 2. A click beside a dialog says so.
   
      Clicking off a dialog that does not close that way was
      silent: nothing moved, and the reasonable reading is that
      the click was missed or the page has stopped responding, so
      the next thing tried is usually another click in the same
      empty place. A short shake answers it — the dialog is still
      there, it is still the thing being asked, and it is waiting
      on one of its own buttons.
   
      A shake rather than a ring or a flash of colour. A ring
      drawn around the box reads as a focus outline, which is a
      thing this app draws around whatever the keyboard is on and
      would be saying the wrong word here; it also has to be
      fitted to whatever corner radius and shadow each dialog
      already has, and looks pasted on when it is not. Movement
      belongs to the box itself, borrows none of its styling and
      is over before it is read as decoration.
   
      Small on purpose: about six pixels, damped to nothing, and
      done inside four tenths of a second. This is a nudge and not
      an alarm, and it happens to people who clicked in the wrong
      place, which is not an error worth shouting about.
   
      Dialogs that do close on a click beside them are left out by
      the script: they are already answering.
   
      `transform` is the only property touched, so every dialog's
      own radius, border and shadow are left exactly as they are,
      and nothing around the box reflows while it moves. */
   @keyframes zp-dialog-attention {
       0%, 100% { transform: translate3d(0, 0, 0); }
       16% { transform: translate3d(-6px, 0, 0); }
       32% { transform: translate3d(5px, 0, 0); }
       48% { transform: translate3d(-3px, 0, 0); }
       68% { transform: translate3d(2px, 0, 0); }
       84% { transform: translate3d(-1px, 0, 0); }
   }
   
   .zp-dialog-attention {
       animation: zp-dialog-attention 400ms cubic-bezier(.36, .07, .19, .97);
   }
   
   /* Where movement is turned down at the system level the dialog
      holds still and dips instead: one slow settle of its opacity,
      nothing that could read as a flash. */
   @media (prefers-reduced-motion: reduce) {
       @keyframes zp-dialog-attention {
           0%, 100% { opacity: 1; }
           45% { opacity: .74; }
       }
   
       .zp-dialog-attention {
           animation-duration: 440ms;
           animation-timing-function: ease-in-out;
       }
   }
   
/* ── Invoice totals, under the line items ─────────────────────────────────
   Worked out by the same service that writes the stored total and draws the
   document, so what is shown while somebody types is what will be saved. */
.zinv-totals {
    max-width: 22rem;
    margin-left: auto;
    font-size: .8125rem;
    line-height: 1.6;
}
.zinv-total-split {
    font-size: .6875rem;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: 700;
    color: #64748b;
    padding-bottom: .35rem;
    margin-bottom: .35rem;
    border-bottom: 1px solid rgb(148 163 184 / .3);
}
.zinv-total-row { display: flex; justify-content: space-between; gap: 1.5rem; padding: .1rem 0; }
.zinv-total-row span:first-child { color: #64748b; }
.zinv-total-row--grand {
    border-top: 2px solid #1d4ed8;
    margin-top: .35rem;
    padding-top: .5rem;
    font-size: 1rem;
    font-weight: 800;
}
.zinv-total-row--grand span:first-child { color: inherit; }
.zinv-total-note {
    margin: .4rem 0;
    padding: .4rem .55rem;
    background: rgb(254 252 232);
    border: 1px solid rgb(253 230 138);
    border-radius: .4rem;
    font-size: .75rem;
    color: #78350f;
}
.dark .zinv-total-split, .dark .zinv-total-row span:first-child { color: #94a3b8; }
.dark .zinv-total-note { background: rgb(69 26 3 / .35); color: #fde68a; border-color: rgb(180 83 9 / .5); }

/* ==========================================================================
   Cards on a phone, tables on a desk
   ==========================================================================

   A table of eight columns is the right shape for a desk and the wrong shape
   for a hand. Made to scroll sideways it is technically all there and
   practically unreadable: you cannot compare two rows you cannot see at once,
   and the column you want is always the one off the edge.

   So the phone gets a different answer rather than the same answer squeezed.
   Two facts at the top, four in a pair of rows beneath, everything else folded
   away behind More. The markup for both lives in the page; these rules only
   decide which of the two is showing.

   `zc-` rather than `zp-`: this block is meant to be reused by every screen
   that currently renders a table, and naming it after the pattern rather than
   after the first screen to use it is what makes the second one cheap.
   -------------------------------------------------------------------------- */

/* The switch. One breakpoint, one rule, so no screen can disagree about where
   a phone stops and a tablet starts. 900px keeps the tables on a tablet in
   landscape, where there is still room to read them. */
.zc-mob { display: none; }

@media (max-width: 900px) {
  /* `display: contents` rather than `none`, so the block stops being a box
     without its children ceasing to exist. Filament lays the page out as a
     flex column with a 2rem gap, and an emptied-out wrapper is still a flex
     item — it was contributing a gap the height of a heading between the New
     Requisition button and the first card, for a div with nothing in it.

     The table's container, and nothing above it.

     Filament's markup is `.fi-ta` wrapping two things: `.fi-ta-ctn`, which is
     the visible table, and the action modals beside it. Hiding `.fi-ta` takes
     the modals with it — so Put aside mounted the action, opened the modal,
     and the browser drew none of it. The confirmation was there the whole
     time, inside a subtree with display:none on it.

     `.fi-ta-ctn` is the table alone — but hiding all of it takes the toolbar
     with it, and that toolbar is where the search, the filter dialog and the
     column picker live. On a phone those are the only way to narrow a list,
     and the cards below are drawn from whatever they decide, so losing them
     loses control of the cards too.

     So: the rows go, and the pager with them since the cards carry their own.
     The toolbar above stays, and so do the modals beside it. */
  .zc-desk { display: contents; }
  .zc-desk .fi-ta-content,
  .zc-desk .fi-ta-pagination,
  .zc-desk .fi-ta-filters-below-content { display: none; }
  .zc-mob { display: flex; flex-direction: column; gap: 8px; }
}

[x-cloak] { display: none !important; }

/* -- One record ----------------------------------------------------------- */

.zc-card {
  border: 1px solid var(--zp-b, #e6e8f0);
  border-radius: 12px;
  background: var(--zp-p, #fff);
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.dark .zc-card { background: rgba(255, 255, 255, .02); border-color: rgba(255, 255, 255, .08); }

/* Name and status on one line. The status is the thing being scanned for, so
   it sits at the end of the line rather than inside the block of facts. */
.zc-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.zc-who { min-width: 0; }

.zc-name {
  display: block;
  font-size: 14px;
  font-weight: 650;
  line-height: 1.25;
  color: var(--zp-tx, #1b1f3b);
  text-decoration: none;
  /* A long name wraps rather than pushing the badge off the card. */
  overflow-wrap: anywhere;
}

.dark .zc-name { color: #e8eaf6; }
.zc-name:hover { text-decoration: underline; }

.zc-sub { display: block; font-size: 11.5px; color: #6b7392; margin-top: 1px; }
.dark .zc-sub { color: #9aa2c0; }

/* -- The facts ------------------------------------------------------------ */

/* Two columns, because a label and a short value do not need a whole line and
   four of them stacked is four lines of scrolling for nothing. */
.zc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 12px; margin: 0; }
.zc-grid > div { min-width: 0; }
.zc-wide { grid-column: 1 / -1; }

.zc-grid dt {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: #8a91ad;
  margin: 0 0 1px;
}

.zc-grid dd {
  margin: 0;
  font-size: 12.5px;
  line-height: 1.3;
  color: var(--zp-tx, #1b1f3b);
  overflow-wrap: anywhere;
}

/* A value that can be acted on says so. On the one device that can place the
   call, an address and a number are the two facts most often wanted as a
   gesture rather than as information. */
.zc-grid dd a { color: var(--zp-accent, #4f46e5); text-decoration: none; }
.zc-grid dd a:hover { text-decoration: underline; }
.dark .zc-grid dd a { color: #a5b4fc; }

.dark .zc-grid dd { color: #dfe3f5; }
.dark .zc-grid dt { color: #8d95b5; }

/* The folded half, once opened. A rule rather than a gap, so it reads as more
   of the same card and not as a second card. */
.zc-more { border-top: 1px dashed var(--zp-b, #e6e8f0); padding-top: 7px; }
.dark .zc-more { border-top-color: rgba(255, 255, 255, .1); }

.zc-warn { color: #b45309; font-weight: 600; }
.dark .zc-warn { color: #fbbf24; }

/* -- Status ---------------------------------------------------------------- */

.zc-pill {
  flex: none;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.5;
  white-space: nowrap;
}

.zc-info   { background: #e0f2fe; color: #0369a1; }
/* Green was the one missing colour. Recruitment reads "Active" as the good
   outcome the way the table does, and amber for a parked application says
   something different — come back to this — that grey would not. */
.zc-ok     { background: #dcfce7; color: #15803d; }
.zc-amber  { background: #fef3c7; color: #b45309; }
.zc-danger { background: #fee2e2; color: #b91c1c; }
.zc-gray   { background: #eef0f6; color: #5b6180; }

.dark .zc-info   { background: rgba(2, 132, 199, .18);  color: #7dd3fc; }
.dark .zc-ok     { background: rgba(21, 128, 61, .22);  color: #86efac; }
.dark .zc-danger { background: rgba(185, 28, 28, .18);  color: #fca5a5; }
.dark .zc-gray   { background: rgba(255, 255, 255, .07); color: #b9c0dc; }
.dark .zc-amber  { background: rgba(180, 83, 9, .22); color: #fcd34d; }

/* -- What you can do with it ---------------------------------------------- */

/* One row, and it stays one row. The destructive gestures live on the record
   itself: putting them here would double the height of every card for
   something used once a quarter. */
.zc-acts { display: flex; flex-wrap: wrap; gap: 6px; }

.zc-btn {
  flex: 1 1 auto;
  min-width: 72px;
  /* Comfortably tappable without being a slab: 32px plus the border is above
     the point where a thumb starts missing. */
  min-height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 10px;
  border: 1px solid var(--zp-b, #e6e8f0);
  border-radius: 8px;
  background: var(--zp-p, #fff);
  color: var(--zp-tx, #1b1f3b);
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.zc-btn:hover { background: #f5f7ff; }
.dark .zc-btn { background: rgba(255, 255, 255, .03); border-color: rgba(255, 255, 255, .1); color: #e2e6f7; }
.dark .zc-btn:hover { background: rgba(255, 255, 255, .07); }

.zc-ghost { flex: 0 0 auto; min-width: 64px; color: #5b6180; }
.dark .zc-ghost { color: #aeb5d4; }

/* Paging, for the card side.

   Hiding the table hides the pager underneath it, and a phone that can reach
   only the first page of a list is a phone that cannot find anybody whose
   name begins late in the alphabet. Two buttons and the number between them:
   a full page-number strip is a row of thumb-sized targets nobody needs when
   the list is already filtered. */
.zc-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 2px;
}

.zc-pager .zc-btn[disabled] { opacity: .45; cursor: not-allowed; }
.zc-pager-at { font-size: 11.5px; font-weight: 600; color: #6b7392; }
.dark .zc-pager-at { color: #9aa2c0; }

.zc-empty {
  padding: 22px 14px;
  text-align: center;
  font-size: 13px;
  color: #6b7392;
  border: 1px dashed var(--zp-b, #e6e8f0);
  border-radius: 12px;
}

/* -- Phone: the filter row, and the ring that escaped it ------------------- */

@media (max-width: 900px) {
  .zp-filters { gap: 8px; }

  /* min-width on a flex item is a floor it will overflow its parent to keep.
     At 375px that floor was wider than the room left after the page padding,
     so the search box, and the focus ring around it, hung off the right
     edge and took the page's width with it. */
  .zp-search { flex: 1 1 100%; min-width: 0; }
  .zp-dept-filter { flex: 1 1 calc(50% - 4px); min-width: 0; }
  .zp-dept-filter select { width: 100%; }

  /* Drawn inside the box rather than around it. An outset ring needs 3px of
     room on every side that nothing was reserving, which is what put it over
     the edge even once the box itself fitted. */
  .zp-search input:focus {
    box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--zp-accent) 55%, transparent) !important;
  }

  /* Nothing on this screen may widen the page. A card that overflows takes
     every other card with it, because the viewport scrolls as one.

     Deliberately not `overflow: hidden` on the card: a focus ring is drawn
     outside the button it belongs to, and a card that clips its own overflow
     cuts the ring off. `max-width` alone keeps the width honest without
     trimming anything drawn at the edges. */
  .zp-filters, .zc-mob, .zc-card { max-width: 100%; }
}

/* -- Phone: tighter ------------------------------------------------------- */

@media (max-width: 900px) {
  .zc-mob { gap: 6px; }

  .zc-card { padding: 8px 10px; gap: 6px; border-radius: 10px; }

  .zc-name { font-size: 13.5px; }
  .zc-sub { font-size: 11px; }

  .zc-grid { gap: 4px 10px; }
  .zc-grid dt { font-size: 9.5px; letter-spacing: .03em; }
  .zc-grid dd { font-size: 12px; line-height: 1.25; }

  .zc-more { padding-top: 5px; }

  .zc-acts { gap: 5px; }
  .zc-btn { min-height: 28px; min-width: 0; font-size: 11.5px; padding: 0 8px; border-radius: 7px; }
}

/* The status and the disclosure travel together at the end of the name line. */
.zc-right { display: flex; align-items: center; gap: 4px; flex: none; }

.zc-chev {
  flex: none;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: transparent;
  color: #8a91ad;
  cursor: pointer;
  padding: 0;
}

.zc-chev svg { width: 15px; height: 15px; transition: transform .15s; }
.dark .zc-chev { color: #9aa2c0; }

.zc-danger-btn { color: #b91c1c; border-color: #f3c8c8; }
.zc-danger-btn:hover { background: #fef2f2; }
.dark .zc-danger-btn { color: #fca5a5; border-color: rgba(185, 28, 28, .35); }
.dark .zc-danger-btn:hover { background: rgba(185, 28, 28, .14); }

/* -- Phone: a focus ring that cannot fall off the edge --------------------- */

/*
   Filament draws focus as a ring *outside* the control. That needs a few
   pixels of room on every side, and at the left and right edges of a 375px
   screen there is none to spare — so the ring was cut in half, or missing
   altogether, on whichever side was against the edge. A control at the very
   edge of a narrow screen is exactly the one being focused most often.

   Drawn inside instead. `outline-offset` with a negative value puts the line
   within the control's own border box, where nothing can clip it and nothing
   has to make room for it. Only on small screens: with space to spare the
   outside ring is the better-looking of the two, and the desktop is unchanged.

   `:focus-visible` rather than `:focus`, so a tap does not leave a ring behind
   on a control somebody has already finished with.
*/
@media (max-width: 900px) {
  .fi-main :is(button, select, input, textarea, summary, [role="button"]):focus-visible,
  .fi-main a:focus-visible,
  /* And inside a dialog, which is where it was worst and was being missed.
     Filament teleports a modal to the end of the body, so it is not inside
     .fi-main and none of this reached it: a field in a dialog kept the ring
     drawn outside it, and at 375px, in a box with 24px of padding, that ring
     had nowhere to go but over the label of the field above. On the Log
     Activity dialog it was drawn straight through the word "Date". */
  .fi-modal-window :is(button, select, input, textarea, summary, [role="button"]):focus-visible,
  .fi-modal-window a:focus-visible,
  .fi-topbar :is(button, input):focus-visible {
    outline: 2px solid var(--zp-accent, #4f46e5);
    outline-offset: -2px;
    border-radius: inherit;
  }

  /* And the ring Filament drew outside is stood down, so the two do not sit
     one inside the other looking like a mistake. */
  .fi-main :is(button, select, input, textarea):focus-visible,
  .fi-modal-window :is(button, select, input, textarea):focus-visible {
    --tw-ring-offset-width: 0px;
    --tw-ring-color: transparent;
  }
}

/* -- Phone: one column, whatever the component was told ------------------- */

/*
   Filament lays a section out with a column count per breakpoint, and the
   components here now ask for one on a phone, two on a tablet, three on a
   desk. This says the same thing again in one place.

   Belt and braces on purpose. The per-component setting is the right way to
   express it and it depends on a Tailwind utility being present in whichever
   build of Filament's CSS a browser happens to have cached; this does not
   depend on anything. Three columns on a 375px screen is about a hundred
   pixels each, which turns "Full-Time · Office" into a column of single words
   and overlaps the label beside it.
*/
@media (max-width: 900px) {
  .fi-main .fi-fo-component-ctn,
  .fi-main .fi-in-component-ctn {
    grid-template-columns: minmax(0, 1fr) !important;
  }

  /* A field told to span two or three of those columns now spans the one. */
  .fi-main .fi-fo-component-ctn > *,
  .fi-main .fi-in-component-ctn > * {
    grid-column: span 1 / span 1 !important;
  }
}

/* -- Cards: the few pieces the second screen needed ------------------------ */

/* Three numbers that only mean anything beside each other: required, filled,
   left. Read one at a time down a column they are just three numbers. */
.zc-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.zc-good { color: #15803d; font-weight: 600; }
.dark .zc-good { color: #86efac; }

.zc-good-btn { color: #15803d; border-color: #bbf7d0; }
.zc-good-btn:hover { background: #f0fdf4; }
.dark .zc-good-btn { color: #86efac; border-color: rgba(21, 128, 61, .35); }
.dark .zc-good-btn:hover { background: rgba(21, 128, 61, .14); }

/* Filament's pager lives inside the table, which is put away on a phone, so
   without one here the second page is unreachable. */
.zc-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 2px 2px 0;
}

.zc-pager-at { font-size: 11.5px; color: #6b7392; }
.dark .zc-pager-at { color: #9aa2c0; }
.zc-pager .zc-btn[disabled] { opacity: .45; cursor: default; }

/* How full an opening is, as a bar rather than only a number. A percentage is
   read; a bar is seen, which is the whole of what a list is scanned for. */
.zc-bar {
  display: inline-block;
  width: 72px;
  height: 5px;
  margin-right: 6px;
  border-radius: 999px;
  background: #e6e8f0;
  overflow: hidden;
  vertical-align: middle;
}

.zc-bar-fill { display: block; height: 100%; background: #4f46e5; border-radius: 999px; }
.dark .zc-bar { background: rgba(255, 255, 255, .12); }
.dark .zc-bar-fill { background: #818cf8; }

/* -- Phone: forms, tightened ---------------------------------------------- */

/*
   Filament spaces a page for a desk: 2rem above the heading, 2rem below it,
   2rem between every section, and 1.5rem inside each. On a 375px screen that
   is most of the first screenful spent on gaps, with the heading and the first
   field pushed apart far enough to look unrelated.

   Everything here is spacing and nothing is layout, so a wider screen is
   untouched and the form is the same form either way.
*/
@media (max-width: 900px) {
  /* Above and below the heading. */
  .fi-main .py-8 { padding-top: .75rem; padding-bottom: 1rem; }
  .fi-main .gap-y-8 { row-gap: .75rem; }
  .fi-main .gap-y-6 { row-gap: .625rem; }

  /* Between the heading and the first section, and between sections. */
  .fi-header { margin-bottom: .25rem; }
  .fi-form > * + * { margin-top: .625rem; }

  /* Inside a section: the padding a card carries on three sides. */
  .fi-section-content { padding: .75rem !important; }
  .fi-section-header { padding: .75rem .75rem .25rem !important; }

  /* Between one field and the next. */
  .fi-fo-component-ctn { gap: .625rem !important; }

  /* The label, the control and the line of help under it. */
  .fi-fo-field-wrp-label { margin-bottom: .125rem; }
  .fi-fo-field-wrp-hint, .fi-fo-field-wrp-helper-text { font-size: 11.5px; line-height: 1.35; }

  /* A textarea that opens at eight rows is most of a screen before anybody has
     typed anything. It still grows. */
  .fi-main textarea { min-height: 72px; }
}

/*
   And the ring around a field, drawn inside it.

   Filament rings the *wrapper* around an input, not the input, so the rule
   that fixed buttons and selects did not reach these: at the edge of a narrow
   screen the ring was still being drawn into space the page does not have, and
   came out clipped down one side.
*/
@media (max-width: 900px) {
  .fi-main .fi-input-wrp:focus-within {
    outline: 2px solid var(--zp-accent, #4f46e5);
    outline-offset: -2px;
    --tw-ring-offset-width: 0px;
    --tw-ring-color: transparent;
    --tw-ring-shadow: 0 0 #0000;
    box-shadow: none;
  }

  /* Nothing in a form may widen the page either. */
  .fi-main .fi-section, .fi-main .fi-fo-component-ctn, .fi-main .fi-input-wrp { max-width: 100%; }
}

/* -- Phone: the page heading and its buttons share a line ------------------ */

/*
   Filament stacks the heading above the header buttons below the `sm`
   breakpoint: heading, then a gap, then the button, on three lines of a screen
   that has few to spare. Put side by side they cost one line between them, and
   the heading has room for it once it stops being a 30px display size.
*/
@media (max-width: 900px) {
  .fi-header {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    flex-wrap: wrap;
  }

  .fi-header-heading { font-size: 1.25rem; line-height: 1.2; }
  .fi-header-subheading { margin-top: .125rem; font-size: .8125rem; line-height: 1.35; }

  /* The subheading sits under both, rather than squeezing the button. */
  .fi-header > div:first-child { flex: 1 1 60%; min-width: 0; }
  .fi-header .fi-ac { flex: 0 0 auto; }
  .fi-header .fi-btn { padding-inline: .625rem; font-size: .8125rem; }

  /* Two or more, and they take a line of their own.

     The rule above was written for a page with one header button, and
     `flex: 0 0 auto` says that button may neither shrink nor wrap. Leave &
     Overtime has three — Mark absent, Apply Overtime, Apply Leave — coming to
     something over 380px, so on a 375px screen they did not wrap, they ran off
     the right-hand edge: Apply Leave half off the screen, and the page given a
     sideways scroll that took every card on it along too.

     `:has(> * + *)` is "more than one action". A single one still sits beside
     the heading, which is the line that rule was saving. */
  .fi-header .fi-ac:has(> * + *) {
    flex: 1 1 100%;
    min-width: 0;

    /* Two to a row, in two equal halves.

       Wrapping alone got them onto the screen, but where the line broke was
       decided by how long the labels happened to be — rename "Mark absent" to
       "Mark somebody absent" and the layout rearranges itself. A grid says it
       instead of discovering it: first and second share the top row, the third
       sits under the first. On this screen that is Mark absent and Apply
       Overtime, with Apply Leave directly beneath Mark absent.

       Equal halves rather than sized to their labels, so the two rows line up
       and the buttons are the same width to aim at. */
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .5rem;
  }

  /* Each button fills its half rather than sitting at the left of it. */
  .fi-header .fi-ac:has(> * + *) > * { min-width: 0; width: 100%; }
  .fi-header .fi-ac .fi-btn { min-width: 0; width: 100%; }
}

/* -- Phone: nothing in a form may reach past the edge ---------------------- */

/*
   Every field was drawing its border past both edges of the screen. Filament
   lets a section run full-bleed on a small screen by design, and inside it the
   grid tracks are sized from the content rather than the room available — so a
   long label or a wide control pushed the track wider than the page and took
   the border out with it.

   `minmax(0, 1fr)` is the fix for the tracks: without the zero floor a grid
   track refuses to shrink below its content. The rest is making sure nothing
   inside claims a width of its own.
*/
@media (max-width: 900px) {
  .fi-main .fi-section { border-radius: .625rem; margin-inline: 0; max-width: 100%; }

  .fi-main .fi-fo-component-ctn,
  .fi-main .fi-in-component-ctn { grid-template-columns: minmax(0, 1fr) !important; }

  .fi-main .fi-fo-field-wrp,
  .fi-main .fi-input-wrp,
  .fi-main .fi-fo-field-wrp-label,
  .fi-main .fi-input,
  .fi-main select,
  .fi-main textarea { min-width: 0; max-width: 100%; }

  /* A label long enough to need two lines wraps rather than widening the
     column it sits in. */
  .fi-main .fi-fo-field-wrp-label { overflow-wrap: anywhere; }

  /* The last line of defence: the page itself never scrolls sideways. Clip
     rather than hidden, so it does not create a scroll container that would
     trap a sticky header. Rings are drawn inside on this breakpoint, so
     nothing legitimate is being trimmed. */
  .fi-main { overflow-x: clip; }
}

/* A pill for an outcome that has gone well. */
.zc-good-p { background: #dcfce7; color: #15803d; }
.dark .zc-good-p { background: rgba(21, 128, 61, .2); color: #86efac; }

/* ═══════════════════════════════════════════════════════════════════════
   A table reads as cards on a phone
   ═══════════════════════════════════════════════════════════════════════

   Six columns is the right shape on a laptop and the wrong shape on a
   phone: the table either scrolls sideways, which nobody discovers, or it
   squeezes until every column is two words wide. So the phone gets the same
   record written out as a small card instead.

   The same markup, restyled — not a second copy of the table. One set of
   columns, one set of actions, one source of the figures, and a desktop that
   is untouched because every rule below is either inside the phone query or
   about the toggle column, which exists only for the phone.

   A row opts in with recordClasses(MobileCards::ROW); its cells say which
   part of the card they are with data-mobile. See App\Support\MobileCards.
   ═══════════════════════════════════════════════════════════════════════ */

/* The More button is a real column, so Livewire owns and re-renders it.
   Hidden at every other width, header and all, so no table grows an empty
   column on a laptop. */
th[data-mobile="toggle"],
td[data-mobile="toggle"] { display: none; }

@media (max-width: 640px) {
    /* The table stops being a table. :has() rather than a class on the
       element, because Filament gives a row classes and the table none —
       and matched on any table rather than Filament's, so a table written
       by hand opts in the same way and is drawn by the same rules. */
    table:has(tr.zmob) { display: block; width: 100%; }
    table:has(tr.zmob) > thead { display: none; }
    table:has(tr.zmob) > tbody {
        display: flex;
        flex-direction: column;
        gap: .45rem;
        padding: .45rem;
        border: 0;
    }

    /* The card. Two columns, because most of what goes on one is a short
       label over a shorter value, and one per line is twice the scrolling
       for no more information. */
    tr.zmob {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        align-items: start;
        gap: .3rem .55rem;
        padding: .45rem .55rem;
        border: 1px solid rgb(226 232 240);
        border-radius: .625rem;
        background: #fff;
    }
    .dark tr.zmob { background: rgb(24 24 27); border-color: rgb(63 63 70); }

    tr.zmob > td,
    tr.zmob > th { display: block; width: auto; padding: 0; border: 0; }

    /* Filament pads the inside of every cell for a table row. A card is
       already padded, and paying for it twice is most of what makes a
       stacked table unreadably tall.

       The padding is not on the wrapper Filament puts straight in the cell —
       that carries none — it is two levels further in, on the text, icon or
       image block itself. Reaching only the first child left `px-3 py-4`
       where it was: thirty-two pixels of air around a seventeen-pixel line,
       four times over, which was most of a 213px card. */
    tr.zmob > td > *,
    tr.zmob > td .fi-ta-text,
    tr.zmob > td .fi-ta-icon,
    tr.zmob > td .fi-ta-image { padding: 0; }


    /* ── Where each part sits ── */
    /* The row as well as the column, and this is the part that was wrong.
       Pinning only the column left the first row to auto-placement: the
       title took column one, and whatever came next in the markup - the
       tick that says a step is needed - dropped into column two beside it,
       where it read as belonging to the title. The state badge was then
       pushed down to sit against a date it has nothing to do with.

       Row one holds the title and the state, always. Everything else
       starts on row two, in the order the table declares it, so a label is
       always directly above its own value. */
    /* Full width by default, narrowed to column one only when there is a
       badge to share the line with.
       It was written the other way round - a title spanning both columns
       unless the row had no badge - and `:not(:has(...))` is not a test to
       stake a layout on: where it answers wrongly the title spreads under
       the badge and the description prints beneath it. The positive test
       cannot fail that way, because the rule that narrows the title is the
       same rule that proves there is something to narrow it for. */
    tr.zmob > td[data-mobile="lead"] { grid-column: 1 / -1; grid-row: 1; min-width: 0; }
    tr.zmob:has(> td[data-mobile="badge"]) > td[data-mobile="lead"] {
        grid-column: 1;
        /* And a little air, so a long description never runs up against the
           badge beside it. */
        padding-right: .35rem;
    }
    tr.zmob > td[data-mobile="badge"] {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
        align-self: start;
        /* Some badges carry a second line under them — a performance cycle
           says which stage it has reached beneath whether it is open. The
           cell is pinned to the right-hand edge, so a line that ranges left
           inside it hangs away from the badge it belongs to. */
        text-align: right;
    }
    /* A line of its own, read across rather than down: the label at the left,
       the value hard against the right. Two of these stacked fill the width
       of a card and cannot collide, which a pair of half-width cells does the
       moment one of the values is an email address. */
    tr.zmob > td[data-mobile="row"] {
        grid-column: 1 / -1;
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: .75rem;
        min-width: 0;
    }
    tr.zmob > td[data-mobile="row"]::before {
        content: attr(data-label);
        flex: none;
        font-size: .625rem;
        font-weight: 600;
        letter-spacing: .04em;
        text-transform: uppercase;
        line-height: 1.4;
        color: rgb(100 116 139);
    }
    .dark tr.zmob > td[data-mobile="row"]::before { color: rgb(148 163 184); }
    /* The value takes what is left and wraps inside it rather than pushing. */
    tr.zmob > td[data-mobile="row"] > * { min-width: 0; text-align: end; }

    tr.zmob > td[data-mobile="wide"],
    tr.zmob > td[data-mobile="more"],
    tr.zmob > td[data-mobile="toggle"],
    tr.zmob > .fi-ta-actions-cell,
    tr.zmob > .fi-ta-selection-cell { grid-column: 1 / -1; }
    tr.zmob > td[data-mobile="off"] { display: none; }

    /* ── What comes after what ──
       The card is auto-placed in DOM order, and the table's column order puts
       a folded-away cell in the middle of the pairs. Opened, that cell spans
       the card and breaks the row, so the pair after it drops to a line of its
       own — a figure that moves when you open the card reads as a different
       figure.

       `order` fixes the reading order without touching the column order, which
       belongs to the desk. Pairs first, then whatever was folded away, then
       the toggle, then the buttons. Grid auto-placement honours `order`, so
       the two pinned cells on row one are unaffected. */
    tr.zmob > td[data-mobile="more"] { order: 1; }
    tr.zmob > td[data-mobile="toggle"] { order: 2; }
    tr.zmob > .fi-ta-actions-cell { order: 3; }

    /* The tick that selects the row does not appear on a card.
       It cannot share the title line — that row is spoken for by the name and
       the state badge — so it took a line of its own between a person's name
       and the policy they have not read, which is a line of card spent on a
       control almost nobody uses from a phone. Captioning it made it legible
       and no more useful.

       What goes with it is the bulk action: selecting several rows and
       chasing them together is a laptop job now. The per-row buttons are
       untouched, so nothing can be done on a laptop and not here — it simply
       takes one press per person. */
    tr.zmob > .fi-ta-selection-cell { display: none; }

    /* ── Label over value ── */
    /* Including the cells that named no part of the card. The vocabulary
       has always said an unmarked column is treated as a pair; it said so
       only in the documentation, so the two columns on the plans table
       nobody had got round to marking printed their figures with no label
       above them - a bare 0 and 1 under the buttons, which reads as a
       fault rather than as a count. */
    tr.zmob > td[data-label]:not([data-mobile])::before,
    tr.zmob > td[data-mobile="pair"]::before,
    tr.zmob > td[data-mobile="wide"]::before,
    tr.zmob > td[data-mobile="more"]::before {
        content: attr(data-label);
        display: block;
        font-size: .625rem;
        font-weight: 600;
        letter-spacing: .04em;
        text-transform: uppercase;
        line-height: 1.25;
        color: rgb(100 116 139);
    }
    .dark tr.zmob > td[data-mobile="pair"]::before,
    .dark tr.zmob > td[data-mobile="wide"]::before,
    .dark tr.zmob > td[data-mobile="more"]::before { color: rgb(148 163 184); }

    /* Smaller, but not smaller than a phone can read. */
    tr.zmob .fi-ta-text-item-label { font-size: .75rem; line-height: 1.35; }
    tr.zmob > td[data-mobile="lead"] .fi-ta-text-item-label {
        font-size: .8125rem;
        line-height: 1.3;
    }
    tr.zmob .fi-ta-text { gap: 0; }

    /* The second line Filament draws under a column — a designation under a
       name, "Overdue" under a date. Kept: it is the line that says what to
       do about the row. */
    tr.zmob .fi-ta-text-item-description,
    tr.zmob .fi-ta-text + .text-sm,
    tr.zmob td .text-sm.text-gray-500 { font-size: .6875rem; line-height: 1.35; }

    /* ── The rest of the record ── */
    tr.zmob:not(.zmob-open) > td[data-mobile="more"] { display: none; }
    tr.zmob > td[data-mobile="toggle"] { display: block; }

    .zmob-toggle {
        font-size: .6875rem;
        font-weight: 600;
        color: rgb(37 99 235);
        padding: 0;
    }
    .zmob-toggle:hover { text-decoration: underline; }
    .dark .zmob-toggle { color: rgb(147 197 253); }

    /* ── Actions ── */
    /* Along the bottom, on one line, above a hairline that separates them
       from the record they act on. Compact enough that opening a card is
       still cheaper than scrolling past it. */
    tr.zmob > .fi-ta-actions-cell .fi-ta-actions {
        justify-content: flex-start;
        flex-wrap: wrap;
        gap: .3rem .5rem;
        margin-top: .1rem;
        padding-top: .4rem;
        border-top: 1px solid rgb(241 245 249);
    }
    .dark tr.zmob > .fi-ta-actions-cell .fi-ta-actions { border-top-color: rgb(39 39 42); }
    tr.zmob .fi-ta-actions .fi-link-label { font-size: .75rem; }

    /* An action written as a button rather than a link keeps its desk size,
       and two of them were the tallest thing on the card — taller than the
       name at the top of it. */
    tr.zmob .fi-ta-actions .fi-btn {
        padding: .1875rem .5rem;
        font-size: .75rem;
        gap: .25rem;
    }
    tr.zmob .fi-ta-actions .fi-btn .fi-btn-icon { width: .875rem; height: .875rem; }

    /* Nothing inside a card may push it wider than the screen.

       The second line is the one that does the work. Filament draws a cell's
       contents in a grid, and a grid item is sized to its own content unless
       it is told it may shrink — so "Niyamathullah Mohammed" measured 169px
       inside a 152px half-card and simply hung out of it, over the badge
       beside it. overflow-wrap alone cannot help: it breaks a long word, and
       these are ordinary short ones that were never given the chance to wrap. */
    tr.zmob > td { overflow-wrap: break-word; }
    tr.zmob > td .fi-ta-col-wrp,
    tr.zmob > td .fi-ta-text,
    tr.zmob > td .fi-ta-text-item { min-width: 0; }
    /* And it is allowed to use the second line it now has room for.
       Filament sets `white-space: nowrap` on a column's text so a table keeps
       its rows one line deep — right for a grid of columns, and the reason a
       name in a card ran out of the card rather than wrapping inside it. */
    tr.zmob > td .fi-ta-text-item-label { overflow-wrap: anywhere; white-space: normal; }
    table:has(tr.zmob) { table-layout: fixed; }



    /* An icon is a value like any other, so it sits under its own label
       rather than centred in the middle of the cell, where it looked like a
       heading of its own. */
    tr.zmob > td[data-label]:not([data-mobile]) .fi-ta-icon,
    tr.zmob > td[data-label]:not([data-mobile]) .fi-ta-text,
    tr.zmob > td[data-mobile="pair"] .fi-ta-icon,
    tr.zmob > td[data-mobile="pair"] .fi-ta-text { justify-items: start; }
    tr.zmob .fi-ta-icon { padding: 0; }
    tr.zmob .fi-ta-icon-item { margin: 0; }

    /* Filament leaves a gap under a column's first line for the second one
       it may draw. A card has a label doing that job already. */
    tr.zmob > td[data-mobile="pair"] .fi-ta-col-wrp,
    tr.zmob > td[data-mobile="more"] .fi-ta-col-wrp { min-height: 0; }
    tr.zmob .fi-ta-placeholder { font-size: .75rem; }
}

/* ═══════════════════════════════════════════════════════════════════════
   A hand-written table reads as cards on a phone
   ═══════════════════════════════════════════════════════════════════════

   The sibling of the block above. That one restyles a Filament table, which
   knows its own columns and can be tagged from PHP; this one does the same
   for the tables written by hand in a page's own Blade — Leave & Overtime,
   and the several like it.

   Same idea, same vocabulary, so the two read as one system: a row opts in
   with `zmob-r`, its table with `zmob-t`, and each cell says which part of
   the card it is with `data-mobile`:

     lead    the title line — a name, usually, and what you scan down
     badge   the state, on the title line, at the right
     pair    a label and a value, two to a row
     wide    a label and a value that needs the whole width
     acts    the buttons, along the bottom
     off     not on a phone at all

   and two modifiers that decide where a pair lands:

     data-full   this cell takes the whole width
     data-late   place after the other pairs, whatever the markup order

   `data-late` exists because a card reads down and a table reads across.
   Leave declares Type, From, To, Days; laid out in that order the dates are
   split across two rows, From beside Type and To beside Days. Marked late
   they sit together on a line of their own, and the desktop column order —
   which is the right one for a desk — is left alone.

   The heading each cell loses when the header row goes is carried on the
   cell itself in `data-label`, written where the column is written, so a
   card cannot end up captioned differently from the table it stands in for.

   Nothing here touches a desk: every rule is inside the query, bar the two
   that hide phone-only furniture.
   ═══════════════════════════════════════════════════════════════════════ */

@media (max-width: 900px) {
    /* The table stops being a table. */
    table.zmob-t { display: block; width: 100%; table-layout: fixed; }
    table.zmob-t > thead { display: none; }
    table.zmob-t > tbody {
        display: flex;
        flex-direction: column;
        gap: 8px;
        padding: 8px;
        border: 0;
    }

    /* One record. Two columns, because most of what goes on a card is a
       short label over a shorter value, and one per line is twice the
       scrolling for no more information. */
    tr.zmob-r {
        display: grid;
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
        align-items: start;
        gap: 7px 12px;
        padding: 10px 12px;
        border: 1px solid var(--zp-b, #e2e8f6);
        border-radius: 11px;
        background: var(--zp-p, #fff);
    }

    /* A hover tint is a pointer's way of saying "this row". A finger has no
       hover, and the one it leaves behind on tap reads as a selection. */
    tr.zmob-r:hover { background: var(--zp-p, #fff); }

    tr.zmob-r > td {
        display: block;
        width: auto;
        padding: 0;
        border: 0;
        overflow-wrap: break-word;
        /* A card cannot be pushed wider than the screen by its own contents:
           the viewport scrolls as one, so one long word would take every
           other card sideways with it. */
        min-width: 0;
        /* And it may use a second line. These tables set `white-space: nowrap`
           on every cell, which is right for a row of money columns — a figure
           broken over two lines is a figure misread — and wrong the moment the
           row becomes a card: "Zeper Go HRM Platform" measured 155px inside a
           151px half-card and ran out of it over the health badge. Badges keep
           their own nowrap; a badge that wraps is a badge that has come apart. */
        white-space: normal;
    }

    /* ── Where each part sits ──
       The row as well as the column. Pinning only the column leaves the
       first row to auto-placement, and whatever follows the title in the
       markup drops in beside it looking like part of it. */
    tr.zmob-r > td[data-mobile="lead"] { grid-column: 1; grid-row: 1; font-size: 13.5px; }
    tr.zmob-r > td[data-mobile="badge"] {
        grid-column: 2;
        grid-row: 1;
        justify-self: end;
        align-self: start;
    }
    tr.zmob-r > td[data-mobile="wide"],
    tr.zmob-r > td[data-mobile="acts"],
    tr.zmob-r > td[data-full] { grid-column: 1 / -1; }

    /* A line of its own, read across: the label at the left, the value hard
       against the right. The same shape the Filament-side cards use, for the
       same reason — a value too long to share a line with another fact and
       too short to deserve a line under its own label. A customer name and a
       quoted item are both that. */
    tr.zmob-r > td[data-mobile="row"] {
        grid-column: 1 / -1;
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: .75rem;
        min-width: 0;
        font-size: 12.5px;
        line-height: 1.3;
        text-align: end;
    }
    tr.zmob-r > td[data-mobile="row"]::before {
        content: attr(data-label);
        flex: none;
        font-size: 9.5px;
        font-weight: 600;
        letter-spacing: .04em;
        text-transform: uppercase;
        line-height: 1.4;
        color: var(--zp-sb, #61688c);
        text-align: start;
    }
    tr.zmob-r > td[data-mobile="off"] { display: none; }
    /* ── The order a card is read in ──
       A table declares its columns in the order a row is read across; a card
       is read down, and the two are not the same order. These four say where
       each kind lands whatever the markup says, so a screen can keep the
       column order that is right for a desk. */
    tr.zmob-r > td[data-late] { order: 1; }
    tr.zmob-r > td[data-mobile="toggle"] { order: 2; }
    tr.zmob-r > td[data-mobile="more"] { order: 3; }
    /* Last of all. Without this the Action cell — declared before the dates in
       the table — was placed before them too, so the buttons landed in the
       middle of the card with From and To underneath them. */
    tr.zmob-r > td[data-mobile="acts"] { order: 4; }

    /* ── The rest of the record ──
       For a table wide enough that half of it is working underneath the half
       being read: Project Costing carries the three figures a total cost is
       made of, which are worth having and are not what anybody opens the
       screen for. Folded, with a button that says what is behind it.

       The open state is `zmob-open`, the same class the Filament-side cards
       use, so one gesture is spelt one way across the application. */
    tr.zmob-r:not(.zmob-open) > td[data-mobile="more"] { display: none; }
    tr.zmob-r > td[data-mobile="more"] { min-width: 0; }
    tr.zmob-r > td[data-mobile="more"]::before {
        content: attr(data-label);
        display: block;
        margin-bottom: 1px;
        font-size: 9.5px;
        font-weight: 600;
        letter-spacing: .04em;
        text-transform: uppercase;
        line-height: 1.4;
        color: var(--zp-sb, #61688c);
    }
    tr.zmob-r > td[data-mobile="more"] { font-size: 12.5px; line-height: 1.3; }

    tr.zmob-r > td[data-mobile="toggle"] { grid-column: 1 / -1; }
    .zmob-more-btn {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        width: 100%;
        min-height: 30px;
        padding: 6px 2px 0;
        border: 0;
        border-top: 1px dashed var(--zp-b, #e2e8f6);
        background: none;
        font: inherit;
        font-size: 10.5px;
        font-weight: 700;
        letter-spacing: .04em;
        text-transform: uppercase;
        color: var(--zp-accent, #4f46e5);
        cursor: pointer;
    }
    .zmob-more-btn::after { content: '▾'; font-size: 11px; }
    tr.zmob-r.zmob-open .zmob-more-btn::after { content: '▴'; }

    /* A cell that says nothing about itself is not a pair — it is the empty
       state, or a colspan, and it takes the width it asked for. */
    tr.zmob-r > td:not([data-mobile]) { grid-column: 1 / -1; }

    /* ── Label over value ── */
    tr.zmob-r > td[data-mobile="pair"]::before,
    tr.zmob-r > td[data-mobile="wide"]::before {
        content: attr(data-label);
        display: block;
        margin-bottom: 1px;
        font-size: 9.5px;
        font-weight: 600;
        letter-spacing: .04em;
        text-transform: uppercase;
        line-height: 1.4;
        color: var(--zp-sb, #61688c);
    }

    tr.zmob-r > td[data-mobile="pair"],
    tr.zmob-r > td[data-mobile="wide"] { font-size: 12.5px; line-height: 1.3; }

    /* ── What can be done with it ──
       Along the bottom, on one line, over a hairline that separates the
       buttons from the record they act on. */
    tr.zmob-r > td[data-mobile="acts"] {
        margin-top: 1px;
        padding-top: 7px;
        border-top: 1px solid var(--zp-b, #e2e8f6);
    }
    tr.zmob-r > td[data-mobile="acts"]:empty { display: none; }

    /* A row with nothing to do on it says so by having no buttons. An em-dash
       under a hairline, in the place buttons live, reads as a button that
       failed to draw. A dash that carries words — "Awaiting approval", "With
       Sainadh" — is a different thing and stays. */
    tr.zmob-r > td[data-mobile="acts"]:has(> .zp-dash-none) {
        border-top: 0;
        padding-top: 0;
        display: none;
    }

    /* An empty state is a sentence, not a record. */
    tr.zmob-r:has(> td.zp-empty) {
        border-style: dashed;
        background: transparent;
    }
    tr.zmob-r > td.zp-empty { padding: 6px 2px; text-align: center; }
}

/* ═══════════════════════════════════════════════════════════════════════
   Figures across the top, on a phone
   ═══════════════════════════════════════════════════════════════════════

   Filament draws a stats widget as one card per figure, stacked, each with
   28px of padding and a 30px number. Five of them — Total Goals, Awaiting
   Approval, Avg Progress, Avg Perf. Score, Scored Goals — came to most of two
   screens before the list they describe had begun.

   They are a glance, not a section. Two to a row and sized to what they say:
   the number is still the biggest thing on the card, and all five fit above
   the fold with the table's first row under them.

   Every screen with a stats widget, not only this one: the shape is wrong on
   all of them for the same reason.
   ═══════════════════════════════════════════════════════════════════════ */

@media (max-width: 640px) {
    /* The grid Filament lays them out in. It sets a column count from `md`
       upwards and leaves everything below it at one, which is the stack in
       question. */
    .fi-wi-stats-overview-stats-ctn {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: .5rem;
    }

    .fi-wi-stats-overview-stat { padding: .625rem .75rem; border-radius: .625rem; }

    .fi-wi-stats-overview-stat-label { font-size: .6875rem; line-height: 1.3; }
    .fi-wi-stats-overview-stat-value { font-size: 1.25rem; line-height: 1.2; margin-top: .125rem; }
    .fi-wi-stats-overview-stat-description { font-size: .625rem; line-height: 1.3; margin-top: .125rem; }
    .fi-wi-stats-overview-stat-description-icon { width: .75rem; height: .75rem; }

    /* A chart under a 20px number is a smudge. */
    .fi-wi-stats-overview-stat-chart { display: none; }

    /* ── The column picker ──
       Gone on a phone, and nothing is lost by it. What it toggles is which
       columns a table shows, and a card shows all of them already — the four
       it would hide are behind More, which is the same gesture with a better
       name. */
    .fi-ta-col-toggle { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════
   The strip over a table, and the pager under it
   ═══════════════════════════════════════════════════════════════════════

   Filament lays both out for a desk: 16px of padding on the container, the
   search field in a grid with room for a heading beside it, and the filters
   in a block of their own underneath with the same padding again. On a phone
   that is a screenful of white before the first record — on Departments, a
   search box and one checkbox came to about ninety pixels of chrome over four
   cards.

   Nothing is removed. It is the same search, the same filters and the same
   pager, given the room they need rather than the room a laptop has spare.
   ═══════════════════════════════════════════════════════════════════════ */

@media (max-width: 640px) {
    .fi-ta-header-ctn { gap: 0; }
    .fi-ta-header-ctn > div { padding: .5rem .75rem; }

    .fi-ta-header-ctn .fi-ta-search-field .fi-input { padding-block: .3125rem; font-size: .8125rem; }

    /* The filters. The padding is on the wrapper, not on the grid inside it —
       written the other way round it added a second inset rather than
       trimming the first. The wrapper is already caught by the rule above,
       which takes every direct child of the strip. */
    .fi-ta-filters-above-content-ctn { padding: .5rem .75rem; }
    .fi-ta-filters .fi-fo-component-ctn { gap: .5rem; }
    .fi-ta-filters .fi-fo-field-wrp-label { font-size: .75rem; }

    /* ── The pager ──
       Centred on one line, and small enough to read as the end of the list
       rather than another row of it. The per-page picker stands down: it is a
       desk convenience, and a phone has one sensible answer and no room to
       offer three. */
    .fi-ta-pagination { padding: .5rem .75rem; }
    /* Both classes sit on the same <nav>, so a descendant combinator between
       them matches nothing. Written as a compound, with the descendant form
       kept for any build that wraps one in the other. */
    .fi-ta-pagination.fi-pagination,
    .fi-ta-pagination .fi-pagination {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: center;
        gap: .5rem;
        width: 100%;
    }

    /* Filament draws three things at once: a Previous button, a Next button,
       and a strip carrying its own arrows plus a numbered item per page. On a
       375px screen that is two ways of going back, two of going forward, and
       a row of page numbers that wraps onto a second line.

       One of each. The big buttons stand down, and the strip keeps its two
       arrows and the page you are actually on. */
    /* The two big buttons are plain .fi-btn in this build — they carry no
       previous/next class of their own, so they have to be named by where
       they sit: a direct child of the nav, which the strip is not. */
    .fi-ta-pagination.fi-pagination > .fi-btn,
    .fi-ta-pagination .fi-pagination > .fi-btn { display: none !important; }

    .fi-ta-pagination .fi-pagination-items > li { display: none; }

    /* The arrows are the first and last entries of the strip. They were
       matched on rel="prev"/rel="next", which this build does not write —
       so the hide above applied, the re-show never did, and the strip was
       left showing the page number and no way off it.

       Position rather than an attribute: it holds whatever language the
       panel is in, and on the first page, where there is no Previous, the
       first entry is the page itself and still reads correctly. */
    .fi-ta-pagination .fi-pagination-items > li.fi-active,
    .fi-ta-pagination .fi-pagination-items > li:first-child,
    .fi-ta-pagination .fi-pagination-items > li:last-child { display: flex; }

    /* And the strip itself is hidden below `sm` by Filament, which is the
       reason none of the rules above had anything to act on. */
    .fi-ta-pagination .fi-pagination-items {
        display: flex !important;
        gap: 0;
        justify-content: center;
    }
    .fi-ta-pagination .fi-pagination-item-button { padding: .375rem .625rem; }
    .fi-ta-pagination .fi-pagination-item-label { font-size: .75rem; }

    /* Underneath, where it reads as a caption rather than competing with the
       controls for the middle of the line. */
    .fi-ta-pagination .fi-pagination-overview {
        /* Hidden below `sm` by Filament, so it needed saying before the rest
           of this rule had anything to place. */
        display: block !important;
        flex: 1 1 100%;
        text-align: center;
        font-size: .6875rem;
    }
    /* Named .fi-pagination-records-per-page-select in this build, so the line
       that went before it hid nothing and the picker was the only control
       left on the row. */
    .fi-ta-pagination .fi-ta-records-per-page-select,
    .fi-ta-pagination .fi-pagination-records-per-page-select { display: none !important; }

    /* ── The rule between the search and the filters ──
       Filament divides the strip's blocks with a hairline, which on a desk
       separates a header from its filters. On a phone the two are one short
       block and the line reads as the end of something. */
    .fi-ta-header-ctn > :not([hidden]) ~ :not([hidden]) { border-top-width: 0; }
}

/* ── The header above a table, on a phone ──────────────────────────────
   Filament stacks the heading, the button beside it, the filters and the
   search into four full-width rows, which is most of a screen before the
   first record. The button belongs beside the heading it acts on, and the
   filters belong under the search rather than above it: you search first
   and narrow afterwards, and the row that is used most should not be the
   one furthest down.

   Order only. How wide the strip is, how many columns it draws and what it
   is padded with are all settled once, above, by the rules that own the
   filter band — a second answer to any of those works only until somebody
   reorders this file. The descendant combinator rather than `>` for the
   same reason: it is a different rule about a different property, and it
   has no business looking like the one that governs the toolbar's width. */
@media (max-width: 640px) {
    /* Grid, and that word is the whole fix.

       The rules above that lay the strip across a wide page give each half a
       flex basis in rem - 18rem for the filters, 22rem for the search. That
       is a width while the two sit side by side, and it becomes a *height*
       the moment they stack: a 352px toolbar with the search floating in the
       middle of it, and 288px of filter band holding two dropdowns. Three
       empty boxes, which is what this looked like.

       Overriding those two declarations is a fight this rule loses. They are
       written as `.fi-ta-header-ctn:has(...) > .fi-ta-header-toolbar`, which
       is more specific than anything that can be said about the container
       alone, and answering them in kind would mean a second rule about the
       toolbar's width - the one thing the strip's own tests forbid, and
       rightly.

       So the container stops being a flex container. A grid item ignores
       flex-basis altogether, so both declarations become inert without being
       contradicted, and `order` works here exactly as it does in flexbox. */
    .fi-ta-header-ctn,
    .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child) {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
        align-items: stretch;
    }

    /* The rule that divided the two panels has nothing left to divide. */
    .fi-ta-header-ctn > * + * { border-top-width: 0; }

    .fi-ta-header-ctn .fi-ta-header { order: 1; }
    .fi-ta-header-ctn .fi-ta-header-toolbar { order: 2; }
    .fi-ta-header-ctn .fi-ta-filters-above-content-ctn { order: 3; }

    /* Heading and its button on one line. Two full-width rows for four words
       and one button is a third of a phone screen. */
    .fi-ta-header {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        gap: .5rem;
        padding: .6rem .75rem;
    }
    .fi-ta-header-heading { font-size: .9375rem; }
    .fi-ta-header .fi-ta-actions { flex: none; width: auto; margin: 0; }
    .fi-ta-header .fi-btn { padding: .3rem .6rem; font-size: .75rem; }
}

/* ── Dialogs on a phone ────────────────────────────────────────────────
   Edit, View history and the rest are the same modal at every width, and
   at a phone's it arrived with desktop padding, desktop headings and
   desktop gaps between fields — so a five-field form needed two screens.
   Nothing is taken out; it is drawn at the size the screen has. */
@media (max-width: 640px) {
    .fi-modal-window { border-radius: .75rem; }
    .fi-modal-header { padding: .75rem .85rem .5rem; }
    .fi-modal-heading { font-size: .9375rem; line-height: 1.3; }
    .fi-modal-description { font-size: .75rem; }
    .fi-modal-content { padding: 0 .85rem; gap: .6rem; }
    .fi-modal-footer { padding: .6rem .85rem .75rem; }
    .fi-modal-footer-actions .fi-btn { padding: .4rem .8rem; font-size: .8125rem; }

    /* The form inside it. */
    .fi-modal-content .fi-fo-component-ctn { gap: .55rem; }
    .fi-modal-content .fi-fo-field-wrp-label { font-size: .75rem; }
    .fi-modal-content .fi-fo-field-wrp-hint,
    .fi-modal-content .fi-fo-field-wrp-helper-text { font-size: .6875rem; line-height: 1.35; }
    .fi-modal-content .fi-input,
    .fi-modal-content .fi-select-input { font-size: .8125rem; padding-top: .35rem; padding-bottom: .35rem; }

    /* And a history list, which is the other thing these dialogs hold. */
    .fi-modal-content .fi-in-component-ctn { gap: .5rem; }
    .fi-modal-content .fi-in-entry-wrp-label { font-size: .6875rem; }
    .fi-modal-content .fi-in-text { font-size: .8125rem; }
}

/* Between a phone and a laptop the table stays a table, so anything a
   column marked as mobile-only secondary would add is left to the card. */
@media (min-width: 641px) and (max-width: 1023px) {
    tr.zmob > td[data-mobile="more"]::before { content: none; }
}

/* -- Phone: one focus indicator, not two ---------------------------------- */

/*
   A text field in Filament is an input inside a wrapper, and both were being
   outlined: the wrapper by the rule that stopped rings falling off the edge,
   the input by the rule that did the same for buttons and selects. Two rounded
   rectangles, one inside the other, which reads as a rendering fault rather
   than as focus.

   The wrapper keeps it. It is the thing that looks like the field — it carries
   the border, the icon and the rounding — so it is the right shape to mark.
*/
@media (max-width: 900px) {
  .fi-main .fi-input-wrp :is(input, select, textarea):focus-visible,
  .fi-main .fi-input-wrp :is(input, select, textarea):focus {
    outline: none;
  }
}

/* -- Phone: the table toolbar ---------------------------------------------- */

@media (max-width: 900px) {
  /* The column picker chooses which of nine columns to show in a table that
     is not being shown. It is the one control in the toolbar that means
     nothing here, and it was taking a third of the row. */
  .zc-desk .fi-ta-col-toggle { display: none; }

  /* Search gets the width the picker gave up. */
  .zc-desk .fi-ta-search-field { flex: 1 1 auto; min-width: 0; }
  .zc-desk .fi-ta-header-ctn { padding: 0; }
  .zc-desk .fi-ta-header-toolbar { padding: .5rem .625rem; gap: .5rem; }

  /* Filters open as a sheet across the foot of the screen rather than as a
     dropdown hanging off a button near the right edge — which on 375px is a
     panel with about eighty pixels of room to render a date range in. */
  .fi-ta-filters-dropdown,
  .fi-dropdown-panel:has(.fi-ta-filters) {
    position: fixed !important;
    inset: auto .5rem .5rem .5rem !important;
    width: auto !important;
    max-width: none !important;
    max-height: 78vh;
    overflow-y: auto;
    border-radius: .875rem;
    transform: none !important;
  }

  .fi-ta-filters-dropdown .fi-ta-filters,
  .fi-dropdown-panel:has(.fi-ta-filters) .fi-ta-filters { padding: .75rem; }
}

/* -- Phone: the page gutter, and the room a ring needs -------------------- */

/*
   Twenty-six pixels each side is the desk gutter. On 375px it is a seventh of
   the screen spent on nothing, and it was set with !important, so the phone
   rule further down never took. Fourteen is enough to keep the content off the
   edge and to give every ring somewhere to be drawn.
*/
@media (max-width: 900px) {
  .fi-main { padding-left: 14px !important; padding-right: 14px !important; }
}

/* -- Phone: filters open as a drawer from the side ------------------------ */

/*
   A dialog centred on a 375px screen has to be nearly the whole screen to hold
   a date range, at which point it is a drawer that arrived from the wrong
   direction. So it arrives from the right, full height, and the form inside
   scrolls rather than the page behind it.

   Only the filters dialog. Every other dialog on the panel keeps the centred
   treatment, because they are read and dismissed rather than worked through.
*/
@media (max-width: 900px) {
  .fi-ta-filters-modal {
    padding: 0 !important;
    justify-items: stretch !important;
    align-items: stretch !important;
    grid-template-rows: 1fr !important;
  }

  .fi-ta-filters-modal .fi-modal-window {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(88vw, 340px);
    max-width: none;
    margin: 0;
    height: 100%;
    max-height: 100%;
    border-radius: 0;
    display: flex;
    flex-direction: column;
  }

  /* The header and the footer stay put; the filters between them scroll. */
  .fi-ta-filters-modal .fi-modal-header { padding: .875rem 1rem .5rem; flex: 0 0 auto; }
  .fi-ta-filters-modal .fi-modal-content {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: .75rem 1rem;
    gap: .625rem;
  }
  .fi-ta-filters-modal .fi-modal-footer { padding: .75rem 1rem; flex: 0 0 auto; }

  /* One filter per line in a 340px drawer; two would be 150px each. */
  .fi-ta-filters-modal .fi-fo-component-ctn { grid-template-columns: minmax(0, 1fr) !important; }
}

/* -- Phone: the search box and the filters are one strip ------------------ */

/*
   Filament puts a table's filters above its toolbar when there are few enough
   of them to sit in a row, and separates the two with a rule. On a phone that
   reads as two panels stacked: filters, a line, then the search you were
   looking for — and then the records in a third panel below.

   The search comes first, because it is what somebody reaches for; the rule
   between them goes, because there is only one strip; and the strip sits
   against the records rather than floating above them.
*/
@media (max-width: 900px) {
  /* The `:has()` above narrow windows is what put these two back into a block,
     one under the other, with a rule between them. It is written with the same
     test so that it is the same rule being answered rather than a heavier one
     talking over it. */
  .zc-desk .fi-ta-header-ctn,
  .zc-desk .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child) {
    display: flex;
    flex-direction: column;
    /* `center` is what keeps the two halves on the same baseline while the
       strip runs across the page. Stacked, the same word centres them
       horizontally instead, and both ended up narrower than the strip they
       sit in, floating in the middle of it. */
    align-items: stretch;
    padding: 0;
  }

  /* Both children size to their own content. The desk rules give them a flex
     basis measured in rem, which was a width while the strip ran across the
     page and becomes a height the moment it stacks — 18rem of empty box above
     the filters, and as much again above the search. */
  .zc-desk .fi-ta-header-ctn > *,
  .zc-desk .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
    > * { flex: 0 0 auto; max-width: none; align-content: start; }

  .zc-desk .fi-ta-header-ctn > .fi-ta-header-toolbar,
  .zc-desk .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
    > .fi-ta-header-toolbar { order: 0; padding: .5rem .625rem; gap: .5rem; }

  .zc-desk .fi-ta-header-ctn > .fi-ta-filters-above-content-ctn,
  .zc-desk .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
    > .fi-ta-filters-above-content-ctn { order: 1; padding: 0 .625rem .5rem !important; }

  /* The rule that divided the two panels has nothing left to divide — and it
     was drawn above the search box, which is the second line somebody kept
     seeing there. */
  .zc-desk .fi-ta-header-ctn > * + *,
  .zc-desk .fi-ta-header-ctn:has(> .fi-ta-filters-above-content-ctn:first-child)
    > * { border-top-width: 0 !important; }

  /* Filament clips the table container so the rows sit inside its rounded
     corners. The count on the filters button is drawn deliberately outside its
     button, the way a notification dot is, and that clip took a slice off it —
     which is why the count read as a half circle at the edge of the strip.

     Nothing in this container needs clipping on a phone: the rows and the
     pager are put away, and the toolbar is all that is left. */
  .zc-desk .fi-ta-ctn { overflow: visible !important; }
  .zc-desk .fi-ta-content { overflow-x: auto; }

  /* The other way a table is put away on a phone — restacked in place rather
     than replaced — clips the same count for the same reason. */
  .fi-ta-ctn:has(tr.zmob) { overflow: visible !important; }

  /* The search box takes the whole line. Filament pushes the group to the
     right of the toolbar with `ms-auto`, which is right on a row it shares
     with filters and leaves it floating in the middle of a row of its own. */
  .zc-desk .fi-ta-header-toolbar > :last-child {
    flex: 1 1 auto;
    width: 100%;
    margin-inline-start: 0;
  }
  .zc-desk .fi-ta-header-toolbar > :first-child:empty { display: none; }
  .zc-desk .fi-ta-search-field { flex: 1 1 auto; min-width: 0; width: 100%; }

  /* Two short filters share a line; a wide one takes its own. The desk rules
     lay these out with flex, which wraps them one to a line at this width.

     Only the form's own grid. Every filter inside it builds a grid of its own,
     and a rule that reached those as well paired each field against nothing,
     so each control came out at half of its half — two 76px dropdowns in a
     320px strip. */
  .zc-desk .fi-ta-filters-above-content-ctn > .fi-ta-filters > .fi-fo-component-ctn {
    display: grid !important;
    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
    gap: .5rem !important;
    width: 100%;
    justify-content: stretch;
  }
  .zc-desk .fi-ta-filters-above-content-ctn { justify-items: stretch; }

  /* Filament nests two grids here: the outer one splits the strip into as many
     columns as the table asks for, and the form sits in the first of them. On
     a phone the strip is one column wide, and the pairing happens inside the
     form instead. */
  .zc-desk .fi-ta-filters-above-content-ctn .fi-ta-filters {
    grid-template-columns: minmax(0, 1fr) !important;
    padding: 0 !important;
  }
  .zc-desk .fi-ta-filters-above-content-ctn > .fi-ta-filters > .fi-fo-component-ctn > * {
    grid-column: span 1 / span 1 !important;
    max-width: none !important;
    min-width: 0;
  }

  /* A filter's own grid is one column, and its control fills it. */
  .zc-desk .fi-ta-filters-above-content-ctn .fi-fo-component-ctn .fi-fo-component-ctn,
  .zc-desk .fi-ta-filters-above-content-ctn .fi-fo-component-ctn .fi-fo-component-ctn > * {
    grid-template-columns: minmax(0, 1fr) !important;
    grid-column: 1 / -1 !important;
    max-width: none !important;
    width: 100%;
  }
  .zc-desk .fi-ta-filters-above-content-ctn .fi-fo-field-wrp,
  .zc-desk .fi-ta-filters-above-content-ctn .fi-input-wrp { width: 100%; max-width: none; }
  .zc-desk .fi-ta-filters-above-content-ctn .fi-fo-field-wrp-label { font-size: 11px; margin-bottom: .125rem; }

  /* One section: the strip, then the records, at the spacing the records use
     between themselves. */
  .zc-desk + .zc-mob { margin-top: -.125rem; }
  .zc-desk .fi-ta { margin-bottom: 0; }
}

/* -- Phone: the candidates screen, tighter -------------------------------- */

@media (max-width: 900px) {
  /* The five counts are a summary, not the content. They were taking the
     first screen and a half of a screen that exists to list people. */
  .zp-cand-counts { gap: 6px; margin-bottom: .5rem; }
  .zp-cc { padding: 5px 7px; border-left-width: 3px; }
  .zp-cc-n { font-size: 14px; line-height: 1.1; }
  .zp-cc-l { font-size: 8px; letter-spacing: .03em; }

  /* The role description is reference — the screening bar, read once and
     checked against. Left at its full length it was a whole screen standing
     between the counts and the first candidate. It keeps every word; it just
     scrolls inside its own box instead of pushing the list down. */
  .zp-jd {
    font-size: 12px;
    line-height: 1.45;
    max-height: 8.5rem;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

/* -- Phone: the edit form, tighter ---------------------------------------- */

/*
   The form is the whole screen here, so the space between its parts is the
   only thing competing with it. Filament's desk spacing puts about a third of
   a phone screen between the heading and the first field.
*/
@media (max-width: 900px) {
  .fi-form { gap: .5rem !important; }
  .fi-form > * + * { margin-top: .5rem; }
  .fi-main .fi-fo-component-ctn { gap: .5rem !important; }
  .fi-main .fi-section-content { padding: .625rem !important; }
  .fi-main .fi-section-header { padding: .625rem .625rem .25rem !important; }
  .fi-main .fi-section-header-heading { font-size: .9375rem; }
  .fi-main .fi-section-header-description { font-size: 11.5px; line-height: 1.4; }

  /* A field: label, control, and the line of help under it. */
  .fi-main .fi-fo-field-wrp-label { font-size: 12px; }
  .fi-main .fi-input, .fi-main select { padding-top: .4375rem; padding-bottom: .4375rem; }

  /* The form's own buttons at the foot of the page. */
  .fi-form .fi-ac { gap: .5rem; }
}

/* -- Phone: the pager is one line ----------------------------------------- */

/*
   Filament lays a pager out as three grid columns — a record count, the page
   numbers, and the per-page select — and hides the first two on a narrow
   screen. The select keeps its place in column three, and the Previous and
   Next buttons, which are given no column of their own, are auto-placed onto
   a second row underneath it. Ninety-nine pixels of pager for two controls.

   Flex instead. The grid placement classes on the children stop meaning
   anything, and whatever the pager is showing sits on one line.
*/
@media (max-width: 900px) {
  .fi-ta-pagination .fi-pagination,
  .fi-pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: .5rem;
    padding: .5rem .625rem;
  }

  .fi-pagination > * { justify-self: auto; align-self: auto; }

  /* The two buttons travel together at the end of the line rather than one at
     each edge, which on 375px put Next somewhere nobody's thumb is. */
  .fi-pagination > .fi-btn,
  .fi-pagination > :has(> .fi-btn) { flex: 0 0 auto; }

  .fi-pagination .fi-btn { padding-inline: .625rem; }
}


/* ═══ The plan preview, and the form behind it ═══════════════════════════
   What a joiner on these terms would actually get. It was four columns of
   markup built in PHP with the padding written into every cell as an inline
   style, which is why it could not be reached from here at all. It is a view
   now, and its rows carry the same markers every other table uses — so on a
   phone the rules above draw them as cards and nothing runs off the side of
   the dialog. */
.zpp-note { margin: 0 0 10px; line-height: 1.5; font-size: .8125rem; }
.zpp-note--warn { color: #b45309; }
.zpp-note--stop { color: #b91c1c; }
.zpp-note--quiet { color: #61688c; }

.zpp-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.zpp-table thead tr { border-bottom: 1px solid #e2e8f6; text-align: left; }
.zpp-table th { padding: 6px 10px; font-weight: 600; text-align: left; }
.zpp-table td { padding: 7px 10px; }
.zpp-table tbody tr { border-bottom: 1px solid #f1f5f9; }
.zpp-name { font-weight: 600; }
.dark .zpp-table thead tr { border-bottom-color: rgb(63 63 70); }
.dark .zpp-table tbody tr { border-bottom-color: rgb(39 39 42); }

@media (max-width: 640px) {
    /* The cell padding written on the table above would fight the card, so
       it is given up at this width along with the rules between rows. */
    .zpp-table tbody tr { border-bottom: 0; }
    .zpp-table td { padding: 0; }
    .zpp { max-width: 100%; }
    .zpp-note { font-size: .75rem; margin-bottom: 8px; }

    /* ── Writing a plan on a phone ──────────────────────────────────────
       The form is a page rather than a dialog, so the dialog rules above do
       not reach it. Its grids already fold to one column on their own —
       Filament reads a column count as an lg breakpoint — so what is left
       is density: a label, a field and two lines of helper text per row is
       a great deal of screen for one answer.

       The helper text stays. Every line of it says something the next
       person to open this screen needs: that an owner is a role rather
       than a person so it survives somebody leaving, that a due date is
       counted from each joiner's own start date. Shrinking it is right;
       dropping it would make the form shorter and the plan wrong. */
    .fi-fo-component-ctn { gap: .6rem; }
    .fi-fo-field-wrp-label { font-size: .75rem; }
    .fi-fo-field-wrp-hint, .fi-fo-field-wrp-helper-text { font-size: .6875rem; line-height: 1.35; }
    .fi-section-header { padding: .7rem .85rem; }
    .fi-section-header-heading { font-size: .9375rem; }
    .fi-section-header-description { font-size: .75rem; line-height: 1.4; }
    .fi-section-content { padding: .7rem .85rem; }

    /* The tasks repeater. Collapsed by default already, so the row that
       matters is the header: its label, and the handle, clone and delete
       beside it. The label wraps rather than pushing them off the screen. */
    .fi-fo-repeater-item-header { padding: .45rem .55rem; gap: .35rem; }
    .fi-fo-repeater-item-header .fi-fo-repeater-item-header-label,
    .fi-fo-repeater-item-header h3 {
        font-size: .8125rem;
        line-height: 1.35;
        min-width: 0;
        overflow-wrap: break-word;
    }
    .fi-fo-repeater-item-content { padding: .6rem .55rem; }
    .fi-fo-repeater-item-header .fi-icon-btn { padding: .25rem; }
}

/* -- Phone: a form set at the size a phone reads -------------------------- */

/*
   Everything on a form was set at desk sizes: a 14px label, a 16px value, a
   38px control and eight pixels of air on both sides of every one of them.
   A single field cost 110px, so six of them filled the screen and the edit
   page ran to 3,884px.

   Smaller, and closer together. The helper text stays — it is the line that
   says what the field decides — but at a size that reads as a footnote rather
   than as a second label.
*/
@media (max-width: 900px) {
  /* The label. Filament sets the size on a span inside the label rather than
     on the label, so a rule on the label alone changed nothing. */
  .fi-main .fi-fo-field-wrp-label,
  .fi-main .fi-fo-field-wrp-label span,
  .fi-main .fi-in-entry-wrp-label,
  .fi-main .fi-in-entry-wrp-label span { font-size: 12px; line-height: 1.35; }

  /* The value. Choices.js replaces every select in the browser, so the size
     has to be set on what it draws as well as on the element it replaced —
     otherwise the field changes size when the script runs. */
  .fi-main .fi-input,
  .fi-main .fi-input-wrp input,
  .fi-main .fi-input-wrp select,
  .fi-main .fi-input-wrp textarea,
  .fi-main .choices,
  .fi-main .choices__inner,
  .fi-main .choices__item,
  .fi-main .choices__input { font-size: 13px; line-height: 1.4; }

  /* The footnote under it. */
  .fi-main .fi-fo-field-wrp-helper-text,
  .fi-main .fi-fo-field-wrp-hint,
  .fi-main .fi-in-entry-wrp-helper-text { font-size: 10.5px; line-height: 1.3; }

  /* Control height: 38px was a desk row. */
  .fi-main .fi-input,
  .fi-main .fi-input-wrp select { padding-top: 5px; padding-bottom: 5px; }
  .fi-main textarea { min-height: 60px; }

  /* And the space around them — eight pixels between a label and its own
     control reads as a gap between two fields. */
  .fi-main .fi-fo-field-wrp > .grid,
  .fi-main .fi-fo-field-wrp .grid.auto-cols-fr { gap: 2px !important; }
  .fi-main .fi-fo-component-ctn { gap: .375rem !important; }
  .fi-main .fi-fo-field-wrp-label { margin-bottom: 0; }
}

/*
   One exception, and it is not a style choice. Mobile Safari zooms the page in
   whenever a field smaller than 16px takes focus, and it does not zoom back
   out. Everywhere else the smaller type above holds; on iOS the control keeps
   its 16px so that tapping a field does not move the whole page.
*/
@supports (-webkit-touch-callout: none) {
  @media (max-width: 900px) {
    .fi-main .fi-input,
    .fi-main .fi-input-wrp input,
    .fi-main .fi-input-wrp select,
    .fi-main .fi-input-wrp textarea,
    .fi-main .choices__inner,
    .fi-main .choices__item,
    .fi-main .choices__input { font-size: 16px; }
  }
}

/* -- Phone: the search box and the filter strip, shorter ------------------ */

@media (max-width: 900px) {
  .zc-desk .fi-ta-search-field .fi-input { padding-top: 4px; padding-bottom: 4px; font-size: 13px; }
  .zc-desk .fi-ta-search-field .fi-input-wrp-prefix { padding-inline-start: .5rem; }
  .zc-desk .fi-ta-search-field .fi-input-wrp-prefix svg { width: 1rem; height: 1rem; }

  .zc-desk .fi-ta-filters-above-content-ctn .fi-fo-field-wrp-label,
  .zc-desk .fi-ta-filters-above-content-ctn label { font-size: 11px; line-height: 1.3; }
  .zc-desk .fi-ta-filters-above-content-ctn .choices__inner,
  .zc-desk .fi-ta-filters-above-content-ctn .fi-input { font-size: 12.5px; }
  .zc-desk .fi-ta-filters-above-content-ctn .fi-input-wrp { min-height: 0; }
}

/* -- Phone: a hand-written table can be put away too ---------------------- */

/*
   The card system hides a Filament table by name. Attendance writes its two
   tables by hand, so the wrapper it puts them in has to be named as well.

   The rest of that screen's phone rules live in the page itself, next to the
   rules they are answering: a page stylesheet is served after this one, so a
   rule here of the same weight loses to it in silence.
*/
@media (max-width: 900px) {
  .zc-desk .zp-att-table-wrap { display: none; }
}

/* -- Phone: the pager sits clear of the numbers --------------------------- */

/*
   The shape of the mobile pager — one page number between two arrows, the
   count underneath — is set further up this file, beside the rest of the
   table's phone rules. These two are what that block does not cover.
*/
@media (max-width: 900px) {
  /* A strip of buttons narrower than a thumb. */
  .fi-pagination-items > li > .fi-pagination-item-button { min-width: 2rem; height: 1.875rem; }
  .fi-pagination-items > li.fi-active > .fi-pagination-item-button { min-width: 2.25rem; font-weight: 700; }

  /* A table that has been replaced by cards keeps its own pager put away; the
     cards carry one of their own.

     Said again here because the rules that shape the pager sit later in this
     file than the rule that hides it, at the same weight — so they were
     bringing it back on every screen that had been converted, which showed as
     two pagers, one of them above the cards rather than under them. */
  .zc-desk .fi-ta-pagination,
  .zc-desk .fi-pagination { display: none !important; }
}

/* -- Phone: the floating assistant is not a lid --------------------------- */

/*
   The assistant button is fixed to the bottom right of the screen, 58px of it,
   above everything else. Whatever is last on a page sits under it — and what
   is last on a list is the pager, so the Next arrow was behind the bubble.

   Room at the foot of the page, so the last row can always be scrolled out
   from under it. Cheaper than moving the button, and it fixes every screen at
   once rather than the one that was reported.
*/
@media (max-width: 900px) {
  .fi-main { padding-bottom: 4.5rem !important; }
}


/* ── Room around a field in a dialog, on a phone ────────────────────────
   A focus ring is drawn outside the box it belongs to, and a dialog's
   content area scrolls — so the ring on the last field, which is usually a
   select sitting directly above the buttons, was clipped along its bottom
   edge and read as a control that had half rendered.

   Padding rather than a margin: the clipping is done by the scrolling box,
   and only its own padding is inside the scroll. */
@media (max-width: 640px) {
    .fi-modal-content { padding-bottom: 1rem; }
    .fi-modal-content > :last-child { margin-bottom: .25rem; }
    .fi-modal-footer { padding-top: .85rem; }

    /* And the same room between the fields themselves, for the same reason.
       At .55rem the ring on one select touched the label of the next. */
    .fi-modal-content .fi-fo-component-ctn { gap: .8rem; }
}
