/* ==========================================================================
   FilterXpert design system — the single source of truth for every FEBOS screen.

   Source: the approved filterxpert.in portal. The reference is written in
   Tailwind utility classes; FEBOS is plain HTML with no build step, so those
   utilities are translated here into the CSS variables and component classes
   the four apps already use. Same design, same values, expressed the way this
   codebase can actually consume.

   WHY THIS FILE EXISTS
   All five surfaces previously carried their own copy of the same tokens. Five
   copies is how a design system drifts — one screen gets a fix and the others
   quietly don't. There is now one copy. Change a colour here and every screen
   changes together.

   Surface-specific rules stay in each page's own <style> block. This file owns
   the shared vocabulary only.
   ========================================================================== */

/* Inter is not installed on any common phone or desktop by default, so without
   this import nearly every user would silently fall back to system-ui and the
   screens would not look like the approved portal. display=swap: text renders
   immediately in the fallback and swaps when the font arrives. Approved by
   Animesh 2026-08-07. */
/* 800 and 900 added 13 Aug 2026: the reference apps set their hero numbers in
   extrabold/black, and without the real weights the browser fake-boldens 700 —
   which is exactly the smudged look the reference never has. */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap');

:root{
  /* ---- foundation ---------------------------------------------------- */
  --font: Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --bg:        #F9FAFB;   /* gray-50  — never a pure-white page */
  --card:      #FFFFFF;
  --ink:       #111827;   /* gray-900 — headings and body */
  --muted:     #6B7280;   /* gray-500 — secondary text */
  --muted-2:   #4B5563;   /* gray-600 — secondary text needing more weight */
  --line:      #E5E7EB;   /* gray-200 — thin borders, never heavy */

  /* ---- brand ----------------------------------------------------------
     The gradient is the signature. It is reserved for primary actions,
     selected/active states and brand icon badges. Used anywhere else it
     stops meaning anything, so secondary buttons, table rows and decoration
     never receive it. */
  --blue:      #2563EB;   /* blue-600 — links, focus, active underlines */
  --blue-dark: #1D4ED8;   /* blue-700 — pressed */
  --cyan:      #06B6D4;   /* cyan-500 — gradient end only */
  --grad:      linear-gradient(90deg, #2563EB 0%, #06B6D4 100%);

  /* ---- semantic -------------------------------------------------------
     Emerald for positive. Deliberately not green-500 and not teal. */
  --green:      #059669;  /* emerald-600 */
  --green-solid:#10B981;  /* emerald-500 — solid fills */
  --amber:      #D97706;  /* amber-600  — readable as text */
  --amber-solid:#F59E0B;  /* amber-500  — solid fills */
  --red:        #DC2626;  /* red-600 */
  --red-solid:  #EF4444;  /* red-500 */

  /* ---- tints ----------------------------------------------------------
     Status never appears as coloured text alone; it always sits on its tint. */
  --blue-50:    #EFF6FF;  --blue-700:    #1D4ED8;
  --emerald-50: #ECFDF5;  --emerald-700: #047857;
  --amber-50:   #FFFBEB;  --amber-700:   #B45309;
  --red-50:     #FEF2F2;  --red-700:     #B91C1C;
  --gray-50:    #F3F4F6;  --gray-700:    #374151;
  /* Mid-tints, added 13 Aug 2026. Screens were already writing
     var(--blue-200,#BFDBFE) and friends WITH fallbacks — the tokens are used,
     they just never existed, so every use silently took its fallback. */
  --blue-200:   #BFDBFE;
  --emerald-200:#A7F3D0;
  --amber-200:  #FDE68A;
  --red-200:    #FECACA;
  --gray-300:   #D1D5DB;

  /* ---- shape ---------------------------------------------------------- */
  --r-sm:   12px;   /* rounded-xl  — inputs, small controls, icon badges */
  --r:      16px;   /* rounded-2xl — buttons, minor cards */
  --r-lg:   24px;   /* rounded-3xl — major cards */
  --r-pill: 999px;
  --shadow: 0 1px 2px 0 rgba(0,0,0,.05);   /* shadow-sm, never heavier */
}

*{ box-sizing: border-box }

body{
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.55 var(--font);
  -webkit-font-smoothing: antialiased;
}

/* ---- cards: the basic unit of every screen --------------------------- */
.card{
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: 20px;
  margin-bottom: 12px;
}
.card.tap{ cursor: pointer }
/* A selected card takes the brand outline plus a pale tint — never a gradient
   fill, which would compete with the primary action inside it. */
.card.selected{
  border-color: var(--blue);
  box-shadow: 0 0 0 2px var(--blue), var(--shadow);
  background: var(--blue-50);
}

h1,h2,h3{ color: var(--ink); text-wrap: balance }
h2{ margin: 0 0 4px; font-size: 18px; font-weight: 650 }
h3{ margin: 0 0 8px; font-size: 15.5px; font-weight: 650 }

/* A heading inside the app bar takes the bar's colour, not the page's.
   The bar is the blue gradient and sets white on itself, but the rule above is
   a rule ON the element and beats anything inherited — so the title rendered
   near-black on blue and was barely readable. Inheriting is right here: the
   bar decides its own foreground, and any bar we build later gets this free. */
header h1, header h2, header h3 { color: inherit }

.muted{ color: var(--muted); font-size: 14.5px }
.note{ color: var(--muted); font-size: 13px; margin-top: 8px }
.empty{ color: var(--muted); text-align: center; padding: 26px 14px; font-size: 14.5px }

a{ color: var(--blue) }

/* ---- buttons ---------------------------------------------------------
   .btn is the primary action and carries the gradient. Everything else is
   deliberately quiet so the gradient keeps its meaning. */
.btn{
  border: 0;
  border-radius: var(--r);
  padding: 12px 18px;
  min-height: 48px;
  font: 600 15px/1.2 var(--font);
  background: var(--grad);
  color: #fff;
  cursor: pointer;
  box-shadow: var(--shadow);
  transition: opacity .12s ease;
}
.btn:hover{ opacity: .9 }
.btn:active{ opacity: .82 }
.btn[disabled]{ opacity: .5; cursor: default; box-shadow: none }

.btn.ghost{ background: var(--card); color: var(--blue); border: 1.5px solid var(--blue); box-shadow: none }
.btn.grey { background: var(--card); color: var(--ink);  border: 1.5px solid var(--line); box-shadow: none }

/* Semantic buttons stay solid — a gradient here would read as "primary". */
.btn.green{ background: var(--green);     color:#fff }
.btn.amber{ background: var(--amber-solid); color:#fff }
.btn.red  { background: var(--red);       color:#fff }

.btn.sm{ min-height: 38px; padding: 8px 13px; font-size: 14px }
/* The de-facto third size, 13 Aug 2026: style="min-height:34px;padding:5px 10px"
   had been hand-written six times in the staff panel alone. A size that keeps
   getting invented inline is a size the system owes a name. */
.btn.xs{ min-height: 34px; padding: 5px 10px; font-size: 13.5px }

:where(button, a, input, select, textarea):focus-visible{
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* ---- inputs ---------------------------------------------------------- */
label{
  display: block; margin: 12px 0 6px;
  font: 600 13px/1.3 var(--font); color: var(--muted-2);
}
input, select, textarea{
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--gray-300);
  border-radius: var(--r-sm);
  font: 16px/1.4 var(--font);
  background: var(--card);
  color: var(--ink);
}
input:focus, select:focus, textarea:focus{
  border-color: var(--blue);
  box-shadow: 0 0 0 3px var(--blue-50);
  outline: none;
}
textarea{ min-height: 88px; resize: vertical }
input[type="checkbox"], input[type="radio"]{ width: auto; accent-color: var(--blue) }

/* ---- status pills ----------------------------------------------------
   Every piece of status metadata is a pill: tinted background with darker
   text of the same hue, small and uppercase. Never bare coloured text. */
.pill{
  display: inline-flex; align-items: center;
  border-radius: var(--r-pill);
  padding: 4px 11px;
  font: 700 11.5px/1.4 var(--font);
  text-transform: uppercase; letter-spacing: .03em;
  white-space: nowrap;
  background: var(--gray-50); color: var(--gray-700);
}
.pill.good{ background: var(--emerald-50); color: var(--emerald-700) }
.pill.warn{ background: var(--amber-50);   color: var(--amber-700) }
.pill.bad { background: var(--red-50);     color: var(--red-700) }
.pill.info{ background: var(--blue-50);    color: var(--blue-700) }
/* Named explicitly even though it matches the base, because writing
   class="pill neutral" states that grey was chosen rather than defaulted to —
   and it completes the set the design system specifies (pending / danger /
   informational / neutral). */
.pill.neutral{ background: var(--gray-50); color: var(--gray-700) }
/* Test data. Deliberately the loudest pill in the set: somebody about to
   act on one of these rows needs to notice before they act, not after. */
.pill.test{ background: #7C2D12; color: #fff; letter-spacing: .08em }
/* Alias, 13 Aug 2026: the Admin Panel writes class="pill ok" in seven places
   and only rendered correctly there because of a page-local `.ok` rule. The
   word is reasonable; the component should own it on every surface. */
.pill.ok{ background: var(--emerald-50); color: var(--emerald-700) }

/* ---- chips: a pill you can TAP ---------------------------------------
   A .pill reports status and is not touchable. A .chip is a CHOICE — what went
   wrong on a feedback form, the time slot a customer wants.

   This lived only in the staff panel and the admin panel, defined differently
   in each, and NOT here. The customer portal used the class and got no styling
   at all, so eight tappable options rendered as one run-on line of text —
   "Arrived lateDid not explain the problemProblem still not fixed" — and the
   booking slots did the same. A customer could not tap either one.

   Exactly the drift this file exists to end: a component defined in each page's
   own <style> ends up existing on some surfaces and not others.

   Sized for a thumb rather than a mouse. The selected state is blue-50 with a
   blue border, NOT the gradient — the gradient is rationed to primary actions. */
.chips{ display: flex; flex-wrap: wrap; gap: 8px; margin-top: 11px }
.chip{
  display: inline-flex; align-items: center;
  min-height: 40px; padding: 8px 15px;
  border-radius: var(--r-pill);
  border: 1.5px solid var(--line);
  background: var(--card); color: var(--ink);
  font: 500 14.5px/1.2 var(--font);
  cursor: pointer; user-select: none; white-space: nowrap;
}
.chip:hover{ border-color: var(--blue) }
.chip:focus-visible{ outline: 2px solid var(--blue); outline-offset: 2px }
.chip.on{ background: var(--blue-50); border-color: var(--blue);
          color: var(--blue-700); font-weight: 650 }

/* ---- messages: the same pill logic at block size --------------------- */
.msg{
  padding: 12px 14px; border-radius: var(--r-sm);
  margin-bottom: 13px; font-size: 14.5px;
}
.msg.err { background: var(--red-50);     color: var(--red-700);     border: 1px solid var(--red-200) }
.msg.ok  { background: var(--emerald-50); color: var(--emerald-700); border: 1px solid var(--emerald-200) }
.msg.warn{ background: var(--amber-50);   color: var(--amber-700);   border: 1px solid var(--amber-200) }
.msg.info{ background: var(--blue-50);    color: var(--blue-700);    border: 1px solid var(--blue-200) }
/* Alias, 13 Aug 2026: `msg bad` is written in seven places across three shared
   scripts and two panels, and rendered as an unstyled grey box everywhere. The
   authors kept reaching for the pill's word — let the component answer to it. */
.msg.bad { background: var(--red-50);     color: var(--red-700);     border: 1px solid var(--red-200) }

/* ---- brand icon badge ------------------------------------------------ */
.badge-icon{
  width: 40px; height: 40px; border-radius: var(--r-sm);
  background: var(--grad); color: #fff;
  display: grid; place-items: center; flex: 0 0 auto;
}

/* ---- tabs: active tab takes the gradient ----------------------------- */
.tabs{ display: flex; gap: 7px; overflow-x: auto; margin-bottom: 14px; padding-bottom: 2px }
/* A row short enough to fit has no business scrolling — and a scrollbar under
   three tabs reads as "there is more here" when there is not. */
.tabs.fit{ flex-wrap: wrap; overflow-x: visible }

/* ---- the mark-up toolbar ---------------------------------------------
   Dark, so it reads as a tool over a picture rather than part of the page,
   and wrapping, because five tools plus four colours plus undo do not fit
   across a phone in one line. */
.fxm-bar{
  display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
  margin-top: 8px; padding: 7px 8px;
  background: var(--ink); border-radius: var(--r-pill);
}
.fxm-t{
  min-width: 38px; min-height: 38px; padding: 0 9px;
  border: 0; border-radius: var(--r-sm); cursor: pointer;
  background: rgba(255,255,255,.10); color: #fff;
  font: 600 15px/1 var(--font);
}
.fxm-t:hover{ background: rgba(255,255,255,.20) }
.fxm-t[aria-pressed="true"]{ background: var(--card); color: var(--ink) }
.fxm-c{
  width: 26px; height: 26px; padding: 0; cursor: pointer;
  border-radius: 50%; border: 2px solid transparent;
}
.fxm-c[aria-pressed="true"]{ border-color: #fff; box-shadow: 0 0 0 2px var(--ink) inset }
.fxm-sep{ width: 1px; height: 24px; background: rgba(255,255,255,.18); margin: 0 2px }

/* ---- segmented switch -------------------------------------------------
   One choice between two worlds, above the tabs that belong to each. Used
   where a person does two different jobs and the screen has to be one or
   the other: service work, or sales. */
.seg{
  display: flex; gap: 4px; padding: 4px; margin-bottom: 12px;
  background: var(--gray-50); border-radius: var(--r-pill);
}
.seg button{
  flex: 1 1 0; min-width: 0; min-height: 40px;
  border: 0; background: none; border-radius: var(--r-pill);
  font: 650 14px/1.2 var(--font); color: var(--muted); cursor: pointer;
  padding: 9px 12px; white-space: nowrap;
}
.seg button[aria-selected="true"]{ background: var(--grad); color: #fff; box-shadow: var(--shadow) }
.tab{
  flex: 0 0 auto;
  border: 1px solid var(--line); background: #fff; color: var(--muted-2);
  border-radius: var(--r-pill); padding: 9px 15px; min-height: 40px;
  font: 600 14px/1.2 var(--font); cursor: pointer; white-space: nowrap;
}
.tab[aria-selected="true"]{ background: var(--grad); border-color: transparent; color: #fff }
/* Same state, second spelling, 13 Aug 2026: the Admin Panel's "Take an enquiry"
   tabs are written class="tab on" and its active tab never highlighted, because
   only the aria spelling existed. Both now work — though aria-selected remains
   the preferred form, because it says the same thing to a screen reader. */
.tab.on{ background: var(--grad); border-color: transparent; color: #fff }

/* ---- tables (Admin Panel density, same card language) ---------------- */
.scroll{ overflow-x: auto }
table{ width: 100%; border-collapse: collapse; font-size: 14.5px }
th{
  text-align: left; padding: 10px 12px;
  font: 700 11.5px/1.4 var(--font); text-transform: uppercase; letter-spacing: .04em;
  color: var(--muted); border-bottom: 1px solid var(--line); white-space: nowrap;
}
td{ padding: 11px 12px; border-bottom: 1px solid var(--line); vertical-align: top }
tr:last-child td{ border-bottom: 0 }
td.num, th.num{ text-align: right; font-variant-numeric: tabular-nums }

code, .code, .serial{
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px; color: var(--muted);
}
code, .code{ background: var(--gray-50); padding: 2px 7px; border-radius: 7px; color: var(--gray-700) }

.row{ display: flex; gap: 11px; align-items: center; flex-wrap: wrap }
.grow{ flex: 1; min-width: 0 }
.hide{ display: none }

/* ---- .row2 and .acts, moved here from the staff panel ----------------
   Both were defined ONLY in staff-panel/index.html and used by the Admin
   Panel and by shared/portal-link.js, which the Admin Panel loads. The
   same fault as .chip, found by sweeping for the class rather than
   waiting for the next screenshot:

     .row2  the Admin Panel's "Add a machine" form lays ten fields out in
            five two-across rows. With no rule they stacked as ten
            full-width controls down a 1180px column — a form twice as
            tall as designed, while the identical form on the technician's
            phone looked right.
     .acts  a row of buttons, evenly sized with an 8px gap. Without it the
            pairs at "Send their portal link / Take it back" and "Register
            this machine / Cancel" sat shoulder to shoulder at their
            natural widths.

   Values are byte-identical to the staff-panel originals, which are now
   deleted, so no surface changes appearance. */
.row2{ display: flex; gap: 10px; flex-wrap: wrap }
.row2 > *{ flex: 1; min-width: 140px }

.acts{ display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px }
.acts .btn, .acts a.btn{
  flex: 1; min-width: 118px; text-align: center;
  text-decoration: none; line-height: 22px;
}

/* ---- list rows and totals -------------------------------------------
   Used by the Admin Panel and the Customer Portal. They were only ever
   defined in the portal, so the Admin Panel had been rendering them
   unstyled — the exact failure that having one shared file prevents. */
.line{ padding: 11px 0; border-top: 1px solid var(--line) }
.line:first-of-type{ border-top: 0 }
.kv{ display: flex; justify-content: space-between; gap: 12px; font-size: 14.5px; padding: 3px 0 }
.kv span:first-child{ color: var(--muted) }
.total{
  font-weight: 700; font-size: 17px;
  border-top: 2px solid var(--line); margin-top: 9px; padding-top: 9px;
  font-variant-numeric: tabular-nums;
}

/* ---- phone numbers: always dialable ----------------------------------
   Rendered by febosPhone() in shared/config.js — the number is a tel: link,
   with a small WhatsApp action beside it where appropriate. Underline-free
   because a phone number reads as data, but it keeps the link colour so a
   thumb knows it will do something. */
.tel{ white-space: nowrap }
.tel a{ text-decoration: none; font-variant-numeric: tabular-nums }
.tel .tel-wa{
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; margin-left: 6px;
  border-radius: 50%; background: var(--emerald-50); color: var(--emerald-700);
  font-size: 14px; vertical-align: middle;
}

/* ==========================================================================
   THE ENTRY SURFACE — dark glass, 13 Aug 2026
   ==========================================================================
   Extracted verbatim from the sign-in screens of the approved reference apps
   (staff.filterxpert.in / filterxpert.in). Their design has TWO moods and the
   original extraction only captured one: the light interior above. The entry —
   sign-in, the launcher, the link interstitial — is a dark slate-950 room with
   two soft glow orbs and a translucent glass card. Everything inside the app
   stays light; the dark vocabulary is for the doorway only (and the Admin
   Panel's side rail, which is a doorway you keep beside you).

   The exact reference values, for whoever tunes this later:
     page      #020617 (slate-950)
     orbs      blue-500 at 25% and cyan-400 at 20%, blur ~64px, huge, decorative
     card      slate-900 at 70% + backdrop-blur, border white/10, radius 24,
               shadow-2xl, padding 32
     inputs    zinc-900/80 fill, zinc-700 border, zinc-100 text,
               labels 12px uppercase tracked zinc-400
     badge     blue-400/10 fill, blue-300/30 border, blue-200 text, small dot
     title     36px extrabold zinc-50; body zinc-300
   ========================================================================== */
.entry{
  position: relative; min-height: 100vh; min-height: 100dvh;
  display: flex; justify-content: center;
  overflow: clip; background: #020617;
  padding: 40px 16px;
  /* No align-items:center and no overflow:hidden here: flex-centering a card
     taller than a short viewport (landscape phone, keyboard up) pushed its top
     above the screen with no way to scroll it back — the Sign in button was
     simply unreachable. The card centres itself with margin:auto instead,
     which degrades to scrollable top-alignment when space runs out; clip
     keeps the glow orbs cropped without making this a scroll trap. */
}
.entry-orb{
  position: absolute; border-radius: 50%; filter: blur(64px);
  pointer-events: none;
}
.entry-orb.a{ width: 288px; height: 288px; left: -80px; top: 0;
              background: rgba(59,130,246,.25) }   /* blue-500/25 */
.entry-orb.b{ width: 384px; height: 384px; right: 0; top: 96px;
              background: rgba(34,211,238,.20) }   /* cyan-400/20 */

.entry-card{
  position: relative; width: 100%; max-width: 420px;
  margin: auto;                     /* centres while space allows, scrolls when it doesn't */
  border-radius: var(--r-lg);
  border: 1px solid rgba(255,255,255,.10);
  background: rgba(15,23,42,.70);                  /* slate-900/70 */
  -webkit-backdrop-filter: blur(24px); backdrop-filter: blur(24px);
  box-shadow: 0 25px 50px -12px rgba(0,0,0,.25);   /* shadow-2xl */
  padding: 32px;
  color: #d4d4d8;                                  /* zinc-300 */
}
.entry-card h1{ margin: 0; font-size: 34px; font-weight: 800;
                letter-spacing: -.02em; color: #fafafa }  /* zinc-50 */
.entry-card .sub{ margin: 8px 0 0; font-size: 14.5px; line-height: 1.6; color: #d4d4d8 }
.entry-card label{ color: #a1a1aa; font-size: 12px;        /* zinc-400 */
                   text-transform: uppercase; letter-spacing: .05em }
.entry-card input, .entry-card select{
  background: rgba(24,24,27,.80);                  /* zinc-900/80 */
  border: 1px solid #3f3f46;                       /* zinc-700 */
  color: #f4f4f5;                                  /* zinc-100 */
}
.entry-card input::placeholder{ color: #71717a }   /* zinc-500 */
.entry-card input:focus, .entry-card select:focus{
  border-color: var(--blue);
  box-shadow: 0 0 0 2px rgba(59,130,246,.55);      /* focus:ring-blue-500 */
}
.entry-card a{ color: #93c5fd }                    /* blue-300 */
.entry-card .muted, .entry-card .note{ color: #a1a1aa }
/* a quiet glass row inside the card — the launcher's app tiles use this */
.entry-row{
  display: block; border-radius: var(--r-sm);
  border: 1px solid rgba(255,255,255,.10);
  background: rgba(255,255,255,.04);
  padding: 14px 16px; margin-top: 10px;
  color: inherit; text-decoration: none;
}
.entry-row:hover{ background: rgba(255,255,255,.07) }
.entry-row h2{ margin: 0 0 2px; font-size: 16px; color: #f4f4f5 }
.entry-row p{ margin: 0; font-size: 13.5px; color: #a1a1aa; line-height: 1.5 }

/* ---- the dotted badge pill ------------------------------------------
   The reference floats one of these above its sign-in title: a rounded
   pill with a tiny dot, naming the brand. Light surfaces get a blue-50
   version; the entry uses the translucent one. */
.badge{
  display: inline-flex; align-items: center; gap: 8px;
  border-radius: var(--r-pill); padding: 5px 13px;
  font: 600 12px/1.4 var(--font); letter-spacing: .04em;
  background: var(--blue-50); color: var(--blue-700);
  border: 1px solid var(--blue-200);
}
.badge::before{
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: currentColor; opacity: .8;
}
.entry-card .badge, .badge.glass{
  background: rgba(96,165,250,.10);                /* blue-400/10 */
  border-color: rgba(147,197,253,.30);             /* blue-300/30 */
  color: var(--blue-200);
}

/* ---- hero numbers ----------------------------------------------------
   Money and counts the eye should land on first. The reference sets these
   extrabold-to-black; the imported weights above make that real rather
   than faux-bold. Tabular, so a list of figures lines up. */
.hero-num{
  font-weight: 800; font-size: 30px; line-height: 1.15;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
}
.hero-num.xl{ font-size: 36px; font-weight: 900 }

/* ---- the glass app bar -----------------------------------------------
   The interior header: sticky, white at 85% with a blur, hairline under it.
   The reference has no bottom navigation (its CSS carries no grid-cols-4/5
   and no pb-20 body padding) — the sticky glass top bar IS the chrome.
   Surfaces that keep a gradient header for brand reasons (the customer
   portal) simply don't use this. */
.appbar{
  position: sticky; top: 0; z-index: 40;
  background: rgba(255,255,255,.85);
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line);
  padding: 12px 16px;
}
.appbar .bar{ display: flex; align-items: center; gap: 12px; max-width: 760px; margin: 0 auto }
.appbar h1{ margin: 0; font-size: 17px; font-weight: 700; color: var(--ink); flex: 1 }

/* ---- hero card escalation --------------------------------------------
   Most cards stay quiet (shadow-sm). The ONE card a screen is about — the
   estimate awaiting a decision, the job being worked — may lift. Two cards
   lifting on one screen means neither is the hero, so use sparingly. */
.card.lift{
  border-radius: var(--r-lg);
  box-shadow: 0 20px 25px -5px rgba(0,0,0,.10), 0 8px 10px -6px rgba(0,0,0,.10);
}

/* ---- the toast ---------------------------------------------------------
   Every surface confirmed a save by writing a line into a box near the top
   of its page. On a phone, by the time you have scrolled to the payment
   step or the bottom of a form, that box is a screen and a half away — so
   the app was answering a question nobody could hear. A toast is fixed to
   the corner, so the answer arrives where the eyes already are.

   Fixed to the viewport, above everything (the sticky app bar is z-index
   20, dropdowns 30). Wide enough to read, never wider than the screen, and
   it steps down out of the way of the bar on a phone. */
.toasts{
  position: fixed; z-index: 60;
  top: calc(env(safe-area-inset-top, 0px) + 68px);
  right: 12px; left: 12px;
  display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
  pointer-events: none;                  /* the page underneath stays usable */
}
@media (min-width: 560px){ .toasts{ left: auto; max-width: 420px } }

.toast{
  pointer-events: auto;                  /* but a toast itself can be tapped away */
  max-width: 100%;
  border-radius: var(--r);
  padding: 12px 14px;
  font: 500 14.5px/1.45 var(--font);
  background: var(--card); color: var(--ink);
  border: 1px solid var(--line);
  box-shadow: 0 12px 28px rgba(2,6,23,.18);
  display: flex; gap: 9px; align-items: flex-start;
  animation: toast-in .18s ease-out;
}
.toast .tick{ flex: 0 0 auto; font-size: 15px; line-height: 1.3 }
/* The kinds carry the same meanings as .msg, so a colour never changes
   meaning between an inline message and its toast. */
.toast.ok  { background: var(--emerald-50); color: var(--emerald-700); border-color: var(--emerald-200) }
.toast.warn{ background: var(--amber-50);   color: var(--amber-700);   border-color: var(--amber-200) }
.toast.err { background: var(--red-50);     color: var(--red-700);     border-color: var(--red-200) }
.toast.info{ background: var(--blue-50);    color: var(--blue-700);    border-color: var(--blue-200) }

/* Fading, not vanishing: a message that disappears between blinks reads as
   a glitch. */
.toast.going{ animation: toast-out .45s ease-in forwards }

@keyframes toast-in  { from { opacity: 0; transform: translateY(-8px) } to { opacity: 1; transform: none } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(-6px) } }

@media (prefers-reduced-motion: reduce){
  .toast, .toast.going{ animation: none }
  .toast.going{ opacity: 0; transition: opacity .3s linear }
}

/* ---- cover: what a machine is entitled to ----------------------------
   Warranty and AMC used to be printed as bare dates in a meta line, so a
   warranty that ended in 2019 read exactly like one ending next year. A
   date on its own is not an answer to "is this free?" — the answer is the
   state (covered / ending / ended) and how long is left, which is what
   this block shows and what the countdown pill carries.

   The tint IS the state: emerald covered, amber ending soon, plain grey
   when cover has ended — so it can be read at arm's length. */
.cover-box{
  margin-top: 10px; padding: 11px 13px;
  border-radius: var(--r-sm);
  background: var(--gray-50); border: 1px solid var(--line);
}
.cover-box.ok  { background: var(--emerald-50); border-color: transparent }
.cover-box.soon{ background: var(--amber-50);   border-color: transparent }
.cover-box .when{
  font-size: 19px; font-weight: 800; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
/* the countdown: a solid pill, so it reads as a number you act on rather
   than another piece of status metadata */
.pill.solid{ color: #fff }
.pill.solid.good{ background: var(--green) }
.pill.solid.warn{ background: var(--amber-solid) }
.pill.solid.bad { background: var(--red) }
.pill.solid.neutral{ background: var(--gray-700) }

/* ---- the catalogue picture and the rack -------------------------------
   Animesh: "implement the image field throughout the software for spare parts
   as well as product... and the item code with rack location — where the
   actual product or spare parts are kept."

   A part on a screen is a person trying to recognise a thing and then walk to
   it. The thumbnail answers the first half and the rack chip the second, so
   they are one pair of styles used by every list that shows an item. */
.thumb{
  width: 46px; height: 46px; object-fit: cover; border-radius: var(--r-sm);
  border: 1px solid var(--line); background: var(--gray-50); cursor: pointer;
  display: block;
}
.thumb.lg{ width: 62px; height: 62px }
.thumb.sm{ width: 34px; height: 34px }
/* the empty state has to hold the same space, or a list with some pictures
   and some without reflows into a staircase */
.thumb.none{
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 17px; cursor: default;
}
.rackbox{ max-width: 118px; font-family: var(--mono, ui-monospace, monospace); font-size: 13px }
/* where it lives, said the way a shelf label is said */
.rack{
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--blue-50); color: var(--blue-700);
  border-radius: var(--r-pill); padding: 1px 9px;
  font: 600 12px/19px var(--font); letter-spacing: .02em; white-space: nowrap;
}
.rack.none{ background: var(--gray-50); color: var(--muted); font-weight: 400 }

/* ---- a row of fields that line up -------------------------------------
   Animesh, 20 Aug 2026, on two panels where a date box sat higher than the
   text box beside it: "two box are not parallel, make this parallel."

   The cause was the weekday hint. It is inserted as a sibling UNDER a date
   input, so that column grew taller than its neighbours, and a flex row
   aligned to the bottom pushed the date box up. Fixing it inline would have
   fixed those two panels and none of the next ten, so it is a utility: every
   field is a column, labels sit on one line, and the CONTROLS line up
   regardless of what hangs below any of them. */
.formrow{ display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-start }
.formrow .f{ display: flex; flex-direction: column; flex: 1 1 190px; min-width: 0 }
.formrow .f.grow{ flex: 3 1 260px }
.formrow .f.fit{ flex: 0 0 auto }
.formrow .f > label{
  margin: 0 0 6px; min-height: 19px; line-height: 19px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.formrow .f > input, .formrow .f > select, .formrow .f > .btnpair{ margin: 0 }
/* The buttons ride the same line as the inputs, not the labels. The spacer is
   an empty pseudo-label rather than a padding, so it stays exactly the height
   of a real label if that ever changes. NOT called .acts — that class already
   exists on this surface with a margin of its own, and borrowing the name put
   the buttons 14px below the inputs. */
.formrow .f.btns{ align-self: flex-start }
.formrow .f.btns::before{ content:''; display:block; min-height:19px; margin-bottom:6px }
.formrow .f.btns .btnpair{ display: flex; gap: 8px; margin: 0 }
.formrow .f.btns .btn{ margin: 0 }
/* the weekday hint hangs below its own input without moving anything else */
.formrow .febos-dayhint{ margin: 5px 0 0 }

/* ---- time slots -------------------------------------------------------
   Animesh, 20 Aug 2026: "leave it for the customer to select the preferred
   date and time according to his availability… whatever the date may be, the
   slots which are booked previously will be faded away, inactive."

   So a slot that is taken is SHOWN and disabled, never removed. A missing
   button tells the customer nothing; a faded one tells them the shape of the
   day, which is what they need to choose a different time on it. */
.slots{
  display: grid; gap: 9px; margin-top: 11px;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
}
.slot{
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 11px 10px; min-height: 54px;
  border: 1px solid var(--blue-200); border-radius: var(--r-sm);
  background: var(--card); color: var(--blue);
  font: 600 14.5px/1.2 var(--font); cursor: pointer;
  transition: background .15s, border-color .15s, transform .1s;
}
.slot:hover{ background: var(--blue-50) }
.slot:active{ transform: scale(.985) }
.slot .cap{ font: 400 11.5px/1.3 var(--font); color: var(--muted) }
.slot.on{ background: var(--grad); border-color: transparent; color: #fff }
.slot.on .cap{ color: rgba(255,255,255,.85) }
.slot[disabled]{
  background: var(--gray-50); border-color: var(--line); color: var(--muted);
  cursor: not-allowed; opacity: .6;
}
.slot[disabled] .time{ text-decoration: line-through }

/* ---- the machine card -------------------------------------------------
   Animesh, 20 Aug 2026, pointing at a booking page that described a machine
   as one grey line of text: "the UI is not attractive at all. I need this
   kind of product template throughout the software."

   A machine is the thing every screen in FEBOS is ultimately about — the
   customer's, the technician's, the office's. It deserves one card, built
   once here, that says which unit it is, what it is entitled to, and (where
   a machine is being CHOSEN) that it is the one selected.

   The gradient strip is the signature and stays rationed: it appears only on
   the selected state, which is exactly the "selected/active" use the design
   system reserves it for. An unselected card is plain white with a thin
   border, like every other card on the surface. */
.mcard{
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow);
  overflow: hidden; margin-bottom: 11px;
}
.mcard.tap{ cursor: pointer; transition: border-color .15s, box-shadow .15s, transform .15s }
.mcard.tap:hover{ border-color: var(--blue-200) }
.mcard.tap:active{ transform: scale(.995) }
.mcard.sel{ border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-200), var(--shadow) }

.mcard-head{
  background: var(--grad); color: #fff;
  font: 700 10.5px/1 var(--font); letter-spacing: .14em; text-transform: uppercase;
  padding: 9px 15px; display: flex; align-items: center; gap: 7px;
}
.mcard-body{ padding: 15px }
.mcard-name{
  font-size: 17px; font-weight: 700; letter-spacing: -.01em; color: var(--ink);
  display: flex; align-items: center; gap: 8px; min-width: 0;
}
/* the tick that marks the chosen unit — a filled brand circle, not a colour
   change on the text, so it reads at a glance on a phone in daylight */
.mcard-tick{
  width: 21px; height: 21px; border-radius: 50%; flex: 0 0 21px;
  background: var(--grad); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700;
}
.mcard-where{ color: var(--muted); font-size: 13.5px; margin-top: 4px }
/* Where on the premises the machine sits - "3rd floor pantry". Deliberately
   LOUDER than the street address above it: on a campus the address is the
   same for every machine and this is the only line that tells them apart,
   for the customer choosing one and the technician finding it. */
.mcard-spot{
  display: inline-flex; align-items: center; gap: 5px; margin-top: 6px;
  background: var(--blue-50); color: var(--blue-700);
  border-radius: var(--r-pill); padding: 4px 11px;
  font: 600 13px/1.25 var(--font);
}
.mcard-photo{
  width: 62px; height: 62px; flex: 0 0 62px; object-fit: cover;
  border-radius: var(--r-sm); border: 1px solid var(--line); background: var(--gray-50);
}
.mcard-note{
  margin-top: 11px; padding-top: 10px; border-top: 1px solid var(--line);
  color: var(--blue-700); font-size: 13px; font-weight: 600;
}
.mcard-note.plain{ color: var(--muted); font-weight: 400 }

/* ---- the dark overview panel -----------------------------------------
   The reference opens a screen you SET UP or CHECK (rather than work
   through) with a deep navy panel: a small cyan kicker, a big white title,
   one plain-language paragraph, and the counts embedded inside it as
   translucent tiles. The Admin Panel had grown its own copy of this; it
   belongs here, where every surface can use it.

   Why the counts sit INSIDE the dark panel: they are the summary of the
   thing the panel is introducing. Put them on white below and they read as
   a separate section that has to be related back by the reader. */
.hero{
  background: #0F1B33; border-radius: var(--r-lg);
  padding: 23px; margin-bottom: 13px;
}
.hero .kick{
  font: 700 10px/1.4 var(--font); letter-spacing: .13em;
  text-transform: uppercase; color: #67E8F9;      /* cyan-300 */
}
.hero h1{ color: #fff; font-size: 25px; margin: 7px 0 6px;
          font-weight: 800; letter-spacing: -.02em }
.hero p{ margin: 0; color: #CBD5E1; font-size: 14px; line-height: 1.6; max-width: 72ch }
.hero p strong{ color: #fff }
.hero-kpis{
  display: grid; gap: 10px; margin-top: 16px;
  /* Pairs, always - the owner's call. auto-fit collapsed to one tall column
     on a phone, and four stacked tiles pushed the actual list off screen. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero-kpi{
  background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.10);
  border-radius: var(--r-sm); padding: 12px 13px;
}
.hero-kpi .k{ font: 700 9.5px/1.4 var(--font); letter-spacing: .09em;
              text-transform: uppercase; color: #94A3B8 }
.hero-kpi .v{ font-size: 26px; font-weight: 800; color: #fff;
              letter-spacing: -.02em; font-variant-numeric: tabular-nums; margin-top: 2px }
.hero-kpi .s{ font-size: 11.5px; color: #94A3B8 }
/* the one tile that is a warning wears it */
.hero-kpi.warn{ background: rgba(245,158,11,.12); border-color: rgba(245,158,11,.45) }
.hero-kpi.warn .v{ color: #FCD34D }
.hero-kpi.warn .k{ color: #FCD34D }

/* ---- numbered steps ---------------------------------------------------
   "How it works" — three short steps with a coloured circle each. Used
   where a screen teaches a sequence rather than showing data. */
.steps{ display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)) }
.step{ display: flex; gap: 10px; align-items: flex-start }
.step .n{
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; color: #fff;
  font: 700 13px/1 var(--font);
}
.step .n1{ background: #7C3AED }   /* violet-600 */
.step .n2{ background: var(--blue) }
.step .n3{ background: var(--green) }
.step strong{ font-size: 14.5px }
.step .muted{ font-size: 12.5px; line-height: 1.5 }

/* ---- two related columns ---------------------------------------------
   A pair of cards that are one flow: left is where things come from,
   right is where they end up. Stacks on a phone, side by side when there
   is room — the order top-to-bottom is the order of the flow. */
.twoup{ display: grid; gap: 12px; grid-template-columns: 1fr }
@media (min-width: 760px){ .twoup{ grid-template-columns: 1fr 1fr } }
.colhead{
  display: flex; gap: 11px; align-items: center;
  padding: 14px 16px; color: #fff;
}
.colhead .n{
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: var(--r-sm);
  background: rgba(255,255,255,.22); display: grid; place-items: center;
  font: 700 13px/1 var(--font);
}
.colhead strong{ font-size: 15.5px }
.colhead .sub{ font-size: 12.5px; color: rgba(255,255,255,.85); margin-top: 1px }
.colhead.a{ background: linear-gradient(90deg, #6D28D9 0%, var(--blue) 100%) }
.colhead.b{ background: var(--grad) }

/* ---- section labels --------------------------------------------------
   The reference introduces groups with a small tracked uppercase label,
   not a heading: "TODAY", "EARLIER", "YOUR MACHINES". */
.label{
  font: 700 12px/1.4 var(--font);
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted);
  margin: 18px 0 8px;
}

/* ---- list rows --------------------------------------------------------
   The row every parts list, estimate list and reminder list is made of.
   Promoted from the staff panel's own <style> on 13 Aug 2026 — it was the
   closest thing FEBOS had to a real list-row component and it lived where
   only one surface could use it. */
.part{
  display: flex; gap: 10px; align-items: center;
  padding: 9px 0; border-bottom: 1px solid var(--line);
  font-size: 14.5px;
}
.part:last-child{ border-bottom: 0 }
.ok-tick{ color: var(--green); font-weight: 700 }

/* ---- accordions -------------------------------------------------------
   A card that opens. This exact header — flex, title left, +/− chevron
   right — had been copy-pasted SEVEN times across the staff panel,
   config.js and feedback.js, each a slightly different inline-styled twin.
   The chevron flips from the aria state, so the toggling code only has to
   tell the truth about whether the panel is open. */
.acc{
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-top: 20px; cursor: pointer;
}
.acc .acc-chev{ font-size: 20px; line-height: 1; color: var(--muted) }
.acc .acc-chev::before{ content: "+" }
.acc[aria-expanded="true"] .acc-chev::before{ content: "−" }

@media (prefers-reduced-motion: reduce){
  *{ transition: none !important; animation: none !important }
}

/* ---- the build brief ---------------------------------------------------
   The handover text, shown the way a handover reads: fixed width, dark, and
   selectable in one go. It is deliberately NOT styled like the rest of the
   page - it is not FEBOS talking to a person, it is FEBOS writing an
   instruction for whoever builds the thing, and it should look like it. */
.brief{
  margin: 9px 0 0; padding: 13px 15px; border-radius: var(--r-sm);
  background: #0F1B33; color: #E2E8F0; border: 1px solid #1E293B;
  font: 400 12.5px/1.65 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap; word-break: break-word; overflow-x: auto;
  max-height: 340px; overflow-y: auto;
}

/* ---- date and time, as one answer ------------------------------------
   Animesh: "make it 12 hours type with am and pm option, and if possible
   merge the date and time in a single sort."

   A native <input type="time"> reads 24-hour or 12-hour depending on the
   machine's locale, which is why the same field showed 16:30 on his desktop
   and 4:30 pm on a phone. Three selects read the same everywhere, and sit on
   one line with the date so the whole answer is one row. */
.whenrow{
  display: flex; gap: 7px; align-items: center; flex-wrap: wrap;
}
.whenrow input[type="date"]{ flex: 1 1 150px; min-width: 140px }
.whenrow select{ flex: 0 0 auto; width: auto; min-width: 68px; padding-right: 30px }
.whenrow .whencolon{ color: var(--muted); font-weight: 700; margin: 0 -3px }
.whenrow select:disabled{ opacity: .45 }
/* the day-name hint the global watcher adds sits under the whole row, not
   between the date box and the hour */
.whenrow + .febos-dayhint{ display: none }

/* ---- foldable master-data lists (Admin > Master data) --------------------- */
/* A long tab folds each card to its heading with a search bar on top. The    */
/* head is the card's own h2/h3 and summary, moved - so closed rows show      */
/* exactly what the open card would, pills included.                          */
.fold-bar{ position: sticky; top: 0; z-index: 5; background: var(--bg);
  display: flex; align-items: center; gap: 14px; padding: 4px 2px 12px }
.fold-bar input{ flex: 1 }
.fold-head{ display: flex; align-items: center; gap: 14px; cursor: pointer;
  user-select: none }
.fold-head h2, .fold-head h3{ margin: 0; font-size: 17px; flex: none }
.fold-head .muted{ margin: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: 13px }
.fold-chev{ margin-left: auto; width: 30px; height: 30px; border-radius: 999px;
  background: var(--gray-50); color: var(--gray-700); display: grid;
  place-items: center; flex: none; transition: transform .22s, background .22s }
.fold:not(.shut) .fold-chev{ transform: rotate(180deg);
  background: var(--blue); color: #fff }
.fold.shut{ padding-top: 14px; padding-bottom: 14px }
.fold.shut > :not(.fold-head){ display: none }
.fold:not(.shut) .fold-head{ margin-bottom: 14px }

/* =========================================================================
   THE PHONE
   =========================================================================
   Until now this file had no max-width rule at all: four @media rules, two of
   them prefers-reduced-motion, and the other two min-width. The field apps
   never needed one because they are mobile-first by construction. The admin
   console is the opposite - it was built for a desk and then explicitly made
   denser than the field apps ("a desk mouse does not need a 48px target").
   This block undoes that on a narrow screen and nowhere else.

   820px, not 768: a tablet in portrait is 768 and the three-pane inbox is
   still usable there, so the break has to sit above it.
   ========================================================================= */
@media (max-width: 820px) {

  /* ---- thumbs, not mice -------------------------------------------------
     44px is the floor Apple and Google both publish, and it is the number
     the admin panel's own comment was written to avoid. */
  .btn            { min-height: 44px; padding: 11px 16px; font-size: 14px }
  .btn.sm         { min-height: 44px; padding: 10px 14px }
  .btn.xs         { min-height: 38px; padding: 8px 12px }
  .chip, .tab     { min-height: 40px; display: inline-flex; align-items: center }
  .navitem, .navhead { min-height: 48px }
  input, select, textarea {
    /* 16px exactly: anything smaller and iOS zooms the page on focus, which
       is the other half of "the panel is unusable on a phone". */
    font-size: 16px; min-height: 44px;
  }
  input[type="checkbox"], input[type="radio"] { min-height: 0; width: 20px; height: 20px }

  /* The WhatsApp circle beside every phone number: 26px, six pixels from the
     dial link, and it LEAVES THE APP. It was the easiest thing on the screen
     to hit by accident and the most expensive one to hit. */
  .tel .tel-wa { width: 44px; height: 44px; margin-left: 10px }

  /* ---- one column -------------------------------------------------------- */
  .row2, .twoup, .formrow { grid-template-columns: 1fr !important; display: grid; gap: 10px }
  .row  { flex-wrap: wrap }
  .hero { padding: 16px }
  .hero h1 { font-size: 21px }
  .card { padding: 14px; border-radius: var(--r) }

  /* A table on a phone is a horizontal scroll or it is nothing. It gets its
     own scroller so the PAGE never scrolls sideways. */
  .scroll, .tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch }
  table { min-width: 520px }

  /* ---- a sheet, not a floating box --------------------------------------
     There is not one modal in the whole of FEBOS today, so this is net-new
     rather than a replacement. On a phone a centred floating panel is always
     wrong: it is either too small to use or too big to fit. */
  .febos-sheet {
    position: fixed; inset: 0; z-index: 200;
    background: var(--card); display: flex; flex-direction: column;
    padding-bottom: env(safe-area-inset-bottom);
  }
  .febos-sheet-head {
    display: flex; align-items: center; gap: 10px;
    padding: 12px 14px; border-bottom: 1px solid var(--line); flex: 0 0 auto;
  }
  .febos-sheet-head .t { font-weight: 700; flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
  .febos-sheet-body { flex: 1 1 auto; overflow: auto; padding: 14px }
  .febos-sheet-body img,
  .febos-sheet-body video { max-width: 100%; height: auto; border-radius: var(--r) }
  .febos-sheet-body iframe { width: 100%; height: 100%; border: 0; min-height: 70dvh }
}

/* The sheet exists at every width - it is only the phone that gets it by
   default. On a desk it stays a centred panel, because there it can. */
@media (min-width: 821px) {
  .febos-sheet { position: fixed; inset: 5dvh 50% auto auto;
    transform: translateX(50%); width: min(760px, 90vw); max-height: 90dvh;
    z-index: 200; background: var(--card); border: 1px solid var(--line);
    border-radius: var(--r-lg); box-shadow: var(--shadow);
    display: flex; flex-direction: column }
  .febos-sheet-head { display: flex; align-items: center; gap: 10px;
    padding: 12px 16px; border-bottom: 1px solid var(--line) }
  .febos-sheet-head .t { font-weight: 700; flex: 1 1 auto }
  .febos-sheet-body { overflow: auto; padding: 16px }
  .febos-sheet-body img { max-width: 100%; height: auto; border-radius: var(--r) }
  .febos-sheet-body iframe { width: 100%; height: 70dvh; border: 0 }
}
.febos-scrim { position: fixed; inset: 0; z-index: 199; background: rgba(15,27,51,.55) }

/* "This screen works best on a computer." Not an error - a redirection. */
.febos-deskonly { text-align: center; padding: 34px 18px }
.febos-deskonly .ic { width: 56px; height: 56px; border-radius: var(--r-lg);
  background: var(--grad); margin: 0 auto 14px; display: flex;
  align-items: center; justify-content: center; font-size: 26px; color: #fff }
.febos-deskonly h3 { margin: 0 0 6px; font-size: 17px }
.febos-deskonly p  { margin: 0 auto 16px; color: var(--muted); max-width: 40ch;
  font-size: 14px; line-height: 1.6 }

/* ---- the call screen (shared/followup.js, 18 Sep 2026) ----------------
   One screen, one job: ring the customer, say what happened, set the next
   call. A full-height sheet on a phone, a centred column on a desk. The
   "when next" chips are a fixed grid — eight boxes that never move, greyed
   when unavailable rather than removed, so nothing shifts under a thumb. */
body.callscr-open{ overflow: hidden }
.callscr{
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(17,24,39,.45);
  display: flex; justify-content: center; align-items: stretch;
}
.callscr-box{
  background: var(--bg); width: 100%; max-width: 620px;
  display: flex; flex-direction: column; min-height: 0;
}
@media (min-width: 700px){
  .callscr{ padding: 24px 16px; }
  .callscr-box{ border-radius: 16px; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,.25) }
}
.callscr-bar{
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 16px; background: var(--card); border-bottom: 1px solid var(--line);
}
.callscr-bar strong{ font-size: 16.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.callscr-body{
  flex: 1; overflow-y: auto; overflow-x: hidden; padding: 12px 16px 28px;
  -webkit-overflow-scrolling: touch;
}
.callscr-body .card{ margin-bottom: 12px }
.callscr-name{ font-size: 20px; font-weight: 700; color: var(--ink); overflow-wrap: anywhere }
.callscr-title{ margin-top: 6px; font-size: 14.5px; overflow-wrap: anywhere }
.callscr-dial{
  display: flex; justify-content: center; width: 100%; margin-top: 12px;
  font-size: 19px; min-height: 54px; text-decoration: none;
}
.callscr-facts{
  display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 8px; margin-top: 12px;
}
@media (min-width: 520px){ .callscr-facts{ grid-template-columns: repeat(4, minmax(0,1fr)) } }
.callscr-facts > div{
  background: var(--gray-50); border: 1px solid var(--line); border-radius: 10px;
  padding: 8px 10px; min-width: 0;
}
.callscr-facts span{ display: block; font-size: 12px; color: var(--muted) }
.callscr-facts b{ display: block; font-size: 14.5px; color: var(--ink); overflow-wrap: anywhere }
.callscr-facts b.callscr-late{ color: var(--red-700) }
.callscr-hist{ list-style: none; margin: 0; padding: 0 }
.callscr-hist li{
  display: flex; gap: 10px; padding: 7px 0; border-top: 1px solid var(--line);
  font-size: 14px; min-width: 0; overflow-wrap: anywhere;
}
.callscr-hist li:first-child{ border-top: 0 }
.callscr-hdate{ flex: 0 0 auto; color: var(--muted); min-width: 92px }

.callscr-fork{ display: grid; grid-template-columns: 1fr 1fr; gap: 10px }
.callscr-big{
  min-height: 64px; border-radius: 14px; cursor: pointer;
  border: 2px solid var(--line); background: var(--card); color: var(--ink);
  font: 700 16.5px/1.2 var(--font); padding: 10px;
}
.callscr-big#csNo[aria-pressed="true"]{ background: var(--amber-50); border-color: var(--amber-700); color: var(--amber-700) }
.callscr-big#csYes[aria-pressed="true"]{ background: var(--emerald-50); border-color: var(--emerald-700); color: var(--emerald-700) }
.callscr-big:focus-visible{ outline: 2px solid var(--blue); outline-offset: 2px }

.callscr-reasons{ display: grid; grid-template-columns: 1fr; gap: 6px }
@media (min-width: 520px){ .callscr-reasons{ grid-template-columns: 1fr 1fr } }
.callscr-reason{
  display: flex; align-items: center; gap: 10px; margin: 0;
  min-height: 46px; padding: 8px 12px; border-radius: 10px; cursor: pointer;
  border: 1.5px solid var(--line); background: var(--card);
  font: 500 15px/1.25 var(--font); color: var(--ink);
}
.callscr-reason input{ width: 20px; height: 20px; margin: 0; flex: 0 0 auto; accent-color: var(--blue) }
.callscr-reason:has(input:checked){ border-color: var(--blue); background: var(--blue-50); color: var(--blue-700); font-weight: 650 }

.callscr-chips{ display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 8px }
@media (min-width: 520px){ .callscr-chips{ grid-template-columns: repeat(4, minmax(0,1fr)) } }
.callscr-chip{
  display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 2px;
  height: 90px; padding: 7px 11px; border-radius: 12px; text-align: left; cursor: pointer;
  border: 1.5px solid var(--line); background: var(--card); color: var(--ink); min-width: 0;
  font-family: var(--font);
}
.callscr-chip[aria-pressed="true"]{ border-color: var(--blue); background: var(--blue-50); color: var(--blue-700) }
.callscr-chip[disabled]{ opacity: .45; cursor: default; background: var(--gray-50) }
.callscr-chip:focus-visible{ outline: 2px solid var(--blue); outline-offset: 2px }
.callscr-chip > span{ display: block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.callscr-chip-l{ font-weight: 650; font-size: 14.5px }
.callscr-chip-w{ font-size: 12.5px; color: var(--muted) }
.callscr-chip[aria-pressed="true"] .callscr-chip-w{ color: var(--blue-700) }
.callscr-chip-r{ font-size: 11.5px; color: var(--amber-700) }
.callscr-sugg{
  font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
  color: var(--emerald-700); margin-left: 2px;
}
.callscr-need{ color: var(--red-700); font-weight: 600 }
.callscr-save{ position: sticky; bottom: -28px; padding: 10px 0 28px; margin-bottom: -28px;
                background: var(--bg); border-top: 1px solid var(--line) }
.callscr-save .btn{ width: 100%; min-height: 54px; font-size: 17px }
/* the old outcome sheet marked its chips with aria-pressed and never had a
   pressed look; the same look as .chip.on, for both */
.chip[aria-pressed="true"]{ background: var(--blue-50); border-color: var(--blue);
                            color: var(--blue-700); font-weight: 650 }
.chip[disabled]{ opacity: .5; cursor: default }

/* ---- lesson bodies (.kb-body) ----------------------------------------
   The Academy course moved out of one 2.5 MB HTML page and into 39 editable
   lessons on 20 September 2026. The lesson HTML kept the course page's own
   class names — tables, flow diagrams, part illustrations, callouts — so the
   rules those names need live here rather than each lesson carrying inline
   styles the Technical Head would have to preserve by hand while editing.

   Scoped to .kb-body on purpose: a lesson is written by a person, in a
   textarea, and must never be able to restyle the app around it. */
.kb-body{ color: var(--ink); font-size: 15px; line-height: 1.65 }
.kb-body > :first-child{ margin-top: 0 }
.kb-body h4{ margin: 20px 0 6px; font-size: 13px; font-weight: 700;
             text-transform: uppercase; letter-spacing: .04em; color: var(--muted) }
.kb-body p{ margin: 0 0 12px }
.kb-body ul, .kb-body ol{ margin: 0 0 12px; padding-left: 20px }
.kb-body li{ margin-bottom: 7px }
.kb-body ul.plain, .kb-body ol.plain{ list-style-position: outside }
.kb-body b, .kb-body strong{ color: var(--ink); font-weight: 650 }
.kb-body .block-lede{ color: var(--muted); margin-bottom: 12px }
.kb-body .mono, .kb-body code{ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
                               font-size: .92em }

/* A card inside a lesson is a grouping, not a surface of its own: the lesson
   already sits on one. Border, no shadow, or the page looks quilted. */
.kb-body .card{ padding: 14px; border: 1px solid var(--line); border-radius: var(--r);
                box-shadow: none; margin: 0 0 12px; background: var(--bg) }
.kb-body .grid{ display: grid; gap: 12px; margin-bottom: 12px }
.kb-body .grid.cols-2{ grid-template-columns: repeat(2, minmax(0, 1fr)) }
.kb-body .grid.cols-3{ grid-template-columns: repeat(3, minmax(0, 1fr)) }

.kb-body .tablewrap{ overflow-x: auto; -webkit-overflow-scrolling: touch; margin-bottom: 14px }
.kb-body table{ min-width: 460px }
.kb-body td.k{ font-weight: 650; color: var(--ink); white-space: nowrap }

/* Diagrams. Each one is drawn at a fixed width and MUST NOT be squeezed to fit
   a phone — a flow diagram scaled to 300px is a decoration, not a diagram. It
   gets its own horizontal scroller, so the page never scrolls sideways. */
.kb-body .diagram-scroll{ overflow-x: auto; -webkit-overflow-scrolling: touch;
                          margin: 6px 0 4px; padding-bottom: 4px }
.kb-body .diagram-scroll svg{ width: 100%; max-width: none; height: auto; display: block }
.kb-body svg.diagram-membrane{ min-width: 420px }
.kb-body svg.diagram-flow{ min-width: 720px }
.kb-body .diagram-fig{ margin: 0 0 14px }
.kb-body .diagram-fig figcaption{ color: var(--muted); font-size: 13px; margin-top: 6px }

/* The dashes that crawl along a flow line are what makes a still diagram read
   as a direction of travel. Anyone who has asked their phone to stop animating
   gets the same diagram, standing still. */
.kb-body .flow-line{ stroke: currentColor; stroke-width: 2; fill: none;
                     stroke-dasharray: 6 6; animation: kbflow 1.2s linear infinite }
.kb-body .flow-line.reject{ stroke: var(--muted); animation-duration: 1.9s }
@keyframes kbflow{ to { stroke-dashoffset: -24 } }
@media (prefers-reduced-motion: reduce){
  .kb-body .flow-line{ animation: none }
}

/* M2's membrane diagram: small molecules cross, large ones are turned away and
   carried off in the reject stream. On the original course page .molecule was
   given a timing function and an iteration count but never a DURATION, so it
   never moved — the diagram has always been a still picture of seven dots.
   The duration is set here, which is what makes the point the caption claims. */
.kb-body .molecule{ animation-timing-function: linear; animation-iteration-count: infinite }
.kb-body .molecule.small{ fill: var(--blue);  animation-name: kbpermeate; animation-duration: 3.2s }
.kb-body .molecule.large{ fill: var(--amber); animation-name: kbreject;   animation-duration: 3.2s }
@keyframes kbpermeate{ from { transform: translateX(0) } to { transform: translateX(380px) } }
@keyframes kbreject{ 0% { transform: translate(0,0) } 55% { transform: translate(178px,0) }
                     100% { transform: translate(178px,58px) } }
@media (prefers-reduced-motion: reduce){
  .kb-body .molecule{ animation: none }
}

/* Links out to third-party servicing videos. They are other people's videos,
   so they look like what they are — a link — and never like a player. */
.kb-body .video-card{ display: block; padding: 11px 13px; margin-bottom: 8px;
  border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--card);
  text-decoration: none; color: var(--ink) }
.kb-body .video-card:hover{ border-color: var(--blue) }
.kb-body .vtitle{ display: block; font-weight: 600 }
.kb-body .vmeta{ display: block; color: var(--muted); font-size: 12px; margin-top: 2px }

@media (max-width: 720px){
  .kb-body .grid.cols-2, .kb-body .grid.cols-3{ grid-template-columns: 1fr }

  /* A five-column comparison table is the heart of several lessons and is
     unreadable on a phone as a table: the scroller works, but the technician
     sees one column of bold words and four columns of nothing until he drags
     sideways, and every row is as tall as the widest cell he cannot see.
     Each row becomes its own labelled block instead. A table whose cells
     carry no column name falls back to the sideways scroller. */
  .kb-body .tablewrap:has(td[data-label]){ overflow-x: visible }
  .kb-body table:has(td[data-label]){ min-width: 0 }
  .kb-body table:has(td[data-label]) thead{ position: absolute; width: 1px; height: 1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap }
  .kb-body table:has(td[data-label]) tr{ display: block; padding: 12px 0;
    border-bottom: 1px solid var(--line) }
  .kb-body table:has(td[data-label]) tr:last-child{ border-bottom: 0 }
  .kb-body table:has(td[data-label]) td{ display: block; padding: 0 0 8px; border: 0;
    white-space: normal }
  .kb-body table:has(td[data-label]) td:last-child{ padding-bottom: 0 }
  .kb-body table:has(td[data-label]) td::before{ content: attr(data-label);
    display: block; font: 700 11px/1.4 var(--font); text-transform: uppercase;
    letter-spacing: .04em; color: var(--muted); margin-bottom: 2px }
  /* The first cell is the thing the row is ABOUT — it gets no label and reads
     as the heading of its block. */
  .kb-body table:has(td[data-label]) td:first-child{ font-weight: 700; font-size: 15px }
  .kb-body table:has(td[data-label]) td:first-child::before{ content: none }
}
