/* ============================================================
   App do Ivo - estetica editorial inspirada nos temas do ano
   Night 1 "furnished" (objetos domesticos, veludo teal/clay)
   Night 2 "swamp" (pantano, musgo/oliva)
   ============================================================ */

/* Fonts live in fonts.css, linked from <head> just ahead of this file
   (not @import here) so the faces download in parallel with this
   stylesheet instead of after it. They are served from /fonts rather
   than fetched from Google - see scripts/fetch-fonts.js. */

:root {
  --paper:    #e7e3d6;
  --paper-2:  #efece1;
  --paper-3:  #f4f1e8;
  --ink:      #2b2721;
  --ink-soft: #635d4f;
  --line:     #cbc4b2;
  --line-2:   #ded7c6;

  /* Night 1 - furnished */
  --rata:     #3f5b54;
  --rata-2:   #52756c;
  /* Night 2 - swamp */
  --outfit:   #59662f;
  --outfit-2: #6f7d42;
  /* acento quente */
  --clay:     #a96b4b;
  --clay-2:   #bd8062;

  --danger:   #9c4a3c;
  /* The red for a destructive button drawn as an outline on paper. It has
     to be its own token because --danger cannot be it: --danger doubles as
     .error TEXT on the login screen's dark artwork, so mode-light tints it
     pale on purpose (see html.mode-light below). That pale tint on pale
     paper is what made the Wall's "remover" button measure 1.3:1 - present
     in the DOM, invisible on the screen. Whenever this is retuned, check it
     against --paper-2 rather than against white. */
  --danger-edge: #9c4a3c;
  --radius:   4px;
  --shadow:   0 12px 40px rgba(43, 39, 33, 0.12);

  /* Small-text and heading scale - consolidates what used to be ~15 one-off
     sizes (0.6-0.78rem for badges/buttons/labels, 1.15-1.5rem for card and
     section headings) into two tiers each. */
  --text-xs:   0.66rem;
  --text-sm:   0.74rem;
  --heading-sm: 1.2rem;
  --heading-md: 1.4rem;

  --serif:  'Cormorant Garamond', Georgia, serif;
  --body:   'EB Garamond', Georgia, serif;
  --script: 'Italianno', cursive;

  /* Light-mode values, duplicated here as the real defaults (not just
     stale placeholder fallbacks) - every var(--role-*/--type-*/--status-*)
     use below used to lean on a hardcoded fallback argument instead, and
     those fallbacks quietly went stale (still the old blue/green/purple
     assignments) the last time this palette changed, since nothing forces
     a fallback constant to track a var() redefinition elsewhere. Defining
     them in :root means there's exactly one light-mode source of truth,
     and it's also what renders during the brief window before JS applies
     html.mode-light/mode-dark on first paint (see theme.js). */
  --brand: #a96b4b;
  --role-owner: #6b4c8a;
  --role-admin: #a3402f;
  --role-user:  #2a6ca6;
  /* The three post-type chips sit side by side in the Wall history, so
     they have to be told apart - but they were three separate hues (brown,
     orange, teal) and read as three unrelated systems rather than three
     kinds of the same thing. Same warm family now, separated by depth and
     a small hue drift instead: deep brown, mid amber, soft ochre. Far
     enough apart to distinguish at chip size, close enough to belong. */
  --type-announcement: #5c3a1f;
  --type-poll: #91541f;
  --type-links: #7d5f2b;
  --type-podium: #6b4a8a;
  /* Four status colours, all built the same way: an outline chip in the
     list, a chip filled with that same colour under paper ink when it's an
     active filter. "Pending" briefly broke that pattern - a bright yellow
     ground with dark ink, chased for contrast - which made it the one chip
     on the page reading as inverted next to its neighbours. It reads as
     amber rather than lemon at this darkness, which is the price of
     matching; the alternative was one odd chip out. Against the paper ink
     these land at fila 4.37, atribuir 4.38, pending 3.70, done 3.74 - none
     of them clear AA, so contrast was never what separated them. */
  --status-pending: #8a6a00;
  --status-done: #2f7d32;
  --status-fila: #5b6472;
  --status-atribuir: #a3406b;

  /* Side motifs for the .highlight box, one per section (see the
     .highlight rules further down). Used as masks, never as images, so
     every shape here is drawn in flat black and takes its real colour
     from the theme accent painted behind it - which is why these live at
     :root rather than inside each body.theme-* block. A 16x40 tile that
     repeats vertically: the box grows with its text, so the motif has to
     be a band, not a fixed drawing. */


  --rata-trail: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='40'%3E%3Cpath d='M8 0 C-4 10 20 30 8 40' fill='none' stroke='%23000' stroke-opacity='0.45' stroke-width='1.6' stroke-linecap='round'/%3E%3Cg fill='%23000'%3E%3Ccircle cx='12' cy='7' r='1.1'/%3E%3Ccircle cx='13.6' cy='9.6' r='0.85'/%3E%3Ccircle cx='4' cy='27' r='1.1'/%3E%3Ccircle cx='2.4' cy='29.6' r='0.85'/%3E%3C/g%3E%3C/svg%3E");
}

/* ==================== Dark / light mode (UI chrome) ======================
   A global preference for screens with no active night theme (settings.html,
   and the main app before/without a specific Best Outfit/Rata look).
   Declared on <html> (not body) as plain custom properties, so it only
   ever wins through normal inheritance: the moment a night theme is
   active, body.theme-rata/furnished/swamp sets these same variables
   directly on body itself, and a direct declaration on an element always
   wins over an inherited one - no specificity fight needed, and the
   furnished/swamp/rata looks stay exactly as distinctive as before.
   #login is unaffected by design (it has its own fixed palette over a
   photo, set directly on #login itself, same protection). */

html.mode-light {
  /* Lilac, not the pale pink this used to be (#f5eef1 family). These are the
     unthemed base tokens - what Settings and the admin chrome actually run
     on - and dark mode's own base is plainly purple (#17131f/#261f35), so
     light mode reading as pink made the same page look like two unrelated
     designs. Contrast went up rather than down: ink 9.1-10.5:1, ink-soft
     4.6-5.6:1 across the three papers. */
  --paper:    #e2d8f2;
  --paper-2:  #ece4f8;
  --paper-3:  #f4eefc;
  --ink:      #3f3550;
  /* Was #8d7f92 (~3.6:1 against --paper-3) - failed WCAG AA for the
     muted text (.muted, .help, table cells) this drives all over
     Settings and the admin panel. Darkened to ~5:1. */
  --ink-soft: #655a7c;
  --line:     #cfc0e6;
  --line-2:   #ded2f0;
  --rata:     #8fbfae;
  --rata-2:   #aad4c5;
  --outfit:   #a7abdb;
  --outfit-2: #c1c4e6;
  --clay:     #e2a798;
  --clay-2:   #ecc0b4;
  --danger:   #d98a7a;
  /* NOT tinted with --danger above: this one sits on paper, not on the
     login art, so it keeps the strong red. */
  --danger-edge: #7a2619;
  /* Solid fill for destructive buttons. --danger itself stays a light tint
     because .error uses it as *text* on dark login art; a button needs the
     opposite - a strong ground with light text on top. */
  --danger-fill: #b04a36;
  --danger-ink:  #fdf6f0;
  /* Back to the original warm brown, after a detour through plum (#602456)
     and lilac (#4a2f70). This is the same warm family as dark mode's
     #e2a878, so the wordmark reads as one colour across both modes - the
     literal dark value can't be reused here, it lands at 1.1-2.0:1 on light
     paper and effectively disappears. 4.0:1 worst case, 9.5:1 best. */
  --brand:    #6b3420;
  /* role-*/type-*/status-* stay at their :root (light-mode) values here -
     only mode-dark below needs to redefine them. */
}

html.mode-dark {
  --paper:    #17131f;
  --paper-2:  #1f1a2b;
  --paper-3:  #261f35;
  --ink:      #e9e1f0;
  --ink-soft: #a897bb;
  --line:     #382f4d;
  --line-2:   #453a5e;
  --rata:     #7c6bb0;
  --rata-2:   #9686c8;
  --outfit:   #5c7fb8;
  --outfit-2: #7898cc;
  --clay:     #cf9a5c;
  --clay-2:   #dcb27c;
  --danger:   #c4685a;
  /* Lighter than light mode's, because here it is drawn on a dark ground. */
  --danger-edge: #e2897a;
  --danger-fill: #a83f2c;
  --danger-ink:  #fbeee7;
  --brand:    #e2a878;
  --role-owner: #b294d6;
  --role-admin: #d9695a;
  --role-user:  #6badde;
  --type-announcement: #c9a07a;
  --type-poll: #d69a63;
  --type-links: #cdae77;
  --type-podium: #b294d6;
  --status-pending: #d4b04a;
  --status-pending-fill: #f2c811;
  --status-pending-ink:  #3d2f00;
  /* Was #7ed181 - a bright mint/lime that clashed with the app's muted,
     earthy palette everywhere it showed up. Deeper, less saturated sage
     green instead - still clearly reads as "done" without looking neon. */
  --status-done: #6aaf6c;
  --status-fila: #9aa4b5;
  --status-atribuir: #e08fb0;
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scrollbar-gutter: stable; overflow-y: scroll; }

body {
  /* Label colour for any button whose ground is dark regardless of
     light/dark mode - the red danger buttons and the darkened-accent CTAs.
     Plain #fff belonged to no theme and read as a foreign white sitting on
     the page; a touch of the section's own accent mixed in keeps it warm
     on the terracotta Wall, green on Furnished, blue on Memo, while staying
     bright enough to clear AA everywhere. 10% was too faint to read as
     anything but white, and the ceiling was not the label but the grounds
     under it: at 60% clay the CTA ground was light enough that any more
     tint failed on Memo's cyan, and the raw danger fill failed on
     Furnished. Darkening both (50% and 82% below) buys the headroom, so
     the tint could triple - worst case now 5.0:1, measured across all six
     themes in both modes. --clay is redefined per theme, so this follows
     on its own. */
  /* The label on a solid dark button - the populate/wipe pair, and the
     danger buttons. Plain white, deliberately. It used to carry 45% of the
     section's accent, which is the sort of tinting that reads as
     considered on a swatch and as "slightly wrong" on a phone: a wipe
     button, the most consequential control on any admin tab, wearing a
     pink-white on one tab and a green-white on another. White says the
     same thing everywhere and clears every ground it sits on by a mile. */
  --on-solid: #fff;
  /* The CTA buttons sit on a much deeper ground than the red ones, so they
     can carry far more of the accent before contrast bites - half, against
     the danger button's 22%. That is the difference between a label that is
     technically tinted and one that actually reads as the section's colour:
     terracotta on the Wall, olive on Swamp, cyan on Memo. Worst case
     4.7:1, on Memo in dark mode. */
  --on-cta: color-mix(in srgb, var(--clay) 50%, #fff);
  /* What a focused field glows with: whichever colour the surface it sits
     on actually belongs to. Every body.theme-* block redefines --clay on
     this same element, so this resolves to the night's own accent without
     any of them having to say so - terracotta on the Wall, yellow on
     Plaza, cyan on Memo.
     The surfaces with no theme at all override it below. */
  --focus-ring: var(--clay);
  margin: 0;
  font-family: var(--body);
  font-size: 18px;
  line-height: 1.55;
  color: var(--ink);
  text-transform: lowercase;
  background-color: var(--paper);
  /* Backs up the viewport meta's maximum-scale/user-scalable=no (some
     browsers still trigger a double-tap zoom regardless) - a real app
     shouldn't zoom on a stray double-tap. Normal scroll/swipe still work,
     this only disables the double-tap/pinch zoom gesture specifically. */
  touch-action: manipulation;
  /* textura de papel subtil */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
  min-height: 100vh;
  padding-bottom: env(safe-area-inset-bottom);
  -webkit-font-smoothing: antialiased;
}

/* The Settings tab carries no night theme, so it keeps the base palette -
   which is violet, while the base --clay is still the terracotta that
   palette had before it was retinted. A salmon halo on a lavender field
   belongs to nothing on that page. Drawn from the surface's own ink
   instead, so it stays in palette in both light and dark. */
body:not([class*="theme-"]) {
  /* The base palette was retinted violet - paper, ink, lines and every
     role badge - but --clay stayed the terracotta it had before, so on the
     Settings tab the accent belonged to no other colour on the page: a
     salmon focus halo, and a brown "test data" button under a lavender
     card while the same button on every other tab wears that tab's own
     colour. Given its own violet here, so an unthemed surface has an
     accent the way a themed one does.
     Only surfaces with no night theme are touched: every body.theme-*
     sets --clay for itself, and the login screen overrides what it needs
     over its photograph. */
  --clay: #6d4f9e;
  --clay-2: #553c7d;
  --clay-active: #6d4f9e;
  --focus-ring: color-mix(in srgb, var(--ink) 62%, var(--paper));
}
html.mode-dark body:not([class*="theme-"]) {
  --clay: #b9a2e0;
  --clay-2: #cdbcec;
  --clay-active: #b9a2e0;
}

.hidden { display: none !important; }

/* For a file input standing in behind a styled button (see photoUrl()'s
   callers in app.js): `display: none` is unreliable on iOS Safari - a
   `.click()` fired at a hidden input from a JS handler often silently
   fails to open the picker at all, worst inside an installed PWA. Kept
   in the layout (zero-size, clipped, off the visual flow) instead, which
   iOS treats as a valid click target. */
/* The username field the change-key form carries purely so Safari has
   something to pair its saved password with (see settings.html). It must
   stay a real, laid-out field - display:none, visibility:hidden and the
   hidden attribute all take it out of autofill's reach, which defeats the
   whole point - so it is parked off-screen instead. */
.autofill-anchor {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.file-input-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Tiny reference number, not a UI feature - lets QA vs prod be compared at
   a glance when reporting a bug (see version.js / GET /api/version). Sits
   as the last element in the DOM (after #login/#app, both of which toggle
   via .hidden), so in normal document flow it always lands at the true
   bottom of whichever page/section is currently showing - a fixed
   floating badge used to sit on top of page content instead. */
.version-badge {
  display: block;
  text-align: right;
  padding: 20px 4px calc(10px + env(safe-area-inset-bottom));
  font-size: var(--text-xs);
  color: var(--ink-soft);
  opacity: 0.4;
  letter-spacing: 0.02em;
  user-select: none;
}

.muted { color: var(--ink-soft); font-size: 0.92rem; }
/* Kill-switch confirmation ("apagado.") sits on its own line below the
   danger button, not crammed beside it like the other inline action msgs. */
.wipe-msg { display: block; margin-top: 10px; }
/* Contestant registration type: solo has a subtle warm tint, duo a cool one. */
@media (prefers-color-scheme: dark) {
  tr.contestant-solo { background: color-mix(in srgb, #e8a76c 8%, transparent); }
  tr.contestant-duo { background: color-mix(in srgb, #7aa3d0 8%, transparent); }
}
html.mode-light tr.contestant-solo { background: color-mix(in srgb, #f5c689 10%, transparent); }
html.mode-light tr.contestant-duo { background: color-mix(in srgb, #a8c9e8 10%, transparent); }
/* A filled chip, not coloured text. --danger as text measured 1.1-2.4:1
   against the theme papers in light mode - on the brick Wall it is the same
   colour as the background - and no red gets far on a mid-tone terracotta,
   so tinting the text harder was never going to fix it. A solid ground with
   light text reads on every theme, and it is what #login-error already does
   for exactly this reason. Empty ones collapse so nothing reserves space. */
.error, .muted.error {
  display: inline-block;
  background: var(--danger-fill);
  color: var(--danger-ink);
  font-family: var(--body);
  font-size: var(--text-sm);
  font-weight: 600;
  padding: 5px 10px;
  border-radius: var(--radius);
  line-height: 1.3;
}
.error:empty, .muted.error:empty { display: none; }
/* Sits directly on the busy cover photo (not a paper/card surface like
   #auth-error), so plain colored text was too easy to miss regardless of
   which part of the photo happened to be behind it - a solid fill
   guarantees contrast no matter what's underneath. Deep red rather than
   the cream this chip used to be: cream is exactly what the "entrar"
   button above it is made of, so a cream chip sitting right under it read
   as a second button instead of an alert. Full form width for the same
   reason the button is - a narrower chip made the stack step in and out.
   (8:1 against the text below, comfortably past AA.) */
/* Always occupies its full box, error or not - the layout you see on a
   fresh login is the same one you see after a rejected key, so nothing
   above it jumps the moment a message appears. The :empty rule keeps a
   single line's worth of height via generated content (generated content
   doesn't count against :empty, so this stays stable), and hides the red
   chip itself with visibility rather than display. */
#login-error {
  display: block;
  width: 100%;
  max-width: 320px;
  margin: 0 auto;
  background: #7a2416;
  color: #ece5d5;
  font-family: var(--body);
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-align: center;
  padding: 10px 16px;
  border-radius: var(--radius);
}
#login-error:empty { visibility: hidden; }
#login-error:empty::before { content: '\00a0'; }

.view { max-width: 620px; margin: 0 auto; padding: 22px 20px 48px; }
/* Built mobile-first (this is a PWA meant to live on a home screen), which
   left every desktop browser window with the same phone-width column and
   everything else empty - most of a 1440px window, on every tab, guest
   and admin alike. Doesn't need a real desktop redesign, just enough room
   that a laptop visit (checking the Wall, moderating Manage Users) doesn't
   look unfinished. #login is unaffected - it already overrides this
   entirely (max-width: none) for its own full-bleed illustration. */
@media (min-width: 900px) {
  .view { max-width: 900px; }
}

/* ------------------------------- Typography ------------------------------ */

h1, h2, h3 { font-family: var(--serif); font-weight: 500; letter-spacing: 0.01em; margin: 0; }
/* h3 had no size of its own, so it fell through to the UA default of
   1.17em - larger than .card h2's 1.15rem, which left every "best outfit"
   / "miss congeniality" subtitle outranking the card header above it.
   Every h3 in the app is one of those category subtitles, so sizing the
   element itself is enough. */
h3 { font-size: 0.95rem; margin: 14px 0 6px; }

/* --------------------------------- Login --------------------------------- */
/* Fundo provisorio ate termos a arte final em alta resolucao. */

#login {
  position: relative;
  overflow: hidden;
  max-width: none;
  width: 100vw;
  min-height: 100vh;
  /* svh, not dvh: dvh is the *dynamic* viewport, so it shrinks the moment
     the keyboard opens and grows back when it closes - the whole screen
     re-centring itself twice per tap, which is what "the page keeps going
     up and down" was. svh is the small viewport: the same number whether
     the keyboard is up or not, so nothing reflows around it. */
  min-height: 100svh;
  margin-left: calc(50% - 50vw);
  padding: 0;
  /* Fallback for any sliver the cover photo doesn't reach (aspect-ratio
     edge cases, overscroll) - same warm brown as the key-entry field's
     own background (--paper-3 below), so an overscroll bounce reads as
     part of the same design instead of a stray white/dark flash. The
     min-height above is what actually stops body (still theme-rata
     pre-login) from ever showing below a shorter #login box. */
  background-color: #241d17;
  --ink: #ece5d5;
  --ink-soft: #c7bca3;
  --line: rgba(236, 229, 213, 0.35);
  /* The same warm cream as this screen's own edges - a focused field
     deepens into the design instead of lighting up in an accent that
     belongs to the app's other palette. */
  --focus-ring: rgba(236, 229, 213, 0.9);
  --paper-3: #241d17;
  /* This screen has a fixed palette of its own over the photo, so every
     token a solid button reads has to be declared here or it falls through
     to whatever light/dark mode is set - which is how "entrar" ended up
     pale-on-pale in light mode: the button paints --ink (cream) as its
     ground and --paper as its label, and --paper was still the app's lilac.
     Same dark brown as --paper-3, since both play the same role here. */
  --paper: #241d17;
}
/* #login's own background-color above only fills its own box - on iOS
   Safari's overscroll/rubber-band bounce past the top or bottom of the
   page, it's actually body's background showing through underneath
   everything, which still carries whatever mode/theme was last active
   (dark mode's own --paper is a dark purple) - jarring against the login
   photo. Matches it to the same brown while the login screen is showing,
   regardless of mode. */
body:has(#login:not(.hidden)) { background-color: #241d17; }
/* Plain "cover" - no extra transform:scale() on top of it. Cover already
   guarantees the illustration fills the entire screen with no gaps at its
   largest non-distorted size; an added scale() only ever throws away more
   of the artwork (cropping the "twenty-nine divine" lettering at the
   sides, showing less of the cottage) without filling any more of the
   page than cover already does on its own. background-position keeps the
   crop anchored to the top, so the lettering stays whole and the bottom
   of the illustration (the cottage) is what gets trimmed on tall/narrow
   screens. The gradient is lighter up top so the artwork and its own
   lettering stay the focus, darkening only toward the bottom where the
   form actually needs the contrast. */
#login::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  background-image:
    linear-gradient(180deg, rgba(10, 16, 8, 0.10) 0%, rgba(10, 14, 8, 0.04) 34%, rgba(8, 12, 6, 0.80) 80%),
    url('/images/login-bg.jpg');
  /* Ivo's pick after seeing ten framings side by side: the house centred,
     with "bem-vinde" sitting over its wall.
     This is the 13 September painting, which never had the title painted
     into it - which is why no trick is needed here. The newer export has
     "twenty-nine divine" across the top, and in that file the lettering
     sits about 2% of the image height above the roof, so pushing it off the
     screen pins the house to the top edge and clips the smoke. Fading it out
     worked but Ivo did not want a fade, and painting it out flattened the
     grass and erased the tall smoke plume with it.
     So: the untitled painting, framed the way he chose. If the newer
     illustration is ever exported without its lettering, it can drop
     straight in here at twice this resolution. */
  /* Plain cover. The painting's top 12% is cropped off the FILE instead of
     being pushed off-screen with a zoom - Ivo spotted on a real phone that
     the tone shifts there (darker at the very edge, brightening around a
     tenth of the way down) and it read as a seam across the top.
     Cropping rather than zooming matters: `auto 112%` hid the band on a
     phone and left black bars down both sides of a tablet, because 112% of
     the height is narrower than a wide viewport. cover always covers. */
  background-size: cover;
  /* Anchored to the bottom rather than centred: on a phone the painting is
     proportionally taller than the screen, so cover leaves a little vertical
     overflow, and spending all of it upward is what Ivo asked for after
     seeing it on his own phone. It also puts the crop where the gradient is
     already darkest, so what gets trimmed is the part fading out anyway. */
  background-position: center bottom;
  background-repeat: no-repeat;
}

/* Landscape is the one shape the framing above cannot serve. The painting is
   about 1:2; a phone on its side is about 2:1, so `cover` scales it to the
   width and the visible slice is a narrow band across the middle of the
   cottage - roof gone, form sitting on its face. Tall phones, short phones
   and a tablet are all fine on the rule above; this is the only shape that
   needed its own.
   Three answers were tried and none shows the cottage well, because a 1:2
   painting cannot fill a 2:1 frame with the house intact. Fitting it whole
   stood it in a narrow column mostly hidden behind the form; keeping cover
   and looking lower put a slice of roof behind the inputs. Moving the frame
   UP to the grass is the least bad: nothing is mangled, the form reads
   cleanly, and the cottage simply is not in shot. A login screen held
   sideways is a corner case for an app installed portrait, and calm grass
   beats a chopped house. */
@media (orientation: landscape) and (max-height: 520px) {
  #login::before {
    /* Back to cover: `auto 112%` of a short, wide viewport is a different
       thing entirely, and the reason for the 112% (trimming the top edge on
       a tall screen) does not apply here. */
    background-size: cover;
    background-position: center 22%;
  }
}

/* Wide screens (a desktop or laptop window). The painting is about 1:2, so
   `cover` on a 16:10 window scales it to the full width and shows only a
   blurred, magnified band of grass - the cottage is off-screen and the form
   floats over nothing. Here the whole painting is shown at full height in
   the middle, its sides fading into the same deep green the grass is painted
   in, so the screen reads as one picture rather than a zoomed crop. Phones,
   tablets and phones on their side keep the rules above. */
@media (min-aspect-ratio: 4/5) and (min-width: 760px) and (min-height: 521px) {
  #login {
    background-color: #10230f;
    /* The same darkening toward the bottom as the painting's own overlay, so
       the sides and the middle meet without a seam. */
    background-image:
      linear-gradient(180deg, rgba(10, 16, 8, 0.10) 0%, rgba(10, 14, 8, 0.04) 34%, rgba(8, 12, 6, 0.80) 80%),
      radial-gradient(ellipse 60% 85% at 50% 40%, #1d4520 0%, #143217 50%, #0c1b0c 100%);
  }
  #login::before {
    /* The dark gradient still fills the screen behind the form; the painting
       is fitted to the height and centred. */
    background-size: 100% 100%, auto 100%;
    background-position: center, center bottom;
    -webkit-mask-image: linear-gradient(90deg, transparent calc(50% - 25.5vh), #000 calc(50% - 10vh), #000 calc(50% + 10vh), transparent calc(50% + 25.5vh));
    mask-image: linear-gradient(90deg, transparent calc(50% - 25.5vh), #000 calc(50% - 10vh), #000 calc(50% + 10vh), transparent calc(50% + 25.5vh));
  }
  body:has(#login:not(.hidden)) { background-color: #0b180b; }
}

#login .cover {
  position: relative;
  z-index: 1;
  max-width: 620px;
  margin: 0 auto;
  padding: 22px 20px calc(36px + env(safe-area-inset-bottom));
  /* Was 88vh, then 94vh, then 99vh - the whole greeting-through-login block
     sat too high up near the top of the illustration; taller here
     (bottom-anchored, see justify-content below) leaves more of the artwork
     visible above the text before it starts. */
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  text-align: center;
  gap: 20px;
}
/* Two beats instead of one run-on sentence - "bem-vinde" is the warm,
   personal welcome (the illustration's own hand-lettering is the real
   headline of this screen, so this stays modest rather than competing for
   header-level weight); "mostra-me a tua chave" is the quieter functional
   instruction underneath. */
/* The <p>s here carry the UA's own default block margins, which stack on
   top of this gap and pushed the greeting far higher up the illustration
   than the 5px suggests. */
.greeting { display: flex; flex-direction: column; gap: 5px; }
.greeting p { margin: 0; }
.greeting .hello {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.3rem;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(4, 6, 2, 0.55);
}
.instruction {
  font-family: var(--body);
  font-size: 0.84rem;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  /* Two shadows, not one: this line now sits lower on the illustration,
     right over the brightest part of the cottage roof, where a single soft
     glow left it barely legible. A tight opaque halo carries the letter
     edges, the wider one keeps it off the busy texture behind. */
  text-shadow: 0 1px 2px rgba(4, 6, 2, 0.95), 0 2px 12px rgba(4, 6, 2, 0.8);
}

#login-form { width: 100%; max-width: 320px; display: flex; flex-direction: column; gap: 12px; }

/* Text-only button - "forgot my key" / "back to login" links, not a
   bordered button competing with the real Entrar submit. Needs the same
   appearance reset as .tab below - without it, a <button>'s native WebKit
   chrome renders its own filled, rounded box (from the general button
   rule's border-radius) right over these "background: none" declarations,
   so on real iOS Safari this shows as a solid blank pill instead of plain
   underlined text. */
.link-btn {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: none;
  border-radius: 0;
  padding: 4px;
  color: var(--ink-soft);
  font-family: var(--body);
  font-size: 0.85rem;
  /* Without these, the generic button/.btn rule's bold weight + wide
     letter-spacing (meant for solid CTAs like "entrar") carried over onto
     what's meant to read as a plain inline link - "primeiro login?" stood
     out as a completely different visual voice from the serif/script
     text surrounding it on the login screen. */
  font-weight: 400;
  letter-spacing: normal;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  /* Only ever sits on the login screen's photo background - a plain
     underline alone can get lost against a busy patch of the image. */
  text-shadow: 0 1px 8px rgba(4, 6, 2, 0.5);
}
/* Explicit background:none here too, not just on the base rule above -
   "button:hover, .btn:hover" (higher specificity than plain .link-btn,
   since :hover adds a specificity point) fills it with --ink otherwise.
   Invisible on a mouse (hover clears on mouseout), but iOS Safari latches
   a tap into a "stuck" :hover with no mouse to clear it - the button then
   transitions to solid --ink and stays that way, permanently, after a
   single tap. */
.link-btn:hover { background: none; color: var(--ink); }
/* Discreet on purpose - a language switch is a nice-to-have on the login
   screen, not a decision that deserves competing with "entrar"/"ainda não
   tens a tua chave?" for attention. Smaller and dimmer than the base
   .link-btn, same underline treatment so it still reads as tappable. */
/* Instruction on the left, language toggle on the right, sharing the row
   directly above the key field - which also keeps the space under "entrar"
   free for the error banner to appear where the eye already is. */
/* Tagline centred over the key field, language toggle tucked at the right
   just beneath it. The toggle is taken out of flow so its width can't pull
   the tagline off-centre - it drops onto its own short line above the
   input, which is the arrangement that was picked. */
.login-meta {
  position: relative;
  width: 100%;
  max-width: 320px;
  /* Cancels most of .cover's own 20px gap so it sits tight on the field. */
  margin-bottom: -16px;
}
/* The instruction sat a line above the language toggle pinned to its right,
   which read as two separate rows rather than one. Nudged down onto the
   toggle's line - as a relative offset, not a margin, so the key field below
   stays exactly where it was. The greeting takes the same offset so the gap
   between the two lines is unchanged. */
.login-meta .instruction { text-align: center; position: relative; top: 20px; }
.greeting { position: relative; top: 20px; }
.login-meta .login-lang-toggle {
  position: absolute;
  right: 0;
  bottom: 0;
}
/* "pt" renders at 12x16 - by some distance the smallest thing anyone is
   asked to hit, and it sits at the very edge of the screen. Nothing else is
   nearby, so the hit area can grow generously without stealing taps. */
.login-lang-toggle::after {
  content: '';
  position: absolute;
  inset: -14px;
}
.login-lang-toggle { position: relative; }
.login-lang-toggle {
  flex: none;
  font-size: 0.68rem;
  opacity: 0.6;
  letter-spacing: 0.06em;
  padding: 0;
}
/* Deliberately almost invisible - this is the only way into the Plaza
   admin tab now (see wall-secret-btn in settings.js), meant to be found by
   someone who already knows it's there, not stumbled onto. No underline,
   no hover fill (unlike .link-btn above) - just a faint mark that gets
   slightly more visible on tap/hover so it doesn't feel entirely broken. */
.wall-secret-btn {
  -webkit-appearance: none;
  appearance: none;
  background: none;
  border: none;
  display: block;
  margin: 28px auto 0;
  padding: 10px;
  font-size: 0.8rem;
  /* The accent, not --ink-soft: on Memo this dot sits on a ground already
     scattered with dots, and a neutral grey one was the only mark on that
     page not wearing the theme's blue. Taking --clay means each tab's
     hidden dot belongs to its own section - the same rule the solid button
     labels follow - and it stays just as easy to miss, because what hides
     it is the opacity, not the colour. */
  color: var(--clay);
  opacity: 0.22;
  cursor: pointer;
}
.wall-secret-btn:hover, .wall-secret-btn:active { opacity: 0.55; }
.login-lang-toggle:hover { opacity: 0.9; }

/* Public demo: the shared keys under the login form. */
.demo-keys {
  width: 100%;
  max-width: 320px;
  margin-top: 18px;
  text-align: center;
}
.demo-keys-help { margin: 0 0 8px; font-size: 0.9rem; color: var(--ink-soft); }
.demo-keys-list { display: flex; gap: 8px; }
/* flex: 1 + min-width: 0 keeps two keys side by side on an iPhone SE
   instead of the second running off the edge. */
.demo-keys-list .find-key-result { flex: 1; min-width: 0; text-align: center; padding: 10px 8px; letter-spacing: normal; }
.demo-keys-list .find-key-result small { display: block; margin-top: 2px; font-size: 0.75rem; letter-spacing: normal; color: var(--ink-soft); overflow-wrap: anywhere; }

#find-key-results {
  width: 100%;
  max-width: 320px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 4px;
}
#find-key-results:empty { display: none; margin-top: 0; }
.find-key-result {
  width: 100%;
  text-align: left;
  padding: 10px 14px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper-3);
  color: var(--ink);
  font-family: var(--body);
  font-size: 0.95rem;
  cursor: pointer;
}
.find-key-result:hover { border-color: var(--clay); }

/* -------------------------------- Inputs --------------------------------- */

input, textarea, select {
  width: 100%;
  padding: 13px 14px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper-3);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1rem;
  text-transform: lowercase;
  transition: border-color 0.2s;
}
input[type="password"] { text-transform: none; }
/* The note to Ivo is the one piece of writing in this app that is kept
   rather than read once, and it is the only place the lowercase golden rule
   is deliberately lifted. The rule is about the app's own voice - labels,
   buttons, announcements - and this is nobody's voice but the person
   writing it. The server never lowercased it and never capped its length
   (see saveDedication in dedications.js, and the PDF that prints it as
   typed); only this stylesheet was flattening it on the way to the screen,
   so someone typing a name or an "Obrigado" watched it go lowercase as they
   wrote. Both the field and the message it becomes are exempt, so what is
   typed, what is shown back, and what reaches the PDF are finally the same
   three things. */
#dedication-text,
.dedication-written { text-transform: none; }
/* Character counter beside a capped field. Tabular figures so the number
   does not jitter sideways as it counts up. */
.char-count { font-variant-numeric: tabular-nums; }
.char-count.at-limit { color: var(--danger); font-weight: 600; }
/* #user-name-input is type=search purely to keep Safari's password autofill
   off it (see the comment on the field in settings.html) - it should still
   look like every other input, so undo WebKit's native search styling and
   its clear button, neither of which any other field here has. */
input[type="search"] { -webkit-appearance: none; appearance: none; }
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
/* Without an explicit color, WebKit's own placeholder default is a
   neutral cool grey - fine on the app's own neutral paper, but it reads
   as distinctly blue-tinted by contrast against every warm theme (mural's
   terracotta, rata's pink, furnished's clay). --ink-soft already tracks
   each active theme, so this stays in-palette everywhere instead. */
::placeholder { text-transform: lowercase; font-weight: 400; color: var(--ink-soft); opacity: 1; }
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--rata); }
textarea { resize: vertical; }

/* Poll option entries in the admin Plaza form (see #plaza-poll-options in
   settings.html) are a word or two ("Pizza", "Sushi") - the shared input
   padding above is sized for full-width text like the message textarea
   next to it, so a short option's placeholder sits in a lot of empty
   vertical space and reads as oversized next to it. Same font-size as
   every other input (consistency), just a tighter box for short content. */
.plaza-poll-option-input { padding: 9px 14px; }

/* A Rata's suggestion box and the Wall's message field read as too thick
   at the generic input padding - thinner here specifically. */
#suggest-text, #chat-text { padding: 9px 12px; }

/* Show/hide toggle inside a password input (e.g. the login access key). */
.password-field { position: relative; }
.password-field input { padding-right: 46px; }
button.password-toggle {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 44px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--ink-soft);
}
button.password-toggle:hover { background: transparent; color: var(--ink); }
.password-toggle svg { width: 19px; height: 19px; }
.password-toggle .icon-eye-off { display: none; }
.password-toggle.visible .icon-eye { display: none; }
.password-toggle.visible .icon-eye-off { display: block; }

select {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  padding-right: 40px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%23a96b4b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 7l5 5 5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 14px;
}
select:hover { border-color: var(--clay); }

/* Night picker atop Best Look admin - the single source of truth for the
   whole tab (voting control, contestants, results, voter log all read it),
   so it sits outside any single card. Reuses the main app's .theme-switch
   component, but this page never gets a body.theme-outfit class (no per-
   night reskin here), so var(--ot) is never set - fill the active state
   with the general accent instead. Scoped by #id rather than .night-switch:
   it needs to outrank .theme-switch button.active on specificity, not just
   source order, or the active label renders as near-white text on a
   transparent fill - invisible against the page background. */
.night-switch { margin-bottom: 18px; }
#admin-night-switch button.active,
#admin-night-switch button.active:hover { background: var(--clay); color: var(--paper-3); }

label.field { display: block; margin: 4px 0 2px; font-size: var(--text-sm); font-weight: 600; text-transform: lowercase; letter-spacing: 0.16em; color: var(--ink-soft); }

/* -------------------------------- Buttons --------------------------------- */
button, .btn {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  border-radius: var(--radius);
  font-family: var(--body);
  font-weight: 600;
  text-transform: lowercase;
  letter-spacing: 0.16em;
  transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s;
  text-decoration: none;
  display: inline-block;
  text-align: center;
}

/* DEFAULT: Primary solid dark button (submit, create, send, save).
   Briefly made this the deep-accent CTA treatment for every button on
   every page and reverted it - see the CTA family further down, which is
   still an explicit list. The blanket version changed far more than the
   handful of buttons it was meant to, and looked wrong. */
button, .btn {
  border: 1px solid var(--ink);
  padding: 12px 20px;
  font-size: 0.78rem;
  background: var(--ink);
  /* --paper, not --paper-3: the palest step reads as plain white against a
     dark ground, which is what these labels were being flagged for. The
     base paper carries the theme's hue visibly and still clears AA (6:1 at
     worst, on Wall). */
  color: var(--paper);
}
/* Guarded, and this is the whole reason the guard exists: on iOS a tap
   applies :hover and leaves it applied until you touch something else. A
   filter chip switched off therefore kept the *on* look under your thumb -
   which is what made deselecting look like nothing had happened. A mouse
   still gets its hover; a finger never does. */
@media (hover: hover) {
  button:hover, .btn:hover {
    background: var(--ink);
    color: var(--paper);
    opacity: 0.8;
  }
}
button:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Secondary: Ghost button (transparent, subtle). */
button.ghost, a.ghost {
  border: 1px solid var(--line);
  padding: 7px 12px;
  font-size: var(--text-xs);
  background: transparent;
  color: var(--ink);
}
@media (hover: hover) {
  button.ghost:hover, a.ghost:hover {
    border-color: var(--ink);
    background: transparent;
    color: var(--ink);
  }
}

/* Size modifier: small buttons, just padding/font size, inherit color scheme. */
button.small { padding: 7px 12px; font-size: var(--text-xs); letter-spacing: 0.14em; }

/* Icon-only row action (edit, remove, pin, regenerate, approve, reset).
   A list row used to carry three words at 0.16em letter-spacing, which is
   wide enough that the row wrapped onto a second line on an iPhone SE and
   the actions stopped lining up down the list. One square each instead:
   same size everywhere, so a column of rows reads as a column.
   Never nameless - see iconButton() in icons.js, which is the only thing
   that builds these and always writes aria-label and title. */
button.icon-action {
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--line-2);
  color: var(--ink);
}
/* Not behind (hover: hover), deliberately. That guard is the right default
   and it is still there on the generic button rule, but it cannot be the
   only thing standing between a finger and a stuck state: a tap leaves
   :hover applied on WebKit until you touch something else, and it was
   measured still applied here even in a touch context. So these say
   "transparent" outright at a specificity the generic rule cannot beat,
   whatever the media query decides. A row action never fills on hover
   anyway - the border and the stroke carry it. */
button.icon-action:hover,
button.icon-action:active,
button.icon-action:focus {
  background: transparent;
  /* colour and border belong here too, not just the background. Without
     them the generic button:hover still handed this its `color:
     var(--paper)` - so a tapped pin kept a pale, washed-out glyph inside a
     suddenly dark border until you touched elsewhere, which is neither the
     pinned state nor the normal one. */
  color: var(--ink);
  border-color: var(--line-2);
  opacity: 1;
}
@media (hover: hover) {
  button.icon-action:hover {
    border-color: var(--ink);
    color: var(--ink);
  }
}
button.icon-action svg { width: 19px; height: 19px; }
/* Destructive keeps its colour but not its fill - a row of outlines with
   one solid red block in it reads as an error state rather than as one
   action among three. The red is in the stroke and the border instead. */
/* The border was mixed 45% toward --line-2, which on most themes left it
   fainter than the two plain buttons beside it - the most destructive
   control in the row reading as the quietest. Both the stroke and the
   border are the full danger colour now; still no fill, so it does not
   read as an error state, but it is unmistakably there. */
/* --danger, not a mix of --danger-fill toward black. That mix is right for
   a label on a solid red ground and exactly wrong here: on a dark page it
   put a near-black stroke on a near-black card, which is how the reset and
   the remove buttons vanished in dark mode. --danger is the token that
   already carries a value per mode for precisely this - red as a line and
   as text, readable on either paper. */
button.icon-action.danger,
button.icon-action.danger:hover,
button.icon-action.danger:active,
button.icon-action.danger:focus {
  background: transparent;
  color: var(--danger-edge);
  border-color: var(--danger-edge);
}
/* The lead action of a card - revealing a consequence, throwing a Memo
   moment open. Round rather than square, and larger: it is the one thing
   its card exists for, and it carries its section's own artwork instead of
   a control glyph. The accent belongs here where it would be noise on a
   row action - there is exactly one of these per card. */
button.icon-action.lead {
  width: 62px;
  height: 62px;
  /* No box of any kind - the artwork is the button. It was a circle first;
     with nothing to hold it the mark can be drawn large enough to carry the
     job on its own, which is the whole argument for using the section's own
     drawing rather than a control glyph. */
  border: none;
  border-radius: 0;
  color: var(--clay);
}
button.icon-action.lead svg { width: 38px; height: 38px; }
/* Same reasoning as the row actions above: no fill on hover, so nothing can
   be left painted under a thumb. */
button.icon-action.lead:hover,
button.icon-action.lead:active,
button.icon-action.lead:focus {
  background: transparent;
  border: none;
  color: var(--clay);
}
/* No "on" state on the pin button. It had a tinted ground, which washed the
   stroke of the very icon it was meant to emphasise - and it was saying
   something already said twice over: the post carries a "fixado" badge, and
   the button's own icon has changed to the struck-through pin that undoes
   it. Three ways of saying pinned, one of them making the other two harder
   to read. */

/* Destructive: Red danger button (delete, wipe, reset). A genuinely solid
   fill, not a tint. This used to color-mix --danger 50% into --paper-2,
   which on a light-mode brick/mural surface diluted an already-pale
   --danger (#d98a7a) into a washed-out pink whose 1px --danger border
   disappeared into it entirely - "remover" read as disabled rather than
   destructive. --danger-fill/--danger-ink exist precisely so this can be a
   strong ground with light text (5.1:1 light, 6.3:1 dark) without touching
   --danger itself, which is still needed as *text* elsewhere (.error). */
button.danger {
  /* Deepened 40% off --danger-fill so the label can carry as much of the
     theme as the CTA buttons do. At the previous 18% the tint had to stay
     at 22% to clear AA, and beside a CTA at 50% that read as plain white -
     which is what "remover" kept being flagged for. Worst case 4.7:1. */
  border: 1px solid color-mix(in srgb, var(--danger-fill) 60%, #000);
  background: color-mix(in srgb, var(--danger-fill) 60%, #000);
  color: var(--on-solid);
}
@media (hover: hover) {
  button.danger:hover {
    background: color-mix(in srgb, var(--danger-fill) 50%, #000);
    border-color: color-mix(in srgb, var(--danger-fill) 50%, #000);
    color: var(--on-solid);
  }
}

/* -------------------------------- Topbar --------------------------------- */

.topbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  /* center, not flex-end: flex-end bottom-aligns the two boxes exactly
     (verified: both hit the same y-coordinate), but a script face's
     visible ink extends well above its own compressed line-box (line-
     height: 0.8) - so box-bottom-alignment still let the tall ascenders
     visually poke up past the icon buttons' top edge, reading as floating
     above the row. Center align splits the difference instead of pinning
     either edge, since there's no alignment that makes an oversized-ascender
     script face and a plain square icon line up by both edges at once. */
  align-items: center;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--line);
  margin-bottom: 14px;
  gap: 12px;
}
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
/* Uses --brand (declared on <html> alongside mode-light/mode-dark, not
   var(--clay)): the logo keeps one identity color regardless of active
   tab or night theme, but still adapts to light/dark mode so it never
   washes out against either background. The fallback matches the old
   fixed color, for the brief moment before applyMode() runs. */
/* No overflow/text-overflow here on purpose - this cursive script font
   relies on rendering outside its own tight line box (line-height: 0.8),
   so clipping overflow cuts the tops/tails off every letter, not just
   whatever would've been truncated. The logo-swap easter egg (see
   logoEasterEgg.js) is capped at a short length instead, so the text
   itself never gets long enough to threaten the role badge next to it. */
.brand .brand-name {
  font-family: var(--script);
  font-weight: 700;
  /* clamp() instead of a fixed rem + breakpoint: a fixed size that looked
     right in one screenshot overflowed off both edges of a real (narrower)
     phone, because "appdoivo" in this wide cursive face plus 4 icon
     buttons plus gaps simply doesn't fit a fixed large size on every
     screen. Scaling continuously with viewport width (8.5vw) guarantees it
     never exceeds the available space, while the min/max bounds keep it
     from ever reading as too small on a narrow phone or comically large on
     a wide one. */
  font-size: clamp(2.05rem, 9.1vw, 2.85rem);
  line-height: 0.8;
  white-space: nowrap;
  color: var(--brand);
  /* The script face only ships 400/700, so anything heavier than 700 has to
     come from a hairline stroke - asking for 800 would silently synthesise.
     Kept well under a pixel: past ~0.5 the cursive's thin joins fill in and
     it turns to mush at small sizes. */
  -webkit-text-stroke: 0.35px currentColor;
  paint-order: stroke fill;
}
.brand .badge,
.user-row-name .badge {
  /* Never shrink to make room for a long brand-name - see the comment
     on .brand .brand-name above. */
  flex-shrink: 0;
  font-family: var(--body);
  text-transform: lowercase;
  letter-spacing: 0.16em;
  font-size: var(--text-xs);
  font-weight: 700;
  border: 1.5px solid var(--clay);
  color: var(--clay);
  padding: 3px 9px;
  border-radius: 999px;
}
/* Role badges: owner / admin / user, each its own color. */
/* Not var(--clay/--outfit/--rata): each role keeps its own hue everywhere it
   appears, regardless of night theme - but DOES need a light/dark-mode pair
   (--role-*), same as --role-owner already had. admin/user used to be a
   single hardcoded dark shade each, which read fine in light mode but was
   nearly illegible against the dark-mode background. */
.badge.role-owner { border-color: var(--role-owner); color: var(--role-owner); }
.badge.role-admin { border-color: var(--role-admin); color: var(--role-admin); }
.badge.role-user { border-color: var(--role-user); color: var(--role-user); }

/* Publication-type badges (Regular/Poll on the Wall admin list) - own
   colors, deliberately outside the role palette above so a post type is
   never visually confused with a user role. Nudged left by exactly the
   pill's own left padding (see .badge's shared padding: 3px 9px) - a pill
   shape needs that padding to not look cramped, but it was throwing the
   badge's own text out of alignment with the plain text (byline,
   question, options) below it in the same list. */
.badge.type-announcement, .badge.type-poll, .badge.type-links { margin-left: -9px; }
.badge.type-announcement { border-color: var(--type-announcement); color: var(--type-announcement); }
.badge.type-poll { border-color: var(--type-poll); color: var(--type-poll); }
.badge.type-links { border-color: var(--type-links); color: var(--type-links); }
.badge.type-podium { border-color: var(--type-podium); color: var(--type-podium); }

/* Rata history: each revealed consequence, who was assigned, and its
   completion status - a standalone pill (not .badge, which is scoped to
   .brand/.user-row-name) since these appear in a plain list, not either
   of those containers. */
.rata-history-item { border-top: 1px solid var(--line); padding: 14px 0; }
.rata-history-item:first-child { border-top: none; padding-top: 0; }
/* The currently-revealed consequence, in the admin list - used to also get
   a separate, much louder .highlight box duplicating the same text above
   this same list. A soft tint here (same color-mix technique .highlight
   uses, just faint) is enough to mark it as "the current one" without
   competing with it. */
.rata-history-item.rata-current {
  background: color-mix(in srgb, var(--rata) 6%, transparent);
  border-radius: var(--radius);
  padding: 14px;
  margin: 0 -14px;
}
.rata-history-text { font-family: var(--serif); font-size: 1.05rem; margin: 0; }
.rata-history-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.rata-badge {
  font-family: var(--body);
  text-transform: lowercase;
  letter-spacing: 0.14em;
  font-size: var(--text-xs);
  font-weight: 700;
  border: 1.5px solid var(--ink-soft);
  color: var(--ink-soft);
  padding: 3px 9px;
  border-radius: 999px;
}
/* Four distinct colors, one per state, reading roughly as a progression:
   fila (grey, nothing to do yet) -> atribuir (rose, needs the owner to
   pick someone) -> pendente (blue, in progress) -> feita (green, done). */
.rata-badge.rata-status-fila { border-color: var(--status-fila); color: var(--status-fila); }
.rata-badge.rata-status-atribuir { border-color: var(--status-atribuir); color: var(--status-atribuir); }
.rata-badge.rata-status-pendente { border-color: var(--status-pending); color: var(--status-pending); }
.rata-badge.rata-status-feita { border-color: var(--status-done); color: var(--status-done); }

/* Users tab role filter - same pill shape as the role badges, but
   clickable toggles. Active state fills solid in that role's own color
   (neutral ink for "all roles"), inactive stays a plain outline. */
.role-pills { display: flex; flex-wrap: wrap; gap: 8px; }

.role-pill {
  font-family: var(--body);
  text-transform: lowercase;
  letter-spacing: 0.16em;
  font-size: var(--text-xs);
  font-weight: 700;
  background: none;
  /* --line-2/--ink (not --line/--ink-soft) - an unselected filter pill is
     still a clickable control, not disabled text, and at 1.5px + --line
     its outline dropped under 2:1 contrast in several themes' dark mode
     (read as barely-there/invisible - easy to miss it's a toggle at all). */
  border: 1.5px solid var(--line-2);
  color: var(--ink);
  padding: 5px 12px;
  border-radius: 999px;
  cursor: pointer;
}
/* Same fix as .link-btn above: without this, "button:hover, .btn:hover"
   outranks the plain (non-hover) rule above on specificity and fills an
   inactive pill solid on a tap that gets stuck in :hover on iOS Safari.
   .active already wins regardless (two classes beats button:hover), so
   this only needs to guard the inactive state. */
.role-pill:hover { background: none; }
.role-pill.active { background: var(--ink); border-color: var(--ink); color: var(--paper); }
/* Inactive: the same outlined pill the list renders for that role/state, so
   an unselected filter still reads as "this is the owner colour" rather than
   a neutral grey blob. Active: the same shape filled in. */
.role-pill[data-role="owner"] { border-color: var(--role-owner); color: var(--role-owner); }
.role-pill[data-role="admin"] { border-color: var(--role-admin); color: var(--role-admin); }
.role-pill[data-role="user"]  { border-color: var(--role-user);  color: var(--role-user); }
.role-pill[data-role="owner"].active { background: var(--role-owner); border-color: var(--role-owner); color: var(--paper); }
.role-pill[data-role="admin"].active { background: var(--role-admin); border-color: var(--role-admin); color: var(--paper); }
.role-pill[data-role="user"].active { background: var(--role-user); border-color: var(--role-user); color: var(--paper); }

/* Rata status filter - same colors as the rata-badge each state renders
   with in the list below, so the filter pill you toggled and what it's
   showing read as the same thing. */
.role-pill[data-status="fila"] { border-color: var(--status-fila); color: var(--status-fila); }
.role-pill[data-status="atribuir"] { border-color: var(--status-atribuir); color: var(--status-atribuir); }
.role-pill[data-status="pendente"] { border-color: var(--status-pending); color: var(--status-pending); }
.role-pill[data-status="feita"] { border-color: var(--status-done); color: var(--status-done); }
.role-pill[data-status="fila"].active { background: var(--status-fila); border-color: var(--status-fila); color: var(--paper); }
.role-pill[data-status="atribuir"].active { background: var(--status-atribuir); border-color: var(--status-atribuir); color: var(--paper); }
.role-pill[data-status="pendente"].active { background: var(--status-pending); border-color: var(--status-pending); color: var(--paper); }
.role-pill[data-status="feita"].active { background: var(--status-done); border-color: var(--status-done); color: var(--paper); }
/* margin-left: auto keeps this pinned to the right even when it wraps onto
   its own line below the brand (a lone flex item ignores justify-content). */
.topbar-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }

/* Botoes de icone (tema, notificacoes, idioma, gerir, sair) na topbar -
   todos a mesma caixa quadrada. Auto-suficiente (nao depende de .ghost)
   para nao entrar em conflito de especificidade com "button.ghost". */
/* 38px: 6px under the 44px minimum, and knowingly left there. The topbar has
   12px of spare width on an iPhone SE, so the boxes cannot grow; an overlay
   that widened only the hit area was tried and made the Manage Users row
   toggle intermittently unclickable (1 run in 3), for reasons never pinned
   down. Six pixels on four rarely-used controls is not worth reintroducing a
   flake in a test that was green - the tab row, which was the real problem
   at 34px with five neighbours, is widened below instead. */
/* No box at all - just the stroke of the icon. The boxes were four outlined
   squares in a row over a papered header, competing with the brand beside
   them for a job the icons already did on their own.
   Two things the box was quietly providing, kept: the hit area (44px now,
   not the 38px the boxes were - under the touch minimum, and nobody
   noticed while there was a visible border to aim at), and the bell's
   "off" signal, which was a tinted ground and is the accent on the stroke
   instead (see #notif-btn below). */
button.icon-btn,
a.icon-btn {
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  color: var(--ink);
  border: none;
  font-size: 1.05rem;
  line-height: 1;
}
@media (hover: hover) {
  button.icon-btn:hover,
  a.icon-btn:hover { background: transparent; color: var(--clay); }
}
.icon-btn svg { width: 19px; height: 19px; display: block; }

.lang-toggle { font-family: var(--serif); font-size: 0.98rem; font-weight: 600; letter-spacing: 0.02em; }

/* Theme toggle: shows the icon for what you'll switch TO. Light (default)
   shows the moon (switch to dark); dark mode shows the sun (switch to light). */
#theme-toggle .icon-sun { display: none; }
html.mode-dark #theme-toggle .icon-sun { display: block; }
html.mode-dark #theme-toggle .icon-moon { display: none; }

/* Notification bell: when ON, it looks like every other icon button
   (conformity - nothing to act on). When OFF, the accent draws the eye,
   signaling "click to turn this on" instead of the reverse.
   Uses --clay (the app's warm brand accent, same as the logo/badges),
   not --rata - --rata is minty green in light mode, an odd, unrelated
   color for this button. It was a tinted ground until the buttons lost
   their boxes; on the stroke it says the same thing, and is if anything
   harder to miss now that nothing else in the row carries colour. */
#notif-btn:not(.active):not(.hidden) { color: var(--clay); }

/* --------------------------------- Tabs ---------------------------------- */

.tabs {
  display: flex;
  /* space-evenly spreads the tabs across the full row width AND gives the
     first/last tab the same breathing room from the edges as the gaps
     between tabs - space-between (tried first) put "wall" and "plaza"
     flush against the page edges with none. gap stays as a floor so they
     never crowd together if the row is narrower than the content
     (justify-content only kicks in once there's surplus width; below that
     it falls back to normal flow + the overflow-x scroll). */
  justify-content: space-evenly;
  /* Was a flat 26px, which overflowed the row by a handful of pixels on a
     phone - just enough that the whole bar slid under a thumb, on a control
     that is supposed to sit still. The gap gives way on narrow screens
     instead, so the tabs fit and there is nothing to drag; the overflow
     scroll below stays as the floor for anything genuinely too wide (a
     longer label, another tab, a large accessibility text size). */
  gap: clamp(5px, 1.7vw, 26px);
  margin-bottom: 24px;
  /* No baseline under the row - the selected tab is marked by colour alone
     now (see .tab.active below), and a rule under all of them put back the
     horizontal line that choice was getting rid of. The topbar above keeps
     its own, which separates the header from the app rather than marking
     anything. */
  border-bottom: none;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* Scrolling itself is for phones with a narrow viewport - the visible
     scrollbar track/thumb it drags in on desktop browsers doesn't belong
     on what's designed as a mobile-first PWA (mobile browsers already
     auto-hide it). Swipe/scroll still works, just without the chrome. */
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
/* In light mode, --line is pale enough to blend into the paper background
   on its own. In dark mode it reads as a bright, hard "limit" line across
   the whole tab row - blend it toward the background so it stays a subtle
   separator instead of a stark bar. */
.tab {
  flex: 0 0 auto;
  background: transparent;
  border: none;
  color: var(--ink-soft);
  padding: 4px 0 12px;
  font-family: var(--serif);
  /* Full size, as it was. Shrinking the type to make the row fit was the
     wrong lever - it made the labels small to solve a spacing problem.
     The gap gives way instead (see .tabs above); the type only steps down
     on a genuinely narrow phone, and even there barely. */
  font-size: clamp(1.1rem, 3.6vw + 0.4rem, 1.35rem);
  /* The tab's own box is the tap target: 44px each way, label centred in
     it. "rata" and "wall" only draw 34-36px of text, and five of them in a
     row means a miss lands on the neighbour rather than on nothing. Sizing
     the box rather than overlaying an invisible one means the targets
     cannot overlap by construction - whatever the gap does at any width. */
  min-height: 44px;
  min-width: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 500;
  text-transform: lowercase;
  letter-spacing: 0.01em;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.tab:hover { background: transparent; color: var(--ink); }
/* Selected state is color + weight + underline only - no enclosing box.
   The general button rule further up gives every button a 4px
   border-radius; combined with a <button>'s native appearance in WebKit,
   that radius alone gets rendered as a full rounded rectangle around the
   tab (all four sides) even though only the bottom border is actually
   visible - overriding both to zero/none removes it, leaving just the
   underline. */
.tab { -webkit-appearance: none; appearance: none; border-radius: 0; position: relative; }
/* No invisible overlay widening the hit area any more - the tab's own box
   is 44px each way (see .tab above). The overlay had to guess how far it
   could reach without meeting its neighbour, and once the row's gap started
   shrinking with the viewport that guess was wrong: two targets claiming
   the same pixels, which is worse than one being small. */
/* outline:none used to be here with nothing in its place, so a keyboard or
   VoiceOver user got no indication of where they were. The ring is drawn on
   the label rather than the widened hit area, which would look detached. */
.tab:focus { outline: none; }
.tab:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
  border-radius: 3px;
}
/* No rule under the selected tab, and no rule under the row either. The
   active tab is said with the night's own colour and full weight; the rest
   step back to --ink-soft, which is a token already tuned for contrast on
   every theme rather than an opacity guessed here.
   The row keeps its 2px transparent border-bottom (see .tab above) so
   nothing shifts by a pixel between the selected tab and its neighbours. */
.tab { color: var(--ink-soft); }
.tab.active {
  color: var(--clay-active, var(--clay));
  font-weight: 700;
  border-bottom-color: transparent;
}
/* Fallback only - reachable when no content theme is active (admin never
   sets one, see settings.js), where the plain light-mode --clay reads
   pale/salmon against the paper, too soft to read as "selected" at a
   glance. Every regular-page theme below (furnished/swamp/rata/mural/
   plaza/memo) sets its own --clay-active matching its own --clay, which -
   set directly on body, closer in the tree than this html-level default -
   wins whenever one of those is active, restoring each tab's own color.
   Dark mode's warmer clays already have enough contrast as-is, no
   equivalent override needed there. */
/* The unthemed pages (Settings, and the admin chrome) now run on a lilac
   base, so the active-tab underline is purple rather than the leftover
   terracotta - 5.6:1 on that paper. */
html.mode-light { --clay-active: #66409a; }

/* --------------------------------- Cards --------------------------------- */

.card {
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
  margin-bottom: 18px;
}
.card h2 { font-size: 1.15rem; margin-bottom: 6px; }
/* Every theme's --ink is tuned within its own hue (see each body.theme-X
   block) - some read distinctly "whiter" than others at similar lightness
   (a cool cyan vs. a warm cream/gold), which made card titles look
   inconsistently bold from one page to the next. Literal white in dark
   mode everywhere, for the same visual weight regardless of theme. Light
   mode keeps each theme's own --ink (already dark, tuned to read against
   its own light paper - literal white there would be invisible). */
html.mode-dark .card h2 { color: #fff; }

.hero-line {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 600;
  font-size: 1.5rem;
  color: var(--ink-soft);
  margin: 2px 0 20px;
  line-height: 1.3;
}
/* --ink-soft is deliberately muted for de-emphasized text, but that reads
   as too faint for this subtitle-sized line in light mode specifically
   (dark mode's --ink-soft already has enough contrast on its own). Use
   the full --ink there instead. */
html.mode-light .hero-line { color: var(--ink); }

/* bloco de destaque (consequencia atual) - a thin 2px border read as too
   little "highlight" for how much space this box takes up at 1.5rem text,
   especially for a single short line with lots of empty room around it.
   A thicker border plus a faint accent-tinted wash (instead of the flat
   --paper-3 every other box already uses) makes the emphasis match the size. */
/* No tinted ground behind it. On a themed page the motif down the side is
   the whole marker (see the ::before rules below) - the fill underneath it
   made a second card inside the card, which reads worst in dark mode where
   two dark grounds an inch apart are hard to tell from a rendering fault.
   The border-left stays for the untinted base, which has no motif to lean
   on; every theme overrides it away. */
.highlight {
  border-left: 5px solid var(--rata);
  padding: 18px 20px;
  font-family: var(--serif);
  font-size: 1.5rem;
  line-height: 1.35;
  color: var(--ink);
  /* A long word with no spaces in it - a pasted address, someone's
     unbroken enthusiasm - has to break rather than push the card open. */
  overflow-wrap: anywhere;
}
/* Set at a size that suits a sentence. A dedication can be a paragraph, and
   at 1.5rem a paragraph is a wall of large type that has to be scrolled
   past rather than read; the class is added by whatever renders it once the
   text passes the length a headline can carry (see renderDedication() in
   app.js). Line height opens up as the size comes down, which is what long
   reading wants. */
.highlight.is-long {
  font-size: 1.12rem;
  line-height: 1.55;
}
/* The .highlight box is the one place each section gets to be playful
   rather than just informative, so instead of a flat accent bar it carries
   a motif drawn from that section's own world. These are masks, not
   coloured images: the shape is painted in the theme's accent via
   background, so a single SVG tracks --rata/--ot across every theme and
   both light and dark, which a fill baked into the data URI could not. */
/* The side motif, for every themed page. Listed theme by theme rather than
   applied to .highlight outright: an unthemed page defines no
   --highlight-motif, and a mask that resolves to nothing paints the whole
   16px block solid instead of nothing at all. Each theme's own token is
   drawn from its world - see the body.theme-* blocks. */
body.theme-rata .highlight,
body.theme-furnished .highlight,
body.theme-swamp .highlight,
body.theme-mural .highlight,
body.theme-memo .highlight {
  position: relative;
  border-left: none;
  padding-left: 20px;
  /* No gutter reserved for the emblem. It sits behind the words (see
     ::before) and the words are what matter: holding a column clear of it
     narrowed every consequence, worst on the smallest phone, to buy space
     for decoration. The text runs straight over it instead - which is the
     whole reason the emblem is painted at 22% and behind the content. */
  padding-right: 20px;
}
body.theme-rata .highlight::before,
body.theme-furnished .highlight::before,
body.theme-swamp .highlight::before,
body.theme-mural .highlight::before,
body.theme-memo .highlight::before {
  /* The night's motif drawn ONCE, large, running the height of the block
     and bleeding off its right edge.
     It was briefly tiled in both directions as a watermark, which is what
     this replaces. Every one of these motifs is a 16px-wide vertical
     composition - a reed, a tail, a string of bunting - drawn to stand up
     on its own; repeated as wallpaper at 26px the swamp's reeds read as a
     picket fence and the rata's tail as static. Used at its own
     proportions it is an emblem again.
     Kept behind the text (the element's own content paints over it) and
     low enough that a long sentence can run across it without fighting. */
  content: '';
  position: absolute;
  /* Right edge. Tried on the left, where it lines up with the margin every
     card starts at - Ivo's call after seeing both, and the reason it works
     is that the text is what leads on the left; a mark there competes with
     the first word of every line, where on the right it closes the block
     instead of opening it. */
  inset: 0 -6px 0 auto;
  /* Scales with the viewport too, so the smallest phone does not end up
     with a full-size emblem and a three-word text column beside it. */
  width: clamp(64px, 24vw, 92px);
  background: var(--clay);
  opacity: 0.22;
  pointer-events: none;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: right center;
  mask-position: right center;
  /* Height-driven, so each theme's tile keeps its own aspect ratio however
     tall the block ends up - Plaza's bunting is 16x72 where the rest are
     16x40, and forcing a fixed width would squash one or stretch the
     other. */
  -webkit-mask-size: auto 96%;
  mask-size: auto 96%;
  -webkit-mask-image: var(--highlight-motif);
  mask-image: var(--highlight-motif);
}

/* No ground here either. This tint predates the emblem and outlived the
   decision that took the ground off .highlight everywhere else, so the one
   on Snatch sat in a tinted box while every other highlight in the app sat
   on the paper - the only place in the app where the same component looked
   like two different things. The border-left stays for the unthemed
   fallback, exactly as it does on the base rule. */
#tab-outfit .highlight {
  border-left-color: var(--outfit);
}

/* Plaza's emblem is a village, and it is built differently from every other
   one in the app. The others are a single drawing pinned to the right edge.
   This one is one house, repeated down the left margin, and the number of
   houses is decided by how much somebody wrote.

   That difference is the whole reason it works here. Plaza went without an
   emblem for a long time - roughly thirty free-standing drawings were tried
   and none belonged, because this is the page where the writing is, and a
   picture in the margin of an eight-line dedication competes with it. A
   repeat does not compete: it reads as texture beside a long message and as
   a single house beside a short one.

   mask-repeat: space is doing the load-bearing work. It lays down only as
   many whole tiles as fit and shares the leftover space out between them,
   so a house is never sliced off at the top or bottom of the block - which
   is the failure every tiled version of this had before. repeat-y would
   clip; space cannot. Supported in WebKit (checked against the same engine
   iOS Safari runs), which is the only place this has to hold up.

   Two consequences worth knowing before changing anything here:

   - min-height exists so that a one-line dedication is still tall enough to
     hold one entire house. Drop it and the shortest messages lose their
     emblem to the no-clipping rule. It is tied to the 64px tile: if the
     tile grows, this has to grow with it.

   - The tile is ONE house, so every house is identical. An earlier version
     alternated a gabled house with a flat-topped block, which looked better,
     but a repeating tile is by definition the same every time - variety and
     never-clipping cannot both come from one tile. Never-clipping won.

   The house itself is lifted from this theme's own background (see the
   background-image in body.theme-plaza): pitched roof, chimney, two lit
   windows, a door. Painted as a mask in --clay like every other emblem, so
   it follows the accent into dark mode on its own. */
body.theme-plaza .highlight {
  position: relative;
  border-left: none;
  padding-left: 38px;
  padding-right: 18px;
  min-height: 72px;
}
body.theme-plaza .highlight::before {
  content: '';
  position: absolute;
  /* Inset top and bottom so the first and last house do not sit flush
     against the edges of the block. */
  inset: 4px auto 4px 0;
  width: 26px;
  background: var(--clay);
  opacity: 0.5;
  pointer-events: none;
  -webkit-mask-image: var(--plaza-casa);
  mask-image: var(--plaza-casa);
  -webkit-mask-repeat: no-repeat space;
  mask-repeat: no-repeat space;
  -webkit-mask-position: left center;
  mask-position: left center;
  -webkit-mask-size: 26px 64px;
  mask-size: 26px 64px;
}

/* ----------------------------- Rata / Outfit ----------------------------- */

.vote-option {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  text-align: left;
  background: var(--paper-3);
  color: var(--ink);
  border: 1px solid var(--line);
  padding: 15px 18px;
  margin-bottom: 10px;
  font-family: var(--serif);
  font-size: 1.25rem;
  text-transform: lowercase;
  letter-spacing: 0;
}
.vote-option::after { content: '\2192'; opacity: 0; transition: opacity 0.2s; color: var(--outfit); }
.vote-option:hover { background: var(--paper-3); color: var(--ink); border-color: var(--outfit); }
.vote-option:hover::after { opacity: 1; }

/* --------------------------------- Tier ---------------------------------- */

.tier { list-style: none; padding: 0; margin: 0; counter-reset: rank; }
.tier li {
  counter-increment: rank;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 4px;
  border-bottom: 1px solid var(--line);
}
.tier li:last-child { border-bottom: none; }
.tier li::before {
  content: counter(rank, decimal-leading-zero);
  font-family: var(--serif);
  font-size: 1.5rem;
  color: var(--clay);
  min-width: 40px;
}
.tier .name {
  flex: 1;
  font-family: var(--serif);
  font-size: 1.2rem;
  line-height: 1.25;
  /* Duo entries append both real names in parens (see contestantLabel() in
     app.js) - that can run long, so clamp to 2 lines with an ellipsis
     rather than letting one row balloon to 4+ lines; the title attribute
     on the element carries the full text for anyone who needs it. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.tier .votes { font-family: var(--body); font-size: var(--text-sm); text-transform: lowercase; letter-spacing: 0.16em; color: var(--ink-soft); }
.tier li:nth-child(-n+3) .name { font-weight: 600; }

/* Podium reveal in progress (before "all"): the row already states its own
   rank in the name ("3rd place · X"), so the auto counter would be wrong
   for a partial list - hide it and just bold every revealed row. */
.tier.podium-reveal li::before { content: none; }
.tier.podium-reveal .name { font-weight: 600; }
.tier li:first-child::before { color: var(--outfit); }

/* Outfit photo thumbnail - shown on a guest's own entry (registration/
   confirmation cards) and, once revealed, in the results tier above. Never
   shown to other guests during voting - see photoUrl() in app.js. */
.entry-photo-preview {
  display: block;
  width: 56px;
  height: 56px;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--line);
}
.tier .entry-photo-preview { width: 44px; height: 44px; flex-shrink: 0; }
#contestants-table .entry-photo-preview { width: 32px; height: 32px; }
/* Confirms a photo actually made it in before submitting - the thumbnail
   alone is easy to miss at a glance on a small screen. */
#register-photo-preview { display: flex; align-items: center; gap: 10px; }
.photo-attached-label { color: var(--ink-soft); font-size: var(--text-sm); }

/* --------------------------------- Admin --------------------------------- */

.row-actions {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  flex-wrap: wrap;
}
/* Publish used to break onto its own full-width line here. On a day row it
   sits beside download, which is the action it pairs with - two halves of
   "get this day out" - and a full-width bar under a short button read as
   two unrelated controls. */

table { width: 100%; border-collapse: collapse; font-size: 0.95rem; margin-top: 8px; }
th, td { text-align: left; padding: 9px 8px; border-bottom: 1px solid var(--line); vertical-align: middle; }
th { font-family: var(--body); text-transform: lowercase; letter-spacing: 0.12em; font-size: var(--text-xs); color: var(--ink-soft); font-weight: 600; }
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Lista de utilizadores em cartoes (evita colunas espremidas em mobile) */
.user-list { margin-top: 8px; }
.user-row { padding: 12px 0; border-bottom: 1px solid var(--line); }
.user-row:last-child { border-bottom: none; }
.user-row-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.user-row-name { font-weight: 600; }
.user-row-name .badge { margin-left: 6px; }
/* .account-token (the Settings "your account" card) shares this exact
   look-and-behavior with Manage Users' own row token below, but stays a
   separate class - Playwright tests query .user-row-token unscoped to mean
   specifically a Manage Users list row, and the account card now renders
   earlier in the DOM than that list. */
.user-row-token,
.account-token {
  font-family: 'SFMono-Regular', ui-monospace, monospace;
  font-size: 0.8rem;
  color: var(--rata);
  word-break: break-all;
  text-align: left;
  cursor: pointer;
  /* Always its own line below the name, not a flex sibling sharing the
     row - otherwise it only wraps once the name is long enough to push
     it off, so short names show it inline and long names don't, an
     inconsistent zig-zag down the list. */
  flex-basis: 100%;
  margin-top: 4px;
}
.user-row-token.censored, .account-token.censored { letter-spacing: 2px; }
/* Brief confirmation that a click also copied the key - doesn't touch the
   text itself (unlike the old copy button, which safely could). */
.user-row-token.just-copied, .account-token.just-copied { color: var(--clay); transition: color 0.15s; }
.user-row .row-actions { justify-content: flex-end; flex-wrap: wrap; white-space: normal; margin-top: 8px; }
/* Collapsed to name+role by default (see renderUsersList() in settings.js) -
   with a real guest list, showing every key/action for every row at once
   made Manage Users an extremely long, mostly-irrelevant scroll. Tap a row
   to reveal its own key/actions instead. */
.user-row-top { cursor: pointer; }
.user-row-chevron {
  color: var(--ink-soft);
  font-size: 1.2rem;
  line-height: 1;
  transition: transform 0.15s ease;
  flex-shrink: 0;
}
.user-row-top[aria-expanded="true"] .user-row-chevron { transform: rotate(90deg); }
.user-row-details { margin-top: 6px; }

/* Click-to-rename a contestant or user (see loadContestants()/renderUsersList()). */
.contestant-name-edit, .user-row-name-text.name-edit { cursor: pointer; border-bottom: 1px dotted var(--line); }
.contestant-name-edit:hover, .user-row-name-text.name-edit:hover { border-bottom-color: var(--ink-soft); }

/* align-items: center - without it, a flex row stretches every item to
   match its tallest sibling by default, so a compact button next to a
   taller input renders at the input's full height regardless of its own
   (much smaller) padding. Affects every .inline form: chat, admin's
   create-user, admin's add-contestant. */
/* flex-wrap: a row of 3+ buttons (e.g. confirm/cancel/change-photo on a
   pending duo entry) has no reason to force itself into one line - nowrap
   was pushing the last one off the edge of the screen on narrower phones
   instead of just dropping it to a second row. */
.inline { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; align-items: center; }
.inline input { margin-bottom: 0; }
.inline button { white-space: nowrap; }
/* Chat: the message field takes the room and "partilhar" sits on its end,
   rather than the input claiming the full width (inputs default to 100%)
   and pushing the button onto a line of its own. */
#chat-form { flex-wrap: nowrap; }
#chat-form input { flex: 1 1 auto; min-width: 0; }
#chat-form button { flex: 0 0 auto; }
/* The search-or-create user input otherwise wraps its "criar" button onto
   its own line (inputs are width:100% by default) - here the button is a
   short, immediate action tied to what's typed, so it reads better sitting
   right next to the input instead of below it. */
#user-form input { flex: 1 1 auto; width: auto; min-width: 0; }

.help { font-size: 0.95rem; color: var(--ink-soft); margin: 0 0 14px; }

/* Search-result chips under the "team up with" input on Best Outfit's
   self-registration form (see #register-partner-results in app.js). */
.partner-results { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }

/* ------------------------------ Responsive -------------------------------- */

@media (max-width: 480px) {
  /* Most small text (badges, muted captions, help text, table cells,
     buttons) is sized in rem, which tracks the root element's font-size,
     not body's - so bumping body alone left all of that stuck at the
     browser's plain 16px default. Raising html's font-size scales that
     whole tier up together with the bigger body copy below. */
  html { font-size: 18px; }
  body { font-size: 19px; }
  /* No brand-name override here anymore - its clamp() above is in vw, which
     is already relative to the real viewport width regardless of this
     breakpoint's html{font-size} bump, so stacking a rem override on top of
     it here would silently inflate it again (exactly what caused the
     overflow this replaces). */
  /* gap and .tab font-size are clamped against the viewport in the base
     rules above, which already do the right thing here - overriding them
     with flat values is what put the row back over the edge. */
  .card { padding: 18px; }
  .highlight, .hero-line { font-size: 1.3rem; }
}

/* ===================== Transicao suave ao trocar de tema ================ */

body { transition: background-color 0.45s ease; }
.card, .vote-option, .highlight, .topbar, .tabs {
  transition: background-color 0.4s ease, border-color 0.4s ease, color 0.4s ease;
}

/* ================= Controlo de votacao (painel admin) =================== */

.voting-controls { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.voting-controls button { width: 100%; }
/* Publishing the night to the Wall is the one action here that leaves the
   admin panel and shows up in front of everyone, so it gets the full width
   rather than sharing a row with the phase controls. */
.publish-wide { grid-column: 1 / -1; width: 100%; }
/* The two exports share a line at half width each; publishing to the Wall
   gets its own full-width row below, the way every other commit-style
   action in the app is shaped. Three equal buttons crammed on one line read
   as three equal choices, when one of them is the one that goes public. */
.memo-day-exports {
  display: flex;
  gap: 8px;
  margin-top: 10px;
}
.memo-day-exports > button { flex: 1 1 50%; min-width: 0; }
.memo-day-exports + .publish-wide { margin-top: 8px; }
.voting-state { font-family: var(--serif); font-size: 1.2rem; }
/* One line per round: what it is on the left, how many votes are in on the
   right. Tabular figures so the numbers line up as they climb. */
.tally-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin: 6px 0;
  font-family: var(--serif);
  font-size: 1.05rem;
}
.tally-line span:first-child { margin-right: auto; }
.tally-line b { font-variant-numeric: tabular-nums; font-size: 1.2rem; }
.voting-state.is-registration { color: var(--clay); }
.voting-state.is-open { color: var(--rata); }
/* --danger is tuned as a fill, and as text on a light card it sits around
   1.9-2.2:1 - the state you most need to read at a glance was the hardest
   one to read. Darkened to clear 4.5 on every light theme paper; dark mode
   keeps --danger, which is already legible there. */
.voting-state.is-closed { color: #9c3f2d; }
html.mode-dark .voting-state.is-closed { color: var(--danger); }

/* ============ Best Outfit: switchable theme (furnished / swamp) ========= */
/* The theme is applied to <body> (the WHOLE page) only while Best Outfit is
   active. Every other section stays plain (no theme class). */

/* --- FURNISHED: cozy interior, teal velvet, warm wallpaper --- */
body.theme-furnished {
  /* Whole theme rebuilt on a single leaf-green hue (~112 degrees). It used
     to drift across the spectrum - papers around 102-136 but accents at
     152-167, which is sea green - and since the accents are what the eye
     picks up (buttons, borders, hero line), the page read as blue-green
     rather than green. 112 is far enough from Swamp's olive (~75) that the
     two nights still look nothing alike, and far enough from teal that
     nothing here reads as blue. Contrast was re-checked on every pair that
     matters after the shift; the earlier saturation and border-visibility
     fixes below are preserved, just restated in the new hue. */
  --paper:    #d2e5cf;
  --paper-2:  #e0ecde;
  --paper-3:  #e9f2e7;
  --ink:      #1f2d1d;     /* 12.6:1 on --paper-3 */
  --ink-soft: #546a50;     /* 5.2:1  on --paper-3 */
  /* Card/post borders have to stay visible without turning into a hard
     rule - this sits at ~2.3:1 against --paper-2, same target as the
     value it replaces. */
  --line:     #84a17f;
  --line-2:   #bfcfbc;
  --clay:     #32502d;     /* 7.9:1 on --paper-3 */
  --clay-2:   #456840;
  --clay-active: #32502d;
  --rata:     #32502d;
  --ot:       #32502d;
  --ot-2:     #456840;
  --ot-tint:  #e0ebdf;
  /* A framed picture hanging from a picture rail, repeating down the edge -
     the wall of a furnished room rather than its skirting. Taller tile than
     the 40px default so the frame has room to read as a frame; at the
     original size it was a speck. */
  --highlight-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='56'%3E%3Cpath d='M8 0 V7' stroke='%23000' stroke-opacity='0.45' stroke-width='1'/%3E%3Cpath d='M8 7 L1.6 15 M8 7 L14.4 15' stroke='%23000' stroke-opacity='0.5' stroke-width='0.9' fill='none'/%3E%3Crect x='1' y='15' width='14' height='17' fill='none' stroke='%23000' stroke-opacity='0.85' stroke-width='1.7'/%3E%3Crect x='3.6' y='17.6' width='8.8' height='11.8' fill='%23000' fill-opacity='0.22'/%3E%3Cpath d='M8 32 V56' stroke='%23000' stroke-opacity='0.22' stroke-width='1'/%3E%3C/svg%3E");
  --highlight-motif-h: 56px;
  --ot-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Crect width='60' height='60' fill='%23e0ebdf'/%3E%3Cg fill='none' stroke='%2332502d' stroke-opacity='0.30'%3E%3Cpath d='M15 0V60M45 0V60'/%3E%3C/g%3E%3Cg fill='%2332502d' fill-opacity='0.22'%3E%3Cpath d='M30 10 L38 22 L30 34 L22 22 Z'/%3E%3Ccircle cx='0' cy='45' r='3'/%3E%3Ccircle cx='60' cy='45' r='3'/%3E%3C/g%3E%3C/svg%3E");
  background-color: #c6d4c4;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='160'%3E%3Crect width='120' height='160' fill='%23ccd9c9'/%3E%3Cg stroke='%2332502d' stroke-opacity='0.16' fill='none'%3E%3Cpath d='M20 0V160M60 0V160M100 0V160'/%3E%3C/g%3E%3Cg fill='%2332502d' fill-opacity='0.13'%3E%3Cpath d='M60 30 L74 55 L60 80 L46 55 Z'/%3E%3Cpath d='M60 45 L67 55 L60 65 L53 55 Z' fill='%23ccd9c9'/%3E%3Ccircle cx='20' cy='120' r='3'/%3E%3Ccircle cx='100' cy='120' r='3'/%3E%3Cpath d='M0 130 L10 145 L0 160 M120 130 L110 145 L120 160'/%3E%3C/g%3E%3C/svg%3E");
}

/* --- SWAMP: bog, moss/olive, still water, reeds and water lilies --- */
body.theme-swamp {
  --paper:    #ccd4b3;
  --paper-2:  #d7dcc0;
  --paper-3:  #e0e4cd;
  --ink:      #242a17;
  --ink-soft: #55613a;
  /* Was #aab291 (~1.6:1 against --paper-2) - same weak-border issue as
     furnished light, fixed the same way (darkened within the family). */
  --line:     #869165;
  --line-2:   #bfc7a4;
  --clay:     #55632c;
  --clay-2:   #6c7b3c;
  --clay-active: #55632c;
  --rata:     #55632c;
  --ot:       #55632c;
  --ot-2:     #6c7b3c;
  --ot-tint:  #d7dcc0;
  /* Reeds standing in water: two stems of different heights, one topped
     with a cattail, rising through a pair of ripple lines. Each tile is a
     small clump, so repeating it reads as a stretch of bank rather than a
     repeated icon. */
  --highlight-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='40'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round'%3E%3Cpath d='M6 40 C6 30 4.5 22 5.5 15' stroke-opacity='0.9' stroke-width='1.5'/%3E%3Cpath d='M11.5 40 C11.5 33 12.5 28 11.5 23' stroke-opacity='0.5' stroke-width='1.2'/%3E%3Cpath d='M0.5 34 Q8 31 15.5 34' stroke-opacity='0.5' stroke-width='1.1'/%3E%3Cpath d='M0.5 38.5 Q8 35.5 15.5 38.5' stroke-opacity='0.32' stroke-width='1'/%3E%3C/g%3E%3Crect x='3.4' y='4' width='4.2' height='11' rx='2.1' fill='%23000' fill-opacity='0.85'/%3E%3Cg stroke='%23000' stroke-opacity='0.55' stroke-width='0.8' fill='none'%3E%3Cpath d='M11.5 21 q2 -1.6 3.5 0.4'/%3E%3Cpath d='M11.5 21 q2 1.6 3.5 -0.4'/%3E%3Cpath d='M11.5 21 V25'/%3E%3C/g%3E%3C/svg%3E");
  --ot-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='60'%3E%3Crect width='80' height='60' fill='%23d7dcc0'/%3E%3Cg fill='none' stroke='%2355632c' stroke-opacity='0.26'%3E%3Cpath d='M0 45 Q20 38 40 45 T80 45'/%3E%3Cpath d='M0 54 Q20 47 40 54 T80 54'/%3E%3C/g%3E%3Cg stroke='%2355632c' stroke-opacity='0.42' stroke-width='1.4'%3E%3Cpath d='M18 42 V18'/%3E%3Cpath d='M24 42 V24'/%3E%3Cpath d='M58 42 V20'/%3E%3Cpath d='M64 42 V26'/%3E%3C/g%3E%3C/svg%3E");
  background-color: #bfc8a3;
  /* Lily pads on open water: four pads at different sizes with one bloom,
     and two ripple lines behind them. The notch cut out of each pad is 6
     degrees - the previous version used about 30, which is what made them
     read as pac-man rather than as pads, and was the actual complaint
     behind "that is not the background you showed me". */
  background-image:
    url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20width='200'%20height='200'>%20<rect%20width='200'%20height='200'%20fill='%23c5cea8'/>%20<g%20fill='none'%20stroke='%2355632c'%20stroke-opacity='0.11'>%20<path%20d='M0%2046%20Q50%2038%20100%2046%20T200%2046'/><path%20d='M0%20150%20Q50%20142%20100%20150%20T200%20150'/>%20</g>%20<g%20fill='%2355632c'>%20<path%20fill-opacity='0.17'%20transform='rotate%28-24%2063%2074%29'%20d='M63%2074%20L77.9%2072.4%20A15%2015%200%201%200%2077.9%2075.6%20Z'/>%20<path%20fill-opacity='0.13'%20transform='rotate%28130%20150%20120%29'%20d='M150%20120%20L161.9%20118.7%20A12%2012%200%201%200%20161.9%20121.3%20Z'/>%20<path%20fill-opacity='0.15'%20transform='rotate%2872%20104%20168%29'%20d='M104%20168%20L113.9%20166.9%20A10%2010%200%201%200%20113.9%20169.1%20Z'/>%20<path%20fill-opacity='0.11'%20transform='rotate%28-140%2028%20140%29'%20d='M28%20140%20L36.9%20139.1%20A9%209%200%201%200%2036.9%20140.9%20Z'/>%20<circle%20cx='150'%20cy='58'%20r='3.4'%20fill-opacity='0.26'/>%20<circle%20cx='150'%20cy='58'%20r='6.5'%20fill-opacity='0.10'/>%20</g></svg>");
}

/* Flourishes tematicos aplicados a pagina inteira quando ha tema */
body.theme-furnished .hero-line,
/* Furnished never got its own hero-line accent (unlike swamp/rata/mural/
   plaza/memo below), so it fell back to the deliberately muted default
   (.hero-line's own color:var(--ink-soft)) - read as much less bold than
   every other theme's hero-line, since bold text.at low contrast still
   looks thin. */
body.theme-furnished .hero-line { color: var(--ot-2); }
body.theme-swamp .hero-line { color: var(--ot-2); }
body.theme-furnished .card,
body.theme-swamp .card {
  box-shadow: 0 12px 34px rgba(34, 48, 43, 0.12);
}
body.theme-swamp .card { box-shadow: 0 12px 34px rgba(36, 42, 23, 0.14); }

/* ================= A Rata: sinistro, mesma simplicidade dos temas ======== */
/* Aplicado a <body> apenas quando o separador A Rata esta ativo (ver
   applyNight() em app.js), mesmo mecanismo dos temas Furnished/Swamp. */
body.theme-rata {
  /* Was a neutral grey-black (#18181b family) - shifted warmer, toward a
     dusty pink-grey (a hint of mauve in the dark, not full grey). */
  --paper:    #201a1d;
  --paper-2:  #271f22;
  --paper-3:  #2e2529;
  --ink:      #e8dfe1;
  --ink-soft: #9c8b8e;
  /* Was #3d3237/#4b3e43 - too close to --paper-2 (#271f22) for the card
     border to read at all, the dark-mode counterpart of the light-mode fix
     above. Lightened within the same dusty-mauve family. */
  --line:     #665261;
  --line-2:   #7a6070;
  /* Was orange (#a86c2f/#c2803f), before that brick-red - orange ended up
     stepping on Wall/mural's own brown-orange territory instead of reading
     as Rata's own color. Pink keeps Rata in its own established dusty
     pink-grey family (see --paper/--ink above) instead of borrowing a warm
     hue that's already spoken for elsewhere. */
  /* Same off-red-toward-mauve shift as the light variant below. */
  --clay:     #b8438a;
  --clay-2:   #d678ad;
  /* Dark is this theme's base (light is the override below), so the
     selected tab is set here. Without it the fallback reached --clay, and
     3.76 sat under the unselected tabs' 5.81 - faint for the same reason
     the other themes were, by a different route. */
  --clay-active: #d678ad;
  --rata:     #b8438a;
  background-color: #190f13;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Crect width='200' height='200' fill='%231a1216'/%3E%3Cg fill='%23705f63' fill-opacity='0.30'%3E%3Cg transform='translate(28,176) rotate(-20)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3Cg transform='translate(58,148) rotate(-10)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3Cg transform='translate(96,168) rotate(15)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3Cg transform='translate(150,60) rotate(160)'%3E%3Cellipse rx='11' ry='8.5'/%3E%3Cellipse cx='-9' cy='-14' rx='4.2' ry='5.4'/%3E%3Cellipse cy='-17' rx='4.2' ry='6'/%3E%3Cellipse cx='9' cy='-14' rx='4.2' ry='5.4'/%3E%3C/g%3E%3Cg transform='translate(172,30) rotate(150)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3C/g%3E%3Cg fill='none' stroke='%23705f63' stroke-opacity='0.25' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M160 110 Q145 95 155 80 Q165 68 152 62'/%3E%3C/g%3E%3C/svg%3E");
  --highlight-motif: var(--rata-trail);
}
body.theme-rata .hero-line { color: var(--clay-2); }
body.theme-rata .card {
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.45);
}

/* --- Dark/light variants per night: each theme gets its own pair --- */
html.mode-dark body.theme-furnished {
  /* The selected tab, in this theme's own hue. Set per mode, and that is
     the whole point: --clay-active used to be declared once in the
     unscoped body.theme-* block, so the LIGHT value went on painting the
     selected tab in dark mode too, where the dark block had already moved
     --clay somewhere brighter. Measured on the tab row, the selected tab
     came out fainter than its own unselected neighbours - Snatch 2.02 next
     to 7.38, Plaza 2.70 next to 6.25 - which is exactly the "too faded"
     this fixes. The var(--clay-active, var(--clay)) fallback could never
     catch it either: --clay-active was defined, just wrong.
     The value is the theme's existing --clay-2 (its own lighter variant),
     which clears the unselected tabs in every theme without inventing a
     colour that is not already in the palette. */
  --clay-active: #7dc072;
  --paper:    #172015;
  --paper-2:  #202c1e;
  --paper-3:  #283725;
  --ink:      #dde8db;
  --ink-soft: #8ead8a;
  /* Was #35493f (~1.5:1 against --paper-2) - lightened within the same
     family so boxed cards/posts get a visible border in dark mode too. */
  --line:     #4f6b4a;
  --line-2:   #435a3f;
  --clay:     #66ae5b;
  --clay-2:   #7dc072;
  --rata:     #66ae5b;
  --ot:       #66ae5b;
  --ot-2:     #7dc072;
  --ot-tint:  #202c1e;
  --ot-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Crect width='60' height='60' fill='%23202c1e'/%3E%3Cg fill='none' stroke='%2366ae5b' stroke-opacity='0.30'%3E%3Cpath d='M15 0V60M45 0V60'/%3E%3C/g%3E%3Cg fill='%2366ae5b' fill-opacity='0.24'%3E%3Cpath d='M30 10 L38 22 L30 34 L22 22 Z'/%3E%3Ccircle cx='0' cy='45' r='3'/%3E%3Ccircle cx='60' cy='45' r='3'/%3E%3C/g%3E%3C/svg%3E");
  background-color: #10170f;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='160'%3E%3Crect width='120' height='160' fill='%23161f14'/%3E%3Cg stroke='%2366ae5b' stroke-opacity='0.20' fill='none'%3E%3Cpath d='M20 0V160M60 0V160M100 0V160'/%3E%3C/g%3E%3Cg fill='%2366ae5b' fill-opacity='0.16'%3E%3Cpath d='M60 30 L74 55 L60 80 L46 55 Z'/%3E%3Cpath d='M60 45 L67 55 L60 65 L53 55 Z' fill='%23161f14'/%3E%3Ccircle cx='20' cy='120' r='3'/%3E%3Ccircle cx='100' cy='120' r='3'/%3E%3Cpath d='M0 130 L10 145 L0 160 M120 130 L110 145 L120 160'/%3E%3C/g%3E%3C/svg%3E");
}
html.mode-dark body.theme-swamp {
  /* Same fix as furnished dark above - the other Snatch night. */
  --clay-active: #a3b768;
  --paper:    #1a2010;
  --paper-2:  #212912;
  --paper-3:  #283114;
  --ink:      #e2e6d0;
  --ink-soft: #9aa580;
  /* Was #3a4420 (~1.5:1 against --paper-2) - lightened within the same
     family, same fix as furnished dark above. */
  --line:     #55632f;
  --line-2:   #47542a;
  --clay:     #8a9c52;
  --clay-2:   #a3b768;
  --rata:     #8a9c52;
  --ot:       #8a9c52;
  --ot-2:     #a3b768;
  --ot-tint:  #212912;
  --ot-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='60'%3E%3Crect width='80' height='60' fill='%23212912'/%3E%3Cg fill='none' stroke='%238a9c52' stroke-opacity='0.30'%3E%3Cpath d='M0 45 Q20 38 40 45 T80 45'/%3E%3Cpath d='M0 54 Q20 47 40 54 T80 54'/%3E%3C/g%3E%3Cg stroke='%238a9c52' stroke-opacity='0.46' stroke-width='1.4'%3E%3Cpath d='M18 42 V18'/%3E%3Cpath d='M24 42 V24'/%3E%3Cpath d='M58 42 V20'/%3E%3Cpath d='M64 42 V26'/%3E%3C/g%3E%3C/svg%3E");
  background-color: #141a0c;
  /* Same pads as light, on the dark water - see the note there. */
  background-image:
    url("data:image/svg+xml,<svg%20xmlns='http://www.w3.org/2000/svg'%20width='200'%20height='200'>%20<rect%20width='200'%20height='200'%20fill='%23192010'/>%20<g%20fill='none'%20stroke='%238a9c52'%20stroke-opacity='0.11'>%20<path%20d='M0%2046%20Q50%2038%20100%2046%20T200%2046'/><path%20d='M0%20150%20Q50%20142%20100%20150%20T200%20150'/>%20</g>%20<g%20fill='%238a9c52'>%20<path%20fill-opacity='0.17'%20transform='rotate%28-24%2063%2074%29'%20d='M63%2074%20L77.9%2072.4%20A15%2015%200%201%200%2077.9%2075.6%20Z'/>%20<path%20fill-opacity='0.13'%20transform='rotate%28130%20150%20120%29'%20d='M150%20120%20L161.9%20118.7%20A12%2012%200%201%200%20161.9%20121.3%20Z'/>%20<path%20fill-opacity='0.15'%20transform='rotate%2872%20104%20168%29'%20d='M104%20168%20L113.9%20166.9%20A10%2010%200%201%200%20113.9%20169.1%20Z'/>%20<path%20fill-opacity='0.11'%20transform='rotate%28-140%2028%20140%29'%20d='M28%20140%20L36.9%20139.1%20A9%209%200%201%200%2036.9%20140.9%20Z'/>%20<circle%20cx='150'%20cy='58'%20r='3.4'%20fill-opacity='0.26'/>%20<circle%20cx='150'%20cy='58'%20r='6.5'%20fill-opacity='0.10'/>%20</g></svg>");
}
html.mode-dark body.theme-furnished .card,
html.mode-dark body.theme-swamp .card {
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
}

html.mode-light body.theme-rata {
  /* Was a neutral light grey (#ececed family), then a dusty pink-grey
     (#ece3e4) that still read as plain grey next to the other themes.
     Properly pink now - roughly triple the chroma at the same lightness, so
     it looks like the rata tab rather than an unthemed page. The two inks
     were neutral greys sitting on top of it; both carry the same pink bias
     now, still 10.7:1 and 4.8:1 against --paper (comfortably past AA). */
  --paper:    #f0d9df;
  --paper-2:  #f6e2e7;
  --paper-3:  #fbebef;
  --ink:      #40202c;
  --ink-soft: #805062;
  /* Was #dcc9cc/#e6d6d8 - barely distinguishable from --paper-2 (#f1e9ea),
     so card borders read as missing entirely on this tab specifically,
     unlike every other night theme. Darkened within the same dusty-pink
     family so the border stays visible without losing the muted mood. */
  --line:     #b8969b;
  --line-2:   #c7a8ac;
  /* Same pink hue-shift as the dark variant above - was #8a5b2c/#6e4822
     (orange), before that #8a3f2c/#6e3122 (brick-red). */
  /* Shifted off red toward mauve-pink, by request: these were #8a2c4b /
     #6e223b, which read as maroon against the (now properly pink) paper
     rather than as part of it. Same lightness, hue rotated toward magenta -
     the hero line still clears 7:1 and the accents 5:1 on --paper. */
  --clay:     #9c3374;
  --clay-2:   #7a2860;
  --rata:     #9c3374;
  /* Missing here despite the html.mode-light fallback's own comment
     claiming every regular-page theme sets one - fell through to that
     stray global default (#b5624a, meant only for admin's theme-less
     Settings tab) instead of Rata's own pink underline. */
  --clay-active: #8a2c4b;
  background-color: #efe6e7;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Crect width='200' height='200' fill='%23f0e8e9'/%3E%3Cg fill='%23978387' fill-opacity='0.14'%3E%3Cg transform='translate(28,176) rotate(-20)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3Cg transform='translate(58,148) rotate(-10)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3Cg transform='translate(96,168) rotate(15)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3Cg transform='translate(150,60) rotate(160)'%3E%3Cellipse rx='11' ry='8.5'/%3E%3Cellipse cx='-9' cy='-14' rx='4.2' ry='5.4'/%3E%3Cellipse cy='-17' rx='4.2' ry='6'/%3E%3Cellipse cx='9' cy='-14' rx='4.2' ry='5.4'/%3E%3C/g%3E%3Cg transform='translate(172,30) rotate(150)'%3E%3Cellipse rx='9' ry='7'/%3E%3Cellipse cx='-8' cy='-12' rx='3.6' ry='4.6'/%3E%3Cellipse cy='-15' rx='3.6' ry='5'/%3E%3Cellipse cx='8' cy='-12' rx='3.6' ry='4.6'/%3E%3C/g%3E%3C/g%3E%3Cg fill='none' stroke='%23978387' stroke-opacity='0.18' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M160 110 Q145 95 155 80 Q165 68 152 62'/%3E%3C/g%3E%3C/svg%3E");
}
html.mode-light body.theme-rata .card {
  box-shadow: 0 12px 34px rgba(60, 60, 65, 0.16);
}

/* ================ Mural: parede de tijolo, mesma logica dos outros ======= */
/* Aplicado a <body> apenas quando o separador Mural esta ativo (ver
   applyNight() em app.js), mesmo mecanismo dos temas Furnished/Swamp/Rata. */
body.theme-mural {
  --paper:    #cf9b7c;
  --paper-2:  #d9ac8f;
  /* Every other theme steps paper -> paper-2 -> paper-3 by a few points of
     lightness; this one used to jump twice as far, so the chat input (the
     tab's only paper-3 surface) read as a glaring bright patch instead of
     a subtle lift. Tightened to match the other themes' rhythm. */
  --paper-3:  #deb69c;
  --ink:      #3d2216;
  /* Was #7a5138 (~3.7:1 against --paper-3), then #5c3c29 (~4.8:1 against
     --paper-2, but only ~4.05:1 against the darker --paper the pattern
     sits on directly - still failed WCAG AA there). Darkened again to
     ~5.1:1 against --paper, ~6:1 against --paper-2/3. */
  --ink-soft: #4a2e1c;
  /* Was #a9744f (~1.4:1 against the page background, effectively invisible -
     the topbar/tabs border-bottom and icon-btn borders rendered as no line
     at all), then #663f26 (~3.3:1, meets the 3:1 WCAG guideline for UI
     boundaries) - but that read as noticeably bolder/thicker than the same
     border on every other theme (furnished/swamp/default all sit much
     lighter), which was its own inconsistency. Settled on a middle value -
     still clearly visible, closer in weight to its siblings. */
  --line:     #8c5a3a;
  --line-2:   #bd8964;
  --clay:     #8f3a26;
  /* Was #a84631 (~2.4:1 against --paper) - the hero-line ("o que se passa
     agora") failed WCAG AA even at large-text size. Darkened within the
     same rust family to ~4.6:1. */
  --clay-2:   #642818;
  /* Not --clay (#8f3a26): on this theme's tan paper that measures 2.68,
     under the unselected tabs' 4.41 - the selected tab read as the faded
     one. Same terracotta hue, taken down to 5.51 (1.25x the idle tabs). */
  --clay-active: #3f1911;
  --rata:     #8f3a26;
  background-color: #c48f6d;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='40'%3E%3Crect width='120' height='40' fill='%23c48f6d'/%3E%3Cg stroke='%238f3a26' stroke-opacity='0.30' stroke-width='2.5' fill='none'%3E%3Cpath d='M0 0H120M0 20H120M0 40H120'/%3E%3Cpath d='M0 0V20M60 0V20M120 0V20'/%3E%3Cpath d='M30 20V40M90 20V40'/%3E%3C/g%3E%3C/svg%3E");
  /* Drawn and approved, but nothing uses it yet: .highlight only exists on
     Rata and Snatch (see index.html), so this waits here for the day one of
     these tabs gains a highlight box rather than being redrawn from
     scratch then. fiadas de tijolo com juntas desencontradas. */
  --highlight-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='40'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.1' stroke-opacity='0.55'%3E%3Cpath d='M0 8H16M0 18H16M0 28H16M0 38H16'/%3E%3Cpath d='M8 0V8M3 8V18M13 8V18M8 18V28M3 28V38M13 28V38'/%3E%3C/g%3E%3C/svg%3E");
}
body.theme-mural .hero-line { color: var(--clay-2); }
/* Stronger shadow + a visible border than the other themes get - the brick
   background pattern sits closer in tone to --paper-2 than the other
   themes' patterns do to theirs, so cards need more definition to read as
   lifted surfaces instead of blending into the wall behind them. */
body.theme-mural .card {
  border-color: var(--line);
  box-shadow: 0 12px 34px rgba(61, 34, 22, 0.28);
}

html.mode-dark body.theme-mural {
  /* Same fix as furnished dark above. */
  --clay-active: #d97a52;
  --paper:    #241512;
  --paper-2:  #2e1c17;
  --paper-3:  #38241d;
  --ink:      #e8d3c4;
  --ink-soft: #a8836e;
  /* Was #4a2e22 (~1.3:1 against --paper-2, the weakest of all the themes -
     this is the one that reads as a missing border in the wild). Lightened
     within the same family to ~2.4:1. */
  --line:     #7e4f3a;
  --line-2:   #5c3b2a;
  --clay:     #c2603f;
  --clay-2:   #d97a52;
  --rata:     #c2603f;
  background-color: #180e0c;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='40'%3E%3Crect width='120' height='40' fill='%231c110e'/%3E%3Cg stroke='%23c2603f' stroke-opacity='0.22' stroke-width='2.5' fill='none'%3E%3Cpath d='M0 0H120M0 20H120M0 40H120'/%3E%3Cpath d='M0 0V20M60 0V20M120 0V20'/%3E%3Cpath d='M30 20V40M90 20V40'/%3E%3C/g%3E%3C/svg%3E");
}
html.mode-dark body.theme-mural .card {
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
}

/* Plaza: a daily-front-page mood - newsprint cream, near-black ink, a
   broadsheet masthead red - with a faint city-square skyline tile (facade
   silhouettes, a small fountain, a cobblestone ground line) as the motif,
   same illustrated-texture technique the other themes use (A Rata's paw
   prints, Best Look's lattice), just low-opacity enough that it reads as
   paper grain rather than a foreground illustration - content on top has
   to stay legible. Applied to <body> only while the Plaza tab is active
   (see applyNight() in app.js), same mechanism as every other night theme. */
body.theme-plaza {
  --paper:    #e8c97a;
  --paper-2:  #eed392;
  --paper-3:  #f3ddac;
  --ink:      #2e2411;
  --ink-soft: #6e5a2c;
  /* Was #c7a659 (~1.6:1 against --paper-2) - darkened within the same
     mustard family, same fix applied to every other theme's weak line. */
  --line:     #a68538;
  --line-2:   #d4b876;
  /* Was a blue accent (#2a4a7a/#3f679c) for newsstand/masthead contrast -
     read as out of place next to every other theme's warm, single-hue
     accent. Back to a monochrome newsprint mood: a darker, more saturated
     yellow/mustard than the paper itself, not a different hue. Was
     #7a5200 (hue 40, orange-leaning) - rotated to hue 48 at the same
     lightness/saturation to read more "yellow", less brown, without the
     contrast hit a straight lightening would cause (this color doubles as
     both a text color on pale paper and a badge/button background under
     light text, so it can't just get brighter). */
  --clay:     #7a6200;
  --clay-2:   #9c6e15;
  /* Both were left at the pre-rotation #7a5200 when --clay above changed -
     every other theme keeps --clay-active/--rata identical to --clay, so
     this diverging quietly undid the yellow-not-brown fix for both the
     tab underline and the .highlight box accent. */
  /* Same story as mural light: #7a6200 measured 3.66 on this gold paper,
     under the unselected tabs' 4.13. Same gold, down to 5.53 (1.34x). */
  --clay-active: #5a4800;
  --rata:     #7a6200;
  background-color: #e8c97a;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='170'%3E%3Cg fill='%23262220' fill-opacity='0.035'%3E%3Crect x='6' y='100' width='30' height='40'/%3E%3Cpolygon points='6,100 21,84 36,100'/%3E%3Crect x='13' y='110' width='5' height='7'/%3E%3Crect x='24' y='110' width='5' height='7'/%3E%3Crect x='54' y='76' width='26' height='64'/%3E%3Crect x='61' y='86' width='5' height='7'/%3E%3Crect x='72' y='86' width='5' height='7'/%3E%3Crect x='61' y='102' width='5' height='7'/%3E%3Crect x='72' y='102' width='5' height='7'/%3E%3Crect x='61' y='118' width='5' height='7'/%3E%3Crect x='72' y='118' width='5' height='7'/%3E%3Crect x='100' y='90' width='28' height='50'/%3E%3Cpolygon points='100,90 114,76 128,90'/%3E%3Crect x='108' y='102' width='5' height='7'/%3E%3Crect x='118' y='102' width='5' height='7'/%3E%3Crect x='192' y='96' width='30' height='44'/%3E%3Crect x='200' y='106' width='5' height='7'/%3E%3Crect x='210' y='106' width='5' height='7'/%3E%3Crect x='200' y='122' width='5' height='7'/%3E%3Crect x='210' y='122' width='5' height='7'/%3E%3C/g%3E%3Cg fill='none' stroke='%23262220' stroke-opacity='0.045' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M160 140 V128 M153 134 H167 M150 126 Q160 116 170 126'/%3E%3Cpath d='M0 152 Q6.5 148 13 152 T26 152 T39 152 T52 152 T65 152 T78 152 T91 152 T104 152 T117 152 T130 152 T143 152 T156 152 T169 152 T182 152 T195 152 T208 152 T221 152 T234 152 T247 152 T260 152'/%3E%3C/g%3E%3C/svg%3E");
  /* Bunting: a cord down the edge with pennants pointing in at the text,
     alternating in tone so a tall box reads as a run of flags rather than
     as the same arrow repeated. Taller tile than the other themes on
     purpose - fewer, larger flags was the call. */
  --highlight-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='72'%3E%3Cpath d='M3 0 V72' stroke='%23000' stroke-opacity='0.65' stroke-width='1.4'/%3E%3Cpath d='M3.9 4 L3.9 18 L15 11 Z' fill='%23000' fill-opacity='0.85'/%3E%3Cpath d='M3.9 40 L3.9 54 L15 47 Z' fill='%23000' fill-opacity='0.45'/%3E%3C/svg%3E");
  --highlight-motif-h: 72px;
  /* One house from the background above, centred in a 64px tile so the gaps
     above and below it are equal. The tile height is the spacing dial: taller
     tile, fewer and airier houses. Must stay in step with the mask-size and
     min-height on .highlight. */
  --plaza-casa: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='26'%20height='64'%3E%3Cg%20fill='%23000'%3E%3Crect%20x='18.5'%20y='15.75'%20width='3.2'%20height='6'%20fill-opacity='0.9'/%3E%3Cpolygon%20points='2,27.75%2013,15.25%2024,27.75'%20fill-opacity='0.9'/%3E%3Crect%20x='4.5'%20y='27.75'%20width='17'%20height='21'%20fill-opacity='0.45'/%3E%3Crect%20x='7.5'%20y='31.25'%20width='4.5'%20height='5.5'/%3E%3Crect%20x='14'%20y='31.25'%20width='4.5'%20height='5.5'/%3E%3Crect%20x='11'%20y='40.75'%20width='4.5'%20height='8'%20fill-opacity='0.8'/%3E%3C/g%3E%3C/svg%3E");
}
/* Every other night theme tints its own .hero-line with its accent
   (furnished/swamp -> --ot-2, rata/mural -> --clay-2) - plaza was missing
   this override, so it fell through to the generic --ink and read as
   noticeably bolder/heavier than the same line on every other tab. */
body.theme-plaza .hero-line { color: var(--clay-2); }
html.mode-dark body.theme-plaza {
  /* Same fix as furnished dark above. */
  --clay-active: #e8bb5c;
  /* Was #1f1a0e/#26200f/#2e2712 - dark enough that the gold hue read as
     plain brown-black. Lifted + more saturated so it reads as dark gold. */
  --paper:    #2b2107;
  --paper-2:  #342809;
  --paper-3:  #3e300c;
  --ink:      #f0e0ac;
  --ink-soft: #b8a06a;
  /* Was #4a3d1f (~1.4:1 against --paper-2) - lightened within the same
     mustard-gold family, same fix applied to every other theme's line. */
  --line:     #725e30;
  --line-2:   #5c4c28;
  /* Same monochrome mustard-on-gold pairing as the light variant above,
     lightened to read against the dark paper instead of darkened against it. */
  --clay:     #d9a536;
  --clay-2:   #e8bb5c;
  --rata:     #d9a536;
  background-color: #2b2107;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='170'%3E%3Cg fill='%23e8e2d3' fill-opacity='0.025'%3E%3Crect x='6' y='100' width='30' height='40'/%3E%3Cpolygon points='6,100 21,84 36,100'/%3E%3Crect x='13' y='110' width='5' height='7'/%3E%3Crect x='24' y='110' width='5' height='7'/%3E%3Crect x='54' y='76' width='26' height='64'/%3E%3Crect x='61' y='86' width='5' height='7'/%3E%3Crect x='72' y='86' width='5' height='7'/%3E%3Crect x='61' y='102' width='5' height='7'/%3E%3Crect x='72' y='102' width='5' height='7'/%3E%3Crect x='61' y='118' width='5' height='7'/%3E%3Crect x='72' y='118' width='5' height='7'/%3E%3Crect x='100' y='90' width='28' height='50'/%3E%3Cpolygon points='100,90 114,76 128,90'/%3E%3Crect x='108' y='102' width='5' height='7'/%3E%3Crect x='118' y='102' width='5' height='7'/%3E%3Crect x='192' y='96' width='30' height='44'/%3E%3Crect x='200' y='106' width='5' height='7'/%3E%3Crect x='210' y='106' width='5' height='7'/%3E%3Crect x='200' y='122' width='5' height='7'/%3E%3Crect x='210' y='122' width='5' height='7'/%3E%3C/g%3E%3Cg fill='none' stroke='%23e8e2d3' stroke-opacity='0.035' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M160 140 V128 M153 134 H167 M150 126 Q160 116 170 126'/%3E%3Cpath d='M0 152 Q6.5 148 13 152 T26 152 T39 152 T52 152 T65 152 T78 152 T91 152 T104 152 T117 152 T130 152 T143 152 T156 152 T169 152 T182 152 T195 152 T208 152 T221 152 T234 152 T247 152 T260 152'/%3E%3C/g%3E%3C/svg%3E");
}

/* ================= Memo: flash de câmara, ao acaso ao longo do dia ======= */
/* Aplicado a <body> apenas quando o separador Memo esta ativo (ver
   applyNight() em app.js), mesmo mecanismo dos outros temas noturnos. The
   one cool accent among five warm ones (teal/olive/mauve/terracotta/gold) -
   deliberate, see the design exploration this was picked from. Motif is a
   scatter of small sparks/dust catching a flash, not an illustrated scene
   like the other themes' paw-prints/bricks/skyline, since this theme's own
   idea is "a camera flash," not a place. */
body.theme-memo {
  --paper:    #11232c;
  --paper-2:  #182e39;
  --paper-3:  #1f3946;
  --ink:      #dff5fa;
  --ink-soft: #7fb8c9;
  /* Was #2c5261 (~1.7:1 against --paper-2) - same weak-border issue fixed
     on every other theme tonight, lightened to ~2.4:1. */
  --line:     #396a7d;
  --line-2:   #4a7f94;
  --clay:     #2bcdee;
  --clay-2:   #6fe0f5;
  --rata:     #2bcdee;
  background-color: #0c1a20;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='160'%3E%3Cg fill='%232bcdee' fill-opacity='0.22'%3E%3Ccircle cx='18' cy='24' r='2.2'/%3E%3Ccircle cx='60' cy='12' r='1.4'/%3E%3Ccircle cx='96' cy='40' r='1.8'/%3E%3Ccircle cx='150' cy='20' r='1.4'/%3E%3Ccircle cx='180' cy='58' r='2.2'/%3E%3Ccircle cx='34' cy='78' r='1.4'/%3E%3Ccircle cx='120' cy='84' r='2.2'/%3E%3Ccircle cx='8' cy='120' r='1.8'/%3E%3Ccircle cx='70' cy='128' r='1.4'/%3E%3Ccircle cx='108' cy='150' r='1.8'/%3E%3Ccircle cx='160' cy='118' r='1.4'/%3E%3Ccircle cx='190' cy='140' r='2.2'/%3E%3C/g%3E%3C/svg%3E");
  /* Drawn and approved, but nothing uses it yet: .highlight only exists on
     Rata and Snatch (see index.html), so this waits here for the day one of
     these tabs gains a highlight box rather than being redrawn from
     scratch then. obturador cheio e perfuração de película. */
  --highlight-motif: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='40'%3E%3Ccircle cx='8' cy='10' r='4.8' fill='%23000' fill-opacity='0.85'/%3E%3Cg fill='%23000' fill-opacity='0.85'%3E%3Crect x='1.4' y='20' width='3.4' height='3.4' rx='0.7'/%3E%3Crect x='11.2' y='20' width='3.4' height='3.4' rx='0.7'/%3E%3Crect x='1.4' y='27' width='3.4' height='3.4' rx='0.7'/%3E%3Crect x='11.2' y='27' width='3.4' height='3.4' rx='0.7'/%3E%3Crect x='1.4' y='34' width='3.4' height='3.4' rx='0.7'/%3E%3Crect x='11.2' y='34' width='3.4' height='3.4' rx='0.7'/%3E%3C/g%3E%3Cg stroke='%23000' stroke-opacity='0.3'%3E%3Cpath d='M6 18V40'/%3E%3Cpath d='M10 18V40'/%3E%3C/g%3E%3C/svg%3E");
}
body.theme-memo .hero-line { color: var(--clay-2); }
body.theme-memo .card {
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
}
html.mode-light body.theme-memo {
  /* Ivo's pick, 16 Sept 2026, from four measured options.
     This accent is drawn as the COLOUR of the section's buttons, icons and
     hero line, on the section's own paper. The old #0e90aa was a lovely
     cyan that read at 1.70:1 against the cards - the controls dissolved
     into them. #0a5464, the darker tone already here, only reached 3.86:1,
     so this was never fixable with what was in the block.
     There is no bright version that works: the paper is too close to the
     hue. Measured against BOTH surfaces, since controls sit on cards
     (#87b6c3) and the hero line on the page (#94c3d1):
       --clay    #08404e -> 5.13:1 cards, 5.93:1 page
       --clay-2  #094552 -> 4.79:1 cards, 5.54:1 page
     Retuning either means re-measuring against both. */

  /* Ivo's pick, 16 Sept 2026, from four measured options.
     The accent here is drawn as the COLOUR of this section's buttons, icons
     and hero line - on this section's own paper. The old #0e90aa was a
     lovely cyan and read at 1.70:1 against the cards, which is to say the
     controls dissolved into them. #0a5464, the darker tone already in this
     block, only reached 3.86:1, so this was never fixable by reaching for
     what was here.
     There is no bright version of this that works: the paper is too close
     to the hue, so clearing 4.5:1 required going genuinely dark. Measured
     against BOTH surfaces, because controls sit on cards (#87b6c3) and the
     hero line sits on the page (#94c3d1):
       --clay    #08404e -> 5.13:1 on cards, 5.93:1 on page
       --clay-2  #094552 -> 4.79:1 on cards, 5.54:1 on page
     Retuning either means re-measuring against both, not just one. */

  /* Was #e2eff3/#d5e6ec/#c8dde4 (L~85-88%) - noticeably lighter/more
     saturated than every other theme's light-mode paper (all sit around
     L65-77%), reading as glaringly bright/washed out by comparison even
     after a first darkening pass wasn't enough. Now in that same L~60-70%
     range within the same cyan family. ink-soft/line/clay-2 all darkened
     alongside to hold their contrast against the now-noticeably-darker
     paper (a color only tuned against the old, much lighter background
     loses contrast when the background gets this much deeper). */
  --paper:    #94c3d1;
  --paper-2:  #82b9c9;
  --paper-3:  #70afc2;
  --ink:      #152c37;
  --ink-soft: #2f4c59;
  --line:     #2d6478;
  --line-2:   #7bb0c2;
  --clay:     #08404e;
  --clay-2:   #094552;
  --rata:     #08404e;
  /* Missing here despite the html.mode-light fallback's own comment
     claiming every regular-page theme sets one - fell through to that
     stray global default (#b5624a, meant only for admin's theme-less
     Settings tab) instead of Memo's own cyan underline. */
  --clay-active: #08404e;
  /* Was #e2eff3 - the stale pre-darkening paper value, never updated when
     --paper darkened above, so the page background stayed pale while cards
     (which use --paper-2 via .card) correctly picked up the new color -
     a jarring mismatch instead of the same small "body slightly darker
     than card" offset every other theme has between its own --paper and
     this. */
  background-color: #87b6c3;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='160'%3E%3Cg fill='%230e90aa' fill-opacity='0.16'%3E%3Ccircle cx='18' cy='24' r='2.2'/%3E%3Ccircle cx='60' cy='12' r='1.4'/%3E%3Ccircle cx='96' cy='40' r='1.8'/%3E%3Ccircle cx='150' cy='20' r='1.4'/%3E%3Ccircle cx='180' cy='58' r='2.2'/%3E%3Ccircle cx='34' cy='78' r='1.4'/%3E%3Ccircle cx='120' cy='84' r='2.2'/%3E%3Ccircle cx='8' cy='120' r='1.8'/%3E%3Ccircle cx='70' cy='128' r='1.4'/%3E%3Ccircle cx='108' cy='150' r='1.8'/%3E%3Ccircle cx='160' cy='118' r='1.4'/%3E%3Ccircle cx='190' cy='140' r='2.2'/%3E%3C/g%3E%3C/svg%3E");
}
html.mode-light body.theme-memo .card {
  box-shadow: 0 12px 34px rgba(21, 44, 55, 0.14);
}

/* ------------------------------- Memo (UI) -------------------------------- */

/* Demure by request: 5 small dots instead of a detailed row-per-slot list -
   waiting is a hollow ring, open pulses solid to invite a tap, done is a
   plain filled dot, missed is filled but dim. No numbers/times on the dots
   themselves (see the aria-label for the roman-numeral name instead). */
/* The five dots span the full width of the card, evenly spaced - a fixed
   gap left them huddled at the left edge with dead space after them, which
   read as four dots and a gap rather than a five-step progress bar. */
.memo-dots { display: flex; justify-content: space-between; gap: 8px; }
.memo-shoot { margin-top: 16px; }
.memo-shoot button { width: 100%; }
/* A plain indicator, not a control - see renderMemoDots() in app.js. */
/* Both a span (the four you cannot act on) and a button (the live one), so
   the button's native chrome has to go or it would sit in a grey rounded
   rectangle of its own. */
.memo-dot {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 2px solid var(--line-2);
  background: transparent;
  padding: 0;
  cursor: default;
}
/* Read as a temperature scale rather than four unrelated colours: the two
   still-to-do states are light blue, done is the deep blue they resolve
   into, and a missed one drains to plain white - the odd one out, which is
   the point. Fixed hex rather than theme tokens: Memo owns the blue, and
   these have to mean the same thing in both light and dark mode. */
.memo-dot-waiting { background: #8fd6e8; border-color: #8fd6e8; opacity: 0.55; }
.memo-dot-uploaded { background: #0b5f77; border-color: #0b5f77; }
.memo-dot-missed { background: #fff; border-color: #fff; opacity: 0.85; }
/* Deeper than waiting, not just the same blue at full strength: side by
   side on a light background the only difference was opacity, and the one
   moment you can actually act on read as one more "still to do". Sits
   between waiting and uploaded so the three stay tellable apart - 2.4
   against waiting, 1.8 against uploaded, both of which they need since
   they line up in a row. */
button.memo-dot-open { cursor: pointer; }
.memo-dot-open {
  background: #1f8ca8;
  border-color: #1f8ca8;
  animation: memo-dot-pulse 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .memo-dot-open { animation: none; }
}
@keyframes memo-dot-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(143, 214, 232, 0.5); }
  50% { box-shadow: 0 0 0 8px rgba(143, 214, 232, 0); }
}

.memo-feed-item { margin-bottom: 18px; }
.memo-feed-item:last-child { margin-bottom: 0; }
.memo-feed-thumb {
  width: 100%;
  max-height: 420px;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: #000;
  display: block;
}
.memo-feed-meta { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.memo-feed-name { font-family: var(--serif); font-size: 1rem; color: var(--ink); }
.memo-reaction-bar { display: flex; gap: 4px; align-items: center; }
/* Mirroring is a CSS transform, never a rewrite of the stored file - see
   setUploadFlipped() in memo.js for why. */
.memo-feed-thumb.flipped { transform: scaleX(-1); }
/* Pushed to the far end: this is your own control over your own photo, not
   another reaction, and sitting it in the same row as the reactions invites
   being tapped as one. */
.memo-flip {
  margin-left: auto;
  display: flex;
  align-items: center;
  border: 1px solid var(--line);
  background: var(--paper-2);
  border-radius: 999px;
  padding: 5px 9px;
  line-height: 1;
  cursor: pointer;
  color: var(--ink-soft);
}
.memo-flip svg { width: 19px; height: 19px; display: block; }
.memo-flip.active { color: var(--rata); border-color: var(--rata); background: color-mix(in srgb, var(--rata) 15%, var(--paper-2)); }
.memo-reaction {
  display: flex;
  align-items: center;
  gap: 3px;
  border: 1px solid var(--line);
  background: var(--paper-2);
  border-radius: 999px;
  padding: 5px 9px;
  line-height: 1;
  cursor: pointer;
  color: var(--ink-soft);
}
.memo-reaction svg { width: 19px; height: 19px; flex-shrink: 0; }
.memo-reaction span { font-size: var(--text-sm); color: var(--ink-soft); font-family: var(--body); }
.memo-reaction.active { color: var(--rata); border-color: var(--rata); background: color-mix(in srgb, var(--rata) 15%, var(--paper-2)); }

/* A photo on a Wall post. Width-constrained by the card and free in height,
   so a portrait photo from a phone is not cropped to a letterbox - the whole
   point is usually what is in it. max-height keeps a very tall one from
   pushing the words it belongs to off the screen entirely. */
.plaza-post-photo {
  display: block;
  width: 100%;
  max-height: 60vh;
  object-fit: contain;
  margin: 10px 0 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-3);
}

/* A tie, said out loud in the standings. The row is not recoloured - a
   whole tinted row reads as an error, and a tie is a normal outcome. */
.tie-flag {
  font-family: var(--body);
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  color: var(--clay-2);
  font-weight: 600;
  white-space: nowrap;
}

/* Plaza feed: pinned-first, then most-recent-first, two banner styles so a
   glance tells an announcement from a poll - a plain torn front-page column
   for announcements, a boxed "magazine spread" card for polls (the
   interactive one). A pinned announcement borrows the same boxed treatment
   so it reads as prominent rather than blending into the column. */
.wall-video-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.wall-video-row:last-child { border-bottom: none; padding-bottom: 0; }
.wall-video-day { font-family: var(--serif); font-size: 1.05rem; color: var(--ink); }
/* The ratio is declared rather than discovered. preload="none" is
   deliberate (these are the heaviest thing the Wall serves, and most people
   scroll past), but it means the browser never reads the file's dimensions
   and falls back to its default 300x150 box - a portrait video sitting in a
   landscape frame until you press play. Generated recaps are always 720x1280
   (see buildDayRecapVideo), so 9:16 is a fact we already know. Capped by
   width so a full-bleed portrait video doesn't swallow the whole feed. */
.wall-video-player {
  display: block;
  width: 100%;
  max-width: min(100%, calc(68vh * 9 / 16));
  margin: 0 auto;
  aspect-ratio: 9 / 16;
  object-fit: contain;
  border-radius: var(--radius);
  background: #000;
}

.plaza-feed { display: flex; flex-direction: column; }
.plaza-post { border-top: 1px solid var(--line); padding: 20px 0; }
/* Excludes the boxed types below (announcement/poll): they carry their own
   full border/padding on all four sides, so the plain divider this zeroes
   doesn't apply to them - without the :not()s, this still won, its
   :first-child pseudo-class outranking their plain-class border/padding
   overrides, leaving whichever post landed first missing its top edge. */
.plaza-post:first-child:not(.plaza-post-announcement):not(.plaza-post-poll):not(.plaza-post-podium):not(.plaza-post-links):not(.plaza-post-video) { border-top: none; padding-top: 0; }
/* A real <h2> now (see renderPlazaPost() in app.js), not a small <span> -
   an announcement/poll post is its own self-contained "boxed" card (see
   the comment on .plaza-post above), so its type label is that card's
   title and should look like one, not a caption underneath it. Matches
   .card h2's size, and gets the same dark-mode-white treatment below. */
.plaza-post-type { font-size: 1.15rem; margin-bottom: 6px; }
html.mode-dark .plaza-post-type { color: #fff; }
.plaza-post-text {
  font-family: var(--serif);
  font-size: 1.05rem;
  color: var(--ink);
  margin: 8px 0 0;
  padding-left: 14px;
  border-left: 3px solid var(--clay);
  text-wrap: balance;
  /* A <p> collapses line breaks by default - an admin's multi-line post
     (one item per line) was rendering as a single run-on paragraph.
     pre-line keeps each \n as a real break while still collapsing any
     repeated spaces and wrapping long lines normally. */
  white-space: pre-line;
}
.plaza-post-text a {
  color: var(--clay);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.plaza-byline { font-family: var(--body); font-size: var(--text-sm); color: var(--ink-soft); margin: 10px 0 0; }

/* Admin's own post-history preview (loadPlazaPosts() in settings.js) - the
   byline (meta), question (content) and poll tally (data) used to all
   render at the same weight/color, making a poll entry read as one
   undifferentiated block. The vote count is now the one bold element per
   row (it's the actual data being scanned for), the option label stays
   plain body text, and voter names stay muted - a top divider plus
   indent separates the tally visually from the question above it. */
.admin-post-options {
  margin: 10px 0 0;
  padding: 10px 0 0 4px;
  border-top: 1px solid var(--line);
  list-style: none;
  font-family: var(--body);
  font-size: 0.92rem;
}
.admin-post-options li {
  padding: 3px 0;
  /* The address is one line, clipped. These used to be shortener links a
     few characters long; the real destinations they were replaced with run
     to 120 characters and spilled straight out of the card. Nobody reads a
     URL here - the label above it is what identifies the row, and the link
     is checked by tapping it on the Wall itself - so it is held to a line
     and cut rather than wrapped into a paragraph of its own. */
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.admin-post-options li > span:first-child { flex: none; }
.admin-post-options li > .muted {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.admin-post-options strong { font-weight: 700; margin-left: 4px; }

/* .highlight paints its own tinted ground and left edge (see .highlight
   above), so the plain text rule's border would draw a second one right
   beside it. */
/* The Wall's own announcement: same motif rail as everywhere else, so the
   padding has to clear it rather than the 20px a bare text block needed. */
.plaza-post-text.highlight { border-left: none; padding-left: 20px; font-size: 1.25rem; }
.plaza-post-announcement, .plaza-post-podium {
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  margin: 10px 0;
}
.plaza-post-announcement:first-child, .plaza-post-podium:first-child { margin-top: 0; }
/* Boxed like a poll now (was a plain unboxed "torn column" that read as
   an afterthought next to the poll's card) - pinning still upgrades the
   border to stand out further, same as polls. */
.plaza-post-announcement.pinned, .plaza-post-podium.pinned { border-color: var(--clay); }
.plaza-pinned-badge {
  display: inline-block;
  font-family: var(--body);
  font-weight: 700;
  font-size: var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: lowercase;
  color: var(--paper-3);
  background: var(--clay);
  border-radius: 999px;
  padding: 2px 8px;
  margin-bottom: 8px;
}

/* Boxed like every other post type. A links post was the one kind left
   sitting straight on the page background, so in a mixed feed it read as
   loose furniture between the cards rather than as a post of its own.
   The Memo day video was the last one still doing that - .plaza-post-video
   had no rule of its own anywhere in this file, so it fell through to the
   plain divider and sat bare between two cards, which is exactly what the
   links post used to do. Same treatment, same reason. */
.plaza-post-poll,
.plaza-post-links,
.plaza-post-video {
  background: var(--paper-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 18px;
  margin: 10px 0;
}
.plaza-post-poll.plaza-post:first-child,
.plaza-post-links.plaza-post:first-child,
.plaza-post-video.plaza-post:first-child { margin-top: 0; }
.plaza-post-links.pinned { border-color: var(--clay); }
.plaza-post-video.pinned { border-color: var(--clay); }
.plaza-post-poll .plaza-post-text { border-left: none; padding-left: 0; font-weight: 600; }
/* A poll is already boxed - pinning it upgrades the border rather than
   changing layout, same "stands out" intent as the announcement treatment. */
.plaza-post-poll.pinned { border-color: var(--clay); }

.plaza-poll-options { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
/* A links post: the same bordered row as a poll option, minus the tally
   fill, so the two list shapes read as siblings rather than two different
   ideas of what a row is. The arrow is the only affordance - these leave
   the app, and nothing else in the feed does. */
/* Whatever someone typed comes back the way they typed it - HTML would
   otherwise collapse the blank lines and runs of spaces people use to give
   a message its shape, and a dedication is exactly the kind of text where
   that shape is the point. */
.dedication-written { white-space: pre-wrap; }

/* The line above the addresses is just a line - no accent bar down its
   side. The highlight treatment belongs to announcements, where the words
   are the whole post; on a links post it made the intro shout over the
   thing it was introducing. */
.plaza-links-intro { border-left: none; padding-left: 0; }
.plaza-links {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* A row each, at reading size. It was a tight little list of small text
   inside a full-width card - the smallest thing on the Wall sitting in the
   biggest component on it - and the tap target was the width of the word.
   Now the emoji leads, the name is set in the serif at the size everything
   else is read at, and the whole row is the thing you press. */
.plaza-links li {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 2px;
  border-bottom: 1px solid var(--line-2);
}
.plaza-links li:last-child { border-bottom: none; }
/* The chevron says "this leaves the app", which every one of these does. */
.plaza-links li::after {
  content: '\203A';
  color: var(--ink-soft);
  font-size: 1.4rem;
  line-height: 1;
  flex: none;
}
/* Demure: the name, its emoji, and an underline. No border, no fill, no
   padding - a links post should read as a note with addresses under it,
   not as a stack of buttons competing with the announcement beside it. */
.plaza-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: 1;
  color: var(--ink);
  font-family: var(--serif);
  font-size: 1.22rem;
  /* No underline now that the row is the target and the chevron marks it as
     a way out - an underline as well made every row read as three
     decorations stacked on one short word. */
  text-decoration: none;
}
@media (hover: hover) { .plaza-link:hover { color: var(--clay); } }
.plaza-link-emoji { flex: none; font-size: 1.7em; line-height: 1; }
/* The name carries the underline, so what is tappable is the thing you read
   rather than the whole strip it sits in. */
.plaza-link:active { background: var(--paper-2); }

/* Name and address side by side while there is room, stacked on the
   narrowest phones - two full-width inputs per row would push the add
   button off the bottom of a short admin card. */
/* emoji / name / address. The emoji box is sized for one glyph so it cannot
   masquerade as a second name field. */
.plaza-link-row { display: flex; gap: 8px; margin-top: 8px; }
.plaza-link-row > input { min-width: 0; flex: 1 1 50%; }
/* The emoji box wraps its input so a drawn smiley can sit behind it as the
   hint - a typed placeholder would be the phone's emoji font, the one thing
   on screen this app didn't draw. The hint hides as soon as there is a
   value, the way a placeholder would. */
.plaza-link-emoji-field { position: relative; flex: 0 0 3.2em; display: block; }
.plaza-link-emoji-field > .plaza-link-emoji-input { width: 100%; text-align: center; }
.plaza-link-emoji-hint {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  transform: translate(-50%, -50%);
  color: var(--ink-soft);
  opacity: 0.45;
  pointer-events: none;
}
.plaza-link-emoji-input:not(:placeholder-shown) ~ .plaza-link-emoji-hint,
.plaza-link-emoji-field:focus-within > .plaza-link-emoji-hint { opacity: 0; }
@media (max-width: 380px) {
  .plaza-link-row { flex-direction: column; gap: 6px; }
  .plaza-link-emoji-field { flex: 0 0 auto; }
}

.plaza-poll-option {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-3);
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
}
.plaza-poll-option.voted { border-color: var(--clay); }
.plaza-poll-option.readonly { cursor: default; }
/* Picked but not yet submitted (see the "votar" button below) - dashed,
   not solid, so a staged choice reads as reversible, unlike .voted's solid
   border once a vote has actually landed and can't change. */
.plaza-poll-option.pending { border-style: dashed; border-color: var(--clay); }
.plaza-poll-option-fill {
  position: absolute;
  inset: 0 auto 0 0;
  background: color-mix(in srgb, var(--clay) 18%, transparent);
  transition: width 0.3s ease;
}
.plaza-poll-option-label, .plaza-poll-option-pct { position: relative; z-index: 1; }
/* Neither side of the row is bold. The option label briefly was (to
   outweigh the tally beside it), but these rows inherit the button rule's
   wide letter-spacing, and bold on top of that made a plain list of choices
   read as four shouting CTAs. */
.plaza-poll-option-label { font-weight: 400; }
.plaza-poll-option-pct { font-variant-numeric: tabular-nums; }
/* Right-aligned like every other form's action on the page. */
.plaza-poll-submit { margin-top: 12px; display: block; margin-left: auto; }

.theme-switch {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-3);
}
.theme-switch button {
  display: flex;
  align-items: center;
  gap: 6px;
  border: none;
  background: transparent;
  color: var(--ink-soft);
  padding: 8px 14px;
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  border-radius: 0;
}
.theme-switch button:hover { background: transparent; color: var(--ink); }
.theme-switch button.active { background: var(--ot); color: var(--paper-3); }
.theme-switch button.active:hover { background: var(--ot); color: var(--paper-3); }

/* Elementos do Best Outfit seguem o acento do tema ativo */
#tab-outfit .card { background: var(--ot-tint); }
#tab-outfit .highlight { border-left-color: var(--ot); }
#tab-outfit .vote-option::after { color: var(--ot); }
#tab-outfit .vote-option:hover { border-color: var(--ot); }
#tab-outfit .tier li:first-child::before { color: var(--ot); }
#tab-outfit input:focus, #tab-outfit textarea:focus { border-color: var(--ot); }

/* Primary CTA buttons take on whichever tab/night they belong to (--clay
   resolves per active body.theme-* already, furnished/swamp/rata/mural/
   plaza each set it - see applyNight() in app.js) instead of the generic
   ink/paper fill every other button uses. A flat fill of the raw accent
   hex fails contrast in some theme/mode combinations (checked: several
   dark-mode accents land under 3:1 against either light or dark text) -
   tinting it into --paper-2 instead and keeping --ink for the label reuses
   the same ink/paper contrast pairing already verified everywhere else in
   the app, so it's accessible by construction rather than per-theme luck.
   Explicit whitelist rather than a blanket "every button in this tab" rule -
   the furnished/swamp toggle and other already-styled buttons live in the
   same containers and would otherwise get wrongly reskinned too. */
#register-form button[type="submit"],
#confirm-entry-btn,
#confirm-vote-btn,
#suggest-form button[type="submit"],
[data-mark-done],
.plaza-poll-submit,
#chat-form button[type="submit"],
#plaza-post-form button[type="submit"],
#admin-register-form button[type="submit"],
/* "fixar" sits next to "publicar" on the same page and does the same kind
   of thing, so it belongs to the same family. On the default button style
   it was a pale block in dark mode against publicar's deep one - the two
   read as unrelated controls depending which mode you were in. */
[data-pin-post],
/* Everything below joined this family after a QA pass on a dark-mode
   phone: on the default style they invert to a near-white block, which
   is the one surface on the page carrying no theme at all - "guardar"
   sitting beside chat's deep "partilhar", and Memo's two export buttons
   floating over a dark blue card as bright white slabs. Same reasoning as
   [data-pin-post] above: buttons doing the same kind of job have to look
   related in both modes, and a white ground belongs to no theme. */
#dedication-save-btn,
#dedications-pdf-btn,
#download-photos-btn,
#publish-podium-btn,
[data-memo-download-day],
[data-memo-download-video],
[data-memo-publish-day],
[data-reveal-next] {
  /* Was a tint of --clay into --paper-2 (22%, then 30%, then 35%) with
     --ink text. Even at 35% it read as a washed-out, half-transparent
     "fake" button rather than a real one. Solid now: --clay darkened
     against black so a single white text colour clears AA on every theme's
     accent (worst case 4.9:1, Memo's cyan; most sit 8-13:1). Darkening is
     what makes one text colour possible at all - several --clay values are
     light enough (#e2a798, #2bcdee) that white on the raw accent fails. */
  background: color-mix(in srgb, var(--clay) 50%, #000);
  border-color: color-mix(in srgb, var(--clay) 50%, #000);
  color: var(--on-cta);
}
#register-form button[type="submit"]:hover,
#confirm-entry-btn:hover,
#confirm-vote-btn:hover,
#suggest-form button[type="submit"]:hover,
[data-mark-done]:hover,
.plaza-poll-submit:hover,
#chat-form button[type="submit"]:hover,
#plaza-post-form button[type="submit"]:hover,
#admin-register-form button[type="submit"]:hover,
[data-pin-post]:hover,
#dedication-save-btn:hover,
#dedications-pdf-btn:hover,
#download-photos-btn:hover,
#publish-podium-btn:hover,
[data-memo-download-day]:hover,
[data-memo-download-video]:hover,
[data-memo-publish-day]:hover,
[data-reveal-next]:hover {
  background: color-mix(in srgb, var(--clay) 42%, #000);
  border-color: color-mix(in srgb, var(--clay) 42%, #000);
  color: var(--on-cta);
}
/* The generic button:disabled { opacity: 0.45 } fades this button's own
   tinted background AND its var(--ink) text toward the same card
   background behind it, which shrinks the gap between the two much more
   than a flat 45% suggests - on a poll with nothing picked yet, "votar"
   read as barely-there instead of just "not yet available". A lighter
   fade keeps the label legible while still reading as disabled. */
#register-form button[type="submit"]:disabled,
.plaza-poll-submit:disabled,
#plaza-post-form button[type="submit"]:disabled {
  opacity: 0.7;
}

/* ----------------------------- Tutorial overlay --------------------------- */

.tutorial-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(20, 16, 12, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.tutorial-card {
  width: 100%;
  max-width: 360px;
  padding: 26px 24px;
  background: var(--paper-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: center;
}
.tutorial-dots { display: flex; justify-content: center; gap: 6px; margin-bottom: 16px; }
.tutorial-dots .dot { width: 6px; height: 6px; border-radius: 999px; background: var(--line); }
.tutorial-dots .dot.active { background: var(--clay); }
.tutorial-card h2 { font-size: 1.4rem; margin-bottom: 10px; }
.tutorial-card p { color: var(--ink-soft); line-height: 1.5; margin: 0 0 20px; }
.tutorial-actions { display: flex; align-items: center; gap: 10px; }
.tutorial-actions button { flex: 1; }
/* .ghost (skip) has smaller padding/font than the primary button by
   design elsewhere in the app - override it here so skip/next actually
   match, not just share a flex-basis. */
.tutorial-actions button.ghost { padding: 12px 20px; font-size: 0.78rem; }
/* The two read as one pair, each wearing the other's colour: skip is the
   card with the solid button's ink on it, next is that ink with the card's
   colour written on it. Swapping them like this is what makes them look
   like two halves of the same control rather than two buttons that
   happened to land side by side. */
.tutorial-actions button.ghost {
  background: transparent;
  border-color: var(--ink);
  color: var(--ink);
}
.tutorial-actions button.ghost:hover { background: transparent; color: var(--ink); }
.tutorial-actions #tutorial-next {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper-2);
}
.tutorial-actions #tutorial-next:hover {
  background: var(--ink);
  color: var(--paper-2);
  opacity: 0.88;
}

/* ------------------------------ Modal (alert/confirm) ---------------------- */
/* Same overlay/card treatment as the tutorial above - a custom stand-in for
   window.alert()/confirm(), so admin actions and error messages read as
   part of the app instead of a jarring unstyled browser dialog on top of it. */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(20, 16, 12, 0.55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.modal-card {
  width: 100%;
  /* Was 300px. At this size that is roughly forty characters a line, which
     wraps a short sentence into three and made every dialog read as a
     block. 340 is still comfortably narrow on the smallest phone (320px
     wide, minus the overlay's own padding) but gives a sentence room to be
     one or two lines rather than three. */
  max-width: 340px;
  padding: 22px 20px 18px;
  background: var(--paper-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  text-align: center;
}
.modal-message {
  color: var(--ink);
  line-height: 1.55;
  margin: 0 0 18px;
  /* Sentences arrive newline-separated from oneSentencePerLine() in
     modal.js - this is what renders them as separate lines. */
  white-space: pre-line;
  font-size: 0.92rem;
  /* Evens out the wrap within a sentence, so a line is not left carrying a
     single trailing word under a full one. Ignored where unsupported. */
  text-wrap: pretty;
}
.modal-actions { display: flex; align-items: center; justify-content: center; gap: 10px; }
.modal-actions button { flex: 1; padding: 9px 16px; font-size: 0.74rem; }
/* A single-button alert (e.g. notif on/off) doesn't need a full-width bar -
   shrink it to its content instead, like a real "OK" rather than a CTA. */
.modal-actions:has(button:only-child) button { flex: none; padding: 8px 22px; }

/* ----------------------------------- Chat ---------------------------------- */

.chat-messages {
  /* Was 360px, which on a phone left the card standing taller than a
     screenful before anyone had written anything - the send box sat below
     the fold on its own tab. The list scrolls anyway (Plaza keeps only the
     20 most recent, see chat.js), so the ceiling only decides how much of
     the card the messages may claim, not how many there can be. */
  max-height: 260px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-right: 4px;
}
.chat-msg { padding: 8px 0; border-bottom: 1px solid var(--line); }
.chat-msg:last-child { border-bottom: none; }
.chat-msg-head { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 2px; }
.chat-msg-author { font-family: var(--body); font-size: var(--text-xs); text-transform: lowercase; letter-spacing: 0.14em; font-weight: 600; color: var(--clay); }
.chat-msg-time { font-family: var(--body); font-size: var(--text-xs); color: var(--ink-soft); white-space: nowrap; }
.chat-msg-text { font-family: var(--serif); font-size: 1.05rem; word-wrap: break-word; }
.chat-empty { color: var(--ink-soft); font-weight: 400; font-size: 0.92rem; }
html.mode-light .chat-empty { color: var(--ink); }

/* ------------------- Miss Congeniality + published podium ----------------- */

/* The single crowned name, in the same script face the app uses for its
   other "this is the moment" text (hero lines, the login greeting) rather
   than the body face a plain result list would get. */
.congeniality-winner {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.35rem;
  color: var(--clay-2);
  margin: 0 0 2px;
}

/* A Snatch result published to the Wall. Reuses the feed's own post frame -
   this is a row of placings inside it, not a new kind of surface. */
.podium-night { font-family: var(--serif); font-style: italic; color: var(--ink-soft); margin: 0 0 10px; }
.podium-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.podium-row { display: flex; align-items: center; gap: 10px; }
.podium-medal { font-size: 1.2rem; flex: none; }
.podium-photo {
  width: 44px;
  height: 44px;
  flex: none;
  object-fit: cover;
  border-radius: 50%;
  border: 1px solid var(--line);
}
/* Same footprint as a real thumbnail so a photoless entry doesn't shunt the
   name and tally out of line with the rows above it. */
.podium-photo-empty { display: inline-block; background: var(--paper-3); }
.podium-name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.podium-row .votes { flex: none; color: var(--ink-soft); font-size: var(--text-xs); }
.podium-congeniality { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.podium-congeniality h3 { font-size: 0.95rem; margin: 0 0 4px; }

/* Separates the phases of one night's control card (outfit vote ->
   congeniality -> publish) without splitting them into separate cards -
   they're the same night being run, in order. */
.voting-divider { border: none; border-top: 1px solid var(--line); margin: 20px 0 16px; }


/* Two equal halves - "escolher foto" and "criar" are the two things that
   finish an entry, so they share a line rather than one hanging under the
   other. Stacks on very narrow screens. */
.form-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 12px;
}
.form-actions button { width: 100%; }
@media (max-width: 360px) { .form-actions { grid-template-columns: 1fr; } }

/* A single action that belongs to the card above it, pushed to the right
   edge like the other per-card actions rather than hanging off the left.
   Any status text shares the line, sitting just left of the button instead
   of dropping to its own row underneath. */
.actions-right {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
}
/* margin-right:auto rather than space-between on the parent: with a lone
   button (most of these rows) space-between would shove it to the left. This
   pushes the status text to the far edge only when there is one. */
.actions-right .muted { margin-right: auto; text-align: left; }
.actions-right .muted:empty { display: none; }

/* The guest Snatch night switch, loose in the page rather than inside the
   bordered banner it used to live in - see index.html. Same right-aligned
   placement, none of the frame. */
.night-switch-loose { display: flex; justify-content: flex-end; margin-bottom: 18px; }

/* The phase button ("iniciar", "fechar votação", "revelar") in both rounds
   of the Snatch card. Deliberately a lighter green than every other filled
   button on the page: it is the step you take over and over through the
   night, while "reiniciar" beside it is the one that throws work away - so
   the destructive-adjacent neighbour should be the one that looks heavier,
   not this. 8.9:1 with its own ink. */
#voting-advance-btn,
#congeniality-advance-btn {
  background: #a8cf94;
  border-color: #8fbf7a;
  color: #16280f;
}
#voting-advance-btn:hover,
#congeniality-advance-btn:hover {
  background: #9cc686;
  border-color: #86b672;
  color: #16280f;
}
#voting-advance-btn:disabled,
#congeniality-advance-btn:disabled { opacity: 0.55; }

/* The wipe switches are the only buttons on the page that destroy data, and
   each one owns its whole card - full width so they read as the weight of
   the thing they do, not as a small action tucked in a corner. */
button[id$="-wipe-btn"] { display: block; width: 100%; }
/* Its pair, directly above: filling a feature and emptying it are the two
   halves of the same job, so they read as one block rather than as a small
   secondary control floating beside a bar. Not danger-styled - this one
   only ever adds. */
button.seed {
  display: block;
  width: 100%;
  margin-bottom: 8px;
  /* Was a pale --paper-3 tile with --ink text - a label that flipped from
     dark to light with the mode, sitting directly above a danger twin whose
     label never flipped at all. The pair read as two unrelated controls,
     differently so in each mode. Both are solid on a dark ground now and
     carry the same label colour, so filling a feature with test data and
     emptying it look like the two halves of one job on every admin tab,
     the hidden ones included. */
  background: color-mix(in srgb, var(--clay) 50%, #000);
  border: 1px solid color-mix(in srgb, var(--clay) 50%, #000);
  color: var(--on-solid);
}
button.seed:hover {
  background: color-mix(in srgb, var(--clay) 42%, #000);
  border-color: color-mix(in srgb, var(--clay) 42%, #000);
  color: var(--on-solid);
  opacity: 1;
}

/* Plain round dot, no stretching: the Furnished and Swamp backgrounds are
   both patterned with diamonds, so a diamond here read as a deliberate mark
   while a dot disappears into them. The extra pixel drops it onto the same
   baseline as the background's own dots - sitting just above them is what
   gave it away. */
#votelog-secret-btn { margin-top: 29px; }

/* Stand-in for a tab with nothing in it yet - centred and quiet, so an empty
   Snatch reads as "not started" rather than as a page that failed to load. */
/* Sits inside the night card now, not loose on the page, so it needs the
   breathing room of a card's contents rather than of a whole empty tab. */
.idle-note {
  font-family: var(--serif);
  font-style: italic;
  font-size: 1.05rem;
  color: var(--ink-soft);
  text-align: center;
  margin: 18px 0 4px;
}


/* Keyboard focus, everywhere it was missing. The app is phone-first, but the
   owner runs it from an iPad with a keyboard, and VoiceOver needs the same
   cue. :focus-visible rather than :focus, so a thumb tap never draws a ring
   nobody asked for. */
button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible,
select:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 2px;
}

/* A field already draws its own border, so a hard ring floating two pixels
   outside it put two contours around one box - and on the login screen the
   accent ring is terracotta over a green photograph, which was the pair
   that looked wrong. A text field gets a soft halo in its own accent
   instead: no second edge, its own border deepens, and the glow is the
   theme's colour rather than a system-looking outline. Still unmistakably
   focused, still only on :focus-visible so a thumb tap never draws it. */
input:focus-visible,
textarea:focus-visible {
  outline: none;
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus-ring) 22%, transparent);
}

/* Only the Memo dot honoured this before, out of ten animated properties -
   and the one that matters most is the theme cross-fade on the night switch,
   which is the largest movement in the app. transitionend still fires at
   0.01ms, so theme.js's colour sync is unaffected (see the listener there). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}


/* Deliberately a strip at the bottom rather than a modal or a toast: it is
   a background condition, not an event, and it must not cover anything or
   demand a dismissal. It stays put until the connection comes back on its
   own. */
.offline-banner {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  max-width: calc(100% - 24px);
  padding: 8px 16px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--danger-fill) 60%, #000);
  color: var(--on-solid);
  border: 1px solid color-mix(in srgb, var(--danger-fill) 40%, #000);
  font-family: var(--body);
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: center;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
}


/* .brand-name is the page's h1 - every document needs one, and the wordmark
   is genuinely what this page is called. It is an h1 purely for structure;
   it must not pick up the default heading margins or size. */
h1.brand-name { margin: 0; font-size: inherit; font-weight: inherit; }

/* Where the post form lands while an existing post is being edited (see
   placePlazaForm() in settings.js). Empty and invisible the rest of the
   time - it only has a border once it is actually holding the form, so a
   row that is not being edited shows nothing at all. */
.plaza-edit-host:not(:empty) {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line-2);
}

/* While a post is being edited in place, its own row actions step aside -
   the form sitting right there has its own cancel and save, and three more
   buttons under them read as a second set of choices for the same thing. */
.user-row.editing .row-actions { display: none; }

/* The voting controls are icons now (the phase is named right above them),
   so the three-column grid holds three squares rather than three labels
   that wrapped onto two lines each on a narrow phone. */
.voting-controls button.icon-action { width: 100%; }
