/* ---------------------------------------------------------------------------
 * Cache-busted by WOOLIE_VERSION (woolie/settings.py). The token is stamped onto
 * this file's URL by woolie/storage.py, so there is no query string to write in
 * the template. If you EDIT this file, BUMP WOOLIE_VERSION - otherwise the URL is
 * byte-identical after the deploy and returning users keep the cached copy.
 * (The rollup bundles are keyed to woolie_frontend/package.json instead.)
 * ------------------------------------------------------------------------ */
/* entrance.css
 * ---------------------------------------------------------------------------
 * The entrance pages: woolie/title_page.html (/), account/signup.html,
 * account/login.html, and the whole password-reset flow - account/password_reset.html,
 * password_reset_done.html, password_reset_from_key.html and
 * password_reset_from_key_done.html. Every one of them overrides the layout's `shell`
 * block and shares this composition. Loaded by allauth/layouts/base.html, which is the
 * shell for every entrance page, so every rule here is scoped to `.ent-*` classes that
 * only those pages emit; allauth's remaining stock templates still render through
 * account/base_entrance.html's centred card and are unaffected by anything in this
 * file.
 *
 * Palette is sampled from static/images/woolie_title_splash_web.jpg - the photo on
 * the brand panel - so the two halves of the split read as one surface rather than
 * unrelated blocks.
 *
 * Layout: two panels side by side from 992px up, one column below. The split is the
 * fix for a tall form pushing the brand mark off screen - each panel sizes and
 * scrolls on its own, so the form can never displace the branding.
 * ---------------------------------------------------------------------------
 */

.ent-page {
  /* Sampled from the splash photo, in the order they appear in it. Declared on the
     body so both panels can use them, not just the form side. */
  --ent-ink: #3a2f28; /* bamboo needle, in shadow */
  --ent-ink-soft: #6b5c50; /* the same, lifted for secondary text */
  --ent-paper: #f7f2e9; /* the stone worktable */
  --ent-paper-edge: #e3d9c8; /* rules and input borders */
  --ent-coral: #cf5b46; /* the salmon skein - the one action on the page */
  --ent-coral-deep: #b3452f;
  --ent-ochre: #c8901f; /* the mustard ball - focus */
  --ent-ochre-lift: #e8b455; /* the same, lifted to read on the dark photo */
  --ent-flag: #a32d22; /* the deep red ball - errors */

  --ent-gutter: 1.5rem; /* width of the yarn-strand column */

  margin: 0;
  min-height: 100vh;

  /* Only the fallback colour lives here now - the photo is on ::before. See below. */
  background-color: #12100e;

  font-family: "Nunito", sans-serif;
  /* cover.css puts a text-shadow and an inset box-shadow on every body. Neither
     belongs on a page that is mostly a light panel. */
  text-shadow: none;
  box-shadow: none;
}

/* Mobile: the photo covers the whole page and the form sits on it as a card, which is
   the arrangement this page already had. The desktop query moves it onto the brand
   panel alone and switches this off.

   On a FIXED PSEUDO-ELEMENT rather than on the body background, which is what stopped
   the image tiling down the page. A body background with no background on <html> is
   propagated to the canvas, but it is still *sized* against the root element's box -
   and allauth/layouts/base.html sets <html class="h-100">, so that box is exactly one
   viewport tall. `cover` therefore covered 100vh, and every pixel of page below that
   got a fresh tile. `position: fixed; inset: 0` is always exactly the viewport, so
   there is nothing left to repeat into; `no-repeat` is belt and braces.

   Deliberately NOT `background-attachment: fixed`, which achieves the same framing but
   is the one property iOS Safari mishandles - it rescales the image mid-scroll.
   A fixed positioned element is handled correctly there.

   Absolute /static/ path to match how woolie.css writes .cover-image and .bkgrdImg -
   a {% static %}-aware URL would mean inlining CSS. */
.ent-page::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background-color: #12100e;
  background-image:
    linear-gradient(rgba(18, 16, 14, 0.15), rgba(18, 16, 14, 0.3)),
    url("/static/images/woolie_title_splash_web.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

/*********************/
/*-- Split shell   --*/
/*********************/

.ent-split {
  min-height: 100vh;
}

/* Mobile first: one column, normal document flow, scrolling from the top.
   Explicitly NOT vertically centred - centring is what pushed the brand mark off the
   top of the screen once the card grew taller than the viewport.

   Carries its own scrim rather than darkening the page gradient: the photograph is
   busiest and lightest around the cream and ochre skeins, which is exactly what the
   ochre "loop" has to compete with. Weighted to the top and faded to transparent by
   the bottom, so it darkens the type and nothing else - no band edge where the panel
   meets the form, and the lighter overall treatment of the photo is preserved.

   Scrolls with the brand text because it is on this element, not on the fixed
   ::before. A scrim on the fixed layer would stay put while the words moved out from
   under it. */
.ent-split__brand {
  display: flex;
  justify-content: center;
  padding: 2.5rem 1.5rem 1.5rem;
  background-image: linear-gradient(
    rgba(18, 16, 14, 0.62) 0%,
    rgba(18, 16, 14, 0.5) 55%,
    rgba(18, 16, 14, 0) 100%
  );
}

.ent-split__form {
  padding: 0 1rem 3rem;
}

/*********************/
/*-- Brand panel   --*/
/*********************/

.ent-brand {
  max-width: 30rem;
  text-align: center;
  font-family: Georgia, "Times New Roman", Times, serif;
}

/* Both entrance pages wrap the mark in an <h1> - it is the only heading either one has.
   Resets the browser's h1 margin so the mark keeps sitting where it did; the mark's own
   sizing is on .ent-brand__mark below, so the h1's font-size never comes into it. */
.ent-brand__title {
  margin: 0;
}

.ent-brand__mark {
  display: block;
  width: 100%;
  max-width: 18rem;
  margin: 0 auto;
}

/* Italic, and sized on a single clamp rather than a value per breakpoint.
   Georgia's italic is a true cut, not a slant, and it is what takes the stiffness out
   of the phrase - an upright serif centred in a symmetrical block reads like a plaque.
   The clamp replaces the old 0.95 / 1.45 / 1.55rem steps, which jumped hard between
   phone and tablet; it lands on exactly 1.55rem from 992px up, so the desktop
   composition is unchanged. */
.ent-brand__line {
  margin: 1.35rem auto 0;
  font-size: clamp(1.15rem, 0.95rem + 1vw, 1.55rem);
  font-style: italic;
  line-height: 1.45;
  color: rgba(247, 242, 233, 0.9);
  /* Evens out the ragged edge once the hard break below is out of the way. */
  text-wrap: balance;
}

/* The author's line break is a desktop composition. Below the split it would land in an
   arbitrary place mid-phrase, so let the text wrap on its own and let `balance` above
   decide where. Re-enabled in the desktop query. */
.ent-brand__line br {
  display: none;
}

/*-- The loop -------------------------------------------------------------------------
   Out here rather than in the desktop query, which is where this first landed: below
   992px the word fell back to Georgia and the whole point of the accent was lost on
   every phone. It is one word, at every width.
--------------------------------------------------------------------------------------*/
.ent-brand__loop {
  position: relative;
  font-family: "Playwrite DK Loopet", cursive;
  font-optical-sizing: auto;
  font-weight: 300;
  /* NOT inherited italic. Playwrite DK Loopet ships no italic cut, so an inherited
     `italic` makes the browser synthesise an oblique - it shears an already-slanted
     cursive and the letterforms buckle. */
  font-style: normal;
  font-size: 1.06em;
  /* Playwrite's ascenders are tall enough to inflate the line box of a mixed line,
     which would space the two lines unevenly. An explicit inline line-height caps it. */
  line-height: 1;
  padding: 0 0.05em;
  color: var(--ent-ochre-lift);
  /* One word, never broken across lines. */
  white-space: nowrap;
}

/* A stroke under the word, drawn left to right as the panel settles. Asymmetric radii
   on purpose: a symmetrical arc reads as a ruled underline, this reads as a mark
   somebody made by hand - which is the whole difference between stiff and not. */
.ent-brand__loop::after {
  content: "";
  position: absolute;
  left: -0.06em;
  right: -0.06em;
  /* The span sets line-height:1, so at the phrase's 1.45 line-height there is roughly
     0.22em of gap below it. Sitting the stroke at 0.14em keeps it clear of the second
     line on the desktop two-line composition. */
  bottom: -0.14em;
  height: 0.4em;
  border-bottom: 2px solid var(--ent-ochre-lift);
  border-radius: 0 0 62% 38% / 0 0 100% 100%;
  opacity: 0.7;
  /* `both` holds the scaleX(0) start state through the delay, so no static transform
     is needed - and with the animation off (reduced motion) it simply sits at full
     width instead of vanishing. */
  transform-origin: left center;
  animation: ent-loop-draw 520ms cubic-bezier(0.22, 0.61, 0.36, 1) 620ms both;
}

@keyframes ent-loop-draw {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

/*********************/
/*-- Form column   --*/
/*********************/

/* On mobile this IS the card - paper ground, rounded, lifted off the photo. The
   desktop query strips the chrome, because there the whole right panel is the paper.

   `.ent-card` is the same thing under an honest name, used by woolie/title_page.html.
   Listed alongside rather than renamed: `.ent-signup` is load-bearing for the
   error-state motion rules further down, which are signup's alone. */
.ent-signup,
.ent-card {
  width: 100%;
  max-width: 26rem;
  margin: 0 auto;
  padding: 1.75rem 1.5rem 2rem;
  text-align: left;
  color: var(--ent-ink);
  background: var(--ent-paper);
  border-radius: 18px;
  box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.35);
}

/*********************/
/*-- Card copy     --*/
/*********************/

/* account/password_reset.html only. Signup and login carry no copy inside the card -
   the visible labels and the submit button already say what the page is - but a lone
   email box is indistinguishable from a signup form without a sentence explaining it.

   An <h2>, not an <h1>: the brand mark is the <h1> on every page in this composition,
   and a second one would leave the page with two competing top-level headings.

   No bottom margin on .ent-heading beyond the gap to the lead, because the two are
   always used together; the space before the field stack is the lead's to own. */
.ent-heading {
  margin: 0 0 0.4rem;
  font-family: "Raleway", sans-serif;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--ent-ink);
}

.ent-lead {
  margin: 0 0 1.6rem;
  font-size: 0.95rem;
  line-height: 1.55;
  color: var(--ent-ink-soft);
}

/*********************/
/*-- Field stack   --*/
/*********************/

.ent-fields {
  position: relative;
  padding-left: var(--ent-gutter);
}

/* The yarn strand. One continuous line down the gutter, behind the stitch markers. */
.ent-fields::before {
  content: "";
  position: absolute;
  left: 5px;
  top: 0.65rem;
  bottom: 0.65rem;
  width: 2px;
  border-radius: 2px;
  background: var(--ent-paper-edge);
}

.ent-field {
  position: relative;
  margin-bottom: 1.35rem;
}

.ent-field:last-child {
  margin-bottom: 0;
}

/* One stitch per field: an open loop on the strand, worked as you go. */
.ent-stitch {
  position: absolute;
  left: calc(-1 * var(--ent-gutter));
  top: 0.15rem;
  width: 12px;
  height: 12px;
  border: 2px solid var(--ent-paper-edge);
  border-radius: 50%;
  background: var(--ent-paper);
  transition:
    transform 160ms ease,
    border-color 160ms ease,
    background-color 160ms ease;
}

.ent-field:focus-within .ent-stitch {
  transform: scale(1.3);
  border-color: var(--ent-ochre);
  background: var(--ent-ochre);
}

.ent-field--error .ent-stitch {
  border-color: var(--ent-flag);
  background: var(--ent-flag);
}

/*********************/
/*-- Labels/inputs --*/
/*********************/

/* woolie.css hides these labels, and restyles these inputs, by their auto-generated ids
   (`.entrance-card label[for="id_username"]`). Every one of those rules is scoped to
   `.entrance-card`, which this page no longer renders - the split shell replaced the
   shared card - so none of them reach here and there is nothing to out-specify. The
   selectors below are plain on purpose. Do not reintroduce `.entrance-card` on this
   page without re-checking that whole block of woolie.css. */
.ent-form .ent-field__label {
  display: block;
  margin-bottom: 0.35rem;
  font-family: "Raleway", sans-serif;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ent-ink-soft);
}

/* Matched on the element, not on a class the form adds. WoolieSignupForm does put
   `ent-input` on every widget, but hanging the whole visual design off that would make
   the page depend on ACCOUNT_FORMS['signup'] resolving - and if it ever stopped, the
   inputs would silently lose their styling with no test failing. */
.ent-form input:not([type="hidden"]):not([type="checkbox"]) {
  display: block;
  width: 100%;
  padding: 0.6rem 0.75rem;
  font-family: "Nunito", sans-serif;
  font-size: 1rem;
  color: var(--ent-ink);
  background: #fffdf8;
  border: 1.5px solid var(--ent-paper-edge);
  border-radius: 10px;
  transition:
    border-color 160ms ease,
    box-shadow 160ms ease;
}

.ent-form input:not([type="hidden"]):not([type="checkbox"]):focus {
  outline: none;
  border-color: var(--ent-ochre);
  box-shadow: 0 0 0 3px rgba(200, 144, 31, 0.22);
}

/* The wrapper carries the error state - signup.html sets it straight off
   `{% if field.errors %}`, so it is true whichever form class is in play. */
.ent-form .ent-field--error input {
  border-color: var(--ent-flag);
}

.ent-form .ent-field--error input:focus {
  box-shadow: 0 0 0 3px rgba(163, 45, 34, 0.18);
}

.ent-form input::placeholder {
  color: #a89b8d;
}

/*********************/
/*-- Reveal toggle --*/
/*********************/

.ent-input-group {
  position: relative;
}

/* Must out-specify the base input rule above (0-3-1) or the shorthand `padding` there
   wins and the text runs under the reveal button. */
.ent-form .ent-input-group input:not([type="hidden"]):not([type="checkbox"]) {
  padding-right: 4.25rem;
}

.ent-reveal {
  position: absolute;
  top: 50%;
  right: 0.5rem;
  transform: translateY(-50%);
  padding: 0.2rem 0.45rem;
  font-family: "Raleway", sans-serif;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ent-ink-soft);
  background: transparent;
  border: 1.5px solid var(--ent-paper-edge);
  border-radius: 7px;
  cursor: pointer;
  transition:
    color 160ms ease,
    border-color 160ms ease;
}

.ent-reveal:hover,
.ent-reveal:focus-visible {
  color: var(--ent-ink);
  border-color: var(--ent-ochre);
}

/* signup.html ships this button with `hidden` so it never appears as a dead control
   when the script has not loaded; entrance-signup.js removes the attribute. The class
   sets `position`, not `display`, but be explicit so a future display rule cannot
   resurrect it. */
.ent-reveal[hidden] {
  display: none;
}

/*********************/
/*-- Help + errors --*/
/*********************/

.ent-help {
  margin: 0.35rem 0 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--ent-ink-soft);
}

/* Django renders password help as a <ul>; woolie.css strips list markers globally. */
.ent-help ul {
  margin: 0;
  padding-left: 0;
}

.ent-error {
  margin: 0.35rem 0 0;
  font-family: "Raleway", sans-serif;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.45;
  color: var(--ent-flag);
}

.ent-error__line {
  display: block;
}

/* An unmissable marker that does not rely on colour alone. */
.ent-error__line::before {
  content: "\2192\00a0"; /* → */
}

.ent-alert {
  margin-bottom: 1.5rem;
  padding: 0.85rem 1rem;
  background: rgba(163, 45, 34, 0.08);
  border-left: 3px solid var(--ent-flag);
  border-radius: 8px;
}

.ent-alert:focus {
  outline: 2px solid var(--ent-flag);
  outline-offset: 2px;
}

.ent-alert__lead {
  margin: 0;
  font-family: "Raleway", sans-serif;
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ent-flag);
}

.ent-alert__list {
  margin: 0.5rem 0 0;
  padding-left: 0;
  font-size: 0.88rem;
  color: var(--ent-ink);
}

.ent-alert__list li + li {
  margin-top: 0.25rem;
}

/*********************/
/*-- Submit + foot --*/
/*********************/

.ent-submit {
  display: block;
  width: 100%;
  margin-top: 1.75rem;
  padding: 0.75rem 1rem;
  font-family: "Raleway", sans-serif;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #fffdf8;
  background: var(--ent-coral);
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  transition:
    background-color 160ms ease,
    transform 160ms ease,
    box-shadow 160ms ease;
}

.ent-submit:hover {
  background: var(--ent-coral-deep);
  transform: translateY(-1px);
  box-shadow: 0 0.4rem 0.9rem rgba(179, 69, 47, 0.3);
}

.ent-submit:active {
  transform: translateY(0);
  box-shadow: none;
}

.ent-submit:focus-visible {
  outline: 3px solid var(--ent-ochre);
  outline-offset: 2px;
}

.ent-divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin: 1.5rem 0;
  font-family: "Raleway", sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ent-ink-soft);
}

.ent-divider::before,
.ent-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--ent-paper-edge);
}

.ent-foot {
  margin: 1.5rem 0 0;
  font-size: 0.9rem;
  color: var(--ent-ink-soft);
}

.ent-foot a {
  color: var(--ent-coral-deep);
  font-weight: 700;
  text-underline-offset: 3px;
}

.ent-legal {
  margin-top: 2.5rem;
  font-family: "Raleway", sans-serif;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: #a89b8d;
}

/*********************/
/*-- Login page    --*/
/*********************/

/* The remember/forgot row. Lifted out of the field stack on purpose - a checkbox
   sitting in the yarn strand between two text inputs reads as a third field. Wraps
   rather than crushes if the two labels ever get long in translation. */
.ent-aside {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-top: 1.1rem;
}

.ent-check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* The base input rule excludes [type="checkbox"] precisely so this can size it - left
   to that rule the box would be a full-width 10px-radius panel. */
.ent-check input[type="checkbox"] {
  width: 1rem;
  height: 1rem;
  margin: 0;
  accent-color: var(--ent-coral);
  cursor: pointer;
}

.ent-check input[type="checkbox"]:focus-visible {
  outline: 3px solid var(--ent-ochre);
  outline-offset: 2px;
}

.ent-check__label {
  margin: 0;
  font-size: 0.85rem;
  color: var(--ent-ink-soft);
  cursor: pointer;
}

.ent-quiet-link,
.ent-quiet-btn {
  font-size: 0.85rem;
  color: var(--ent-coral-deep);
  text-underline-offset: 3px;
}

/* The same treatment for a control that has to be a <button>, not an anchor.
   account/password_reset_from_key.html's Cancel submits allauth's hidden logout form,
   so it cannot be a link - but it is an escape hatch, not an action, and giving it
   .ent-btn would set a second full-width button against the real one.

   The reset is not optional: bootstrap.min.css is loaded on these pages and puts a
   border, padding and its own font stack on every button. */
.ent-quiet-btn {
  padding: 0;
  font-family: inherit;
  background: none;
  border: 0;
  text-decoration: underline;
  cursor: pointer;
}

.ent-quiet-btn:hover {
  color: var(--ent-coral);
}

.ent-quiet-btn:focus-visible {
  outline: 3px solid var(--ent-ochre);
  outline-offset: 2px;
}

/* Third-party providers. allauth's provider_list element emits a bare <ul><li><a>, so
   the list is reset here and the anchors borrow the secondary button styling below. */
.ent-providers ul {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

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

/* .ent-actions and .ent-btn are woolie/title_page.html's two navigation buttons, and
   the alternative sign-in methods on account/login.html. Everything below is scoped to
   classes those pages emit, so the signup composition is untouched by construction.

   .ent-brand__title is not here - it moved up to the Brand panel section, because every
   page in this composition wraps the mark in an <h1>. */

/* No padding override on .ent-split__form here. There was one - 1.25rem, to close the
   gap between the card and the colophon - and it was wrong twice over: the auto-margin
   centring in the Centred pages section now owns that spacing on mobile, and at 0-2-0
   it was also beating the desktop `.ent-split__form { padding: 3rem 3.5rem }` shorthand
   (0-1-0), cropping the desktop panel's bottom padding to 1.25rem and sitting the card
   low in its column. */

.ent-actions {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

/* Same shape and weight as .ent-submit, so a person moving from here to the signup
   form meets the same button twice. Anchors, not buttons - these navigate. */
.ent-btn,
.ent-providers a {
  display: block;
  padding: 0.75rem 1rem;
  font-family: "Raleway", sans-serif;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  border: 1.5px solid transparent;
  border-radius: 10px;
  cursor: pointer;
  transition:
    background-color 160ms ease,
    border-color 160ms ease,
    color 160ms ease,
    transform 160ms ease,
    box-shadow 160ms ease;
}

.ent-btn:hover,
.ent-providers a:hover {
  transform: translateY(-1px);
}

.ent-btn:active,
.ent-providers a:active {
  transform: translateY(0);
  box-shadow: none;
}

.ent-btn:focus-visible,
.ent-providers a:focus-visible {
  outline: 3px solid var(--ent-ochre);
  outline-offset: 2px;
}

.ent-btn--primary {
  color: #fffdf8;
  background: var(--ent-coral);
}

.ent-btn--primary:hover {
  color: #fffdf8;
  background: var(--ent-coral-deep);
  box-shadow: 0 0.4rem 0.9rem rgba(179, 69, 47, 0.3);
}

.ent-btn--secondary,
.ent-providers a {
  color: var(--ent-coral-deep);
  background: transparent;
  border-color: var(--ent-paper-edge);
}

.ent-btn--secondary:hover,
.ent-providers a:hover {
  color: var(--ent-coral-deep);
  border-color: var(--ent-coral);
  background: rgba(207, 91, 70, 0.06);
}

/*********************/
/*-- Centred pages --*/
/*********************/

/* woolie/title_page.html and every account page except signup: brand and card centred
   on the y-axis, copyright pinned to the foot.

   Turned on by `ent-page--centred` on the body, NOT by `.ent-page` itself, because
   account/signup.html must keep the top-aligned mobile column it has - centring is what
   pushed the brand mark off the top of the screen there in the first place, and its form
   is tall enough to need every pixel. The modifier is the opt-in for the pages short
   enough to centre, which is all of them bar that one.

   Was `ent-page--title` until 2026-08-12, when login and the whole password-reset flow
   asked for the same arrangement. That modifier is gone entirely - its last rule, an
   .ent-foot timing, now keys off whether the card holds a form. See the Motion
   section. */

/* Copyright and version.

   A sibling of the two panels rather than a child of the brand panel, which is the
   whole trick: in mobile block flow it falls last on the page naturally, with no
   ordering rules at all, and the desktop grid below places it into the foot of the
   left column. One element, both positions.

   Sits on the photo at every width, so it is light-on-dark here and stays that way.

   The version line is the title page's alone; login and password_reset render the
   copyright by itself, because a build number is noise on a page somebody is trying to
   get through. */
.ent-colophon {
  padding: 0.5rem 1.5rem 2rem;
  text-align: center;
  font-family: "Raleway", sans-serif;
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  line-height: 1.5;
  color: rgba(247, 242, 233, 0.72);
  /* On mobile this lands on the page-wide photo layer, which is only lightly darkened
     (0.15-0.3) - at this size and weight the type would otherwise disappear into the
     skeins. Cheaper and less heavy-handed than darkening the photo for everyone. */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}

.ent-colophon p {
  margin: 0;
}

.ent-colophon__version {
  margin-top: 0.3rem;
  font-variant-numeric: tabular-nums;
  color: rgba(247, 242, 233, 0.38);
}

/* Mobile arrangement: brand and card centred in the page, colophon pinned to the very
   bottom edge.

   A max-width query rather than a mobile-first rule, deliberately. `.ent-page--centred
   .ent-split` is 0-2-0 and the desktop `.ent-split { display: grid }` is 0-1-0, so a
   mobile-first `display: flex` here would out-specify the grid and follow the page up
   to the desktop breakpoint. Bounding it by width is the honest fix; adding a
   `display: grid` to the desktop block to undo it would be a trap for whoever edits
   this next.

   Scoped to the modifier and nowhere near signup, whose mobile column is top-aligned
   on purpose - centring it is what pushed the brand mark off screen in the first
   place, and it has a form tall enough to need the room.

   The login and password-reset cards are taller than the title page's two buttons, and
   taller again with an error summary showing. That is handled rather than avoided: the
   auto margins collapse to zero the moment the content fills the viewport, so a long
   page stacks from the top exactly as it did before and nothing is pushed off screen. */
@media (max-width: 991.98px) {
  .ent-page--centred .ent-split {
    display: flex;
    flex-direction: column;
    /* dvh so the footer sits on the real bottom edge rather than behind a mobile
       browser's collapsing toolbar. vh first as the fallback. */
    min-height: 100vh;
    min-height: 100dvh;
  }

  /* Auto margins, not justify-content: they take priority over it, and splitting the
     free space between exactly these two puts an equal gap above the brand and below
     the card - which centres the pair in the space over the colophon and pushes the
     colophon to the bottom edge in the same stroke. justify-content: center would
     centre all three together and leave the colophon floating mid-page.

     When the content outgrows the viewport the free space is zero, both margins
     collapse, and it degrades to plain stacking. */
  .ent-page--centred .ent-split__brand {
    margin-top: auto;
    /* Scrim off. It reads seamlessly on signup because the brand panel starts at the
       top of the page there, so its leading edge lands exactly on the screen edge and
       there is nothing to see. Centred, the panel begins mid-screen and that same edge
       becomes a visible band of darkening above the logo. Moved to the fixed layer
       below, which is the only thing here that is reliably flush with the screen. */
    background-image: none;
  }

  .ent-page--centred .ent-split__form {
    margin-bottom: auto;
  }

  /* The scrim, re-homed onto the fixed photo layer. `position: fixed; inset: 0` makes
     it exactly the viewport at all times, so 0% IS the top of the screen and 100% the
     bottom - the gradient cannot start anywhere a person can see.

     Fairly even top to bottom, lifting slightly at both ends where the light type sits
     (logo and phrase above centre, colophon on the bottom edge). A steeper ramp would
     put its own visible transition back into the middle of the picture, which is the
     problem this is solving.

     0-2-0 beats the base `.ent-page::before` at 0-1-0, so this wins on specificity
     rather than on file order. Only background-image is restated; the positioning,
     sizing and no-repeat all still come from that base rule. */
  .ent-page.ent-page--centred::before {
    background-image:
      linear-gradient(
        rgba(18, 16, 14, 0.58) 0%,
        rgba(18, 16, 14, 0.5) 55%,
        rgba(18, 16, 14, 0.62) 100%
      ),
      url("/static/images/woolie_title_splash_web.jpg");
  }

  /* Clears the home indicator on notched phones now that this really is the last row
     of pixels. max() so it never comes out under 2rem on hardware without an inset. */
  .ent-page--centred .ent-colophon {
    padding-bottom: max(2rem, env(safe-area-inset-bottom, 0px) + 1rem);
  }
}

@media (min-width: 992px) {
  /* A second row, sized to the colophon, under the brand panel. `auto` means it takes
     exactly the height of its content, so the brand panel keeps the rest. */
  .ent-page--centred .ent-split {
    grid-template-rows: 1fr auto;
  }

  /* The photograph moves onto a pseudo-element spanning BOTH rows of the left column.
     Leaving it on .ent-split__brand would stop it at the row boundary, and giving the
     colophon its own copy would re-crop `cover` for a short strip and show a seam
     right across the bottom corner. As the grid's first child it paints under both. */
  .ent-page--centred .ent-split::before {
    content: "";
    grid-column: 1;
    grid-row: 1 / -1;
    background-image:
      linear-gradient(rgba(18, 16, 14, 0.5), rgba(18, 16, 14, 0.72)),
      url("/static/images/woolie_title_splash_web.jpg");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  }

  .ent-page--centred .ent-split__brand {
    grid-column: 1;
    grid-row: 1;
    /* Carried by ::before now - see above. */
    background-image: none;
  }

  .ent-page--centred .ent-split__form {
    grid-column: 2;
    grid-row: 1 / -1;
  }

  .ent-page--centred .ent-colophon {
    grid-column: 1;
    grid-row: 2;
    padding: 0 3rem 2rem;
  }
}

/* Django messages. Rare on this page, but the shared card used to render them and
   dropping the shell must not drop them silently. */
.ent-note {
  margin-bottom: 1.25rem;
  padding: 0.75rem 0.9rem;
  border-left: 3px solid var(--ent-ochre);
  border-radius: 8px;
  background: rgba(200, 144, 31, 0.1);
  font-size: 0.9rem;
  color: var(--ent-ink);
}

.ent-note--error {
  border-left-color: var(--ent-flag);
  background: rgba(163, 45, 34, 0.08);
}

.ent-note--success {
  border-left-color: #7d8f6d; /* the green skein */
  background: rgba(125, 143, 109, 0.12);
}

/*********************/
/*-- Desktop split --*/
/*********************/

/* 992px is Bootstrap's lg breakpoint, which the rest of the site already turns on. */
@media (min-width: 992px) {
  /* The photo belongs to the brand panel now, not the page, so the fixed layer behind
     everything is switched off entirely. The brand panel below re-declares its own
     background-image, which replaces the mobile scrim in the same stroke. */
  .ent-page::before {
    display: none;
  }

  .ent-split {
    display: grid;
    /* Brand panel wider than the form, as in the reference: the form only ever needs
       26rem, and giving it the surplus would just stretch the inputs. */
    grid-template-columns: 1.25fr 1fr;
    height: 100vh;
  }

  /* min-height: 0 on both panels: a grid item defaults to min-height:auto, which
     refuses to shrink below its content and would push the row past 100vh instead of
     letting the panel scroll. */
  .ent-split__brand {
    align-items: center;
    min-height: 0;
    padding: 3rem;
    overflow-y: auto;
    background-image:
      linear-gradient(rgba(18, 16, 14, 0.5), rgba(18, 16, 14, 0.72)),
      url("/static/images/woolie_title_splash_web.jpg");
    background-size: cover;
    background-position: center;
  }

  /* Each panel scrolls on its own. This is what actually fixes the brand mark being
     pushed off screen - however tall the form gets, it cannot displace the other
     column, and its own overflow stays inside this box. */
  .ent-split__form {
    display: flex;
    justify-content: center;
    min-height: 0;
    padding: 3rem 3.5rem;
    overflow-y: auto;
    /* The panel is the paper on desktop. Same colour the shared entrance card used
       (rgba(247,242,233,.94)), opaque here because there is no longer a photograph
       behind it for the alpha to do anything with. */
    background: var(--ent-paper);
  }

  /* `margin: auto` rather than `align-items: center` on the parent. Both centre a
     short form, but a flex item centred with align-items has its overflowing top
     clipped and unreachable once the content is taller than the container - which is
     exactly the situation on a short laptop screen with three errors showing.

     The card chrome comes off here: the panel itself is the paper, so a second paper
     rectangle inside it with a shadow would just look like a seam. */
  .ent-split__form .ent-signup,
  .ent-split__form .ent-card {
    margin: auto;
    padding: 0;
    background: none;
    border-radius: 0;
    box-shadow: none;
  }

  .ent-brand__mark {
    max-width: 24rem;
  }

  /* The authored two-line composition, only where there is room for it. The clamp on
     .ent-brand__line already resolves to 1.55rem at this width, so there is no
     font-size override here any more. */
  .ent-brand__line br {
    display: inline;
  }
}

/*********************/
/*-- Motion        --*/
/*********************/

/* One orchestrated entrance, not scattered effects. Pure CSS: this page overrides the
   layout's `shell` block, so it no longer inherits #intro-main's `opacity: 0` or the
   anime.js call that clears it - which also means it cannot be left blank by a stale
   bundle. Transform + opacity only: both are compositor-only, so there is no per-frame
   layout or paint, and every animation is one-shot. Nothing loops. */
@keyframes ent-rise {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.ent-brand,
.ent-heading,
.ent-lead,
.ent-field,
.ent-actions,
.ent-aside,
.ent-submit,
.ent-divider,
.ent-foot,
.ent-colophon {
  animation: ent-rise 380ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

.ent-brand {
  animation-delay: 80ms;
}
/* Ahead of the field stack: the copy explains the box, so it should not arrive after
   it. Both land inside signup's first-field slot, which nothing on this page uses. */
.ent-heading {
  animation-delay: 140ms;
}
.ent-lead {
  animation-delay: 175ms;
}
.ent-field:nth-child(1) {
  animation-delay: 200ms;
}
.ent-field:nth-child(2) {
  animation-delay: 255ms;
}
.ent-field:nth-child(3) {
  animation-delay: 310ms;
}
.ent-submit {
  animation-delay: 375ms;
}
.ent-divider {
  animation-delay: 410ms;
}
.ent-foot {
  animation-delay: 430ms;
}

/* The title page has two buttons where signup has three fields, so it settles sooner.
   Same for the three pages at the far end of the reset flow, which are a sentence and a
   button. */
.ent-actions {
  animation-delay: 200ms;
}
/* ...but on login the same block is the ALTERNATIVE sign-in methods, sitting below the
   form, so it has to land after it. The sibling combinator does the distinguishing: the
   title page has no .ent-form for .ent-actions to follow. */
.ent-form ~ .ent-actions {
  animation-delay: 430ms;
}
.ent-aside {
  animation-delay: 320ms;
}
/* Same idea for the foot, expressed as "this card has no form" rather than by naming
   the pages. 430ms is right behind a field stack and far too late behind a single
   button - and that distinction IS the presence of a form, so :has() states it directly
   instead of maintaining a list of page classes. This is what retired
   `ent-page--title`, whose last remaining job it was.

   :has() is already load-bearing further down (.ent-signup:has(.ent-alert)), so nothing
   new is being relied on. Where it is unsupported the foot simply arrives late, which
   is the pre-2026-08-12 behaviour and costs nothing. */
.ent-card:not(:has(.ent-form)) .ent-foot {
  animation-delay: 265ms;
}
.ent-colophon {
  animation-delay: 330ms;
}

/* When the form comes back with errors the stagger is wrong - the person has already
   seen the page and is looking for what changed. Show it immediately and tug the
   errored rows once, like a snagged strand.
   Scoped from .ent-signup because .ent-alert sits inside .ent-form but the elements
   being retimed are not all descendants of the form. */
.ent-signup:has(.ent-alert) .ent-field,
.ent-signup:has(.ent-alert) .ent-submit,
.ent-signup:has(.ent-alert) .ent-divider,
.ent-signup:has(.ent-alert) .ent-foot {
  /* Two values because .ent-field--error carries two animations (rise, then snag).
     A single value would apply to both and fire the snag before the rise finishes. */
  animation-delay: 0ms, 380ms;
}

@keyframes ent-snag {
  0%,
  100% {
    transform: none;
  }
  30% {
    transform: translateX(-4px);
  }
  65% {
    transform: translateX(3px);
  }
}

.ent-field--error {
  animation:
    ent-rise 380ms cubic-bezier(0.22, 0.61, 0.36, 1) both,
    ent-snag 320ms ease-out 380ms 1;
}

@media (prefers-reduced-motion: reduce) {
  .ent-brand,
  .ent-heading,
  .ent-lead,
  .ent-field,
  .ent-field--error,
  .ent-actions,
  .ent-aside,
  .ent-submit,
  .ent-divider,
  .ent-foot,
  .ent-colophon {
    animation: none;
  }

  .ent-btn:hover {
    transform: none;
  }

  /* The stroke stays - it is part of the composition, not an effect. Only the drawing
     of it stops, leaving it at full width. */
  .ent-brand__loop::after {
    animation: none;
  }

  .ent-stitch,
  .ent-form input,
  .ent-reveal,
  .ent-submit,
  .ent-btn {
    transition: none;
  }

  .ent-submit:hover {
    transform: none;
  }
}

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

@media (max-width: 575.98px) {
  .ent-signup {
    --ent-gutter: 1.15rem;
  }

  .ent-split__brand {
    padding: 2rem 1.25rem;
  }

  /* No .ent-brand__line font-size here. It used to drop to 0.95rem, which put the
     phrase two-thirds the size of the tablet treatment and made the phone look like a
     different design; the clamp in the base rule now scales it smoothly instead. */
  .ent-brand__mark {
    max-width: 15rem;
  }
}
