/* =============================================================================
   app.css -- the shared layout vocabulary for authenticated pages.

   WHY THIS FILE EXISTS

   Before it, base.html handed every page an empty <div class="container py-4">
   and nothing else: no page header, no section pattern, no rhythm. So every
   template privately rebuilt the same three things inside its own <style> tag.
   The result was ~25 independent hero implementations, ~20 different classes
   all meaning "a labelled rule between sections", 6 breadcrumbs, and 10,676
   lines of CSS living in templates -- 67% of all the CSS in the app.

   Nothing here is a new colour or a new number. Every value is a token from
   variables.css. This file only assembles them into the handful of shapes a
   page is actually made of.

   THE FOUR RULES THIS FILE ENCODES

   1. Internal <= external. --card-pad never exceeds --card-gap, and both stay
      under --section-gap. Padding bigger than the gap inverts the grouping.
   2. One focal point per screen, meaningfully bigger. Around 30% larger than
      the secondary values is the floor; below that a page reads flat.
   3. Sizes come from the modular scale, never from a guess.
   4. Body copy is capped at --measure so lines stay in the 45-75 character band.

   The features/ pages already proved this pattern works in this codebase:
   six shared partials, one shared stylesheet, and 11 leaf pages that are 23
   lines of pure composition each. This brings that inside the login.
   ============================================================================= */

/* -----------------------------------------------------------------------------
   Page shell and vertical rhythm
   -------------------------------------------------------------------------- */
.app-page {
  padding-block: var(--page-pad);
  display: flex;
  flex-direction: column;
  gap: var(--section-gap);
}
/* base.html wraps every page in .container.py-4. A converted page owns its own
   vertical rhythm through --page-pad, so cancel the wrapper's padding instead
   of stacking 24px on top of 48px for 72px. The :has() keeps this off every
   unconverted page, which still depends on py-4. !important is required only
   because Bootstrap's spacing utilities carry it. */
.container.py-4:has(> .app-page) {
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}

/* A section is the unit a page is built from. Its children sit a card-gap
   apart; sections themselves sit a section-gap apart, which is what makes a
   section read as a group rather than as more of the same stack. */
.app-section {
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}

/* .app-page without the page padding, for when a column inside an existing
   layout has to supply the section rhythm itself. A page half-converted has
   blocks that carry mb-4 and blocks that carry nothing, so the spacing between
   them is whatever each block happened to bring; this makes the container own
   it instead.

   The .row reset is load-bearing. Bootstrap's .row sets
   margin-top: calc(-1 * var(--bs-gutter-y)), which is -24px under g-4, so a row
   pulls itself up into whatever is above it. Inside a stack that quietly eats
   half the gap, and where the block above had no bottom margin it produced a
   0px gap: two cards touching with nothing between them. */
.app-stack {
  display: flex;
  flex-direction: column;
  gap: var(--section-gap);
}
.app-stack > .row { margin-top: 0; }
/* A child that already carries its own bottom margin would double up. */
.app-stack > .mb-4,
.app-stack > .mb-3 { margin-bottom: 0 !important; }

/* -----------------------------------------------------------------------------
   Eyebrow and section lead -- replaces ~20 bespoke *-divider / *-section-label
   -------------------------------------------------------------------------- */
.app-eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--font-semibold);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--color-primary);
}
.app-eyebrow::before {
  content: "";
  width: 22px;
  height: 1px;
  flex: 0 0 22px;
  background: currentColor;
}

.app-lead {
  display: flex;
  align-items: baseline;
  gap: var(--space-md);
  margin: 0;
}
.app-lead h2 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-normal);
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  color: var(--color-text);
}
.app-lead::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--color-border);
}
.app-lead .app-lead-tag {
  order: 3;
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--color-text-subtle);
}

/* -----------------------------------------------------------------------------
   Card -- the base .card in components.css declares no padding at all, which
   is why 62 different card paddings grew across the templates. This one has
   padding, and it comes from the token.
   -------------------------------------------------------------------------- */
.app-card {
  background: var(--color-surface);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--card-pad);
}

/* -----------------------------------------------------------------------------
   Hero -- NavRun's locked signature: a dark pine header fused onto a cream
   card, with ONE low-opacity ridgeline behind the dark half and nowhere else.
   Replaces ~25 separate *-hero implementations.
   -------------------------------------------------------------------------- */
.app-hero {
  position: relative;
  overflow: hidden;
  background: var(--color-surface);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.app-hero-top {
  position: relative;
  overflow: hidden;
  background: var(--brand-pine);
  color: var(--color-pine-ink);
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}
.app-hero-ridge {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: .2;
  pointer-events: none;
}
/* Lift the real content above the ridgeline. The :not() is load-bearing: a bare
   `> *` also matches the ridge image, and because it is the more specific
   selector it would override .app-hero-ridge's position:absolute, drop the
   image back into normal flow, and let width/height:100% inflate the hero to
   the full height of the viewport. */
.app-hero-top > *:not(.app-hero-ridge) { position: relative; z-index: 1; }
.app-hero-top .app-eyebrow { color: var(--brand-cream); }

.app-hero-headline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--card-gap);
}
.app-hero-headline .app-hero-what {
  font-family: var(--font-display);
  font-weight: var(--font-normal);
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
}

.app-hero-body {
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--card-gap);
}

/* The focal point. One per screen, and it is a sentence handing the runner
   their own record back: "You ran 42 miles this week." Describe the day, never
   grade it.

   It is NOT a slot for an instruction. A settings page that puts "Configure
   your preferences to generate personalized plans" here is shouting a task at
   someone in 40px display type, which is the guru voice the Mission
   Constitution rules out. Page-level explanation goes in .app-lede. If the page
   has no record to hand back, it has no .app-hero-said. */
.app-hero-said {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-normal);
  font-size: var(--text-3xl);
  line-height: var(--leading-tight);
  letter-spacing: .01em;
  color: var(--color-text);
  max-width: var(--measure);
}
.app-hero-then {
  margin: 0;
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--color-text-muted);
  max-width: var(--measure);
}

/* -----------------------------------------------------------------------------
   Stat grid. Three columns, so six stats fill two even rows. The live grid was
   five columns holding six tiles, which orphaned the sixth onto its own row.
   -------------------------------------------------------------------------- */
.app-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--card-gap);
}
/* The number of stats is data-dependent almost everywhere: Day Streak only
   exists once there is a streak, a plan may have one summary figure or four.
   Three fixed columns strands whatever does not divide by three, which is the
   bug the old five-column-six-tile dashboard grid had. So the grid counts its
   own children and picks a column count that comes out flush.

   :nth-child(N):last-child means "there are exactly N". */
.app-stats:has(> .app-stat:first-child:last-child) { grid-template-columns: 1fr; }
.app-stats:has(> .app-stat:nth-child(2):last-child) { grid-template-columns: repeat(2, 1fr); }
.app-stats:has(> .app-stat:nth-child(4):last-child) { grid-template-columns: repeat(4, 1fr); }
/* Five is the odd one out: three columns is right, and the lead tile widens to
   fill the spare cell so the last row is still flush. */
.app-stats:has(> .app-stat:nth-child(5):last-child) .app-stat--lead {
  grid-column: span 2;
}
.app-stat {
  background: var(--color-surface);
  border: 2px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--card-pad);
}
/* One step below --text-2xl on purpose. The hero sentence sits at --text-3xl
   (40.27px); against 22.66px here it reads ~78% larger, which is the focal
   contrast the design was tuned to. At --text-2xl the gap falls to 33% and the
   hero stops carrying the page. */
.app-stat-value {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--font-normal);
  font-size: var(--text-xl);
  line-height: 1;
  color: var(--color-text);
  font-variant-numeric: tabular-nums;
}
.app-stat-label {
  display: block;
  margin-top: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--color-text-subtle);
}
/* The lead stat carries the dark half of the signature. Border removed rather
   than recoloured, so the pine plate reads as one solid object. */
.app-stat--lead {
  background: var(--brand-pine);
  border-color: var(--brand-pine);
  color: var(--color-pine-ink);
}
.app-stat--lead .app-stat-value { color: var(--color-pine-ink); }
.app-stat--lead .app-stat-label { color: var(--brand-cream); }

/* -----------------------------------------------------------------------------
   List rows -- the "recent runs" shape, reusable for any dated list
   -------------------------------------------------------------------------- */
.app-list { list-style: none; margin: 0; padding: 0; }
.app-list li {
  display: grid;
  grid-template-columns: 5rem 1fr auto auto;
  align-items: baseline;
  gap: var(--space-md);
  padding-block: var(--space-2sm);
  border-bottom: 1px solid var(--color-border-light);
}
.app-list li:last-child { border-bottom: 0; }
.app-list-day {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--color-text-subtle);
}
.app-list-title { color: var(--color-text); min-width: 0; }
.app-list-num {
  font-family: var(--font-display);
  text-align: right;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}
.app-list-num--soft { color: var(--color-text-muted); }

/* -----------------------------------------------------------------------------
   Empty state -- welcome the return, credit what is already there, never scold
   -------------------------------------------------------------------------- */
.app-empty {
  background: var(--brand-pine);
  color: var(--color-pine-ink);
  border: 2px solid var(--brand-pine);
  border-radius: var(--radius-lg);
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--card-gap);
  text-align: center;
}
.app-empty p {
  margin: 0;
  max-width: var(--measure);
  line-height: var(--leading-body);
  color: color-mix(in srgb, var(--color-pine-ink) 85%, transparent);
}
.app-empty-figure {
  width: 120px;
  height: 120px;
  object-fit: contain;
}
.app-empty-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
}

/* The heading inside an empty state. It cannot use .app-eyebrow, which resolves
   to rust and disappears against the pine plate. */
.app-empty-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-normal);
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  color: var(--color-pine-ink);
}

/* A column header in a data table. Same voice as .app-list-day, but named for
   what it is so a <th> does not have to borrow a list class. */
.app-th {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-weight: var(--font-semibold);
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--color-text-subtle);
  text-align: left;
}

/* A quiet inline note, for the empty state inside an ordinary card. */
.app-note {
  margin: 0;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  max-width: var(--measure);
}

/* -----------------------------------------------------------------------------
   Buttons used on the pine plate
   -------------------------------------------------------------------------- */
.app-btn-ghost {
  background: transparent;
  border: 1px solid color-mix(in srgb, var(--color-pine-ink) 35%, transparent);
  color: var(--color-pine-ink);
}
.app-btn-ghost:hover {
  border-color: var(--color-primary-light);
  color: var(--color-pine-ink);
}

/* -----------------------------------------------------------------------------
   Shapes promoted from page-local CSS.

   Each of these was independently re-declared by more than one page during the
   conversion. That repetition is the signal: a shape two pages invent on their
   own is a shape the vocabulary was missing, and leaving it out is how the app
   grew ~25 heroes and ~20 section dividers in the first place.
   -------------------------------------------------------------------------- */

/* Card grid, 3 across. */
.app-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--card-gap);
}
.app-grid--2 { grid-template-columns: repeat(2, 1fr); }

/* A two-up row that goes full width when it holds a single child, so a
   conditional second panel disappearing never leaves a half-empty row. */
.app-pair {
  display: grid;
  grid-template-columns: 1.5fr 1fr;
  gap: var(--card-gap);
}
.app-pair > :only-child { grid-column: 1 / -1; }

/* Four stats do not divide into three columns. */
.app-stats--four { grid-template-columns: repeat(4, 1fr); }

/* Title and description inside a card. Without these a card falls back to a
   bare <h3>, which is Bootstrap's 1.75rem and off the modular scale. */
.app-card-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-normal);
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  color: var(--color-text);
}
.app-card-desc,
.app-lede {
  margin: 0;
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--color-text-muted);
  max-width: var(--measure);
}

/* A link on the pine plate. --color-primary measures roughly 2.5:1 against
   --brand-pine, well under the 4.5:1 body-text bar. --brand-peach clears it.
   Every dark plate hits this, so it is settled once here. */
.app-plate-link {
  color: var(--brand-peach);
  text-decoration-color: color-mix(in srgb, var(--brand-peach) 45%, transparent);
}
.app-plate-link:hover { color: var(--color-pine-ink); }

/* An icon chip. */
.app-icon {
  width: 48px;
  height: 48px;
  flex: 0 0 48px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-primary-subtle);
  color: var(--color-primary);
  font-size: var(--text-xl);
}
.app-icon--solid {
  background: var(--color-primary);
  color: var(--color-on-rust);
}

/* A filter pill. */
.app-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-2sm);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  line-height: 1;
  cursor: pointer;
}
.app-chip.active,
.app-chip[aria-pressed="true"] {
  background: var(--brand-pine);
  border-color: var(--brand-pine);
  color: var(--color-pine-ink);
}

/* A trailing action in a section lead, e.g. an "Add race" button, placed after
   the rule that .app-lead draws with ::after. */
.app-lead-action { order: 4; flex: 0 0 auto; }

/* Semantic chips. Always the -ink colour on the -bg surface: the plain hue does
   not clear 4.5:1 on its own tint, which variables.css records in detail. A
   status chip states the status once; it is not licence to also colour the
   number, the border and the icon the same red. */
.app-chip--success { background: var(--color-success-bg); border-color: var(--color-success-bg); color: var(--color-success-ink); }
.app-chip--warning { background: var(--color-warning-bg); border-color: var(--color-warning-bg); color: var(--color-warning-ink); }
.app-chip--danger  { background: var(--color-danger-bg);  border-color: var(--color-danger-bg);  color: var(--color-danger-ink); }
.app-chip--info    { background: var(--color-info-bg);    border-color: var(--color-info-bg);    color: var(--color-info-ink); }

/* A meter. "How close am I" rather than pass/fail, so the fill is rust by
   default and only takes a semantic colour when the value genuinely means one. */
.app-meter {
  height: 8px;
  border-radius: var(--radius-full);
  background: var(--color-bg-subtle);
  overflow: hidden;
}
.app-meter-fill {
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--color-primary);
}
.app-meter-fill--success { background: var(--color-success); }
.app-meter-fill--warning { background: var(--color-warning); }
.app-meter-fill--danger  { background: var(--color-danger); }

/* A whole card that is a link. Without this every page re-declares the same
   four resets and its own hover. */
a.app-card,
.app-card--link {
  display: block;
  text-decoration: none;
  color: inherit;
  transition: transform var(--transition-base), box-shadow var(--transition-base);
}
a.app-card:hover,
.app-card--link:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
  color: inherit;
}

/* A card carrying a semantic left accent, for alert-shaped content. */
.app-card--success { border-left: 4px solid var(--color-success); }
.app-card--warning { border-left: 4px solid var(--color-warning); }
.app-card--danger  { border-left: 4px solid var(--color-danger); }

/* Pagination. components.css styles .page-link in TWO dark-mode blocks and no
   light-mode one, so light mode fell straight through to Bootstrap blue. That
   was invisible while every page carried private pagination CSS, and appeared
   the moment one stopped. These plain selectors sit below the dark blocks'
   specificity, so dark mode is unaffected. */
.page-link {
  color: var(--color-primary);
  border-color: var(--color-border);
}
.page-link:hover {
  color: var(--color-link-hover);
  background: var(--color-bg-subtle);
  border-color: var(--color-border);
}
.page-item.active .page-link {
  background: var(--color-primary);
  border-color: var(--color-primary);
  color: var(--color-on-rust);
}
.page-item.disabled .page-link {
  color: var(--color-text-subtle);
  background: var(--color-bg-subtle);
}

/* Strava's own orange, for their connect and follow affordances only. Their
   brand guidelines require it; it is never a NavRun accent. */
.app-btn-strava {
  background: var(--brand-strava);
  border-color: var(--brand-strava);
  color: var(--color-on-rust);
}
.app-btn-strava:hover {
  background: color-mix(in srgb, var(--brand-strava) 85%, black);
  border-color: color-mix(in srgb, var(--brand-strava) 85%, black);
  color: var(--color-on-rust);
}

/* -----------------------------------------------------------------------------
   Responsive. Four breakpoints for the whole app, not the 24 that had
   accumulated (767 / 767.98 / 768 / 769 all existed simultaneously).
   -------------------------------------------------------------------------- */
@media (max-width: 992px) {
  .app-stats,
  .app-stats--four,
  .app-grid { grid-template-columns: repeat(2, 1fr); }
  /* Two content-heavy panels side by side stop being readable well before the
     one-column breakpoint, and col-lg-* collapsed here too. */
  .app-pair,
  .app-grid--2 { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  .app-page { padding-block: var(--space-xl); gap: var(--space-xl); }
  .app-list li { grid-template-columns: 4rem 1fr auto; }
  .app-list-num--soft { display: none; }
}
@media (max-width: 576px) {
  .app-stats,
  .app-stats--four,
  .app-grid,
  .app-grid--2 { grid-template-columns: 1fr; }
  .app-hero-said { font-size: var(--text-2xl); }
}

/* Motion. animations.css only silences .skeleton under reduced motion, so every
   converted page was carrying its own copy of this block. Settled once. */
@media (prefers-reduced-motion: reduce) {
  .stagger-item,
  [class*="animate-"],
  .app-card,
  .app-stat {
    animation: none !important;
    transition: none !important;
    transform: none !important;
  }
}
