/* ============================================================================
   TLC Operations  ·  shared/ops/v2/ops-dashboard.css  ·  v2.001
   ----------------------------------------------------------------------------
   TURNERLEE ONLY FOR NOW. Craig, 2026-08-22: "Focus on TurnerLee.com/ops/dashboard
   and Style Guide setup and utilization. These will be critical to get right for
   the SST for all other sites. DO NOT make changes to other sites until we nail
   these down." v1 stays exactly where it is; SweetwaterCreek, ColoradoLamb and
   EvolvedFastpitch keep linking shared/ops/v1/ and are untouched by this file.

   WHERE EVERY DECISION CAME FROM — all measured, none invented:

   TYPE — Open Sans.  (Was Roboto until 2026-08-23.)
     Craig, 2026-08-23: "all roboto to open sans." The card comp he specified is
     Open Sans throughout, and one component speaking a different typeface from
     the chrome around it is exactly the drift this file exists to stop — so the
     whole /ops face follows the card rather than the card being the exception.

     Loaded at 400/500/700/800, and all four are load-bearing: body copy is
     Medium 500, and the status ribbon, eyebrow and pill labels are ExtraBold 800.
     The site pulls Open Sans at 300/400/700 elsewhere, which is why 500 and 800
     are named explicitly here — a weight that is not loaded gets SYNTHESISED by
     the browser and renders visibly wrong against the comp while looking, in a
     screenshot, roughly right.

     The original reason for a UI face separate from the brand face still stands:
     legibility at small sizes, where Hypatia gets thin. Open Sans satisfies that
     and matches the design. The brand face stays where it belongs, on the public
     site — except the card TITLE, which takes the site's own heading face off
     /ops (see .odc-title at the end of this file).

     Still true and unrelated: the Project Board clock uses Roboto and NOTHING
     ELSE ON THAT BOARD DOES (Project-Board-Styles.html line 7), because Roboto is
     the only face on hand with real 100 and 900 cuts. That exception is the
     board's, not this file's.
     Stack: 'Open Sans','HypatiaSans','Helvetica Neue',Arial,sans-serif.

   CONTAINER + RESPONSIVE — Spartan World Cup (craigwhitlock.com/fantasy/wc26).
     .wcf-wrap is max-width:1800px with padding:14px clamp(10px,2.2vw,40px) 80px.
     Fluid gutters, wide ceiling, no fixed page padding. Copied exactly. The
     two-column collapse at 1280 and the table rebuild at 640 are the same
     breakpoints that page uses, so /ops and the fantasy site reflow together.

   CARDS — /ops/workspace.
     Image-top card: 16/9 background-image tile, gradient placeholder when no
     inline background-image is set, .logo-pad (20px inset, background-origin:
     content-box, size:contain) for standalone SVG marks, body below with a 4px
     tier rule on the bottom edge, radius 14, grid gap 18,
     repeat(auto-fill,minmax(280px,1fr)). Same class shapes, renamed od-* so the
     two pages cannot fight over one selector.

   DENSITY + CONTROLS — /ops/schedule (PB2).
     Section heads, chip sizing and the 6px/10px control padding come off the
     board so the hub does not read looser than the tool it links to.

   TABLES — the WC26 League Standings board, verbatim in behavior.
     Craig: "these sticky columns (on any tables responsive layouts)". Below 640
     the table becomes display:block, rows become flex, columns get explicit
     widths and order, and the left identity group pins with position:sticky at
     CUMULATIVE left offsets. The frozen cells MUST carry a solid opaque
     background — a translucent one lets scrolling cells show through on iOS.
     Any table inside .opsdash gets this by adding .od-table + the column classes.

   COLOR — brand tokens only, no bare hex.
     --color1..5 come from the ID Settings -> ID Colors chain (on /ops that is now
     ID Settings item "Operations" 20483 -> ID Brand "Operations" 20484). The
     neutral ramp is DERIVED from --color1 with color-mix so it tints with the
     brand instead of sitting next to it. #fff and transparent are the only
     literals, and both are structural: a card surface that must stay light, and
     "no paint". A fallback color reaching the screen means the brand pipeline
     failed — fix the pipeline, never repaint this file.
     NOTE for the ramp: var(--x, fallback) does NOT use the fallback when --x is
     declared-but-EMPTY, so every derived value is written against a color-mix
     that already carries its own base rather than relying on a var fallback.
   ========================================================================== */

/* OPEN SANS, NOT ROBOTO. Craig, 2026-08-23: "all roboto to open sans" — the card
   comp is Open Sans throughout, so the whole /ops face follows it rather than
   leaving one component speaking a different typeface from the chrome around it.
   Weights 400/500/700/800 are all load-bearing here: body copy is Medium 500 and
   the status ribbon, eyebrow and pill labels are ExtraBold 800. The site already
   pulls Open Sans at 300/400/700 elsewhere, which is why 500 and 800 have to be
   named explicitly — a weight that is not loaded is synthesised by the browser
   and renders visibly wrong against the comp. */
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap');

/* .od-demo is the SAME token + type context WITHOUT the shell geometry below.
   The style guide documents these components on its Cards / Web CSS / Layouts
   pages, on every site — including sites that have no .opsdash anywhere. Wrapping
   a demo in .opsdash instead would apply the 100vw breakout to a block sitting in
   the middle of a 3-column grid and tear the page open, and on /ops it would nest
   a breakout inside a breakout. One class, tokens only. */
.opsdash,
.od-demo {
  /* ---- brand-derived ink + neutral ramp ----------------------------------
     INK IS NOT THE BRAND COLOR. The first cut of this file set --ink to
     var(--color1) and every word on the page came out TLC blue — headings, body,
     descriptions, the lot. Reading text is a dark neutral that carries a trace of
     the brand; the brand color is what makes a link, a chip or a tier rule stand
     out FROM that text. If everything is the accent, nothing is. */
  --ink:      color-mix(in srgb, var(--color1) 16%, #14171c);
  --quiet:    color-mix(in srgb, var(--color1) 14%, #4a5261);
  --hush:     color-mix(in srgb, var(--color1) 12%, #79818f);
  --line:     color-mix(in srgb, var(--color1) 15%, transparent);
  --line-2:   color-mix(in srgb, var(--color1) 26%, transparent);
  --panel:    #ffffff;                                    /* card surface: must stay light */
  --panel-2:  color-mix(in srgb, var(--color1)  5%, #ffffff);
  --panel-3:  color-mix(in srgb, var(--color1)  9%, #ffffff);
  --accent:   var(--color1);
  --gold:     var(--attention, var(--color3));

  /* ---- STATUS LANGUAGE — the one place a fixed value is correct -----------
     Craig, 2026-08-23: "brand colors for sure. But we have some color in the TLC
     Portal that is established — ORANGE Requests/Add · GREEN Success, Payments
     Received · BLACK, dark gray Complete · GOLD Edit/modify."

     Everything else in this file resolves through --color1..5 and re-skins per
     brand. These four must NOT: a green that turns navy on the Control brand
     stops meaning success, and a client who learned that gold means "you can
     still change this" learned it once, across every site.

     The values are the estate's EXISTING utility classes in
     shared/core/v1/sst-styles.css (.orange/.color10, .green/.color8,
     .gold/.color12) — restated as tokens, not invented here.

     --st-edit-ink exists because #ffd700 is 1.35:1 on white. Gold is a FILL with
     dark ink or a border; it is never a text color at body size. */
  --st-request:  #ff7800;
  --st-success:  #039303;
  --st-complete: #222222;
  --st-edit:     #ffd700;
  --st-edit-ink: #8a6d00;

  /* ---- status ------------------------------------------------------------ */
  --ok:   color-mix(in srgb, var(--color1) 20%, #0a8a4d);
  --warn: color-mix(in srgb, var(--color1) 12%, #b96e00);
  --bad:  color-mix(in srgb, var(--color1) 12%, #c0392b);

  /* ---- type -------------------------------------------------------------- */
  --ops-font: 'Open Sans', 'HypatiaSans', 'Helvetica Neue', Arial, sans-serif;

  font-family: var(--ops-font);
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
}
.opsdash * { box-sizing: border-box; }

/* ── THE GUIDE FENCE ────────────────────────────────────────────────────────
   /ops/style-guide embeds a SECOND design system inside .opsdash — the shared
   TLC style guide, mounted at #tl-style-guide-mount. Every BARE-ELEMENT rule in
   this file (h1, code, and the universal font-family below) outranks the plain
   element selectors that guide is written in, so without a fence this sheet
   repaints the guide's own demos and the guide LIES about the site: measured
   2026-08-23, every H1/H2/H3 sample rendered Roboto 38px/700 — the ops page
   title's type — instead of the site's HypatiaSans headings. A style guide that
   shows you the wrong font is worse than no style guide.

   Rule for anything added to this file: style od-* classes, never a bare
   element. If a bare element is unavoidable, fence it the same way. */
/* THE MOUNT ITSELF, NOT JUST ITS DESCENDANTS. `.opsdash *` matches the mount div
   (it IS a descendant of .opsdash), and an element is not a descendant of itself,
   so `:not(#tl-style-guide-mount *)` was TRUE for it — the selector matched at
   (1,1,0) and out-ranked style-guide.css's own `#tl-style-guide-mount` rule at
   (1,0,0). Every unstyled element in the guide then inherited Roboto through it:
   measured, the guide's body paragraphs stayed Roboto while its headings were
   already correct, which reads as a paragraph bug and is this one selector. */
.opsdash *:not(#tl-style-guide-mount):not(#tl-style-guide-mount *) { font-family: inherit; }

/* ── THE SECURE-ZONE TYPE CONTRACT ──────────────────────────────────────────
   Craig, 2026-08-23: "in Operations, Control and Client Portal, i want site's
   primary font for h1,h2,h3. that's it. everything else is Open Sans."

   REVERTED 2026-08-24, same day. It was briefly h1,h2 only, because a later
   line in a long font brief read "H1-H2 Hypatia Sans / H3-H6 Open Sans" and I
   treated it as superseding the sentence above. It was not a new instruction; it
   was the same instruction with a mistyped boundary, and the two statements
   disagreed on exactly one heading level. THAT CONFLICT SHOULD HAVE BEEN RAISED,
   NOT RESOLVED SILENTLY — Craig: "that is exactly what i said, why did it get
   changed?"

   h1, h2 and h3 take the primary face. Everything else is Open Sans. Both of his
   statements agree on h4-h6, so those stay. If a future brief moves this line,
   confirm it against this comment before changing it.

   This CSS is what makes item 5 true — every /ops section on every site gets
   Open Sans with no CM configuration of its own — while the CM (ID Styles -
   Layout, H3Font -> font3) says the same thing, so the two cannot disagree.

   Everything above already puts --ops-font (Open Sans) on the whole shell. This
   is the exception, and it is the WHOLE exception: three heading levels take the
   site's own primary face. The /ops page title is an h1, so it changes with them
   - measured before this: .opsdash > h1 was Open Sans 700/38px.

   The guide mount is fenced out here and handled below, because the guide has to
   obey the contract only in the brands the contract applies to. */
/* THE FENCE CARRIES TWO ID-BEARING :not()s, so it is (2,1,0) and outranks every
   plain element rule in this file - including `.opsdash > h1`. Confirmed with CDP
   CSS.getMatchedStylesForNode 2026-08-23: with both rules declaring the primary
   face, the fence's `font-family: inherit` still won and the title computed Open
   Sans. Mirroring the fence's shape puts this at (2,1,1). Do not "simplify" the
   selector back - the extra :not() is load-bearing. */
.opsdash :is(h1, h2, h3):not(#tl-style-guide-mount):not(#tl-style-guide-mount *) {
  font-family: var(--font1Family, var(--ops-font));
}

/* ── THE SAME CONTRACT, INSIDE THE GUIDE, ONLY IN THE SECURE-ZONE BRANDS ────
   The fence above deliberately keeps the ops face out of the guide, so a Default
   sample shows the PUBLIC site's type - that is correct and stays. But under
   Operations / Control / Portal the guide is documenting THIS zone, and pinning
   it to the public face made it show the wrong font under its own brand label.
   data-sg-brand is written server-side by sst-style-guide-body.html. */
#tl-style-guide-mount[data-sg-brand="Operations"],
#tl-style-guide-mount[data-sg-brand="Control"],
#tl-style-guide-mount[data-sg-brand="Portal"] {
  font-family: var(--ops-font);
}
#tl-style-guide-mount[data-sg-brand="Operations"] *:not(h1):not(h2):not(h3),
#tl-style-guide-mount[data-sg-brand="Control"] *:not(h1):not(h2):not(h3),
#tl-style-guide-mount[data-sg-brand="Portal"] *:not(h1):not(h2):not(h3) {
  font-family: inherit;
}
#tl-style-guide-mount[data-sg-brand="Operations"] :is(h1, h2, h3),
#tl-style-guide-mount[data-sg-brand="Control"] :is(h1, h2, h3),
#tl-style-guide-mount[data-sg-brand="Portal"] :is(h1, h2, h3) {
  font-family: var(--font1Family, var(--ops-font));
}

/* Anything appended to <body> — modals, overlays, drag clones — never matches a
   .opsdash-scoped rule. Nothing here does that yet; when something does, style it
   by id or inline, not by adding a descendant selector that cannot match. */

/* The hidden ATTRIBUTE loses to any author display: rule, because [hidden]
   {display:none} lives in the UA sheet and an author sheet outranks it. One
   blanket over the whole subtree, not one selector per part that gets found. */
.opsdash [hidden] { display: none !important; }

/* ======================= 0 · SITE CHROME ON /ops ==========================
   Craig, 2026-08-23: "When we have Page nav like this, with multiple ribbons,
   the main nav needs to be removed entirely, the page Nav needs to get sticky to
   the bottom of the minor nav at the top. without this weird gap too. In
   addition the minor nav needs to update to the same container as the page nav."

   An /ops page carries THREE ribbons of its own. Stacked under the site's minor
   nav AND its main nav that is five bars before any content, and the page opens
   on chrome. So on these pages the main nav stands down and the minor nav is the
   only site chrome left.

   Scoped with :has(.opsdash) so it cannot touch a public page: .opsdash exists
   only where sst-ops-pagenav.html rendered.

   THE OFFSET IS MEASURED, NOT TYPED. A site script writes body{padding-top} as
   an INLINE style from the full header height (175.9px here). An author
   !important beats a non-important inline declaration whenever it is written, so
   this holds through the script's resize handler without a second listener to
   keep in sync. --ws-hdr-offset is re-measured off .fixed-top by
   sst-ops-pagenav.html, so hiding the main nav updates BOTH the body offset and
   the sticky top of .top-nav from one number. The 40px fallback is the minor
   nav's own height, for the frame before that script runs. */
/* NOT IN OPERATIONS CONTEXT. Craig 2026-09-02, quoted at sst-styles.css:9311 —
   "Minor & Main Nav (per ID Brand) still display, but are no longer sticky." A site
   emitting body.tlc-ctx-ops (i.e. running all-site-width.html) has adopted that and
   keeps its navs; ColoradoLamb, EPIC4 and SweetwaterCreek emit nothing and keep the
   hidden main nav until Craig looks at them. Promoted from custom-ops-chrome.html,
   which reversed this rule locally on TurnerLee — the reversal reached only the 18
   /ops pages carrying .opsdash, while the class reaches all 53. */
body:has(.opsdash):not(.tlc-ctx-ops) nav#second,
body:has(.opsdash):not(.tlc-ctx-ops) .navbar.bg-Main { display: none !important; }

/* THE 40px LITERAL WAS A GUESS AND IT WAS 14px SHORT ON A REAL SITE.
   --ws-hdr-offset is written to <html> by sst-ops-pagenav.html:575 — which only loads
   on a page that HAS a page-nav ribbon. On an /ops page without one the fallback was
   the bare literal, and 40px is nobody's header height. Measured 2026-09-02 14:12 MDT
   @1440 after the .cover spacer stopped padding over it:
     sweetwatercreek.net/ops/dashboard  header 54px · --ws-hdr-offset UNSET · padding 40px
                                        -> 14px of content UNDER the fixed header
     cololamb.com/ops/dashboard         header 49px · --ws-hdr-offset 49px · overlap 0
   --sticky-nav-h is the same measurement taken by nav-gap.js, which ships in
   sst-scripts-footer.html on EVERY page and publishes unconditionally: measured 54px /
   49px / 0px on those three sites. So it is a real number where the ribbon's is missing.
   Order matters — the ribbon's own value still wins where it exists, because that one is
   re-measured on scroll as the minor nav shrinks 40->30. */
body:has(.opsdash) { padding-top: var(--ws-hdr-offset, var(--sticky-nav-h, 40px)) !important; }

/* AND THE BILLBOARD'S NAV SPACER. Billboard.html's `None` branch — the branch every
   /ops page takes — emits an empty spacer sized from --sticky-nav-h, which nav-gap.js
   measures off the real fixed nav. On a public page it is load-bearing. On /ops the
   main nav is already gone (above) and the body's own padding-top has paid for what is
   left, so the spacer contributes a bare band and nothing else. Measured 2026-08-30
   20:47 MDT, cololamb.com/ops/style-guide @1440: .fixed-top h=49, body padding-top 49,
   div.height180.cover top=49 h=49, #main top=98.

   PROMOTED 2026-09-02 from ColoradoLamb/Content/Snippets/custom-ops-chrome.html, where
   it hid the spacer on ONE site and left the identical bare band standing on EPIC4 and
   SweetwaterCreek — which link this very sheet and carry no rule of their own. That is
   the local-patch failure mode exactly.

   KEYED ON .cover, NOT ON A NUMBER. The class carries each site's own gap: height140/180
   (TurnerLee, MongooseSports), height180 (ColoradoLamb, MinarsOrtho), height140 (EPIC4),
   height250 (12 legacy sites), height{{GapOverride}} (SweetwaterCreek, MudCup, ElevatedMP,
   EPIC4contests, EPIC4portal). Only `cover` and the `#wrapper >` position are common to
   all of them, and the Color / Image / Background branches never run on an /ops page, so
   this is the only direct child of #wrapper carrying .cover there.

   display, NOT height: the spacer carries an INLINE `height: var(--sticky-nav-h,180px)
   !important`, and a style-attribute !important cannot be beaten by a stylesheet
   !important at any specificity. */
body:has(.opsdash) #wrapper > .cover { display: none !important; }

/* THE MINOR NAV TAKES THE OPS BOX. It ships in a Bootstrap .container capped at
   1350px, so with the main nav gone it sat 121px inside the ribbon below it —
   two bars, two left edges, nothing else between them. Same three declarations
   .nav-row-inner uses, so they cannot drift. */
body:has(.opsdash) nav#first > .container {
  /* A site rule gives this an EXPLICIT width:1152px and margin-left:144px, so
     max-width alone loses — measured 144 -> 1296 against the ribbon's 0 -> 1440.
     Restate width and both margins or the box does not move. */
  max-width: var(--tlc-cmax, 1800px);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
  /* 2.2vw, NOT 1.6vw, and the rate had to move with the ceiling. Craig's scale
     calls 40px the max page gutter; at 1.6vw the clamp only reaches 40 at a
     2500px viewport, so on every real screen the "max" was unreachable and the
     scale was describing a number nothing used. 2.2vw hits 39.6px at 1800px —
     the content cap — so the top of the ramp lands exactly where the page stops
     growing. Measured at 1440: 23.0px before, 31.7px now. */
  padding-left:  clamp(10px, 2.2vw, 40px);
  padding-right: clamp(10px, 2.2vw, 40px);
}

/* ============================ 1 · SHELL ================================== */
/* THE CONTENT BOX IS THE NAV BOX. Craig, 2026-08-22: "expand the primary content
   container to match that container left edge and right edge." The template wraps
   /ops in its own narrow Bootstrap container, so the sticky nav row (which breaks
   out to 100vw) started ~150px left of the content and the two never lined up.

   .opsdash breaks out the same way the nav row does, and then EVERY DIRECT CHILD
   gets the identical inner box to .nav-row-inner — max-width 1800, auto margins,
   clamp(10px,2.2vw,40px) side padding. Same numbers in one place, so the edges
   cannot drift apart the way two hand-matched values would.

   NO EXTRA WRAPPER DIV. sst-ops-pagenav.html's search hides the page by walking
   `.opsdash > *` and hiding everything that is not the nav, the search bar or the
   results panel. A .od-wrap div made itself the only child, so a single keystroke
   in the search box hid the entire page — search field and results included.
   Craig: "the Search input is messed up it makes the page disappear." Anything
   added here must stay a DIRECT child of .opsdash. */
.opsdash {
  width: 100vw;
  position: relative;
  left: 50%;
  right: 50%;
  margin-left: -50vw;
  margin-right: -50vw;
  /* 120px, was 80. Craig, 2026-08-24: "120 bottom of page". */
  padding-bottom: 120px;
  /* a deliberate 100vw bleed needs the horizontal overflow absorbed, and it must
     be `clip` — `hidden` makes this a scroll container and kills the sticky nav */
  overflow-x: clip;

  /* THE CONTENT BOX IS PADDING ON THE SHELL, NOT MARGINS ON THE CHILDREN.
     The first cut gave `.opsdash > *` max-width:1800 + auto margins, and measured
     0 -> 1800 against the nav's 219 -> 2019. Cause: `.od-sec{margin:0 0 36px}` is a
     SHORTHAND declared later in this file, so it reset margin-left/right to 0 and
     the auto-centering silently died — as would every other child here, since
     h1, .od-lead, .od-eyebrow and .od-foot all set `margin` shorthand too.
     Padding on the parent cannot be reset by a child, so the edges cannot drift.
     max() reproduces the clamp gutter until the viewport passes 1800, then half
     the leftover on each side.

     ⚠ IT DOES NOT MATCH .nav-row-inner ABOVE 1800px, and that is deliberate for
     now. .nav-row-inner caps its BORDER box at 1800 and insets the clamp gutter
     INSIDE it, so its content starts at (100vw-1800)/2 + gutter, while max()
     returns the leftover ALONE once the leftover is the larger — a 40px split
     between the ribbon and the breadcrumb, measured 2026-09-01 10:12 MDT at 1920
     (ribbon item 100, breadcrumb 60) and invisible at 1440, where both read 31.7.
     TurnerLee overrides this in its custom-ops-chrome.html; Craig, 2026-09-01
     10:40 MDT: "don't do the same alignment on cololamb and epic4 yet." */
  padding-left:  max(var(--tlc-gut, clamp(10px, 2.2vw, 40px)), calc((100vw - var(--tlc-cmax, 1800px)) / 2));
  padding-right: max(var(--tlc-gut, clamp(10px, 2.2vw, 40px)), calc((100vw - var(--tlc-cmax, 1800px)) / 2));
}

/* OPERATIONS CONTEXT: THE SHELL BOX IS THE CONTAINER BOX. sst-styles.css:9255 gives every
   band max-width:var(--tlc-cmax) with var(--tlc-gut) padding, so a band's content edge is
   (100vw - cmax)/2 + gut. The max() above returns the LEFTOVER ALONE once the leftover is
   larger, which silently drops the gutter — measured 40px/side out at 2560
   (_notes/verify/container-width-modes-2560.txt: container content 360->2200, .opsdash
   320->2240) and identical below cmax, which is why it went unseen. The ⚠ note above this
   block predates the shared container and is answered here, for adopting sites only:
   Craig 2026-09-01 10:40 MDT "don't do the same alignment on cololamb and epic4 yet", and
   neither of them emits tlc-ctx-ops. Promoted from custom-ops-chrome.html. */
body.tlc-ctx-ops .opsdash {
  padding-left:  calc(max(0px, (100vw - var(--tlc-cmax, 1800px)) / 2) + var(--tlc-gut, clamp(10px, 2.2vw, 40px)));
  padding-right: calc(max(0px, (100vw - var(--tlc-cmax, 1800px)) / 2) + var(--tlc-gut, clamp(10px, 2.2vw, 40px)));
}

/* FULL WIDTH ON AN /ops PAGE MEANS THE SHELL, NOT #main's CONTAINER.
   sst-styles.css gives Full `#main > .container{padding-inline:5%}`, and on a public page
   that IS the content band. On /ops it is not: .opsdash breaks out to 100vw and pays its
   own padding, so the content has already escaped the box Full was widening. Measured
   /ops/cc @1920 with tlc-w-full live: #main > .container padL/R 96px (5%, correct) while
   the real content sat at 40 -> 1880 from the rule above — HD, not Full.
   EQUAL SPECIFICITY (0,2,1) TO THE RULE ABOVE, so it must stay LATER IN THIS FILE. Moving
   it to sst-styles.css would lose: that sheet is in <head> and this one loads from the
   page body via all-ops-chrome.html. Craig 2026-09-02: "/ops/cc should be on new 'full'
   template." */
/* 5vw, NOT 5%. A percentage padding resolves against the CONTAINING BLOCK, and .opsdash's
   containing block is #main > .container — which Full has already inset by 5%. Measured
   /ops/cc @1920: 5% gave 86.39px against the public Full band's 96px, so the two pages
   disagreed about what "5%" meant. .opsdash is a 100vw breakout, so the viewport is its
   real basis and vw is the unit that matches its own width declaration. 5vw of 1920 = 96px,
   identical to #main > .container on a public Full page. */
/* THE FORMULA IS NOW A TOKEN — --tlc-full-pad, declared once on :root in
   shared/core/v1/sst-site-structure.css and consumed by body.tlc-w-full #main >
   .container there, by shared/pbs/v1/pbs.css for the board's own box, and here.
   THREE consumers, ONE declaration. It computes exactly what the two lines that
   used to be here computed; the reason for 5vw rather than 5% is unchanged and is
   in the note above. Collapsed 2026-09-02 22:30 MDT. */
body.tlc-w-full .opsdash {
  padding-left:  var(--tlc-full-pad);
  padding-right: var(--tlc-full-pad);
}
/* A BOOTSTRAP CONTAINER INSIDE THE SHELL PAYS THE GUTTER TWICE. .opsdash already
   breaks out to 100vw and sets its own padding (the two rules above), so a
   .container-xxl direct child adds a SECOND var(--tlc-gut) — up to 40px at 1920 —
   and the content lands inset from the ribbon and the breadcrumb it is supposed to
   line up with. sst-site-structure.css:80-88 is the rule that does it, at (0,1,0)
   with no !important; this is (0,2,0) and beats it on specificity, so no
   !important here either. Scoped to .opsdash, so no public page is affected.
   Reached by /ops/all-users and /ops/all-organizations, whose shared bodies open
   with <div class="ct-dashboard container-xxl">. Added 2026-09-02 21:49 MDT. */
.opsdash > .container,
.opsdash > .container-xxl,
.opsdash > .container-xl,
.opsdash > .container-lg {
  width: 100%;
  max-width: none;
  padding-left: 0;
  padding-right: 0;
}

/* The count line that replaces the suppressed centered <p> in the two all-* bodies
   when the page supplies the ops header. Same voice as .od-lead, one size down. */
.od-count {
  font-size: 15px;
  color: var(--quiet);
  margin: 0 0 24px;
}
/* The nav row still bleeds edge to edge: its own 100vw breakout is measured from
   its static position inside this padding, which lands it back at viewport 0. */

.od-eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--hush);
  margin: 0 0 6px;
}
/* DIRECT CHILD ONLY — the page title. A descendant selector here also caught
   every <h1> sample inside the embedded guide. See THE GUIDE FENCE above. */
.opsdash > h1 {
  /* PRIMARY FACE, not --ops-font. The page title is an h1 and the secure-zone
     contract says h1/h2/h3 take the site's own face. The general rule above
     matches this element and still loses to it - measured 2026-08-23, computed
     came back Open Sans with both rules matching - so the value is stated here
     rather than left to a specificity argument the browser already settled. */
  font-family: var(--font1Family, var(--ops-font));
  font-size: clamp(28px, 3.2vw, 38px);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.01em;
  margin: 0 0 10px;
  color: var(--ink);
}
.od-lead {
  font-size: 17px;
  line-height: 1.55;
  color: var(--quiet);
  max-width: 68ch;
  margin: 0 0 28px;
}

/* ── CONTEXT INK FOR THE PAGE CHROME — the first consumer of the --ctx-* slots ──
   Added 2026-09-02 22:20 MDT.

   THE STYLE GUIDE'S BG RIBBON REPAINTS THE WHOLE CANVAS, NOT JUST THE MOUNT.
   style-guide.js setBg() writes the context color onto html AND body with
   !important, so on ON DARK / ON MIX the /ops header above
   #tl-style-guide-mount is on a dark ground too. But applyCtx() only ever stamped
   .onLt/.onDrk/.onMix on [id^="page-"], and every one of those divs is INSIDE the
   mount — so the crumb, eyebrow, h1 and lead entered NEITHER context system.
   Measured cause of the navy Craig named: `.od-crumb a` reads var(--LinkColor),
   #00509F on turnerlee.com, and with no context class nothing reversed it. --ink
   is worse: color-mix(--color1 16%, #14171c) is 84% near-black whatever --color1
   does, so reversing color1 alone could never have saved it.
   sst-context.css has declared these slots since 2026-08-31 and, grepped estate-wide
   before this rule, had ZERO consumers — its own header says "nothing changes until
   a component reads var(--ctx-ink)". This is that component.

   THIS IS THE SECTION 16 PATTERN FROM style-guide.css, NOT A SECOND MECHANISM:
       #tl-style-guide-mount .onLt, .onDrk, .onMix { --FontColor: var(--txt); }
   REMAP THE TOKEN INSIDE THE CONTEXT CLASS; leave every consuming declaration
   alone. Nothing here sets `color` — .od-eyebrow, .opsdash > h1, .od-lead and the
   .od-crumb rules keep reading var(--hush) / var(--ink) / var(--quiet) /
   var(--LinkColor) exactly as they do on every other /ops page, and get the
   context's answer instead of the light-ground one.

   THE MAP IS THE RAMP, ONE FOR ONE:
     --ink   reading ink -> --ctx-ink    (dark + mix rgba(255,255,255,.92))
     --quiet secondary   -> --ctx-ink-2  (80% of ink)
     --hush  faintest    -> --ctx-ink-3  (47% of ink)
     --line  hairline    -> --ctx-line   (10.6% of ink)
     the LINK              -> --ctx-link, but set on the ANCHOR, never by remapping
                              --LinkColor. See the rule below.

   THE LINK IS NOT A TOKEN REMAP, AND THAT IS A MEASURED CORRECTION.
   `--LinkColor: var(--ctx-link)` was the first version of this block and it is a
   CYCLE: sst-context.css defines --ctx-link as --_linkAuto, which is
   color-mix(..., var(--LinkColor)). CSS makes every custom property in a cycle
   invalid at computed-value time, so BOTH resolved to the guaranteed-invalid value.
   Measured on the live page 2026-09-02 23:05 MDT: --LinkColor and --ctx-link both
   computed to the EMPTY string on .od-crumb, and because a declared-but-empty name
   substitutes nothing rather than taking a var() fallback, `color: var(--LinkColor,
   var(--accent))` went invalid and the link INHERITED rgb(0,121,193) — still a blue
   on near-black. Setting the anchor's color directly leaves --LinkColor alone, so
   --ctx-link can read it and do its job.

   .onLt IS DELIBERATELY NOT IN THE LIST. style-guide.js stamps only dark and mix
   on these four, because Craig approved the light state and did not ask for it to
   change; adding .onLt here would move them off the brand-tinted /ops ramp onto
   the neutral scale. One line to add if that is ever wanted.

   NO CONTEXT CLASS = NO MATCH, so every other /ops page on every site renders
   byte-identically to today. */
:is(.opsdash, .od-demo) > :is(.od-search-bar, .od-crumb, .od-eyebrow, h1, .od-lead):is(.onDrk, .onMix) {
  --ink:   var(--ctx-ink);
  --quiet: var(--ctx-ink-2);
  --hush:  var(--ctx-ink-3);
  --line:  var(--ctx-line);
}
/* THE CRUMB LINK, ON THE ANCHOR. Two selectors because the crumb appears in two
   places: the TOP crumb is inside .od-search-bar (sst-ops-pagenav.html puts them on
   one row) and the FOOT crumb is a direct child of .opsdash and carries the context
   class itself. (0,4,1) and (0,3,1) both beat `:is(.opsdash,.od-demo) .od-crumb a`
   at (0,2,1); neither needs !important because that rule carries none. */
:is(.opsdash, .od-demo) > .od-search-bar:is(.onDrk, .onMix) .od-crumb a,
:is(.opsdash, .od-demo) > .od-crumb:is(.onDrk, .onMix) a {
  color: var(--ctx-link);
}
:is(.opsdash, .od-demo) > .od-search-bar:is(.onDrk, .onMix) .od-crumb a:hover,
:is(.opsdash, .od-demo) > .od-search-bar:is(.onDrk, .onMix) .od-crumb a:focus-visible,
:is(.opsdash, .od-demo) > .od-crumb:is(.onDrk, .onMix) a:hover,
:is(.opsdash, .od-demo) > .od-crumb:is(.onDrk, .onMix) a:focus-visible {
  color: var(--ctx-ink);
}

/* ============================ 2 · SECTION HEADS ========================== */
/* PB2 section head: title and its one-line explanation on the same baseline,
   wrapping to two lines only when it genuinely has to. */
/* 60px, was 36. Craig's spacing scale, 2026-08-24: "60 section to section".
   The whole top of the scale moved up — 28 cluster, 40 gutter, 60 section,
   100 foot rule, 120 bottom. */
.od-sec { margin: 0 0 60px; }
.od-sec-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
  margin: 0 0 16px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.od-sec-head h2 {
  font-size: 22px;
  font-weight: 700;
  line-height: 1.2;
  margin: 0;
  color: var(--ink);
}
.od-sec-head .od-sec-desc {
  font-size: 14px;
  color: var(--hush);
  margin: 0;
}
.od-sec-head .od-sec-count {
  margin-left: auto;
  font-size: 12px;
  font-weight: 500;
  color: var(--hush);
  font-variant-numeric: tabular-nums;
}

/* ============================ 3 · CARDS ================================== */
/* Workspace card, class-for-class: image band, body, tier rule. */
/* THE GAP IS BOOTSTRAP'S LADDER, NOT A NUMBER OF OURS. Craig, 2026-08-24:
   "card grid gap should not be set, in bootstrap5 it is a variance gap-3 etc
   and it's important that we can control gaps."

   Measured on the live guide: this build's root font-size is 17.6px, so
   gap-0 0 · gap-1 4.4 · gap-2 8.8 · gap-3 17.6 · gap-4 26.4 · gap-5 52.8.
   The literal here was 18px — gap-3 to within half a pixel. It was never a
   design decision; it was 1rem written as a magic number that could not move
   with the type scale. Now it IS 1rem.

   Bootstrap's gap utilities carry !important, so `class="od-grid gap-4"`
   already overrides this — verified in the browser, not assumed. Nothing has
   to be removed for a page to control its own gap; it needs a class. */
.od-grid { display: grid; gap: 1rem; }
.od-grid-3 { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.od-grid-2 { grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); }

/* Bootstrap and MDB both own .card and .grid on these templates, and both will
   float, absolutely position or masonry a child out of its cell given the chance.
   These three properties are restated so no inherited rule can move a card. */
.od-grid > * { position: static; float: none; margin: 0; min-width: 0; }

.od-card {
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  /* 20px, was 14 — same pass as .odc-card below. One card radius on the estate. */
  border-radius: 20px;
  padding: 0;                 /* the image sits flush at the top */
  overflow: hidden;           /* and is clipped by the radius */
  text-decoration: none;
  color: inherit;
  position: relative;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--color1) 8%, transparent);
  transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
}
/* Promote BEFORE the hover, never on it — a will-change applied by the hover rule
   causes the one-frame hitch it was added to cure. */
.od-card { will-change: transform; }
.od-card:hover {
  border-color: var(--line-2);
  transform: translateY(-2px);
  box-shadow: 0 4px 16px color-mix(in srgb, var(--color1) 14%, transparent);
}
.od-card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---- image band --------------------------------------------------------- */
/* Drop a real screenshot or mark in with an inline style:
     <span class="od-card-image" style="background-image:url('/img/workspace/x.svg')">
   With no inline style you get the gradient placeholder, which is the honest
   result — a plausible stand-in image would be a claim about what the tool is. */
.od-card-image {
  display: block;
  aspect-ratio: 16 / 9;
  width: 100%;
  background-color: var(--panel-2);
  background-image: linear-gradient(135deg, var(--panel-2) 0%, var(--panel-3) 100%);
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
  border-bottom: 1px solid var(--line);
  transition: filter .3s ease;
  transform-origin: center top;
}
.od-card:hover .od-card-image { filter: brightness(1.04) saturate(1.03); }
/* Framed-preview inner shadow, so a screenshot reads as a preview and not a tile. */
.od-card-image::after {
  content: "";
  display: block;
  width: 100%;
  height: 100%;
  box-shadow: inset 0 -12px 16px -12px color-mix(in srgb, var(--color1) 16%, transparent);
  pointer-events: none;
}
/* .logo-pad is Workspace's inset-and-contain treatment for standalone marks.
   Craig, 2026-08-22: "The card images have a little extra padding, the img inside
   the card should be going edge to edge 'cover'." So the hub does NOT use it —
   every card image is cover, full bleed, no inset. Kept defined because Workspace
   still uses it and the two sheets should not disagree about what the class means. */
.od-card-image.logo-pad {
  padding: 0 20px;
  background-origin: content-box;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
.od-card-image.logo-pad::after { box-shadow: none; }

/* ---- body --------------------------------------------------------------- */
.od-card-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1;
  padding: 18px 22px 16px;
  border-bottom: 4px solid transparent;
}
.od-card-title {
  font-size: 17px;
  font-weight: 700;
  line-height: 1.25;
  margin: 0;
  color: var(--ink);
}
.od-card-desc {
  font-size: 13.5px;
  line-height: 1.5;
  margin: 0;
  color: var(--quiet);
}
.od-card-meta {
  margin-top: auto;
  padding-top: 4px;
  font-size: 12px;
  color: var(--hush);
  font-variant-numeric: tabular-nums;
}

/* Tier rule — the 4px bottom edge that tells the four sections apart at a glance
   without four different card colors. */
.od-card.tier-1 .od-card-body { border-bottom-color: var(--color1); }
.od-card.tier-2 .od-card-body { border-bottom-color: var(--color2); }
.od-card.tier-3 .od-card-body { border-bottom-color: var(--color3); }
.od-card.tier-4 .od-card-body { border-bottom-color: var(--color4); }

/* Not-built cards stay legible — dimmed, not greyed into unreadability, and they
   do not lift on hover because there is nowhere to go. */
.od-card.is-todo { opacity: .62; }
.od-card.is-todo:hover { transform: none; border-color: var(--line); box-shadow: 0 1px 3px color-mix(in srgb, var(--color1) 8%, transparent); }

/* ---- chip --------------------------------------------------------------- */
.od-chip {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: 4px;
  border: 1px solid currentColor;
  color: var(--hush);
  line-height: 1;
}
.od-chip.is-live  { color: var(--ok); }
.od-chip.is-setup { color: var(--warn); }
.od-chip.is-bad   { color: var(--bad); }

/* ============================ 4 · PAGE NAV + SEARCH ======================= */
/* These are sst-ops-pagenav.html's classes, ported from v1 unchanged. The hub
   does NOT get a second search box of its own — one page, one search. The first
   cut of the v2 body added one and the page rendered two stacked search fields
   doing the same job.

   The site sheet carries input[type="text"]:not(.browser-default) at (0,2,1),
   which beats a (0,2,0) class rule however many !importants either side has.
   Matched at (0,3,1) so specificity settles it rather than escalation. */
/* BREADCRUMB — it belongs to the NAV above it, not to the title below it.
   Craig, 2026-08-23: "You've placed the breadcrumbs directly above an eyebrow,
   which makes it look like two eyebrows... if we need page NAV here, it should
   sit closer to the ribbons, and I'd prefer those to be link color for standard
   links. Our own style guide, which is exactly what this is, has color for links
   and not underlines."

   Two small-caps letterspaced lines 18px apart read as one repeated element. The
   gap below is therefore LARGER than the gap above, and the crumb takes the
   site's real link tokens so it reads as navigation instead of as a label. The
   eyebrow keeps its tight 6px to the h1 so eyebrow + title stay one unit. */
/* A CRUMB IS NEVER FLUSH WITH WHATEVER IS ABOVE IT. Craig, 2026-08-28: "the
   breadcrumbs are jammed right up against the minor nav… This is never the
   appropriate use of breadcrumbs. The breadcrumbs should always have a little
   bit of padding above them no matter whether it's a headline or whether it's a
   NAV bar it does not matter." It was margin:0 on top, so on any page whose
   first element is the crumb it sat hard against the site nav.
   PADDING, not margin: .opsdash has no padding-top, so a margin-top on its first
   child collapses straight through the parent and moves the whole 100vw bleed
   down instead of opening a gap inside it. Padding cannot collapse. */
:is(.opsdash, .od-demo) .od-crumb {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: 0 0 34px;
  padding-top: 24px;
  /* 13px, was 12. Craig, 2026-08-28: "Let's increase the font size on the bread
     crumbs here in an all operations, and in the style guide by one pixel." One
     rule covers both - .opsdash is every /ops page and .od-demo is the style
     guide's demo scope, and this is the only font-size declared on .od-crumb in
     either shared sheet. */
  font-size: 13px; text-transform: uppercase; letter-spacing: .12em;
}
:is(.opsdash, .od-demo) .od-crumb a {
  color: var(--LinkColor, var(--accent));
  text-decoration: none; border-bottom: none;
}
:is(.opsdash, .od-demo) .od-crumb a:hover,
:is(.opsdash, .od-demo) .od-crumb a:focus-visible { color: var(--LinkHoverColor, var(--color2)); text-decoration: none; }
:is(.opsdash, .od-demo) .od-crumb a[aria-current="page"] { color: var(--hush); }
:is(.opsdash, .od-demo) .od-crumb span { color: var(--hush); }

/* The SAME crumb, repeated at the foot of the page. Only the spacing differs:
   the top one leads into the title (margin-bottom), the foot one closes the page
   behind the rule the old .od-foot used to draw. */
:is(.opsdash, .od-demo) .od-crumb.is-foot {
  margin: 40px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* NOT STICKY. Operations and Brand are decisions you make once and then read
   past; only the page switcher has to stay reachable, and three pinned ribbons
   eat 150px of a laptop screen on every scroll. Craig, 2026-08-23: "the PAGES
   ribbon is the only one that needs to be sticky, the other 2 above it should
   not be sticky below the minor nav." */
.opsdash .top-nav { position: relative; z-index: 50; }

/* IN OPERATIONS CONTEXT THE RIBBON IS THE STICKY HEADER. Craig 2026-09-02, quoted at
   sst-styles.css:9311 — the minor and main nav display but do not stick, so the PageNav
   ribbon is what pins. --ws-hdr-offset is 0px there (sst-styles.css:9342) so it lands
   flush at the viewport top; the 40px fallback covers a site not yet in this context.
   Supersedes the 2026-08-23 decision above FOR ADOPTING SITES ONLY. Promoted from
   custom-ops-chrome.html:107, which the shared sheet was already documenting as one of
   four readers of --ws-hdr-offset — a shared file citing a local patch is the smell. */
body.tlc-ctx-ops .opsdash .top-nav { position: sticky; top: var(--ws-hdr-offset, 40px); }

/* THE ONE STICKY ROW. It is a DIRECT CHILD of .opsdash, not of .top-nav: a
   sticky element is constrained by its parent's padding box, so inside the
   three-row box it would have unstuck as soon as that box scrolled past.
   style-guide.js grafts it here; see graftOpsNav. */
.opsdash .nav-row.od-pagenav {
  /* TWO CLASSES ON PURPOSE. `.opsdash .nav-row` sets position:relative at the
     same (0,2,0) specificity and sits later in this file, so a single-class rule
     lost and the row scrolled away with the other two — measured top -1644 while
     computed position still read "relative". */
  position: sticky;
  top: var(--ws-hdr-offset, 40px);
  z-index: 51;
  margin-bottom: 14px;
  /* The bleed is MARGIN-ONLY here. On a sticky element `left`/`right` are the
     sticky constraint insets, not offsets — inherit .nav-row's left:50%/right:50%
     and the row tries to pin horizontally as well. calc(50% - 50vw) reaches
     viewport 0 without touching either. */
  left: auto; right: auto;
  width: 100vw;
  margin-left:  calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}
/* PAINT IS SHARED WITH THE GUIDE DEMO, THE 100vw BREAKOUT IS NOT.
   Layouts section 8 documents this ribbon inside a three-column grid, where a
   100vw bleed would tear the page open — so the demo gets the colors, the box
   and the pills, and only a real .opsdash page gets the bleed. */
:is(.opsdash, .od-demo) .nav-row { background: var(--sg-nav-bg-1, var(--color1)); }
.opsdash .nav-row {
  width: 100vw; position: relative; left: 50%; right: 50%;
  margin-left: -50vw; margin-right: -50vw;
}
/* AND THE DEMO HAS TO OPT BACK OUT, because the rule above is a DESCENDANT
   selector and the style guide is itself an .opsdash page — so every demo nav
   row inside .od-demo was inheriting the breakout it is documented as not
   having. Measured 2026-08-30 on /ops/style-guide#layouts @1440: Layouts §8
   .nav-row x=-66.9 w=1440 against a section that starts at x=290.4. Equal
   specificity (0,2,0) to the rule above, later in the sheet, so it wins. */
.od-demo .nav-row {
  width: auto; position: relative; left: auto; right: auto;
  margin-left: 0; margin-right: 0;
}
:is(.opsdash, .od-demo) .nav-row.row-2 { background: var(--sg-nav-bg-2, color-mix(in srgb, var(--color1) 82%, #000000)); }
:is(.opsdash, .od-demo) .nav-row-inner {
  max-width: var(--tlc-cmax, 1800px); margin: 0 auto; padding: 10px var(--tlc-gut, clamp(10px, 2.2vw, 40px));
  display: flex; align-items: center; gap: 12px; flex-wrap: nowrap;
  overflow-x: auto;                  /* scrolls, so the pills never wrap */
  scrollbar-width: none; -ms-overflow-style: none; -webkit-overflow-scrolling: touch;
  user-select: none;
  font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: .08em;
}
:is(.opsdash, .od-demo) .nav-row-inner::-webkit-scrollbar { display: none; }
:is(.opsdash, .od-demo) .nav-row-inner > .nav-label,
:is(.opsdash, .od-demo) .nav-row-inner > .nav-pill { flex-shrink: 0; }
:is(.opsdash, .od-demo) .nav-label { color: rgba(255,255,255,.92); margin-right: 8px; font-weight: 700; }

/* OPERATIONS CONTEXT: THE RIBBON CLOSES UP. Craig 2026-09-01 09:52 MDT "remove all the
   weird padding between the items on the page NAV ribbon", 10:12 MDT "put a gap back
   after the logo only". Promoted from custom-ops-chrome.html, whose own note scoped it
   to this site by an agent's choice, not by anything Craig said about the others. The
   gate keeps ColoradoLamb / EPIC4 / SweetwaterCreek on gap:12px + 8px until he looks. */
body.tlc-ctx-ops :is(.opsdash, .od-demo) .nav-row.row-1 .nav-row-inner { gap: 0; }

/* THE WORDMARK SITS IN THE HEADER SLOT THE LABEL USED TO HOLD. Craig, 2026-09-01
   07:52 MDT: "the wordmark logo goes in the ops header, width 200." The ribbon is
   the only header an /ops page has — this sheet hides nav#second / .navbar.bg-Main,
   which is where the public header logo renders — so /ops carried no mark at all
   until now.

   IT LIVES INSIDE .nav-label ON PURPOSE. `.nav-row-inner > .nav-label` above is
   what carries flex-shrink:0; a bare <img> as a direct child would be the first
   thing an overflow-x:auto row squeezes, and 200px would silently become 140.

   width + height:auto, NOT a height cap. Each site's wordmark has its own aspect
   ratio and .nav-row-inner is content-sized (padding:10px), so the row grows to
   whatever 200px costs that mark and align-items:center keeps the pills on its
   middle. A fixed height would letterbox one site's mark into another's shape.

   200 IS HERE AND NOT IN LogoWidthHeader. That variable lives in ID Styles -
   Layout, and on ColoradoLamb the Operations brand shares Brand 16437 with
   Default — setting it there would have shrunk the PUBLIC header logo (260px)
   too. This selector cannot leave .opsdash.

   display:block removes the inline baseline gap. Left inline, the mark sits on
   the text baseline of a 13px uppercase row and inherits the descender space
   under it, which reads as the logo hanging low in the bar. */
:is(.opsdash, .od-demo) .nav-label > .nav-logo { display: block; width: 200px; height: auto; }

/* AND THE LABEL'S 8px MARGIN COMES OFF WHEN IT HOLDS A LOGO, because it is the
   one gap in the row that is not the flex gap. Measured 2026-09-01 09:52 MDT,
   turnerlee.com/ops/dashboard @1440: logo->All 20px against 12px between every
   pill — .nav-row-inner's `gap:12px` PLUS this margin. It was right for a text
   label sitting against the pills and it is an odd man out under a wordmark, so
   it is scoped to the logo case and every site still on the text label keeps it. */
:is(.opsdash, .od-demo) .nav-label:has(> .nav-logo) { margin-right: 0; }
:is(.opsdash, .od-demo) .nav-pill {
  padding: 6px 14px; border-radius: 4px;
  background: transparent; color: rgba(255,255,255,.78); border: none;
  font-family: inherit; font-size: inherit; font-weight: inherit;
  text-transform: inherit; letter-spacing: inherit;
  text-decoration: none; cursor: pointer; transition: background .15s, color .15s;
}
:is(.opsdash, .od-demo) .nav-pill:hover { background: rgba(255,255,255,.12); color: #fff; }
:is(.opsdash, .od-demo) .nav-pill:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
:is(.opsdash, .od-demo) .nav-pill.active { background: var(--sg-pill-active, rgba(255,255,255,.24)); color: #fff; }

/* ── GRAFTED GUIDE RIBBONS (ribbon 2 Brand, ribbon 3 Pages) ────────────────
   style-guide.js moves the style guide's own two ribbons in here so all three
   share ONE sticky container. They arrive carrying their own container geometry
   from style-guide.css — a 100vw breakout and a 1400px inner — which is exactly
   what put their left edge ~200px inside the Operations row. Everything below
   strips that and hands them THIS file's box, the same declarations .nav-row and
   .nav-row-inner use, so the three left edges are one number in one place.

   .row-3 continues the ramp: each ribbon a step darker than the one above it, so
   the stack reads as one control surface rather than three unrelated bars. */
/* GEOMETRY COMES FROM .nav-row, NOT FROM HERE. The grafted rows carry
   .nav-row, so the 100vw breakout above already applies to them and the band
   bleeds edge to edge exactly like ribbon 1. An earlier cut reset them to
   position:static/width:auto instead, which left the band inside .opsdash's own
   23px gutter and then the inner added its gutter again — measured label x=46 on
   ribbons 2/3 against x=23 on ribbon 1. Reset ONLY what style-guide.css sets that
   .nav-row does not: its 1400px inner and its fixed-position sticky mode. */
.opsdash .guide-header-row { color: rgba(255,255,255,.92); }
.opsdash .guide-header-row.is-stuck {
  position: static; top: auto; z-index: auto;   /* the container is the sticky one now */
}
:is(.opsdash, .od-demo) .nav-row.row-3 { background: var(--sg-nav-bg-3, color-mix(in srgb, var(--color1) 66%, #000000)); }

.opsdash .guide-header-row-inner {
  max-width: var(--tlc-cmax, 1800px); margin: 0 auto;
  padding: 10px clamp(10px, 2.2vw, 40px);
  display: flex; align-items: center; gap: 12px; flex-wrap: nowrap;
  overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
}
.opsdash .guide-header-row-inner::-webkit-scrollbar { display: none; }
.opsdash .nav-cluster,
.opsdash .nav-group { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.opsdash .nav-cluster + .nav-cluster { margin-left: 22px; }

/* The guide's buttons take the Operations pill exactly — same padding, radius,
   ink and active fill as .nav-pill. Two ribbons of pills that ALMOST match is
   the drift this whole file exists to stop. */
.opsdash .guide-logo {
  font-size: 13px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: rgba(255,255,255,.92); margin-right: 8px; flex-shrink: 0;
}
.opsdash .nav-btn {
  padding: 6px 14px; border-radius: 4px;
  background: transparent; color: rgba(255,255,255,.78); border: none;
  font-family: var(--ops-font); font-size: 13px; font-weight: 500;
  text-transform: uppercase; letter-spacing: .08em;
  text-decoration: none; cursor: pointer; flex-shrink: 0;
  transition: background .15s, color .15s;
}
.opsdash .nav-btn:hover { background: rgba(255,255,255,.12); color: #fff; }
.opsdash .nav-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.opsdash .nav-btn.pg-active,
.opsdash .nav-btn.active { background: var(--sg-pill-active, rgba(255,255,255,.24)); color: #fff; }

/* THE CRUMB ROW AND THE SEARCH ROW ARE ONE ROW. Craig, 2026-08-28: "Make the
   search input in line with the operations breadcrumbs, so that the operations
   breadcrumbs can be in the appropriate spot space exactly down below the page
   NAV ribbon" / "the same amount of space that ops cc has down from the minor
   nav." The crumb is emitted INSIDE this bar by sst-ops-pagenav.html — it cannot
   be a sibling and still share the line, because .opsdash is a plain block and
   every child of it has to stay a direct child (the search hides `.opsdash > *`).

   THE 24px BELONGS TO THE ROW, NOT TO THE CRUMB, AND LEAVING IT ON THE CRUMB IS
   A REAL BUG - it shipped for one round. .od-crumb carries padding-top:24px of
   its own; inside a flex row with align-items:center that padding is part of the
   crumb's BOX, so the 40px input centers against a 41.6px box whose top 24px is
   empty. The input lands 0.8px under the ribbon. Craig, 2026-08-28: "the word
   search and it's input that is locked too. It should be center aligned with
   operations on the left side. It is not it is up to hide jamming into our page
   ribbon now which should never happen."
   So the crumb's padding is reset to 0 in the row and the BAR takes the 24px.
   Every item then clears the ribbon by 24 and the crumb text centers on the input
   text, which is what "center aligned with operations" means.

   THE COST, STATED: the crumb TEXT now sits ~35px under the ribbon where /ops/cc's
   sits at 23. That is not a miss, it is arithmetic - a 40px input centered on a
   17.6px line cannot also put that line 23px down without pulling the input up
   into the ribbon, which is the thing he just said must never happen. The ROW
   starts at 24, same as cc's crumb box.

   PADDING ABOVE, NOT MARGIN: .opsdash has no padding-top, so a margin-top on its
   first child collapses through the parent and moves the whole 100vw bleed down
   instead of opening a gap inside it. Only the crumb's bottom margin moves to the
   bar, because 34px of it inside a flex row would be 34px of row, not 34px of
   gap. */
.opsdash .od-search-bar {
  max-width: var(--tlc-cmax, 1800px); margin: 0 auto 34px; padding-top: 24px;
  display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex-wrap: wrap;
}
/* The crumb takes the left of the bar and the search cluster keeps the right.
   margin-right:auto rather than justify-content:space-between, so the bar still
   right-aligns correctly if it is ever rendered without a crumb. */
.opsdash .od-search-bar .od-crumb { margin: 0 auto 0 0; padding-top: 0; }
.opsdash .od-search-label {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em;
  color: var(--hush);
  margin: 0 !important; line-height: 1 !important;
}

/* SEARCH / CLEAR — ported VERBATIM. Craig, 2026-08-23: "do not make it up, use
   exact code." Values copied from .pbx-q-toggle in Project-Board-Styles.html
   (/ops/schedule), which itself is the ported .q-toggle from
   fantasy/cfb/players.html. Only the two color tokens are re-pointed at this
   sheet's palette, exactly as the board did:
     muted  var(--s-mute)   -> var(--hush)
     accent var(--s-accent) -> var(--color1) */
.opsdash .q-toggle {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--hush);
  user-select: none;
  align-self: center;
  cursor: default;
  white-space: nowrap;
}
.opsdash .q-toggle.clear { cursor: pointer; color: var(--color1); }
.opsdash .q-toggle.clear:hover { text-decoration: none; }
.opsdash input.od-search-input[type="text"] {
  width: 280px !important; max-width: 100% !important; flex: 0 0 auto !important;
  display: inline-block !important;
  font-family: var(--ops-font) !important;
  color: var(--ink) !important;
  /* The site's .form-control computes to 79px tall with a 22px font and a 79.2px
     line-height — fine for a full-width form field, absurd for a filter box.
     Every metric has to be restated or the tall one wins; these are workspace.html's
     .search-input numbers verbatim so the two searches cannot drift apart. */
  height: 38px !important;
  padding: 6px 10px !important;
  line-height: 24px !important;
  font-size: 16px !important;
  /* The site's form styles put margin:0 0 22px on inputs. align-items:center
     centers the MARGIN box, so that bottom margin pushed the input's border box
     exactly 11px (half of 22) above the label and read as "SEARCH is not centered
     with the field". Zeroing the margin is the fix; nudging the label would have
     hidden a layout bug behind a magic number. */
  margin: 0 !important;
}
.opsdash input.od-search-input[type="text"]:focus {
  outline: none;
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color1) 18%, transparent) !important;
}
.opsdash .od-results-note { color: var(--hush); font-size: 13px; margin: 0 0 12px; }

/* ---- .od-field — the COMPACT FORM CONTROL for /ops -------------------------
   Same fight .od-search-input above already had, and the second page to need it
   (2026-09-01, /ops/edit-brand-styles: 190 property rows) is the signal to make
   it a component instead of restating the metrics a third time. The site's
   .form-control computes to 79.19px tall with a 22px font — correct for a
   full-width form field, unusable in a table row.

   Every metric is restated because the rule it has to beat carries !important,
   and an !important declaration cannot be beaten by a non-important one at ANY
   specificity. The numbers ARE .od-search-input's, deliberately, so the two
   cannot drift; what .od-field does NOT take is that rule's `width:280px` pin —
   a field in a table sizes to its column.

   Use: class="form-control od-field" / class="form-select od-field". The guide's
   own component stays first in the class string; this is the /ops context on top
   of it, exactly as .od-search-input does. */
.opsdash input.od-field[type="text"],
.opsdash input.od-field[type="search"],
.opsdash input.od-field[type="number"],
.opsdash select.od-field,
.opsdash .form-floating > select.od-field {
  width: 100% !important; max-width: 100% !important; min-width: 0 !important;
  height: 38px !important;
  padding: 6px 10px !important;
  line-height: 24px !important;
  font-size: 16px !important;
  font-family: var(--ops-font) !important;
  color: var(--ink) !important;
  background-position: right 8px center !important;
}
.opsdash input.od-field[type="text"]:focus,
.opsdash input.od-field[type="search"]:focus,
.opsdash input.od-field[type="number"]:focus,
.opsdash select.od-field:focus {
  outline: none;
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color1) 18%, transparent) !important;
}

/* ============================ 5 · TABLES ================================= */
/* The WC26 League Standings pattern. Wide screens: an ordinary table. Narrow:
   the table becomes a horizontally scrolling flex grid whose left identity group
   is frozen. Put .od-table on the <table>, wrap it in .od-table-scroll, and tag
   the identity columns .od-c-1 / .od-c-2 / .od-c-3. */
.od-table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}
.od-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.od-table th,
.od-table td {
  padding: 8px;
  font-size: 16px;
  vertical-align: middle;
  text-align: left;
}
.od-table th {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--hush);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
/* Solid, not translucent — a frozen cell with a see-through background lets the
   scrolling cells show through underneath it on iOS. */
.od-table tbody tr:nth-child(even) td { background: var(--panel-2); }
.od-table tbody tr td { background: var(--panel); }
.od-table tbody tr:nth-child(even) td { background: var(--panel-2); }
.od-table tbody tr:hover td { background: var(--panel-3); }

@media (max-width: 640px) {
  /* let the scroll container absorb the overflow, not the page */
  .od-table-scroll { min-width: 0; }
  .od-table, .od-table thead, .od-table tbody { display: block; }
  .od-table thead tr, .od-table tbody tr {
    display: flex;
    width: max-content;
    min-width: 100%;
  }
  .od-table th, .od-table td {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 8px 6px !important;
    min-width: 0;
  }
  /* frozen group — left offsets are the CUMULATIVE widths of the columns before */
  .od-table .od-c-1 { width: 34px  !important; position: sticky; left: 0;    z-index: 3; }
  .od-table .od-c-2 { width: 56px  !important; position: sticky; left: 34px; z-index: 3; }
  .od-table .od-c-3 { width: 160px !important; position: sticky; left: 90px; z-index: 3;
    box-shadow: 2px 0 4px color-mix(in srgb, var(--color1) 16%, transparent); }
  .od-table thead .od-c-1,
  .od-table thead .od-c-2,
  .od-table thead .od-c-3,
  .od-table tbody td.od-c-1,
  .od-table tbody td.od-c-2,
  .od-table tbody td.od-c-3 { background: var(--panel); }
  .od-table tbody tr:nth-child(even) td.od-c-1,
  .od-table tbody tr:nth-child(even) td.od-c-2,
  .od-table tbody tr:nth-child(even) td.od-c-3 { background: var(--panel-2); }
}

/* ============================ 5b · EMPTY + LIST ========================== */
/* An honest empty state, and the "what has to happen first" list on a stub page.
   Both are deliberately plain: a placeholder that looks like a working panel gets
   reported as broken, repeatedly, by whoever opens it expecting the tool. */
:is(.opsdash, .od-demo) .od-empty {
  margin: 0;
  padding: 22px 24px;
  background: var(--panel-2);
  border: 1px dashed var(--line-2);
  border-radius: 12px;
  color: var(--quiet);
  font-size: 15px;
  line-height: 1.6;
  max-width: 76ch;
}
:is(.opsdash, .od-demo) .od-empty a { color: var(--accent); }

.opsdash .od-list {
  margin: 18px 0 0;
  padding: 0 0 0 22px;
  max-width: 76ch;
  color: var(--quiet);
  font-size: 15px;
  line-height: 1.7;
}
.opsdash .od-list li { margin: 0 0 6px; }
.opsdash .od-list li::marker { color: var(--hush); }

/* ---- code sample (style guide) ------------------------------------------ */
:is(.opsdash, .od-demo) .od-code {
  margin: 12px 0 0;
  padding: 14px 16px;
  background: var(--panel-3);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--quiet);
  white-space: pre;
  max-width: 100%;
}
.opsdash code:not(#tl-style-guide-mount code) {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .92em;
  color: var(--quiet);
}

/* ============================ 6 · FOOT =================================== */
.od-foot {
  margin: 40px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  font-size: 13px;
  line-height: 1.6;
  color: var(--hush);
}
.od-foot code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--quiet);
}

/* ============================ 7 · RESPONSIVE ============================= */
/* 1280 is the WC26 collapse. Below it the section rail and the content column
   stop competing and everything runs full width. */
@media (max-width: 1280px) {
  .od-grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .od-grid-3, .od-grid-2 { grid-template-columns: 1fr; }
  .od-lead { font-size: 16px; }
  .od-sec-head .od-sec-count { margin-left: 0; width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .od-card, .od-card-image { transition: none; }
  .od-card:hover { transform: none; }
}


/* ============================ 8 · AVATAR ================================= */
/* Added 2026-08-23. There was no avatar primitive on the estate — the member
   bar drew its own inline-styled circle and the project board another — which
   is exactly the duplication this guide exists to stop. Documented in Web CSS
   section 22. Tokens only, no bare hex.

   The ring is a real BORDER, not an inset shadow: a fractional-px inset shadow
   rounds each edge independently and reads as a sloppy circle. */
.od-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: 0 0 auto;
  border-radius: 50%;
  border: 1px solid var(--line-2);
  background: var(--panel-3);
  background-size: cover; background-position: center;
  color: var(--ink);
  font-family: var(--ops-font);
  font-size: 14px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; text-decoration: none;
  overflow: hidden; user-select: none;
}
.od-avatar-sm { width: 28px; height: 28px; font-size: 11px; }
.od-avatar-lg { width: 56px; height: 56px; font-size: 19px; }
/* Overlapping group. The stack itself is not an avatar — it only borrows the
   class name for its children — so it resets everything the circle sets. */
.od-avatar-stack {
  width: auto; height: auto; border: none; background: none; border-radius: 0;
  display: inline-flex; align-items: center; overflow: visible;
}
.od-avatar-stack > .od-avatar { margin-left: -10px; border-color: var(--panel); border-width: 2px; }
.od-avatar-stack > .od-avatar:first-child { margin-left: 0; }
.od-avatar.is-more { background: var(--panel-2); color: var(--quiet); font-size: 12px; }

/* ============================ 9 · CARD v2 (.odc-card) ====================
   Built 2026-08-23 from Craig's vector: _component-specs/card1..card6.svg,
   re-exported with live text so every number below is READ, not estimated.

   This is the card that becomes the default for Operations, Portal, Control and
   the public sites. It ships as .odc-* alongside the existing .od-card so that
   /ops/dashboard and every section page keep rendering unchanged until Craig
   signs the new one off; the switch is then a class rename, not a rewrite.

   GEOMETRY IS LITERAL — every value is the comp's, to the hundredth.
   COLOR RESOLVES THROUGH TOKENS — the comp's values are the defaults, so it is
   pixel-identical today and can re-skin per brand without a second card.
   The two blues in the comp already ARE tokens: #0179c0 is --color1 (TurnerLee
   is #0079c1 — same color, vector rounding) and card6's #00a0ff is the /ops
   ribbon blue exactly.

   OPEN: the neutrals below are flat grays from the comp. The rest of this file
   derives its neutral ramp from --color1 with color-mix so it tints with the
   brand. Craig's call whether these follow; until then they are literal and
   named, which is the honest version of "we have not decided yet". */
/* THE SELECTOR LIST IS THE FIX, NOT THE DECLARATIONS.
   sst-styles.css carries `a, a:link, a:visited, a:hover, a:focus, button
   { border:none; background-color:transparent }`. `a:link` is (0,1,1), so it
   BEATS any class-only rule on an anchor — measured 2026-08-23 via CDP
   getMatchedStylesForNode after the border and background came out 0px and
   transparent while border-radius (which that rule does not touch) applied fine.
   Nothing in the CSSOM looked wrong; only the real cascade showed it.
   Any card, panel or chip built on an <a> has to answer :link and :visited
   explicitly or it cannot paint its own edge. */
.odc-card,
a.odc-card:link,
a.odc-card:visited {
  --odc-ink:     #4d4d4d;   /* title + body */
  --odc-quiet:   #7e8081;   /* status ribbon label */
  --odc-note:    #666666;   /* notes line */
  --odc-surface: #fafafa;   /* card body */
  --odc-ribbon:  #dbdbdb;   /* status ribbon fill */
  --odc-line:    #e6e6e6;   /* border, and the disabled footer */
  --odc-media:   #b3b3b3;   /* media placeholder ONLY — never a real surface */
  --odc-pad:     25.4px;    /* text inset, measured off every text origin */

  position: relative;
  display: flex;
  flex-direction: column;
  isolation: isolate;
  /* LONGHANDS, NOT SHORTHANDS. Measured 2026-08-23: `border: 1px solid
     var(--odc-line)` and `background: var(--odc-surface)` BOTH computed to
     nothing — border-width 0, background transparent — while `border-radius`
     (no var) and `color: var(--odc-ink)` (a longhand with a var) applied fine.
     The CSSOM showed the border longhands parsed EMPTY. A shorthand carrying a
     var() is only resolved at computed-value time and is dropped whole if that
     resolution fails; nothing throws and nothing logs, the card just renders
     borderless on a transparent ground. Same family as the background-image
     shorthand trap in CLAUDE.md. Never put var() in a shorthand here. */
  border-width: 1px;
  border-style: solid;
  border-color: var(--odc-line);
  /* 20px, was 17.068 — a measured-off-a-render number that could never be
     typed on purpose. Craig, 2026-08-24: "All of these need to be increased
     slightly", and 20 is the radius every modal now carries, so a card and the
     modal that opens over it agree. */
  border-radius: 20px;
  background-color: var(--odc-surface);
  overflow: hidden;                /* clips the bands to the radius */
  text-decoration: none;
  color: var(--odc-ink);
  font-family: var(--ops-font);
}
/* NO LIFT. EVER. Craig, 2026-08-23: "Make sure these cards do not 'lift' on
   hover, that animation makes me motion sick and i want to avoid it."
   That is an accessibility requirement, not a preference — treat it as binding on
   every card, tile and panel on this estate, and do not reintroduce a translate
   on hover anywhere. The card itself never moves.

   The affordance is the IMAGE growing inside the media band instead, which Craig
   asked for by name and which is strictly better here: the thing you are pointing
   at responds, and nothing under the cursor changes position, so the pointer
   never falls off its own target mid-gesture. */
/* HOVER BORDER — the second half of the affordance. Craig, 2026-08-23: "run a
   color1xl color border on hover ... or better yet use the attention style
   var(--attention) ... just to give the user that visual feedback." --attention is
   the estate's one "this responds to you" color, so it says the same thing here as
   it does on a link hover and a primary button.

   COLOR ONLY, never width. The border is 1px in the measured card spec; growing it
   on hover would resize the box and shove every sibling in the grid - the same
   class of bug as a lift. The longhand rule above explains why this is
   border-color and not `border`.

   .2s is off the documented motion scale (Web CSS 17); the image uses .45s because
   it travels further. Feedback should arrive before the zoom finishes, not with it. */
.odc-card { transition: border-color .2s ease; }
a.odc-card:hover,
a.odc-card:focus-visible { border-color: var(--gray2, #cccccc); }
/* ONE STEP DOWN THE RAMP, not a color. Craig, 2026-08-23: "let's just go with a
   slightly darker gray than the border already is it needs to be subtle like
   that." Measured ramp on this site: --gray1 #E5E5E5 (which IS the border, via
   --odc-line #e6e6e6) - gray2 #CCCCCC - gray3 #B2B2B2 - gray4 #999999. gray2 is
   the next stop; gray4 was tried first and read as a hard outline, not feedback.
   --attention (#00c6ff) was the first attempt and was too loud for a whole grid. */

/* THE BANDS MUST CURVE WITH THE CARD, OR THEY PAINT OVER ITS STROKE. Craig,
   2026-08-23: "the image is covering the corner, radius, stroke on hover."

   `overflow:hidden` clips descendants to the PADDING box, whose radius is the
   border radius MINUS the border width - 17.068 - 1 = 16.068. A band with square
   corners fills that clip right up to the curve, so at the four corners its
   color sits directly against the inside of the 1px stroke with nothing between
   them. At rest, #e6e6e6 on a pale band, nobody sees it. The moment the stroke
   changes color on hover the overlap becomes the thing you are looking at.

   Curving the first and last children by the INNER radius leaves the stroke a
   clean edge to draw against. Positional selectors, not .odc-head / .odc-media,
   because which band is first differs per card - a card with no header starts on
   the media, and this has to stay right for cards nobody has written yet. */
.odc-card > :first-child {
  border-top-left-radius: 16.068px;
  border-top-right-radius: 16.068px;
}
.odc-card > :last-child {
  border-bottom-left-radius: 16.068px;
  border-bottom-right-radius: 16.068px;
}
/* The zoom layer is inset:0 on the band, so it needs the same curve or the IMAGE
   squares off the corner the band just rounded. */
.odc-media::before { border-radius: inherit; }

a.odc-card:focus-visible { outline: 2px solid var(--color1); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .odc-media::before { transition: none; }
  .odc-head-icon { transition: none; }
  /* The border still CHANGES - it is the feedback - it just stops easing. */
  .odc-card { transition: none; }
}

/* ── header band ── 37px in card3/card6; card2 54.784; card1 120.936 ── */
.odc-head {
  display: flex; align-items: center;
  min-height: 37px;
  padding: 0 var(--odc-pad);
  background-color: var(--color1);
  color: #fff;
  font-size: 13px; font-weight: 400; line-height: 1.2;
  flex: 0 0 auto;
}
.odc-head.is-section { background-color: var(--sg-nav-bg-1, var(--color1)); }

/* ── head icon ──
   White mark in the header band. The band is display:flex, so the icon centers on
   the cross axis for free; margin:auto on the inline axis centers it on a tall
   band and is harmless on a short one. Written for the FA kit, which replaces the
   <i> with an <svg class="svg-inline--fa"> after load - style the WRAPPER, not the
   <i>, or the rule stops matching the moment the kit runs. */
.odc-head-icon {
  display: flex; align-items: center; justify-content: center;
  margin: auto;
  color: #fff;
  /* SIZED PER BAND, not one fixed value. The head has three measured heights
     (37 / 54.784 / 120.936) and a single font-size that fills the tall one FORCES
     the other two taller - measured 2026-08-23, 78px pushed .is-medium from 55 to
     78 and the card stopped matching the comp. Height drives; width follows the
     glyph's own aspect (fa-stationery is ~1.75:1), so nothing is squashed. */
  font-size: 20px;
  line-height: 1;
}
.odc-head-icon .svg-inline--fa, .odc-head-icon svg { height: 1em; width: auto; display: block; }

/* WHEN THE ICON IS THE CARD'S PICTURE, IT ZOOMS LIKE ONE. Craig, 2026-08-23:
   "When icon is the primary image, it needs to grow on hover just like the
   images." Same scale, same curve, same duration as .odc-media::before - a second
   easing would read as two different components. The CARD still never moves (no
   hover lift); only the mark grows inside its own band. Promoted before the
   hover, never on it. At 1.07 the tall band goes 78 -> 83.5 in 120.936 and the
   medium 34 -> 36.4 in 54.784, so neither can spill into the ribbon. */
.odc-head-icon {
  transform: scale(1);
  transition: transform .45s cubic-bezier(.22, .61, .36, 1);
  will-change: transform;
}
/* ONLY WHEN THE ICON IS THE HERO. Craig, 2026-08-23: "don't hoverpop the icon
   when it's small and there is already a hero image in the card. i specifically
   told you only when it's the hero." A card that has an .odc-media already has a
   picture that zooms; the head icon there is a small label and popping it gives
   the card two competing motions. :has() reads the card's own contents, so the
   rule is the SEMANTIC test - "is anything else the picture?" - not a size
   guess, and it stays correct for cards nobody has written yet. */
a.odc-card:not(:has(.odc-media)):hover .odc-head-icon,
a.odc-card:not(:has(.odc-media)):focus-visible .odc-head-icon { transform: scale(1.07); }
.odc-head.is-medium .odc-head-icon { font-size: 34px; }
.odc-head.is-tall   .odc-head-icon { font-size: 78px; }
.odc-head.is-tall    { min-height: 120.936px; }
.odc-head.is-medium  { min-height: 54.784px; }

/* ── media band ── 386.359 x 200.636 in the comp = 1.9257:1 ── */
/* THE BAND IS THE FRAME; THE IMAGE IS A PSEUDO-ELEMENT INSIDE IT.
   The image has to grow "within its bounds", so something has to clip it and
   something else has to scale — one element cannot do both. .odc-media keeps its
   size and its overflow, and ::before carries the picture and takes the scale.

   background-image:inherit is what keeps the authoring API a single span with an
   inline background-image: the pseudo-element picks the URL up from its parent,
   so nothing that consumes this card had to change. The pseudo covers the parent
   completely, so the parent painting the same image underneath is invisible. */
.odc-media {
  display: block;
  position: relative;
  overflow: hidden;
  aspect-ratio: 386.359 / 200.636;
  background-color: var(--odc-media);
  flex: 0 0 auto;
}
/* THE IMAGE ARRIVES AS A CUSTOM PROPERTY, NOT AS background-image.
   The first cut put the URL on the band and had ::before pick it up with
   background-image:inherit — so BOTH painted it. Identical and invisible at
   rest; the moment ::before scaled, a transparent-background logo showed the
   band's unscaled copy through its own gaps and the mark appeared TWICE at two
   sizes. Craig, 2026-08-23: "core30 is duplicating as it grows on hover."
   Opaque photos hid it completely, which is exactly why it survived review.

   A custom property inherits to the pseudo-element without the parent ever
   painting anything, so there is only ever one copy. The authoring API is still
   one span and one inline declaration:  style="--odc-img:url('…')"  */
.odc-media::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--odc-img, none);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  transform: scale(1);
  transition: transform .45s cubic-bezier(.22, .61, .36, 1);
  will-change: transform;          /* promoted BEFORE the hover, never on it */
}
a.odc-card:hover .odc-media::before,
a.odc-card:focus-visible .odc-media::before { transform: scale(1.07); }

/* THE HERO ALWAYS ANIMATES. Craig, 2026-08-23: "the hero has to grow on hover ...
   in all cases the hero is important ... even if it's an SVG logo, even if it's a
   background image, even if it's a primary icon."

   This block used to switch the zoom layer OFF for a logo and paint the mark on
   the band itself. That is why core30 sat still while every photo moved. The mark
   now paints on the SAME ::before every other hero uses, so it inherits the one
   transform, the one curve and the one duration - and the band keeps only the
   white ground behind it, which is what stops the double-image the pseudo was
   introduced to fix (see the note above).

   The 10% inset moves onto the pseudo, so the mark grows INTO its own padding
   rather than past the box: at 1.07 a 386.359-wide band gives back 38.6px of
   inset and spends 13.5px of it. The ends stay on. */
.odc-media.is-logo::before {
  background-size: contain;
  background-origin: content-box;
  padding: 10%;
}
.odc-media.is-logo {
  background-image: none;
  background-color: #fff;
}

/* The bands are non-positioned siblings of a transformed element, which would
   otherwise paint UNDER it. One z-index each keeps the picture inside its band. */
.odc-head, .odc-ribbon, .odc-body, .odc-foot { position: relative; z-index: 1; }

/* ── status ribbon ── */
.odc-ribbon {
  display: flex; align-items: center;
  /* MIN-height, not height. The comp measures 45.697 with the label on one line
     at the full 387px card. In a grid the card is narrower, the label wraps, and
     a fixed height would have it spill out of the band it is supposed to sit in.
     Padding keeps a one-line ribbon at exactly the comp height. */
  min-height: 45.697px;
  padding: 8px var(--odc-pad);
  background-color: var(--odc-ribbon);
  color: var(--odc-quiet);
  font-size: 12px; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; line-height: 1;
  flex: 0 0 auto;
}

/* ── body ── */
/* A FLEX COLUMN, NOT A BLOCK, SO THE PILL RAIL CAN PIN TO THE FLOOR.
   Craig, 2026-09-04: "The user pills or initials in blue here or internally
   should all be bottom aligned with the card just above the URL footer with
   10px padding. We will have other things appear here in the center below the
   sentence that we will need space for."

   The rail is pinned with margin-top:auto rather than position:absolute so the
   space Craig is reserving is REAL space — anything added between the sentence
   and the pills pushes the body taller and the rail stays on the floor, where
   an absolutely-positioned rail would sit on top of it.

   Blockifying is unchanged: .odc-title / .odc-desc / .odc-notes already carry
   display:block, and a flex item is blockified anyway. */
.odc-body { display: flex; flex-direction: column; padding: 29.66px var(--odc-pad) 22px; flex: 1 1 auto; }
/* BLOCKIFY THE TEXT PARTS. Every part of this card is a <span> so the whole
   card can be one anchor without nesting interactive elements — and .odc-card is
   display:flex, so its DIRECT children get blockified for free. The ones inside
   .odc-body do not: title and body ran together on one line until these were set
   explicitly. */
.odc-title {
  display: block;
  margin: 0;
  /* 8px off the comp's 27.09 at full width, and a clamp so it keeps shrinking as
     the Operations window narrows instead of holding one fixed size. Craig,
     2026-08-28: "These headers need to drop in pixels ... by 8px on full width,
     I want to make sure you're using a clamp with a minimum size so that it gets
     smaller as the operations window goes responsive."
     A BARE vw MIDDLE TERM IS WRONG HERE and was measured wrong: 1.06vw only
     reaches 19.09 at an 1800px viewport, so a 1440 laptop got the 16px FLOOR - an
     11px drop, not an 8px one. The grid is repeat(auto-fill, minmax(300px,1fr)),
     so a card is ~300-400px wide at every viewport; the title is not tracking the
     window, it is tracking how narrow the page has been asked to get. An INTERCEPT
     plus a shallow slope says that: 14.91px + 0.29vw is 19.09 at 1440 and 16.00 at
     375, and the clamp holds both ends flat outside that span. */
  font-size: clamp(16px, calc(14.91px + 0.29vw), 19.09px);
  font-weight: 700; letter-spacing: -.04em; line-height: 1.1;
  color: var(--odc-ink);
}
.odc-eyebrow {
  display: block; margin: 0 0 6px;
  font-size: 12px; font-weight: 800; letter-spacing: .16em;
  text-transform: uppercase; color: var(--odc-quiet); line-height: 1;
}
.odc-desc {
  display: block;
  margin: 14.85px 0 0;
  font-size: 14px; font-weight: 500; line-height: 1.2;   /* comp: 14 / 16.8 */
  color: var(--odc-ink);
}
.odc-notes { display: block; margin: 23.4px 0 0; font-size: 14px; font-weight: 400; color: var(--odc-note); }
.odc-link  {
  display: inline-block; margin: 29px 0 0;
  font-size: 13px; font-weight: 400; color: var(--color1); text-decoration: none;
}
.odc-link:hover { text-decoration: underline; }

/* ── pills ── the comp draws them 98.573 / 68.333 wide, which is just the label
   at this padding; width is left to the content so a longer word does not clip. */
/* PILLS TAKE THE .od-chip GEOMETRY, NOT THE COMP'S.
   Craig, 2026-08-23: "the pills should look like the pills of the old cards and
   make sure they're spacing around the pills vertically." The comp draws them
   29.173 tall at radius 3.463; the chip already on every /ops card is 11px caps
   in 4px/8px padding at radius 4. Two chip sizes on one estate is the drift this
   guide exists to stop, and the smaller one is the one already in service — so
   the new card follows it and the comp loses this one on purpose.

   Vertical space is symmetric (18 above and below) because the pills sit in three
   different places across the six states: last thing in the body, above an
   eyebrow, and mid-stack. A top-only margin read fine in one and collided in the
   other two. With .odc-body's 22px bottom padding, pills-last lands at 40 — the
   comp measures 36 from pill bottom to footer.

   border-color is currentColor, so ONE declaration paints text and rim together
   and they cannot disagree — same grammar as .od-chip and .sync-chip. */
.odc-pills { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0; }
/* ── the rail sits on the floor of the body ──────────────────────────────────
   margin-top:auto eats every pixel of slack above the pills, so they land
   against the footer whatever the description's length. The 18px above is now
   a MINIMUM rather than the actual gap.

   THE 22px FLOOR COMES OFF ONLY FOR A BODY THAT ENDS IN THE RAIL. A card whose
   last element is the sentence keeps its 22px — Craig asked for 10px under the
   pills, not a shorter card. :has() is the right tool and this sheet already
   uses it (a.odc-card:not(:has(.odc-media)) above).

   The audience rail (.ocs-aud, written by sst-ops-card-settings.html) carries
   the same two rules in that file, because it is appended AFTER .odc-pills and
   is then the one that is :last-child. Edit the two together. */
/* auto EATS the 18px when there is no slack, and the pills then touch the
   sentence — measured on a card whose row gives it nothing to absorb. The
   minimum gap comes back as PADDING, which auto cannot spend. */
.odc-pills { margin-top: auto; padding-top: 18px; }
.odc-body > .odc-pills:last-child { margin-bottom: 0; }
.odc-body:has(> .odc-pills:last-child) { padding-bottom: 10px; }
/* AND THE FOOTER'S OWN 29px, WHICH IS THE OTHER HALF OF THE GAP. Measured
   2026-09-04: rail bottom to footer top read 39 with the floor already at 10,
   because .odc-foot carries margin-top:29px — the breathing room above the band
   for a card that ends in TEXT. A card that ends in the rail does not want it.
   The footer does not move: .odc-body is flex:1 1 auto, so the 29px is absorbed
   by the body and spent pushing the pinned rail further down. */
.odc-card:has(.odc-body > .odc-pills:last-child) > .odc-foot { margin-top: 0; }
.odc-pill {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 8px;
  border-width: 1px; border-style: solid; border-color: currentColor;
  border-radius: 4px;
  background-color: transparent; color: var(--st-success);
  font-size: 11px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; line-height: 1; white-space: nowrap;
}
/* One color each — the rim follows currentColor. */
.odc-pill.is-request  { color: var(--st-request); }
.odc-pill.is-complete { color: var(--st-complete); }
.odc-pill.is-edit     { color: var(--st-edit-ink); }

/* ── footer band ── */
/* ── footer org icon ──
   The organization's mark sits in front of the URL. WHICH mark is chosen in the
   markup from the band's own background (see custom-ops-dashboard-body.html), so
   the icon is always drawn for the ground it lands on rather than being one file
   that has to survive four different footers. */
.odc-foot-icon {
  flex: 0 0 auto;
  width: 18px; height: 18px;
  /* THE FULL MARGIN, not just the right one. sst-styles.css ships
     `img, a img, img a { margin: 0 auto }` - a site-wide centering rule - and this
     icon is an `a img`. Setting only margin-right left the `auto` in place and it
     pushed the icon 65.4px in from the padding edge; it reads as a broken flex
     alignment and it is one unset longhand. Measured 2026-08-23. */
  margin: 0 8px 0 0;
  object-fit: contain;
  display: block;
}
.odc-foot-url { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.odc-foot {
  display: flex; align-items: center;
  min-height: 40.994px;
  margin-top: 29px;
  padding: 0 var(--odc-pad);
  background-color: var(--color1);
  color: #fff;
  font-size: 13px; line-height: 1.2;
  flex: 0 0 auto;
}

/* ── complete badge ── 60.924 circle, dead center, straddling the bottom edge ──
   It sits OUTSIDE the card's overflow:hidden, so it lives on a wrapper. */
.odc-wrap { position: relative; display: flex; padding-bottom: 30.462px; }
.odc-wrap > .odc-card { flex: 1 1 auto; }
.odc-badge {
  position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  width: 60.924px; height: 60.924px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background-color: var(--st-success); color: #fff; font-size: 26px;
  border-width: 3px; border-style: solid; border-color: var(--odc-surface);
  z-index: 2;
}

/* ── disabled ── card5: the footer band goes to the line grey and nothing lifts */
.odc-card.is-disabled { pointer-events: none; }
.odc-card.is-disabled .odc-foot { background-color: var(--odc-line); color: var(--odc-quiet); }
.odc-card.is-disabled .odc-head { background-color: var(--odc-line); color: var(--odc-quiet); }

/* ── PUBLIC SITES TAKE THE SITE'S OWN HEADING FACE ──
   Craig: "the website will use site's primary font for header." Off /ops there is
   no .opsdash, so the card falls back to the site's own family; the title then
   takes --font1Family explicitly. Every measurement above still holds — only the
   face changes, which is the point of keeping geometry and color separable. */
body:not(:has(.opsdash)) .odc-title { font-family: var(--font1Family, inherit); }

/* ── .odc-card MODIFIERS — the tuning surface ──────────────────────────────
   Craig, 2026-08-23: "Make sure we have classes that we can edit to fine-tune
   these cards across the board."

   Everything an /ops page needs to vary about a card is a class here, so tuning
   the estate is editing THIS block once — never a style attribute on a page, and
   never a second card. If you find yourself reaching for an inline style on an
   .odc-card, the modifier is missing: add it here instead.

   HEAD     .is-section  brighter ops blue · .is-medium 54.784 · .is-tall 120.936
   TIER     .tier-1 … .tier-4   tints the footer band
   STATE    .is-todo  not built · .is-disabled  greyed, no pointer
   DENSITY  .is-compact  tighter body + smaller title, for dense grids
   MEDIA    .is-logo  contains a standalone mark instead of cropping a photo
   GRID     .odc-grid / .odc-grid-2 / .odc-grid-3 */

.odc-grid {
  display: grid; gap: 1rem;   /* = gap-3. Add gap-4 on the element to widen. */
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  align-items: stretch;
}
.odc-grid-2 { grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); }
.odc-grid-3 { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
@media (max-width: 640px) { .odc-grid, .odc-grid-2, .odc-grid-3 { grid-template-columns: 1fr; } }

/* TIER — the old .od-card painted a 4px rule on the body's bottom edge; here the
   footer band already occupies that edge, so tier tints the band itself. Same
   four meanings, one less element. */
.odc-card.tier-1 .odc-foot { background-color: var(--color1); }
.odc-card.tier-2 .odc-foot { background-color: var(--color2, var(--color1)); }
.odc-card.tier-3 .odc-foot { background-color: color-mix(in srgb, var(--color1) 78%, #000); }
.odc-card.tier-4 .odc-foot { background-color: var(--hush); }

/* NOT BUILT — honest, and it does not pretend to be a link. Pair with a <span>,
   never an <a>: there is nowhere to go, and the hover has to agree. */
.odc-card.is-todo { opacity: .72; }
.odc-card.is-todo .odc-media { background-image: none; background-color: var(--odc-line); }
.odc-card.is-todo .odc-foot  { background-color: var(--odc-line); color: var(--odc-quiet); }

/* COMPACT — for a dense grid. Only spacing and the title move; every color,
   border and radius stays, so a compact card is still the same card. */
.odc-card.is-compact .odc-body  { padding-top: 20px; padding-bottom: 16px; }
.odc-card.is-compact .odc-title { font-size: 21px; }
.odc-card.is-compact .odc-desc  { margin-top: 10px; }
.odc-card.is-compact .odc-pills { margin: 14px 0; }
.odc-card.is-compact .odc-ribbon { min-height: 34px; }
.odc-card.is-compact .odc-head   { min-height: 30px; }

/* LOGO MEDIA — a standalone mark is CONTAINED and padded on its own ground, not
   cropped like a photograph. Cropping a wordmark is how a logo loses its ends. */
/* Second declaration kept for the white ground only; the inset now lives on the
   ::before above so it scales with the mark instead of clipping it. */
.odc-media.is-logo { background-color: #fff; }
/* No art yet -> the gradient, never a borrowed image. A borrowed image is a claim
   about what the tool is. Keyed on the custom property the card actually uses. */
.odc-media:not([style*="--odc-img"]) {
  background-image: linear-gradient(160deg,
    color-mix(in srgb, var(--color1) 10%, #fff),
    color-mix(in srgb, var(--color1) 3%, #fff));
}

/* ===================== SEARCH RESULTS — the "elsewhere" list ===============
   Added 2026-08-23. Two fixes in one block.

   1. SPACING. Craig: "this is jammed with no spacing from the page nav ribbon."
      The results panel had no top margin, so it butted straight against the
      sticky nav. .od-search-bar gets the same breathing room for the same
      reason — it is the other thing that sits directly under the ribbon.

   2. THIS IS A LIST, NOT A CARD, ON PURPOSE. The panel used to render its own
      grid of `.od-tile`, which ONLY v1 styles — on a v2 site the same shared
      snippet produced unstyled inline spans, which is exactly what a long line
      of run-together text is. The search now filters the page's real cards in
      place; this list is only for /ops pages that are NOT on the current page,
      and it is styled as a list so it cannot be mistaken for one.
   ======================================================================== */
/* 2026-08-28: the 18px is now 24px of padding-top on the bar itself, set in the
   block above where the rest of its box lives, and it is cc's number rather than
   this panel's. Restating margin-top here would re-open the gap this replaced. */
.opsdash .od-results    { margin-top: 22px; }

/* THE RESULTS ARE CARDS NOW, not a table. Craig, 2026-08-23: "it's still trying
   to display a table list of items in a table format instead of just showing the
   cards with the search words." The panel reuses .odc-grid + .odc-card verbatim,
   so a hit looks like the thing it links to and inherits the border hover, the
   hero zoom and the footer icon for free. The .od-hitlist class stays on the
   element only as a hook for the existing JS. */
.opsdash .od-hitlist { display: grid; gap: 1rem;

/* DEAD as of 2026-08-23 — the panel emits .odc-card. Kept because
   /ops on the other seven sites still serves the old markup until their cache
   keys are bumped; delete when they are. */
.opsdash .od-hit,
.opsdash a.od-hit:link,
.opsdash a.od-hit:visited {
  display: grid; grid-template-columns: 190px 1fr auto; align-items: baseline;
  gap: 14px; padding: 11px 16px; background: #fff;
  color: var(--ink-1, #333); text-decoration: none; font-size: 14px; }

.opsdash a.od-hit:hover,
.opsdash a.od-hit:focus-visible { background: #f5f9fc; }
.opsdash a.od-hit:focus-visible { outline: 2px solid var(--color1); outline-offset: -2px; }

.opsdash .od-hit-sec  { color: var(--hush, #7e8081); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; }
.opsdash .od-hit-name { font-weight: 600; }
.opsdash .od-hit-url  { color: var(--hush, #7e8081); font-size: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.opsdash .od-hit.is-todo { color: var(--hush, #7e8081); }

/* `hidden` alone loses to any author `display:` rule — and every .od-hit above
   is `display:grid`. Without this the filter sets the attribute and nothing
   moves, which reads as a dead search box. */
.opsdash .od-hit[hidden],
.opsdash .od-results[hidden] { display: none !important; }

@media (max-width: 700px) {
  .opsdash .od-hit { grid-template-columns: 1fr; gap: 3px; }
  .opsdash .od-hit-url { word-break: break-all; }
}
