/* index.css - Life Balance Index — formal / academic self-assessment design system */

:root {
  /* Color tokens — warm-paper professional palette.

     These names used to describe the palette this app had BEFORE the move to
     warm paper, and were kept on the argument that JS inline styles referenced
     them. The names lost that argument: --color-obsidian, "obsidian", held
     #eceae4 — a near-white rail fill. A token whose name says black and whose
     value is off-white is a trap for whoever next reaches for black, and on a
     solo project that is the author. Renamed to say what they are:
       gold    -> accent   (burgundy #6d2e3f; never was gold)
       astral  -> slate    (navy-slate; never was neon)
       nectar  -> success
       obsidian-> track    (progress-bar and rail backgrounds)
     Safe to do mechanically, and safe to do again: no call site builds a
     property name dynamically — zero hits for getPropertyValue and for
     var(--...${...}) across the codebase — so every reference is a literal a
     find-replace can see. --color-navy and --color-crimson kept their names
     because #24344d and #a23b3b are in fact navy and crimson. */
  --bg-primary: #f7f5f0;           /* warm paper */
  --color-card-bg: #ffffff;        /* cards */
  --color-card-border: #e3ded4;    /* muted borders */
  /* Form-control boundaries, deliberately NOT --color-card-border.
     WCAG 1.4.11 wants 3:1 for the visual boundary of an input, and #e3ded4 on
     the field's own white fill is 1.34:1. Card edging is decorative and exempt,
     so the two cases need two tokens; #767676 is 4.54:1 on #ffffff. */
  --color-input-border: #767676;

  /* Restrained burgundy accent */
  --color-accent: #6d2e3f;
  --color-accent-light: #8a4a5a;
  --color-accent-glow: rgba(109, 46, 63, 0.14);

  --color-navy: #24344d;           /* primary / navy */

  /* Quiet navy-slate for links, progress fills, and focus rings */
  --color-slate: #3a5170;
  --color-slate-dark: #24344d;
  --color-slate-glow: rgba(36, 52, 77, 0.08);

  --color-success: #3f7d5a;        /* success */
  --color-crimson: #a23b3b;        /* danger */
  --color-crimson-light: #c25a5a;

  --color-text-primary: #1f2733;   /* ink */
  --color-text-secondary: #5c6672; /* secondary */
  --color-text-chart: #1f2733;     /* chart labels */
  --color-track: #eceae4;          /* progress track / rail background */

  /* Typography: Source Serif 4 headings, Inter body, Sarabun for Thai */
  --font-serif: 'Source Serif 4', 'Sarabun', Georgia, serif;
  --font-sans: 'Inter', 'Sarabun', system-ui, sans-serif;
  --font-mono: ui-monospace, 'Sarabun', 'SFMono-Regular', Menlo, monospace;

  /* Type scale.
     Before this existed the sheet carried 25 distinct font sizes and the JS
     inline styles carried 10 more, sixteen of them inside the 0.62-0.95rem
     span — a 5px range holding sixteen steps, which is nudging, not a scale.
     The steps below were chosen so the three most-used values (0.75, 0.8,
     0.85) keep their exact numbers and roughly two thirds of call sites
     render identically; only the strays moved, and none moved by more than
     0.8px. Two tiers on purpose: ~1.06-1.11 ratios through the UI text range,
     where small differences carry hierarchy inside a paragraph, and ~1.3
     jumps for display figures, where the number IS the content.
     One deliberate hold-out: the 16px literal in the mobile form-control
     block near the end of this file. iOS zooms the viewport on focus for any
     input under 16px, so that value is a behavioural guard, not a type
     choice, and must not be folded into this scale. */
  --text-2xs: 0.68rem;
  --text-xs: 0.75rem;
  --text-sm: 0.8rem;
  --text-base: 0.85rem;
  --text-md: 0.9rem;
  --text-lg: 1rem;
  --text-xl: 1.15rem;
  --text-2xl: 1.5rem;
  --text-3xl: 1.9rem;
  --text-4xl: 2.6rem;

  /* Layout */
  --radius: 4px;
  --transition-speed: 0.2s;
}

/* Reset */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-sans);
  background-color: var(--bg-primary);
  color: var(--color-text-primary);
  min-height: 100vh;
  overflow-x: hidden;
  line-height: 1.55;
  position: relative;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Reserve the scrollbar's column whether or not a scrollbar is present.
   Without this the centered .app-container shifts sideways by the scrollbar
   width every time a route swaps a tall view for a short one — the dashboard
   scrolls, the Weekly Review often does not — so the header, the tabs, and the
   whole card grid visibly jump on navigation. `stable` keeps the gutter
   allocated on the inline-end edge at all times. It belongs on <html>, the
   scrolling element; body already carries overflow-x: hidden, which would
   suppress the gutter rather than reserve it. */
html {
  scrollbar-gutter: stable;
}

/* Scrollbar — quiet neutral */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: #cfc9bd;
  border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
  background: #b9b2a3;
}

/* Layout Utilities */
/* 1240px, not a round 1200: the deep assessment renders its questions in plain
   .deep-section cards with no max-width of their own, so they stretch to this
   container's content box — 1240 minus the 2x24px padding = 1192px. The widest
   .radio-group in DEEP_INSTRUMENTS is cfc12 at 1100px, and 1100 + 84px of card
   and fieldset chrome = 1184, which 1192 covers with 8px to spare.
   The shell is sized by the widest thing that has to fit inside it. That thing
   is .deep-section, NOT .onboarding-container — that container is capped at
   981px for its own reasons and is never the constraint here. */
.app-container {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  max-width: 1240px;
  margin: 0 auto;
  /* Bottom padding keeps content clickable above the floating assistant */
  padding: 24px 24px 160px;
  position: relative;
}

header {
  border-bottom: 1px solid var(--color-card-border);
  padding: 4px 4px 18px;
  margin-bottom: 24px;
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 16px;
  flex-wrap: wrap;
}

.brand {
  display: flex;
  flex-direction: column;
}

/* Footer: privacy / source / version links (kept above the assistant pad). */
.app-footer {
  margin-top: auto;
  padding-top: 20px;
  border-top: 1px solid var(--color-card-border);
  display: flex;
  gap: 10px;
  /* The links below are 24px tall while the "·" separators are only as tall as
     their own text, so without this the dots hang from the top of the row. */
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

/* WCAG 2.2 SC 2.5.8 (Target Size, Minimum) asks for 24x24 CSS px. These links
   were a 19.8px line box — the height of their own text and nothing more — so
   all three failed on the dashboard, both failed on privacy.html, and they
   failed at DESKTOP width too: 2.5.8 governs pointer input generally, not just
   touch. Width was never the problem; the narrowest is "Methodology" at 79.8.

   min-height rather than vertical padding: padding would have to be recomputed
   the day --text-sm moves, whereas a floor stays a floor. It also reads as a
   floor in the source, which is the point of the rule.

   This is the AA criterion. SC 2.5.5 (Target Size) asks 44x44 and is AAA — a
   44px floor here would add 28px to the two-row phone footer, and this is
   secondary navigation sitting under a screen whose length was the whole
   subject of the two preceding releases. Left at AA deliberately. */
.app-footer a {
  color: var(--color-text-secondary);
  text-decoration: underline;
  text-underline-offset: 2px;
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

.app-footer a:hover {
  color: var(--color-navy);
}

.brand h1 {
  font-family: var(--font-serif);
  font-size: var(--text-3xl);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--color-navy);
  line-height: 1.1;
}

.brand p {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--color-text-secondary);
  margin-top: 4px;
}

/* Navigation Tabs */
.nav-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 24px;
  border-bottom: 1px solid var(--color-card-border);
}

.tab-btn {
  font-family: var(--font-sans);
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--color-text-secondary);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 10px 16px;
  cursor: pointer;
  transition: color var(--transition-speed) ease, border-color var(--transition-speed) ease;
  position: relative;
  margin-bottom: -1px;
}

.tab-btn:hover {
  color: var(--color-navy);
}

.tab-btn.active {
  color: var(--color-navy);
  border-bottom-color: var(--color-accent);
  font-weight: 600;
}

/* Cards / panels */
.card {
  background: var(--color-card-bg);
  border: 1px solid var(--color-card-border);
  box-shadow: 0 1px 2px rgba(31, 39, 51, 0.03);
  border-radius: var(--radius);
  padding: 22px 24px;
  margin-bottom: 18px;
  position: relative;
}

.card-header {
  font-family: var(--font-serif);
  font-size: var(--text-xl);
  font-weight: 600;
  border-bottom: 1px solid var(--color-card-border);
  padding-bottom: 10px;
  margin-bottom: 18px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--color-navy);
}

/* Grid Layouts */
.dashboard-grid {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 18px;
}

@media (max-width: 768px) {
  .dashboard-grid {
    grid-template-columns: 1fr;
  }
  .nav-tabs {
    flex-wrap: wrap;
    gap: 2px;
  }
  .tab-btn {
    flex: 1 1 auto;
    text-align: center;
    padding: 8px 10px;
  }
}

/* On phones the four tabs cannot share one row; a balanced 2x2 grid beats
   flex-wrap's stretched orphan row (three tabs up top, one full-width below). */
@media (max-width: 540px) {
  .nav-tabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
  .tab-btn {
    padding: 8px 6px;
  }
}

/* Buttons */
.btn {
  font-family: var(--font-sans);
  font-weight: 500;
  font-size: var(--text-md);
  padding: 9px 18px;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background var(--transition-speed) ease, border-color var(--transition-speed) ease, color var(--transition-speed) ease;
  border: 1px solid var(--color-card-border);
  background: #ffffff;
  color: var(--color-navy);
}

.btn:hover {
  background: #f2efe8;
  border-color: #cfc9bd;
}

.btn-primary {
  background: var(--color-navy);
  color: #fff;
  border-color: var(--color-navy);
}

.btn-primary:hover {
  background: #1a2739;
  border-color: #1a2739;
  color: #fff;
}

.btn-danger {
  color: var(--color-crimson);
  border-color: #e0cccc;
  background: #ffffff;
}
.btn-danger:hover {
  background: var(--color-crimson);
  color: #fff;
  border-color: var(--color-crimson);
}

/* Pressed state.
   Every button above had :hover and nothing else, which is a desktop-shaped
   assumption: a finger produces no hover, so on the app's primary surface the
   only confirmation that a tap landed was whatever the tap went on to do —
   and a slow render reads as a dead button, so people tap again. Same failure
   as the v51 level tooltip: an affordance delivered only through hover is
   not delivered at all on a phone.
   translateY(1px) rather than a colour shift, because the colour change has
   to survive :hover having already fired on desktop while never firing on
   touch; 1px of travel reads identically in both cases. transition is
   deliberately not extended to transform — the press must feel immediate,
   and easing it in would reintroduce the lag this is here to deny.
   Deliberately absent from the prefers-reduced-motion block further down:
   1px over 0ms is feedback, not decoration, and provokes no vestibular
   response. Suppressing it would withdraw the tap confirmation from exactly
   the readers most likely to need confirming. */
.btn:active,
.tab-btn:active {
  transform: translateY(1px);
}

/* Onboarding & Forms */

/* 981px is measured, not chosen: it is the widest .radio-group the INSTRUMENTS
   set produces (897px — who5's five labels, in English) plus the 84px of card
   and fieldset padding between this container's border box and the group's
   content box. At the previous 660px the group had 576px, so every scale above
   cfpb10 wrapped its options onto a second line.
   This container governs onboarding and the check-in, and those two screens
   only ever render INSTRUMENTS — so who5, not the wider DEEP_INSTRUMENTS
   scales, sets the number. The deep assessment renders its questions in plain
   .deep-section cards, which stretch to the shell instead; see .app-container.
   Sizing this one for the deep worst case (1184px) is what left a 400px empty
   gutter beside every option row on onboarding.
   Measured across all 151 items in both languages; Thai's worst is who5 at
   753, so English sets the requirement on both screens.
   who5 fits with zero pixels to spare: 981 leaves the group exactly its 897.
   That is deliberate — any slack here is dead space on every other row — but
   it means lengthening a who5 label, or changing the body font, wraps it.
   Re-measure if either changes.
   Below roughly a 1080px viewport the shell can no longer supply 981 and who5
   wraps again — that is a limit of the layout, not a regression.
   The legend rule below caps the question text so it does not inherit this
   width as its line length. */
.onboarding-container {
  max-width: 981px;
  margin: 24px auto;
}

.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 500;
  margin-bottom: 6px;
  color: var(--color-text-primary);
}

.form-control {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--color-input-border);
  border-radius: var(--radius);
  background: #ffffff;
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  transition: border-color var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.form-control:focus {
  outline: none;
  border-color: var(--color-navy);
  box-shadow: 0 0 0 3px var(--color-slate-glow);
  background: #ffffff;
}

/* Inline onboarding validation message (validation.js / doSubmit) */
.field-error {
  display: block;
  margin-top: 4px;
  color: var(--color-crimson);
  font-size: var(--text-sm);
  font-weight: 600;
}

/* Blank-first forms: the red "*" mandatory marker and invalid-control cues. */
.req {
  color: var(--color-crimson);
  font-weight: 700;
  margin-left: 1px;
}
.input-invalid {
  border-color: var(--color-crimson) !important;
}
.survey-question-invalid {
  border-color: var(--color-crimson);
}
.survey-question-invalid legend {
  color: var(--color-crimson);
}

.survey-question {
  background: #faf9f6;
  border: 1px solid var(--color-card-border);
  padding: 16px;
  border-radius: var(--radius);
  margin-bottom: 14px;
  min-inline-size: auto; /* fieldset shrink-wrap fix */
}

/* The container widened to 1184px so the option row fits on one line. The
   question text must not inherit that as its measure: a 1100px line is roughly
   170 characters, well past the ~75 where the eye starts losing its place on
   the return sweep. The option row still gets the full width; only the prose
   is capped. ch, not px, so the cap tracks the font rather than fighting it. */
.survey-question legend {
  font-weight: 500;
  font-size: var(--text-md);
  color: var(--color-text-primary);
  padding: 0 6px;
  max-width: 75ch;
}

.instrument-block {
  border-top: 1px solid var(--color-card-border);
  padding-top: 16px;
  margin-top: 20px;
}

.instrument-title {
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-lg);
  color: var(--color-navy);
  margin-bottom: 12px;
}

.grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

@media (max-width: 600px) {
  .grid-2 {
    grid-template-columns: 1fr;
  }
}

/* Header action buttons. These values are exactly what used to sit in inline
   `style` attributes on #btn-lang / #btn-profile; they moved here only so the
   mobile layer can override them, which inline styles would have prevented. */
.header-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

.btn-compact {
  font-size: var(--text-sm);
  padding: 4px 10px;
}

/* The Remove control on each pledge row. Its font-size and padding lived in a
   style="" attribute on the button (views/quests.js:36) — the same trap
   index.html documents for the header buttons: an inline declaration outranks
   every media query, so the mobile layer could never have reached it. Moved
   here for the same reason .btn-compact exists, and floored at 24px.

   Measured at 1440px: 68.1x21 (EN), 36.8x22 (TH) — under the 24px floor of
   WCAG 2.2 SC 2.5.8 (Target Size Minimum, AA) on HEIGHT only. Width passed in
   both languages; an earlier note claiming the Thai button also failed on
   width was wrong. Desktop-only, because the mobile layer floors .btn at 44px.

   inline-flex and centring rather than taller padding: padding would widen the
   button as well, and three of these sit in a column beside the pledge text
   where the extra width has nowhere to go. */
.pledge-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 24px;
  padding: 2px 10px;
  font-size: var(--text-xs);
}

.radio-group {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

.radio-option {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--text-md);
  cursor: pointer;
  color: var(--color-text-secondary);
  transition: color var(--transition-speed) ease;
}

.radio-option:hover {
  color: var(--color-text-primary);
}

.radio-option input[type="radio"] {
  accent-color: var(--color-navy);
}

/* Floating guidance assistant */
.assistant-wrapper {
  position: fixed;
  bottom: 24px;
  right: 24px;
  display: flex;
  align-items: flex-end;
  gap: 12px;
  z-index: 100;
  pointer-events: none;
}

.speech-bubble {
  background: #ffffff;
  border: 1px solid var(--color-card-border);
  border-left: 3px solid var(--color-accent);
  border-radius: var(--radius);
  padding: 13px 16px;
  max-width: 280px;
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: 1.5;
  box-shadow: 0 6px 20px rgba(31, 39, 51, 0.1);
  position: relative;
  margin-bottom: 14px;
  pointer-events: auto;
  color: var(--color-text-primary);
}

.speech-bubble::after {
  content: '';
  position: absolute;
  right: -8px;
  bottom: 14px;
  width: 0;
  height: 0;
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
  border-left: 8px solid var(--color-card-border);
}

.speech-bubble::before {
  content: '';
  position: absolute;
  right: -6px;
  bottom: 15px;
  width: 0;
  height: 0;
  border-top: 7px solid transparent;
  border-bottom: 7px solid transparent;
  border-left: 7px solid #ffffff;
  z-index: 1;
}

/* Guidance assistant (Lumi) avatar image */
.assistant-avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  background: #ffffff;
  border: 2px solid var(--color-navy);
  box-shadow: 0 6px 16px rgba(31, 39, 51, 0.14);
  cursor: pointer;
  pointer-events: auto;
  flex-shrink: 0;
  transition: transform var(--transition-speed) ease, box-shadow var(--transition-speed) ease;
}

.assistant-avatar:hover {
  transform: scale(1.05);
  box-shadow: 0 8px 20px rgba(31, 39, 51, 0.2);
}

/* Level / progress */
.level-badge {
  font-family: var(--font-sans);
  background: var(--color-navy);
  color: #ffffff;
  padding: 3px 10px;
  border-radius: var(--radius);
  font-size: var(--text-sm);
  font-weight: 600;
  border: 1px solid var(--color-navy);
}

/* Says, in plain sight, that the badge above is an age and the bar beside it is
   this year's points — two unrelated clocks. This was a title attribute until it
   was pointed out that a title is a hover affordance and the app is used mostly
   on a phone, so the reader who most needs the sentence never saw it. 0.75rem is
   the type floor the rest of the sheet holds to; the secondary colour keeps it a
   footnote to the name rather than a third figure competing on the card. */
.level-note {
  font-family: var(--font-serif);
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
  margin-top: 4px;
}

/* The "Your year ›" link under the level note.
   Was four declarations inline on the anchor in views/dashboard.js, which is
   how it ended up at 64x19 — a --text-xs line box, under the 24px WCAG 2.2
   SC 2.5.8 floor, and invisible to any sweep of the stylesheet because the
   sizing did not live here.

   inline-flex, not inline-block: an inline-block honours min-height but still
   aligns on the text baseline, which would leave the extra height hanging below
   the glyphs instead of splitting evenly around them. The 6px top margin the
   inline style carried is kept, so spacing under .level-note is unchanged. */
.card-link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  margin-top: 6px;
  font-size: var(--text-xs);
  font-weight: 600;
}

.xp-bar-container {
  width: 100%;
  height: 8px;
  background: var(--color-track);
  border-radius: 4px;
  margin-top: 8px;
  overflow: hidden;
  border: 1px solid var(--color-card-border);
}

.xp-bar-fill {
  height: 100%;
  background: var(--color-navy);
  width: 0%;
  transition: width 0.6s ease;
}

/* Recent-activity log panel */
.terminal {
  background: #f4f2ec;
  color: var(--color-text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  padding: 14px 16px;
  border-radius: var(--radius);
  max-height: 250px;
  overflow-y: auto;
  border: 1px solid var(--color-card-border);
}

.terminal-line {
  margin-bottom: 8px;
  line-height: 1.45;
  border-left: 2px solid var(--color-card-border);
  padding-left: 10px;
  color: var(--color-text-secondary);
}

.terminal-accent {
  color: var(--color-text-primary);
  font-weight: 600;
}

.terminal-gold {
  color: var(--color-accent);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

/* Small tag pills (mission type, tier) */
.holo-badge {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  background: #f2efe8;
  color: var(--color-navy);
  border: 1px solid var(--color-card-border);
  padding: 2px 8px;
  border-radius: 3px;
}

/* Score marker on the profile card */
.seal {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--color-navy);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-serif);
  font-weight: 700;
  font-size: var(--text-lg);
  border: 1px solid var(--color-navy);
}

/* Routine action cards */
.action-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 12px;
}

.action-card {
  position: relative;
  border: 1px solid var(--color-card-border);
  padding: 14px;
  border-radius: var(--radius);
  background: #ffffff;
  text-align: center;
  cursor: pointer;
  transition: border-color var(--transition-speed) ease, background var(--transition-speed) ease;
}

.action-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  border: none;
  border-radius: 50%;
  background: #f2e6e6;
  color: var(--color-crimson);
  font-size: var(--text-2xs);
  cursor: pointer;
  line-height: 1;
}

.action-remove:hover {
  background: var(--color-crimson);
  color: #fff;
}

.action-card:hover {
  background: #faf9f6;
  border-color: #cfc9bd;
}

.action-title {
  font-family: var(--font-serif);
  font-weight: 600;
  font-size: var(--text-md);
  margin-bottom: 6px;
  color: var(--color-navy);
}

.action-impacts {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  color: var(--color-accent);
  font-weight: 600;
}

/* Popups & dialogs */
.popup-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(31, 39, 51, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.popup-card {
  background: #ffffff;
  border: 1px solid var(--color-card-border);
  border-top: 3px solid var(--color-accent);
  border-radius: var(--radius);
  width: 90%;
  max-width: 440px;
  padding: 28px;
  box-shadow: 0 20px 50px rgba(31, 39, 51, 0.25);
  text-align: center;
}

.popup-title {
  font-family: var(--font-serif);
  font-size: var(--text-2xl);
  font-weight: 700;
  margin-bottom: 10px;
  color: var(--color-navy);
}

/* Share sheet. `.card-header` is already a space-between flex row, so the
   trigger needs no wrapper of its own. */
.share-open {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--color-accent);
  background: none;
  border: 1px solid var(--color-card-border);
  border-radius: var(--radius);
  padding: 4px 12px;
  cursor: pointer;
}

.share-open:hover {
  background: var(--color-accent-glow);
}

/* The sheet is taller than the other popups (preview + two control rows), so
   it scrolls internally rather than pushing its buttons off a short screen. */
.share-card {
  max-width: 420px;
  max-height: 92vh;
  overflow-y: auto;
}

/* The canvas carries a 1080x1920 backing store; these bounds only scale how it
   is displayed, and width/height auto keeps the 9:16 ratio intact. */
.share-preview {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 42vh;
  margin: 0 auto 16px;
  border: 1px solid var(--color-card-border);
  border-radius: var(--radius);
}

.share-options {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 14px;
}

.share-option-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-wrap: wrap;
}

.share-option-label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}

.share-toggle-set {
  display: flex;
  gap: 4px;
}

.share-toggle {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  padding: 5px 10px;
  border: 1px solid var(--color-card-border);
  background: #ffffff;
  color: var(--color-text-secondary);
  border-radius: var(--radius);
  cursor: pointer;
}

.share-toggle[aria-pressed="true"] {
  background: var(--color-navy);
  border-color: var(--color-navy);
  color: #ffffff;
}

.share-note,
.share-care {
  font-size: var(--text-xs);
  line-height: 1.5;
  text-align: left;
  margin-bottom: 12px;
}

.share-note {
  color: var(--color-text-secondary);
}

.share-care {
  color: var(--color-text-primary);
  background: var(--color-accent-glow);
  border-left: 3px solid var(--color-accent);
  padding: 8px 10px;
  border-radius: 0 var(--radius) var(--radius) 0;
}

.share-actions {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* Dashboard aspect rows (links to aspect pages) */
.aspect-row {
  display: block;
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius);
  padding: 6px 8px;
  margin: -2px -8px;
  transition: background var(--transition-speed) ease;
}

.aspect-row:hover {
  background: #faf9f6;
}

.aspect-row-arrow {
  color: var(--color-text-secondary);
  font-weight: 600;
}

/* Aspect detail pages */
.aspect-back {
  /* 21.08px tall before this — the line-height of a 15px sans line, 2.9px short
     of the 24px floor in WCAG 2.2 SC 2.5.8 (Target Size Minimum, AA). It is the
     back link on four routes (#/aspect/*, #/methodology, #/deep, #/checkin) and
     failed identically at 375px and 1440px in both languages, so this is not a
     phone-only fix. Width was never the problem: 77px in English, 62px in Thai.

     inline-flex rather than inline-block: inline-block honours min-height but
     still sits on the text baseline, so the added 3px would hang below the
     glyphs. Centring splits it evenly and the arrow stays optically aligned
     with the label. Not padding, because there is no wrapping to preserve here
     — this link is one short phrase that never breaks. 24px (AA) not 44px
     (2.5.5, AAA): the extra 23px would be pure dead space above a heading. */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  margin-bottom: 14px;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--color-navy);
  text-decoration: none;
}

.aspect-back:hover {
  text-decoration: underline;
}

.aspect-header-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}

.aspect-title {
  font-family: var(--font-serif);
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--color-navy);
}

.aspect-blurb {
  font-size: var(--text-base);
  color: var(--color-text-secondary);
  margin-top: 4px;
}

/* The non-goal statement on #/methodology. Set apart from the surrounding
   blurbs on purpose: it is the one paragraph that says what the eight numbers
   are NOT, and it must not read as fine print. */
.methodology-nongoal {
  color: var(--color-navy);
  border-left: 3px solid var(--color-navy);
  padding-left: 12px;
  margin-top: 14px;
  line-height: 1.55;
}

.aspect-score-badge {
  flex-shrink: 0;
  background: #faf9f6;
  border: 1px solid var(--color-card-border);
  border-radius: var(--radius);
  padding: 10px 18px;
  text-align: center;
}

.aspect-score-value {
  font-family: var(--font-mono);
  font-size: var(--text-3xl);
  font-weight: 700;
  color: var(--color-navy);
}

.aspect-score-max {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

/* Percentile gauge */
.gauge-track {
  position: relative;
  height: 12px;
  border-radius: 6px;
  background: var(--color-track);
  border: 1px solid var(--color-card-border);
  overflow: visible;
  margin: 8px 0 6px;
}

.gauge-fill {
  height: 100%;
  border-radius: 6px;
  background: var(--color-slate);
}

.gauge-marker {
  position: absolute;
  top: -4px;
  width: 3px;
  height: 20px;
  border-radius: 2px;
  background: var(--color-accent);
  transform: translateX(-50%);
}

.gauge-caption {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 8px;
  font-family: var(--font-sans);
  color: var(--color-navy);
}

.gauge-summary {
  font-size: var(--text-base);
  color: var(--color-text-secondary);
  margin-top: 6px;
}

.gauge-note {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  margin-top: 4px;
  padding-left: 10px;
  border-left: 2px solid var(--color-card-border);
}

/* Component breakdown rows */
.component-row {
  margin-bottom: 14px;
}

.component-head {
  display: flex;
  justify-content: space-between;
  font-size: var(--text-base);
  font-weight: 500;
  margin-bottom: 2px;
}

.component-detail {
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
  margin-top: 3px;
}

/* Benchmark percentiles (dashboard) */
.benchmark-line {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
  margin-top: 4px;
}

.benchmark-method {
  opacity: 0.8;
  font-style: italic;
}

.benchmark-sources {
  margin-top: 14px;
  border-top: 1px dashed var(--color-card-border);
  padding-top: 10px;
  font-size: var(--text-xs);
}

.benchmark-sources summary {
  cursor: pointer;
  color: var(--color-navy);
  font-weight: 600;
}

.benchmark-disclaimer {
  margin: 8px 0;
  color: var(--color-text-secondary);
  line-height: 1.45;
}

.benchmark-sources ul {
  margin: 6px 0 0 18px;
  padding: 0;
}

/* 26px, not the 1.4 (16.8px) this used to be, so consecutive citation lines sit
   far enough apart for the 24px hit areas below to tile without overlapping.
   Applied to the line boxes that actually hold links — not to .benchmark-sources
   as a whole, which would also loosen .benchmark-disclaimer, a paragraph of
   prose that wants its 1.45. */
.benchmark-sources li,
.component-detail.benchmark-sources {
  line-height: 26px;
}

.benchmark-sources li {
  margin-bottom: 5px;
}

/* Citation links against WCAG 2.2 SC 2.5.8 (24x24). Measured at 1440px before
   this rule: 14 links on the methodology page at 15px tall, plus the dashboard's
   own source list once its <details> is opened. On a phone most of them wrap to
   two lines and clear 24 by accident, which is not the same as passing.

   These are NOT covered by the criterion's inline exception. That exemption is
   for a target "in a sentence, or whose size is constrained by the line-height
   of non-target text" — but citeLinks() emits nothing except links joined by
   " · ", and the dashboard list is one bare <a> per <li>. There is no sentence
   around them to be exempt inside of.

   padding-block, and NOT display:inline-block, which was considered and
   rejected: inline-block makes each citation an unbreakable box, and the widest
   label here measures 560px against a 375px phone. That trades a 9px shortfall
   for horizontal overflow. Vertical padding on an inline element expands the
   hit area — getBoundingClientRect and hit-testing both include it — while
   leaving line breaking exactly as it was, which is what a citation that has to
   wrap needs. 15px text box + 2x5 = 25px, inside a 26px line. */
.benchmark-sources a {
  color: var(--color-navy);
  word-break: break-word;
  padding-block: 5px;
}

/* Guideline checks (criterion-referenced card on aspect pages).
   Visually quieter than the grade badge on purpose: a guideline check is
   secondary to the cited population comparison, never a competing verdict. */
.criteria-caption {
  margin: 0 0 12px;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  line-height: 1.5;
}

.criteria-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.criterion-row {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 10px 0;
  border-top: 1px solid var(--color-card-border);
}

.criterion-row:first-child {
  border-top: none;
}

.criterion-chip {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.criterion-chip-met {
  background: rgba(31, 122, 90, 0.14);
  color: #1f7a5a;
}

.criterion-chip-unmet {
  background: rgba(176, 106, 22, 0.14);
  color: #8a5312;
}

.criterion-chip-unmeasured {
  background: rgba(32, 50, 76, 0.09);
  color: var(--color-text-secondary);
}

.criterion-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.criterion-name {
  font-weight: 600;
  font-size: var(--text-base);
}

.criterion-detail {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  line-height: 1.45;
}

/* Narrow screens: the chip drops above the text rather than squeezing it. */
@media (max-width: 420px) {
  .criterion-row {
    flex-direction: column;
    gap: 5px;
  }
}

/* Suggestions (dashboard + aspect pages) */
.suggestion-link {
  text-decoration: none;
  color: inherit;
  display: block;
}

.suggestion-item {
  border-left: 3px solid var(--color-slate);
  background: #faf9f6;
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 10px 14px;
  margin-bottom: 10px;
  transition: background var(--transition-speed) ease;
}

.suggestion-link:hover .suggestion-item {
  background: #f2efe8;
}

.suggestion-title {
  font-weight: 600;
  font-size: var(--text-md);
  color: var(--color-navy);
}

.suggestion-text {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  margin-top: 2px;
  line-height: 1.45;
}

.suggestion-meta {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--color-accent);
  margin-top: 4px;
}

/* Monthly re-assessment banner */
.checkin-banner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 14px;
  border: 1px solid var(--color-card-border);
  border-left: 3px solid var(--color-accent);
  background: #faf9f6;
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 18px;
}

.checkin-banner p {
  font-size: var(--text-base);
  color: var(--color-text-primary);
  line-height: 1.45;
}

/* Normalised prompt parts. These replace what used to be ad-hoc inline styles
   on each banner's text and action elements; the rendered desktop result is
   the same. The mobile layer relies on these hooks to demote every prompt
   after the first one to a compact row. */
.prompt-text {
  min-width: 0;
}

.prompt-actions {
  display: flex;
  gap: 8px;
  white-space: nowrap;
  flex-shrink: 0;
}

/* Duty-of-care mental-health support banner (finding #4) */
.care-banner {
  border: 1px solid #cdd9e4;
  border-left: 3px solid var(--color-navy);
  background: #eef3f8;
  border-radius: var(--radius);
  padding: 14px 18px;
  margin-bottom: 18px;
}
.care-banner-title {
  font-family: var(--font-serif);
  font-weight: 700;
  color: var(--color-navy);
  margin-bottom: 4px;
}
.care-banner-text {
  font-size: var(--text-base);
  color: var(--color-text-primary);
  line-height: 1.45;
  margin-bottom: 10px;
}
.care-resources {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.care-resources li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  font-size: var(--text-base);
  color: var(--color-text-secondary);
}
.care-resource-tel {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--color-navy);
  text-decoration: none;
  white-space: nowrap;
}
.care-resource-tel:hover { text-decoration: underline; }

/* Weekly commitment */
.commit-head {
  display: flex;
  justify-content: space-between;
  font-size: var(--text-base);
  font-weight: 500;
}

.commit-note {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  margin-top: 6px;
  line-height: 1.45;
}

/* --- Side by Side: an aspect matrix, deliberately not a scoreboard ---
   No rank badges, no gold/silver first-and-second styling, no tier chips —
   those all existed to express an ORDER, and the screen no longer has one.
   The only emphasis left is on the population-average column and the mark
   telling you whether a cell clears it, because that is the app's yardstick. */
.sbs-intro {
  font-size: var(--text-base);
  color: var(--color-text-secondary);
  margin-bottom: 14px;
  line-height: 1.45;
}
.sbs-empty {
  font-size: var(--text-base);
  color: var(--color-text-secondary);
  padding: 18px;
  text-align: center;
  border: 1px dashed var(--color-card-border);
  border-radius: 4px;
}
.sbs-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
  font-size: var(--text-base);
}
.sbs-table th,
.sbs-table td {
  padding: 9px 10px;
  border-bottom: 1px solid var(--color-card-border);
  white-space: nowrap;
}
.sbs-table thead th {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  font-weight: 700;
  color: var(--color-navy);
  vertical-align: bottom;
}
/* The population column is the reference every other column is read against,
   so it is set apart typographically rather than by rank-style decoration. */
.sbs-avg-head,
.sbs-avg {
  color: var(--color-text-secondary);
  background: var(--bg-primary);
}
.sbs-you { color: var(--color-accent); }
.sbs-aspect { font-weight: 500; }
.sbs-cell { text-align: center; font-family: var(--font-mono); }
.sbs-score { font-weight: 600; }
.sbs-mark { margin-left: 5px; font-size: var(--text-2xs); }
.sbs-above { color: var(--color-accent); }
.sbs-below { color: var(--color-text-secondary); }
.sbs-notes {
  margin-top: 14px;
  padding-left: 18px;
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  line-height: 1.5;
}
.sbs-notes li { margin-bottom: 4px; }

.friend-remove {
  background: none;
  border: none;
  color: var(--color-crimson);
  font-size: var(--text-sm);
  cursor: pointer;
  margin-left: 6px;
  padding: 2px 4px;
  vertical-align: middle;
}

.friend-remove:hover {
  color: var(--color-crimson-light);
}

/* Helper text colors */
.text-gold { color: var(--color-accent); }
.text-purple { color: var(--color-navy); }
.text-green { color: var(--color-success); }
.text-crimson { color: var(--color-crimson); }
/* Keyboard focus ring.
 *
 * :focus-visible, not :focus — a mouse click should not leave a ring, but Tab
 * always must.
 *
 * outline-offset is the load-bearing line, NOT the colour. This comment used to
 * claim gold was chosen because "a navy ring vanishes on .btn-primary"; gold on
 * navy is 1.26:1, so it would vanish just as completely. What saves it is the
 * 2px offset, which draws the ring entirely outside the element on the paper
 * background — 9.12:1 for gold, and navy would have been 12.55:1 there. Either
 * colour works. Drop the offset to 0 and every filled navy control loses its
 * focus indicator at once, so change that number only with a contrast check.
 *
 * .form-control is untouched: it already replaces the outline with its own
 * border + glow, and its class+pseudo specificity wins here. */
:focus-visible {
  outline: 3px solid var(--color-accent);
  outline-offset: 2px;
  border-radius: 2px;
}

.d-none { display: none !important; }

/* Skip link (WCAG 2.4.1). Seven controls sit before <main> on every route, so
 * a keyboard user re-tabs the whole header on each navigation without one.
 * Parked off-screen with `top` rather than `.sr-only`, because it has to become
 * a real, visible, hit-testable target the moment it takes focus. No transition
 * on `top`: a skip link that slides is a skip link you can miss. */
.skip-link {
  position: absolute;
  top: -60px;
  left: 8px;
  z-index: 1300;
  padding: 10px 16px;
  background: var(--color-navy);
  color: #ffffff;
  border-radius: var(--radius);
  font-size: var(--text-md);
  font-weight: 600;
  text-decoration: none;
}
.skip-link:focus { top: 8px; }

/* Visually hidden but available to screen readers (finding #12). */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* --- Onboarding: progress, rationale, express path --- */
.onb-progress {
  margin-bottom: 22px;
}
.onb-progress-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  margin-bottom: 6px;
}
#onb-step-label { font-weight: 600; color: var(--color-navy); }
.onb-progress-track {
  height: 6px;
  background: var(--color-track);
  border-radius: 3px;
  overflow: hidden;
}
.onb-progress-fill {
  height: 100%;
  width: 0;
  background: var(--color-navy);
  border-radius: 3px;
  transition: width 0.35s ease;
}
.onb-why {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.45;
  color: var(--color-text-secondary);
  margin: -6px 0 18px;
}
/* Profile & Data page (#/profile) */
.profile-view {
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.profile-note {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--color-text-secondary);
  margin: -6px 0 14px;
}
.profile-data-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* Connected apps — one row per sibling app on this origin. */
.conn-row {
  border: 1px solid var(--color-card-border);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 10px;
}
.conn-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.conn-id {
  display: flex;
  flex-direction: column;
}
.conn-name {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 700;
}
.conn-role,
.conn-fills {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
}
.conn-switch {
  display: flex;
  align-items: center;
  gap: 6px;
  /* 44px keeps the toggle a comfortable tap target on a phone, the same floor
     the rest of the mobile layout was held to. */
  min-height: 44px;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}
.conn-switch input {
  width: 18px;
  height: 18px;
  cursor: pointer;
}
.conn-status {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: 1.45;
  margin: 6px 0 4px;
}
.conn-fills {
  margin: 0;
}

/* Weekly Review: a box filled in by a connected app. The chip sits in the
   label so the source is named before the number is read, and the caption
   under the input says which dates it covers. */
.prefill-chip {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--color-card-bg);
  background: var(--color-navy);
  border-radius: 999px;
  padding: 2px 8px;
  vertical-align: middle;
}
.field-note {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--color-text-secondary);
  margin: 4px 0 0;
}
.conn-banner {
  border-left: 3px solid var(--color-navy);
  padding: 4px 0 4px 12px;
  margin-bottom: 14px;
}
.conn-banner p {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--color-text-secondary);
  margin: 0 0 6px;
}
.conn-banner p:last-child {
  margin-bottom: 0;
}
.onb-optional {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--color-success);
  background: rgba(63, 125, 90, 0.1);
  border: 1px solid rgba(63, 125, 90, 0.3);
  border-radius: 999px;
  padding: 1px 8px;
  vertical-align: middle;
  margin-left: 6px;
}
.onb-nav {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-top: 22px;
}
.onb-nav-right {
  display: flex;
  align-items: center;
  gap: 10px;
}
.btn-onb-express {
  background: none;
  border: 1px solid var(--color-navy);
  color: var(--color-navy);
  font-weight: 600;
}
.btn-onb-express:hover {
  background: var(--color-slate-glow);
}

/* --- Dashboard: quick-start (express baseline) note --- */
.quickstart-note {
  background: #faf9f6;
  border: 1px solid var(--color-card-border);
  border-left: 3px solid var(--color-navy);
  border-radius: var(--radius);
  padding: 12px 16px;
  margin-bottom: 18px;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.45;
  color: var(--color-text-secondary);
}
.quickstart-note strong { color: var(--color-navy); }

/* Completeness note (Phase 2b): estimated-score cue, amber-flagged. */
.completeness-note { border-left-color: var(--color-accent); }
.completeness-note strong { color: var(--color-accent); }
.completeness-note a { color: var(--color-accent); font-weight: 600; }

/* --- Confidence tiers (Phase 2a) ---
   0.75rem (12px), not the 0.62rem this used to be. These badges are the
   trust layer: "High" / "Partial" / "Estimated" is what tells a reader how
   much to believe the number sitting next to it, and at 9.92px it was the
   least legible text on a page whose entire pitch is cited honesty. Uppercase
   with 0.03em tracking costs legibility on top of the size, so the floor
   matters more here than for ordinary body copy. */
.confidence-badge,
.component-confidence {
  display: inline-block;
  vertical-align: middle;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid currentColor;
  line-height: 1.5;
}
.confidence-high { color: var(--color-success); }
.confidence-partial { color: var(--color-accent); }
.confidence-estimated { color: var(--color-text-secondary); }

.aspect-confidence-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.confidence-caption {
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
}
/* "In-depth" (deep-assessment) tier reads as the strongest — filled, not outlined. */
.confidence-verified {
  color: #fff;
  background: var(--color-navy);
  border-color: var(--color-navy);
}

/* --- Friendlier percentile presentation --- */
.benchmark-plain,
.benchmark-plain-lead {
  font-weight: 600;
  color: var(--color-navy);
}
/* This sentence is prose, so it sets its own font rather than inheriting the
   monospace stack from .benchmark-line — monospace is both wrong for prose and
   ~20% wider, which is what pushed the band chip onto its own line (the widest
   chip, "Around average", overflowed the 429px card by exactly 1px). Sans at
   0.86rem leaves ~56px of slack, and also restores the hierarchy: at 0.95rem
   this secondary line was larger than the aspect name above it (0.85rem).

   Inline flow, not flex: as a flex row the sentence is one unbounded item, so
   the chip is the only thing that can wrap and it drops to a line by itself.
   Inline lets the sentence wrap mid-phrase and the chip trail the last line —
   which is what keeps this readable on a phone, where sentence + chip cannot
   share one line at any legible size. */
.benchmark-plain-lead {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: 1.45;
  margin-bottom: 4px;
}
.benchmark-plain-lead .percentile-band {
  margin-left: 6px;
}
/* Matches .benchmark-line, which now sets 0.75rem for this whole block. This
   was held at 0.72rem because 0.75rem made the longest detail
   ("… (vs participation rates)") overshoot the card by 6px; the 12px floor is
   worth more than the saved line, and overshoot is no longer the outcome —
   the nowrap below keeps the parenthetical whole, so a long percentile now
   wraps cleanly inside the card instead of spilling past its edge. Kept as a
   rule rather than deleted because the color still differs from the parent. */
.benchmark-detail {
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
}
/* Keep the method tag whole: "(vs participation rates)" was breaking after
   "participation", leaving an orphaned "rates)". Scoped to .benchmark-detail so
   the standalone <p class="benchmark-method-line"> on the aspect page, which is
   meant to be a block, is unaffected. */
.benchmark-detail .benchmark-method {
  white-space: nowrap;
}
.benchmark-verified { color: var(--color-navy); font-weight: 700; }
.percentile-definition {
  margin-top: 6px;
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
  line-height: 1.4;
}
.benchmark-method-line { margin-top: 4px; }

/* Band chip: a coarse plain-language label next to the number. nowrap so a
   two-word band ("Around average") can never split, and tight tracking because
   it is a label, not something to read word by word. The size was trimmed to
   0.6rem to keep it on the sentence's line; at 9.6px it was the smallest text
   in the app, and "Bottom 25%" is a verdict about the reader — too important
   to be the hardest thing on the page to read. Back to the 12px floor; the
   nowrap still stops it splitting, it just takes the line it needs. */
.percentile-band {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid currentColor;
  line-height: 1.5;
  white-space: nowrap;
}
.band-top10, .band-top25 { color: var(--color-success); }
.band-above { color: var(--color-accent); }
.band-around { color: var(--color-text-secondary); }
.band-below, .band-bottom { color: var(--color-accent-light); }

/* --- Letter grades (Phase L1) ---
   Same chip geometry as the confidence and percentile chips, one step larger
   because a single letter needs the weight to read at a glance.
   Only A is filled: a filled F would shout, and the app's duty-of-care stance
   is that a low grade is information, not an alarm. C is deliberately neutral
   ink rather than a warning color — the C band is the typical middle, where
   most people honestly are. */
.grade-badge {
  display: inline-block;
  vertical-align: middle;
  min-width: 1.55rem;
  text-align: center;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid currentColor;
  line-height: 1.5;
}
.grade-a {
  color: #fff;
  background: var(--color-success);
  border-color: var(--color-success);
}
.grade-b { color: var(--color-success); }
.grade-c { color: var(--color-text-secondary); }
.grade-d { color: var(--color-accent); }
.grade-f { color: var(--color-crimson); }
.grade-none {
  color: var(--color-text-secondary);
  border-style: dashed;
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
/* "Not ranked" is a deliberate statement, not missing data, so it reads as a
   solid chip rather than the dashed "not filled in yet" one. */
.grade-unranked {
  color: var(--color-text-secondary);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.benchmark-unranked-lead {
  color: var(--color-text-secondary);
  font-style: italic;
}

/* Comparison-provenance table (methodology page). Scrolls inside its own box on
   narrow screens so the page body never scrolls sideways. */
.provenance-scroll {
  overflow-x: auto;
  margin: 12px 0;
}
.provenance-table {
  border-collapse: collapse;
  width: 100%;
  min-width: 520px;
  font-size: var(--text-sm);
}
.provenance-table th,
.provenance-table td {
  text-align: left;
  vertical-align: top;
  padding: 7px 10px;
  border-bottom: 1px solid var(--color-card-border);
}
.provenance-table thead th {
  font-weight: 700;
  color: var(--color-text-secondary);
  text-transform: uppercase;
  font-size: var(--text-2xs);
  letter-spacing: 0.04em;
}
.provenance-table tbody th {
  font-weight: 600;
  white-space: nowrap;
}
.provenance-table td {
  color: var(--color-text-secondary);
}
.provenance-unranked th,
.provenance-unranked td {
  color: var(--color-accent);
}

/* Keeps the grade chip and the raw score on one baseline in a dashboard row. */
.aspect-row-figures {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}

/* The "/100" after the raw aspect score.
 *
 * This exists because the row used to render the score as "20%" directly
 * beside a grade chip reading "B" — two numbers from two unrelated scales,
 * same size, same weight, 8px apart. The letter comes from the percentile
 * (Environment: 86th, hence B); the number is the raw 0-100 component score.
 * A reader scanning left to right got a B and then a 20, and nothing on the
 * row said which one was the verdict.
 *
 * The "%" was the active lie: a raw score out of 100 is not a percentage of
 * anything, and "%" is exactly the suffix the neighbouring percentile would
 * carry. Spelling the denominator out kills the ambiguity in three glyphs.
 * Dimmed and lighter so "/100" reads as a unit on the number rather than as a
 * second figure competing with it. */
.aspect-row-score-max {
  font-weight: 400;
  opacity: 0.65;
}

.grade-explainer p {
  font-size: var(--text-base);
  color: var(--color-text-primary);
  margin-bottom: 6px;
}
.grade-explainer p:last-child { margin-bottom: 0; }
.grade-explainer-note {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}

/* --- Balance Index (Phase L1) --- */
.balance-index {
  display: flex;
  align-items: center;
  gap: 18px;
}
.balance-index-figure {
  flex-shrink: 0;
  display: flex;
  align-items: baseline;
  gap: 2px;
}
.balance-index-value {
  font-family: var(--font-serif);
  font-size: var(--text-4xl);
  font-weight: 700;
  line-height: 1;
  color: var(--color-navy);
}
.balance-index-max {
  font-family: var(--font-serif);
  font-size: var(--text-md);
  color: var(--color-text-secondary);
}
.balance-index-body { min-width: 0; }
.balance-index-title {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--color-navy);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 4px;
}
/* The one sentence the app exists to say. Sized between the title (1rem) and
   the methodology caption (0.76rem) so the reading order is: what the number
   is -> what it means about you -> how it was built. Navy, not gold: this is a
   statement of fact, not a call to action — the gold line below it is the
   action. */
.balance-index-standing {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--color-navy);
  line-height: 1.4;
  margin-bottom: 6px;
}
.balance-index-caption {
  font-size: var(--text-xs);
  color: var(--color-text-secondary);
  line-height: 1.45;
}
.balance-index-weakest {
  font-size: var(--text-sm);
  color: var(--color-accent);
  font-weight: 600;
  margin-top: 4px;
}
.balance-band {
  display: inline-block;
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid currentColor;
  line-height: 1.5;
}
.band-strong { color: var(--color-success); }
.band-steady { color: var(--color-accent); }
.band-uneven { color: var(--color-accent-light); }
.band-strained { color: var(--color-crimson); }

/* Stack the index on narrow screens so the caption keeps a readable measure. */
@media (max-width: 480px) {
  .balance-index {
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
  }
}

/* --- Deep (in-depth) assessment --- */
.deep-banner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 14px;
  border: 1px solid var(--color-card-border);
  border-left: 3px solid var(--color-navy);
  background: #faf9f6;
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 18px;
}
.deep-banner p { font-size: var(--text-base); color: var(--color-text-primary); line-height: 1.45; }
.deep-progress { font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: 4px; }
.deep-section-head {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: space-between;
}
.deep-section-done { border-left: 3px solid var(--color-navy); }
.deep-done-note {
  font-size: var(--text-sm);
  color: var(--color-navy);
  font-weight: 600;
  margin-bottom: 8px;
}
/* Explains why a full-length scale shows fewer questions than its name implies:
   the shared items were already answered at the baseline assessment. */
.carry-note {
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
  margin: -6px 0 12px;
}

/* --- Log feedback moment: reward pop --- */
.reward-pop {
  position: fixed;
  bottom: 84px;
  right: 32px;
  z-index: 1100;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 320px;
  padding: 12px 18px 12px 14px;
  background: #ffffff;
  border: 1px solid var(--color-card-border);
  border-left: 4px solid var(--color-success);
  border-radius: 8px;
  box-shadow: 0 10px 28px rgba(31, 39, 51, 0.18);
  pointer-events: none;
  animation: reward-in 0.28s cubic-bezier(0.18, 0.9, 0.32, 1.4) both,
             reward-out 0.5s ease-in 1.4s both;
}
.reward-check {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--color-success);
  color: #ffffff;
  font-size: var(--text-lg);
  font-weight: 700;
}
.reward-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.reward-xp {
  font-family: var(--font-mono);
  font-size: var(--text-md);
  font-weight: 700;
  color: var(--color-navy);
}
.reward-gains {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--color-success);
  font-weight: 600;
}
@keyframes reward-in {
  from { transform: translateY(14px) scale(0.94); opacity: 0; }
  to { transform: translateY(0) scale(1); opacity: 1; }
}
@keyframes reward-out {
  from { transform: translateY(0); opacity: 1; }
  to { transform: translateY(-22px); opacity: 0; }
}
/* This block is only half the story, and for a long time it was the only half
   that existed. A media query cannot reach motion built in script: the toast
   is a WAAPI element.animate(), the Lumi tip is a setInterval typewriter, and
   scrollIntoView takes its behaviour from the options object rather than from
   scroll-behavior here. Those four sites now ask prefersReducedMotion() in
   views/helpers.js — change one side and check the other.
   .btn:active / .tab-btn:active are excluded on purpose; see the note on that
   rule for why 1px of press feedback should survive the preference. */
@media (prefers-reduced-motion: reduce) {
  .reward-pop { animation: reward-out 0.4s ease-in 1.5s both; }
  .onb-progress-fill { transition: none; }
}

/* --- Wide tables scroll inside their card instead of panning the page --- */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* --- Radar chart + legend (you vs population average) --- */
/* Column layout: the SVG on top, the legend in a centered row below it.
   (A plain flex row squeezed the legend into a side column next to the chart.) */
#radar-chart-container {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.radar-legend {
  margin-top: 0.75rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.35rem 1.25rem;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--color-text-secondary);
}
.radar-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}
.radar-legend-swatch {
  display: inline-block;
  width: 22px;
  height: 0;
  border-top: 2px solid var(--color-slate);
}
.radar-legend-swatch--average {
  border-top: 2px dashed rgba(32, 50, 76, 0.55);
}
.radar-legend-caption {
  flex-basis: 100%;
  margin: 0.1rem 0 0;
  font-size: var(--text-xs);
  text-align: center;
  color: var(--color-text-secondary);
}

/* ==========================================================================
   MOBILE LAYER — phones (max-width: 640px)
   ==========================================================================
   Everything below is scoped to small screens on purpose: the desktop layout
   is deliberately unchanged by the v45 mobile redesign, so no rule here may
   leak upward. 640px is the consolidated phone breakpoint; the 768px tablet
   grid collapse above still stands.

   Written against measurements taken at 375x812 with real data in both
   languages. The defects it answers:
     - banner prose squeezed into a 98px column by justify-content: space-between
     - 917px of prompts before any content; the radar 1800px down
     - static navigation on a 5.6-screen page
     - every form field at 15.2px, which makes iOS Safari zoom on focus
     - tap targets at 19-37px against a 44px standard
     - text down to 9.6px
   ========================================================================== */
@media (max-width: 640px) {

  /* --- Density -----------------------------------------------------------
     Container and card padding each charged 24px a side, so 26% of a 375px
     screen was padding and the content column came to 277px. Halving both
     buys back 32px without disturbing the desktop rhythm.
     The bottom padding clears the fixed tab bar AND the floating assistant
     that now sits above it; see the assistant rule below. */
  .app-container {
    padding: 14px 16px calc(176px + env(safe-area-inset-bottom));
  }

  .card {
    padding: 16px;
    margin-bottom: 12px;
  }

  .card-header {
    font-size: var(--text-lg);
    padding-bottom: 8px;
    margin-bottom: 12px;
  }

  /* --- Header ------------------------------------------------------------
     124px of an 812px screen was masthead. The subtitle repeats what the page
     title already says, so it goes on phones; the wordmark stays. */
  header {
    padding: 2px 0 12px;
    margin-bottom: 14px;
    align-items: center;
    flex-wrap: nowrap;
    gap: 10px;
  }

  .brand h1 { font-size: var(--text-xl); }
  .brand p { display: none; }

  .header-actions { gap: 6px; flex-shrink: 0; }

  /* Overriding what were inline styles until this release. */
  .btn-compact {
    font-size: var(--text-base);
    padding: 10px 12px;
    min-height: 44px;
  }

  /* --- Bottom tab bar ----------------------------------------------------
     The tabs were position: static, so on a five-screen page the only way to
     change section was to scroll back to the top. Fixed to the bottom they
     are always within thumb reach, and the top of the screen is freed.
     .d-none carries !important, so onboarding still hides this correctly. */
  .nav-tabs {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 200;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 0;
    margin: 0;
    background: var(--color-card-bg);
    border-top: 1px solid var(--color-card-border);
    border-bottom: none;
    /* env() resolves only because index.html sets viewport-fit=cover. */
    padding-bottom: env(safe-area-inset-bottom);
    box-shadow: 0 -2px 10px rgba(31, 39, 51, 0.06);
  }

  .tab-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    /* Four labels share 375px and none fits on one line at a readable size, so
       they wrap. `overflow-wrap: anywhere` is what makes Thai work: "ทบทวนราย-
       สัปดาห์" (Weekly Review) is a single 92px token with no spaces to break
       on, which overflowed a 94px column at 375px and an 80px one at 320px.
       Thai breaks legitimately at any character, so allowing it costs nothing
       and keeps the label at the 12px floor instead of shrinking to fit.
       Verified clipping-free in EN and TH at both 375px and 320px. */
    font-size: var(--text-xs);
    overflow-wrap: anywhere;
    line-height: 1.15;
    padding: 6px 2px;
    min-height: 54px;
    margin-bottom: 0;
    border-bottom: none;
  }

  /* A bottom border would sit on the screen edge where it cannot be seen. */
  .tab-btn.active {
    border-bottom-color: transparent;
    box-shadow: inset 0 2px 0 var(--color-accent);
  }

  /* --- Floating assistant ------------------------------------------------
     It was fixed at bottom:24px right:24px and 348px wide, which is exactly
     where the tab bar now lives. Lift it clear and let it span the width. */
  .assistant-wrapper {
    left: 12px;
    right: 12px;
    bottom: calc(64px + env(safe-area-inset-bottom));
  }

  .speech-bubble { max-width: none; font-size: var(--text-base); }

  /* --- Forms -------------------------------------------------------------
     THE iOS ZOOM FIX. Safari zooms the viewport whenever a focused field is
     under 16px, and every one of the app's 30 fields was 15.2px — so each tap
     zoomed the page and the reader had to pinch back out. 16px is a hard
     threshold, not a preference: 15.9px still zooms.
     This is the one font-size in the sheet that is deliberately NOT a
     --text-* token. It is a behavioural guard against a Safari rule, so it
     must stay pinned to 16px even if the type scale is later retuned. */
  .form-control,
  input[type="text"],
  input[type="number"],
  input[type="date"],
  input[type="file"],
  select,
  textarea {
    font-size: 16px;
    padding: 11px 12px;
    min-height: 44px;
  }

  .form-group label { font-size: var(--text-base); }
  .form-group { margin-bottom: 14px; }

  /* --- Tap targets -------------------------------------------------------
     Measured before this release: header buttons 25px, inline links 19px,
     tabs 37px. */
  /* inline-flex is load-bearing, not cosmetic: `.btn` sets no display, so an
     <a class="btn"> computes to `display: inline` — and min-height does not
     apply to a non-replaced inline box. The anchor buttons stayed 40px while
     the <button>s obeyed, until this line. */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    padding: 11px 18px;
  }

  /* .btn's 11px 18px would now reach the pledge Remove control and widen it —
     it could not before, because that padding was an inline style no media
     query could outrank. Restated here so extracting the inline style changes
     nothing on the phone: the button still reaches 44px through min-height
     alone, exactly as it did. Verified unchanged at 68.1x44 (EN), 36.8x44 (TH). */
  .pledge-remove {
    padding: 2px 10px;
  }

  .share-open {
    min-height: 44px;
    padding: 6px 10px;
  }

  .radio-option {
    min-height: 44px;
    font-size: var(--text-md);
  }

  .radio-option input[type="radio"] {
    width: 20px;
    height: 20px;
  }

  /* --- Type floor --------------------------------------------------------
     Nothing below 12px on a phone. These are the classes that measured
     9.6-11.5px; the mobile e2e flow guards against new ones appearing. */
  .percentile-band,
  .balance-band,
  .grade-badge,
  .radar-legend-caption,
  .benchmark-detail,
  .benchmark-disclaimer,
  .suggestion-meta,
  .deep-progress,
  .level-badge,
  .radar-legend-item,
  .radar-legend,
  .balance-index-caption,
  .balance-index-weakest,
  .component-detail,
  .criterion-chip,
  .gauge-note,
  .percentile-definition,
  .share-open {
    font-size: var(--text-sm);
  }

  /* The provenance tables already scroll inside their card, so making their
     text legible costs width that was never on screen anyway. `thead th` is
     spelled out because the base rule for it carries specificity (0,1,2) and
     would otherwise keep its 0.68rem. */
  .provenance-table th,
  .provenance-table td,
  .provenance-table thead th {
    font-size: var(--text-xs);
  }

  .app-footer { font-size: var(--text-sm); }

  /* --- Prompt stack ------------------------------------------------------
     Two separate fixes.

     First: these banners were display:flex with justify-content:space-between
     and no mobile rule, so the button took its natural width and the prose was
     left a 98px column — about twelve characters a line, which is what turned
     short sentences into 345px-tall blocks. Stacking ends that.

     Second: only the FIRST prompt is the next step. The rest demote to a
     compact title-and-action row. Done in CSS rather than JS so that rotating
     the phone cannot leave a stale layout behind. */
  .checkin-banner,
  .deep-banner {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
    padding: 12px 14px;
    margin-bottom: 10px;
  }

  .checkin-banner .btn,
  .deep-banner .btn {
    width: 100%;
    text-align: center;
  }

  .prompt-stack .prompt:not(:first-child) {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 12px;
  }

  .prompt-stack .prompt:not(:first-child) .prompt-body {
    display: none;
  }

  .prompt-stack .prompt:not(:first-child) .prompt-title {
    font-size: var(--text-base);
  }

  .prompt-stack .prompt:not(:first-child) .btn {
    width: auto;
    flex-shrink: 0;
    font-size: var(--text-sm);
    padding: 10px 12px;
    min-height: 44px;
  }

  .prompt-stack .prompt:not(:first-child) .prompt-actions {
    flex-shrink: 0;
  }

  /* --- Dashboard order ---------------------------------------------------
     The radar is the point of the app and it started 1800px down, behind the
     identity card and the prompts. display:contents dissolves the two desktop
     columns so every card becomes an orderable sibling; the order values are
     inert on desktop, where the grid still has exactly two children. */
  .dashboard-grid {
    display: flex;
    flex-direction: column;
    gap: 0;
  }

  .dashboard-grid > div { display: contents; }

  /* Default first, so a card added later without an order class lands at the
     end of the page rather than jumping to the very top — which is what an
     unset `order: 0` would otherwise do. (Caught exactly that way: the Recent
     Reviews card had no class and sorted above the radar.) */
  .dashboard-grid .card { order: 90; }

  .dash-radar    { order: 1; }
  .dash-index    { order: 2; }
  .dash-scores   { order: 3; }
  .dash-suggest  { order: 4; }
  .dash-activity { order: 5; }
  .dash-identity { order: 6; }

  /* Aspect Scores, collapsed to one statement per aspect.
     ---------------------------------------------------
     Measured at 375px before this rule: every ranked row was 132.7px, of which
     head (name + grade + score) was 23.2 and the bar 5. The remaining 83 was
     .benchmark-plain-lead at 42.9 and .benchmark-detail at 39.7 — two
     paragraphs, each wrapping to TWO lines in the card's ~311px of content
     width. Eight aspects made a 1174px card: the list alone ran longer than one
     and a half phone screens, and nothing below it was reachable without a
     deliberate scroll.

     Two problems, one cause. The obvious one is length. The subtler one is
     raggedness: row height is a function of where each label happens to wrap,
     so English ran 133/133/133/70 (Relationships is unranked, so it has no
     .benchmark-detail at all) and Thai ran 93/113/133 — a list of identical
     things rendered at four different heights, which reads as disorder rather
     than as a scale.

     Hiding the prose fixes both, because what remains is fixed-height: a chip
     of two words, or the one-line unranked sentence. Rows land within a pixel
     of each other in either language, so the eye can compare them.

     What is NOT lost: the grade badge and the score both stay on the head line,
     and the chip states the standing. The exact percentile, the typical range,
     and the method tag move to the aspect page one tap away — the only place
     they appear together with the definition that makes a percentile mean
     anything. Showing a bare "68th percentile" here with its range hidden
     beside it would be the worse option, not the safer one.

     Scoped to .aspect-row so the aspect page's own standing block — same
     classes, rendered by the non-compact branch of benchmarkStanding — keeps
     every line. */
  .aspect-row .benchmark-phrase,
  .aspect-row .benchmark-detail {
    display: none;
  }

  /* With the phrase gone the chip is the paragraph's first child, so its 6px
     word-gap would render as a stray indent, and the 4px that used to separate
     it from the detail line would now hang off the bottom of the row. */
  .aspect-row .benchmark-plain-lead {
    margin-bottom: 0;
  }

  .aspect-row .benchmark-plain-lead .percentile-band {
    margin-left: 0;
  }
}
