/* =============================================================================
   login.css — [P5-L3] the login entry's own sheet. GENERATED; do not hand-edit.
   =============================================================================
   WHAT IS NOT HERE, and why that is the point. Every class the drawn markup uses
   (rg-field, rg-field__label, rg-input, rg-button--primary, rg-mono) is already
   defined in /design/export/components.css, which this entry links. The drawn
   frames' own geometry is INLINE in the extracted markup. So this sheet carries
   only what nothing else provides: the entry's page chrome, the host that turns
   a drawn artboard into a page, and the viewport switch between the two drawn
   compositions.

   THE FORBIDDEN OVERRIDE KEYWORD DOES NOT APPEAR IN THIS FILE, and that is not a
   near miss - it is the one rule a surface like this cannot satisfy by layering,
   because @layer never reaches an element-attached declaration. Two measurements
   are what make it hold rather than hope:
     · this sheet DOES resize both drawn roots (inline "height: 780px" desktop,
       "height: 760px" mobile) and still needs no override keyword, because it
       clamps with min-block-size/max-block-size - DIFFERENT properties from the
       inline "height", so the used height is bounded whatever the height came
       from and nothing is out-specified. (An earlier revision of this paragraph
       claimed the sheet "does not try" to resize a drawn root. It was written
       before the two viewport-fit passes and it was left stale through both;
       a docblock that describes an older build is a claim, and this one was
       false in the exact place a reader would trust it.)
     · the viewport switch sets `display`, and NEITHER drawn root carries an
       inline `display` (measured: both roots' inline declarations are position,
       width, height, background, border, border-radius, overflow). So the rule
       is UNCONTESTED and needs nothing to out-specify.
   css-ratchet reads 0 for this file: no id selectors (selector-max-id: 0), nothing
   above 0,2,0 (selector-max-specificity), no hex, no named colour, no disallowed
   colour function - every value is a var() the design export defines.
   (This paragraph names the keyword obliquely on purpose. Spelling it made the
   contract test's own probe match its explanation - PB-00027, the instrument
   inside its own population. Reword, never loosen the probe.)
   ============================================================================= */
@layer surfaces {
  .glyph-login {
    min-height: 100dvh;
    display: grid;
    place-items: center;
    background: var(--color-page);
    color: var(--color-text);
    /* With every composition clamped to at most one viewport (below), the host is
       at most one viewport, so there is nothing to scroll TO. Declared anyway,
       because a clamp that is ever defeated must CLIP rather than silently restore
       the scroll those clamps exist to remove. */
    overflow: hidden;
  }

  /* Each drawn root is full-bleed horizontally, which is what the 1440 artboard
     draws too (width: 100%, a band). Each keeps its drawn height. */
  .glyph-login > [data-glyph-login] {
    width: 100%;
    max-width: 100%;
  }

  /* =====================================================================
     [DESKTOP-VIEWPORT-FIT] The DESKTOP composition is clamped too.
     =====================================================================
     🔴 THE MOBILE FIX WAS ONLY EVER HALF THE DEFECT, and the other half was
     still live. The desktop drawn root carries `height: 780px` INLINE, the
     same way the mobile one carried 760 — and the host only asked for
     `min-height: 100dvh`. So on any desktop viewport shorter than 780 the
     login SCROLLED. Measured on the served preview at 1280x720 — a laptop, or
     any window with devtools docked — `documentElement.scrollHeight` read 780
     against a 720 viewport. It survived because 1440x900 and 1920x1080 both
     happen to be taller than 780, which is the same reason the mobile bug
     survived at 390x844: the sizes everybody tests are the sizes that fit.

     `max-block-size` ONLY — this clamps DOWN and never up, so every viewport
     at or above the drawn 780px renders byte-for-byte as drawn and the
     composition the owner has been reviewing does not move. Below it, the box
     is the viewport: the canvas is drawn `inset: 0` so it follows, the engine
     re-measures on `resize`, and the mark keeps its drawn proportion of the
     smaller stage exactly as it does on mobile.

     Same lever as the mobile clamp and for the same reason: an inline
     declaration outranks every layered rule for the SAME property, but
     `min-block-size`/`max-block-size` are DIFFERENT properties and bound the
     used height whatever the height came from — so no override keyword, and
     not one byte of extracted markup re-typed.

     🔴 MIN AS WELL AS MAX SINCE [R-OV-13] (owner ruling, 2026-08-28): the stage
     FILLS the viewport rather than merely fitting inside it. With max alone the
     drawn 780px root letterboxed on anything taller — measured 150px of dead
     ground top AND bottom at 1920x1080, with the particle field stopping short
     of all four edges against the uniform ground. The owner's ruling is "crop
     the clear space to the screen", so the box is now EXACTLY one viewport in
     both directions.
     What moves with it is drawn, not invented: the canvas is drawn `inset: 0`
     so the field reaches every edge, the engine re-measures on `resize`, the
     mark keeps its drawn 27%-of-canvas proportion, and the form overlay keeps
     the drawing's own `flex-end` — so the form sits the drawn distance off the
     BOTTOM OF THE SCREEN instead of the drawn distance off the bottom of a
     780px band. Nothing here touches the animation.
     ⚠️ Unlike the max-only clamp this DOES move the composition on a viewport
     taller than 780px, which is the point of the ruling. Revert is this one
     declaration. */
  .glyph-login > [data-glyph-login="desktop"] > * {
    min-block-size: 100vh;
    min-block-size: 100dvh;
    max-block-size: 100vh;
    max-block-size: 100dvh;
  }

  /* =====================================================================
     THE RESET LINK IS ONE LINE, AND ON TOUCH IT IS A TARGET.
     =====================================================================
     Measured on the served page: at 1440x900 "Forgot password" wrapped onto
     TWO lines inside its 93px box (the drawn row puts the link and the stage
     caption in one `space-between` flex), and at 390x844 the link measured
     18px tall — against the 44px minimum every other control on this form
     already meets (email 44, password 44, submit 44, all drawn). A 18px
     target in the thumb zone is the one control on the screen a finger
     misses, and it is the recovery path — the control a person reaches for
     precisely when they are already stuck.
     The height is scoped to the mobile composition on purpose: the desktop row
     draws the link BESIDE a caption, and growing it to 44px there would move
     drawn geometry to solve a problem a mouse does not have.

     🔴 THE LOCATOR CARRIES NO TYPE SELECTOR, AND THAT IS THE RATCHET, NOT STYLE.
     `.host a[href="#reset"]` computes to (0,2,1), one step above this repo's
     `selector-max-specificity` of 0,2,0 — so the obvious spelling raised
     `css-ratchet` TOTAL from 1083 to 1085 (measured, both rules), and owner ruling
     R3 says that number only goes DOWN. The attribute alone identifies the drawn
     control exactly as well: it is the reset link's own drawn `href`. */
  .glyph-login [href="#reset"] {
    white-space: nowrap;
  }

  /* THE GUTTER RULE THAT USED TO SIT HERE IS GONE, AND ITS ABSENCE IS THE RECORD OF
     WHY. With the link on one line it exactly filled the drawn 332px `space-between`
     row and TOUCHED the stage caption beside it (measured: link right edge 657, caption
     left edge 657), so a trailing margin was added under a min-width query. [R-OV-14]
     then removed that caption as artboard instrumentation, leaving the link alone in its
     row — at which point the margin styles nothing and a rule that styles nothing is a
     rule the next reader has to disprove. `login-auth-contract` asserts the row now
     carries exactly one child, which is the same fact stated as a measurement. */

  /* =====================================================================
     THE ONE BOUNDARY BETWEEN THE STAGE AND THE FORM — BOTH COMPOSITIONS.
     =====================================================================
     Defined ONCE, outside the media query, since [R-OV-23] (owner ruling,
     2026-08-28). Two things read it: each composition's form overlay (transform
     T5, one per composition) and — on mobile only — the particle canvas clamp.

     🔴 WHY DESKTOP JOINED IT. [R-OV-13] made the desktop stage full-bleed, which
     also made the drawing's own `flex-end` measure from the SCREEN's bottom
     instead of from the bottom of a 780px band: the username input fell from
     61.7% to 70.5% of the viewport at 1440x900. The ruling is to anchor the form
     to the MARK rather than to the screen edge — and the mobile anchor already
     does that. Giving desktop its own number would be two values that must agree,
     which is precisely the defect the [FORM-60] pass removed. So there is ONE
     property and the desktop overlay reads it.

     🔴 IT LANDS THE INPUT AT 60dvh ON EITHER COMPOSITION WITHOUT KNOWING WHICH.
     The subtrahend is the MEASURED distance from the drawn card's top to the
     username input's top, published by `public/js/login-boot.js` from whichever
     composition is actually DISPLAYED, on that composition itself. That gap is content — a label, its wrap, a
     padding token — and it differs between the two compositions, so it is measured
     rather than written down. Whatever it is, the INPUT is what sits at 60%.

     min() is the degradation, not decoration: 60dvh when there is room, and
     bottom-anchored-as-drawn when there is not — the software-keyboard case, where
     without it the Sign in button would leave the screen. The env() floor keeps the
     form clear of a notch on a viewport too short for either. */
  .glyph-login > [data-glyph-login] {
    --glyph-login-form-top: max(
      env(safe-area-inset-top),
      min(calc(60dvh - var(--glyph-login-email-offset, 34px)),
          calc(100dvh - var(--glyph-login-form-block, 274px))));
  }

  /* THE VIEWPORT SWITCH. 768px is the estate's breakpoint, not a new one: it is
     what 6 of the 7 media queries across public/css/surfaces/*.css already use. */
  .glyph-login > [data-glyph-login="mobile"] { display: none; }
  @media (max-width: 768px) {
    .glyph-login > [data-glyph-login="desktop"] { display: none; }
    .glyph-login > [data-glyph-login="mobile"] { display: block; }

    /* =====================================================================
       [MOBILE-VIEWPORT-FIT] The login is LOCKED to the device viewport.
       =====================================================================
       Owner feedback, 2026-08-28, PREVIEW ONLY. The animation is untouched:
       every rule here is box geometry.

       🔴 THE DEFECT, MEASURED BEFORE ANY OF THIS WAS WRITTEN. The drawn root
       carries `height: 760px` INLINE (extracted geometry, R17.1) and the host
       only asked for `min-height: 100dvh`, so on any viewport shorter than
       760px the page SCROLLED and the form ran off the bottom. At 390x664 — a
       real iPhone with its address bar showing — the document measured 760px
       against a 664px viewport: **96px of overflow, and the Sign in button and
       Forgot-password link were BELOW THE FOLD** (card bottom 727 vs viewport
       664). 390x844 happened to fit, which is exactly why this survived: the
       one size everybody tests is the one size that worked.

       WHY `dvh` AND NOT `svh` IS THE PRIMARY. `svh` is the SMALLEST viewport
       (chrome showing) and never changes, so it cannot shrink when the software
       keyboard opens — the form would sit behind the keyboard. `dvh` tracks the
       CURRENT viewport, so with `interactive-widget=resizes-content` on the meta
       the box shrinks with the keyboard and the fields stay on screen. `overflow`
       is hidden and the drawn root is clamped to this box, so the growth/shrink
       of `dvh` as the address bar hides can never produce a scrollbar — which is
       the usual reason people reach for `svh`. All three are declared: the last
       one an engine understands wins.

       ⚠️ `viewport-fit=cover` ON THE META IS PART OF THIS RULE, not decoration.
       Without it iOS letterboxes the page and every `env(safe-area-inset-*)`
       resolves to 0px, so the insets below would silently do nothing and the
       boundary would sit under the notch. Neither half works alone.

       The gutter is a `max()` of the safe-area inset and a comfort floor, so a
       device with no inset still shows the drawn boundary the owner asked to
       keep, and a notched device gets whichever is larger. */
    .glyph-login {
      box-sizing: border-box;
      block-size: 100vh;      /* floor for an engine with neither unit */
      block-size: 100svh;     /* stable smallest viewport */
      block-size: 100dvh;     /* the exact current viewport — wins where supported */
      min-block-size: 0;
      place-items: stretch;
      /* 🔴 AN EXPLICIT TRACK, BECAUSE A DEFINITE CONTAINER IS NOT A DEFINITE ROW.
         Measured after the first attempt did nothing: the host was correctly 664px
         and `place-items: stretch` was correctly applied, and the drawn root still
         read 760px. The implicit grid row is `auto`, so it sized to ITS CONTENT —
         the 760px drawn box — and the item stretched to that 760px ROW rather than
         to the 664px host. A percentage height on the child then resolved against
         760 and changed nothing, which is why the min/max clamp below looked inert
         while being perfectly applied. `minmax(0, 1fr)` makes the row exactly the
         host's content box and lets it shrink below its content, which is the whole
         point of the `0` — a bare `1fr` has an automatic minimum equal to the
         content and would reproduce the bug. */
      grid-template-rows: minmax(0, 1fr);
      grid-template-columns: minmax(0, 1fr);
      overflow: hidden;
      overscroll-behavior: none;
      /* 🔴 NO PADDING HERE ANY MORE — the safe area MOVED, it was not dropped
         [OWNER-CHROME, 2026-08-28]. While the login had a drawn boundary, insetting the
         HOST was what kept that boundary clear of the notch. The owner has removed the
         boundary, and the requirement became "one background bleeding to EVERY edge": an
         inset host would clip the particle field ~12px early on all four sides against a
         uniform ground, which is the one place that inset would still be visible.
         So the stage is full-bleed and the per-side `max(env(safe-area-inset-*), <drawn>)`
         insets are applied to the FORM OVERLAY instead (generator transform T3), which is
         the element that actually has to stay clear of the notch and the home bar. The
         drawn value is the floor, so a device with no inset renders exactly as drawn. */
    }

    /* NO html/body RULE HERE, AND THAT IS MEASURED RATHER THAN AN OVERSIGHT. The obvious
       instinct is to belt-and-brace with `body { overflow: hidden }`. It was written, and
       then removed after measuring what it bought: with the host exactly one viewport tall,
       clipping, and the design export's reset leaving `body` at margin 0, the document
       already cannot scroll — `documentElement.scrollHeight` equals `innerHeight` at every
       size tested and a real `window.scrollTo(0, 500)` does not move the page. It also cost
       something: the only form specific enough to stay inside this sheet's own subtree used
       `:has()`, which raised `css-ratchet`'s TOTAL from 1083 to 1084 — and owner ruling R3
       says that number must go DOWN. A redundant rule that spends the ratchet is a bad
       trade, and a sheet that reaches for `body` is one link away from styling a document
       that merely includes it. */

    /* 🔴 min + max, NEVER `block-size`, and that is the whole trick. The drawn
       root's `height: 760px` is an INLINE declaration and an inline declaration
       outranks every layered rule for the SAME property — `@layer` settles
       stylesheet-vs-stylesheet and never reaches an element-attached one. But
       `min-block-size` and `max-block-size` are DIFFERENT properties, and the
       used height is clamped by both whatever the height came from. So the
       drawn box is pinned to exactly the safe-area box without the override
       keyword this sheet is forbidden to use, and without re-typing a byte of
       extracted markup. */
    .glyph-login > [data-glyph-login="mobile"] { block-size: 100%; min-block-size: 0; }
    .glyph-login > [data-glyph-login="mobile"] > * {
      min-block-size: 100%;
      max-block-size: 100%;
    }

    /* THE ANIMATION IS SIZED TO THE SPACE ABOVE THE FORM, not to the whole box.
       The canvas is drawn `inset: 0` and the engine places the mark at 27% of
       the CANVAS height, so shrinking the canvas moves the mark up with it — the
       proportion the drawing specifies is preserved at every size. The subtrahend
       is MEASURED at run time by `public/js/login-boot.js` (a ResizeObserver on
       the drawn form card) and published as `--glyph-login-form-block`; the
       fallback is only for the moments before that first measurement lands.
       Same min/max reasoning as above: the canvas carries `height: 100%` inline. */
    /* THE ONE BOUNDARY BETWEEN THE STAGE AND THE FORM, DEFINED EXACTLY ONCE.
       Three things read it: this overlay's top padding (transform T5), the canvas
       clamp below, and the DOM glow + wordmark anchor (T4). The previous round's
       defect was two boxes that agreed by accident and then diverged the moment one
       was clamped; a single named property is what makes that unrepresentable.
       min() is the degradation, not decoration: 60dvh when there is room, and
       bottom-anchored-as-drawn when there is not — the software-keyboard case, where
       without it the Sign in button would leave the screen. The env() floor keeps the
       form clear of a notch on a viewport too short for either. */
    /* No composition attribute in this locator, and that is the ratchet again:
       adding it computes to (0,3,0). It is also redundant — inside this media query
       the desktop composition is `display: none`, so the media query IS the scope. */
    .glyph-login [href="#reset"] {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-block-size: 44px;
    }

    /* THE CANVAS CLAMP IS MOBILE-ONLY, AND THAT ASYMMETRY IS [R-OV-13].
       On mobile the particle field stops above the form, which is what keeps the
       drawn 27% mark proportion in the space that is actually left. On DESKTOP the
       owner ruled the stage FULL-BLEED, so the canvas keeps the whole viewport and
       only the FORM moves — the two rulings pull in opposite directions here and
       this is the line where they are separated. Do not "tidy" this out of the
       media query to match the anchor above: that would re-letterbox the desktop. */
    .glyph-login [data-particles] {
      max-block-size: var(--glyph-login-form-top);
    }
  }

  /* =======================================================================
     [GM-1] THE 16px FLOOR IS THE ONLY THING STOPPING iOS AUTO-ZOOM HERE.
     =======================================================================
     🔴 AND THIS SURFACE HAD THE VIEWPORT META AND NOT THE FLOOR, WHICH IS THE
     WORSE HALF TO HAVE. iOS Safari has ignored `user-scalable=no` since iOS 10, so
     the meta lock this document carries does NOT stop Safari zooming the viewport
     when a text input under 16px takes focus — the >=16px floor is what does, and
     it was absent. Measured by the global matrix: every other surface computes
     16px; the login's inputs computed 14.03-15px (they inherit `--text-body`,
     which clamps down to 14px at narrow widths), 8 breaches across 6 viewports.
     It is the ONE screen everybody types into on a phone.

     ⚠️ IT FAILS ONLY AT NARROW WIDTHS, WHICH IS WHY A DESK CHECK MISSED IT — and
     why the earlier "zoom is locked" note on this surface was a claim about the
     META and about the canvas's `touch-action`, never about the input type scale.
     A gate that reads a computed style at 390 and 360 is what closes it.

     SAME SHAPE AS `css/surfaces/overlays.css` (R-OV-02), deliberately: one floor
     expressed the same way on every surface that takes typed input, rather than
     moving the brand type scale. `touch-action: manipulation` removes the 300ms
     double-tap delay WITHOUT disabling pinch — the a11y-hostile alternatives stay
     ruled out.
     The reset control is included: it is a text input on the same document, on the
     same phone, with the same defect — the ruling named the sign-in fields, and
     leaving the recovery field zooming would be the same bug under another name. */
  .glyph-login input,
  .glyph-login-reset-live input {
    font-size: max(16px, var(--text-body));
    touch-action: manipulation;
  }

  /* =======================================================================
     [TOUCH-DILATION] The canvas OWNS the drag gesture, at every width.
     =======================================================================
     🔴 THE DILATION WAS WIRED AND DEAD ON TOUCH, and the wiring is why it went
     unnoticed: `stage.js` binds `touchmove` (and `pointermove`, which touch also
     raises), so every static check — the listener exists, it sets `mouse`, it
     sets `hot` — passed. What no static check can see is that with the default
     `touch-action: auto` the BROWSER claims a finger drag as a pan/zoom gesture
     and stops delivering the move stream to the element (it fires
     `pointercancel` instead), so the handler that provably exists is provably
     never called after the first few pixels. Desktop was unaffected because a
     mouse drag is not a scroll gesture.
     `touch-action: none` tells the browser this element handles its own
     gestures. It is UNCONTESTED: the drawn canvas's inline style is
     `position/inset/width/height/display` and carries no `touch-action`, so this
     needs no specificity fight and no override keyword.
     ⚠️ It is deliberately OUTSIDE the max-width media query. The dilation is a
     pointer affordance on both compositions, a touchscreen laptop is a real
     device at desktop widths, and there is nothing to scroll on this page at any
     size — so scoping it to mobile would only re-break the case the owner asked
     about, on hardware that is harder to notice. */
  .glyph-login [data-particles] {
    touch-action: none;
  }
}

/* --- [FIX-SHELL R2] the legal footer ---------------------------------------
   Production's login carries Privacy and Terms; the drawn composition does not, and
   R-OV-53/55 says a behaviour the incumbent HAS is BUILT. Tokens only, and it sits
   under the form rather than beside it so the card's drawn column is unchanged. */
@layer surfaces {
  .glyph-login__legal {
    margin-block-start: var(--rogue-space-4);
    margin-block-end: 0;
    text-align: center;
    color: var(--color-text-muted);
  }
}

/* --- [H1 · S1-MFA] the SIGN-IN step ------------------------------------------
   The sign-in form is step ONE of the same flow, and until now it was the only step with
   no way to be turned off: `mfa.js` stamped the `display: contents` <form> (0x0, renders
   nothing) while `login-boot.js` stamped the drawn CARD — which is the MFA host's ANCESTOR,
   so hiding it would hide the challenge too. `login-boot.js` now wraps the card's drawn
   content in `.glyph-login-step`, a SIBLING of `.glyph-login-mfa`, and these two rules are
   the whole hiding mechanism.

   `gap: inherit` IS THE POINT, and it is why there is no magic number here. The card is
   `display:flex; flex-direction:column` with `gap: var(--rogue-space-4)` on the desktop
   composition and `var(--rogue-space-3)` on the 390 one (measured in `surfaces/login/
   template.js`), and no `align-items`, i.e. `stretch`. Inheriting the card's own computed
   gap makes the wrapper reproduce the drawn column exactly on BOTH compositions, with one
   declaration and nothing to re-tune when a token moves.

   Specificity: (0,1,0) and (0,2,0) — inside TDR-075's 0,2,0 budget, so `css-ratchet`'s
   TOTAL is unmoved. No id selector (`selector-max-id: 0`), no hex, no magic number. */
@layer surfaces {
  .glyph-login-step {
    display: none;
    flex-direction: column;
    gap: inherit;
  }
  .glyph-login-step.active {
    display: flex;
  }
}

/* --- [FIX-SHELL R2 / R-OV-69 A] the five MFA steps ---------------------------
   The composition draws NO MFA chrome (see `surfaces/login/mfa.js` for the extraction
   control that proves it), so these steps are built from the design SYSTEM and this block
   does exactly two things: it makes one step current at a time, and it gives the built
   column the drawn card's own rhythm. Tokens only — no hex, no rgb(), no magic number, and
   nothing here restyles a drawn node.
   `.step` also exists in the legacy sheet; that sheet is in the `legacy` layer and this is
   in `surfaces`, so layer order settles it without either file out-specifying the other. */
@layer surfaces {
  .glyph-login-mfa {
    display: contents;
  }
  /* `.glyph-login-mfa .step.active` scores (0,3,0) and blows TDR-075's 0,2,0 budget —
     the ratchet caught it at TOTAL 1084 against a baseline of 1083. `:where()` contributes
     zero, so the ancestor moves inside it and the pair is (0,1,0)/(0,2,0) while selecting
     exactly the same nodes. Same lever `frame-app.css` records for its own 8 findings. */
  :where(.glyph-login-mfa) .step {
    display: none;
    flex-direction: column;
    gap: var(--rogue-space-4);
  }
  :where(.glyph-login-mfa) .step.active {
    display: flex;
  }
  .glyph-login-mfa :where(.rg-h3) {
    margin: 0;
    color: var(--color-text);
  }
  .glyph-login-mfa :where(code) {
    padding: var(--rogue-space-2);
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-control);
    text-align: center;
  }
  /* A step heading is focused when the flow moves to it; that focus is programmatic and
     must not paint a ring the pointer user sees. */
  .glyph-login-mfa :where(.rg-h3:focus) { outline: none; }
}
