/* lk-teacher-library.css — shared skin for the teacher-page on-demand library engine
 * (lk-teacher-library.js). Structure + behavior are shared; each page THEMES via the
 * --lklib-* CSS variables below. Defaults = the caley-alyssa dark palette so it renders
 * out of the box; a page overrides the vars to its own look.
 *
 * Vars are on :root (namespaced --lklib-) because the drawer + lightbox are appended to
 * <body> (outside the library section) and must inherit the theme.
 */
:root {
  --lklib-accent: #C9A86A;             /* primary accent (rows, CTAs, active pills) */
  --lklib-accent-deep: #8A7140;
  --lklib-ink: #FAF4E8;                /* SURFACE text (row/tile titles, meta) — a LIGHT-themed page sets this dark */
  --lklib-ink-dim: rgba(250,244,232,0.62);
  --lklib-ink-faint: rgba(250,244,232,0.55);
  --lklib-glass-ink: #FAF4E8;          /* icons/labels that sit on the dark glass overlays — stays light on ANY page theme */
  --lklib-scrim: rgba(8,6,16,0.92);    /* LIGHTBOX ONLY -- a translucent veil over playing video */
  --lklib-on-accent: #1E1A17;          /* ink on a filled teacher accent. The system ink (Mackey, 2026-11-02),
                                        * the default for pages that declare none. Measured on the four that
                                        * don't: 8.30 / 5.73 / 4.58 / 6.79 (marc-holzman was 4.15 on the scrim
                                        * fallback). Per-page declarations win. GI #1785. */
  /* The see-all drawer is a full-screen CATALOGUE, not a veil. It is the teacher's own library
     content, so it takes the teacher's own OPAQUE surface. Splitting this off fixes a defect
     class: the drawer used to be a module-owned dark ground painted with page-ground tokens, so
     a teacher whose accent suited her light page vanished on it (marybethlarue 1.15:1,
     liz-arch 1.71:1 -- both live). It also makes the ground PROVABLE: every page declared a
     TRANSLUCENT scrim (0.92, one 0.45, one 0.118), so the drawer composited against whatever
     was scrolled behind it and had no fixed contrast ratio at all. */
  /* 🔴 DO NOT declare --lklib-drawer-bg here as `var(--lklib-tile-bg-solid)`. A var() inside a
     custom-property DECLARATION resolves in the scope it is declared in -- this :root -- so it
     would freeze to the module's own default and inherit that frozen value downward, ignoring the
     teacher's override entirely. Measured: it resolved to #1a1632 on the drawer even though
     --lklib-tile-bg-solid was #f6f3ee on the same element. The fallback belongs at the USE SITE,
     where it resolves against the element's own cascade. Left undeclared on purpose. */
  --lklib-dj-bg: rgba(216,123,44,0.85);
  --lklib-skel: rgba(255,255,255,0.06);
  --lklib-skel-shine: rgba(255,255,255,0.14);
  --lklib-tile-bg: rgba(20,16,42,0.55);
  --lklib-tile-bg-solid: #1a1632;
  --lklib-tile-border: rgba(250,244,232,0.08);
  --lklib-tile-hover-border: rgba(216,123,44,0.5);
  --lklib-radius: 8px;
  /* ── THE CARD, 1.20.0 ─────────────────────────────────────────────────────────────────────
   * Mackey, 2026-10-13: "One card. One size. Everywhere." And his condition for shipping it:
   * "The root cause of this entire brief is that the card width was never a token, so it became
   * an opinion." Until now 340px was written as a bare number in .lib-tile and again in
   * .lib-skel-tile, each with its own 280px override, and the library grid picked an unrelated
   * 252px while the drawer picked 320px -- three card systems nobody reconciled.
   *
   * 🔴 340 IS THE DERIVED NUMBER, not a preference. 340 − 2px border = 338 content × 9/16 =
   * 190.1px, which is exactly the --lklib-thumb-h fallback the arrows centre on. 252 derives
   * nothing (it yields 140.6). Move --lklib-card-w and the thumb height must move with it.
   *
   * The band exists because a GRID cell cannot be a fixed width without leaving ragged gaps, and
   * a ROW tile must not stretch. So: the track stretches, the card does not. ±6% from 340 is
   * below the threshold at which two cards read as different sizes, and it is the only band whose
   * arithmetic lands at every common width -- measured 336-360 across 768→1920. */
  --lklib-card-w: 340px;     /* the row tile. Fixed, by Mackey's refusal of a flexing tile. */
  --lklib-card-min: 320px;   /* grid floor: the narrowest a card may be before a column is dropped */
  --lklib-card-max: 360px;   /* grid cap: the widest, so a stretched cell never stretches the CARD */
  --lklib-grid-gap: 16px;    /* the ONE gap. Both grids and the closed-form cap below all read it. */
  --lklib-card-peek: 24px;   /* the phone row's peek. DERIVED: --lklib-grid-gap + --lklib-radius =
                              * 16 + 8. The gap shows nothing; the remaining 8 is one corner radius of
                              * the next card, the least that reads as a rounded card edge rather than
                              * a line. Written as a literal because a :root var() of other tokens
                              * freezes at :root and ignores per-page overrides; test_library_row_peek
                              * asserts the sum. Mackey, 2026-11-02, GI #1789. */
  /* 🔴 A DERIVED CONSTANT, NOT A PREFERENCE, AND CSS CANNOT COMPUTE IT.
   *   --lklib-card-pitch-ratio = (card-max + grid-gap) / (card-min + grid-gap) = 376 / 336
   * calc() can multiply a length by a number but cannot divide one length by another, so this
   * ratio is the single figure in the system that has to be written out. It is NOT free to edit:
   * test_library_card_band.js recomputes it from the other three tokens and fails on a mismatch,
   * so moving --lklib-card-min or -max without moving this is caught in CI, not on a teacher's
   * page. It exists so the grid cap below needs no breakpoints at all -- see that comment. */
  --lklib-card-pitch-ratio: 1.119047619;
  /* ── FILTER CONTROLS (1.21.0) ───────────────────────────────────────────────────────────────
   * All four are lengths or numbers; none is a colour. The fade is DERIVED, not picked: it is
   * exactly the chip's own horizontal padding, so a chip sitting on the boundary has its border
   * and padding ghosted while its letters stay legible. Mackey rendered 16 and 32 side by side on
   * the live page: at 32 the trailing word reads as DISABLED rather than CONTINUING. */
  --lklib-chip-pad-x: 16px;                    /* the chip's own horizontal padding, AND the edge-mask
                                                * width -- ONE token used for both, because they are
                                                * the same measurement: the fade must end exactly where
                                                * the next chip's first glyph begins.
                                                * 🔴 There is deliberately no second token aliasing
                                                * this one. A :root custom property whose value is a
                                                * var() of another --lklib token resolves IN :root, so
                                                * it freezes to the module default and inherits that
                                                * frozen value past every per-teacher override -- and
                                                * teacher pages do override these individually.
                                                * test_library_fullset.js asserts it and caught the
                                                * first draft of this block. The derivation belongs at
                                                * the USE SITE, where it survives an override, which is
                                                * where the nine mask declarations put it. */
  --lklib-note-lh: 1.5;                        /* the description slot's line-height, as a number */
  --lklib-note-more-gap: 2px;                  /* .lib-note-more's margin-top, which it always keeps */
  /* 🔴 THERE IS NO --lklib-card-w-sm. Mackey deleted it: "the phone card is not an independent
   * number, it is a derived one. A fourth token there re-creates exactly the condition that caused
   * this brief — a width that is an opinion rather than a rule." That deletion STANDS.
   *
   * 1.26.0 (GI #1789, Mackey 2026-11-02). Below the breakpoint the row tile and its skeleton are
   * min(--lklib-card-max, 100% - --lklib-card-peek), where 100% is the TRACK's own content box.
   * The 1.20.0 form sized the card off the VIEWPORT and assumed the row's 16px was the only gutter;
   * on the six pages that frame the library with their own padding the card overran its track by
   * up to 80px and hid its own audio and favourite controls, and on the other ten it filled the
   * track exactly, so nothing on screen said the row continues. The arrows that 1.20.0 counted on
   * to say it never render below 700px (see .lib-arrow), and that gate is correct: on touch the
   * swipe is the interaction. So the peek is now a design, declared as --lklib-card-peek.
   * 🔴 The cards measure only their own track. The page's frame is the page's business.
   * test_library_row_peek asserts it. */
  --lklib-serif: Georgia, 'Times New Roman', serif;
  --lklib-serif-display: Georgia, 'Times New Roman', serif;
  --lklib-sans: -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --lklib-sans-display: -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

/* ===== rows ===== */
.lib-row { margin-bottom: 48px; padding: 0 24px; }
/* 1.19.10 -- P0 (Yarden, 2026-10-13): the row header overflowed the page on almost every teacher,
 * pushing "N SESSIONS" and "See all" past the right edge into an unreachable position.
 * Cause: .lib-row-sub carried `white-space: nowrap` while the backend fills that slot with the
 * FULL series description (rows[].subtitle, since the rows-are-series change). A nowrap flex item
 * of arbitrary length cannot shrink, so it blows the 1500px container and pushes .lib-row-meta out.
 * The @media (max-width:600px) block has ALWAYS set `white-space: normal` here, so this was
 * DESKTOP-ONLY -- which is why checking a narrow viewport never caught it.
 * Three parts, each load-bearing: the sub may wrap; the h3 may shrink below its content width
 * (min-width:0 -- a flex item's default `min-width:auto` is what actually blocks the shrink);
 * and .lib-row-meta is never the thing that gives way. */
.lib-row-head { display: flex; align-items: baseline; justify-content: space-between; max-width: 1500px; margin: 0 auto 14px; gap: 16px; }
.lib-row-title { font-family: var(--lklib-serif-display); font-weight: 400; font-size: 26px; line-height: 1.15; color: var(--lklib-ink); letter-spacing: -0.01em; margin: 0; min-width: 0; }
.lib-row-sub { font-family: var(--lklib-sans); font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--lklib-ink-dim); margin-left: 12px; white-space: normal; }
.lib-row-count { font-family: var(--lklib-sans); font-size: 11px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--lklib-accent); white-space: nowrap; }
.lib-track-wrap { position: relative; max-width: 1500px; margin: 0 auto; }
.lib-track { display: flex; gap: 16px; overflow-x: auto; overflow-y: hidden; scroll-snap-type: x mandatory; scroll-behavior: smooth; scrollbar-width: none; -ms-overflow-style: none; padding: 8px 0 16px; scroll-padding: 0 24px; }
.lib-track::-webkit-scrollbar { display: none; }
.lib-arrow { position: absolute; top: calc(var(--lklib-thumb-h, 190px) / 2); transform: translateY(-50%); z-index: 4; width: 44px; height: 88px; border: none; background: var(--lklib-arrow-bg, rgba(10,8,20,0.7)); color: var(--lklib-arrow-ink, var(--lklib-ink)); cursor: pointer; display: none; align-items: center; justify-content: center; border-radius: 4px; transition: background .2s; backdrop-filter: blur(6px); font: 400 26px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; overflow: hidden; padding: 0; box-sizing: border-box; }
.lib-arrow:hover { background: var(--lklib-accent); color: var(--lklib-on-accent); }
.lib-arrow.is-prev { left: -8px; }
.lib-arrow.is-next { right: -8px; }
@media (min-width: 700px) { .lib-arrow { display: flex; } }
/* 1.19.11 -- Mackey, 2026-10-13. `[hidden]` must actually hide: the rule above sets display:flex
 * inside a media query, and the HTML `hidden` attribute's UA default (display:none) loses to any
 * author `display`. Specificity 0,2,0 beats the 0,1,0 above, so no !important is needed -- but
 * WITHOUT this selector the JS gate silently does nothing, which is the same shape of defect as
 * the arrows it exists to switch off. The JS sets .hidden on every arrow whose track does not
 * overflow (syncArrowState). */
.lib-arrow[hidden] { display: none; }
/* The end-of-track state. An arrow that cannot move the track is still a control -- it reads as
 * unavailable rather than disappearing, so the row's edges stay where the eye left them. */
.lib-arrow:disabled { opacity: 0.32; cursor: default; }
.lib-arrow:disabled:hover { background: var(--lklib-arrow-bg, rgba(10,8,20,0.7)); color: var(--lklib-arrow-ink, var(--lklib-ink)); }

/* ===== tile ===== */
.lib-tile { flex: 0 0 var(--lklib-card-w); scroll-snap-align: start; background: var(--lklib-tile-bg); border: 1px solid var(--lklib-tile-border); border-radius: var(--lklib-radius); overflow: hidden; transition: border-color .2s ease; display: flex; flex-direction: column; color: inherit; position: relative; }
@media (max-width: 600px) { .lib-tile { flex-basis: min(var(--lklib-card-max), calc(100% - var(--lklib-card-peek))); } }
/* #842-regression v1.4.5: ALL :hover rules on the tile must be gated behind a real pointer.
   iOS synthesizes :hover on the first tap; WebKit's ContentChangeObserver treats an
   opacity 0 -> visible change during that simulation as a hover-reveal and WITHHOLDS the
   click entirely, so the speaker AND expand both needed two taps. The gate lives on the
   shared .lib-tile ancestor, which is why every control on the tile was affected while
   scroll-in autoplay (IntersectionObserver, no click) was fine. Touch reveals controls via
   .is-active / .is-invp instead (below). Never add a bare :hover rule to a tile again. */
@media (hover: hover) and (pointer: fine) {
  .lib-tile:hover { border-color: var(--lklib-tile-ring, var(--lklib-accent)); outline: 2px solid var(--lklib-tile-ring, var(--lklib-accent)); outline-offset: -2px; }
  .lib-tile:hover .lib-tile-thumb video.is-playing { transform: scale(1.02); }
  .lib-tile:hover .lib-tile-playbtn { opacity: 0.85; }
  .lib-tile-thumb.is-playing:hover .lib-tile-playbtn { opacity: 0.85; }
  .lib-tile-audio:hover { background: var(--lklib-accent-strong, rgba(216,123,44,0.85)); border-color: var(--lklib-accent); }
  .lib-tile-expand:hover { background: var(--lklib-glass-bg-strong, rgba(0,0,0,0.85)); border-color: var(--lklib-accent); color: var(--lklib-accent); }
  .lib-tile-fav:hover svg { transform: scale(1.08); }
}
.lib-tile-thumb { aspect-ratio: 16/9; position: relative; background: var(--lklib-tile-bg-solid); overflow: hidden; cursor: pointer; }
.lib-tile-thumb video, .lib-tile-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.lib-tile-thumb video { opacity: 1; transition: transform .4s ease; }
.lib-tile-thumb::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.55) 100%); pointer-events: none; z-index: 1; }
.lib-tile-playbtn { position: absolute; inset: 0; z-index: 2; display: flex; align-items: center; justify-content: center; background: transparent; border: none; cursor: pointer; color: var(--lklib-ink); padding: 0; opacity: 0; transition: opacity .25s ease; -webkit-tap-highlight-color: transparent; }
.lib-tile-playbtn .icon-play, .lib-tile-playbtn .icon-pause { background: var(--lklib-glass-bg, rgba(0,0,0,0.55)); border-radius: 50%; padding: 14px; display: flex; backdrop-filter: blur(6px); }
.lib-tile-playbtn .icon-pause { display: none; }
.lib-tile-thumb.is-playing .lib-tile-playbtn { opacity: 0; }
.lib-tile-thumb.is-playing .lib-tile-playbtn .icon-play { display: none; }
.lib-tile-thumb.is-playing .lib-tile-playbtn .icon-pause { display: flex; }
.lib-tile-dj { position: absolute; right: 12px; top: 12px; z-index: 2; background: rgba(216,123,44,0.85); color: var(--lklib-ink); padding: 4px 10px; border-radius: 4px; font-family: var(--lklib-sans); font-size: 10px; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; backdrop-filter: blur(6px); }
.lib-tile-free { position: absolute; left: 12px; top: 12px; z-index: 2; background: var(--lklib-badge-bg, rgba(0,0,0,0.7)); color: var(--lklib-ink); padding: 4px 10px; border-radius: 4px; font-family: var(--lklib-sans); font-size: 10px; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase; backdrop-filter: blur(6px); }
.lib-tile-audio { position: absolute; right: 12px; bottom: 12px; z-index: 3; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--lklib-glass-border, rgba(250,244,232,0.25)); background: var(--lklib-glass-bg, rgba(0,0,0,0.55)); color: var(--lklib-ink); cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(6px); opacity: 0; transition: opacity .2s ease, background .2s, color .2s; padding: 0; }
.lib-tile-audio.is-on { opacity: 1; }
@media (hover: hover) { .lib-tile:hover .lib-tile-audio, .lib-tile:focus-within .lib-tile-audio { opacity: 1; } }
.lib-tile-audio .icon-unmuted { display: none; }
.lib-tile-audio.is-on { background: var(--lklib-accent-strong, rgba(216,123,44,0.85)); border-color: var(--lklib-accent); color: var(--lklib-ink); }
.lib-tile-audio.is-on .icon-muted { display: none; }
.lib-tile-audio.is-on .icon-unmuted { display: block; }
.lib-tile-expand { position: absolute; left: 12px; bottom: 12px; z-index: 3; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--lklib-glass-border, rgba(250,244,232,0.25)); background: var(--lklib-glass-bg, rgba(0,0,0,0.55)); color: var(--lklib-ink); cursor: pointer; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(6px); opacity: 0; transition: opacity .2s ease, background .2s, color .2s; padding: 0; }
@media (hover: hover) { .lib-tile:hover .lib-tile-expand, .lib-tile:focus-within .lib-tile-expand { opacity: 1; } }
/* #842: reveal overlay icons on the single ACTIVE card (touch has no :hover). .is-active is set by the single-active arbiter, independent of play success (fixes autoplay-blocked double-tap). */
.lib-tile-thumb.is-active .lib-tile-audio, .lib-tile-thumb.is-active .lib-tile-expand { opacity: 1; }
.lib-tile-thumb.is-invp .lib-tile-audio, .lib-tile-thumb.is-invp .lib-tile-expand { opacity: 1; }  /* #842-regression: show controls on any in-view card (touch) so the speaker is single-tap */

/* ===== favorite heart (rendered only when favEnabled) ===== */
/* #789 v1.4.4: match the app on-demand card heart (app-favorite-heart, chip + light): 44px tap
   target top-RIGHT, 34px cream disc, app heart path, dark glyph @0.7, taupe fill when saved. */
.lib-tile-fav { position: absolute; right: 5px; top: 5px; z-index: 3; width: 44px; height: 44px; display: grid; place-items: center; border: none; background: none; cursor: pointer; padding: 0; }
.lib-tile-fav::before { content: ''; position: absolute; top: 50%; left: 50%; width: 34px; height: 34px; transform: translate(-50%, -50%); border-radius: 50%; background: rgba(251,248,242,0.85); box-shadow: 0 1px 4px rgba(42,38,32,0.18); z-index: 0; }
.lib-tile-fav[disabled] { cursor: default; }
.lib-tile-fav svg { position: relative; z-index: 1; width: 18px; height: 18px; fill: none; stroke: #2A2620; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; opacity: 0.7; filter: none; transition: transform .15s ease, fill .15s ease, stroke .15s ease, opacity .15s ease; }
.lib-tile-fav.is-on svg { fill: #6B6257; stroke: none; opacity: 1; }
.lib-tile-fav:focus-visible { outline: 2px solid rgba(42,38,32,0.4); outline-offset: 3px; border-radius: 50%; }
/* #789: hearts stay hidden until the favorites relay hydrates the saved-set (kills the empty->filled flash). */
html.lk-fav-hydrating .lib-tile-fav { opacity: 0; pointer-events: none; }

/* ===== preparing (in-progress) tile ===== */
/* GI #1785 (Mackey, 2026-11-02): brightness(0.62) is a multiplier, so its output depends on the poster and
 * cannot hold a floor; it left a blown poster at #9E9E9E and the label at 2.68. The plate below does the
 * dimming. grayscale(0.2) stays: it says "not ready", which a plate does not. */
.lib-tile--preparing .lib-tile-thumb img { filter: grayscale(0.2); }
/* The plate matches the Angular card's --lk-scrim by value, so the migration off S3 is a no-op. Label over
 * it, worst ground (blown white poster): 5.37 #ffffff, 4.67 #F0EFEB, 4.70 #f6efe4, 4.90 module default.
 * Floor 4.5. test_library_prep_plate.js holds it. */
.lib-tile-prep { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; gap: 10px; align-items: center; justify-content: center; text-align: center; padding: 12px; color: var(--lklib-ink); background: rgba(30, 26, 23, 0.65); }
.lib-tile-prep-text { font-family: var(--lklib-sans); font-size: 12px; font-weight: 600; letter-spacing: .3px; }
.lib-tile-prep-spin { width: 26px; height: 26px; border-radius: 50%; border: 3px solid rgba(255,255,255,0.35); border-top-color: var(--lklib-ink); animation: lklib-spin 0.9s linear infinite; }
@keyframes lklib-spin { to { transform: rotate(360deg); } }
.lib-tile-cta--disabled { color: var(--lklib-ink-faint); border-bottom-color: transparent !important; cursor: default; }

/* ===== tile body ===== */
.lib-tile-body { padding: 14px 16px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
/* 🔴 1.19.7 — 17px -> 21px (Yarden, 2026-10-11). Measured on the live page, the class title was the
 * SMALLEST-PAINTING text on its own card while being the largest DECLARED:
 *
 *     class title   17px declared   6.56px painted   (contrast 7.17)
 *     TAKE THIS CLASS 11px          8.00px painted   (contrast 4.61)
 *     FREE badge      10px          7.27px painted
 *
 * Because the title is sentence-case in a display serif whose x-height is 0.386 of the em, while
 * the chrome is UPPERCASE and paints cap-height on every letter. Declared size said 17 against 11;
 * the eye saw the reverse. The hierarchy was being carried entirely by COLOUR -- the title is the
 * darkest thing on the card at 7.17 against the chrome's 4.4-4.6 -- which is why it did not read as
 * broken, only as wrong.
 *
 * 21px gives a painted x-height of 8.11px, so the title now leads everything on its own card on
 * BOTH axes. Not larger: this is what 17px was already claiming to be. */
.lib-tile-title { font-family: var(--lklib-serif); font-weight: 400; font-size: 21px; line-height: 1.25; color: var(--lklib-ink); }
/* 1.24.0 -- the meta is a BLOCK of two lines, not one wrapping row. The row direction moved down
   to .lib-meta-line. Before this the single flex row broke wherever the content ran out of room,
   so the block was 13px on 35 of Mary Beth's cards and 34px on the other 155 -- a 21px swing that
   was a function of how many style tags a class carries, not of any decision. Now every card is
   26px and the card is 8px shorter. */
.lib-tile-meta { font-family: var(--lklib-sans); font-size: 11px; letter-spacing: 1px; color: var(--lklib-ink-faint); display: block; }
/* min-width: 0 so a long style list can still shrink inside the card rather than forcing it wider;
   flex-wrap so the second line degrades to three lines instead of overflowing if a teacher ever
   tags a class past what the sweep covered. */
.lib-meta-line { display: flex; gap: 8px; flex-wrap: wrap; min-width: 0; }
.lib-tile-meta .sep { opacity: 0.55; }
/* 1.24.0 -- THE DATE IS THE SCANNING ANCHOR (Yarden, 2026-10-16: "a visual way ... so it's easy to
   hone in on it with the eye as a user scanning through the cards").
   Every item on this line painted --lklib-ink-faint, so a reader scanning a shelf had nothing to
   fix on. The lift is VALUE and WEIGHT, never hue: Mackey's 1.19.9 ruling is that "value
   separation survives monochrome; hue never did", and hue would also have to clear contrast on all
   17 palettes -- donovan-mcgrath runs this surface on a DARK ground, where an accent that reads on
   cream disappears. --lklib-ink-dim is one step up from faint in the SAME ramp, so the relationship
   holds whichever way the ground goes, and no new token is introduced.
   Size is untouched on purpose: the line must not reflow, and a bigger date would reorder the
   reading before it aids it.
   🔴 NO FONT-WEIGHT (Yarden + Mackey, 2026-10-16). It shipped to review at 600 and both rejected
   it on sight. Rendered both at 11px on the real page: the value step ALONE separates the date
   cleanly, and 600 at 11px with 1px tracking fights itself -- tracking opens the counters, weight
   closes them -- and blooms further on donovan-mcgrath's dark ground, where ink optically gains.
   MEASURED on the card ground: meta run 3.96:1, the lifted date 5.85:1, a 1.48:1 separation
   between the two. That is the whole effect, and it costs one declaration. The date also crosses
   the 4.5 floor that the surrounding meta sits below, which was not the goal but is worth keeping. */
.lib-tile-when { color: var(--lklib-ink-dim); }
/* ===== card description (2026-10-07) =====
   ONE line collapsed, expanding in place. Mirrors the card on livekick.com, which clamps to a
   single line and reveals the rest inline rather than in a dialog.
   Measured on prod: the fleet median note is ~180-230 characters, so TRUNCATION IS THE NORMAL
   STATE, not the edge case -- the "more" affordance is designed as part of the resting card, not
   as an exception. The text is already in the DOM; collapsing is purely this clamp, so expanding
   costs no fetch and no measurement. */
.lib-tile-teaser { font-family: var(--lklib-sans); font-size: 12.5px; line-height: 1.45; color: var(--lklib-ink-dim); margin-top: 2px; }
.lib-tile-teaser[hidden] { display: none; }
/* PER-VIEW RESERVATION (Yarden, 2026-10-09). A card with no note emits an EMPTY slot rather than
 * nothing, so cards can be made the same height -- but it only takes up space on a view that
 * contains at least one note (the .lk-reserve-note flag, set in ONE place: reserveNoteClass).
 * Without the flag the slot collapses, which keeps 1.17.0's reason intact: the 7 of 17 teachers
 * with almost no class notes must not get a blank band on every card.
 * 2.9em = one clamped line + the more/less control, which is what a card WITH a note occupies. */
.lib-tile-teaser.is-empty { display: none; }
.lk-reserve-note .lib-tile-teaser.is-empty { display: block; min-height: 2.9em; }
.lib-tile-teaser-text { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.lib-tile-teaser.is-expanded .lib-tile-teaser-text { -webkit-line-clamp: unset; overflow: visible; }
/* `more` inherits the note's font and colour and is underlined, so it reads as part of the
   sentence rather than as a second CTA competing with "Take this class". */
.lib-tile-more { background: none; border: 0; padding: 0; margin-top: 2px; font: inherit; color: inherit; opacity: 0.75; text-decoration: underline; cursor: pointer; }
.lib-tile-more:hover, .lib-tile-more:focus-visible { opacity: 1; color: var(--lklib-ink); }
/* The series blurb, once, in the drawer head -- never on a card. It describes the SERIES, and on
   Mary Beth 11 distinct values cover all 500 classes, so per-card it reads as a copy-paste bug. */
.lib-drawer-note { font-family: var(--lklib-sans); font-size: 13px; line-height: var(--lklib-note-lh); color: var(--lklib-ink-dim); max-width: 70ch; margin-top: 6px; }
/* 🔴 ALWAYS RESERVED (Yarden, 2026-10-09). This used to be `[hidden] { display: none }`, so the
 * whole grid jumped up and down every time a chip was toggled. He chose always-reserve over
 * conditional reservation, accepting that a teacher with no series descriptions carries a blank
 * band under the title. min-height is 4.65em -- about THREE lines at this line-height, not two:
 * the comment said "two" and the value never matched it. The value is correct as a 3-line cap
 * at 70ch and is deliberately NOT changed here; cutting it to 3em would reintroduce the grid
 * jump the reservation exists to prevent. Comment corrected 1.19.9. One line is the common case
 * and two is the cap, so the header does not resize between one description and the next either. */
/* 🔴 TWO LINES OF EM, FOR ONE LINE OF TEXT, AND THAT IS CORRECT. `.lib-note-more` ALWAYS occupies
 * its own line -- `[hidden]` gives it `visibility: hidden; display: block` (the 1.19.5 fix, two
 * rules above), because a `more` that collapses makes the slot step between a short description and
 * a long one. So the reservation is one clamped line + the more line + the gap between them:
 * 19.5 + 19.5 + 2 = 41px, against 60/61 today. Measured 41 empty and 41 filled, which also removes
 * the 1px empty-vs-filled step the old 4.65em carried. Neither the font-size nor the line-height
 * moves (Mackey: 13px / 1.5 both stay). */
.lib-drawer-note { min-height: calc(2em * var(--lklib-note-lh) + var(--lklib-note-more-gap)); }
.lib-drawer-note.is-empty { visibility: hidden; }

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * 1.19.3 — the series description clamps to TWO lines with a more/less toggle (Yarden, 2026-10-11).
 *
 * The slot was already RESERVED at two lines and its own comment said so: "one line is the common
 * case and two is the cap". Nothing enforced the cap. Shiva's Daily Prana Flow description runs six
 * lines and pushed the whole row down, which is the layout jump the reservation exists to prevent,
 * arriving by the other door.
 *
 * Same mechanism as the card teaser (.lib-tile-teaser), which has clamped since 1.18.0 — one
 * pattern, four surfaces, so there is nothing new for a reader to learn.
 * ══════════════════════════════════════════════════════════════════════════════════════════════ */
/* 🔴 ONE clamped line, not two (1.21.0). Not a compromise -- a consistency fix: the card-level
 * teaser in this same module already shows ONE clamped line of the same text from the same source
 * with the same inline more/less (.lib-tile-teaser-text), and two different treatments of identical
 * copy sat 400px apart on one page. Yarden: "you're reserving too much vertical space for the
 * series descriptions." The full text stays one tap away. */
.lib-note-text { display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.is-expanded > .lib-note-text { -webkit-line-clamp: unset; overflow: visible; }
.lib-note-more { background: none; border: 0; padding: 0; margin-top: var(--lklib-note-more-gap); font: inherit; color: inherit; opacity: 0.75; text-decoration: underline; cursor: pointer; display: block; }
.lib-note-more:hover, .lib-note-more:focus-visible { opacity: 1; color: var(--lklib-ink); }
/* 🔴 1.19.5 — the button RESERVES its line even when it is not needed. Measured live on the
 * drawer: with `display:none` the slot was 39px without a `more` and 60.5px with one, so the chips
 * still stepped 21.5px between a two-line description and a six-line one. visibility:hidden keeps
 * the line and is the same technique the empty note already uses one rule above. */
.lib-note-more[hidden] { visibility: hidden; display: block; }
/* Expanding must not be capped by the reserved height, and the row note is a <span> in an <h3>. */
.lib-row-note, .lib-all-note, .lib-drawer-note { display: block; }

.lib-tile-cta { margin-top: auto; padding-top: 10px; font-family: var(--lklib-sans-display); font-size: 11px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--lklib-accent); font-weight: 600; border-bottom: 1.5px solid transparent; align-self: flex-start; transition: border-color .2s, color .2s; cursor: pointer; display: inline-block; }
.lib-tile-cta:hover { border-bottom-color: var(--lklib-accent); }
.lib-empty { padding: 40px; text-align: center; color: var(--lklib-ink-faint); font-size: 14px; }

/* ===== series card ===== */
.lib-series { max-width: 1500px; margin: 0 auto 28px; padding: 0 24px; }
.lib-series-card { display: grid; grid-template-columns: 5fr 7fr; background: var(--lklib-tile-bg); border: 1px solid rgba(250,244,232,0.10); border-radius: 10px; overflow: hidden; cursor: pointer; transition: border-color .2s ease; position: relative; }
@media (hover: hover) and (pointer: fine) { .lib-series-card:hover { border-color: var(--lklib-tile-ring, var(--lklib-accent)); outline: 2px solid var(--lklib-tile-ring, var(--lklib-accent)); outline-offset: -2px; } }
.lib-series-thumb { aspect-ratio: 16/9; position: relative; background: var(--lklib-tile-bg-solid); overflow: hidden; }
.lib-series-thumb video, .lib-series-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.lib-series-thumb::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(0,0,0,0) 60%, rgba(20,16,42,0.6) 100%); pointer-events: none; z-index: 1; }
.lib-series-count-badge { position: absolute; left: 16px; top: 16px; z-index: 2; background: var(--lklib-badge-bg, rgba(0,0,0,0.7)); color: var(--lklib-ink); padding: 6px 12px; border-radius: 20px; font-family: var(--lklib-sans); font-size: 11px; font-weight: 600; letter-spacing: 1.2px; text-transform: uppercase; backdrop-filter: blur(6px); }
.lib-series-body { padding: 28px 32px; display: flex; flex-direction: column; justify-content: center; gap: 12px; }
.lib-series-title { font-family: var(--lklib-serif-display); font-weight: 400; font-size: 28px; line-height: 1.15; color: var(--lklib-ink); letter-spacing: -0.01em; margin: 0; }
.lib-series-sub { font-family: var(--lklib-sans); font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--lklib-ink-dim); }
.lib-series-meta { font-family: var(--lklib-sans); font-size: 13px; color: rgba(250,244,232,0.72); line-height: 1.5; }   /* NOT --lklib-note-lh: this is the series card's meta line, not a description slot. The reservation arithmetic must not start tracking a value that has no reason to move with it. */
.lib-series-cta { margin-top: 8px; font-family: var(--lklib-sans-display); font-size: 12px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--lklib-accent); font-weight: 600; border-bottom: 1.5px solid var(--lklib-accent); align-self: flex-start; padding-bottom: 4px; }
@media (max-width: 700px) { .lib-series-card { grid-template-columns: 1fr; } .lib-series-body { padding: 20px 22px; } .lib-series-title { font-size: 22px; } }

/* ===== drawer ===== */
/* THE DRAWER SCROLLS, NOT ITS BODY, so the head can stick to the top of it.
   WHY: `position: fixed` inside an IFRAME pins to the iframe's box, not the browser window. On a
   teacher page embedded in Squarespace the iframe is thousands of pixels tall, so `inset: 0` makes
   the drawer that tall too and its header lands at the very top of the iframe -- far above
   whatever the visitor is actually looking at, and underneath the host's own nav. That is what the
   `lk-overlay` postMessage handshake is for (the loader pins the IFRAME to the real viewport at
   z-index 2147483000), but the module must not DEPEND on the host cooperating: an older per-teacher
   loader, or an embed that does not pass ?host=ssloader, silently gets none of it.
   A sticky head degrades correctly in every case -- the title, chips, sorters and close button
   travel with the scroll instead of being stranded off-screen.

   --lklib-drawer-top IS THE ESCAPE HATCH for a host that cannot take the handshake. On
   livekick.com the teacher library is embedded in the Angular app, whose own 110px top nav is
   NOT covered because that app does not implement `lk-overlay` the way the ssloader does -- so
   the drawer head renders under it and the Save button is clipped. Until the app handles the
   message, that surface sets --lklib-drawer-top to its nav height and the drawer starts below
   the chrome instead of behind it. Default 0 keeps every other surface unchanged. */
.lib-drawer { position: fixed; inset: var(--lklib-drawer-top, 0) 0 0 0; z-index: 2147483001; background: var(--lklib-drawer-bg, var(--lklib-tile-bg-solid)); display: none; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; }
.lib-drawer.is-open { display: flex; }
/* 🔴 STACKING BANDS. Tile chrome (hearts, arrows, badges, play) occupies 1-4. A sticky HEAD must
   sit above all of it, so it starts at 10. The head was at z-index 3 -- EQUAL to .lib-tile-fav, and
   equal z-index falls back to DOM order, so the hearts (later in the document) painted straight
   over the sticky header while scrolling. .lib-arrow at 4 was over it outright. Reported by Yarden
   2026-10-07; the arrows were the same bug, unreported.
     1-4   tile chrome          10  sticky heads
     2147483001 drawer          2147483002 lightbox                                              */
.lib-drawer-head { position: sticky; top: 0; z-index: 10; padding: 22px 32px; border-bottom: 1px solid var(--lklib-pill-border, rgba(250,244,232,0.22)); display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; background: var(--lklib-drawer-bg, var(--lklib-tile-bg-solid)); }
/* The close button must stay reachable even when the head wraps on a narrow embed. */
/* The close control is pinned to the head's top-right corner rather than flowing after the
   chips and sorters. In the flow it wrapped onto its own line BELOW the sort row and read as one
   more filter control instead of a way out (Yarden, 2026-10-07). */
/* 🔴 THE DRAWER'S OWN R2 (1.21.0). Addy measured the headline claim FAILING here: jenna's drawer
 * moved the first card 53.8px on 613 of 1681 widths (36.5%), band [939, 1551], and caley-alyssa and
 * marybethlarue each failed in a DIFFERENT band -- three teachers, three bands, so no sample of
 * widths catches all three. Cause: Julie's R2 was applied to .lib-all-chips and the drawer's
 * equivalent block was an unnamed <div>, so it was missed. In the All/All state the chip block is
 * ~1056px wide and .lib-drawer-actions wraps to its own line; filtered, the chips narrow and the
 * actions tuck back up beside them. That is Julie's source 4, on the second surface.
 * `flex: 1 0 100%` makes the block take its own line always, so the actions are always below it --
 * which is also what the on-page library does, so the two surfaces now compose identically. */
/* 🔴 `min-width: 0` IS BACK, AND ONLY BECAUSE THE TITLE IS NOW ONE LINE. Read both rules together
 * or you will re-run the whole loop.
 *
 * A flex item defaults to `min-width: auto`, which floors it at its CONTENT's minimum width. The
 * drawer's title is a series name that can be very long, so the floor pushed the whole block past
 * the drawer and the content ran off the side: measured 1,713px of horizontal overflow on
 * marybethlarue at 320, and 609px at 1440, so it was never a phone-only problem.
 *
 * 1.21.1 REMOVED `min-width: 0` to stop a different defect: with it, the block could shrink below
 * its content and the TITLE WRAPPED — and the title names the current selection, so its height
 * became a function of which series was picked (24.5 / 49 / 73.5px at 320, moving the grid 49px).
 * That is rule 14, so it had to go.
 *
 * The two are only in tension while the title is allowed to wrap. Clamp the title to one line and
 * both hold at once: the block may shrink, and shrinking changes nothing about its height. Measured
 * after: 0 horizontal overflow and ONE title height, at every width and in every selection. */
.lib-drawer-main { flex: 1 0 100%; min-width: 0; }
/* 🔴 NO `min-width: 0` HERE, AND IT WAS IN THE FIRST DRAFT. It is the standard remedy for a flex
 * item that WON'T shrink; this is an item that must NOT shrink. With it the block can go below its
 * min-content width, so the drawer TITLE wraps -- and the title names the current selection, so
 * its height then becomes a function of which series is picked. Measured on marybethlarue: with
 * min-width:0 the title took 24.5 / 49 / 73.5px at 320 and the grid moved 49px, where live 1.20.0
 * moved 0.05px. Without it: one title height and 0px, at every width. A fifth source of movement,
 * introduced by the fix for the fourth, and found only because the sweep measured from the DRAWER
 * rather than from the head -- measuring from the head's bottom hid a head that was changing
 * height and reported a clean 0. The origin you measure from is part of the measurement. */

.lib-drawer-head .lib-drawer-close { position: absolute; top: 18px; right: 18px; margin: 0; }
/* The close button is position:absolute, so it is OUT of the header's flex flow and nothing
 * reserved its space -- whatever sat rightmost in the header ran underneath it. Measured on
 * marc-holzman, whose sort row carries five options: the last one (POPULAR) rendered clipped
 * behind the X (Yarden, 2026-10-09: "the filter chips are in the wrong place (and under the X
 * button)"). The X is 44px wide at right:18px, so the header reserves 44+18+12 of gutter and
 * every child -- title, count, note, chips and sorts -- now stops short of it. Fixing it on the
 * HEADER rather than per child is the point: the next control added to this header inherits the
 * clearance instead of rediscovering the bug. */
.lib-drawer-head { padding-right: 74px; }
.lib-drawer-title { font-family: var(--lklib-serif-display); font-weight: 400; font-size: 24px; color: var(--lklib-ink); letter-spacing: -0.01em; margin: 0;
  /* ONE line, with an ellipsis. Same idiom and the same reason as .lib-row-sub (1.19.10) and the
   * description slot (1.21.0): a title whose line count depends on the selection is a title that
   * moves everything under it. The full name is still on the chip she pressed and in the `title`
   * attribute. 🟡 Mackey: line-clamp vs text-overflow is yours — I used the module's existing
   * line-clamp idiom so all three truncations here are spelled the same way. */
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; }
.lib-drawer-sub { font-family: var(--lklib-sans); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--lklib-ink-dim); }
.lib-drawer-close { background: transparent; border: 1px solid var(--lklib-pill-border, rgba(250,244,232,0.22)); color: var(--lklib-ink); width: 44px; height: 44px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .2s; padding: 0; font-size: 20px; line-height: 1; }
/* 🔴 An accent FILL must always state its own ink. This rule filled with the accent and set no
   colour, so the glyph inherited --lklib-ink: on a teacher whose accent IS her ink that is
   near-black on near-black and the X vanished on hover. Same defect class as 1.14.4/1.14.5, in the
   one control that had no `color` declaration for the guard to look at -- which is exactly why the
   guard now flags an accent fill with NO ink, not just one with the wrong ink. */
.lib-drawer-close:hover, .lib-drawer-close:focus-visible { background: var(--lklib-accent); border-color: var(--lklib-accent); color: var(--lklib-on-accent); }
.lib-drawer-body { flex: 1 0 auto; padding: 28px 32px 60px; }
/* 1.20.0 -- the drawer was a THIRD independent card width (a bare 320px) that the card-sizing
 * brief missed entirely. Worst spread before this: the same class at 165px in the library and
 * 343px in the drawer, one tap apart on the same phone -- 2.07x. Same scale as everything else. */
/* 🔴 NO `margin: 0 auto`. It was there to centre the grid under its old fixed 1500px ceiling,
 * and it survived the move to the closed-form cap, where it is actively wrong: the cap now
 * binds at most widths, so an auto margin centres the whole grid and takes the first card off
 * the drawer's own 32px rail. Measured by Addy at 954 of 1320 widths, up to 184px off, and at
 * vw 601 it put the library's card at x=24 and the drawer's identical 360px card at x=120.5 --
 * the same class, one tap apart, on two different rails. Mackey's rule reaches this too:
 * leftover space does not get to move the card. The leftover belongs at the right edge. */
.lib-drawer-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--lklib-card-min), 1fr)); gap: var(--lklib-grid-gap); }
.lib-drawer-grid > *,
.lib-drawer-grid .lib-tile { max-width: var(--lklib-card-max); width: 100%; justify-self: start; }

body.lib-drawer-open { overflow: hidden; }

/* ===== quick-jump / filter ===== */
/* 1.23.0 -- THE BAR IS AN INDEX (Yarden, 2026-10-15). It was one flex row mixing filter pills
   and jump pills, drawn identically. It is now two stacked parts: the single action + the true
   count, then a quiet text-only index of the shelves. The container stops being a flex row
   because it no longer holds one row of like things. Every colour below is a token already
   painted on this same bar -- --lklib-pill-ink from .lib-jump-btn, --lklib-accent from
   .lib-all-viewall, --lklib-ink-dim from .lib-all-count -- so no new contrast decision enters
   the fleet. That matters: donovan-mcgrath runs this surface on a DARK ground
   (--lklib-ink: #E8E6E2), where any hardcoded grey would have been invisible. */
.lib-jump { display: block; margin: 0 24px 30px; }
/* 🔴 1.23.1 -- the stack owns the layout, NOT #lib-jump. Five pages set
   `#lib-jump { display:flex; flex-wrap:wrap }` in their own style block and an ID beats a class,
   so the rule above is advisory on those pages. `flex: 1 1 100%` makes this element take a full
   line of a flex parent; `width: 100%` does the same under a block parent. Under either, the two
   rows stack. `min-width: 0` so the index inside can still shrink and scroll -- a flex item
   defaults to min-width:auto and would otherwise refuse to be narrower than its own text. */
.lib-jump-stack { display: block; flex: 1 1 100%; width: 100%; min-width: 0; }
.lib-jump-lead { display: flex; align-items: baseline; justify-content: center; gap: 14px; flex-wrap: wrap; }
/* Same affordance and the same token as .lib-all-viewall, which this replaces -- it is the same
   action, moved from the foot of the page to the head of the library. */
.lib-jump-viewall { background: transparent; border: none; padding: 0; cursor: pointer; font-family: var(--lklib-sans); font-size: 13px; font-weight: 600; letter-spacing: 1.1px; text-transform: uppercase; color: var(--lklib-accent); text-decoration: underline; text-underline-offset: 4px; }
.lib-jump-viewall:hover { text-decoration: none; }
.lib-jump-total { font-family: var(--lklib-sans); font-size: 12px; letter-spacing: 1.1px; text-transform: uppercase; color: var(--lklib-ink-dim); white-space: nowrap; }
/* `justify-content` is set by fitJumpAnchors() at runtime -- centred while the row fits,
   flex-start once it overflows, because centring an overflowing scroller puts its first items
   out of reach. The declaration here is the pre-measurement default and the no-JS fallback. */
.lib-jump-anchors { display: flex; gap: 20px; margin: 18px 0 0; padding: 2px 0 4px; overflow-x: auto; white-space: nowrap; justify-content: center; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.lib-jump-anchors::-webkit-scrollbar { display: none; }
/* min-width: 0 is not cosmetic -- a flex item defaults to min-width: auto, which would refuse to
   be narrower than its own text and defeat the scroller. Same reason .lib-chipset carries it. */
.lib-jump-anchor { flex: 0 0 auto; min-width: 0; background: transparent; border: 0; border-bottom: 1px solid transparent; padding: 0 0 3px; cursor: pointer; font-family: var(--lklib-sans); font-size: 13px; font-weight: 400; line-height: 1.3; color: var(--lklib-pill-ink, var(--lklib-ink-dim)); transition: color .2s, border-color .2s; }
.lib-jump-anchor:hover { color: var(--lklib-ink); border-bottom-color: var(--lklib-accent); }
/* The pre-1.23.0 pill row, unchanged, rendered only when cfg.jumpBarFilters is on. */
.lib-jump-filters { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin: 18px 0 0; }
.lib-jump-btn { background: transparent; border: 1px solid var(--lklib-pill-border, rgba(250,244,232,0.22)); color: var(--lklib-pill-ink, rgba(250,244,232,0.85)); border-radius: 30px; padding: 8px 16px; font-family: var(--lklib-sans); font-size: 12px; font-weight: 500; letter-spacing: 1px; cursor: pointer; transition: all .2s; display: inline-flex; align-items: center; gap: 6px; }
.lib-jump-btn:hover { border-color: var(--lklib-accent); color: var(--lklib-accent); }
.lib-jump-btn.is-active { background: var(--lklib-accent); border-color: var(--lklib-accent); color: var(--lklib-on-accent); }
.lib-jump-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: 0.6; }

/* ===== the FULL LIBRARY section (v5 PR-A) =====
   Sits BELOW the series rows. Renders every published class as a grid, not a track: a row is a
   curated shelf you scroll, the library is an index you scan. Same tile markup, so a class looks
   identical in both -- Robin's R4 (identical treatment, no dimming, no demotion) applies between
   the rows and the library exactly as it does between the two row groups. */
.lib-all { max-width: 1500px; margin: 48px auto 0; padding: 0 24px; }
/* 1.19.9 -- `space-between` is not a layout decision, it is the absence of one: it threw the
 * title and the filter bar at opposite walls, so the composition was a function of how many
 * styles a teacher happened to tag. Measured across the fleet that is 2 chips (rytoast,
 * andrew7sealy, celest) to 10 (marybethlarue) -- balanced at ten, visibly broken at two, and
 * worst on the three teachers who now render no shelves, where this row is the first thing on
 * the page. The heading NAMES the collection; the chips OPERATE on it, so the filter bar takes
 * its own full-width line on the grid's left edge. Content-independent: identical at 2 chips
 * and at 10, no media query, no container query, no JS. Same idiom as .lib-all-note below --
 * one mechanism in this file, not two. Mackey, 2026-10-12. */
.lib-all-head { display: flex; align-items: flex-start; justify-content: flex-start; flex-wrap: wrap; gap: 12px 16px; margin: 0 0 18px; }
.lib-all-title { font-family: var(--lklib-serif-display); font-weight: 400; font-size: 26px; line-height: 1.15; color: var(--lklib-ink); letter-spacing: -0.01em; margin: 0; }
/* The TRUE count, not countLabel()'s floored "N+". Accent + uppercase matches .lib-row-count. */
/* 1.19.9 -- the count and VIEW ALL were BOTH 11px uppercase accent, 14px apart inside one h3,
 * so they read as a single run of accent text rather than a label and an action. Value
 * separation survives monochrome; hue never did. On the per-shelf heads the same pair is safe
 * only because `space-between` holds them at opposite ends. Mackey, 2026-10-12. */
.lib-all-count { font-family: var(--lklib-sans); font-size: 11px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--lklib-ink-dim); white-space: nowrap; margin-left: 12px; }
/* Same affordance as .lib-row-seeall on the rows, on purpose (Yarden 2026-10-06, option 2): one
   "show me more" control on the page, not two competing ones. */
.lib-all-viewall { background: transparent; border: none; color: var(--lklib-accent); font-family: var(--lklib-sans); font-size: 11px; letter-spacing: 1.2px; text-transform: uppercase; cursor: pointer; padding: 0 0 0 14px; white-space: nowrap; }
.lib-all-viewall:hover { text-decoration: underline; }
.lib-all-controls { flex: 1 0 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
/* 1.19.10 -- SECOND defect, found only by sweeping breakpoints after Yarden asked for every one.
 * At <=600px .lib-all-head and .lib-all-controls become flex COLUMNS, and `align-items: flex-start`
 * (1.19.9, correct for the row direction) stops sizing children to the container on the cross
 * axis -- so each one sized to its own MAX-CONTENT and the filter row ran 14px past the page edge
 * at 320px. Not caused by the row-header fix: measured identically on the pre-fix build.
 * `max-width: 100%` re-binds each level to its parent; `.lib-chipset` additionally needs
 * `min-width: 0` because a flex item's default `min-width: auto` would otherwise refuse to be
 * narrower than its unwrapped chip row and defeat its own `overflow-x: auto`.
 * 🔴 Verified the scroller still scrolls after this (content 266px inside a 236px box) -- the
 * point is to make the CONTAINER fit the page, never to stop the chips from scrolling.
 * Tried first and REJECTED by measurement: min-width:0 alone on the chain changed nothing,
 * because the cause is cross-axis sizing in a column, not a min-width floor. */
.lib-all-controls, .lib-all-chips, .lib-chiprow { max-width: 100%; }
.lib-chipset { min-width: 0; }
/* 🔴 NOT `flex-wrap: wrap` (1.21.0). This rule predates the two-family chip block and was never
 * revisited when it arrived: the later .lib-all-chips rule sets flex-direction: column but does
 * NOT reset the wrap, so the two .lib-chiprow children could wrap into a second COLUMN. It never
 * fired because nothing constrains the block's height -- which is exactly the kind of latent
 * declaration that starts firing the day someone adds a max-height. Two declarations of the same
 * property on the same selector, 380 lines apart, is the smell; the second one now owns it. */
.lib-all-chips { display: flex; gap: 8px; }
/* Sort is a quieter affordance than the style chips on purpose: the chips change WHICH classes
   you are looking at, sort only changes their order. Same family, lower contrast. */
.lib-all-sort, .lib-drawer-sort { display: flex; flex-wrap: wrap; gap: 6px; }
/* 🔴 1.19.8 — scoped with :not(.lib-sort-opt) (Yarden, 2026-10-11).
 * 1.19.7 kept these classes on the dropdown's OPTIONS so the ten handlers and two test files that
 * bind them would keep working. The data role carried over -- and so did the PAINT. Measured on
 * the open menu: items rendered 13px UPPERCASE at 0.9px tracking, painting 9.46px inside a trigger
 * painting 6.00px. 1.58x, and shouted, where the duration menu one row over runs 1.08x.
 *
 * The gate written in 1.19.7 asserted the sort dropdown SHARES the duration dropdown's rules, and
 * it passed -- because it does. The leak came from the EXTRA class bolted on top, which no rule-
 * level assertion can see. That is why 1.19.8 also gates the RENDERED proportion. */
.lib-all-sort-btn:not(.lib-sort-opt), .lib-drawer-sort-btn:not(.lib-sort-opt) { background: transparent; border: 1px solid transparent; color: var(--lklib-ink-faint, rgba(250,244,232,0.55)); border-radius: 30px; padding: 6px 12px; font-family: var(--lklib-sans); font-size: 11px; letter-spacing: 0.9px; text-transform: uppercase; cursor: pointer; transition: all .2s; }
.lib-all-sort-btn:not(.lib-sort-opt):hover, .lib-drawer-sort-btn:not(.lib-sort-opt):hover { color: var(--lklib-accent); }
.lib-all-sort-btn.is-active:not(.lib-sort-opt), .lib-drawer-sort-btn.is-active:not(.lib-sort-opt) { color: var(--lklib-accent); border-color: var(--lklib-accent); }
/* Pager. "Load N more (M left)" — the remaining count is the honest part: it tells you how deep
   the library is before you commit to scrolling it. */
.lib-all-pager, .lib-drawer-pager { display: flex; justify-content: center; margin: 28px 0 8px; }
.lib-all-more, .lib-drawer-more { background: transparent; border: 1px solid var(--lklib-pill-border, rgba(250,244,232,0.22)); color: var(--lklib-ink, #FAF4E8); border-radius: 30px; padding: 12px 28px; font-family: var(--lklib-sans); font-size: 12px; font-weight: 500; letter-spacing: 1px; cursor: pointer; transition: all .2s; }
.lib-all-more:hover, .lib-drawer-more:hover { border-color: var(--lklib-accent); color: var(--lklib-accent); }
.lk-pager-left { opacity: 0.6; font-size: 11px; margin-left: 4px; }
/* Deliberately the same affordance as .lib-jump-btn -- a filter is a filter, and a second chip
   vocabulary on one page teaches the member that these two do different things. They do not. */
/* 🔴 1.19.2 line-height pin (Mackey c2, same review). The chip never declared one, so its height
 * was whatever the teacher's font metrics handed it: measured 32.5px on shiva-rea, 34.0px on
 * marc-holzman, 34.5px on marybethlarue and 37.8px on jenna, whose host page's 19.8px line-height
 * inherits straight into a platform control. A 5.3px spread on a control the spec calls fixed.
 * 15px -> a 33px box; the drawer's 13px -> 27px. Padding, radius and border are unchanged.
 * Tap targets are untouched: the 44px hit area is an absolutely-positioned ::after with its own
 * min-height, independent of visual height. */
.lib-all-chip { background: transparent; border: 1px solid var(--lklib-pill-border, rgba(250,244,232,0.22)); color: var(--lklib-pill-ink, rgba(250,244,232,0.85)); border-radius: 30px; padding: 8px var(--lklib-chip-pad-x); font-family: var(--lklib-sans); font-size: 12px; font-weight: 500; line-height: 15px; white-space: nowrap; letter-spacing: 1px; cursor: pointer; transition: all .2s; display: inline-flex; align-items: center; gap: 7px; }
/* 🔴 A CHIP THAT YIELDS NOTHING IS GREYED, NEVER REMOVED (Yarden, 2026-10-14). The `disabled`
 * attribute does the real work -- unclickable, out of the tab order, announced as unavailable --
 * and this is the only thing that paints. OPACITY ONLY: no size, no border, no padding, so the row
 * cannot change height or width when a chip dies, which is the whole point. The hover rule below
 * is scoped off it for the same reason a disabled control must not look pressable.
 * 🟡 Filed for Mackey: this is a new visual state and the exact value is his. 0.38 is the lowest
 * step that still reads as present rather than absent on the lightest teacher palette. */
.lib-all-chip.is-dead, .lib-drawer-chip.is-dead { opacity: 0.38; cursor: default; }
.lib-all-chip:not(.is-dead):hover { border-color: var(--lklib-accent); color: var(--lklib-accent); }
/* ON-ACCENT INK (1.14.5). A filled accent chip is the ONE place the module paints text onto the
   teacher's accent, and it was using `--lklib-ink` -- which means "text on a TILE". The chip sits
   on the SCRIM. Measured across all 17 live pages, cream-on-accent failed AA on 12 of them (worst
   1.43:1 liz-arch, 1.67:1 donovan). 1.14.5 used the teacher's scrim as the ink; 1.26.0 (GI #1785)
   retired that fallback -- the scrim is a veil, not an ink -- and declares --lklib-on-accent in :root
   as the system ink #1E1A17.
   `--lklib-on-accent` lets the two pages whose accent is DARK (cristi-christensen, hannah-barrett,
   where the tile ink wins instead) override per page without a module fork. */
.lib-all-chip.is-active { background: var(--lklib-accent); border-color: var(--lklib-accent); color: var(--lklib-on-accent); }
/* 🔴 THE COUNT RESERVES ITS WIDEST VALUE, so a chip's width never changes (Yarden, 2026-10-14:
 * "it should be locked in, it should never be moving around as the user is clicking around").
 * "Vinyasa 21" narrowing to "Vinyasa 7" is one digit less, and every chip to its right slides.
 * --lklib-chip-n-w is published per row by the JS as the digit count of the widest number that
 * family will ever show; tabular-nums makes 1ch an exact digit, so N digits is an exact box.
 * Right-aligned so the number sits against the chip edge rather than floating in its reservation. */
.lib-all-chip-n { font-size: 10px; letter-spacing: 0; opacity: 0.7; font-variant-numeric: tabular-nums; display: inline-block; width: var(--lklib-chip-n-w, 2ch); text-align: right; }
/* 🔴 `width`, not `min-width`, and `letter-spacing: 0`, and BOTH are load-bearing. min-width
 * lets the natural width win when it is wider, which it was: letter-spacing adds a trailing
 * space PER CHARACTER, so "21" carried 1.2px that "7" did not and the chip still wobbled
 * 1.2px on every count change -- measured on caley-alyssa at 375, 1024 and 1440 alike. A
 * fixed width plus zero tracking plus tabular figures makes the box exactly N digits, full
 * stop. 🟡 Mackey: the 0.6px tracking on the numeral is what I removed to get there; it is
 * imperceptible at 10px on two digits, but it is yours to restore if you disagree -- the
 * reservation would then need to carry it. */
/* The drawer carries the same chips as the library grid, same affordance.
 * 🔴 A COLUMN, like .lib-all-chips (1.21.0). It was `flex-wrap: wrap` in row direction while
 * holding two .lib-chiprow children, so the SERIES and STYLE families sat side by side whenever
 * they happened to fit and stacked when they did not -- a two-row design whose row count was a
 * function of chip text. Julie's R5: identical rules on both surfaces. The scrolling now happens
 * one level in, on .lib-chipset, which is why this element is no longer a scroller itself. */
.lib-drawer-chips { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.lib-drawer-chip { white-space: nowrap; background: transparent; border: 1px solid var(--lklib-pill-border, rgba(250,244,232,0.22)); color: var(--lklib-pill-ink, rgba(250,244,232,0.85)); border-radius: 30px; padding: 6px 14px; font-family: var(--lklib-sans); font-size: 11px; font-weight: 500; line-height: 13px; letter-spacing: 1px; cursor: pointer; transition: all .2s; display: inline-flex; align-items: center; gap: 7px; }
.lib-drawer-chip:not(.is-dead):hover { border-color: var(--lklib-accent); color: var(--lklib-accent); }
.lib-drawer-chip.is-active { background: var(--lklib-accent); border-color: var(--lklib-accent); color: var(--lklib-on-accent); }
/* auto-fill, not auto-fit: a 2-class library keeps tile width instead of stretching two cards
   across 1500px. */
/* 1.20.0 -- the library grid joins the row's card scale. The floor decides how many columns fit;
 * the CAP stops a stretched cell from stretching the card, which is the whole mechanism.
 * 🔴 NOT `minmax(340px, 1fr)` -- that was the instrument Claire proposed and Mackey rejected by
 * arithmetic: 1fr stretches without limit, so raising the floor to 340 buys FEWER columns of much
 * WIDER cards (453px at 1440, 480px at 1024 -- a third larger than the row tile). It does not
 * remove the inversion, it reverses it. Floor + cap is what actually converges the two families. */
.lib-all-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--lklib-card-min), 1fr)); gap: var(--lklib-grid-gap); }
/* ── THE LEFTOVER GOES TO THE CONTAINER, NOT BETWEEN THE CARDS (1.20.0) ──────────────────────
 * Reported by Yarden on his own screen: the full-library cards sat visibly further apart than the
 * row cards directly above them, at some widths and not others.
 *
 * CAUSE, measured at viewport 1012: the grid CELLS stretch with `1fr` while the CARD is capped at
 * --lklib-card-max, so each cell's unused width showed up as space BETWEEN the cards -- row gap
 * 16px against library gap 130px, same page, same declared `gap`. It is not an alignment problem:
 * centring the card gives the identical 130px, it only moves the first card off the x=24 rail.
 * Swept rather than sampled: 647 of 1320 desktop widths, worst 143.5px at 1039.
 *
 * FIX, which is Mackey's own rule applied literally -- "space left over is a container problem,
 * never a card problem": cap the GRID at exactly the width its column count needs, so a cell can
 * never exceed --lklib-card-max. What is left over then sits to the RIGHT of the grid instead of
 * being shared out between every pair of cards.
 *
 * 🔴 WHY THIS IS ONE LINE AND NOT A BREAKPOINT LADDER. The first version of this fix was three
 * `@media (min-width: 704|1040|1376px)` rules carrying the same arithmetic. Addy rejected it and
 * he was right: a media query measures the VIEWPORT while the grid measures its own CONTENT BOX,
 * and anything that separates the two puts the bug back. He reproduced it with a classic 15px
 * scrollbar -- two 15px-wide bands where the ladder picks the wrong rung and the 130px gap returns
 * at 143.5px. An iframe narrower than the viewport, page zoom, and a host page with its own
 * padding each do the same thing. The closed form below reads `100%`, which IS the content box, so
 * there is nothing left to disagree with. It also needs no knowledge of the parent's padding --
 * which is why the identical declaration is correct in .lib-all AND inside the drawer, whose
 * padding is different.
 *
 * THE ARITHMETIC, so it can be re-derived rather than trusted:
 *     pitch_min = card-min + gap        the width one column occupies at its floor
 *     columns   = floor((W + gap) / pitch_min)                 <- what auto-fill itself computes
 *     wanted    = columns * (card-max + gap) - gap             <- the widest those columns may be
 * CSS has floor() via round(down, x, step):
 *     round(down, 100% + gap, pitch_min)  ==  columns * pitch_min
 * and multiplying that by --lklib-card-pitch-ratio turns pitch_min into pitch_max, giving exactly
 * `wanted`. The max() floor stops the expression going to zero below one column (a 240px-wide
 * container would otherwise compute -16px).
 *
 * MEASURED IN CHROME, every integer container width 240-1872 (1,633 of them), both grids:
 * gap is 16.00px at every one of them, the card never leaves [320, 360], no row mixes two card
 * widths, and the column count never disagrees with what auto-fill alone would have chosen --
 * i.e. the cap costs zero columns. Compare the same sweep before this rule: 647 widths wrong.
 *
 * FALLBACK, for a browser without round() (pre-Chrome 125 / Firefox 118 / Safari 15.4): fixed
 * --lklib-card-min columns. That is also gap-correct and column-count-identical; it just gives a
 * flat 320px card instead of the 320-360 band. Degraded, never broken -- which is the point of
 * putting the modern form inside @supports rather than leaving the old ladder as the fallback. */
.lib-all-grid,
.lib-drawer-grid {
  grid-template-columns: repeat(auto-fill, var(--lklib-card-min));
  justify-content: start;
}
@supports (max-width: round(down, 100px, 7px)) {
  .lib-all-grid,
  .lib-drawer-grid {
    grid-template-columns: repeat(auto-fill, minmax(var(--lklib-card-min), 1fr));
    max-width: max(var(--lklib-card-min), calc(
      round(down, 100% + var(--lklib-grid-gap),
            calc(var(--lklib-card-min) + var(--lklib-grid-gap)))
      * var(--lklib-card-pitch-ratio) - var(--lklib-grid-gap)));
  }
  /* The drawer keeps its own editorial ceiling -- at most four across, however wide the window.
   * 🔴 It is written as four PITCHES (4 * (card-max + gap) - gap = 1488), not as the old bare
   * 1500px, and that is load-bearing: 1500 is not a whole number of columns, so it left the fourth
   * cell 3px over the cap and a permanent 19px gap above 1555px. A ceiling that is not on the
   * pitch grid re-introduces the exact defect this block removes. */
  .lib-drawer-grid {
    max-width: min(
      calc(4 * (var(--lklib-card-max) + var(--lklib-grid-gap)) - var(--lklib-grid-gap)),
      max(var(--lklib-card-min), calc(
        round(down, 100% + var(--lklib-grid-gap),
              calc(var(--lklib-card-min) + var(--lklib-grid-gap)))
        * var(--lklib-card-pitch-ratio) - var(--lklib-grid-gap))));
  }
}
/* 🔴 THE CAP MUST OUT-SPECIFY `.lib-all-grid .lib-tile`, WHICH SITS DIRECTLY BELOW IT.
 * First version of this line was `.lib-all-grid > *` — specificity (0,1,0) — against that rule's
 * (0,2,0) `width: auto`. `width: auto` on a grid item only fills its cell while justify-self is
 * `stretch`; set it to anything else and the card falls back to FIT-CONTENT, i.e. its width
 * becomes a function of its own title. Measured on caley-alyssa at 1440: SEVEN distinct card
 * widths (286.76…336) and NINE distinct left edges where four columns should give four. It
 * survived review because her first eight cards all render 336 — the ragged ones are below the
 * fold of a spot check, and checking the FIRST card is what let it through (Mackey, 2026-10-13).
 * The drawer was the control that proved it: same `> *` rule, no `width: auto` override, one
 * width at every viewport.
 * 🔴 AND THE CARD SITS ON THE RAIL. `justify-self: center` inside an over-wide cell pushed the
 * first column off the module's x=24 rail by up to 104px — every other element in this module
 * (row header, row tiles, library heading, chips, group heads) sits on that line. Mackey:
 * "space left over is a container problem — it does not get to move the card." Centring is kept
 * only where the grid is a single column, where there is no rail to break. */
.lib-all-grid > *,
.lib-all-grid .lib-tile { max-width: var(--lklib-card-max); width: 100%; justify-self: start; }
.lib-all-grid .lib-tile { flex: none; }   /* 1.20.0: `width: auto` removed — it defeated the cap above */
.lib-all-empty { font-family: var(--lklib-sans); font-size: 14px; color: var(--lklib-ink-dim); padding: 28px 0; text-align: center; }

/* ===== group headings (v5 PR-B reads these; the module ships tolerant of their absence) =====
   "On the calendar now" / "From the library". Stated, never implied by order (Robin R4 + Erika
   E3). NO dimming and NO collapse on the library group: if it ships visually demoted, Robin
   rejects. So this is a LABEL only -- it sets no opacity, no scale, nothing on the rows below. */
.lib-group-head { font-family: var(--lklib-sans); font-size: 12px; font-weight: 600; letter-spacing: 1.6px; text-transform: uppercase; color: var(--lklib-ink-dim); max-width: 1500px; margin: 44px auto 18px; padding: 0 24px; }
.lib-group-head:first-child { margin-top: 0; }

/* ===== preview-enlarge lightbox ===== */
.lib-lightbox { position: fixed; inset: 0; background: rgba(0,0,0,0.92); z-index: 2147483002;   /* #842: above the see-all drawer so the lightbox opens IN FRONT, not behind. Both now sit near the top of the int32 z range because a teacher page is embedded in a HOST site whose own nav we must cover. */ display: none; align-items: center; justify-content: center; padding: 24px; }
.lib-lightbox.is-open { display: flex; }
.lib-lightbox-inner { position: relative; max-width: 90vw; max-height: 88vh; }
.lib-lightbox-video { display: block; max-width: 90vw; max-height: 88vh; width: auto; height: auto; border-radius: 8px; background: #000; }
.lib-lightbox-close { position: absolute; top: -44px; right: 0; background: transparent; border: none; color: var(--lklib-ink); font-size: 28px; line-height: 1; cursor: pointer; padding: 6px 10px; }
.lib-lightbox-close:hover { color: var(--lklib-accent); }
@media (max-width: 600px) { .lib-lightbox-close { top: 8px; right: 8px; background: rgba(0,0,0,0.5); border-radius: 999px; width: 36px; height: 36px; padding: 0; } }
/* Step 2c (#268): the card shows <=2 style tags; its preview-enlarge modal shows the FULL trusted list. */
.lib-lightbox-styles { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 12px; }
.lib-lightbox-styles[hidden] { display: none; }
.lib-lb-style { font-family: var(--lklib-sans); font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: var(--lklib-ink); background: rgba(255,255,255,0.12); border-radius: 999px; padding: 4px 10px; }

/* ===== mobile row tweaks ===== */
@media (max-width: 600px) {
  .lib-row-head { flex-direction: column; align-items: flex-start; gap: 4px; }
  .lib-row-title { font-size: 20px; }
  .lib-row-sub { margin-left: 0; white-space: normal; }
  .lib-row { padding: 0 16px; }
  .lib-all { padding: 0 16px; margin-top: 36px; }
  .lib-all-title { font-size: 20px; }
  .lib-all-count { margin-left: 0; display: block; margin-top: 4px; }
  .lib-all-viewall { padding: 0; display: block; margin-top: 6px; }
  .lib-all-head { flex-direction: column; align-items: flex-start; }
  .lib-all-controls { flex-direction: column; align-items: flex-start; gap: 10px; }
  .lib-all-sort, .lib-drawer-sort { gap: 4px; }
  .lib-all-sort-btn:not(.lib-sort-opt), .lib-drawer-sort-btn:not(.lib-sort-opt) { padding: 6px 10px; font-size: 10px; }
  /* 1.20.0 -- Mackey: "92px of video is not a card, it is a thumbnail with ambitions." The old
   * two-column phone grid gave a 165px card at 375px wide, i.e. 92px of video under a declared
   * 18px title -- the only place in the system where the type outweighed the image. One column,
   * still capped by --lklib-card-max so it cannot balloon at the top of this band. The DRAWER
   * already did this correctly at <=600; the library grid now matches the surface that was right. */
  .lib-all-grid { grid-template-columns: 1fr; gap: 12px; justify-items: center; max-width: none; }
  /* `max-width: none` releases the closed-form grid cap. The cap exists to stop a stretched CELL
   * stretching the CARD, which cannot happen here: this band is literally `1fr`, one column, no
   * gaps. Leaving it would shrink the grid box to the card width -- invisible, but noise.
   * Measured with and without the release at 320/360/375/414/480/600: identical card widths
   * (288/328/343/360/360/360), identical insets, no overflow at any of them.
   *
   * 🔴 AND THE COMMENT BELOW IS WRONG ABOUT WHAT HAPPENS -- LEFT IN PLACE DELIBERATELY SO THE
   * CLAIM IS NOT QUIETLY LOST. `justify-items: center` has never taken effect: `.lib-all-grid > *`
   * sets `justify-self: start`, which is the per-item property and wins over the container's
   * `justify-items`. Measured at 600px: the card sits at inset 16 with 224px of void to its
   * right, i.e. hugging the left, which is what Mackey's rule says it should NOT do. This is
   * pre-existing (it predates 1.20.0 and is not caused by the release above) and it is a visual
   * call, so it is NOT fixed here -- the one-line fix is a `justify-self: center` in this block,
   * and it is Mackey's to approve. Filed rather than silently taken. */
  /* single column: there is no rail to break, so the capped card centres rather than hugging
   * the left edge of a full-width cell. Mackey's rule, and the only surviving centring. */
  .lib-group-head { padding: 0 16px; margin: 32px auto 14px; }
  .lib-tile-body { padding: 12px 14px 16px; }
  .lib-tile-title { font-size: 18px; }   /* 1.19.7: was 15px, scaled with the desktop 17->21 */
}

/* ===== light-theme safety: controls on the dark GLASS overlays keep glass-ink,
   independent of the page's surface --lklib-ink (which a light page sets dark). ===== */
.lib-tile-playbtn, .lib-tile-audio, .lib-tile-expand, .lib-tile-dj, .lib-tile-free,
.lib-tile-prep, .lib-tile-prep-text, .lib-lightbox-close { color: var(--lklib-glass-ink); }
/* The drawer is NO LONGER on this list. It renders on the teacher's own surface now, so its title,
   sub, close and sort take her page ink from their own rules. Keeping them here is what made the
   drawer's text light on a light ground. */
.lib-tile-dj { background: var(--lklib-dj-bg); }
.lib-tile-prep-spin { border-top-color: var(--lklib-glass-ink); }

/* ===== first-paint skeleton ===== */
.lib-skel { display: flex; gap: 16px; max-width: 1500px; margin: 0 auto; padding: 8px 24px 16px; overflow: hidden; }
.lib-skel-tile { flex: 0 0 var(--lklib-card-w); aspect-ratio: 16/9; border-radius: var(--lklib-radius);
  background: linear-gradient(100deg, var(--lklib-skel) 30%, var(--lklib-skel-shine) 50%, var(--lklib-skel) 70%);
  background-size: 200% 100%; animation: lklib-shimmer 1.3s ease-in-out infinite; }
@media (max-width: 600px) { .lib-skel-tile { flex-basis: min(var(--lklib-card-max), calc(100% - var(--lklib-card-peek))); } }

/* ===== 1.22.0 -- the recent shelf's first-paint placeholder =====
 * 🔴 It is built from the ROW'S OWN classes (.lib-row / .lib-row-head / .lib-track-wrap /
 * .lib-track / .lib-tile / .lib-tile-thumb / .lib-tile-body) so that its geometry is produced by
 * the same rules that will lay out the real row, rather than by a parallel model which can agree
 * with itself while the artifact differs.
 * `.lib-skel` could not be reused: it reserves a bare 16/9 thumb and nothing else -- no row
 * header, no card text block -- so it under-reserves a real shelf by the header (a 26px/1.15
 * title plus its 14px margin) plus the tile body's 32px of padding and two lines of text. Using
 * it for this mount would have hidden a reflow behind something that looked like a reservation.
 * Only the SHIMMER is new; every box in here is an existing rule. */
.lib-skel-shine {
  background: linear-gradient(100deg, var(--lklib-skel) 30%, var(--lklib-skel-shine) 50%, var(--lklib-skel) 70%);
  background-size: 200% 100%; animation: lklib-shimmer 1.3s ease-in-out infinite;
  border-radius: var(--lklib-radius); }
.lib-row--skel .lib-row-title { width: 240px; max-width: 55%; }
/* The body's three placeholders ARE .lib-tile-title / -meta / -cta, so their boxes come from the
 * card's own rules. Only the shine and the inline-block are added here. The generic
 * .lib-skel-line pair that used to live here is GONE: it reserved two 1em lines against a real
 * body of 174px, i.e. 104px short, and was replaced rather than tuned. */
.lib-row--skel .lib-tile-title,
.lib-row--skel .lib-tile-meta,
.lib-row--skel .lib-tile-cta { display: block; border-radius: 4px; }
.lib-row--skel .lib-tile-meta { width: 45%; }
.lib-row--skel .lib-tile-cta  { width: 60%; }
@keyframes lklib-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

/* ===== reduced motion ===== */
@media (prefers-reduced-motion: reduce) {
  .lib-tile, .lib-tile-thumb video, .lib-tile-playbtn, .lib-tile-audio, .lib-tile-expand, .lib-tile-fav svg { transition: none; }
  .lib-tile:hover { transform: none; }
  .lib-tile-prep-spin, .lib-skel-tile, .lib-skel-shine { animation: none; }
}

/* ===== duration filter dropdown (brand-var driven) ===== */
.lib-dur-menu, .lib-sort-menu { position: relative; display: inline-flex; cursor: pointer; }
.lib-dur-trigger, .lib-sort-trigger { display: inline-flex; align-items: center; gap: 6px; }
.lib-dur-caret, .lib-sort-caret { font-size: 10px; line-height: 1; opacity: .75; }
.lib-dur-list, .lib-sort-list { position: absolute; top: calc(100% + 6px); right: 0; left: auto; z-index: 20; display: none; flex-direction: column; min-width: 168px; max-width: calc(100vw - 24px); padding: 6px; border-radius: var(--lklib-radius); background: var(--lklib-tile-bg-solid); border: 1px solid var(--lklib-tile-border); box-shadow: 0 12px 32px rgba(0,0,0,0.16); }
.lib-dur-menu[data-open="1"] .lib-dur-list, .lib-sort-menu[data-open="1"] .lib-sort-list { display: flex; }
.lib-dur-opt, .lib-sort-opt { text-align: left; background: transparent; border: none; color: var(--lklib-ink-dim); font-family: var(--lklib-sans); font-size: 13px; letter-spacing: .2px; padding: 11px 14px; min-height: 44px; border-radius: 8px; cursor: pointer; transition: background .15s, color .15s; }
.lib-dur-opt:hover, .lib-sort-opt:hover { background: var(--lklib-skel); color: var(--lklib-ink); }
.lib-dur-opt.is-active, .lib-sort-opt.is-active { color: var(--lklib-accent-deep); font-weight: 600; }
/* 🔴 1.19.7 — the five sort buttons become ONE dropdown (Yarden, 2026-10-11). They painted 8.00px
 * against a class title painting 6.56px, and they could not be de-capped alone without clashing
 * with "VIEW ALL" and "195 CLASSES" sitting beside them. A dropdown is not a new pattern here: the
 * Duration filter in the same row has been one since 1.16, so this reuses its markup, its CSS and
 * its open/close behaviour rather than inventing a second dropdown language. */
.lib-sort-list { right: auto; left: 0; }
/* 🔴 1.19.8 — the sort trigger now carries .lib-jump-btn, exactly as the duration trigger does,
 * so the two dropdowns sitting in the SAME ROW are the same control rather than two controls that
 * happen to both open. 1.19.7 gave this one a bespoke rule: 11px/400/0.2px against the duration
 * trigger's 12px/500/1px. Two dropdowns a thumb apart, styled differently, is the inconsistency
 * this whole change set is about, one level up. The bespoke rule is deleted, not overridden. */
.lib-sort-menu[data-open="1"] .lib-sort-trigger { border-color: var(--lklib-accent); color: var(--lklib-accent); }

/* ===== See all (opens the drawer grid) ===== */
.lib-row-meta { display: inline-flex; align-items: baseline; gap: 16px; flex: 0 0 auto; }  /* 1.19.10: never squeezed, never pushed off-canvas by a long series description */
.lib-row-seeall { background: transparent; border: none; color: var(--lklib-accent); font-family: var(--lklib-sans); font-size: 12px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; cursor: pointer; padding: 0; white-space: nowrap; }
.lib-row-seeall:hover { text-decoration: underline; }

/* ===== mobile overrides. MUST stay last: these beat the base rules on SOURCE ORDER, not
   specificity. Placed mid-file they silently lost to `.lib-drawer-chips { flex-wrap: wrap }`
   declared below them, and the chips kept wrapping into four rows on a phone. ===== */
/* MOBILE. The head is sticky, so every pixel it takes is permanently off the catalogue. Measured
   at 375px before this block: 250px of head against an 812px viewport -- 31% of the screen spent on
   chrome. The chips and sorters scroll horizontally instead of wrapping into stacked rows, the
   title drops a step, and the close keeps its 44px target clear of the title. */
@media (max-width: 600px) {
  .lib-drawer-head { padding: 14px 16px; gap: 10px; }
  .lib-drawer-body { padding: 18px 16px 50px; }
  .lib-drawer-title { font-size: 18px; padding-right: 52px; }
  .lib-drawer-grid { grid-template-columns: 1fr; max-width: none; }   /* same reason as .lib-all-grid above: one column, no gaps, nothing to cap */
  .lib-drawer-head .lib-drawer-close { top: 12px; right: 12px; }
  /* 44 + 12 + 12. The title's own padding-right is now redundant -- the header reserves the
   * gutter for every child -- and leaving both would double-indent the title on a phone. */
  .lib-drawer-head { padding-right: 68px; }
  .lib-drawer-title { padding-right: 0; }
  /* 1.21.0 -- .lib-drawer-chips is no longer in this rule. It is a COLUMN of .lib-chiprow now and
   * the scrolling happens one level in, on .lib-chipset, at every width. Leaving it here would
   * make the column itself scroll horizontally, which does nothing and hides nothing. The sort row
   * is unchanged: it is a genuine single row of buttons. */
  .lib-drawer-sort {
    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    -webkit-overflow-scrolling: touch; padding-bottom: 2px;
  }
  .lib-drawer-sort::-webkit-scrollbar { display: none; }
  .lib-drawer-sort-btn { flex: 0 0 auto; }
}

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * TAP TARGETS — every interactive control gets a 44px hit area (2026-10-08)
 *
 * MEASURED, live CDN, before this block: 12 of 14 interactive controls were under the 44px minimum
 * (Apple HIG; WCAG 2.5.5). `.lib-series-cta` -- the card's primary action -- was **16px**, as was
 * `.lib-row-seeall`. Sorts 25px, drawer chip 26px, grid chip and jump 32px, pagers 40px. Only the
 * drawer close and the duration option passed, and both had been fixed reactively after a bug.
 * Full audit: ops/MOBILE_TAP_TARGET_AUDIT_2026-10-08.md.
 *
 * 🔴 THE RULE WAS NOT INVENTED HERE. livekick-team/references/ACCOUNT_DESIGN_SYSTEM_v1.md already
 * specifies 44px controls AND this exact remedy: the Email-follow toggle pill keeps a 34px *visual*
 * height and reaches 44 through transparent padding on its wrapper. The library was simply built
 * outside that system. Mackey's review (ops/reviews/TEACHER_PAGE_MOBILE_TAP_TARGETS_DESIGN_
 * DIRECTION_MACKEY_2026-10-08.md) is "adopt it, do not invent a second answer".
 *
 * WHY ::after AND NOT PADDING. Mackey's binding condition is that **nothing in the layout moves** --
 * "if a screenshot diff shows a pixel change, it is wrong". Padding would reflow every one of these
 * controls and change the density of the chip rows. An absolutely-positioned, transparent ::after
 * overlay is outside flow: it costs zero layout and the rendered page is pixel-identical. Clicks on
 * a pseudo-element target its host, so the control simply becomes easier to hit.
 *
 * WHY NOT A VISIBLE 44px BOX. Also Mackey, explicitly: inflating chips to 44px of visible height on
 * a 375px screen turns a quiet row of filters into a stack of fat buttons and destroys the density
 * that makes a 500-session library legible. The hand needs 44. The eye does not.
 *
 * Verified safe before writing: none of these controls declares `position`, none carries a ::before
 * or ::after already, and no ancestor clips with overflow:hidden. Gated by
 * tools/check-tap-targets.js so a new control cannot ship under the minimum.
 * ══════════════════════════════════════════════════════════════════════════════════════════════ */
:root { --lklib-tap-min: 44px; }

.lib-series-cta, .lib-row-seeall, .lib-all-viewall,
.lib-all-chip, .lib-drawer-chip, .lib-jump-btn,
.lib-all-sort-btn, .lib-drawer-sort-btn,
.lib-all-more, .lib-drawer-more,
.lib-jump-anchor, .lib-jump-viewall,
.lib-lightbox-close { position: relative; }

.lib-series-cta::after, .lib-row-seeall::after, .lib-all-viewall::after,
.lib-all-chip::after, .lib-drawer-chip::after, .lib-jump-btn::after,
.lib-all-sort-btn::after, .lib-drawer-sort-btn::after,
.lib-all-more::after, .lib-drawer-more::after,
/* 1.23.0 -- the two new controls join the SAME mechanism rather than getting their own.
 * The 20px gap between anchors is wider than the 44px box overhangs a short label, so the
 * hit areas of two neighbouring series cannot overlap. */
.lib-jump-anchor::after, .lib-jump-viewall::after,
.lib-lightbox-close::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  height: 100%;
  min-width: var(--lklib-tap-min, 44px);
  min-height: var(--lklib-tap-min, 44px);
  /* No background, no border, no box-shadow: invisible by construction, not by a transparent
   * colour that a future edit could set. Nothing here may ever paint. */
}


/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * TWO CHIP FAMILIES — series and style (1.19.0)
 *
 * Mackey Saturday, 2026-10-09, ops/reviews/LIBRARY_CHIP_FAMILIES_DESIGN_DIRECTION_MACKEY_2026-10-09.md
 * Anchor: "a series is a THING, a style is a PROPERTY of a thing." You browse INTO Thrive 75; you
 * filter BY Yoga Nidra.
 *
 * 🔴 HUE CANNOT CARRY THIS. The accent is per-teacher and the measured spread across the live
 * fleet is enormous -- marybethlarue #1d1916 (near black), liz-arch #123F34 (deep green), jenna
 * #A78BB0 (plum), rytoast #E6357A (hot pink). "Series are warm" would render as a different idea
 * on all seventeen pages and as nothing at all on Mary Beth. The distinction is value-only and
 * survives monochrome.
 *
 * TWO ROWS, NOT ONE. Measured: marybethlarue would reach 22 chips in a single row (10 style + 11
 * series + All), shiva 18, amber 18, jenna 16, median 8. "A 22-chip scrolling row is not a filter,
 * it is a bin -- the reader cannot see the set, so they cannot form an intention."
 *
 * ONE DELTA AT REST: the GROUND. Geometry, radius, border width, padding and size are identical to
 * the style chip, so a screenshot of the style row against 1.18.3 shows nothing. The ACTIVE state
 * is shared -- an active chip means the same thing in both rows and must not look like two things.
 * ══════════════════════════════════════════════════════════════════════════════════════════════ */
/* 🔴 `flex: 1 0 100%` IS JULIE'S R2, AND IT IS THE SAME MECHANISM 1.19.9 APPLIED ONE LEVEL UP
 * (.lib-all-controls, css:380). Without it the sort control is a flex sibling of the chip block and
 * lands wherever the chips leave room -- which is how Yarden photographed "Recently added" floating
 * in mid-air to the right, vertically centred between the SERIES and STYLE rows. Content
 * independent, no media query, no JS. */
.lib-all-chips { display: flex; flex-direction: column; flex-wrap: nowrap; gap: 8px; flex: 1 0 100%; }
.lib-chiprow { display: flex; align-items: center; gap: 10px; min-width: 0; }
.lib-chiprow-label {
  flex: 0 0 auto;
  font-family: var(--lklib-sans);
  font-size: 10px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--lklib-ink-faint, var(--lklib-ink-dim));
  /* A word is cheaper and more durable than a shape language nobody is taught (Mackey c3), and on
   * a scrolled row it is the only thing telling the reader which family they are dragging (c6). */
}
/* ── ONE LINE PER FACET FAMILY, AT EVERY WIDTH (1.21.0) ──────────────────────────────────────
 * Julie's R1, lifting the ≤760px idiom to base scope. Her argument is not that scrolling is nicer
 * than wrapping: it is that a WRAPPING chip row makes the page's vertical rhythm a function of how
 * a teacher tagged her catalogue, which is data we do not control and never will. We have paid for
 * that three times -- 1.19.9 (the filter bar's composition), 1.19.10 (the row subtitle's length),
 * and the defect Yarden found on 2026-10-13, where picking a series moved the first card by up to
 * 125px because the style row left the flow. A row that is always one line tall is one we can
 * reason about without measuring.
 *
 * 🔴 `white-space: nowrap` ON THE CHIP (above) IS PART OF THIS RULE, NOT A TIDY-UP. Without it,
 * nowrap on the scroller alone lets the chips shrink below max-content and wrap their own labels:
 * Mackey measured a 78px series row at 1440 on marybethlarue, with "Embodied Sol Yoga + Sol…"
 * folded onto four lines inside a 114px chip. Remove one and the other stops working.
 *
 * 🔴 `min-height: var(--lklib-tap-min)` IS A HIT-TEST FIX, NOT PADDING. A mask (below) clips
 * hit-testing to the element's BOX in Chrome, and the chips' 44px tap target is an absolutely
 * positioned ::after that overhangs the chip by 5.5px top and bottom. Masking a 33px scroller
 * clipped exactly that overhang -- every chip on 13 of 17 pages silently dropping from a 44px hit
 * area to 33px, a 25% loss, with `tools/check-tap-targets.js` green throughout because it is a
 * static check of declarations and this is a runtime clip. Oversizing the mask does NOT help
 * (`mask-size: 100% 300%` measured: still clipped), because the clip is to the box.
 * `align-items: center` is equally load-bearing: the flex default is `stretch`, and a 44px content
 * box with stretch inflates every chip to 44px of VISIBLE height -- the fat row Mackey refused on
 * 2026-10-08. With it: chips 33px, scroller 44px, hit area restored at ±21px.
 *
 * The scrollbar hiding lives HERE and not in a media query, deliberately. If nowrap is
 * unconditional and the hiding is not, then on a classic-scrollbar platform the scroller reserves
 * a gutter only WHEN IT OVERFLOWS -- and whether it overflows is a function of the selection. That
 * is the defect walking back in through a different door, on the one platform this fix protects. */
.lib-chipset {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 8px;
  min-width: 0;
  min-height: var(--lklib-tap-min);
  align-items: center;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* so a chip scrolled to by keyboard lands clear of the fade, not under it. Same idiom as
   * .lib-track's scroll-padding on the card rails. */
  scroll-padding-inline: var(--lklib-chip-pad-x);
}
.lib-chipset::-webkit-scrollbar { display: none; }
.lib-chipset > * { flex: 0 0 auto; }

/* ── THE OVERFLOW AFFORDANCE: AN EDGE MASK (Mackey, 2026-10-14) ──────────────────────────────
 * A nowrap row at 1440 with 12 chips and a hidden scrollbar is a row whose extent the reader
 * cannot see. On marybethlarue the series row NEVER fits at any width in [240,1920] -- the host
 * page caps the library at 1100px, so the scroller tops out at 920px against 2042px of chips,
 * 45.1% visible at 1440 AND at 1920. On caley-alyssa it is 100% visible above 1024. Two teachers,
 * opposite answers, which is why the gate is overflow and nothing else.
 *
 * REFUSED in favour of this: the 1.19.11 row arrows (chip widths measured 70-214px, so a 44px
 * arrow covers 21-63% of whichever chip it lands on, and what it covers is a WORD -- on the card
 * rail it covers a photograph; also four more buttons in a bar that already has 23 controls), and
 * a visible scrollbar at pointer:fine (laid out inside the content box and present only when the
 * row overflows, i.e. a function of the selection -- the same defect again).
 *
 * 🔴 THE `#000` BELOW IS NOT A COLOUR. In an alpha mask it is opacity 1. It is the one place in
 * this module where a hex is not a visual decision, and a raw-hex sweep must not "fix" it. A mask
 * also needs no ground colour, which is why it works on all 17 teacher palettes without knowing
 * any of them -- a painted gradient in the page colour would need to know each one.
 *
 * It is NOT a control: no hover, no active, no disabled, no focus, no tab stop, and nothing for
 * check-tap-targets.js's CONTROLS list. It does not claim to be something you press. */
.lib-chipset.is-fade-end {
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - var(--lklib-chip-pad-x)), transparent 100%);
          mask-image: linear-gradient(to right, #000 0, #000 calc(100% - var(--lklib-chip-pad-x)), transparent 100%);
}
.lib-chipset.is-fade-start {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--lklib-chip-pad-x), #000 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--lklib-chip-pad-x), #000 100%);
}
.lib-chipset.is-fade-start.is-fade-end {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--lklib-chip-pad-x), #000 calc(100% - var(--lklib-chip-pad-x)), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 var(--lklib-chip-pad-x), #000 calc(100% - var(--lklib-chip-pad-x)), transparent 100%);
}

/* ── THE RESERVATION: ONE LINE PER FAMILY THAT CAN EVER RENDER (Julie's R3) ──────────────────
 * 🔴 A CLOSED FORM OFF A RENDER-TIME COUNT, NEVER A JS-MEASURED MAXIMUM. `data-lk-fam` is the
 * number of facet families that render chips in the All/All state, and that state is PROVABLY the
 * maximum: narrowing one family can only remove names from the other, never introduce one, so the
 * kept set is monotonically non-increasing. The one exception is the force-kept active chip, which
 * adds at most ONE CHIP to a family -- and under the nowrap rule above a chip cannot add a LINE.
 * Checked against all 170 reachable chip states on all 17 live payloads: no family ever gains a
 * row. Measuring the maximum instead would be wrong by construction (the All/All render is not the
 * largest chip COUNT) and would be layout-after-first-paint, which this module forbids.
 *
 * The sort control is deliberately NOT in this expression. It never varies, so it contributes its
 * own height and nothing has to know what that height is. */
.lib-all-chips[data-lk-fam="1"],
.lib-drawer-chips[data-lk-fam="1"] { min-height: var(--lklib-tap-min); }
.lib-all-chips[data-lk-fam="2"],
.lib-drawer-chips[data-lk-fam="2"] { min-height: calc(2 * var(--lklib-tap-min) + 8px); }
/* THE one delta at rest. A faint ground at the teacher's own ink value -- value, never hue. */
.lib-all-chip.is-series, .lib-drawer-chip.is-series {
  background: var(--lklib-chip-series-bg, rgba(127,127,127,0.10));
}
/* ...and it yields entirely to the shared active state, which is identical in both families.
 * 🔴 The ink is restated, not inherited. Caught by test_library_fullset.js: an accent FILL that
 * declares no colour inherits --lklib-ink, and on marybethlarue the accent IS her ink (#1d1916),
 * so the label would vanish into its own fill. Same defect class as the 1.14.4 invisible-sort bug.
 * This rule exists at all only because it sits later in the file than .is-active and would
 * otherwise win the background at equal specificity. */
.lib-all-chip.is-series.is-active, .lib-drawer-chip.is-series.is-active {
  background: var(--lklib-accent);
  border-color: var(--lklib-accent);
  color: var(--lklib-on-accent);
}

/* SPEC v2 RULE 3 — the series description in the ON-PAGE row header.
 * Without this the card-level suppression has nowhere to put the text, and a series behaving
 * correctly reads as a series with no description at all. */
.lib-row-note {
  display: block;
  font-family: var(--lklib-sans);
  font-size: 13px;
  line-height: var(--lklib-note-lh);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--lklib-ink-dim);
  max-width: 70ch;
  margin-top: 6px;
}

@media (max-width: 760px) {
  /* 1.21.0 -- the scrolling rules that used to live here are now unconditional (see .lib-chipset).
   * What stays is only what is genuinely phone-specific. `align-items: flex-start` and the label's
   * padding-top are GONE: they offset the label against a 33px row, and the scroller is 44px with
   * centred chips at every width now, so the base `align-items: center` on .lib-chiprow puts the
   * label on the chips' own centre line. Leaving them would have pushed the label 7px low here and
   * nowhere else -- a phone-only misalignment that no sweep of the on-page layout would catch. */
  .lib-row-note { font-size: 12.5px; }
}

/* The full library's own description slot, on the page. Same always-reserved behaviour as the
 * drawer's (Yarden chose always-reserve), so selecting or clearing a series never moves the grid. */
.lib-all-note {
  font-family: var(--lklib-sans);
  font-size: 13px;
  line-height: var(--lklib-note-lh);
  color: var(--lklib-ink-dim);
  max-width: 70ch;
  margin-top: 6px;
  min-height: calc(2em * var(--lklib-note-lh) + var(--lklib-note-more-gap));   /* one clamped line + the always-reserved `more` line. See .lib-drawer-note. */
}
.lib-all-note.is-empty { visibility: hidden; }
/* 1.19.9 -- the slot is reserved so selecting/clearing a series cannot move the grid. But when
 * a teacher has ONE series family the series chips collapse entirely (chipsMarkup returns ""
 * at facets.length < 2), so no chip exists that could ever fill it. Reserve where a series CAN
 * be selected; collapse where it cannot. Yarden's always-reserve ruling is untouched everywhere
 * a chip can fill the slot.
 *
 * 🔴 CORRECTED 1.22.0. This comment named "sophiejaffe, rytoast, andrew7sealy" as the pages with
 * a guaranteed void. Re-measured against live data by reading the emitted markup: 16 of 17
 * teachers render BOTH chip families, rytoast among them. The original figure came from a sweep
 * that had re-implemented chipsMarkup including a dead-facet filter 1.21.0 retired hours later --
 * §13 rule 19, failing silently. The JS twin of this comment was corrected in the fix commit and
 * this copy was not, so the module briefly carried a corrected claim and its uncorrected clone.
 * 🔴 ANDREW7SEALY IS THE ONLY PAGE WHOSE STATE CHANGES, and 1.22.0 ADDS a filled ~41px slot
 * there (he renders one family, so he gets a line) rather than removing a defect. */
.lib-all-note.is-nofill { min-height: 0; margin-top: 0; }

/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * 1.19.1 — Mackey c3, specified in his first review and not built in 1.19.0.
 *
 * "A series is a proper noun and is set like one; a style is a category label." Without this the
 * two families render identically and the ONLY separation is a ground tint he described as "near
 * the threshold of visibility on a light teacher palette". Typography does the work the tint was
 * only ever meant to hint at — and it costs nothing, survives monochrome, and cannot be broken by
 * a teacher's palette.
 * ══════════════════════════════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════════════════════════
 * 1.19.6 — ONE TYPE TREATMENT FOR BOTH CHIP FAMILIES (Yarden, 2026-10-11).
 *
 * "They look terribly different. The style chips font type is much larger in appearance and much
 * more dominant. I'd like them to be the same treatment."
 *
 * This REVERSES Mackey's c3 ("a series is a proper noun and a style is a category label"), and the
 * reversal is Yarden's call, recorded as his. The reasoning that 1.19.0-1.19.5 chased is what made
 * the case against it: the two families ended up differing on THREE axes at rest -- face, case and
 * ground -- and Mackey's own follow-up found they read as a difference in RANK rather than in kind,
 * with the row placed FIRST reading as the subordinate one. 1.19.2 normalised the painted
 * cap-height to fix that, and it did, but caps-vs-sentence still carries most of the dominance a
 * reader actually sees: an all-caps label paints cap-height on every letter.
 *
 * Mackey's own evidence also narrows what is lost. On donovan-mcgrath and myrah-penaloza the serif
 * and sans tokens resolve to the SAME face, and on jenna the STYLE chips are themselves a serif --
 * so on 3 of 17 pages the face already carried nothing and the distinction was "case plus ground
 * alone". What remains is the part that worked on every page: the two LABELLED rows, and the ground
 * tint he insisted was load-bearing precisely on those three.
 *
 * font-size-adjust goes with it. It existed only to reconcile two faces; with one face it is a
 * no-op that implies a variation the CSS no longer has.
 * ══════════════════════════════════════════════════════════════════════════════════════════════ */
.lib-all-chip, .lib-drawer-chip,
.lib-all-chip.is-series, .lib-drawer-chip.is-series,
.lib-all-chip:not(.is-series), .lib-drawer-chip:not(.is-series) {
  font-family: var(--lklib-sans);
  text-transform: none;
  letter-spacing: 0.2px;
  font-size-adjust: none;
}

/* MACKEY: the description belongs BELOW the title, full width — not beside it.
 * It rendered to the right of "Full library · 150 CLASSES · VIEW ALL" only because .lib-all-head
 * is a flex row; nobody chose that, the container did. His argument, which is the right one: it
 * looks tolerable at the one width it was screenshotted, and at a narrower window the row wraps
 * while the slot still reserves a fixed two lines — so the jump the reservation exists to prevent
 * comes back, at the widths where it is least forgivable. A description is not a sibling of a
 * count. Moving it also carries it clear of a teacher's own sticky bar, which on marc-holzman was
 * crossing it mid-scroll. */
.lib-all-head { flex-wrap: wrap; }
.lib-all-note { flex: 1 0 100%; order: 10; }
