/* Design system ported from _artifacts/hmm-course-portal-ui.html (the
   verified, contrast-measured mockup). Tokens, type scale, and component
   classes below are a direct lift, not a reinterpretation -- see that file
   for the contrast measurements and reasoning behind each decision.
   Self-hosted fonts (both Perfectly Nineties and Rethink Sans) rather than
   the mockup's base64-embedded/Google-Fonts-linked versions -- those exist
   only so the mockup renders standalone; this is a live product. */

@font-face {
  font-family: "Perfectly Nineties";
  src: url("fonts/PerfectlyNineties-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Perfectly Nineties";
  src: url("fonts/PerfectlyNineties-Italic.ttf") format("truetype");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_400Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_500Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_600SemiBold.ttf") format("truetype");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rethink Sans";
  src: url("fonts/RethinkSans_700Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* =========================================================================
   RESET
   ========================================================================= */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
input { font: inherit; color: inherit; }

/* =========================================================================
   TOKENS -- values copied verbatim from the mockup, which copied them
   verbatim from lib/theme.ts. Do not introduce new colors here.
   ========================================================================= */
:root {
  --dark-brown: #593904;
  --mid-brown: #af9983;
  --light-brown: #c8b19b;
  --beige: #efdece;
  --additional-brown: #eadfd4;
  --off-white: #f9f1e9;
  --site-white: #fffcf8;
  --green: #848241;
  --warn: #b85c00;
  --overlay: rgba(89, 57, 4, 0.55);

  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;

  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-xxl: 32px;
  --radius-xxxl: 40px;
  --radius-pill: 999px;

  --font-heading: 'Perfectly Nineties', Georgia, 'Times New Roman', serif;
  --font-body: 'Rethink Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}

/* =========================================================================
   CONTRAST DECISIONS (measured against this palette, not eyeballed --
   see the mockup file for the full ratio table). The consequences that
   matter while editing this file:
   1. midBrown is NEVER a text color. Dividers, disabled fills, and resting
      placeholder text only (placeholders are contrast-exempt under WCAG).
   2. Input borders use darkBrown, not lightBrown/midBrown -- the lighter
      browns fail the 3:1 non-text minimum against both the page and the
      input's own near-white fill.
   3. Buttons keep the app's real green fill/text (matches Button.tsx).
   4. Italic emphasis renders in darkBrown, never green, so it stays
      legible at body-copy sizes.
   ========================================================================= */

body {
  background: var(--off-white);
  color: var(--dark-brown);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* =========================================================================
   TYPOGRAPHY
   ========================================================================= */
.pn-72, .pn-56, .pn-36, .pn-24, .pn-18 {
  font-family: var(--font-heading);
  font-weight: 400;
  font-style: normal;
  line-height: 1.1;
  color: var(--dark-brown);
  margin: 0;
}
.pn-72 { font-size: clamp(42px, 5vw + 24px, 72px); }
.pn-56 { font-size: clamp(34px, 4vw + 20px, 56px); }
.pn-36 { font-size: clamp(27px, 3vw + 16px, 36px); }
.pn-24 { font-size: clamp(21px, 1.5vw + 15px, 24px); }
.pn-18 { font-size: clamp(17px, 1vw + 12px, 18px); }

/* Emphasis: Perfectly Nineties italic, never bold, always darkBrown. */
.emph {
  font-family: var(--font-heading);
  font-style: italic;
  font-weight: 400;
  color: var(--dark-brown);
}

.rs-24 { font-family: var(--font-body); font-size: 24px; font-weight: 400; }
.rs-20 { font-family: var(--font-body); font-size: 20px; font-weight: 400; }
.rs-18 { font-family: var(--font-body); font-size: 18px; font-weight: 400; }
.rs-16 { font-family: var(--font-body); font-size: 16px; font-weight: 400; }
.rs-14 { font-family: var(--font-body); font-size: 14px; font-weight: 400; }
.rs-semibold { font-weight: 600; }
.rs-bold { font-weight: 700; }

.lede { font-family: var(--font-body); font-size: 18px; line-height: 1.5; color: var(--dark-brown); max-width: 46em; margin: 0; }
.body-copy { font-family: var(--font-body); font-size: 16px; line-height: 1.5; color: var(--dark-brown); max-width: 46em; margin: 0; }

.field-label {
  display: block;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 12px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--dark-brown);
  margin-bottom: var(--space-sm);
}

/* =========================================================================
   ICONS
   ========================================================================= */
.icon { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
.icon-sm { width: 16px; height: 16px; }
.icon-fill { fill: currentColor; stroke: none; }

/* =========================================================================
   LAYOUT SHELL
   ========================================================================= */
.shell {
  max-width: 720px;
  margin-inline: auto;
  padding-inline: clamp(20px, 5vw, 40px);
}
.view { min-height: 100vh; }

/* =========================================================================
   SHARED PORTAL HEADER
   Sign-in intentionally does not use this header -- an unauthenticated
   visitor has nowhere it could take her yet.
   ========================================================================= */
.portal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
  padding: 18px clamp(20px, 5vw, 40px);
  background: var(--site-white);
  border-bottom: 1px solid var(--light-brown);
}
.wordmark {
  font-family: var(--font-heading);
  font-size: 20px;
  color: var(--dark-brown);
}
.header-links { display: flex; align-items: center; gap: var(--space-md); }

/* Practitioner tab bar (_artifacts/practitioner-nav-spec.md §5). Scoped to
   .portal-header--tabs (added only by practitioner.js's
   renderPractitionerHeader() when a tab bar actually follows) rather than
   changing .portal-header's own base rule -- that base rule is also used
   as-is by app.js's renderHeader() (index/product/lesson/protocol/
   messages, wordmark + header-links as direct children, no wrapper) and
   by this same header's own non-tabbed Approved Systems variant. Redoing
   .portal-header globally would have silently broken both. --tabs
   switches .portal-header itself to a plain block container so
   .header-top-row (the identical wordmark/links flex row, just moved into
   its own element) can stack above .header-tabs via ordinary block flow. */
.portal-header--tabs { display: block; }
.header-top-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm);
}
.header-tabs {
  display: flex;
  gap: var(--space-lg);
  margin-top: var(--space-sm);
  overflow-x: auto;              /* defensive only -- 3 short labels won't
                                     overflow even a 320px phone, but this
                                     is a one-line safety net, not tuned UX */
  -webkit-overflow-scrolling: touch;
}
.header-tab {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: var(--space-sm) 0;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.header-tab.is-active { color: var(--green); font-weight: 700; border-bottom-color: var(--green); }
.header-tab-badge {
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--warn);
  color: var(--site-white);
  font-size: 11px;
  font-weight: 700;
  display: none;
  align-items: center;
  justify-content: center;
}
.header-tab-badge.has-count { display: inline-flex; }
.signout-link, .help-link {
  font-family: var(--font-body);
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: var(--space-sm);
  margin: calc(var(--space-sm) * -1);
  border-radius: var(--radius-sm);
}
.signout-link:hover, .help-link:hover { text-decoration: underline; }

/* =========================================================================
   BUTTONS -- mirrors components/ui/Button.tsx (padding, radius, fill,
   weight). min-height asserted explicitly for the 44-52px touch target.
   ========================================================================= */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 52px;
  padding: 16px 32px;
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 16px;
  width: 100%;
  text-align: center;
}
.btn-primary { background: var(--green); color: var(--site-white); }
.btn-secondary { background: var(--dark-brown); color: var(--site-white); }
.btn-outline { background: transparent; border: 1.5px solid var(--green); color: var(--green); }
/* Aug 2026, practitioner.js's delete-document confirmation -- the first
   genuinely destructive action in this app's UI, warrants its own real
   variant rather than an inline color override in a JS template string. */
.btn-danger { background: var(--warn); color: var(--site-white); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
.btn-inline { width: auto; padding: 12px 20px; min-height: 44px; font-size: 14px; }

a:focus-visible, button:focus-visible, input:focus-visible, [tabindex]:focus-visible {
  outline: 3px solid var(--dark-brown);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* =========================================================================
   FORM FIELDS
   ========================================================================= */
.field { margin-bottom: var(--space-lg); }
.text-input {
  width: 100%;
  min-height: 52px;
  padding: 0 var(--space-md);
  background: var(--site-white);
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--dark-brown);
}
.text-input::placeholder { color: var(--mid-brown); }
.text-input:focus { outline: none; border-width: 2px; box-shadow: 0 0 0 3px rgba(89, 57, 4, 0.15); }

/* =========================================================================
   CARDS
   ========================================================================= */
.card {
  display: block;
  background: var(--beige);
  border-radius: var(--radius-lg);
  padding: 20px;
  box-shadow: 0 1px 3px rgba(89, 57, 4, 0.08);
}

/* =========================================================================
   COVER IMAGERY -- shared by the library card grid and the product hero
   (product.js). A real Kajabi cover photo/screenshot renders in .cover-img;
   .cover-fallback sits underneath it at all times, so a product with no
   mapped cover, or an <img> that fails to load, always resolves to a
   deliberate typographic panel -- never a broken image, never a grey box.
   ========================================================================= */
.cover-media {
  position: relative;
  overflow: hidden;
  background: var(--beige);
}
.cover-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.cover-fallback {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--space-xs);
  padding: var(--space-md);
  background: var(--dark-brown);
  overflow: hidden;
}
.cover-fallback-mark {
  position: absolute;
  top: -0.2em;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-heading);
  font-style: italic;
  font-size: 200px;
  line-height: 1;
  color: rgba(249, 241, 233, 0.08);
  pointer-events: none;
  user-select: none;
}
.cover-fallback-eyebrow {
  position: relative;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--light-brown);
}
.cover-fallback-title {
  position: relative;
  font-family: var(--font-heading);
  font-style: italic;
  font-size: clamp(19px, 2vw + 14px, 24px);
  line-height: 1.25;
  color: var(--site-white);
  max-width: 22ch;
}
.card-cover { aspect-ratio: 16 / 9; }
.hero-cover {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-lg);
}

/* Icon chips -- ported from the app's numbered lesson-row circle (ModuleCard/
   LessonRow in app/courses/). Video rows fill solid olive (the "real
   accent" this pass is meant to bring in); file/download rows stay on the
   neutral additionalBrown so the colour carries actual meaning (playable
   vs. not), not decoration for its own sake. */
.icon-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}
.icon-chip .icon { width: 18px; height: 18px; opacity: 1; }
.icon-chip--video { background: var(--green); }
.icon-chip--video .icon { color: var(--site-white); }
.icon-chip--file { background: var(--additional-brown); }
.icon-chip--file .icon { color: var(--dark-brown); }

/* =========================================================================
   REDUCED MOTION -- motion is opted IN only here, so a user who prefers
   reduced motion (or whose browser doesn't support the query) gets instant
   state changes by default.
   ========================================================================= */
@media (prefers-reduced-motion: no-preference) {
  .module-panel { transition: grid-template-rows 0.25s ease; }
  .icon-chevron { transition: transform 0.2s ease; }
  .auth-card, .library-state { transition: opacity 0.2s ease; }

  /* Timings mirror components/motion/FadeInOnScroll.tsx and
     useScrollReveal.ts (600ms, Easing.out(Easing.cubic)) -- there is no
     scroll-linked measurement on a static page like this one, so the reveal
     runs once on load rather than on viewport entry, but the feel matches. */
  .card-grid .product-card {
    animation: cover-reveal 0.6s cubic-bezier(0.215, 0.61, 0.355, 1) both;
  }
  .card-grid .product-card:nth-child(2) { animation-delay: 0.06s; }
  .card-grid .product-card:nth-child(3) { animation-delay: 0.12s; }
  .card-grid .product-card:nth-child(4) { animation-delay: 0.18s; }
  .card-grid .product-card:nth-child(n+5) { animation-delay: 0.24s; }
  @keyframes cover-reveal {
    from { opacity: 0; transform: translateY(12px); }
    to { opacity: 1; transform: translateY(0); }
  }

  .product-card {
    transition: transform 0.25s cubic-bezier(0.215, 0.61, 0.355, 1), box-shadow 0.25s ease;
  }
  a.product-card:hover, a.product-card:focus-visible {
    transform: translateY(-3px);
    box-shadow: 0 10px 24px rgba(89, 57, 4, 0.16);
  }
}

/* =========================================================================
   BANNERS -- not in the mockup (it has no error states to design), kept
   from the working build. Styled with the ported tokens.
   ========================================================================= */
.banner {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  margin: 0 0 var(--space-lg);
  font-family: var(--font-body);
  font-size: 15px;
  color: var(--dark-brown);
  background: var(--additional-brown);
  border: 1.5px solid var(--light-brown);
}
.banner.error { border-color: var(--warn); }

/* Loading skeletons, not in the mockup either -- neutral fill only. */
.skeleton { background: var(--beige); border-radius: var(--radius-lg); height: 88px; opacity: 0.6; }

/* =========================================================================
   SIGN IN
   ========================================================================= */
.auth-shell {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-xl) var(--space-md);
  background: var(--beige);
}
/* The single flex child of .auth-shell -- see the comment on this element
   in index.html for why it must stay the only one. Banner and both cards
   stack inside it in normal block flow. */
.auth-card-wrap { width: 100%; max-width: 420px; }
/* A real card surface (the mockup left this bare) -- floats the sign-in
   flow above the shell's beige ground instead of the two tones reading as
   one flat field. */
.auth-card {
  width: 100%;
  max-width: 420px;
  background: var(--site-white);
  border-radius: var(--radius-xl);
  padding: var(--space-xl) clamp(20px, 5vw, 32px);
  box-shadow: 0 4px 20px rgba(89, 57, 4, 0.1);
}
.auth-wordmark { font-family: var(--font-heading); font-size: 22px; text-align: center; margin-bottom: var(--space-xl); }
.auth-heading { margin-bottom: var(--space-sm); }
.auth-lede { margin-bottom: var(--space-xl); }

/* Real Turnstile widget mounts inside .turnstile-box (interaction-only
   appearance -- invisible unless Cloudflare's risk engine forces a
   challenge). The box's border/padding gives it a defined, findable home
   on the page even when the widget itself renders nothing. */
.turnstile-box {
  display: flex;
  align-items: center;
  min-height: 24px;
  margin-bottom: var(--space-lg);
}

.help-row { text-align: center; margin-top: var(--space-lg); font-size: 14px; }
.help-row a { text-decoration: underline; font-weight: 600; }

.sent-icon {
  width: 56px; height: 56px;
  border-radius: 50%;
  background: var(--green);
  color: var(--site-white);
  display: flex; align-items: center; justify-content: center;
  margin: 0 auto var(--space-lg);
}
.resend-row { margin-top: var(--space-lg); }
.resend-note { text-align: center; font-size: 14px; margin-top: var(--space-sm); }

/* Cross-device sign-in (see index.html for the two screens this serves).
   No monospace webfont added for one small feature -- a system stack
   renders a UUID-shaped code perfectly well. */
.code-paste-row {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--light-brown);
}
.code-display {
  background: var(--site-white);
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  text-align: center;
}
.code-display span {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 18px;
  letter-spacing: 0.5px;
  color: var(--dark-brown);
  word-break: break-all;
  user-select: all;
}

/* =========================================================================
   LIBRARY
   ========================================================================= */
.library-main { padding-top: var(--space-xl); padding-bottom: var(--space-xxl); }
.library-main h1 { margin-bottom: var(--space-sm); }
.library-main .lede { margin-bottom: var(--space-xl); }

.card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}
@media (min-width: 640px) {
  .card-grid { grid-template-columns: repeat(2, 1fr); }
}
/* Single-item state: cap the card width instead of letting grid stretch
   one card across a whole desktop row. Not an edge case -- 598 of 1,193
   members own exactly one product. */
.card-grid.card-grid--single {
  grid-template-columns: minmax(0, 420px);
}

/* The card itself is now just the outer frame -- .cover-media bleeds to its
   edges (top corners follow from overflow:hidden on the card), .product-card-body
   holds the padding the flat version used to put directly on .card. */
.product-card {
  padding: 0;
  overflow: hidden;
  background: var(--site-white);
  box-shadow: 0 4px 14px rgba(89, 57, 4, 0.12);
}
.product-card-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-sm);
  padding: 18px 20px 20px;
}
.tag {
  display: inline-flex;
  align-items: center;
  padding: 3px 12px;
  border-radius: var(--radius-pill);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}
/* Course/Workbook is real, derived data (productTag() in app.js), not a
   decorative label -- the fill says something true about the product, so it
   gets the app's real green rather than a hairline outline. */
.tag--course { background: var(--green); color: var(--site-white); }
.tag--workbook { background: var(--light-brown); color: var(--dark-brown); }
.product-card-body h2 { margin-top: var(--space-xs); }
.product-card-meta { font-size: 15px; color: var(--dark-brown); margin: 0; }
.card-cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-sm);
  font-size: 15px;
  font-weight: 700;
  color: var(--dark-brown);
}
.card-cta .icon { width: 16px; height: 16px; }

.empty-state {
  text-align: center;
  padding: var(--space-xxl) var(--space-md);
  max-width: 440px;
  margin: 0 auto;
}
.empty-state-icon-wrap {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  background: var(--beige);
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto var(--space-lg);
}
.empty-state h1 { margin-bottom: var(--space-sm); }
.empty-state .body-copy { margin: 0 auto var(--space-lg); }
.empty-state .help-text { font-size: 14px; }
.empty-state .help-text a { text-decoration: underline; font-weight: 600; }

/* Plain external links to publicly hosted PDFs -- no entitlement gate, no
   /api/document proxy. Shown identically below the product grid (or below
   the empty-state) to every signed-in member; see freeResourcesHtml() in
   index.js. Same .card base and .card-cta "Open" pattern as the product
   cards above it, laid out as a row instead of a stack since there's no
   cover image here. */
/* "Your protocol" card, above the course grid (index.js) -- same
   .resource-card shape the free-resources row already uses, just alone
   and first, since it's the most personal reason she opened the app
   today. Absent from the DOM entirely when she has no protocol row --
   see protocolCardHtml() in index.js. */
.your-protocol { margin-bottom: var(--space-xxl); }
.your-protocol h2 { margin-bottom: var(--space-md); }

.free-resources { margin-top: var(--space-xxl); }
.free-resources h2 { margin-bottom: var(--space-xs); }
.free-resources .lede { margin-bottom: var(--space-lg); }
.resource-card {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}
.resource-card-title { flex: 1; font-size: 17px; font-weight: 700; color: var(--dark-brown); }
.resource-card .card-cta { margin-top: 0; }
a.resource-card:hover, a.resource-card:focus-visible { box-shadow: 0 6px 18px rgba(89, 57, 4, 0.14); }

/* =========================================================================
   PRODUCT
   ========================================================================= */
.product-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); }
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  font-weight: 600;
  margin-bottom: var(--space-lg);
  padding: var(--space-xs) 0;
}
.back-link .icon { width: 18px; height: 18px; }
.product-main .tag { margin-bottom: var(--space-sm); }
.product-main h1 { margin-bottom: var(--space-sm); }
#course-meta { color: var(--dark-brown); font-size: 15px; margin-bottom: var(--space-md); }

.module {
  border-top: 1px solid var(--light-brown);
}
.module:last-child { border-bottom: 1px solid var(--light-brown); }
.module-header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-md) 4px;
  min-height: 56px;
  text-align: left;
  border-radius: var(--radius-md);
}
.module-header-text { display: flex; flex-direction: column; gap: 2px; }
.module-title { font-family: var(--font-body); font-weight: 700; font-size: 17px; color: var(--dark-brown); }
.module-meta { font-size: 14px; color: var(--dark-brown); opacity: 0.75; }
.module-header[aria-expanded="true"] .icon-chevron { transform: rotate(180deg); }
/* An open module gets its own ground, the same tan the lesson-row hover
   uses -- one visual language for "this is where you are" across the page. */
.module-header[aria-expanded="true"] { background: var(--additional-brown); }

.module-panel {
  display: grid;
  grid-template-rows: 0fr;
}
.module-panel.is-open { grid-template-rows: 1fr; }
.module-panel-inner { overflow: hidden; min-height: 0; padding-bottom: var(--space-sm); }

/* Single-module products (all 3 flat products) skip the accordion header
   entirely and render lesson rows directly -- see product.js. */
.no-module-heading { padding-top: var(--space-sm); }

.lesson-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 4px;
  border-radius: var(--radius-md);
  color: var(--dark-brown);
}
a.lesson-row:hover, a.lesson-row:focus-visible { background: var(--additional-brown); }
.lesson-row-title { flex: 1; font-size: 16px; }
.lesson-row-files {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 13px;
  color: var(--dark-brown);
  opacity: 0.75;
  flex-shrink: 0;
}
.lesson-row-duration { font-size: 14px; color: var(--dark-brown); opacity: 0.75; flex-shrink: 0; }
.lesson-row--download { cursor: default; }
.lesson-row-dl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: 6px 12px;
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-pill);
  flex-shrink: 0;
}

/* =========================================================================
   LESSON
   ========================================================================= */
.lesson-main { padding-top: var(--space-lg); padding-bottom: var(--space-xxl); max-width: 760px; }
/* Matches the app's sectionLabel treatment (rs14Bold, textAccent, 1.5px
   tracking) instead of a dimmed neutral line -- olive as a real accent. */
.lesson-eyebrow { font-size: 13px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; color: var(--green); margin-bottom: var(--space-xs); }
.lesson-main h1 { margin-bottom: var(--space-lg); }

/* Real Cloudflare Stream iframe, not the mockup's decorative placeholder
   transport bar -- the iframe embed already provides its own native
   controls (play/pause, scrubber, captions, fullscreen), so there is
   nothing for a second, non-functional control bar to do. The frame
   itself keeps the mockup's exact container treatment. */
.video-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--dark-brown);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: var(--space-xl);
}
.video-frame iframe { width: 100%; height: 100%; border: 0; }
.video-placeholder-text {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--site-white);
  font-size: 14px;
  text-align: center;
  padding: var(--space-md);
}

/* Sanitized lesson body text (see lesson.js -- DOMPurify strips everything
   but plain structural/inline tags before this ever reaches the DOM).
   Typography matches body-copy; headings inside the source content are
   downgraded so they read as in-body emphasis, not a second page title
   competing with the real <h1> above. */
.lesson-body {
  margin-bottom: var(--space-xl);
  padding: var(--space-lg);
  background: var(--site-white);
  border-radius: var(--radius-lg);
  box-shadow: 0 1px 3px rgba(89, 57, 4, 0.08);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  color: var(--dark-brown);
  max-width: 46em;
}
.lesson-body p { margin: 0 0 var(--space-md); }
.lesson-body p:last-child { margin-bottom: 0; }
.lesson-body ul, .lesson-body ol { margin: 0 0 var(--space-md); padding-left: 1.4em; list-style: disc; }
.lesson-body ol { list-style: decimal; }
.lesson-body li { margin-bottom: var(--space-xs); }
.lesson-body a { text-decoration: underline; font-weight: 600; }
.lesson-body blockquote {
  margin: 0 0 var(--space-md);
  padding-left: var(--space-md);
  border-left: 3px solid var(--light-brown);
  color: var(--dark-brown);
}
.lesson-body h1, .lesson-body h2, .lesson-body h3,
.lesson-body h4, .lesson-body h5, .lesson-body h6 {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 16px;
  color: var(--dark-brown);
  margin: 0 0 var(--space-sm);
}
.lesson-body hr { border: none; border-top: 1px solid var(--light-brown); margin: var(--space-lg) 0; }

/* A distinct, neutral surface from .lesson-body's white card -- files read
   as a separate block of the page, not a continuation of the body copy. */
.files-section {
  background: var(--additional-brown);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
}
.files-section h2 { margin-bottom: var(--space-md); }
.file-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.file-row:last-child { border-bottom: none; }
.file-row-name { flex: 1; font-size: 16px; }
.file-row-dl {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  color: var(--dark-brown);
  padding: 8px 14px;
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-pill);
  flex-shrink: 0;
}
.file-row-dl:hover, .file-row-dl:focus-visible { background: var(--site-white); }

/* =========================================================================
   PRACTITIONER -- roster list (practitioner.html, Phase One: read-only,
   no per-row action yet). Same row/divider shape as .file-row above, not a
   card grid -- a name+email pair has no cover image or "Open" affordance
   to justify one.
   ========================================================================= */
.roster-list { list-style: none; margin: 0; padding: 0; }
.roster-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.roster-row:last-child { border-bottom: none; }
.roster-row-name { font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.roster-row-email { font-size: 14px; color: var(--mid-brown); }
/* Same look as .roster-row-email -- a generic secondary metadata line
   (Aug 2026: reused for a document's "Published <date>" row, not just an
   email), named for what it means rather than the one context it started in. */
.roster-row-meta { font-size: 14px; color: var(--mid-brown); }

/* Roster rows are buttons in the publish flow (Phase Two) -- reset button
   chrome on top of the existing flex/spacing rules above rather than
   duplicating them. */
.roster-row--button {
  width: 100%;
  background: none;
  border: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.roster-row--button:hover .roster-row-name, .roster-row--button:focus-visible .roster-row-name { text-decoration: underline; }

/* Roster rows as real links (Aug 2026: client-facing document list,
   protocol.js) -- same flex/spacing rules, no button chrome to reset,
   just the underline-on-hover an <a> doesn't get by default here. */
.roster-row--link { display: flex; text-decoration: none; color: inherit; }
.roster-row--link:hover .roster-row-name, .roster-row--link:focus-visible .roster-row-name { text-decoration: underline; }

/* Practitioner-facing "what this client already has" list (Aug 2026,
   practitioner.js's publish panel) -- title+date stacked on the left,
   Delete pinned right. A new class rather than reusing .roster-row: that
   one puts name/email side by side with no justify-content, the wrong
   shape for a row that also needs a button pinned to the far right. */
.doc-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.doc-row:last-child { border-bottom: none; }
.doc-row-title { display: block; font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.doc-row-date { display: block; font-size: 14px; color: var(--mid-brown); }

/* Delete affordance on each row above. Small, quiet, red only on hover --
   not the primary action on this row; opening the document isn't offered
   here at all (there is nothing to preview after publish; she already
   saw it before confirming). */
.doc-row-delete {
  background: none;
  border: none;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--mid-brown);
  cursor: pointer;
  padding: 6px 10px;
  flex-shrink: 0;
}
.doc-row-delete:hover, .doc-row-delete:focus-visible { color: var(--warn); }

/* =========================================================================
   PRACTITIONER -- publish panel (practitioner.html, Phase Two)
   ========================================================================= */
.publish-section { max-width: 640px; }
.publish-section .field { margin-top: var(--space-lg); }
#publish-preview-wrap { margin-top: var(--space-lg); }
#publish-open-confirm { margin-top: var(--space-xl); }

.publish-confirm {
  margin-top: var(--space-xl);
  padding: var(--space-lg);
  background: var(--additional-brown);
  border-radius: var(--radius-lg);
}
.publish-confirm-name { font-size: 20px; font-weight: 700; color: var(--dark-brown); margin: var(--space-xs) 0 0; }
.publish-confirm-email { font-size: 15px; color: var(--mid-brown); margin: 2px 0 var(--space-lg); }

.banner.success { border-color: var(--green); }

/* =========================================================================
   CLIENT MESSAGING (messages.html, client-facing; the "Messages" hub
   section in practitioner.js, staff-facing) -- _artifacts/
   client-messaging-spec.md.
   ========================================================================= */

/* "Message your coach" library card -- identical wrapper shape to
   .your-protocol above it, deliberately: same reasoning, same spacing. */
.your-messages { margin-bottom: var(--space-xxl); }
.your-messages h2 { margin-bottom: var(--space-md); }

/* The always-visible SLA/crisis block (S4). Never a dismissible banner --
   no close affordance anywhere in this rule, on purpose. A left accent
   bar on the crisis paragraph specifically (not the whole block, and not
   .banner.error's full-border treatment) draws the eye to it without
   reading as "something is broken," which it isn't. */
.safety-note {
  background: var(--additional-brown);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
  margin-bottom: var(--space-lg);
}
.safety-note p { margin: 0; }
.safety-note p + p { margin-top: var(--space-sm); }
.safety-note-crisis {
  border-left: 3px solid var(--warn);
  padding-left: var(--space-md);
}

/* Thread history -- chronological, oldest at top, ordinary conversation
   reading order (S9). "Not live" is carried by the absence of any
   presence/typing/read-receipt chrome here, not by the layout. */
.msg-thread { margin-bottom: var(--space-lg); }
.msg-thread-empty { padding: var(--space-lg) 0; }
.msg-row { display: flex; margin-bottom: var(--space-sm); }
.msg-row--mine { justify-content: flex-end; }
.msg-row--theirs { justify-content: flex-start; }
.msg-bubble {
  max-width: 80%;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-lg);
  background: var(--site-white);
  border: 1.5px solid var(--light-brown);
}
.msg-row--mine .msg-bubble { background: var(--beige); border-color: var(--beige); }
.msg-bubble-body { margin: 0; font-size: 15px; color: var(--dark-brown); line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.msg-bubble-time { margin: var(--space-xs) 0 0; font-size: 12px; color: var(--mid-brown); }
/* Team chat only (teamChat.js) -- a group/Team conversation can have more
   than one "theirs" sender, unlike a client thread's own two-side layout,
   so a non-mine bubble names who sent it. */
.msg-bubble-sender { margin: 0 0 2px; font-size: 12px; font-weight: 700; color: var(--mid-brown); }

/* Composer -- pinned at the bottom of the thread on both surfaces. No
   typing indicator, no send-state beyond the button's own disabled/label
   lifecycle -- deliberately nothing that implies a watching human. */
.composer { margin-top: var(--space-md); }
.composer-textarea {
  width: 100%;
  min-height: 96px;
  padding: var(--space-sm) var(--space-md);
  background: var(--site-white);
  border: 1.5px solid var(--dark-brown);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--dark-brown);
  resize: vertical;
  margin-bottom: var(--space-sm);
}
#composer-send { margin-top: var(--space-sm); width: auto; padding: 12px 32px; min-height: 44px; }

/* Frozen (completed/withdrawn) thread -- expected, not broken. Reuses
   .banner's shape with a neutral border, never .banner.error's warn
   color -- same tone the practitioner spec's video-expired copy uses. */
.banner.frozen-note { border-color: var(--light-brown); background: var(--additional-brown); }

/* Staff-facing thread list row (practitioner.js) -- same shape as
   .roster-row, plus an unread badge pinned right. */
.thread-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  min-height: 56px;
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
  width: 100%;
  background: none;
  border-top: none;
  border-left: none;
  border-right: none;
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.thread-row:last-child { border-bottom: none; }
.thread-row:hover .thread-row-name, .thread-row:focus-visible .thread-row-name { text-decoration: underline; }
.thread-row-name { font-size: 16px; font-weight: 700; color: var(--dark-brown); }
.thread-row-preview { display: block; font-size: 14px; color: var(--mid-brown); max-width: 42em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.unread-badge {
  flex-shrink: 0;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: var(--warn);
  color: var(--site-white);
  font-size: 12px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* =========================================================================
   PRACTITIONER -- client hub (practitioner.js). One page per client, five
   stacked sections (Documents, Messages, Forms, Scheduling, Video
   sessions) -- everything visible at once, no tabs hiding what's there.
   ========================================================================= */
.hub-nav { display: flex; flex-wrap: wrap; gap: var(--space-md); margin: var(--space-md) 0 var(--space-xl); }
.hub-nav a { font-size: 14px; font-weight: 600; color: var(--green); text-decoration: underline; }
.hub-section { margin-bottom: var(--space-xxl); scroll-margin-top: var(--space-lg); }
.hub-section h2 { margin-bottom: var(--space-md); }

/* Forms/Scheduling/Video -- visually present, unmistakably not
   interactive: muted fill, dashed border, no card hover/CTA affordance,
   nothing to click. A quiet, clearly-labelled state, not a disabled
   button she could click anyway. */
.hub-section--placeholder {
  background: var(--off-white);
  border: 1.5px dashed var(--light-brown);
  border-radius: var(--radius-lg);
  padding: var(--space-lg);
}
.hub-section--placeholder h2 { color: var(--mid-brown); margin-bottom: var(--space-sm); }
.hub-placeholder-tag {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--mid-brown);
  background: var(--beige);
  border-radius: var(--radius-pill);
  padding: 4px 10px;
  margin-bottom: var(--space-sm);
}
.hub-placeholder-note { margin: 0; font-size: 15px; color: var(--mid-brown); line-height: 1.5; }

/* =========================================================================
   PRACTITIONER LANDING -- "Today's schedule" (Aug 2026, Shawn: page one
   has to answer "what does my day look like" before "which client do I
   want," which the client list alone never did). Fed by real Cal.com
   webhook data as of this pass (see practitioner.js's fetchUpcomingBookings
   and functions/api/cal-webhook.ts) -- the dashed/off-white "not real yet"
   treatment this used as a placeholder is gone; a solid border reads as an
   ordinary contained module now, same as everywhere else real content sits
   in a box on this page.
   ========================================================================= */
.schedule-section { margin-bottom: var(--space-xxl); }
.schedule-list {
  list-style: none;
  margin: 0;
  padding: var(--space-sm) var(--space-lg);
  background: var(--off-white);
  border: 1px solid var(--light-brown);
  border-radius: var(--radius-lg);
}
.schedule-row {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--light-brown);
}
.schedule-row:last-child { border-bottom: none; }
.schedule-row-time { font-size: 15px; font-weight: 700; color: var(--dark-brown); min-width: 84px; flex-shrink: 0; }
.schedule-row-detail { font-size: 15px; color: var(--mid-brown); }

/* =========================================================================
   APPROVED SYSTEMS LIST -- Exhibit-B-style table + prose, shared by the
   live read-only view, any historical printable view, and the owner's
   edit rows (which reuse the same table shape with inline inputs). No
   table styling existed anywhere in this stylesheet before this feature,
   even though the protocol-document allowlist has permitted <table> in
   published client content since Aug 2026 -- that gap is real but out of
   scope here; these classes are scoped to .exhibit-table only, not a
   generic <table> reset.
   ========================================================================= */
.exhibit-table { width: 100%; border-collapse: collapse; margin-bottom: var(--space-xl); }
.exhibit-table th, .exhibit-table td { text-align: left; padding: var(--space-sm) var(--space-md); border-bottom: 1px solid var(--light-brown); vertical-align: top; font-size: 15px; }
.exhibit-table th { background: var(--additional-brown); color: var(--dark-brown); font-weight: 700; }
.exhibit-table td { color: var(--dark-brown); }
.exhibit-table tr:last-child td { border-bottom: none; }
.exhibit-row--retiring td { color: var(--mid-brown); }
.retiring-tag { display: inline-block; margin-left: var(--space-sm); padding: 2px 10px; border-radius: var(--radius-pill); background: var(--warn); color: var(--site-white); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
.exhibit-notes p { font-size: 15px; line-height: 1.6; color: var(--dark-brown); margin: 0 0 var(--space-md); }
.exhibit-notes p:last-child { margin-bottom: 0; }

.unpublished-banner { display: flex; align-items: center; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; }
/* Non-dismissible on purpose -- no close affordance anywhere in this
   markup. A published-but-unnotified change is genuinely not in effect
   (NDA 3.3); the owner must keep seeing this until a retry actually
   succeeds, not until she clicks it away. */
.banner.unnotified-banner { border-color: var(--warn); }

.history-list { list-style: none; margin: 0; padding: 0; }
.history-row { padding: var(--space-md) 0; border-bottom: 1px solid var(--light-brown); }
.history-row:last-child { border-bottom: none; }
.history-row-summary { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-md); flex-wrap: wrap; cursor: pointer; }
.history-row-date { font-size: 15px; font-weight: 700; color: var(--dark-brown); }
.history-row-meta { font-size: 14px; color: var(--mid-brown); }
.history-row-diff { margin-top: var(--space-sm); font-size: 14px; color: var(--dark-brown); display: none; }
.history-row-diff.is-open { display: block; }
.history-row-diff ul { list-style: disc; padding-left: 1.4em; margin: var(--space-xs) 0 var(--space-sm); }
.history-row-diff li { margin-bottom: var(--space-xs); }

.owner-edit-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--space-sm); padding: var(--space-md) 0; border-bottom: 1px solid var(--light-brown); }
.owner-edit-row .field { margin-bottom: 0; }
.owner-edit-row .field-label { margin-bottom: 4px; }

@media print {
  body * { visibility: hidden; }
  #printable-exhibit, #printable-exhibit * { visibility: visible; }
  #printable-exhibit { position: absolute; top: 0; left: 0; width: 100%; }
  .btn, .unpublished-banner, .banner.unnotified-banner, #approved-systems-header { display: none !important; }
}
