/* fh-shell.css - one shell for every module page.
   Created September 1, 2026 (Lane 4, Phase 1).

   WHY THIS FILE EXISTS
   --------------------
   Twenty-two module pages each carried their own header, their own number of stat
   tiles, their own bottom bar and their own word for "the list of records". The
   August 31 UI review measured it: two different page headers, seven names for the
   same list, stat tiles in counts of none, two and three, and a primary action that
   moved around the bottom bar. A user learned the vocabulary once per module.

   This stylesheet is the visual half of the fix. fh-shell.js is the behavior half.
   The mock at _documents/ui-review-2026-08-31/mock.html is the target, and its CSS
   is where most of these rules came from.

   TWO RULES THIS FILE OBEYS
   -------------------------
   1. EVERY BRAND COLOR IS A TOKEN. Not one violet literal. --brand, --brand-bright,
      --brand-deep and --brand-rgb are set by brand-apply.js from the client's own
      primary, so the hero, the rings, the tabs, the chips, the buttons and the
      bottom nav recolor for a client with no further work. The fallbacks in the
      var() calls are F&H's own purple, used only when no client brand has loaded.
   2. THE STOPLIGHT IS FIXED ON EVERY CLIENT. Red means "require attention" and
      amber means "coming up" whatever color the client picked, because risk has to
      read the same everywhere. Those four are literals on purpose.

   NAMING. Every class here starts fhs-. The module pages already own .card, .chip,
   .empty, .stat, .tabs, .search and .btn, and an unprefixed rule in a stylesheet
   that loads after their inline <style> would silently restyle their innards. The
   prefix is what keeps this additive.

   SCOPE. Everything is scoped to body:has(#scroll), the same test fh-theme.css uses
   to mean "a module page". index.html and the Executive Dashboard carry header.bar
   too and are deliberately untouched. */

/* #cdView is here for one reason: inside the builder's console the client home is a
   PREVIEW, and its palette is set on that box rather than on the page, so the
   operator's own screens keep F&H violet. Custom properties inherit downward, so the
   tokens have to be re-declared at the box for the value set there to be the one the
   tiles inside it read. On a client's own phone the same box is their whole screen
   and this changes nothing. TD-19, September 4, 2026. */
body:has(#scroll), body.fhs-host, #cdView{
  --fhs-b:var(--brand,#5B2BD6);
  --fhs-bb:var(--brand-bright,#7138EE);
  --fhs-bd:var(--brand-deep,#3F1C9C);
  --fhs-brgb:var(--brand-rgb,91,43,214);
  /* THE INK ON A BRAND FILL. brand-apply.js works out which of near-black and
     near-white is more readable on this client's mid tone and publishes it as
     --brand-on; the same question for the accent is --accent-on. Every place a
     label or an icon sits on a brand-colored fill reads this instead of writing
     #fff, because #fff on a safety-yellow client is 1.9:1 and unreadable. The
     fallback is white, which is right for F&H's own violet. */
  --fhs-b-on:var(--brand-on,#fff);
  --fhs-a-on:var(--accent-on,#fff);
  /* THE COUNTER-SHADE ON A BRAND PANEL. brand-apply.js works out the smallest wash
     that puts --brand-on over 4.5:1 on the worst pixel a word can land on, and it is
     the opposite of the ink: black under white words, white under dark ones. The
     fallback is F&H violet's own answer, so a page that somehow loads without
     brand-apply.js gets the same panel it would have had with it. */
  --fhs-scrim:var(--brand-scrim,rgba(0,0,0,.26));
  /* Amber, readable on a dark panel. The stoplight amber above is tuned for ink on
     white; on a dark tile it is the same hue lifted until it clears the floor. */
  --fhs-amb-dk:#F3C47E;
  /* The same stoplight amber as an r,g,b triplet, so a translucent amber fill is
     written as the token rather than as three numbers that happen to match it. */
  --fhs-amb-rgb:245,183,0;
  --fhs-ink:var(--ink,#16181F);
  --fhs-ink2:var(--ink-2,#4A4C66);
  /* The tone this replaced was measured at 4.16:1 on the home's own background on
     September 4,
     2026, which is under the 4.5 body line. It carries the area counts and every
     tile's fact line, so it is the second most read tone on the screen. Darkened
     until it clears, and no further: it still has to read as the quiet one. */
  --fhs-ink3:var(--ink-3,#585F73);
  --fhs-line:var(--line,rgba(20,22,45,.09));
  --fhs-surface:var(--surface,#fff);
  /* fixed stoplight, every client */
  --fhs-red:#E12D3D;
  --fhs-amb:#F5B700;
  --fhs-amb-ink:#714B00;
  --fhs-grn:#00A85A;
  --fhs-shadow:0 10px 26px -18px rgba(var(--fhs-brgb),.55),0 1px 3px rgba(20,22,45,.06);
}

/* ============================================================================
   DARK MODE, AND THE BUG BRYANT REPORTED ON SEPTEMBER 4, 2026
   ============================================================================
   "Dark text on dark tiles in dark mode." Measured on that date, on a Basin Ridge
   home in a real browser: a tile's own name came out at 1.01:1. Not close, not
   marginal: the same color as the tile it was printed on.

   The cause is one missing half of a pair. The tokens above read the app's own
   --surface, which dashboard.css redefines per theme, and the app's own --ink, which
   dashboard.css DOES NOT DEFINE AT ALL. So the surface followed the theme into the
   dark and the ink stayed on the literal fallback written beside it, which is a
   near-black picked for white paper. Every fallback in that block is a light-mode
   answer, so every one of them is wrong in the dark, and each was wrong quietly.

   The fix is the pair completed rather than a color patched. This block gives the
   same four ink tones their dark-mode values, and the stoplight amber the lifted
   version it needs on a dark panel.

   DARK IS THE DEFAULT AND LIGHT IS THE OVERRIDE in this product: dashboard.css puts
   the dark values on :root and the light ones on :root[data-theme="light"]. So the
   selector here is :not([data-theme="light"]), which covers a page that has not been
   told either way as well as one explicitly set to dark. Writing it the other way
   round would leave the default state on the light-mode inks, which is the state a
   page is in before the theme script has run.

   tests/verify_td19_contrast_dom.js measures every word and every glyph on both home
   screens in both themes and holds the 4.5:1 and 3:1 floors. */
:root:not([data-theme="light"]) body:has(#scroll),
:root:not([data-theme="light"]) body.fhs-host,
:root:not([data-theme="light"]) #cdView{
  --fhs-ink:#EEF0F6;        /* titles, near-white */
  --fhs-ink2:#B6BED2;       /* area names and second-rank labels */
  --fhs-ink3:#A7AFC4;       /* fact lines, a readable gray-blue */
  --fhs-line:rgba(255,255,255,.09);
  /* Amber that means "late" has to keep meaning it on a dark tile, so it is the
     lifted version rather than the ink-on-white one. */
  --fhs-amb-ink:var(--fhs-amb-dk);
}

/* ============================================================================
   THE HEADER
   Back chevron, the area label over the module's noun, the client's logo.

   fh-theme.css already gives header.bar the brand gradient, the hairline weave
   and the client emblem watermark on every module page. That treatment is kept
   verbatim; what changes here is what the bar SAYS. It used to carry the client's
   company name in 48px serif, which is the right thing on a home screen and the
   wrong thing on a working page: a page you opened by tapping a word should say
   that word back to you.

   #barTitle stays in the DOM and stays the module's own element, because every
   module's setTitle() writes to it and none of that code is touched. fh-shell.js
   reads it and mirrors it into #fhsTitle. Hidden with visibility rather than
   display so the module's own layout math, which measures it, still works. */
body:has(#scroll) header.bar #barTitle{
  position:absolute;
  width:1px;
  height:1px;
  overflow:hidden;
  clip-path:inset(50%);
  white-space:nowrap;
}
body:has(#scroll) header.bar h1.fhs-title,
body:has(#scroll) header.bar .fhs-title{
  margin:0;
  position:relative;
  z-index:1;
  flex:1;
  min-width:0;
  color:#fff;
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-weight:800;
  font-size:17px;
  letter-spacing:-.01em;
  line-height:1.22;
  /* two lines at most, then it clips rather than pushing the logo off the bar */
  overflow:hidden;
}
body:has(#scroll) header.bar .fhs-title small{
  display:block;
  font-family:var(--mono,'IBM Plex Mono',ui-monospace,monospace);
  font-weight:500;
  font-size:9.5px;
  letter-spacing:.14em;
  text-transform:uppercase;
  color:rgba(255,255,255,.82);
  margin-bottom:2px;
  white-space:nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}
/* The chevron is always there now. On the module's own home it goes back to the
   dashboard; on a sub-screen the module's own #back handler is still what runs.
   fh-shell.js decides which; this only says what it looks like. */
body:has(#scroll) header.bar .fhs-back{
  position:relative;
  z-index:1;
  width:44px;
  height:44px;
  min-width:44px;
  min-height:44px;
  border-radius:12px;
  display:flex;
  align-items:center;
  justify-content:center;
  flex:0 0 auto;
  color:#fff;
  background:rgba(255,255,255,.14);
  border:1px solid rgba(255,255,255,.24);
}

/* THE MODULE HERO RETIRES. The header says the area and the noun, so a second
   headline block at the top of the page body says it again in different words,
   which is findings 3 and 4 of the review sitting one above the other.
   This rule hides it for the one frame between the module drawing it and
   fh-shell.js taking it out of the document, so it never flashes. Taking it out is
   what actually retires it: a block hidden with display:none keeps its h1 in the
   outline, and the page then has two h1s that disagree.
   Scoped to .fhs-shelled, which fh-shell.js puts on the body when it mounts, so a
   page whose script does not load renders exactly as it did before this file
   existed. The explanatory paragraph inside is not hidden, it is moved to About. */
body.fhs-shelled:has(#scroll) #scroll.fhs-home > .home-head{
  /* SCOPED TO THE MODULE'S OWN HOME, and to a DIRECT child of #scroll.
     Several modules reuse .home-head for a sub-screen heading: the title of the record
     you opened, or "Mark this action done". Those are the page saying what screen you
     are on, which is the opposite of the duplication this rule removes. */
  display:none;
}

/* ============================================================================
   THE TABS: Records, Reports, About. Three, in that order, on every module.

   "Records" is the word everywhere. The review counted seven names for the same
   list across the estate (REPORTS, ANALYSES, INSPECTIONS, CERTIFICATION BOARD,
   MEETINGS, OVERDUE, POLICIES), and one vocabulary is item I of the proposal. The
   mock's frame 4 draws Insurance with Policies / Requirements / About; that frame
   predates item I in the same document and Records wins, which is also what the
   Lane 4 work order asks for by name. */
.fhs-tabs{
  display:flex;
  gap:6px;
  margin:0 0 12px;
  background:color-mix(in srgb,var(--fhs-surface) 55%,transparent);
  border:1px solid var(--fhs-line);
  border-radius:12px;
  padding:4px;
}
.fhs-tab{
  flex:1;
  text-align:center;
  font-size:12.5px;
  font-weight:600;
  padding:0;
  /* 44, not the 38 the mock draws. Every control in this estate that a finger
     lands on is 44px, because the hand pressing it is wearing gloves in a truck,
     and audit_mobile.js measures it. The mock was drawn at desk scale. */
  min-height:44px;
  border-radius:9px;
  border:0;
  background:none;
  color:var(--fhs-ink2);
  cursor:pointer;
  font-family:inherit;
}
.fhs-tab[aria-selected="true"]{
  background:linear-gradient(135deg,var(--fhs-bb),var(--fhs-b));
  color:#fff;
  box-shadow:0 6px 14px -8px rgba(var(--fhs-brgb),.7);
}

/* ============================================================================
   THE STAT STRIP: exactly three, always in the same order.

   Red "Require attention", amber "Active" or "Coming up", neutral "Closed this
   year" or "Total". The review's finding 6 was that some modules showed two tiles
   with a third orphaned onto the next row and nothing told the eye which number
   was the one to worry about. Three in a fixed order fixes both: the leftmost is
   always the one that needs you.

   The numbers still come from each module's own engine. fh-shell.js asks the page
   for them; it does not compute them. */
.fhs-stats{
  display:grid;
  grid-template-columns:1fr 1fr 1fr;
  gap:8px;
  margin:0 0 12px;
}
.fhs-st{
  background:var(--fhs-surface);
  border:1px solid var(--fhs-line);
  border-radius:14px;
  padding:10px 11px 9px;
  box-shadow:var(--fhs-shadow);
  position:relative;
  overflow:hidden;
  min-width:0;
}
.fhs-st::before{
  content:'';
  position:absolute;
  left:0;
  top:0;
  bottom:0;
  width:4px;
  background:#C9C4DE;
}
.fhs-st .fhs-n{
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-size:22px;
  font-weight:800;
  line-height:1;
}
.fhs-st .fhs-l{
  font-size:10.5px;
  color:var(--fhs-ink3);
  margin-top:3px;
  line-height:1.25;
}
.fhs-st.fhs-attention::before{background:var(--fhs-red)}
.fhs-st.fhs-attention .fhs-n{color:var(--fhs-red)}
.fhs-st.fhs-active::before{background:var(--fhs-amb)}
.fhs-st.fhs-active .fhs-n{color:#C27A00}
.fhs-st.fhs-closed::before{background:var(--fhs-grn)}
.fhs-st.fhs-closed .fhs-n{color:var(--fhs-ink)}

/* ============================================================================
   SEARCH AND STATUS CHIPS, on every Records tab.

   Finding 9: filters existed only where a module author thought of them, and
   there was no search anywhere in the product. The box is on every Records tab
   from Phase 1; Phase 6 puts Postgres full-text behind it. Until then it filters
   the records already on the page, which is the honest half and is useful on its
   own for a client with two hundred JSAs. */
.fhs-search{
  display:flex;
  align-items:center;
  gap:8px;
  background:var(--fhs-surface);
  border:1px solid var(--fhs-line);
  border-radius:12px;
  padding:0 12px;
  margin:0 0 10px;
  box-shadow:var(--fhs-shadow);
}
.fhs-search svg{
  width:15px;
  height:15px;
  flex:0 0 auto;
  color:var(--fhs-b);
}
.fhs-search input{
  flex:1;
  min-width:0;
  border:0;
  outline:none;
  background:none;
  color:var(--fhs-ink);
  font:inherit;
  font-size:14px;
  /* 44px so a gloved thumb can land on it, matching every other control here */
  min-height:44px;
}
.fhs-search input::placeholder{color:var(--fhs-ink3)}
.fhs-chips{
  display:flex;
  gap:6px;
  margin:0 0 10px;
  flex-wrap:wrap;
}
.fhs-chip{
  font-size:11.5px;
  font-family:inherit;
  padding:0 14px;
  min-height:44px;      /* same rule as the tabs above */
  border-radius:22px;
  border:1px solid rgba(var(--fhs-brgb),.25);
  background:var(--fhs-surface);
  color:var(--fhs-ink2);
  cursor:pointer;
}
.fhs-chip[aria-pressed="true"]{
  background:linear-gradient(135deg,var(--fhs-bb),var(--fhs-b));
  color:#fff;
  border-color:transparent;
}
/* A record the search or the chips filtered out. Set on the module's own card
   element, which is why this is one property and nothing else: the card itself
   is the best thing in the product and the review says keep it unchanged. */
.fhs-hidden{display:none !important}
.fhs-nohits{
  padding:26px 16px;
  text-align:center;
  color:var(--fhs-ink3);
  font-size:13px;
}

/* ============================================================================
   THE BOTTOM BAR: one primary action, fixed bottom right, always.

   Finding 7: the primary action was sometimes left, sometimes right, sometimes
   absent, and sometimes sat beside a second button of equal weight. A user should
   be able to reach for the bottom right corner with a gloved thumb and always get
   "new thing".

   The module's own #actionbar element is KEPT and restyled rather than replaced.
   It already carries the safe-area padding, the max-width centering and the
   fh-theme.css rule that keeps it up on an empty module, and all of that is still
   right. What changes is that its contents are sorted: one primary on the right,
   everything else behind More. */
body:has(#scroll) .actionbar.fhs-bar{
  justify-content:flex-end;
  gap:9px;
  border-top:0;
  background:linear-gradient(to top,var(--bg,#F3F4FA) 62%,transparent);
  backdrop-filter:none;
  box-shadow:none;
}
body:has(#scroll) .actionbar.fhs-bar .fhs-primary{
  flex:0 1 auto;
  min-height:52px;
  padding:0 26px;
  border:0;
  border-radius:14px;
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-weight:700;
  font-size:15px;
  color:#fff;
  cursor:pointer;
  background:linear-gradient(135deg,var(--fhs-bb),var(--fhs-b) 60%,var(--fhs-bd));
  box-shadow:0 14px 28px -14px rgba(var(--fhs-brgb),.8),inset 0 1px 0 rgba(255,255,255,.18);
}
body:has(#scroll) .actionbar.fhs-bar .fhs-more{
  flex:0 0 auto;
  min-width:52px;
  min-height:52px;
  border-radius:14px;
  border:1.5px solid rgba(var(--fhs-brgb),.35);
  background:var(--fhs-surface);
  color:var(--fhs-b);
  font:inherit;
  font-weight:700;
  font-size:15px;
  cursor:pointer;
}
/* The More sheet. Everything the module used to put in the bottom bar beside the
   primary: export, broker request, self assessment, client rules, the digest. */
.fhs-sheet{
  position:fixed;
  inset:0;
  z-index:40;
  display:flex;
  align-items:flex-end;
  justify-content:center;
  background:rgba(12,14,32,.45);
}
.fhs-sheet[hidden]{display:none}
.fhs-sheet .fhs-sheet-in{
  width:100%;
  max-width:720px;
  background:var(--fhs-surface);
  border-radius:20px 20px 0 0;
  padding:16px 16px calc(20px + env(safe-area-inset-bottom,0px));
  box-shadow:0 -18px 44px -20px rgba(20,22,45,.5);
}
.fhs-sheet h2{
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-size:12px;
  letter-spacing:.08em;
  text-transform:uppercase;
  color:var(--fhs-ink3);
  margin:0 0 12px;
}
.fhs-sheet .fhs-sheet-list{display:flex;flex-direction:column;gap:9px}
.fhs-sheet .fhs-sheet-list > *{
  width:100%;
  min-height:50px;
  border-radius:12px;
  border:1.5px solid var(--fhs-line);
  background:var(--fhs-surface);
  color:var(--fhs-ink);
  font:inherit;
  font-size:15px;
  font-weight:600;
  cursor:pointer;
}

/* ============================================================================
   THE ABOUT TAB.

   Finding 4: the tile said "Driver Qualification Files" and the page headline said
   "Every file 391.51 asks for". Good copy, bad wayfinding. The sentence headline
   and the explanatory paragraphs are not deleted, they move here, where a new user
   reads them once and an experienced one never has to scroll past them again.

   .fhs-about-moved is the module's own headline block after fh-shell.js has moved
   it. It keeps whatever styling the module gave it, minus the hero treatment,
   because that treatment now lives on the header. */
.fhs-panel[hidden]{display:none}
.fhs-about-lead{
  font-size:14px;
  line-height:1.5;
  color:var(--fhs-ink2);
  background:var(--fhs-surface);
  border:1px solid var(--fhs-line);
  border-radius:14px;
  padding:15px 16px;
  box-shadow:var(--fhs-shadow);
  margin:0 0 12px;
}
.fhs-about-moved{
  display:block !important;
  background:none !important;
  color:inherit !important;
  box-shadow:none !important;
  border-radius:0 !important;
  padding:0 !important;
  margin:0 0 12px !important;
  animation:none !important;
}
.fhs-about-moved h1{
  color:var(--fhs-ink) !important;
  font-size:19px !important;
  margin:0 0 6px !important;
}
.fhs-about-moved p{color:var(--fhs-ink2) !important}
/* The hero watermark image inside a moved headline block has no job in About. */
.fhs-about-moved .hero-mark,
.fhs-about-moved img{display:none !important}

/* A checklist row that is also the way to go and do the thing. The empty state
   offers ONE button; a prerequisite is a row here rather than a second button
   competing with it, and the row itself is the tap target so nothing is lost. */
.fhs-steps li[data-edit],
.fhs-steps li[data-add]{
  cursor:pointer;
  color:var(--fhs-b);
  font-weight:600;
  min-height:44px;
}
.fhs-steps li[data-edit]::after,
.fhs-steps li[data-add]::after{
  content:'\203A';
  margin-left:auto;
  font-size:19px;
  line-height:1;
  color:var(--fhs-b);
}

/* The section rule the home page uses, reused above each About block so the tab
   does not read as a wall of text. */
.fhs-sech{
  display:flex;
  align-items:center;
  gap:10px;
  margin:14px 2px 8px;
}
.fhs-sech h3{
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-weight:700;
  font-size:11.5px;
  letter-spacing:.2em;
  text-transform:uppercase;
  color:var(--fhs-ink2);
  white-space:nowrap;
  margin:0;
}
.fhs-sech .fhs-rule{
  flex:1;
  height:1px;
  background:linear-gradient(90deg,var(--fhs-b),transparent);
}

/* ============================================================================
   THE EMPTY STATE: one sentence, one button, prerequisites as a checklist.

   Finding 8: "Add your first policy" and "Set your legal entity name first" were
   two buttons side by side, and the second is a prerequisite for the first. New
   clients live in empty states for their first week. One button, and the order
   shown as a checklist under it rather than as a competing tap target. */
.fhs-empty{
  background:var(--fhs-surface);
  border:1px solid var(--fhs-line);
  border-radius:16px;
  padding:20px 16px;
  text-align:center;
  box-shadow:var(--fhs-shadow);
}
.fhs-empty .fhs-ring{
  width:52px;
  height:52px;
  border-radius:50%;
  margin:0 auto 10px;
  display:grid;
  place-items:center;
  background:radial-gradient(circle at 50% 38%,rgba(var(--fhs-brgb),.18),rgba(var(--fhs-brgb),.04));
  box-shadow:0 0 0 1px rgba(var(--fhs-brgb),.35),inset 0 0 0 1px rgba(255,255,255,.6);
}
.fhs-empty .fhs-ring svg{width:24px;height:24px;color:var(--fhs-b)}
.fhs-empty b{
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-size:15px;
}
.fhs-empty p{
  font-size:13px;
  color:var(--fhs-ink2);
  line-height:1.45;
  margin:6px 0 12px;
}
.fhs-steps{
  text-align:left;
  list-style:none;
  font-size:12.5px;
  margin:0 auto;
  padding:0;
  max-width:290px;
}
.fhs-steps li{
  padding:7px 0;
  border-bottom:1px solid var(--fhs-line);
  display:flex;
  gap:8px;
  align-items:center;
  color:var(--fhs-ink2);
}
.fhs-steps li:last-child{border-bottom:0}
.fhs-steps li::before{
  content:'';
  width:14px;
  height:14px;
  border-radius:50%;
  border:2px solid #C9C4DE;
  flex:0 0 auto;
}
.fhs-steps li.fhs-done::before{background:var(--fhs-grn);border-color:var(--fhs-grn)}
.fhs-steps li.fhs-done{color:var(--fhs-ink3)}

/* ============================================================================
   ROOM AT THE BOTTOM.

   Every module page already pads .scroll by 120px plus the safe area, which was
   sized for a full-width bar of one or two buttons. The shell's bar is one pill
   and is no taller, so that padding still holds. What is new is the tab strip at
   the top of the Records panel, which scrolls with the content the way the mock
   draws it, so nothing needs to be pushed down for it. */
body:has(#scroll) .fhs-top{
  margin:0 0 2px;
}
/* The Reports panel lists what used to sit in the bottom bar, plus whatever the
   module offers as a report. It is deliberately plain: these are actions, not a
   second dashboard. */
.fhs-report-list{display:flex;flex-direction:column;gap:9px}
.fhs-report-list > *{
  width:100%;
  min-height:52px;
  border-radius:12px;
  border:1.5px solid var(--fhs-line);
  background:var(--fhs-surface);
  color:var(--fhs-ink);
  font:inherit;
  font-size:15px;
  font-weight:600;
  text-align:left;
  padding:0 16px;
  cursor:pointer;
  box-shadow:var(--fhs-shadow);
}
.fhs-note{
  font-size:12.5px;
  color:var(--fhs-ink3);
  line-height:1.45;
  margin:10px 2px 0;
}

/* A phone in a truck, with gloves on. Every control a finger lands on in this file
   is at least 44 by 44, which is what audit_mobile.js and verify_taps.js measure.
   Type gets smaller on a narrow screen; the press target does not. */
@media(max-width:430px){
  .fhs-tab{font-size:12px}
  .fhs-st .fhs-n{font-size:20px}
  .fhs-st .fhs-l{font-size:10px}
}

/* ============================================================================
   WHAT A TAB ACTUALLY HIDES.

   The Records tab IS the module's own rendered content, so switching to Reports
   or About cannot mean "render something else": it means hide the module's own
   children and show one panel. Everything the shell owns is excluded by id, and
   the module's children are hidden by being everything else. Written that way
   round on purpose: a module that renders a new kind of block tomorrow is hidden
   correctly without this rule knowing the block exists. */
body:has(#scroll) #scroll.fhs-off-records > *{
  display:none !important;
}
body:has(#scroll) #scroll.fhs-off-records > #fhsTop,
body:has(#scroll) #scroll.fhs-off-records > #fhsReports:not([hidden]),
body:has(#scroll) #scroll.fhs-off-records > #fhsAbout:not([hidden]){
  /* :not([hidden]) is load-bearing. display:block with !important beats the sheet
     rule that hides a [hidden] element, so without it BOTH panels showed at once
     and the Reports tab printed the About text underneath its own. */
  display:block !important;
}
/* The stat strip and the filters belong to the record list, so they go with it. */
body:has(#scroll) #scroll.fhs-off-records #fhsStats,
body:has(#scroll) #scroll.fhs-off-records .fhs-search,
body:has(#scroll) #scroll.fhs-off-records .fhs-chips{
  display:none !important;
}

/* A figure the Reports tab shows rather than an action it offers. Same tile shape
   as the stat strip, because it is the same kind of thing: a number with a label.
   It is here and not there because the three tiles answer "what needs me today"
   and an average score does not. */
.fhs-report-figures{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:8px;
  margin:0 0 14px;
}
.fhs-report-figures .fhs-fig{
  background:var(--fhs-surface);
  border:1px solid var(--fhs-line);
  border-radius:14px;
  padding:12px 13px;
  box-shadow:var(--fhs-shadow);
}
.fhs-report-figures .fhs-fig b{
  display:block;
  font-family:var(--disp,'Archivo',system-ui,sans-serif);
  font-size:22px;
  font-weight:800;
  line-height:1;
  color:var(--fhs-ink);
}
.fhs-report-figures .fhs-fig span{
  display:block;
  font-size:11px;
  color:var(--fhs-ink3);
  margin-top:4px;
}

/* ============================================================================
   THE BOTTOM NAV, Phase 3.

   Home, New, Records, Expiring, Profile on a phone, and the field tier gets Today,
   New, My Records, Profile. Finding 1 of the review is that there was no navigation
   between modules at all: every module opened in its own browser tab and there was no
   way from one to another except back to the dashboard. Modules open in this tab now,
   so the browser's own Back works, and this is what makes going FORWARD possible.

   IT IS ONLY ON THE MODULE'S OWN HOME. On a sub-screen the module's own bar is in
   charge and the offline layer spends most of its code stopping you from leaving a
   half-filled form; a nav that invited you to would be working against it.

   The height is a token because two other things have to make room for it: the action
   bar sits on top of it rather than under it, and the scroll area has to end above
   both. Everything below reads --fhs-nav-h rather than repeating the number. */
body:has(#scroll), body.fhs-host{
  /* 68 is the nav's own box: a 52px control with 7 above and 9 below. The safe area
     is INSIDE the token rather than added by the nav alone, because the action bar
     sits on top of the nav and has to be pushed up by the whole of it. Measured in a
     browser at 390px: with the token at 64 the bar overlapped the nav by five pixels,
     which is a five pixel strip of a button nobody can press. Re-measured at 69. */
  --fhs-nav-h:calc(69px + env(safe-area-inset-bottom,0px));
}
.fhs-nav{
  position:fixed;
  left:0;
  right:0;
  bottom:0;
  z-index:26;
  display:flex;
  justify-content:space-around;
  max-width:720px;
  margin:0 auto;
  padding:7px 4px calc(9px + env(safe-area-inset-bottom,0px));
  min-height:var(--fhs-nav-h);
  box-sizing:border-box;
  background:color-mix(in srgb,var(--fhs-surface) 92%,transparent);
  backdrop-filter:blur(12px);
  border-top:1px solid var(--fhs-line);
}
.fhs-nav .fhs-nav-i{
  flex:1 1 0;
  min-width:0;
  min-height:52px;
  border:0;
  background:none;
  cursor:pointer;
  font:inherit;
  font-size:10.5px;
  font-weight:600;
  color:var(--fhs-ink3);
  display:flex;
  flex-direction:column;
  align-items:center;
  gap:3px;
  padding:0 2px;
}
.fhs-nav .fhs-nav-i i{
  display:flex;
  width:44px;
  height:26px;
  border-radius:13px;
  align-items:center;
  justify-content:center;
}
.fhs-nav .fhs-nav-i svg{width:20px;height:20px}
.fhs-nav .fhs-nav-i.fhs-on{color:var(--fhs-b);font-weight:700}
.fhs-nav .fhs-nav-i.fhs-on i{background:rgba(var(--fhs-brgb),.13)}

/* MAKING ROOM. The action bar was fixed to the bottom of the viewport and carried the
   safe-area padding itself; with a nav under it, it sits on top of the nav and the nav
   carries the safe area instead. The scroll area's own bottom padding is set by each
   module page at 120px plus the safe area, which was right when the bar was the last
   thing on screen; it needs the nav's height on top of that now. */
body.fhs-has-nav:has(#scroll) .actionbar{
  bottom:var(--fhs-nav-h);
  padding-bottom:12px;
}
body.fhs-has-nav:has(#scroll) .scroll{
  padding-bottom:calc(120px + var(--fhs-nav-h));
}

/* A SHEET STOPS ABOVE THE NAV rather than covering it. Opening New and then wanting
   Profile is an ordinary thing to do, and with the scrim over the nav the second tap
   landed on the scrim and did nothing. Measured, not guessed: a click on the Profile
   entry with the New sheet open was intercepted by the sheet for thirty seconds. */
body.fhs-has-nav .fhs-sheet{
  bottom:var(--fhs-nav-h);
}
body.fhs-has-nav .fhs-sheet .fhs-sheet-in{
  padding-bottom:20px;
}

/* THE NEW PICKER. Big targets, grouped by area, in area order. */
.fhs-new-list{display:flex;flex-direction:column;gap:2px;max-height:62vh;overflow:auto}
.fhs-new-item{
  display:flex;
  align-items:center;
  gap:10px;
  min-height:56px;
  padding:0 14px;
  border-radius:12px;
  border:1.5px solid var(--fhs-line);
  background:var(--fhs-surface);
  color:var(--fhs-ink);
  text-decoration:none;
  font-weight:600;
  font-size:15px;
  margin-bottom:7px;
}
.fhs-new-item .fhs-new-t{
  margin-left:auto;
  font-size:11px;
  font-weight:500;
  color:var(--fhs-ink3);
}
.fhs-new-item.fhs-here{border-color:rgba(var(--fhs-brgb),.45)}

/* ===================================================================
   DESKTOP, 1024px AND UP
   ===================================================================
   Lane 4, Phase 5, September 1, 2026.

   WHAT CHANGES AND WHAT DOES NOT. The phone layout is a single column with a
   bar along the bottom. On a laptop that column sits in the middle of a wide
   screen with nothing either side of it, which is the fault the review names.
   So above 1024px the bar becomes a sidebar down the left, the search box and
   the primary button move up into the header where there is room for them, and
   the record list uses the width it has been given.

   THE RECORD CARD IS UNCHANGED. So is every module's own markup. Everything
   here is either a rule in this file or a node the shell already owns being
   moved by the shell, which is why a module page needs no desktop code of its
   own and cannot drift from this one.

   THE SIDEBAR IS F&H NAVY, NOT THE CLIENT'S BRAND. It is chrome: the frame the
   product is delivered in, the same on every client, the way a title bar is.
   The client's color is inside it, on the logo, the current entry, the tabs,
   the tiles and the buttons. A sidebar in the client's own color and a hero in
   the same color under it reads as one enormous block of one hue, and the
   review's frame 5 does not do that either.
   =================================================================== */
@media (min-width:1024px){

  body:has(#scroll), body.fhs-host{
    --fhs-side-w:264px;
    /* F&H NAVY, WRITTEN DOWN, AND THAT IS DELIBERATE.
       The first version read var(--navy-1), which is the right instinct and the
       wrong token: dashboard.css redefines --navy-0 through --navy-line to LIGHT
       grays under data-theme=light, so in light mode the sidebar came out pale
       with white text on it and was, measurably, invisible.

       Navy here is chrome, not palette. It is the frame the product is delivered
       in: the same on every client, the same in both themes, the same as the
       stoplight is the same. So it is fixed, in the same way and for the same
       reason, and tests/verify_shell.js allows these three the way it allows the
       three stoplight colors. */
    --fhs-navy:#0A0E25;
    --fhs-navy-2:#111942;
    --fhs-navy-line:#161F51;
  }

  /* The bar down the bottom is a phone control. On a laptop it would be a strip
     of five buttons across an otherwise empty foot of screen. */
  body.fhs-has-nav .fhs-nav{ display:none; }

  .fhs-side{
    position:fixed; top:0; left:0; bottom:0; width:var(--fhs-side-w);
    z-index:60;
    display:flex; flex-direction:column;
    background:var(--fhs-navy);
    border-right:1px solid var(--fhs-navy-line);
    overflow-y:auto; overscroll-behavior:contain;
    padding:0 0 14px;
  }
  .fhs-side[hidden]{ display:none !important; }

  /* The client's mark and their name, in the serif the suite uses for a company
     name everywhere else. The name wraps rather than truncating: a client whose
     name is four words is not a client whose name should end in an ellipsis. */
  .fhs-side-brand{
    display:flex; align-items:center; gap:11px;
    padding:18px 16px 16px;
    border-bottom:1px solid var(--fhs-navy-line);
  }
  .fhs-side-brand img{
    width:40px; height:40px; flex:0 0 auto;
    border-radius:10px; object-fit:contain;
    background:#f7f5f2; padding:3px;
  }
  .fhs-side-brand .fhs-side-name{
    font-family:var(--fh-font-serif,'FH Serif',Georgia,serif);
    font-size:17px; line-height:1.18; font-weight:600;
    color:#fff; letter-spacing:-.01em;
    min-width:0; overflow-wrap:anywhere;
  }

  .fhs-side-group{ padding:14px 12px 2px; }
  .fhs-side-group h2{
    margin:0 0 7px 6px;
    font-family:var(--fh-font-label,'FH Condensed','Arial Narrow',sans-serif);
    font-size:10.5px; font-weight:600; letter-spacing:.18em; text-transform:uppercase;
    color:rgba(255,255,255,.44);
  }
  .fhs-side a.fhs-side-i,
  .fhs-side button.fhs-side-i{
    display:flex; align-items:center; gap:10px;
    width:100%;
    /* 44 is the house minimum and it is the minimum here too, even with a mouse:
       the same page is opened on a touchscreen laptop. */
    min-height:44px;
    padding:0 10px; margin:0 0 2px;
    border:0; border-radius:10px;
    background:none; color:rgba(255,255,255,.78);
    font:inherit; font-size:13.5px; font-weight:600; text-align:left;
    text-decoration:none; cursor:pointer;
  }
  .fhs-side a.fhs-side-i:hover,
  .fhs-side button.fhs-side-i:hover{ background:var(--fhs-navy-2); color:#fff; }
  .fhs-side .fhs-side-i svg{ width:18px; height:18px; flex:0 0 auto; opacity:.8 }
  /* THE ACTIVE ENTRY. A wash that fades out to the right instead of a flat block,
     and a rail down the left that is a gradient rather than a bar of one color.
     The rail is a pseudo-element so it can be rounded on its outer edge and inset
     from the top and bottom of the row; an inset box-shadow could do neither.
     September 4, 2026 (TD-19). */
  .fhs-side .fhs-side-i.fhs-on{
    position:relative;
    background:linear-gradient(90deg,rgba(var(--fhs-brgb),.30),rgba(var(--fhs-brgb),.07) 70%,transparent);
    color:#fff;
  }
  .fhs-side .fhs-side-i.fhs-on:before{
    content:"";
    position:absolute; left:0; top:6px; bottom:6px; width:3px;
    border-radius:0 3px 3px 0;
    background:linear-gradient(180deg,var(--fhs-bb),var(--fhs-bd));
  }
  .fhs-side .fhs-side-i.fhs-on svg{ opacity:1; color:var(--fhs-bb) }
  .fhs-side-foot{ margin-top:auto; padding:10px 12px 0; border-top:1px solid var(--fhs-navy-line) }

  /* The page moves over to make room. Both the module pages and the home. */
  body:has(#scroll).fhs-desk,
  body.fhs-host.fhs-desk{ padding-left:var(--fhs-side-w); }
  body.fhs-desk header.bar,
  body.fhs-desk .fhs-nav{ left:var(--fhs-side-w); width:auto; }

  /* The header gains a row for the search box and the primary button, which on
     a phone live at the top of the list and at the bottom of the screen. */
  .fhs-deskbar{
    display:flex; align-items:center; gap:10px;
    margin-left:auto; flex:0 0 auto;
  }
  .fhs-deskbar:empty{ display:none }
  body.fhs-desk .fhs-deskbar .fhs-search{ margin:0; width:270px; }
  body.fhs-desk .fhs-deskbar .fhs-search input{ min-height:44px }
  body.fhs-desk .fhs-deskbar .btn,
  body.fhs-desk .fhs-deskbar button{ min-height:44px; white-space:nowrap }

  /* With the primary lifted into the header the fixed bottom bar has nothing in
     it, so it stops taking a strip of the screen. It comes back the moment the
     window is narrowed, because the shell puts the button back. */
  body.fhs-desk .actionbar:empty{ display:none }

  /* THE WIDTH. Every module page caps its own .app at 720px, which is right on a
     phone and is the fault itself on a laptop: a phone column in the middle of the
     screen. The cap is lifted here rather than in twenty-two files, and the record
     card is not touched at all; it is the room around it that changes.

     This rule outranks the module's own .app on specificity alone, so it works on
     every page whatever order the stylesheets happen to load in. */
  body.fhs-desk .app{ max-width:1180px; }
  /* NOT margin:auto on #scroll. .app is a column flex container, and an auto
     cross-axis margin on a flex item makes it size to its content instead of
     filling the line: the record list rendered 485px wide in a 1016px column and
     sat off center. .app carries the cap; #scroll fills it. Measured, after the
     first version of this rule did exactly that. */
  body.fhs-desk #scroll{ width:100%; }
  body.fhs-desk .fhh-home{ max-width:1180px; margin-left:auto; margin-right:auto; }
  /* NOT A MULTI-COLUMN LIST. Every module renders its own record markup and the
     wrapper around the cards differs from page to page, so a column rule written
     here would find the right element on some pages and the wrong one on others.
     Capping the width is the part that is true on all twenty-two. */

  /* The home's tiles have four across to use instead of three. */
  body.fhs-host.fhs-desk .fhh-tiles{ grid-template-columns:repeat(4,1fr) }
  body.fhs-host.fhs-desk .fhh-hero .fhh-big{ font-size:23px }
  /* And the work sentence under it grows with it, keeping the same step down. TD-23. */
  body.fhs-host.fhs-desk .fhh-hero:not(.fhh-slim) .fhh-sub{ font-size:16px }
}

/* Below 1024 the sidebar is not merely hidden, it is not built: paintSide()
   removes the node. This rule is the belt to that braces, for the instant
   between a resize and the next frame. */
@media (max-width:1023px){
  .fhs-side{ display:none !important }
}

/* ===================================================================
   THE BAR AND THE SIDEBAR ON THE CLIENT HOME
   ===================================================================
   The client home is not a document that scrolls the window: it is a
   .pageview, which dashboard.css gives position:fixed, inset:0 and
   z-index:200. Everything the shell adds to the body is a sibling of that
   view, so at z-index 26 the bottom bar was DRAWN, in the right place, at the
   right size, and painted underneath the page. Every geometric check passed:
   the entries were there, they measured 44 pixels, they did not overlap the
   last tile. None of that is worth anything if the thing on top of them is
   the page.

   tests/verify_home_dom.js now asks elementFromPoint what is actually at the
   middle of each entry, which is the only question that answers this.
   =================================================================== */
body.fhs-host .fhs-nav{ z-index:260; }
body.fhs-host .fhs-side{ z-index:260; }
@media (min-width:1024px){
  /* The view is fixed to the viewport, so padding the body moves nothing. The
     room for the sidebar has to be made inside the view itself. */
  body.fhs-host.fhs-desk{ padding-left:0; }
  body.fhs-host.fhs-desk .pageview{ padding-left:var(--fhs-side-w); }
}
