/* /join/ — youth member accounts, phase 1 (2026-08-16).
   Everything structural comes from styles.css: .interest-card, .field,
   .ministry-list, .btn-gold, .form-status. This file only adds what the join
   flow itself needs — the step meter, the three-step rail, and the preview. */

.join-h2 {
  font-family: var(--font-display);
  font-size: clamp(1.55rem, 1.2rem + 1.6vi, 2.1rem);
  line-height: 1.15;
  color: var(--ink);
  margin: 0 0 22px;
}

/* The second heading of the explainer band. It is a peer of the first, not a
   sub-head, so it takes the same type and only the air above it says a new
   topic has started. */
.join-h2-next { margin-top: 46px; }

.join-list .ministry-row p { max-width: 58ch; }

/* ── How it works (2026-08-16) ─────────────────────────────────────────────
   The one piece of this page allowed to be bold, and it is the house numbered
   process from STREAMLINED §5 rather than a new invention: mono step number in
   a gold ring, one line of body beside it, hairlines carrying the structure
   like every other list on the site.

   IT IS NOT A PROGRESS METER. The rail that was deleted from this page earlier
   today tracked which of three screens you were on, and there is one screen
   now. This tracks nothing — it is the answer to "what happens if I press
   Join", printed as the three things that happen.

   The ring and the digits are --accent-deep, not --accent: the brand gold
   measures 2.1:1 on this band and would be unreadable type and an invisible
   edge. --accent-deep is the site's own text-safe gold (4.7:1 here, and past
   the 3:1 non-text line for the ring). */
.join-how {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--line);
}
.join-how li {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 16px 4px;
  border-bottom: 1px solid var(--line);
}
.join-how-n {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--accent-deep);
  /* the ring may stay --accent-deep (3.6:1 clears the 3:1 non-text floor), but
     the digit inside it is 11px type on --oak-200 and measured 3.6:1 — under
     4.5:1. --gold-text is the light-canvas label gold: 5.92:1 here.
     (visual QA 2026-09-04) */
  color: var(--gold-text);
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  display: flex;
  align-items: center;
  justify-content: center;
  /* the digits sit on the body's baseline rather than the top of the line box */
  margin-top: 1px;
}
.join-how-t {
  min-width: 0;
  font-size: 16px;
  line-height: 1.6;
  color: var(--ink);
  max-width: 52ch;
}

/* The fine print, shut. Same disclosure the form above uses for the optional
   email — one control, two places, so this band does not introduce a second
   kind of expander. Only the spacing and the paragraph type are added here. */
.join-privacy {
  margin-top: 34px;
  border-top: 1px solid var(--line);
  padding-top: 6px;
}
/* (the summary's own 16px now comes from .join-optional, one step above) */
/* 16px: somebody who opens a disclosure headed "what we can see, and what we
   never do" is there to read it, so this is body copy by definition. */
.join-privacy p {
  margin: 0 0 14px;
  color: var(--steel);
  font-size: 16px;
  line-height: 1.7;
  max-width: 58ch;
}
.join-privacy p:last-child { margin-bottom: 6px; }

/* THE SEAM (2026-08-16). .dark-chapter ends its gradient on --oak-100 and
   .on-light opens on --oak-200, so the hero met the form band on a hard step of
   tone — a cut, which STREAMLINED §6 says reads as a rendering fault rather
   than a decision. The band now opens on the tone the hero closed with and
   ramps into its own over 96px, and the padding it had zeroed comes back so the
   line above the card is not sitting on the seam. */
.join-form-band {
  padding-top: 26px;
  background: linear-gradient(180deg, var(--oak-100) 0%, var(--bone) 96px);
}

/* ONE COLUMN, CENTRED (2026-08-16 night). .ministry-flow is a 5fr/7fr two-column
   grid from 1024px up, which was right while a line of explanation stood beside
   the card. That line described the request-an-account sequence and came off
   tonight — leaving the card alone in the 5fr column, i.e. a sign-in form
   pinned to the narrow left third of a laptop screen. The band is a single
   column now and the card sits in the middle of it at its natural reading
   width, which is what a sign-in has always looked like. Phones are unchanged:
   .ministry-flow is a one-column grid below 1024px either way. */
.join-flow { align-items: start; }
@media (min-width: 1024px) {
  .join-flow { grid-template-columns: minmax(0, 560px); justify-content: center; }
}

.join-alt[hidden],
.join-flow [hidden] { display: none; }

/* styles.css types text and tel inputs; these two are the same control.
   THE FILE INPUT IS NOT IN THIS LIST ANY MORE (2026-08-16). It used to be, and
   `width: 100%` at that selector's weight beat the 1px in .visually-hidden-file
   below — so the input everybody is supposed to never see was an absolutely
   positioned 380px box hanging 37px past the right edge of the scroller (caught
   by mobile-qa's content-clipped check). It is never visible, so it never
   needed typing. */
.join-card .field input[type="email"],
.join-card .field input#j-code {
  width: 100%;
  font-family: var(--font-body);
  font-size: 16px;
  padding: 12px 16px;
  border-radius: var(--radius-input);
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
}
.join-card .field input#j-code {
  font-family: var(--font-mono);
  font-size: 26px;
  letter-spacing: 0.34em;
  text-align: center;
  padding: 14px 16px;
}
/* A REAL BUTTON SINCE 2026-08-21. This was a <label> in front of an off-screen
   <input type="file">; there is no file input on this page any more, so it is a
   <button> and carries its own focus ring and its own press state instead of
   borrowing the hidden input's. appearance:none is what stops iOS repainting it
   as a push button. */
.selfie-btn {
  display: block;
  width: 100%;
  box-sizing: border-box;
  appearance: none;
  -webkit-appearance: none;
  font-family: var(--font-body);
  font-size: 16px;
  text-align: center;
  padding: 14px 16px;
  min-height: 52px;
  border-radius: var(--radius-input);
  border: 1px dashed var(--accent);
  background: var(--paper);
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease),
              transform var(--press-up) var(--ease-press);
}
@media (hover: hover) {
  .selfie-btn:hover {
    border-color: var(--accent-glow);
    box-shadow: var(--shadow-accent-soft);
    transform: translateY(-2px);
  }
}
/* Two changes on the press, never one — and the site suppresses the tap
   highlight, so without this the button is dead to a finger (2026-08-13). */
.selfie-btn:active {
  transition-duration: var(--press-down);
  transform: scale(0.98);
  border-color: var(--accent-glow);
  box-shadow: var(--shadow-accent-soft);
}
.selfie-btn:disabled { opacity: 0.5; cursor: default; transform: none; }
.selfie-btn:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(201, 154, 46, 0.15);
}
@media (prefers-reduced-motion: reduce) {
  .selfie-btn { transition: none; }
  .selfie-btn:hover, .selfie-btn:active { transform: none; }
}
.join-card .field input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(201, 154, 46, 0.15);
}

/* 15px OR 16px, DECIDED PER NODE (2026-08-16 night). The earlier pass put every
   small line on one 15px step, which was tidier than the 12/13/14/15 spread it
   replaced but put five sentences a person actually READS under the 16px
   body-copy floor — mobile-qa failed six nodes on it.
   The rule that settles each one: is this a sentence the reader has to read, or
   is it an annotation on a control?
     16px — .join-opt-block (what the photo is for, and what the code is and how
            long it lasts: both are read at the moment of deciding), the account
            note, the two fine-print paragraphs (a person taps that disclosure
            in order to read it), and the disclosure summaries themselves.
     15px — .join-opt when it sits INSIDE a form label. That is the one true
            annotation left on the page: a fragment hanging off a 12px mono
            label, not a sentence anybody sits down to. It clears the 14px
            secondary floor with a point to spare, and it is stated as the
            deliberate exception rather than quietly left under the line. */
.join-opt {
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--steel);
}
/* The same voice, but as its own line under a control rather than inside a
   label — and at that point it is reading copy, so it takes the body floor. */
.join-opt-block {
  margin: 8px 0 0;
  font-size: 16px;
  line-height: 1.6;
  color: var(--steel);
  max-width: 46ch;
}

/* The photo caption is a CAPTION now, not a second <label for="j-selfie">
   (2026-08-16 night) — it pointed at the same camera as the 52px button below
   it while measuring 286x19, which is a tap target nobody can hit for a control
   they already have. styles.css types `.field label`, so the span carries the
   same mono label voice here rather than inheriting body copy. */
.join-card .field-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 8px;
}

/* THE CODE FIELD, revealed in place. Set off by a rule and a little air so the
   eye lands on it the moment it appears, without the card feeling like it
   changed screens. */
.join-code-field {
  margin-top: 22px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}
.join-code-field .join-alt { margin-top: 12px; }

/* EMAIL, SHUT BY DEFAULT. A disclosure rather than a hidden field: it is real,
   it is findable, and it costs the ten-second path nothing. */
.join-optional {
  margin: 20px 0 0;
}
/* The default triangle is drawn INSIDE the text flow, so a summary that wraps
   at 390px loses its marker and reads as a stray sentence rather than a
   control (measured on an iPhone-width shot, 2026-08-16). A drawn +/- sits
   outside the text and cannot come apart from it. */
.join-optional > summary {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 10px 0;
  font-size: 16px;
  line-height: 1.4;
  color: var(--steel);
  list-style: none;
}
.join-optional > summary::-webkit-details-marker { display: none; }
.join-optional > summary::before {
  content: '+';
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border: 1px solid var(--line);
  border-radius: 50%;
  font-family: var(--font-mono);
  font-size: 14px;
  line-height: 20px;
  text-align: center;
  /* --accent-deep, not --accent (2026-08-16). The brand gold measures 2.1:1 on
     this band, so the +/- glyph — the only thing saying this line is a control —
     was under both the 4.5:1 text line and the 3:1 non-text one. Flagged as
     pre-existing on the optional-email disclosure and fixed here because the
     fine-print block now leans on the same control. */
  color: var(--accent-deep);
}
.join-optional[open] > summary::before { content: '\2212'; }
.join-optional > summary:hover { color: var(--ink); }
.join-optional > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.join-optional[open] > summary { color: var(--ink); font-weight: 600; }
.join-optional .field { margin-top: 6px; }

/* ---------------------------------------------------------------------------
   THE PHOTO STEP (2026-08-17; camera-only since 2026-08-21)

   One route — the camera, running in the page — and then one panel showing the
   crop it would keep. Everything here is built out of tokens the page already
   uses: no new colors and no new type sizes, because the boldness in this step
   is spent in exactly one place, the live frame itself. That is the only thing
   on /join/ that is not a form control, and it earns it by being the whole
   answer to "is this you".
   --------------------------------------------------------------------------- */

.face-stage { margin: 18px 0 22px; }

/* The question, in the card's own display voice — it is a heading in every
   sense except the outline, which belongs to the form's real headings. */
.face-stage-title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1.3;
  color: var(--ink);
}

/* line-height 0 on the frame: an inline <img> otherwise leaves a descender gap
   under itself, which shows as a stripe of --line along the bottom edge. */
.face-frame {
  position: relative;
  display: block;
  width: 100%;
  max-width: 420px;
  border-radius: var(--radius-card);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--line);
  line-height: 0;
}
.face-frame img { display: block; width: 100%; height: auto; }

/* THE RING CHOOSER IS GONE (2026-08-21, Daniel: the photo step must not put
   candidates in front of anybody). .face-pin drew one tappable ring per face in
   a group photo; join.js now picks the highest-chance face itself, so the frame
   only ever shows the crop below. */

/* The crop, shown at the size it will be kept rather than filling the frame —
   "is this you" is a question about a small round photograph, so it is asked
   with one. */
.face-frame[data-mode="crop"] {
  width: 168px;
  height: 168px;
  max-width: 168px;
  border-radius: var(--radius-card);
}
.face-frame[data-mode="crop"] img { height: 100%; object-fit: cover; }

.face-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  margin-top: 14px;
}
.face-actions .join-link { align-self: flex-start; }
@media (min-width: 460px) {
  .face-actions { flex-direction: row; align-items: center; gap: 18px; }
  .face-actions .btn { flex: 0 0 auto; }
}

.join-preview {
  display: block;
  width: 128px;
  height: 128px;
  object-fit: cover;
  border-radius: var(--radius-card);
  border: 1px solid var(--line);
  margin: 0 0 22px;
  background: var(--line);
}

.join-alt { margin: 16px 0 0; display: flex; flex-wrap: wrap; gap: 18px; }

/* A real button, styled as a link, sized as a tap target. */
.join-link {
  appearance: none;
  border: 0;
  background: none;
  padding: 11px 0;
  min-height: 44px;
  font-family: var(--font-body);
  font-size: 16px;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.join-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* The sign-in path reads as a sentence rather than a stray link, because since
   2026-08-16 the menu on every youth page points here for people who already
   have an account as well as for people who want one. Aligned on one baseline:
   the label and the button share a row, and the button keeps its 44px floor. */
.join-alt-strong {
  align-items: baseline;
  gap: 8px;
  font-size: 16px;
  color: var(--ink-2, var(--ink));
}

/* THE SECOND DOOR IS A DOOR, NOT A WORD (2026-09-04, page-scrutiny M-07). The
   only sign-up control on the page was an underlined text link — the weakest
   element on a page whose own callout points at it — so it is drawn as the
   house secondary pill: the hairline-over-fill pair .btn-ghost uses, at this
   sentence's own 16px rather than the 15px button size, so it still sits in
   the line beside "First time here?" instead of becoming a second CTA
   competing with the gold one above it. */
.join-signup {
  padding: 10px 18px;
  border: 1.5px solid var(--control-line);
  border-radius: 9999px;
  background: var(--control-bg);
  box-shadow: var(--shadow-control);
  text-decoration: none;
  font-weight: 600;
  transition: border-color var(--t-fast) var(--ease),
              background-color var(--t-fast) var(--ease),
              transform var(--press-up) var(--ease-press);
}
@media (hover: hover) {
  .join-signup:hover { border-color: var(--ink); background: rgba(var(--ink-rgb), 0.04); }
}
.join-signup:active { transition-duration: var(--press-down); transform: scale(0.97) translateY(1px); }

/* THE SUBMIT IS AT LEAST AS WIDE AS WHAT IT SUBMITS (2026-09-04, M-08). "Text
   me a code" was a ~165px left-aligned pill under a full-width phone field, so
   the one action on the page read as the secondary of the pair. Phone only —
   from 481px the row has space and a full-bleed button would read as a banner. */
@media (max-width: 480px) {
  /* #j-start only — the card has two other .form-foot rows (the add-to-home
     pair and Sign out) and neither is the page's primary action. */
  #j-start { width: 100%; }
}

/* ── WHO THE DOOR IS FOR (2026-08-18) ─────────────────────────────────────
   The same house rule that used to close this card as a quiet hairline aside
   (.join-house-note, deleted tonight), moved to the top of the card and given
   a surface, because a rule read after the form is filled in is a rule nobody
   acted on.
   It is drawn as the ONE tinted thing on a page of hairlines: --oak-200 is a
   real step off the card's --paper, and the 3px --accent-deep rule down the
   left is the site's own text-safe brass, so the panel reads as the house
   speaking rather than as a browser warning. No icon, no red, no border box —
   this is a welcome with a condition in it, not an alert. Its heading takes
   the display face at 18px so it out-ranks the body beside it and still sits
   below the 22px .interest-card h3 under it; the sentence is body copy at the
   16px floor, capped at 46ch so it breaks into three or four readable lines on
   a phone instead of a paragraph. */
.join-door {
  margin: 0 0 24px;
  padding: 15px 17px;
  background: var(--oak-200);
  border-left: 3px solid var(--accent-deep);
  border-radius: 4px;
}
.join-door-h {
  margin: 0;
  font-family: var(--font-display);
  font-size: 18px;
  line-height: 1.25;
  font-weight: 600;
  color: var(--ink);
}
.join-door-b {
  margin: 6px 0 0;
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink-2);
  max-width: 46ch;
}

/* ── ADD IT TO YOUR HOME SCREEN (2026-08-20) ──────────────────────────────
   The same tinted panel as the door above, reused inside the account card so
   the nudge is part of the page instead of floating over it. The panel sits
   between the account facts and the sign-out row, so its own foot is tighter
   than a form's 26px and the two buttons keep their 44px floor.
   .btn sets display:inline-flex, which outranks the browser's own
   [hidden] { display: none } — without this the Install button would show on
   iOS, where there is nothing for it to do. */
.join-a2hs { margin: 18px 0 0; }
.join-a2hs-foot { margin-top: 14px; gap: 14px; }
.join-a2hs .btn[hidden] { display: none; }

/* ── The account view (2026-08-16) ────────────────────────────────────────
   Three facts and a way out. It is a description list because that is what it
   is; the rows stack on a phone and pair up from 560px, and nothing here is a
   control except the sign-out button. */
.join-acc { margin: 18px 0 0; display: grid; gap: 14px; }
.join-acc > div {
  display: grid;
  gap: 2px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.join-acc > div:last-child { border-bottom: 0; padding-bottom: 0; }
.join-acc dt {
  margin: 0;
  font-family: var(--font-mono, inherit);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted, #706b61);
}
.join-acc dd { margin: 0; font-size: 17px; line-height: 1.55; }
.join-acc-note {
  margin: 18px 0 0;
  font-size: 16px;
  line-height: 1.7;
  color: var(--muted, #706b61);
}
@media (min-width: 560px) {
  .join-acc > div { grid-template-columns: 150px 1fr; align-items: baseline; gap: 12px; }
}

/* THE STRANGER'S NOTICE IS NOT FOR MEMBERS (2026-08-16 night). join.js puts the
   sales pitch and the "one screen" line away when it finds a live session, but
   the hero line under them stayed — so a signed-in member's own account page
   opened "You are signed in." and then asked them "Not part of the youth?", now
   in two languages. Done here rather than in join.js because the account view is
   already a state the page can see: #step-account loses [hidden] exactly when a
   member is looking. No :has() support means today's behaviour, which is safe. */
main:has(#step-account:not([hidden])) .media-head-band .chat-note { display: none; }

/* EVERY MESSAGE THIS PAGE SPEAKS WAS INVISIBLE (found 2026-08-17, live).
   styles.css:2274 is `.form-status { display: none }` and it only re-opens for
   `.form-status.ok` / `.form-status.err` — the class pair the site's other
   forms add. join.js has never added those; it writes `data-tone` instead. So
   `.form-status.join-status` computed `display: none` with text in it, and
   "Texting you a code...", "That code is not right.", "We need a clear
   close-up photo of just you" and every network error on the youth signup have
   been painting nothing at all. Verified in webkit against the live page: text
   set, height 0, display none.
   The fix is the display this element always needed. It sits AFTER the
   :empty rule so the collapse still wins at equal weight when there is nothing
   to say, and join.css is linked after styles.css, so it wins the tie against
   .form-status. The .ok/.err half of that rule is left alone — other forms on
   the site use it. */
.join-status:empty { display: none; }
.join-status { display: block; margin-top: 18px; }
.join-status[data-tone="bad"] { color: #8a2b2b; }

.join-card[data-busy="1"] button[type="submit"] { opacity: 0.6; cursor: wait; }

/* The old rail's desktop rule went with the rail (2026-08-16); .join-then owns
   its own bottom margin now. */

/* ═══ THE MEMBER'S OWN PAGE (2026-08-17) ══════════════════════════════════
   Direction: a member's page opens with their own face on a quiet white-oak
   name plate — portrait, name, one mono line — and everything under it is the
   photographs. The boldness is spent in exactly one place, the wall: a dense
   square grid of every picture the archive has of one person, which no other
   page on this site gives anybody. Everything else here is the account card
   the page already was.

   NOTHING NEW IS INVENTED. The circle is the site's face circle (--control-line
   at 2px over --shadow-control, background-origin: border-box — the same edge
   .pp-face and .album-find-face wear, and the same reason: a 1px --line ring
   measures 1.24:1 on this band). The change-photo control is the signup's own
   .selfie-btn. The grid's numbers are .shot-grid's numbers, deliberately:
   2 columns at 390 / 4 from 620 / 5 from 980, because three on a phone was
   measured at 111px and rejected on 2026-07-29. */

/* ── the name plate ─────────────────────────────────────────────────────── */
.acc-head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 0 0 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--line);
}
/* NOT a control: it is their own photograph sitting on their own page, and the
   thing that changes it is the labelled button below. So it takes the house
   circle's edge and none of its hover/press states — STREAMLINED §4, a
   non-clickable object never hovers. */
.acc-portrait {
  flex: 0 0 auto;
  display: block;
  width: 88px;
  height: 88px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--oak-200);
  border: 2px solid var(--control-line);
  box-shadow: var(--shadow-control);
  background-origin: border-box;
}
.acc-portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }
.acc-portrait img[hidden] { display: none; }
.acc-head-t { min-width: 0; display: block; }
/* The name is the headline of this card now, so it takes the display voice and
   the card's h3 rule is overridden here rather than sitewide. */
.acc-head-t #j-acc-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 1.1rem + 1.1vi, 1.7rem);
  line-height: 1.15;
  letter-spacing: -0.02em;
}
/* "Fix my name" under the plate: link voice, quieter and smaller than the
   headline it sits beneath, still a 44px tap target. */
.acc-name-edit { display: block; padding: 8px 0 0; min-height: 44px; font-size: 14px; color: var(--fg-quiet); }
.acc-name-form { margin: 4px 0 20px; }
.acc-name-form input {
  width: 100%; box-sizing: border-box; margin-top: 8px;
  padding: 14px 16px; font: inherit; font-size: 16px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--surface, #fff); color: var(--ink);
}
.acc-name-form input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.acc-name-actions { display: flex; gap: 16px; align-items: center; margin-top: 12px; }
/* The one mono line on the plate — the label voice, saying what this page is. */
.acc-meta {
  display: block;
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-quiet);
}

/* ── NEW PHOTOS OF YOU (2026-08-23) ───────────────────────────────────────
   News, not chrome: the ONE tinted row on a card of hairlines, the same
   recipe .join-door already uses above the form (--oak-200 fill + 3px
   --accent-deep rule down the left) so it reads as the house speaking rather
   than a second button competing with "Change your photo" below it. Ships
   empty and [hidden] — join.js only reveals it when there is actual count
   news, same as the strip it leads to on /my-photos/. The digit gets its own
   pill so the row scans in one glance; the sentence beside it carries the
   same count again for anyone reading with a screen reader, so the pill
   itself is decorative (aria-hidden). Full-row tap target, same 150ms/300ms
   timings and press-down feel as every other control on this card. */
.acc-news {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0 0 20px;
  padding: 12px 16px;
  min-height: 44px;
  box-sizing: border-box;
  background: var(--oak-200);
  border-left: 3px solid var(--accent-deep);
  border-radius: 4px;
  text-decoration: none;
  color: var(--ink);
  touch-action: manipulation;
  transition: background-color var(--t-fast) var(--ease), transform var(--press-up) var(--ease-press);
}
.acc-news[hidden] { display: none; }
.acc-news-count {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 26px;
  padding: 0 7px;
  border-radius: 9999px;
  background: var(--accent-deep);
  color: var(--paper, #fff);
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
}
.acc-news-text {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
}
@media (hover: hover) {
  .acc-news:hover { background: var(--oak-100); }
}
.acc-news:active {
  transition-duration: var(--press-down);
  transform: scale(0.98);
}
.acc-news:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── changing the photograph ────────────────────────────────────────────── */
.acc-photo-edit { margin: 0 0 6px; }

/* ── ONE ROW UNTIL SOMEBODY ASKS (2026-08-23) ───────────────────────────────
   Daniel, on the account card: "it says the camera opens right here, so it
   takes up a ton of space... that looks horrible." He is right — the signup's
   camera panel is 3:4 of the card's width plus two 52px buttons plus a caption,
   roughly 480px of empty frame on a 390px phone, sitting there before anybody
   has said they want to change anything. The card is a settings surface; the
   camera is a moment you summon.
   So the whole thing lives behind one disclosure row that wears the card's own
   control edge (--control-line + --shadow-control, exactly as .selfie-btn-quiet
   does) and the card's own mono label voice. Nothing about the panel below
   changed: same ids, same factories, same wording, same /photo route. */
.acc-photo-open {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  box-sizing: border-box;
  appearance: none;
  -webkit-appearance: none;
  min-height: 52px;
  padding: 12px 16px;
  text-align: left;
  border-radius: var(--radius-input);
  border: 1px solid var(--control-line);
  background: var(--paper);
  box-shadow: var(--shadow-control);
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
  transition: border-color var(--t-fast) var(--ease), background-color var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease), transform var(--press-up) var(--ease-press);
}
/* The label keeps .field-label — the same 12px mono the card labels every field
   with — and only drops the 8px it needs when it STANDS OVER a control instead
   of sitting inside one. */
.acc-photo-open .field-label { margin: 0; }
.acc-photo-open-i { font-size: 18px; line-height: 1; flex: 0 0 auto; }
@media (hover: hover) {
  .acc-photo-open:hover {
    border-color: var(--accent-glow);
    box-shadow: var(--shadow-accent-soft);
    transform: translateY(-2px);
  }
}
.acc-photo-open:active {
  transition-duration: var(--press-down);
  transform: scale(0.98);
  border-color: var(--accent-glow);
  box-shadow: var(--shadow-accent-soft);
}
.acc-photo-open:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(201, 154, 46, 0.15);
}
/* Open is TWO changes, never one: the accent edge and the tinted fill. */
.acc-photo-open[aria-expanded="true"] {
  border-color: var(--accent);
  background: var(--oak-100);
}

/* 0fr to 1fr on a one-row grid: the panel opens to its own height with no
   number written down anywhere and nothing above it moving. Shut it is
   visibility:hidden, so a finger, a Tab key and a screen reader all agree it is
   not there — and visibility interpolates the way this needs (visible the whole
   way out, hidden only at the end of the way back). */
.acc-photo-panel {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows 200ms var(--ease), visibility 200ms var(--ease);
}
.acc-photo-panel[data-open="1"] { grid-template-rows: 1fr; visibility: visible; }
/* min-height:0 is what lets a grid item actually go to zero; overflow:hidden is
   what clips the camera frame on the way. The gap under the row is the first
   child's margin, INSIDE the clip — padding on this box would leak its height
   while the panel is shut. */
.acc-photo-panel-in { overflow: hidden; min-height: 0; }
.acc-photo-panel-in > :first-child { margin-top: 14px; }
.acc-photo-foot { margin-top: 2px; }
@media (prefers-reduced-motion: reduce) {
  .acc-photo-open { transition: none; }
  .acc-photo-open:hover, .acc-photo-open:active { transform: none; }
  .acc-photo-panel { transition: none; }
}
/* The signup button says "add"; this one says "change", and it is the secondary
   action on a page whose primary content is below it — so it drops the dashed
   "there is nothing here yet" edge for the ordinary control hairline and keeps
   every one of the signup button's dimensions, including the 52px floor. */
.selfie-btn-quiet {
  border-style: solid;
  border-color: var(--control-line);
  box-shadow: var(--shadow-control);
  transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease),
              transform var(--press-up) var(--ease-press);
}
@media (hover: hover) {
  .selfie-btn-quiet:hover {
    border-color: var(--accent-glow);
    box-shadow: var(--shadow-accent-soft);
    transform: translateY(-2px);
  }
}
/* Two changes on the press, never one — and the site suppresses the tap
   highlight, so without this the button is dead to a finger (2026-08-13). */
.selfie-btn-quiet:active {
  transition-duration: var(--press-down);
  transform: scale(0.98);
  border-color: var(--accent-glow);
  box-shadow: var(--shadow-accent-soft);
}
@media (prefers-reduced-motion: reduce) {
  .selfie-btn-quiet { transition: none; }
  .selfie-btn-quiet:hover, .selfie-btn-quiet:active { transform: none; }
}

/* The wall is gone (2026-08-20): a member's photos live only on /my-photos/.
   The account card below keeps .acc-portrait. */
@media (min-width: 620px) {
  .acc-portrait { width: 96px; height: 96px; }
}

/* The photo control's own answer line. .join-status already owns the empty
   collapse, the bad-news colour and the top margin; this only pulls it in
   tight under the button it belongs to, instead of inheriting the form
   footer's 18px. */
#j-acc-photo-status { margin-top: 10px; }

/* ---------------------------------------------------------------------------
   THE CAMERA IS A SHEET, NOT A SECTION (Daniel, 2026-08-25, second telling:
   "when you open that, it says the camera opens right here, so it takes up a
   ton of space. Like, that looks horrible.")

   The 08-23 pass put the panel behind a disclosure row, which fixed the CLOSED
   card and left the open one exactly as he described it: tapping the row grew
   a 381px empty box that said "The camera opens right here" and shoved the
   phone rows, the home-screen nudge and Sign out off the bottom of the screen.
   A settings card is not where a camera belongs at any size.

   So the lens now lives in a <dialog> that rises from the bottom edge: the card
   never changes shape, the sheet owns the whole screen for the one moment it is
   up, and closing it puts the card back untouched with the new photograph in
   the circle. Nothing inside the sheet is new — the same .cam-panel, the same
   .face-stage, the same two factories in join.js, the same /photo route and the
   same 422 wording. What changed is the surface they stand on.

   IT IS A REAL <dialog>, opened with showModal(), for the top layer: the sheet
   is a descendant of #page-scroll (the iOS 26 status-band scroller) and the top
   layer is the one place an overlay inside that box is not clipped by it, not
   measured against it, and not fighting the story viewer's z-index. Escape,
   focus containment and focus-back-to-the-row are then the browser's job rather
   than thirty lines of ours. join.js keeps the 08-23 collapsing panel as the
   answer for a browser without showModal.
   --------------------------------------------------------------------------- */
dialog.photo-sheet {
  position: fixed;
  inset: 0;
  box-sizing: border-box;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: hidden;
}
/* The UA sheet hides a closed dialog; this only says what an open one is. */
dialog.photo-sheet[open] {
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
/* The same navy veil the trial notice wears — a neutral black over a cream page
   reads grey and dead, and the navy keeps the card underneath recognisably this
   site while it is out of reach. */
dialog.photo-sheet::backdrop {
  background: rgba(15, 20, 38, 0.55);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}

.photo-sheet-in {
  box-sizing: border-box;
  width: 100%;
  max-width: 460px;
  /* dvh, not svh: an overlay that does not scroll the page cannot hit the
     large-viewport trap the site's svh rule exists for (design-standards
     Mobile, 2026-08-06 — dvh is for non-scrolling overlays). */
  max-height: calc(100dvh - 10px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--paper);
  border: 1px solid var(--line);
  border-top-left-radius: var(--radius-card);
  border-top-right-radius: var(--radius-card);
  box-shadow: var(--shadow-slab);
  /* FOUR LONGHANDS, not the shorthand: mobile-qa's safe-area check walks the
     declared properties of the matching rule and looks for safe-area-inset-* in
     each value, and a shorthand hides the inset from that read while the pixels
     are identical (the .navbar lesson in styles.css). */
  padding-top: 8px;
  padding-right: max(20px, env(safe-area-inset-right, 0px));
  padding-bottom: max(20px, env(safe-area-inset-bottom, 0px));
  padding-left: max(20px, env(safe-area-inset-left, 0px));
  animation: photo-sheet-rise 200ms var(--ease-glide, ease) both;
}

/* The grabber says "this came up from the bottom and goes back down" before
   anybody reads a word. It is decoration only — every way out of this sheet is
   a real control or a real key. */
.photo-sheet-grip {
  width: 40px;
  height: 4px;
  margin: 4px auto 12px;
  border-radius: 2px;
  background: var(--control-line);
}

.photo-sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 2px;
}
/* TWO CLASSES, not one: the page's own `.section h2` is 0,1,1 and would win
   against a single class, which is how a sheet title first rendered at 37px
   across two lines. */
.photo-sheet .photo-sheet-title {
  font-family: var(--font-display);
  /* rem term + a 1.17x range, well inside the 2.5x zoom ceiling (WCAG F94) */
  font-size: clamp(1.2rem, 1.1rem + 0.5vi, 1.4rem);
  letter-spacing: -0.01em;
  line-height: 1.2;
  font-weight: 700;
  color: var(--ink);
  margin: 0;
  min-width: 0;
}
/* 44x44 at the top-right corner, where a thumb closing a sheet already goes. */
.photo-sheet-x {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -8px;
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: transparent;
  border-radius: 50%;
  color: var(--steel);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              transform var(--press-up) var(--ease-press);
}
@media (any-hover: hover) {
  .photo-sheet-x:hover { background: var(--oak-100); color: var(--ink); }
}
/* Two changes on the press, never one — the site suppresses the tap highlight,
   so without this the button is dead to a finger (2026-08-13). */
.photo-sheet-x:active {
  transition-duration: var(--press-down);
  transform: scale(0.92);
  background: var(--oak-100);
  color: var(--ink);
}
.photo-sheet-x:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The answer line belongs where the eyes are. #j-acc-photo-status stays on the
   card for the sentence that lands AFTER the sheet closes ("That is your photo
   now."); this is its twin inside, so a 422 refusal is read on the surface that
   caused it instead of under a sheet covering it. join.js writes both. */
.photo-sheet-status { margin-top: 6px; }
.photo-sheet-body > :first-child { margin-top: 12px; }

/* The frame keeps its 3:4 and its 420px cap, and gains a ceiling: on a short
   phone the shutter has to stay on screen with it, so the picture crops before
   the button leaves. */
.photo-sheet .cam-frame {
  max-height: min(52dvh, 460px);
  margin-left: auto;
  margin-right: auto;
}
.photo-sheet .face-stage { margin-bottom: 8px; }
.photo-sheet .face-frame { margin-left: auto; margin-right: auto; }
/* AN EMPTY VIEWFINDER IS NOT A VIEWFINDER. Before the lens is on there is
   nothing to frame, so the 3:4 box shrinks to the height of its own sentence and
   grows to the full frame the moment there is a picture in it — the sheet opens
   compact and the shutter is on screen with the preview at every phone height.
   Absolute positioning is what makes .cam-msg not count towards a height, so in
   these two states it goes back in the flow and IS the height (a refusal runs
   three lines and must not spill out of the box explaining it). */
.photo-sheet .cam-frame[data-state="idle"],
.photo-sheet .cam-frame[data-state="error"] {
  aspect-ratio: auto;
  min-height: 108px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.photo-sheet .cam-frame[data-state="idle"] .cam-msg,
.photo-sheet .cam-frame[data-state="error"] .cam-msg {
  position: relative;
  inset: auto;
  padding: 22px 20px;
}
/* The second way out, at the end of the flow where a thumb already is. It keeps
   .join-link's 44px floor and only stops hugging the left edge. */
.photo-sheet .acc-photo-foot { margin-top: 8px; text-align: center; }

@keyframes photo-sheet-rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
/* The site's global reduced-motion clamp squeezes durations to .001ms, which
   makes a `both`-filled entrance snap rather than not happen — killed outright
   so the sheet is simply there. */
@media (prefers-reduced-motion: reduce) {
  .photo-sheet-in { animation: none; }
  .photo-sheet-x { transition: none; }
  .photo-sheet-x:hover, .photo-sheet-x:active { transform: none; }
}

/* From the width where the account card stops being the whole screen, the sheet
   stops being a sheet and becomes what it is on a desktop: a centred card. */
@media (min-width: 620px) {
  dialog.photo-sheet[open] { align-items: center; }
  .photo-sheet-in {
    max-height: calc(100dvh - 48px);
    border-radius: var(--radius-card);
    padding-top: 22px;
    padding-bottom: max(22px, env(safe-area-inset-bottom, 0px));
  }
  .photo-sheet-grip { display: none; }
}

/* The scroll lock, on the real scroller. html/body are already overflow:hidden
   sitewide (the iOS 26 architecture) — #page-scroll is the thing that moves, so
   it is the thing that has to stop. Same mechanism as html.nav-open and
   html.lb-open in styles.css. */
html.photo-sheet-open .page-scroll { overflow: hidden; }


/* ---------------------------------------------------------------------------
   THE CAMERA PANEL (2026-08-21)

   Nothing new is invented here. The frame is the face panel's frame (same
   .face-frame numbers: 420px cap, --radius-card, one --line hairline, the
   line-height:0 that keeps a media element from leaving a descender stripe);
   the buttons are the page's own .btn.btn-gold and .selfie-btn; the message
   inside the frame is the card's own body voice at the 16px floor. The one
   thing that is genuinely new is the mirror on the preview, and it is on the
   PREVIEW only — the frame this page keeps is the way round the room actually
   was.
   --------------------------------------------------------------------------- */

.cam-frame {
  position: relative;
  display: block;
  width: 100%;
  max-width: 420px;
  /* 3:4 upright, because a phone held to a face is upright and a 16:9 letterbox
     would ask somebody to compose a portrait inside a landscape. */
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-card);
  overflow: hidden;
  border: 1px solid var(--line);
  background: var(--bone, #f5f2ed);
  line-height: 0;
}

/* THE FRAME ENDS WHERE THE BOXES ABOVE IT END (walkthrough QA, 2026-08-28).
   The 420px cap is a phone number: on a phone the field column is narrower than
   that and the frame simply fills it. On a laptop the signup card's column is
   478px, so the cap left the viewfinder 58px short of the name and phone inputs
   above it AND of its own two buttons below it — the card's right edge stepped
   inwards for one element and back out again, with the caption centred inside
   the narrow box. Only the SIGNUP's panel is released: the sheet on the account
   card keeps the cap (.photo-sheet-in is 460px wide and .photo-sheet .cam-frame
   carries the ceiling that keeps the shutter on screen with it). */
.join-card .cam-frame { max-width: none; }

.cam-frame video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* A front camera everybody has ever used shows them the mirror. */
  transform: scaleX(-1);
}

/* The frame has four states and the attribute says which:
   idle (nothing running, the message explains what will happen), live (the lens
   is on), error (the refusal, in words, with the retry underneath), and done —
   a photo has been taken, so the frame gets out of the way entirely and the
   picture they kept is the only picture on the card. */
.cam-frame[data-state="idle"] video,
.cam-frame[data-state="error"] video { display: none; }
.cam-frame[data-state="live"] .cam-msg { display: none; }
.cam-frame[data-state="done"] { display: none; }

.cam-msg {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 24px;
  text-align: center;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--steel);
}
/* A refusal is not decoration: it gets the card's ink so it reads as the
   sentence the person has to act on. */
.cam-frame[data-state="error"] .cam-msg { color: var(--ink); }

/* Same shape as .face-actions, because it is the same row one step earlier. */
.cam-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  margin-top: 14px;
}
/* Exactly one of these two is ever on screen, so they stay a column at every
   width — a "row" of one button is just a button that has stopped filling its
   column. The shutter is the gold pill because taking the photo IS the action
   on this screen; turning the lens on and taking another one wear the quieter
   dashed outline the photo routes have always worn here. */
.cam-actions .btn,
.cam-actions .selfie-btn { min-height: 52px; }
.cam-actions [hidden] { display: none; }
