/* /css/base.css - reset plus cross-page primitives shared by every tab: the
   form-control theme, tables, icons/icon-buttons, help text, sign coloring,
   and the shared focus ring. Anything used by more than one page lives here.
   Every bare <input>/<select>/<textarea>/<button> gets its whole appearance
   from this file; a page file sizes and positions a control, never restyles
   one. */

* {
	box-sizing: border-box;
}

/* Always reserve the page scrollbar's gutter, so the layout doesn't shift
   sideways when content grows past the fold (or shrinks back). A no-op
   with overlay scrollbars (macOS, phones). */
html {
	scrollbar-gutter: stable;
	scrollbar-color: var(--button-accent) transparent;
}

/* scrollbar-color inherits; the root's value styles the page scrollbar, and
   this hands every inner scroller back the default (register columns set
   their own). */
body {
	scrollbar-color: auto;
}

body {
	font-family: system-ui, sans-serif;
	margin: 0;
	background: var(--bg);
	color: var(--fg);
}

main {
	padding: 1.5rem;
	/* Clearance so the fixed .scroll-top never rests on a tab's last row. */
	padding-bottom: 9.5rem;
}

/* Every tab's own page title except Dashboard's (which opens straight on
   its card grid): a direct child of the tab's <section data-panel>, which
   reliably matches only this heading -- every other heading on these pages
   nests at least one level deeper. Larger and unbordered, so it reads above
   a page's own section headings (e.g. .settings-block h3's divider). */
[data-panel] > h2 {
	margin: 0 0 1.5rem;
	font-size: 2rem;
	text-align: center;
	color: var(--heading);
}

:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 1px;
}

/* `font: inherit` is load-bearing: a UA stylesheet sets family and size
   directly on every form control, so they inherit nothing from an ancestor.
   A real background and border suppresses the native bevel without
   `appearance: none`, which would also strip a select's arrow. */
input:not([type='checkbox'], [type='radio']),
select,
textarea,
button {
	font: inherit;
	/* Explicit: `normal` resolves to different line boxes for a button's text
	   and an input's, so controls side by side would differ in height. */
	line-height: 1.25;
	color: var(--fg);
	border: 1px solid var(--border);
	border-radius: var(--control-radius);
	padding: 0.4rem 0.7rem;
}

input:not([type='checkbox'], [type='radio']),
select,
textarea {
	background: var(--surface);
}

/* A top-level category in a folded category/subcategory select (ts/ledger.ts).
   font-weight is one of the few properties every browser honors on <option>,
   even without appearance: base-select. A class, not style="": the page CSP
   (lib/Http.php) blocks inline style attributes. */
option.option-heading {
	font-weight: 600;
}

/* Its subcategories, dimmed so the headings stand apart from them. 75% stays
   clearly brighter than the disabled Income/Expense separators (and ~7.5:1
   in both themes). :not(:checked) leaves the selected row's own fill colors
   alone and lifts this to (0,2,1), over css/select-picker.css's
   `.account-ledger option` color whatever the load order. var(--fg), not
   currentColor: --fg is what that file re-declares inside ::picker(select). */
option.option-sub:not(:checked) {
	color: color-mix(in srgb, var(--fg) 75%, transparent);
}

/* The inline <symbol> sheet: referenced by <use>, never drawn itself. */
.sprite-sheet {
	display: none;
}

/* Every button's default fill, at zero specificity (:where) so any rule that
   names a background of its own wins without having to out-specify this.
   Per theme in css/tokens.css: white in light, black in dark.
   The transition sits here, not on the hover rule: a rule that matches only
   while hovered leaves mouse-out instant. */
:where(button) {
	background: var(--button-bg);
	transition:
		background-color 0.2s ease,
		color 0.2s ease;
}

/* Fine pointers only, like every hover on a button or toggle: iOS keeps
   :hover on a tapped element until the next tap elsewhere, so a hover look
   on touch is a stuck look. Page files gate their own button hovers the
   same way, or this rule's absence on touch would be the only thing their
   ungated hover had to beat. */
@media (hover: hover) and (pointer: fine) {
	:where(button:not(:disabled):hover) {
		background: var(--button-bg-hover);
	}

	/* Not :where(), unlike the fill above. The bare `button` selector in
	   the control theme sets `color: var(--fg)` at (0,0,1), which
	   out-specifies anything inside :where() at (0,0,0) -- so a :where()
	   text color would never apply at all. Equal specificity, later in the
	   file, wins on source order; any named class still beats this without
	   trying. */
	button:not(:disabled):hover {
		color: var(--button-accent);
	}
}

button {
	cursor: pointer;

	&:disabled {
		opacity: 0.5;
		cursor: default;
	}
}

/* Native rendering, just bigger. accent-color recolors the check/dot while
   keeping the platform's shape, focus ring and disabled look; --accent-fill
   follows any per-instance override. */
input[type='checkbox'],
input[type='radio'] {
	width: 1.6rem;
	height: 1.6rem;
	accent-color: var(--accent-fill);
}

/* Centers label text against the inflated 1.6rem checkbox, which inline
   baseline alignment does not. Direct child only. */
label:has(> input[type='checkbox']) {
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
}

/* em, so a hint tracks its own control's size. --fg at 50% tracks the theme;
   the UA's fixed placeholder gray is near-illegible on the dark page. */
::placeholder {
	font-size: 0.9em;
	color: color-mix(in srgb, var(--fg) 50%, transparent);
}

table {
	width: 100%;
	border-collapse: collapse;
}

th,
td {
	text-align: left;
	padding: 0.4rem 0.6rem;
	border-bottom: 1px solid var(--border);
}

th {
	font-weight: 600;
}

/* Every formatted number, output or input: a fixed-width system mono so
   digits don't shift as a value changes. Numeric inputs are matched by
   inputmode too, since some are type="text" for formatting control. */
.num,
input[type='number'],
input[inputmode='decimal'] {
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
}

td.num,
th.num {
	text-align: right;
}

/* A row of buttons (and any inline result text) under a section. */
.actions-row {
	display: flex;
	align-items: center;
	gap: 0.75rem;
	margin-bottom: 0.75rem;
}

.help-text {
	max-width: 60rem;
	color: var(--help);
	font-size: 0.9rem;
	margin: 0.5rem 0 1rem;
}

/* Sprite consumers inherit color and size from context. pointer-events is
   off so a click resolves to the enclosing button, which delegated
   listeners key off. */
.icon {
	width: 1em;
	height: 1em;
	fill: currentColor;
	flex: 0 0 auto;
	pointer-events: none;
}

/* Icon-only button: the accessible name lives on aria-label/title. */
.icon-button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	line-height: 1;
	aspect-ratio: 1;
	flex: 0 0 auto;
	align-self: center;
	padding: 4px;
	border: 1px solid var(--border);
	border-radius: var(--control-radius);
	background: var(--surface);
	color: var(--muted-strong);
	cursor: pointer;

	/* Its own hover pair, not the shared button theme: this class sets
	   `background`/`color` at (0,1,0), which the theme's :where() hover
	   (0,0,0) can never outrank, so without this an icon button's hover
	   showed the general fill but a stale resting glyph color. Matches the
	   theme's tokens exactly, so an icon button and a text button hover
	   identically. */
	&:hover:not(:disabled) {
		background: var(--button-bg-hover);
		color: var(--button-accent);
	}

	.icon {
		width: 1.44rem;
		height: 1.44rem;
	}
}

/* See ts/util.ts's amountSignClass(); zero keeps the default color. */
.amount-neg {
	color: var(--amount-neg);
}

.amount-pos {
	color: var(--amount-pos);
}

.manual-tag {
	font-size: 0.85em;
	color: var(--muted);
}

tr.empty td {
	text-align: center;
	color: var(--subtle);
	font-style: italic;
}

/* ts/scrolltop.ts adds .visible; until then it sits translated off-screen
   and unclickable.
   One duration and one easing across every state below, exit included. The
   reveal is a reveal, not a travelling UI: a curve that ramps up before it
   lands, or an exit faster than the entrance, reads as the widget jumping
   rather than moving. The opacities are three steps -- off, resting, hover
   -- with one uniform linear-ish fade between each so the phase a
   transition starts from never changes how fast it looks. */
.scroll-top {
	--icon-off: 0;
	--icon-rest: 0.33;
	--icon-ease: ease-in-out;
	--icon-duration: 0.2s;

	position: fixed;
	right: 1.5rem;
	bottom: 1.5rem;
	z-index: 100;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 6rem;
	height: 6rem;
	padding: 0;
	/* The global 1px var(--border) the control reset gives every button is
	   cleared rather than kept: a ring around a bare icon this large reads
	   as a second, empty shape rather than as the button's edge, so the
	   icon alone carries the control. */
	border: 0;
	border-radius: 999px;
	background: none;
	/* The resting label is the icon accent, not --fg: at 20% it reads as a
	   tint rather than a grey wash over the UI. */
	color: var(--button-accent);
	cursor: pointer;
	opacity: 0;
	transform: translateY(150%);
	pointer-events: none;
	/* The fade overrides below name opacity alone, so at rest this is still
	   the list that slides; the colour fades off the shared reset are
	   re-stated because this shorthand replaces them. */
	transition:
		opacity var(--icon-duration) var(--icon-ease),
		transform var(--icon-duration) var(--icon-ease),
		color 0.2s ease;

	.icon {
		width: 4.5rem;
		height: 4.5rem;
	}

	&.visible {
		opacity: var(--icon-rest);
		transform: translateY(0);
		pointer-events: auto;
	}

	&.visible:hover,
	&.visible:focus-visible {
		/* The hover stop, stated rather than inherited, so hover-out ramps
		   .66 -> .33 the same way hover-in ramps .33 -> .66; without it the
		   fade departs from wherever .visible set it and the two directions
		   differ in length. */
		opacity: 0.66;
		transition: opacity var(--icon-duration) var(--icon-ease);
	}

	/* Stated rather than left to fall back to the base rule. Removing
	   .visible otherwise drops the hover's opacity: 1 and .visible's own
	   values in the same frame, so opacity decays from wherever the hover
	   left it while transform is already most of the way down -- the
	   float-out reads about twice as fast as the float-in. Re-stating the
	   slide keeps the exit the same length as the entrance.
	   The hover's own fade above rides on top of this rather than
	   replacing it, so the pointer still being over the widget when it
	   leaves cannot shorten the travel. */
	&:not(.visible) {
		opacity: 0;
		transform: translateY(150%);
		transition:
			opacity var(--icon-duration) var(--icon-ease),
			transform var(--icon-duration) var(--icon-ease),
			color 0.2s ease;
	}
}

@media (prefers-reduced-motion: reduce) {
	/* The custom properties must be set on the element itself, not via
	   transition-duration -- resolving var() with the element as the
	   substitution context is what makes a state rule's own transition
	   shorthand pick this up instead of its own longhand overrides. */
	.scroll-top {
		--icon-duration: 0.01ms;
	}
}

/* ts/toast.ts's newest-first stack. z-index clears the sticky header (10);
   the brief overlap with the theme toggle is accepted rather than tracking
   the header's height across its one-row and stacked layouts.
   A fixed width, not max-width: a width-less stack resizes to its widest
   toast, shifting every toast's left edge as one joins or leaves. */
.toast-stack {
	position: fixed;
	top: 1.5rem;
	right: 1.5rem;
	z-index: 100;
	display: flex;
	flex-direction: column;
	/* Each toast sizes to its own content, flush right, rather than
	   stretching to the stack's width. */
	align-items: flex-end;
	gap: 0.5rem;
	width: min(24rem, calc(100vw - 3rem));
	/* Gaps must not swallow clicks; each toast opts back in. */
	pointer-events: none;
}

.toast {
	pointer-events: auto;
	position: relative;
	max-width: 100%;
	display: flex;
	align-items: flex-start;
	padding: 1.1rem 1.4rem;
	border-radius: var(--control-radius);
	background: color-mix(in srgb, var(--accent) 6%, var(--surface));
	border: 2px solid var(--accent);
	color: var(--fg);
	box-shadow: 0 4px 16px rgb(0 0 0 / 0.25);
	font-size: 1.05rem;
	cursor: pointer;
	opacity: 0;
	transform: translateY(-2rem);
	transition:
		opacity 0.4s ease,
		transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);

	&.visible {
		opacity: 1;
		transform: translateY(0);
	}

	/* Added alongside .visible and wins on source order. Both properties
	   must share one duration and easing: ts/toast.ts removes the node on
	   the first `transitionend` ({ once: true }), so a mismatched pair would
	   cut the slower one off mid-flight. */
	&.leaving {
		opacity: 0;
		transform: translateY(-1rem);
		transition:
			opacity 1s cubic-bezier(0.16, 1, 0.3, 1),
			transform 1s cubic-bezier(0.16, 1, 0.3, 1);
	}
}

.toast-message {
	flex: 1 1 auto;
	/* Multi-line messages (the Reset & Reload summary) keep their breaks. */
	white-space: pre-line;
}

/* Clears the corner-pinned close button on a persistent toast. */
.toast:has(.toast-close) .toast-message {
	padding-right: 2.1rem;
}

/* A sprite icon rather than a "×" glyph, whose centering depends on font
   metrics. currentColor tint adapts to both --fg and .toast-error's
   --danger-fg. */
.toast-close {
	position: absolute;
	top: 0.6rem;
	right: 0.6rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 1.4rem;
	height: 1.4rem;
	border-radius: 50%;
	background: color-mix(in srgb, currentColor 15%, transparent);
	border: none;
	padding: 0;
	margin: 0;
	color: inherit;
	/* The 1em icon resolves against this, a little under the 1.4rem circle. */
	font-size: 0.85rem;
	cursor: pointer;
	opacity: 0.7;

	&:hover,
	&:focus-visible {
		opacity: 1;
		background: color-mix(in srgb, currentColor 25%, transparent);
	}
}

.toast-error {
	background: var(--danger);
	border-color: var(--danger);
	color: var(--danger-fg);
}

@media (prefers-reduced-motion: reduce) {
	.toast {
		transition-duration: 0.01ms;
	}
}
