/* ============================================================================
   RevelPics — slideshow, likes, sorting, guestbook and the cover editor.

   Loaded after style.css and luxe.css on the guest page, and after style.css on
   the dashboard, so the tokens and the .grid/.card/.like/.bub vocabulary are
   already defined. Nothing here redefines a colour token: theme.js writes those
   inline on <html> and per-event theming must keep working.
   ========================================================================== */

/* ============================================================ sort bar */
/* One "Sort by" menu, right-aligned above the photos, instead of a row of chips:
   three chips wrapped onto their own line on a phone and pushed the grid down. */
.galhead{
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--s-3) var(--s-4);flex-wrap:wrap;
  margin:0 0 var(--s-4);
}
.galhead h2{margin:0}
.sortbar{
  display:flex;align-items:center;gap:7px;flex-wrap:wrap;
  margin:0 0 0 auto;
}
.sortbar .lbl{
  display:inline-flex;align-items:center;
  font-size:var(--t-xs);color:var(--dim);font-weight:650;
  text-transform:uppercase;letter-spacing:.07em;margin:0;
}
/* The caret is a pseudo-element on the wrapper rather than a background image on
   the select, so it can take the theme's own colour: theme.js rewrites the tokens
   per event, and a baked-in grey would be wrong on half the palettes. */
.sortselwrap{position:relative;display:inline-flex}
.sortselwrap::after{
  content:"";position:absolute;right:.72rem;top:50%;
  width:7px;height:7px;margin-top:-5px;pointer-events:none;
  border-right:1.7px solid var(--dim);border-bottom:1.7px solid var(--dim);
  border-radius:1px;transform:rotate(45deg);
}
.sortbar select{
  appearance:none;-webkit-appearance:none;-moz-appearance:none;
  width:auto;min-width:8.75rem;
  padding:.42rem 1.9rem .42rem .78rem;
  background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius-pill);
  color:var(--ink);font-size:.78rem;font-weight:640;font-family:inherit;
  cursor:pointer;
  transition:border-color .16s cubic-bezier(.2,.7,.3,1),background .16s cubic-bezier(.2,.7,.3,1);
}
.sortbar select:hover{border-color:var(--dim)}
.sortbar select:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);
}
.chip{
  display:inline-flex;align-items:center;gap:6px;
  background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius-pill);padding:.34rem .8rem;
  font-size:.76rem;font-weight:640;color:var(--dim);
  cursor:pointer;width:auto;margin:0;box-shadow:none;
  transition:color .16s cubic-bezier(.2,.7,.3,1),border-color .16s cubic-bezier(.2,.7,.3,1),
             background .16s cubic-bezier(.2,.7,.3,1);
}
.chip:hover{color:var(--ink);border-color:var(--dim)}
.chip.on{
  background:var(--accent-soft);border-color:transparent;color:var(--accent-text);
}
.chip .n{font-variant-numeric:tabular-nums;font-weight:600;opacity:.75}

/* ============================================================ play button */
/* The slideshow was a sentence in small print ("Play the album as a slideshow,
   best on a TV or a projector"), which is a lot of reading to explain a play
   triangle. Same link, same destination, now an obvious control. */
.playbtn{
  display:inline-flex;align-items:center;gap:8px;width:auto;
  /* No bottom margin: it lives in .galhead's flex row now, beside the sort
     control, and the row's own gap does the spacing. Its padding matches the
     select's, so the two line up. */
  margin:0;padding:.42rem .95rem .42rem .8rem;
  background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius-pill);
  color:var(--ink);font-size:.78rem;font-weight:650;text-decoration:none;
  transition:border-color .16s cubic-bezier(.2,.7,.3,1),background .16s cubic-bezier(.2,.7,.3,1);
}
.playbtn:hover{border-color:var(--dim);background:var(--panel-2);text-decoration:none}
.playico{
  width:0;height:0;flex:none;
  border-left:7px solid var(--accent);
  border-top:5px solid transparent;border-bottom:5px solid transparent;
}

/* ============================================================ tile actions */
/* The heart sits INSIDE the tile, on a translucent dark pill over the photo's
   bottom-right corner. The earlier version hung it under the tile so nothing
   covered a face; the owner's call is that the in-tile placement is the more
   premium one, and the pill is deliberately dark-on-photo with a blur so it stays
   legible over any picture. The tile keeps clipping, so the pill follows the
   corner radius, and the grid goes back to a normal row gap because nothing hangs
   outside a tile any more. .hasacts is what the page adds when the album shows
   likes, so an album with likes off keeps the old spacing and the dashboard grid
   is untouched. */
/* Shared photo controls use fixed overlay colours, independent of album theme. */
:root{
  --photo-control-bg:rgba(33,26,23,.48);
  --photo-control-hover:rgba(33,26,23,.68);
  --photo-control-ink:#fff;
  --photo-control-line:rgba(255,255,255,.22);
  --photo-control-shadow:rgba(18,12,10,.3);
}
.grid.hasacts{row-gap:10px;padding-bottom:0}
.grid.hasacts .cell{overflow:hidden}
.grid.hasacts .cell img,.grid.hasacts .cell video,.grid.hasacts .cell .ph{border-radius:inherit}
/* The badge reserves exactly the row's footprint (see .cellacts below) so a long
   name truncates before it reaches the controls rather than under them. The row
   holds one pill now that the download lives in the viewer, so the reservation
   shrinks with it and names keep more of the tile. */
.grid.hasacts .cell .badge{padding:.5rem .5rem;padding-right:5.5rem;border-radius:0}
.grid.hasacts .cell .cellacts{
  position:absolute;right:5px;bottom:5px;left:auto;top:auto;transform:none;
  z-index:2;display:flex;align-items:center;gap:4px;
}
.reactionctrl{
  display:inline-flex;align-items:center;gap:0;
  background:var(--photo-control-bg);color:var(--photo-control-ink);
  border:1px solid var(--photo-control-line);border-radius:var(--radius-pill);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
}
.reactionctrl[hidden]{display:none!important}
.grid.hasacts .cell .reactionctrl .likechip,.reactionctrl .rxmore{
  display:inline-flex;align-items:center;justify-content:center;gap:4px;
  margin:0;border:0;background:transparent;color:inherit;box-shadow:none;
  width:auto;height:36px;min-height:36px;padding:0 6px;
  border-radius:var(--radius-pill);font-size:.74rem;font-weight:650;line-height:1;
  font-variant-numeric:tabular-nums;cursor:pointer;
}
.grid.hasacts .cell .reactionctrl .likechip{min-width:44px;padding-right:2px;position:relative}
.reactionctrl .rxmore{width:24px;min-width:24px;padding:0 3px;position:relative}
.reactionctrl .rxmore svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.reactionctrl .rxmore:after{content:"";position:absolute;inset:-4px 0}
.reactionctrl .rxmore:hover,.grid.hasacts .cell .reactionctrl .likechip:hover{background:var(--photo-control-hover);transform:none}
.grid.hasacts .cell .likechip .h{display:flex;font-size:16px;line-height:1}
.rxheart{display:block;width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linejoin:round}
.rxheart.filled{fill:var(--accent);stroke:var(--accent)}
.grid.hasacts .cell .likechip.n0 .n{display:none}
.lb .lb-dl{
  display:inline-flex;align-items:center;justify-content:center;flex:none;
  width:34px;height:38px;padding:0;margin:0;position:relative;
  background:var(--photo-control-bg);color:var(--photo-control-ink);
  border:1px solid var(--photo-control-line);border-radius:10px;box-shadow:none;
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);text-decoration:none;
}
/* Tappable boxes, not visible boxes. The pill keeps its 36px height (and the
   10px-radius download in the viewer its 38px), so no control grows taller over
   the photograph; the heart segment is at least 44px wide in both states, the
   width it already took as soon as a count appeared beside it. The hit areas
   reach 44px tall: the heart gets its own transparent extension and the arrow
   already had one, so the two targets in a 139-154px phone tile never overlap.
   The download is in the viewer only, where there is room for all three;
   tests_ux_polish_guest_controls.py asserts the measured boxes and that no two
   of them share a pixel. */
.grid.hasacts .cell .reactionctrl .likechip:after{
  content:"";position:absolute;inset:-4px 0;
}
.lb .lb-dl:after{content:"";position:absolute;inset:-4px}
.downloadico{display:block;width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.lb .lb-dl:hover{background:var(--photo-control-hover);text-decoration:none}
.lb .lb-dl:active{transform:scale(.96)}
.reactionctrl button:focus-visible,.lb .lb-dl:focus-visible{
  outline:2px solid var(--photo-control-ink);outline-offset:2px;
}
/* Guest chrome that sits on the page (not on a photograph) takes the theme's
   readable accent for its ring. Without this the browser's own `auto` ring is
   the only signal, and on the tinted cards it is easy to lose. */
body.guestpage .guestacts .btn:focus-visible,
body.guestpage .snaprow .linkbtn:focus-visible,
body.guestpage .galhead .playbtn:focus-visible,
body.guestpage .gbtab:focus-visible,
body.guestpage .gbcompose button:focus-visible,
body.guestpage .gbmeta button:focus-visible,
body.guestpage #gallerycard > .btn:focus-visible,
.grid .cell.failed > .tilebtn:focus-visible{
  outline:2px solid var(--accent-text);outline-offset:3px;
}
/* A failed upload is the one moment a guest must be able to hit Retry with a
   thumb; the tile is already the smallest thing on the page. */
.grid .cell.failed > .tilebtn{
  display:inline-flex;align-items:center;justify-content:center;
  min-width:44px;min-height:44px;font-size:.72rem;
}
/* Preserve the current two-type tally and total. Tallies sit just above the
   controls so they cannot push the new download button away from the arrow. */
.grid.hasacts .cell .cellacts > .rxtally{
  position:absolute;right:0;bottom:44px;display:flex;gap:5px;white-space:nowrap;
  background:transparent;padding:0;box-shadow:none;
  font-size:.72rem;font-weight:650;color:var(--photo-control-ink);
  text-shadow:0 1px 6px var(--photo-control-shadow);font-variant-numeric:tabular-nums;
}
.grid.hasacts .cell .cellacts > .rxtally:empty{display:none}
.rxpicker{
  position:fixed;z-index:260;display:flex;gap:2px;padding:7px;
  background:var(--panel);border-radius:var(--radius-pill);
  box-shadow:0 10px 26px var(--photo-control-shadow);
  transform-origin:20% 130%;animation:rxrise .16s ease-out both;
}
.rxpicker.below{transform-origin:20% -30%}
@keyframes rxrise{from{opacity:0;transform:translateY(6px) scale(.86)}to{opacity:1;transform:none}}
.rxpicker .rxopt{
  width:44px;height:44px;min-width:44px;margin:0;padding:0;
  display:grid;place-items:center;font-size:25px;line-height:1;
  border:0;background:none;box-shadow:none;cursor:pointer;border-radius:50%;
  transition:transform .12s,background .12s;
}
.rxpicker .rxopt:hover,.rxpicker .rxopt:focus-visible{background:var(--panel-2);transform:translateY(-2px) scale(1.08)}
.rxpicker .rxopt.sel{background:var(--accent-soft);box-shadow:inset 0 0 0 2px var(--accent)}
@media(prefers-reduced-motion:reduce){.rxpicker{animation:none}.rxpicker .rxopt{transition:none}.rxpicker .rxopt:hover,.rxpicker .rxopt:focus-visible{transform:none}}

/* ============================================================ hero */
/* A tall photograph still leaves room for the album identity and first action.
   Keep sizing guest-scoped so the storefront and dashboard proof stay unchanged.
   Small viewport units keep browser chrome from pushing the action offscreen;
   vh is the older-browser fallback. Below titles get their own budget because
   their identity occupies normal flow. */
/* The invisible editor metric uses the SAME sizing rules as the real guest
   hero. Its border matches the guest hero's 1px border from style.css. This
   keeps a scaled proof honest without changing the guest's responsive layout. */
:root{--guest-cover-max-width:760px}
.cover-frame-metric{position:fixed;left:-10000px;top:0;visibility:hidden;pointer-events:none;
  box-sizing:border-box;border:1px solid transparent;width:min(100vw,var(--guest-cover-max-width));height:230px}
body.guestpage #hero.hero.sz-banner,.cover-frame-metric.sz-banner{height:230px}
body.guestpage #hero.hero.sz-tall,.cover-frame-metric.sz-tall{height:min(64vh,620px);height:min(64svh,620px)}
@media (max-width:420px){
  /* Follow the narrower wrap gutter, rather than bleeding 4px past the screen. */
  body.guestpage #hero.hero{margin-inline:calc(-1 * var(--s-3))}
}
body.guestpage #hero.hero + .guestacts{margin-top:var(--s-5)}
@media (max-width:520px){
  body.guestpage #hero.hero.sz-banner,.cover-frame-metric.sz-banner{height:200px}
  body.guestpage #hero.hero.sz-tall,.cover-frame-metric.sz-tall{height:min(52vh,440px);height:min(52svh,440px)}
  body.guestpage #hero.hero.sz-tall.pos-below,.cover-frame-metric.sz-tall.pos-below{height:min(42vh,340px);height:min(42svh,340px)}
  body.guestpage #head{margin:var(--s-4) 0}
}

/* Guest legal navigation is a quiet signature, not checkout reassurance. Use
   theme-derived readable ink instead of fading the whole block with opacity.
   The storefront and host dashboard retain their existing footer treatment. */
body.guestpage .brandfoot{margin-top:var(--s-5)}
body.guestpage .brandfoot .mark-img{width:20px;height:auto}
body.guestpage .brandfoot .wordmark-img{width:104px}
body.guestpage .legal-foot{
  display:flex;justify-content:center;align-items:center;flex-wrap:wrap;
  gap:0 12px;margin-top:var(--s-4);font-size:var(--t-xs);
}
body.guestpage .legal-foot a{
  display:inline-flex;align-items:center;min-height:44px;max-width:100%;
  margin:0;color:var(--dim);font-weight:400;overflow-wrap:anywhere;
  text-decoration:underline;text-underline-offset:3px;
}
body.guestpage .legal-foot a:hover{color:var(--ink)}
body.guestpage .legal-foot a:focus-visible{
  outline:2px solid var(--accent-text);outline-offset:3px;border-radius:2px;
}

/* The photo treatment the host picks. "colour" is the photo as taken and needs no
   rule at all; "bw" is a true grayscale with a little more contrast, which is what
   makes a cover read as a printed photograph rather than a screenshot with the
   colour switched off. The cover only, never the album grid: a black and white
   cover over a colour album is the look, and desaturating everybody's snapshots
   is not. */
#hero.hero.tn-bw img{filter:grayscale(1) contrast(1.06) brightness(.99)}

/* The photo's own framing, chosen in the host's cover editor: which part of the
   photo the frame shows (object-position) and how far in it is zoomed. Both are
   custom properties the page sets inline, and both default to what this page did
   before the control existed, so an album nobody has edited renders identically.
   The zoom is a unitless factor because that is what scale() takes; it scales the
   image inside the hero, which clips, so it reads as a closer crop rather than a
   bigger tile. Scoped with the id to match luxe.css's `#hero.hero img` rule,
   which sets object-fit and outranks a bare `.hero img`. */
#hero.hero img{
  object-position:var(--cv-fx,50%) var(--cv-fy,50%);
  transform:scale(var(--cv-zoom,1));
  transform-origin:var(--cv-fx,50%) var(--cv-fy,50%);
}

/* "Below" takes the title off the photo and leaves it on the page underneath,
   which frees the photo to fade into the page instead of ending on a hard edge.
   The mask does the fading, and the hero's own background is set to the page
   colour so the transparent tail of the mask shows the page and not the
   placeholder panel the base .hero rule paints. */
#hero.hero.pos-below{background:var(--bg)}
#hero.hero.pos-below img{
  -webkit-mask-image:linear-gradient(180deg,#000 72%,rgba(0,0,0,.5) 90%,transparent 100%);
  mask-image:linear-gradient(180deg,#000 72%,rgba(0,0,0,.5) 90%,transparent 100%);
}

/* The plan gate in the dashboard. The cover editor's controls stay in the DOM and
   are hidden, rather than not rendered: the dashboard script binds every one of them
   by id, and a control that is absent from the page took the photo grid down once
   already when a variable was dropped the same way. */
.plan-hidden{display:none!important}

/* ============================================================ cover presets */
/* The hero carries --cv-scale / --cv-color / --cv-scrim inline and one class per
   preset, so a preset is a font stack plus a couple of type details and nothing
   else has to move.

   Every selector below is scoped with #hero on purpose. luxe.css styles the same
   caption as `#hero.hero .cap` (one id, two classes), and a bare `.hero .cap`
   rule loses to it no matter where it sits in the cascade - which is exactly how
   the size slider and the position control silently did nothing the first time
   round. Matching the id count and loading later is what makes them win. */
#hero.hero .cap{transition:padding .2s cubic-bezier(.2,.7,.3,1)}
#hero.hero .cap h1,#hero.hero .cap h2{
  font-size:calc(var(--t-3xl) * var(--cv-scale,1) / 100);
  color:var(--cv-color,#fff);
}
#hero.hero .cap .sub{color:var(--cv-color,#fff);opacity:.86}
#hero.hero .cap .sb{color:var(--cv-color,#fff)}

/* Tracked capitals on the serif and geometric faces, which is the editorial
   treatment: the title reads as a title on the photograph rather than as a
   heading that happens to be on top of it. Deliberately NOT applied to the
   script or handwritten faces, where uppercasing destroys the letterforms the
   host chose on purpose. */
#hero.hero .cap.cf-classic h1,
#hero.hero .cap.cf-editorial h1,
#hero.hero .cap.cf-modern h1{
  text-transform:uppercase;letter-spacing:.085em;
  font-size:calc(var(--t-3xl) * var(--cv-scale,1) / 100 * .86);
}
/* The date, as tracked micro-caps, both in the caption over a cover and in the
   header block of a page that has no cover photo. */
#head .sub .dt,#hero.hero .cap .sub .dt{
  display:inline-block;font-size:.72rem;letter-spacing:.22em;
  text-transform:uppercase;font-weight:600;
}
/* And the title follows the same rule when there is no cover to put it on.
   #head is removed from the page whenever a cover exists, so this only ever
   applies to the coverless case. */
body:not(.cf-script):not(.cf-handwritten) #head h1{
  text-transform:uppercase;letter-spacing:.085em;
}

#hero.hero .cap.cf-editorial h1{font-family:'Playfair Display',Georgia,serif;font-weight:500}
#hero.hero .cap.cf-elegant h1{font-family:'Cormorant Garamond',Georgia,serif;font-weight:500}
#hero.hero .cap.cf-script h1{
  font-family:'Great Vibes',cursive;font-weight:400;line-height:1.25;
  font-size:calc(var(--t-3xl) * var(--cv-scale,1) / 100 * 1.15);
}
#hero.hero .cap.cf-modern h1{font-family:'Jost',sans-serif;font-weight:500}
#hero.hero .cap.cf-deco h1{
  font-family:'Josefin Sans',sans-serif;font-weight:600;
  text-transform:uppercase;letter-spacing:.1em;line-height:1.3;
  font-size:calc(var(--t-3xl) * var(--cv-scale,1) / 100 * .84);
}
#hero.hero .cap.cf-handwritten h1{
  font-family:'Caveat',cursive;font-weight:600;
  font-size:calc(var(--t-3xl) * var(--cv-scale,1) / 100 * 1.2);
}

#hero.hero .cap.al-left{text-align:left}
#hero.hero .cap.al-center{text-align:center}
#hero.hero .cap.al-right{text-align:right}
#hero.hero .cap.pos-top{
  top:0;bottom:auto;
  padding:var(--s-5) var(--s-5) var(--s-6);
  background:linear-gradient(rgba(0,0,0,calc(var(--cv-scrim,66) / 100)),transparent);
}
#hero.hero .cap.pos-middle{
  top:50%;bottom:auto;transform:translateY(-50%);
  padding:var(--s-5);
  background:none;
}
#hero.hero .cap.pos-middle h1,#hero.hero .cap.pos-middle .sub,
#hero.hero .cap.pos-middle .sb{text-shadow:0 2px 22px rgba(0,0,0,.75)}
#hero.hero .cap.pos-bottom{
  bottom:0;top:auto;
  background:linear-gradient(transparent,rgba(0,0,0,calc(var(--cv-scrim,66) / 100)));
}
/* Free placement: the host dragged the title somewhere of their own choosing.
   It has to undo the left/right from the base rule above before it can take its
   own offset, and it shrinks to its text so the box does not sit across the
   whole photo and swallow the pointer. The scrim does not apply: an arbitrary
   point in the middle of a photo has no edge to fade from, so the text carries
   its own shadow instead. */
#hero.hero .cap.pos-free{
  top:var(--cv-y,50%);left:var(--cv-x,50%);right:auto;bottom:auto;
  transform:translate(-50%,-50%);
  padding:var(--s-4);background:none;text-align:center;
  max-width:min(90%,32ch);
}
#hero.hero .cap.pos-free h1,#hero.hero .cap.pos-free .sub,
#hero.hero .cap.pos-free .sb{text-shadow:0 2px 22px rgba(0,0,0,.85)}

/* Subtitle, on the cover and on a page with no cover photo. */
#head .sb{margin:.1rem 0 .45rem;font-size:var(--t-lg);color:var(--dim)}
#hero.hero .cap .sb{margin:.15rem 0 .35rem;font-size:var(--t-base);
  color:var(--cv-color,#fff);opacity:.9}

/* No cover photo: the title honours the chosen face at the page's own ink
   colour. Colour and overlay are deliberately not applied here, because a white
   title chosen for a dark photo would vanish on the page background. */
body.cf-editorial #head h1{font-family:'Playfair Display',Georgia,serif;font-weight:500}
body.cf-elegant #head h1{font-family:'Cormorant Garamond',Georgia,serif;font-weight:500}
body.cf-script #head h1{font-family:'Great Vibes',cursive}
body.cf-modern #head h1{font-family:'Jost',sans-serif;font-weight:500}
body.cf-deco #head h1{font-family:'Josefin Sans',sans-serif;
  font-weight:600;text-transform:uppercase;letter-spacing:.09em}
body.cf-handwritten #head h1{font-family:'Caveat',cursive;font-weight:600}

/* ============================================================ guestbook
   The vocabulary a guestbook entry is made of: .gbmsg > .av + .bub, with the
   entries list in .gb. These live here, not in luxe.css, because BOTH pages
   render a guestbook and luxe.css is only loaded on the guest album. The host's
   dashboard used to have the same markup with none of the styling: no avatar, no
   bubble, no flex layout, so the two looked nothing alike. The tokens are the
   shared ones from style.css, never the lux- ones. */
.gb{display:flex;flex-direction:column;gap:12px;margin-top:var(--s-3)}
/* Messages / Voice messages, the same underline-tab idea the dashboard uses,
   sized for the guest card. The count rides in the tab so the two kinds can be
   told apart at a glance without opening either. */
.gbtabs{
  display:flex;gap:26px;margin:var(--s-4) 0 0;
  border-bottom:1px solid var(--line);
}
.gbtab{
  width:auto;margin:0 0 -1px;padding:0 0 11px;background:none;box-shadow:none;
  border:0;border-bottom:2px solid transparent;border-radius:0;
  display:inline-flex;align-items:center;gap:7px;cursor:pointer;
  font-size:.72rem;font-weight:650;letter-spacing:.13em;text-transform:uppercase;
  color:var(--dim);
  transition:color .16s cubic-bezier(.2,.7,.3,1),border-color .16s cubic-bezier(.2,.7,.3,1);
}
.gbtab:hover{background:none;color:var(--ink);transform:none}
.gbtab.on{color:var(--accent-text);border-bottom-color:var(--accent)}
.gbtabn{font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:0;color:var(--dim)}
.gbtab.on .gbtabn{color:var(--accent-text);opacity:.75}
/* One card per entry: the message, then the name and the time as a footer row
   underneath it. The avatar disc and the chat-bubble tail are gone, and the name
   moved from the top of the bubble to the bottom, which is what makes a wall of
   notes read as a page of cards rather than as a transcript. The vocabulary is
   still shared with the dashboard's moderation list on purpose, so both render
   from these rules. */
.gbmsg{display:block}
.bub{
  background:var(--bg-tint);
  border-radius:16px;
  padding:.95rem 1.05rem .85rem;max-width:100%;
}
.bub .tx{font-size:.95rem;color:var(--ink);line-height:1.55}
.bub .nm{
  display:flex;align-items:center;gap:7px;margin-top:12px;
  font-size:.8rem;font-weight:680;color:var(--ink);
}
.bub .nm .ico{flex:none;color:var(--accent-text);opacity:.85}
.bub .nm span{font-weight:400;color:var(--dim);font-size:.72rem;margin-left:0}
.bub .nm span::before{content:"\00b7";margin-right:6px}

/* Voice auto-save status and own-entry deletion. */
#gbcard #gbvoiceretry[hidden]{display:none}
#gbcard .gbvoicestatus:empty{display:none}
#gbcard .gbmsg .bub{position:relative}
#gbcard .gbmsg:has(.gbvoicedelete) .bub{padding-right:44px}
#gbcard .gbvoicedelete{position:absolute;right:0;top:0;display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;min-width:44px;min-height:44px;padding:0;margin:0;border:0;border-radius:50%;background:transparent;color:var(--dim);font:inherit;font-size:22px;cursor:pointer}
#gbcard .gbvoicedelete:focus-visible{outline:2px solid currentColor;outline-offset:2px}
#gbcard .gbvoicedelete:disabled{opacity:.5;cursor:wait}
#gbcard .gbvoicedeleteerror{display:block;font-size:12px;color:var(--dim)}
#gbcard .gbvoicedeleteerror:empty{display:none}

/* the voice-note player */
.voice{
  display:flex;align-items:center;gap:11px;margin-top:9px;
  background:var(--panel);border-radius:var(--radius-pill);
  padding:6px 15px 6px 6px;width:fit-content;cursor:pointer;
  box-shadow:var(--shadow-sm);
  transition:transform .18s cubic-bezier(.2,.7,.3,1),box-shadow .18s;
}
.voice:hover{transform:translateY(-1px);box-shadow:var(--shadow)}
.voice .play{
  width:30px;height:30px;border-radius:50%;flex:none;
  display:grid;place-items:center;padding-left:2px;
  background:linear-gradient(145deg,var(--accent),var(--accent-hover));
  color:var(--accent-ink);font-size:.7rem;
  box-shadow:0 3px 9px var(--accent-soft);
}
.voice .wave{display:flex;align-items:center;gap:2px;height:18px}
.voice .wave i{
  width:2.5px;border-radius:2px;background:var(--accent);
  opacity:.5;display:block;transition:opacity .18s;
}
.voice:hover .wave i{opacity:.85}
.voice .dur{font-size:.72rem;color:var(--dim);font-variant-numeric:tabular-nums}
.voice.playing .wave i{opacity:1;animation:gbpulse .9s ease-in-out infinite}
@keyframes gbpulse{0%,100%{transform:scaleY(.55)}50%{transform:scaleY(1.15)}}

/* ============================================================ guestbook composer */
.gbcompose{margin-bottom:var(--s-5)}
/* The name a guestbook entry is signed with. One line above the box, styled like
   the textarea it sits over so it reads as part of the same composer. */
.gbcompose .gbwho{display:flex;align-items:center;gap:10px;margin-bottom:8px}
.gbcompose .gbwho span{font-size:var(--t-xs);color:var(--dim);white-space:nowrap}
.gbcompose .gbwho input{
  flex:0 1 240px;min-width:0;
  font:inherit;font-size:.88rem;
  background:var(--panel-2);color:var(--ink);
  border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:.45rem .7rem;
}
.gbcompose .gbwho input:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);
}
.gbcompose textarea{
  width:100%;min-height:86px;resize:vertical;
  font:inherit;font-size:.94rem;line-height:1.55;
  /* style.css styles the input types but not textarea, and this is the app's
     first one, so it is matched to the inputs here rather than left at the
     browser default. */
  background:var(--panel-2);color:var(--ink);
  border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:.8rem .95rem;
  transition:border-color .15s,box-shadow .15s;
}
.gbcompose textarea::placeholder{color:var(--dim);opacity:.75}
.gbcompose textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);
}
.gbcompose .row{margin-top:var(--s-3);align-items:center}
.gbcount{
  font-size:var(--t-xs);color:var(--dim);margin-left:auto;
  font-variant-numeric:tabular-nums;
}
.gbcount.over{color:var(--danger);font-weight:700}
.gbrow{display:flex;align-items:center;gap:9px;margin-top:var(--s-2)}
.gbrow .nm{font-weight:650;font-size:.85rem}
.gbrow .when{color:var(--dim);font-size:.72rem}
.gbmsg.mine .bub{background:var(--accent-soft)}
.gbmsg.ghosted{opacity:.55}
/* The player is the card's first child now, so it must not add its own top gap. */
.bub > .voice:first-child{margin-top:0}
.gbmeta{
  display:flex;align-items:center;gap:7px;flex-wrap:wrap;
  margin-top:6px;font-size:.7rem;color:var(--dim);
}
.gbmeta button{
  background:none;border:0;padding:0;width:auto;margin:0;
  color:var(--accent-text);font:inherit;font-weight:650;
  cursor:pointer;text-decoration:underline;
}
.gbmeta button.danger{color:var(--danger)}
.gbempty{color:var(--dim);font-size:var(--t-sm);margin:var(--s-3) 0 0}
.gbtag{
  font-size:.64rem;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;padding:.1rem .4rem;border-radius:6px;
  background:var(--panel);border:1px solid var(--line);color:var(--dim);
}
.gbtag.wait{color:var(--accent-text);border-color:var(--accent)}
.gbtag.hid{color:var(--danger);border-color:var(--danger)}

/* ============================================================ lightbox like */
.lb .lb-like{
  display:inline-flex;align-items:center;justify-content:center;gap:5px;
  background:transparent;color:inherit;border:0;border-radius:var(--radius-pill);
  height:44px;min-height:44px;min-width:44px;padding:0 10px;font-size:var(--t-sm);font-weight:650;
  cursor:pointer;width:auto;margin:0;box-shadow:none;font-variant-numeric:tabular-nums;
}
.lb .lb-like:hover{background:var(--photo-control-hover);transform:none}
.lb .lb-like .h{display:flex;font-size:18px;line-height:1}
.lb .rxheart{width:18px;height:18px}
.lb .rxmore{height:44px;min-height:44px;width:32px;min-width:32px}
.lb .lb-dl{width:44px;height:44px}
.lb .lb-dl[hidden],.lb .lb-like[hidden]{display:none!important}
.lb .downloadico{width:19px;height:19px}

.lb{
  /* The viewer's bar sits on near-black, not on a photograph, so its controls keep
     the original pale-glass fill: the in-tile dark glass would disappear here. */
  --photo-control-bg:rgba(255,255,255,.16);
  --photo-control-hover:rgba(255,255,255,.28);
  --photo-control-line:transparent;
}
.lb .reactionctrl{border-radius:var(--radius-pill)}
.lb .lb-dl{border-radius:10px}

/* ============================================================ admin: cover editor */
.cvedit{display:flex;flex-direction:column;gap:var(--s-4)}
.cvprev{
  position:relative;border-radius:var(--radius-sm);overflow:hidden;
  border:1px solid var(--line);background:var(--panel-2);
  aspect-ratio:16/7;display:grid;place-items:center;
}
.cvprev img{
  /* Out of the grid's intrinsic sizing: a portrait image must not retain its
     natural aspect height and overflow the shorter editing frame. */
  position:absolute;inset:0;
  width:100%;height:100%;object-fit:cover;display:block;
  /* The same framing controls the guest page uses, so the preview shows the
     crop the guests get rather than a different one. */
  object-position:var(--cv-fx,50%) var(--cv-fy,50%);
  transform:scale(var(--cv-zoom,1));
  transform-origin:var(--cv-fx,50%) var(--cv-fy,50%);
}
.cvprev .cap{
  position:absolute;left:0;right:0;
  padding:18px 20px;
}
.cvprev .cap h1{margin:0;line-height:1.1;
  font-size:calc(1.5rem * var(--cv-scale,1) / 100);
  color:var(--cv-color,#fff);text-shadow:0 1px 14px rgba(0,0,0,.5)}
.cvprev .cap{color:var(--cv-color,#fff)}
.cvprev .cap .sub{margin:.1rem 0 0;font-size:.78rem;opacity:.88}
.cvprev .cap .sb{margin:.15rem 0 .3rem;font-size:.78rem;opacity:.88}
.cvprev.pos-top .cap{top:0;bottom:auto;
  background:linear-gradient(rgba(0,0,0,calc(var(--cv-scrim,66) / 100)),transparent)}
.cvprev.pos-middle .cap{top:50%;bottom:auto;transform:translateY(-50%)}
.cvprev.pos-bottom .cap{bottom:0;top:auto;
  background:linear-gradient(transparent,rgba(0,0,0,calc(var(--cv-scrim,66) / 100)))}
.cvprev .empty{color:var(--dim);font-size:var(--t-sm);text-align:center;padding:var(--s-4)}
/* The preview echoes the host's cover height so the choice is visible here
   rather than only on the guest page after saving. Not 74vh: this sits in the
   dashboard column, so it shows the SHAPE, only taller than the banner. */
.cvprev.sz-tall{aspect-ratio:4/3}
/* The preview echoes the host's photo treatment, so picking Black and white shows
   the result here rather than only on the guest page after saving. */
.cvprev.tn-bw img{filter:grayscale(1) contrast(1.06) brightness(.99)}
/* The preview cannot show a title that sits under its own box, so in "below" it
   shows the photo alone and the row's hint says where the title went. */
.cvprev.pos-below .cap{display:none}
.cvprev.cf-editorial .cap h1{font-family:'Playfair Display',Georgia,serif}
.cvprev.cf-elegant .cap h1{font-family:'Cormorant Garamond',Georgia,serif}
.cvprev.cf-script .cap h1{font-family:'Great Vibes',cursive}
.cvprev.cf-modern .cap h1{font-family:'Jost',sans-serif}
.cvprev.cf-deco .cap h1{font-family:'Josefin Sans',sans-serif;
  text-transform:uppercase;letter-spacing:.1em;font-size:calc(1.25rem * var(--cv-scale,1) / 100)}
.cvprev.cf-handwritten .cap h1{font-family:'Caveat',cursive}
.cvprev.al-left .cap{text-align:left}
.cvprev.al-center .cap{text-align:center}
.cvprev.al-right .cap{text-align:right}
/* Dragging the title on the preview. Same idea as the guest page's pos-free,
   and touch-action:none stops a drag on a phone from scrolling the panel
   instead of moving the title. */
.cvprev{touch-action:none;cursor:grab}
.cvprev[data-drag-mode="title"]{cursor:crosshair}
.cover-framing{margin:24px 0;padding:18px;border:1px solid var(--line);border-radius:12px}
.cover-framing .cvrow{flex-wrap:wrap;gap:10px}
.cover-framing input[type="range"]{flex:1;min-width:120px}
.framing-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.framing-actions button{min-height:44px}
.cvbelowproof{padding:20px;text-align:center;color:var(--ink)}
.cvbelowproof[hidden]{display:none}
.cvbelowproof h1{font-size:28px;margin:0;line-height:1.15}
.cvbelowproof .sb{margin:8px 0 0}

.cvprev.dragging{cursor:grabbing}
.cvprev.pos-free .cap{
  left:var(--cv-x,50%);right:auto;top:var(--cv-y,50%);bottom:auto;
  transform:translate(-50%,-50%);
  background:none;text-align:center;max-width:min(88%,26ch);
}
.cvprev.pos-free .cap h1,.cvprev.pos-free .cap .sub,.cvprev.pos-free .cap .sb{
  text-shadow:0 2px 18px rgba(0,0,0,.85);
}

.fontgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px}
.fontopt{
  background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius-sm);padding:.6rem .5rem .5rem;
  cursor:pointer;width:auto;margin:0;text-align:center;box-shadow:none;
  transition:border-color .16s,background .16s;
}
.fontopt:hover{border-color:var(--dim)}
.fontopt.on{border-color:var(--accent);background:var(--accent-soft)}
.fontopt .sample{display:block;font-size:1.28rem;line-height:1.25;color:var(--ink)}
.fontopt .name{
  display:block;margin-top:.28rem;font-size:.62rem;font-weight:700;
  letter-spacing:.06em;text-transform:uppercase;color:var(--dim);
}
.fontopt.on .name{color:var(--accent-text)}
.fontopt[data-font="editorial"] .sample{font-family:'Playfair Display',Georgia,serif}
.fontopt[data-font="elegant"] .sample{font-family:'Cormorant Garamond',Georgia,serif}
.fontopt[data-font="script"] .sample{font-family:'Great Vibes',cursive;font-size:1.6rem}
.fontopt[data-font="modern"] .sample{font-family:'Jost',sans-serif}
.fontopt[data-font="deco"] .sample{font-family:'Josefin Sans',sans-serif;letter-spacing:.08em}
.fontopt[data-font="handwritten"] .sample{font-family:'Caveat',cursive;font-size:1.6rem}

.cvrow{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap}
.cvrow label{margin:0;font-size:var(--t-xs);font-weight:700;
  letter-spacing:.05em;text-transform:uppercase;color:var(--dim)}
.cvrow input[type=range]{flex:1;min-width:120px;accent-color:var(--accent)}
.cvchips{display:inline-flex;gap:4px;flex-wrap:wrap}
.cvchips .chip{padding:.22rem .6rem;font-size:.7rem}
.cvrow input[type=color]{
  width:44px;height:32px;padding:2px;border:1px solid var(--line);
  border-radius:9px;background:var(--panel);cursor:pointer;
}
.cvval{font-size:var(--t-xs);color:var(--dim);font-variant-numeric:tabular-nums;
  min-width:2.6rem;text-align:right}
.gbmod{display:flex;flex-direction:column;gap:var(--s-3)}
.gbmod .gbmsg{align-items:center}
.gbmod .bub{flex:1}

/* ============================================== demo album (guest page)
   One line at the top of the guest page, so somebody who scanned the QR on the
   site knows why the album is full of strangers' photos and that their own
   upload will not be kept. */
.demopill{
  display:flex;align-items:center;justify-content:center;gap:8px;
  margin:0 0 var(--s-4);padding:.5rem .95rem;text-align:center;
  background:var(--accent-soft);color:var(--accent-text);
  border-radius:var(--radius-pill);
  font-size:.78rem;font-weight:650;line-height:1.4;
}
.demopill b{font-weight:750}

/* ================================================ getting set up (host)
   The first run list on the dashboard. Deliberately quiet: it is a nudge, not an
   alarm, and the host can hide it for this album. */
/* The first-run strip. It is a <details> that is collapsed by default and hides
   itself once every item is done, so the Overview opens on the album's actual
   state rather than on a recap of things the server can already see. */
.ck.card{padding:0}
.ck .cksum{
  display:flex;align-items:baseline;gap:var(--s-3);padding:.85rem 1.1rem;
  cursor:pointer;list-style:none;font-size:var(--t-sm);
}
.ck .cksum::-webkit-details-marker{display:none}
.ck .cksum b{font-weight:650;margin-right:auto}
.ck .cksum::after{content:'\25BE';color:var(--dim);font-size:.72rem;line-height:1}
.ck[open] .cksum::after{content:'\25B4'}
.ck[open] .cksum{border-bottom:1px solid var(--line)}
.ck .ckwrap{padding:var(--s-4) 1.1rem}
.ckcount{font-size:var(--t-xs);color:var(--dim);font-weight:650;white-space:nowrap}
.ckbar{height:4px;background:var(--line);border-radius:99px;overflow:hidden;margin:0 0 var(--s-3)}
.ckbar i{display:block;height:100%;width:0;background:var(--accent);transition:width .3s ease}
.cklist{list-style:none;margin:0;padding:0}
.ckitem{display:flex;gap:12px;padding:11px 0;border-top:1px solid var(--line)}
.ckitem:first-child{border-top:0}
.cktick{
  flex:none;width:19px;height:19px;margin-top:2px;border-radius:50%;
  border:1.5px solid var(--line);position:relative;
  transition:background .16s cubic-bezier(.2,.7,.3,1),border-color .16s cubic-bezier(.2,.7,.3,1);
}
.ckitem.done .cktick{background:var(--accent);border-color:var(--accent)}
.ckitem.done .cktick::after{
  content:"";position:absolute;left:6px;top:2px;width:5px;height:9px;
  border:solid var(--accent-ink);border-width:0 2px 2px 0;transform:rotate(45deg);
}
.cklabel{display:inline-block;font-size:var(--t-sm);font-weight:640;color:var(--ink);text-decoration:none}
.cklabel:hover{text-decoration:underline}
.ckitem.done .cklabel{color:var(--dim)}
.ckhint{margin:2px 0 0;font-size:var(--t-xs);color:var(--dim);line-height:1.5}
.ckall{margin:var(--s-3) 0 0;font-size:var(--t-sm);font-weight:640;color:var(--ok)}
.ckhide{
  margin-top:var(--s-3);padding:0;width:auto;background:none;border:0;box-shadow:none;
  color:var(--dim);font:inherit;font-size:var(--t-xs);text-decoration:underline;cursor:pointer;
}
.ckhide:hover{color:var(--ink)}

/* ============================================================ dashboard tabs
   The eleven stacked cards became five sections behind a nav. The panes are
   only hidden once the script has added .tabs-on to the wrap, so a scripting
   failure leaves a long page rather than an empty one. */
.tabs{
  display:flex;gap:2px;flex-wrap:nowrap;overflow-x:auto;margin:var(--s-4) 0 var(--s-5);
  border-bottom:1px solid var(--line);scrollbar-width:none;
}
.tabs::-webkit-scrollbar{display:none}
.tab{
  width:auto;flex:none;margin:0;padding:.55rem .85rem;font-size:var(--t-sm);
  font-weight:650;background:none;box-shadow:none;color:var(--dim);
  border:0;border-bottom:2px solid transparent;border-radius:0;
  display:inline-flex;align-items:center;gap:6px;cursor:pointer;
}
.tab:hover{background:none;color:var(--ink);border-bottom-color:var(--line)}
.tab.on{color:var(--accent-text);border-bottom-color:var(--accent)}
.tabcount{
  min-width:18px;height:18px;padding:0 5px;border-radius:99px;font-size:.68rem;
  font-weight:700;line-height:18px;text-align:center;
  background:var(--accent);color:var(--accent-ink);
}
.tabs-on .tabpane{display:none}
.tabs-on .tabpane.on{display:block}

/* ========================================================== approval queue
   The pending photos, on their own, with the two buttons that clear a hundred of
   them. The album grid below still shows them, marked, so the host has one place
   to look and one place to act. */
.queue{border-left:3px solid var(--accent)}
.qhead{
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--s-3);flex-wrap:wrap;margin-bottom:var(--s-2);
}
.qgrid{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));
  gap:var(--s-3);margin-top:var(--s-3);
}
.qcell img{
  width:100%;aspect-ratio:1/1;object-fit:cover;display:block;
  border-radius:var(--radius-sm);background:var(--panel-2);
}
.qname{
  font-size:.7rem;color:var(--dim);margin-top:4px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.qacts{display:flex;gap:5px;margin-top:4px}
.qacts .tilebtn{font-size:.66rem;padding:.2rem .45rem}

/* ============================================================ saved toast
   Every settings control writes itself now, so it has to say so. */
.toast{
  position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:200;
  max-width:calc(100vw - 32px);text-align:center;
  background:var(--ink);color:var(--bg);font-size:var(--t-sm);font-weight:650;
  padding:.5rem 1rem;border-radius:var(--radius-pill);box-shadow:var(--shadow);
}

/* ========================================================= settings groups */
.sgrouphead{
  font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.08em;
  color:var(--dim);font-weight:700;margin-bottom:var(--s-3);
}
/* The list of switches used to carry the same inline font and colour on every
   single label. */
.check label{margin:0;color:var(--ink);font-weight:400}

/* =========================================================== cover picker
   Picking the cover from inside the cover card, so the host is not sent to the
   Photos tab and back. It replaces the old hint that pointed at a grid that, once
   the sections were tabbed, might not even have been on screen. */
.covpick{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(58px,1fr));
  gap:6px;margin-bottom:var(--s-4);
}
.covpick button{
  width:100%;padding:0;margin:0;background:none;box-shadow:none;cursor:pointer;
  border:2px solid transparent;border-radius:9px;overflow:hidden;position:relative;
}
.covpick button:hover{border-color:var(--dim)}
.covpick button.on{border-color:var(--accent)}
/* The suggested cover: dashed, so it reads as advice rather than as an already
   made choice, and it is never applied without the host tapping it. */
.covpick button.suggested{border-color:var(--accent);border-style:dashed}
.covpick button .sg{
  position:absolute;left:5px;bottom:5px;z-index:2;
  background:var(--accent);color:#fff;border-radius:999px;
  font:600 10px/1 system-ui,-apple-system,sans-serif;letter-spacing:.02em;
  padding:3px 6px;
}
.covpick img{
  width:100%;aspect-ratio:1/1;object-fit:cover;display:block;
  border-radius:7px;background:var(--panel-2);
}
.cvhint{min-width:0;text-align:left;color:var(--dim)}
/* Uploading a cover of your own, under the strip of the album's own photos. */
.covup{display:flex;align-items:center;gap:10px;margin:0 0 var(--s-4);flex-wrap:wrap}
.covup label.btn{cursor:pointer;text-decoration:none}

/* ============================================================ responsive */
@media (max-width:520px){
  .sortbar{gap:6px}
  /* The select carries its own accessible name ("Sort photos by"), so the visible
     "Sort by" label is the one thing here that can go, and dropping it is what lets
     the slideshow button sit beside the sort control on a phone instead of wrapping
     to the next line, where two album-wide controls on two lines read as an
     accident. */
  .sortbar .lbl{display:none}
  .sortbar select{min-width:7.75rem;font-size:.74rem}
  .chip{padding:.3rem .68rem;font-size:.72rem}
  .fontgrid{grid-template-columns:repeat(auto-fill,minmax(88px,1fr))}
  .qgrid{grid-template-columns:repeat(auto-fill,minmax(78px,1fr))}
}
