/*
 * The whole of the client's presentation. Colours come from theme.css and no
 * literal belongs here.
 *
 * Three screens share one panel, so nearly everything here is the panel, the
 * fields and the buttons. What differs per screen is arrangement, and there is
 * little enough of it to keep in one file (CLAUDE.md §Framework — a file is
 * split when it is several subjects, not when it is long).
 */

/* --------------------------------------------------------------------------
 * Type and metrics
 * -------------------------------------------------------------------------- */

:root {
  /* A tight technical scale. The ratio is deliberately small — a login screen
     with a dramatic type scale looks like a landing page. */
  --step-0: 0.9375rem;   /* body */
  --step-1: 1.0625rem;   /* field text, buttons */
  --step-2: 1.375rem;    /* screen heading */
  --step--1: 0.8125rem;  /* labels, help */
  --step--2: 0.6875rem;  /* the eyebrow, the meta line */

  --radius: 10px;
  --radius-lg: 16px;
  --panel-width: 25.5rem;

  /* One duration and one easing for everything, so the screen moves as one
     thing. The curve is a soft deceleration; nothing here overshoots, because
     a bounce on an authentication screen reads as playful and this is not. */
  --ease: cubic-bezier(0.22, 0.68, 0.32, 1);
  --fast: 130ms;
  --slow: 380ms;

  --mono: ui-monospace, 'SF Mono', SFMono-Regular, 'JetBrains Mono', Menlo,
    Consolas, 'Liberation Mono', monospace;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto,
    'Helvetica Neue', Arial, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; }

/* The user-agent's own `[hidden] { display: none }` is a type-selector rule and
 * loses to any class rule setting `display` — which here is every button and
 * several blocks. Without this, `hidden` silently does nothing on exactly the
 * elements the registration screen reveals in stages, and the whole second half
 * of the form is visible before an enrolment exists. Declared once, with
 * `!important`, so no later rule has to remember it. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100dvh;
  font-family: var(--sans);
  font-size: var(--step-0);
  line-height: 1.5;
  color: var(--text);
  background: var(--page-bg);
  /* Three layers, painted once and never animated (AF-VIS-006):
     a faint grid, a broad accent glow behind where the panel sits, and a
     vertical fall-off to the deeper ground at the edges. Together they give
     the panel something to be lit against — "depth from light, not from
     boxes" (SPEC.md §How it looks). */
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
    radial-gradient(ellipse 70% 45% at 50% 26%, var(--accent-glow), transparent 70%),
    linear-gradient(180deg, var(--page-bg) 0%, var(--page-bg-deep) 100%);
  background-size: 46px 46px, 46px 46px, 100% 100%, 100% 100%;
  background-attachment: fixed;
  -webkit-font-smoothing: antialiased;
}

/* --------------------------------------------------------------------------
 * Layout
 * -------------------------------------------------------------------------- */

#app {
  min-height: 100dvh;
  display: grid;
  /* Head, stage, foot. The stage takes the slack so the panel stays centred in
     what is left after the language control and the version line — giving the
     head the `1fr` instead would push the panel down the screen. */
  grid-template-rows: auto 1fr auto;
  /* The safe-area insets matter on a phone with a notch and a home indicator;
     without them the footer sits under the drag bar. */
  padding:
    max(1.25rem, env(safe-area-inset-top))
    max(1.25rem, env(safe-area-inset-right))
    max(1.25rem, env(safe-area-inset-bottom))
    max(1.25rem, env(safe-area-inset-left));
  gap: 1.25rem;
}

.stage {
  display: flex;
  align-items: center;
  justify-content: center;
  /* min-height rather than height, so the registration screen may exceed the
     viewport and scroll (SPEC.md §How it looks — the login screen must not,
     the registration screen may). */
  min-height: 0;
}

.panel {
  width: 100%;
  max-width: var(--panel-width);
  background: var(--panel-bg);
  border-radius: var(--radius-lg);
  padding: 1.75rem 1.625rem 1.5rem;
  /* The edge is one hairline drawn as an inset ring, brighter along the top —
     a lit edge rather than a border. The outer shadow is broad and soft so the
     panel appears to float in the glow rather than to sit on a surface. */
  box-shadow:
    inset 0 1px 0 0 var(--panel-edge-top),
    inset 0 0 0 1px var(--panel-edge),
    0 24px 60px -20px var(--panel-shadow),
    0 2px 8px -2px var(--panel-shadow);
  position: relative;
}

/* The accent hairline across the top of the panel. It fades out at both ends,
   so it reads as a highlight catching an edge rather than as a rule. */
.panel::before {
  content: '';
  position: absolute;
  inset: 0 auto auto 0;
  width: 100%;
  height: 1px;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  background: linear-gradient(90deg, transparent, var(--accent-line) 22%,
    var(--accent-line) 78%, transparent);
}

/* --------------------------------------------------------------------------
 * The mark and the heading
 * -------------------------------------------------------------------------- */

.brand {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-bottom: 1.375rem;
}

/* The mark is drawn in CSS rather than being an image: it is two shapes, and a
   file would be a request on a screen whose whole job is to be fast and
   current (SPEC.md §Nothing is cached). It is not a logo — SPEC.md forbids a
   logo lockup — it is a locating glyph, the same role a favicon plays. */
.brand-mark {
  position: relative;
  width: 1.75rem;
  height: 1.75rem;
  flex: none;
  border-radius: 7px;
  background: linear-gradient(150deg, var(--accent-bright), var(--accent));
  box-shadow: 0 0 0 1px var(--accent-line), 0 4px 14px -4px var(--accent-glow);
}

.brand-mark::after {
  /* A keyhole: a disc over a tapered stem, in the panel's own colour, so the
     glyph reads as cut out of the accent rather than drawn on top of it. */
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle 3.2px at 50% 40%, var(--accent-text) 99%, transparent),
    linear-gradient(var(--accent-text), var(--accent-text)) 50% 56% / 3px 6px no-repeat;
}

.brand-name {
  font-size: var(--step--2);
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.screen-title {
  margin: 0 0 0.375rem;
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.014em;
  line-height: 1.2;
}

/* The heading is focused on a screen change (AF-VIS-014), which means it takes
   focus programmatically — and a heading showing a focus ring for a mouse user
   who did not ask for it is noise. `:focus-visible` would not fire for a
   programmatic focus, so the ring is suppressed outright here; the heading is
   not interactive and the announcement is what the focus was for. */
.screen-title:focus { outline: none; }

.screen-lede {
  margin: 0 0 1.25rem;
  color: var(--text-dim);
  font-size: var(--step--1);
}

/* --------------------------------------------------------------------------
 * Fields
 * -------------------------------------------------------------------------- */

.field { margin-bottom: 0.875rem; }

.field-label {
  display: block;
  margin-bottom: 0.3125rem;
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--text-dim);
}

.field-input {
  width: 100%;
  padding: 0.6875rem 0.8125rem;
  font-family: inherit;
  font-size: var(--step-1);
  color: var(--text);
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: var(--radius);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
  /* iOS zooms the page when a field smaller than 16px is focused, which throws
     the layout away for the rest of the visit. --step-1 is 17px for that
     reason and this keeps the appearance from being overridden. */
  -webkit-appearance: none;
  appearance: none;
}

.field-input::placeholder { color: var(--text-faint); }

.field-input:hover:not(:disabled) { border-color: var(--field-border-hover); }

/* Focus is the accent, and it is a ring plus a border rather than the browser
   default removed and never replaced (AF-VIS-013). `:focus-visible` on the
   input specifically, so a click does not draw the ring but a tab does — and
   the border still changes for both, so a mouse user sees where they are. */
.field-input:focus {
  outline: none;
  border-color: var(--accent);
}

.field-input:focus-visible {
  box-shadow: 0 0 0 3px var(--accent-soft), 0 0 18px -6px var(--accent-glow);
}

.field-input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Machine values in a monospace face (AF-VIS-004). A one-time code in a
   proportional face looks like a phone number; spaced out in a mono face it
   looks like what it is. */
.field-input.is-code {
  font-family: var(--mono);
  font-size: 1.25rem;
  letter-spacing: 0.34em;
  /* The letter-spacing is applied after each glyph including the last, which
     visually shifts the text left of centre; the padding compensates. */
  padding-right: calc(0.8125rem - 0.34em);
}

/* Chrome and Safari paint their own background on an autofilled field, which
   is white and destroys the dark theme. There is no property to disable it —
   the standing workaround is an inset shadow large enough to cover the field,
   with a long transition so the browser's own repaint does not win. This is
   the one place in this file where a rule is a workaround rather than a
   design, and it is load-bearing: without it an autofilled login form (which
   is the case this client is built around) is unreadable in the dark theme. */
.field-input:-webkit-autofill,
.field-input:-webkit-autofill:hover,
.field-input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 100px var(--field-bg) inset;
  box-shadow: 0 0 0 100px var(--field-bg) inset;
  transition: background-color 100000s ease-in-out 0s;
  caret-color: var(--text);
}

.field-hint {
  margin: 0.375rem 0 0;
  font-size: var(--step--2);
  color: var(--text-faint);
}

/* --------------------------------------------------------------------------
 * Buttons
 * -------------------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.6875rem 1rem;
  font-family: inherit;
  font-size: var(--step-1);
  font-weight: 550;
  letter-spacing: -0.006em;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--fast) var(--ease),
    border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease),
    transform var(--fast) var(--ease), opacity var(--fast) var(--ease);
}

.button:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--accent-soft), 0 0 0 1px var(--accent);
}

.button:disabled { cursor: not-allowed; opacity: 0.5; }

/* The accent's only other home (AF-VIS-003). */
.button-primary {
  color: var(--accent-text);
  background: linear-gradient(180deg, var(--accent-bright), var(--accent));
  box-shadow: 0 1px 0 0 rgb(255 255 255 / 0.16) inset,
    0 6px 20px -8px var(--accent-glow);
}

.button-primary:hover:not(:disabled) { background: var(--accent-bright); }

.button-primary:active:not(:disabled) { transform: translateY(1px); }

.button-primary:disabled { box-shadow: none; }

.button-quiet {
  color: var(--text);
  background: var(--panel-bg-raised);
  border-color: var(--field-border);
}

.button-quiet:hover:not(:disabled) { border-color: var(--field-border-hover); }

.button-link {
  display: inline;
  width: auto;
  padding: 0;
  font-size: inherit;
  font-weight: 500;
  color: var(--accent);
  background: none;
  border: none;
  border-radius: 3px;
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--accent-line);
}

.button-link:hover { text-decoration-color: currentColor; }

/* The spinner shown while a request is in flight (AF-LOG-009). It is the one
   looping animation in the client, and it exists only while a request is
   outstanding — SPEC.md forbids a looping animation as ornament, not a
   progress indicator. Under reduced motion it stops turning and the button's
   text still says what is happening (AF-VIS-008). */
.spinner {
  width: 0.9375rem;
  height: 0.9375rem;
  flex: none;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  opacity: 0.85;
  animation: spin 620ms linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* --------------------------------------------------------------------------
 * Messages
 * -------------------------------------------------------------------------- */

/* One region per screen, and one message in it (AF-LOG-010). It is a live
   region so a screen reader reaches a failure without hunting for it
   (AF-VIS-012), and it is focused on a failed submission (AF-VIS-014). */
.message {
  display: flex;
  align-items: flex-start;
  gap: 0.5625rem;
  margin: 0 0 1rem;
  padding: 0.625rem 0.75rem;
  font-size: var(--step--1);
  border: 1px solid;
  border-radius: var(--radius);
}

.message:focus { outline: none; }

.message-icon {
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-top: 0.125rem;
}

.message-error {
  color: var(--danger);
  background: var(--danger-soft);
  border-color: var(--danger-line);
}

.message-notice {
  color: var(--notice);
  background: var(--notice-soft);
  border-color: var(--notice-line);
}

.message-ok {
  color: var(--ok);
  background: var(--ok-soft);
  border-color: var(--ok-line);
}

.message code {
  font-family: var(--mono);
  font-size: 0.95em;
  word-break: break-all;
}

/* --------------------------------------------------------------------------
 * The footer under the panel
 * -------------------------------------------------------------------------- */

.panel-foot {
  margin-top: 1.125rem;
  padding-top: 1rem;
  border-top: 1px solid var(--panel-edge);
  font-size: var(--step--1);
  color: var(--text-dim);
  text-align: center;
}

.page-foot {
  text-align: center;
  font-size: var(--step--2);
  color: var(--text-faint);
  letter-spacing: 0.02em;
}

.page-foot code { font-family: var(--mono); }

/* --------------------------------------------------------------------------
 * The hub
 * -------------------------------------------------------------------------- */

/* Wider than the other screens, because it holds a row of up to three tiles
   where they hold one column of fields. */
.panel-hub { max-width: 37rem; }

/* The sentence. Centred, and the first thing read — it answers "where am I"
   before anything answers "what can I do" (SPEC.md §The sentence). */
.hub-state {
  margin: 0 0 0.25rem;
  font-size: var(--step-2);
  font-weight: 600;
  letter-spacing: -0.014em;
  line-height: 1.25;
  text-align: center;
  text-wrap: balance;
  /* The sentence ends in an email address (AF-SES-002), which is one long
     unbreakable token — `nobody@rutilo.nl` fits and
     `christopher.wainwright@rutilo.nl` does not. Without this it overflows the
     panel at a phone width instead of wrapping. It was not needed while this
     drew a short username; it is needed now. `anywhere` rather than
     `break-word` so the break can fall inside the address rather than only
     between words, which is the whole difficulty here. */
  overflow-wrap: anywhere;
}

.hub-state:focus { outline: none; }

/* Which application sent the visitor here. Named, not addressed — see
   AF-HUB-003 — so this is ordinary prose rather than a machine value, and is
   not set in the monospace face the destination host used to use. */
.hub-purpose {
  margin: 0 0 1.25rem;
  font-size: var(--step--1);
  color: var(--text-dim);
  text-align: center;
  text-wrap: balance;
}

/* The state alone, with no purpose line under it, still needs the gap. */
.hub-state:last-of-type { margin-bottom: 1.25rem; }

/* --------------------------------------------------------------------------
 * Tiles
 * -------------------------------------------------------------------------- */

/* A row, not a column: three equal choices read as three choices, where a
   stack reads as a ranked list with a preferred first item. They collapse to a
   column only when the row would make each one too narrow to read. */
.tiles {
  display: grid;
  gap: 0.625rem;
  grid-template-columns: repeat(var(--tile-count, 1), 1fr);
}

.tiles-1 { --tile-count: 1; }
.tiles-2 { --tile-count: 2; }
.tiles-3 { --tile-count: 3; }

/* Taller than a plain button and left-aligned, because each carries a line of
   explanation under its name. `align-content: start` keeps the name at the top
   so names line up across tiles whose details wrap to different heights. */
.tile {
  width: 100%;
  display: grid;
  gap: 0.25rem;
  align-content: start;
  padding: 0.875rem;
  min-height: 5.25rem;
  text-align: left;
  justify-items: start;
}

.tile-label {
  font-size: var(--step-0);
  font-weight: 600;
  line-height: 1.25;
}

.tile-detail {
  font-size: var(--step--2);
  font-weight: 400;
  line-height: 1.35;
  opacity: 0.75;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
 * The language control
 * -------------------------------------------------------------------------- */

/* Above the panel and outside it: it belongs to the page rather than to
   whichever screen is showing (AF-LANG-001). */
.page-head {
  display: flex;
  justify-content: flex-end;
  width: 100%;
  max-width: 37rem;
  margin: 0 auto;
}

.language {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.25rem 0.5rem;
  border-radius: var(--radius);
  color: var(--text-faint);
  transition: color var(--fast) var(--ease);
}

.language:hover { color: var(--text-dim); }

.language-glyph { display: flex; }

/* A native select, styled to match. The appearance is reset and the arrow left
   to the browser's own rendering inside the popup — what is styled here is the
   closed state, which is all that shows on the page. */
.language-select {
  font-family: inherit;
  font-size: var(--step--2);
  color: inherit;
  background: none;
  border: none;
  border-radius: 4px;
  padding: 0.125rem 0.25rem;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.language-select:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent-soft), 0 0 0 1px var(--accent);
}

/* The popup is drawn by the operating system and does not inherit the panel's
   colours, so its options are given the page's own instead of showing white
   text on a white list in the dark theme. */
.language-select option {
  color: var(--text);
  background: var(--panel-bg);
}

/* --------------------------------------------------------------------------
 * Registration: the enrolment
 * -------------------------------------------------------------------------- */

.enrolment {
  display: grid;
  gap: 0.875rem;
  margin-bottom: 1.125rem;
  padding: 0.875rem;
  background: var(--panel-bg-raised);
  border: 1px solid var(--panel-edge);
  border-radius: var(--radius);
}

.qr {
  width: 11.5rem;
  height: 11.5rem;
  justify-self: center;
  padding: 0;
  background: var(--qr-light);
  border-radius: 8px;
  box-shadow: 0 0 0 1px var(--panel-edge), 0 6px 22px -12px var(--panel-shadow);
}

.qr svg { display: block; width: 100%; height: 100%; border-radius: 8px; }

/* Which authenticator entry to keep (AF-REG-025). In the notice colour rather
   than as a hint, because missing it fails at a later sign-in with a message
   that deliberately does not say why. */
.enrolment-warning {
  margin: 0;
  padding: 0.5rem 0.625rem;
  font-size: var(--step--1);
  line-height: 1.45;
  color: var(--notice);
  background: var(--notice-soft);
  border: 1px solid var(--notice-line);
  border-radius: 8px;
}

/* The secret as copyable text, always shown alongside the QR code and never
   behind a disclosure (AF-REG-005). A desktop password manager has no camera
   pointed at the screen, and this is its only way in. */
.secret {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.secret-value {
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.625rem;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  color: var(--text);
  background: var(--field-bg);
  border: 1px solid var(--field-border);
  border-radius: 8px;
  overflow-wrap: anywhere;
  user-select: all;
}

.secret-copy {
  width: auto;
  flex: none;
  padding: 0.5rem 0.75rem;
  font-size: var(--step--1);
}

/* --------------------------------------------------------------------------
 * Screen transitions
 * -------------------------------------------------------------------------- */

/* One entrance, once per screen (SPEC.md §How it looks — motion is short and
   few). A short rise and fade; nothing loops and nothing draws attention while
   the user is typing. */
@keyframes rise {
  from { opacity: 0; transform: translateY(8px) scale(0.995); }
  to { opacity: 1; transform: none; }
}

.panel { animation: rise var(--slow) var(--ease) both; }

/* The refusal shake. It accompanies the message and never replaces it
   (AF-VIS-008) — the message is what carries the meaning, and this is what
   makes a second failure feel different from the first when the text has not
   changed. */
@keyframes shake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(3px); }
  30%, 50%, 70% { transform: translateX(-5px); }
  40%, 60% { transform: translateX(5px); }
}

.panel.is-refused { animation: shake 420ms var(--ease); }

/* Everything above, suppressed (AF-VIS-007). Not `animation: none` on a
   wildcard — that would also stop the spinner, which is the one place motion
   is carrying information about something still happening. It is slowed to
   near-still instead, so it remains a progress indicator without being motion
   anyone asked to be rid of. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  .spinner {
    animation-duration: 2400ms !important;
    animation-iteration-count: infinite !important;
  }
}

/* --------------------------------------------------------------------------
 * Small screens
 * -------------------------------------------------------------------------- */

@media (max-width: 30rem) {
  .panel {
    padding: 1.375rem 1.125rem 1.25rem;
    border-radius: var(--radius);
  }

  .panel::before { border-radius: var(--radius) var(--radius) 0 0; }

  /* Tiles stack. Three across a phone leaves each about six characters of
     width, narrower than the words in them — the row is what makes three
     choices read as equals, and below this width it stops doing that and
     starts hiding them instead. Stacked, they are still three equal blocks. */
  .tiles { grid-template-columns: 1fr; }

  .tile {
    min-height: 0;
    padding: 0.75rem 0.875rem;
  }

  /* The sign-in screen must fit without scrolling (AF-VIS-005), and the
     enrolment block is the tallest thing in the client — it is the
     registration screen's, which may scroll. */
  .qr { width: 9.5rem; height: 9.5rem; }
}

/* A short viewport — a phone in landscape, or a desktop window dragged small.
   The vertical rhythm is what gives first, before anything is allowed to
   scroll. */
@media (max-height: 44rem) {
  .brand { margin-bottom: 1rem; }
  .screen-lede { margin-bottom: 1rem; }
  .field { margin-bottom: 0.6875rem; }
}
