/* ═══════════════════════════════════════════════════════════════════════
   Forms, modals & tables — v3
   Loads after /assets/style.css (the compiled theme), before app.css.

   WHAT CHANGED IN v3, AND WHY
   ---------------------------------------------------------------------
   v2 was written for the SIDEBAR shell, and it was driven by erp-ui.js.
   Both are gone. What was left behind was not neutral — it was actively
   breaking two things:

   1. THE PAGE LAYOUT.  v2 §0 turned #content into its own scrollport
      (max-height: 100dvh - var(--erp-shell-offset); overflow-y: auto),
      because in the sidebar shell #content WAS the scroller. In the
      top-nav shell the document scrolls and #content is an ordinary
      block. app.css says so in a comment, but it only re-declared
      max-width / margin / padding, so max-height and overflow-y survived
      and nothing cancelled them.

      Measured on a 40-row table at 1440x900: #content clientHeight 788px
      against scrollHeight 1861px — 1073px of the table sealed behind a
      second scrollbar, while the page itself only scrolled 228px. The
      last row sat 1036px below the inner scroller's own bottom edge, and
      the footer was stranded at y=1019 on a 900px viewport. Two nested
      scrollbars, and no way to reach the bottom of a long list with the
      wheel. That rule is deleted, not overridden.

      --erp-shell-offset made this worse: erp-ui.js measured it at runtime
      and no longer exists, so it was permanently frozen at its 7rem
      fallback — a number describing a shell that is no longer rendered.
      The variable is gone with it.

   2. LONG MODALS.  v2 capped the dialog and scrolled its body, but every
      one of those rules was keyed on .modal-dialog-scrollable, and
      erp-ui.js was the only thing that ever added that class. Without it
      a long form simply grew past the viewport, and because Bootstrap's
      .modal-dialog-centered centres with align-items:center, the overflow
      spilled off BOTH edges — so the Save button could not be reached by
      scrolling either.

      Measured on the add form at three common laptop sizes, Save landed
      at y=903..938 with viewports ending at 720 / 768 / 900.
      elementFromPoint returned nothing: the button was not merely awkward,
      it was unclickable. That is the "add form modal doesn't work" report.

      §1 below now does the same job with NO class and NO JavaScript: the
      dialog is capped against the viewport, the form is the flex column,
      and the body is the only part that scrolls. Header and footer stay
      pinned. It cannot regress the way a JS-applied class can.

   Also removed: the student picker, image picker, .form-section,
   .table-erp / .table-scroll, .erp-filters, .is-dense and the
   #sidebar / .content-header / #page-loader print rules. Those came from
   a school ERP — admissions, fee collection, tabulation sheets — and are
   referenced by exactly zero templates in this project. ~300 lines.
   ═══════════════════════════════════════════════════════════════════════ */

:root {
	--erp-gutter: 1.5rem;
	--erp-label:  .8125rem;
	--erp-help:   .6875rem;
	--erp-radius: .5rem;
}


/* ═══ 1. MODALS ════════════════════════════════════════════════════════
   The rule: a dialog is never taller than the window, and the Save button
   is never the thing that falls off the bottom.

   The cap goes on .modal-dialog rather than .modal-content so it survives
   .modal-dialog-centered, which sets min-height on the same element. Both
   resolve to roughly the viewport, so the dialog fills the space it is
   allowed and its content is what gives.

   These selectors match the markup this project actually ships:
   .modal-content > .modal-header + <form> > (.modal-body + .modal-footer).
   v2 also had rules for `> form > .modal-header`, which never matched
   anything — the header is a SIBLING of the form, not a child of it.
   ═════════════════════════════════════════════════════════════════════ */

/* A DEFINITE height, not max-height. This is the whole trick, and getting it
   wrong is silent: `max-height: 100%` on .modal-content resolves against its
   containing block, and a percentage max-height against a parent whose own
   height is auto computes to `none`. .modal-dialog-centered only sets
   min-height, so the parent stayed auto and the cap did nothing — measured
   .modal-content max-height resolving to 100% while the dialog sat at
   height:auto and grew to 923px inside a 720px window.

   `height` here is definite because .modal is position:fixed at height:100%,
   so the percentage has a real basis. It is also exactly what Bootstrap's own
   .modal-dialog-scrollable does — the difference is that this applies to every
   dialog, with no class for a deleted script to forget to add.

   A SHORT dialog does not get stretched by this: .modal-dialog-centered is
   display:flex with align-items:center, so .modal-content stays its content
   height and simply centres in the taller box. */
.modal-dialog {
	height: calc(100% - var(--bs-modal-margin) * 2);
}

.modal-content {
	max-height: 100%;
	overflow: hidden;          /* the body scrolls, not the dialog */
}

/* The <form> is the flex column. Without flex:1 1 auto AND min-height:0 it
   refuses to shrink below its content and the cap above achieves nothing —
   min-height:auto on a flex item is the default that eats this every time. */
.modal-content > form {
	display: flex;
	flex-direction: column;
	flex: 1 1 auto;
	min-height: 0;
}

.modal-content > .modal-header,
.modal-content > form > .modal-footer,
.modal-content > .modal-footer { flex: 0 0 auto; }

.modal-content > form > .modal-body,
.modal-content > .modal-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	overscroll-behavior: contain;
	scrollbar-width: thin;
	scroll-padding-block: 1rem;
}

.modal-body::-webkit-scrollbar { width: 8px; }
.modal-body::-webkit-scrollbar-thumb {
	background: var(--line-strong, rgba(0, 0, 0, .18));
	border-radius: 4px;
}

/* Short laptop screens: never let the body collapse to nothing. Better to
   scroll a 10rem window than to stare at a header and a footer. */
@media (max-height: 700px) {
	.modal-content > form > .modal-body,
	.modal-content > .modal-body { min-height: 9rem; }
}

/* Hairlines appear once the body has actually scrolled away from the pinned
   edges, so "pinned" reads as pinned instead of as a gap. app.js toggles
   these; if the script is absent the dialog still works, it is just flat. */
.modal-header.is-stuck { box-shadow: 0 1px 0 var(--line, rgba(0, 0, 0, .1)); }
.modal-footer.is-stuck {
	box-shadow: 0 -1px 0 var(--line, rgba(0, 0, 0, .1)),
	            0 -8px 16px -14px rgba(0, 0, 0, .35);
}

.modal-header { padding: .875rem 1.25rem; }
.modal-title  { font-size: .9375rem; font-weight: 600; }
.modal-body   { padding: 1.25rem; }
.modal-footer { padding: .75rem 1.25rem; gap: .5rem; }
.modal-footer > * { margin: 0; }

/* ── The close button ─────────────────────────────────────────────────
   The compiled theme ships `--bs-btn-close-bg: none` and the markup has no
   text, so .btn-close rendered as a completely blank 38x38 box: every
   dialog in the application had an invisible close control. Measured
   background-image: none, textContent: "".

   The glyph is restored here as an inline SVG so it is fixed for every
   .btn-close everywhere — modals, dismissible alerts, anything added
   later — with no template edits. currentColor is not available inside a
   data URI, so the stroke is the ink neutral and opacity does the rest. */
.btn-close {
	--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2317150F' stroke-width='1.8' stroke-linecap='round'%3e%3cpath d='M3 3l10 10M13 3L3 13'/%3e%3c/svg%3e");
	--bs-btn-close-opacity: .55;
	--bs-btn-close-hover-opacity: 1;
	border-radius: 50%;
	transition: background-color .12s ease, opacity .12s ease;
}
.btn-close:hover { background-color: var(--bg, rgba(0, 0, 0, .05)); }


/* ═══ 2. FORM CONTROLS ═════════════════════════════════════════════════ */

.form-label {
	font-size: var(--erp-label);
	font-weight: 500;
	margin-bottom: .3rem;
}
.form-label .req,
.form-label .text-danger { color: var(--bs-danger); margin-left: .1rem; }

.form-text {
	font-size: var(--erp-help);
	line-height: 1.4;
	margin-top: .25rem;
	color: var(--bs-secondary-color);
}

.form-control, .form-select {
	font-size: .8125rem;
	min-height: 2.25rem;
}
.form-control[readonly] { background: var(--bs-tertiary-bg); }

/* Money and quantity fields. Right-aligned with tabular figures — a column
   of amounts you cannot scan by eye is a column you cannot check. */
.form-control.is-amount,
input[inputmode="decimal"],
input[type="number"] {
	text-align: right;
	font-variant-numeric: tabular-nums;
	font-feature-settings: "tnum";
}
.input-group-text.unit {
	font-size: .75rem;
	color: var(--bs-secondary-color);
	background: var(--bs-tertiary-bg);
}

/* Validation. Bootstrap only paints :invalid after .was-validated, which is
   correct — nobody wants a form that is red before it has been touched. */
.was-validated .form-control:invalid,
.was-validated .form-select:invalid { border-color: var(--bs-danger); }

.form-error-summary {
	background: var(--bs-danger-bg-subtle, rgba(220, 53, 69, .08));
	border-radius: var(--erp-radius);
	padding: .75rem 1rem;
	margin-bottom: 1rem;
	font-size: .8125rem;
}
.form-error-summary ul { margin: .4rem 0 0; padding-left: 1.1rem; }
.form-error-summary a  { color: inherit; }

/* Save button while posting. Blocks the double submit — on a deposit or a
   voucher screen a second POST is a second ledger row. app.js sets .is-busy
   on submit; it never sets `disabled`, because a disabled submit button
   does not send its own name/value and some of these forms rely on that. */
.btn.is-busy { pointer-events: none; opacity: .65; }
.btn.is-busy > .spinner-border { display: inline-block !important; }
.btn > .spinner-border { display: none; width: .85rem; height: .85rem; border-width: .15em; }

/* Unsaved-changes hint. app.js marks the form dirty on first input. */
.form-dirty-hint {
	display: none;
	margin-right: auto;
	font-size: var(--erp-help);
	color: var(--bs-warning-text-emphasis, var(--bs-secondary-color));
}
form.is-dirty .form-dirty-hint { display: inline-flex; align-items: center; gap: .3rem; }


/* ═══ 3. TABLES ════════════════════════════════════════════════════════
   The project's tables are plain Bootstrap .table; app.css owns their
   look. All that is needed here is the numeric column treatment and a
   readable empty state. */

.table .num,
.table th.num {
	text-align: right;
	font-variant-numeric: tabular-nums;
	font-feature-settings: "tnum";
	white-space: nowrap;
}
.table tfoot .num { font-weight: 600; }

/* An empty screen is an invitation to act, not a shrug. */
.table-empty {
	text-align: center;
	padding: 3rem 1rem;
	color: var(--bs-secondary-color);
}
.table-empty > i { font-size: 2rem; opacity: .45; display: block; margin-bottom: .5rem; }
.table-empty > .table-empty-title { font-weight: 500; color: var(--bs-body-color); }
