/* ══════════════════════════════════════════════════════════════════════════════════════
   OKF Portal — sign-in page (/login).  Served at /assets/css/login.css.

   THIS PAGE IS DELIBERATELY SINGLE-THEME (DARK). There is no light theme and there must
   not be one: the design IS the near-black canvas with the crimson key visual. Please do
   not "fix" this by adding a `prefers-color-scheme: light` branch or by wiring it to
   app.css's `[data-theme]` — see the three reasons below.

   WHY A PRIVATE `--lg-` TOKEN SET INSTEAD OF app.css's TOKENS
     1. app.css's dark palette keys off `:root[data-theme="dark"]` (app.css:21), a
        JS-set attribute. This page ships no <script> and runs pre-auth, so reusing
        --bg/--fg would render the sign-in page in the LIGHT palette (#f6f7f9 on
        #1e2326) by default. That is a functional break, not a taste call.
     2. The hues no longer differ — the portal accent is now the same #E8365D crimson,
        unified 2026-08 (see the brief; app.css's orange-leaning red is retired). This
        file still does not import app.css's tokens, for reasons (1) and (3), which are
        the two that were ever load-bearing. It was the hue difference that made this
        reason read as a third one, and it is gone.
     3. app.css:214-220 already owns `.login` / `.login-card` / `.login-list` — the
        SPA's POST-auth identity picker. Hence the `lg-` prefix on both the tokens and
        the classes, and hence this is a standalone stylesheet that does not load
        app.css at all (that file is workbench chrome this page needs none of).

   CSP (portal.CSPMiddleware, every response):
     default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';
     img-src 'self' data:; connect-src 'self'; base-uri 'self'; frame-ancestors 'none'
   => no webfont, no CDN, no @import, no remote image. Pure system font stack; EVERY
   pixel this page draws is inline SVG, the key visual included. /assets/* is served
   with Cache-Control: no-cache, so no `?v=` needed.

   THE RASTER SHIPS (since 2026-08-06): `portal_static/img/ciel-hero.webp`, 1024x1536,
   175 KB, generated line art on PURE BLACK, re-inked to a single deep wine (#6B1229). It has no alpha and is not cut out —
   `mix-blend-mode: screen` is what disposes of the black, so it must never be
   re-exported transparent or on a non-black matte.

   THE TWO ART LAYERS ARE ALTERNATIVES, NEVER LAYERS, and getting that wrong is visible
   from across the room. portal._login_shell emits BOTH the inline SVG stand-in
   (`.lg-art__media > svg.lg-art__figure`) and the `<img>`, plus the modifier
   `lg-art--raster` which switches the SVG OFF. Under `prefers-reduced-data: reduce` the
   raster is dropped and the SVG comes back, which is the whole reason it still exists.
   The first integration nested the `<img>` inside `.lg-art__media` and shipped no
   exclusion rule, so both painted at once: two different line drawings of the same
   subject, slightly out of alignment, reading exactly like a misregistered print.
   The `<img>` must be a DIRECT CHILD of `.lg-art` — only `.lg-art > img` carries the
   position/opacity/blend/mask.

   The rest of the art layer is unchanged: the `.lg-art::before` radial glow and
   `.lg-art__contour`.

   MARKUP CONTRACT (portal.login_get). Both SSO modes share the skeleton; only
   `.lg-card__action` differs, so the two modes are the same design.
     body.okf-login
       [_banner() verbatim — placeholder mode only]
       .lg-art.lg-art--raster[aria-hidden]
                              > .lg-art__media > svg.lg-art__figure   (switched off)
                              > img.ciel-hero                          (+ .lg-art__contour)
       .lg-grid > .lg-stack   > .lg-brand / .lg-product (+ .lg-product__desc)
                                / .lg-rule / .lg-tagline
                                / .lg-card / .lg-footer
       .lg-card > .lg-sparkle / .lg-card__title / .lg-card__body / .lg-divider
                  / .lg-card__action / .lg-secure
   Strings pinned by the test gate and not to be paraphrased by any restyle:
     SSO on  -> <a href="/auth/login"> labelled "Sign in with Microsoft"
     SSO off -> <form method="post" action="/login"> with a button "Login with SSO",
                plus portal._banner()'s "SSO PLACEHOLDER" strip, rendered byte-identical.
   Gate: infra/scripts/run-tests.sh — never bare pytest (repo foot-guns #8 / #11).
   ══════════════════════════════════════════════════════════════════════════════════════ */


/* ══ 1. TOKENS ════════════════════════════════════════════════════════════════════════
   Every colour in this file comes from here. The single exception is the Microsoft
   4-square logo, whose four brand colours are hard-coded at its rule and commented as
   such — Microsoft's brand guidance forbids recolouring that mark.
   ════════════════════════════════════════════════════════════════════════════════════ */
:root {
  color-scheme: dark;                /* dark UA form controls + scrollbars; single-theme page */

  /* surfaces */
  --lg-bg-0: #0B0810;
  --lg-bg-1: #14101A;
  --lg-bg-2: #191720;
  --lg-bg-deep: #07050B;             /* the page gradient's far corner */
  --lg-card-bg: rgba(25, 23, 32, .92); /* rgba, not hex: on mobile the art reads through it */
  --lg-input-bg: #100D16;
  --lg-btn-bg: #15121C;
  --lg-btn-bg-hover: #1E1926;

  /* lines. --lg-line is DECORATIVE ONLY (card edge, divider): 1.22:1 on the card fill,
     which is fine for a quiet edge and is NOT fine for a control. */
  --lg-line: #2A2630;
  --lg-line-soft: #221E29;
  --lg-line-hover: #3A3442;
  /* The email input's boundary is a UI COMPONENT boundary and owes WCAG 1.4.11 3:1.
     Measured from screenshot pixels 2026-08-06, not computed from the token: #6E6878
     renders rgb(110,104,120) and is 3.36:1 against the composited card rgb(24,21,30)
     outside the field and 3.59:1 against --lg-input-bg rgb(16,13,22) inside it — both
     sides of the boundary clear 3:1. It is deliberately NOT --lg-line: sharing that
     token would drag the card edge and the divider up with it, and those are decorative
     and correctly quiet. What shipped before was --lg-line, measured 1.22:1 — the field
     had no perceivable boundary at all, on the product's only login form. */
  --lg-line-field: #6E6878;
  --lg-line-field-hover: #857F92;   /* 4.68:1 card / 4.99:1 fill — hover never quieter */

  /* text */
  --lg-fg: #F4F5F7;
  --lg-fg-muted: #9AA0A8;            /* 6.73:1 on the card — body copy */
  --lg-fg-dim: #7C838C;              /* 5.19:1 on the page — footer. NOT darker: #6E747C measured 4.21:1 and failed */

  /* accent */
  --lg-accent: #E8365D;              /* text on the PAGE background (4.84:1) + all non-text use */
  --lg-accent-hi: #F2456B;           /* hover border */
  --lg-accent-ink: #F2456B;          /* text on the CARD fill: #E8365D is 4.32:1 there and FAILS AA */
  --lg-accent-deep: #8E1D38;         /* 2.26:1 — DECORATIVE ONLY. Never text, never a focus ring */
  --lg-accent-focus: #FF6E8B;        /* 7.44:1 page / 6.63:1 card, and a different hue from hover */

  /* glows + shadows */
  /* Art layer opacities. Two values, not one — see the note above
     `.lg-art > picture` for why the raster must sit lower than the SVG. */
  --lg-art-opacity-svg: .42;      /* the sparse inline stand-in */
  --lg-art-opacity-raster: .42;   /* the dense generated key visual — see the note at .lg-art > img */
  --lg-glow: rgba(232, 54, 93, .28);
  --lg-glow-soft: rgba(232, 54, 93, .10);
  --lg-glow-figure: rgba(232, 54, 93, .22);
  --lg-btn-shadow-hover: 0 0 0 1px rgba(242, 69, 107, .55), 0 8px 28px rgba(232, 54, 93, .20);
  --lg-card-shadow: 0 1px 0 rgba(255, 255, 255, .03) inset, 0 24px 64px rgba(0, 0, 0, .55);

  /* art masks. These are alpha ramps, not palette colours — they live up here so no rule
     below carries a literal, and so the -webkit- prefix duplication stays DRY. */
  --lg-mask-art-x: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, .35) 12%, #000 34%);
  --lg-mask-art-y: linear-gradient(to bottom, #000 0%, #000 74%, rgba(0, 0, 0, .55) 92%, transparent 100%);
  /* --lg-mask-art-top is the BELOW-1024 ramp, and it is a legibility control, not just a
     fade. In that band the art is a strip across the top of the page and the lockup sits
     INSIDE it; measured 2026-08-06, the tagline occupies 48-80% of the strip's height
     depending on width and SSO mode (the placeholder banner pushes it down). The ramp
     therefore has to be spent by ~48%, or crimson line-art crosses crimson text. It used
     to end at 100% with .6 still standing at 58%, which is how the tagline shipped at
     3.58:1. Re-measure .lg-tagline before touching these stops. */
  --lg-mask-art-top: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, .45) 32%, transparent 60%);

  /* higher-contrast swaps, applied by the prefers-contrast branch at the end of the file */
  --lg-hc-line: #4A4453;
  --lg-hc-line-field: #8E8899;       /* 5.18:1 on the opaque HC card (measured, hover
                                        6.35:1). --lg-hc-line (#4A4453) is 1.89:1 there,
                                        so before this token the ONE branch written for
                                        people who ask for more contrast still left the
                                        email field under the 3:1 bar. */
  --lg-hc-line-field-hover: #9E98AA;
  --lg-hc-fg-muted: #C3C8CF;
  --lg-hc-fg-dim: #AEB4BC;
  --lg-hc-card-bg: #191720;          /* fully opaque — the art no longer reads through */

  /* geometry */
  --lg-radius-card: 14px;
  --lg-radius-btn: 8px;
  --lg-radius-sm: 4px;
  --lg-col-content: 42fr;
  --lg-col-art: 58fr;
  --lg-stack-w: 420px;
  --lg-card-w: 360px;
  --lg-pad-x: clamp(28px, 6.5vw, 96px);
  --lg-pad-y: clamp(32px, 5vh, 72px);
  --lg-gap-xs: 8px;
  --lg-gap-sm: 14px;
  --lg-gap-md: 22px;
  --lg-gap-lg: 34px;
  --lg-gap-xl: 48px;

  /* motion */
  --lg-ease: cubic-bezier(.22, .7, .2, 1);
  --lg-dur-in: 480ms;
  --lg-dur-ui: 160ms;

  /* type. CSP forbids webfonts, so this is a pure system stack. "Noto Sans TC" is kept to
     match app.css:32 and because the product is CJK-adjacent, but it sits AFTER the Latin
     faces so it never wins for Latin copy. The airy look is carried by the letter-spacing
     values, never by font-weight alone — Linux boxes that resolve system-ui to DejaVu
     have no 300 and the design still has to hold there. */
  --lg-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI",
             Roboto, "Helvetica Neue", "Noto Sans TC", Arial, sans-serif;
  --lg-font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}


/* ══ 2. PAGE SHELL ════════════════════════════════════════════════════════════════════
   Written mobile-first. The desktop two-column layout is the min-width:1024px branch.
   ════════════════════════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }   /* root font-size is never overridden — respect the user's */

.okf-login {
  position: relative;
  display: flex;                     /* banner (if any), then the grid, which takes the rest */
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;                /* dvh where supported; the vh above is the fallback */
  margin: 0;
  overflow-x: hidden;                /* the page NEVER scrolls sideways, 320px and up */
  background:
    radial-gradient(120% 90% at 78% 42%, var(--lg-glow-soft) 0%, transparent 60%),
    linear-gradient(160deg, var(--lg-bg-1) 0%, var(--lg-bg-0) 55%, var(--lg-bg-deep) 100%);
  color: var(--lg-fg);
  font-family: var(--lg-font);
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  overflow-wrap: break-word;
}


/* ══ 3. THE SSO PLACEHOLDER BANNER ════════════════════════════════════════════════════
   portal._banner() returns a bare `<div style="background:#b45309;…">` with INLINE styles
   and tests assert on it (test_portal.py:392, test_ui_consolidation.py:329). It is
   rendered byte-identical and full-bleed as the first child, and it is NOT restyled to
   blend in: it is a security warning, and it only ever appears when sso.enabled() is
   False. All this file does is guarantee it paints above the art layer.
   Both selectors are here because the banner may be wrapped in .lg-banner or emitted
   verbatim; nothing else on this page carries an inline style attribute.
   ════════════════════════════════════════════════════════════════════════════════════ */
.okf-login > .lg-banner,
.okf-login > div[style] { position: relative; z-index: 2; flex: none; }


/* ══ 4. ART LAYER ═════════════════════════════════════════════════════════════════════
   Absolutely positioned rather than a grid cell, so it can bleed past the grid's
   max-width and past the viewport's right and bottom edges.
   NOTE ON `z-index: 0`: it makes .lg-art a stacking context, so the media's `screen`
   blend composites against the glow below it rather than against the page gradient.
   That matches the approved mockup exactly. If the delivered raster's black turns out
   LIGHTER than --lg-bg-0 and a rectangle shows through outside the glow, change this to
   `z-index: auto` — .lg-grid's z-index:1 still keeps the content above the art — and the
   blend will then reach the page background. Needs eyes-on once the raster exists.
   ════════════════════════════════════════════════════════════════════════════════════ */
.lg-art {
  position: absolute;
  inset: 0 0 auto 0;
  width: 100%;
  height: min(52vh, 420px);
  pointer-events: none;
  z-index: 0;
}

/* The crimson radial behind the figure is CSS, not baked into the raster: baked in it
   arrives as a rectangle whose edges betray themselves against the page gradient. As a
   gradient it composites perfectly and it is what the ambient breathe animation drives.
   Requirement for whoever produces the artwork: deliver it on PURE BLACK, glow removed. */
.lg-art::before {
  content: "";
  position: absolute;
  inset: 0;
  /* PINNED AT BASE LEVEL, and it must stay pinned. The lg-breathe keyframe (.58 -> .82)
     is defined only inside prefers-reduced-motion: no-preference — correct per §12 —
     but with no base value that left `reduce` rendering this glow at the UA default of
     1, i.e. BRIGHTER than any frame the animation ever shows. The one branch written
     for people who asked for less motion was the loudest canvas on the page. .7 sits
     inside the keyframe's own range, so reduce is now never the worst case; verified
     2026-08-06 by measuring the tagline at the animation's bright extreme (.82) as well
     as at rest — 5.00:1 worst, both. */
  opacity: .7;
  background: radial-gradient(46% 42% at 46% 44%,
    var(--lg-glow) 0%, var(--lg-glow-soft) 42%, transparent 72%);
  /* Below 1024 the art is a band across the TOP of the page and the lockup sits inside
     it, so the glow is BEHIND the tagline rather than beside it. Masked with the same
     top ramp as .lg-art__media, the glow is spent by the time it reaches the text — see
     .lg-tagline for the measurements. The >=1024 branch takes this mask off again:
     there the art is the right column, the glow's inner edge is the intended backlight,
     and nothing legible sits on it. */
  -webkit-mask-image: var(--lg-mask-art-top);
          mask-image: var(--lg-mask-art-top);
}

/* The media wrapper — what carries the opacity, the screen blend and the mask for
   whatever is inside it. TODAY THAT IS THE INLINE FIGURE, and only the figure:
   portal._login_shell emits `.lg-art__media > svg.lg-art__figure`. The other two
   selectors are for the raster shapes (a bare <picture>/<img> child of .lg-art); they
   are mutually exclusive with the first, so the mask is never applied twice. */
.lg-art__media,
.lg-art > picture,
.lg-art > img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: var(--lg-art-opacity-svg);
  /* screen over the near-black canvas erases any residual black box in the raster and
     is the insurance behind the "no hard seam" requirement */
  mix-blend-mode: screen;
  -webkit-mask-image: var(--lg-mask-art-top);
          mask-image: var(--lg-mask-art-top);
}

/* [2026-08-06] THE RASTER IS DIMMER THAN THE SVG, ON PURPOSE — and this is the knob to
   turn if it ever needs adjusting again.

   The two layers are not interchangeable at the same opacity. The inline SVG is sparse
   line work — a few dozen strokes — so .42 reads as a faint suggestion. The generated
   raster is dense: thousands of hair strands, filigree and hatching, plus a bright
   specular bloom at the book. At .42 it stopped being a backdrop and became THE visual,
   pulling the eye off the sign-in card, which is the one thing this page exists to get
   somebody to click.

   The value was picked by compositing the actual asset against --lg-bg-0 through the
   real screen blend at .42 / .30 / .22 / .16 / .11 and looking at each, not by taste.
   .42 made the figure compete with the sign-in card; .30 still did; .22 was where it
   recedes to atmosphere while staying legible as a figure; at .16 the linework begins
   dissolving and the bloom at the book is most of what survives; .11 is effectively
   gone.

   THEN THE VALUE WENT BACK UP, because opacity was the wrong knob. Asked for "darker"
   twice, the fix was read as "fade it more" and taken to .16 — which does not make the
   art darker, it makes bright pink lines washed out over the background, a different
   and worse thing. What was actually wanted was a darker TONE: linework whose own
   colour sits close to the page.

   So the darkening now lives IN THE ASSET. `ciel-hero.webp` is recoloured to a deep
   wine — every pixel is `max(R,G,B) * #6B1229`, i.e. the generated line intensity
   re-inked in a single dark hue — and opacity returns to .42 so the drawing is present
   rather than faint. Judged the same way as before, by compositing the real asset
   through the real screen blend: #8E1D38 still reads pink, #6B1229 sits close to the
   background while the whole figure survives, #4A0C1C is nearly gone.

   TO GO DARKER STILL, re-ink the asset toward #4A0C1C — do NOT drop this opacity. The
   two knobs look similar and are not: opacity fades the drawing toward the background,
   re-inking changes what colour it is. Only the second is what "darker" meant here.
   (Recolouring also shrank the file: 321 KB -> 175 KB, one hue compresses better.)

   NOT DONE HERE, deliberately: `.lg-art::before`'s glow stays at .7. Its value was
   picked against a measured 5.00:1 tagline contrast at the animation's bright extreme,
   so moving it would invalidate that measurement to solve a problem it is not causing. */
.lg-art > picture,
.lg-art > img { opacity: var(--lg-art-opacity-raster); }

.lg-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 12%;
}

/* [2026-08-06] THE MUTUAL EXCLUSION, MADE REAL.
   The block above says the media wrapper and the raster "are mutually exclusive so the
   mask is never applied twice" — but nothing enforced it, because when it was written
   only the SVG figure existed. The moment the raster landed, both painted: two different
   line drawings of the same subject, each at opacity .42 with a screen blend, slightly
   out of alignment. It looks precisely like a misregistered print, and it was spotted on
   sight.

   `lg-art--raster` is emitted by portal._login_shell only when it also emits the <img>,
   so the switch and the thing it switches can never disagree. A modifier class rather
   than `:has(> img)` deliberately: `:has()` is well supported now, but a browser without
   it would silently fall back to *drawing both*, which is the bug — a fallback must fail
   toward the correct picture, not toward the defect. */
.lg-art--raster > .lg-art__media { display: none; }

/* Inline-SVG stand-in figure — THIS IS WHAT THE PAGE ACTUALLY DRAWS TODAY, emitted by
   portal._LOGIN_ART_FIGURE inside .lg-art__media. It stays as the reduced-data layer
   once a raster lands (§15 drops the raster, not this). Recolourable because it is
   currentColor throughout. If you delete this rule, delete the constant with it. */
.lg-art__figure {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--lg-accent);
  filter: drop-shadow(0 0 18px var(--lg-glow-figure));
}

/* The topographic contour lines along the bottom edge.
   THE OPACITY IS A LEGIBILITY CONTROL BELOW 1024, not a taste value. There the contour
   band is the bottom 150px of a 416-520px art strip, and in PLACEHOLDER mode the
   SSO-PLACEHOLDER banner pushes the tagline down to 69-80% of that strip — i.e. straight
   onto these lines. At full strength the brightest of them (opacity .55 of --lg-accent)
   measured rgb(141,36,62) behind the tagline: 3.11:1, a 1.4.3 failure, and it was the
   dominant contributor at 320-390px. At .5 the same pixel is dark enough to pass. The
   >=1024 branch puts it back to full — there the contour is at the foot of the right-hand
   column and no text is near it. */
.lg-art__contour {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: block;
  width: 100%;
  height: 150px;
  opacity: .5;
  color: var(--lg-accent);
}


/* ══ 5. GRID + STACK ══════════════════════════════════════════════════════════════════ */
.lg-grid {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  display: grid;
  grid-template-columns: 1fr;
  place-items: center;
  padding: var(--lg-pad-y) 20px;
}

.lg-stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  width: min(var(--lg-stack-w), 100%);
  min-width: 0;                      /* never let a long word push the grid sideways */
}


/* ══ 6. BRAND / PRODUCT / RULE / TAGLINE ══════════════════════════════════════════════ */
.lg-brand {
  display: flex;
  align-items: center;
  gap: 11px;
  margin-block-start: 0;
}
/* [2026-08-06] The OFFICIAL lockup — mark and wordmark in one asset, from the brand
   pack, served from /assets/img/ and never inlined (see portal._login_shell for why its
   own <style> block makes inlining unsafe).

   SIZED BY HEIGHT, width auto. The asset's viewBox is 190x73.22, so pinning a width
   would leave the height to rounding and the lockup would sit a pixel off the tagline
   baseline at some zoom levels. Height is the dimension that has to agree with the type
   next to it.

   NOT RECOLOURABLE, and that is correct. This is a brand mark carrying its own
   #ff0037 -> #9d1f63 gradient and a white wordmark — the same rule that applies to the
   Microsoft four-square logo further down. Do not add a filter to tint it to
   --lg-accent; the page accent (#E8365D) is NOT a VicOne brand colour, it was chosen to
   sit near one. The design and the brand are allowed to differ; the mark is not. */
.lg-brand__logo {
  display: block;
  flex: none;
  height: 30px;
  width: auto;
}
.lg-brand__rule { display: none; }   /* the vertical hairline is a desktop-only detail */

.lg-product {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .42em;
  margin: var(--lg-gap-lg) 0 0;
  font-size: clamp(30px, 3.4vw, 44px);
  line-height: 1.08;
  font-weight: 300;
  letter-spacing: .06em;
  color: var(--lg-fg);
  font-synthesis-weight: none;       /* a missing 300 falls back to a real 400, not a smear */
}
.lg-product__hex {
  display: block;
  flex: none;
  width: .86em;
  height: .86em;
  color: var(--lg-accent);
  transform: translateY(-.02em);
}
/* [2026-08-06] The descriptor line, under "Ciel". Tight to the name on purpose — the
   two are one lockup, and `--lg-gap-*` here would separate them into two announcements.

   NOT UPPERCASE, deliberately. The tagline below ("SHARED KNOWLEDGE. / EMPOWERED.") is
   uppercase and letterspaced; giving this the same treatment put two small caps-tracked
   lines four rems apart and the eye stopped being able to tell which was the product and
   which was the slogan. Sentence case keeps them distinguishable at a glance.

   The size floor is 14px and it is a legibility floor, not a taste value: this is the
   only place on the page that says what the product IS. */
.lg-product__desc {
  margin: .5em 0 0;
  font-size: clamp(14px, 1.05vw, 15px);
  line-height: 1.45;
  font-weight: 400;
  letter-spacing: .05em;
  color: var(--lg-fg-muted);
}
/* On the PAGE background — hence --lg-accent (4.84:1), not the --lg-accent-ink variant
   that exists for the lighter card fill. */
.lg-product__accent { color: var(--lg-accent); }

/* thin rule with a rotated-square accent sitting on it */
.lg-rule {
  position: relative;
  width: 100%;
  height: 1px;
  margin-block-start: var(--lg-gap-md);
  background: linear-gradient(to right, transparent, var(--lg-line-hover), transparent);
}
.lg-rule__d {
  position: absolute;
  top: 50%;
  left: 50%;
  display: block;
  width: 12px;
  height: 12px;
  color: var(--lg-accent);
  transform: translate(-50%, -50%);
}

/* CONTRAST, AND WHY THE COLOUR CHANGES AT 1024. --lg-accent is 4.84:1 on the BARE page
   background, and that number is what an earlier revision of this comment claimed for
   the tagline outright. It is not the background the tagline actually sits on below
   1024px: there the art layer is a full-width band across the TOP of the page and
   .lg-art::before's crimson glow lands directly behind these two lines. Measured
   2026-08-06 from screenshot pixels, on the glyph pixels only, glow un-animated (the
   worst case), both SSO modes: 320px 2.06:1, 390px 2.08:1, 600px 4.13:1, 768-1023px
   3.58:1 — every one of them a 1.4.3 AA failure, and the placeholder mode is the worst
   because the SSO-PLACEHOLDER banner pushes the tagline down into the glow's core.
   #FF6E8B (--lg-accent-focus) clears 4.5:1 at every width measured while still reading
   as the same crimson family. At >=1024 the art moves to the right column, the tagline
   is back on near-black, and --lg-accent measures 4.62-4.63:1 — so the approved crimson
   is restored there (see the 1024 branch). If you touch the glow, the mask or these
   margins, RE-MEASURE both bands; the token's own ratio does not answer this question.
   Do NOT move this text onto the card fill without switching to --lg-accent-ink. */
.lg-tagline {
  margin: var(--lg-gap-md) 0 0;
  font-size: 13px;
  line-height: 1.75;
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--lg-accent-focus);
}


/* ══ 7. THE CARD ══════════════════════════════════════════════════════════════════════ */
.lg-card {
  width: min(var(--lg-card-w), 100%);
  margin-block-start: var(--lg-gap-xl);
  padding: 30px 28px 26px;
  text-align: center;
  background: var(--lg-card-bg);
  border: 1px solid var(--lg-line);
  border-radius: var(--lg-radius-card);
  box-shadow: var(--lg-card-shadow);
}
.lg-sparkle {
  display: block;
  width: 22px;
  height: 22px;
  margin: 0 auto;
  color: var(--lg-accent);
  filter: drop-shadow(0 0 10px var(--lg-glow));
}
.lg-card__title {
  margin: 18px 0 12px;
  font-size: 30px;
  line-height: 1.25;
  font-weight: 400;
  letter-spacing: .005em;
  color: var(--lg-fg);
  font-synthesis-weight: none;
}
/* Three <br>-separated lines. The breaks are deliberate; the 30ch cap keeps them from
   reflowing into a ragged four. */
.lg-card__body {
  max-width: 30ch;
  margin: 0 auto;
  font-size: 15px;
  line-height: 1.62;
  letter-spacing: .005em;
  color: var(--lg-fg-muted);
}
.lg-divider {
  display: flex;
  align-items: center;
  margin: 22px 0;
  color: var(--lg-line-soft);
}
.lg-divider::before,
.lg-divider::after { content: ""; flex: 1; height: 1px; background: currentColor; }
.lg-divider svg {
  display: block;
  width: 12px;
  height: 12px;
  margin: 0 10px;
  color: var(--lg-accent);
}


/* ══ 8. THE ACTION SLOT — the ONLY thing that differs between the two SSO modes ═══════ */
.lg-card__action { display: grid; gap: 10px; }

.lg-btn {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 12px 16px;
  font-family: inherit;
  font-size: 15px;
  line-height: 1;
  font-weight: 500;
  letter-spacing: .01em;
  color: var(--lg-fg);
  text-decoration: none;
  cursor: pointer;
  background: var(--lg-btn-bg);
  border: 1px solid var(--lg-accent);   /* 4.32:1 on the card — non-text, past the 3:1 bar */
  border-radius: var(--lg-radius-btn);
  transition: border-color var(--lg-dur-ui) ease,
              background-color var(--lg-dur-ui) ease,
              box-shadow var(--lg-dur-ui) ease;
}
.lg-btn:hover {
  background: var(--lg-btn-bg-hover);
  border-color: var(--lg-accent-hi);
  box-shadow: var(--lg-btn-shadow-hover);
}
/* The Microsoft 4-square mark keeps its own colours — the fills live in the inline SVG.
   THIS IS THE ONE ELEMENT ON THE PAGE THAT IGNORES THIS PALETTE, and it must stay that
   way: Microsoft's brand guidance forbids recolouring the mark (#F25022 / #7FBA00 /
   #00A4EF / #FFB900). Do not add a fill/currentColor rule here. */
.lg-btn__ms { display: block; flex: none; width: 18px; height: 18px; }

/* Placeholder mode only (sso.enabled() False). */
.lg-form { gap: 6px; }
.lg-form .lg-btn { margin-top: 6px; }
.lg-label {
  display: block;
  text-align: left;
  font-size: 12px;
  line-height: 1.4;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--lg-fg-muted);
}
.lg-input {
  width: 100%;
  padding: 11px 14px;
  font-family: inherit;
  font-size: 15px;               /* >=16px is the iOS zoom threshold; 15px + the input being
                                    the only field is the accepted trade for the design.
                                    If iOS zoom-on-focus is ever reported, raise to 16px. */
  line-height: 1.4;
  color: var(--lg-fg);
  background: var(--lg-input-bg);
  /* NOT --lg-line. This is the only form field on the product's only unauthenticated
     page; its boundary is what tells a low-vision user there is something to type into,
     and WCAG 1.4.11 puts that at 3:1. See --lg-line-field for the measured numbers. */
  border: 1px solid var(--lg-line-field);
  border-radius: var(--lg-radius-btn);
  transition: border-color var(--lg-dur-ui) ease, box-shadow var(--lg-dur-ui) ease;
}
.lg-input::placeholder { color: var(--lg-fg-dim); opacity: 1; }
.lg-input:hover { border-color: var(--lg-line-field-hover); }
/* Chromium paints autofilled fields pale yellow, which on this canvas looks like a bug. */
.lg-input:-webkit-autofill,
.lg-input:-webkit-autofill:hover,
.lg-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--lg-fg);
  -webkit-box-shadow: 0 0 0 1000px var(--lg-input-bg) inset;
  caret-color: var(--lg-fg);
}


/* ══ 9. REASSURANCE + FOOTER ══════════════════════════════════════════════════════════ */
.lg-secure {
  margin: 18px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--lg-fg-muted);
}
.lg-secure svg {
  display: block;
  width: 13px;
  height: 13px;
  margin: 0 auto 8px;
  color: var(--lg-fg-muted);
}
/* Centred within the CARD's 360px measure, not across the whole 42% column — that is
   what the reference shows; centring it across the column leaves it floating unrelated
   to the card. */
.lg-footer {
  width: min(var(--lg-card-w), 100%);
  margin-block-start: var(--lg-gap-md);
  text-align: center;
  font-size: 12px;
  line-height: 1.5;
  letter-spacing: .02em;
  color: var(--lg-fg-dim);
}
.lg-footer a { color: inherit; }


/* ══ 10. FOCUS — never animated, never suppressed ═════════════════════════════════════
   This is a sign-in page: keyboard and screen-reader users must be able to operate it.
   #FF6E8B measures 6.63:1 on the card fill and 7.44:1 on the page — past the 3:1
   non-text minimum in both places — and it is a different hue-lightness from the
   button's own #E8365D border, so focus is distinguishable from hover.
   ════════════════════════════════════════════════════════════════════════════════════ */
.lg-btn:focus-visible,
.lg-input:focus-visible,
.okf-login a:focus-visible,
.okf-login :focus-visible {
  outline: 2px solid var(--lg-accent-focus);
  outline-offset: 3px;
}
.lg-input:focus-visible { border-color: var(--lg-accent-hi); }

/* Fallback for any UA without :focus-visible — a visible ring beats a correct one. */
@supports not selector(:focus-visible) {
  .lg-btn:focus,
  .lg-input:focus,
  .okf-login a:focus {
    outline: 2px solid var(--lg-accent-focus);
    outline-offset: 3px;
  }
}


/* ══ 11. BREAKPOINTS ══════════════════════════════════════════════════════════════════ */

/* ── >=480px ─────────────────────────────────────────────────────────────────────────── */
@media (min-width: 480px) {
  .lg-grid { padding: var(--lg-pad-y) 28px; }
  .lg-product { font-size: clamp(32px, 3.4vw, 44px); }
}

/* ── >=768px — tablet. Still one column. The reference only shows a phone and a desktop,
      so this band is an interpolation, and it is stated as one. ────────────────────── */
@media (min-width: 768px) {
  .lg-stack { width: min(480px, 100%); }
  .lg-art { height: min(60vh, 520px); }
  .lg-art__media,
  .lg-art > picture,
  .lg-art > img { opacity: .5; }
}

/* ══ >=1024px — THE structural switch ════════════════════════════════════════════════ */
@media (min-width: 1024px) {
  .lg-grid {
    grid-template-columns: var(--lg-col-content) var(--lg-col-art);
    place-items: center start;       /* align-items: center; justify-items: start */
    width: 100%;
    max-width: 1680px;
    margin-inline: auto;
    padding: var(--lg-pad-y) 0 var(--lg-pad-y) var(--lg-pad-x);
  }
  /* Grid column 2 holds no DOM at all — the art is the absolutely-positioned layer. */
  .lg-stack {
    align-items: flex-start;
    align-self: center;
    text-align: left;
    width: min(var(--lg-stack-w), 100%);
  }
  .lg-brand__rule {
    display: block;
    width: 1px;
    height: 26px;
    margin-left: 4px;
    background: linear-gradient(to bottom, transparent, var(--lg-line-hover), transparent);
  }
  .lg-product { justify-content: flex-start; }
  .lg-rule { background: linear-gradient(to right, var(--lg-line-hover), transparent); }
  .lg-rule__d { left: 0; transform: translate(-3px, -50%); }
  /* The art is the right column from here up, so the tagline is on near-black again:
     --lg-accent measures 4.62:1 (1024), 4.63:1 (1366/1440). Below this breakpoint it is
     on the glow and fails — that is what the base rule's lighter ink is for. */
  .lg-tagline { color: var(--lg-accent); }
  .lg-card { align-self: flex-start; }

  /* Full-height right-bleed layer. Its left fade (0 -> 34%) starts LEFT of the 42%
     column boundary on purpose, so the fade overlaps the content column and there is no
     visible seam at 42%. The glow's inner edge then reads as backlight behind the
     tagline. If overlap ever hurts legibility, narrow .lg-art — never move the content. */
  .lg-art {
    inset: 0 0 0 auto;
    width: min(72vw, 1180px);
    height: auto;
  }
  /* Unmask the glow: from here up it is beside the content, not behind it. */
  .lg-art::before {
    -webkit-mask-image: none;
            mask-image: none;
  }
  .lg-art__media,
  .lg-art > picture,
  .lg-art > img {
    opacity: 1;
    -webkit-mask-image: var(--lg-mask-art-x), var(--lg-mask-art-y);
            mask-image: var(--lg-mask-art-x), var(--lg-mask-art-y);
    -webkit-mask-composite: source-in;
            mask-composite: intersect;
  }
  .lg-art img { object-position: 44% 18%; }
  .lg-art__contour { height: 190px; opacity: 1; }
}

/* ── 1024-1200px mitigation. In this band the figure crowds the tagline's right edge.
      Narrow the art rather than moving the content. THIS NEEDS AN EYES-ON CHECK at 1024,
      1280 and 1440 once the real raster exists — the delivered crop decides it, and no
      amount of reading this file will tell you. ─────────────────────────────────────── */
@media (min-width: 1024px) and (max-width: 1200px) {
  .lg-art { width: min(64vw, 1180px); }
}

/* ── >=1440px — --lg-pad-x resolves toward its 96px ceiling on its own. The grid is
      capped at 1680px and centred, so ultrawide does not stretch the artwork. ───────── */
@media (min-width: 1440px) {
  .lg-art { width: min(70vw, 1180px); }
}

/* ══ SHORT VIEWPORTS ═════════════════════════════════════════════════════════════════
   THESE THREE BANDS ARE HEIGHT-ONLY ON PURPOSE — do not put a min-width back on them.
   The first two used to be gated on `min-width: 1024px`, which meant a landscape phone
   (360-430px tall) matched NEITHER, and measured 2026-08-06 that is where the page was
   worst: at 740x360 the document was 748px (entra) / 889px (placeholder) tall and the
   sign-in control sat 200px / 355px BELOW the fold, with only the top 79px of the card
   on screen. A short viewport is short whether or not it is wide.

   THE TWO SSO MODES ARE NOT THE SAME HEIGHT, and the earlier version of this comment
   was measured in one of them. The placeholder form (label + input + button) adds
   ~140px over the Entra card, so every budget here is sized against PLACEHOLDER, the
   taller document, and then re-checked in Entra. Numbers below are measured in
   Chromium, both modes, with the trims active.
   ════════════════════════════════════════════════════════════════════════════════════ */

/* ── Tier 1, <=760px tall. A 1366x768 fleet laptop gives ~660-700px of viewport once
      browser chrome is off; a 375x667 phone lands here too. ──────────────────────── */
@media (max-height: 760px) {
  .okf-login {
    --lg-pad-y: 28px;
    --lg-gap-xl: 30px;
    --lg-gap-lg: 24px;
  }
  .lg-product { font-size: clamp(28px, 3.4vw, 38px); }
  .lg-card { padding: 24px 26px 22px; }
}

/* ── Tier 2, <=700px tall. MEASURED IN BOTH MODES, 2026-08-06, which is what the older
      version of this band did not do: it claimed "at 1366x660 … these trims take it to
      ~645px" and that was true of the ENTRA document only. The placeholder document was
      760px at the same viewport, and at 1024x600 its `Login with SSO` button ran from
      588 to 629 — the lower half below a 600px fold, on a machine this band explicitly
      claimed to have handled. Sized against placeholder now:
        1024x600  entra: fits (600), button bottom 437 · placeholder: 712, button 561
        1366x660  entra: fits (660), button bottom 469 · placeholder: 694, button 542
      i.e. the actionable control is inside the fold in both modes at both sizes; the
      placeholder footer still needs a ~35px nudge at 1366x660. Re-measure BOTH
      documents if you touch any of it. ──────────────────────────────────────────────*/
@media (max-height: 700px) {
  .okf-login { --lg-pad-y: 20px; --lg-gap-xl: 24px; --lg-gap-lg: 18px; }
  .lg-rule,
  .lg-tagline { margin-block-start: 12px; }
  .lg-tagline { line-height: 1.5; }
  .lg-card { margin-block-start: 18px; padding: 18px 24px 18px; }
  .lg-card__title { margin: 10px 0 8px; font-size: 24px; }
  .lg-card__body { line-height: 1.5; }
  .lg-divider { margin: 14px 0; }
  .lg-form .lg-btn { margin-top: 4px; }
  .lg-input { padding: 9px 14px; }
  .lg-secure { margin-block-start: 12px; }
  .lg-footer { margin-block-start: 12px; }
}

/* ── Tier 3, <=560px tall — the landscape-phone band, and the one that was missing.
      Here the lockup is what stands between the visitor and the sign-in control, so it
      collapses: the rule and the tagline go, the product name drops to a single small
      line, the card's sparkle goes, and the card starts near the top. MEASURED at
      740x360 with all three tiers active, before -> after:
        entra        document 748 -> 431, card top 267 -> 86,  button bottom 560 -> 292
        placeholder  document 889 -> 566, card top 331 -> 150, button bottom 701 -> 427
      So in Entra — the production mode — the whole page now fits a landscape phone with
      the button 68px inside the fold. Placeholder cannot: its card is 277px tall on its
      own and the SSO-PLACEHOLDER banner takes another ~72px, so on a 360px viewport
      SOMETHING must scroll; the button went from 341px past the fold to 67px, and it is
      inside the fold from 430px of height up. Do not "restore" the tagline here — it is
      the thing being traded, and the brand lockup is deliberately NOT traded with it: a
      sign-in page that does not say whose it is has a worse problem than a scroll. ── */
@media (max-height: 560px) {
  .okf-login { --lg-pad-y: 12px; --lg-gap-lg: 10px; --lg-gap-xl: 14px; }
  .lg-grid { padding-top: 12px; }
  .lg-product { margin-block-start: 10px; font-size: 22px; }
  .lg-rule,
  .lg-tagline { display: none; }
  .lg-card { margin-block-start: 12px; }
  .lg-sparkle { display: none; }
  .lg-card__title { margin: 0 0 8px; font-size: 22px; }
  .lg-secure { margin-block-start: 10px; }
  .lg-footer { margin-block-start: 10px; }
  /* The art strip is sized off vh, so it is already short here; drop it further so the
     lockup is not fighting a figure that only has 200px to be legible in. */
  .lg-art { height: min(46vh, 220px); }
}


/* ══ 12. MOTION — OPT-IN ONLY ═════════════════════════════════════════════════════════
   Every animation is defined INSIDE `prefers-reduced-motion: no-preference`. Nothing is
   defined at base level and then cancelled under `reduce`.
   THAT INVERTED PATTERN IS THE TRAP HERE, and it is why this is spelled out: the
   entrance keyframe starts at opacity 0, so an `animation: none` override under `reduce`
   would leave the sign-in card permanently invisible and lock people out of the product.
   Stated explicitly so nobody "simplifies" it back.
   BASE (always, no media query): every element is already at its final state —
   opacity 1, transform none.
   ════════════════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  @keyframes lg-rise {
    from { opacity: 0; transform: translate3d(0, 14px, 0); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes lg-fade {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  @keyframes lg-breathe {
    from { opacity: .58; transform: scale(1); }
    to   { opacity: .82; transform: scale(1.045); }
  }

  /* `both` so the opacity-0 pre-start state exists only while the animation does. */
  .lg-brand,
  .lg-product,
  .lg-rule,
  .lg-tagline,
  .lg-card,
  .lg-footer { animation: lg-rise var(--lg-dur-in) var(--lg-ease) both; }
  .lg-product { animation-delay: 70ms; }
  .lg-rule,
  .lg-tagline { animation-delay: 140ms; }
  .lg-card { animation-delay: 200ms; }
  .lg-footer { animation-delay: 280ms; }
  /* The tagline deliberately gets no motion of its own: letterspaced 13px uppercase text
     in motion is the least legible thing on the page. It fades with its group and stops. */

  .lg-art { animation: lg-fade 900ms var(--lg-ease) both; }   /* the raster's decode must not pop */

  /* Ambient backlight. 14s and a <=25% opacity delta keep this under the "distracting"
     threshold — it is a lighting effect, not a moving object — and it only ever touches
     opacity and transform, both compositor-only, never layout. */
  .lg-art::before { animation: lg-breathe 14s ease-in-out infinite alternate; }

  .lg-btn:active { transform: translateY(1px); }
}

/* The two UI transitions live at base level: they are user-initiated, sub-200ms and
   non-vestibular. Only their duration collapses under `reduce`. */
@media (prefers-reduced-motion: reduce) {
  .lg-btn,
  .lg-input { transition-duration: 1ms; }
}


/* ══ 13. HIGHER CONTRAST ══════════════════════════════════════════════════════════════ */
@media (prefers-contrast: more) {
  :root {
    --lg-line: var(--lg-hc-line);
    --lg-line-field: var(--lg-hc-line-field);
    --lg-line-field-hover: var(--lg-hc-line-field-hover);
    --lg-fg-muted: var(--lg-hc-fg-muted);
    --lg-fg-dim: var(--lg-hc-fg-dim);
    --lg-card-bg: var(--lg-hc-card-bg);
  }
  .lg-art__media,
  .lg-art > picture,
  .lg-art > img { opacity: .35; }
}


/* ══ 14. FORCED COLOURS ═══════════════════════════════════════════════════════════════
   A masked, screen-blended art layer is meaningless in forced colours — hide it rather
   than ship a grey smear. The button is deliberately NOT given forced-color-adjust:none;
   let the UA restyle it.
   ════════════════════════════════════════════════════════════════════════════════════ */
@media (forced-colors: active) {
  .lg-art { display: none; }
  .lg-card,
  .lg-btn,
  .lg-input { border: 1px solid CanvasText; }
  /* Chromium does not force SVG fill/stroke, so the glyphs would stay crimson on a
     forced light canvas. `color` is what actually lands: every glyph is authored with
     fill/stroke="currentColor", and currentColor inherits — including through <use>'s
     shadow tree, which a descendant selector cannot reach. fill/stroke are kept for any
     glyph that hard-codes them. */
  .lg-sparkle,
  .lg-rule__d,
  .lg-divider svg,
  .lg-product__hex,
  .lg-secure svg,
  .lg-btn svg:not(.lg-btn__ms) { color: CanvasText; fill: CanvasText; stroke: CanvasText; }
  .lg-sparkle *,
  .lg-rule__d *,
  .lg-divider svg *,
  .lg-product__hex *,
  .lg-secure svg * { fill: CanvasText; stroke: CanvasText; }
  /* .lg-brand__logo is deliberately ABSENT from the two lists above. It is an
     <img>, so these fill/stroke rules could not reach inside it anyway — but the
     point is that they should not: forced-colors exists to guarantee legibility,
     and a corporate mark repainted in CanvasText is no longer that mark. The UA
     leaves images alone here, which is the right outcome. */
  .lg-btn:focus-visible,
  .lg-input:focus-visible,
  .okf-login a:focus-visible { outline: 2px solid Highlight; outline-offset: 3px; }
}


/* ══ 15. METERED LINKS ════════════════════════════════════════════════════════════════
   Drop the ~150KB raster, keep the CSS glow and the inline-SVG figure. Low support
   today; costs three lines.
   ════════════════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-data: reduce) {
  .lg-art img,
  .lg-art picture { display: none; }
  /* …and bring the SVG figure back, or dropping the raster would leave an empty column.
     This is the whole reason the inline figure survives after the raster landed: it is
     the version of the key visual that costs nothing on a metered connection. */
  .lg-art--raster > .lg-art__media { display: block; }
}
