// ============================================================================= // Global stylesheet — imported exactly once, in main.tsx. Contains only // truly app-wide rules: the theme tokens and a minimal reset/base styling // that every page inherits. Anything specific to one component or page // belongs in a .scss file colocated next to that component/page instead. // ============================================================================= @use "./theme"; // Include borders/padding in an element's declared width/height everywhere, // rather than the browser default of adding them on top. *, *::before, *::after { box-sizing: border-box; } // Minimal reset: remove the default body margin so pages can control their // own layout without fighting the browser's default 8px margin. body { margin: 0; font-family: var(--font-body); font-size: var(--font-size-base); line-height: 1.55; color: var(--color-text); background: var(--color-background); } // Headings use the condensed "label" face app-wide — see _theme.scss for // the rationale. `text-wrap: balance` avoids a lone short word wrapping // onto its own line in multi-line titles. h1, h2, h3, h4, h5, h6 { margin: 0; font-family: var(--font-display); font-weight: 700; text-wrap: balance; } // Default to the page-title size; a heading used as a smaller component // title (e.g. the auth card's

) overrides this in its own stylesheet. h1 { font-size: var(--font-size-2xl); } // A visible, consistent focus ring for keyboard navigation — the browser // default varies a lot between elements and browsers. :focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } // Checkbox/radio appearance, app-wide — "selectable card" style: the native // control itself is visually hidden (still real, focusable and // screen-reader-visible — see the `input[type=...]` rule below, not // `display: none`) and the whole label row it lives in becomes the // interactive surface instead: a flat bordered box that fills in with a // tinted background + primary border once selected, with a checkmark // fading in on the leading edge. // // The base (unselected) look below is detected structurally with `:has()` // — safe, since "does this label contain a checkbox/radio" never changes // after mount. The *selected* look is instead driven by the `is-selected` // class {@link CheckboxOption}/{@link RadioOption} (components/ui/) toggle // in JS from the same boolean their caller already passes to `checked` — // chaining a second `:has(:checked)` to react to that live state turned // out to be unreliable across browsers, so this only needs one // always-true `:has()`. // // Every checkbox/radio in the app goes through this one place (the allergy // grid, the theme picker, anywhere future) rather than each feature styling // its own — see profile-forms.scss / settings-pages.scss, which only // arrange these within their own layout (grid vs. stacked list) and // intentionally don't re-style the control/label look itself. label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) { position: relative; display: flex; align-items: center; gap: var(--space-xs); padding: var(--space-xs) var(--space-sm); // Overrides the generic `label { font-weight: 600 }` base rule // (profile-forms.scss) — without this, an *unselected* row reads just as // bold as a selected one (only `.allergy-select__option` happened to set // its own 400 already; `.theme-select__option` didn't, so its rows were // all permanently bold until this was centralized here). font-weight: 400; border: 1.5px solid var(--color-border); border-radius: var(--radius-base); background: var(--color-surface); cursor: pointer; transition: background-color 0.15s ease, border-color 0.15s ease; &:hover { border-color: var(--color-primary); } &.is-selected { border-color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)); color: var(--color-primary); font-weight: 600; } &:has(:focus-visible) { outline: 2px solid var(--color-accent); outline-offset: 2px; } } // The control itself is removed from the visual flow — hidden the // "sr-only" way (not `display: none`) so it stays focusable/tabbable and // announced correctly by screen readers; the label above carries the // entire visible selected/unchecked look. input[type="checkbox"], input[type="radio"] { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; } // The checkmark — a real element (see components/ui/Checkbox.tsx / // Radio.tsx) shown via the same `is-selected` class as the label's own // look above, not a separate CSS-only trigger. Scaled in from nothing so // toggling has a bit of motion. Same mark for both checkbox and radio: one // consistent "selected" language app-wide rather than a checkmark here and // a dot there. Sits first in the row (before the label text, per DOM // order) — a classic "control on the left" layout rather than trailing. .check-mark { flex: none; width: 0.9rem; height: 0.9rem; background: var(--color-primary); clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); transform: scale(0); transition: transform 0.1s ease; } .is-selected .check-mark { transform: scale(1); }