/* ==========================================================================
   forms-standard.css — THE FORM CONTROL STANDARD (border + focus ring).

   Linked LAST from all five layouts:
     • layouts/app.php, layouts/dashboard.php   (primunity.css bundle)
     • layouts/auth.php, layouts/marketing.php, layouts/admin.php
       (site.css bundle — linked AFTER motion-standard-compat.css, so the
        tokens site.css lacks are already aliased when this file resolves)

   WHAT THIS SHEET EXISTS TO ENFORCE
   ---------------------------------
   Every text-entry control in the product wears ONE treatment:

     rest      crisp 1px hairline (--line-control), the component's recessed
               inner shadow preserved, ring present at 0 spread / 0 alpha
     hover     the hairline warms toward brand (--line-brand)
     focus     the hairline goes brand (--focus-line) + ONE tight ring
               (3px spread, ZERO blur, low alpha). Never a thick blurred halo,
               never two stacked halos, never a ring drawn on a wrapper that
               has no border-radius of its own.
     invalid   identical geometry, danger hue only
     disabled  dimmed hairline, no hover/focus reaction, not-allowed cursor
     readonly  muted surface, still focusable, still rings (people copy out of
               readonly fields and must be able to see where they are)

   The reference implementation is the booking form (scheduling.css, the
   `.book-wrap .input` block). That treatment replaced a muddy 4px/18% wash
   stacked on top of a SECOND square shadow drawn on the borderless
   `.input-group` wrapper. This file promotes exactly that geometry to a
   CMS-wide standard, so login, dashboard, portal, superadmin console and
   booking all express focus the same way.

   WHY A RING AND NOT AN OUTLINE
   -----------------------------
   A spread-only `box-shadow` hugs `border-radius` exactly, composites over the
   recessed inner shadow instead of replacing it, and — being semi-transparent
   — stays visible over EVERY surface the app paints: the page background, a
   `.card`, a `.panel`, a modal, a dropdown popover. Forced-colors mode gets a
   real `outline` instead (§11), because there the engine drops box-shadows.

   TOKENS
   ------
   Every value resolves through a `--fs-*` alias with a full fallback chain, so
   this sheet behaves identically on BOTH bundles:
     • primunity.css defines --line-control / --line-brand / --focus-line /
       --danger-fg / --inset-sunken and remaps them all for dark.
     • site.css defines none of those; the chain falls back to its --border-2 /
       --brand / --err, which site.css DOES remap for dark. Note that site.css
       also references an undefined `--ring` in `.input:focus`, so today the
       LOGIN screen's focused field gets no ring at all — this sheet is what
       finally gives auth.php a visible focus indicator.
   Nothing here reads `var(--focus-ring)`: on the primunity bundle that token is
   a 4px box-shadow, but motion-standard-compat.css defines it on the site
   bundle as a COLOUR. Same name, two types — so the standard composes its own.

   SPECIFICITY POLICY (read before touching a selector)
   ----------------------------------------------------
   Page-scoped module sheets (posts.css, scheduling.css, portal-home.css, …)
   are <link>ed AFTER the layout's own stylesheets, and turbo's page-style sync
   appends more of them at RUNTIME — so being last in <head> is NOT enough.
   Same convention as motion-standard.css / overlay-fx.css:

     * selectors are written `html :is(…)`. An `:is()` list takes the MAX
       specificity of its arguments; the heaviest argument here is
       `input:is([type="text"], …)` = (0,1,1), so BASE rules land at (0,1,2)
       and STATE rules at (0,2,2).
     * (0,2,2) beats every single-class component state rule in the app:
       `.input:focus` (0,2,0), `.phome-notes__input:focus-visible` (0,2,0),
       `.sb2-search__input:focus` (0,2,0), `.topbar__search input:focus` (0,2,1),
       `.biz-selector__filterfield input:focus-visible` (0,2,2, and we load later).
     * (0,2,2) deliberately LOSES to any two-class page rule —
       `.book-wrap .input:focus` (0,3,0), `.srch-field .input:focus` (0,3,0) —
       so a component that really means to differ still can, by writing a
       scoped two-class rule. That is the documented way to opt out.
     * the resting-border rule for design-system controls (§3) is held DOWN at
       (0,1,2) on purpose, so `.srch-field .input { border:0 }` (0,2,0) still
       wins and the search bar stays one framed group instead of a box in a box.
     * per-component exemptions (§10) are expressed as `:not(:where(…))`.
       `:where()` contributes ZERO specificity, and `:not()` inherits the
       specificity of its argument — so the guard excludes without inflating
       the selector past (0,2,2). This is why the exemption list is repeated
       inline rather than written as a separate "undo" rule: an undo rule would
       have to use `revert`, which rolls back to the USER-AGENT origin, not to
       the component's own author rule, and would therefore erase the very
       styling it was meant to protect.
     * no @layer — unlayered page rules would beat any layer we declared.

   WHAT THIS SHEET DOES **NOT** TOUCH
   ----------------------------------
   Geometry (padding, radius, width, font) of CLASSED controls; checkboxes;
   radios; the `.switch` toggle; `input[type="range"]`; `input[type="color"]`;
   buttons; and the bespoke chip / segment / preference-card pickers. None of
   those are text boxes, and the ask was about the text-box border.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. TOKENS
   -------------------------------------------------------------------------- */
:root{
  /* Hairline at rest. primunity: --line-control (4.35:1 on dark — clears WCAG
     1.4.11, where the border is the control's only affordance). site: --border-2. */
  --fs-line: var(--line-control, var(--border-2, #D2DBEC));
  /* Hover warms the hairline toward brand without committing to focus. */
  --fs-line-hover: var(--line-brand, color-mix(in srgb, var(--brand, #2563EB) 42%, var(--fs-line)));
  /* Focus hairline + ring pigment. --focus-line is theme-resolved (#1E50C8
     light / #6F9DFF dark); on the site bundle --brand is likewise remapped
     (#2563EB / #3B82F6). Deliberately NOT --brand-2: that is a single mid-dark
     blue with no dark value, so a ring mixed from it vanishes on dark surfaces. */
  --fs-focus: var(--focus-line, var(--brand, #2563EB));
  --fs-danger: var(--danger-fg, var(--err, #E11D48));

  /* Ring geometry — spread only, zero blur. 3px is the booking-form value. */
  --fs-ring-w: 3px;
  --fs-ring-a: 20%;
  --fs-ring-off: 0 0 0 0 color-mix(in srgb, var(--fs-focus) 0%, transparent);
  --fs-ring-on:  0 0 0 var(--fs-ring-w) color-mix(in srgb, var(--fs-focus) var(--fs-ring-a), transparent);
  --fs-ring-err: 0 0 0 var(--fs-ring-w) color-mix(in srgb, var(--fs-danger) var(--fs-ring-a), transparent);

  /* The recessed inner well, preserved from components.css so fields keep
     reading as sunken. It is listed in EVERY box-shadow below, in the same
     position, so the shadow lists stay layer-for-layer identical and the ring
     INTERPOLATES instead of snapping (motion standard, mistake #3). */
  --fs-inset: inset 0 1px 2px rgba(11,27,58,.05);
  --fs-inset-2: var(--inset-sunken, inset 0 0 0 0 rgba(0,0,0,0));

  --fs-dur: var(--dur, 200ms);
  --fs-ease: var(--ease, cubic-bezier(.4,0,.2,1));
}

/* Dark needs a hotter ring: 20% alpha over a #0A1122 page reads as nothing.
   Both bundles are covered — the explicit attribute AND the first-paint OS
   preference, which primunity.css and site.css both honour. */
[data-theme="dark"]{
  --fs-ring-a: 34%;
  --fs-inset: inset 0 1px 2px rgba(0,0,0,.22);
}
@media (prefers-color-scheme: dark){
  :root:not([data-theme]){
    --fs-ring-a: 34%;
    --fs-inset: inset 0 1px 2px rgba(0,0,0,.22);
  }
}

/* --------------------------------------------------------------------------
   2. BASE — the animated contract, for every text-entry control.

   MOTION STANDARD: the transition lives on the BASE rule and names EVERY
   property the state rules below move, so focus fades IN and OUT symmetrically.
   The rest box-shadow is a THREE-layer list whose first layer is the ring at
   zero spread / zero alpha; §6 swaps only that first layer's spread + alpha,
   keeping the list layer-for-layer identical.
   -------------------------------------------------------------------------- */
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"], [type="file"]),
  textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger
):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *,
  .biz-selector__search *, .pf-ms__search, .sb2-title__input, .mm2__select
)){
  transition:
    border-color var(--fs-dur) var(--fs-ease),
    box-shadow var(--fs-dur) var(--fs-ease),
    background-color var(--fs-dur) var(--fs-ease),
    color var(--fs-dur) var(--fs-ease),
    opacity var(--fs-dur) var(--fs-ease);
  box-shadow: var(--fs-ring-off), var(--fs-inset), var(--fs-inset-2);
}

/* --------------------------------------------------------------------------
   3. REST BORDER — design-system boxed controls.

   Held at (0,1,2) so a two-class page rule still wins. These already own their
   padding / radius / background from components.css | site.css |
   core/datepicker.css — only the hairline is restated here.
   -------------------------------------------------------------------------- */
html :is(.input, .textarea, select.input, .pmu-dropdown__trigger, .pmu-dp__trigger):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *
)){
  border-width: 1px;
  border-style: solid;
  border-color: var(--fs-line);
}

/* --------------------------------------------------------------------------
   4. REST BORDER — bare, unclassed native controls.

   `:not([class])` is the whole guard: a control carrying ANY class is owned by
   its module and receives only the STATE treatment (§5–§8), so bespoke widgets
   keep their geometry. Unclassed controls — the superadmin console filter rows,
   ad-hoc admin forms — currently render as raw UA boxes; they get the hairline
   and the radius and nothing else. Background is left alone on purpose:
   `color-scheme` is set per theme, so a UA-painted control is already dark in
   dark mode, and overriding it would fight the modules that style these by
   descendant selector (e.g. `.biz-selector__filterfield input`).
   -------------------------------------------------------------------------- */
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"]):not([class]),
  textarea:not([class]), select:not([class])
):not(:where(
  [data-form-standard="off"], [data-form-standard="off"] *, .biz-selector__search *
)){
  border: 1px solid var(--fs-line);
  border-radius: var(--r-sm, 10px);
  color: var(--text, inherit);
  font: inherit;
}

/* --------------------------------------------------------------------------
   5. HOVER — the hairline warms. No ring yet.
   -------------------------------------------------------------------------- */
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"], [type="file"]),
  textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger
):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *,
  .biz-selector__search *, .pf-ms__search, .sb2-title__input, .mm2__select,
  :disabled, [disabled], [aria-disabled="true"]
)):hover{
  border-color: var(--fs-line-hover);
}

/* --------------------------------------------------------------------------
   6. FOCUS — one crisp brand hairline + one tight ring. `outline:none` is safe
   ONLY because the ring replaces it; forced-colors restores a real outline in
   §11.

   Both `:focus` and `:focus-visible` are listed: text fields must ring on
   pointer focus too (people click into them). The two custom TRIGGER buttons
   already scope themselves to `:focus-visible` in their own sheets, and the
   geometry here matches either way.
   -------------------------------------------------------------------------- */
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"], [type="file"]),
  textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger
):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *,
  .biz-selector__search *, .pf-ms__search, .sb2-title__input, .mm2__select
)):is(:focus, :focus-visible){
  outline: none;
  border-color: var(--fs-focus);
  box-shadow: var(--fs-ring-on), var(--fs-inset), var(--fs-inset-2);
}

/* A wrapper must never paint a SECOND halo. `.input-group` is a borderless,
   unrounded flex box — a ring on it renders as a square smear around a rounded
   field. It only tints its leading glyph, exactly as the booking form does. */
html .input-group.input-group{ border: 0; box-shadow: none; }
html .input-group.input-group:focus-within{ box-shadow: none; }
html .input-group .icon{ transition: color var(--fs-dur) var(--fs-ease); }
html .input-group:hover .icon{ color: var(--text-2, var(--text)); }
html .input-group:focus-within .icon{ color: var(--brand-fg, var(--brand, #2563EB)); }

/* --------------------------------------------------------------------------
   7. INVALID — same geometry, danger hue only.

   `:user-invalid` fires only after the user has interacted, so a blank required
   field is not red on load. It sits inside `:is()`, which is FORGIVING: an
   engine that does not know the selector drops that one argument instead of
   invalidating the whole rule. `[aria-invalid="true"]` and the `.field--error`
   wrapper are the hooks the app already ships server-side.
   -------------------------------------------------------------------------- */
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"]),
  textarea, select
):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *,
  .biz-selector__search *, .pf-ms__search, .sb2-title__input, .mm2__select
)):is(:user-invalid, [aria-invalid="true"]),
html .field--error :is(.input, .textarea, select.input){
  border-color: var(--fs-danger);
}
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"]),
  textarea, select
):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *,
  .biz-selector__search *, .pf-ms__search, .sb2-title__input, .mm2__select
)):is(:user-invalid, [aria-invalid="true"]):is(:focus, :focus-visible),
html .field--error :is(.input, .textarea, select.input):is(:focus, :focus-visible){
  border-color: var(--fs-danger);
  box-shadow: var(--fs-ring-err), var(--fs-inset), var(--fs-inset-2);
}

/* --------------------------------------------------------------------------
   8. DISABLED / READONLY
   -------------------------------------------------------------------------- */
html :is(
  input:is(:not([type]), [type="text"], [type="email"], [type="password"],
           [type="search"], [type="tel"], [type="url"], [type="number"],
           [type="date"], [type="datetime-local"], [type="month"],
           [type="week"], [type="time"], [type="file"]),
  textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger
):not(:where(
  .fs-exempt, [data-form-standard="off"], [data-form-standard="off"] *
)):is(:disabled, [disabled], [aria-disabled="true"]){
  opacity: .55;
  cursor: not-allowed;
  border-color: color-mix(in srgb, var(--fs-line) 60%, transparent);
  box-shadow: var(--fs-ring-off), var(--fs-inset), var(--fs-inset-2);
}

/* Matched on the ATTRIBUTE, not `:read-only`: per spec a `<select>`, a button
   and every non-editable control also match `:read-only`, which would paint
   half the app as "readonly". A readonly field stays focusable and still rings
   (§6) — it just reads as a surface you cannot type into. */
html :is(input, textarea)[readonly]{
  background-color: var(--surface-2, var(--bg-2, transparent));
  cursor: default;
}

/* --------------------------------------------------------------------------
   9. FILE INPUTS — the control follows the standard; the browser's own button
   is restyled to the app's secondary-button look so the pair does not read as
   a UA leftover. `margin-inline-end` keeps the gap correct in RTL. Most file
   inputs in the app carry the `hidden` attribute (a styled label triggers
   them), and those are skipped.
   -------------------------------------------------------------------------- */
html input[type="file"]:not([hidden]):not(.fs-exempt){
  padding: 8px 10px;
  border: 1px solid var(--fs-line);
  border-radius: var(--r-sm, 10px);
  color: var(--text, inherit);
  font: inherit;
  cursor: pointer;
}
html input[type="file"]::file-selector-button{
  margin-inline-end: 10px;
  padding: 6px 12px;
  border: 1px solid var(--fs-line);
  border-radius: var(--r-sm, 10px);
  background: var(--surface-2, var(--bg-2, transparent));
  color: var(--text, inherit);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  transition: border-color var(--fs-dur) var(--fs-ease),
              background-color var(--fs-dur) var(--fs-ease),
              color var(--fs-dur) var(--fs-ease);
}
html input[type="file"]::file-selector-button:hover{
  border-color: var(--fs-line-hover);
  color: var(--brand-fg, var(--brand, #2563EB));
}

/* Date/time pickers: the UA indicator is a black glyph, which disappears on a
   dark field. Give it a hover fade and invert it in dark. */
html :is(input[type="date"], input[type="time"], input[type="datetime-local"],
         input[type="month"], input[type="week"])::-webkit-calendar-picker-indicator{
  cursor: pointer;
  opacity: .6;
  transition: opacity var(--fs-dur) var(--fs-ease);
}
html :is(input[type="date"], input[type="time"], input[type="datetime-local"],
         input[type="month"], input[type="week"]):hover::-webkit-calendar-picker-indicator{
  opacity: 1;
}
[data-theme="dark"] input::-webkit-calendar-picker-indicator{ filter: invert(1); }
@media (prefers-color-scheme: dark){
  :root:not([data-theme]) input::-webkit-calendar-picker-indicator{ filter: invert(1); }
}

/* --------------------------------------------------------------------------
   10. EXEMPTIONS — what the `:not(:where(…))` guards above exclude, and why.

   NEW code should use the generic hooks:
     • `class="… fs-exempt"` on the control, or
     • `data-form-standard="off"` on the control or any ancestor.

   The four named components are excluded because none of them is a standalone
   text box:
     • `.biz-selector__search input` — a borderless input INSIDE a wrapper that
       already draws the frame and rings on `:focus-within`. Standardising it
       would produce a box inside a box and a double halo.
     • `.pf-ms__search` — same pattern inside the posts multi-select popover.
     • `.sb2-title__input` — the SiteBuilder inline page-title editor. Its
       resting border is transparent BY DESIGN (it must read as a heading, not
       a field) and it grows its frame only on hover/focus.
     • `.mm2__select` — the muscle-map viewer's themed select, which rings in
       the map's own accent so it stays legible over the canvas.

   Components that keep their own treatment WITHOUT needing a guard, purely
   because their two-class rules out-specify (0,2,2):
     • `.book-wrap .input` / `.textarea` / `select.input` (the booking form —
       the reference implementation, identical geometry already);
     • `.srch-field .input` (the global search bar, borderless inside a frame).
   -------------------------------------------------------------------------- */

/* --------------------------------------------------------------------------
   11. FORCED COLORS / HIGH CONTRAST
   Those engines drop box-shadows, so the ring would simply vanish. Hand the
   job back to a real system-coloured outline and let the system paint borders.
   -------------------------------------------------------------------------- */
@media (forced-colors: active){
  html :is(input, textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger){
    border-color: ButtonBorder;
    box-shadow: none;
  }
  html :is(input, textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger):is(:focus, :focus-visible){
    outline: 2px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
  html :is(input, textarea, select):is(:disabled, [disabled]){
    border-color: GrayText;
    opacity: 1;
  }
}

/* --------------------------------------------------------------------------
   12. REDUCED MOTION — the states still change, they just stop animating.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce){
  html :is(input, textarea, select, .pmu-dropdown__trigger, .pmu-dp__trigger),
  html input[type="file"]::file-selector-button,
  html .input-group .icon{
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   13. THE ERROR MESSAGE — the non-colour half of "this field is wrong".

   §7 above paints an invalid control in the danger hue and nothing else. That
   was the whole signal, and a hue is not a signal: measured in a real browser,
   an empty required "Full name" produced a `.field--error` wrapper whose entire
   text content was the LABEL — no message anywhere — so a colour-blind user and
   a screen-reader user were told nothing at all. WCAG 1.4.1 (Use of Colour).

   core/form-errors.js injects the missing message; this section is what makes it
   read as one. TEXT plus a GLYPH carry the meaning; the hue is decoration on top,
   which is the correct order of those two.

   `.fe-msg` composes with the app's existing `.field__error` (components.css:169
   for the console, site.css:675 for auth/marketing) rather than restating its
   colour or size — one definition of what an error message looks like, extended
   here with the layout the glyph needs.

   RTL: flex + `gap` + logical properties only, so Hebrew needs no second rule.
   -------------------------------------------------------------------------- */
html .fe-msg{
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin-block-start: 6px;
  text-align: start;
}
html .fe-msg[hidden]{ display: none; }
html .fe-msg__icon{
  flex: none;
  inline-size: 1em;
  block-size: 1em;
  margin-block-start: .15em;
}
html .fe-msg__text{ min-inline-size: 0; }

/* The summary box becomes focusable (tabindex="-1") when a form-level failure
   has no single field to blame, so it must show WHERE focus went. Same ring
   geometry as §6 — a focused thing in this product looks like one thing. */
html [data-form-error][tabindex]:focus-visible{
  outline: none;
  box-shadow: var(--fs-ring-err);
}

@media (forced-colors: active){
  /* Engines here drop the danger hue entirely, which is precisely why the text
     and the glyph had to exist: they survive. Keep the glyph visible. */
  html .fe-msg__icon{ forced-color-adjust: none; color: currentColor; }
  html [data-form-error][tabindex]:focus-visible{
    outline: 2px solid Highlight;
    outline-offset: 2px;
    box-shadow: none;
  }
}
