/* ---------------------------------------------------------------------
   Type — Space Grotesk (self-hosted variable font), one family, weight
   does the work.
--------------------------------------------------------------------- */

@font-face {
  font-family: "Space Grotesk";
  font-style: normal;
  font-weight: 300 700;
  /* optional, not swap — this is a full-page-reload MPA, not a persisted
     SPA session, so swap's fallback-then-reflow cycle repeated on every
     single navigation, visibly jumping the large hero numbers/headings
     whose width differs between Space Grotesk and the fallback stack.
     optional pairs with the <link rel=preload> in base.html: the font is
     almost always ready in time (self-hosted, same-origin, preloaded), so
     it renders correctly on the first paint instead of swapping in after. */
  font-display: optional;
  src: url("/static/fonts/space-grotesk.woff2") format("woff2");
}

/* Second voice for prose (About's essay copy, notes, action items, help
   text) — everywhere else (headings, numbers, buttons, labels) stays
   Space Grotesk. Not on Google Fonts (Fontshare-exclusive); same
   self-hosting/preload/optional treatment as Space Grotesk above. */
@font-face {
  font-family: "General Sans";
  font-style: normal;
  font-weight: 200 700;
  font-display: optional;
  src: url("/static/fonts/general-sans.woff2") format("woff2");
}

/* ---------------------------------------------------------------------
   Tokens — a dusk skyline. The towers are lit buildings at blue hour;
   everything else recedes so the accumulated color is what glows.
--------------------------------------------------------------------- */

:root {
  --sky-top: #0c0b16;
  --sky-mid: #15101f;
  --sky-low: #1f1526;
  --horizon-glow: rgba(255, 175, 110, 0.10);

  --paper: #f4efe7;
  /* Both are used by functional copy and controls, so neither can be
     atmospheric to the point of disappearing. Decorative separation has
     its own --line tokens below. */
  --paper-dim: rgba(244, 239, 231, 0.74);
  --paper-faint: rgba(244, 239, 231, 0.52);
  --line: rgba(244, 239, 231, 0.1);
  --line-strong: rgba(244, 239, 231, 0.2);

  --glass: rgba(244, 239, 231, 0.045);
  --glass-border: rgba(244, 239, 231, 0.1);

  /* The four semantic colors spent outside the project palette — the
     status reads on progress/staleness. Left at full brightness on
     purpose: a warning needs to read as urgent, not blend in. */
  --amber: #f2b84b;
  --coral: #e0685a;
  --jade: #5fb88a;
  --danger: #e0847a;

  --radius: 18px;
  --shell-width: 1040px;
  --reading-width: 680px;
  --sans: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --body: "General Sans", ui-sans-serif, system-ui, sans-serif;
}

* { box-sizing: border-box; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; }
}

html {
  color-scheme: dark;
  background: linear-gradient(180deg, var(--sky-top) 0%, var(--sky-mid) 55%, var(--sky-low) 100%);
  min-height: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: transparent;
  color: var(--paper);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.container {
  width: 100%;
  max-width: var(--shell-width);
  margin: 0 auto;
  /* Grows to fill whatever the nav-bar and site-footer (flex siblings in
     body's own column, see body's flex-direction:column above) don't
     already take up — the footer this feeds anchors to the bottom of the
     viewport on a short page instead of floating right under the content. */
  flex: 1 0 auto;
  /* 56px, not the original 88px — before the floating timer joined
     .nav-bar's own flow, this was the only source of "breathing room below
     the nav" on every page, and 88px read fine under a fixed-height nav.
     Now nav-bar's own height varies (taller whenever a Block is running),
     so this fixed padding sits on top of however tall nav-bar already
     grew — the two used to trade off against each other implicitly, but a
     nav that can grow makes a padding sized for the shortest case read as
     too much air on ordinary content, most visibly on short pages like New
     Project where there's little else below to balance it. */
  padding: 56px 20px 72px;
}

a { color: inherit; }

/* ---------------------------------------------------------------------
   Nav
--------------------------------------------------------------------- */

.nav-bar {
  flex: 0 0 auto;
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(12, 11, 22, 0.72);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

nav {
  width: 100%;
  max-width: var(--shell-width);
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 28px;
  padding: 26px 20px 22px;
}

nav .brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 19px;
  letter-spacing: -0.01em;
  margin-right: auto;
  color: var(--paper);
  text-decoration: none;
}

/* favicon.svg's own colors come from the project palette's mineral tones
   (COLOR_PALETTE in app/logic.py), not the brighter
   semantic --amber/--coral above — the mark represents the mosaic's own
   material, so it should read as flat mineral, not a shiny UI accent. */
.brand-mark {
  display: inline-block;
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  background: url("/static/favicon.svg") center / contain no-repeat;
}

nav a {
  color: var(--paper-dim);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  transition: color 0.15s ease;
}
nav a:hover, nav a:focus-visible { color: var(--paper); }

.nav-settings { display: inline-flex; align-items: center; gap: 8px; }
.nav-settings-label { display: none; }

nav a.here { color: var(--paper); }

.nav-login-link {
  color: var(--paper-dim);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  transition: color 0.15s ease;
}
.nav-login-link:hover, .nav-login-link:focus-visible { color: var(--paper); }

/* Account menu — a single <details>/<summary> trigger, reusing the exact
   popover look already established by .project-manage-menu (see project
   detail page) so the two dropdowns in this app read as one component. */
.account-menu { position: relative; flex: 0 0 auto; }
.account-menu summary {
  list-style: none;
  cursor: pointer;
  user-select: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  color: var(--paper-dim);
  transition: color 0.15s ease;
}
.account-menu summary::-webkit-details-marker { display: none; }
.account-menu summary:hover, .account-menu summary:focus-visible { color: var(--paper); outline: none; }
.nav-account-trigger svg { width: 17px; height: 17px; }

.account-popover {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  right: 0;
  width: 196px;
  padding: 6px;
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  background: #191624;
  box-shadow: 0 14px 34px rgba(4, 3, 10, 0.42);
}
.account-email {
  padding: 9px 10px 8px;
  font-family: var(--body);
  font-size: 12px;
  color: var(--paper-faint);
  overflow-wrap: anywhere;
}
.account-divider { height: 1px; margin: 2px 4px 4px; background: var(--line); }
.account-action {
  display: block;
  width: 100%;
  min-height: 38px;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--paper-dim);
  font: 500 13px/1.25 var(--sans);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.account-action:hover, .account-action:focus-visible { background: rgba(244, 239, 231, 0.07); color: var(--paper); outline: none; }
.account-action-form { margin: 0; }

.site-footer {
  flex: 0 0 auto;
  width: 100%;
  max-width: var(--shell-width);
  margin: 0 auto;
  padding: 26px 20px 40px;
  text-align: center;
  border-top: 1px solid var(--line);
}
.site-footer a {
  color: var(--paper-dim);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
}
.site-footer a:hover, .site-footer a:focus-visible { color: var(--paper); }

/* ---------------------------------------------------------------------
   Confirm dialog — a styled stand-in for the browser's native confirm(),
   built on <dialog> for its free focus trap and Esc-to-cancel.
--------------------------------------------------------------------- */

.confirm-dialog {
  width: min(360px, calc(100% - 40px));
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: #191624;
  color: var(--paper);
}
.confirm-dialog::backdrop { background: rgba(4, 3, 10, 0.55); }
.confirm-dialog-body { padding: 24px; }
.confirm-dialog-message {
  margin: 0 0 22px;
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.5;
  color: var(--paper-dim);
}
.confirm-dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 20px; }

/* ---------------------------------------------------------------------
   Floating timer — persists across every page while a Block is active.

   Every earlier version of this widget was position:fixed, overlaid on
   top of whatever the current page happened to render, and positioned by
   *guessing* at empty space (the first card's top edge, an offset for
   pages with a back-link, a shorter offset for the dashboard's tighter
   padding...). Each guess held only for the pages it was checked
   against — it broke on the Projects list (landed on a row further down
   than the first one checked) and on About (a page with no card at all,
   just centered lede text where the guess had never been tested). The
   set of "pages this could collide with" is unbounded because it's
   driven by page content, not by the widget itself.

   Fix: stop overlaying. This block is a second row of the sticky
   .nav-bar (see base.html — it's a sibling of <nav>, not of
   .container), in normal document flow. It *reserves* real layout
   height, so nothing can ever render underneath it — not because every
   page was checked, but because there's no coordinate for it to get
   wrong. Page content simply starts after it, on every page,
   unconditionally, including page-about's own locked-viewport layout,
   which already resizes its content area around nav-bar's height via
   flexbox — this row just adds to that height the same way a taller nav
   already would.
--------------------------------------------------------------------- */

.floating-timer {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "name name"
    "display actions";
  align-items: center;
  row-gap: 10px;
  column-gap: 14px;
  max-width: var(--shell-width);
  margin: 0 auto;
  padding: 14px 20px 18px;
  border-top: 1px solid var(--line);
}

.floating-timer-project {
  grid-area: name;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 14px;
  font-weight: 600;
  color: var(--paper-dim);
  text-decoration: none;
  line-height: 1.35;
  transition: color 0.15s ease;
}
.floating-timer-project:hover, .floating-timer-project:focus-visible { color: var(--paper); }
/* the row's line-height (1.35, for readable two-line wrapping) puts
   half-leading above the first line's glyphs — flex-start alone aligns
   the swatch to that inflated line box, not the visible text. Nudge down
   by the half-leading (font-size * (line-height - 1) / 2) to compensate. */
.floating-timer-project .swatch { margin-top: 2.5px; }

.floating-timer-display {
  grid-area: display;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: 30px;
  letter-spacing: -0.01em;
  color: var(--paper);
}

.floating-timer-actions { grid-area: actions; display: flex; align-items: center; gap: 10px; }
.floating-timer-action { margin: 0; }

/* Pause/Resume and Discard as one bordered pill split by a hairline,
   rather than a labeled button and a separate text link floating apart —
   icon-only, so this reads as a compact control instead of competing with
   the timer digits for attention. Each half is still its own <form> (they
   POST to different routes), styled to look like one continuous object. */
.icon-pill {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  overflow: hidden;
}
.icon-pill form { margin: 0; display: flex; }
.icon-pill form:first-child { border-right: 1px solid var(--line-strong); }
.icon-pill-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: none;
  background: transparent;
  color: var(--paper);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.icon-pill-btn svg { pointer-events: none; }
.icon-pill-btn:hover, .icon-pill-btn:focus-visible { background: rgba(244, 239, 231, 0.05); outline: none; }
.icon-pill-btn:focus-visible { box-shadow: inset 0 0 0 2px var(--paper-faint); }
.icon-pill-btn.discard { color: var(--paper-faint); }
.icon-pill-btn.discard:hover, .icon-pill-btn.discard:focus-visible { color: var(--danger); background: rgba(224, 132, 122, 0.08); }

/* Below this, the row is a two-line stack (name on its own line; digits +
   actions share the line below) — the default above, so mobile needs no
   override. At and above 641px there's enough width to fit name, digits,
   and actions on one line, so collapse to that instead. */
@media (min-width: 641px) {
  .floating-timer {
    grid-template-columns: none;
    grid-template-areas: none;
    display: flex;
    align-items: center;
    gap: 20px;
    padding: 12px 20px;
  }
  .floating-timer-project { flex: 1 1 auto; min-width: 0; }
  /* Ellipsis lives on the name span, not the whole .floating-timer-project
     link — that link also wraps the swatch, and overflow:hidden on it was
     clipping the swatch's glow (box-shadow) against the row's own tight
     bounding box, since a text line's height leaves the glow no room to
     render clipping-free above/below. min-width:0 is the usual flex-item
     override needed for overflow/ellipsis to actually kick in at all. */
  .floating-timer-project-name {
    display: inline-block;
    max-width: 320px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    vertical-align: bottom;
    min-width: 0;
  }
  .floating-timer-display { font-size: 22px; flex: 0 0 auto; }
  .floating-timer-actions { flex: 0 0 auto; }
}

/* ---------------------------------------------------------------------
   Glass cards — translucent panels floating over the dusk sky
--------------------------------------------------------------------- */

.card {
  position: relative;
  background: var(--glass);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  padding: 28px 30px;
  margin-bottom: 20px;
}
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(160deg, rgba(244, 239, 231, 0.14), transparent 40%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  pointer-events: none;
}

.card h2 {
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  /* Uppercase caps have no descenders, so their ink sits in the upper
     portion of the inherited 1.5 line-height box — align-items:center on
     .project-header-row was centering the icon buttons against that whole
     inflated box, not the glyphs, so the icons read as sitting visibly
     lower than the label. Tightening the box itself, not nudging the
     icons with a margin, is what actually fixes it. */
  line-height: 1;
  color: var(--paper-dim);
  margin: 0 0 18px;
}

.card-header {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-bottom: 18px;
}
.card-header h2 { margin: 0; }

/* Page-level titles sit outside cards. Cards name a task or section; they
   no longer have to double as the page's only orientation cue. */
.page-heading {
  max-width: var(--reading-width);
  margin: 0 0 28px;
}
.page-heading h1,
.login-heading h1 {
  margin: 0;
  color: var(--paper);
  font-size: 30px;
  line-height: 1.15;
  letter-spacing: -0.025em;
}
.page-heading p,
.login-heading p {
  margin: 8px 0 0;
  color: var(--paper-dim);
  font-family: var(--body);
  font-size: 15px;
  line-height: 1.5;
}

.container.settings-shell { max-width: 720px; }
.container.project-shell { max-width: 760px; }

.settings-shell .card,
.project-shell .card { margin-bottom: 16px; }

/* .dash-active reuses .card-header for its title row but has no .card
   ancestor to inherit heading style from (see the card-free dashboard
   comment above), so it needs its own copy of .card h2's look. */
.dash-active h2 {
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--paper-dim);
  margin: 0;
}

/* Dashboard sections stay card-free. The completed-block formation below
   grows directly out of the page rather than living in a framed panel. */
.container:has(.dash-stats) { padding-top: 16px; padding-bottom: 16px; }
.dash-active { padding-bottom: 8px; }

/* Stratum — only completed stones are visible. Its transparent canvas has
   no frame or bed, so the silhouette itself communicates accumulation.
   The stage is deliberately taller than the canvas it holds (1.5x — see
   the starfield below) and always renders, even with zero Blocks yet;
   the canvas itself keeps its own unchanged size (--wall-height) and sits
   at the bottom of that extra headroom, where a growing wall belongs. */
.dash-mosaic {
  --wall-height: clamp(140px, 20dvh, 176px);
  position: relative;
  overflow: hidden;
  margin: 0 0 8px;
  height: calc(var(--wall-height) * 1.5);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
/* A subtle horizon glow — warmth pooling low, roughly where the sky meets
   the wall, echoing the stones' own amber palette. Spans the FULL stage
   (sky + wall), not just the sky strip above the wall, and sits behind
   the canvas (z-index below it) — the canvas is transparent everywhere it
   has no stone yet, so the glow bleeds through those gaps near the top of
   the wall and tapers into it naturally, rather than being clipped in a
   hard line right at the sky/wall seam. Stage height is always exactly
   1.5x --wall-height, so the seam sits at a fixed 66.7% up from the
   bottom regardless of the actual pixel size — these stop percentages
   don't need to change if --wall-height's clamp() ever does. */
.dash-mosaic::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(
    0deg,
    transparent 0%,
    rgba(226, 173, 82, 0.11) 62%,
    rgba(226, 173, 82, 0.045) 78%,
    transparent 95%
  );
}
/* The starfield lives in its own box that stops exactly at the wall's own
   top edge (bottom: var(--wall-height)) — geometrically separate from the
   canvas, not just visually behind it. Hovering a project dims every
   OTHER stone to near-transparent (see stratum.js), and a same-box
   background would shine straight through that dimmed fill; a box that
   never overlaps the wall at all can't.

   Two ideas combined, not per-star glow (already tried, read as "too
   much" — the glow belongs to the sky, see ::after above): a two-tier
   magnitude split (7 of 38 stars are deliberately bigger/brighter
   "anchors" — visibility comes from a few genuinely standout points, not
   from raising every star's average) plus a raised opacity floor and a
   slight warm/cool color jitter on every star (nothing recedes to
   near-invisible, and the color variation adds interest without any one
   star popping on its own). */
.dash-mosaic::before {
  content: "";
  position: absolute;
  inset: 0;
  bottom: var(--wall-height);
  z-index: 1;
  /* The tile is sized to --shell-width (this box never exceeds that,
     since .container caps it) so it never actually repeats in practice —
     a smaller, visibly-repeating tile was an earlier version's mistake;
     this one covers the whole box in one non-repeating placement. */
  background-image:
    radial-gradient(1.1px 1.1px at 124px 46px, rgba(246, 235, 219, 0.47), transparent 65%),
    radial-gradient(1px 1px at 894px 13px, rgba(246, 235, 218, 0.41), transparent 65%),
    radial-gradient(1.4px 1.4px at 625px 51px, rgba(244, 239, 231, 0.58), transparent 65%),
    radial-gradient(1.4px 1.4px at 570px 65px, rgba(244, 239, 231, 0.57), transparent 65%),
    radial-gradient(1px 1px at 781px 53px, rgba(246, 234, 215, 0.41), transparent 65%),
    radial-gradient(2.6px 2.6px at 639px 14px, rgba(247, 230, 205, 0.88), transparent 65%),
    radial-gradient(1.3px 1.3px at 468px 72px, rgba(244, 238, 229, 0.53), transparent 65%),
    radial-gradient(1px 1px at 707px 22px, rgba(247, 232, 209, 0.41), transparent 65%),
    radial-gradient(1.4px 1.4px at 1019px 82px, rgba(245, 237, 224, 0.59), transparent 65%),
    radial-gradient(1px 1px at 1040px 47px, rgba(246, 233, 213, 0.4), transparent 65%),
    radial-gradient(1px 1px at 354px 51px, rgba(247, 232, 210, 0.42), transparent 65%),
    radial-gradient(1.3px 1.3px at 476px 77px, rgba(248, 230, 204, 0.54), transparent 65%),
    radial-gradient(1.3px 1.3px at 601px 7px, rgba(247, 233, 212, 0.55), transparent 65%),
    radial-gradient(1.1px 1.1px at 378px 40px, rgba(246, 233, 213, 0.44), transparent 65%),
    radial-gradient(1px 1px at 609px 46px, rgba(245, 238, 227, 0.4), transparent 65%),
    radial-gradient(1.2px 1.2px at 527px 67px, rgba(245, 236, 222, 0.5), transparent 65%),
    radial-gradient(1px 1px at 511px 80px, rgba(246, 234, 215, 0.41), transparent 65%),
    radial-gradient(1.1px 1.1px at 437px 81px, rgba(247, 232, 209, 0.46), transparent 65%),
    radial-gradient(1px 1px at 539px 7px, rgba(248, 230, 204, 0.41), transparent 65%),
    radial-gradient(2.5px 2.5px at 134px 9px, rgba(246, 234, 217, 0.86), transparent 65%),
    radial-gradient(1.2px 1.2px at 922px 13px, rgba(247, 232, 211, 0.51), transparent 65%),
    radial-gradient(1px 1px at 489px 45px, rgba(245, 238, 227, 0.41), transparent 65%),
    radial-gradient(1.1px 1.1px at 921px 57px, rgba(245, 237, 225, 0.45), transparent 65%),
    radial-gradient(2.8px 2.8px at 488px 82px, rgba(246, 234, 214, 0.93), transparent 65%),
    radial-gradient(1.3px 1.3px at 567px 51px, rgba(248, 230, 204, 0.53), transparent 65%),
    radial-gradient(2.3px 2.3px at 320px 71px, rgba(247, 231, 206, 0.83), transparent 65%),
    radial-gradient(1.1px 1.1px at 498px 78px, rgba(246, 234, 217, 0.45), transparent 65%),
    radial-gradient(1px 1px at 306px 27px, rgba(244, 238, 229, 0.4), transparent 65%),
    radial-gradient(1.1px 1.1px at 682px 54px, rgba(247, 233, 212, 0.45), transparent 65%),
    radial-gradient(1.6px 1.6px at 544px 45px, rgba(247, 230, 205, 0.67), transparent 65%),
    radial-gradient(2.3px 2.3px at 271px 45px, rgba(247, 232, 209, 0.83), transparent 65%),
    radial-gradient(2.4px 2.4px at 859px 72px, rgba(247, 231, 205, 0.83), transparent 65%),
    radial-gradient(1px 1px at 944px 73px, rgba(245, 236, 222, 0.42), transparent 65%),
    radial-gradient(1.3px 1.3px at 953px 13px, rgba(246, 235, 219, 0.54), transparent 65%),
    radial-gradient(1.1px 1.1px at 816px 47px, rgba(247, 232, 208, 0.45), transparent 65%),
    radial-gradient(2.6px 2.6px at 1004px 85px, rgba(246, 234, 216, 0.89), transparent 65%),
    radial-gradient(1px 1px at 498px 10px, rgba(245, 237, 225, 0.42), transparent 65%),
    radial-gradient(1.4px 1.4px at 518px 13px, rgba(244, 239, 231, 0.56), transparent 65%);
  background-repeat: no-repeat;
  background-size: 1040px 96px;
  background-position: top left;
}
.mosaic-canvas {
  display: block;
  width: 100%;
  height: var(--wall-height);
  position: relative;
  z-index: 2;
  cursor: default;
  touch-action: manipulation;
}
.stratum-stage-cropped { position: relative; overflow: hidden; }
.stratum-stage-cropped > canvas {
  position: absolute;
  left: 0;
  bottom: 0;
}
.mosaic-tooltip {
  position: absolute;
  z-index: 3;
  max-width: min(280px, calc(100% - 32px));
  padding: 7px 10px;
  border: 1px solid rgba(244, 239, 231, 0.12);
  border-radius: 7px;
  background: rgba(12, 9, 17, 0.92);
  color: var(--paper-dim);
  font-size: 11px;
  line-height: 1.35;
  letter-spacing: 0.01em;
  pointer-events: none;
  opacity: 0;
  transform: translate(-50%, calc(-100% - 12px));
  transition: opacity 100ms ease;
  text-align: center;
  white-space: normal;
  overflow-wrap: anywhere;
}
.mosaic-tooltip.visible { opacity: 1; }
.project-row.stratum-source {
  background: rgba(244, 239, 231, 0.035);
}
.dash-active .swatch { box-shadow: none; }

.dash-active .card-header { align-items: center; }
.active-projects-actions { display: flex; align-items: center; gap: 10px; }

/* .container's own default padding (88px 20px 72px, sized for card-based
   content pages) plus .login-page's own 40px top stacked to 128px before
   the card even started, and the unused 72px bottom padding pushed total
   page height past the viewport on ordinary screens — enough to force a
   scroll for a card short enough to otherwise fit on its own. Trimmed to
   what this page (no card-header, no multi-card stack) actually needs. */
body:has(.login-page) { min-height: 100dvh; display: flex; flex-direction: column; }
.container:has(.login-page) {
  flex: 1 1 auto;
  width: 100%;
  padding-top: 24px;
  padding-bottom: 24px;
  display: flex;
}
.login-page { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; }
.login-card {
  isolation: isolate;
  overflow: hidden;
  width: 100%;
  max-width: 720px;
  min-height: 420px;
  padding: 48px;
  border-radius: 14px;
  background: rgba(244, 239, 231, 0.035);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.login-card::before { display: none; }

.login-card form,
.login-card .field-error,
.login-card .auth-alt-link,
.login-heading {
  position: relative;
  z-index: 1;
  width: min(100%, 340px);
}

.login-heading { margin-bottom: 26px; }

/* The artwork is part of the card's edge, not a centered illustration
   sitting above the form. Its left/bottom fade keeps the fields quiet while
   the hard crop at top-right makes the geology feel environmental. */
.login-boulder-frame {
  position: absolute;
  z-index: 0;
  inset: 0;
  margin: 0;
  pointer-events: none;
}
.login-boulder-img {
  position: absolute;
  display: block;
  width: 470px;
  height: auto;
  top: -36px;
  right: -112px;
  opacity: 0.68;
  filter: saturate(0.88) brightness(0.86);
  -webkit-mask-image: linear-gradient(90deg, transparent 3%, #000 34%);
  mask-image: linear-gradient(90deg, transparent 3%, #000 34%);
}

.login-tagline {
  text-align: center;
  font-size: 13px;
  margin: 0 0 24px;
}

.login-about-link,
.auth-alt-link {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  text-align: center;
  margin-top: 20px;
  font-size: 13px;
  color: var(--paper-faint);
  text-decoration: none;
  transition: color 0.15s ease;
}
.login-about-link:hover, .login-about-link:focus-visible,
.auth-alt-link:hover, .auth-alt-link:focus-visible { color: var(--paper-dim); }

.forgot-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-top: 2px;
  font-size: 13px;
  color: var(--paper-faint);
  text-decoration: none;
  font-weight: 400;
}
.forgot-link:hover, .forgot-link:focus-visible { color: var(--paper-dim); }

.login-card .auth-alt-link {
  justify-content: flex-start;
  text-align: left;
}

.login-card h2 { margin: 0 0 20px; }

/* Verification is a real auth state, not a modal pasted over the product.
   It keeps the login card's proportions and type, then uses one small
   faceted-color mark as the only new visual element. */
.verification-card {
  min-height: 0;
  max-width: 600px;
  padding: 44px 52px 40px;
}
.verification-state {
  width: min(100%, 400px);
  margin: 0 auto;
}
.verification-mark {
  display: grid;
  place-items: center;
  width: 54px;
  height: 48px;
  margin-bottom: 24px;
  color: var(--paper);
  background: linear-gradient(145deg, var(--amber), var(--coral));
  clip-path: polygon(15% 5%, 82% 0, 100% 32%, 86% 91%, 28% 100%, 0 70%);
}
.verification-mark svg { width: 27px; height: 27px; }
.verification-mark-expired {
  color: var(--paper-dim);
  background: linear-gradient(145deg, rgba(169, 87, 109, 0.72), rgba(134, 105, 143, 0.72));
}
.verification-heading { margin-bottom: 20px; }
.verification-kicker {
  display: block;
  margin-bottom: 7px;
  color: var(--amber);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.verification-heading h1 {
  margin: 0 0 9px;
  color: var(--paper);
  font-size: 34px;
  line-height: 1.08;
  letter-spacing: -0.035em;
}
.verification-heading p,
.verification-help {
  margin: 0;
  color: var(--paper-dim);
  font-family: var(--body);
  font-size: 14px;
  line-height: 1.55;
}
.verification-email {
  padding: 13px 15px;
  margin-bottom: 18px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
  border-radius: 9px;
  color: var(--paper);
  background: rgba(244, 239, 231, 0.025);
  font-size: 14px;
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.verification-help { margin-bottom: 20px; }
.verification-confirmation {
  margin: -4px 0 16px;
  color: var(--jade);
  font-size: 13px;
  font-weight: 600;
}
.verification-resend-form { width: 100% !important; }
.verification-resend-form .btn { width: 100%; }
.verification-card .field-error { width: 100%; }
.verification-card .auth-alt-link { justify-content: center; width: 100%; margin-top: 12px; }
.verification-primary-link { width: 100%; }

/* Shared shape for a left-accented inline notice. */
.callout {
  margin: 0 0 18px;
  border-left: 2px solid;
  border-radius: 0 10px 10px 0;
  color: var(--paper-dim);
  font-size: 14px;
  line-height: 1.5;
}

.field-error {
  padding: 10px 14px;
  border-left-color: var(--danger);
  background: rgba(224, 132, 122, 0.1);
}

.card-header-title { display: flex; align-items: center; gap: 8px; }

/* Replaces the old Active/Planned/Archived tab links — one filterable list
   now, this just picks which bucket is visible. Reuses the same dropdown
   look as .project-manage-menu/.project-manage-popover (see below) rather
   than inventing a second popover style. */
.status-filter { position: relative; flex: 0 0 auto; }
.status-filter summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 0 12px;
  border: 1px solid var(--line-strong);
  border-radius: 9px;
  color: var(--paper-dim);
  font-size: 13px;
  font-weight: 600;
  user-select: none;
}
.status-filter summary b { color: var(--paper); font-weight: 700; }
.status-filter summary::-webkit-details-marker { display: none; }
.status-filter-chev { width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg); margin-top: -2px; }
.status-filter summary:hover, .status-filter summary:focus-visible { color: var(--paper); border-color: var(--paper-faint); outline: none; }
.status-filter-popover {
  position: absolute;
  z-index: 20;
  top: calc(100% + 7px);
  left: 0;
  width: 168px;
  padding: 6px;
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  background: #191624;
  box-shadow: 0 14px 34px rgba(4, 3, 10, 0.42);
}
.filter-option {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  min-height: 36px;
  padding: 7px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--paper-dim);
  font: 500 13px/1.25 var(--sans);
  text-align: left;
  cursor: pointer;
}
.filter-option:hover, .filter-option:focus-visible { background: rgba(244, 239, 231, 0.07); color: var(--paper); outline: none; }
.filter-option .count { color: var(--paper-faint); font-variant-numeric: tabular-nums; }

/* A quiet per-row echo of the current filter bucket — most useful in the
   "All" filter, where rows of different statuses sit side by side with
   nothing else marking which is which. */
.status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.status-dot.is-active { background: var(--jade); }
.status-dot.is-planned { background: var(--amber); }
.status-dot.is-archived { background: var(--paper-faint); }

.muted { color: var(--paper-dim); }
/* Sentence-level .muted (a full explanatory message) gets the body face;
   scoped to block-level tags only, never bare .muted, so a shorter inline
   fragment reusing this same class as a unit label isn't caught by this. */
p.muted, div.muted { font-family: var(--body); }
.faint { color: var(--paper-faint); }

/* Empty states used to just be a stray .muted paragraph — same weight,
   same alignment, same everything as body copy, which read as "text that
   forgot to load" rather than a deliberate state. Centered and quieter
   than .muted (not louder — the point is recessive, not decorative),
   with enough padding to read as its own considered block. */
.empty-state {
  text-align: center;
  color: var(--paper-dim);
  padding: 18px 16px;
}
.empty-state a { color: var(--paper-dim); }
.empty-state a:hover, .empty-state a:focus-visible { color: var(--paper); }
.project-description { white-space: pre-wrap; line-height: 1.6; }

/* ---------------------------------------------------------------------
   Stat strip — lifetime + last-7/last-30 read as one row of architecturally
   identical cells (label on top, number below) rather than a big hero
   number with a smaller companion trying to match its height. That
   number-vs-companion shape went through three rounds of alignment fixes
   that never landed, because a flex .stat-cell tall enough to hold the
   *primary* cell's bigger number pushes that cell's whole label+number
   pair down further than the shorter secondary cells' — the labels end
   up at different heights even though they're all the same font-size.
   A single CSS grid shared across all three cells fixes this by
   construction: every label lives in row 1, every number in row 2, so a
   row's height is the max of every cell's content in that row and every
   cell's label (and separately every number) shares the same top/bottom
   bounds — not just approximately, but because the grid guarantees it.
--------------------------------------------------------------------- */

.stat-strip {
  display: grid;
  grid-auto-flow: column;
  grid-template-columns: max-content 1px max-content 1px max-content;
  gap: 7px 30px;
  padding: 2px 0 24px;
}
/* display:contents drops the wrapper from layout so its label+number
   become direct grid items, placed into the shared row/column tracks
   above — the wrapper grouping the cell's markup isn't what aligns it. */
.stat-cell { display: contents; }

.stat-cell-label {
  grid-row: 1;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--paper-faint);
  white-space: nowrap;
}
.stat-cell-primary .stat-cell-label { color: var(--paper-dim); }

.stat-cell-num {
  grid-row: 2;
  align-self: end;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--paper);
  font-size: 30px;
}
.stat-cell-primary .stat-cell-num { font-size: 56px; }

.stat-gold {
  background: linear-gradient(160deg, #fff8ec 10%, #e8c98f 90%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  /* background-clip:text on an element inside .card's backdrop-filter:blur()
     is a documented Chromium rendering bug — the filter creates a new
     compositing context, and background-clip:text descendants inside one
     can fail to paint at all. isolation:isolate gives this element its own
     stacking context up front instead of inheriting into the blurred one,
     which is the standard fix for exactly this interaction. */
  isolation: isolate;
  width: fit-content;
}

.stat-div { grid-row: 1 / 3; align-self: stretch; width: 1px; background: var(--line); }

.hero-sub { margin-top: 10px; font-size: 14px; color: var(--paper-dim); }

.project-dates { margin: -8px 0 18px; font-size: 13px; }

/* ---------------------------------------------------------------------
   Project list / detail
--------------------------------------------------------------------- */

.back-link {
  display: inline-block;
  margin-bottom: 16px;
  font-size: 14px;
  font-weight: 500;
  color: var(--paper-dim);
  text-decoration: none;
}
.back-link:hover, .back-link:focus-visible { color: var(--paper); }

.project-item { border-bottom: 1px solid var(--line); }
.project-item:last-child { border-bottom: none; }

.project-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 0;
  cursor: pointer;
  color: inherit;
}
.project-row:focus-visible { outline: 2px solid var(--paper); outline-offset: -2px; border-radius: 6px; }

.project-row .chevron {
  width: 7px;
  height: 7px;
  border-right: 1.5px solid var(--paper-faint);
  border-bottom: 1.5px solid var(--paper-faint);
  transform: rotate(-45deg);
  flex-shrink: 0;
}

.swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  flex-shrink: 0;
  border: 1px solid rgba(12, 11, 22, 0.38);
}

/* Truncated to one line with an ellipsis rather than left to wrap — a
   wrapped title made the row's height unpredictable, which is what forced
   the whole two-row mobile grid below and its own alignment fixes. A
   single-line title keeps row height constant everywhere, so there's
   nothing left to re-center against. min-width:0 overrides the flex/grid
   default of "never shrink below content size", which would otherwise
   block the ellipsis from ever kicking in. */
.project-row .name {
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

/* Groups last-active/progress/action-buttons so they can be pushed right as
   one unit on desktop, and reflowed onto their own line on mobile (see the
   max-width:640px override below). Plain flex, not a fixed grid — rows
   within any one filtered bucket are homogeneous (every active row has a
   last-active badge, every planned row an added-days badge, archived rows
   neither), so there's no cross-bucket column to keep aligned the way the
   old three-tabs version needed. */
.project-meta {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-left: auto;
  flex-shrink: 0;
}

.project-row .progress {
  font-variant-numeric: tabular-nums;
  color: var(--paper);
  font-size: 14px;
  white-space: nowrap;
}
.project-row .last-active { font-size: 12px; white-space: nowrap; }

/* Traffic-light read on a "last active" badge (see staleness_risk in
   app/logic.py) — green comfortably recent, amber past the halfway point
   to STALE_AFTER_DAYS, red once it's reached, neutral (a completed
   project) just the plain muted color. Purely informational. Color
   deliberately lives only here, not on .last-active itself — every usage
   always pairs one of these four, and .project-row .last-active's own
   two-class specificity would otherwise beat a bare .risk-* rule and win
   regardless of source order. */
.risk-safe { color: var(--jade); }
.risk-warning { color: var(--amber); }
.risk-danger { color: var(--danger); }
.risk-neutral { color: var(--paper-faint); }

.quick-start-form { margin: 0; flex-shrink: 0; }

.icon-play {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  padding: 0;
  border: 1px solid var(--paper-faint);
  border-radius: 50%;
  background: transparent;
  color: var(--paper-dim);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
/* Invisible 44x44 hit area — the visible circle stays 26px so it doesn't
   dominate the row next to the 14px swatch and 7px chevron, but touch still
   gets a real target. */
.icon-play::before { content: ""; position: absolute; inset: -7px; }
.icon-play svg { margin-left: 1px; }
.icon-play:hover, .icon-play:focus-visible { color: var(--paper); border-color: var(--paper-dim); background: rgba(244, 239, 231, 0.06); outline: none; }

/* ---------------------------------------------------------------------
   Buttons & forms
--------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  background: var(--paper);
  color: var(--sky-top);
  border: none;
  border-radius: 10px;
  padding: 12px 22px;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.btn:hover { opacity: 0.86; }
.btn:active { transform: scale(0.98); }
.btn:disabled,
.btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; transform: none; }
/* Opacity alone (the hover treatment) isn't a strong enough signal on its
   own for keyboard focus — a real outline replaces the default UA ring,
   which would otherwise be an unstyled, theme-clashing blue. */
.btn:focus-visible { outline: 2px solid var(--paper); outline-offset: 2px; }

.btn.secondary { background: transparent; color: var(--paper); border: 1px solid var(--line-strong); }
.btn.big { padding: 16px 34px; font-size: 16px; border-radius: 12px; }
.btn.danger { background: transparent; color: var(--danger); border: 1px solid var(--danger); opacity: 0.85; }
.btn.danger:hover, .btn.danger:focus-visible { opacity: 1; }

.btn-link {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 13px;
  font-weight: 500;
  color: var(--paper-dim);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.btn-link:hover, .btn-link:focus-visible { color: var(--paper); }
.btn-link:focus-visible { outline: 2px solid var(--paper); outline-offset: 3px; border-radius: 3px; }
.btn-link.danger { color: var(--danger); opacity: 0.75; }
.btn-link.danger:hover, .btn-link.danger:focus-visible { opacity: 1; }

.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  padding: 0;
  border: none;
  border-radius: 6px;
  background: none;
  color: var(--paper-dim);
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
/* Invisible 44x44 hit area, same reasoning as .icon-play above — these sit
   in dense rows (action items, notes) where a visibly bigger box would
   throw off the row's rhythm, but the tap target still needs to be real. */
.icon-btn::before { content: ""; position: absolute; inset: -6px; }
.icon-btn:hover, .icon-btn:focus-visible { color: var(--paper-dim); background: rgba(244, 239, 231, 0.06); outline: none; }
.icon-btn:focus-visible { box-shadow: 0 0 0 2px var(--paper); }
.icon-btn.danger:hover, .icon-btn.danger:focus-visible { color: var(--danger); }
.icon-btn.accent { color: var(--paper); }
.icon-btn.accent:hover, .icon-btn.accent:focus-visible { color: var(--paper); }

form label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--paper-dim);
  margin-bottom: 6px;
}

form .field { margin-bottom: 20px; }

form input[type="text"],
form input[type="date"],
form input[type="number"],
form input[type="email"],
form input[type="password"],
form textarea {
  width: 100%;
  min-height: 44px;
  padding: 12px 14px;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  background: rgba(244, 239, 231, 0.03);
  color: var(--paper);
  font-family: var(--sans);
  font-size: 15px;
  color-scheme: dark;
}
form textarea { resize: vertical; line-height: 1.5; font-family: var(--body); }
form input:focus, form textarea:focus { outline: none; border-color: var(--paper); }
form input:focus-visible, form textarea:focus-visible {
  box-shadow: 0 0 0 3px rgba(244, 239, 231, 0.12);
}
form input:disabled { color: var(--paper-faint); cursor: not-allowed; }
form input::placeholder, form textarea::placeholder { color: var(--paper-faint); }

/* A step below form label — for a sentence of explanation under a field
   rather than the field's own name, so labels and their help text read as
   two distinct tiers instead of competing at the same size/weight. */
.field-help {
  font-size: 13px;
  color: var(--paper-dim);
  line-height: 1.6;
  margin: 10px 0 0;
  font-family: var(--body);
}
.field-help.collapsed { display: none; }

/* Chrome/Safari's autofill repaint (yellow/blue tint + its own font) ignores
   normal background/color rules entirely — the standard workaround is a
   transparent-looking inset box-shadow the same color as the field, painted
   with a very long transition delay so it only ever "animates" on autofill. */
form input:-webkit-autofill,
form input:-webkit-autofill:hover,
form input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--paper);
  -webkit-box-shadow: 0 0 0 1000px rgba(244, 239, 231, 0.03) inset;
  box-shadow: 0 0 0 1000px rgba(244, 239, 231, 0.03) inset;
  transition: background-color 600000s 0s, color 600000s 0s;
}

.save-confirmation {
  color: var(--jade);
  font-size: 14px;
  font-weight: 600;
  animation: save-confirmation-in 180ms ease both;
}
@keyframes save-confirmation-in { from { opacity: 0; transform: translateY(-2px); } }

.settings-actions {
  min-height: 44px;
  display: flex;
  align-items: center;
  gap: 14px;
}
.settings-shell .field-help { max-width: 58ch; overflow-wrap: anywhere; }

.note-form { display: flex; flex-direction: column; gap: 12px; }
.note-form .btn { align-self: flex-start; }

.note-list { display: flex; flex-direction: column; gap: 16px; margin-top: 22px; }
.note {
  padding-top: 16px;
  border-top: 1px solid var(--line);
}
.note-content { white-space: pre-wrap; line-height: 1.6; font-family: var(--body); }
.note-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 12px;
  color: var(--paper-faint);
}
/* gap is 20px, not the tighter 6px it looks like it wants — the icon
   buttons' invisible 44px hit areas (see .icon-btn) extend 9px past each
   visible edge, so anything under 18px of real gap would let a neighboring
   pair's hit zones overlap and risk a tap landing on the wrong (here,
   destructive) button. */
.note-actions { display: flex; gap: 20px; }

.note-edit-form { display: none; flex-direction: column; gap: 10px; margin-top: 4px; }
.note-edit-actions { display: flex; align-items: center; gap: 16px; }

.note.editing .note-content,
.note.editing .note-meta { display: none; }
.note.editing .note-edit-form { display: flex; }

.action-add-form { display: flex; gap: 10px; }
.action-add-form input { flex: 1; }

.action-list { display: flex; flex-direction: column; gap: 2px; margin-top: 18px; }
.action-item { padding: 10px 0; border-top: 1px solid var(--line); }
.action-item:first-child { border-top: none; }

.action-row { display: flex; align-items: center; gap: 12px; }

/* display:flex (not the default block) so the button is a flex item, not
   inline-block content on a text baseline — otherwise the form's line box
   inherits body's line-height and the checkbox sits baseline-aligned near
   its top instead of centered against the item text. */
.action-check-form { margin: 0; display: flex; }
.action-check {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  border-radius: 5px;
  /* --line-strong (the general hairline-divider token) reads fine for a
     passive border, but this is an interactive control — fixed via
     contrast (a token already used for hover elsewhere, now the resting
     state here) rather than adding any fill/shadow. */
  border: 2px solid var(--paper-faint);
  background: transparent;
  cursor: pointer;
  padding: 0;
  position: relative;
  transition: border-color 0.15s ease, background 0.15s ease;
}
/* Same invisible-44x44-hit-area reasoning as .icon-btn/.icon-play. */
.action-check::before { content: ""; position: absolute; inset: -13px; }
.action-check:hover, .action-check:focus-visible { border-color: var(--paper-dim); outline: none; }
.action-item.done .action-check {
  background: var(--project-color, var(--paper));
  border-color: var(--project-color, var(--paper));
}
.action-item.done .action-check::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid var(--sky-top);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.action-content { flex: 1; line-height: 1.5; font-family: var(--body); }
.action-item.done .action-content { color: var(--paper-faint); text-decoration: line-through; }

/* Same visual language as .action-check, applied to a real checkbox input
   (not a fake button) since this one needs to submit its checked state as
   part of a normal form post rather than toggling instantly via fetch. */
.checkbox-field { display: flex; align-items: center; gap: 8px; cursor: pointer; }
.checkbox-field input[type="checkbox"] {
  appearance: none;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  margin: 0;
  border-radius: 5px;
  border: 2px solid var(--paper-faint);
  background: transparent;
  cursor: pointer;
  position: relative;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.checkbox-field input[type="checkbox"]:hover,
.checkbox-field input[type="checkbox"]:focus-visible { border-color: var(--paper-dim); outline: none; }
.checkbox-field input[type="checkbox"]:checked { background: var(--paper); border-color: var(--paper); }
.checkbox-field input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 5px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid var(--sky-top);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.action-actions { display: flex; gap: 20px; flex-shrink: 0; } /* see .note-actions comment — hit-zone overlap margin */

.action-edit-form { display: none; align-items: center; gap: 10px; margin-top: 8px; }
.action-edit-form input { flex: 1; }
.action-edit-actions { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }

.action-item.editing .action-row { display: none; }
.action-item.editing .action-edit-form { display: flex; }

.size-picker { display: flex; flex-wrap: wrap; gap: 8px; }
.size-option {
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 10px 16px;
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  background: rgba(244, 239, 231, 0.03);
  color: var(--paper);
  transition: background 0.15s ease, border-color 0.15s ease;
}
/* Visually hidden, not display:none — display:none removes an element
   from the tab order in every browser, which made this whole picker
   unreachable by keyboard (confirmed by tabbing through it: focus jumped
   straight from the field above to the submit button). This keeps the
   radio focusable and in the tab order while the label still supplies all
   the visible styling. */
.size-option input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.size-option:has(input:checked) { background: var(--paper); color: var(--sky-top); border-color: var(--paper); }
.size-option:has(input:focus-visible) { outline: 2px solid var(--paper); outline-offset: 2px; }

.project-header-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.card .project-header h2 {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 14px;
  color: var(--paper);
  font-size: 24px;
  font-weight: 650;
  line-height: 1.15;
  letter-spacing: -0.02em;
  text-transform: none;
}
.card .project-header h2::before {
  content: "";
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  border-radius: 3px;
  background: var(--project-color);
}
.project-header-actions { display: flex; align-items: center; gap: 20px; } /* see .note-actions comment — hit-zone overlap margin */
.project-header-actions .icon-btn {
  border: 1px solid var(--line);
  background: rgba(244, 239, 231, 0.025);
  color: var(--paper-dim);
}
.project-header-actions .icon-btn svg { width: 16px; height: 16px; }
.header-edit-form { display: none; flex-direction: column; margin-top: 4px; }
.header-edit-actions { display: flex; align-items: center; gap: 16px; margin: -6px 0 16px; }
.header-edit-actions .btn.secondary { padding: 6px 14px; font-size: 13px; border-radius: 8px; }
.project-header.editing .project-header-display { display: none; }
.project-header.editing .header-edit-form { display: flex; }

/* Project detail is built around the project's own geological record, not
   another generic glass card. The hairlines divide working regions without
   putting the title, progress and controls into one oversized container. */
.project-overview {
  position: relative;
  padding: 8px 0 26px;
  margin-bottom: 22px;
  border-bottom: 1px solid var(--line);
}
.project-overview .project-header-row { align-items: flex-start; }
.project-overview .project-header h1 {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  margin: 0;
  color: var(--paper);
  font-size: clamp(27px, 4vw, 36px);
  font-weight: 650;
  line-height: 1.12;
  letter-spacing: -0.035em;
}
.project-overview .project-header h1::before {
  content: "";
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  border-radius: 3px;
  background: var(--project-color);
}
/* One continuous panel for project info, Action items, and Notes —
   equal-weight sections divided by hairlines flush with the panel's own
   edge, rather than three separately bordered boxes stacked with their
   own margins. Each section is a flex column with its own gap (not
   per-child margins) so spacing between whichever of a section's pieces
   actually render stays consistent regardless of which are present — a
   project with no description just has one less gap, not a leftover
   margin from a paragraph that never printed. Accent color (set inline
   as --project-color) is confined to the title dot, the stones, and the
   two section labels — deliberately nowhere else, including hover/focus
   states below, so it reads as an identity marker rather than a UI habit. */
.project-panel {
  margin-top: 18px;
  border: 1px solid var(--glass-border);
  border-radius: var(--radius);
  background: var(--glass);
  overflow: hidden;
}
.project-panel-section {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 22px 24px;
}
.project-panel-section + .project-panel-section { border-top: 1px solid var(--line); }
/* Editing the title/description swaps in _project_fields.html's own form
   for the info section's content — showing the old description here at
   the same time would just be a stale duplicate of what's being typed
   above, so this section (and only this one) hides while editing. */
.project-panel.editing .project-panel-section:first-child { display: none; }

.project-section-heading-row { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; }
.project-section-title {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1;
  color: var(--project-color);
}
.project-panel .btn:hover { background: rgba(244, 239, 231, 0.04); border-color: var(--line-strong); }
.project-panel .btn:focus-visible,
.project-panel input:focus-visible,
.project-panel textarea:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--sky-top), 0 0 0 4px var(--paper-faint);
}
.project-panel input:focus, .project-panel textarea:focus {
  border-color: var(--line-strong);
  background: rgba(244, 239, 231, 0.045);
}

.project-description {
  max-width: 590px;
  margin: 0;
  color: var(--paper-dim);
  font: 16px/1.55 var(--body);
}
.project-meta-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 7px 22px;
  margin: 0;
  color: var(--paper-dim);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}
.project-meta-label {
  margin-right: 5px;
  color: var(--paper-faint);
}
.project-meta-sep { color: var(--paper-faint); margin: 0 -14px 0 -14px; }

.project-manage-menu { position: relative; flex: 0 0 auto; }
.project-manage-menu summary {
  min-height: 38px;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0 12px;
  list-style: none;
  border: 1px solid var(--line-strong);
  border-radius: 9px;
  color: var(--paper-dim);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  user-select: none;
}
.project-manage-menu summary::-webkit-details-marker { display: none; }
.project-manage-menu summary::after {
  content: "";
  width: 6px;
  height: 6px;
  margin-top: -3px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}
.project-manage-menu[open] summary::after { margin-top: 3px; transform: rotate(225deg); }
.project-manage-menu summary:hover,
.project-manage-menu summary:focus-visible { color: var(--paper); border-color: var(--paper-faint); outline: none; }

/* Same menu, icon-only trigger — a project list row has neither the room
   nor the need for a labeled "Manage" button, just the one status-move
   action a row like this ever needs. Shares .project-manage-popover/
   .project-menu-action wholesale rather than a second popover style. */
.row-manage-menu { position: relative; flex: 0 0 auto; }
.row-manage-menu summary {
  position: relative;
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 6px;
  color: var(--paper-dim);
}
.row-manage-menu summary::-webkit-details-marker { display: none; }
/* Invisible 44x44 hit area, same reasoning as .icon-btn above. */
.row-manage-menu summary::before { content: ""; position: absolute; inset: -6px; }
.row-manage-menu summary:hover,
.row-manage-menu summary:focus-visible { color: var(--paper); background: rgba(244, 239, 231, 0.06); outline: none; }
.row-manage-menu svg { pointer-events: none; }

.project-manage-popover {
  position: absolute;
  z-index: 20;
  top: calc(100% + 7px);
  right: 0;
  width: 178px;
  padding: 6px;
  border: 1px solid var(--line-strong);
  border-radius: 11px;
  background: #191624;
  box-shadow: 0 14px 34px rgba(4, 3, 10, 0.42);
}
.project-manage-popover form { margin: 0; }
.project-menu-action {
  width: 100%;
  min-height: 38px;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--paper-dim);
  font: 500 13px/1.25 var(--sans);
  text-align: left;
  cursor: pointer;
}
.project-menu-action:hover,
.project-menu-action:focus-visible { background: rgba(244, 239, 231, 0.07); color: var(--paper); outline: none; }
.project-menu-action:disabled { opacity: 0.38; cursor: not-allowed; }
.project-menu-action.danger { color: var(--danger); }

.project-overview .header-edit-form {
  padding: 0 0 18px 23px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 18px;
}
.project-edit-setting {
  min-width: 0;
  padding: 0;
  margin: 0 0 18px;
  border: 0;
}
.project-edit-setting legend {
  padding: 0;
  margin-bottom: 8px;
  color: var(--paper-dim);
  font-size: 13px;
  font-weight: 600;
}
.project-edit-setting .size-option { padding: 8px 13px; }
.project-overview .header-edit-actions { margin: 0; }

.project-formation-row { display: flex; align-items: center; gap: 10px; }
/* One flat stone per Block the project is sized for — lit (full color,
   full opacity) once completed, otherwise a dim, unlit silhouette in the
   same shape. Wraps for a large project instead of scrolling or capping
   at some arbitrary count. */
.project-stones { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; row-gap: 6px; }
.project-stone {
  width: 18px;
  height: 14px;
  flex: 0 0 auto;
  clip-path: polygon(14% 0%, 100% 8%, 86% 100%, 0% 88%);
  background: var(--project-color);
  opacity: 0.3;
}
.project-stone.lit { opacity: 1; }
/* The stone count already says how far along this project is — this is
   just what that count is measured against, so it stays a plain, quiet
   number rather than competing with the wall for attention. */
.project-size-total {
  flex: 0 0 auto;
  color: var(--paper-dim);
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.project-complete-mark {
  flex: 0 0 auto;
  color: var(--jade);
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.project-section-heading-row span {
  color: var(--paper-faint);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.project-panel-section .action-add-form { gap: 8px; }
.project-panel-section .action-add-form input { min-height: 40px; padding-top: 8px; padding-bottom: 8px; }
.project-panel-section .action-add-form .btn { min-height: 40px; padding: 8px 15px; }
.project-panel-section .action-list { margin-top: 0; gap: 0; }
.project-panel-section .action-item { padding: 8px 2px; }
.project-panel-section .action-content { font-size: 14px; line-height: 1.4; }

/* ---------------------------------------------------------------------
   Responsive

   One shared mobile breakpoint, 640px — the nav collapses here, and it's
   the default place to reach for a new @media rule for anything that
   doesn't degrade on its own. 640px and 1490px below are NOT general
   layout breakpoints — they're specific to the floating timer's own two
   transition points (pill overlay, then in-flow card on phone). Touch targets (anything clickable) are 44x44px minimum everywhere,
   not just below the breakpoint — see .icon-btn / .icon-play / .action-check
   above — so mouse and touch users get the same-sized controls.
--------------------------------------------------------------------- */

@media (max-width: 640px) {
  .verification-card { padding: 30px 24px 26px; }
  .verification-heading h1 { font-size: 30px; }
  .verification-mark { margin-bottom: 20px; }

  /* .card-header packs the title (+ optional "+" button) and the
     Active/Planned/Archived tabs onto one row with justify-content:
     space-between — fine at desktop width, but "Planned Projects" or
     "Archived Projects" alongside all three tabs has nowhere near enough
     room here and either wraps mid-phrase or crams everything together.
     Stacking unconditionally (title row, then tabs) keeps every view
     consistent regardless of how long its title happens to be, instead of
     only the shortest ("Projects") title fitting on one row by luck. */
  .card-header { flex-direction: column; align-items: flex-start; gap: 10px; }

  /* The title's own row doesn't have room to sit alongside last-active,
     progress, and the action button at this width — switch to two grid
     rows: title (still one line — see .project-row .name's ellipsis
     above) spans the full width up top, metadata reflows onto its own
     line underneath. Row heights are fixed either way now, so a plain
     align-items:center on the container is enough for every item; no
     per-item nudging needed. */
  .project-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    grid-template-areas:
      "swatch name  name"
      ".      meta  chevron";
    row-gap: 6px;
    column-gap: 14px;
    align-items: center;
  }
  .project-row .swatch { grid-area: swatch; }
  .project-row .name { grid-area: name; }
  .project-row .chevron { grid-area: chevron; }
  /* Places .project-meta into the second grid row's middle area (see the
     grid-template-areas above) — desktop already has it as a flex row, so
     only its placement needs to change here, not its display. */
  .project-meta { grid-area: meta; margin-left: 0; }
  .project-row .last-active { margin-right: auto; }

  /* Nav + the floating timer bar together were eating roughly a quarter
     of a typical phone viewport before any page content showed at all —
     both kept their desktop vertical padding regardless of width, and the
     timer's own digits were actually *larger* on mobile (30px) than the
     desktop single-row layout (22px) despite the tighter two-row stack
     they sit in here. Trimmed padding/gap and brought the digits in line
     with desktop's scale instead of exceeding it. */
  nav { padding: 18px 20px 16px; }
  .floating-timer { padding: 10px 20px 14px; row-gap: 6px; }
  .floating-timer-display { font-size: 24px; }

  /* Same reasoning as the timer bar above — full desktop padding on every
     card, stacked three deep on a narrow screen, adds up fast. */
  .card { padding: 20px 20px; }

  .project-shell { padding-top: 18px; }
  .project-overview { padding-top: 2px; margin-bottom: 16px; }
  .project-overview .project-header-row { gap: 14px; }
  .project-overview .project-header h1 { font-size: 27px; }
  .project-overview .header-edit-form { padding-left: 0; }
  .project-panel-section { gap: 14px; padding: 16px 18px; }
  .project-meta-strip { gap: 6px 16px; }
  .project-manage-menu summary { min-height: 42px; }
  .project-panel-section .action-add-form input { min-width: 0; }
  .project-panel-section .action-actions { gap: 18px; }

  .container:has(.login-page) { padding-top: 8px; padding-bottom: 8px; }
  .login-card {
    max-width: 380px;
    min-height: 0;
    padding: 88px 18px 16px;
  }
  .login-heading { margin-bottom: 20px; }
  .login-card form .field { margin-bottom: 14px; }
  .login-card .auth-alt-link { margin-top: 12px; }
  .login-boulder-img {
    width: 250px;
    top: -72px;
    right: -82px;
    opacity: 0.62;
    -webkit-mask-image: linear-gradient(180deg, #000 40%, transparent 96%);
    mask-image: linear-gradient(180deg, #000 40%, transparent 96%);
  }

  /* The dashboard is a single glance on phones too. Its project rows keep
     the same information as desktop, but no longer expand into two-line
     list rows (the full Projects screen still does). */
  .container:has(.dash-stats) { padding-top: 18px; padding-bottom: 18px; }
  .stat-strip { gap: 6px 18px; padding: 0 0 18px; }
  .stat-cell-primary .stat-cell-num { font-size: 38px; }
  .stat-cell-num { font-size: 21px; }
  .stat-cell-label { font-size: 10px; }
  .dash-mosaic { --wall-height: 118px; margin: 4px 0 10px; }
}

@media (max-width: 560px) {
  .timer-display { font-size: 84px; }
}


/* ---------------------------------------------------------------------
   About — type on the dusk gradient, with a low geological stratum
   growing out of the page edge. No card, no in-page heading, no footer.
--------------------------------------------------------------------- */

body.page-about {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow-x: clip;
  overflow-y: auto;
}

body.page-about .container.about-stage {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  min-width: 0;
  max-width: 1080px;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.about-lede {
  font-size: 24px;
  line-height: 1.32;
  font-weight: 500;
  color: var(--paper);
  letter-spacing: -0.02em;
  margin: 0 0 20px;
  font-family: var(--body);
}

.about-body {
  font-size: 17px;
  line-height: 1.55;
  font-weight: 400;
  color: var(--paper-dim);
  margin: 0 0 18px;
  font-family: var(--body);
}

/* The two standout lines borrow the favicon's own warm amber / fired coral
   — the same mineral palette (app/logic.py COLOR_PALETTE) the stratum
   artwork below is built from — so the page has a couple of deliberate
   color beats instead of reading flat black-and-white until the artwork. */
.about-rest {
  font-size: 28px;
  line-height: 1.2;
  font-weight: 500;
  color: #e2ad52;
  letter-spacing: -0.03em;
  margin: 36px 0;
  font-family: var(--body);
}

.about-thesis {
  font-size: 17px;
  line-height: 1.55;
  font-weight: 400;
  color: #df705d;
  margin: 0 0 18px;
  font-family: var(--body);
}

.about-cite {
  font-size: 14px;
  line-height: 1.5;
  font-weight: 400;
  color: var(--paper-dim);
  margin: 32px 0 0;
  font-family: var(--body);
}

.about-cite a {
  color: var(--paper-dim);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--paper-faint);
}

.about-cite a:hover,
.about-cite a:focus-visible {
  color: var(--amber);
  text-decoration-color: var(--amber);
}

.about-copy {
  flex: 0 0 auto;
  width: 100%;
  max-width: 640px;
  padding: 0 20px;
  margin-inline: auto;
}

.about-horizon {
  pointer-events: none;
  flex: 0 0 auto;
  width: 100%;
  min-width: 0;
  overflow: hidden;
  padding: 6px 20px 24px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  opacity: 0.58;
}

/* The About horizon is drawn by the same engine as the user's real wall.
   Its smaller footprint and lower opacity keep it atmospheric rather than
   turning it into a second progress display. */
.about-formation {
  position: relative;
  width: min(680px, 68vw);
  max-width: 100%;
}

.about-horizon .about-wall {
  display: block;
  width: 100%;
  height: 120px;
  margin: 0 auto;
  pointer-events: none;
}

@media (min-width: 641px) {
  body.page-about {
    height: 100dvh;
  }
  /* Extra air splits equally above the lede and below the cite.
     The city stays grounded; it is not part of the centered group. */
  .about-copy {
    margin-top: auto;
    margin-bottom: auto;
  }
}

@media (max-width: 640px) {
  body.page-about {
    height: 100dvh;
  }
  body.page-about .container.about-stage { padding: 12px 0 0; }
  .about-lede { font-size: 22px; margin-bottom: 12px; }
  .about-body { margin-bottom: 12px; }
  .about-rest { font-size: 24px; margin: 16px 0; }
  .about-thesis { margin-bottom: 12px; }
  .about-cite { margin-top: 12px; }
  /* Ground the shallow formation at the viewport edge when space permits;
     auto margins collapse to zero on shorter screens, so the copy can still
     flow naturally without forcing overflow. */
  .about-horizon { margin-top: auto; padding: 8px 28px 12px; opacity: 0.48; }
  .about-formation { width: min(440px, 88vw); }
  .about-horizon .about-wall { height: 78px; }
}

@media (max-width: 640px) and (max-height: 700px) {
  body.page-about .container.about-stage { padding-top: 6px; }
  .about-lede { font-size: 20px; margin-bottom: 8px; }
  .about-body { margin-bottom: 8px; }
  .about-rest { font-size: 22px; margin: 8px 0; }
  .about-thesis { margin-bottom: 8px; }
  .about-cite { margin-top: 8px; }
  .about-horizon {
    margin-top: 8px;
    padding: 4px 20px 0;
  }
  .about-horizon .about-wall { height: 78px; }
}

@media (max-width: 640px) and (max-height: 580px) {
  .about-horizon { display: none; }
}
