/* winsorlabs shared components — the one copy every app ships, after tokens.css.
   Canonical source: build-an-app/assets/components.css in catjwin/claude-skills.
   Copy it unchanged next to tokens.css; never edit an app's copy.
   Picked 15 Oct 2026 (component picks 1D 2A+2B 3A 4C 5B 6A 7B 8B 9D 10D 11B 12B).
   Rules are kept shallow (one class, or an element plus one class; inputs use :where(),
   so no specificity at all), so an app's own rule, written after this file, always wins.
   Sections 14–18 (31 Oct 2026) hold the pieces every app used to copy into its own
   <style>: page base, app header, bottom tabs, bottom sheet, icon and link buttons. */

/* Page base — the same in every app, so the apps' <style> starts with their own screens. */
:root { color-scheme:dark; }
* { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
[hidden] { display:none !important; }   /* hidden stays hidden, whatever display a class gives it */
html, body { margin:0; background:var(--bg); color:var(--text); }
html { min-height:100%; }
body { font:var(--type-body); -webkit-font-smoothing:antialiased; overflow-x:clip; }
button, input, textarea, select { font:inherit; color:inherit; }
main { max-width:var(--measure); margin:0 auto; padding:var(--pad-screen); }
.num { font-variant-numeric:tabular-nums; }
.sub { color:var(--dim); font-size:var(--text-xs); }   /* a quiet second line */

/* The page itself never bounces past its ends: on iOS the overshoot dragged the
   sticky header up under the status bar, where the system blurs it. Scrolling
   is unchanged; panels and lists keep their own overscroll. */
html, body { overscroll-behavior-y:none; }

/* 0 · App frame — opt in with <html class="frame"> and wl.frame(). The page itself
   never scrolls: the header stays put and only <main> scrolls, so nothing slides up
   under the iOS status bar, which blurs whatever sits under it (header included).
   The body and full-screen panels are pinned to the visible area (--vvh/--vvtop,
   kept current by wl.frame()), so the keyboard can't push the header off the top
   either. Fixed bars (tabs, toasts, sheets) sit over it as before. The one place
   this file reaches past one class: it has to beat each app's sticky header. */
html.frame { height:100%; overflow:hidden; overscroll-behavior:none; }
html.frame body { position:fixed; left:0; right:0; top:var(--vvtop,0px); height:var(--vvh,100%); min-height:0;
  overflow:hidden; display:flex; flex-direction:column; }
html.frame body > header { flex:none; position:relative; top:auto; backdrop-filter:none; -webkit-backdrop-filter:none; }
html.frame body > main { flex:1; min-height:0; width:100%; overflow-y:auto; overscroll-behavior-y:contain; }
html.frame .panel { top:var(--vvtop,0px); bottom:auto; height:var(--vvh,100%); }

/* Icons from icons.js: text-sized and inline unless a component sizes them. */
svg.ph { width:1em; height:1em; display:inline-block; vertical-align:-.125em; flex:none; }

/* 1 · Primary button — the one commit action. 46px, pill. */
.btn { display:inline-flex; align-items:center; justify-content:center; gap:.4rem;
  min-height:var(--tap-min); padding:var(--pad-button) 1.4rem; border:0; border-radius:var(--radius-pill);
  background:var(--action-primary); color:var(--text-on-primary); box-shadow:var(--shadow-primary);
  font:var(--fw-bold) var(--text-body-size)/1.2 var(--font-body); text-decoration:none; text-align:center;
  cursor:pointer; transition:var(--transition-button); }
.btn:active { transform:var(--press-shift); background:var(--action-primary-press); }
.btn:disabled { opacity:.45; box-shadow:none; cursor:default; transform:none; }
.btn.full { width:100%; }
/* the one small size: panel headers and tight rows */
.btn.small { min-height:2.3rem; padding:.35rem .95rem; font-size:.9rem; box-shadow:none; }

/* 2 · Secondary buttons. Quiet is the standard; voice is the app offering something. */
.btn.quiet { background:var(--raised); color:var(--ink2); border:1px solid var(--line); box-shadow:none; }
.btn.quiet:active { background:var(--panel); }
.btn.voice { background:var(--accent-tint); color:var(--accent-light); border:1px solid var(--accent-line); box-shadow:none; }
.btn.voice:active { background:color-mix(in srgb, var(--accent) 20%, transparent); }
.btn.danger { color:var(--bad); border-color:var(--bad-line); }
/* Tinted buttons for the other jobs (31 Oct 2026), so buttons that do different things look
   different: .tint (violet) records or adds something, .data (cyan) is about something
   measured, .good (green) says all is well. Same shape as .voice. */
.btn.tint { background:var(--primary-tint); color:var(--primary-ink); border:1px solid color-mix(in srgb, var(--primary) 45%, transparent); box-shadow:none; }
.btn.tint:active { background:color-mix(in srgb, var(--primary) 32%, transparent); }
.btn.data { background:var(--neon-tint); color:var(--neon-dim); border:1px solid color-mix(in srgb, var(--neon) 35%, transparent); box-shadow:none; }
.btn.data:active { background:color-mix(in srgb, var(--neon) 22%, transparent); }
.btn.good { background:var(--good-tint); color:var(--good); border:1px solid color-mix(in srgb, var(--good) 35%, transparent); box-shadow:none; }
.btn.good:active { background:color-mix(in srgb, var(--good) 22%, transparent); }
/* two-tap confirm (shell.js arm()): the first tap arms. The button turns solid with a ring,
   red when the second tap removes something, green when it goes ahead; it says "Tap again"
   on itself, a bar along its bottom runs down the seconds left, and a toast says what the
   next tap does. The colours are !important: an app's own style for a row or icon button
   (background:none) must never hide the armed state. */
.btn.armed, .armed { color:var(--bg) !important; background:var(--bad) !important; border-color:var(--bad) !important; box-shadow:0 0 0 4px var(--bad-tint); }
.btn.armed-go, .armed-go { color:var(--bg) !important; background:var(--good) !important; border-color:var(--good) !important; box-shadow:0 0 0 4px var(--good-tint); }
.btn.armed, .btn.armed-go, .chip.armed, .chip.armed-go { font-weight:700; }
.arming { position:relative; overflow:hidden; }
.arming::after { content:""; position:absolute; left:0; right:0; bottom:0; height:3px; background:currentColor; opacity:.75;
  transform-origin:left; animation:wl-arm-left var(--arm-ms, 5s) linear forwards; pointer-events:none; }
@keyframes wl-arm-left { from { transform:scaleX(1); } to { transform:scaleX(0); } }
/* An icon-only button, armed, grows into a pill that says "Tap again" in place of its icon
   (shell.js adds .arm-pill and the .arm-say words). !important beats the app's round size. */
.arm-pill { width:auto !important; min-width:var(--tap-min); padding:0 .75rem !important; border-radius:var(--radius-pill) !important;
  display:inline-flex; align-items:center; justify-content:center; }
.arm-pill > :not(.arm-say) { display:none !important; }
.arm-pill > .arm-say { font:700 var(--text-2xs)/1.15 var(--font-body); white-space:nowrap; }

/* 3 · Selectable chips — tap to choose. Dim until picked. Use them where you can pick
   several (weekdays); for one choice see section 13. */
.chips { display:flex; flex-wrap:wrap; gap:.35rem; }
.chip { display:inline-flex; align-items:center; justify-content:center; gap:.3rem;
  min-height:2.5rem; padding:.4rem .75rem; border-radius:var(--radius-pill);
  background:var(--raised); border:1px solid var(--line); color:var(--dim);
  font:600 .84rem/1.2 var(--font-body); cursor:pointer; }
.chip.on { background:var(--primary-tint); border-color:var(--primary); color:var(--primary-ink); }
.chip.small { min-height:2rem; padding:.2rem .65rem; font-size:.8rem; }

/* 4 · Label chips — not tappable: a status, a type, a count. Tones by job. */
.tag { display:inline-flex; align-items:center; gap:.3rem; padding:.2rem .6rem; border-radius:var(--radius-pill);
  font:600 var(--text-2xs)/1.3 var(--font-body); white-space:nowrap;
  background:var(--raised); color:var(--dim); border:1px solid var(--line); }
.tag.data { background:var(--neon-tint); color:var(--neon); border-color:color-mix(in srgb, var(--neon) 35%, transparent); }
.tag.warn { background:var(--warn-tint); color:var(--warn); border-color:transparent; }
.tag.good { background:var(--good-tint); color:var(--good); border-color:transparent; }
.tag.bad { background:var(--bad-tint); color:var(--bad-ink); border-color:transparent; }
.tag.type { background:var(--primary-tint); color:var(--primary-ink); border-color:transparent; }

/* 5 · Section label — the uppercase line that divides a screen. */
.eyebrow { display:flex; justify-content:space-between; align-items:baseline; gap:.6rem;
  margin:1.6rem 0 .6rem; color:var(--dim);
  font:var(--type-label); letter-spacing:var(--track-label); text-transform:uppercase; }
.eyebrow.warn { color:var(--warn); }   /* this section needs attention */
.eyebrow .hint { letter-spacing:0; text-transform:none; font-weight:var(--fw-medium); color:var(--dim2); }

/* 6 · Full-screen panel: fixed header (Back · title · action), only the body scrolls. */
.panel { position:fixed; inset:0; z-index:30; background:var(--bg); display:flex; flex-direction:column; }
.panel-head { flex:none; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:.5rem;
  padding:calc(.55rem + env(safe-area-inset-top)) .8rem .55rem; background:var(--panel); border-bottom:1px solid var(--line); }
.panel-head h2 { margin:0; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  font:var(--fw-black) 1.05rem/1.2 var(--font-display); letter-spacing:-.01em; }
.panel-head h2.data { font:var(--fw-bold) .85rem/1.2 var(--font-body); letter-spacing:0; color:var(--neon); font-variant-numeric:tabular-nums; }
.panel-head .back { display:inline-flex; align-items:center; gap:.15rem; min-width:4.2rem; padding:.5rem .3rem; background:none; border:0; text-align:left; cursor:pointer;
  color:var(--accent); font:600 .95rem var(--font-body); }
.panel-head .act { min-width:4.2rem; display:flex; justify-content:flex-end; }
.panel-body { flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; overscroll-behavior:contain;
  width:100%; max-width:var(--measure); margin:0 auto; padding:.9rem 1rem calc(1.5rem + env(safe-area-inset-bottom)); }
body.panel-open { overflow:hidden; }

/* 7 · Empty state — two short lines: <div class="empty"><b>No trips yet</b>Scan your inbox…</div> */
.empty { text-align:center; color:var(--dim); padding:3rem 1rem; line-height:1.6; }
.empty b { display:block; margin-bottom:.4rem; color:var(--text); font:var(--type-title); }

/* 8 · Sign-in — the app's name, one line, the primary button. */
.signin { text-align:center; padding:4rem 1rem; }
.signin h1 { margin:0 0 .4rem; font:var(--fw-black) 1.7rem/1.1 var(--font-display); letter-spacing:var(--track-display); }
.signin p { margin:0 0 1.6rem; color:var(--dim); }

/* 9 · Text inputs — raised, a 1.5px border you can find on a dark screen, 44px tall. */
:where(input:not([type=checkbox], [type=radio], [type=range], [type=file], [type=color], [type=button], [type=submit]), select, textarea) {
  width:100%; min-height:var(--tap-min); padding:.8rem; border-radius:var(--radius-sm);
  background:var(--raised); color:var(--text); border:var(--border-input) solid var(--line-strong);
  font:var(--type-body); font-size:16px; outline:none; }
:where(input, select, textarea):focus { border-color:var(--primary); box-shadow:0 0 0 4px var(--primary-tint); }
:where(input, textarea)::placeholder { color:var(--dim2); }

/* 10 · Banner — one shape, four tones by job. Plain text (with <br> or <b>) flows as a
   block; with a .dismiss button at the end it becomes a row. */
.banner { display:block; margin:.8rem 0; padding:.7rem .9rem;
  border-radius:var(--radius-card); font-size:var(--text-xs); line-height:1.45; }
.banner:has(> .dismiss) { display:flex; align-items:flex-start; gap:.6rem; }
.banner.warn { background:var(--warn-tint); border:1px solid var(--warn-line); color:var(--warn); }
.banner.bad { background:var(--bad-tint); border:1px solid var(--bad-line); color:var(--bad-ink); }
.banner.voice { background:var(--accent-tint); border:1px solid var(--accent-line); color:var(--accent-light); }
.banner.data { background:var(--neon-tint); border:1px solid color-mix(in srgb, var(--neon) 35%, transparent); color:var(--neon); }
.banner .dismiss { margin-left:auto; flex:none; padding:0 .2rem; background:none; border:0; color:inherit; cursor:pointer; font:inherit; }

/* 11 · Toast — above everything; 2.6s for a message, 6s with Undo (set in the app's toast()). */
/* Centred by margin, not left:50% + translate, which capped it at half the screen and wrapped
   longer messages into a tall, narrow pill. One line reads as a pill; two lines as a rounded card. */
.toast { position:fixed; left:1rem; right:1rem; margin-inline:auto; width:fit-content; max-width:min(30rem, calc(100% - 2rem));
  bottom:calc(5.4rem + env(safe-area-inset-bottom)); z-index:50;
  display:flex; gap:.8rem; align-items:center;
  padding:.65rem .6rem .65rem 1.1rem; border-radius:1.4rem; line-height:1.35;
  background:var(--raised); border:1px solid var(--line-strong); box-shadow:var(--shadow-float); font-size:.9rem; }
/* Taps go through the toast to what's under it (an armed button's second tap), except its Undo. */
.toast { pointer-events:none; }
.toast button { pointer-events:auto; }
.toast span { flex:1; min-width:0; }
.toast:not(:has(button)) { padding-right:1.1rem; }
.toast button { flex:none; white-space:nowrap; padding:.4rem .8rem; border:0; border-radius:var(--radius-pill); cursor:pointer;
  background:var(--primary-tint); color:var(--primary-ink); font-weight:var(--fw-bold); }

/* 12 · Version footer — "health 261015-d · you@… · Sign out" */
.appver { margin:2rem 0 1rem; text-align:center; color:var(--dim2); font-size:.75rem; font-variant-numeric:tabular-nums; }
.appver a { color:var(--accent); font-weight:600; text-decoration:none; }

/* 13 · Choosing one thing. Pick the control by the options (wl.toggle / wl.seg / wl.dropdown):
     on or off → a switch: <button class="switch" role="switch" aria-checked="true"><span></span></button>,
       on a row with its label: <div class="switch-field"><span>Label</span>{switch}<span class="hint">…</span></div>
     two or three short options, or a short scale that fits one row (30d 3m 6m 1y) → segmented:
       <div class="chips seg"><button class="chip on">…</button>…</div>
     four or more, or long labels → a dropdown: a plain <select> (styled here, with its chevron).
   Chips that act on one tap (log "Mild", open a condition, fill a place) are buttons, not
   choices, and stay as they are. */
/* A segmented control drawn by hand (<div class="seg"><button class="on">…</button>…</div>,
   the app setting .on itself) looks the same as wl.seg()'s. */
.chips.seg, .seg { display:grid; grid-auto-flow:column; grid-auto-columns:minmax(0,1fr); gap:3px; padding:3px;
  background:var(--raised); border:1px solid var(--line); border-radius:var(--radius-sm); }
.chips.seg .chip, .seg > button { min-height:2.4rem; border:0; background:transparent; border-radius:calc(var(--radius-sm) - 3px); padding:.3rem .5rem; }
.seg > button { display:inline-flex; align-items:center; justify-content:center; gap:.3rem; cursor:pointer;
  color:var(--dim); font:600 .84rem/1.2 var(--font-body); }
.chips.seg .chip.on, .seg > button.on { background:var(--primary-tint); color:var(--primary-ink); }
.chips.seg .chip.on.p-C, .chips.seg .chip.on.p-R { background:var(--p-tint); color:var(--p-ink); }
select { appearance:none; -webkit-appearance:none; padding-right:2.4rem; cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='%23A99BBF' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right .8rem center; background-size:14px; }
.switch { position:relative; width:3.1rem; height:1.85rem; flex:none; padding:0; border-radius:var(--radius-pill); cursor:pointer;
  background:var(--raised); border:1px solid var(--line-strong); transition:var(--transition-button); }
.switch > span { position:absolute; top:3px; left:3px; width:calc(1.85rem - 8px); height:calc(1.85rem - 8px); border-radius:50%;
  background:var(--dim2); transition:transform .18s ease, background .18s ease; }
.switch[aria-checked="true"] { background:var(--primary); border-color:var(--primary); }
.switch[aria-checked="true"] > span { transform:translateX(1.25rem); background:#fff; }
.switch-field, .field.switch-field { display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; column-gap:.8rem; }
.switch-field > *, .field.switch-field > * { grid-column:1 / -1; }
.switch-field > span:first-child, .field.switch-field > span:first-child { grid-column:1; }
.switch-field > .switch, .field.switch-field > .switch { grid-column:2; grid-row:1; }

/* 14 · App header — wordmark on the left, the app's own controls on the right, a quiet
   status line (.pending: offline, waiting to send) under it. Solid, and outside the scroll in
   the app frame (section 0), so nothing passes under it or the status bar. With the frame, the
   status-bar gap sits on the page above the header, so the newer iOS doesn't fade its top edge.
   <header class="app"><div class="inner"><div class="wordmark">health</div>…</div></header> */
html.frame body { padding-top:env(safe-area-inset-top, 0px); }   /* every app: not only those with header.app, or iOS blurs the top */
header.app { z-index:5; background:var(--bg); border-bottom:2px solid var(--app);
  padding:var(--space-4) max(16px, env(safe-area-inset-right, 0px)) var(--space-4) max(16px, env(safe-area-inset-left, 0px)); }
header.app .inner { max-width:var(--measure); margin:0 auto; display:flex; align-items:center; justify-content:space-between; gap:.8rem; min-height:2.3rem; }
/* A switcher for the whole screen (Health's day), in the header's own row between the name and
   the gear, so every header stays one row high: arrows either side, what's showing in the middle
   on two short lines.
   <div class="hdr-switch"><button class="ibtn">‹</button><button class="hdr-title"><b>Today</b><span>Sun 1 Nov</span></button><button class="ibtn">›</button></div> */
header.app .hdr-switch { flex:1; min-width:0; display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; }
.hdr-title { display:flex; flex-direction:column; align-items:center; justify-content:center; min-width:0; min-height:var(--tap-min); padding:0 var(--space-2);
  background:none; border:0; color:var(--text); cursor:pointer; font:inherit; line-height:1.15; }
.hdr-title b { font:var(--fw-black) var(--text-body-size)/1.15 var(--font-display); letter-spacing:-.01em; white-space:nowrap; }
.hdr-title span { color:var(--dim); font-size:var(--text-xs); font-variant-numeric:tabular-nums; white-space:nowrap; }
.hdr-title span.jump { color:var(--accent); font-weight:var(--fw-bold); }
/* the header's right-hand controls, with the Settings gear last */
header.app .acts { display:flex; align-items:center; gap:var(--space-2); }
header.app .pending { max-width:var(--measure); margin:.2rem auto 0; color:var(--warn); font-size:var(--text-2xs); }
.wordmark { font:var(--fw-black) var(--text-header)/1 var(--font-display); letter-spacing:var(--track-display); color:var(--app); user-select:none; cursor:default; }

/* 15 · Bottom tabs — fixed to the foot of the screen; the page keeps room for them. Each tab
   is an icon over a word; the one you're on takes the app's colour (--app).
   <nav class="tabs"><div class="inner"><a href="#day" class="on">{icon}Day</a>…</div></nav>
   Tabs can be links or buttons. */
body:has(> nav.tabs) { padding-bottom:calc(4.6rem + env(safe-area-inset-bottom)); }
nav.tabs { position:fixed; left:0; right:0; bottom:0; z-index:5; transform:translateZ(0); display:flex; justify-content:center;
  background:var(--chrome); backdrop-filter:var(--blur-chrome); -webkit-backdrop-filter:var(--blur-chrome);
  border-top:1px solid var(--line); padding:.35rem .6rem calc(.35rem + env(safe-area-inset-bottom)); }
nav.tabs .inner { display:flex; gap:.3rem; width:100%; max-width:var(--measure); }
nav.tabs :is(a, button) { flex:1; display:flex; flex-direction:column; align-items:center; gap:.15rem; min-height:var(--tap-min);
  padding:.35rem 0; background:none; border:0; border-radius:var(--radius-xs); cursor:pointer; text-decoration:none;
  color:var(--dim2); font:var(--fw-bold) var(--text-micro)/1.2 var(--font-body); letter-spacing:.02em; }
nav.tabs :is(a, button) svg.ph { width:1.45rem; height:1.45rem; }
nav.tabs :is(a, button).on { color:var(--app); background:var(--app-tint); }

/* 16 · Bottom sheet — a short form or choice over the screen; tap the dim to close.
   <div class="scrim"><div class="sheet" role="dialog"><h2>Rename</h2>…<div class="row-btns">…</div></div></div>
   It sits over full-screen panels (30) and under the toast (50). */
.scrim { position:fixed; inset:0; z-index:40; background:var(--scrim); display:flex; align-items:flex-end; justify-content:center; }
.sheet { width:100%; max-width:var(--measure); max-height:88vh; overflow-y:auto;
  background:var(--panel); border:1px solid var(--line-strong); border-bottom:0; border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  padding:1.1rem 1.1rem calc(1.1rem + env(safe-area-inset-bottom)); }
.sheet h2 { margin:0 0 .9rem; font:var(--fw-black) var(--text-title)/var(--lh-snug) var(--font-display); letter-spacing:var(--track-display); }
.sheet label { display:block; margin:.6rem 0 .3rem; color:var(--dim); font-size:var(--text-xs); }
/* a row of buttons sharing the width: Cancel · Save */
.row-btns { display:flex; gap:.5rem; margin-top:1rem; }
.row-btns .btn { flex:1; }

/* 17 · Icon button — a round, tap-sized button holding one icon (edit, remove, refresh).
   Quiet until pressed; .on (and .voice) in pink. Armed, it grows into a pill (section 2). */
.ibtn { display:inline-flex; align-items:center; justify-content:center; flex:none; width:var(--tap-min); height:var(--tap-min);
  padding:0; background:none; border:0; border-radius:var(--radius-pill); color:var(--dim); cursor:pointer; }
.ibtn svg.ph { width:var(--icon); height:var(--icon); }
.ibtn:active { background:var(--raised); }
.ibtn:disabled { opacity:.3; cursor:default; }
.ibtn.on, .ibtn.voice { color:var(--accent); }

/* 18 · Link button — a button that reads as a pink link: "Open in Notion", a name to rename.
   Tap-sized unless .small, for one inside a line of text or a section label. */
.linkbtn { display:inline-flex; align-items:center; gap:.3rem; min-height:2.3rem; padding:.4rem .2rem;
  background:none; border:0; color:var(--accent); font:600 var(--text-sm)/1.2 var(--font-body); letter-spacing:0; text-transform:none;
  text-decoration:none; text-align:left; cursor:pointer; }
.linkbtn.small { min-height:0; padding:0; font-size:var(--text-2xs); }

/* 19 · Settings (1 Nov 2026) — the gear (an .ibtn) at the right of every header opens one page:
   the app's own settings, then this foot, the same in every app (shell.js settingsFoot()):
   Developer labels, "<app> <version> · <email>", Sign out. */
.settings-foot { display:flex; flex-direction:column; gap:var(--space-6); margin-top:var(--space-9); padding-top:var(--space-7); border-top:1px solid var(--line); }
.settings-foot .appver { margin:0; }
.settings-foot:first-child { margin-top:0; padding-top:0; border-top:0; }   /* an app with no settings of its own */
.settings-foot .switch-field .hint { color:var(--dim2); font-size:var(--text-2xs); }
