/* Shared components used on more than one screen: feedback, cards, tables, forms,
   buttons, tags and pills, dialogs, the date picker, toggles and charts. */

/* ============================================================================
   Feedback.

   A change confirms itself on the element that changed; a failure sits next to
   what failed and stays there. Neither one floats over the page, which is what
   the previous toast did — on a 375px screen its 280px minimum, parked below the
   navbar, covered the header controls and the first card to report that
   something had been saved.
   ============================================================================ */

/* Success: a ring blooms out of the element's own edge and clears. The tint
   carries the meaning; the bloom is what catches the eye that was elsewhere. */
@keyframes flashSuccessRing {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent), var(--shadow-md); }
  22%  { box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 28%, transparent), 0 0 22px 2px color-mix(in srgb, var(--accent) 32%, transparent), var(--shadow-md); }
  100% { box-shadow: 0 0 0 0 transparent, 0 0 0 0 transparent, var(--shadow-md); }
}

@keyframes flashSuccessTint {
  0%, 100% { background-color: var(--flash-ground); }
  25%      { background-color: var(--tint-green-bg); }
}

.flash-success {
  /* The element being flashed may be a card, a table row or a panel, each with a
     different resting ground, so the tint returns to whatever it already had. */
  --flash-ground: var(--bg-card);
  border-color: var(--tint-green-border) !important;
  animation: flashSuccessRing 1.5s cubic-bezier(0.16, 1, 0.3, 1) forwards,
             flashSuccessTint 1.5s ease forwards;
}

/* A table row's wash has to be painted on its cells, not on the row.
   A striped table sets background-color on the td, so a background animated
   on the tr sits behind opaque cells and is never seen — which is why confirming a
   change on a row appeared to do nothing at all.

   An inset shadow rather than a background: it covers the cell whatever stripe it is
   on, without having to know which, and it leaves background-color alone so the row
   returns to its own stripe rather than to a guess about it. */
@keyframes flashRowWash {
  0%, 100% { box-shadow: inset 0 0 0 9999px transparent; }
  25%      { box-shadow: inset 0 0 0 9999px var(--flash-row-green); }
}

/* A button that resolves in place, for an action with no element of its own. */
.btn-resolved {
  background: var(--tint-green-bg) !important;
  border-color: var(--tint-green-border) !important;
  color: var(--tint-green-fg) !important;
  pointer-events: none;
}

.btn-resolved-tick {
  width: 13px;
  height: 13px;
  display: block;
}

/* Error: a nudge and a ring that stay, rather than a bloom that clears. */
@keyframes flashErrorNudge {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-4px); }
  45% { transform: translateX(3px); }
  70% { transform: translateX(-2px); }
}

.flash-error {
  border-color: var(--tint-red-border) !important;
  box-shadow: 0 0 0 3px var(--tint-red-bg), var(--shadow-md);
  animation: flashErrorNudge 0.34s ease;
}

/* The panel itself: same shape as .load-error-banner, which does this job for a
   whole view, scaled down to sit inside one card or modal. */
.action-error {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  margin-top: 0.85rem;
  padding: 0.7rem 0.8rem;
  background: var(--tint-red-bg);
  border: 1px solid var(--tint-red-border);
  border-radius: var(--radius-md);
  animation: modalPop 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}

.action-error-icon {
  color: var(--accent-red);
  flex-shrink: 0;
  margin-top: 1px;
}

.action-error-icon svg,
.alert-bar-icon svg {
  width: 15px;
  height: 15px;
  display: block;
}

.action-error-body {
  min-width: 0;
  flex: 1;
}

.action-error-title {
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--tint-red-fg);
}

.action-error-detail {
  font-size: var(--fs-meta);
  color: var(--text-secondary);
  margin-top: 0.15rem;
  line-height: 1.45;
  /* A server message can be one long unbroken token — a URL, an ID — and a
     message clipped at the card's edge is worse than no message. */
  overflow-wrap: anywhere;
}

.action-error-actions,
.alert-bar-actions {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  margin-top: 0.5rem;
}

.action-error-retry,
.action-error-dismiss,
.alert-bar-retry {
  font: inherit;
  font-size: var(--fs-label);
  font-weight: 600;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--accent-blue);
}

.action-error-dismiss {
  color: var(--text-muted);
}

.action-error-retry:focus-visible,
.action-error-dismiss:focus-visible,
.alert-bar-retry:focus-visible,
.alert-bar-dismiss:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* The alert bar: the last resort, for a failure with nothing on screen to attach
   to. Bottom-anchored so it clears the header on a phone, and it sits above the
   mobile tab bar rather than under it. */
#alert-bar-host {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 1.25rem;
  z-index: 99999;
  width: min(560px, calc(100vw - 2rem));
  pointer-events: none;
}

.alert-bar {
  pointer-events: auto;
  display: flex;
  gap: 0.7rem;
  align-items: flex-start;
  padding: 0.8rem 0.9rem;
  border-radius: var(--radius-md);
  background: var(--bg-frost-strong);
  backdrop-filter: blur(25px) saturate(180%);
  -webkit-backdrop-filter: blur(25px) saturate(180%);
  box-shadow: var(--shadow-lg);
  animation: alertBarRise 0.28s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Only failures reach the bar. "Signed out" belongs on the sign-in card, not at the
   bottom of a page the reader has just been thrown off. */
.alert-bar {
  border: 1px solid var(--tint-red-border);
  border-left: 3px solid var(--accent-red);
}

.alert-bar-icon {
  color: var(--accent-red);
  flex-shrink: 0;
  margin-top: 2px;
}

.alert-bar-body {
  flex: 1;
  min-width: 0;
}

.alert-bar-title {
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--text-primary);
}

.alert-bar-detail {
  font-size: var(--fs-meta);
  color: var(--text-secondary);
  line-height: 1.45;
  margin-top: 0.1rem;
  overflow-wrap: anywhere;
}

.alert-bar-actions {
  margin-top: 0;
  flex-shrink: 0;
  align-items: flex-start;
}

.alert-bar-dismiss {
  font: inherit;
  background: none;
  border: 0;
  padding: 0.15rem;
  cursor: pointer;
  color: var(--text-muted);
  display: flex;
}

.alert-bar-dismiss svg {
  width: 14px;
  height: 14px;
  display: block;
}

@keyframes alertBarRise {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Motion is the attention-getter in every one of these; without it the states
   still have to read, so each falls back to its resting colour. */
@media (prefers-reduced-motion: reduce) {
  .flash-success {
    animation: none;
    background-color: var(--tint-green-bg);
  }
  .flash-error { animation: none; }
  .action-error, .alert-bar { animation: none; }
}

.status-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background-color: var(--accent-green);
  display: inline-block;
}

.status-dot.warning {
  background-color: var(--accent-amber);
}

.status-dot.danger {
  background-color: var(--accent-red);
}

/* Named explicitly rather than left to the element default, so that every caller
   states the tone it means and an unstyled dot is a visible mistake. */
.status-dot.green {
  background-color: var(--accent-green);
}

/* A settled user choice: nothing is wrong and nothing needs doing, so this one
   drops the glow the live-connection dots carry rather than merely recolouring it. */
.status-dot.muted {
  background-color: var(--text-muted);
  box-shadow: none;
}

/* A brief ring on whatever a "review this" link just scrolled to, so the eye lands on the
   right block rather than somewhere near it. Fades on its own — this marks an arrival, not
   a state, and a highlight that stayed would read as a warning. */
.section-revealed {
  animation: sectionReveal 1.8s ease-out;
}

.section-revealed:focus {
  outline: none;
}

@keyframes sectionReveal {
  0%, 35% {
    box-shadow: 0 0 0 3px var(--accent-blue);
  }
  100% {
    box-shadow: 0 0 0 3px transparent;
  }
}

@media (prefers-reduced-motion: reduce) {
  .section-revealed {
    animation: none;
    box-shadow: 0 0 0 3px var(--accent-blue);
  }
}

/* The one card: surface, a hairline, the 16px radius and the card elevation. A card is
   not a control, so it does not lift on hover. */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  position: relative;
  overflow: hidden;
  box-shadow: var(--shadow-card);
}

.metric-title {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Card and chart titles read as names, in sentence case; the small capitals stay
   for eyebrow labels that sit above a heading. */
.metric-title.title-sentence {
  font-family: var(--font-text);
  font-size: var(--fs-body-sm);
  font-weight: 600;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-secondary);
}

.metric-subtext {
  font-size: var(--fs-body-sm);
  color: var(--text-muted);
}

/* Tables & Lists (Strong Visual Contrast & Structure) */
.table-container {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  margin-bottom: 2rem;
}

.table-header-bar {
  padding: 1.125rem 1.5rem;
  background: var(--bg-inset);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.table-title {
  font-family: var(--font-display);
  font-size: var(--fs-body);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

.acc-row-link {
  color: var(--text-primary);
  font-weight: 500;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  transition: color 0.15s ease;
}

.acc-row-link:hover {
  color: var(--accent-blue);
  text-decoration: underline;
}

/* Filter controls */

.form-input {
  background: var(--bg-card);
  color: var(--text-primary);
  border: 1px solid var(--border-strong);
  padding: 0.4rem 0.8rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-meta);
  font-weight: 500;
  font-family: inherit;
  outline: none;
  transition: all 0.2s ease;
}

.form-input:focus {
  border-color: var(--accent-blue);
  box-shadow: 0 0 0 3px var(--border-focus);
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  /* A button that wraps grows taller than the row holding it. In the navbar that row
     is a fixed 3.75rem, so a label breaking across two lines pushed the control out
     of the bar rather than making the bar taller. */
  white-space: nowrap;
  font-weight: 500;
  font-size: var(--fs-meta);
  min-height: 2.25rem;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background-color 0.18s ease-out, border-color 0.18s ease-out, color 0.18s ease-out;
  text-decoration: none;
  font-family: inherit;
}

/* The four kinds of button. Primary is ink: one per view, the thing to do next.
   Secondary is an outline for the other actions, quiet is text for the least, and
   destructive is red because it cannot be taken back. */
.btn-primary {
  background: var(--ink);
  color: var(--text-on-accent);
  font-weight: 600;
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent-fill-hover);
}

.btn-secondary {
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-strong);
}

.btn-secondary:hover:not(:disabled) {
  background: var(--sunk);
}

.btn-quiet {
  background: transparent;
  color: var(--ink-2);
}

.btn-quiet:hover:not(:disabled) {
  background: var(--sunk);
  color: var(--ink);
}

/* The solid red of a destructive primary action, off its own token — the confirm
   dialog is where this belongs, since a dialog's primary button is the one that does
   the irreversible thing and should not look like the one that backs out. */
.btn-danger {
  background: var(--solid-danger-bg);
  color: var(--text-on-accent);
  font-weight: 600;
}

.btn-danger:hover:not(:disabled) {
  background: color-mix(in srgb, var(--crit) 88%, var(--ink));
}

/* Shown in place of a page's content when it fails to load, so a broken fetch reads as
   "something went wrong" rather than as "you have no data" — the two look identical once
   a page silently falls back to empty state, and only one of them is true. Icon and
   title/detail on the left, an action on the right, in red rather than amber: amber
   means "act on this when you get a chance", red means "the page you're looking at
   right now didn't load". */
.load-error-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.85rem;
  padding: 1rem 1.25rem;
  background: var(--tint-red-bg);
  border: 1px solid var(--tint-red-border);
  border-radius: var(--radius-md);
  animation: modalPop 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  margin-bottom: 1.5rem;
}

.load-error-row {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.load-error-icon {
  font-size: 1.25rem;
  line-height: 1;
  flex-shrink: 0;
}

.load-error-body {
  flex: 1;
}

.load-error-title {
  font-size: var(--fs-body-sm);
  font-weight: 600;
  color: var(--accent-red);
  margin-bottom: 0.2rem;
}

.load-error-detail {
  font-size: var(--fs-meta);
  color: var(--tint-red-fg);
  line-height: 1.45;
}

/* An icon inside a button. Sized in em so it tracks the label rather than needing a
   second rule per button size, and shrink-proofed so a long label can't squash it. */
.btn-icon {
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  display: block;
}

.btn-sm {
  min-height: 1.875rem;
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-label);
}

/* Loading & Disabled Button States */
.btn:disabled, .btn.loading {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
  transform: none !important;
}

/* Waiting for the server (whileWaiting in uikit.js): the control greys out and takes no
   press until the answer is in. The grey waits a beat, so an answer that comes at once
   doesn't flash it. A switch's own input is invisible, so its slider greys instead,
   keeping the slide it has. */
:is(button, select, input:not(.apple-toggle-switch input))[aria-busy="true"] {
  cursor: progress;
  opacity: 0.5;
  transition: opacity 150ms ease 100ms;
}

.apple-toggle-switch input[aria-busy="true"] + .apple-toggle-slider {
  opacity: 0.5;
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 150ms ease 100ms;
}

.apple-toggle-switch:has(input[aria-busy="true"]),
.apple-toggle-switch input[aria-busy="true"] {
  cursor: progress;
}

.btn-spinner {
  width: 12px;
  height: 12px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--text-primary);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  display: inline-block;
}

.btn-primary .btn-spinner {
  border: 2px solid color-mix(in srgb, var(--text-on-accent) 30%, transparent);
  border-top-color: var(--text-on-accent);
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Pull to refresh (js/pullrefresh.js). The disc waits, unseen, just above the navbar's
   lower edge and comes down with the finger by --pull-y, passing beneath the navbar. Past
   the point where letting go refreshes its arrow turns over, and while the screen
   reloads it is a spinner. */
.pull-indicator {
  --pull-y: 0px;
  position: fixed;
  top: calc(3.75rem - 36px);
  left: calc(50% - 18px);
  z-index: 45;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  box-sizing: border-box;
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  background: var(--surface);
  box-shadow: var(--shadow-sheet);
  color: var(--ink-3);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateY(var(--pull-y));
  transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease-out), visibility 0s linear var(--dur);
}

.pull-indicator.is-pulling,
.pull-indicator.is-refreshing {
  opacity: 1;
  visibility: visible;
  transition: transform var(--dur) var(--ease-out), opacity var(--dur) var(--ease-out);
}

/* Under the finger it goes where the finger goes, at once. */
.pull-indicator.is-pulling {
  transition: none;
}

.pull-indicator.is-armed {
  color: var(--accent);
}

.pull-arrow {
  transition: transform var(--dur) var(--ease-out);
}

.pull-indicator.is-armed .pull-arrow {
  transform: rotate(180deg);
}

.pull-indicator.is-refreshing .pull-arrow {
  display: none;
}

.pull-indicator.is-refreshing::after {
  content: '';
  width: 16px;
  height: 16px;
  box-sizing: border-box;
  border: 2px solid var(--line-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

/* Still: the disc and its arrow change place at once, and a whole ring in the accent
   stands for the spinner. */
@media (prefers-reduced-motion: reduce) {
  .pull-indicator,
  .pull-indicator.is-pulling,
  .pull-indicator.is-refreshing,
  .pull-arrow {
    transition: none;
  }

  .pull-indicator.is-refreshing::after {
    border-color: var(--accent);
    animation: none;
  }
}

/* Modal Overlay */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--bg-scrim);
  backdrop-filter: blur(15px);
  -webkit-backdrop-filter: blur(15px);
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal-backdrop.active {
  display: flex;
}

/* The scrim fades in under the dialog's pop, and on closing the dialog leaves the way it
   came: the scrim fades and the card sinks back (a phone's sheet drops) rather than
   vanishing. Closing only takes .active off; display waits out the fade, and a leaving
   dialog takes no presses, so what was behind it answers at once. Reopened mid-fade, it
   turns straight back. scale and translate leave the pop's transform to its keyframes. */
.modal-backdrop {
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--fade-dur) var(--ease-fade), display var(--fade-dur) allow-discrete;
}

.modal-backdrop.active {
  opacity: 1;
  pointer-events: auto;
}

@starting-style {
  .modal-backdrop.active {
    opacity: 0;
  }
}

.modal-backdrop .modal {
  transition: scale var(--dur) var(--ease-out), translate var(--dur) var(--ease-out);
}

.modal-backdrop:not(.active) .modal {
  scale: 0.96;
}

.modal {
  background: var(--bg-frost-strong);
  backdrop-filter: blur(30px);
  -webkit-backdrop-filter: blur(30px);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 500px;
  padding: 2rem;
  box-shadow: var(--shadow-lg);
  animation: modalPop 0.25s cubic-bezier(0.16, 1, 0.3, 1);
  /* Without this a modal grows past the viewport and takes its own footer with it —
     Save and Close end up below the fold with no way to scroll to them. The account
     inspector already had enough fields to come close; adding the merge controls pushed
     it over on a 1280x800 screen. Capped and scrollable, so what a modal contains can
     never make its own buttons unreachable. */
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
  /* Belt and braces alongside the page lock: a wheel that reaches the end of this dialog
     stops there rather than passing the rest on to whatever is behind it. */
  overscroll-behavior: contain;
}

/* The confirmation dialog. It borrows .modal-backdrop and .modal wholesale so it is
   the same object as the inspectors, and only changes what a question needs: narrower
   than a panel of fields, an icon that says which kind of question this is, and a
   footer whose two buttons are visibly different weights.

   hidden rather than a class here, so the dialog is inert to assistive technology
   between uses; .modal-backdrop.active only sets display. */
.confirm-backdrop {
  z-index: 200;
}

.confirm-backdrop[hidden] {
  display: none;
}

.confirm-modal {
  max-width: 27rem;
  padding: 1.75rem;
}

.confirm-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.85rem;
}

.confirm-icon {
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-md);
  flex-shrink: 0;
  display: grid;
  place-items: center;
  background: var(--confirm-tint-bg);
  border: 1px solid var(--confirm-tint-border);
  color: var(--confirm-tint-fg);
}

.confirm-icon svg {
  width: 1.125rem;
  height: 1.125rem;
  display: block;
}

/* Danger is the default: almost everything worth confirming is something you cannot
   take back. A neutral question — one that only needs a decision, not a warning —
   asks in the app's own blue instead. */
.confirm-backdrop {
  --confirm-tint-bg: var(--tint-red-bg);
  --confirm-tint-border: var(--tint-red-border);
  --confirm-tint-fg: var(--accent-red);
}

.confirm-backdrop.confirm-neutral {
  --confirm-tint-bg: var(--tint-blue-bg);
  --confirm-tint-border: var(--tint-blue-border);
  --confirm-tint-fg: var(--accent-blue);
}

.confirm-body {
  font-size: var(--fs-body-sm);
  line-height: 1.55;
  color: var(--text-secondary);
  margin: 0;
}

.confirm-field {
  margin-top: 1.1rem;
}

.confirm-field .field-label {
  margin-bottom: 0.35rem;
}

@keyframes modalPop {
  from { transform: scale(0.96); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.5rem;
}

.modal-title {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

.modal-close {
  background: none;
  border: none;
  color: var(--text-secondary);
  font-size: 1.5rem;
  cursor: pointer;
  transition: color 0.2s;
}

.modal-close:hover {
  color: var(--text-primary);
}

.modal-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

/* Custom Date Picker Trigger & Popover */
.bs-datepicker-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.85rem;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--text-primary);
  cursor: pointer;
  transition: all 0.2s ease;
}

.bs-datepicker-trigger:hover {
  border-color: var(--accent-blue);
  background: var(--bg-inset);
}

.bs-trigger-arrow {
  transition: transform 0.2s ease;
  color: var(--text-secondary);
}

.bs-datepicker-trigger.open .bs-trigger-arrow {
  transform: rotate(180deg);
}

.bs-datepicker-popover {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 1000;
  width: 340px;
  background: var(--bg-card);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sheet);
  padding: 1rem;
  animation: popoverSlide 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes popoverSlide {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* A disclosure's contents fade in as it opens rather than appearing at once. Opacity
   only: they already sit where they will stay. */
details[open] > :not(summary) {
  animation: disclosureOpen var(--dur) var(--ease-out);
}

@keyframes disclosureOpen {
  from { opacity: 0; }
}

.bs-dp-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}

.bs-dp-title-group {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}

.bs-dp-select {
  padding: 0.3rem 0.5rem;
  font-size: var(--fs-meta);
  font-weight: 600;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: var(--bg-inset);
  color: var(--text-primary);
  cursor: pointer;
  outline: none;
}

.bs-dp-select:focus {
  border-color: var(--accent-blue);
}

.bs-dp-nav-btn {
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  background: var(--bg-card);
  cursor: pointer;
  font-size: 1.1rem;
  font-weight: bold;
  color: var(--text-secondary);
  transition: all 0.15s ease;
}

.bs-dp-nav-btn:hover {
  background: var(--bg-inset);
  color: var(--text-primary);
  border-color: var(--border-strong);
}

.bs-dp-presets {
  margin-bottom: 0.85rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--border-color);
}

.bs-dp-preset-label {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 0.4rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.bs-dp-preset-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0.25rem;
}

.bs-dp-preset-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.2rem;
  padding: 0.35rem 0.2rem;
  font-size: var(--fs-label);
  font-weight: 600;
  border-radius: var(--radius-sm);
}

/* A quarter that hasn't ended yet. Faded alone it looked like an unselected
   quarter; the dashed outline and empty fill say "not yet", and pointer events
   stay on so its tooltip saying why can still be read. */
.bs-dp-preset-btn.disabled {
  opacity: 0.75;
  cursor: not-allowed;
  pointer-events: auto;
  background: transparent;
  border: 1px dashed var(--border-strong);
  color: var(--text-muted);
  box-shadow: none;
}

.bs-dp-calendar {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.bs-dp-weekdays {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  text-align: center;
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}

.bs-dp-days-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.25rem;
  text-align: center;
}

.bs-dp-day {
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  font-family: inherit;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 36px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--fs-meta);
  font-weight: 500;
  color: var(--text-primary);
  transition: all 0.15s ease;
  position: relative;
  user-select: none;
}

.bs-dp-day:hover:not(.bs-dp-day-future) {
  background: var(--bg-inset);
}

.bs-dp-day-selected {
  background: var(--accent-fill) !important;
  color: var(--text-on-accent) !important;
  font-weight: 700;
}

.bs-dp-day-selected .dp-dot {
  border: 1px solid var(--text-on-accent);
}

.bs-dp-day-future {
  opacity: 0.3;
  cursor: not-allowed;
  pointer-events: none;
  color: var(--text-muted);
}

.bs-dp-day-empty {
  height: 36px;
  pointer-events: none;
}

/* Freshness Dots */
.dp-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  display: inline-block;
  margin-top: 1px;
  flex-shrink: 0;
}

.dp-dot-fresh {
  background-color: var(--solid-success-bg);
}

.dp-dot-moderate {
  background-color: var(--solid-warn-bg);
}

.dp-dot-stale {
  background-color: var(--solid-danger-bg);
}

.dp-dot-none {
  background-color: transparent;
  border: 1.5px solid var(--border-strong);
  box-sizing: border-box;
}

.bs-dp-legend {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 0.75rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border-color);
  font-size: var(--fs-label);
  color: var(--text-secondary);
}

.bs-dp-legend-item {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

/* On a phone the trigger sits about 29px in from the screen edge, so a 340px popover
   hanging from it runs off a 360px screen and out of its card (which clips or scrolls
   its overflow). Here the trigger's group spans the toolbar, and the popover reaches
   back across the toolbar's side padding (--toolbar-pad-x, set by the balance sheet's
   toolbar) to fill the card's inner width, which
   leaves the page gutter plus the card border on each side. The month grid and the
   header then shrink to fit down to a 320px screen. */
@media (max-width: 600px) {
  .bs-datepicker-anchor {
    flex: 1 1 100%;
    min-width: 0;
  }

  .bs-datepicker-popover {
    left: calc(-1 * var(--toolbar-pad-x, 0px));
    right: calc(-1 * var(--toolbar-pad-x, 0px));
    width: auto;
    padding: 0.75rem;
  }

  .bs-dp-header {
    gap: 0.35rem;
  }

  .bs-dp-nav-btn {
    flex-shrink: 0;
  }

  .bs-dp-title-group {
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
  }

  .bs-dp-select {
    min-width: 0;
  }

  .bs-dp-preset-grid > *,
  .bs-dp-days-grid > * {
    min-width: 0;
  }

  .bs-dp-legend {
    flex-wrap: wrap;
    gap: 0.25rem 0.5rem;
  }
}

/* On a phone, where the list is alone (the split view starts at 600pt), the popover opens
   in the page's flow, pushing what is under it down, so the page is always long enough
   to scroll all of it up above the tab bar (keepDatePickerInView in datepicker.js). It is
   drawn over the contents row stuck under the navbar, and under the navbar. --dp-room is
   the height between the navbar and the tab bar, set by the same script: on a screen
   shorter than the picker it scrolls within itself. */
@media (max-width: 599px) {
  .bs-datepicker-popover {
    position: relative;
    top: auto;
    left: auto;
    right: auto;
    z-index: 45;
    margin: 8px calc(-1 * var(--toolbar-pad-x, 0px)) 0;
    max-height: var(--dp-room, none);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
}

.row-timestamp-detail {
  font-size: var(--fs-label);
  color: var(--text-muted);
  margin-top: 0.15rem;
  font-weight: 400;
}

/* Apple-Style Toggle Switch */
.apple-toggle-switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  user-select: none;
  font-size: var(--fs-meta);
  color: var(--text-secondary);
  font-weight: 500;
}

.apple-toggle-switch input {
  position: absolute;
  opacity: 0;
  width: 34px;
  height: 20px;
  top: 0;
  left: 0;
  margin: 0;
  cursor: pointer;
  z-index: 1;
}

.apple-toggle-slider {
  position: relative;
  display: inline-block;
  width: 34px;
  height: 20px;
  background-color: var(--border-strong);
  border-radius: var(--radius-full);
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  flex-shrink: 0;
}

.apple-toggle-slider:before {
  position: absolute;
  content: "";
  height: 16px;
  width: 16px;
  left: 2px;
  bottom: 2px;
  background-color: var(--bg-card);
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(var(--shade), calc(0.2 * var(--shade-strength)));
  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.apple-toggle-switch input:checked + .apple-toggle-slider {
  background-color: var(--solid-success-bg);
}

.apple-toggle-switch input:focus-visible + .apple-toggle-slider {
  box-shadow: 0 0 0 3px var(--border-focus);
}

.apple-toggle-switch input:checked + .apple-toggle-slider:before {
  transform: translateX(14px);
}

.action-alert-icon {
  font-size: 1.25rem;
  line-height: 0;
  flex-shrink: 0;
  margin-top: 0.1rem;
}

/* Account Type Dropdown Selector. Qualified by element so it outranks base.css's
   select:focus-visible: this control shows focus by the ring in its :focus rule, not
   an outline. */
select.account-type-select {
  background: var(--bg-card);
  color: var(--text-primary);
  border: 1px solid var(--border-strong);
  padding: 0.25rem 0.6rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-label);
  font-weight: 500;
  font-family: inherit;
  outline: none;
  cursor: pointer;
  transition: all 0.2s ease;
}

.account-type-select:focus {
  border-color: var(--accent-blue);
  box-shadow: 0 0 0 3px var(--border-focus);
}

select.account-type-select.unknown-type {
  border-color: var(--tint-amber-border);
  background: var(--tint-yellow-bg);
  color: var(--tint-orange-fg);
  font-weight: 600;
}

.account-type-select:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.metric-value {
  font-variant-numeric: tabular-nums;
}

/* ============================================================================
   Shared layout & detail patterns

   Small pieces reused across the dashboard, the two inspector modals and
   Settings: a form field's label, a muted loading/empty message, a row that
   splits its label from its value, and the "amount + label" grid the two
   modals both open with. Named here so the copies share one definition rather
   than repeating the same inline declarations at every call site.
   ============================================================================ */

/* The caption above a form control: sentence case at a medium weight, so a form
   of several fields reads as labels rather than a column of shouted headings. */
.field-label {
  display: block;
  margin-bottom: 0.35rem;
  font-size: var(--fs-meta);
  font-weight: 500;
  color: var(--text-secondary);
}

/* A <select>, <input> or <textarea> at full width with the roomier padding a
   modal's forms use, layered onto the same .form-input every other field in
   the app already shares. */
.form-input-block {
  width: 100%;
  padding: 0.5rem 0.75rem;
  font-size: var(--fs-body-sm);
}

/* Centered placeholder text for a table body or panel that hasn't loaded yet,
   or has nothing to show. */
.empty-state {
  padding: 2rem;
  text-align: center;
  color: var(--text-muted);
}

.detail-label {
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

/* A short monospace value shown read-only next to a copy button — an account
   or bank reference ID. */
.detail-value-mono {
  flex: 1;
  background: var(--bg-inset);
  font-family: var(--font-mono);
  font-size: var(--fs-label);
}

/* A small code-styled chip for a short read-only value that isn't a headline
   figure — an aggregator's raw type, a currency code. */
.detail-value-chip {
  padding: 0.4rem 0.6rem;
  font-size: var(--fs-meta);
  font-weight: 500;
  color: var(--text-secondary);
  background: var(--bg-inset);
  border-radius: var(--radius-sm);
}

/* An plain two-column grid, for pairing fields that don't need a card of their own. */
.grid-2col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

/* Common flex arrangements that recurred without a name of their own. Reached
   for only when nothing more specific fits — a control-specific layout still
   gets a component class of its own rather than being built from these. */

.flex-row-gap-sm {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* A compact inline error, sized for sitting inside a modal rather than as a
   page-level banner (.load-error-banner is that banner, for a whole view
   failing to load; this is one field or one action failing). */
.error-box {
  padding: 0.75rem;
  font-size: var(--fs-meta);
  color: var(--accent-red);
  background: var(--tint-red-bg);
  border: 1px solid var(--tint-red-border);
  border-radius: var(--radius-sm);
}

/* A small solid-color dot used as a legend or status marker outside of
   .status-dot's own contexts (which carries a glow and live-connection
   meaning that doesn't apply to a plain legend key). */
.dot-marker {
  display: inline-block;
  flex-shrink: 0;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
}

.dot-marker.net-worth {
  background-color: var(--chart-net-worth);
}

.dot-marker.assets {
  background-color: var(--chart-assets);
}

.dot-marker.liabilities {
  background-color: var(--chart-liabilities);
}

/* ============================================================================
   Chart

   The drawing keeps square geometry (preserveAspectRatio is not "none"), so the
   line holds one weight and the hover dot stays a circle.
   ============================================================================ */
.chart-axis-label {
  font-size: var(--fs-label);
  font-weight: 500;
  fill: var(--chart-axis-text);
}

.chart-grid-line {
  stroke: var(--chart-grid);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

.chart-delta {
  font-size: var(--fs-meta);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.chart-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
  min-height: 1.35rem;
}

/* Both readouts are absolutely placed inside this slot, which holds its own
   size. Whatever they say, and however long the hovered date and figure turn
   out to be, the header keeps its height and the chart never moves. */
.chart-readout {
  position: relative;
  flex: 1 1 auto;
  min-width: 0;
  align-self: stretch;
  overflow: hidden;
}

.chart-readout > .chart-delta {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease;
}

.chart-readout > .chart-delta.is-shown {
  opacity: 1;
  visibility: visible;
}

/* Narrow screens give the readout its own line rather than squeezing it against
   the title; it still occupies a reserved, fixed slot. */
@media (max-width: 640px) {
  .chart-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
  }

  .chart-readout {
    width: 100%;
    height: 1.2rem;
    align-self: auto;
  }

  .chart-readout > .chart-delta {
    right: auto;
    left: 0;
  }
}

/* The path is drawn in on first render by animating its dash offset to zero. */
@keyframes chartDraw {
  from { stroke-dashoffset: var(--chart-path-length, 2000); }
  to { stroke-dashoffset: 0; }
}

.chart-line-animated {
  animation: chartDraw 0.9s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes chartFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.chart-area-animated {
  animation: chartFadeIn 0.9s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

/* The latest point arrives once the line has drawn up to it, 0.7s in: renderHistoryChart
   sets each entrance's delay on its element. */
.chart-end-animated {
  animation: chartFadeIn 0.3s ease both;
}

.chart-end-label {
  font-size: var(--fs-meta);
  font-weight: 700;
  fill: var(--text-primary);
  /* A halo in the card's colour, so the label stays legible over the line and the
     fill beneath it. */
  stroke: var(--bg-card);
  stroke-width: 4px;
  stroke-linejoin: round;
  paint-order: stroke;
}

/* A line chart's floor, and its high and low (extremeMarkersSVG in charts.js), which the
   Overview's chart and an account's chart both draw in place of a value axis. */
.chart-baseline {
  stroke: var(--chart-grid);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

.chart-extreme circle {
  fill: var(--surface);
  stroke: var(--ink-3);
  stroke-width: 1.5;
}

.chart-extreme-label {
  font-size: var(--fs-label);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  fill: var(--ink-2);
  /* A halo in the card's colour keeps the label legible over the fill and the line. */
  stroke: var(--surface);
  stroke-width: 4px;
  stroke-linejoin: round;
  paint-order: stroke;
}

.chart-tick {
  stroke: var(--chart-grid);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

/* A day an account joined net worth (joinMarksSVG in charts.js), on the chart's floor:
   the jump above it is the account arriving, not money made. */
.chart-join path {
  fill: var(--surface);
  stroke: var(--ink-3);
  stroke-width: 1.5;
}

/* The day whose statement is on screen above. */
.chart-selected line {
  stroke: var(--chart-net-worth);
  stroke-width: 1.5;
  opacity: 0.45;
}

.chart-selected circle {
  fill: none;
  stroke: var(--chart-net-worth);
  stroke-width: 2;
}

/* Follows the pointer, so it never takes clicks meant for the chart under it. */
.chart-tooltip {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  min-width: 11rem;
  padding: 0.55rem 0.7rem;
  background: var(--bg-elevated);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  font-size: var(--fs-meta);
  font-variant-numeric: tabular-nums;
}

.chart-tooltip-date {
  font-weight: 600;
  color: var(--text-primary);
  margin-bottom: 0.3rem;
}

.chart-tooltip-row {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--text-secondary);
  line-height: 1.6;
}

.chart-tooltip-row strong {
  margin-left: auto;
  padding-left: 1rem;
  color: var(--text-primary);
  font-weight: 600;
}

/* An account that joined on the day, and how the day opens. */
.chart-tooltip-join {
  margin-top: 0.3rem;
  color: var(--text-secondary);
}

.chart-tooltip-hint {
  margin-top: 0.3rem;
  color: var(--ink-3);
}

.chart-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 1.1rem;
}

.chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-label);
  font-weight: 500;
  color: var(--text-secondary);
}

.chart-legend-hint {
  font-size: var(--fs-label);
  color: var(--text-muted);
  margin-left: auto;
}

/* Staggered entrance for freshly rendered rows. Each row's place in the stagger,
   --row-i, is set by the script that adds the class (staggerRows in uikit.js). */
@keyframes rowFadeIn {
  from { opacity: 0; transform: translateY(var(--row-enter-rise)); }
  to { opacity: 1; transform: translateY(0); }
}

/* An entering row is drawn below its place, and WebKit anchors the page's scroll on where
   a row is drawn: scrolled to mid-entrance, it later scrolled the page back by the rise,
   at the first layout after the row had entered. Entering rows are no anchor. */
.row-enter {
  animation: rowFadeIn var(--row-enter-dur) var(--ease-out) backwards;
  animation-delay: calc(var(--row-i, 0) * var(--row-enter-step));
  overflow-anchor: none;
}

@media (max-width: 768px) {
  .metric-title {
    font-size: var(--fs-label);
  }

  .table-container {
    overflow-x: auto;
  }

  /* The alert bar has to clear the tab bar it would otherwise sit under, plus
     whatever the home indicator takes below that. This is the whole reason the
     bar is anchored to the bottom rather than the top: at the top it would cover the
     header and the first KPI card, which on a phone is most of what's on screen. */
  #alert-bar-host {
    bottom: calc(4.25rem + env(safe-area-inset-bottom, 0px));
  }
}

.btn-danger-text {
  color: var(--accent-red);
}

.field-help {
  margin: 0.35rem 0 0;
  font-size: var(--fs-label);
  color: var(--text-muted);
  line-height: 1.5;
}

/* Lined up with the checkbox's text rather than the box. */
/* A switch that saves the moment it is flipped (saveSwitch in uikit.js): its words, and the
   switch at the end of the line, the whole line a press on it. The spot the switch sits in
   washes green once the save lands (glowSwitch), leaving the switch its own colour. */
.switch-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 2.5rem;
  cursor: pointer;
  font-size: var(--fs-body-sm);
  color: var(--ink);
}

.switch-spot {
  position: relative;
  display: inline-flex;
  flex: none;
  padding: 6px 8px;
  margin: -6px -8px;
  border-radius: var(--radius-full);
}

.switch-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}

.switch-track {
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: var(--radius-full);
  background: var(--border-strong);
  transition: background-color 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 150ms ease 100ms;
}

.switch-track::before {
  content: "";
  position: absolute;
  left: 2px;
  top: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--bg-card);
  box-shadow: 0 1px 3px rgba(var(--shade), calc(0.2 * var(--shade-strength)));
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.switch-input:checked + .switch-track {
  background: var(--solid-success-bg);
}

.switch-input:checked + .switch-track::before {
  transform: translateX(14px);
}

.switch-input:focus-visible + .switch-track {
  box-shadow: 0 0 0 3px var(--border-focus);
}

.switch-input[aria-busy="true"] {
  cursor: progress;
}

.switch-input[aria-busy="true"] + .switch-track,
.switch-input:disabled:not([aria-busy="true"]) + .switch-track {
  opacity: 0.5;
}

.switch-spot.is-saved {
  animation: switchWash 1.5s ease;
}

@keyframes switchWash {
  0%, 100% { background-color: transparent; }
  25%      { background-color: color-mix(in srgb, var(--accent) 24%, transparent); }
}

/* A finger needs about 44px: the spot grows to it, taking the room back by its margin. */
@media (pointer: coarse) {
  .switch-row {
    min-height: 44px;
  }

  .switch-spot {
    padding: 12px 8px;
    margin: -12px -8px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .switch-track,
  .switch-track::before {
    transition: none;
  }

  .switch-spot.is-saved {
    animation: none;
  }
}

/* A long explanation folded under its first sentence: "How this works" opens the rest, so
   the controls it explains stand out to someone looking for them. */
.how-this-works {
  margin: var(--space-1) 0 0;
  font-size: var(--fs-meta);
  line-height: 1.5;
  color: var(--ink-3);
}

.how-this-works > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  width: fit-content;
  color: var(--accent);
  font-weight: 500;
  cursor: pointer;
  list-style: none;
}

.how-this-works > summary::-webkit-details-marker {
  display: none;
}

.how-this-works > summary::after {
  content: "";
  width: 0.35rem;
  height: 0.35rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-25%) rotate(45deg);
  transition: transform 150ms ease;
}

.how-this-works[open] > summary::after {
  transform: translateY(15%) rotate(-135deg);
}

.how-this-works > p {
  margin: var(--space-1) 0 0;
  max-width: 65ch;
}

@media (pointer: coarse) {
  .how-this-works > summary {
    min-height: 44px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .how-this-works > summary::after {
    transition: none;
  }
}

.field-help-indented {
  padding-left: 1.5rem;
}

/* Says what is wrong with one field, directly beneath it, so the message is on screen
   wherever the field is — a summary at the foot of a long form is below the fold on a
   phone, and does not say which box to fix. */
.field-error {
  margin: 0.35rem 0 0;
  font-size: var(--fs-label);
  font-weight: 500;
  color: var(--accent-red);
}

.form-input[aria-invalid="true"] {
  border-color: var(--accent-red);
}

.field-optional {
  font-weight: 400;
  color: var(--text-muted);
}

@media (max-width: 600px) {
  .card {
    padding: 1rem;
  }
}

/* Touch screens.

   iPhone Safari zooms the whole page into any text box, dropdown or text area whose text
   is under 16px the moment it is tapped, and leaves it zoomed. Every field in this app
   was 12-14px. :is() takes its most specific argument's weight, so this one rule
   outranks the class-based sizes it corrects without !important.

   And an iPhone draws a native dropdown 22px tall whatever its padding or min-height,
   a short sliver beside 35px text boxes. Without the native styling it sizes like the
   boxes around it, so it is drawn plain with a chevron of its own. */
@media (pointer: coarse) {
  :is(input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), select, textarea) {
    font-size: var(--fs-body);
  }

  select.form-input,
  select.account-type-select,
  select.account-type-select.unknown-type,
  select.bs-dp-select {
    -webkit-appearance: none;
    appearance: none;
    padding-right: 2.25rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5l5 5 5-5' fill='none' stroke='%238a8f98' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.85rem center;
    background-size: 12px 8px;
  }

  /* Every field is sized for a finger, the read-only references with copy buttons
     beside them included; and so are the standalone dropdowns, such as the date
     picker's month and year. */
  .form-input,
  select.account-type-select,
  select.bs-dp-select {
    min-height: 44px;
  }

  /* A checkbox is too small to aim at, so its whole label row is the target. */
  .check-row,
  .net-worth-exclude-row,
  .apple-toggle-switch {
    min-height: 44px;
  }

  .modal-close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }

  /* The as-of date picker: its trigger, the month arrows and every day. Seven 44pt
     days leave no room for gaps between them, and from a 360pt screen up they get
     their 44; on a 320pt screen they narrow rather than run off it. The weeks touch too,
     which keeps a month of six on an iPhone SE between the navbar and the tab bar. */
  .bs-datepicker-trigger {
    min-height: 44px;
  }

  .bs-dp-nav-btn {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
  }

  .bs-dp-days-grid {
    gap: 0;
  }

  .bs-dp-day,
  .bs-dp-day-empty {
    height: 44px;
  }
}

/* Above phone width the picker has a fixed width; on a touch screen it grows to hold
   seven 44pt days. A phone's spans the screen (see the max-width: 600px rules). */
@media (pointer: coarse) and (min-width: 601px) {
  .bs-datepicker-popover {
    width: 344px;
  }
}

/* ----------------------------------------------------------------------------
   Dialogs as bottom sheets (phones)

   On a phone every dialog rises from the bottom edge, full width, where a thumb can
   reach its buttons: rounded top corners, a grab handle, room for the home indicator,
   and its own scrolling. Wider screens keep the centred card.
   ---------------------------------------------------------------------------- */
@keyframes sheetRise {
  from { transform: translateY(100%); }
  to { transform: translateY(0); }
}

@media (max-width: 600px) {
  /* The sheet rises the whole way from the bottom edge and goes back down it, opaque
     throughout, as an iPhone's does; only the dimming behind it fades, so it is drawn
     apart from the sheet (::before) rather than fading the backdrop and the sheet in it. */
  .modal-backdrop {
    align-items: flex-end;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    opacity: 1;
    transition: display var(--sheet-out-dur) allow-discrete;
  }

  @starting-style {
    .modal-backdrop.active {
      opacity: 1;
    }
  }

  .modal-backdrop::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--bg-scrim);
    -webkit-backdrop-filter: blur(15px);
    backdrop-filter: blur(15px);
    opacity: 0;
    transition: opacity var(--sheet-out-dur) var(--ease-fade);
  }

  /* --scrim-shown is how much of the dimming a sheet dragged down leaves (sheetdrag.js). */
  .modal-backdrop.active::before {
    opacity: var(--scrim-shown, 1);
  }

  @starting-style {
    .modal-backdrop.active::before {
      opacity: 0;
    }
  }

  /* Scoped under the backdrop so it outranks each dialog's own width (.acc-modal,
     .confirm-modal, .tx-modal): a sheet is always the full screen wide. */
  .modal-backdrop .modal {
    width: 100%;
    max-width: none;
    max-height: calc(100dvh - var(--space-8));
    margin: 0;
    border-width: 1px 0 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: var(--space-3) var(--space-4) calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
    background: var(--surface);
    box-shadow: var(--shadow-sheet);
    /* Over the dimming. */
    position: relative;
    animation: sheetRise var(--sheet-in-dur) var(--ease-sheet);
    transition: translate var(--sheet-out-dur) var(--ease-sheet);
  }

  .modal-backdrop:not(.active) .modal {
    scale: none;
    translate: 0 100%;
  }

  /* The grab handle. */
  .modal::before {
    content: "";
    display: block;
    width: 36px;
    height: 5px;
    margin: 0 auto var(--space-4);
    border-radius: var(--radius-full);
    background: var(--line-strong);
  }

  .modal-footer {
    flex-wrap: wrap;
  }

  .modal-footer .btn {
    flex: 1 1 auto;
    min-height: 44px;
  }

  .modal-footer :is(.btn-primary, .btn-danger) {
    min-height: 50px;
  }
}

@media (max-width: 600px) and (prefers-reduced-motion: reduce) {
  .modal-backdrop .modal {
    animation: none;
  }
}

/* Dragged down by a finger (sheetdrag.js), the sheet and its scrim follow it exactly,
   with no easing of their own behind it. */
.modal-backdrop.is-dragging,
.modal-backdrop.is-dragging::before,
.modal-backdrop .modal.is-dragging {
  transition: none;
}

.modal-backdrop .modal.is-dragging {
  will-change: transform;
}

/* A finger needs about 44px; the buttons are otherwise sized for a mouse pointer. */
@media (pointer: coarse), (max-width: 600px) {
  .btn,
  .btn-sm {
    min-height: 44px;
  }
}

@media (pointer: coarse) {
  .btn,
  .btn-sm {
    min-width: 44px;
  }
}

/* ----------------------------------------------------------------------------
   Loading skeletons

   While a screen's first data is on the way it shows the shape of what is coming:
   blocks where the figures, lines and rows will be, drawn by skelHTML and skelRowsHTML
   in uikit.js. Nothing in them could be read as a value.

   A block is unseen for its first 150ms and then fades in (SKELETON_DELAY_MS in
   uikit.js is the same pause), so a quick load goes from the empty frame to the data
   without a skeleton blinking in and out. Once in, it pulses slowly between the line
   colour and half of it, which reads on the paper and on a card in both themes; under
   reduced motion it holds still.
   ---------------------------------------------------------------------------- */
.skel {
  position: relative;
  display: inline-block;
  vertical-align: middle;
  width: 8em;
  max-width: 100%;
  height: 0.75em;
  border-radius: var(--radius-sm);
  overflow: hidden;
  animation: skel-appear 200ms var(--ease-out) 150ms both;
}

.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--line);
  animation: skel-pulse 1.6s ease-in-out 350ms infinite;
}

@keyframes skel-appear {
  from { opacity: 0; }
}

@keyframes skel-pulse {
  50% { opacity: 0.45; }
}

@media (prefers-reduced-motion: reduce) {
  .skel::after {
    animation: none;
  }
}

/* A headline figure: about as wide as "$12,345", as tall as its digits. The figure's
   own line height still sets the box, so the data lands without moving anything. */
.skel-figure {
  width: 5.5em;
  height: 0.78em;
  border-radius: var(--radius-md);
}

.skel-short { width: 4em; }
.skel-long { width: 14em; }

/* Fills its box: a chart's area, a bar. */
.skel-block {
  display: block;
  width: 100%;
  height: 100%;
}

/* A row of a list: a square for a monogram, a name over a shorter line, a figure. */
.skel-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 3.5rem;
  padding: var(--space-2) var(--space-4);
  font-size: var(--fs-body-sm);
  list-style: none;
}

.skel-row + .skel-row {
  border-top: 1px solid var(--line);
}

.skel-lines {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.skel-line {
  width: 11em;
}

.skel-line-sub {
  width: 7em;
  height: 0.65em;
}

/* Rows differ a little, as real ones do. */
.skel-row:nth-child(3n + 2) .skel-line:not(.skel-line-sub) { width: 8em; }
.skel-row:nth-child(3n) .skel-line:not(.skel-line-sub) { width: 13em; }
.skel-row:nth-child(3n + 2) .skel-line-sub { width: 9em; }

.skel-avatar {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 11px;
}

.skel-trail {
  flex: none;
  width: 4.5em;
}

/* ----------------------------------------------------------------------------
   Utilities

   Small single-purpose classes for the markup in index.html, which used to carry
   these as inline style attributes. !important because that is what an inline style
   was: the last word on its one element, whatever a component rule says.
   ---------------------------------------------------------------------------- */
.u-mb-0 { margin-bottom: 0 !important; }
.u-mb-2 { margin-bottom: var(--space-2) !important; }
.u-mb-4 { margin-bottom: var(--space-4) !important; }
.u-mt-6 { margin-top: var(--space-6) !important; }

.u-flex { display: flex !important; }
.u-block { display: block !important; }
.u-gap-2 { gap: var(--space-2) !important; }
.u-relative { position: relative !important; }
.u-w-full { width: 100% !important; }
.u-resize-v { resize: vertical !important; }

.u-meta { font-size: var(--fs-meta) !important; }
.u-label { font-size: var(--fs-label) !important; }
.u-medium { font-weight: 500 !important; }
.u-mono { font-family: var(--font-mono) !important; }
.u-text-2 { color: var(--ink-2) !important; }
.u-text-3 { color: var(--ink-3) !important; }
.u-text-attn { color: var(--attn) !important; }

/* Amounts, from amountClass in format.js. Money in is income or a gain, in the accent;
   money out is ordinary spending, in ink. Red never marks spending. */
.amount-in { color: var(--accent); }
.amount-out { color: var(--ink); }

/* An amount inside longer text (nowrapMoneyHTML in format.js) never breaks inside
   itself, between its sign or currency code and its figure. */
.money { white-space: nowrap; }

/* The cents of a headline amount (headlineMoneyHTML in format.js): half size, in ink-3,
   so the whole figure reads first. Never below the 12px every text run is held to. */
.amount-cents {
  font-size: max(0.5em, var(--fs-label));
  color: var(--ink-3);
}

/* Layouts that recur in the markup. */

/* A checkbox and its label, the whole row clickable. */
.check-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--fs-body-sm);
  cursor: pointer;
}

/* The contents row on Settings, Accounts and the admin console (js/pagetoc.js): links to
   the page's sections that stay under the navbar, on the page's own colour frosted so
   what scrolls beneath is not read through the links. On a phone the row scrolls
   sideways when the links are wider than the screen, rather than wrapping into a block
   that would cover more of a small screen; wider, they wrap (below). The link to the
   section being read is drawn as a pressed chip. */
.page-toc {
  --toc-pad: var(--space-2);
  position: sticky;
  top: calc(3.75rem + 1px);
  z-index: 40;
  display: flex;
  gap: var(--space-2);
  margin: calc(-1 * var(--toc-pad)) 0;
  padding: var(--toc-pad) 0;
  overflow-x: auto;
  scrollbar-width: none;
  background: color-mix(in srgb, var(--bg-primary) 88%, transparent);
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
}

.page-toc::-webkit-scrollbar {
  display: none;
}

.page-toc[hidden] {
  display: none;
}

.page-toc-link {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  padding: 0 var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-2);
  font: 500 var(--fs-meta)/1 var(--font-text);
  white-space: nowrap;
  text-decoration: none;
}

.page-toc-link:hover {
  color: var(--ink);
  border-color: var(--line-strong);
}

/* A blank link, while the page's sections are on their way. */
.page-toc-skel {
  width: 5.5rem;
  justify-content: center;
}

.page-toc-skel .skel {
  width: 100%;
}

.page-toc-link[aria-current="location"] {
  border-color: var(--ink);
  background: var(--sunk);
  color: var(--ink);
}

/* 44pt to tap, as Review's chips are. */
@media (pointer: coarse), (max-width: 600px) {
  .page-toc {
    --toc-pad: 6px;
  }

  .page-toc-link {
    min-height: 44px;
  }
}

/* Above a phone's width, where a link cut at the row's edge reads as the last one, the
   links that don't fit wrap onto another line. */
@media (min-width: 601px) {
  .page-toc {
    flex-wrap: wrap;
  }
}

/* On a phone the row runs to the screen's edges, past the page gutter (base.css), so a
   link scrolled out of view slides off the screen rather than being cut at the gutter. */
@media (max-width: 600px) {
  .page-toc {
    margin-left: -0.75rem;
    margin-right: -0.75rem;
    padding-left: 0.75rem;
    padding-right: 0.75rem;
  }
}

/* Under the navbar and the contents row together (32px links and 8px either side, or
   44px and 6px, on each of its lines), a section the browser scrolls to, such as a field
   focused from the keyboard, is held below both. --page-toc-h is the row's height as
   drawn, set by js/pagetoc.js, whose own links work out where to scroll for themselves.
   Not with an account's page open beside the list: the page holds its own place under
   the navbar, and the deeper padding would have the window scroll each time something in
   it is focused. */
html:has(.view-section.active .page-toc:not([hidden])):not(:has(.acct-layout.has-detail)) {
  scroll-padding-top: calc(3.75rem + var(--page-toc-h, 3rem) + 1.25rem);
}

@media (pointer: coarse), (max-width: 600px) {
  html:has(.view-section.active .page-toc:not([hidden])):not(:has(.acct-layout.has-detail)) {
    scroll-padding-top: calc(3.75rem + var(--page-toc-h, 3.5rem) + 1.25rem);
  }
}
