/* ============================================================================
   TRADE HANDY AUSTRALIA — th-components.css  ·  v1.0.0  ·  4 Aug 2026

   Load order: th-core.css → th-tokens.css → th-semantic.css → th-components.css

   WHY THIS EXISTS — Sean, 4 Aug:
     "you keep using the same moduals for infomation"

   He was right. Four patterns were doing every job: icon+title+subtitle row,
   label/value grid, chip, card. All four are FLAT, UNRANKED, NON-TEMPORAL and
   NON-COMPARATIVE. They can express identity and attribute. They cannot
   express any of this:

     magnitude vs expectation  -> needs a stat tile with a delta
     trend over time           -> needs a sparkline
     sequence of events        -> needs a timeline
     position in a process     -> needs a stepper
     part-to-whole             -> needs a share bar
     density over a period     -> needs a heatmap
     consequence / risk        -> needs a callout with a severity taxonomy
     absence                   -> needs a real empty state
     who is involved           -> needs an avatar stack

   So this file is a VOCABULARY KEYED TO INFORMATION TYPE, not more cards.

   EVERY module here obeys:
     · tokens only, zero raw hex
     · colour is never the sole carrier of meaning (WCAG 1.4.1)
     · tabular numerals on every number
     · a designed loading state and a designed empty state
     · prefers-reduced-motion honoured via th-tokens.css
   ========================================================================= */

/* ═══════════════════════════════════════════════════════════════════════
   ICONS
   ═══════════════════════════════════════════════════════════════════════ */
.ic{display:block;flex:none;stroke:currentColor;fill:none}
.ic-12{width:12px;height:12px}
.ic-16{width:16px;height:16px}
.ic-20{width:20px;height:20px}
.ic-24{width:24px;height:24px}

/* ═══════════════════════════════════════════════════════════════════════
   BUTTONS — the five things that stop a button being a coloured div:
   1 inner edge ring · 2 top highlight · 3 press transform
   4 filter hover (works for any brand colour, can't drift) · 5 160ms ease-out
   ═══════════════════════════════════════════════════════════════════════ */
.btn{
  --h:var(--ctl-md);
  height:var(--h); line-height:var(--h);      /* mathematically centred label */
  padding:0 14px;                              /* ~= 0.4 x height             */
  gap:var(--sp-075);
  display:inline-flex; align-items:center; justify-content:center;
  font-family:inherit; font-size:var(--fs-14); font-weight:var(--fw-medium);
  letter-spacing:var(--ls-14);
  border:0; border-radius:var(--rad-md); cursor:pointer; white-space:nowrap;
  text-decoration:none; position:relative;
  transition:var(--dur) var(--ease-out);
  transition-property:background-color,color,box-shadow,opacity,filter,transform;
}
.btn-sm{--h:var(--ctl-sm);padding:0 12px;font-size:var(--fs-13)}
.btn-lg{--h:var(--ctl-lg);padding:0 20px;font-size:var(--fs-15)}

.btn-primary{
  background:var(--brand-solid); color:var(--brand-fg);
  box-shadow:var(--btn-edge), var(--btn-highlight), var(--sh-xs);
}
.btn-primary:hover{filter:brightness(1.08)}
.btn-primary:active{filter:brightness(.98);transform:scale(.97)}

.btn-secondary{
  background:var(--surface-2); color:var(--tx);
  box-shadow:var(--ring), var(--sh-xs);
}
.btn-secondary:hover{background:var(--surface-3)}
.btn-secondary:active{transform:scale(.97)}

.btn-ghost{background:transparent;color:var(--mut)}
.btn-ghost:hover{background:var(--surface-2);color:var(--tx)}
.btn-ghost:active{transform:scale(.97)}

/* Destructive is RESTRAINED at rest — red text on neutral. It only goes solid
   red on hover, or inside a confirm dialog (the moment of commitment).
   Solid red at rest over-alarms for a delete nobody has chosen yet. */
.btn-danger{background:transparent;color:var(--sig-danger-on);box-shadow:var(--ring)}
.btn-danger:hover{background:var(--sig-danger);color:var(--fg-on-solid);box-shadow:none}
.btn-danger:active{transform:scale(.97)}
.btn-danger-solid{background:var(--sig-danger);color:var(--fg-on-solid);box-shadow:var(--btn-edge),var(--sh-xs)}

/* Explicit disabled tokens — NOT opacity on the whole element. Opacity also
   fades borders and shadows and makes mud on tinted backgrounds. */
.btn:disabled,.btn[aria-disabled="true"]{
  opacity:.5; cursor:not-allowed; transform:none; filter:none; pointer-events:none;
}

/* Loading WITHOUT layout shift: grid-stack the label and spinner in one cell
   and toggle opacity. The button keeps its width; the label stays in the
   accessibility tree. */
.btn-stack{display:grid;place-items:center}
.btn-stack>*{grid-area:1/1}
.btn[data-loading]{pointer-events:none}
.btn[data-loading] .btn-label{opacity:0}
.btn:not([data-loading]) .btn-spin{opacity:0}
.btn-spin{width:15px;height:15px;border:2px solid currentColor;border-right-color:transparent;
  border-radius:50%;animation:th-spin .7s linear infinite}
@keyframes th-spin{to{transform:rotate(360deg)}}

/* ═══════════════════════════════════════════════════════════════════════
   INPUTS — heights bound to button heights. A 40px input beside a 36px
   button in the same row is a named amateur tell.
   Padding-x 13px is optical correction for the 1px border + side bearing.
   ═══════════════════════════════════════════════════════════════════════ */
.field{display:block;margin-bottom:var(--sp-2)}
.field>label{display:block;font-size:var(--fs-13);font-weight:var(--fw-medium);
  margin-bottom:var(--sp-1);color:var(--tx)}
.field .opt{font-weight:var(--fw-regular);color:var(--mut)}
.input,.select,.textarea{
  width:100%; min-height:var(--ctl-md);
  padding:10px 13px;
  font-family:inherit; font-size:var(--fs-14); line-height:var(--lh-14); color:var(--tx);
  background:var(--surface-1); border:1px solid var(--hairline);
  border-radius:var(--rad-md); outline:0;
  transition:var(--dur-fast) var(--ease-out);
  transition-property:border-color,box-shadow,background-color;
}
.textarea{min-height:88px;resize:vertical}
.input::placeholder,.textarea::placeholder{color:var(--mut);opacity:.75}
.input:hover,.select:hover,.textarea:hover{border-color:var(--hairline)}
.input[aria-invalid="true"]{border-color:var(--sig-danger-line)}

/* Inputs clear the UA outline above; the visible ring is declared here so the
   component file is self-contained (and so a per-file audit can see it). */
.input:focus-visible,.select:focus-visible,.textarea:focus-visible{
  outline:2px solid var(--focus); outline-offset:2px;
  border-color:transparent; box-shadow:none;
}
.field .hint{font-size:var(--fs-12);color:var(--mut);margin-top:var(--sp-05)}
.field .err{font-size:var(--fs-12);color:var(--sig-danger-on);margin-top:var(--sp-05);
  display:flex;gap:var(--sp-05);align-items:center}

/* 🔴 iOS Safari zooms the viewport when an input under 16px takes focus. */
@media (max-width:640px){ .input,.select,.textarea{font-size:var(--fs-16)} }

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 1 · STAT TILE — magnitude vs expectation
   The four cards were flat: they could show a number but never whether it
   was GOOD. A stat tile carries value + delta + baseline + trend.
   ═══════════════════════════════════════════════════════════════════════ */
.stat{
  background:var(--surface-1); border:1px solid var(--hairline);
  border-radius:var(--rad-lg); padding:var(--sp-25);
  display:flex; flex-direction:column; gap:var(--sp-05);
}
.stat .k{font-size:var(--fs-12);color:var(--mut);text-transform:uppercase;
  letter-spacing:.05em;font-weight:var(--fw-medium)}
.stat .v{font-size:var(--fs-32);line-height:var(--lh-32);letter-spacing:var(--ls-32);
  font-weight:var(--fw-semibold);font-variant-numeric:tabular-nums}
.stat .row{display:flex;align-items:center;gap:var(--sp-1);flex-wrap:wrap}
/* Delta carries an ARROW as well as colour — never colour alone. */
.stat .d{display:inline-flex;align-items:center;gap:3px;font-size:var(--fs-13);
  font-weight:var(--fw-medium);font-variant-numeric:tabular-nums}
.stat .d-up{color:var(--c-gain)} .stat .d-down{color:var(--c-loss)}
.stat .d-flat{color:var(--mut)}
.stat .base{font-size:var(--fs-12);color:var(--mut)}   /* "vs last month"      */
.stat .spark{margin-top:var(--sp-1)}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 2 · SPARKLINE — trend, inline. Works in a table cell.
   ═══════════════════════════════════════════════════════════════════════ */
.spark{display:block;width:100%;height:32px;overflow:visible}
.spark path{fill:none;stroke:currentColor;stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
.spark .area{fill:currentColor;opacity:.10;stroke:none}
.spark .dot{fill:currentColor;stroke:var(--surface-1);stroke-width:2}
.spark-sm{height:18px;width:64px}          /* the in-table-cell variant */

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 3 · TIMELINE — sequence of events. Audit trails, job history,
   payment history. The four patterns had no way to express ordering.
   ═══════════════════════════════════════════════════════════════════════ */
.tl{list-style:none;margin:0;padding:0;position:relative}
.tl>li{position:relative;padding:0 0 var(--sp-3) var(--sp-4);}
.tl>li:last-child{padding-bottom:0}
.tl>li::before{content:'';position:absolute;left:9px;top:20px;bottom:-4px;
  width:1px;background:var(--hairline)}
.tl>li:last-child::before{display:none}
.tl .node{position:absolute;left:0;top:4px;width:19px;height:19px;border-radius:50%;
  display:grid;place-items:center;background:var(--surface-2);
  box-shadow:0 0 0 3px var(--surface-0)}
.tl .node-done{background:var(--sig-safe);color:var(--fg-on-solid)}
.tl .node-now{background:var(--brand-solid);color:var(--brand-fg)}
.tl .node-fail{background:var(--sig-danger);color:var(--fg-on-solid)}
.tl .when{font-size:var(--fs-12);color:var(--mut);font-variant-numeric:tabular-nums}
.tl .what{font-size:var(--fs-14);font-weight:var(--fw-medium);margin-top:1px}
.tl .who{font-size:var(--fs-12);color:var(--mut);margin-top:2px}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 4 · STEPPER — position in a process. Quote→Accepted→Scheduled→Done.
   Each step carries a NUMBER and a LABEL, not just a colour.
   ═══════════════════════════════════════════════════════════════════════ */
.step{display:flex;align-items:flex-start;gap:0;counter-reset:s}
.step>li{flex:1;list-style:none;position:relative;text-align:center;padding-top:var(--sp-4)}
.step>li::before{counter-increment:s;content:counter(s);
  position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  font-size:var(--fs-12);font-weight:var(--fw-semibold);
  background:var(--surface-2);color:var(--mut);border:1px solid var(--hairline)}
.step>li::after{content:'';position:absolute;top:13px;left:calc(50% + 17px);
  right:calc(-50% + 17px);height:1px;background:var(--hairline)}
.step>li:last-child::after{display:none}
.step .s-done::before{content:'';background:var(--sig-safe);border-color:transparent;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  background-size:14px;background-position:center;background-repeat:no-repeat}
.step .s-done::after{background:var(--sig-safe)}
.step .s-now::before{background:var(--brand-solid);color:var(--brand-fg);border-color:transparent;
  box-shadow:0 0 0 4px var(--brand-tint)}
.step .lbl{font-size:var(--fs-13);font-weight:var(--fw-medium)}
.step .sub{font-size:var(--fs-12);color:var(--mut);margin-top:1px}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 5 · SHARE BAR — part-to-whole. Where the money went, load mix.
   Segments carry a LABEL in the legend, never colour alone.
   ═══════════════════════════════════════════════════════════════════════ */
.share{display:flex;height:10px;border-radius:var(--rad-full);overflow:hidden;
  background:var(--surface-2);gap:2px}
.share>span{display:block;height:100%}
.share-legend{display:flex;flex-wrap:wrap;gap:var(--sp-15);margin-top:var(--sp-1)}
.share-legend>span{display:inline-flex;align-items:center;gap:var(--sp-075);
  font-size:var(--fs-12);color:var(--mut)}
.share-legend i{width:9px;height:9px;border-radius:3px;display:block;flex:none}
.share-legend b{color:var(--tx);font-weight:var(--fw-medium);font-variant-numeric:tabular-nums}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 6 · PROGRESS — bar and ring.
   🔴 GREEN ONLY AT 100%. A bar that is green at 30% is lying.
   ═══════════════════════════════════════════════════════════════════════ */
.prog{height:6px;border-radius:var(--rad-full);background:var(--surface-2);overflow:hidden}
.prog>span{display:block;height:100%;border-radius:var(--rad-full);
  background:var(--brand-solid);transition:width var(--dur-slow) var(--ease-out)}
.prog-done>span{background:var(--sig-safe)}     /* apply at 100% ONLY */
.prog-fail>span{background:var(--sig-danger)}
.prog-lbl{display:flex;justify-content:space-between;font-size:var(--fs-12);
  color:var(--mut);margin-bottom:var(--sp-05);font-variant-numeric:tabular-nums}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 7 · CALLOUT — consequence and risk. Muted tint + emphasis icon +
   NEUTRAL body text. Never set body copy in the signal colour.
   ═══════════════════════════════════════════════════════════════════════ */
.callout{display:flex;gap:var(--sp-15);padding:var(--sp-15) var(--sp-2);
  border-radius:var(--rad-lg);border:1px solid;font-size:var(--fs-14);align-items:flex-start}
.callout .ic{margin-top:2px}
.callout b{display:block;font-weight:var(--fw-semibold);margin-bottom:2px;color:var(--tx)}
.callout p{color:var(--mut);margin:0}
.callout-info  {background:var(--c-info-bg);   border-color:var(--c-info-line);   color:var(--c-info)}
.callout-safe  {background:var(--sig-safe-bg); border-color:var(--sig-safe-line); color:var(--sig-safe-on)}
.callout-warn  {background:var(--sig-warn-bg); border-color:var(--sig-warn-line); color:var(--sig-warn-on)}
.callout-severe{background:var(--c-severe-bg); border-color:var(--sig-warn-line); color:var(--c-severe)}
.callout-danger{background:var(--sig-danger-bg);border-color:var(--sig-danger-line);color:var(--sig-danger-on)}
.callout-must  {background:var(--sig-must-bg); border-color:var(--sig-must-line); color:var(--sig-must-on)}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 8 · PROPERTY PANEL — the right rail of a record page.
   A definition list, not a card grid.
   ═══════════════════════════════════════════════════════════════════════ */
.props{margin:0}
.props>div{display:flex;gap:var(--sp-2);padding:var(--sp-1) 0;
  border-bottom:1px solid var(--hairline-2);align-items:flex-start}
.props>div:last-child{border-bottom:0}
.props dt{flex:0 0 38%;font-size:var(--fs-13);color:var(--mut)}
.props dd{flex:1;margin:0;font-size:var(--fs-13);font-weight:var(--fw-medium);
  font-variant-numeric:tabular-nums;word-break:break-word}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 9 · AVATAR STACK — who is involved, at a glance
   ═══════════════════════════════════════════════════════════════════════ */
.faces{display:inline-flex;align-items:center}
.faces>*{width:26px;height:26px;border-radius:50%;margin-left:-7px;
  border:2px solid var(--surface-0);background:var(--surface-3);
  display:grid;place-items:center;font-size:var(--fs-11);
  font-weight:var(--fw-semibold);color:var(--tx);overflow:hidden}
.faces>*:first-child{margin-left:0}
.faces .more{background:var(--surface-2);color:var(--mut);font-variant-numeric:tabular-nums}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 10 · CALENDAR HEATMAP — density over a period.
   Jobs per day, fuel burn, incidents. A monochrome ramp, never a rainbow.
   ═══════════════════════════════════════════════════════════════════════ */
.heat{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,10px);
  gap:3px;overflow-x:auto;padding-bottom:var(--sp-05)}
.heat i{width:10px;height:10px;border-radius:2px;background:var(--surface-2);display:block}
.heat .l1{background:var(--brand-40)} .heat .l2{background:var(--brand-60)}
.heat .l3{background:var(--brand-70)} .heat .l4{background:var(--brand-80)}
.heat-key{display:flex;align-items:center;gap:var(--sp-05);font-size:var(--fs-11);
  color:var(--mut);margin-top:var(--sp-1)}

/* ═══════════════════════════════════════════════════════════════════════
   MODULE 11 · TABLE — dense index. Right-aligned tabular numerics,
   sticky header, zebra. NEVER a card per row.
   ═══════════════════════════════════════════════════════════════════════ */
.tbl{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--fs-13)}
.tbl thead th{position:sticky;top:0;z-index:1;text-align:left;
  font-size:var(--fs-11);text-transform:uppercase;letter-spacing:.06em;
  font-weight:var(--fw-medium);color:var(--mut);background:var(--surface-1);
  padding:var(--sp-1) var(--sp-15);border-bottom:1px solid var(--hairline);white-space:nowrap}
.tbl tbody td{padding:var(--sp-075) var(--sp-15);border-bottom:1px solid var(--hairline-2);
  height:var(--row-default)}
.tbl tbody tr:hover{background:var(--surface-1)}
.tbl .num{text-align:right;font-variant-numeric:tabular-nums}
.tbl tfoot td{padding:var(--sp-1) var(--sp-15);font-weight:var(--fw-semibold);
  border-top:1px solid var(--hairline);font-variant-numeric:tabular-nums}
.tbl-compact tbody td{height:var(--row-compact);padding:var(--sp-05) var(--sp-15)}

/* Drill-down row */
.tbl .drill{cursor:pointer}
.tbl .drill .caret{transition:transform var(--dur-fast) var(--ease-out)}
.tbl .drill[aria-expanded="true"] .caret{transform:rotate(90deg)}
.tbl .detail td{background:var(--surface-1);padding:var(--sp-2) var(--sp-15)}

/* ═══════════════════════════════════════════════════════════════════════
   SKELETON — for CONTENT of known shape. A spinner is for an ACTION.
   🔴 Never show a loading state that lives under ~300ms — it reads as a
   glitch, not as speed.
   ═══════════════════════════════════════════════════════════════════════ */
.sk{border-radius:var(--rad-md);background:
  linear-gradient(90deg,var(--surface-1) 25%,var(--surface-2) 50%,var(--surface-1) 75%);
  background-size:200% 100%;animation:th-sk 1.3s infinite}
@keyframes th-sk{0%{background-position:200% 0}100%{background-position:-200% 0}}
.sk-line{height:12px;margin:var(--sp-1) 0}
.sk-row{height:56px;margin:var(--sp-075) 0}
.sk-tile{height:104px}

/* ═══════════════════════════════════════════════════════════════════════
   EMPTY STATE — anatomy: icon · what · why · the action.
   A blank panel reading "No results" is a wasted screen.
   ═══════════════════════════════════════════════════════════════════════ */
.empty{padding:var(--sp-6) var(--sp-3);text-align:center;color:var(--mut)}
.empty .ic{margin:0 auto var(--sp-15);color:var(--mut);opacity:.65}
.empty h3{font-size:var(--fs-15);font-weight:var(--fw-semibold);color:var(--tx);
  margin-bottom:var(--sp-05)}
.empty p{font-size:var(--fs-13);max-width:38ch;margin:0 auto}
.empty .btn{margin-top:var(--sp-2)}

/* ═══════════════════════════════════════════════════════════════════════
   TOAST · SEGMENTED CONTROL · SUMMARY BAR
   ═══════════════════════════════════════════════════════════════════════ */
.toast{position:fixed;left:50%;transform:translateX(-50%);bottom:var(--sp-3);z-index:60;
  background:var(--surface-2);border:1px solid var(--hairline);border-radius:var(--rad-lg);
  box-shadow:var(--sh-modal);padding:var(--sp-15) var(--sp-25);font-size:var(--fs-14);
  display:flex;align-items:center;gap:var(--sp-15);opacity:0;pointer-events:none;
  transition:opacity var(--dur) var(--ease-out)}
.toast.on{opacity:1;pointer-events:auto}
.toast .undo{color:var(--brand-90);font-weight:var(--fw-medium);cursor:pointer;background:none;border:0}

.seg{display:inline-flex;gap:2px;padding:3px;background:var(--surface-2);
  border-radius:var(--rad-lg)}
.seg button{flex:1;padding:6px 12px;border-radius:var(--rad-md);font-size:var(--fs-13);
  font-weight:var(--fw-medium);color:var(--mut);border:0;background:none;cursor:pointer;
  transition:var(--dur-fast) var(--ease-out)}
.seg button[aria-selected="true"]{background:var(--surface-0);color:var(--tx);box-shadow:var(--sh-xs)}

.sumbar{display:flex;gap:var(--sp-3);flex-wrap:wrap;padding:var(--sp-15) var(--sp-2);
  background:var(--surface-1);border:1px solid var(--hairline);border-radius:var(--rad-lg)}
.sumbar>div{display:flex;flex-direction:column;gap:2px}
.sumbar .k{font-size:var(--fs-11);text-transform:uppercase;letter-spacing:.05em;color:var(--mut)}
.sumbar .v{font-size:var(--fs-20);font-weight:var(--fw-semibold);font-variant-numeric:tabular-nums}

/* ═══════════════════════════════════════════════════════════════════════
   BENTO — a LAYOUT container, not an information module.
   🔴 Right for a landing surface where genuinely different densities coexist.
      WRONG for a set of items that are all the same type — ten equal tiles
      is a card grid, not a bento.
   ═══════════════════════════════════════════════════════════════════════ */
.bento{display:grid;gap:var(--sp-15);grid-template-columns:repeat(4,1fr)}
.bento>.w2{grid-column:span 2} .bento>.w3{grid-column:span 3}
.bento>.w4{grid-column:span 4} .bento>.h2{grid-row:span 2}
@media (max-width:900px){
  .bento{grid-template-columns:repeat(2,1fr)}
  .bento>.w3,.bento>.w4{grid-column:span 2}
}
@media (max-width:560px){
  .bento{grid-template-columns:1fr}
  .bento>.w2,.bento>.w3,.bento>.w4{grid-column:span 1}
}

/* ═══════════════════════════════════════════════════════════════════════
   FIELD MODE — gloved thumb, sunlight, one hand.
   Standard work gloves reduce effective touch precision to 20-25mm;
   44px is NOT enough for a primary field action.
   ═══════════════════════════════════════════════════════════════════════ */
.field-mode .btn{--h:var(--tap-field);font-size:var(--fs-16);padding:0 var(--sp-3)}
.field-mode .input,.field-mode .select{min-height:var(--tap-field);font-size:var(--fs-16)}
.field-mode .tbl tbody td{height:var(--row-comfortable)}
