/* © 2026 Created with ♥ by Alex Munteanu | alexmunteanu.com */

/*
 * The published client surface: the reel's page shell, the row and job scaffolding, the two
 * gallery-only frame layers, and the gate.
 *
 * THE PLAYER IS NOT HERE ANY MORE. Every .player* rule moved to shared/player.css, whole,
 * because the admin tool opens the same player and does not load this sheet - a player styled
 * from the published site's own stylesheet is a player that is unstyled everywhere else. There
 * is no copy left here and there must not be one: two files declaring one component is two
 * values that agree until somebody changes one. The .btn family below STAYS, because it is the
 * product's button system rather than the player's; player.css declares what it needs from it
 * scoped under .player.
 *
 * WHAT THIS FILE IS ALLOWED TO CONTAIN. shared/tokens.css is the only file in the project
 * with a colour value, so every colour below is a var(). There is not one hex, rgb(), hsl(),
 * color-mix(), named colour or `transparent` here - measured, not intended. Anything this
 * file restated would be a second place for the design to drift, and `transparent` is
 * excluded with the rest because a sweep for a colour value should not have to know which
 * keywords are exceptions.
 *
 * EVERY RULE BELOW WAS RE-AUTHORED AGAINST THE 6px SCALE RATHER THAN CARRIED ACROSS from the
 * sheet this replaces, and that is a correctness measure rather than tidiness. The step names
 * survived the token rewrite with DIFFERENT VALUES: --space-4 was 4px and is 24px, --space-8
 * was 8px and is 48px, --space-12 was 12px and is 72px, --space-16 was 16px and is 96px, and
 * --leading-title was a unitless 1.2 and is a fixed 24px. The old scale named PIXELS; this
 * one names UNITS on a 6px base. A rule carried over compiles, resolves and renders at six
 * times its intended spacing, with nothing anywhere reporting a problem - which is strictly
 * worse than a retired name, because a retired name at least makes the declaration invalid at
 * computed-value time. So no line here was preserved: each one was written from the design's
 * own figure.
 *
 * The --wv-* custom properties it reads are RUNTIME STATE, not design values: the sprite sheet
 * and the cell under the pointer, written through the CSSOM by assets/scrub.js. Each carries a
 * fallback, so the sheet renders correctly before any script has written one. --wv-ar-num /
 * --wv-ar-den are read only by shared/strip.css, and deliberately without a fallback - see the
 * failing-closed note there.
 *
 * WHAT IT DELIBERATELY DOES NOT CONTAIN. The reset, [hidden], the focus ring, the five type
 * role classes, the icon box and the studio mark are shared/base.css. The strip, the cell, the
 * frame, the poster and the label are shared/strip.css, which BOTH surfaces load from one copy
 * on disk - that shared copy is what makes cross-surface geometry parity true by construction
 * rather than by discipline. The player is shared/player.css, for the same reason.
 *
 * WHY THE TWO FRAME LAYERS ARE HERE AND NOT IN strip.css. .wv-scrub and .wv-frame__scrim are
 * in the frozen markup contract but they are client-gallery enhancements: the admin tool has
 * no sprite to preview and no client to reveal a play glyph to. strip.css carries only what
 * both surfaces render identically.
 *
 * MARKUP CONTRACT for web/index.php, web/login.php, assets/app.js and assets/scrub.js:
 *
 *   .page--reel / .page--login  the two surfaces
 *   .site-brand                 the header bar: a 1fr auto 1fr grid, the lockup in the
 *                               MIDDLE track (v0.12.1), Sign out end-justified in the third,
 *                               the contact link start-justified in the first (v0.20.0)
 *     .site-brand__contact      the studio's email, as a ghost button (.btn .btn--text)
 *       .site-brand__contact-address   the address itself; hidden below 840px
 *     .brandlogo                the full lockup, from base.css
 *     .site-brand__exit         Sign out, in the edge-code register
 *   .site-main                  the content column
 *   .reel__back                 the step out of a folder, a direct child of #reel above the
 *                               row. Emitted only where there IS a parent, so it is absent at
 *                               the root rather than present and inert.
 *   .wv-row / .wv-row__header / .wv-row__client / .wv-row__count   one client
 *   .wv-job / .wv-job__label                                       one job strip
 *   .reel__empty                nothing published, and the <noscript> line
 *   .wv-scrub                   the sprite layer, appended to a hovered .wv-frame
 *   .wv-frame__scrim            the hover glyph - play on a clip, folder on a folder tile -
 *                               appended to the same frame
 *   .btn / .btn--primary / .btn--icon / .btn--text
 *   .login / .login__card / __brand / __lede / __lock / __form / __field / __label /
 *     __input / __error / __submit
 */

/* ==== Page shell =====================================================================
 *
 * A horizontal header bar, then the work. The lockup sits in the CENTRE of the band
 * (v0.12.1, founder-directed) with Sign out at the right edge, and nothing else in the bar.
 *
 * THIS REPLACES THE TURNED EDGE COLUMN, and the column took three things with it when it
 * went: the fixed 36px margin column, the --margin-column measurement, and .site-main's
 * matching left offset. Those were ONE measurement - the column was fixed, so a wider column
 * with an unchanged offset covered the content instead of moving it - and a measurement with
 * one of its two sites deleted is a defect waiting for the next reader.
 */

.page {
  min-height: 100dvh;
}

.page--login {
  display: flex;
  flex-direction: column;
}

/*
 * THE HEADER SCROLLS AWAY. It is static rather than fixed, and that is the whole reason it can
 * afford to carry the lockup at full size: a fixed bar spends its height on every screen of
 * every session, and this one is paid for once at the top of the document. The frames are the
 * product and the design's rule is that nothing competes with them - a band that leaves the
 * viewport as soon as the viewer starts reading does not.
 *
 * It also means .site-main needs no offset of any kind, which is the trap the old fixed column
 * carried: nothing here has to agree with anything below it.
 */
/*
 * THREE TRACKS, AND THE MIDDLE ONE IS THE POINT (v0.12.1, founder instruction: the lockup
 * sits in the horizontal middle of the band). `1fr auto 1fr` is what makes the centre EXACT:
 * the two flexible tracks stay equal whatever width Sign out takes, so the lockup's track is
 * the true middle of the bar - in the flex layout this replaced, space-between could only
 * ever balance the lockup against Sign out, and base.css's auto margins would have absorbed
 * the free space and pushed Sign out to the track edge. Explicit grid-column on both
 * children, because source order would otherwise seat the lockup in track ONE and
 * re-left-align it with every rule here intact.
 *
 * THE ONE BOUND ON "EXACT": `1fr` cannot shrink a track below its min-content, and Sign out
 * is nowrap - so under ~371px of viewport (144px narrow lockup + margins + the 65px SIGN OUT
 * run) track three floors and the mark drifts left of true centre, 25px at 320px. Nothing
 * clips; the drift is the honest reading of the constraint and is not chased with per-width
 * nudges.
 */
.site-brand {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-4) var(--page-margin);
  border-bottom: var(--hairline) solid var(--border-hairline);
}

/*
 * margin-inline: 0 IS STILL NOT COSMETIC. base.css centres .brandlogo with margin-inline:
 * auto for the login card; inside a sized `auto` track the auto margins are inert today, but
 * a later track-width change would hand them free space again - the zero states that this
 * element's centring is the GRID's job, never the margin's.
 *
 * TWICE THE SIZE, AND ONLY HERE. Founder instruction. The selector carries .site-brand for
 * the same reason margin-inline does: base.css's 96px is also the login card's lockup, and
 * that one was sized against a centred card rather than against a header bar. Doubling the
 * base would have taken the gate with it, unasked.
 *
 * Expressed as a multiple rather than as a new token because 192 is not on the 6px spacing
 * scale and does not belong on it - the scale tops out at 96 (16 units) and its steps are
 * spacing, not component dimensions. `x 2` also states the instruction literally, so the
 * relationship survives any later change to the base.
 */
.site-brand .brandlogo {
  grid-column: 2;
  margin-inline: 0;
  width: calc(var(--space-16) * 2);
  height: calc(var(--space-16) * 2);
}

.site-brand__exit {
  grid-column: 3;
  justify-self: end;
  transition: color var(--dur-state) var(--ease-out);
}

.site-brand__exit:hover {
  color: var(--text-primary);
}

/*
 * THE CONTACT LINK, IN THE TRACK THAT WAS ALREADY THERE (v0.20.0, operator request).
 *
 * Track one has existed since v0.12.1 with nothing in it: `1fr auto 1fr` keeps the two flexible
 * tracks equal so the lockup's track is the exact middle of the bar, and the empty first track is
 * half of that mechanism. Seating a child in it changes no rule above - the lockup keeps
 * grid-column 2, Sign out keeps 3 - which is why this rule declares placement and nothing else.
 *
 * The chrome is .btn .btn--text and none of it is restated here. That family is the product's
 * ghost button: --surface-raised fill, a 1px --border-operable boundary, zero radius, DM Mono at
 * the edge size. Restating any of it would be a second place for the button to drift from itself.
 *
 * WHY A BOUNDARY RATHER THAN JUST TYPE. The operator asked for it to read clearly and to be more
 * than plain text. There is no accent to spend - the palette is achromatic by design - so the
 * available devices are luminance, a hairline and a glyph. --border-operable is defined as the
 * boundary of anything operable, so a bordered control beside a bare-text Sign out states the
 * difference in the system's own vocabulary instead of inventing one.
 *
 * THE BOUND ON "EXACT CENTRE" IS NOW TWO-SIDED, and the arithmetic is the reason the narrow
 * treatment below is not cosmetic. DM Mono is 600/1000 upem, so at --type-edge with
 * --tracking-edge each character advances 8.16px: SIGN OUT measures 65.3px (the figure the note
 * above already carried) and hello@wadavfx.com measures 138.7px, which with the icon, the gap,
 * the padding and the border makes this control ~195px. `1fr` cannot shrink below min-content, so
 * where 195px exceeds a track's share the lockup is pushed RIGHT of centre - the mirror of the
 * drift Sign out causes. It clears its share down to a ~630px viewport and stops fitting at all
 * near 500px, which is inside the range the rule below covers.
 */
.site-brand__contact {
  grid-column: 1;
  justify-self: start;
}

/*
 * THE COLUMN IS PADDED ON BOTH SIDES AGAIN, and the right side is the half that came back.
 *
 * It was zero for the right bleed: the strip scrolled, it carried a negative right margin of
 * one page margin, and its last visible frame was cut by the viewport edge rather than ending
 * inside it - which was the whole of what told a producer the row continued. The strip wraps
 * now. There is nothing past the edge to point at, a cut frame would be a crop of a clip
 * nobody asked to crop, and an unpadded right edge would leave every row flush against one
 * side of the screen and inset from the other.
 *
 * overflow-x went with the bleed. It existed to keep the negative margin from becoming a
 * document scrollbar, and it clipped at this column's padding edge - which on a frame wider
 * than the column would now CROP that frame, silently, which is the one thing this system may
 * never do. A frame that overruns is left to overrun: it is visible, and a visible defect is
 * one somebody fixes.
 */
.site-main {
  padding: var(--space-4) var(--page-margin) var(--space-8);
}

/*
 * The lockup comes down a step here for the same reason the type scale does - the element it
 * replaced also had a narrow viewport treatment, and dropping one when the component changed
 * would have been a regression that no rule below reports.
 *
 * IT DOUBLES HERE TOO, so the instruction holds at every width rather than at one of them.
 * 144px is a large share of a 390px screen, and it is affordable for the reason the header is
 * static in the first place (see above): the bar scrolls away, so it is paid for once at the
 * top of the document instead of on every screen of every session.
 */
/*
 * THE CONTACT LINK DROPS TO ITS GLYPH HERE, and the bar's gap comes down one step with it. Both
 * are arithmetic rather than taste - see the two-sided bound stated on .site-brand__contact.
 *
 * At 320px the bar has 272px to spend (320 less two --page-margin). The three children plus two
 * gaps have to fit inside it: an icon-only control is ~38px, the narrow lockup is 144px and SIGN
 * OUT is 65.3px, which is 247.3px, leaving 24.7px for two gaps. --space-4 needs 48 and overflows;
 * --space-2 needs 24 and fits with 0.7px to spare. Keeping the address instead would need 195px
 * for this control alone and overflow by 132px, and the operator asked for the link to be visible
 * rather than for it to be dropped - so what is given up at phone widths is the address text, not
 * the control.
 *
 * THE KEY IS WIDTH ALONE, DELIBERATELY, and this is the one place in this stylesheet where that
 * is right. The stack rule in strip.css carries `and (max-aspect-ratio: 1/1)` because its premise
 * is about a viewport's SHAPE - it wants "height to spend, no width" - and keyed on width it fired
 * on the landscape phone whose proportions are reversed. This premise is a genuine width claim:
 * the question is whether 138.7px of type has room. A landscape phone at ~814px has that room and
 * correctly keeps its address. Adding an aspect limb here would take it away for no reason.
 *
 * The address is removed with `display: none` rather than by gating the rule that shows it. A
 * rule that merely stops matching hands the property to the user-agent stylesheet; an explicit
 * declaration is what actually decides the outcome. The control keeps its aria-label, so it is
 * never an unlabelled icon.
 */
@media (max-width: 839px) {
  .site-brand {
    padding: var(--space-2) var(--page-margin);
    gap: var(--space-2);
  }

  .site-brand .brandlogo {
    width: calc(var(--space-12) * 2);
    height: calc(var(--space-12) * 2);
  }

  .site-brand__contact {
    padding-inline: var(--space-1);
  }

  .site-brand__contact-address {
    display: none;
  }
}

/* ==== The folder trail ================================================================
 *
 * The gallery is the operator's own folder tree and one folder is on screen at a time, so
 * this is the way back out of it. It is set in the edge-code register, which is where every
 * code, count and label in this system lives - the trail is a location readout, not a title.
 *
 * The separator is a ::before on every step but the first, so the markup carries no
 * punctuation node that a screen reader would read out between two names.
 *
 * IT IS EMITTED ONLY WHERE THERE IS SOMEWHERE TO GO. app.js builds nothing at the root, so
 * the empty nav collapses and the first row starts at the top of the column, which is where
 * the design puts it.
 */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  color: var(--text-meta);
}

.crumbs__step {
  transition: color var(--dur-state) var(--ease-out);
}

.crumbs__step + .crumbs__step::before {
  content: "/";
  padding: 0 var(--space-1);
  color: var(--text-meta);
}

.crumbs__step[aria-current="page"],
a.crumbs__step:hover {
  color: var(--text-primary);
}

.crumbs:not(:empty) {
  margin-bottom: var(--space-4);
}

/* ==== The reel: rows and job strips ==================================================
 *
 * Air is the only grouping device: 72px between clients, 36px between jobs inside a client,
 * 6px between frames inside a job. The closer two frames sit, the more closely they are
 * related, and there is no card, no panel and no divider doing that work.
 *
 * BOTH TRAILING MARGINS ARE DROPPED ON THE LAST CHILD, which closes an arithmetic defect in
 * the design system's own kit rather than reproducing it: there, every job carries 36px below
 * it and every client row carries 72px below that, so client-to-client actually measures 108px
 * while the spec says 72px. Zeroing the last of each makes the two figures the stated ones.
 */

/*
 * THE STEP BACK OUT, and app.js emits it only where there is a parent to go to. A control that
 * is present at the root and disabled would be a second thing to keep in sync with the tree;
 * absent, there is nothing to desynchronise.
 *
 * It is an <a href>, so it is real navigation - Back, middle-click, open in a new tab and the
 * status bar all work, and none of them would if it were a button running history.back().
 *
 * IT IS BIGGER THAN EVERY OTHER ICON CONTROL IN THE PRODUCT, deliberately: 72px around a 48px
 * glyph against the standard 48 around 24. It is the only control on a page whose content is
 * otherwise entirely frames, and it stands alone above them with nothing beside it to give it
 * scale - at the standard size it reads as a stray mark on a wall of pictures.
 *
 * display: flex rather than the .btn family's inline-flex: an inline-level box sits on a line
 * box and picks up its leading, which would put a few unaccountable pixels between this and
 * the row header below it.
 *
 * THE SELECTOR IS COMPOUND, AND IT HAS TO BE. .btn--icon declares the 48px box and .btn
 * declares inline-flex, both at one class of specificity - so a bare .reel__back ties with
 * them and loses on source order, because the button section is further down this file. The
 * control would render at 48px inline with every rule here still reading correctly. Two
 * classes settles it by specificity rather than by position, so moving either section leaves
 * the outcome unchanged.
 */
.btn.reel__back {
  display: flex;
  width: var(--space-12);
  height: var(--space-12);
  margin-bottom: var(--space-3);
}

.reel__back .wv-icon {
  width: var(--space-8);
  height: var(--space-8);
}

.wv-row {
  margin-bottom: var(--row-gap);
}

.wv-row:last-child {
  margin-bottom: 0;
}

.wv-row__header {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--header-to-rail);
}

/* One line, then ellipsis. A client name that wrapped would move the row's first frame down
 * off the height every other row's frames stand at. */
.wv-row__client {
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
 * The count never shrinks and is never omitted. It reads how much work is under the folder on
 * screen, which is the one number a producer opens a folder to find out.
 */
.wv-row__count {
  flex: 0 0 auto;
}

.wv-job {
  margin-bottom: var(--space-6);
}

.wv-job:last-child {
  margin-bottom: 0;
}

.wv-job__label {
  margin-bottom: var(--space-1);
}

/* Nothing published, and the no-script line. Muted, because it is a statement about the page
 * rather than any of the work. */
.reel__empty {
  color: var(--text-meta);
}

/* ==== The two gallery-only frame layers ==============================================
 *
 * Both are appended to a hovered .wv-frame by assets/scrub.js and removed when the pointer
 * leaves it. Both are absolutely positioned in the frame's own positioned layer, which is
 * where strip.css already puts the poster, so DOM ORDER IS PAINT ORDER: poster, sprite, scrim.
 * Both carry pointer-events: none, so a press always reaches the frame under them.
 */

/*
 * The sprite preview. It costs no video decode at all: the frame under the pointer is one
 * rectangle of a single already-fetched JPEG, addressed by background-position, which is the
 * whole point across a page that can hold two hundred frames.
 *
 * THE FILL IS OPAQUE AND HAS TO BE. This layer covers the poster, so any part of it the sheet
 * does not cover would show the poster through the preview - two frames of the same clip on
 * screen at once. scrub.js scales the sheet on both axes so the cell fills the frame exactly,
 * and this is what that arithmetic falls back to if a sheet ever fails to decode.
 *
 * The four properties are written by scrub.js from the VTT cue under the pointer, through the
 * CSSOM rather than a style attribute: this page runs under default-src 'self' with no
 * style-src, so an inline style attribute would be dropped without a word.
 */
.wv-scrub {
  position: absolute;
  inset: 0;
  background-color: var(--surface-poster);
  background-image: var(--wv-sheet, none);
  background-repeat: no-repeat;
  background-size: var(--wv-sheet-w, auto) var(--wv-sheet-h, auto);
  background-position: var(--wv-cell-x, 0) var(--wv-cell-y, 0);
  pointer-events: none;
}

/*
 * A second, structural guarantee. scrub.js already refuses to paint a sprite under `reduce`;
 * this makes the preview unrenderable even if a later code path forgets to ask.
 */
@media (prefers-reduced-motion: reduce) {
  .wv-scrub {
    display: none;
  }
}

/*
 * The hover play glyph over the minimum neutral black wash that keeps it legible on a blown
 * white frame.
 *
 * --scrim-frame IS THE TOKEN FOR THIS, and using it closes a defect in the design system's own
 * kit rather than copying it: there the token is declared and then a different, lighter black
 * is hardcoded past it at the one place it was for. A wash declared in one file and written in
 * another is two values that agree until somebody changes one.
 */
.wv-frame__scrim {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--scrim-frame);
  color: var(--text-primary);
  pointer-events: none;
}

.wv-frame__scrim .wv-icon {
  width: var(--space-8);
  height: var(--space-8);
}

/*
 * The folder mark in a folder tile's index slot - "[folder glyph] 3 CLIPS" where a clip reads
 * "01". Gallery-only, in this file for the same reason the scrim is: the admin renders its own
 * label band and never emits this. The glyph sits at the edge-code's own cap height (12px, one
 * --space-2) so the slot stays one 18px line, and it inherits --ink-muted from the register
 * rather than declaring a colour - the icon rule's stroke is currentColor.
 *
 * vertical-align: the icon is a box on a text baseline, and at 12px in a 12/18 line, -1px is
 * the optical centre - metric centring puts a stroke-drawn glyph visibly high against digits.
 */
.wv-index .wv-icon {
  width: var(--space-2);
  height: var(--space-2);
  vertical-align: -1px;
  margin-right: var(--space-1);
}

/* ==== Buttons ========================================================================
 *
 * One system. THE BASE IS THE GHOST - every icon button in the product is a ghost with a 1px
 * --border-operable boundary, uniformly - and --primary is the one variant that fills.
 *
 * The resting fill is --surface-raised rather than no fill at all, and the hover step goes
 * DOWN to --surface-page rather than up. Every button on this surface sits on a raised ground
 * (the transport bar, the gate card), so a raised hover on a raised ground is a state change
 * that renders as nothing. Both values are elevation steps that already exist; neither is a
 * new one.
 *
 * THIS FAMILY STAYS HERE WITH THE PLAYER GONE, and it is not left over. It is the product's
 * button system, spent on the gate's submit and the reel's back arrow; shared/player.css
 * declares what the transport needs from it scoped under .player, because that surface does
 * not load this sheet. .btn--text has no consumer on this surface at present and is kept for
 * the same reason the rest of the family is - it is one system, and half a system in one file
 * is the drift the split exists to prevent.
 *
 * The focus ring is base.css's single --focus ring and is not restated. Removing it anywhere
 * would leave a control with no focus indication at all.
 */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-3);
  border: var(--hairline) solid var(--border-operable);
  border-radius: var(--radius);
  background-color: var(--surface-raised);
  color: var(--text-primary);
  font-family: var(--font-display);
  font-size: var(--type-title);
  font-weight: var(--weight-medium);
  line-height: var(--leading-title);
  transition: opacity var(--dur-state) var(--ease-out),
    background-color var(--dur-state) var(--ease-out),
    border-color var(--dur-state) var(--ease-out);
}

.btn:hover {
  background-color: var(--surface-page);
}

.btn:active {
  background-color: var(--surface-page);
  border-color: var(--border-strong);
}

/*
 * The one primary action on a surface. --ink fill, --field label: 16.81:1, computed.
 *
 * The border is kept and coloured as the fill rather than removed, so a primary and a ghost
 * sitting side by side are the same size. A border-width change between variants moves a
 * control by two pixels for nothing.
 */
.btn--primary {
  border-color: var(--fill-primary);
  background-color: var(--fill-primary);
  color: var(--text-inverse);
}

/* Opacity rather than a second and third fill token: this system has one primary fill and a
 * hover state is not a new colour. */
.btn--primary:hover {
  background-color: var(--fill-primary);
  opacity: var(--opacity-hover);
}

.btn--primary:active {
  background-color: var(--fill-primary);
  border-color: var(--fill-primary);
  opacity: var(--opacity-active);
}

.btn--primary:disabled {
  border-color: var(--surface-raised);
  background-color: var(--surface-raised);
  color: var(--text-meta);
  opacity: 1;
  cursor: not-allowed;
}

/* Icon-only: a 48px hit area around a 24px icon box. Eight units, which clears the 44px
 * practical minimum with room rather than by a pixel. */
.btn--icon {
  flex: none;
  width: var(--hit-min);
  height: var(--hit-min);
  padding: 0;
}

/*
 * The glyph's box, its stroke and its absence of a fill all come from base.css's .wv-icon
 * rule and from the attributes the script inlining the glyph writes on the <svg> itself, so
 * nothing about the icon set is restated here. Only the pointer is: a click landing on the
 * glyph must be the control's click, and event.target on the <svg> would fail every
 * closest() test a delegated handler makes.
 *
 * NO fill IN THIS RULE, and that is the load-bearing part. .btn--icon svg and base.css's
 * .wv-icon-play svg have equal specificity and this sheet loads second, so a fill declared
 * here would win over the one rule that fills the play triangle - the single filled glyph in
 * the design's whole icon set.
 */
.btn--icon svg {
  pointer-events: none;
}

/* A label-bearing icon-slot button: the quality trigger reads its current rung. */
.btn--text {
  width: auto;
  padding-inline: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--type-edge);
  letter-spacing: var(--tracking-edge);
  font-variant-numeric: tabular-nums;
}

/* ==== The gate =======================================================================
 *
 * The whole of the login surface: one lockup, one lock, one field, one action. The single
 * most-used input in the product, and every failure is reported at the field rather than in a
 * corner.
 *
 * THE CARD IS CENTRED ON BOTH AXES, AND IT TOOK TWO SEPARATE FIXES.
 *
 * Horizontally it was not centred at all. This is a COLUMN flex container, so its cross axis
 * is the horizontal one and align-items governs it - left at its initial `stretch`, the 48ch
 * card was pinned to the left edge while justify-content, which everybody reads first, was
 * doing the vertical axis perfectly. `center` is the fix and it is on the cross-axis property.
 *
 * Vertically justify-content DID centre it, but only inside the band above the page-end
 * footer, which was a sibling consuming its own height - so the card sat high of the viewport
 * centre by half that. The footer existed to hold the version string, the version string is no
 * longer rendered, and removing the element rather than zeroing its height is what makes this
 * true rather than tuned: there is now nothing else in the column to share the space with.
 */
.login {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: var(--page-margin);
}

.login__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  width: 100%;
  max-width: var(--measure-prose);
  padding: var(--space-6);
  background-color: var(--surface-raised);
  /* A hairline, never a shadow. --raised against --field is 1.09:1 - a surface, not a
   * boundary - so without this the card has no edge at all. There is no shadow token in this
   * system and none may be added. */
  border: var(--hairline) solid var(--border-hairline);
  border-radius: var(--radius);
}

/* The lockup is an <h1>, so its user-agent 2em and bold are neutralised here. There is no text
 * beside the artwork - the studio name is inside it. */
.login__brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--type-title);
  font-weight: var(--weight-regular);
  line-height: var(--leading-title);
}

/*
 * TWICE THE 96px BASE, expressed as the doubling rather than as 192. base.css owns the lockup's
 * size for the whole product and this is the login surface asking for two of it; writing the
 * result would be a second figure that has to be recomputed by hand the day the base changes.
 * The scale has no 32-unit step and none is added for one call site.
 *
 * Scoped to .login__brand so the header bar's lockup on the gallery is untouched.
 */
.login__brand .brandlogo {
  width: calc(var(--space-16) * 2);
  height: calc(var(--space-16) * 2);
}

/*
 * The lock, alone and centred, at twice the icon box.
 *
 * The sentence that used to sit beside it is gone, so there is no line for the glyph to be
 * sized to any more - it was 16px to match prose that no longer exists, and 16px alone on a
 * card reads as a stray mark. 48px is the icon box doubled, the same step the lockup above it
 * takes, and it is on the 6px scale where 32 would not be.
 */
.login__lede {
  display: flex;
  justify-content: center;
  color: var(--text-meta);
}

.login__lock {
  width: var(--space-8);
  height: var(--space-8);
}

.login__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.login__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.login__label {
  display: block;
}

/*
 * The fill is the card's own surface and the 1px --border-operable boundary is the entire
 * control. That is the design's rule for anything operable, and it is what keeps the field
 * readable as a field on a raised card without inventing a third elevation step.
 *
 * The value is set in the mono face: a password is a code, not a name.
 */
.login__input {
  width: 100%;
  padding: var(--space-2);
  background-color: var(--surface-raised);
  border: var(--hairline) solid var(--border-operable);
  border-radius: var(--radius);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: var(--type-title);
  line-height: var(--leading-title);
  transition: border-color var(--dur-state) var(--ease-out);
}

.login__input:hover {
  border-color: var(--border-strong);
}

/* The border change AND the ring. base.css supplies the ring; either half alone is a field
 * that reports focus on only one of the two. Both are gated on keyboard modality by the same
 * :where() prefix - shared/focus.js writes data-pointer on <html> - and :where() adds zero
 * specificity, so this rule still resolves against .login__input--error exactly as it did. */
:where(html:not([data-pointer="true"])) .login__input:focus-visible {
  border-color: var(--ring-focus);
}

/*
 * The one place a fenced value reaches a client surface, and it reaches it as a border rather
 * than as a signal in its own right: the message below the field is what carries the meaning,
 * and the colour only agrees with it.
 */
.login__input--error {
  border-color: var(--admin-fail);
}

/* Directly beneath the field it describes, bound to it by aria-describedby. Never a corner
 * toast. The role class sets nowrap for codes on one line; a sentence needs to wrap. */
.login__error {
  color: var(--admin-fail);
  white-space: normal;
}

/* Intrinsic width. A submit stretched across the card would be the largest object on a surface
 * whose only real content is one field. */
.login__submit {
  align-self: flex-start;
}
