/* obi-web.css: the web layer for the Obi design system (Obi v2) on the Obi website.
   Spec: plans/WEB-EXTENSIONS.md in William's Ovation folder (outside this repo).
   obi/ is Obi v2's dist/web, copied verbatim (Design Systems/obi-v2). Never edit it; override here. */

/* 1. Obi in a low-priority cascade layer: any app rule outranks Obi's base defaults. */
@import url("obi/styles.css") layer(obi);

/* 2. Body text in --ink. Obi v2's base does the same (its A13); kept here, unlayered, so the colour never depends on
      the layer order. --text-body is a size, never a colour. */
body { color: var(--ink); }

/* 3. Type on Obi's scale (Pass 3, WEB-EXTENSIONS §15.1). The Pass 1 holdover (16px / 1.45) is gone: the body is
      Obi's own 17px / 1.55, from Obi's base. Obi's type sizes are restated here in rem, the same pixels at the
      browser's default 16px, so every size follows the reader's own text-size setting (owner ruling R26,
      plans/DURABILITY.md; WCAG 1.4.4). Nothing is set below --text-caption. */
:root {
  --text-display: 2.5rem; --text-h1: 2rem; --text-h2: 1.625rem; --text-h3: 1.3125rem;
  --text-body-lg: 1.1875rem; --text-body: 1.0625rem; --text-label: .9375rem; --text-caption: .8125rem;
}
h1 { font-size: var(--text-h2); line-height: var(--lh-h2); margin: 0 0 var(--space-2); } /* a view title */
/* §15.3: margins the browser computes from em become Obi steps; :where() keeps every module rule ahead of these. */
:where(p) { margin: 0 0 var(--space-3); }
:where(ul, ol) { margin: 0 0 var(--space-3); padding-left: var(--space-5); }
:where(h4, h5, h6) { margin: 0 0 var(--space-2); }
/* §15.3: the one compact density (the switch in Settings sets body.ov-compact). Rows that hold only text drop to
   --control-dense; rows that hold a control keep --row-dense; text sizes never change with density. */
body.ov-compact.ov-shelled { --ov-pady: var(--space-4); }
body.ov-compact .card { padding: var(--space-4); }
body.ov-compact :is(td, th) { height: var(--control-dense); padding-block: var(--space-1); }
body.ov-compact :is(td, th):has(input, select, button) { height: var(--row-dense); }
body.ov-compact .ov-tile { min-height: var(--control-dense); }

/* 4. Foundations. */
:root {
  color-scheme: light;                 /* Obi has no dark mode */
  accent-color: var(--accent-primary);
  /* WEB-EXTENSIONS §3 (amended for Pass 2: navigation rows and pills as well as grids) */
  --row-dense: 40px;                   /* a navigation row: rail destination, menu item */
  --control-dense: var(--space-6);     /* 32px: a mode pill, a header button */
}

/* 4a. The skip link (WEB-EXTENSIONS §14.12): hidden until focused, then top left in the secondary Button look. */
.ov-skip { position: absolute; left: var(--space-3); top: var(--space-3); z-index: 500; transform: translateY(-200%);
  background: var(--surface); color: var(--ink); border: 1px solid var(--line-strong); border-radius: var(--radius-control);
  padding: var(--space-2) var(--space-4); font-weight: var(--fw-semibold); text-decoration: none; box-shadow: var(--elev-2);
  max-width: calc(100vw - 2 * var(--space-3)); overflow-wrap: anywhere; } /* stays on screen in the narrowest window (R26) */
.ov-skip:focus, .ov-skip:focus-visible { transform: none; }

/* 4b. Pass 4 (WEB-EXTENSIONS §16): text for assistive technology only, and a real <button> that looks exactly like
       the element it replaced (a clickable card, row or chip), keeping Obi's focus ring. */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
.ov-plain { appearance: none; background: none; border: 0; padding: 0; margin: 0; font: inherit; color: inherit;
  text-align: inherit; line-height: inherit; letter-spacing: inherit; cursor: pointer; }

/* 5. Scrollbars (WEB-EXTENSIONS §5): 12px with a 2px transparent border leaves an 8px warm-grey thumb. */
html { scrollbar-width: thin; scrollbar-color: var(--text-muted) transparent; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-thumb { background: var(--text-muted); border-radius: var(--radius-pill);
  border: 2px solid transparent; background-clip: content-box; }

/* 6. kbd (WEB-EXTENSIONS §5), for the Ctrl/Cmd-K hint. */
kbd { background: var(--surface-raised); border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 2px var(--space-1); font-size: var(--text-caption); }

/* 7. Forced colours (WEB-EXTENSIONS §8), bound to this app's classes. In Windows High Contrast every fill
      becomes a system colour: the seven CAPS levels would collapse into one, and the button hierarchy
      would flatten. */
@media (forced-colors: active) {
  :focus-visible { outline-color: Highlight; }
  .lvl1, .lvl2, .lvl3, .lvl4, .lvl5, .lvl6, .lvl7, .code-btn.on { forced-color-adjust: none; }
  .btn { background: ButtonText; color: ButtonFace; border: 1px solid ButtonText; }
  /* Filled buttons opt out: the browser otherwise paints a Canvas-coloured backplate behind the label, and a
     ButtonFace label on that backplate cannot be seen (measured 2026-09-23). Their colours stay system colours.
     The secondary (ghost) button keeps the browser's forcing, so its text always follows the user's theme. */
  .btn:not(.ghost) { forced-color-adjust: none; }
  .btn.ghost { background: ButtonFace; color: ButtonText; border: 1px solid ButtonText; }
  .btn:disabled { background: ButtonFace; color: GrayText; border-color: GrayText; }
  .danger, nav.tabs button.active, .ov-tile.on { forced-color-adjust: none; }
  /* Pass 2 (WEB-EXTENSIONS §14.12): where you are still reads. The lit destination, the current mode and Capture opt
     out of forcing, as the filled buttons do, and take system colours; lock icons draw in currentColor, so they stay. */
  .ov-tile.on, .ov-mode[aria-current="page"] { background: Highlight; color: HighlightText; border-color: Highlight; }
  .ov-mode[aria-current="page"], .ov-capture { forced-color-adjust: none; }
  .ov-capture { background: ButtonText; color: ButtonFace; border: 1px solid ButtonText; }
  .ov-capture[aria-current="page"] { background: Highlight; color: HighlightText; }
}

/* 8. Print (WEB-EXTENSIONS §7), in addition to the app's own print block in ovation.css.
      A4 is stated; the 16mm @page margin is not, because with it the Reports parent summary grew from one
      page to two (measured 2026-09-23), so the browser's default margin stays. */
@page { size: A4 portrait; }
@media print {
  #reportArea :is(.notes, .ev-summary, .hq-kpi, .bl-stat) { background: none; }
  /* Pass 2: navigation never prints (WEB-EXTENSIONS §14.12) */
  #ovModes, #ovFabs, #ovCapture, .ov-skip, #ovAcctMenu, #ovAcct { display: none !important; }
  #ovMain { padding-bottom: 0; }
  .lvl1, .lvl2, .lvl3, .lvl4, .lvl5, .lvl6, .lvl7 { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}

/* 9. The contrast amendments (R24, A1-A12) and the focus-shape fix are part of Obi v2 now (its AMENDMENTS.md; R50),
      so the separate obi-r24 fix-up layer that carried them here is gone (Pass 5, 5a). --on-accent comes from Obi v2.
      What follows is the web layer's own. */
:root {
  /* §16.3 #8: native checkbox, radio and range accent. Apricot 2.63 on cream -> --caps-2 (Obi's control-accent
     step of the same ramp) cream 3.60, bone 3.43, sand 3.11. */
  accent-color: var(--caps-2);
  /* The app's muted text before ovation-theme.js runs (ovation.css :root still says var(--warm-grey); the
     theme's apply() now writes var(--text-muted) inline too). */
  --muted-dim: var(--text-muted);
}

/* Section 5's scrollbar thumb stays warm-grey (WEB-EXTENSIONS §5; 3:1 is enough for it): it read --text-muted,
   which A1 now darkens for text. (Equivalent: change section 5's two var(--text-muted) to var(--warm-grey).) */
html { scrollbar-color: var(--warm-grey) transparent; }
::-webkit-scrollbar-thumb { background-color: var(--warm-grey); } /* longhand: keeps section 5's content-box clip */

/* §16.3 #12: the current rail destination was marked only by the apricot fill, 2.50:1 against the page. A 2px
   --apricot-ink edge (1px border and 1px inset shadow, so nothing moves), as the current mode pill has: bone 8.05,
   and 3.2:1 against the apricot fill inside it. Forced colours keep section 7's Highlight. */
@media (forced-colors: none) {
  .ov-tile.on, .ov-tile.on:hover, .ov-tile.on:active,
  /* 4.V: O'Chat's view toggles (Chats, Directory, Moderation, Rules) mark the current one the same way */
  .oc-sub .btn[aria-current="true"], .oc-sub .btn[aria-current="true"]:hover, .oc-sub .btn[aria-current="true"]:active {
    border-color: var(--apricot-ink); box-shadow: inset 0 0 0 1px var(--apricot-ink);
  }
}

/* §17 (Pass 5): the sign-in card waits for the splash. ovation-auth.js draws the card before ovation-loginfx.js (the
   splash) has loaded, so on a slow connection the card could show for a moment before the splash covers it. Until
   loginfx marks the overlay (data-fx), the card is hidden; if loginfx never runs, it appears by itself after 4 s.
   (Reduced motion turns animations off app-wide, so there the card simply shows: the safe direction.) */
#authOverlay:not([data-fx]) .auth-card { animation: ov-card-wait 0s linear 4s both; }
@keyframes ov-card-wait { from { visibility: hidden; } to { visibility: visible; } }

/* §20 (Pass 6, Stage 6b; WEB-EXTENSIONS §20): layout at every width. The page never scrolls sideways; a wide table scrolls inside its own box
   with its first column pinned; a pill is one box. Each value that is not a token carries its reason beside it. */

/* 20.1 A labelled field never widens the page (item 1, and the date field of item 4). ovation.css gives every select
   min-width:190px and a label.fld its content's width, so a learner's long name made the label (and, at 320px, the page) as
   wide as the longest option. The label may now shrink to its box; the select stretches to the label, never past it, and
   keeps its 190px only while there is room (min() lets it go below that in a box narrower than 190px). The closed select
   ends a long name in an ellipsis; the list the select opens stays whole. */
label.fld { min-width: 0; max-width: 100%; }
label.fld > select { max-width: 100%; min-width: min(190px/*keep: ovation.css's select floor, still the width wherever there is room*/, 100%); text-overflow: ellipsis; }
/* The data router's "Concept / sub-module" label carries min-width:250px inline (ovation-intel.js, in the markup), which a stylesheet
   beats only with !important (the one use here, and why: the value is an inline style). It pushed the label 4px past a 320px
   card's edge and, below about 290px of screen, widened the page. Same value, but never more than its box. */
.di-row > label.fld[style*="min-width:250px"] { min-width: min(250px/*keep: the module's own value*/, 100%) !important; }

#obDate { min-width: 0; max-width: 100%; }
label.fld[for="obDate"] { flex-wrap: wrap; }
.domain-hd > :has(> label.fld[for="obDate"]) { min-width: 0; }

/* 20.2 A pill is one box (item 3). An .ai-chip was an inline span, which breaks across lines in pieces (a heading's chip at
   320px: "memory" at the end of one line, "+ learning engine" on the next). As an inline-block it moves to the next line whole,
   so a row of pills wraps between pills; only a pill longer than the whole line wraps its words, inside its one box (anywhere:
   a single word longer than the line breaks too, rather than leave the box). The Advisory board's heading chip has done this since
   Pass 5. A pill that holds pills (Progress insights: "3 observations", then one pill per kind of evidence) wraps between its
   inner pills, which keep their own line, instead of squeezing each into a column of two or three letters. */
.ai-chip { display: inline-block; max-width: 100%; white-space: normal; overflow-wrap: anywhere;
  line-height: normal/*keep: an inline chip's fill was the font's own height plus its padding; inline-block would otherwise add the heading's line-height (16.9px) and grow a 21px heading's line by half a pixel*/; }
.tag:has(> .tag) { flex-wrap: wrap; row-gap: var(--space-1); }
.tag > .tag { flex: 0 1 auto; }

/* 20.3 A report table fits, or scrolls in its own box with its first column pinned (item 2; guides/web.md "Tables and
   grids"). Cause of the cut Mathematics table at about 765-815px: a Detail card (Synergy: one card in a two-column .grid2;
   My progress: two) gets half the row from 761px, and the table, 335px at least, is wider than the half; it scrolled in
   .rc-scroll with nothing pinned and the % column out of sight.
   (a) A lone card takes the whole row, up to 1080px; above that the wide rail leaves a half row of 343px or more, so the
       design stays as it was. 1080px is Obi's web breakpoint (literal: custom properties do not work in a media query).
   (b) The progress bar's floor drops from 80px to 48px (--space-7): the table's narrowest width falls from 335px to about
       303px, so a two-column table (My progress) fits the half row it gets from 761px.
   (c) Where a table still has to scroll (a 700px Synergy marksheet in a 700px window, large text, a long domain name), its
       first column is pinned: opaque, above the cells that slide under it. The rows stay transparent (the fade is painted on the
       box, behind the table, and a filled row would hide it); the pinned cell carries the card's fill itself, or its row's tint
       where the row has one. The cue that more is out of
       sight: a fade on the box's right edge while there is more to see (the scroll-shadow technique: the fade is fixed to the
       box, a cover of the card's fill is attached to the content and hides it at the end), and a 1px hairline on the pinned
       column's edge only while the box scrolls (the register's cue, bd-sx, here without a script: a scroll-state container
       query). A browser without that query shows the fade and no hairline. Screens only: print keeps the report as it was.
       border-collapse is separate, as in the grids (WEB-EXTENSIONS §4): a pinned cell's borders must stay with the cell. The
       rules apply to the report card's box (.rc-scroll: the Detail card, My progress, the Personal Marksheet's domain tables) and to
       the Synergy marksheet's own scroll wrapper, which has no class (its inline style is the hook). */
@media (max-width: 1080px/*keep: Obi's web breakpoint, literal in a media query*/) {
  .grid2:has(> :only-child) { grid-template-columns: 1fr; }
}
.rc-bar .bar { min-width: var(--space-7); }
@media screen {
  :is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]) {
    background:
      linear-gradient(to left, var(--panel) 25%/*keep: the cover is opaque over the last quarter, so the fade is gone at the end*/, transparent) 100% 0 / var(--space-6) 100% no-repeat local,
      linear-gradient(to left, color-mix(in srgb, var(--ink) 14%/*keep: strong enough to read on the card's cream, soft enough to stay a hint (the card's own shadow is 5-6%)*/, transparent), transparent) 100% 0 / var(--space-3) 100% no-repeat scroll;
  }
  @media (forced-colors: active) { /* a gradient is not recoloured by forced colours: on a dark theme it would be a cream strip. The hairline below is a border, which is */
    :is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]) { background: none; }
  }
  /* Pinning waits for a box wide enough to share: the box is a size container, and its first column (6.7 to 7.6em at the default text size,
     in em like the register's, so it follows the reader's text size) is pinned from 14em of box (224px at 16px; the 320px screen's 238px
     box passes, a 200% text size's 7.4em does not). In a narrower box a pinned column would take half the box or more, and at 300%
     text all of it, leaving the rest unreachable; there the table scrolls whole, with the fade. The same holds for a box wide enough whose
     first column is nevertheless more than half of it (a 34-letter learner's name in the Synergy marksheet; the Personal Marksheet's
     "Assessment item" column at 320px, 51%): ovation-nav.js measures the column and puts .ov-nopin on the box, and every rule below
     leaves a .ov-nopin box alone (6VA, durability round F2). */
  @supports (container-type: inline-size) {
    :is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]) { container-type: inline-size; }
    @supports (container-type: scroll-state) {
      :is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]) { container-type: inline-size scroll-state; }
    }
    @container (min-width: 14em/*keep: the pinned column is at most about half the box; em, so it follows the text size*/) {
      :is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]):not(.ov-nopin) > table { border-collapse: separate; border-spacing: 0; }
      /* A pinned cell must be opaque, and must look like its row. The rows themselves are not filled: the fade is drawn on the box, behind
         the table, and a filled row (the first version filled every one with the card's colour) paints over it, so the fade never showed.
         The pinned cell carries the card's fill itself; a row that carries its own tint (an inline background, as the Personal Marksheet's
         "Section total" row has) passes it to its pinned cell, which takes it from the row (background: inherit, that case only). Both rules
         are inside :where(), at no weight: a row's inline tint is never outweighed, and the table's own hover rule (tbody tr:hover td)
         still tints a pinned cell. */
      :where(:is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]):not(.ov-nopin) > table > :is(thead, tbody) > tr > :first-child) {
        position: sticky; left: 0; z-index: 1/*keep: above the cells that slide under it; nothing else here is stacked*/; background: var(--panel);
      }
      :where(:is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]):not(.ov-nopin) > table > :is(thead, tbody) > tr[style*="background"] > :first-child) {
        background: inherit;
      }
      @container scroll-state(scrollable: inline) {
        :where(:is(.rc-scroll, #reportArea > .card > div[style*="overflow-x:auto"]):not(.ov-nopin) > table > :is(thead, tbody) > tr > :first-child) {
          border-right: 1px/*keep: a hairline, as the register's (bd-sx)*/ solid var(--line-strong);
        }
      }
    }
  }
}

/* 20.4 The Observation Books page at the smallest screens (item 4). The grid always scrolled in its own box; the page did not
   survive the date field (its own fields, the label around it and the flex row that holds them kept their full width, so the
   row held the header open) at the durability matrix's cells narrower than 320px: 34 of 48 scrolled sideways. The date field,
   its label and its row may now shrink (20.1 above, and the three rules before 20.2); the label wraps its word "Date" above
   the field when the two do not fit one line. Below 240px, where page and card padding alone take 82px (2 x 16 + 2 x 24 + 2),
   the padding steps down to Obi's spacing tokens and a word longer than the line breaks inside itself instead of leaving the
   card, as §18.2's board does at the same width. Tables are left out of the word-breaking: a column squeezed to one letter
   would be worse than a grid that scrolls. */
@media (max-width: 239px/*keep: the board's own step (§18.2), literal in a media query*/) {
  html body.ov-shelled { --ov-padx: var(--space-2); }  /* html body.* outranks the shell's own body.ov-shelled, which loads after this file */
  .card, body.ov-compact .card { padding: var(--space-3); }  /* compact density's own card padding (above) would win otherwise */
  .card :is(h1, h2, h3, h4, p, label, .ob-lg, .btn):not(table *) { overflow-wrap: anywhere; }
}

/* 20.5 Home's rows may wrap (6VA n4, the text-spacing check of the accessibility round). Two kinds of row on Home were flex rows that
   could not wrap, so each one's least width was the sum of its items': the header of a "Today's focus" card (avatar, name and reasons,
   status pill: 231px) and a "Needs me now" line (icon, text, button: 255px). With WCAG 1.4.12's text spacing at 320px (line 1.5,
   letters .12em, words .16em) each item's longest word is wider, and Home's one-column grid track (1fr, which is minmax(auto, 1fr)) grew
   to hold the widest row: 27px past the 288px the page leaves for the first, 17px for the second, so the six cards ended 11px past the
   window's edge and the page scrolled sideways. A row that may wrap puts its last item on its own line where the one-line row would
   have squeezed its text (.nm and .tx are flex:1, so the line breaks at their longest word plus the pill or button): a visible change
   at 320 to 768px at the default text size too (the name no longer
   squeezed into a narrow column beside the pill), none at 1440 (measured byte-identical). Decided, awaiting ratification. */
.hm-card .fh, .hm-list .hm-item { flex-wrap: wrap; }

/* 20.6 A long word in a heading wraps inside its line at every width (6VA, durability round F5). §20.4 breaks a heading's words with
   overflow-wrap: anywhere, but only below 240px of screen; a larger text size never reaches that query, so at 300% text on a 390px screen the
   Synergy detail card's "Mathematics" h3 was wider than its card (56px) and the page scrolled sideways. break-word breaks a word only
   when it would not otherwise fit its line, so it changes nothing at the default text size. By itself it is not enough where the heading
   is a flex item (.domain-hd, a wrapping row): a flex item's least width is its longest word, so the row gave "Mathematics" a line
   of its own and the word still stood past the card (measured: 15px past the window, 56px past the card, with break-word alone).
   min-width: 0 removes a flex item's automatic least width, so the item can be as narrow as its line, and then break-word breaks the
   word there. In a row that wraps (every heading row here) the heading takes its own line before it shrinks; in a row that does not
   wrap, a heading may now shrink and break a word where it used to hold the row open (not seen on screen). :where() gives both no
   weight, so any rule that sets a heading's own wrapping or width still wins. */
:where(h1, h2, h3, h4) { overflow-wrap: break-word; min-width: 0; }
