/* ==========================================================================
   course-shared.css — components shared by Course Detail and Courses Listing

   ADAPTER LAYER. Hand-authored. The token files (vth-tokens.css, sectors.css)
   are generated/read-only; everything written by hand lives here.

   Phase 2 in progress. Sections carrying rules are built; the rest are still
   headings. INSTRUCTIONS §4 lists what each one covers; DESIGN-SCOPE.md §12 is
   the shared-with-the-listing contract.

   RULES FOR EVERY RULE ADDED BELOW
     - Scope: every selector starts inside `.vth`. Never a bare element selector.
       `.vth .vth-btn {}` is correct; `button {}` is a rejection.
     - Namespace: every class is `vth-` prefixed. Never `.btn`, `.card`, `.chip`,
       `.badge`, `.modal`, `.row`, `.col` — all collide with Materialize v0.100.
     - No literals: no hex, rgb, hsl or px outside the token files
       (INSTRUCTIONS §9.1). Reference var(--…).
     - No !important unless css-architecture.md §4's specificity ladder genuinely
       fails — and say why in a comment when it does.
     - Build each component ONCE, generically, with a documented parameter list.
       Do not tailor any of them to Course Detail; the listing consumes the same
       partials later.

   SECTOR THEMING — read before styling a button or a tag
     Only four things ever take sector colour: primary buttons, secondary buttons,
     the sector tag, the sector logo. They consume the ROLE tokens only:
       --vth-sector-primary / -hover / -focus / --vth-sector-on-primary
       --vth-sector-secondary / -hover / -focus / --vth-sector-on-secondary
       --vth-sector-tag-bg / --vth-sector-tag-text
     A component must NEVER reference a --vth-palette-* variable directly. If a
     design need cannot be expressed by those ten, that is a design-system gap:
     STOP and report it. Do not add an eleventh token here.
     (sector-theming.md §2.)

     NOTE: the sector SECONDARY role tokens are currently unconsumed on both
     course-detail pages — the second action uses the design system's `white`
     variant, not a sector secondary (PHASE-0-ANALYSIS.md R-16). Leave them wired
     but unused rather than deleting them; the listing may need them.

   NEVER SECTOR THEMED (sector-theming.md §6) — these must be pixel-identical
   across all nine sectors:
     - semantic colours: success, warning, error, info
     - format icon colours: Online green, Virtual light blue, At Your Venue yellow
       (they encode delivery method, not identity — a mixed listing must stay
       readable at a glance)
     - meaning-carrying badges: CPD yellow/brown, Level & Country light/dark blue,
       Competency purple
     - text, surface and border greys
     - the public footer, #262626 in every sector
     If a sector accent and a semantic colour collide visually, the SEMANTIC
     colour wins and the collision is reported as a design-system issue.
   ========================================================================== */


/* ---- tiny layout utilities used by the tab panels ---- */
.vth .vth-mb-4{margin-bottom:var(--space-4);}


/* ==========================================================================
   1. course-badge — CPD, Level, Country, Competency
   --------------------------------------------------------------------------
   Four themes. The category badge does NOT ship (PO ruling, PHASE-0-ANALYSIS
   §8.2 G-1) — do not add a fifth.
   These carry meaning — never sector themed.
   ========================================================================== */
.vth .vth-coursebadges{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center;}

/* Fixed HEIGHT, not vertical padding: a badge containing a flag <img> and one
   containing only text must come out the same height, and padding alone cannot
   guarantee that once an image raises the line box. */
.vth .vth-coursebadge{display:inline-flex;align-items:center;gap:var(--space-2);
  min-height:34px;padding:var(--space-2) var(--space-5);border-radius:var(--radius-pill);
  font-size:var(--type-size-xs);line-height:var(--type-leading-xxs);font-weight:var(--weight-medium);
  white-space:nowrap;}
.vth .vth-coursebadge--level{background:var(--badge-blue-bg);color:var(--badge-blue-ink);}
.vth .vth-coursebadge--country{background:var(--badge-countries-bg);color:var(--badge-countries-ink);}
.vth .vth-coursebadge--cpd{background:var(--badge-yellow-bg);color:var(--badge-yellow-ink);}
.vth .vth-coursebadge--competency{background:var(--badge-purple-bg);color:var(--badge-purple-ink);}

/* The competency badge is the one badge that DOES something — it opens the shared
   #competencyQuestionModal — so it ships as a <button>. vth-reset.css already strips
   the UA/Materialize button chrome inside the scope (`.vth button`), and the theme
   modifier above out-specifies it, so the resting state is pixel-identical to the
   sibling <span> badges. Everything here is the affordance the spans must NOT have.
   `border:0` because a <button> still carries the UA border in some engines even
   after the reset, which would make this badge 2px taller than its siblings. */
.vth .vth-coursebadge--action{border:0;cursor:pointer;font-weight:var(--weight-medium);
  font-family:inherit;letter-spacing:inherit;
  transition:background .15s ease,box-shadow .15s ease;}
.vth .vth-coursebadge--action:hover{background:var(--badge-purple-ink);color:var(--badge-purple-bg);}
/* Visible keyboard focus: it is a real control and must be reachable by tab. */
.vth .vth-coursebadge--action:focus-visible{outline:2px solid var(--badge-purple-ink);
  outline-offset:2px;}

/* Circle, cover-cropped: the stored flags have different aspect ratios, so without
   a fixed box each one sizes its badge differently. `.vth img{display:block}` in
   vth-reset.css already applies. */
.vth .vth-flag{border-radius:50%;object-fit:cover;flex-shrink:0;}


/* ==========================================================================
   2. format-icons — Online / Virtual / At Your Venue
   --------------------------------------------------------------------------
   Colours encode delivery method — never sector themed. See the KNOWN DIVERGENCE
   note in format-icons.blade.php: reports-v2 paints these three rotated by one and
   is deliberately left alone.
   ========================================================================== */
.vth .vth-fmtrow{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;}

/* Equal width and height with border-radius:50%, NOT a padded pill: the chips carry
   no label, and the three glyphs differ in advance width, so a padded pill came out
   three different widths. line-height:0 stops the glyph's own leading offsetting it
   inside the circle. */
.vth .vth-fmt{flex-shrink:0;border-radius:50%;display:inline-flex;align-items:center;
  justify-content:center;line-height:0;background:var(--surface-muted);color:var(--text-secondary);}
.vth .vth-fmt--1{background:var(--format-online-bg);color:var(--format-online-ink);}
.vth .vth-fmt--2{background:var(--format-venue-bg);color:var(--format-venue-ink);}
.vth .vth-fmt--3{background:var(--format-virtual-bg);color:var(--format-virtual-ink);}

/* ---- CSS tooltip -------------------------------------------------------------
   `.vth-tip[data-tip]`. No JS init, so it cannot fall out of sync with an ng-repeat
   re-render — the failure mode Materialize's .tooltipped has on paged-in rows.

   ACCEPTANCE CRITERION 10: never clipped by a card's stacking context. Two rules
   make that hold, and both matter:
     - the tooltip is a ::after on the chip itself, so there is no separate layer to
       be trapped by an ancestor's overflow;
     - the chip sets NO transform, filter, opacity<1 or will-change, because each of
       those would create a containing block and re-trap the tooltip. Position it
       with top/left only.
   Any ancestor that needs overflow:hidden must therefore not sit between the chip
   and the card edge — see the rail rules in course-detail.css. */
.vth .vth-tip{position:relative;}
.vth .vth-tip::after{content:attr(data-tip);position:absolute;bottom:calc(100% + 8px);left:50%;
  margin-left:-110px;width:220px;z-index:60;
  background:var(--surface-dark);color:var(--text-on-blue);
  padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
  font-size:var(--type-size-2xs);line-height:1.4;font-weight:var(--weight-regular);
  text-align:center;white-space:normal;
  opacity:0;visibility:hidden;transition:opacity .12s ease;pointer-events:none;
  box-shadow:var(--shadow-md);}
.vth .vth-tip:hover::after,.vth .vth-tip:focus-visible::after{opacity:1;visibility:visible;}
/* An empty data-tip would otherwise show an empty black box on hover. */
.vth .vth-tip[data-tip=""]::after{content:none;}


/* ==========================================================================
   3. price-statement — the ONLY place a price string is composed
   --------------------------------------------------------------------------
   Layout: eyebrow label / value / note. The composition rules live in
   price-statement.blade.php; this file only paints them.
   ========================================================================== */
.vth .vth-prices{display:flex;flex-direction:column;gap:var(--space-4);}
/* In the identity card the statements sit SIDE BY SIDE — one column per delivery
   type — so ONLINE and AT YOUR VENUE read as a comparison rather than a list. The
   rail keeps the stacked default, where width is the constraint. They still never
   blend: each is its own statement (§1.1), only the axis changes. */
.vth .vth-prices--row{flex-direction:row;flex-wrap:wrap;gap:var(--space-8);}
.vth .vth-prices--row .vth-price{flex:0 1 auto;min-width:0;}
.vth .vth-price{display:flex;flex-direction:column;gap:var(--space-1);}
/* MEASURED OFF THE DESIGN SOURCE, not a screenshot:
   `.claude/design-system/course-detail-reskin/Course Detail.dc.html` lines 1014-1041
   (rail) and 305-313 (hero). Both surfaces state the same block, so one spec serves
   them: label 12px/700/.04em, value 28px/600, note 13px. Every value lands on a token
   (--type-size-2xs, --type-size-title-5, --type-size-xxs), so nothing is hardcoded. */
.vth .vth-price__label{font-size:var(--type-size-2xs);line-height:1.4;
  font-weight:var(--weight-bold);letter-spacing:.04em;text-transform:uppercase;
  color:var(--text-muted);}
/* The value is the only thing here at display size — it is the answer to the
   question the block is asking, and it is never blended across formats. */
.vth .vth-price__value{font-size:var(--type-size-title-5);line-height:1.2;
  font-weight:var(--weight-semibold);color:var(--text-primary);}
.vth .vth-price__note{font-size:var(--type-size-xxs);line-height:1.5;color:var(--text-subtle);}


/* ==========================================================================
   4. share-popover
   --------------------------------------------------------------------------
   Anchored to its trigger with a pointing arrow — a popover, NOT a modal.
   Built on <details>/<summary>, so it opens, closes and takes keyboard focus with
   no JS at all.
   ========================================================================== */
.vth .vth-share{position:relative;display:inline-block;}
/* Strip the native disclosure triangle. list-style on the summary covers Firefox;
   ::-webkit-details-marker covers older WebKit. Both are needed. */
.vth .vth-share>summary{list-style:none;}
.vth .vth-share>summary::-webkit-details-marker{display:none;}
.vth .vth-share__trigger{display:inline-flex;align-items:center;justify-content:center;
  gap:var(--space-2);min-height:44px;padding:0 var(--space-4);border-radius:var(--radius-lg);
  cursor:pointer;font-family:var(--font-sans);font-size:var(--type-size-xs);
  font-weight:var(--weight-semibold);white-space:nowrap;
  background:var(--surface-card);color:var(--text-primary);
  border:1px solid var(--border-light);}
/* Icon-only variant, for surfaces with no room for the label. */
.vth .vth-share--icon .vth-share__trigger{width:44px;padding:0;border-radius:50%;}
.vth .vth-share__trigger:hover{background:var(--surface-muted);}
.vth .vth-share__trigger:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);}

.vth .vth-share__panel{position:absolute;top:calc(100% + 12px);z-index:70;width:380px;
  background:var(--surface-card);border:1px solid var(--border-light);
  border-radius:var(--radius-xl);box-shadow:var(--shadow-lg);
  padding:var(--space-6);display:flex;flex-direction:column;gap:var(--space-4);}
.vth .vth-share--end .vth-share__panel{right:0;}
.vth .vth-share--start .vth-share__panel{left:0;}
/* The pointer. A rotated square rather than a border triangle so it can carry the
   panel's own border and background and read as part of the panel. */
.vth .vth-share__panel::before{content:"";position:absolute;top:-6px;width:11px;height:11px;
  background:var(--surface-card);border-left:1px solid var(--border-light);
  border-top:1px solid var(--border-light);transform:rotate(45deg);}
.vth .vth-share--end .vth-share__panel::before{right:16px;}
.vth .vth-share--start .vth-share__panel::before{left:16px;}

/* Close, top right. Sized to the 44px touch target even though the glyph is 18px:
   it is a real control, and it sits next to nothing else that could absorb a miss. */
.vth .vth-share__close{position:absolute;top:var(--space-4);right:var(--space-4);
  width:44px;height:44px;display:inline-flex;align-items:center;justify-content:center;
  border:0;border-radius:50%;background:var(--surface-muted);color:var(--text-secondary);
  cursor:pointer;transition:background .15s ease,color .15s ease;}
.vth .vth-share__close:hover{background:var(--border-light);color:var(--text-primary);}
.vth .vth-share__close:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);}

/* Bigger than the old 12px label — in the reference it is the panel's heading, and
   the panel is now wide enough to carry it. padding-right keeps it clear of close. */
.vth .vth-share__title{font-size:var(--type-size-title-6);line-height:1.25;
  font-weight:var(--weight-semibold);color:var(--text-primary);
  padding-right:var(--space-10);}

/* Thumbnail + course name. Fixed thumb box so titles of any length line up. */
.vth .vth-share__course{display:flex;align-items:flex-start;gap:var(--space-4);}
.vth .vth-share__thumb{width:88px;height:66px;flex-shrink:0;object-fit:cover;
  border-radius:var(--radius-md);background:var(--surface-muted);}
.vth .vth-share__name{min-width:0;font-size:var(--type-size-base);line-height:1.35;
  font-weight:var(--weight-medium);color:var(--text-primary);}

/* Full width, centred, above the channels — the reference's emphasis: copying the
   link is the common case and the channels are the alternatives. */
.vth .vth-share__copybtn{display:flex;align-items:center;justify-content:center;
  gap:var(--space-3);width:100%;min-height:52px;padding:var(--space-3) var(--space-4);
  border:1px solid var(--border-light);border-radius:var(--radius-md);
  background:var(--surface-card);color:var(--text-primary);cursor:pointer;
  font-size:var(--type-size-base);font-weight:var(--weight-medium);font-family:inherit;
  transition:background .15s ease,border-color .15s ease;}
.vth .vth-share__copybtn:hover{background:var(--surface-muted);}
.vth .vth-share__copybtn:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);}
/* Two columns: six channels in three rows reads faster than a six-deep list. */
.vth .vth-share__channels{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--space-3);}
/* Each channel is its own bordered card in the reference, not a bare list row.
   text-align:left because the <button> variant (the Email override) would otherwise
   centre its label and sit out of line with the <a> rows beside it. */
.vth .vth-share__ch{display:flex;align-items:center;gap:var(--space-3);
  min-height:52px;padding:var(--space-3) var(--space-4);
  border:1px solid var(--border-light);border-radius:var(--radius-md);
  background:var(--surface-card);font-family:inherit;text-align:left;cursor:pointer;
  font-size:var(--type-size-base);font-weight:var(--weight-medium);
  color:var(--text-primary);text-decoration:none;
  transition:background .15s ease,border-color .15s ease;}
.vth .vth-share__ch:hover{background:var(--surface-muted);color:var(--text-primary);}
.vth .vth-share__ch:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);}
/* Monochrome, per DESIGN-SCOPE §4.1 — brand colours here would turn a tidy list
   into a pile of logos and pull the eye away from the copy-link row. */
.vth .vth-share__mark{display:inline-flex;width:20px;height:20px;flex-shrink:0;color:var(--text-secondary);}
/* The mark is a FONT GLYPH now, not an inline <svg> — the old `svg{width:100%}` rule
   had nothing left to match. vth/components/icon sizes its own box inline, so this
   only has to supply the ink. */
.vth .vth-share__mark .vth-icon{color:inherit;}

/* OFF-SCREEN, NOT display:none. The clipboard fallback has to select() this input
   on insecure origins (staging is plain HTTP), and a display:none or
   visibility:hidden input cannot be selected. Kept 1px rather than zero-sized
   because some engines refuse selection on a zero-area box.

   THE `input` IN THE SELECTOR IS LOAD-BEARING — do not simplify it to
   `.vth .vth-share__url`. Materialize 0.100 ships

       input[type=text]:not(.browser-default){ width:100%; ... }

   which is specificity (0,2,1): type + attribute + :not(class). A two-class
   selector is only (0,2,0), so MATERIALIZE WINS and the input renders full width —
   measured at 378px inside the panel, i.e. visible, despite position/left/opacity
   all applying from this same rule. Adding the element type makes this (0,2,1) too,
   and course-shared.css loads after Materialize, so the tie breaks our way.

   This is why the file's other rules that touch a form control also name the
   element. It is NOT a candidate for !important — this file has none. */
.vth input.vth-share__url{position:absolute;width:1px;height:1px;padding:0;border:0;
  opacity:0;pointer-events:none;left:-9999px;top:0;}
.vth .vth-share__note{font-size:var(--type-size-2xs);line-height:1.45;color:var(--text-subtle);}

@media (max-width:420px){
  /* The panel is wider than the gutter allows; pin it to the trigger's edge and
     let it use the full width rather than hanging off-screen. */
  .vth .vth-share__panel{width:min(380px,calc(100vw - 32px));padding:var(--space-4);}
  .vth .vth-share__close{top:var(--space-3);right:var(--space-3);}
  /* One column below 420px: two 52px cards plus the gap no longer fit the gutter. */
  .vth .vth-share__channels{grid-template-columns:minmax(0,1fr);}
}


/* ==========================================================================
   5. course-card — listing card, consumed here by the Popular courses rail
   --------------------------------------------------------------------------
   The Blade is SHARED with reports-v2 (vth/components/course-card.blade.php, two
   render modes, one data contract). The SKIN is not: reports-v2 keeps its own in
   vth-reports.css and is untouched by this file, which Course Detail alone loads.
   The reasoning is in PHASE-0-ANALYSIS.md §12.2 — the two surfaces genuinely want
   different treatments (full-width list row vs 360px rail card) and currently
   disagree on the format colours, so forcing one skin would have changed a shipped
   page. Same classes, same DOM, different paint.

   WHY THE CARD KEEPS ITS OWN PILL AND CHIP CLASSES (.vth-cc, .vth-cpvmode) RATHER
   THAN REUSING .vth-coursebadge / .vth-fmt FROM SECTIONS 1 AND 2:
   those two components are PHP partials taking PHP data. The card's Angular branch
   renders from an ng-repeat row and cannot @include them, so reusing their classes
   would mean one branch composing partials and the other hand-rolling the same
   markup — the drift this module is trying to avoid. The card owns its internals in
   both branches; the TOKENS are shared, so it still looks like one system.

   Rail sizing is driven by a CONTAINER class, `.vth-cpv--rail`, never a media
   query: the rail is 360px wide inside a 1920px viewport, so a breakpoint cannot
   tell it apart from a full-width list.
   ========================================================================== */
.vth .vth-cpv{display:flex;flex-direction:column;gap:var(--space-4);}
.vth .vth-cpvcard{position:relative;display:flex;gap:var(--space-4);
  background:var(--surface-card);border:1px solid var(--border-light);
  border-radius:var(--radius-xl);padding:var(--space-4);box-shadow:var(--shadow-sm);}
/* Only the MEDIA clips, never a wrapper: an overflow:hidden between a format chip
   and the card edge would trap its tooltip (see the tooltip note in section 2). */
.vth .vth-cpvcard__img{position:relative;width:200px;flex-shrink:0;aspect-ratio:16/9;
  border-radius:var(--radius-lg);overflow:hidden;
  background:var(--surface-muted) center/cover no-repeat;}
.vth .vth-cpvcard__sec{position:absolute;left:10px;top:10px;
  background:var(--vth-sector-tag-bg);color:var(--vth-sector-tag-text);
  border-radius:var(--radius-pill);padding:5px 12px;
  font-size:var(--type-size-2xs);font-weight:var(--weight-medium);line-height:1.2;}
/* Mandatory, bottom-left of the media (DESIGN-SCOPE §5). Semantic, never sector
   themed — it means the same thing on every sector's site. */
.vth .vth-cpvcard__flag{position:absolute;left:10px;bottom:10px;
  background:var(--badge-yellow-bg);color:var(--badge-yellow-ink);
  border-radius:var(--radius-pill);padding:5px 12px;
  font-size:var(--type-size-2xs);font-weight:var(--weight-semibold);line-height:1.2;}
.vth .vth-cpvcard__fav{position:absolute;right:10px;top:10px;
  width:32px;height:32px;border-radius:50%;display:inline-flex;align-items:center;
  justify-content:center;background:var(--fav-scrim);color:var(--fav-heart);cursor:pointer;}
.vth .vth-cpvcard__fav.is-on{color:var(--fav-heart-selected);}
.vth .vth-cpvcard__bd{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--space-2);}
.vth .vth-cpvcard__bd h4{font-size:var(--type-size-lg);line-height:1.35;font-weight:var(--weight-semibold);}
.vth .vth-cpvcard__bd p{font-size:var(--type-size-xs);line-height:1.5;color:var(--text-secondary);}

/* The whole card opens the course, but only the TITLE is the anchor — the card also
   holds a favourite button and the format chips, and nesting those inside an <a> is
   invalid HTML and unreachable by keyboard. The title stretches its hit area with an
   absolutely-positioned ::after; the controls sit above it on z-index. */
.vth .vth-cpvcard__title{color:inherit;text-decoration:none;}
.vth .vth-cpvcard__title:hover{color:var(--vth-blue);text-decoration:underline;}
.vth .vth-cpvcard__title::after{content:"";position:absolute;inset:0;border-radius:var(--radius-xl);z-index:0;}
.vth .vth-cpvcard__title:focus-visible{outline:none;}
.vth .vth-cpvcard__title:focus-visible::after{box-shadow:var(--shadow-focus-blue);}
.vth .vth-cpvcard__fav,.vth .vth-cpvcard__modes,.vth .vth-cpvcard__sec,
.vth .vth-cpvcard__flag{z-index:1;}

.vth .vth-cpvcard__tags{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center;}
/* Fixed height so a pill holding a flag <img> matches one holding only text. */
.vth .vth-cc{display:inline-flex;align-items:center;gap:6px;min-height:26px;padding:var(--space-1) 10px;
  font-size:var(--type-size-2xs);line-height:1;font-weight:var(--weight-medium);
  border-radius:var(--radius-pill);white-space:nowrap;}
.vth .vth-cc--b{background:var(--badge-blue-bg-2);color:var(--badge-blue-ink);}
.vth .vth-cc--y{background:var(--badge-yellow-bg);color:var(--badge-yellow-ink);}
.vth .vth-cc--g{background:var(--badge-green-bg);color:var(--badge-green-ink);}
.vth .vth-cc--n{background:var(--badge-grey-bg);color:var(--badge-grey-ink);}
.vth .vth-cc__flag{width:14px;height:14px;object-fit:cover;border-radius:50%;flex-shrink:0;}

.vth .vth-cpvcard__layers{display:flex;align-items:center;gap:6px;
  font-size:var(--type-size-2xs);color:var(--text-muted);}

.vth .vth-cpvcard__modes{display:flex;align-items:center;gap:var(--space-2);
  margin-top:auto;flex-wrap:wrap;}
/* Square box + border-radius:50%, matching .vth-fmt in section 2 — the chips carry
   no label and the glyphs differ in advance width, so a padded pill comes out three
   different sizes. */
.vth .vth-cpvmode{width:30px;height:30px;flex-shrink:0;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;line-height:0;
  background:var(--surface-muted);color:var(--text-secondary);}
/* The CORRECT mapping, from the design tokens — reports-v2 has these rotated by
   one and is deliberately left alone. See PHASE-0-ANALYSIS.md §12.1. */
.vth .vth-cpvmode--1{background:var(--format-online-bg);color:var(--format-online-ink);}
.vth .vth-cpvmode--2{background:var(--format-venue-bg);color:var(--format-venue-ink);}
.vth .vth-cpvmode--3{background:var(--format-virtual-bg);color:var(--format-virtual-ink);}

/* ---- Rail variant: stacked, 360px column. A CONTAINER class, not a breakpoint. ---- */
.vth .vth-cpv--rail{gap:var(--space-3);}
/* FULL-BLEED MEDIA, PADDED BODY — the reference's rail card. The base card is the
   other way round (padding on the card, none on the body), which insets the image and
   leaves a visible margin on all four sides.

   The card keeps overflow:visible and the MEDIA still does the clipping, per the base
   rule above: an overflow:hidden wrapper would trap a format chip's tooltip. Here the
   media simply takes the card's own top corners instead. -1px because the card has a
   1px border, so its INNER radius is one pixel smaller — without it the image pokes
   past the border at the two top corners.

   Scoped to .vth-cpv--rail, so the catalogue and reports-v2 keep the inset card. */
/* NO RESTING SHADOW here, unlike the base card. The source gives this card a border
   and nothing else at rest (line 1138), raising it only on hover — and the base's
   --shadow-sm at rest made the hover lift almost invisible, because sm and md differ
   by very little. Flat to raised is the affordance. */
.vth .vth-cpv--rail .vth-cpvcard{flex-direction:column;gap:0;padding:0;cursor:pointer;
  box-shadow:none;transition:box-shadow .15s ease;}
/* HOVER, from the design source (line 1138: style-hover="box-shadow:0 4px 6px -1px
   rgba(0,0,0,.1)"). --shadow-md's first layer IS that value, so the token is used
   rather than the literal. :focus-within gives the keyboard the same affordance — the
   card is reached by tabbing to the title link inside it, and a pointer-only hover
   would leave that journey with no feedback at all. */
.vth .vth-cpv--rail .vth-cpvcard:hover,
.vth .vth-cpv--rail .vth-cpvcard:focus-within{box-shadow:var(--shadow-md);}
/* FIXED 130px, not the base card's 16/9. aspect-ratio must be cleared explicitly:
   leaving it set alongside a definite height is the exact conflict that made the
   identity-card media resolve its WIDTH backwards through the ratio and overflow its
   column. Width here is definite (the grid track), so height simply applies. */
.vth .vth-cpv--rail .vth-cpvcard__img{width:100%;aspect-ratio:auto;height:130px;
  border-radius:calc(var(--radius-xl) - 1px) calc(var(--radius-xl) - 1px) 0 0;}
.vth .vth-cpv--rail .vth-cpvcard__bd{padding:var(--space-4);gap:var(--space-1);}
/* Green here, not the identity card's yellow: the reference uses green for this badge
   on the rail cards. Scoped, so the hero's Mandatory flag is untouched. Flagged for
   the PO as a deliberate difference between the two surfaces, not an accident. */
.vth .vth-cpv--rail .vth-cpvcard__flag{background:var(--badge-green-bg);
  color:var(--badge-green-ink);}
.vth .vth-cpv--rail .vth-cpvcard__bd h4{font-size:var(--type-size-sm);line-height:1.35;}
/* The rail card is a pointer to another course, not a place to read one. The
   reference reduces it to: image, sector tag + flag on the image, title, and ONE
   plain meta line ("Level 2 · 60 CPD minutes"). So the excerpt, the Educational
   Layers line and the format chips are dropped, and the badge pills become that
   meta line — pills at 360px wrap to three rows and turn a pointer into a block. */
.vth .vth-cpv--rail .vth-cpvcard__bd p:not(.vth-cpvcard__layers){display:none;}
.vth .vth-cpv--rail .vth-cpvcard__layers{display:none;}
.vth .vth-cpv--rail .vth-cpvcard__modes{display:none;}
.vth .vth-cpv--rail .vth-cpvcard__tags{gap:0;font-size:var(--type-size-2xs);color:var(--text-subtle);}
.vth .vth-cpv--rail .vth-cc{background:transparent;color:var(--text-subtle);
  padding:0;min-height:0;font-weight:var(--weight-regular);}
/* A middot between the remaining meta items, drawn rather than marked up so the
   shared card's DOM is untouched. */
.vth .vth-cpv--rail .vth-cc + .vth-cc::before{content:"·";margin:0 var(--space-2);color:var(--text-muted);}
.vth .vth-cpv--rail .vth-cc__flag{display:none;}

/* Below 760px the full-width card stacks too — here a media query IS right, because
   at that width the viewport genuinely is the constraint. */
@media (max-width:760px){
  .vth .vth-cpvcard{flex-direction:column;}
  .vth .vth-cpvcard__img{width:100%;}
}


/* ==========================================================================
   6. state-block — loading / empty / filtered-empty / error
   --------------------------------------------------------------------------
   FOUR visually distinct variants. The distinction is carried by the ICON TINT,
   not by the layout, so the four read as one family while still being
   tellable apart at a glance.
   ========================================================================== */
.vth .vth-state{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--space-2);padding:var(--space-10) var(--space-6);text-align:center;}
.vth .vth-state__icon{display:inline-flex;align-items:center;justify-content:center;
  width:52px;height:52px;border-radius:50%;margin-bottom:var(--space-2);
  background:var(--surface-muted);color:var(--text-muted);}
.vth .vth-state__title{font-size:var(--type-size-lg);line-height:1.4;
  font-weight:var(--weight-semibold);color:var(--text-primary);}
/* A readable measure. Centred text past ~52 characters a line is hard to track
   back to the start of the next one. */
.vth .vth-state__body{font-size:var(--type-size-xs);line-height:1.55;
  color:var(--text-subtle);max-width:42ch;}
.vth .vth-state__chips{display:flex;flex-wrap:wrap;gap:var(--space-2);justify-content:center;
  margin-top:var(--space-2);}
.vth .vth-state--filtered-empty .vth-state__icon{background:var(--surface-blue-soft);color:var(--vth-blue);}
.vth .vth-state--error .vth-state__icon{background:var(--surface-error-soft);color:var(--status-error);}
.vth .vth-state .vth-btn{margin-top:var(--space-4);}

/* ---- loading skeletons ----
   They mirror the target layout, so nothing jumps when the content lands.
   prefers-reduced-motion stops the shimmer for anyone who has asked it to; the
   skeleton still reads as a placeholder without it. */
.vth .vth-state--loading{padding:0;gap:var(--space-4);align-items:stretch;}
.vth .vth-skel-row{display:flex;gap:var(--space-4);align-items:flex-start;}
.vth .vth-skel-lines{display:flex;flex-direction:column;gap:var(--space-2);flex:1;min-width:0;}
.vth .vth-skel{display:block;border-radius:var(--radius-sm);
  background:linear-gradient(90deg,var(--surface-muted) 25%,var(--border-light) 37%,var(--surface-muted) 63%);
  background-size:400% 100%;animation:vthSkelShimmer 1.4s ease infinite;}
.vth .vth-skel--media{width:96px;height:72px;border-radius:var(--radius-lg);flex-shrink:0;}
.vth .vth-skel--title{height:16px;width:62%;}
.vth .vth-skel--text{height:12px;width:100%;}
.vth .vth-skel--short{width:44%;}
@keyframes vthSkelShimmer{0%{background-position:100% 50%;}100%{background-position:0 50%;}}
@media (prefers-reduced-motion:reduce){
  .vth .vth-skel{animation:none;background:var(--surface-muted);}
}

/* Visually hidden, still announced. The loading state has no visible text, so
   without this a screen reader is told nothing is happening. */
.vth .vth-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;}


/* ==========================================================================
   6b. state-notice — the banners (DESIGN-SCOPE §10.2)
   --------------------------------------------------------------------------
   Semantic tones only, never sector themed: a declined request is red on every
   sector's site. The tone is carried by the icon and a left rule rather than a
   full colour wash — a page can show two notices at once (a venue-only course to
   a learner whose request was declined) and two full-bleed coloured blocks
   stacked read as an error state rather than as information.
   ========================================================================== */
.vth .vth-notice{display:flex;gap:var(--space-3);align-items:flex-start;
  padding:var(--space-4);border-radius:var(--radius-lg);
  border:1px solid var(--border-light);border-left-width:4px;
  background:var(--surface-card);}
.vth .vth-notice + .vth-notice{margin-top:var(--space-3);}
/* Inside the Learning Outcome tab the notices follow CMS-authored prose, which ends
   flush; without this they sit on top of the last paragraph. */
.vth .vth-prose .vth-notice{margin-top:var(--space-5);}
.vth .vth-notice__icon{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;}
.vth .vth-notice__copy{min-width:0;display:flex;flex-direction:column;gap:var(--space-1);}
.vth .vth-notice__title{font-size:var(--type-size-xs);font-weight:var(--weight-semibold);color:var(--text-primary);}
.vth .vth-notice__body{font-size:var(--type-size-2xs);line-height:1.55;color:var(--text-subtle);}
.vth .vth-notice--info{border-left-color:var(--vth-blue);background:var(--surface-blue-soft);}
.vth .vth-notice--info .vth-notice__icon{color:var(--vth-blue);}
.vth .vth-notice--success{border-left-color:var(--status-success);background:var(--surface-success-soft);}
.vth .vth-notice--success .vth-notice__icon{color:var(--status-success);}
.vth .vth-notice--warning{border-left-color:var(--status-warning);background:var(--surface-warning-soft);}
.vth .vth-notice--warning .vth-notice__icon{color:var(--status-warning);}
.vth .vth-notice--error{border-left-color:var(--status-error);background:var(--surface-error-soft);}
.vth .vth-notice--error .vth-notice__icon{color:var(--status-error);}


/* ==========================================================================
   7. confirm-dialog
   --------------------------------------------------------------------------
   Gates every destructive or credit-consuming action and NAMES THE CREDIT IMPACT.
   Our own, not Materialize's .modal — see confirm-dialog.blade.php.
   ========================================================================== */
.vth .vth-confirm{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;
  justify-content:center;padding:var(--space-4);}
/* NOTE: `display:flex` here would beat the `hidden` attribute this dialog ships with —
   the UA's [hidden] rule loses to any author declaration. vth-reset.css now guards that
   for the whole scope; do not remove it. */
/* Above Materialize's .modal (z-index 1003) and its overlay (1002), so a confirm
   raised from inside a legacy modal is not painted behind it. */
.vth .vth-confirm__backdrop{position:absolute;inset:0;background:var(--scrim-backdrop);}
.vth .vth-confirm__panel{position:relative;width:100%;max-width:440px;
  background:var(--surface-card);border-radius:var(--radius-xl);
  box-shadow:var(--shadow-lg);padding:var(--space-8) var(--space-6) var(--space-6);
  display:flex;flex-direction:column;align-items:center;gap:var(--space-2);text-align:center;}
.vth .vth-confirm__icon{display:inline-flex;align-items:center;justify-content:center;
  width:52px;height:52px;border-radius:50%;margin-bottom:var(--space-2);}
.vth .vth-confirm__icon--primary{background:var(--surface-blue-soft);color:var(--vth-blue);}
.vth .vth-confirm__icon--danger{background:var(--surface-error-soft);color:var(--status-error);}
.vth .vth-confirm__title{font-size:var(--type-size-xl);line-height:1.3;
  font-weight:var(--weight-semibold);color:var(--text-primary);}
.vth .vth-confirm__body{font-size:var(--type-size-xs);line-height:1.55;color:var(--text-subtle);max-width:42ch;}
/* The credit impact. Given its own ground because it is the one line a person
   must not skim past — it is what the action costs. */
.vth .vth-confirm__meta{margin-top:var(--space-2);padding:var(--space-2) var(--space-3);
  border-radius:var(--radius-md);background:var(--surface-muted);
  font-size:var(--type-size-2xs);line-height:1.45;font-weight:var(--weight-medium);color:var(--text-secondary);}
.vth .vth-confirm__actions{display:flex;gap:var(--space-3);margin-top:var(--space-5);
  width:100%;justify-content:center;flex-wrap:wrap;}
.vth .vth-confirm__actions .vth-btn{flex:1 1 140px;max-width:200px;}
/* Set while a dialog is open; stops the page scrolling behind it. */
body.vth-noscroll{overflow:hidden;}


/* ==========================================================================
   8. select-search — searchable multi-select
   --------------------------------------------------------------------------
   Over a real <select>, which stays the source of truth. See
   select-search.blade.php for why that matters.
   ========================================================================== */
.vth .vth-selectsearch{position:relative;display:flex;flex-direction:column;gap:var(--space-1);}
.vth .vth-selectsearch__trigger{display:flex;align-items:center;justify-content:space-between;
  gap:var(--space-2);width:100%;height:40px;padding:0 var(--space-3);cursor:pointer;
  background:var(--surface-card);border:1px solid var(--border-light);
  border-radius:var(--radius-md);box-shadow:var(--shadow-xs);
  font-size:var(--type-size-xs);color:var(--text-primary);text-align:left;}
.vth .vth-selectsearch__trigger:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);}
.vth .vth-selectsearch__summary{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* A CSS chevron — two borders rotated. No glyph, so nothing to render as a box. */
.vth .vth-selectsearch__chev{width:8px;height:8px;flex-shrink:0;margin-top:-4px;
  border-right:1.5px solid var(--text-muted);border-bottom:1.5px solid var(--text-muted);
  transform:rotate(45deg);transition:transform .15s ease;}
.vth .vth-selectsearch__trigger[aria-expanded="true"] .vth-selectsearch__chev{transform:rotate(-135deg);margin-top:2px;}

.vth .vth-selectsearch__panel{position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:70;
  background:var(--surface-card);border:1px solid var(--border-light);
  border-radius:var(--radius-md);box-shadow:var(--shadow-lg);overflow:hidden;}
.vth .vth-selectsearch__searchwrap{padding:var(--space-2);border-bottom:1px solid var(--border-light);}
.vth .vth-selectsearch__search{width:100%;height:34px;padding:0 var(--space-3);
  border:1px solid var(--border-light);border-radius:var(--radius-sm);
  background:var(--surface-page);font-size:var(--type-size-xs);color:var(--text-primary);}
/* The ONLY scroller in this component, and it is bounded: a long learner list has
   to scroll somewhere, and a panel that grows past the viewport cannot be closed. */
.vth .vth-selectsearch__list{max-height:240px;overflow-y:auto;padding:var(--space-1);}
.vth .vth-selectsearch__opt{padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);
  font-size:var(--type-size-xs);color:var(--text-primary);cursor:pointer;}
.vth .vth-selectsearch__opt:hover{background:var(--surface-muted);}
.vth .vth-selectsearch__opt:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);}
.vth .vth-selectsearch__opt.is-on{background:var(--surface-blue-soft);color:var(--vth-blue);font-weight:var(--weight-medium);}
.vth .vth-selectsearch__nomatch{padding:var(--space-4);text-align:center;
  font-size:var(--type-size-2xs);color:var(--text-subtle);}


/* ==========================================================================
   9. action-row — measured overflow-to-kebab
   --------------------------------------------------------------------------
   The fold point is measured in course-detail.js, never a breakpoint.
   ========================================================================== */
.vth .vth-actionrow{display:flex;align-items:center;gap:var(--space-2);min-width:0;}
/* nowrap + overflow:hidden is what MAKES the fit test measurable: it lets
   scrollWidth exceed clientWidth so the JS can see the overflow. Without it the
   row would wrap and always appear to fit. min-width:0 stops a flex child
   refusing to shrink below its content width. */
.vth .vth-actionrow__items{display:flex;align-items:center;gap:var(--space-2);
  flex:1;min-width:0;flex-wrap:nowrap;overflow:hidden;}
.vth .vth-actionrow__item{flex-shrink:0;}
.vth .vth-actionrow__overflow{position:relative;flex-shrink:0;}
.vth .vth-kebab{display:inline-flex;flex-direction:column;gap:3px;align-items:center;justify-content:center;}
.vth .vth-kebab i{width:4px;height:4px;border-radius:50%;background:currentColor;display:block;}
.vth .vth-actionrow__menu{position:absolute;top:calc(100% + 6px);right:0;z-index:70;min-width:220px;
  background:var(--surface-card);border:1px solid var(--border-light);
  border-radius:var(--radius-md);box-shadow:var(--shadow-lg);padding:var(--space-1);
  display:flex;flex-direction:column;gap:2px;}
/* Folded actions become menu rows, not buttons in a stack — a column of pills
   inside a 220px menu reads as a toolbar that has fallen over. */
.vth .vth-actionrow__menuitem{display:flex;align-items:center;gap:var(--space-2);
  width:100%;height:auto;padding:var(--space-2) var(--space-3);border:none;background:none;
  border-radius:var(--radius-sm);cursor:pointer;text-align:left;box-shadow:none;
  font-family:var(--font-sans);font-size:var(--type-size-xs);font-weight:var(--weight-medium);
  color:var(--text-primary);text-decoration:none;white-space:nowrap;}
.vth .vth-actionrow__menuitem:hover{background:var(--surface-muted);color:var(--text-primary);}

/* An inert control is a REAL enabled button (see action-row.blade.php) — it must
   LOOK unavailable while staying clickable, so it can explain itself. Hence
   opacity and a not-allowed cursor, never pointer-events:none. */
.vth .vth-btn.is-inert{opacity:.5;cursor:not-allowed;}

/* The `white` variant: the design system's second action. Not in vth-reskin.css
   because that file predates it; it is this module's second-most-used button and
   the public page's Request Online Training Price. */
.vth .vth-btn--white{background:var(--btn-white-bg);color:var(--btn-white-ink);
  border-color:var(--border-light);box-shadow:var(--shadow-xs);}
.vth .vth-btn--white:hover{background:var(--surface-muted);color:var(--btn-white-ink);}

/* The soft-blue variant. In the reference, Request Call Back is neither the primary
   action nor a plain white one — it is a tinted tertiary, which is what keeps a
   three-button row from reading as three equal choices. */
.vth .vth-btn--soft{background:var(--btn-secondary-blue-bg);color:var(--vth-blue);
  border-color:transparent;box-shadow:none;}
.vth .vth-btn--soft:hover{background:var(--surface-blue-soft);color:var(--vth-blue-focus);}

/* The soft-GREY variant. Evaluate Course had this before the reskin — the theme's
   `.btn.grey-btn{color:var(--secondary-color);background:var(--tertiary-color)}`,
   i.e. brand-blue text on #F5F5F5 — and it should keep it: it is an
   already-in-your-account housekeeping action, not a transaction, so it must not
   compete with Add To Account or Allocate.

   The tokens were already there and unused: --btn-secondary-grey-bg is #F6F6F6, the
   grey counterpart of --btn-secondary-blue-bg that --soft uses. Nothing was invented;
   this variant is the one that was missing from the pair. */
.vth .vth-btn--grey{background:var(--btn-secondary-grey-bg);color:var(--vth-blue);
  border-color:transparent;box-shadow:none;}
.vth .vth-btn--grey:hover{background:var(--border-light);color:var(--vth-blue-focus);}

/* The live region that answers "why is this disabled?" and confirms a copy.
   Appended to <body>, which carries the scope class, so it is a descendant of
   .vth and is scoped like every other rule here. */
.vth .vth-live{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:1300;
  max-width:min(420px,calc(100vw - 32px));
  background:var(--surface-dark);color:var(--text-on-blue);
  padding:var(--space-3) var(--space-4);border-radius:var(--radius-md);
  box-shadow:var(--shadow-lg);font-family:var(--font-sans);
  font-size:var(--type-size-xs);line-height:1.45;text-align:center;
  opacity:0;visibility:hidden;transition:opacity .15s ease;pointer-events:none;}
.vth .vth-live.is-visible{opacity:1;visibility:visible;}


/* ==========================================================================
   10. read-more — measured two-line truncation
   --------------------------------------------------------------------------
   The clamp is applied by JS (`is-clamped`), never by default, so a no-JS visitor
   gets the full text rather than two lines and no way out. See read-more.blade.php.
   ========================================================================== */
.vth .vth-readmore{display:flex;flex-direction:column;align-items:flex-start;gap:var(--space-1);}
.vth .vth-readmore__text{font-size:var(--type-size-xs);line-height:1.55;color:var(--text-secondary);}
/* -webkit-line-clamp is the only way to clamp to a LINE COUNT rather than a
   height, and it is supported everywhere this app runs. The line count comes from
   the partial as a custom property, so the same component can clamp to three
   lines elsewhere without a second class. NO fixed height — acceptance criterion
   13 forbids fixed-height text containers, and a clamp does not need one. */
.vth .vth-readmore.is-clamped .vth-readmore__text{
  display:-webkit-box;-webkit-box-orient:vertical;
  -webkit-line-clamp:var(--vth-rm-lines,2);overflow:hidden;}
.vth .vth-readmore__toggle{padding:0;background:none;border:none;cursor:pointer;
  font-family:var(--font-sans);font-size:var(--type-size-xs);font-weight:var(--weight-medium);
  color:var(--text-link);text-decoration:none;}
.vth .vth-readmore__toggle:hover{text-decoration:underline;}
.vth .vth-readmore__toggle:focus-visible{outline:none;box-shadow:var(--shadow-focus-blue);border-radius:var(--radius-xs);}


/* ==========================================================================
   11. sector-tag
   --------------------------------------------------------------------------
   Consumes --vth-sector-tag-bg / --vth-sector-tag-text and NOTHING else — never a
   --vth-palette-* variable. With no [data-vth-sector] on <body> those resolve to
   the brand defaults in sectors.css section 0/2, so an unknown sector renders a
   brand-blue tag rather than an unstyled one.
   ========================================================================== */
.vth .vth-sectortag{display:inline-flex;align-items:center;min-height:32px;padding:var(--space-1) var(--space-4);
  border-radius:var(--radius-pill);font-size:var(--type-size-xxs);line-height:1;
  font-weight:var(--weight-medium);white-space:nowrap;
  background:var(--vth-sector-tag-bg);color:var(--vth-sector-tag-text);}


/* ==========================================================================
   12. sector-logo — shared asset (nation-flag lives with the badges, section 1)
   ========================================================================== */
.vth .vth-sectorlogo{display:inline-flex;align-items:center;justify-content:center;
  border-radius:50%;overflow:hidden;flex-shrink:0;
  background:var(--surface-card);box-shadow:0 0 0 2px var(--vth-sector-tag-bg);}
.vth .vth-sectorlogo img{width:100%;height:100%;object-fit:contain;}

/* ==========================================================================
   LOADING, ERROR AND EMPTY STATES — the four content states from the design
   source (`Course Detail.dc.html`, lines 338-372: loading / errorState /
   emptyState / ready).

   WHY THESE EXIST, AND WHERE THEY MAY BE USED.

   An earlier attempt at this put a skeleton over the SERVER-RENDERED body of
   the public page. It made things worse, measurably: CLS went from 0.8988 to
   1.4843, because the markup was already in the HTML and the skeleton was a
   second layout that had to be torn down. The real cause of the flicker there
   was an ng-cloak hiding a fully-rendered section for 2.4s; removing it took
   CLS to 0.0033 and the skeleton was deleted.

   The rule that came out of that: A SKELETON IS ONLY EVER CORRECT WHERE THE
   REGION IS GENUINELY EMPTY WHILE A REQUEST IS IN FLIGHT. On this module that
   is exactly one place — the platform page's tab panels, which
   `getSectionHTML()` blanks (`$scope.admin_content = ''`) before its $http
   round-trip and refills afterwards. Everything else on both pages is
   server-rendered and must never be masked.

   The shimmer itself is NOT defined here. components/vth-loading.css already
   ships `.vth-skeleton` and the in-button spinner, and it is reused verbatim
   so this page's loading affordances are the same object the certificates
   module uses. Only the ARRANGEMENT is this module's business.
   ========================================================================== */

/* --- Entrance -------------------------------------------------------------
   `vthRise` in the design source (line 25): opacity 0 -> 1 over an 8px lift,
   .14s-.18s ease-out. The source uses it for every surface that appears after
   a state change — popovers, drawers, modals, toasts — and it is what stops a
   panel swap reading as a flicker: the eye is given a direction to follow
   instead of a frame that simply changes. */
@keyframes vth-rise{ from{ opacity:0; transform:translateY(8px); } to{ opacity:1; transform:none; } }
.vth .vth-rise{ animation:vth-rise .18s ease-out both; }

/* The public page's tabs are CSS radios, so switching panels is instantaneous
   and there is nothing to load. It still SNAPPED, which is the "fluctuation"
   this rule answers — the panel now rises in. Driven off :checked so it costs
   no JavaScript and replays on every switch. */
.vth #vth-tab-outcome:checked ~ .vth-coursetabs__panel--outcome,
.vth #vth-tab-curriculum:checked ~ .vth-coursetabs__panel--curriculum{
  animation:vth-rise .18s ease-out both;
}

/* --- Skeleton panel -------------------------------------------------------
   Six bars in the source's proportions (line 2561): 22px/42%, three text lines,
   a 120px block, and a short tail.

   This is a NEW SHAPE, not a new skeleton. `.vth-state--loading` (section 6)
   draws a media-plus-three-lines row, which is the right mask for a card list
   and the wrong one for a prose panel. Both are built from the SAME `.vth-skel`
   base, so there is one shimmer on the page, not two.

   IT SITS IN `.vth-card` + `.vth-card__body` ON PURPOSE. The thing it stands in
   for is a `.vth-card`, so it inherits the same 16px radius, the same border and
   the same 24px padding. A skeleton whose box differs from the box that replaces
   it just moves the jump one frame later. */
.vth .vth-skelpanel{ display:flex; flex-direction:column; gap:var(--space-4); }
.vth .vth-skelpanel__title{ height:22px; width:42%; }
.vth .vth-skelpanel__line{ height:14px; }
.vth .vth-skelpanel__block{ height:120px; width:100%; border-radius:var(--radius-md); }
/* The source's own widths (line 2561), not a rounded approximation: a skeleton
   whose lines all stop at tidy fractions reads as a graphic, where ragged ones
   read as text. */
.vth .vth-skel-w-92{ width:92%; } .vth .vth-skel-w-84{ width:84%; }
.vth .vth-skel-w-68{ width:68%; } .vth .vth-skel-w-56{ width:56%; }

/* --- Error and empty -----------------------------------------------------
   NOT REDEFINED HERE. Section 6 of this file already ships `.vth-state` in four
   variants (loading / empty / filtered-empty / error) with the 52px icon,
   --surface-error-soft tint and Try Again slot the design asks for, and
   vth/components/state-block.blade.php renders it. The platform page's error
   state uses that component; a second, near-identical card would be the exact
   inconsistency the design system exists to prevent. */

/* --- Busy buttons OUTSIDE the .vth scope ----------------------------------
   `.vth .vth-btn.is-loading` (vth-loading.css) covers every button on the
   reskinned page. It cannot reach the two LEGACY modals — the booking form and
   the share-by-email form — which this view deliberately leaves outside the
   scope because they carry Materialize form controls (see the view, line 696).
   Their submit buttons are `.btn.btn-red`, and today they give either NO
   feedback at all (booking) or swap their label to "Please wait.." (share),
   which resizes the button — the exact thing vth-loading.css tells us not to do.

   Hence one deliberately unscoped class. It is safe because this stylesheet is
   loaded by nothing but the two course-detail reskin views
   (vth/course-detail/styles.blade.php), and the class is applied by
   js/vth-reskin/course-detail.js, which those same two views alone load. It is
   named `vth-` like everything else so it cannot collide with theme CSS.

   `color:transparent` rather than hiding children: the controller writes a
   bare TEXT NODE into the button ($('#btn-submit').html('Please wait..')), and
   a text node has no element to hide. The width is held by w-100 on both
   buttons, so nothing beside them can move. */
/* THE CLASS IS DOUBLED ON PURPOSE. The share controller calls
   $('#btn-submit').prop('disabled', true), and Materialize then paints the
   button with its own disabled treatment — measured as background
   rgb(114,173,180) and colour rgb(210,210,210), both !important, from
   materialize.min.css. A single `.vth-busybtn` (0,1,0) loses to its
   `.btn:disabled` (0,1,1) even with !important, because !important ties are
   broken by specificity. `.vth-busybtn.vth-busybtn` is (0,2,0) and wins.
   (materialize.min.css is CORS-unreadable, so rule enumeration cannot show
   this — it was found by measuring the computed values.)

   --vth-busy-bg is captured by course-detail.js from the button's RESTING
   background before anything disables it, so a busy button keeps whatever
   colour its sector theme gave it instead of going grey. */
.vth-busybtn.vth-busybtn{
  position:relative; pointer-events:none; cursor:progress;
  color:transparent !important;
  background-color:var(--vth-busy-bg, currentColor) !important;
  opacity:1 !important;
}
.vth-busybtn > *{ opacity:0; }
.vth-busybtn::after{
  content:""; position:absolute; top:50%; left:50%;
  width:18px; height:18px; margin:-9px 0 0 -9px;
  border-radius:50%;
  border:2px solid #fff; border-top-color:transparent;
  opacity:.9;
  animation:vth-spin .6s linear infinite;
}

/* The platform page's four tab panels are injected by Angular (bind-unsafe-html)
   and sit OUTSIDE the .vth scope, so `.vth .vth-rise` cannot reach them. Same
   justification as .vth-busybtn above: this stylesheet loads on two views only.
   Applied with ng-class, so it is removed while loading and re-added when the
   content lands — which is what makes the animation replay on every tab switch
   instead of only the first. */
.vth-risein{ animation:vth-rise .18s ease-out both; }

/* Motion is decoration here, not information: under reduced-motion the rise
   becomes a plain fade and the spinner slows rather than disappearing, so the
   affordance survives. */
@media (prefers-reduced-motion:reduce){
  .vth .vth-rise,
  .vth #vth-tab-outcome:checked ~ .vth-coursetabs__panel--outcome,
  .vth #vth-tab-curriculum:checked ~ .vth-coursetabs__panel--curriculum,
  .vth-risein{
    animation:none;
  }
  .vth-busybtn::after{ animation-duration:2.4s; }
}
