/* ==========================================================================
   Tripo-Pulse base typography and layout primitives
   P1.F2.T1 · ©2026 Vexstorm Studios – All Rights Reserved

   Two layers in one file, in this order:

     1. Base typography  — the second and last place element selectors are
                           permitted (project_code_standards.md §9.3).
     2. Shell chrome     — the layout primitives the two base templates are
                           built from: page frame, container, header, nav,
                           main, footer, skip link, notice region, theme
                           toggle. These are structure, not components; the
                           component library under components/ is what pages
                           compose their content from.

   Tokens only. Media-query conditions carry raw pixel values because CSS does
   not permit a custom property in a media condition — that is a language
   limitation, and it is the only exemption in the whole stylesheet set.

   Breakpoints, used consistently everywhere:
     up to 47.99rem  narrow   — single column, disclosure navigation
     48rem  (768px)  medium   — two columns available, horizontal navigation
     80rem  (1280px) wide     — sidebar layout gains its full width
   The page container caps at --tp-space-container-max so that a 2560px display
   shows a readable measure rather than a stretched one (UX-004).
   ========================================================================== */

/* ==========================================================================
   1. BASE TYPOGRAPHY
   ========================================================================== */

html {
  font-family: var(--tp-font-sans);
  font-size: var(--tp-text-base);
  line-height: var(--tp-leading-normal);
}

body {
  background-color: var(--tp-color-surface-base);
  color: var(--tp-color-text-primary);
  font-size: var(--tp-text-base);
  /* Kerning and ligatures on, discretionary alternates off: this interface
     shows identifiers and counts, where a stylistic alternate is a hazard. */
  font-variant-ligatures: common-ligatures;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  color: var(--tp-color-text-primary);
  font-weight: var(--tp-weight-bold);
  line-height: var(--tp-leading-tight);
  /* Long observation subjects and user-supplied titles must not overflow at
     320px, and a heading is the most common place that happens. */
  overflow-wrap: break-word;
}

h1 {
  font-size: var(--tp-text-2xl);
}

h2 {
  font-size: var(--tp-text-xl);
}

h3 {
  font-size: var(--tp-text-lg);
}

h4,
h5,
h6 {
  font-size: var(--tp-text-base);
}

p {
  overflow-wrap: break-word;
}

a {
  color: var(--tp-color-text-link);
  /* Underlined by default: a link identified only by colour fails ACC-004 for
     a reader who cannot distinguish the accent from body text. Navigation and
     button-styled links opt out explicitly, because their role is conveyed by
     position and shape instead. */
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

a:hover {
  color: var(--tp-color-accent-hover);
}

a:active {
  color: var(--tp-color-accent-active);
}

strong,
b {
  font-weight: var(--tp-weight-bold);
}

small {
  font-size: var(--tp-text-sm);
}

code,
kbd,
samp {
  background-color: var(--tp-color-surface-sunken);
  border-radius: var(--tp-radius-sm);
  padding: 0 var(--tp-space-1);
}

abbr[title] {
  text-decoration: underline dotted;
  cursor: help;
}

::selection {
  background-color: var(--tp-color-accent-subtle);
  color: var(--tp-color-text-primary);
}

/* ==========================================================================
   2. SHELL CHROME
   ========================================================================== */

/* --------------------------------------------------------------------------
   .tp-page — the frame both shells are built on
   -------------------------------------------------------------------------- */

.tp-page {
  /* Flex rather than a three-row grid, because the number of bands varies:
     the authenticated shell adds a notice region between header and main, and
     a grid with a fixed row template silently mis-assigns it. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* dvh where supported, so a mobile browser's collapsing toolbar does not
     leave a strip of background below the footer. */
  min-height: 100dvh;
}

/* Pushes the footer to the bottom on a short page without pinning it. */
.tp-page > .tp-main {
  flex: 1 0 auto;
}

/* --------------------------------------------------------------------------
   .tp-container — the horizontal measure every band shares
   -------------------------------------------------------------------------- */

.tp-container {
  width: 100%;
  max-width: var(--tp-space-container-max);
  margin-inline: auto;
  padding-inline: var(--tp-space-4);
}

@media (min-width: 48rem) {
  .tp-container {
    padding-inline: var(--tp-space-6);
  }
}

/* --------------------------------------------------------------------------
   .tp-skip-link — the first focusable element on every page (ACC-002)
   Visually hidden until focused, then pinned to the top-left where a keyboard
   user is already looking.
   -------------------------------------------------------------------------- */

.tp-skip-link {
  position: absolute;
  top: var(--tp-space-2);
  left: var(--tp-space-2);
  z-index: var(--tp-z-tooltip);
  background-color: var(--tp-color-surface-raised);
  color: var(--tp-color-text-link);
  border: var(--tp-border-width-thick) solid var(--tp-color-border-focus);
  border-radius: var(--tp-radius-md);
  padding: var(--tp-space-2) var(--tp-space-4);
  font-weight: var(--tp-weight-medium);
  text-decoration: none;
  box-shadow: var(--tp-shadow-md);
  /* Off-screen rather than display:none, so it stays focusable. */
  transform: translateY(-200%);
}

.tp-skip-link:focus-visible {
  transform: translateY(0);
}

/* --------------------------------------------------------------------------
   .tp-header — banner landmark
   -------------------------------------------------------------------------- */

.tp-header {
  position: sticky;
  top: 0;
  z-index: var(--tp-z-sticky);
  background-color: var(--tp-color-surface-raised);
  border-bottom: var(--tp-border-width-hairline) solid var(--tp-color-border-subtle);
}

.tp-header__inner {
  display: flex;
  align-items: center;
  gap: var(--tp-space-4);
  min-height: var(--tp-space-header-height);
  padding-block: var(--tp-space-2);
  flex-wrap: wrap;
}

/* Wraps on purpose (UX-004, fixed 2026-08-17). `flex-wrap` was never declared here,
   so it took the `nowrap` default: at 320px the four children -- theme toggle,
   Preferences, identity, Sign out -- measure 315px inside a 288px container, and the
   row overflowed by 11px. `body` carries `overflow-x: hidden`, so the excess was
   CLIPPED rather than scrollable: the Sign out button was cut off at the right edge
   with no way to reach the rest of it.

   The nearby "the header must stay one row tall" note governs `__identity`, which
   truncates; it was never a rule about this row. Two short rows beat a clipped
   control. Measured after the change: zero elements right of the viewport at 320px. */
.tp-header__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tp-space-2);
  margin-inline-start: auto;
}

/* The signed-in account. Truncated rather than wrapped, because an email
   address is long and the header must stay one row tall. */
.tp-header__identity {
  max-width: var(--tp-space-sidebar-width);
  color: var(--tp-color-text-muted);
  font-size: var(--tp-text-sm);
}

/* Below 30rem the identity is the first thing to go: the controls beside it
   are actionable and it is not. */
@media (max-width: 30rem) {
  .tp-header__identity {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   .tp-brand — product mark and name
   -------------------------------------------------------------------------- */

.tp-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--tp-space-2);
  color: var(--tp-color-text-primary);
  font-size: var(--tp-text-lg);
  font-weight: var(--tp-weight-bold);
  letter-spacing: -0.01em;
  text-decoration: none;
  padding: var(--tp-space-1) var(--tp-space-1);
  border-radius: var(--tp-radius-md);
  white-space: nowrap;
}

.tp-brand:hover {
  color: var(--tp-color-accent-hover);
}

.tp-brand__mark {
  display: block;
  width: var(--tp-space-icon-lg);
  height: var(--tp-space-icon-lg);
  color: var(--tp-color-accent-default);
  flex: none;
}

/* --------------------------------------------------------------------------
   .tp-nav — navigation landmark, responsive disclosure below 48rem
   -------------------------------------------------------------------------- */

/* A two-row header: identity and account actions on top, the navigation bar
   beneath. One row would be tighter, but it forces the nav list to wrap out of
   its own flex container at narrow widths, and every arrangement that achieves
   that is fragile. Two rows is what an instrument panel looks like anyway. */
.tp-nav {
  border-top: var(--tp-border-width-hairline) solid var(--tp-color-border-subtle);
}

.tp-nav__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--tp-space-2);
  margin-block: var(--tp-space-2);
  min-width: var(--tp-space-target-min);
  min-height: var(--tp-space-target-min);
  padding: var(--tp-space-2) var(--tp-space-3);
  border: var(--tp-border-width-hairline) solid var(--tp-color-border-default);
  border-radius: var(--tp-radius-md);
  background-color: var(--tp-color-surface-raised);
  color: var(--tp-color-text-secondary);
  font-size: var(--tp-text-sm);
  font-weight: var(--tp-weight-medium);
}

.tp-nav__toggle:hover {
  background-color: var(--tp-color-surface-sunken);
  color: var(--tp-color-text-primary);
}

.tp-nav__toggle:active {
  background-color: var(--tp-color-accent-subtle);
}

.tp-nav__toggle-icon {
  width: var(--tp-space-icon);
  height: var(--tp-space-icon);
  flex: none;
}

.tp-nav__list {
  /* Collapsed by default at narrow widths; nav.js sets .tp-is-open and keeps
     aria-expanded on the toggle in step with it. The list becomes an ordinary
     horizontal bar at 48rem, where the toggle disappears. */
  display: none;
  flex-direction: column;
  gap: var(--tp-space-1);
  width: 100%;
  padding-block: var(--tp-space-2);
}

.tp-nav__list.tp-is-open {
  display: flex;
}

/* The <li> wrapping each link. partials/_nav.html has always emitted it; this
   file did not declare it until the project-wide vocabulary check found the
   gap. It is structural rather than decorative — the link owns the padding and
   the hit area — but an undeclared class is an undeclared class. */
.tp-nav__item {
  display: flex;
  min-width: 0;
}

.tp-nav__link {
  display: flex;
  align-items: center;
  gap: var(--tp-space-2);
  min-height: var(--tp-space-target-min);
  padding: var(--tp-space-2) var(--tp-space-3);
  border-radius: var(--tp-radius-md);
  color: var(--tp-color-text-secondary);
  font-size: var(--tp-text-sm);
  font-weight: var(--tp-weight-medium);
  text-decoration: none;
  white-space: nowrap;
}

.tp-nav__link:hover {
  background-color: var(--tp-color-surface-sunken);
  color: var(--tp-color-text-primary);
}

.tp-nav__link:active {
  background-color: var(--tp-color-accent-subtle);
}

/* The current page carries a left rail as well as a colour and
   aria-current="page" in the markup, so it is never colour alone (ACC-004). */
.tp-nav__link--current {
  background-color: var(--tp-color-accent-subtle);
  color: var(--tp-color-text-primary);
  box-shadow: inset var(--tp-border-width-thick) 0 0 0 var(--tp-color-accent-default);
}

@media (min-width: 48rem) {
  .tp-nav__toggle {
    display: none;
  }

  .tp-nav__list {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--tp-space-1);
    width: auto;
    padding-block: 0;
  }

  .tp-nav__link--current {
    /* Horizontally the rail reads better underneath than beside. */
    box-shadow: inset 0 calc(-1 * var(--tp-border-width-thick)) 0 0
      var(--tp-color-accent-default);
  }
}

/* --------------------------------------------------------------------------
   .tp-main — main landmark
   -------------------------------------------------------------------------- */

.tp-main {
  padding-block: var(--tp-space-6) var(--tp-space-9);
  /* The skip link moves focus here; without a tabindex the target is not
     focusable and the jump silently does nothing in Safari. The template sets
     tabindex="-1"; this removes the ring that would otherwise appear on a
     programmatic focus that the user did not initiate with Tab. */
  outline: none;
}

/* --------------------------------------------------------------------------
   .tp-page-header — the page title band
   -------------------------------------------------------------------------- */

.tp-page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--tp-space-4);
  margin-bottom: var(--tp-space-6);
}

.tp-page-header__text {
  min-width: 0;
}

/* The line above the page title: an observation's public identifier, a report's
   period. Monospace and tabular because it is almost always an identifier, and
   an identifier is compared character by character rather than read. It sits
   above the <h1> visually but is not a heading — the document outline stays
   one <h1> deep. */
.tp-page-header__eyebrow {
  margin-bottom: var(--tp-space-1);
  color: var(--tp-color-text-muted);
  font-family: var(--tp-font-mono);
  font-size: var(--tp-text-sm);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
}

.tp-page-header__description {
  margin-top: var(--tp-space-2);
  max-width: var(--tp-space-prose-max);
  color: var(--tp-color-text-secondary);
}

.tp-page-header__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tp-space-2);
}

/* --------------------------------------------------------------------------
   .tp-layout — optional content + sidebar split
   -------------------------------------------------------------------------- */

.tp-layout {
  display: grid;
  gap: var(--tp-space-6);
  /* minmax(0, …) rather than 1fr: without it a wide table inside the content
     column forces the whole grid wider than the viewport, which is the single
     most common cause of an accidental horizontal body scroll (UX-004). */
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
}

/* The second column appears only when a page actually filled the `sidebar`
   block. :has() means the shell needs no extra block for pages to say so —
   the presence of the element IS the declaration. */
@media (min-width: 80rem) {
  .tp-layout:has(> .tp-layout__sidebar) {
    grid-template-columns: minmax(0, 1fr) var(--tp-space-sidebar-width);
  }
}

.tp-layout__content {
  min-width: 0;
}

.tp-layout__sidebar {
  min-width: 0;
}

/* --------------------------------------------------------------------------
   .tp-notice-region — the band P1.F7.T3 publishes into, plus Django messages
   -------------------------------------------------------------------------- */

.tp-notice-region {
  display: flex;
  flex-direction: column;
  gap: var(--tp-space-3);
  padding-block: var(--tp-space-4);
}

.tp-notice-region:empty {
  /* An empty live region must not leave a gap in the layout. */
  display: none;
}

/* --------------------------------------------------------------------------
   .tp-footer — contentinfo landmark
   -------------------------------------------------------------------------- */

.tp-footer {
  background-color: var(--tp-color-surface-raised);
  border-top: var(--tp-border-width-hairline) solid var(--tp-color-border-subtle);
  color: var(--tp-color-text-muted);
  font-size: var(--tp-text-sm);
}

.tp-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--tp-space-3);
  padding-block: var(--tp-space-5);
}

.tp-footer__links {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--tp-space-4);
}

/* A footer link is still an interactive target. Left as a bare inline anchor it
   measures 22px tall at the default type size, which is under ACC-006's 24px
   floor — small enough that nobody notices by eye and a measurement catches
   immediately. The negative inline margin keeps the padding from disturbing the
   row's alignment. */
.tp-footer__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--tp-space-target-min);
  padding-inline: var(--tp-space-1);
  margin-inline: calc(-1 * var(--tp-space-1));
  border-radius: var(--tp-radius-sm);
  color: var(--tp-color-text-link);
}

.tp-footer__link:hover {
  background-color: var(--tp-color-surface-sunken);
  color: var(--tp-color-accent-hover);
}

.tp-footer__link:active {
  background-color: var(--tp-color-accent-subtle);
}

.tp-footer__link:focus-visible {
  outline-offset: 0;
}

.tp-footer__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--tp-space-3);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   .tp-theme-toggle — three-state segmented control: light · dark · system
   Three states rather than two, because UX-002 makes the system preference the
   default and a two-state toggle makes "follow the system" unreachable once a
   user has chosen once.
   -------------------------------------------------------------------------- */

.tp-theme-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--tp-space-1);
  padding: var(--tp-space-1);
  border: var(--tp-border-width-hairline) solid var(--tp-color-border-default);
  border-radius: var(--tp-radius-pill);
  background-color: var(--tp-color-surface-base);
}

.tp-theme-toggle__option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--tp-space-target-min);
  min-height: var(--tp-space-target-min);
  padding: var(--tp-space-1) var(--tp-space-2);
  border-radius: var(--tp-radius-pill);
  color: var(--tp-color-text-muted);
}

.tp-theme-toggle__option:hover {
  background-color: var(--tp-color-surface-sunken);
  color: var(--tp-color-text-primary);
}

.tp-theme-toggle__option:active {
  background-color: var(--tp-color-accent-subtle);
}

/* aria-pressed is the accessible source of truth and is maintained by
   theme.js; styling from the attribute keeps the two from disagreeing. */
.tp-theme-toggle__option[aria-pressed="true"] {
  background-color: var(--tp-color-accent-default);
  color: var(--tp-color-text-on-accent);
}

.tp-theme-toggle__option:focus-visible {
  /* The pill sits inside a bordered track; without the reduced offset the ring
     is clipped by the track's overflow. */
  outline-offset: 0;
}

.tp-theme-toggle__icon {
  width: var(--tp-space-icon);
  height: var(--tp-space-icon);
  flex: none;
}

.tp-theme-toggle__option:disabled {
  opacity: var(--tp-opacity-disabled);
  background-color: transparent;
  color: var(--tp-color-text-muted);
}
