/*
 * cx.css — customer-experience modernization stylesheet.
 * Loaded only when !admin_section?. All rules in this file are scoped
 * under .app-cx (set on the customer layout's <body>) so they cannot
 * accidentally apply to admin views. Admin retains style.css unchanged.
 */

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');

:root {
  /* Surfaces */
  --color-bg: #f5f5f7;
  --color-card: #ffffff;
  --color-line: #e5e7eb;
  --color-line-strong: #d1d5db;

  /* Text */
  --color-ink: #111827;
  --color-muted: #6b7280;
  --color-muted-strong: #4b5563;

  /* Primary blue ramp */
  --color-primary: #1e3a8a;
  --color-primary-soft: #2d4baf;
  --color-primary-hover: #1e4ed8;
  --color-link: #1e3a8a;

  /* Notification colors */
  --color-success: #059669;
  --color-success-bg: #ecfdf5;
  --color-success-border: #10b981;
  --color-warning: #d97706;
  --color-warning-bg: #fef3c7;
  --color-warning-strong: #b45309;
  --color-warning-ink: #78350f;
  --color-danger: #dc2626;
  --color-danger-hover: #b91c1c;
  --color-danger-bg: #fef2f2;
  --color-danger-ink: #7f1d1d;
  --color-suggest-bg: #eff6ff;
  --color-suggest-border: #1e3a8a;
  --color-info-bg: #f0f9ff;
  --color-info-border: #0284c7;
  --color-info-ink: #075985;
  --color-testing-bg: #b91c1c;

  /* Spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  /* Type scale */
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 16px;
  --text-lg: 18px;
  --text-xl: 22px;
  --text-2xl: 28px;

  /* Line-height scale */
  --lh-tight: 1.3;
  --lh-normal: 1.5;
  --lh-relaxed: 1.7;

  /* Price-specific type sizes */
  --text-price-hero: 28px;
  --text-price-total: 20px;
  --text-price-line: 14px;

  /* Radii */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 9999px;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.07);
  --shadow-lg: 0 10px 25px rgba(0,0,0,0.1);

  /* Motion */
  --duration-fast: 150ms;
  --duration-normal: 200ms;
  --duration-slow: 300ms;
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);

  /* Font */
  --font-body: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* Reserved for the printed-pass artifact only; in-app UI is single-sans. */
  --font-serif: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Box-sizing reset — width/height include padding + border. Modern default;
   scoped to .app-cx so admin views (style.css) keep their existing model. */
.app-cx,
.app-cx *,
.app-cx *::before,
.app-cx *::after { box-sizing: border-box; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

.app-cx .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;
}

/* ==================================================
   PROSE — typography for WYSIWYG-authored studio content
   ================================================== */

/* Studio admins author rich text via CKEditor and the result lands
   in session/addon/qualifier/promotion descriptions and deposit
   instructions. The legacy style.css resets `ol, ul { list-style:
   none }` and `* { line-height: 1 }` would otherwise nuke the
   structure. .cx-prose re-establishes typography on prose
   containers; .cx-prose--compact tightens spacing for in-card use
   where vertical room is tight (320-400px session/addon cards).

   These rules are baselines: studio inline `style=` attributes
   resolve at higher specificity than class selectors, so author
   cosmetic choices (color, font-size, font-weight, text-align,
   etc.) always win. Container classes like .session-desc and
   .addon-desc still own their font-size and color so the cx-modern
   visual hierarchy holds when the studio leaves cosmetic choices
   to defaults. */
.app-cx .cx-prose                  { line-height: 1.5; overflow-wrap: anywhere; overflow-x: clip; overflow-clip-margin: 0; }
.app-cx .cx-prose p                { margin: 0 0 var(--space-2); }
.app-cx .cx-prose p:last-child     { margin-bottom: 0; }
.app-cx .cx-prose ul,
.app-cx .cx-prose ol               { margin: 0 0 var(--space-2); padding-left: var(--space-4); }
.app-cx .cx-prose ul               { list-style-type: disc; }
.app-cx .cx-prose ol               { list-style-type: decimal; }
.app-cx .cx-prose li + li          { margin-top: var(--space-1); }
.app-cx .cx-prose a                { color: var(--color-link); text-decoration: underline; }
.app-cx .cx-prose strong,
.app-cx .cx-prose b                { font-weight: 600; }
.app-cx .cx-prose em,
.app-cx .cx-prose i                { font-style: italic; }
.app-cx .cx-prose blockquote {
  margin: 0 0 var(--space-2);
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-line);
  color: var(--color-muted-strong);
}
.app-cx .cx-prose h2 { font-size: var(--text-lg);  margin: var(--space-3) 0 var(--space-2); line-height: 1.25; }
.app-cx .cx-prose h3 {                              font-size: var(--text-md);  font-weight: 600; margin: var(--space-3) 0 var(--space-2); line-height: 1.3;  }
.app-cx .cx-prose h4 {                              font-size: var(--text-base); font-weight: 600; margin: var(--space-2) 0 var(--space-1); line-height: 1.3;  }
.app-cx .cx-prose h2:first-child,
.app-cx .cx-prose h3:first-child,
.app-cx .cx-prose h4:first-child   { margin-top: 0; }

/* Containment for studio-authored content. The sanitizer
   guarantees well-formed markup but not layout-safe sizing, and
   the WYSIWYG policy intentionally allows width/font-size on
   style. Bound the blast radius: images never exceed the column,
   wide tables scroll inside it instead of distending the page
   (overflow-wrap on the container above handles long unbroken
   tokens). */
.app-cx .cx-prose img   { max-width: 100% !important; height: auto; }
.app-cx .cx-prose table { display: block; max-width: 100%; overflow-x: auto; }
.app-cx .cx-prose pre   { white-space: pre-wrap; overflow-wrap: anywhere; }

/* Compact modifier — applied alongside .cx-prose for in-card prose
   where the description shares a tight vertical column with the
   title, price, and action button. */
.app-cx .cx-prose--compact         { line-height: 1.4; }
.app-cx .cx-prose--compact p       { margin: 0 0 var(--space-1); }
.app-cx .cx-prose--compact ul,
.app-cx .cx-prose--compact ol      { padding-left: var(--space-3); }
.app-cx .cx-prose--compact li + li { margin-top: 0; }

/* ==================================================
   BASE CLASSES — buttons, fields, banners, badges
   All scoped under .app-cx; admin pages never load this file.
   ================================================== */

/* Universal focus-visible — every interactive descendant of .app-cx */
.app-cx a:focus-visible,
.app-cx button:focus-visible,
.app-cx select:focus-visible,
.app-cx input:focus-visible,
.app-cx textarea:focus-visible,
.app-cx [role="button"]:focus-visible,
.app-cx [tabindex]:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ==================================================
   LEGACY STYLE.CSS RESETS
   style.css applies several element-level rules that
   leak into modernized markup. Reset them here so each
   modernized component doesn't have to remember to
   override individually. Class-specific rules with
   higher specificity opt back into intended values
   where the leak is desirable.
   ================================================== */

/* `*, body { font-family: 'Helvetica' ...; font-size: 12px;
   line-height: 1 }` (style.css:49). The universal selector
   sets these directly on every descendant, so inheritance
   from .app-cx alone won't propagate. Force font-family on
   every descendant; restore inherit on font-size and line-
   height so the normal cascade resumes inside .app-cx. */
.app-cx,
.app-cx * { font-family: var(--font-body); }
.app-cx { font-size: var(--text-base); line-height: var(--lh-normal); }
.app-cx * { font-size: inherit; line-height: inherit; }

/* `button { ... text-shadow: -1px -1px 0px #155CD2 }` and
   `button:hover { background-color: #ff7648; text-shadow:
   -1px -1px 0px #ff7648 }` (style.css:82, 90, 408, 412).
   Centralize text-shadow:none plus a defensive hover bg
   reset; class-specific :hover rules with higher
   specificity opt into intended hover backgrounds. */
.app-cx button { text-shadow: none; }
.app-cx button:hover,
.app-cx button:active,
.app-cx button.active,
.app-cx button.active:hover,
.app-cx button.active:focus {
  background-color: transparent;
  text-shadow: none;
}
.app-cx .btn { text-shadow: none; }
.app-cx .tooltip-badge { text-shadow: none; }

/* `a, a:link, a:visited, a:active { color: #192a88; text-
   decoration: none }` (style.css:71). Reset to the modern
   link token (#1e3a8a) and restore underline as the default
   link affordance. Link-shaped buttons (.btn, .mobile-cart-
   cta, etc.) override via higher specificity. */
.app-cx a { color: var(--color-link); text-decoration: underline; }

/* `p { margin-bottom: 1em; line-height: 1.5 }` (style.css:75).
   line-height is fine; bottom margin reset to a modern token. */
.app-cx p { margin: 0 0 var(--space-3); }

/* `ul li { margin-bottom: .5em }` (style.css:79). Neutralize the
   legacy list-item gap; .cx-prose re-establishes intended list
   spacing via its own higher-specificity rules. */
.app-cx li { margin-bottom: 0; }

/* `td { padding: .5em }` and `th { font-weight: bold; font-
   size: 14px }` (style.css:447, 450). Modernized table
   cells in _order.html.erb currently override via inline
   styles; these defaults make future cells safe without
   inline overrides. */
.app-cx td { padding: var(--space-2); }
.app-cx th { font-weight: 600; font-size: var(--text-base); }

/* Skip link */
.app-cx .skip-link {
  position: absolute;
  top: -40px;
  left: var(--space-3);
  background: var(--color-primary);
  color: white;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  text-decoration: none;
  font-weight: 600;
  font-size: var(--text-sm);
  z-index: 200;
  transition: top var(--duration-fast) var(--ease-standard);
}
.app-cx .skip-link:focus { top: var(--space-3); }

/* Buttons */
.app-cx .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 11px 20px;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  border: 1px solid transparent;
  font-family: inherit;
  min-height: 44px;
  text-align: center;
  line-height: 1.4;
  transition: background-color var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}
.app-cx .btn-primary { background: var(--color-primary); color: white; }
.app-cx .btn-primary:hover { background: var(--color-primary-hover); }
.app-cx .btn-secondary { background: white; color: var(--color-primary); border-color: var(--color-primary); }
.app-cx .btn-secondary:hover { background: var(--color-suggest-bg); border-color: var(--color-primary-soft); color: var(--color-primary-soft); }
.app-cx .btn-tertiary { background: transparent; color: var(--color-primary); border: none; text-decoration: underline; padding: 11px 4px; min-height: 44px; }
.app-cx .btn-tertiary:hover { color: var(--color-primary-soft); }
.app-cx .btn-danger { background: var(--color-danger); color: white; }
.app-cx .btn-danger:hover { background: var(--color-danger-hover); }
.app-cx .btn-block { display: flex; width: 100%; }
.app-cx .btn-row { display: flex; gap: var(--space-3); flex-wrap: wrap; }
.app-cx .btn[disabled], .app-cx .btn[aria-disabled="true"] { cursor: not-allowed; }

/* Class-based disable. Unlike the [disabled] attribute, .is-disabled
   keeps the button click-receivable so JS can intercept the click and
   show feedback (e.g., pulse the unset recap chips) instead of the
   browser silently ignoring the press. */
.app-cx .btn.is-disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.app-cx .btn .spinner {
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}
.app-cx .btn-secondary .spinner { border-color: rgba(30, 58, 138, 0.25); border-top-color: var(--color-primary); }
@keyframes spin { to { transform: rotate(360deg); } }

/* Generic in-flight state: hide the button label, render the spinner
   from the existing .btn .spinner rule centered over the button.
   Survives jQuery .text()/.html() collisions because the label hide
   is via color, not innerHTML replacement. Add aria-busy on the
   button (or its container) when applying. */
.app-cx .btn.is-loading {
  position: relative;
  pointer-events: none;
  color: transparent;
}
.app-cx .btn.is-loading > * { visibility: hidden; }
.app-cx .btn.is-loading::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: white;
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}
.app-cx .btn-secondary.is-loading::after,
.app-cx .btn-tertiary.is-loading::after {
  border-color: rgba(30, 58, 138, 0.25);
  border-top-color: var(--color-primary);
}

@media (max-width: 480px) {
  .app-cx .btn-row { flex-direction: column-reverse; }
  .app-cx .btn-row .btn { width: 100%; }
}

/* Form fields — with custom select chevron */
.app-cx .cx-field { margin-bottom: var(--space-4); }
.app-cx .cx-field-label { display: block; font-size: var(--text-sm); font-weight: 500; color: var(--color-ink); margin-bottom: 6px; }
.app-cx .cx-field-required { color: var(--color-danger); margin-left: 2px; }
.app-cx .cx-field-input,
.app-cx .cx-field-select,
.app-cx .cx-field-textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-md);
  font-size: var(--text-md);
  font-family: inherit;
  background: white;
  min-height: 44px;
  color: var(--color-ink);
  transition: border-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
}
.app-cx .cx-field-select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px;
  padding-right: 36px;
  cursor: pointer;
}
.app-cx .cx-field-input:focus,
.app-cx .cx-field-select:focus,
.app-cx .cx-field-textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.18);
}
.app-cx .cx-field-help {
  font-size: var(--text-xs);
  color: var(--color-muted-strong);
  margin-top: calc(var(--space-2) * -1);
  margin-bottom: var(--space-4);
}
/* When help text sits directly below a card header (section caption
   role), the standard negative top margin still anchors it tightly
   to the header above; tighten the bottom margin so the gap to the
   first field row reads as separation rather than another section
   break. */
.app-cx .card-header + .cx-field-help {
  margin-bottom: var(--space-3);
}
/* Inline "(optional)" indicator — sits next to a label or a
   subsection header. Renders muted-strong, normal weight, and
   resets any uppercase/letter-spacing inherited from a styled
   parent (subsection-header in particular) so the parenthetical
   reads as supplementary, not as part of the heading. */
.app-cx .cx-field-optional {
  color: var(--color-muted-strong);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
.app-cx .cx-field-with-errors .cx-field-label { color: var(--color-danger); }
.app-cx .cx-field-with-errors .cx-field-input,
.app-cx .cx-field-with-errors .cx-field-select { border-color: var(--color-danger); background: var(--color-danger-bg); }
.app-cx .cx-field-error { color: var(--color-danger); font-size: var(--text-xs); margin-top: 4px; font-weight: 500; }
.app-cx .cx-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
@media (max-width: 600px) { .app-cx .cx-field-row { grid-template-columns: 1fr; } }

/* Modifier — three-control row that keeps a single 3-up grid at
   every viewport, with the third column fixed to a tight width
   (used by the payment row's Exp Month / Year / CVV). The row
   does not collapse to single-column at mobile — selects and
   the CVV input remain side-by-side. minmax(0, 1fr) on the
   first two columns prevents native select chevrons from
   pushing the row into horizontal overflow at narrow viewports.
   Pairs with .cx-field-row-span-2 on a label that spans the
   first two columns (e.g., "Expiration Date" above Month + Year). */
.app-cx .cx-field-row.cx-field-row-3up {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 120px;
  column-gap: var(--space-3);
  row-gap: var(--space-2);
  align-items: end;
}
@media (max-width: 600px) {
  .app-cx .cx-field-row.cx-field-row-3up {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 90px;
    column-gap: var(--space-2);
  }
}
.app-cx .cx-field-row-span-2 { grid-column: span 2; margin-bottom: 0; }
/* Labels inside the compact 3-up row align to the bottom edge of the
   row (align-items: end), so a label bottom-margin would push the
   control out of alignment with its siblings. */
.app-cx .cx-field-row.cx-field-row-3up .cx-field-label { margin-bottom: 0; }

/* ==================================================
   SELECT2 — cx-modern theme
   select2 hides the native <select> and renders its
   own widget, so cx.css's .cx-field-select chrome
   never paints. Re-skin select2's container, single
   selection, chevron, focus state, and dropdown
   panel to match the same tokens — same height,
   padding, border, radius, font, focus ring — so
   cx-modern forms read as one design language. The
   dropdown panel is attached to <body>, but body
   carries .app-cx on customer pages, so it stays
   within the scope automatically. Admin pages lack
   .app-cx and keep the default select2 theme.
   ================================================== */
.app-cx .select2-container--default .select2-selection--single {
  height: 44px;
  padding: 10px 36px 10px 12px;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-md);
  background: white;
  font-size: var(--text-md);
  font-family: inherit;
  color: var(--color-ink);
  transition: border-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
}
.app-cx .select2-container--default .select2-selection--single .select2-selection__rendered {
  padding: 0;
  line-height: 1.5;
  color: var(--color-ink);
}
.app-cx .select2-container--default .select2-selection--single .select2-selection__placeholder {
  color: var(--color-muted-strong);
}
.app-cx .select2-container--default .select2-selection--single .select2-selection__arrow {
  height: 100%;
  top: 0;
  right: 0;
  width: 36px;
  background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%234b5563' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px;
}
.app-cx .select2-container--default .select2-selection--single .select2-selection__arrow b {
  display: none;
}
.app-cx .select2-container--default.select2-container--focus .select2-selection--single,
.app-cx .select2-container--default.select2-container--open .select2-selection--single {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.18);
  outline: none;
}

/* Width: cx-modern selects render full-width in their card column.
   select2 sets the wrapper width from the native select's width
   attribute or inline style; this override eliminates the per-site
   `style="width:100%"` workaround for selects that opt into cx. */
.app-cx select.cx-field-select + .select2-container {
  width: 100% !important;
}

/* Open dropdown panel — attached to body, so .app-cx is the body
   class ancestor that scopes this to customer pages. */
.app-cx .select2-dropdown {
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-md);
  background: white;
  box-shadow: var(--shadow-md);
}
.app-cx .select2-results__option {
  padding: 8px 12px;
  font-size: var(--text-md);
  color: var(--color-ink);
  line-height: 1.5;
}
.app-cx .select2-container--default .select2-results__option--highlighted[aria-selected] {
  background: var(--color-suggest-bg);
  color: var(--color-primary);
}
.app-cx .select2-container--default .select2-results__option[aria-selected="true"] {
  background: var(--color-line);
  color: var(--color-ink);
}
.app-cx .select2-search--dropdown .select2-search__field {
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  font-size: var(--text-sm);
}
.app-cx .select2-search--dropdown .select2-search__field:focus {
  outline: none;
  border-color: var(--color-primary);
}

/* Checkbox / radio rows */
.app-cx .checkbox-row,
.app-cx .radio-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) 0;
  min-height: 44px;
}
.app-cx .checkbox-row input,
.app-cx .radio-row input { width: 20px; height: 20px; accent-color: var(--color-primary); }

/* Highlighted variant of .checkbox-row — soft suggest tint to draw the
   eye to an opt-in choice (e.g., the emphasized-add-on opt_outable
   checkbox). */
.app-cx .checkbox-row.checkbox-row-suggest {
  background: var(--color-suggest-bg);
  border: 1px solid var(--color-suggest-border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  cursor: pointer;
}

/* Step indicator — neutral wayfinding pill above an extras-flow card.
   Muted bg + line-strong border on purpose: this is progress UI, not a
   recommendation, so it does not borrow the suggest tone. */
.app-cx .step-indicator {
  display: inline-flex;
  align-items: center;
  background: var(--color-bg);
  border: 1px solid var(--color-line-strong);
  color: var(--color-muted-strong);
  font-size: var(--text-sm);
  font-weight: 600;
  padding: 6px var(--space-3);
  border-radius: var(--radius-pill);
  margin: 0 0 var(--space-3);
}

/* Help label — text with a dotted underline that triggers a Tippy
   tooltip on click/hover/focus. Replaces the older .tooltip-badge
   circle pattern with a learned affordance (Wikipedia/Stripe-style
   dotted underline). The whole label is the trigger, giving a much
   larger tap target than a separate icon button. */
.app-cx .help-label {
  appearance: none;
  background: transparent;
  border: none;
  border-bottom: 1px dotted var(--color-muted-strong);
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: help;
  text-align: left;
  text-decoration: none;
  text-underline-offset: 2px;
  transition: color var(--duration-fast) var(--ease-standard),
              border-bottom-color var(--duration-fast) var(--ease-standard);
}
.app-cx .help-label:hover,
.app-cx .help-label:focus-visible {
  color: var(--color-primary);
  border-bottom-color: var(--color-primary);
  outline: none;
}

/* Banners */
.app-cx .banner-suggest {
  background: var(--color-suggest-bg);
  border-left: 4px solid var(--color-suggest-border);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-primary);
  margin-bottom: var(--space-3);
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.app-cx .banner-suggest .icon { color: var(--color-primary); flex-shrink: 0; }

.app-cx .banner-info,
.app-cx .banner-warning,
.app-cx .banner-error {
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  margin-bottom: var(--space-3);
  min-height: 44px;
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}
.app-cx .banner-info { background: var(--color-info-bg); border-left: 3px solid var(--color-info-border); color: var(--color-info-ink); }
.app-cx .banner-info .icon { color: var(--color-info-border); flex-shrink: 0; }
.app-cx .banner-warning { background: var(--color-warning-bg); border-left: 3px solid var(--color-warning); color: var(--color-warning-ink); }
.app-cx .banner-warning .icon { color: var(--color-warning); flex-shrink: 0; }
.app-cx .banner-error { background: var(--color-danger-bg); border-left: 3px solid var(--color-danger); color: var(--color-danger-ink); flex-direction: column; align-items: stretch; }
.app-cx .banner-error .banner-error-head { display: flex; align-items: center; gap: var(--space-2); font-weight: 600; }
.app-cx .banner-error .icon { color: var(--color-danger); flex-shrink: 0; }

/* Studio-authored note at the bottom of the order summary. Outlined
   card with a navy "Studio Note" label tab projecting above the top
   edge — stationery / filing genre. Sits visually distinct from the
   filled amber `.cart-balance-deferred` warning callout above when
   both render (outlined+tab vs filled). Title is studio-authored
   (any length); body may contain studio-authored HTML. */
.app-cx .studio-note {
  position: relative;
  background: #fff;
  color: var(--color-muted-strong);
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-sm);
  padding: 18px var(--space-4) var(--space-3);
  margin-top: 24px;
  font-size: var(--text-sm);
  line-height: 1.55;
}
.app-cx .studio-note::before {
  content: "Studio Note";
  position: absolute;
  top: -11px;
  left: 14px;
  background: var(--color-primary);
  color: #fff;
  padding: 3px 10px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border-radius: 2px 2px 0 0;
}
/* Title rule keeps high specificity (0,3,0) defensively. The
   .studio-note used to carry .cx-prose directly and competed with
   .cx-prose h4 (0,2,1) for the heading; the WYSIWYG body now lives
   in an inner .cx-prose wrapper, so the title sits outside that
   scope. Targeting a dedicated title class (rather than a bare
   <strong>) also keeps this treatment from leaking onto a <strong>
   the studio admin bolds inside the WYSIWYG body -- those fall to
   .cx-prose strong as intended. */
.app-cx .studio-note .studio-note-title {
  font-weight: 600;
  font-size: 14px;
  line-height: 1.35;
  color: var(--color-ink);
  margin: 0 0 4px;
}

/* Tag family */
.app-cx .tag {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  margin-right: 6px;
}
.app-cx .tag-net-new  { background: #fff7e6; color: #92400e; border: 1px solid #fde68a; }
.app-cx .tag-styling  { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; }
.app-cx .tag-locked   { background: #f5f3ff; color: #5b21b6; border: 1px solid #ddd6fe; }
.app-cx .tag-decision { background: #eff6ff; color: #1e3a8a; border: 1px solid #bfdbfe; }

/* Trust pill */
.app-cx .secure-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-xs);
  font-weight: 500;
  padding: 4px 10px;
  background: var(--color-success-bg);
  color: var(--color-success);
  border-radius: var(--radius-pill);
}
.app-cx .secure-badge .icon { width: 12px; height: 12px; }

/* ==================================================
   APP SHELL
   .app-cx is the customer body. These rules style the
   body itself plus app-level chrome (testing banner,
   header, container, narrow variant, icon utility).
   ================================================== */

/* Body itself — visible change: light-gray background,
   modernized off-black text. */
.app-cx {
  background: var(--color-bg);
  min-height: 400px;
  color: var(--color-ink);
  position: relative;
}

.app-cx .app-banner-testing {
  background: var(--color-testing-bg);
  color: white;
  text-align: center;
  padding: 6px var(--space-4);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.5px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}

.app-cx .app-header {
  background: white;
  padding: var(--space-4);
  text-align: center;
  border-bottom: 1px solid var(--color-line);
}
.app-cx .app-header img { max-height: 50px; max-width: 100%; }

/* Studio banner header (custom logo each studio uploads). The
   paperclip :normal style is 960x100>, so caps at 960 wide and
   100 tall but may be smaller. Container max-width matches the
   asset cap; the image gets max-width: 100% so it scales down
   on narrow viewports without forcing horizontal scroll. */
.app-cx .studio-banner {
  display: block;
  max-width: 960px;
  margin: 0 auto;
  padding: var(--space-3) var(--space-4);
  text-align: center;
}
.app-cx .studio-banner-link {
  display: inline-block;
  line-height: 0;
}
.app-cx .studio-banner-link img {
  max-width: 100%;
  height: auto;
}

/* Sticky-footer scaffolding. The customer-flow body becomes a flex
   column so .app-container can fill the available vertical space
   (flex: 1) and push the footer to the viewport bottom on short
   pages; on long pages, the container takes its natural height and
   the footer sits at the end of content. Scoped to .app-cx.main so
   the unsupported-browser layout (also .app-cx but no .main) is
   unaffected. 100dvh follows the visible viewport on iOS Safari as
   the URL bar collapses/expands, avoiding the footer jump that
   100vh would produce. */
.app-cx.main {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

/* Page container. The customer layout wraps yield in
   .app-container; pages opt into a narrow (720px) variant by
   wrapping their own content in <div class="app-narrow">. 1280
   sits comfortably above the 1366 effective laptop floor (~86px
   gutter on a 1366 viewport) and gives the layout-sidebar content
   column room to breathe alongside the 380px cart sidebar. */
.app-cx .app-container {
  max-width: 1280px;
  margin: 0 auto;
  padding: var(--space-5) var(--space-4);
  flex: 1;
  width: 100%;
}
.app-cx .app-narrow { max-width: 720px; margin: 0 auto; }
.app-cx .app-medium { max-width: 960px; margin: 0 auto; }
@media (max-width: 480px) {
  .app-cx .app-container { padding: var(--space-4) var(--space-3); }
}

/* SVG icon utility — sized for inline use beside text */
.app-cx .icon { width: 20px; height: 20px; flex-shrink: 0; display: inline-block; vertical-align: middle; }
.app-cx .icon-sm { width: 16px; height: 16px; }
.app-cx .icon-lg { width: 24px; height: 24px; }
.app-cx .icon-xl { width: 32px; height: 32px; }

/* ==================================================
   GENERIC CARD
   Reusable rounded-box pattern with header/body/footer
   slots. Used by billing fieldsets, receipt sections,
   and most pages.
   ================================================== */

.app-cx .card {
  background: var(--color-card);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  margin-bottom: var(--space-4);
  box-shadow: var(--shadow-sm);
  border: 2px solid transparent;
}
/* Generic active-card modifier — used by addon cards and any
   future card-grid that signals selection through the card itself.
   Body bg is intentionally untouched; the affordance is the navy
   border (addon cards add the green .addon-added-stamp on top). */
.app-cx .card.tile-active {
  border-color: var(--color-primary);
  position: relative;
}
/* Image placeholder — used when a model lacks an image variant for
   the current sex (e.g., addon image returns nil). Lavender-neutral
   block with a centered short label. Caller sizes via inline style. */
.app-cx .img-placeholder {
  background: #f3f4f6;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-muted-strong);
  font-size: 12px;
  border-radius: var(--radius-md);
}
.app-cx .card-header {
  font-size: var(--text-md);
  font-weight: 600;
  margin: 0 0 var(--space-4);
  color: var(--color-ink);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.app-cx .card-header .icon { color: var(--color-primary); }
.app-cx .card-header .icon.icon-success { color: var(--color-success); }
/* Trust pill floats to the end of the payment card header, opposite
   the heading text. */
.app-cx .card-header .secure-badge { margin-left: auto; }
.app-cx .card-footer {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-line);
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  flex-wrap: wrap;
}
.app-cx .card-footer.card-footer-noborder { border-top: none; padding-top: 0; }
.app-cx .card-footer.card-footer-block {
  flex-direction: column;
  align-items: stretch;
}
.app-cx .card-footer.card-footer-block .btn { width: 100%; }
@media (max-width: 480px) {
  .app-cx .card-footer { flex-direction: column-reverse; }
  .app-cx .card-footer .btn { width: 100%; }
}

/* ==================================================
   CART SIDEBAR (desktop)
   The white-box totals column. .badge-credit is the
   negative-value pill; .addon-remove is the X button
   inside cart line items.
   ================================================== */

.app-cx .cart {
  background: white;
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--shadow-sm);
}
.app-cx .cart-header { font-size: var(--text-md); font-weight: 600; margin: 0 0 var(--space-3); padding-bottom: var(--space-3); border-bottom: 1px solid var(--color-line); display: flex; align-items: center; gap: var(--space-2); }
.app-cx .cart-section { margin-bottom: var(--space-3); }
/* Start-over release valve -- the ghost secondary to the navy
   Continue-to-Checkout primary, full-width to mirror its footprint
   so the pair reads as primary/secondary rather than stray text.
   Lives in the cart static chrome (outside .body) so cxPartials
   refreshes do not touch it, and renders in every cart context
   including the mobile slide-up sheet. Subordinate but deliberate
   (DEF-025: discoverable, not prominent). */
.app-cx .cart-startover { margin-top: var(--space-5); }
.app-cx .cart-startover-btn {
  display: block;
  width: 100%;
  background: transparent;
  border: 1px solid var(--color-line-strong);
  color: var(--color-muted-strong);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}
.app-cx .cart-startover-btn:hover {
  border-color: var(--color-muted);
  color: var(--color-ink);
}
.app-cx .cart-section-title { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-muted-strong); margin: 0 0 var(--space-2); font-weight: 600; display: flex; align-items: center; }
.app-cx .cart-section-count { color: var(--color-muted-strong); font-weight: 500; text-transform: none; letter-spacing: 0; font-size: 11px; margin-left: auto; }

/* Section-edit affordance — small "Edit" link inline with each section
   title on the billing order summary. Routes users back to the relevant
   picker without leaving a heavyweight button row at the bottom of the
   card. Hidden on the picker-page sidebar (no `editable` local passed)
   and on the receipt (is_receipt=true → editable=false). Pushes itself
   right via margin-left:auto; when a section count is present, the
   count claims the auto-margin and the edit link sits next to it. */
.app-cx .section-edit {
  margin-left: auto;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--color-muted-strong);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
}
.app-cx .section-edit:hover { background: var(--color-bg); color: var(--color-primary); }
.app-cx .section-edit:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.app-cx .section-edit .icon { width: 11px; height: 11px; }
.app-cx .cart-section-count + .section-edit { margin-left: 6px; }
.app-cx .cart-line { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: var(--text-sm); }
.app-cx .cart-line .label { color: var(--color-ink); display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.app-cx .cart-line .price { font-weight: 500; color: var(--color-ink); font-size: var(--text-price-line); }
.app-cx .cart-line-sub {
  border-top: 1px dashed var(--color-line);
  margin-top: 6px;
  padding: 6px 0 0;
  font-size: var(--text-xs);
  color: var(--color-muted-strong);
  font-weight: 500;
}
.app-cx .cart-line-sub .price { font-size: var(--text-xs); color: var(--color-muted-strong); }
.app-cx .cart-line-total { border-top: 1px solid var(--color-line); margin-top: var(--space-3); margin-bottom: var(--space-4); padding: var(--space-3) 0 0; font-size: var(--text-price-total); font-weight: 700; }
.app-cx .cart-line-total .price { font-size: var(--text-price-total); font-weight: 700; }

.app-cx .cart-thumb { width: 28px; height: 28px; border-radius: var(--radius-sm); background: #f3f4f6; object-fit: contain; flex-shrink: 0; }

/* Credit row — pill alongside minus sign + green tint */
.app-cx .cart-line.credit-row { color: var(--color-success); }
.app-cx .cart-line.credit-row .price { color: var(--color-success); font-weight: 600; }
.app-cx .badge-credit {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  background: var(--color-success-bg);
  color: var(--color-success);
  border: 1px solid var(--color-success);
  font-size: 10px;
  font-weight: 700;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.app-cx .badge-credit .icon { width: 10px; height: 10px; }

/* Editable add-on row — single-row layout. Cart-line carries the
   thumb, name (flex), optional qty-stepper, price, and remove button
   all on one line. align-items: center keeps every control aligned
   on the row's vertical center; when a long name wraps to two lines
   the controls center between them. */
.app-cx .cart-line.addon-row {
  justify-content: flex-start;
  align-items: center;
  gap: var(--space-2);
}
.app-cx .cart-line.addon-row .cart-thumb { flex-shrink: 0; }
.app-cx .cart-line.addon-row .addon-name {
  flex: 1;
  min-width: 0;
  color: var(--color-ink);
  font-size: var(--text-base);
  /* Override the addon-picker's .addon-name font-weight: 600 (which
     gives /select/addons cards their bold title styling) — in the
     cart-line context, the addon is a recap line item alongside
     "Senior 2022" / "Yearbook" choices that render at normal weight.
     Matching the surrounding text weight also frees ~10-15% of the
     name width budget so single-word names fit on one line in the
     narrow billing sidebar. */
  font-weight: 400;
  line-height: 1.35;
}
.app-cx .cart-line.addon-row .price {
  font-size: var(--text-sm);
  white-space: nowrap;
  flex-shrink: 0;
}

/* Hide the addon thumbnail on narrow viewports inside the billing
   summary. The thumb is decoration in the cart-recap context, and
   reclaiming its ~40px gives long add-on names room to fit on one
   line. Scoped to #order-summary so the picker pages and mobile-
   cart dialog keep the thumb. */
@media (max-width: 600px) {
  .app-cx #order-summary .cart-line.addon-row .cart-thumb { display: none; }
}

/* Addon X-to-remove — destructive button. The OUTER 44x44 box is the
   tap target (invisible, no border, no background). A ::before
   pseudo-element draws the visible 28x28 outlined circle, centered
   inside the hit area. Hover/focus repaint the ::before only, so
   the colored ring stays at 28x28 instead of expanding to fill the
   full 44x44. Negative margin compensates so the wide hit area
   doesn't push surrounding cells out of place. */
.app-cx .addon-remove {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: none;
  color: var(--color-muted-strong);
  width: 44px;
  height: 44px;
  padding: 0;
  margin: -8px -8px -8px 0;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.app-cx .addon-remove::before {
  content: '';
  position: absolute;
  inset: 8px;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-pill);
  background: transparent;
  transition: background-color var(--duration-fast) var(--ease-standard),
              border-color var(--duration-fast) var(--ease-standard);
}
.app-cx .addon-remove .icon {
  position: relative;
  z-index: 1;
  width: 14px;
  height: 14px;
}
.app-cx .addon-remove:hover::before,
.app-cx .addon-remove:focus-visible::before {
  background: var(--color-danger-bg);
  border-color: var(--color-danger);
}
.app-cx .addon-remove:hover,
.app-cx .addon-remove:focus-visible {
  color: var(--color-danger);
  outline: none;
}

/* Qty stepper — compact 28x28 visible buttons (industry norm — Shopify
   28, Stripe 24, Amazon 30, Etsy 32). The bordered container prevents
   the inset-padding trick that .addon-remove uses, so the buttons
   themselves are the tap target. Sits between .addon-name and .price
   in the editable row. */
.app-cx .qty-stepper {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-md);
  overflow: hidden;
  flex-shrink: 0;
}
.app-cx .qty-stepper .qty-up,
.app-cx .qty-stepper .qty-down {
  appearance: none;
  background: white;
  border: none;
  width: 26px;
  height: 26px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--color-ink);
  cursor: pointer;
}
.app-cx .qty-stepper .qty-up .icon,
.app-cx .qty-stepper .qty-down .icon {
  width: 14px;
  height: 14px;
}
.app-cx .qty-stepper .qty-up:hover,
.app-cx .qty-stepper .qty-down:hover { background: var(--color-bg); }
.app-cx .qty-stepper .qty-val {
  min-width: 22px;
  padding: 2px var(--space-1) 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-ink);
}
.app-cx .addon-item.is-removing {
  opacity: 0.5;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-standard);
}
.app-cx .addon-error {
  display: block;
  font-size: var(--text-xs);
  color: var(--color-danger);
  margin: 4px 0 0;
}

/* Deferred-balance highlight at the bottom of the cart */
.app-cx .cart-balance-deferred {
  background: var(--color-warning-bg);
  color: var(--color-warning-ink);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin: var(--space-3) -8px 0;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.app-cx .cart-balance-deferred .deferred-content { flex: 1; }
.app-cx .cart-balance-deferred .deferred-label { font-size: var(--text-xs); display: block; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.85; font-weight: 700; margin-bottom: 2px; }

/* ==================================================
   TILE GRID
   Add-on selection. 3 columns desktop, 2 columns tablet,
   1 column mobile. Includes hover/active/required/updating
   states.
   ================================================== */

.app-cx .tile-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
}
@media (max-width: 768px) { .app-cx .tile-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .app-cx .tile-grid { grid-template-columns: 1fr; } }

/* Modifier: 2-column variant for grids whose tile content is wider
   than the default (qualifier banners are 375x105, ~3.57:1).
   Drops to 1-col at <=768 so the banner image stays large enough
   for varied photographic content; at 2-col tablet widths the slot
   shrinks below useful resolution. */
.app-cx .tile-grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 768px) { .app-cx .tile-grid.cols-2 { grid-template-columns: 1fr; } }

.app-cx .tile {
  background: white;
  border-radius: var(--radius-lg);
  border: 2px solid var(--color-line);
  cursor: pointer;
  position: relative;
  display: block;
  overflow: hidden;
  transition: box-shadow 180ms ease, border-color 180ms ease;
}
.app-cx .tile:hover { box-shadow: 0 10px 25px rgba(0,0,0,.08); }
.app-cx .tile.tile-active {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 4px rgba(30,58,138,.10), 0 10px 25px rgba(0,0,0,.08);
}
.app-cx .tile.tile-required {
  border-color: var(--color-success);
  box-shadow: 0 0 0 4px rgba(4,120,87,.10), 0 10px 25px rgba(0,0,0,.08);
}

/* Visually hide the checkbox -- still in the DOM so form submission
   and the label-for click-to-toggle keep working; state lives on
   the .tile element via .tile-active. */
.app-cx .tile-checkbox {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Inline marker in the chrome row, before the label. Always visible
   so every tile has an at-rest "selectable" cue (universal checkbox
   pattern) without putting chrome on the image itself. Empty outline
   default, fills navy on select, fills green on required. */
.app-cx .tile-marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: white;
  color: transparent;
  box-shadow: inset 0 0 0 1.5px var(--color-muted-strong);
  transition: background 180ms ease, color 180ms ease, box-shadow 180ms ease;
  flex-shrink: 0;
}
.app-cx .tile-marker svg { width: 11px; height: 11px; }
.app-cx .tile.tile-active .tile-marker {
  background: var(--color-primary);
  color: white;
  box-shadow: 0 0 0 2px white, 0 1px 2px rgba(0, 0, 0, 0.12);
}
.app-cx .tile.tile-required .tile-marker {
  background: var(--color-success);
  color: white;
  box-shadow: 0 0 0 2px white, 0 1px 2px rgba(0, 0, 0, 0.12);
}

/* Slot aspect matches the paperclip "banner" variant: qualifier.rb
   pins boy/girl_banner_image :normal at 375x105# (scale-and-crop to
   exact dimensions), so a 25/7 slot fits the asset edge-to-edge at
   any tile width without the letterbox bars a square-ish slot would
   leave around a 3.57:1 image. */
.app-cx .tile-image {
  width: 100%;
  aspect-ratio: 25 / 7;
  background: #f3f4f6;
  overflow: hidden;
  display: block;
}
.app-cx .tile-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Seam strip between image and chrome. Quietly carries state color
   alongside the border + lift: gray default, navy when selected,
   green when required. */
.app-cx .tile-seam {
  height: 4px;
  background: var(--color-line);
  transition: background 180ms ease;
}
.app-cx .tile.tile-active .tile-seam { background: var(--color-primary); }
.app-cx .tile.tile-required .tile-seam { background: var(--color-success); }

/* Chrome row beneath the image -- label on the left, info button
   right-aligned. Replaces the previous centered label + underlined
   "Description" text link. */
.app-cx .tile-chrome {
  padding: var(--space-3) var(--space-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.app-cx .tile-chrome-left {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.app-cx .tile-label {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--color-ink);
  transition: font-weight 180ms ease;
}
.app-cx .tile.tile-active .tile-label,
.app-cx .tile.tile-required .tile-label { font-weight: 700; }
.app-cx .tile-included-text {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-success);
}

/* Info button (was "Description" text link). Opens the qualifier
   lightbox via the qualifier-image-trigger handler. */
.app-cx .tile-details-link {
  background: transparent;
  border: 1px solid transparent;
  padding: 6px;
  border-radius: 50%;
  font: inherit;
  color: var(--color-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  transition: color 180ms ease, background 180ms ease, border-color 180ms ease;
}
.app-cx .tile-details-link:hover {
  color: var(--color-ink);
  border-color: var(--color-line);
}
.app-cx .tile-details-link .icon { width: 16px; height: 16px; }

/* Generic loading state — opacity dim + interaction blocked.
   Applied via cxLoading.markUpdating($el). */
.app-cx .is-updating { opacity: 0.6; pointer-events: none; }

/* Updating state — shows a spinner over the tile while
   the add/remove request is in flight. */
.app-cx .tile.is-updating { opacity: 0.6; pointer-events: none; }
.app-cx .tile.is-updating::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 24px;
  height: 24px;
  margin: -12px 0 0 -12px;
  border: 2px solid var(--color-line-strong);
  border-top-color: var(--color-primary);
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

/* ==================================================
   ADD-ONS GRID  (/select/addons)
   Each addon is its own .card.addon-card. Image-on-the-
   left horizontal layout at desktop; flips to image-top
   stacked layout at the tablet breakpoint. The active
   state is the .card.tile-active border + a green
   .addon-added-stamp on the top-left of the thumbnail.
   ================================================== */

.app-cx .addon-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-4);
}
/* Collapse the addons grid to 1-up below the old 1100px wrapper cap.
   2-up addon-card tiles need ~324px each to keep the image-left
   horizontal layout readable; in the 901-1099 band the sidebar is
   still inline and the content column is too narrow for two tiles,
   so a single full-width tile per row reads better.

   At ≤900 the layout-sidebar collapses and the addon-grid gets the
   full wrapper width back, so 2-up becomes practical again until
   the addon-card-media stacking at ≤768 (image on top, content
   below) takes over and the tile shape no longer wants 2-up. */
@media (max-width: 1099px) {
  .app-cx .addon-grid { grid-template-columns: 1fr; }
}
@media (max-width: 900px) and (min-width: 769px) {
  /* minmax(320px, 1fr) so the grid auto-falls-back to 1-up if any
     future content-column chrome (sticky banner, promo bar, etc.)
     ever narrows the column below 2 × 320 + gap; today the column
     is the full wrapper width at this breakpoint so the minmax is
     never the active constraint. */
  .app-cx .addon-grid { grid-template-columns: repeat(2, minmax(320px, 1fr)); }
}

/* Override the legacy style.css `#addons { text-align: center }` rule
   from its max-width: 975 media query. The legacy rule was written
   for the pre-modernization addon list (no image-left tile); in
   cx-modern the addon-card has an image-left horizontal layout and
   the centered text reads as off-axis with the image still flush
   left. Scoped to .app-cx so admin contexts keep the legacy
   centering if they ever render #addons. */
.app-cx #addons { text-align: left; }

/* Card lays out as a flex column so .card-footer can margin-top: auto
   itself to the card's bottom edge -- gives every Add/Remove button in
   a row the same y-position regardless of description length, since
   the .addon-grid rows stretch to max-content-height by CSS Grid
   default. Right alignment of the button comes from the base
   .card-footer { justify-content: flex-end } rule (cx.css:903).

   padding-top reserves the breathing room above the button. The
   .card-footer-noborder modifier zeroes the base padding-top, and
   margin-top: auto displaces the base margin-top: var(--space-4)
   that would otherwise carry the gap; without an explicit pad here
   a long-description card resolves margin-top: auto to 0 and the
   description ends flush against the button. */
.app-cx .card.addon-card { display: flex; flex-direction: column; }
/* The addon footer holds a single button, so it stays a right-
   aligned row at every width. This overrides the generic
   max-width: 480 .card-footer { flex-direction: column-reverse }
   (meant for multi-button footers) -- with the addon button now a
   fixed 130px, column-reverse would drop it to the cross-axis
   start and left-align it. */
.app-cx .addon-card .card-footer {
  margin-top: auto;
  padding-top: var(--space-4);
  flex-direction: row;
  justify-content: flex-end;
}

/* Snap the image and the title/price/desc column to the top of the
   media row (was vertically centered). Reads as image-anchored top-
   left of the card content area; title flows naturally below. */
.app-cx .addon-card-media {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
  position: relative;
}
.app-cx .addon-card-media > img,
.app-cx .addon-card-media > .img-placeholder {
  width: 120px;
  height: 120px;
  border-radius: var(--radius-md);
  object-fit: cover;
  object-position: 50% 20%;
  background: #f3f4f6;
}

/* Title and price share one baseline-aligned row: name on the left,
   price pinned to the right. Replaces the stacked name-over-price
   layout so the card reads as a product row, not a flat list. */
.app-cx .addon-title-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.app-cx .addon-title-row .addon-name  { margin: 0; }
.app-cx .addon-title-row .addon-price { margin: 0; white-space: nowrap; }

.app-cx .addon-name {
  font-size: var(--text-base);
  font-weight: 600;
  margin: 0 0 4px;
}
.app-cx .addon-price {
  color: var(--color-primary);
  font-weight: 700;
  font-size: var(--text-base);
  margin-bottom: var(--space-2);
}
.app-cx .addon-desc {
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  margin: 0;
}

/* Smaller, tighter button inside an addon card-footer than the
   default cx button — keeps the action proportional to the card
   without dominating it. Bumped to a wider full-width target at
   tablet/mobile where the card stacks. The min-width keeps the
   button stable across the label cycle (Add, Adding…, Remove,
   Removing…) so the icon and text do not jump as the AJAX state
   updates the inner content. */
.app-cx .addon-card .card-footer .btn {
  padding: 6px 12px;
  min-height: 36px;
  min-width: 130px;
  font-size: 13px;
  /* Hard-lock the width so the button never reflows as the AJAX
     label cycles (Add / Adding… / Remove / Removing…). This also
     opts the addon button out of the generic
     .card-footer .btn { width: 100% } that the global max-width:
     480 rule applies -- the addon button only goes full-width when
     the card itself stacks (max-width: 380); in the image-left
     horizontal band it holds a fixed 130px. min-width pins it
     against flex-shrink in a tight footer. */
  width: 130px;
}

/* The image-left horizontal card holds from desktop all the way
   down: the addons grid is already 1-up by this point, so each
   card spans the column at full width and the 120px image + text
   column still reads well. Below 420 the text column drops under
   ~200px, so shrink the image to 88px to buy back room and keep
   the horizontal layout comfortable down to the stack point. */
@media (max-width: 420px) {
  .app-cx .addon-card-media { grid-template-columns: 88px minmax(0, 1fr); }
  .app-cx .addon-card-media > img,
  .app-cx .addon-card-media > .img-placeholder {
    width: 88px;
    height: 88px;
  }
}

/* At small-phone widths the 88px image + text column no longer
   coexist, so stack: full-width square image on top, content and
   a full-width button below. */
@media (max-width: 380px) {
  .app-cx .addon-card-media { grid-template-columns: minmax(0, 1fr); }
  .app-cx .addon-card-media > img,
  .app-cx .addon-card-media > .img-placeholder {
    width: 100%;
    height: auto;
    aspect-ratio: 1 / 1;
    margin-bottom: var(--space-2);
  }
  .app-cx .addon-card .card-footer .btn {
    width: 100%;
    padding: 6px;
    min-height: 40px;
  }
}

/* "Added" stamp — a check-only success-green disc on the top-left of
   the addon thumbnail. Replaces the corner pill, which sat on top of
   the price and could not share a row with a long addon name. The
   white ring + drop shadow keep the disc legible over any thumbnail
   colour or photo. */
.app-cx .addon-added-stamp {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 1;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--color-success);
  color: white;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 0 0 2px white, 0 1px 3px rgba(0, 0, 0, 0.25);
  animation: addon-stamp-in 150ms var(--ease-standard);
}
.app-cx .addon-added-stamp .icon { width: 14px; height: 14px; }
@keyframes addon-stamp-in {
  from { transform: scale(0.92); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .app-cx .addon-added-stamp { animation: none; }
}

/* ==================================================
   SESSION CARD + RECAP BAR
   The session-selection card on the qualifier picker and
   session-selection pages, plus the recap bar that
   replaces the qualifier column at the session step.
   ================================================== */

.app-cx .session-card {
  --sb-pad: var(--space-4);
  background: white;
  border: 2px solid var(--color-line);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-4);
  /* Clip the banner-suggest-inside header to the card's rounded
     corners. Note: any future absolutely-positioned overflow children
     (e.g. the dormant .suggested-corner-ribbon at top: -10px) would
     also be clipped — drop overflow:hidden if/when one of those
     variants is activated. */
  overflow: hidden;
  transition: border-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
  position: relative;
}

/* Inner content row. Padding lives here so the banner above can sit
   flush at the card edges without margin trickery. align-items
   defaults to stretch so the right column matches the image height
   and .session-info can anchor its button via margin-top: auto. */
.app-cx .session-card-body {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: var(--space-5);
  padding: var(--sb-pad);
}
/* `.suggested` is reserved for a future server-side state that means
   "system recommends this session" (no user pick yet). Today nothing
   emits it — the selector is left in place so the future render is
   one ERB change away. */
.app-cx .session-card.suggested { border-color: var(--color-primary); }

/* `.is-selected` = the user has picked this session. Green border with
   a thicker (4px) left edge plus a soft success-tinted body so the
   current selection reads as one unit at a glance, regardless of
   whether the user's qualifiers happen to fit it (the warning
   compound below adjusts the border tone but keeps the same fill). */
.app-cx .session-card.is-selected {
  border-color: var(--color-success);
  border-left-width: 4px;
  background: var(--color-success-bg);
}

/* Compound: the selected session does not cover all the user's current
   qualifier picks. Rendered as `.session-card.is-selected.qualifier-
   mismatch`; the compound selector keeps the override scoped to the
   selected card and only adjusts the border tone (the body fill comes
   from .is-selected). */
.app-cx .session-card.is-selected.qualifier-mismatch {
  border-color: var(--color-success-border);
}

/* Selected-card banner row. Shown above the card body when the user
   has picked this session ("Your Selection") with an inline divider
   carrying the same recommendation tag the system would show without
   a selection. */
.app-cx .session-card-banner {
  background: var(--color-success-bg);
  color: var(--color-success);
  font-weight: 700;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  padding: 10px var(--space-4);
  display: flex;
  align-items: center;
  gap: 6px;
  border-bottom: 1px solid var(--color-success);
}
.app-cx .session-card-banner .icon { width: 16px; height: 16px; }
.app-cx .session-card-banner-label { display: inline-flex; align-items: center; gap: 6px; }
.app-cx .session-card-banner-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding-left: var(--space-3);
  border-left: 1px solid var(--color-success);
  font-weight: 600;
}

/* Dormant: banner-suggest-inside is the future system-recommendation
   row (.session-card.suggested). Hidden until that state ships. */
.app-cx .session-card .banner-suggest-inside { display: none; }
.app-cx .session-card.suggested .banner-suggest-inside { display: flex; }

/* Dormant — RIBBON variant element. Declared so future
   variant-switching only needs a CSS rule change. */
.app-cx .your-selection-ribbon {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--color-success);
  color: white;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  margin-bottom: var(--space-2);
}
.app-cx .your-selection-ribbon .icon { width: 12px; height: 12px; }

@media (max-width: 600px) {
  .app-cx .session-card-body { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); }
  /* Image becomes a full-width banner header on narrow viewports — the
     gutter the centered fixed-square left at this width was the worst
     part of the prior collapse. Cover-fits so each tier still leads
     with a real photo crop. */
  .app-cx .session-card .session-photo {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
    align-self: stretch;
  }
  .app-cx .session-card .session-photo img { object-fit: cover; max-width: 100%; max-height: 100%; }
  /* Select stretches to the bottom thumb zone. */
  .app-cx .session-card .session-actions .btn { width: 100%; }
}

.app-cx .session-photo {
  width: 200px;
  height: 200px;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #f3f4f6;
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: start;
  flex-shrink: 0;
}
.app-cx .session-photo img { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }

/* .session-photo.muted = direct-link-specific empty state when the order
   has no sex yet. The model defaults the image to the boy variant in
   that case, which would make the card look settled while the recap
   chip and disabled Continue say it isn't. The muted treatment
   desaturates the default photo and lays a dark mid-image bar that
   reads as "preview pending — go pick gender." Position: relative
   scopes the overlay to this slot. */
.app-cx .session-photo.muted { position: relative; }
.app-cx .session-photo.muted img { filter: grayscale(75%) brightness(0.85); }
.app-cx .session-photo.muted::after {
  content: 'Pick gender to preview';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(17, 24, 39, 0.85);
  color: white;
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  padding: 10px var(--space-3);
  text-align: center;
}

.app-cx .session-info {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
}

/* Top row: name fills, price anchors right. H3 wraps to a second line
   on long names (flex: 1 + min-width: 0); price stays full-size. */
.app-cx .session-info .session-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.app-cx .session-info .session-top h3 {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: 1.25;
}
.app-cx .session-info .session-top .session-price {
  flex: 0 0 auto;
  margin: 0;
  font-size: var(--text-price-hero);
  font-weight: 700;
  color: var(--color-primary);
}

.app-cx .session-desc { font-size: var(--text-sm); color: var(--color-muted-strong); margin: 0; }

/* Pin Select to the card's bottom-right. margin-top: auto absorbs any
   slack between the description and the cell bottom, so cards with
   short descriptions still anchor the action at the same corner as
   long-description cards do — predictable target position across
   the list (Fitts/Jakob). */
.app-cx .session-actions {
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
  justify-content: flex-end;
  margin-top: auto;
  padding-top: var(--space-3);
}

/* Recap bar — semantic <dl> + CSS pseudo separators */
.app-cx .recap-bar {
  background: white;
  border-radius: var(--radius-lg);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--color-ink);
  box-shadow: var(--shadow-sm);
  flex-wrap: wrap;
}
.app-cx .recap-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  align-items: center;
  margin: 0;
  padding: 0;
}
.app-cx .recap-summary > div {
  display: flex;
  gap: 4px;
  align-items: baseline;
  padding: 0 var(--space-3);
  position: relative;
}
.app-cx .recap-summary > div:first-child { padding-left: 0; }
.app-cx .recap-summary > div:not(:last-child)::after {
  content: "";
  position: absolute;
  right: 0;
  top: 4px;
  bottom: 4px;
  width: 1px;
  background: var(--color-line-strong);
}
.app-cx .recap-summary dt { font-weight: 600; margin: 0; }
.app-cx .recap-summary dd { margin: 0; color: var(--color-muted-strong); }
.app-cx .recap-edit { padding: 8px 12px; min-height: 36px; font-size: var(--text-sm); }
@media (max-width: 600px) {
  .app-cx .recap-bar { padding: var(--space-3); flex-direction: column; align-items: flex-start; }
  .app-cx .recap-edit { align-self: flex-end; }
  .app-cx .recap-summary > div { padding: 2px 0; }
  .app-cx .recap-summary > div:not(:last-child)::after { display: none; }
}

/* ==================================================================
   Recap chip bar — locked ship-candidate design (A2 chips + popover
   edit). Replaces the older `.recap-summary` definition-list pattern;
   the outer `.recap-bar` container is reused so existing card chrome
   carries over. Three chips: School (i-cap), Gender (i-gender),
   Choices (i-camera). Each chip opens an a11y-dialog popover for that
   field. Source: mockups/modernization-mockups-ship-candidate.html.
   ================================================================== */
.app-cx .recap-chips {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  width: 100%;
}
.app-cx .recap-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 8px 14px;
  min-height: 36px;
  border: 1px solid var(--color-line-strong);
  border-radius: var(--radius-pill);
  background: white;
  color: var(--color-ink);
  text-decoration: none;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard),
              box-shadow var(--duration-fast) var(--ease-standard);
}
.app-cx .recap-chip:hover,
.app-cx .recap-chip:focus-visible {
  border-color: var(--color-primary-soft);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
}

/* Empty/placeholder state — chip has no value yet (e.g., direct-link
   landing before the user picks school or gender). Dashed border +
   italicized placeholder copy reads as "fillable input" rather than
   "settled value," without raising an alarm. */
.app-cx .recap-chip.is-empty {
  border-style: dashed;
  border-color: var(--color-warning);
}
.app-cx .recap-chip.is-empty .recap-chip-icon {
  color: var(--color-warning);
}
.app-cx .recap-chip.is-empty .recap-chip-value {
  color: var(--color-muted-strong);
  font-style: italic;
  font-weight: 500;
}
.app-cx .recap-chip-icon {
  width: 16px;
  height: 16px;
  color: var(--color-muted-strong);
  flex-shrink: 0;
}
.app-cx .recap-chip-value {
  color: var(--color-ink);
  font-weight: 600;
  min-width: 0;
  overflow: hidden;
  text-overflow: clip;
  white-space: nowrap;
}
.app-cx .recap-chip-edit {
  width: 12px;
  height: 12px;
  color: var(--color-muted);
  flex-shrink: 0;
}
.app-cx .recap-chip-sep {
  width: 14px;
  height: 14px;
  color: var(--color-muted);
  flex-shrink: 0;
  align-self: center;
}

/* Responsive — Hybrid strategy (locked):
   <720px: 2-row grid. Row 1 = School (full width — accommodates avg-25-char
     names like "Roosevelt Academy"). Row 2 = Gender (auto-sized to content)
     + Choices (1fr — takes remaining). Qualifiers chip switches to the
     "+N" truncated value to fit the narrower column. The direct-link
     variant (school + gender, no Choices chip) collapses to one column
     so Gender stretches full width instead of stranding the empty
     Choices track beside it.
   <481px: vertical stack; recap-bar drops desktop chrome (no shadow / no
     rounded corners / minimal padding) so chips read as a true mobile recap
     row, not as a card containing chips. */
@media (max-width: 720px) and (min-width: 481px) {
  .app-cx .recap-chips {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas:
      "school   school"
      "gender   choices";
    gap: 6px;
  }
  .app-cx .recap-chips > .recap-chip-sep { display: none; }
  .app-cx .recap-chip[data-recap-field="school"]     { grid-area: school;  width: 100%; justify-content: space-between; }
  .app-cx .recap-chip[data-recap-field="gender"]     { grid-area: gender;  justify-content: space-between; }
  .app-cx .recap-chip[data-recap-field="qualifiers"] { grid-area: choices; width: 100%; justify-content: space-between; min-width: 0; }
  .app-cx .recap-chips--direct {
    grid-template-columns: 1fr;
    grid-template-areas:
      "school"
      "gender";
  }
  .app-cx .recap-chips--direct .recap-chip[data-recap-field="gender"] { width: 100%; }
}
@media (max-width: 480px) {
  .app-cx .recap-bar {
    box-shadow: none;
    padding: var(--space-2) 0;
    border-radius: 0;
    background: transparent;
    border-top: 1px solid var(--color-line);
    border-bottom: 1px solid var(--color-line);
  }
  .app-cx .recap-chips {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
  }
  .app-cx .recap-chips > .recap-chip-sep { display: none; }
  .app-cx .recap-chip { width: 100%; justify-content: space-between; }
}

/* ==================================================================
   Sticky chrome. sticky_chrome.js adds .cx-sticky-chrome to <body> on
   every customer page and measures the live stack heights into the
   --cx-* custom properties read below. The testing banner pins at the
   top of the viewport and the studio banner pins full-width beneath it;
   the banner compresses to a slim bar (.is-compressed) once the page
   scrolls far enough that the first content element reaches it — the
   same shrink-on-scroll behavior on every page. On the session page,
   where a recap bar is present, the recap bar also pins beneath the
   banner and takes a solid fill once stuck (.is-recap-stuck). Both
   states toggle off thresholds that do not move when the banner
   compresses, so there is no jitter and the banner always expands again
   at the top. Gated on .cx-sticky-chrome so the rules stay inert if the
   script does not run.
   ================================================================== */
/* Disable scroll anchoring for this layout. When the banner compresses
   it gives up normal-flow height; with anchoring on, the browser shifts
   scrollY to compensate, which re-crosses the compression trigger and
   makes the logo flap while scrolling through the compression band. Off,
   scrollY stays put and the content simply slides up under the shrinking
   banner — the intended shrink-on-scroll motion, and what Safari (which
   has no scroll anchoring) already does. */
.app-cx.cx-sticky-chrome { overflow-anchor: none; }
.app-cx.cx-sticky-chrome .app-banner-testing {
  position: sticky;
  top: 0;
  z-index: 30;
}
.app-cx.cx-sticky-chrome .studio-banner {
  position: sticky;
  top: var(--cx-testing-h, 0px);
  z-index: 20;
  max-width: none;
  margin: 0;
  width: 100%;
  background: var(--color-bg);
  transition: padding var(--duration-normal) var(--ease-standard),
              box-shadow var(--duration-normal) var(--ease-standard);
}
/* The banner's background is the page color, so when it is pinned over
   scrolling content it needs an edge. Use a shadow (not a border) so it
   appears only once the banner is compressed over content -- no stray
   hairline at rest, and no 1px reflow from toggling a border. */
.app-cx.cx-sticky-chrome.is-compressed .studio-banner {
  padding-top: 6px;
  padding-bottom: 6px;
  box-shadow: var(--shadow-md);
}
.app-cx.cx-sticky-chrome .studio-banner-link img {
  max-height: 80px;
  transition: max-height var(--duration-normal) var(--ease-standard);
}
.app-cx.cx-sticky-chrome.is-compressed .studio-banner-link img {
  max-height: 36px;
}
.app-cx.cx-sticky-chrome .recap-bar {
  position: sticky;
  top: var(--cx-recap-top, 0px);
  z-index: 10;
  transition: top var(--duration-normal) var(--ease-standard);
}
/* Opaque background + lift only while pinned. At rest this keeps the
   <481px recap bar transparent (its mobile "not a card" look); pinned,
   it needs a solid fill so scrolled content does not show through. */
.app-cx.cx-sticky-chrome.is-recap-stuck .recap-bar {
  background: var(--color-card);
  box-shadow: var(--shadow-md);
}
/* Repin the sticky cart below the whole chrome stack so the banner
   does not cover its header. Mirrors the >=901px guard on the base
   sticky-cart rule. */
@media (min-width: 901px) {
  .app-cx.cx-sticky-chrome .layout-sidebar > #cart {
    top: calc(var(--cx-recap-top, 0px) + var(--cx-recap-h, 0px) + var(--space-4));
  }
}
@media (prefers-reduced-motion: reduce) {
  .app-cx.cx-sticky-chrome .studio-banner,
  .app-cx.cx-sticky-chrome .studio-banner-link img,
  .app-cx.cx-sticky-chrome .recap-bar { transition: none; }
}

/* ==================================================================
   Recap-picker dialog body — shared styling for the school / gender /
   qualifier picker forms loaded into #recap-picker-body via alp_path.
   ================================================================== */
.app-cx .recap-picker-label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted-strong);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: var(--space-2);
}
.app-cx .recap-picker-fieldset {
  border: 0;
  padding: 0;
  margin: 0;
}

/* Qualifier picker — squares grid by default */
.app-cx .recap-picker-q-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
}
.app-cx .recap-picker-q-tile {
  position: relative;
  aspect-ratio: 1 / 1;
  border: 2px solid var(--color-line-strong);
  border-radius: var(--radius-md);
  overflow: hidden;
  cursor: pointer;
  background: var(--color-bg);
  transition: border-color var(--duration-fast) var(--ease-standard);
  display: block;
}
.app-cx .recap-picker-q-tile:hover {
  border-color: var(--color-primary-soft);
}
.app-cx .recap-picker-q-tile:focus-within {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.18);
}
.app-cx .recap-picker-q-tile.is-selected {
  border-color: var(--color-primary);
}
.app-cx .recap-picker-q-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.app-cx .recap-picker-q-name {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 6px 10px;
  background: rgba(0, 0, 0, 0.55);
  color: white;
  font-size: var(--text-xs);
  font-weight: 600;
  text-shadow: 0 1px 2px rgba(0,0,0,0.6);
}
.app-cx .recap-picker-q-check {
  position: absolute;
  top: 6px; right: 6px;
  width: 24px; height: 24px;
  background: var(--color-primary);
  color: white;
  border-radius: 50%;
  display: none;
  align-items: center;
  justify-content: center;
}
.app-cx .recap-picker-q-check svg { width: 14px; height: 14px; }
.app-cx .recap-picker-q-tile.is-selected .recap-picker-q-check { display: inline-flex; }
.app-cx .recap-picker-q-tile.is-required { cursor: default; }
.app-cx .recap-picker-q-tile.is-required:hover { border-color: var(--color-line-strong); }
.app-cx .recap-picker-q-tile.is-required .recap-picker-q-check { display: none; }
.app-cx .recap-picker-q-required-badge {
  position: absolute;
  top: 6px; left: 6px;
  background: var(--color-success);
  color: white;
  font-size: 10px;
  padding: 3px 8px;
  border-radius: var(--radius-pill);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.app-cx .recap-picker-q-required-badge .icon { width: 10px; height: 10px; }

/* Suggested-banner variants. Locked: variant A
   (.banner-suggest-inside, rendered inside the suggested
   session-card). Other variants below are declared but
   hidden by default — kept so a future variant change is
   one CSS rule away rather than re-importing the whole
   block. */

.app-cx .session-card.is-selected .banner-suggest-inside,
.app-cx .session-card.suggested .banner-suggest-inside {
  margin: 0;
  border-left: none;
  border-bottom: 1px solid var(--color-suggest-border);
  border-radius: 0;
}

/* Dormant suggested-* variants — corner ribbon, outside
   banner, inline tag, coverage indicator. Hidden by
   default; mockup demo'd switching via body classes. */
.app-cx .session-card .suggested-corner-ribbon,
.app-cx .session-card .suggested-inline-tag,
.app-cx .session-card .suggested-coverage,
.app-cx .suggested-outside-banner { display: none; }

.app-cx .suggested-corner-ribbon {
  position: absolute;
  top: -10px;
  left: var(--space-4);
  background: var(--color-primary);
  color: white;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  align-items: center;
  gap: 4px;
  box-shadow: var(--shadow-sm);
  z-index: 2;
  white-space: nowrap;
}
.app-cx .suggested-corner-ribbon .icon { width: 12px; height: 12px; }

.app-cx .suggested-outside-banner {
  background: var(--color-suggest-bg);
  border-left: 4px solid var(--color-suggest-border);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--color-primary);
  margin-bottom: var(--space-3);
  min-height: 44px;
  align-items: center;
  gap: var(--space-2);
}
.app-cx .suggested-outside-banner .icon { color: var(--color-primary); flex-shrink: 0; }
.app-cx .suggested-outside-banner strong { font-weight: 700; }

.app-cx .suggested-inline-tag {
  background: var(--color-suggest-bg);
  color: var(--color-primary);
  border: 1px solid var(--color-suggest-border);
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  vertical-align: middle;
  margin-left: var(--space-2);
  align-items: center;
  gap: 3px;
  white-space: nowrap;
}
.app-cx .suggested-inline-tag .icon { width: 11px; height: 11px; }

.app-cx .suggested-coverage {
  font-size: var(--text-xs);
  color: var(--color-success);
  font-weight: 600;
  margin: 0 0 var(--space-2);
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
}
.app-cx .suggested-coverage .icon { width: 14px; height: 14px; flex-shrink: 0; }
.app-cx .suggested-coverage strong { color: var(--color-ink); font-weight: 700; margin-right: 2px; }
.app-cx .suggested-coverage .coverage-list { color: var(--color-muted-strong); font-weight: 500; }

/* ==================================================
   LAYOUT GRIDS + TOP BAND
   Two-column desktop layout (main content + sticky cart)
   and the direct-link landing-page top band.
   ================================================== */

.app-cx .layout-sidebar {
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: var(--space-5);
  align-items: flex-start;
}
@media (max-width: 900px) {
  .app-cx .layout-sidebar { grid-template-columns: 1fr; }
  /* Stack the order summary above the form on mobile — billing only.
     On the picker pages (main/show, extras/index, sessions/show) the
     cart is a recap of choices in progress, not a checkout artifact,
     so it stays below the content on narrow viewports where the user
     is reading downward. order is visual only — DOM order, tab
     order, and screen-reader order all remain form-first, which is
     the correct behavior on a checkout page (typing > recapping). */
  .app-cx .billing-layout > #cart { order: -1; }
}

/* Sticky cart — pin #cart to the top of the viewport so it follows
   the user as they scroll past taller #sessions content. Applied to
   the grid item itself (not the inner .cart-sidebar) because the
   inner element's direct parent is content-sized, leaving sticky no
   room to stick; the grid item uses the grid row as its containing
   block, which is row-height tall. align-self: start keeps the cart
   content-sized within its cell; max-height + overflow-y let very
   tall carts scroll internally instead of overflowing the viewport.
   Skipped under 901px where layout-sidebar collapses to one column. */
@media (min-width: 901px) {
  .app-cx .layout-sidebar > #cart {
    position: sticky;
    top: var(--space-4);
    align-self: start;
    max-height: calc(100vh - var(--space-4) * 2);
    overflow-y: auto;
  }
}

/* Order partial inside a layout-sidebar drops its built-in
   action row. Its "Back to Sessions" / "Back to Start" /
   "Continue to Checkout" buttons are meant for the legacy
   single-column layout where the partial sat above the form;
   in the layout-sidebar grid the form is right next to the
   summary, so the form's submit button is the real Continue
   and the back-nav doubles up with browser controls. */
.app-cx .layout-sidebar #cart .btn-row { display: none; }

/* At narrow viewports the desktop cart-sidebar is replaced by the
   .mobile-cart-dialog bottom-sheet. Hide the sidebar so cart content
   isn't rendered (and hooked) twice. The dialog ALSO renders
   shared/cart (which outputs .cart.cart-sidebar), so the second rule
   un-hides the dialog's nested instance with higher specificity. */
@media (max-width: 600px) {
  .app-cx .cart-sidebar { display: none; }
  .app-cx .mobile-cart-dialog .cart-sidebar { display: block; }
}

.app-cx .top-band {
  background: white;
  padding: var(--space-4);
  margin-bottom: var(--space-4);
  border-radius: var(--radius-lg);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  align-items: start;
  box-shadow: var(--shadow-sm);
}
.app-cx .top-band .subsection-header { margin-bottom: 6px; }
@media (max-width: 600px) { .app-cx .top-band { grid-template-columns: 1fr; } }

/* Legacy override: style.css declares .cta-help-msg with color: #FFFFFF
   for the old dark-blue-button context. In the modernized direct-link
   layout the message renders below the Continue button on a light card
   surface — flip to a muted ink that reads against white/success-bg. */
.app-cx .cta-help-msg { color: var(--color-muted-strong); }

/* ==================================================
   MOBILE CART
   Sticky bar at ≤600px; opens .mobile-cart-dialog
   (a11y-dialog markup) as a bottom-sheet variant of the
   base .dialog. The bar is a separate element so it stays
   visible regardless of dialog open/close state. JS only
   flips an `is-open` class on the bar so the chevron can
   rotate; everything else (ARIA, focus trap, ESC, backdrop
   dismissal) belongs to a11y-dialog.
   ================================================== */

.app-cx .mobile-cart-bar { display: none; }

@media (max-width: 600px) {
  .app-cx .mobile-cart-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 50;
    background: var(--color-primary);
    color: white;
    padding: var(--space-3) var(--space-4);
    font-weight: 500;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.15);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    transition: border-radius var(--duration-fast) var(--ease-standard);
  }
}

.app-cx .mobile-cart-bar.is-open { border-radius: 0; }
.app-cx .mobile-cart-bar .total { font-weight: 700; font-size: var(--text-price-total); }
.app-cx .mobile-cart-bar .label { font-size: var(--text-xs); opacity: 0.9; display: block; }

.app-cx .mobile-cart-open {
  appearance: none;
  background: transparent;
  border: none;
  padding: 0;
  margin: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  text-align: left;
}

.app-cx .mobile-cart-totals-text { display: inline-flex; flex-direction: column; line-height: 1.2; }

.app-cx .mobile-cart-chev {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  transition: transform var(--duration-normal) var(--ease-standard);
}
.app-cx .mobile-cart-bar.is-open .mobile-cart-chev { transform: rotate(180deg); }

.app-cx .mobile-cart-cta {
  appearance: none;
  -webkit-appearance: none;
  min-height: 36px;
  padding: 8px 14px;
  background: white;
  color: var(--color-primary);
  font-family: inherit;
  font-weight: 600;
  font-size: var(--text-sm);
  border-radius: var(--radius-md);
  border: none;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard);
}
.app-cx .mobile-cart-cta:hover { background: var(--color-bg); }
.app-cx .mobile-cart-cta:active { background: var(--color-line); }
.app-cx .mobile-cart-cta:focus-visible {
  outline: 2px solid white;
  outline-offset: 2px;
}
.app-cx .mobile-cart-cta .icon { width: 14px; height: 14px; }

/* Bottom-sheet override of the centered .dialog-content.
   The `bottom` value is offset by the bar height so the
   bar (which paints above the dialog at z:50) doesn't
   cover the dialog content's last line. The CSS variable
   is set in mobile_cart.js from the bar's measured height
   on load and resize. The fallback covers the moment
   before JS runs. */
/* Drop the mobile-cart wrapper below the sticky bar (z:50) so the
   slide-up animation reveals the sheet from behind the bar instead
   of covering it from above. Other dialogs keep the default z:100. */
.app-cx .dialog.mobile-cart-dialog { z-index: 40; }

.app-cx .mobile-cart-dialog .dialog-content {
  position: fixed;
  inset: auto 0 0 0;
  bottom: var(--mobile-cart-bar-height, 64px);
  margin: 0;
  width: 100%;
  max-width: 100%;
  max-height: 70vh;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  animation: dialog-slide-up var(--duration-slow) var(--ease-standard);
}

@keyframes dialog-slide-up {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

.app-cx .mobile-cart-dialog .cart-line { font-size: var(--text-sm); padding: 6px 0; }
/* The sticky bar already shows the running total, so the sheet must
   not repeat it. Suppressed in CSS rather than via a render-time
   local so it stays hidden across cart-body refreshes too. */
.app-cx .mobile-cart-dialog .cart-line-total { display: none; }

/* The .cart partial carries card chrome (white bg, padding,
   radius, shadow) for the desktop cart-sidebar. Inside the
   dialog body that produces a card-within-a-card with
   stacked padding. Strip the inner chrome so the dialog
   body is the only container. */
.app-cx .mobile-cart-dialog .cart {
  background: transparent;
  padding: 0;
  border-radius: 0;
  box-shadow: none;
}

/* Suggest-tinted callout for value-prop framing inside dialog
   bodies (e.g., the upsell modal's price-delta line). Uses the
   same suggest-bg + primary tokens as the recommended-state
   pattern shipped on the session-card family. */
.app-cx .dialog-callout-suggest {
  background: var(--color-suggest-bg);
  color: var(--color-primary);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-lg);
  font-weight: 600;
  text-align: center;
  margin-bottom: var(--space-3);
}
.app-cx .dialog-callout-suggest strong { font-weight: 700; }
@media (max-width: 480px) {
  .app-cx .dialog-callout-suggest {
    font-size: var(--text-md);
    padding: var(--space-2) var(--space-3);
  }
}

/* Upsell modal — editorial card. Asymmetric image-LEFT plus
   price-RIGHT pair below a serif headline, separated from a prose
   description by a hairline divider. Frames the locked 120x120
   source image as an intentional gallery print (white mat plus
   hairline plus soft shadow) instead of treating it as an
   undersized hero. Description retains its admin-authored
   paragraph structure with scoped <p> margins so nested rich-text
   does not inherit browser defaults that would expand the modal. */
.app-cx .upsell-session-name {
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
  color: var(--color-ink);
  margin: 0 0 var(--space-4);
}
.app-cx .upsell-pitch {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  margin-bottom: var(--space-4);
}
.app-cx .upsell-portrait {
  flex-shrink: 0;
  width: 120px;
  height: 120px;
  border-radius: var(--radius-sm);
  background: white;
  box-shadow:
    0 0 0 1px var(--color-line),
    0 0 0 6px white,
    var(--shadow-md);
}
.app-cx .upsell-image {
  width: 100%;
  height: 100%;
  display: block;
  border-radius: var(--radius-sm);
  object-fit: cover;
}
.app-cx .upsell-delta {
  margin: 0;
  font-size: 28px;
  font-weight: 500;
  line-height: 1.15;
  color: var(--color-ink);
}
.app-cx .upsell-delta strong {
  font-weight: 700;
  color: var(--color-primary);
}
.app-cx .upsell-divider {
  border: 0;
  height: 1px;
  background: var(--color-line);
  margin: var(--space-4) 0;
}
.app-cx .upsell-description {
  margin: 0;
  font-size: var(--text-base);
  color: var(--color-ink);
  line-height: 1.6;
}

/* Stack the pitch row below 400px so the image and price stop
   competing for horizontal room on small phones. */
@media (max-width: 400px) {
  .app-cx .upsell-pitch { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
  .app-cx .upsell-delta { font-size: 22px; }
}

/* Upsell-specific dialog width — 600 keeps the image+price horizontal
   pair anchored without diluting the photo into too much frame, while
   giving the WYSIWYG bullet description enough line length to stop
   wrapping every clause. Stays narrower than the default so the dialog
   reads as image-first, not prose-first. */
.app-cx #upsell-dialog .dialog-content { max-width: 600px; }

/* Qualifier-details dialog — wider than default because the body lays
   the qualifier image (~96px) beside the description prose. Without
   the extra room the text column gets cramped to ~70 chars next to
   the image. 640 yields a ~66-char text column at 14px Inter, inside
   the comfortable reading band. */
.app-cx #qualifier-details-dialog .dialog-content { max-width: 640px; }

/* Warning-accent variant for dialogs whose header should signal
   a recoverable corrective state (incompatible-session message,
   etc). Apply by adding .dialog-incompat-accent to the .dialog
   wrapper. Header background, ink, and bottom-border-color flip
   to the warning-amber palette; the warning glyph picks up the
   warning fill so it reads as part of the header treatment. */
.app-cx .dialog-incompat-accent .dialog-header {
  background: var(--color-warning-bg);
  color: var(--color-warning-ink);
  border-bottom-color: var(--color-warning);
}
.app-cx .dialog-incompat-accent .dialog-header .icon { color: var(--color-warning); }

/* Upgrade modal — body content. Studio-customizable incompat
   message at top, then a flex-row of qualifier thumbnails (the
   choices that conflict with the current session) with the
   qualifier name centered beneath each thumb. */
.app-cx .upgrade-incompat-message {
  font-size: var(--text-base);
  color: var(--color-ink);
  line-height: 1.6;
  margin: 0 0 var(--space-4);
}
.app-cx .upgrade-incompat-message strong { font-weight: 600; }
.app-cx .upgrade-qualifiers {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: center;
}
.app-cx .upgrade-qualifier {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  flex: 0 0 auto;
  max-width: 96px;
}
.app-cx .upgrade-qualifier-thumb {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-line);
  overflow: hidden;
  background: var(--color-bg);
}
.app-cx .upgrade-qualifier-thumb img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
.app-cx .upgrade-qualifier-name {
  font-size: var(--text-xs);
  color: var(--color-muted-strong);
  text-align: center;
  line-height: 1.3;
}

/* Fill-up modal — body content. Progress bar plus a grid of
   checkbox tiles for the qualifiers the user has not yet picked.
   Tiles are full-row click targets (label wraps a checkbox); the
   checked state tints the tile so the selection is unambiguous. */
.app-cx .fill-up-progress {
  margin-bottom: var(--space-4);
}
.app-cx .fill-up-progress-text {
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  margin: 0 0 var(--space-2);
}
.app-cx .fill-up-progress-bar {
  height: 8px;
  background: var(--color-line);
  border-radius: 4px;
  overflow: hidden;
}
.app-cx .fill-up-progress-bar-fill {
  height: 100%;
  background: var(--color-success);
  border-radius: 4px;
  transition: width var(--duration-slow) var(--ease-standard);
}
.app-cx .fill-up-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (max-width: 480px) {
  .app-cx .fill-up-grid { grid-template-columns: 1fr; }
}
.app-cx .fill-up-tile {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  min-height: 44px;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  background: white;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard),
              background-color var(--duration-fast) var(--ease-standard);
}
.app-cx .fill-up-tile:hover { border-color: var(--color-primary-soft); }
.app-cx .fill-up-tile:focus-within {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.18);
}
.app-cx .fill-up-tile.is-checked {
  border-color: var(--color-primary);
  background: var(--color-suggest-bg);
}
.app-cx .fill-up-tile input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin: 0;
  flex-shrink: 0;
  accent-color: var(--color-primary);
  cursor: pointer;
}
.app-cx .fill-up-tile-thumb {
  width: 60px;
  height: 60px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-line);
  overflow: hidden;
  background: var(--color-bg);
  flex-shrink: 0;
}
.app-cx .fill-up-tile-thumb img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}
.app-cx .fill-up-tile-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--color-ink);
  flex: 1;
  min-width: 0;
}
.app-cx .fill-up-empty {
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  text-align: center;
  margin: var(--space-4) 0 0;
}
.app-cx .fill-up-footnote {
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  margin: 0;
  flex: 1;
  min-width: 0;
}
.app-cx .fill-up-footnote.is-ready { color: var(--color-success); font-weight: 600; }

/* Fill-up footer — message on the left, submit button on the right.
   Overrides the default flex-end on dialog-footer for this dialog
   only. Mobile keeps the dialog-footer's column-reverse stack so the
   primary action lands above the message under thumb. */
.app-cx #fill-up-dialog .dialog-footer {
  justify-content: space-between;
  align-items: center;
}
@media (max-width: 480px) {
  .app-cx #fill-up-dialog .dialog-footer { align-items: stretch; }
  .app-cx #fill-up-dialog .fill-up-footnote { text-align: center; }
}

/* Slate-filled awaiting state for the fill-up Continue button reads
   as "armed and waiting" rather than the default washed-primary that
   can look like a low-contrast normal button. Uses aria-disabled
   (not the native disabled attribute) so the button remains in the
   tab order and keyboard users can discover it. Reverts to the
   primary navy the moment aria-disabled flips. */
.app-cx #fill-up-dialog .btn-primary[aria-disabled="true"] {
  background: var(--color-muted-strong);
  border-color: var(--color-muted-strong);
  color: white;
  cursor: not-allowed;
}

/* ==================================================
   MODAL FAMILY
   Centered card with header / body / footer slots.
   .modal-incompat-accent is the warning-header variant
   for the upgrade modal's incompatible-session message.
   ================================================== */

.app-cx .modal-overlay {
  background: rgba(0, 0, 0, 0.5);
  padding: var(--space-5);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 500px;
  border-radius: var(--radius-md);
}
.app-cx .modal {
  background: white;
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 560px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  max-height: 90%;
}
.app-cx .modal-header {
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-line);
  font-size: var(--text-lg);
  font-weight: 600;
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-shrink: 0;
}
.app-cx .modal-close {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--color-muted-strong);
  padding: 8px;
  line-height: 1;
  min-width: 36px;
  min-height: 36px;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--duration-fast) var(--ease-standard);
}
.app-cx .modal-close:hover,
.app-cx .modal-close:focus-visible { background: var(--color-bg); color: var(--color-ink); }
.app-cx .modal-body { padding: var(--space-5); font-size: var(--text-base); line-height: var(--lh-normal); overflow-y: auto; }
.app-cx .modal-footer { padding: var(--space-4) var(--space-5); border-top: 1px solid var(--color-line); display: flex; gap: var(--space-3); justify-content: flex-end; flex-shrink: 0; }
@media (max-width: 480px) {
  .app-cx .modal-overlay { padding: var(--space-3); }
  .app-cx .modal-footer { flex-direction: column-reverse; }
  .app-cx .modal-footer .btn { width: 100%; }
}

/* Locked: Modal 14 (incompatible session) — warning-accent
   header. Applied directly to the modal element when used. */
.app-cx .modal-incompat-accent .modal-header {
  background: var(--color-warning-bg);
  color: var(--color-warning-ink);
  border-bottom-color: var(--color-warning);
}

/* Dormant: Modal 14 alternative — thumb-ring treatment
   for the incompatible-session image. Declared so a future
   variant change is one CSS rule away. */
.app-cx .modal-incompat-thumbring .incompat-thumb {
  width: 96px;
  height: 96px;
  border: 2px solid var(--color-warning);
  border-radius: var(--radius-md);
  box-shadow: 0 0 0 3px var(--color-warning-bg);
}

/* Dormant: Modal 15 alternative — bold-count treatment
   for the fill-up modal. Locked variant is the default
   (no extra class), so this is unused but declared. */
.app-cx .modal-fillup-bold-count .fillup-count {
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--color-primary);
  display: block;
  margin-bottom: 4px;
}

/* ==================================================
   FORM HELPERS — gender select + payment options
   Click-to-select card patterns for radio inputs.
   Includes the [data-pay-state] CSS-only toggle that
   shows/hides the credit-card fieldset based on whether
   the user picks pay-now vs pay-at-studio.
   ================================================== */

/* Gender select — two side-by-side cards, larger
   ("default" variant for the start screen) or compact
   (.gender-min variant for the qualifier picker). */
.app-cx .gender-options { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
.app-cx .gender-option {
  background: white;
  border: 2px solid var(--color-line);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  text-align: center;
  cursor: pointer;
  font-weight: 500;
  font-size: var(--text-base);
  font-family: inherit;
  min-height: 80px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color var(--duration-fast) var(--ease-standard),
              background var(--duration-fast) var(--ease-standard),
              color var(--duration-fast) var(--ease-standard);
}
.app-cx .gender-option:hover { border-color: var(--color-primary-soft); }
.app-cx .gender-option:focus-within {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(30, 58, 138, 0.18);
}
.app-cx .gender-option.selected { border-color: var(--color-primary); background: var(--color-suggest-bg); color: var(--color-primary); font-weight: 600; }

.app-cx .gender-min { display: flex; gap: var(--space-2); }
.app-cx .gender-min .gender-option { padding: var(--space-2) var(--space-3); min-height: 44px; flex: 1; font-size: var(--text-sm); }

/* Payment-option — radio-style click cards on the
   billing page. .selected applies the navy border +
   tinted background. */
.app-cx .payment-option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: 12px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-line);
  background: white;
  margin-bottom: 8px;
  min-height: 44px;
  cursor: pointer;
  transition: border-color var(--duration-fast) var(--ease-standard),
              background-color var(--duration-fast) var(--ease-standard);
  flex-wrap: wrap;
}
.app-cx .payment-option > input[type="radio"] { margin-top: 2px; }
.app-cx .payment-option > label { font-weight: 500; flex: 1; cursor: pointer; }
.app-cx .payment-option.selected {
  border: 2px solid var(--color-primary);
  background: var(--color-suggest-bg);
  padding: 11px;
}
.app-cx .payment-option.selected > label { font-weight: 600; }

/* Two-line label inside a payment-option: a bold main label and
   a muted sub-line carrying the amount or the timing detail. The
   sub-line keeps the option self-explanatory without forcing the
   user to look elsewhere on the page. */
.app-cx .payment-option-main { display: block; font-weight: 600; }
.app-cx .payment-option-sub  {
  display: block;
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  margin-top: 2px;
}
/* The label content (main + sub lines) grows to fill the row beside
   the radio input. */
.app-cx .payment-option-body { flex: 1; }

/* Scrollable Terms-of-Service panel inside the ToS card. Fixed
   height with internal scroll so a long policy does not push the
   accept-checkbox and submit button below the fold. */
.app-cx .tos-scroll {
  overflow: auto;
  height: 200px;
  margin-bottom: var(--space-3);
  padding: var(--space-3);
  background: var(--color-page-bg, #f9fafb);
  border-radius: var(--radius-md);
}

/* Centered muted helper text -- the "after submitting..." note and
   the dual pay-helper line that bracket the submit button. */
.app-cx .cx-form-note {
  text-align: center;
  color: var(--color-muted-strong);
  font-size: var(--text-sm);
  margin: var(--space-2) 0;
}
/* The charged-amount inside the pay-helper line lifts to ink so the
   dollar figure reads stronger than the surrounding muted copy. */
.app-cx .cx-form-note strong { color: var(--color-ink); }

/* Fee advisory — info-tinted single-line note that sits above the
   studio-mode fill panel (inside the fill slot). Pay-now mode hides
   the entire fill slot via display:none, so this rule no longer needs
   a visibility-hidden baseline; the slot's display toggle cascades. */
.app-cx .payment-fee-advisory {
  display: flex;
  margin: 0;
  background: var(--color-info-bg);
  border-left: 3px solid var(--color-info-border);
  padding: 6px var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-xs);
  color: var(--color-info-ink);
  align-items: center;
  gap: var(--space-2);
}
.app-cx .payment-fee-advisory .icon { color: var(--color-info-border); flex-shrink: 0; }

/* Credit-card / fill-content stack. Two children share a single grid
   cell so the payment card height does not jump when the user toggles
   between Pay now and Pay at the studio.

   Pay-now: fill slot removed via display:none, so the cell sizes only
            to the credit-card fields (~140px).
   Studio:  fill slot becomes a flex column rendering the fee advisory
            above the active fill panel; credit-card fields keep
            visibility-hidden to anchor the cell at their reserved
            ~140px so the form below stays visually stable.

   Pay-now mode is now ~32px shorter overall since the advisory no
   longer reserves a permanent row above the credit-card fields. */
.app-cx .cc-fill-stack {
  display: grid;
  grid-template-areas: 'stack';
}
.app-cx .cc-fill-stack > * { grid-area: stack; }
.app-cx [data-pay-state="studio"] .cc-fill-stack [data-cc-fields] { visibility: hidden; }
.app-cx .cc-fill-stack [data-fill-slot] { display: none; }
.app-cx [data-pay-state="studio"] .cc-fill-stack [data-fill-slot] {
  display: flex;
  flex-direction: column;
  align-self: stretch;
  min-height: 0;
  gap: var(--space-2);
}

/* Studio-mode fill panel — geometric stripes wallpaper with an
   eyebrow pill and a serif headline showing the dollar amount due
   at the studio. Background pattern fills the panel regardless of
   the inner content size, eliminating the empty-space perception
   that a small content block in a large slot would otherwise have. */
.app-cx .fill-stripes {
  flex: 1;
  background-image: repeating-linear-gradient(135deg, rgba(30, 58, 138, 0.04) 0 8px, transparent 8px 16px);
  background-color: white;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.app-cx .fill-stripes .label {
  display: inline-block;
  background: white;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  font-weight: 700;
  color: var(--color-primary);
  border: 1px solid var(--color-line);
  margin-bottom: var(--space-2);
}
.app-cx .fill-stripes .headline {
  font-size: 22px;
  font-weight: 700;
  color: var(--color-ink);
  letter-spacing: -0.01em;
  line-height: 1.2;
}

/* Submit-button label and helper-text variants swap based on the
   active payment-state attribute on the form wrapper. */
.app-cx .submit-label-studio,
.app-cx .pay-helper-studio { display: none; }
.app-cx [data-pay-state="studio"] [data-submit-btn] .submit-label-now,
.app-cx [data-pay-state="studio"] [data-pay-helper] .pay-helper-now { display: none; }
.app-cx [data-pay-state="studio"] [data-submit-btn] .submit-label-studio,
.app-cx [data-pay-state="studio"] [data-pay-helper] .pay-helper-studio { display: inline; }

/* ==================================================
   UTILITIES — typography, disclosure, skeleton loaders,
   responsive show/hide.
   ================================================== */

/* Section headers — typographic anchors above page
   regions and form sections. */
.app-cx .section-header { font-size: 24px; font-weight: 700; color: var(--color-ink); margin: 0 0 var(--space-4); padding: 0; letter-spacing: -0.01em; }
/* Muted lede directly under a section-header. Negative top margin
   pulls it up tight against the header (which carries its own
   bottom margin). */
.app-cx .section-subhead { color: var(--color-muted-strong); margin: calc(var(--space-4) * -1) 0 var(--space-4); }
.app-cx .subsection-header { font-size: var(--text-xs); font-weight: 600; color: var(--color-muted-strong); text-transform: uppercase; letter-spacing: 0.5px; margin: 0 0 var(--space-2); }

/* Soft neutral pill — used to surface a piece of read-only context
   alongside a card header (e.g., the gender of the senior on the
   billing page, where the value is established earlier in the flow
   and shown here as wayfinding rather than an editable field). */
.app-cx .card-meta-chip {
  display: inline-flex;
  align-items: center;
  background: #f3f4f6;
  color: var(--color-muted-strong);
  font-size: var(--text-xs);
  font-weight: 500;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  margin-left: var(--space-2);
  vertical-align: middle;
}

/* Disclosure pattern — <details>-based, with a chevron
   that rotates 180° when open. */
.app-cx .disclosure summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-weight: 600;
  padding: var(--space-2) 0;
}
.app-cx .disclosure summary::-webkit-details-marker { display: none; }
.app-cx .disclosure summary .chev { transition: transform var(--duration-fast) var(--ease-standard); }
.app-cx .disclosure[open] summary .chev { transform: rotate(180deg); }

/* Skeleton loaders — shimmering placeholder bars for
   content that's still loading. */
.app-cx .skeleton {
  background: linear-gradient(90deg, #eef0f3 0%, #f8f9fb 50%, #eef0f3 100%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
@keyframes skeleton-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.app-cx .skeleton-line { height: 14px; margin: 6px 0; }
.app-cx .skeleton-line.w-60 { width: 60%; }
.app-cx .skeleton-line.w-40 { width: 40%; }
.app-cx .skeleton-line.w-80 { width: 80%; }
.app-cx .skeleton-image {
  width: 100%;
  height: 200px;
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
}

/* Responsive show/hide */
.app-cx .hide-on-mobile {}
@media (max-width: 480px) { .app-cx .hide-on-mobile { display: none !important; } }
.app-cx .show-on-mobile { display: none; }
@media (max-width: 480px) { .app-cx .show-on-mobile { display: block; } }

/* ==================================================
   DIALOG — host styles for a11y-dialog v8 markup.
   The library manages aria-hidden / focus trap / ESC /
   click-outside; all visual chrome lives here.
   Reduced-motion is covered by the global rule above.
   ================================================== */

.app-cx .dialog {
  position: fixed;
  inset: 0;
  z-index: 100;
}
.app-cx .dialog[aria-hidden="true"] { display: none; }

.app-cx .dialog-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.5);
  animation: dialog-fade-in var(--duration-slow) var(--ease-standard);
}

.app-cx .dialog-content {
  position: fixed;
  inset: 0;
  margin: auto;
  z-index: 2;
  width: 100%;
  max-width: 620px;
  max-height: 90vh;
  height: max-content;
  background: var(--color-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  animation: dialog-slide-in var(--duration-slow) var(--ease-standard);
}

@keyframes dialog-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes dialog-slide-in {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

.app-cx .dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-line);
  flex-shrink: 0;
}
.app-cx .dialog-header h2 { margin: 0; font-size: var(--text-lg); font-weight: 600; }

.app-cx .dialog-close {
  appearance: none;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  color: var(--color-muted-strong);
  font-size: 24px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  transition:
    background var(--duration-fast) var(--ease-standard),
    color      var(--duration-fast) var(--ease-standard);
}
.app-cx .dialog-close:hover { background: var(--color-bg); color: var(--color-ink); }

.app-cx .dialog-body {
  padding: var(--space-5);
  overflow-y: auto;
  flex-grow: 1;
  min-height: 0;
}
.app-cx .dialog-body p:last-child { margin-bottom: 0; }

.app-cx .dialog-footer {
  display: flex;
  gap: var(--space-3);
  justify-content: flex-end;
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--color-line);
  flex-shrink: 0;
  flex-wrap: wrap;
}
/* The upsell/upgrade modals wrap their two action buttons in a data-
   carrying div the JS reads; that div absorbed the footer's flex gap,
   leaving the buttons stacked tight. display: contents drops the wrapper
   from layout so the buttons become flex children of the footer and pick
   up its gap, end-justify, and the mobile column-reverse rule below —
   while the div stays in the DOM for the JS to read. */
.app-cx .dialog-footer > #upsell-form,
.app-cx .dialog-footer > #upgrade-form { display: contents; }

@media (max-width: 480px) {
  .app-cx .dialog-footer { flex-direction: column-reverse; }
  .app-cx .dialog-footer .btn { width: 100%; }
  .app-cx .dialog-content { max-width: calc(100vw - 16px); }
}

/* Qualifier Details modal — constrain the :detail image
   (960×481 paperclip variant) so it doesn't blow the dialog
   body to full viewport height. max-height keeps the
   description visible alongside the image. */
/* Image-lightbox variant — honors admin-uploaded promotional graphics
   that are wider than the standard dialog. No header bar; close X
   floats over the top-right of the image with a translucent scrim
   for contrast on light image regions. Caption below the image
   carries the qualifier name (also the aria-labelledby target). */
.app-cx .dialog.dialog-image-lightbox .dialog-content {
  max-width: min(95vw, 1100px);
  max-height: 90vh;
  background: white;
}
.app-cx .dialog-image-close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
  width: 44px;
  height: 44px;
  background: rgba(0, 0, 0, 0.6);
  color: white;
  border: none;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-standard);
}
.app-cx .dialog-image-close:hover { background: rgba(0, 0, 0, 0.85); }
.app-cx .dialog-image-close:focus-visible {
  outline: 2px solid white;
  outline-offset: 2px;
  background: rgba(0, 0, 0, 0.85);
}
.app-cx .dialog-image-close .icon { width: 20px; height: 20px; }
.app-cx .dialog-image-container {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg);
  min-height: 200px;
}
.app-cx .dialog-image-img {
  display: block;
  width: 100%;
  height: auto;
  max-height: 80vh;
  object-fit: contain;
}
.app-cx .dialog-image-caption {
  margin: 0;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  text-align: center;
  background: white;
  border-top: 1px solid var(--color-line);
}

.app-cx .qualifier-details-image {
  display: block;
  width: 100%;
  height: auto;
  max-height: 40vh;
  object-fit: cover;
  border-radius: var(--radius-md);
  margin-bottom: var(--space-3);
}
@media (max-width: 480px) {
  .app-cx .qualifier-details-image { max-height: 25vh; }
}
.app-cx .qualifier-details-description {
  margin: 0;
  color: var(--color-ink);
  font-size: var(--text-sm);
  line-height: 1.5;
}

/* ==================================================
   DIRECT-LINK LOCKED STATE
   Replaces homepage and session-picker bodies when an order
   was created via a direct booking link. The picker UI does
   not apply (school + session are already predetermined),
   so we show the locked summary and route the user back to
   the direct landing.
   ================================================== */
.app-cx .direct-locked {
  max-width: 480px;
  margin: var(--space-6) auto;
  padding: var(--space-6) var(--space-5) var(--space-4);
  text-align: center;
}
.app-cx .direct-locked-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--color-suggest-bg);
  color: var(--color-primary);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  margin-bottom: var(--space-4);
}
.app-cx .direct-locked-eyebrow .icon { width: 14px; height: 14px; }
.app-cx .direct-locked-headline {
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
  color: var(--color-ink);
  margin: 0 0 var(--space-3);
}
.app-cx .direct-locked-lede {
  font-size: var(--text-base);
  color: var(--color-muted-strong);
  line-height: 1.6;
  margin: 0 auto var(--space-5);
  max-width: 38ch;
}
.app-cx .direct-locked-summary {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0 0 var(--space-5);
  padding: var(--space-3) var(--space-4);
  background: white;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  text-align: left;
}
.app-cx .direct-locked-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
}
.app-cx .direct-locked-row dt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--color-muted-strong);
}
.app-cx .direct-locked-row dt .icon { width: 14px; height: 14px; }
.app-cx .direct-locked-row dd {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--color-ink);
  text-align: right;
}
.app-cx .direct-locked-row-empty dd {
  color: var(--color-muted-strong);
  font-weight: 500;
}
.app-cx .direct-locked-cta { margin-top: 0; }
/* Secondary escape hatch under the locked-card primary CTA for a
   customer who reached a direct link by mistake. */
.app-cx .direct-locked-startover {
  display: block;
  margin: var(--space-3) auto 0;
  background: none;
  border: none;
  color: var(--color-muted);
  font-size: var(--text-sm);
  text-decoration: underline;
  cursor: pointer;
}
.app-cx .direct-locked-startover:hover { color: var(--color-ink); }
@media (max-width: 480px) {
  .app-cx .direct-locked {
    padding: var(--space-5) var(--space-4) var(--space-4);
    margin: var(--space-4) auto;
  }
  .app-cx .direct-locked-headline { font-size: 24px; }
}

/* ==================================================
   ORDER-NOT-FOUND
   Terminal page in the customer flow when an order
   hash doesn't resolve. Visually paired with
   .direct-locked — same eyebrow chip, serif headline,
   lede, and block CTA, plus a muted contact line that
   stacks each tap row on mobile.
   ================================================== */
.app-cx .not-found {
  max-width: 480px;
  margin: var(--space-6) auto;
  padding: var(--space-6) var(--space-5);
  text-align: center;
}
.app-cx .not-found-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--color-suggest-bg);
  color: var(--color-primary);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  margin-bottom: var(--space-4);
}
.app-cx .not-found-headline {
  font-size: 28px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
  color: var(--color-ink);
  margin: 0 0 var(--space-3);
}
.app-cx .not-found-lede {
  font-size: var(--text-base);
  color: var(--color-muted-strong);
  line-height: 1.6;
  margin: 0 auto var(--space-5);
  max-width: 38ch;
}
.app-cx .not-found-cta { margin-bottom: var(--space-4); }
.app-cx .not-found-contact {
  font-size: var(--text-base);
  color: var(--color-muted-strong);
  margin: 0;
  line-height: 1.5;
}
.app-cx .not-found-contact a {
  color: var(--color-muted-strong);
  text-decoration: underline;
}
.app-cx .not-found-contact a:hover { color: var(--color-ink); }
.app-cx .not-found-contact .divider {
  color: var(--color-muted);
  margin: 0 6px;
}
@media (max-width: 480px) {
  .app-cx .not-found {
    padding: var(--space-5) var(--space-4);
    margin: var(--space-4) auto;
  }
  .app-cx .not-found-headline { font-size: 24px; }
  .app-cx .not-found-contact .divider { display: none; }
  .app-cx .not-found-contact .contact-line {
    display: block;
    padding: 12px 0;
  }
}

/* ==================================================
   TIPPY.JS THEME — 'session-selector'
   White-card popover chrome for customer-facing
   .tooltip-badge popovers. Scoped via data-theme so
   future Tippy upgrades cannot restyle defaults used
   elsewhere.
   ================================================== */
.tippy-box[data-theme~='session-selector'] {
  background: white;
  color: var(--color-ink);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  font-size: var(--text-sm);
  line-height: 1.5;
}
.tippy-box[data-theme~='session-selector'] .tippy-content {
  padding: var(--space-2) var(--space-3);
}
.tippy-box[data-theme~='session-selector'][data-placement^='top']    > .tippy-arrow::before { border-top-color: white; }
.tippy-box[data-theme~='session-selector'][data-placement^='bottom'] > .tippy-arrow::before { border-bottom-color: white; }
.tippy-box[data-theme~='session-selector'][data-placement^='left']   > .tippy-arrow::before { border-left-color: white; }
.tippy-box[data-theme~='session-selector'][data-placement^='right']  > .tippy-arrow::before { border-right-color: white; }

/* ==================================================
   RECEIPT PAGE
   Post-checkout confirmation: success badge, two-up
   next-action grid (print + appointment / school),
   email confirmation card. Single narrow column on
   mobile via the receipt-steps grid breakpoint.
   ================================================== */
.app-cx .receipt-success {
  padding: var(--space-6) var(--space-5);
  text-align: center;
  margin: var(--space-5) auto var(--space-4);
}
.app-cx .success-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--color-success-bg);
  color: var(--color-success);
  border: 2px solid var(--color-success-border);
  margin-bottom: var(--space-3);
}
.app-cx .success-badge .icon { width: 28px; height: 28px; }
.app-cx .receipt-headline {
  font-size: 32px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.15;
  color: var(--color-ink);
  margin: 0 0 var(--space-2);
}
.app-cx .receipt-lede {
  font-size: var(--text-base);
  color: var(--color-muted-strong);
  line-height: 1.6;
  margin: 0;
}
.app-cx .receipt-steps {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}
@media (max-width: 720px) {
  .app-cx .receipt-steps { grid-template-columns: 1fr; }
}
.app-cx .receipt-step {
  padding: var(--space-5) var(--space-4);
  display: flex;
  flex-direction: column;
}
.app-cx .receipt-step-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--color-primary);
  color: white;
  font-weight: 700;
  font-size: var(--text-md);
  margin-bottom: var(--space-3);
}
.app-cx .receipt-step-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--color-ink);
}
.app-cx .receipt-step-body {
  margin: 0 0 var(--space-4);
  font-size: var(--text-base);
  color: var(--color-muted-strong);
  line-height: 1.5;
  flex: 1;
}
.app-cx .receipt-step-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.app-cx .receipt-step-cta .icon { width: 16px; height: 16px; }
.app-cx .receipt-step-instructions {
  font-size: var(--text-base);
  color: var(--color-ink);
  line-height: 1.6;
  padding: var(--space-3) var(--space-4);
  background: var(--color-bg);
  border-radius: var(--radius-md);
  margin-top: var(--space-2);
}
.app-cx .pass-preview {
  background: #f9fafb;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  text-align: center;
  margin-bottom: var(--space-4);
}
.app-cx .pass-preview-label {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--color-muted-strong);
  margin: 0 0 var(--space-1);
}
.app-cx .pass-preview-name {
  font-weight: 600;
  font-size: var(--text-base);
  color: var(--color-ink);
  margin: 0 0 var(--space-2);
}
.app-cx .pass-preview-barcode {
  display: block;
  max-width: 220px;
  margin: 0 auto var(--space-2);
}
.app-cx .pass-preview-barcode svg {
  display: block;
  width: 100%;
  height: 36px;
}
.app-cx .pass-preview-order {
  font-size: var(--text-xs);
  color: var(--color-muted-strong);
  margin: 0;
  font-family: var(--font-mono);
  letter-spacing: 0.5px;
}
.app-cx .receipt-step-calendar {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-bottom: var(--space-4);
  padding: var(--space-3);
  background: #f9fafb;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
}
.app-cx .receipt-step-calendar-day {
  min-width: 72px;
  text-align: center;
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  background: white;
  border: 1px solid var(--color-line);
}
.app-cx .receipt-step-calendar-day.is-active {
  background: var(--color-primary);
  color: white;
  border-color: var(--color-primary);
}
.app-cx .receipt-step-calendar-day-muted {
  opacity: 0.35;
  filter: blur(0.5px);
}
.app-cx .receipt-step-calendar-day-name {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  display: block;
  margin-bottom: 2px;
  opacity: 0.7;
}
.app-cx .receipt-step-calendar-day-num {
  font-size: 18px;
  font-weight: 600;
  display: block;
  line-height: 1;
}
.app-cx .receipt-step-credentials {
  background: #f9fafb;
  border: 1px solid var(--color-line);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  text-align: center;
}
.app-cx .receipt-step-credentials-label {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--color-muted-strong);
  margin: 0 0 var(--space-1);
}
.app-cx .receipt-step-credentials-value {
  font-family: var(--font-mono);
  font-weight: 600;
  font-size: var(--text-base);
  color: var(--color-ink);
  margin: 0 0 var(--space-2);
  word-break: break-all;
}
.app-cx .receipt-step-credentials-help {
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  margin: 0;
  line-height: 1.5;
}

.app-cx .receipt-email-card {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-4);
  padding-left: var(--space-5);
  background: var(--color-info-bg);
  border-left: 4px solid var(--color-primary);
  border-radius: var(--radius-md);
}
.app-cx .receipt-email-card > .icon {
  width: 20px;
  height: 20px;
  color: var(--color-info-ink);
  flex-shrink: 0;
  margin-top: 2px;
}
.app-cx .receipt-email-card-text { margin: 0; }
.app-cx .receipt-email-card-label {
  font-weight: 600;
  font-size: var(--text-base);
  color: var(--color-ink);
  margin: 0 0 2px;
}
.app-cx .receipt-email-card-label strong { font-weight: 700; }
.app-cx .receipt-email-card-body {
  font-size: var(--text-sm);
  color: var(--color-muted-strong);
  margin: 0;
  line-height: 1.5;
}

/* ==================================================
   PRINTABLE SESSION PASS
   Letter-page artifact opened in a popup and auto-printed.
   Hero (name + barcode), gutter-labeled ledger (Session /
   Choices / Add-ons / Fees / Total), on-session-day panel
   (conditional), deferred-balance warning, and a tear-off
   claim stub. TEST / REFUNDED stamps overlay both barcodes
   so a test pass cannot be scanned without first removing
   the stamps.
   ================================================== */

.app-cx .pass {
  background: white;
  width: 7.5in;
  max-width: 100%;
  margin: 0 auto;
  padding: 28px 32px;
  position: relative;
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  font-family: var(--font-serif);
  color: var(--color-ink);
}
.app-cx .pass-brand-bar {
  height: 4px;
  background: var(--color-primary);
  margin: -28px -32px 20px;
}

.app-cx .pass-reminder {
  max-width: 7.5in;
  margin: 0 auto 20px;
  padding: 18px 20px;
  background: var(--color-suggest-bg);
  border-radius: var(--radius-md);
  text-align: center;
}
.app-cx .pass-reminder-lede {
  font-family: var(--font-serif);
  font-size: 20px;
  margin: 0 0 var(--space-3);
  color: var(--color-ink);
}
.app-cx .pass-reminder-body {
  font-size: var(--text-base);
  color: var(--color-ink);
  margin: 0 auto;
  max-width: 540px;
  line-height: 1.6;
}

.app-cx .pass-hero {
  display: grid;
  grid-template-columns: 1fr 220px;
  gap: var(--space-5);
  align-items: end;
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--color-line-strong);
  margin-bottom: 20px;
}
.app-cx .pass-eyebrow {
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  color: var(--color-primary);
  font-weight: 700;
  margin: 0 0 4px;
}
.app-cx .pass-name {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: 50px;
  font-weight: 400;
  margin: 0 0 var(--space-2);
  line-height: 0.98;
  letter-spacing: -0.01em;
  color: var(--color-ink);
}
.app-cx .pass-identity {
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 13px;
  color: var(--color-muted-strong);
  margin: 0;
  line-height: 1.5;
}
.app-cx .pass-identity .row { display: block; }
.app-cx .pass-identity .id-num {
  font-family: var(--font-mono);
  color: var(--color-ink);
  letter-spacing: 0.5px;
}
.app-cx .pass-barcode-box {
  text-align: right;
  position: relative;
}
.app-cx .pass-banner {
  display: block;
  max-height: 36px;
  margin-left: auto;
  margin-bottom: 10px;
  opacity: 0.85;
}
.app-cx .pass-barcode-wrap { position: relative; display: block; }
.app-cx .pass-barcode-wrap svg { display: block; width: 100%; height: 56px; }
.app-cx .pass-barcode-text {
  font-family: var(--font-mono);
  font-size: 10px;
  text-align: center;
  color: var(--color-muted-strong);
  margin-top: 4px;
  letter-spacing: 1px;
}

.app-cx .pass-stamp {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(-6deg);
  border: 3px solid #b91c1c;
  color: #b91c1c;
  background: rgba(255, 255, 255, 0.92);
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 14px;
  font-weight: 800;
  letter-spacing: 0.18em;
  padding: 4px 12px;
  border-radius: 4px;
  z-index: 2;
  pointer-events: none;
  white-space: nowrap;
}
.app-cx .pass-stamp.pass-stamp-stub {
  font-size: 11px;
  padding: 3px 8px;
  border-width: 2px;
}

.app-cx .ledger {
  display: grid;
  grid-template-columns: 92px 1fr;
  column-gap: var(--space-4);
  row-gap: 18px;
  font-family: system-ui, -apple-system, sans-serif;
}
.app-cx .ledger-label {
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  color: var(--color-muted-strong);
  padding-top: 4px;
}
.app-cx .ledger-content { font-size: 13px; }
.app-cx .ledger-line {
  display: grid;
  grid-template-columns: 1fr auto 84px;
  column-gap: var(--space-3);
  padding: 2px 0;
  align-items: baseline;
}
.app-cx .ledger-line .name { color: var(--color-ink); }
.app-cx .ledger-line .qty {
  color: var(--color-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.app-cx .ledger-line .amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.app-cx .ledger-content.session .ledger-line { grid-template-columns: 1fr 84px; }
.app-cx .ledger-content.session .ledger-line .name,
.app-cx .ledger-content.session .ledger-line .amount {
  font-size: 16px;
  font-weight: 600;
}
.app-cx .ledger-content.choices {
  font-size: 12px;
  line-height: 1.6;
  color: var(--color-ink);
  padding-top: 2px;
  padding-right: 100px;
}
.app-cx .ledger-content.choices .middot { color: var(--color-muted); margin: 0 6px; }
.app-cx .ledger-content.fees .ledger-line { grid-template-columns: 1fr 84px; }
.app-cx .ledger-content.fees .name,
.app-cx .ledger-content.fees .amount { color: var(--color-muted-strong); }
.app-cx .ledger-content.fees .ledger-line.credit .name,
.app-cx .ledger-content.fees .ledger-line.credit .amount { color: #047857; }
.app-cx .ledger-content.total .ledger-line {
  grid-template-columns: 1fr 84px;
  padding-top: var(--space-2);
  border-top: 1px solid var(--color-line-strong);
}
.app-cx .ledger-content.total .name,
.app-cx .ledger-content.total .amount { font-size: 15px; font-weight: 700; }

.app-cx .pass-onday {
  margin-top: 20px;
  display: grid;
  grid-template-columns: 92px 1fr;
  column-gap: var(--space-4);
}
.app-cx .pass-onday-label {
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 10px;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  color: var(--color-muted-strong);
  padding-top: 4px;
}
.app-cx .pass-onday-body {
  background: #f9fafb;
  border-left: 3px solid var(--color-primary);
  padding: 10px 14px;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 12px;
  color: var(--color-ink);
  line-height: 1.5;
}

.app-cx .pass-balance-deferred {
  margin-top: var(--space-4);
  padding: 10px 14px;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  background: white;
  border: 2px solid var(--color-warning);
  color: var(--color-warning-ink, var(--color-ink));
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.app-cx .pass-balance-deferred .icon { width: 16px; height: 16px; }

.app-cx .pass-footer {
  text-align: center;
  padding-top: var(--space-3);
  margin-top: 18px;
  border-top: 1px solid var(--color-line-strong);
  font-size: 11px;
  color: var(--color-muted-strong);
  font-family: system-ui, -apple-system, sans-serif;
  font-style: italic;
}

.app-cx .pass-stub {
  margin: var(--space-5) -32px -28px;
  padding: var(--space-4) 32px;
  border-top: 2px dashed var(--color-line-strong);
  background: #fcfcfc;
  font-family: system-ui, -apple-system, sans-serif;
}
.app-cx .pass-stub-label {
  display: inline-block;
  background: var(--color-primary);
  color: white;
  font-size: 8px;
  font-weight: 800;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  margin-bottom: 10px;
}
.app-cx .pass-stub-grid {
  display: grid;
  grid-template-columns: 1.3fr 1.2fr 140px;
  gap: 18px;
  align-items: center;
}
.app-cx .pass-stub-grid > div:last-child { position: relative; }
.app-cx .pass-stub-name {
  font-family: var(--font-serif);
  font-size: 16px;
  font-weight: 600;
  font-style: italic;
  margin: 0 0 2px;
  line-height: 1.1;
}
.app-cx .pass-stub-meta {
  font-size: 11px;
  color: var(--color-muted-strong);
  margin: 0;
  line-height: 1.4;
}
.app-cx .pass-stub-meta .id-num {
  font-family: var(--font-mono);
  color: var(--color-ink);
}
.app-cx .pass-stub-session {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-ink);
  margin: 0 0 2px;
}
.app-cx .pass-stub-choices {
  font-size: 11px;
  color: var(--color-muted-strong);
  margin: 0;
  line-height: 1.4;
}
.app-cx .pass-stub-choices .middot { color: var(--color-muted); margin: 0 4px; }
.app-cx .pass-stub-barcode-wrap { position: relative; display: block; }
.app-cx .pass-stub-barcode-wrap svg { display: block; width: 100%; height: 34px; }
.app-cx .pass-stub-bcode {
  font-family: var(--font-mono);
  font-size: 9px;
  text-align: center;
  color: var(--color-muted-strong);
  letter-spacing: 1px;
  margin-top: 3px;
}

/* ==================================================
   APP FOOTER
   Customer-facing page footer. Single muted line,
   hairline top border, modest vertical breathing
   room. Hidden on print via the .not-print utility
   already provided by print.css.
   ================================================== */
.app-cx .app-footer {
  margin-top: var(--space-6);
  padding: var(--space-4) var(--space-3);
  border-top: 1px solid var(--color-line);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--color-muted-strong);
  line-height: 1.5;
}
.app-cx .app-footer p { margin: 0; }

@media print {
  @page { size: letter; margin: 0.5in; }
  html, body { background: white; }
  .app-banner-testing { display: none !important; }
  .app-cx .pass-reminder { display: none !important; }
  .app-cx .pass {
    box-shadow: none;
    width: 100%;
    max-width: 100%;
    margin: 0;
    padding: 16px 0;
  }
  .app-cx .pass-brand-bar { margin: -16px 0 16px; }
  .app-cx .pass-stub { margin: 24px 0 0; padding: 16px 0 0; }
  .app-cx .pass-hero,
  .app-cx .ledger,
  .app-cx .pass-onday,
  .app-cx .pass-stub { break-inside: avoid; }

  /* Paper has a fixed width and cannot scroll, so the screen
     containment (overflow-x: clip and table overflow-x: auto)
     would silently drop studio content off the printed pass --
     a data-loss defect on a keepsake the customer keeps. Reflow
     instead: prose overflow stays visible, long tokens break,
     and tables shrink to fit the page rather than scroll. */
  .app-cx .cx-prose,
  .app-cx .cx-prose * { overflow: visible !important; word-break: break-word; }
  .app-cx .cx-prose table { display: table; table-layout: fixed; width: 100%; }
}


/* ============================================================
   EMPHASIZED ADDON PAGE — MAGAZINE SPREAD
   ============================================================
   Layout for app/views/extras/new.html.erb. Renders one emphasized
   addon as a single-decision page: full-bleed hero photo with title
   and price overlaid on a scrim, a two-column prose body beneath,
   and a suggest-tinted action bar that takes one of two compositions
   depending on whether the addon is opt-outable (persuade row +
   Continue) or not (No Thanks + Yes Please).

   Structure:
     article.extras-spread
       .extras-spread-hero                | full-bleed image
         img.extras-spread-img            | object-fit:cover photo
         (::after on .extras-spread-hero) | gradient scrim
         .extras-spread-overlay           | absolute, bottom-left
           .extras-spread-eyebrow         | "Featured addon . Step 1 of 2"
           h2.extras-spread-name          | addon name
           .extras-spread-price           | currency-formatted price
       .extras-spread-body
         .extras-spread-prose.cx-prose    | studio prose, 2-col at desktop
       .extras-spread-action  (one of:)
         .extras-spread-action--persuade  | persuade row + Continue
         .extras-spread-action--choose    | No Thanks + Yes Please

   Hardcoded literals (rgba black for the hero scrim, #ffffff for the
   hero text fills, and the warm gradient for the no-image
   placeholder) are intrinsic to the treatment -- there is no
   white-on-image token in the cx palette. Everything else uses
   :root tokens.

   .extras-spread does NOT extend .card. Removing card padding so the
   hero reaches the rounded corners was a hard requirement, so the
   wrapper defines its own bg / border / radius / shadow directly.
*/

/* ------------------------------------------------------------
   CARD CONTAINER
   ------------------------------------------------------------ */

/* Article wrapper. No internal padding so the hero image runs
   edge-to-edge. overflow:hidden clips both the image on the top edge
   and the suggest-tinted action bar on the bottom edge to the card's
   12px radius. */
.app-cx .extras-spread {
  background: var(--color-card);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin: 0 auto var(--space-4);
  max-width: 600px;
  box-shadow: var(--shadow-sm);
}


/* ------------------------------------------------------------
   HERO
   ------------------------------------------------------------ */

/* Full-bleed image area at the top of the card. aspect-ratio reserves
   the slot height before the image loads (eliminates layout shift). The
   16/7 ratio gives the magazine spread its letterboxed cinematic feel.
   bg sits behind the image as a fallback color while the asset is
   downloading. */
.app-cx .extras-spread-hero {
  position: relative;
  background: var(--color-ink);
  overflow: hidden;
}

/* Scrim -- gradient on a pseudo-element that darkens the LOWER ~55% of
   the hero so the white overlay text reads against any photo. The scrim
   sits BETWEEN the image (default flow, z 0) and the overlay (z 1).
   pointer-events:none so the scrim never blocks taps on the image
   itself (no current click target, but defensive). */
.app-cx .extras-spread-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0)    25%,
    rgba(0, 0, 0, 0.45) 60%,
    rgba(0, 0, 0, 0.85) 100%
  );
  pointer-events: none;
}

.app-cx .extras-spread-img {
  display: block;
  width: 100%;
  height: auto;
}

/* Placeholder rendered when the addon has no image variant for the
   current order's sex (model returns nil). Same dimensions as the real
   image so the overlay layout doesn't shift. Calm warm gradient that
   reads as a stand-in for a photo rather than a broken image icon. */
.app-cx .extras-spread-img-placeholder {
  background: linear-gradient(135deg, #d8d0c4 0%, #8c8579 100%);
  /* Hero adapts to the real image's natural dimensions when present;
     when missing, the placeholder needs its own intrinsic size or the
     hero collapses to zero. 16:9 gives a sensible default block. */
  aspect-ratio: 16 / 9;
}

/* Overlay block -- eyebrow + name + price, pinned to the lower-left of
   the hero. Sits ABOVE the scrim (z 1). The horizontal inset on both
   sides gives long addon names room to wrap before colliding with the
   right edge. */
.app-cx .extras-spread-overlay {
  position: absolute;
  inset: auto var(--space-6) var(--space-5) var(--space-6);
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  color: #ffffff;
  /* Defensive text-shadow that inherits to eyebrow + name + price.
     Guarantees legibility even when the scrim falls short on photos
     with light areas under the text (which is most likely on the
     16/9 mobile hero where the overlay sits over more of the image). */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
}

/* Eyebrow -- markup is title-case ("Featured addon . Step 1 of 2") so
   screen readers announce it naturally; CSS uppercases for the spread
   label treatment. letter-spacing gives the small-caps an editorial
   silhouette. */
.app-cx .extras-spread-eyebrow {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.9);
}

/* Addon name -- bold sans, tight leading, slight negative tracking.
   Stays h2 in the DOM (the page-level h1 lives in the layout). The
   line-height 1.05 keeps two-line names compact without the lines
   touching. */
.app-cx .extras-spread-name {
  margin: 0;
  font-size: 36px;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: #ffffff;
}

/* Price -- sits beneath the name. Slightly translucent white so the
   hierarchy reads name > price without the price competing for weight.
   Currency formatting comes from Rails' number_to_currency. */
.app-cx .extras-spread-price {
  margin: 0;
  font-size: 18px;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.92);
}


/* ------------------------------------------------------------
   BODY
   ------------------------------------------------------------ */

/* Body slot below the hero. Generous padding sets the editorial-page
   feel; horizontal padding matches the action bar's padding so the two
   regions read as one continuous spread. */
.app-cx .extras-spread-body {
  padding: var(--space-6) var(--space-7) var(--space-5);
}

/* Two-column read at desktop. Multi-column flows the studio's rich-text
   automatically -- paragraphs fill the left column top-to-bottom, then
   continue in the right. Studio descriptions are variable length; a
   short body leaves the right column shorter than the left (acceptable),
   a long body balances naturally. column-fill:balance gives even column
   heights when content is shorter than the available slot. */
.app-cx .extras-spread-prose {
  max-width: 60ch;
}

/* Suppress top margin on the very first prose element so the columns
   align flush at the top edge. cx-prose's paragraph and heading rules
   set bottom margins for rhythm, but the first element here needs to
   sit at the top of the body padding without an extra gap above it. */
.app-cx .extras-spread-prose > :first-child {
  margin-top: 0;
}

/* Keep block-level elements from being SPLIT across a column boundary.
   Headings, images, and blockquotes look broken when the column break
   slices through them; lists and paragraphs are allowed to break
   naturally so the columns balance. */
.app-cx .extras-spread-prose h2,
.app-cx .extras-spread-prose h3,
.app-cx .extras-spread-prose h4,
.app-cx .extras-spread-prose img,
.app-cx .extras-spread-prose blockquote {
  break-inside: avoid;
}


/* ------------------------------------------------------------
   ACTION BAR
   ------------------------------------------------------------ */

/* Suggest-tinted bar at the foot of the card. Spans full card width;
   the surrounding .extras-spread has overflow:hidden, so the tint
   reaches the rounded bottom corners flush. The 1px hairline at the
   top separates the action region from the body without competing
   visually with the card border. */
.app-cx .extras-spread-action {
  background: var(--color-suggest-bg);
  border-top: 1px solid var(--color-line);
  padding: var(--space-4) var(--space-7);
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: nowrap;
}

/* Opt-outable variant -- persuade label on the left, Continue pinned
   to the right. */
.app-cx .extras-spread-action--persuade {
  justify-content: space-between;
}

/* Non-opt-outable variant -- this is a focused, single-decision page,
   so decline and commit sit at opposite ends: "No Thanks" at the far
   left, "Yes, Please!" at the far right. min-width on each button
   keeps the pair stable across viewport widths. */
.app-cx .extras-spread-action--choose {
  justify-content: space-between;
}
.app-cx .extras-spread-action--choose .btn {
  min-width: 140px;
}

/* Persuade label -- wraps the hidden field, the checkbox, and the
   persuasion span. The WHOLE label is clickable so the customer can
   tap anywhere on the row to toggle the box. flex:1 lets the label
   absorb the bar's free space; min-width:0 lets the span wrap
   gracefully when the bar narrows. */
.app-cx .extras-spread-persuade {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  cursor: pointer;
  flex: 1 1 auto;
  min-width: 0;
}

.app-cx .extras-spread-persuade input[type="checkbox"] {
  width: 22px;
  height: 22px;
  flex-shrink: 0;
  accent-color: var(--color-primary);
  cursor: pointer;
}

.app-cx .extras-spread-persuade-text {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--color-ink);
  line-height: 1.4;
}


/* ------------------------------------------------------------
   RESPONSIVE
   ------------------------------------------------------------ */

/* Tablet+phone -- collapse the prose to a single column. The 720px
   threshold matches cx.css's tablet-stack band so the page snaps in
   sync with the surrounding chrome; by that width the left column
   would also be below the comfortable character count for prose
   reading (~45-75 chars). Also tightens the surrounding paddings and
   the hero overlay so the page reads as a phone artifact, not a
   shrunken desktop. */
@media (max-width: 720px) {
  .app-cx .extras-spread-body {
    padding: var(--space-5) var(--space-5) var(--space-4);
  }
  .app-cx .extras-spread-action {
    padding: var(--space-4) var(--space-5);
  }
  .app-cx .extras-spread-overlay {
    inset: auto var(--space-4) var(--space-4) var(--space-4);
  }
  .app-cx .extras-spread-name {
    font-size: 26px;
  }
  .app-cx .extras-spread-price {
    font-size: 16px;
  }
}

/* Phone -- stack the action bar vertically. The non-opt-outable choose
   variant reverses so the primary (Yes, Please!) lands on top (thumb
   zone), matching the customer-flow button-order convention. The
   persuade variant keeps the Continue under the persuade row, which
   is natural reading order on a vertical stack. */
@media (max-width: 480px) {
  .app-cx .extras-spread-action {
    flex-direction: column;
    align-items: stretch;
  }
  .app-cx .extras-spread-action--choose {
    flex-direction: column-reverse;
  }
  .app-cx .extras-spread-action .btn {
    width: 100%;
  }
  .app-cx .extras-spread-action--choose .btn {
    min-width: 0;
  }
  .app-cx .extras-spread-persuade {
    flex: none;
    width: 100%;
  }
}
