/* ==========================================================================
   Track8 — Deep Emerald Precision
   Mobile-first. Designed for a phone held in one hand, then allowed to grow.

   Two things the previous stylesheet got wrong on real hardware and that are
   handled deliberately here:
     - safe-area insets, so the bottom bar clears the iPhone home indicator
     - dynamic viewport units, so the layout does not jump as the mobile
       browser's address bar collapses
   ========================================================================== */

:root {
  --bg-primary: #121212;
  --bg-surface: #1e1e1e;
  --bg-surface-elevated: #262626;
  --bg-surface-hover: #333333;

  --primary-emerald: #10b981;
  --primary-emerald-dark: #059669;
  --primary-emerald-light: #34d399;
  --primary-glow: rgba(16, 185, 129, 0.25);

  --amber-warning: #f59e0b;
  --amber-warning-dark: #d97706;
  --red-danger: #ef4444;
  --red-danger-dark: #dc2626;
  --slate-muted: #64748b;

  /* One colour per activity, used by the status pill, the stacked week bars,
     the day timeline and the stat row alike, so a colour means the same thing
     everywhere in the app. */
  --activity-work: #10b981;
  --activity-work-dark: #059669;
  --activity-meeting: #3b82f6;
  --activity-meeting-dark: #2563eb;
  --activity-break: #f59e0b;
  --activity-break-dark: #d97706;
  --activity-lunch: #a855f7;
  --activity-lunch-dark: #9333ea;
  --activity-paused: #64748b;

  --text-primary: #f3f4f6;
  --text-secondary: #9ca3af;
  /* 4.6:1 on --bg-surface. The old #6b7280 measured 3.45:1, below the 4.5:1
     AA floor, and it is the colour of nearly every small label in the app. */
  --text-muted: #8d95a3;

  --border-subtle: rgba(255, 255, 255, 0.08);
  --border-accent: rgba(16, 185, 129, 0.4);

  /* System fonts only: no CDN request, so the app is genuinely offline-capable
     once installed, and there is no flash of unstyled text on a slow network. */
  --font-main: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;

  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 20px;
  --radius-full: 9999px;

  --glass-bg: rgba(30, 30, 30, 0.78);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
  --shadow-glow: 0 0 20px rgba(16, 185, 129, 0.28);

  /* Histogram geometry. The track is a minimum rather than a fixed height —
     the chart grows into whatever the week card is not using — so the goal
     line cannot be placed at a fixed pixel offset. Instead every piece of
     chrome around the track is a token, the two sums below describe the track
     band in terms of the wrapper, and ui.js supplies only --goal-ratio. That
     keeps the line correct through a resize, an orientation change and the
     first paint, none of which run the renderer again. */
  --bar-track-height: 140px;
  --bar-label-height: 22px;
  --bar-val-height: 14px;
  --bar-gap: 4px;
  --histogram-pad-top: 18px;

  /* Height between the wrapper's bottom edge and the bottom of a track. */
  --bar-base-offset: calc(var(--bar-gap) + var(--bar-label-height));
  /* Everything in the wrapper that is not track. */
  --bar-chrome: calc(
    var(--histogram-pad-top) + var(--bar-val-height) + var(--bar-gap) + var(--bar-base-offset)
  );

  --nav-height: 62px;
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-top: env(safe-area-inset-top, 0px);

  /* The timer is a glance screen: reaching for the last button should never
     mean scrolling first. Every vertical dimension on it is therefore tied to
     the viewport rather than fixed, so a short phone — or a tall one with the
     browser's address bar showing — compresses instead of overflowing.
     The floors are the point at which compressing further would start hurting:
     140px still reads as a ring, 44px is the minimum comfortable tap target. */
  --ring-size: clamp(132px, min(24vh, 54vw), 220px);
  --stack-gap: clamp(7px, 1.5vh, 14px);
  --btn-height: clamp(44px, 5.8vh, 48px);

  /* Motion budget. Everything animated in this file moves `transform` or
     `opacity` only — the two properties the compositor can run without asking
     the main thread to lay out or paint. Anything animating width, height,
     top, filter or box-shadow costs a frame on a mid-range Android and is not
     worth the polish. One-shot durations live here so they stay consistent. */
  --motion-fast: 140ms;
  --motion-base: 200ms;
  --motion-slow: 420ms;
  --ease-out: cubic-bezier(0.22, 0.9, 0.32, 1);
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: transparent;
}

html {
  background: var(--bg-primary);
}

body {
  font-family: var(--font-main);
  background: var(--bg-primary);
  color: var(--text-primary);
  line-height: 1.5;
  min-height: 100vh;
  min-height: 100dvh;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

/* Set while a modal is open. Without it the page behind a bottom sheet keeps
   scrolling under the user's finger once the sheet's own scroll bottoms out,
   which on a phone reads as the sheet having lost the gesture. */
body.modal-open {
  overflow: hidden;
}

.app-container {
  width: 100%;
  max-width: 520px;
  margin: 0 auto;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  position: relative;
}

/* No backdrop-filter here on purpose. These cards sit on a flat --bg-primary
   body, and blurring a solid colour returns the same solid colour: the effect
   was invisible while costing a GPU blur pass over the largest area on screen,
   every frame of every scroll. The blur is kept only where there is real
   content behind it — the sticky header, the fixed nav and the modal scrim.
   The 1px light inner stroke gives the same "lifted glass" edge for free. */
.glass-panel {
  background: var(--glass-bg);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.04);
}

/* ------------------------------------------------------------------ header */

.app-header {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: calc(12px + var(--safe-top)) 16px 12px;
  background: rgba(18, 18, 18, 0.85);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border-subtle);
}

.header-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.logo-icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-md);
  background: linear-gradient(135deg, var(--primary-emerald), var(--primary-emerald-dark));
  color: #06231a;
  flex-shrink: 0;
}

.brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.15;
  min-width: 0;
}

.brand-text strong {
  font-size: 16px;
  letter-spacing: -0.2px;
}

.sub-badge {
  font-size: 11px;
  color: var(--primary-emerald-light);
  font-weight: 600;
}

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

.icon-btn,
.icon-btn-sm {
  display: grid;
  place-items: center;
  border: 1px solid var(--border-subtle);
  background: var(--bg-surface);
  color: var(--text-secondary);
  border-radius: var(--radius-full);
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}

.icon-btn {
  position: relative;
  width: 38px;
  height: 38px;
}

.icon-btn-sm {
  width: 30px;
  height: 30px;
  font-size: 17px;
  line-height: 1;
}

.icon-btn:active,
.icon-btn-sm:active {
  background: var(--bg-surface-hover);
}

/* There is no data ahead of today, so forward stops at the current period.
   Greyed rather than removed: a control that disappears reads as a bug. */
.icon-btn-sm:disabled {
  opacity: 0.3;
  cursor: default;
}

.icon-btn-sm:disabled:active {
  background: var(--bg-surface);
}

.notify-btn.enabled {
  color: var(--primary-emerald);
  border-color: var(--border-accent);
}

.notify-dot {
  position: absolute;
  top: 7px;
  right: 8px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--primary-emerald);
  box-shadow: 0 0 6px var(--primary-glow);
}

/* ---------------------------------------------------------------- profiles */

.person-selector-wrapper {
  position: relative;
}

.person-select-btn {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px 5px 5px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-full);
  color: var(--text-primary);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  max-width: 160px;
}

.avatar-sm {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--primary-emerald), var(--primary-emerald-dark));
  color: #06231a;
  font-size: 12px;
  font-weight: 700;
  flex-shrink: 0;
}

.person-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chevron {
  color: var(--text-muted);
  flex-shrink: 0;
}

.person-dropdown-menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: 240px;
  padding: 8px;
  z-index: 50;
  background: var(--bg-surface-elevated);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
}

.dropdown-header {
  padding: 4px 8px 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-muted);
}

.person-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 240px;
  overflow-y: auto;
}

.person-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.person-item:hover,
.person-item.active {
  background: var(--bg-surface-hover);
}

.person-item-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.person-item-name {
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.person-item-role {
  font-size: 11px;
  color: var(--text-muted);
}

.person-check {
  color: var(--primary-emerald);
  font-weight: 700;
}

.dropdown-divider {
  height: 1px;
  background: var(--border-subtle);
  margin: 8px 4px;
}

.add-person-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 9px 8px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--primary-emerald);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}

.add-person-btn:hover {
  background: var(--bg-surface-hover);
}

/* ----------------------------------------------------------------- content */

.app-content {
  flex: 1;
  padding: var(--stack-gap) 14px calc(var(--nav-height) + var(--safe-bottom) + var(--stack-gap));
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap);
}

.view {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap);
}

/* View change. Applied by showView() to the view being revealed, then removed
   on animationend so re-entering the same view replays it. Transform and
   opacity only, so the whole thing runs off the main thread. */
.view.entering {
  animation: view-in var(--motion-base) var(--ease-out) both;
}

@keyframes view-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

[hidden] {
  display: none !important;
}

/* Present to screen readers, invisible on screen. Must stay in the layout
   (not display:none) or live-region announcements are dropped. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- banners */

.alert-banner {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  border-radius: var(--radius-md);
  border: 1px solid;
  font-size: 13px;
}

.alert-banner.warn {
  background: rgba(245, 158, 11, 0.1);
  border-color: rgba(245, 158, 11, 0.35);
  color: #fcd9a0;
}

.alert-banner strong {
  display: block;
  color: var(--amber-warning);
  margin-bottom: 3px;
}

.alert-actions {
  display: flex;
  gap: 8px;
}

/* ------------------------------------------------------------- timer card */

.timer-card {
  padding: clamp(12px, 1.9vh, 18px) 16px clamp(12px, 1.7vh, 16px);
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap);
}

.timer-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

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

.day-name {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.2px;
}

.full-date {
  font-size: 12px;
  color: var(--text-muted);
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 600;
  border: 1px solid;
  white-space: nowrap;
}

.status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}

.status-idle {
  color: var(--text-secondary);
  background: rgba(148, 163, 184, 0.1);
  border-color: rgba(148, 163, 184, 0.25);
}

.status-working {
  color: var(--primary-emerald);
  background: rgba(16, 185, 129, 0.12);
  border-color: var(--border-accent);
}

.status-working .status-dot {
  animation: pulse 2s ease-in-out infinite;
}

.status-meeting {
  color: var(--activity-meeting);
  background: rgba(59, 130, 246, 0.12);
  border-color: rgba(59, 130, 246, 0.4);
}

.status-meeting .status-dot {
  animation: pulse 2s ease-in-out infinite;
}

.status-break {
  color: var(--activity-break);
  background: rgba(245, 158, 11, 0.12);
  border-color: rgba(245, 158, 11, 0.35);
}

.status-lunch {
  color: var(--activity-lunch);
  background: rgba(168, 85, 247, 0.12);
  border-color: rgba(168, 85, 247, 0.35);
}

.status-paused {
  color: var(--activity-paused);
  background: rgba(100, 116, 139, 0.14);
  border-color: rgba(100, 116, 139, 0.4);
}

.status-finished {
  color: var(--primary-emerald-light);
  background: rgba(52, 211, 153, 0.12);
  border-color: rgba(52, 211, 153, 0.3);
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* --------------------------------------------------------- progress ring */

.progress-ring-container {
  position: relative;
  display: grid;
  place-items: center;
  margin: 0 auto;
  width: var(--ring-size);
  height: var(--ring-size);
}

/* The SVG carries width/height 220 as attributes so it still draws correctly
   with CSS disabled; these override them. The viewBox is untouched, so the
   radius of 92 that js/ui.js uses to compute the circumference stays valid at
   any rendered size. */
.progress-ring {
  width: 100%;
  height: 100%;
  /* Explicit paint order for the four things stacked in this box: the halo and
     the sweep are pseudo-elements and would otherwise paint over the stroke and
     the digits, since generated content comes last in tree order. */
  z-index: 1;
}

/* Static glow behind the ring. This replaces the drop-shadow filter that used
   to sit on .ring-fill: a filter on an element whose stroke-dashoffset is
   transitioning re-rasterises the whole filter region on every frame of that
   transition, once per second, for the length of a shift. A gradient painted
   once costs nothing and reads the same. */
/* The stops are tuned to the ring's own geometry: r=92 inside a 220px box, so
   the stroke sits at 92/110 ≈ 84% of this element's radius. The halo peaks
   there and fades either side of it. */
.progress-ring-container::before {
  content: '';
  position: absolute;
  z-index: 0;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 50%,
    transparent 72%,
    rgba(16, 185, 129, 0.20) 84%,
    transparent 97%);
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease-out);
  pointer-events: none;
}

.progress-ring-container.lit::before {
  opacity: 1;
}

/* ------------------------------------------------------ the earning sweep */

/* A soft light that travels around the ring while, and only while, time is
   being credited. It is not the progress: the ring advances 0.0125° a second,
   eighty seconds to move one degree, so real progress cannot be seen moving at
   all. This is a heartbeat, and the thing it reports is the one users get
   wrong — whether the clock is earning. It runs during work and meetings and
   fades out on break, lunch and pause, where nothing is being counted.
   Fading rather than freezing: a stopped blob at whatever angle it reached
   reads as a rendering artefact, and fading costs nothing while resting.

   Built as an off-centre radial gradient on a rotating box rather than a conic
   gradient with a mask: masking is the part older Android WebViews get wrong,
   and a blob at 42% from the centre already sits on the stroke, which is at
   92/110 of this box's radius. */
.progress-ring-container::after {
  content: '';
  position: absolute;
  z-index: 0;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 50% 8%,
    rgba(52, 211, 153, 0.42),
    transparent 34%);
  opacity: 0;
  transition: opacity var(--motion-slow) var(--ease-out);
  pointer-events: none;
}

/* Nine seconds a turn. A loading spinner is about one, and reading as a
   spinner is the failure mode here: this has to say "running", never "busy". */
.progress-ring-container.earning::after {
  opacity: 1;
  animation: dial-sweep 9s linear infinite;
  will-change: transform;
}

@keyframes dial-sweep {
  to { transform: rotate(360deg); }
}

/* Meetings are credited too, so the sweep runs; it just takes their colour. */
.progress-ring-container.tone-meeting::after {
  background: radial-gradient(circle at 50% 8%,
    rgba(96, 165, 250, 0.42),
    transparent 34%);
}

/* The blanket reduced-motion rule collapses the animation to a single 0.01ms
   pass, which would leave the blob parked at the top looking like a deliberate
   highlight. Nothing here carries information the ring does not, so it goes. */
@media (prefers-reduced-motion: reduce) {
  .progress-ring-container::after {
    display: none;
  }
}

/* Fires once when the daily target is reached. Scale only — no layout. */
.progress-ring-container.celebrate::before {
  animation: goal-pulse 900ms var(--ease-out) 1;
}

@keyframes goal-pulse {
  0%   { transform: scale(1);    opacity: 1; }
  45%  { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}

.progress-ring {
  transform: rotate(-90deg);
}

.ring-bg {
  fill: none;
  stroke: rgba(255, 255, 255, 0.06);
}

.ring-fill {
  fill: none;
  stroke: var(--primary-emerald);
  stroke-linecap: round;
  transition: stroke-dashoffset var(--motion-slow) var(--ease-out), stroke var(--motion-base) ease;
}

.ring-fill.complete {
  stroke: var(--primary-emerald-light);
}

.ring-fill.meeting {
  stroke: var(--activity-meeting);
}

/* One colour per activity, the same rule the stat row, the week bars and the
   status pill follow. The three resting states used to share the break's amber,
   which only became visibly wrong once the caption inside the ring started
   naming the activity in its own colour: an amber ring under a purple "LUNCH"
   contradicted itself. */
.ring-fill.resting {
  stroke: var(--activity-break);
}

.ring-fill.lunch {
  stroke: var(--activity-lunch);
}

.ring-fill.paused {
  stroke: var(--activity-paused);
}

/* The glow tracks the ring's colour so the halo never contradicts the stroke. */
.progress-ring-container.tone-meeting::before {
  background: radial-gradient(circle at 50% 50%,
    transparent 72%, rgba(59, 130, 246, 0.22) 84%, transparent 97%);
}

.progress-ring-container.tone-resting::before {
  background: radial-gradient(circle at 50% 50%,
    transparent 72%, rgba(245, 158, 11, 0.20) 84%, transparent 97%);
}

.progress-ring-container.tone-lunch::before {
  background: radial-gradient(circle at 50% 50%,
    transparent 72%, rgba(168, 85, 247, 0.20) 84%, transparent 97%);
}

.progress-ring-container.tone-paused::before {
  background: radial-gradient(circle at 50% 50%,
    transparent 72%, rgba(100, 116, 139, 0.20) 84%, transparent 97%);
}

.timer-center-content {
  position: absolute;
  z-index: 2;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
}

/* Plays once each time the activity changes, so switching to lunch reads as
   the clock being handed over rather than the digits jumping. Transform and
   opacity only, on one element, once per state change - not per tick: ui.js
   gates it on the state actually differing. */
.timer-center-content.swap {
  animation: dial-swap 260ms var(--ease-out) both;
}

@keyframes dial-swap {
  from { opacity: 0; transform: translateY(7px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.timer-label {
  font-size: 10px;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: color var(--motion-base) ease;
}

/* The caption is decorative while it reads "On the clock" and load-bearing the
   moment it names an activity: it is what tells you whether the digits are the
   day or the break. So it takes the state's colour. */
[data-state="BREAK"] .timer-label { color: var(--activity-break); }
[data-state="LUNCH"] .timer-label { color: var(--activity-lunch); }
[data-state="MEETING"] .timer-label { color: var(--activity-meeting); }
[data-state="PAUSED"] .timer-label { color: var(--activity-paused); }

.timer-digits {
  font-family: var(--font-mono);
  font-size: clamp(25px, 4.2vh, 34px);
  font-weight: 700;
  letter-spacing: -1px;
  font-variant-numeric: tabular-nums;
}

.target-progress-text {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-secondary);
}

.percentage-badge {
  padding: 2px 7px;
  border-radius: var(--radius-full);
  background: rgba(16, 185, 129, 0.15);
  color: var(--primary-emerald);
  font-size: 11px;
  font-weight: 700;
}

.percentage-badge.over {
  background: rgba(52, 211, 153, 0.2);
  color: var(--primary-emerald-light);
}

/* Sits beside the percentage and carries what the banner below used to say -
   the reminder countdown, the overrun, or the day total while the digits are
   busy with a meeting. Muted, because the digits above are the headline. */
.dial-chip {
  font-size: 10.5px;
  color: var(--text-muted);
  white-space: nowrap;
}

.dial-chip.over {
  color: var(--amber-warning);
  font-weight: 600;
}

/* On the narrowest phones the ring's interior cannot hold a percentage and a
   chip side by side. The chip is the one that has a home elsewhere - the
   banner appears on the overrun, and the stat row carries the totals - so it
   is what gives way. */
@media (max-width: 344px) {
  .dial-chip {
    display: none;
  }
}

/* ------------------------------------------------------------ break banner */

.break-banner {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: clamp(9px, 1.4vh, 12px) 14px;
  border-radius: var(--radius-md);
  background: rgba(245, 158, 11, 0.1);
  border: 1px solid rgba(245, 158, 11, 0.3);
}

.break-banner.over {
  background: rgba(239, 68, 68, 0.12);
  border-color: rgba(239, 68, 68, 0.4);
  animation: nudge 2.4s ease-in-out infinite;
}

@keyframes nudge {
  0%, 92%, 100% { transform: translateX(0); }
  94% { transform: translateX(-3px); }
  96% { transform: translateX(3px); }
  98% { transform: translateX(-2px); }
}

.break-banner-text {
  flex: 1;
  min-width: 0;
}

.break-banner-text strong {
  display: block;
  font-size: 14px;
  color: var(--amber-warning);
}

.break-banner.over .break-banner-text strong {
  color: #fca5a5;
}

.break-banner-text p {
  font-size: 11.5px;
  color: var(--text-secondary);
  line-height: 1.35;
  margin-top: 2px;
}

/* ------------------------------------------------------------ stats & row */

.session-stats-bar {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--border-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.stat-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: clamp(6px, 1.1vh, 9px) 4px;
  background: var(--bg-surface);
  /* A hairline in the activity's own colour ties each figure to its slice of
     the week chart without adding another legend. */
  border-top: 2px solid transparent;
}

.stat-meeting { border-top-color: var(--activity-meeting); }
.stat-break   { border-top-color: var(--activity-break); }
.stat-lunch   { border-top-color: var(--activity-lunch); }
.stat-paused  { border-top-color: var(--activity-paused); }

.stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-muted);
}

.stat-value {
  font-family: var(--font-mono);
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.clock-row {
  display: flex;
  justify-content: center;
  gap: 22px;
  font-size: 12px;
  color: var(--text-muted);
}

.clock-row strong {
  font-family: var(--font-mono);
  color: var(--text-secondary);
}

.reminder-status {
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
  text-align: center;
  padding-top: 2px;
}

/* ---------------------------------------------------------------- buttons */

.action-buttons-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: clamp(7px, 1.1vh, 9px);
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--btn-height);
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  font: inherit;
  font-size: 14px;
  font-weight: 650;
  cursor: pointer;
  transition: transform 0.1s ease, opacity 0.15s ease, background 0.15s ease;
}

.btn:active:not(:disabled) {
  transform: scale(0.975);
}

/* One visible focus ring for every interactive element. Keyboard and
   switch-control users had nothing to follow outside the calendar grid. */
.btn:focus-visible,
.icon-btn:focus-visible,
.icon-btn-sm:focus-visible,
.nav-item:focus-visible,
.close-btn:focus-visible,
.person-item:focus-visible,
.add-person-btn:focus-visible,
.settings-group-header:focus-visible,
.person-select-btn:focus-visible {
  outline: 2px solid var(--primary-emerald);
  outline-offset: 2px;
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.btn.wide {
  grid-column: 1 / -1;
}

/* End day is the last button on its row in every state that shows it, so it
   spans the grid rather than leaving a hole beside itself. */
[data-state="WORKING"] #btnEnd,
[data-state="MEETING"] #btnEnd,
[data-state="BREAK"] #btnEnd,
[data-state="LUNCH"] #btnEnd,
[data-state="PAUSED"] #btnEnd {
  grid-column: 1 / -1;
}

.btn-meeting {
  background: linear-gradient(135deg, var(--activity-meeting), var(--activity-meeting-dark));
  color: #f8fafc;
  box-shadow: 0 0 18px rgba(59, 130, 246, 0.28);
}

.btn.full {
  width: 100%;
  margin-top: 12px;
}

.btn-small {
  min-height: 38px;
  font-size: 13px;
  padding: 0 12px;
}

.btn-primary {
  background: linear-gradient(135deg, var(--primary-emerald), var(--primary-emerald-dark));
  color: #052e22;
  box-shadow: var(--shadow-glow);
}

.btn-secondary {
  background: var(--bg-surface-elevated);
  border-color: var(--border-subtle);
  color: var(--text-primary);
}

.btn-danger {
  background: rgba(239, 68, 68, 0.14);
  border-color: rgba(239, 68, 68, 0.4);
  color: #fca5a5;
}

.btn-ghost {
  background: transparent;
  border-color: var(--border-subtle);
  color: var(--text-secondary);
}

.btn-icon {
  flex-shrink: 0;
}

/* -------------------------------------------------------------- analytics */

.analytics-card,
.calendar-card {
  padding: 16px 14px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.card-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.card-title-row h2 {
  font-size: 16px;
  font-weight: 700;
  letter-spacing: -0.2px;
}

.month-nav {
  display: flex;
  align-items: center;
  gap: 6px;
}

.month-title {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  min-width: 108px;
  text-align: center;
}

/* Jump back to the current period. Hidden while you are already on it, so it
   costs nothing in the common case. */
.return-chip {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 11px 5px 8px;
  border: 1px solid var(--border-accent);
  border-radius: var(--radius-full);
  background: rgba(16, 185, 129, 0.1);
  color: var(--primary-emerald);
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: transform var(--motion-fast) var(--ease-out), background 0.15s;
}

.return-chip:active {
  transform: scale(0.96);
  background: rgba(16, 185, 129, 0.18);
}

.return-chip:focus-visible {
  outline: 2px solid var(--primary-emerald);
  outline-offset: 2px;
}

/* The week chart takes whatever height the card is not using, instead of
   leaving a dead band above the bottom nav. Everything else in the card is
   intrinsically sized, so the wrapper is the only thing that grows. */
#viewWeek {
  flex: 1;
  min-height: 0;
}

#viewWeek .analytics-card {
  flex: 1;
  min-height: 0;
}

.histogram-wrapper {
  position: relative;
  padding-top: var(--histogram-pad-top);
  flex: 1;
  min-height: 0;
  display: flex;
}

.target-line-indicator {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  /* Label on the right: Monday's bar is usually the tallest and the weekend
     end of the chart is usually empty, so the badge sits over nothing. */
  flex-direction: row-reverse;
  align-items: center;
  gap: 6px;
  pointer-events: none;
  z-index: 2;
  /* The track band is the wrapper minus its chrome, and --goal-ratio (set by
     ui.js) is where the target falls on the bar scale. Percentages here
     resolve against the wrapper, so this stays right at any height without
     anything being measured. Shifting down by half the row puts the dashes on
     the line rather than above it. */
  bottom: calc(var(--bar-base-offset) + (100% - var(--bar-chrome)) * var(--goal-ratio, 1));
  transform: translateY(50%);
}

.target-label {
  font-size: 10px;
  font-weight: 600;
  color: var(--primary-emerald);
  background: var(--bg-surface);
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

.dash-line {
  flex: 1;
  height: 0;
  border-top: 1px dashed rgba(16, 185, 129, 0.45);
}

.histogram-bars {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  align-items: stretch;
  flex: 1;
  min-height: 0;
}

/* A button, not a div: tapping a day opens its log. The three rows are the
   value caption, the track, and the day label; the track takes the rest, which
   is what lets the chart grow into the card's spare height. --bar-track-height
   is now the floor rather than the height. */
.bar-column {
  display: grid;
  grid-template-rows: var(--bar-val-height) minmax(var(--bar-track-height), 1fr) var(--bar-label-height);
  justify-items: center;
  gap: var(--bar-gap);
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: transform var(--motion-fast) var(--ease-out);
}

.bar-column:active:not(:disabled) {
  transform: scale(0.96);
}

.bar-column:focus-visible {
  outline: 2px solid var(--primary-emerald);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Future days hold nothing to open and cannot be corrected in advance. */
.bar-column:disabled {
  cursor: default;
}

.bar-val {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  height: var(--bar-val-height);
  font-variant-numeric: tabular-nums;
}

.bar-track {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 0;
  background: rgba(255, 255, 255, 0.035);
  border-radius: var(--radius-sm);
}

/* Segments are laid out bottom-up: the first child in the DOM ends up on top,
   so ui.js emits them in reverse order with work last. */
.bar-stack {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-sm);
  overflow: hidden;
}

/* The old `transition: height` never actually ran: renderWeek() replaces the
   whole bar container, so every segment is a brand new node with no previous
   height to animate from. It was pure cost on a property that forces layout.
   The grow is now a one-shot scaleY on the compositor, which does animate,
   and the segment's own height is set once at build time. */
.bar-seg {
  display: block;
  width: 100%;
  min-height: 2px;
  transform-origin: bottom center;
}

/* Only when the week on screen actually changed — see renderWeek(). Regrowing
   every bar because someone tapped "Short break" is noise, not polish. */
.histogram-bars.animate .bar-seg {
  animation: seg-grow var(--motion-slow) var(--ease-out) both;
}

@keyframes seg-grow {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

.seg-work    { background: linear-gradient(180deg, var(--activity-work), var(--activity-work-dark)); }
.seg-meeting { background: linear-gradient(180deg, var(--activity-meeting), var(--activity-meeting-dark)); }
.seg-break   { background: linear-gradient(180deg, var(--activity-break), var(--activity-break-dark)); }
.seg-lunch   { background: linear-gradient(180deg, var(--activity-lunch), var(--activity-lunch-dark)); }

/* Second channel for the two uncounted activities.
   Hue alone cannot separate emerald work from amber break for a red-green
   colourblind reader, and these bars carry no numbers of their own. A diagonal
   hatch on break and lunch makes "does this count towards my 8 hours?"
   answerable without seeing colour at all: solid counts, hatched does not. */
.seg-break,
.seg-lunch {
  background-image:
    repeating-linear-gradient(135deg,
      rgba(0, 0, 0, 0.28) 0 3px,
      transparent 3px 7px),
    linear-gradient(180deg, var(--activity-break), var(--activity-break-dark));
}

.seg-lunch {
  background-image:
    repeating-linear-gradient(135deg,
      rgba(0, 0, 0, 0.28) 0 3px,
      transparent 3px 7px),
    linear-gradient(180deg, var(--activity-lunch), var(--activity-lunch-dark));
}


.bar-day {
  height: var(--bar-label-height);
  display: grid;
  place-items: center;
  font-size: 11px;
  color: var(--text-muted);
}

.bar-column.today .bar-day {
  color: var(--primary-emerald);
  font-weight: 700;
}

.bar-column.future {
  opacity: 0.4;
}

.activity-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  font-size: 11px;
  color: var(--text-secondary);
}

.swatch {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  flex-shrink: 0;
}

.swatch-work    { background: var(--activity-work); }
.swatch-meeting { background: var(--activity-meeting); }
.swatch-break   { background: var(--activity-break); }
.swatch-lunch   { background: var(--activity-lunch); }

/* Must follow the `background` shorthands above — the shorthand resets
   background-image, so declaring the hatch earlier silently lost it. The
   legend has to carry the same solid-vs-hatched distinction as the bars or
   the key does not explain the chart. */
.swatch-break,
.swatch-lunch {
  background-image: repeating-linear-gradient(135deg,
    rgba(0, 0, 0, 0.4) 0 2px, transparent 2px 4px);
}

.chart-note {
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* ---------------------------------------------------------- summary grid */

.summary-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--border-subtle);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.summary-grid.three {
  grid-template-columns: repeat(3, 1fr);
}

.summary-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 10px 4px;
  background: var(--bg-surface);
  text-align: center;
}

.summary-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
}

.summary-value {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.summary-value.positive { color: var(--primary-emerald); }
.summary-value.negative { color: var(--amber-warning); }

/* ----------------------------------------------------------------- calendar */

.calendar-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  font-size: 11px;
  color: var(--text-secondary);
}

.legend-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

/* Legend sample for a worked day. Matches the figure in the grid, so the key
   shows the thing it is describing rather than a dot that no longer appears
   on days with a record. */
.legend-hours {
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.legend-hours.met   { color: var(--primary-emerald); }
.legend-hours.short { color: var(--amber-warning); }

.dot-completed { background: var(--primary-emerald); box-shadow: 0 0 6px var(--primary-glow); }
.dot-partial   { background: var(--amber-warning); }
.dot-off       { background: var(--slate-muted); opacity: 0.45; }
.dot-weekend   { background: transparent; border: 1px solid var(--slate-muted); }
.dot-future    { background: transparent; }

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

.weekday-header {
  text-align: center;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-muted);
  padding-bottom: 2px;
}

.calendar-days-container {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}

.cal-day-cell {
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background var(--motion-fast) ease, transform var(--motion-fast) var(--ease-out);
}

/* Staggered reveal when the month is rebuilt. The delay is set inline by
   ui.js as --i (the cell's index) and capped there, so a 31-day month
   finishes in ~240ms rather than trickling in. Opacity + transform only. */
.calendar-days-container.animate .cal-day-cell:not(.empty) {
  animation: cell-in 260ms var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 7ms);
}

@keyframes cell-in {
  from { opacity: 0; transform: scale(0.86); }
  to   { opacity: 1; transform: scale(1); }
}

.cal-day-cell:not(.empty):not(.future):active {
  transform: scale(0.92);
}

.cal-day-cell:active {
  background: var(--bg-surface-hover);
}

.cal-day-cell:focus-visible {
  outline: 2px solid var(--primary-emerald);
  outline-offset: 1px;
}

.cal-day-cell.empty {
  background: none;
  cursor: default;
  pointer-events: none;
}

.cal-day-cell.today {
  border-color: var(--border-accent);
  background: rgba(16, 185, 129, 0.08);
}

.cal-day-cell.weekend .cal-day-num { color: var(--text-muted); }

/* Days that have not happened yet are not selectable: there is nothing to see
   and nothing sensible to log against them. */
.cal-day-cell.future {
  opacity: 0.32;
  background: none;
  cursor: default;
  pointer-events: none;
}

.cal-day-num {
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.cal-status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}

/* Sits where the dot would have been on a day that was worked. Deliberately
   smaller and lighter than the date above it: the month grid is read by date
   first, and hours are the answer to a question you ask about one cell at a
   time. */
.cal-day-hours {
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.2px;
}

.cal-day-hours.met   { color: var(--primary-emerald); }
.cal-day-hours.short { color: var(--amber-warning); }

/* ------------------------------------------------------------- bottom nav */

.bottom-nav {
  position: fixed;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: 520px;
  z-index: 45;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* The extra bottom padding is the iPhone home indicator: without it the
     nav sits underneath the gesture bar and the last item is unreachable. */
  padding: 8px 6px calc(8px + var(--safe-bottom));
  background: rgba(18, 18, 18, 0.92);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-top: 1px solid var(--border-subtle);
}

.nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 6px 2px;
  min-height: 46px;
  background: none;
  border: none;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  transition: color var(--motion-fast) ease;
}

.nav-item svg {
  transition: transform var(--motion-base) var(--ease-out);
}

.nav-item.active {
  color: var(--primary-emerald);
}

/* Small lift on the selected tab's icon. One transform on one 19px element. */
.nav-item.active svg {
  transform: translateY(-1px) scale(1.08);
}

.nav-item:active svg {
  transform: scale(0.9);
}

/* ----------------------------------------------------------------- modals */

.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 16px;
  padding-bottom: calc(16px + var(--safe-bottom));
  background: rgba(0, 0, 0, 0.65);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  animation: fade-in 0.18s ease;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

.modal-card {
  width: 100%;
  max-width: 480px;
  max-height: 88vh;
  max-height: 88dvh;
  padding: 18px 16px;
  overflow-y: auto;
  overscroll-behavior: contain;
  animation: slide-up 0.22s cubic-bezier(0.2, 0.9, 0.3, 1);
}

@keyframes slide-up {
  from { transform: translateY(18px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}

/* Settings is a destination rather than a glance, so on a phone it takes the
   whole screen: a 88vh card floating on a dimmed backdrop wasted the edges and
   still needed a scroll. Above 560px the shared rule re-centres it and the
   width cap below turns it back into a normal card. */
.modal-overlay.fullscreen {
  padding: 0;
  align-items: stretch;
  /* The sheet covers the overlay completely, so blurring what is behind it is
     a full-screen GPU pass nobody can see. */
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Opaque rather than the usual translucent glass: nothing shows through a
   full-screen sheet anyway, and the sticky header below has to paint the same
   colour as the panel it sits on. */
.modal-card.sheet-full {
  max-width: none;
  max-height: none;
  height: 100%;
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  background: var(--bg-surface);
  box-shadow: none;
  display: flex;
  flex-direction: column;
  padding: calc(14px + var(--safe-top)) 16px calc(18px + var(--safe-bottom));
}

/* The title stays put while the groups scroll: with six groups the close
   button was otherwise several screens up. */
.sheet-full .modal-header {
  position: sticky;
  top: calc(-14px - var(--safe-top));
  z-index: 1;
  margin: 0 -16px 12px;
  padding: 0 16px 12px;
  background: var(--bg-surface);
  border-bottom: 1px solid var(--border-subtle);
}

.sheet-full .settings-body {
  flex: 1;
}

/* Scoped mode. The header bell opens settings showing reminders alone, because
   a bell that lands on six collapsed groups makes the user hunt for the one it
   was named after. app.js sets data-only to the group it wants. */
.settings-body[data-only] .settings-group {
  display: none;
}

.settings-body[data-only="reminders"] [data-group="reminders"],
.settings-body[data-only="workday"] [data-group="workday"],
.settings-body[data-only="profile"] [data-group="profile"],
.settings-body[data-only="account"] [data-group="account"],
.settings-body[data-only="data"] [data-group="data"],
.settings-body[data-only="install"] [data-group="install"] {
  display: block;
}

/* Alone on the screen, the group is the screen: its header would just repeat
   the modal title, and there is nothing to collapse it in favour of. */
.settings-body[data-only] .settings-group {
  border: 0;
  background: none;
}

.settings-body[data-only] .settings-group-header {
  display: none;
}

.settings-body[data-only] .settings-group-body {
  padding: 0;
}

/* A way through to the rest, so the scoped sheet is not a dead end. app.js
   unhides it only while data-only is set. */
.settings-all-link {
  margin-top: 14px;
  width: 100%;
}

/* Back to a normal centred card once there is room for one. */
@media (min-width: 560px) {
  .modal-overlay.fullscreen {
    padding: 16px;
    padding-bottom: calc(16px + var(--safe-bottom));
    align-items: center;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
  }

  .modal-card.sheet-full {
    max-width: 480px;
    max-height: 88dvh;
    height: auto;
    border-radius: var(--radius-lg);
    border-left: 1px solid var(--border-subtle);
    border-right: 1px solid var(--border-subtle);
    box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.04);
    padding: 18px 16px;
  }

  /* Stays opaque here too: the sticky header has to paint the same colour as
     the panel, and a translucent one would show the groups sliding under it. */
  .sheet-full .modal-header {
    top: -18px;
  }
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-bottom: 14px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--border-subtle);
}

.modal-header h3 {
  font-size: 16px;
  font-weight: 700;
}

.close-btn {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  background: none;
  border: none;
  border-radius: var(--radius-full);
  color: var(--text-muted);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}

.close-btn:active {
  background: var(--bg-surface-hover);
}

/* ------------------------------------------------------------------ forms */

.form-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 14px;
}

.form-group label,
.setting-row label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
}

.form-hint {
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.45;
  margin-bottom: 14px;
}

.label-hint {
  font-weight: 500;
  color: var(--text-muted);
  text-transform: none;
}

/* Every text-ish input, matched by exclusion rather than by listing types.
   The previous rule named three types, so `email` fell through to the browser
   default and rendered as a white box on the sign-in screen. Anything added
   later — url, search, password — is covered without touching this again. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"]) {
  width: 100%;
  min-height: 44px;
  padding: 0 12px;
  background: var(--bg-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font: inherit;
  font-size: 15px; /* 16px-ish keeps iOS from zooming the page on focus */
  -webkit-appearance: none;
  appearance: none;
}

input::placeholder {
  color: var(--text-muted);
  opacity: 1;   /* Firefox dims placeholders further by default */
}

/* Chrome paints its own pale-blue fill over autofilled inputs, which on a dark
   form looks like a rendering fault. There is no property to turn that off;
   an inset shadow the size of the field is the standard way to cover it. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text-primary);
  -webkit-box-shadow: 0 0 0 1000px var(--bg-primary) inset;
  caret-color: var(--text-primary);
  transition: background-color 9999s ease-in-out 0s;
}

input:focus {
  outline: none;
  border-color: var(--border-accent);
  box-shadow: 0 0 0 3px var(--primary-glow);
}

.input-pair {
  display: flex;
  gap: 8px;
}

.input-with-unit {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 1;
}

.input-with-unit input {
  width: 100%;
  min-width: 60px;
  text-align: center;
  font-family: var(--font-mono);
}

.input-with-unit span {
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

.modal-actions {
  display: flex;
  gap: 9px;
  margin-top: 4px;
}

.modal-actions .btn {
  flex: 1;
}

.modal-actions.spread {
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
}

.modal-actions.spread .btn {
  flex: 0 0 auto;
}

.modal-actions-right {
  display: flex;
  gap: 9px;
}

/* ------------------------------------------------------------ day details */

.detail-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 0;
  border-bottom: 1px solid var(--border-subtle);
  font-size: 13px;
}

.detail-label {
  color: var(--text-secondary);
}

.detail-val {
  font-family: var(--font-mono);
  font-weight: 600;
}

.detail-val.accent   { color: var(--primary-emerald); }
.detail-val.positive { color: var(--primary-emerald); }
.detail-val.negative { color: var(--amber-warning); }

.empty-note {
  padding: 16px 0;
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
  line-height: 1.5;
}

.day-note {
  margin-top: 12px;
  padding: 10px 12px;
  background: rgba(255, 255, 255, 0.035);
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  color: var(--text-secondary);
}

.timeline-heading {
  margin: 18px 0 8px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  color: var(--text-muted);
}

.timeline-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.timeline-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.03);
  border-left: 3px solid var(--slate-muted);
  font-size: 12px;
}

.timeline-row.seg-working { border-left-color: var(--activity-work); }
.timeline-row.seg-meeting { border-left-color: var(--activity-meeting); }
.timeline-row.seg-break   { border-left-color: var(--activity-break); }
.timeline-row.seg-lunch   { border-left-color: var(--activity-lunch); }
.timeline-row.seg-paused  { border-left-color: var(--activity-paused); }

.timeline-time {
  font-family: var(--font-mono);
  color: var(--text-secondary);
}

.timeline-state {
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.timeline-dur {
  font-family: var(--font-mono);
  font-weight: 600;
}

/* --------------------------------------------------------------- settings */

.settings-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* Settings are grouped into collapsible cards rather than one long scroll.
   Each header carries a summary of its own current values, so the common case
   — "what is my break reminder set to?" — is answered without opening
   anything. Only one group is open at a time; app.js enforces that. */
.settings-group {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.022);
  overflow: hidden;
}

.settings-group-header {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 13px 14px;
  background: none;
  border: none;
  color: var(--text-primary);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.settings-group-icon {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  background: rgba(16, 185, 129, 0.12);
  color: var(--primary-emerald);
}

.settings-group-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.settings-group-title {
  font-size: 14px;
  font-weight: 650;
}

.settings-group-summary {
  font-size: 11.5px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.settings-group-chevron {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform var(--motion-base) var(--ease-out);
}

.settings-group.open .settings-group-chevron {
  transform: rotate(180deg);
}

/* Height is not animated: `height: auto` cannot be transitioned without either
   measuring in JS every open or animating a layout property 60 times a second.
   The panel appears immediately and its contents fade up, which reads as
   motion while costing one compositor pass. */
.settings-group-body {
  padding: 0 14px 14px;
}

.settings-group:not(.open) .settings-group-body {
  display: none;
}

.settings-group.open .settings-group-body {
  animation: view-in var(--motion-base) var(--ease-out) both;
}

/* A labelled block inside a group: "Short break" and "Lunch" each become one
   of these, so four loose number rows read as two settings with two fields. */
.setting-block {
  padding: 11px 0;
  border-top: 1px solid var(--border-subtle);
}

.setting-block:first-child {
  border-top: none;
  padding-top: 4px;
}

.setting-block-title {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  font-weight: 650;
  margin-bottom: 2px;
}

.setting-block-title .swatch {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.setting-block-note {
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
  margin-bottom: 6px;
}

.settings-section h4 {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.7px;
  color: var(--primary-emerald);
  margin-bottom: 10px;
}

.settings-note {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
  margin-bottom: 10px;
}

.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
}

.setting-row .input-with-unit {
  flex: 0 0 128px;
}

.switch-row {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  padding: 11px 0;
  border-top: 1px solid var(--border-subtle);
  cursor: pointer;
}

.switch-row input[type="checkbox"] {
  width: 20px;
  height: 20px;
  margin-top: 2px;
  accent-color: var(--primary-emerald);
  flex-shrink: 0;
}

.switch-row strong {
  display: block;
  font-size: 13px;
  font-weight: 600;
}

.switch-row small {
  display: block;
  margin-top: 3px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
}

.inline-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 10px;
}

/* ------------------------------------------------ notification permission */

/* The state of notifications is the one thing in settings that can be broken
   from outside the app, so it gets a card of its own rather than a sentence.
   Three tones: on, not asked yet, blocked. */
.perm-card {
  padding: 12px 13px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border-subtle);
  background: rgba(255, 255, 255, 0.028);
  margin-bottom: 12px;
}

.perm-card.granted {
  background: rgba(16, 185, 129, 0.08);
  border-color: var(--border-accent);
}

.perm-card.denied {
  background: rgba(245, 158, 11, 0.08);
  border-color: rgba(245, 158, 11, 0.35);
}

.perm-head {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 5px;
}

.perm-status-dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex-shrink: 0;
  background: var(--slate-muted);
}

.perm-card.granted .perm-status-dot {
  background: var(--primary-emerald);
  box-shadow: 0 0 7px var(--primary-glow);
}

.perm-card.denied .perm-status-dot {
  background: var(--amber-warning);
}

.perm-title {
  font-size: 13.5px;
  font-weight: 650;
}

.perm-body {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* The step list only appears for the blocked state. There is no web API that
   can open browser or OS notification settings, so the honest fix is exact
   instructions for the platform the user is actually on plus a permission
   watcher that heals the card the moment they come back. */
.perm-steps {
  margin: 10px 0 0;
  padding: 11px 12px 11px 28px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.28);
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
}

.perm-steps li + li {
  margin-top: 4px;
}

.perm-steps strong {
  color: var(--text-primary);
  font-weight: 650;
}

.perm-site {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 9px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--bg-primary);
  border: 1px solid var(--border-subtle);
}

.perm-site code {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- sign in */

/* Sits above everything, including the nav, because until it is answered
   there is nothing behind it worth reaching. */
.signin-screen {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 18px calc(24px + var(--safe-bottom));
  background: var(--bg-primary);
  overflow-y: auto;
}

.signin-card {
  width: 100%;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  animation: view-in var(--motion-base) var(--ease-out) both;
}

/* The sign-in fields are the first thing anyone sees, so they get more room
   than the dense settings rows this stylesheet is otherwise tuned for. */
.signin-card .form-group {
  margin-bottom: 12px;
}

.signin-card .form-group label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-muted);
}

.signin-card input {
  min-height: 50px;
  padding: 0 14px;
  font-size: 16px;   /* below 16px iOS zooms the page on focus */
  background: var(--bg-surface);
  border-color: rgba(255, 255, 255, 0.12);
}

.signin-card input:focus {
  background: var(--bg-surface-elevated);
}

.signin-card .btn.full {
  margin-top: 6px;
  min-height: 50px;
  font-size: 15px;
}

.signin-logo {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin: 0 auto 14px;
  border-radius: var(--radius-lg);
  background: linear-gradient(135deg, var(--primary-emerald), var(--primary-emerald-dark));
  color: #06231a;
}

.signin-title {
  font-size: 22px;
  font-weight: 700;
  text-align: center;
  letter-spacing: -0.3px;
  margin-bottom: 8px;
}

.signin-sub {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-secondary);
  text-align: center;
  margin-bottom: 20px;
}

.signin-note {
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
  text-align: center;
  margin-top: 12px;
}

/* The code is read back digit by digit off a notification, so it gets room. */
#signinCode {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 24px;
  letter-spacing: 8px;
  padding: 0;
}

.signin-links {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-top: 14px;
}

.btn-link {
  background: none;
  border: none;
  padding: 4px 2px;
  color: var(--primary-emerald);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.btn-link:disabled {
  color: var(--text-muted);
  cursor: not-allowed;
  text-decoration: none;
}

.signin-error {
  margin-top: 14px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: rgba(239, 68, 68, 0.12);
  border: 1px solid rgba(239, 68, 68, 0.4);
  color: #fca5a5;
  font-size: 12.5px;
  line-height: 1.45;
  text-align: center;
}

/* ------------------------------------------------------------------ toast */

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--nav-height) + var(--safe-bottom) + 14px);
  transform: translate(-50%, 12px);
  z-index: 120;
  max-width: min(440px, calc(100vw - 28px));
  padding: 11px 15px;
  border-radius: var(--radius-md);
  background: var(--bg-surface-elevated);
  border: 1px solid var(--border-subtle);
  box-shadow: var(--shadow-lg);
  font-size: 12.5px;
  line-height: 1.4;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.22s ease, transform 0.22s ease;
}

.toast.show {
  opacity: 1;
  transform: translate(-50%, 0);
}

.toast.ok   { border-color: var(--border-accent); color: var(--primary-emerald-light); }
.toast.warn { border-color: rgba(245, 158, 11, 0.45); color: #fcd9a0; }

/* ----------------------------------------------------------- wider screens */

@media (min-width: 560px) {
  .app-content {
    padding: 18px 18px calc(var(--nav-height) + var(--safe-bottom) + 24px);
  }

  .modal-overlay {
    align-items: center;
  }

  .timer-digits {
    font-size: 38px;
  }
}

/* Short screens: a small phone, or any phone with the browser's address bar
   still showing. A running break is the tallest the timer ever gets — it adds
   the reminder banner — and that is exactly when the last button must stay
   reachable without scrolling. The clock-in and clock-out times are the one
   thing on the card that is static and repeated in the day details, so they
   are what gives way. */
@media (max-height: 720px) {
  [data-state="BREAK"] .clock-row,
  [data-state="LUNCH"] .clock-row {
    display: none;
  }
}

/* Very short: an older small phone, or a tall one whose keyboard or address
   bar is eating the viewport. Past this point the ring is already at its floor,
   so the remaining height has to come from chrome rather than content. Nothing
   here removes a number the user cannot get elsewhere: the caption above the
   digits is decorative, and clock-in and clock-out live in the day details. */
@media (max-height: 620px) {
  /* "On the clock" is decorative and goes. The activity captions stay: they
     are what tells you whether the digits are the day's total or the break you
     are currently on, and there is no room for that to be a guess. */
  [data-state="IDLE"] .timer-label,
  [data-state="WORKING"] .timer-label,
  [data-state="ENDED"] .timer-label {
    display: none;
  }

  .clock-row {
    display: none;
  }

  /* Label beside the value instead of above it: same four figures, one row
     shorter. */
  .stat-item {
    flex-direction: row;
    justify-content: center;
    align-items: baseline;
    gap: 5px;
    padding: 7px 2px;
  }

  .stat-label {
    font-size: 10px;
  }

  .stat-value {
    font-size: 13px;
  }

  /* During a break the banner is what the user came to look at — how long they
     have been away and when the next nudge lands. The four running totals are
     a glance-at-the-end-of-day number, and they are still on the week and
     calendar screens, so on a screen this short they yield to the banner. */
  [data-state="BREAK"] .session-stats-bar,
  [data-state="LUNCH"] .session-stats-bar {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
