/* F&H STAC visual theme preview, August 18, 2026.
   Additive only. Load after dashboard.css and page-level styles.
   Removing this one stylesheet restores the prior presentation. */

:root{
  --fh-font-body:'FH Sans',system-ui,-apple-system,'Segoe UI',sans-serif;
  --fh-font-display:'FH Sans',system-ui,-apple-system,'Segoe UI',sans-serif;
  --fh-font-label:'FH Condensed','Arial Narrow',sans-serif;
  --fh-font-serif:'FH Serif',Georgia,serif;
  --fh-font-mono:'FH Mono','Cascadia Code',ui-monospace,monospace;

  /* EVERY CHAIN ENDS IN A LITERAL, AND THAT IS NOT BELT AND BRACES.
     Only index.html links dashboard.css. The 18 module pages, osha-301 and the Executive
     Dashboard each carry their own tokens inline and define a different subset, so a chain that
     bottoms out in another custom property bottoms out in NOTHING on most of the estate.

     Measured in the browser on 2026-08-19, before this change:
       module pages          8 of these tokens resolved to empty
       osha-301.html         6
       Executive Dashboard   4
       index.html            0, which is why it looked fine everywhere I first checked

     An empty custom property is not "no rule". The declaration using it becomes invalid at
     computed-value time, which for an inherited property means it INHERITS. That is how the
     primary button on every module page ended up with body text color on a purple fill:
     --fh-on-brand resolved to nothing, so color fell back to the inherited near-black, and the
     page's own correct .btn.primary{color:#fff} had already been overridden. It is also why the
     elevation scale did nothing on modules: all three shadows are built from --fh-edge-hi.

     The literals below are the F&H dark-theme values. They are last in each chain, so a client
     palette and dashboard.css both still win ahead of them. */
  --fh-canvas:var(--navy-0,var(--bg,#060815));
  --fh-panel:var(--surface,var(--navy-2,#0F1639));
  --fh-panel-hi:var(--surface-hi,var(--surface-2,#131B47));
  --fh-panel-low:var(--surface-3,var(--recess,#0B1130));
  --fh-line:var(--navy-line,var(--line,#1E2748));
  --fh-line-strong:var(--ctl-line,var(--line-2,#2A3560));
  --fh-text:var(--text,var(--ink,#dfe3ee));
  --fh-muted:var(--text-dim,var(--ink-2,#8b93ad));
  --fh-quiet:var(--soon,var(--ink-3,#7e88a6));
  --fh-brand:var(--brand-500,var(--brand,var(--purple-bright,#7914ED)));
  --fh-brand-strong:var(--brand-700,var(--brand-deep,var(--purple,#5F0EBD)));
  --fh-brand-deep:var(--brand-900,var(--purple-dark,#400A7E));
  --fh-brand-bright:var(--brand-300,var(--brand-bright,var(--purple-bright,#7138EE)));
  --fh-brand-soft:var(--brand-100,var(--brand-soft,var(--ctl-bg,#1A2251)));
  /* White last, because the default brand is F&H violet and violet is dark. When brand-apply.js
     has run, --brand-on is computed for that client's mid tone and wins ahead of this. */
  --fh-on-brand:var(--brand-on,var(--brand-ink,var(--metal-hi,#ffffff)));
  --fh-recess:var(--recess,var(--surface-3,#0B1130));

  /* Operational stoplight colors are deliberately separate from the unchanged
     F&H navy and purple brand palette. These are for state, risk, and urgency. */
  --fh-signal-green:#39E58C;
  --fh-signal-green-text:#8AF4B9;
  --fh-signal-green-wash:color-mix(in srgb,var(--fh-signal-green) 15%,transparent);
  --fh-signal-yellow:#FFD43B;
  --fh-signal-yellow-text:#FFE17B;
  --fh-signal-yellow-wash:color-mix(in srgb,var(--fh-signal-yellow) 14%,transparent);
  --fh-signal-red:#FF5D68;
  --fh-signal-red-text:#FFABB2;
  --fh-signal-red-wash:color-mix(in srgb,var(--fh-signal-red) 14%,transparent);
  --fh-signal-on-red:#2B0508;

  --fh-edge:color-mix(in srgb,var(--fh-text) 10%,transparent);
  /* A literal, not a token. This is a white sheen along a top edge; it is not brand colored
     and it never was. Pointing it at --metal-hi made all three shadows collapse on the 18
     module pages, because only dashboard.css defines that token and only index.html links
     dashboard.css. The elevation scale was doing nothing on most of the estate. */
  --fh-edge-hi:color-mix(in srgb,#ffffff 11%,transparent);
  --fh-shadow-color:color-mix(in srgb,var(--fh-canvas) 76%,transparent);
  --fh-shadow-1:0 1px 0 var(--fh-edge-hi) inset,0 10px 24px -20px var(--fh-shadow-color);
  --fh-shadow-2:0 1px 0 var(--fh-edge-hi) inset,0 20px 48px -30px var(--fh-shadow-color);
  --fh-shadow-3:0 1px 0 var(--fh-edge-hi) inset,0 30px 72px -34px var(--fh-shadow-color);
  --fh-focus:color-mix(in srgb,var(--fh-brand) 28%,transparent);
  --fh-hover:color-mix(in srgb,var(--fh-brand) 8%,var(--fh-panel));
  --fh-radius-sm:10px;
  --fh-radius-md:14px;
  --fh-radius-lg:20px;
  --fh-speed:160ms;

  --disp:var(--fh-font-display);
  --body:var(--fh-font-body);
  --mono:var(--fh-font-mono);
}

:root[data-theme="light"]{
  --fh-edge:color-mix(in srgb,var(--fh-text) 13%,transparent);
  /* THE LIGHT BLOCK IS WHERE THIS ACTUALLY BIT. The dark declaration above had a fallback and
     this one did not, and every module page runs with data-theme="light", so on 18 of the 24
     pages --fh-edge-hi collapsed and took --fh-shadow-1, -2 and -3 down with it. Only
     index.html was fine, because only index.html links dashboard.css, which is the one file
     that defines --metal-hi. Fixing the dark declaration alone changed nothing and the count
     of unresolved tokens stayed at four, which is how this second copy was found. */
  --fh-edge-hi:color-mix(in srgb,var(--metal-hi,#ffffff) 74%,transparent);
  --fh-shadow-color:color-mix(in srgb,var(--fh-text) 18%,transparent);
  --fh-signal-green:#00A85A;
  --fh-signal-green-text:#006B38;
  --fh-signal-green-wash:color-mix(in srgb,var(--fh-signal-green) 11%,transparent);
  --fh-signal-yellow:#F5B700;
  --fh-signal-yellow-text:#714B00;
  --fh-signal-yellow-wash:color-mix(in srgb,var(--fh-signal-yellow) 14%,transparent);
  --fh-signal-red:#E12D3D;
  --fh-signal-red-text:#9D1523;
  --fh-signal-red-wash:color-mix(in srgb,var(--fh-signal-red) 10%,transparent);
  --fh-signal-on-red:#ffffff;
}

html{
  font-family:var(--fh-font-body);
  font-variant-numeric:tabular-nums lining-nums;
  text-rendering:optimizeLegibility;
}
body,
button,
input,
select,
textarea{
  font-family:var(--fh-font-body);
}
body{
  color:var(--fh-text);
}
h1,h2,h3,h4,
.wordmark,
.pv-title,
.ttl,
.home-head h1,
.step-q,
.d-head h1,
.cd-brandname,
.cd-exec-h{
  font-family:var(--fh-font-display);
  letter-spacing:-.025em;
}
.mono,
.case,
.clock,
[data-count],
[data-value],
.stat .n,
.metric,
.metric-value,
.fnum,
.kv dd,
td{
  font-variant-numeric:tabular-nums lining-nums;
}
.mono,
.case,
.step-eyebrow{
  font-family:var(--fh-font-mono);
}
.subtitle,
.sec-head h2,
.list-lbl,
.step-eyebrow,
.card h3,
.field > label,
.field label,
.as-dr-section,
.as-fld label,
.badge,
.stbadge,
th{
  font-family:var(--fh-font-label);
  font-weight:600;
  letter-spacing:.105em;
  text-transform:uppercase;
}

:where(a,button,input,select,textarea,[contenteditable="true"]):focus-visible{
  outline:3px solid var(--fh-focus);
  outline-offset:3px;
}

/* Application chrome */
header.bar,
.bar,
.pv-bar,
.toolbar{
  border-color:var(--fh-line);
  background:color-mix(in srgb,var(--fh-panel) 88%,transparent);
  box-shadow:var(--fh-shadow-1);
  backdrop-filter:blur(18px) saturate(130%);
}
header.bar{
  border-radius:var(--fh-radius-lg);
  padding:16px 18px;
}
.wordmark{
  font-size:clamp(22px,2.5vw,29px);
  font-weight:750;
  letter-spacing:-.03em;
}
.subtitle{
  font-size:11px;
  color:var(--fh-muted);
}
.clock .t{
  font-family:var(--fh-font-mono);
  font-size:19px;
  letter-spacing:-.035em;
}
.seal,
.brand-logo{
  box-shadow:0 0 0 1px var(--fh-line),0 10px 28px -18px var(--fh-brand);
}
.iconbtn,
.navlink,
.signout-btn,
.back,
.bar .back,
.pv-back{
  min-height:44px;
  border-color:var(--fh-line);
  background:var(--fh-panel-low);
  box-shadow:0 1px 0 var(--fh-edge-hi) inset;
  transition:border-color var(--fh-speed) ease,background var(--fh-speed) ease,transform var(--fh-speed) ease;
}
.iconbtn:hover,
.navlink:hover,
.signout-btn:hover,
.back:hover,
.bar .back:hover,
.pv-back:hover{
  border-color:var(--fh-brand);
  background:var(--fh-hover);
}

/* Page hierarchy */
.sec-head{
  margin-top:38px;
  margin-bottom:16px;
}
.sec-head h2{
  font-size:12px;
  letter-spacing:.16em;
  color:var(--fh-muted);
}
.sec-head .rule{
  background:linear-gradient(90deg,var(--fh-brand),color-mix(in srgb,var(--fh-brand) 8%,transparent),transparent);
}
.home-head{
  position:relative;
  border:1px solid var(--fh-line) !important;
  border-radius:var(--fh-radius-lg) !important;
  background:linear-gradient(145deg,var(--fh-panel-hi),var(--fh-panel)) !important;
  color:var(--fh-text) !important;
  box-shadow:var(--fh-shadow-2) !important;
  overflow:hidden;
}
.home-head::before{
  opacity:.2 !important;
}
.home-head::after{
  content:"";
  position:absolute;
  inset:0 auto 0 0;
  width:4px;
  background:linear-gradient(180deg,var(--fh-brand),var(--fh-brand-strong));
}
.home-head h1{
  font-size:clamp(28px,4.4vw,38px);
  font-weight:760;
  line-height:1.04;
  color:var(--fh-text) !important;
}
.home-head p,
.home-head .sub{
  color:var(--fh-muted) !important;
}
.step-q,
.d-head h1{
  font-size:clamp(25px,5vw,34px);
  font-weight:760;
  line-height:1.08;
}
.step-eyebrow{
  color:var(--fh-text);
  font-size:11px;
  border-left:3px solid var(--fh-brand);
  padding-left:8px;
}

/* Material surfaces */
.tile,
.cd-tile,
.as-card,
.stat,
.card,
.rec-card,
.sum,
.panel,
.modal,
.auth-card,
.choice,
.optrow,
.person{
  border-color:var(--fh-line);
  background:linear-gradient(155deg,var(--fh-panel-hi),var(--fh-panel));
  box-shadow:var(--fh-shadow-1);
}
.tile,
.cd-tile,
.as-card,
.rec-card,
.choice,
.optrow{
  transition:transform var(--fh-speed) ease,border-color var(--fh-speed) ease,box-shadow var(--fh-speed) ease,background var(--fh-speed) ease;
}
.tile:hover,
.cd-tile:hover,
.as-card:hover,
.rec-card:hover,
.choice:hover,
.optrow:hover{
  transform:translateY(-2px);
  border-color:color-mix(in srgb,var(--fh-brand) 58%,var(--fh-line));
  background:linear-gradient(155deg,color-mix(in srgb,var(--fh-brand) 6%,var(--fh-panel-hi)),var(--fh-panel));
  box-shadow:var(--fh-shadow-2);
}
.stat{
  padding:15px 16px;
}
.stat .n{
  color:var(--fh-text);
  font-family:var(--fh-font-mono);
  font-size:clamp(27px,4vw,34px);
  font-weight:650;
  line-height:1;
  letter-spacing:-.055em;
}
.stat .l{
  margin-top:8px;
  color:var(--fh-muted);
  font-size:11px;
  line-height:1.35;
}

/* Dashboard modules */
.featured{
  gap:16px;
}
.action,
.op-action,
.cd-exec{
  border:1px solid color-mix(in srgb,var(--fh-brand) 55%,var(--fh-line));
  background:var(--fh-brand);
  box-shadow:0 1px 0 var(--fh-edge-hi) inset,0 24px 54px -30px var(--fh-brand);
}
.action h3,
.op-action,
.cd-exec-h{
  color:var(--fh-on-brand);
}
.action p,
.op-action .oa-sub,
.cd-exec-s{
  /* WAS 76%, WHICH FAILED FOR EVERY BRAND INCLUDING F&H's OWN. Measured on the flat fill, the
     worst client cleared only 3.40 at 76% and F&H itself sat at 4.17, under the 4.5 body-text
     line. Raising the opacity does not rescue it either: the same worst case is 3.91 at 88% and
     4.34 at full strength, because the binding constraint is the fill and not the alpha. So the
     sub-line carries the same color at full strength, and the hierarchy is left to size and
     weight, which is where hierarchy belongs on a colored fill. */
  color:var(--fh-on-brand);
}
.statgrid{
  gap:14px;
}
.tile{
  text-align:left;
  min-height:154px;
  padding:21px 18px 17px;
}
.tile .ring{
  width:46px;
  height:46px;
  margin:0 0 20px;
  border-radius:var(--fh-radius-md);
  background:var(--fh-panel-low);
  box-shadow:0 0 0 1px var(--fh-line),0 10px 24px -18px var(--fh-shadow-color);
}
.tile .label,
.cd-name,
.as-cname{
  font-family:var(--fh-font-display);
  font-size:15px;
  font-weight:700;
  letter-spacing:-.015em;
}
.tile .sub,
.cd-desc,
.as-cdesc{
  color:var(--fh-muted);
  line-height:1.45;
}
.badge,
.stbadge,
.chip,
.pill,
.flag{
  border-color:var(--fh-line);
  box-shadow:0 1px 0 var(--fh-edge-hi) inset;
}
.badge.ai{
  color:var(--fh-text);
  border-color:var(--fh-brand);
  background:var(--fh-brand-soft);
}

/* Operational status, risk, and urgency. The label and icon remain present, so
   the interface never depends on color alone. */
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag){
  display:inline-flex!important;
  align-items:center;
  gap:7px;
  border:1.5px solid color-mix(in srgb,currentColor 72%,var(--fh-line))!important;
  background:color-mix(in srgb,currentColor 14%,var(--fh-panel))!important;
  box-shadow:0 1px 0 var(--fh-edge-hi) inset,0 0 14px -10px currentColor;
  font-weight:750!important;
  filter:saturate(1.22);
}
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)::before{
  content:"";
  width:7px;
  height:7px;
  flex:none;
  border-radius:50%;
  background:currentColor;
  box-shadow:0 0 0 3px color-mix(in srgb,currentColor 15%,transparent),0 0 10px -1px currentColor;
}
/* THE MARKER HAS TO SAY SOMETHING THE COLOR DID NOT ALREADY SAY.
   The brief requires status meaning to be redundant through a label, a marker or an icon AND
   color, because these screens are read in direct sunlight by people some of whom are colorblind.
   A dot that is the same circle in all three states is the color drawn twice, not redundancy, and
   this palette makes that worse than usual: measured on the dark surface, signal green and signal
   yellow sit at a 1.15 luminance ratio to each other, which is the pair a red-green colorblind
   reader separates least well.

   So each state gets its own silhouette, readable with the color removed entirely:
     safe     a circle,   which is what a dot already meant
     caution  a diamond,  a square turned 45 degrees
     stop     a square,   the heaviest shape, and the one that reads as a full stop
   The words stay where they were. This is a third channel, not a replacement for either.

   clip-path rather than a glyph on purpose: a glyph would depend on the font loading, and the
   font is the one thing in this stylesheet that can be slow on a rig with two bars. */
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--caution"]::before,
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--warn"]::before{
  border-radius:2px;
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);
  width:9px;
  height:9px;
}
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--high"]::before,
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--danger"]::before,
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--stop"]::before{
  border-radius:2px;
  clip-path:none;
  width:8px;
  height:8px;
}
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--safe"]{
  color:var(--fh-signal-green-text)!important;
  border-color:var(--fh-signal-green)!important;
  background:var(--fh-signal-green-wash)!important;
}
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--caution"],
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--warn"]{
  color:var(--fh-signal-yellow-text)!important;
  border-color:var(--fh-signal-yellow)!important;
  background:var(--fh-signal-yellow-wash)!important;
}
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--high"],
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--danger"],
:is(.stbadge,.chiprow,.riskchip,.bandchip,.bpill,.ol-flag,.rec-tag)[style*="--stop"]{
  color:var(--fh-signal-red-text)!important;
  border-color:var(--fh-signal-red)!important;
  background:var(--fh-signal-red-wash)!important;
}
.badge.live{
  color:var(--fh-signal-green-text);
  border-color:var(--fh-signal-green);
  background:var(--fh-signal-green-wash);
  box-shadow:0 0 14px -8px var(--fh-signal-green);
}
.badge.soon{
  color:var(--fh-signal-yellow-text);
  border-color:var(--fh-signal-yellow);
  background:var(--fh-signal-yellow-wash);
  box-shadow:0 0 14px -8px var(--fh-signal-yellow);
}
.note.safe{
  color:var(--fh-signal-green-text)!important;
  border:1px solid var(--fh-signal-green);
  border-left-width:4px;
  background:var(--fh-signal-green-wash)!important;
}
.note.warn,
.note.caution{
  color:var(--fh-signal-yellow-text)!important;
  border:1px solid var(--fh-signal-yellow);
  border-left-width:4px;
  background:var(--fh-signal-yellow-wash)!important;
}
.note.danger,
.note.high,
.note.stop{
  color:var(--fh-signal-red-text)!important;
  border:1px solid var(--fh-signal-red);
  border-left-width:4px;
  background:var(--fh-signal-red-wash)!important;
}
:is(.note.safe,.note.warn,.note.caution,.note.danger,.note.high,.note.stop) b{
  color:inherit!important;
}
.badge.sev-Low{
  color:var(--fh-signal-green-text)!important;
  border-color:var(--fh-signal-green)!important;
  background:var(--fh-signal-green-wash)!important;
}
.badge.sev-Medium{
  color:var(--fh-signal-yellow-text)!important;
  border-color:var(--fh-signal-yellow)!important;
  background:var(--fh-signal-yellow-wash)!important;
}
.badge.sev-High,
.badge.sev-Critical{
  color:var(--fh-signal-red-text)!important;
  border-color:var(--fh-signal-red)!important;
  background:var(--fh-signal-red-wash)!important;
}
.sdot.on[data-n="1"],
.sdot.on[data-n="2"]{
  background:var(--fh-signal-green)!important;
  box-shadow:0 0 0 3px var(--fh-signal-green-wash),0 0 14px -3px var(--fh-signal-green);
}
.sdot.on[data-n="3"]{
  background:var(--fh-signal-yellow)!important;
  box-shadow:0 0 0 3px var(--fh-signal-yellow-wash),0 0 14px -3px var(--fh-signal-yellow);
}
.sdot.on[data-n="4"],
.sdot.on[data-n="5"]{
  background:var(--fh-signal-red)!important;
  box-shadow:0 0 0 3px var(--fh-signal-red-wash),0 0 14px -3px var(--fh-signal-red);
}
.as-config,
.auth-card .auth-alt a{
  color:var(--fh-text);
  text-decoration:underline;
  text-decoration-color:var(--fh-brand);
  text-underline-offset:3px;
}

/* Tables and dense records */
.table-wrap,
.table-responsive{
  border:1px solid var(--fh-line);
  border-radius:var(--fh-radius-md);
  background:var(--fh-panel);
  box-shadow:var(--fh-shadow-1);
  overflow:auto;
}
table{
  border-collapse:separate;
  border-spacing:0;
  color:var(--fh-text);
}
th{
  color:var(--fh-muted);
  background:var(--fh-panel-low);
  font-size:11px;
  text-align:left;
}
th,
td{
  border-color:var(--fh-line);
  padding:12px 14px;
}
td{
  font-size:13px;
}
tbody tr{
  transition:background var(--fh-speed) ease;
}
tbody tr:hover{
  background:var(--fh-hover);
}

/* Forms */
.field > label,
.field label,
.as-fld label{
  color:var(--fh-muted);
  font-size:11px;
}
.ctl,
.field input,
.field select,
.field textarea,
.as-fld input,
.as-fld select,
.as-fld textarea,
.srcwrap textarea{
  min-height:46px;
  border-color:var(--fh-line-strong);
  border-radius:var(--fh-radius-sm);
  background:linear-gradient(180deg,var(--fh-recess),var(--fh-panel-low));
  color:var(--fh-text);
  box-shadow:0 1px 0 var(--fh-edge) inset,0 1px 0 var(--fh-edge-hi);
  transition:border-color var(--fh-speed) ease,box-shadow var(--fh-speed) ease,background var(--fh-speed) ease;
}
.ctl:hover,
.field input:hover,
.field select:hover,
.field textarea:hover,
.as-fld input:hover,
.as-fld select:hover,
.as-fld textarea:hover,
.srcwrap textarea:hover{
  border-color:color-mix(in srgb,var(--fh-brand) 45%,var(--fh-line-strong));
}
.ctl:focus,
.field input:focus,
.field select:focus,
.field textarea:focus,
.as-fld input:focus,
.as-fld select:focus,
.as-fld textarea:focus,
.srcwrap textarea:focus{
  outline:none;
  border-color:var(--fh-brand);
  box-shadow:0 0 0 4px var(--fh-focus),0 1px 0 var(--fh-edge-hi) inset;
}
.btn,
.addbtn,
.photo-add,
.as-preview-btn,
.as-include-btn,
.cd-share{
  min-height:44px;
  border-radius:var(--fh-radius-sm);
  transition:transform var(--fh-speed) ease,border-color var(--fh-speed) ease,background var(--fh-speed) ease,box-shadow var(--fh-speed) ease;
}
.btn,
.addbtn,
.photo-add,
.as-preview-btn{
  border-color:var(--fh-line-strong);
  background:linear-gradient(180deg,var(--fh-panel-hi),var(--fh-panel));
  color:var(--fh-text);
  box-shadow:0 1px 0 var(--fh-edge-hi) inset,0 8px 22px -20px var(--fh-shadow-color);
}
.btn:hover,
.addbtn:hover,
.photo-add:hover,
.as-preview-btn:hover{
  border-color:var(--fh-brand);
  background:var(--fh-hover);
}
/* THE FILL IS FLAT ON PURPOSE, AND THE REASON IS ARITHMETIC.
   brand-apply.js computes --brand-on as onColor(brand-500): the readable color ON THE MID TONE.
   This rule used to paint that text over linear-gradient(--brand-700, --brand-900), the two
   darkest steps of the ramp, which is a surface --brand-on was never chosen for. Measured against
   six plausible client primaries, the text-to-fill contrast at the dark end of that gradient was:

     F&H violet 14.69   safety yellow 2.25   hi-vis green 2.18
     cyan 2.17          orange 1.68          deep navy 1.45

   F&H's own violet is dark, so onColor returns near-white and it read correctly. Every brand that
   normalizes to a lighter mid tone gets near-black text, and near-black on --brand-900 is invisible.
   Nobody saw it because no client has been provisioned.

   Filling with --brand-500 honors the contract by construction, because that is the surface
   --brand-on was computed for. Same six brands, flat fill: 6.31, 10.45, 11.12, 9.82, 6.06, 4.34.
   The raised look is kept by the inset highlight and the drop shadow rather than by a dark end.

   KNOWN EDGE: a mid-blue primary lands at 4.34, just under the 4.5 body-text threshold. That is a
   property of the fill color itself, not of this rule; pure black would only reach 4.70. Closing
   it properly means brand-apply.js emitting a per-surface on-color, which is application code and
   is not this stylesheet's to change. */
.btn.primary,
.btn.pri,
.as-include-btn.add,
.cd-share{
  border-color:var(--fh-brand);
  background:var(--fh-brand);
  color:var(--fh-on-brand);
  box-shadow:0 1px 0 var(--fh-edge-hi) inset,0 14px 32px -20px var(--fh-brand);
}
.btn:active,
.addbtn:active,
.photo-add:active,
.as-preview-btn:active,
.as-include-btn:active,
.cd-share:active{
  transform:translateY(1px);
}

/* Status and decision displays */
.verdict,
.note,
.msg,
.banner{
  border:1px solid var(--fh-line);
  border-left-width:4px;
  box-shadow:var(--fh-shadow-1);
}
.verdict .vlabel{
  font-size:23px;
  font-weight:760;
}
.verdict .vcol,
.verdict .vbasis{
  line-height:1.55;
}
.prog{
  height:6px;
  background:var(--fh-panel-low);
}
.prog i{
  background:linear-gradient(90deg,var(--fh-brand-strong),var(--fh-brand));
}

/* Empty and first-use states */
.empty,
.cd-empty{
  border:1px dashed var(--fh-line-strong);
  border-radius:var(--fh-radius-lg);
  background:linear-gradient(145deg,var(--fh-panel-hi),var(--fh-panel));
  box-shadow:var(--fh-shadow-1);
  padding:48px 24px;
}
.empty h2,
.empty-t{
  color:var(--fh-text);
  font-family:var(--fh-font-display);
  font-size:21px;
  font-weight:740;
  letter-spacing:-.02em;
}
.empty p,
.empty-b{
  color:var(--fh-muted);
  max-width:54ch;
  margin-left:auto;
  margin-right:auto;
}
.empty .btn,
.empty .addbtn{
  width:auto;
  min-width:190px;
  margin-top:18px;
}

/* Authentication */
#authGate{
  background-color:var(--fh-canvas);
  background-image:
    radial-gradient(90% 70% at 50% 0%,color-mix(in srgb,var(--fh-brand) 28%,transparent),transparent 62%),
    linear-gradient(180deg,var(--fh-panel-low),var(--fh-canvas));
}
.auth-card{
  position:relative;
  max-width:410px;
  border-radius:24px;
  padding:36px 32px 30px;
  overflow:hidden;
  box-shadow:var(--fh-shadow-3),0 0 0 1px var(--fh-edge-hi) inset;
}
.auth-card::before{
  content:"";
  position:absolute;
  inset:0 0 auto;
  height:4px;
  background:linear-gradient(90deg,var(--fh-brand-deep),var(--fh-brand),var(--fh-brand-strong));
}
.auth-card h2{
  font-size:25px;
  font-weight:760;
  letter-spacing:-.03em;
}
.auth-card .auth-sub{
  color:var(--fh-muted);
  line-height:1.5;
}
.auth-card input{
  min-height:48px;
  border-color:var(--fh-line-strong);
  background:var(--fh-recess);
  color:var(--fh-text);
  font-size:16px;
  box-shadow:0 1px 0 var(--fh-edge) inset;
}
.auth-card input:focus{
  border-color:var(--fh-brand);
  box-shadow:0 0 0 4px var(--fh-focus);
}
#authGo{
  min-height:48px;
}

/* Bottom bars and safe areas */
.actionbar,
.as-dr-foot{
  border-color:var(--fh-line);
  background:color-mix(in srgb,var(--fh-panel) 92%,transparent);
  box-shadow:0 -18px 40px -34px var(--fh-shadow-color);
  backdrop-filter:blur(18px) saturate(130%);
}
.actionbar{
  padding-bottom:calc(12px + env(safe-area-inset-bottom,0px));
}
.scroll{
  padding-bottom:calc(126px + env(safe-area-inset-bottom,0px));
}
/* THE ACTION BAR STAYS UP ON AN EMPTY MODULE, AND THAT IS A REVERSAL.
   This block used to read:

     body:has(#scroll .empty) #actionbar { display:none; }
     body:has(#scroll .empty) .scroll    { padding-bottom:calc(32px + safe-area); }

   which was aimed at a real problem the brief named: on module-18 the fixed bar can cover the
   empty state. Hiding the bar does clear the overlap. It also removes the only control that
   creates the first record, on a screen whose own words are "No permits yet. Tap below to raise
   one." Rendered on 2026-08-19 across the 18 module pages, that is exactly what happened: the
   empty state told the user to tap below and there was nothing below to tap.

   It is the first thing a new client sees in a module, so it is the worst possible screen to take
   the button off.

   The overlap is fixed the other way instead: leave the bar where it is and keep the full bottom
   padding on the scroller, which is what .scroll already declares above, so the empty card clears
   the bar rather than hiding behind it. Nothing is covered and nothing is removed. */

/* Known phone corrections that can be made safely with presentation alone. */
.board-row .bmain,
.board-row .bsub{
  display:block;
}
.board-row .bsub{
  margin-top:3px;
  line-height:1.4;
}

@media(max-width:820px){
  .wrap{
    padding-left:16px;
    padding-right:16px;
  }
  header.bar{
    border-radius:var(--fh-radius-md);
  }
  .statgrid{
    grid-template-columns:repeat(2,minmax(0,1fr));
  }
  .featured{
    grid-template-columns:1fr;
  }
  .tile{
    min-height:142px;
  }
  .home-head{
    padding:22px 20px 30px !important;
  }
}

@media(max-width:430px){
  .stat-row{
    display:grid;
    grid-template-columns:repeat(2,minmax(0,1fr));
  }
  .stat-row .stat{
    min-width:0;
  }
  .auth-card{
    padding:30px 22px 24px;
  }
}

@media(max-width:820px) and (pointer:coarse){
  input,
  select,
  textarea,
  .ctl,
  .fin,
  .fta,
  .srcwrap textarea{
    font-size:16px !important;
  }
  button,
  .btn,
  .back,
  .bar .back,
  .navlink,
  .iconbtn,
  .exbtn,
  .chip,
  .pill{
    min-height:44px;
  }
}

@media(prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;
    animation-iteration-count:1 !important;
    scroll-behavior:auto !important;
    transition-duration:.001ms !important;
  }
}

@media print{
  .sheet,
  .sheet *{
    box-shadow:none !important;
    text-shadow:none !important;
  }
}

/* ============================================================================
   THE MODULE TOP BAR CARRIES THE HERO TREATMENT, August 19, 2026.
   The bar reading the company name sits at the top of all 18 modules, and the theme had left it
   as a pale sticky strip. The hero block on the 11 modules that have one already had the look
   Bryant wants, so this is that same recipe moved onto the bar, which every module has.

   Scoped with body:has(#scroll) because only the module pages carry #scroll. index.html and the
   Executive Dashboard use the same header.bar class and are deliberately left alone.

   Every color here is a brand token, so a client with an orange palette gets an orange bar. The
   three background layers are the original recipe, verbatim: a fine diagonal weave, a corner
   highlight, and the brand ramp underneath. */
body:has(#scroll) header.bar{
  position:sticky;
  overflow:hidden;
  color:#fff;
  border-bottom-color:transparent;
  background:
    repeating-linear-gradient(135deg,rgba(255,255,255,.05) 0 1px,transparent 1px 12px),
    radial-gradient(130% 150% at 88% -25%,rgba(255,255,255,.16),transparent 55%),
    linear-gradient(135deg,var(--fh-brand-bright) 0%,var(--fh-brand) 48%,var(--fh-brand-strong) 100%);
  box-shadow:0 18px 40px -24px color-mix(in srgb,var(--fh-brand) 55%,transparent);
}
body:has(#scroll) header.bar .ttl,
body:has(#scroll) header.bar .ttl.co{
  position:relative;
  z-index:1;
  color:#fff;
}
/* The back control and the nav that module-nav.js injects both have to stay legible on the brand
   fill, and both are outside this stylesheet's markup, so they are restyled rather than moved. */
body:has(#scroll) header.bar .back,
body:has(#scroll) header.bar .fhnav,
body:has(#scroll) header.bar .fhnav-back,
body:has(#scroll) header.bar .fhnav-fwd,
body:has(#scroll) header.bar .fhnav-close{
  position:relative;
  z-index:1;
  color:#fff;
  background:rgba(255,255,255,.14);
  border-color:rgba(255,255,255,.24);
}
body:has(#scroll) header.bar .fhnav-lbl{
  color:#fff;
}
/* THE WATERMARK. #brandLogo already holds the client logo when one is published and the F&H seal
   otherwise, so this needs no new markup and no new asset: it takes the image that is already in
   the bar and enlarges it into the corner mark, the way the hero block does. soft-light keeps it
   as a texture rather than a second logo competing with the name. */
body:has(#scroll) header.bar .brand-logo{
  /* !important because the page hides this image from JAVASCRIPT, as an inline style, and an
     inline style beats any selector. It is hidden because the bar shows the module name instead
     of the logo, which was the right call when the bar was a pale strip. On the brand fill there
     is room for both, and the image is already the client's when one is published. */
  display:block !important;
  position:absolute;
  /* Far enough right that the seal is a corner texture and not a disc sitting under the close
     control. The hero block does the same thing with right:-30px bottom:-40px; the bar is shorter,
     so it needs more horizontal offset to read the same way. z-index keeps it behind the title
     and the nav, which are both set position:relative above for exactly that reason. */
  right:-52px;
  top:50%;
  transform:translateY(-50%);
  z-index:0;
  width:150px;
  height:150px;
  max-width:none;
  border-radius:50%;
  opacity:.3;
  mix-blend-mode:soft-light;
  pointer-events:none;
}

/* ============================================================================
   THE KPI TILE ACCENT BARS, Executive Dashboard.
   .tiles carries margin-top:-22px so the cards straddle the hero, which looks good and hides the
   top row's 3px accent: that bar is the first 3px of the tile, so on the top row it meets the
   purple hero rather than the page. .tile.b's accent IS purple, so on the top row it had no edge
   to read against at all. The bottom row was never affected, which is why the two rows looked
   inconsistent rather than broken.

   Dropping the overlap puts every accent on the light page. The cards keep their shadow, so they
   still read as lifted. */
/* Positive, not zero. Zero was not enough: the hero ends at the exact pixel the tile begins, so
   .tile.b's purple accent still had purple immediately above it and no edge to read against.
   Measured on the rendered page, the hero's last row and the tile's first row were adjacent, both
   rgb(91,43,214). A real gap is what makes a 3px bar of ANY color legible. */
.tiles{
  margin-top:14px;
}
/* A tile with no state still gets a bar, and at .3 opacity on a light surface it read as a
   rendering fault rather than as "nothing to report". Full strength, quiet color. */
.tile::before{
  opacity:1;
  background:var(--fh-line-strong);
}
.tile.s::before{ background:var(--fh-signal-green); }
.tile.c::before{ background:var(--fh-signal-yellow); }
.tile.d::before{ background:var(--fh-signal-red); }
.tile.b::before{ background:var(--fh-brand); }
