/* =====================================================================
 * ACCOUNT PAGES - layout only. Linked by the templates under
 * templates/publisher/account/, never by base.html: it is the one part of
 * the site anonymous visitors mostly never see, so it does not belong in the
 * critical path of every page.
 * =====================================================================
 *
 * ONE EXCEPTION, and it is at the FOOT of this file: the header bell
 * (.notif-bell) is part of the account area but ships in the site header, so
 * templates/publisher/components/notification_bell.html links this stylesheet
 * on every page that has a header. Keep that section small for that reason. If
 * it ever needs more than a button, a badge and a dropdown, split it out rather
 * than putting the rest of the account area into every page's critical path.
 *
 * THREE OWNERS, THREE SECTIONS. The sign-in flow (login, register, logout, the
 * four password-reset steps) writes under .auth-*; the signed-in account area
 * writes under its own names; the bell writes under .notif-bell-*. Keep new
 * rules inside the section that owns the component so the halves can be edited
 * without reading each other.
 *
 * PAINT IS main.css's. Everything below is layout, spacing and the states a
 * form has that the foundation has no rule for (a field in error, a summary of
 * what went wrong, a file the browser has just refused). The surfaces, the
 * fields and the buttons on these pages are .panel, .field/.field-label/
 * .field-input and .btn/.btn-brand exactly as they ship - there is no parallel
 * form system here, and there must not be.
 *
 * WHERE A GRADIENT IS ALLOWED, AND WHAT IT MEASURES. Three, all of them
 * tokens from tokens/<theme>.css, all of them on CHROME or on a PLATE, and
 * none of them behind a line of body copy or under a form field:
 *
 *   .auth-card::before  --grad-rule, the site's 3px signature, as the card's
 *                       top edge. It is the one gradient in the token file
 *                       that CARRIES NO INK - nothing is ever printed on it -
 *                       so what it has to do is stay visible against the card
 *                       it rules rather than clear a text ratio. Measured
 *                       against --surface anyway: light #4f46e5 6.29:1,
 *                       #7c3aed 5.69:1, #0ea5e9 2.77:1; dark #6366f1 3.81:1,
 *                       #7c3aed 2.99:1, #38bdf8 7.96:1.
 *   .auth-submit        --grad-brand, the primary action, carrying a white
 *                       label. THIS one carries ink, so both stops clear
 *                       4.5:1 in BOTH themes and the worst is quoted:
 *                         rest   #4f46e5 6.29:1  /  #7c3aed 5.69:1
 *                         hover  light #4338ca 7.94:1 / #7c3aed 5.69:1
 *                                dark  #5a51ea 5.56:1 / #7c3aed 5.69:1
 *                       Worst case on the loudest control in this file:
 *                       5.56:1. Both stops of --grad-brand are tokens that are
 *                       DELIBERATELY THE SAME IN BOTH THEMES (--brand-fill,
 *                       --chip-popular), which is why one measurement answers
 *                       for the rest state twice over.
 *   .avatar--none       --grad-brand again, as the plate behind the initial
 *                       shown when a reader has no picture. Same two stops,
 *                       same 6.29:1 / 5.69:1, and the glyph on it is --on-brand.
 *
 * Nowhere else. The section nav's current pill stays a FLAT --brand-fill so it
 * reads as the same object as .chip.is-active on every other page of the site,
 * and a sweep behind a settings panel or a bookmark row would be a gradient
 * under running text.
 *
 * ------------------------------------------------------------------
 * SIGN-IN FLOW (apps/accounts/views/auth.py)
 * ------------------------------------------------------------------ */

/* One column, centred, and never wider than a form wants to be. The width is
   a min() rather than a media query: a form is the same 34rem on a phone that
   happens to be 34rem wide as it is in the middle of a desktop. */
.auth {
  display: flex;
  justify-content: center;
  padding-block: var(--space-7);
}

.auth-card {
  padding: var(--space-6);
  /* The 3px edge below hangs off this box. */
  position: relative;
  width: min(100%, 34rem);
}

/* The site's own top rule, repeated as the card's top edge - the same sweep
   the document already wears above the header, which is what ties a page with
   no nav, no rail and no bands back to the rest of the site. A pseudo-element
   and not a border-image so the panel keeps its hairline and its radius, and
   NOT overflow:hidden on the card, which would clip the focus ring off any
   control that reached its edge. */
.auth-card::before {
  background: var(--grad-rule);
  border-radius: var(--r-panel) var(--r-panel) 0 0;
  content: "";
  height: 3px;
  inset: 0 0 auto;
  pointer-events: none;
  position: absolute;
}

.auth-card__title { margin-bottom: var(--space-2); }

.auth-card__lead {
  color: var(--text-2);
  margin-bottom: var(--space-5);
}

/* The way to the other page in the pair - "no account yet?", "already have
   one?". Below the form and separated from it, because it is a different
   errand from the one the visitor came here for. */
.auth-card__alt {
  border-top: 1px solid var(--border);
  color: var(--text-2);
  font-size: var(--fs-meta);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
}

.auth-form { margin: 0; }

/* The submit and its escape hatch on one line, wrapping to two when the button
   plus the link no longer fit rather than at a breakpoint nobody measured. */
.auth-actions {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

/* The one action on the page: a full tap target, because on a phone this is
   the button between a reader and their account, and the only gradient in this
   file that carries a word. See the block at the top for both stops' ratios.
   Only the BACKGROUND moves - the border stays --brand-fill, because a token
   that a rule feeds to border-color can never be a gradient (an invalid
   border-color drops to currentColor and puts a white hairline round the
   button). The focus ring is main.css's and is drawn at offset 2px, OUTSIDE
   this plate and on the card behind it, so the default indigo is what clears
   there and there is no --ring override to make. */
.auth-submit {
  background: var(--grad-brand);
  min-height: var(--tap);
}

.auth-submit:hover { background: var(--grad-brand-hover); }

.auth-link { font-size: var(--fs-meta); }

/* What the field expects, printed before it is typed in. The password rules
   are AUTH_PASSWORD_VALIDATORS' own help texts. */
.auth-hint,
.auth-rules {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  margin-top: var(--space-2);
}

.auth-rules {
  list-style: disc;
  padding-left: var(--space-5);
}

.auth-rules__item + .auth-rules__item { margin-top: var(--space-1); }

/* What a page with no form has to say - the three notes under "check your
   email". Body size, not meta: it is the content of that page, not a caption. */
.auth-notes {
  color: var(--text-2);
  list-style: disc;
  margin-bottom: var(--space-5);
  padding-left: var(--space-5);
}

.auth-notes__item + .auth-notes__item { margin-top: var(--space-2); }

/* --- the two error states -------------------------------------------------
 * --hot is a SIGNAL token and never carries a word (tokens/default.css), so it
 * draws the rule and the field's border while --danger-ink carries the text.
 */

.auth-error {
  color: var(--danger-ink);
  font-size: var(--fs-meta);
  margin-top: var(--space-2);
}

/* THREE CUES, NOT ONE. The border is the glance, the label recolours so the
   state survives for anyone who cannot separate rose from slate at a 1px
   hairline, and the sentence under the field is what actually says what is
   wrong. Colour is never the only carrier. */
.field.is-bad .field-input { border-color: var(--hot); }
.field.is-bad .field-label { color: var(--danger-ink); }

/* A rejected field stays rejected while it is being corrected: main.css's
   .field-input:focus repaints the border --brand, and this pairing has the
   same weight, so it needs the later declaration to win. Focus is still
   unmistakable - it is the 2px ring outside the control, not the border. */
.field.is-bad .field-input:focus { border-color: var(--hot); }

/* The summary above the form: a wrong password, a throttled pair, a site with
   no mailer. role="alert" in the markup, so this only has to look like one. */
.auth-alert {
  background: var(--surface-inset);
  border-left: 3px solid var(--hot);
  border-radius: var(--r-card);
  color: var(--danger-ink);
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
}

.auth-alert__line { margin: 0; }
.auth-alert__line + .auth-alert__line { margin-top: var(--space-2); }


/* ------------------------------------------------------------------
 * SIGNED-IN ACCOUNT AREA (apps/accounts/views/area.py)
 * ------------------------------------------------------------------
 *
 * Bookmarks, history, settings and notifications - the four pages behind
 * templates/publisher/account/base.html, which also carries the section nav
 * they share. Same contract as the half above: PAINT IS main.css's, .panel /
 * .btn / .chip / .field / .empty / .meta are reused exactly as they ship, and
 * what is below is only the shapes the foundation has no rule for.
 *
 * Three more things this half holds itself to:
 *   - No literal colours. --new and --hot are SIGNAL tokens and never carry a
 *     word here either: the unread count is white on a --new PLATE, and every
 *     row that shows one also spells "unread" in the text beside it, so the
 *     state survives a screen reader and anyone who cannot separate the hues.
 *   - Every collection is repeat(auto-fit, minmax(min(<--fit-* floor>, 100%),
 *     1fr)) over a floor that already exists, or a flex line that wraps on its
 *     own bases. No column counts, no fixed cell widths, and NOT ONE @media
 *     width added to the site - the one row that has to change SHAPE rather
 *     than size asks @container how much room it has.
 *   - Hover may change colour, border, shadow and --lift and nothing else.
 *     These are dense scanning lists and a row that reflows under the cursor
 *     loses the reader's place.
 *
 * BLOCKS
 *   shell     .account .account-nav(__list/__item/__link/__label/__badge)
 *             .account-flash(__item) .account-toolbar .account-count
 *             .account-error
 *   bookmarks .follow-grid .follow(__cover/__body/__title/__link/__latest/
 *             __chapter/__state/__unread/__caught/__actions/__remove)
 *   history   .hist-list .hist(__cover/__body/__title/__link/__place/__resume/
 *             __meta/__latest/__behind) .confirm(__summary/__body/__go)
 *   notices   .notif-list .notif .notif--unread
 *             .notif(__kind/__body/__link/__plain/__text/__time)
 *   settings  .settings-grid .setting(__title/__hint/__notice/__value/__help/
 *             __form/__pair/__actions) .avatar .avatar--none .avatar-row
 *             .avatar-row__forms
 *   bell      .notif-bell(__glyph/__count/__pop/__head/__title/__read/__list/
 *             __item/__row/__row-title/__row-body/__row-time/__note/__all)
 *             - its own section at the FOOT of this file, because it is the one
 *             block here that ships in the site header on every page.
 */

.account {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* The section nav. Same pill language as the filter chips so it reads as part
   of the site rather than as a control panel bolted onto it - but it is
   navigation and not a filter, so it keeps its own name and its own current
   state (aria-current="page", where a chip carries "true").

   IT WRAPS, IT DOES NOT SCROLL. A four-pill row is two rows on a 360px phone,
   which costs 52px and shows every destination; a horizontal scroller would
   fit on one line and hide the last two behind a gesture with no affordance -
   on the primary navigation of the whole area. */
.account-nav__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.account-nav__link {
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  color: var(--text-2);
  display: inline-flex;
  font-size: var(--fs-body);
  font-weight: 600;
  gap: var(--space-2);
  letter-spacing: var(--track-chip);
  /* --tap: this is the primary navigation of the whole area and on a phone it
     is operated with a thumb. */
  min-height: var(--tap);
  padding: 0 var(--space-4);
  transition: background var(--t-fast), border-color var(--t-fast),
              color var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
}

.account-nav__link:hover {
  background: var(--surface-alt);
  border-color: var(--brand);
  box-shadow: var(--shadow-1);
  color: var(--link);
  text-decoration: none;
  transform: var(--lift);
}

/* --brand-fill, not --brand: --brand lightens in the dark theme so that it
   still works as ink, which would drop this white label under 4.5:1. Flat, not
   --grad-brand - see the gradient note at the top of the file. */
.account-nav__link.is-current {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  box-shadow: var(--shadow-1);
  color: var(--on-brand);
}

.account-nav__link.is-current:hover {
  background: var(--brand-fill-hover);
  border-color: var(--brand-fill-hover);
  color: var(--on-brand);
}

/* The unread count, on either kind of pill. currentColor for both the plate and
   the ink, so it inherits whichever pill it is sitting on and needs no second
   rule for the current one. 16% is the figure that survives BOTH: white at 16%
   over --brand-fill composites to #6b64e9 and leaves the white numeral at
   4.55:1; --text-2 at 16% over --surface composites to #dee1e4 and leaves the
   slate numeral at 7.87:1. A heavier veil fails the first of those. */
.account-nav__badge {
  background: color-mix(in srgb, currentColor 16%, transparent);
  border-radius: var(--r-pill);
  font-size: var(--fs-meta);
  font-weight: 700;
  line-height: 1;
  min-width: 1.5em;
  padding: .3em .5em;
  text-align: center;
}

/* Flash messages, rendered on arrival after a redirect. They are the only
   confirmation a reader gets that a form did anything, so they carry a colour
   band rather than being a tinted line of body text. As above the signal
   tokens draw the rule and the *-ink tokens carry the words. The base rule is
   the one Django's `info` and `debug` tags land on, so every level has a
   design and none of them falls through to an unpainted line. */
.account-flash {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.account-flash__item {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--brand);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
  color: var(--text-2);
  font-size: var(--fs-body);
  padding: var(--space-3) var(--space-4);
}

.account-flash__item--success {
  border-left-color: var(--new);
  color: var(--success-ink);
}

.account-flash__item--error {
  border-left-color: var(--hot);
  color: var(--danger-ink);
}

.account-flash__item--warning { border-left-color: var(--warn); }

/* The twin of .auth-error, under this half's own name so the two sections stay
   editable without reading each other. Same tokens, same reason: --danger-ink
   is the text stop for the rose. */
.account-error {
  color: var(--danger-ink);
  font-size: var(--fs-meta);
  font-weight: 600;
  margin: 0 0 var(--space-3);
}

/* The band over a list: count on the left, that list's controls on the right,
   wrapping to two rows when they no longer fit rather than at a width. */
.account-toolbar {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  justify-content: space-between;
}

.account-count { margin: 0; }


/* --- the cover box, shared by both list pages ----------------------------
 * A RATIO-LOCKED BOX, NOT AN IMAGE THAT HAPPENS TO ARRIVE AT THE RIGHT SIZE.
 * The wrapper carries the width and the aspect-ratio, so the row's geometry is
 * settled at first paint and stays settled whether the artwork loads late,
 * loads at some other ratio, or 404s. main.css does the same on .cover-wrap
 * and the three card covers; these two anchors are not in that selector list,
 * so they state it here - including the inner hairline as a ::after, because
 * an inset shadow on a replaced element is painted under its content and never
 * seen.
 */

.follow__cover,
.hist__cover {
  aspect-ratio: var(--cover-ratio);
  border-radius: var(--r-card);
  display: block;
  flex: 0 0 auto;
  position: relative;
}

.follow__cover { width: clamp(48px, 8vw, 64px); }
.hist__cover { width: clamp(44px, 7vw, 58px); }

.follow__cover::after,
.hist__cover::after {
  border-radius: var(--r-card);
  box-shadow: var(--shadow-cover);
  content: "";
  inset: 0;
  pointer-events: none;
  position: absolute;
}

/* The box above is the size now, so the image fills it: .cover-sm's 69px cap
   would otherwise be a second opinion about the same width. */
.follow__cover .cover,
.hist__cover .cover {
  height: 100%;
  max-width: none;
  width: 100%;
}


/* --- bookmarks ----------------------------------------------------------- */

.follow-grid {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-row), 100%), 1fr));
}

/* Cover, body, remove. A FLEX LINE and not a three-column grid, because a grid
   can only squeeze: at one card across a 360px phone the old middle track was
   down to ~150px and the title wrapped to four lines beside a button. The line
   below wraps instead - the body keeps a 13rem basis, and when the cover plus
   that basis plus the button no longer fit, the button takes a second row and
   its auto start-margin keeps it on the trailing edge. Nothing here names a
   width the card has to be. */
.follow {
  align-items: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.follow__body {
  display: flex;
  flex: 1 1 min(13rem, 100%);
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.follow__title {
  font-size: var(--fs-card);
  font-weight: var(--fw-head);
  line-height: var(--lh-head);
  margin: 0;
}

.follow__link { color: var(--text); }
.follow__link:hover { color: var(--link-hover); }

.follow__latest,
.follow__state {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  gap: var(--space-2);
  margin: 0;
}

/* :visited BEFORE :hover, always - the other order makes hover unreachable on a
   chapter the reader has already opened, and the recolouring is a read-state
   cue they rely on. */
.follow__chapter { color: var(--link-chapter); }
.follow__chapter:visited { color: var(--link-visited); }
.follow__chapter:hover { color: var(--link-hover); }

/* The number this page exists to show. A plate, because --new as a word is
   2.5:1 on white; the markup spells "unread" beside it either way.

   --badge-new AND NOT --new, WHICH IS THE POINT THE COMMENT ABOVE WAS MAKING
   AND THE RULE WAS MISSING. --new is the SIGNAL emerald: measured on the
   running site, --badge-ink on it is 2.54:1 in light and 1.92:1 in dark, so
   this plate was failing by more than the word it was avoiding. --badge-new is
   the same hue taken down until white clears - 5.48:1 in both themes - and it
   is already what .hist__behind and .notif-bell__count carry, so the three
   unread markers in this file are now one object. --new keeps the two jobs it
   is legal for: the 3px rail on an unread row, where nothing is printed. */
.follow__unread {
  background: var(--badge-new);
  border-radius: var(--r-pill);
  color: var(--badge-ink);
  font-weight: 700;
  letter-spacing: var(--track-chip);
  padding: .2em .6em;
}

.follow__caught { color: var(--success-ink); font-weight: 600; }

.follow__actions {
  flex: 0 0 auto;
  margin: 0 0 0 auto;
}

.follow__remove { min-height: var(--tap); }


/* --- history ------------------------------------------------------------- */

/* A timeline, so it stays one column at every width: these rows are in reading
   order and a grid that wrapped them would put "yesterday" beside "last week".
   One column is not a column COUNT - nothing here has to be told a width. */
.hist-list,
.notif-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* Two tracks only, so this one never has to reflow: the body simply takes what
   is left of the row after a cover that is at most 58px. */
.hist {
  align-items: start;
  display: grid;
  gap: var(--space-3);
  grid-template-columns: auto minmax(0, 1fr);
}

.hist__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.hist__title {
  font-size: var(--fs-card);
  font-weight: var(--fw-head);
  line-height: var(--lh-head);
  margin: 0;
}

.hist__link { color: var(--text); }
.hist__link:hover { color: var(--link-hover); }

.hist__place,
.hist__meta {
  align-items: baseline;
  display: flex;
  flex-wrap: wrap;
  font-size: var(--fs-meta);
  gap: var(--space-2);
  margin: 0;
}

.hist__resume { color: var(--link); font-weight: 600; }
.hist__resume:hover { color: var(--link-hover); }

.hist__latest { color: var(--link-chapter); }
.hist__latest:visited { color: var(--link-visited); }
.hist__latest:hover { color: var(--link-hover); }

.hist__behind {
  background: var(--badge-new);
  border-radius: var(--r-pill);
  color: var(--badge-ink);
  font-size: var(--fs-meta);
  font-weight: 700;
  padding: .15em .55em;
}

/* The destructive disclosure. Closed by default, and it never looks like the
   primary action on the page: the summary is quiet and only the button inside
   it - which nobody reaches by accident - is emphatic.

   THE MAX-WIDTH IS WHAT STOPS IT EATING THE TOOLBAR. A <details> is a flex
   item whose basis is its content, and its content when OPEN is a paragraph:
   measured on the running site it went from a 122px summary to a 771px panel
   the moment it was opened, which threw the count halfway across a 1248px row.
   Capped at a paragraph's measure it opens to 416px on a desktop, and on a
   360px phone min(...,100%) makes the cap the row itself, so the line breaks
   and the disclosure takes the width it needs underneath the count. A measure
   and a percentage - no width is named twice and no breakpoint is added. */
.confirm {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  flex: 0 1 auto;
  font-size: var(--fs-body);
  max-width: min(26rem, 100%);
}

.confirm__summary {
  align-items: center;
  color: var(--text-2);
  cursor: pointer;
  display: flex;
  font-weight: 600;
  min-height: var(--tap);
  padding: 0 var(--space-4);
}

.confirm__summary:hover { color: var(--danger-ink); }

.confirm__body {
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
}

.confirm__body p { margin: 0; }

/* The one button on the site allowed to wrap. .btn is white-space:nowrap
   because a control that reflows mid-row loses its shape - but this label is a
   sentence, and nowrap made it the widest thing in a closed <details>, sticking
   155px out of a 106px box that the browser only happens not to paint. A
   sentence that can wrap is also a sentence that can be read on a phone. */
.confirm__go {
  align-self: flex-start;
  border-color: var(--hot);
  color: var(--danger-ink);
  min-height: var(--tap);
  padding-block: var(--space-2);
  white-space: normal;
}

.confirm__go:hover {
  background: var(--surface-alt);
  border-color: var(--hot);
  color: var(--danger-ink);
}


/* --- notifications ------------------------------------------------------- */

/* The one row on these pages that changes SHAPE rather than size, so it is the
   one that gets a query - and it asks the LIST how wide it is, never the
   window. A notice row is always the full width of its list, so the list's
   inline size is the row's inline size, which is what makes this container the
   right one to ask. */
.notif-list { container: notif / inline-size; }

.notif {
  align-items: baseline;
  display: grid;
  gap: var(--space-2) var(--space-3);
  grid-template-areas: "kind body time";
  grid-template-columns: auto minmax(0, 1fr) auto;
}

/* The rail sits on top of the word "Unread." that the markup puts in .sr-only,
   never instead of it. The padding gives the 3px back, so an unread row and a
   read one start their text on the same vertical line. */
.notif--unread {
  border-left: 3px solid var(--new);
  padding-left: calc(var(--space-4) - 3px);
}

.notif__kind {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  font-weight: 700;
  grid-area: kind;
  letter-spacing: var(--track-chip);
  text-transform: uppercase;
  white-space: nowrap;
}

.notif__body { grid-area: body; min-width: 0; }

.notif__link { color: var(--link); font-weight: 600; }
.notif__link:visited { color: var(--link-visited); }
.notif__link:hover { color: var(--link-hover); }

.notif__plain { color: var(--text-2); font-weight: 600; }

.notif__text {
  color: var(--text-2);
  font-size: var(--fs-body);
  margin: var(--space-1) 0 0;
}

.notif__time { grid-area: time; white-space: nowrap; }

/* Narrow: the kind and the timestamp share the top line and the notice itself
   takes the whole width under them, instead of three tracks fighting over
   ~270px. LAST in the block, because a container query adds no specificity and
   a later plain rule would win. */
@container notif (max-width: 30rem) {
  .notif {
    grid-template-areas:
      "kind time"
      "body body";
    grid-template-columns: auto minmax(0, 1fr);
  }
  .notif__time { justify-self: end; }
}


/* --- settings ------------------------------------------------------------ */

/* --fit-row and not --fit-fact: these cells are FORMS, not the series page's
   attribute pairs. At the 300px floor a 1280px container counts four tracks
   and a password panel with three inputs and a rules list becomes a column of
   slivers; at 440 the same container counts two and each panel is ~616px,
   which is a form. The floor is still one of the tokens, and still the only
   number this grid knows.

   ALIGN-ITEMS:START IS THE OTHER HALF OF THAT. These five panels are wildly
   different heights - a name field beside a three-field password form - and a
   stretched grid gave the Picture panel 340px of empty white below its last
   button on a 1440px screen. Each panel is now the height of what is in it and
   the slack falls on the page, where it is background rather than an
   unfinished card. */
.settings-grid {
  align-items: start;
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-row), 100%), 1fr));
}

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

.setting__title {
  font-size: var(--fs-h2);
  font-weight: var(--fw-head);
  line-height: var(--lh-head);
  margin: 0;
}

.setting__hint,
.setting__help {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  line-height: var(--lh-body);
  margin: 0;
}

/* AUTH_PASSWORD_VALIDATORS ship their help as a <ul>, same as .auth-rules. */
.setting__help ul { list-style: disc; padding-left: var(--space-5); }

.setting__value { color: var(--text); font-weight: 600; word-break: break-all; }

/* THE UNVERIFIED-EMAIL NOTICE. The email panel is the one place on this page
   where pressing the button does NOT change anything: the address only moves
   when the link sent to it is opened, and a reader who does not read that
   sentence will believe they have changed their login when they have not. So
   it is a notice with a --warn rail and body ink, not a grey caption under a
   heading. --warn is a SIGNAL token and draws the rule only; the words are
   --text-2, 9.13:1 on --surface-inset in the light theme. Declared after
   .setting__hint, which it shares an element with, so it wins on order. */
.setting__notice {
  background: var(--surface-inset);
  border-left: 3px solid var(--warn);
  border-radius: var(--r-card);
  color: var(--text-2);
  padding: var(--space-3) var(--space-4);
}

/* A column, so the fields, the pair and the actions stack in one order however
   many fields the form was built with - two of these are {% for field %} loops
   and neither template knows how many it will print. */
.setting__form {
  display: flex;
  flex-direction: column;
  margin: 0;
}

.setting__form .field:last-of-type { margin-bottom: 0; }

/* A 616px panel does not want a 584px-wide "Display name" box. The cap is a
   measure, not a breakpoint: below it the field is still width:100%. */
.setting__form .field-input { max-width: 30rem; }

/* THE SAME THREE CUES THE AUTH FORMS GET, WITH NO CLASS TO ADD. The sign-in
   templates hang .is-bad on the field themselves; these five forms are two
   {% for field %} loops and three hand-written panels, and none of them knows
   in advance which field the server will refuse - it just prints
   <p class="account-error"> after the offending one. So the rule reads the
   shape the markup already has: a .field whose NEXT sibling is that message,
   or - in the password panel, where AUTH_PASSWORD_VALIDATORS' help sits
   between them - whose next-but-one is. Anything looser (a ~ combinator) would
   paint every field above the broken one.

   The message stays the thing that says WHAT is wrong; this is only the glance
   and the recoloured label, so colour is never the sole carrier. */
.setting__form .field:has(+ .account-error, + .setting__help + .account-error) .field-input {
  border-color: var(--hot);
}

.setting__form .field:has(+ .account-error, + .setting__help + .account-error) .field-label {
  color: var(--danger-ink);
}

/* The two reading defaults sit side by side once there is room for both and
   stack when there is not - the same auto-fit rule as every other collection. */
.setting__pair {
  display: grid;
  gap: 0 var(--space-3);
  grid-template-columns: repeat(auto-fit, minmax(min(var(--fit-genre), 100%), 1fr));
}

.setting__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-4);
}

/* --- the picture ---------------------------------------------------------
 * THE ONE HOSTILE INPUT ON THESE PAGES, and the only control the browser
 * refuses on its own: js/account.js hands the file input a setCustomValidity()
 * when the chosen picture is over the cap, which raises the browser's own
 * bubble and blocks the submit. :user-invalid and NOT :invalid - the field is
 * required, so :invalid matches an empty one before anybody has touched it and
 * would paint every fresh settings page with a red box. The server's own
 * refusal (a file that is not an image, an SVG, too many pixels) comes back as
 * .account-error under the same field.
 */

.avatar-row {
  align-items: start;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.avatar-row__forms {
  display: flex;
  flex: 1 1 min(var(--fit-genre), 100%);
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.avatar {
  aspect-ratio: 1;
  background: var(--surface-inset);
  border: 1px solid var(--border);
  border-radius: 50%;
  flex: 0 0 auto;
  height: clamp(64px, 12vw, 88px);
  object-fit: cover;
  width: clamp(64px, 12vw, 88px);
}

/* No picture: the initial on the brand plate, which is a PLATE and therefore
   somewhere a gradient is allowed. --on-brand clears both stops of
   --grad-brand - 6.29:1 on #4f46e5 and 5.69:1 on #7c3aed - though the glyph is
   decoration either way: the reader's name is printed beside it. */
.avatar--none {
  align-items: center;
  background: var(--grad-brand);
  border-color: var(--brand-fill);
  color: var(--on-brand);
  display: flex;
  font-size: var(--fs-h1);
  font-weight: var(--fw-head);
  justify-content: center;
  line-height: 1;
}

/* A file input is not a text input: the 40px well and the two-sided padding
   .field-input gives it wrap a button the browser draws itself. Enough height
   for that button, room round it, and the picker's own control borrowed from
   .btn so it is the same object as every other button on the page. */
input[type="file"].field-input {
  cursor: pointer;
  line-height: var(--lh-body);
  padding: var(--space-2);
}

input[type="file"].field-input::file-selector-button {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-card);
  color: var(--text-2);
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  margin-right: var(--space-3);
  padding: var(--space-1) var(--space-3);
  transition: background var(--t-fast), border-color var(--t-fast),
              color var(--t-fast);
}

input[type="file"].field-input:hover::file-selector-button {
  background: var(--surface-alt);
  border-color: var(--brand);
  color: var(--link);
}

/* The rejected file, before the form is ever sent. Same three cues as
   .field.is-bad: the border, the label, and the browser's own message. */
.account .field-input:user-invalid,
.auth .field-input:user-invalid { border-color: var(--hot); }

.account .field:has(.field-input:user-invalid) .field-label,
.auth .field:has(.field-input:user-invalid) .field-label { color: var(--danger-ink); }


/* --- motion -------------------------------------------------------------- */

/* The one preference this half answers, answered once. Everything above moves
   with transform and opacity alone, so there is nothing else to disarm. */
@media (prefers-reduced-motion: reduce) {
  .account-nav__link {
    transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
  }
  .account-nav__link:hover { transform: none; }
}


/* ------------------------------------------------------------------
 * HEADER BELL (templates/publisher/components/notification_bell.html)
 * ------------------------------------------------------------------
 *
 * THE ONLY PART OF THIS FILE THAT LOADS ON EVERY PAGE. It is deliberately six
 * rules and a dropdown: see the note at the top of the file before adding a
 * seventh.
 *
 * NOTHING IN THE HEADER MOVES WHEN THE COUNT ARRIVES. The page ships from a
 * cookie-blind cache with an empty badge and js/account.js fills it in after
 * load, so the layout has to be settled before the number exists. It is:
 * the button is main.css's .icon-btn, which is already exactly --tap square,
 * and the badge is taken out of flow on top of it - there is no track for it
 * to widen and no sibling for it to push. It is hidden with `visibility` and
 * not `display`, so even its own corner is reserved from the first paint.
 */

.notif-bell {
  /* The dropdown hangs off this box. z-index over the log-in pair, which comes
     after it in the header and would otherwise paint on top of the panel. */
  position: relative;
  z-index: 2;
}

.notif-bell__glyph {
  height: clamp(18px, 1.25rem, 22px);
  width: clamp(18px, 1.25rem, 22px);
}

/* White on the deep --badge-new plate, which is the pairing .badge-new already
   ships and the one the account nav's own unread figure follows: a signal
   colour, always with the word "unread" beside it in .sr-only, never on its
   own. */
.notif-bell__count {
  background: var(--badge-new);
  border: 2px solid var(--surface);
  border-radius: var(--r-pill);
  color: var(--badge-ink);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  min-width: 1.5em;
  padding: 2px 4px;
  position: absolute;
  right: -2px;
  text-align: center;
  top: -2px;
  visibility: hidden;
}

.notif-bell.has-unread .notif-bell__count { visibility: visible; }

/* --- the dropdown -------------------------------------------------------- */

/* Right-aligned under the button because the button is at the right end of the
   header, and never wider than the viewport it has to fit in - a fixed rem
   width would hang off the edge of a 320px phone. */
.notif-bell__pop {
  animation: notif-bell-in var(--t-fast) ease-out;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-2);
  padding: var(--space-4);
  position: absolute;
  right: 0;
  top: 100%;
  width: min(22rem, calc(100vw - var(--space-5)));
}

/* Opacity and a transform, so nothing above or below it reflows while it
   arrives. */
@keyframes notif-bell-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

.notif-bell__head {
  align-items: center;
  display: flex;
  gap: var(--space-3);
  justify-content: space-between;
}

.notif-bell__title {
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: var(--track-chip);
  margin: 0;
  text-transform: uppercase;
}

/* A quiet control, not a second call to action: the reader came here to read
   the list, not to clear it. */
.notif-bell__read {
  background: none;
  border: 0;
  color: var(--link);
  cursor: pointer;
  font-size: var(--fs-meta);
  font-weight: 600;
  padding: 0;
}

.notif-bell__read:hover { color: var(--link-hover); }

/* The list scrolls inside the panel rather than growing it past the fold. */
.notif-bell__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  max-height: min(60vh, 24rem);
  overflow-y: auto;
}

.notif-bell__item {
  border-radius: var(--r-card);
  /* The rail for an unread row, reserved on every row so marking one read
     cannot shuffle the list sideways. See .notif--unread above: it is
     decoration on top of the word, never instead of it. */
  border-left: 3px solid transparent;
}

.notif-bell__item.is-unread { border-left-color: var(--new); }

.notif-bell__row {
  color: var(--text-2);
  display: grid;
  gap: 2px;
  padding: var(--space-2) var(--space-3);
}

a.notif-bell__row:hover {
  background: var(--surface-alt);
  text-decoration: none;
}

.notif-bell__row-title {
  color: var(--text);
  font-size: var(--fs-body);
  font-weight: 600;
}

.notif-bell__row-body,
.notif-bell__row-time {
  font-size: var(--fs-meta);
}

.notif-bell__note {
  color: var(--text-muted);
  font-size: var(--fs-meta);
  margin: 0;
}

.notif-bell__all {
  border-top: 1px solid var(--border);
  font-size: var(--fs-meta);
  font-weight: 600;
  padding-top: var(--space-3);
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .notif-bell__pop { animation: none; }
}
