.creator-page {
  overflow-x: hidden;
  position: relative;
  z-index: 0;
}

/* The page's real scrollbar is owned by <html> (the root scrolling element), not <body> -
   .creator-page is a class on <body>, so styling only .creator-page::-webkit-scrollbar never
   actually touched the visible scrollbar at all. Plain html selector is fine here (no :has()
   guard needed) since this stylesheet is only ever loaded on the public creator page itself. */
html {
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.35) transparent;
  /* Reserves the scrollbar's own width as permanent gutter space rather than letting it eat into
     the content area only on pages tall enough to actually scroll - without this, a page just
     long enough to need scrolling shifts its centered content a few pixels versus a shorter one
     that doesn't, and viewport-edge-anchored elements (the corner share/menu buttons) sit flush
     against the scrollbar instead of with consistent breathing room either way. */
  scrollbar-gutter: stable;
}

html::-webkit-scrollbar {
  width: 10px;
}

html::-webkit-scrollbar-track {
  background: transparent;
}

html::-webkit-scrollbar-thumb {
  background-color: rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: padding-box;
}

.creator-corner-bar {
  position: absolute;
  top: 14px;
  left: 14px;
  right: 14px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.creator-corner-btn {
  width: 46px;
  height: 46px;
  min-width: 46px;
  min-height: 46px;
  flex-shrink: 0;
  box-sizing: border-box;
  border-radius: 50%;
  border: 0;
  display: grid;
  place-items: center;
  background: rgba(20, 20, 30, 0.35);
  color: #fff;
  cursor: pointer;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition: background-color 0.16s ease, transform 0.16s ease;
}

.creator-corner-btn i,
.creator-corner-btn svg,
.creator-corner-btn img {
  font-size: 1.1rem;
  width: 24px;
  height: 24px;
}

.creator-corner-btn:hover {
  background: rgba(20, 20, 30, 0.5);
  transform: translateY(-1px);
}

/* Entice-to-click: the same two effects .creator-verified-badge already does (see
   .creator-badge-shine-rect/.creator-badge-spin below) - a diagonal shine bar sweeping across the
   icon, plus a gentle rotate wobble, both continuous, same durations as the badge's own for real
   parity rather than an independently-invented animation. The badge builds its sweep from an SVG
   rect clipped to its own path; this icon is a raster <img> with no such path to clip to, so a
   ::after overlay shaped exactly like the icon (mask-image using that same PNG as the mask) sits
   on top of the real <img> instead - a wide diagonal-band gradient sliding via background-position
   underneath that mask, so only the icon's own silhouette ever shows the moving highlight. The
   wobble is a plain 2D rotate() (matching the badge's own, not a 3D rotateY flip) - deliberately
   not the earlier flip attempt, which passed through 90deg where a flat 2D image is being viewed
   exactly edge-on and has nothing left to render, reading as the icon randomly vanishing rather
   than a deliberate effect. */
#creatorLogoBtn {
  position: relative;
  animation: creator-badge-wobble 4.5s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  #creatorLogoBtn {
    animation: none;
  }
}

#creatorLogoBtn::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 24px;
  height: 24px;
  -webkit-mask-image: url('/assets/images/branding/icon-transparent.png');
  mask-image: url('/assets/images/branding/icon-transparent.png');
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  background: linear-gradient(115deg, transparent 40%, rgba(255, 255, 255, 0.95) 50%, transparent 60%);
  background-size: 200% 100%;
  pointer-events: none;
  animation: creator-logo-shine-sweep 2.6s ease-in-out infinite;
}

@keyframes creator-logo-shine-sweep {
  from {
    background-position: -100% 0;
  }
  to {
    background-position: 100% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  #creatorLogoBtn::after {
    animation: none;
  }
}

.creator-corner-btn img {
  width: 24px;
  height: 24px;
  object-fit: contain;
}

.creator-promo-cta {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  width: min(360px, 92%);
  text-align: center;
  z-index: 999;
  animation:
    creator-promo-jiggle 0.6s ease-in-out 1,
    creator-promo-bounce 1.8s ease-in-out infinite 0.6s;
  filter: drop-shadow(0 26px 34px rgba(0, 0, 0, 0.45));
}

@keyframes creator-promo-jiggle {
  0%, 100% { transform: translateX(-50%) rotate(0deg); }
  15% { transform: translateX(-50%) rotate(-8deg); }
  30% { transform: translateX(-50%) rotate(8deg); }
  45% { transform: translateX(-50%) rotate(-6deg); }
  60% { transform: translateX(-50%) rotate(6deg); }
  75% { transform: translateX(-50%) rotate(-3deg); }
  90% { transform: translateX(-50%) rotate(3deg); }
}

@keyframes creator-promo-bounce {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(-10px); }
}

.creator-promo-dismiss {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 0;
  background: none;
  color: #0b0f1a;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
  margin-left: 4px;
}

.creator-qr-widget {
  display: flex;
  position: fixed;
  right: 24px;
  bottom: 24px;
  z-index: 900;
  flex-direction: column;
  align-items: center;
  /* The QR graphic already has its own 2-module quiet zone baked into the SVG itself (a real
     margin scanners rely on - see creator.js), which reads as extra empty space above the actual
     QR pattern on top of this gap. A smaller gap here (was 6px) keeps the label visually grouped
     with the QR instead of looking like two separate floating pieces. */
  gap: 0px;
}

.creator-qr-label {
  /* Same ink color as the QR graphic itself, including the dark-background flip to white -
     see creator.js's pageIsDark check - flat, no shadow, matching the QR code's own flat/
     transparent look. */
  color: #0b0f1a;
  font-size: 0.92rem;
  font-weight: 700;
}

.creator-qr-widget.is-on-dark .creator-qr-label {
  color: #ffffff;
}

/* Toggled by creator.js whenever a full-screen overlay (.pub-lightbox / .pub-share-backdrop) is
   open - the widget sits behind those in z-index, so it was already dimmed by their translucent
   backdrop, but still visibly showing through read as broken. Fully hidden instead, matching how
   it's already absent on mobile rather than a fainter version of the same problem. */
.creator-qr-widget.is-hidden-for-modal {
  display: none;
}

.creator-qr-code {
  width: 168px;
  height: 168px;
}

/* "View on mobile" only makes sense when the viewer is on a larger screen. */
@media (max-width: 640px), (max-height: 640px) {
  .creator-qr-widget {
    display: none;
  }
}

.creator-promo-backdrop {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 160px;
  z-index: 998;
  pointer-events: none;
  background: linear-gradient(0deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);
}

.creator-promo-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 14px;
  border-radius: 999px;
  background: var(--button-color, #ffffff);
}

.creator-promo-pill a {
  color: var(--button-text, #0f172a);
  font-weight: 700;
  font-size: 0.94rem;
  text-decoration: none;
}

.creator-promo-cta p {
  margin-top: 10px;
  color: #fff;
  font-size: 0.86rem;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}

.creator-promo-cta strong {
  color: #fff;
}

.theme-icon-toggle {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1.5px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--ink);
  display: grid;
  place-items: center;
  font-size: 0.72rem;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(15, 23, 42, 0.12);
  transition: transform 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease;
}

.theme-icon-toggle:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 16px rgba(15, 23, 42, 0.18);
}

body[data-background] .theme-icon-toggle {
  border-color: rgba(255, 255, 255, 0.5);
  background: rgba(20, 20, 30, 0.35);
  color: #fff;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

body[data-background] .theme-icon-toggle:hover {
  background: rgba(20, 20, 30, 0.5);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.32);
}

.creator-missing {
  min-height: 60vh;
  display: grid;
  place-items: center;
  padding: 40px 16px;
}

.creator-missing-card {
  max-width: 420px;
  text-align: center;
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius-xl);
  padding: 32px 26px;
  box-shadow: var(--shadow);
}

.creator-missing-card i {
  font-size: 2rem;
  color: var(--brand-blue);
  margin-bottom: 14px;
}

.creator-missing-card h1 {
  font-family: "Bricolage Grotesque", sans-serif;
  font-size: 1.4rem;
  margin-bottom: 8px;
}

.creator-missing-card p {
  color: var(--ink-soft);
  font-size: 0.92rem;
  margin-bottom: 18px;
}

body.creator-page {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.creator-canvas {
  width: min(480px, 94%);
  margin: 24px auto;
  border-radius: 32px;
  overflow: hidden;
  position: relative;
  z-index: 0;
  /* A tight, higher-opacity contact shadow close to the edge (definition) layered under a
     softer, wider one further out (depth) - a single large soft blur alone just reads as haze. */
  box-shadow:
    0 4px 10px rgba(15, 23, 42, 0.22),
    0 24px 48px rgba(15, 23, 42, 0.38);
  padding-bottom: 30px;
}

.creator-card {
  position: relative;
  background-size: cover;
  background-repeat: no-repeat;
  margin-bottom: -30px;
  padding-bottom: 30px;
}

.creator-card.no-banner .creator-identity {
  margin-top: 0;
  padding-top: 56px;
}

/* Banner present but no avatar photo (.no-avatar, creators/index.php): with no avatar circle to
   do a shallow -46px overlap, the name/badge sit directly on the banner instead, as a true
   overlay - pulled up deeper so they land inside the blurred/gradiented lower half of the banner
   (.creator-banner::before/::after below), which exists specifically to keep overlaid text
   legible against whatever photo is behind it. Scoped to :not(.no-banner) so the no-banner-at-all
   case above stays in sole control of its own spacing. -85px (deeper than the original -74px)
   gives the name a bit more breathing room above the seam instead of sitting right on top of it. */
.creator-card:not(.no-banner).no-avatar .creator-identity {
  margin-top: -85px;
  padding-bottom: 12px;
}

/* .creator-banner-transition's blur band (further down) is tuned for the with-avatar case - its
   mask keeps it transparent for the top ~78% of its own height, which is exactly where this
   layout's name/handle text actually sits once pulled up -85px, so it does nothing useful there.
   Rather than retune that shared band (risking the working avatar case), this photo-hero variant
   gets its own stronger, taller darkening scrim plus a rounded "bottom sheet" that overlaps the
   photo - the modern equivalent of what an avatar circle would otherwise do to hide the seam. */
/* Fades to the page's own --canvas-color, not a fixed black - a hardcoded dark fade only ever
   looks seamless on a creator whose page also happens to be near-black; reaching full opacity of
   the actual canvas color by 100% means the seam is a genuine exact color match regardless of
   what theme a creator picked, the same reason a solid-black reference page looks seamless (its
   fade and its background happen to already be the same color everywhere). Only an approximation
   for 'gradient'/'image' canvas styles (no single color to match exactly there), but still closer
   than always fading to black regardless of theme. */
.creator-banner[data-banner-style="photo"]::before {
  height: 75%;
  background: linear-gradient(to bottom, transparent 0%, var(--canvas-color, #0f0f14) 100%);
}

/* margin-top/padding-top affect real layout (close the gap, add breathing room before the first
   link/block) regardless of Card color mode. The rounded corners only render as a visible
   "bottom sheet" overlapping the photo when Card color isn't 'off' (the default) - since
   .creator-card is otherwise fully transparent there and border-radius has nothing to clip -
   but they're harmless either way, so left on rather than special-cased per card mode. */
.creator-card:not(.no-banner).no-avatar {
  position: relative;
  z-index: 2;
  margin-top: -28px;
  border-radius: 24px 24px 0 0;
  padding-top: 26px;
}

/* --card-opacity (Pro "Card transparency" slider, dashboard.php) is a percentage string like
   "62%" - color-mix() fades the card color toward transparent by that amount rather than toward
   whatever's beneath it, so it works identically over a solid canvas, a gradient, or a Canvas
   Image. Defaults to 100% (fully opaque), matching every card render from before this existed. */
.creator-card[data-card-background="solid"] {
  background-color: color-mix(in srgb, var(--card-color, #ffffff) var(--card-opacity, 100%), transparent);
}

.creator-card[data-card-background="gradient"] {
  background-image: linear-gradient(
    165deg,
    color-mix(in srgb, var(--card-color, #ffffff) var(--card-opacity, 100%), transparent),
    color-mix(in srgb, var(--card-secondary-color, #0ea5e9) var(--card-opacity, 100%), transparent)
  );
}

.creator-canvas[data-canvas-background="solid"] {
  background-color: var(--canvas-color, #ff5f6d);
}

.creator-canvas[data-canvas-background="gradient"] {
  background-image: linear-gradient(165deg, var(--canvas-color, #ff5f6d), var(--canvas-secondary-color, #0ea5e9));
}

.creator-canvas[data-canvas-background="image"] {
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center; /* actual crop/zoom is set inline per-profile, same as .creator-card's image mode */
}

body[data-background="gradient"] {
  background: linear-gradient(165deg, var(--accent), var(--secondary));
}

body[data-background="solid"] {
  background: var(--accent);
}

body[data-background="midnight"] {
  background: linear-gradient(165deg, #0b0f1a, #1b2036 60%, #101425);
}

body[data-background="photo"] {
  background: linear-gradient(165deg, var(--accent), var(--secondary));
}

/* Page Background (var(--accent)/var(--secondary), set by the rules just above) is reset to none
   below the mobile breakpoint (see body[data-background-disabled="mobile"] in the media query
   below) - Canvas is the only thing that shows on mobile, same as it always has, regardless of
   which Background style is picked. data-background-disabled="mobile" is a literal string
   creators/index.php always emits (not read from the profile - there's no longer a stored
   per-account choice), kept so the media query below can target mobile specifically. */
@media (max-width: 640px), (max-height: 640px) {
  /* Deliberately NOT fighting the browser's native document scroll here (no position:fixed body,
     no dedicated inner scroll container) - that whole approach (tried and reverted) never actually
     fixed the reported top-of-page gap across a few rounds of real-device testing. Checked how
     Linktree's own profile pages handle this: plain, completely default scrolling - static body,
     visible overflow, <html> as the normal scrolling element, no scroll-lock machinery at all. The
     actual lesson to take from that isn't a specific technique, it's that fighting the browser's
     own scroll/toolbar-resize handling wasn't the fix - keeping every layer's background color
     consistent (so whatever gap the browser's native behavior reveals on its own is the same color
     as what's supposed to be there) is what matters, which is what the rules below do. */
  html[data-canvas-background="solid"],
  html[data-canvas-background="image"] {
    background: var(--canvas-color, #ff5f6d);
  }

  html[data-canvas-background="gradient"] {
    background: linear-gradient(165deg, var(--canvas-color, #ff5f6d), var(--canvas-secondary-color, #0ea5e9));
  }

  /* body.creator-page (above) centers .creator-canvas via `min-height: 100vh` +
     `justify-content: center`, kept as-is on mobile so a short profile's card keeps its natural,
     content-sized height and rounded "floating card" look, centered with backdrop above and
     below - same as desktop - instead of being stretched edge-to-edge with the corners squared
     off. +1px (not a bare 100dvh/100vh) is deliberate on top of that: some mobile browsers only
     auto-collapse their toolbar into the page's theme-color once an actual scroll gesture
     happens - if the page has zero scrollable overflow (content short enough that the viewport
     alone fits it, nothing more), there's nothing to scroll, that transition never fires, and the
     browser keeps showing its full static chrome indefinitely instead of blending in. This
     guarantees a sliver of real overflow always exists, regardless of how little content a
     creator has. Tried also forcing this via a JS scrollTo() on load to preempt the gap entirely -
     reverted: iOS Safari and Android Chrome only animate the toolbar collapse for a genuine
     touch-driven scroll, not a script-triggered one (both platforms locked this down after sites
     abused it to spoof hiding their address bar), so no offset value ever changes anything
     there. */
  body[data-background-disabled="mobile"] {
    /* Overrides the body[data-background="..."] rules above regardless of which style is picked -
       Canvas (see the html[data-canvas-background="..."] rules just above, which paint the same
       gutter color/gradient at the html level instead) is what's meant to show here, not Page
       Background. !important, not just relying on this attribute selector's own specificity -
       this exact rule has been touched by a long history of mobile rendering fixes (color-
       matching, scrollability, safe-area) and needs to keep winning regardless of what a plain
       element/class selector elsewhere in the cascade grows to include later. */
    background: none !important;
    min-height: calc(100vh + 1px) !important;
    min-height: calc(100dvh + 1px) !important;
    /* viewport-fit=cover (creators/index.php's <meta name="viewport">) tells the browser to draw
       the page edge-to-edge, under the status bar/notch/punch-hole instead of leaving a reserved
       inset gap there by default - without this, a centered card tall enough to reach the top of
       a short viewport could land its rounded corner right under that hardware cutout. env()
       resolves to 0 on devices/browsers with no inset at all, so this is a no-op there. */
    padding-top: env(safe-area-inset-top);
  }

  /* The canvas card itself expands edge-to-edge on mobile instead of floating on a backdrop -
     matches the original mobile fix (see 6ddeb1a, before the later scroll/safe-area rework this
     media query is now part of): background:none above only stops Page Background from painting
     through the gutter, it doesn't remove the gutter itself. Zero-margin/zero-radius does that,
     so there's no separate backdrop layer to color-match in the first place - just Canvas, full
     bleed. Same +1px vh/dvh pairing as body[data-background-disabled="mobile"] above so the
     canvas fills that exact height.
     display: flex + justify-content: center is what actually keeps a short profile's content
     (little to no bio, few/no blocks) centered in that now-taller box instead of stuck to the
     top - .creator-canvas is a plain block otherwise (its .creator-card/.creator-footer children
     just flow top-down), so forcing it to fill the viewport without this left a short page's
     content sitting up near the notch on mobile while desktop (canvas sized to its own content,
     centered as a whole compact unit by body.creator-page's own flex centering) still looked
     right - this makes mobile match that same look instead of just being tall and empty below. A
     page with enough content to already exceed the viewport is unaffected: centering an
     already-overflowing flex container has nothing extra to redistribute. */
  body[data-background-disabled="mobile"] .creator-canvas {
    width: 100% !important;
    min-height: calc(100vh + 1px) !important;
    min-height: calc(100dvh + 1px) !important;
    margin: 0 !important;
    border-radius: 0 !important;
    display: flex !important;
    flex-direction: column !important;
    justify-content: center !important;
  }
}

body[data-font="sora"] { font-family: "Sora", sans-serif; }
body[data-font="bricolage"] { font-family: "Bricolage Grotesque", sans-serif; }
body[data-font="poppins"] { font-family: "Poppins", sans-serif; }
body[data-font="manrope"] { font-family: "Manrope", sans-serif; }

.creator-canvas::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.3));
  pointer-events: none;
  z-index: 0;
}

.creator-canvas[data-canvas-background="solid"]::before {
  display: none;
}

/* Hidden by default (not just on mobile via a later override) - filter: blur(30px) on a
   full-viewport fixed layer is expensive to composite, and toggling a heavy layer like that to
   display:none *after* it was already given the chance to paint can lag behind the DOM/CSSOM
   state on real mobile GPUs, producing a brief full-screen blurred flash at first paint even
   though computed styles report display:none moments later. Only ever handing this layer its
   position/filter/transform inside the desktop-only media query below means mobile viewports
   never attempt that paint in the first place - nothing to race. */
.bg-blur-layer {
  display: none;
  pointer-events: none;
}

@media (min-width: 641px) and (min-height: 641px) {
  .bg-blur-layer {
    display: block;
    position: fixed;
    inset: 0;
    z-index: -1;
    background-size: cover;
    background-position: center;
    filter: blur(30px);
    transform: scale(1.15);
  }
}

/* Desktop-hidden by default - this is purely a mobile defensive backstop (see the media query
   below and creators/index.php's rendering comment for why). */
.creator-banner-blur-backdrop {
  display: none;
}

@media (max-width: 640px), (max-height: 640px) {
  /* Sized/positioned as an exact duplicate of the real .creator-banner box (same aspect-ratio,
     same background-position from the same banner_position_x/y - see creators/index.php), then
     flipped directly above the viewport so its bottom edge sits exactly at the real banner's top
     edge. Because both use background/object cover-fit at the same aspect ratio and same anchor
     point, they render the identical crop of the same photo - so whatever the browser's toolbar
     exposes right above the real banner during its collapse/expand animation is visually the same
     image continuing, not a separate hazy patch. An earlier version filled the whole viewport with
     a heavily blurred, differently-scaled copy of the banner - that read as a distinct washed-out
     block butted up against a sharp banner (an obvious seam in itself), which is why it kept
     looking like "a bar" even after the fix landed. */
  .creator-banner-blur-backdrop {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    aspect-ratio: 2.8 / 1;
    transform: translateY(-100%);
    z-index: -1;
    background-size: cover;
    background-repeat: no-repeat;
    pointer-events: none;
  }
}

.creator-banner {
  position: relative;
  /* aspect-ratio, not a fixed height - the canvas widens from 480px (mobile cap) up to 680px at
     the 900px desktop breakpoint (see below), and a fixed px height stayed at the old 480/170
     (~2.8/1) ratio only at that one mobile width, drifting to ~4/1 once the canvas widened -
     which the reposition editor and its phone-frame live preview (.phone-banner in
     dashboard.css) never knew about, since both hardcode 2.8/1. Matching that same ratio here
     with height:auto keeps what you position in the editor accurate at every width instead of
     only the narrowest one. */
  aspect-ratio: 2.8 / 1;
  height: auto;
  background-color: rgba(255, 255, 255, 0.16);
  background-repeat: no-repeat;
  overflow: hidden;
}

/* No avatar uploaded, so the banner photo automatically stands in as a taller hero instead of the
   usual thin 2.8/1 strip (see $isPhotoBanner in creators/index.php) - moderately taller, not a
   portrait poster (an earlier version used 4/5 here, which read as way too dominant on a phone
   screen). 2/1 (was 16/9) trims it further still - on a link-in-bio page the links are the actual
   point, so shaving height off the purely decorative photo means more of them are visible without
   scrolling immediately on landing. */
.creator-banner[data-banner-style="photo"] {
  aspect-ratio: 2 / 1;
}

.creator-banner-solid {
  width: 100%;
  height: 100%;
}

/* Sits behind .creator-banner-photo, always at background-size:cover so it fully fills the strip
   no matter what zoom the sharp photo on top is set to - see the render comment in
   creators/index.php for why this exists. blur()'s own soft edge would otherwise show as a
   lighter fringe right at the box's boundary, since a blurred image's edge pixels sample past
   where the sharp image ends; scaling up hides that fringe outside the visible area. */
.creator-banner-blur-fill {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  background-size: cover;
  filter: blur(28px) brightness(0.75);
  transform: scale(1.15);
}

.creator-banner-photo {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
}

/* An explicit, independent choice (Header style: Solid/Gradient in the dashboard) - not coupled
   to the page's own background style. Uses the same accent/secondary colors as everything else,
   but its own style pick, so switching the page background doesn't silently change the banner
   underneath a creator who never asked for that. */
.creator-banner-solid[data-banner-style="gradient"] {
  background: linear-gradient(150deg, var(--accent, #ff5f6d), var(--secondary, #0ea5e9));
}

.creator-banner-solid[data-banner-style="solid"] {
  background: var(--accent, #ff5f6d);
}

.creator-banner-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* A plain gradient darkening of the banner's own bottom half, replacing the old flat opacity
   fade - inherently smooth on its own (no mask needed), since it's just the banner's own pixels
   getting progressively darker, not blending with anything below it. The actual hand-off to
   .creator-card happens in .creator-banner-transition below - a plain gradient here can't bridge
   that seam by itself, since .creator-banner clips to its own (overflow: hidden) box and has no
   visibility into what's rendered past its own bottom edge. */
.creator-banner::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 50%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.45) 100%);
}

/* The actual banner -> card hand-off. Deliberately a normal-flow sibling of .creator-banner (not
   a pseudo-element confined inside it, and not absolutely positioned floating above everything) -
   the negative top/bottom margins pull its own box up over the banner's bottom edge and down
   over the card's top edge without displacing anything else (90px height - 70px - 20px = 0px net
   flow space), so its backdrop-filter genuinely straddles the seam and blurs real pixels from
   *both* surfaces together into one blended band, instead of only ever blurring the banner's own
   content the way a .creator-banner-confined blur could.
   z-index: 1 matches .creator-identity's own z-index - both are real (non-auto) values, so this
   band correctly paints above .creator-card's plain z-index: auto background (letting the bottom
   of the band actually blend the card's real color, not just the banner's), while .creator-
   identity - later in the DOM tree, so it wins position: relative's z-index tie-break - still
   paints above this band in turn, so a real avatar photo keeps rendering crisply on top of it
   exactly as it always has. */
.creator-banner-transition {
  position: relative;
  z-index: 1;
  height: 90px;
  margin-top: -70px;
  margin-bottom: -20px;
  pointer-events: none;
}

.creator-banner-transition::before {
  content: "";
  position: absolute;
  inset: 0;
  -webkit-backdrop-filter: blur(22px);
  backdrop-filter: blur(22px);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 78%, #000 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000 78%, #000 100%);
}

.creator-banner-transition::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 100%);
}

.creator-identity {
  position: relative;
  z-index: 1;
  padding: 0 24px;
  margin-top: -46px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.creator-identity::before {
  content: "";
  position: absolute;
  top: -58px;
  right: 0;
  bottom: -16px;
  left: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.08) 34%,
    rgba(255, 255, 255, 0.02) 72%,
    rgba(255, 255, 255, 0) 100%
  );
}

/* Wraps .creator-avatar (which clips to a circle via overflow: hidden) plus the verified badge,
   which needs to overlap that circle's edge - positioned here instead, on an unclipped ancestor,
   since a child of overflow:hidden can never render outside its parent's bounds. */
.creator-avatar-wrap {
  position: relative;
  display: inline-block;
  margin-bottom: 12px;
}

.creator-avatar {
  width: 92px;
  height: 92px;
  border-radius: 50%;
  border-style: solid;
  border-width: var(--avatar-border-width, 4px);
  border-color: var(--avatar-border-color, #fff);
  /* Not white - the default border color is also white, so a photo-less avatar (always the
     case on free tier, which can't upload one) would blend straight into its own border with
     no visible edge between them. Matches the dashboard live preview's fallback (.phone-avatar). */
  background: #e2e8f0;
  overflow: hidden;
  box-shadow: 0 12px 26px rgba(0, 0, 0, 0.25);
  display: grid;
  place-items: center;
}

.creator-avatar.is-square {
  border-radius: 18px;
}

.creator-avatar-photo {
  width: 100%;
  height: 100%;
  background-repeat: no-repeat;
}

.creator-avatar-fallback {
  font-family: "Bricolage Grotesque", sans-serif;
  font-weight: 800;
  font-size: 1.8rem;
  color: #0f172a;
}

.pub-share-avatar-fallback {
  font-family: "Bricolage Grotesque", sans-serif;
  font-weight: 800;
  font-size: 1.4rem;
  color: #0f172a;
}

.creator-avatar-fallback.is-branded,
.pub-share-avatar-fallback.is-branded {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  /* The mark itself is solid white (same asset used in the QR code's center plate, which gets
     this same accent-color backing for contrast) - shown directly on the avatar's white circle
     it's nearly invisible, so this fallback state needs its own colored plate behind it. */
  background: linear-gradient(135deg, var(--accent, #ff5f6d), var(--secondary, #0ea5e9));
}

.creator-avatar-fallback.is-branded img,
.pub-share-avatar-fallback.is-branded img {
  width: 55%;
  height: 55%;
  object-fit: contain;
}

.creator-name.is-over-light,
.creator-bio.is-over-light,
.creator-slug.is-over-light,
.pub-link-stack-heading.is-over-light {
  /* A soft blurred halo reads as a natural shadow, unlike a hard 4-direction pixel outline -
     same purpose (keep white text legible over a bright patch of a background image), less
     "sticker outline" looking. */
  text-shadow:
    0 0 4px rgba(0, 0, 0, 0.85),
    0 0 10px rgba(0, 0, 0, 0.6),
    0 1px 3px rgba(0, 0, 0, 0.5);
}

/* Name/bio/slug all share this exact baseline shadow (previously bio-only, with name using a
   near-duplicate premium-only value and slug having none at all) - one consistent treatment
   across the whole identity block now, regardless of tier. Font-size is what carries the
   hierarchy: name (1.5rem/800) reads as the biggest and most prominent, bio and slug both
   smaller and lighter-weight beneath it. */
.creator-name,
.creator-bio,
.creator-slug {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85), 0 2px 10px rgba(0, 0, 0, 0.5);
}

.creator-name {
  color: var(--name-color, #fff);
  font-weight: 800;
  font-size: 1.5rem;
  letter-spacing: -0.01em;
  /* A baseline gap that holds up on its own - .creator-slug/.creator-bio add their own margins
     on top of this when they render, but branding-hidden Pro accounts with no bio skip both of
     those entirely, and 2px alone left the name sitting almost flush against the first block. */
  margin-bottom: 12px;
}

/* creators/index.php only adds this when nothing else (slug, bio, location) renders directly
   under the name - tightens the gap to the first block for that specific case instead of
   touching the shared 12px above, which .creator-slug/.creator-bio still rely on. */
.creator-name-standalone {
  /* Negative, not a small positive value - .creator-blocks' own 2px margin-top (shared, every
     case) adds on top of this regardless, so this has to cancel most of it to land on the full
     1px gap to the first block. */
  margin-bottom: -1px;
}

.creator-slug {
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.85rem;
  font-weight: 600;
  /* Negative to cancel most of .creator-name's own margin-bottom above (that 12px exists as a
     standalone baseline gap for when this element isn't rendered at all - stacked underneath it,
     the two read as needlessly far apart for what's really one identity block). */
  margin-top: -8px;
  margin-bottom: 22px;
}

.creator-bio {
  color: var(--name-color, rgba(255, 255, 255, 0.94));
  font-size: 0.92rem;
  line-height: 1.55;
  max-width: 38ch;
  margin-bottom: 16px;
  margin-left: auto;
  margin-right: auto;
}

/* Only wraps the bio when bio_position is 'below' (see creators/index.php) - the 'above' case
   renders inside .creator-identity and gets its padding/centering from that container instead. */
.creator-bio-standalone {
  position: relative;
  z-index: 1;
  padding: 4px 20px 0;
  text-align: center;
}

.creator-location {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0 16px;
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: var(--name-color, rgba(255, 255, 255, 0.94));
  font-size: 0.82rem;
  font-weight: 600;
}

.creator-location i { font-size: 0.78rem; opacity: 0.85; }

/* Master switch (Pro-only "Text shadow" toggle, dashboard.php) for the shared baseline shadow
   above plus the .is-over-light adaptive boost - !important because .is-over-light's higher
   selector specificity would otherwise win over a plain body[data-text-shadow="0"] rule
   regardless of source order. */
body[data-text-shadow="0"] .creator-name,
body[data-text-shadow="0"] .creator-bio,
body[data-text-shadow="0"] .creator-slug,
body[data-text-shadow="0"] .pub-link-stack-heading {
  text-shadow: none !important;
}

/* The corner logo/share buttons have no literal text-shadow, but their dark glassy backing plate
   reads the same way visually - folded into the same master switch. */
body[data-text-shadow="0"] .creator-corner-btn {
  background: none !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

.creator-blocks {
  position: relative;
  z-index: 1;
  padding: 0 14px;
  display: grid;
  gap: 12px;
  /* Small on top of whatever's already above (.creator-bio's own margin-bottom: 16px, or
     .creator-slug's 22px when there's no bio) - just enough that they're never flush/touching
     without stacking into a big gap. */
  margin-top: 2px;
  margin-bottom: 10px;
}

/* A grid item's default min-width is auto, not 0 - it won't shrink below its own content's
   preferred size no matter how narrow the track actually is. The gallery carousel's track
   (flex-shrink: 0, width: max-content) is exactly that kind of content: overflow: hidden on its
   own wrapper only clips what gets painted, it does nothing to stop that preferred width from
   bubbling up through .pub-gallery-block into this shared grid track - which then stretches
   every block sharing it (a music card, a link chip, anything) out past the card's edge right
   along with it, not just the gallery itself. Resetting every block to min-width: 0 here is what
   actually makes overflow: hidden further down take effect, instead of never being reached
   because the track was already forced wide enough that nothing needed to be clipped. */
.creator-blocks > * {
  min-width: 0;
}

.pub-block-title-row {
  margin-top: 2px;
}

.pub-block-title {
  color: rgba(255, 255, 255, 0.92);
  font-weight: 700;
  font-size: 0.9rem;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.85), 0 2px 8px rgba(0, 0, 0, 0.5);
}

.pub-map {
  position: relative;
  width: 100%;
  height: 220px;
  border-radius: 18px;
  overflow: hidden;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.16);
}

.pub-map-directions-btn {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--button-color, #ffffff);
  color: var(--button-text, #0f172a);
  font-weight: 700;
  font-size: 0.82rem;
  text-decoration: none;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.pub-map-directions-btn:hover {
  transform: translateY(-2px);
}

.pub-map-address-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  padding: 10px 12px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.08);
}

.pub-map-address-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.82rem;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

.pub-map-copy-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.9);
  cursor: pointer;
  font-size: 0.82rem;
}

.pub-map-copy-btn:hover {
  background: rgba(255, 255, 255, 0.24);
}

.pub-hold-hint {
  text-align: center;
  font-weight: 800;
  letter-spacing: 0.04em;
  font-size: 0.78rem;
  color: rgba(255, 255, 255, 0.94);
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
  margin: 2px 0 -2px;
}

.pub-link-card-wrap {
  position: relative;
}

.pub-link-card {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  /* Unconditional, not just on .pub-link-card-no-icon - keeps a no-icon card's height matching
     every other card's (previously determined solely by .pub-link-icon's own 34px, so removing
     the icon silently shrunk the row instead of just leaving more room for centered text).
     66px = .pub-link-icon's 34px + this rule's own 14px top/bottom padding (28px) + the 3D
     card-shadow style's 2px top/bottom border (4px) when that's also on - box-sizing: border-box
     (global) means min-height already needs to account for all three, not just the icon alone. */
  min-height: 66px;
  background: var(--button-color, #ffffff);
  padding: 14px 40px 14px 16px;
  border-radius: 16px;
  text-decoration: none;
  color: var(--button-text, #0f172a);
  font-weight: 700;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.16);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.pub-link-share-trigger {
  position: absolute;
  top: 50%;
  right: 16px;
  transform: translateY(-50%);
  z-index: 1;
  /* 24px is the minimum touch-target size Lighthouse/WCAG 2.5.8 checks for - the icon itself
     (.pub-link-share-trigger svg below) stays a slim 4x16px, place-items: center just gives it
     a bigger invisible hit area to sit in. */
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--button-text, #94a3b8);
  opacity: 0.6;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.pub-link-share-trigger:hover {
  opacity: 1;
}

.pub-link-share-trigger svg {
  width: 4px;
  height: 16px;
}

.pub-link-card:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 28px rgba(0, 0, 0, 0.22);
}

/* Premium "3D popup" card style: a solid, unblurred offset shadow instead of a soft glow -
   reads as a card sitting on top of a hard block behind it. Pressing/hovering closes the gap,
   like the card is being pushed down into its own shadow. Grid layout only, not Icon Row - both
   share .pub-link-chip, but a hard offset border on a small 46px circular icon (Icon Row) reads
   cramped/broken, and the social icons row (.pub-social-footer, also .pub-link-chip) is excluded
   entirely on purpose regardless of this setting. Also applies to Featured links and the
   Countdown block - both are card-shaped link surfaces same as a plain link card, just with an
   image/gradient background instead of a flat one, so they get the same treatment. Each override
   here beats that block's own unconditional :hover on specificity alone (an extra attribute
   selector), regardless of source order, same as it already did for .pub-link-card. */
body[data-card-shadow="1"] .pub-link-card,
body[data-card-shadow="1"] .pub-link-row--grid .pub-link-chip,
body[data-card-shadow="1"] .pub-link-featured,
body[data-card-shadow="1"] .pub-countdown {
  border: 2px solid var(--card-shadow-color, var(--shadow-color, #0f172a));
  box-shadow: 5px 5px 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

body[data-card-shadow="1"] .pub-link-card:hover,
body[data-card-shadow="1"] .pub-link-row--grid .pub-link-chip:hover,
body[data-card-shadow="1"] .pub-link-featured:hover,
body[data-card-shadow="1"] a.pub-countdown:hover {
  transform: translate(2px, 2px);
  box-shadow: 3px 3px 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
}

body[data-card-shadow="1"] .pub-link-card:active,
body[data-card-shadow="1"] .pub-link-row--grid .pub-link-chip:active,
body[data-card-shadow="1"] .pub-link-featured:active,
body[data-card-shadow="1"] a.pub-countdown:active {
  transform: translate(5px, 5px);
  box-shadow: 0 0 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
}

body[data-card-shadow="1"] .pub-gallery-block {
  border: 2px solid var(--card-shadow-color, var(--shadow-color, #0f172a));
  box-shadow: 5px 5px 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
}

/* .pub-music-card is specifically the uploaded-audio-file block (has an <audio> element and its
   own seek bar) - a linked/embedded streaming service card, if this app ever adds one, would be
   a different class and wouldn't pick this up. */
body[data-card-shadow="1"] .pub-music-card {
  border: 2px solid var(--card-shadow-color, var(--shadow-color, #0f172a));
  box-shadow: 5px 5px 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
}

.pub-link-icon {
  width: 34px;
  height: 34px;
  border-radius: 10px;
  display: grid;
  place-items: center;
  background: #f1f5f9;
  color: #475569;
  flex: 0 0 auto;
  overflow: hidden;
}

.pub-link-icon img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pub-link-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Optional second line under the title (card variant only, data.subtitle) - wraps both lines so
   they can stack while still behaving as the one flex item .pub-link-title alone used to be. */
.pub-link-title-group {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  gap: 1px;
}

.pub-link-subtitle {
  font-size: 0.78em;
  font-weight: 500;
  opacity: 0.7;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Opt-in per card (data.hide_icon) - .pub-link-icon simply isn't rendered at all for these, so
   .pub-link-title/.pub-link-title-group naturally expand to fill the freed-up space on their own
   with no extra rule needed here. */
.pub-link-card-no-icon {
  gap: 0;
}

/* Opt-in per card (data.center_text) - centers the label (and subtitle, if any) within whatever
   width .pub-link-title/.pub-link-title-group already occupy. With an icon still showing that's
   the space to its right, not the full card - pairs with .pub-link-card-no-icon above for a
   label truly centered across the whole card. */
.pub-link-card-center-text .pub-link-title,
.pub-link-card-center-text .pub-link-subtitle {
  text-align: center;
}

.pub-link-card-center-text .pub-link-title-group {
  align-items: center;
}

/* BLOCK_TYPE_LINK_STACK - a titled stack of plain-text link buttons, each with an optional
   thumbnail and subtitle. Deliberately its own class tree (not a reuse of .pub-link-card), since
   it never has a service icon at all, unlike a default .pub-link-card which renders one. */
.pub-link-stack {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.pub-link-stack-heading {
  margin: 0;
  text-align: center;
  font-size: 1rem;
  font-weight: 800;
  /* Same customizable button-text color the buttons below it use, not a fixed color - a creator
     who tunes their button text color against their own background/button color choice expects
     this heading (visually "belongs to" the button group under it) to follow that choice too. */
  color: var(--button-text, #0f172a);
}

.pub-link-stack-items {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.pub-link-stack-item-wrap {
  position: relative;
}

/* A fixed 3-column track (thumb slot / text / balancing spacer, the same width as the thumb
   slot and reserved for the share trigger below) keeps the title/subtitle visually centered on
   the row's true center regardless of whether this particular button has a thumbnail - a plain
   flex row would instead center text only in the leftover space next to the image, which drifts
   off-center exactly when a thumbnail is present. */
.pub-link-stack-item {
  display: grid;
  grid-template-columns: 44px 1fr 44px;
  align-items: center;
  gap: 12px;
  background: var(--button-color, #ffffff);
  color: var(--button-text, #0f172a);
  padding: 12px 16px;
  border-radius: 16px;
  text-decoration: none;
  font-weight: 700;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.16);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.pub-link-stack-item:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 28px rgba(0, 0, 0, 0.22);
}

.pub-link-stack-item-thumb {
  grid-column: 1;
  width: 44px;
  height: 44px;
  border-radius: 10px;
  overflow: hidden;
}

.pub-link-stack-item-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pub-link-stack-item-text {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  min-width: 0;
  text-align: center;
}

.pub-link-stack-item-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pub-link-stack-item-subtitle {
  font-size: 0.78em;
  font-weight: 500;
  opacity: 0.7;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

body[data-button="pill"] .pub-link-stack-item {
  border-radius: 999px;
}

body[data-button="square"] .pub-link-stack-item {
  border-radius: 4px;
}

body[data-card-shadow="1"] .pub-link-stack-item {
  border: 2px solid var(--card-shadow-color, var(--shadow-color, #0f172a));
  box-shadow: 5px 5px 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

body[data-card-shadow="1"] .pub-link-stack-item:hover {
  transform: translate(2px, 2px);
  box-shadow: 3px 3px 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
}

body[data-card-shadow="1"] .pub-link-stack-item:active {
  transform: translate(5px, 5px);
  box-shadow: 0 0 0 0 var(--card-shadow-color, var(--shadow-color, #0f172a));
}

/* Countdown block: manually-set target date/time, ticked client-side (assets/js/creator.js reads
   data-countdown-target). Renders as either <a> (a url was set) or <div> (informational only) -
   same visual card either way. Background is the uploaded thumbnail (inline style) when there is
   one, else falls back to the same accent/secondary gradient other card-less blocks already use. */
.pub-countdown {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 20px 16px;
  border-radius: 18px;
  background: linear-gradient(135deg, var(--accent, #ff5f6d), var(--secondary, #0ea5e9));
  background-size: cover;
  background-position: center;
  text-decoration: none;
  color: #fff;
  text-align: center;
  overflow: hidden;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

a.pub-countdown:hover {
  transform: translateY(-3px);
  box-shadow: 0 16px 28px rgba(0, 0, 0, 0.26);
}

.pub-countdown-scrim {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: rgba(10, 10, 20, 0.5);
  pointer-events: none;
}

.pub-countdown-type-icon,
.pub-countdown-platform-icon {
  position: absolute;
  top: 12px;
  z-index: 1;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.32);
  color: #fff;
  font-size: 0.85rem;
}

.pub-countdown-type-icon {
  left: 12px;
}

.pub-countdown-platform-icon {
  right: 12px;
}

.pub-countdown-platform-icon svg {
  width: 15px;
  height: 15px;
}

.pub-countdown-heading {
  position: relative;
  z-index: 1;
  font-weight: 800;
  /* Clearly smaller than .pub-countdown-value's 2.3rem, not just technically - a long title
     (e.g. "Grand Theft Auto VI: An Extended Look") shouldn't out-compete the countdown itself
     for attention just by wrapping across more visual space than four short digits do. The
     countdown is the whole point of this block; the title is a label for it, not a headline. */
  font-size: 0.85rem;
  letter-spacing: -0.01em;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
  /* Reserves room for whatever sits absolutely-positioned in each top corner - .pub-countdown-
     type-icon (left, always present) and .pub-countdown-platform-icon/.pub-countdown-notify-btn
     (right, each conditional but never both at once in the same spot post-fix above) - a long
     title truncates cleanly here instead of running underneath either one.
     align-self: stretch overrides .pub-countdown's own align-items: center specifically for this
     child - without a real, resolvable width to constrain against (align-items: center leaves
     every flex item shrink-to-fit sized instead), max-width/overflow/text-overflow below were
     silently no-ops: the box still sized itself to the full unwrapped text's natural width. */
  align-self: stretch;
  text-align: center;
  max-width: 100%;
  padding: 0 46px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  box-sizing: border-box;
}

.pub-countdown-timer {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  gap: 14px;
}

.pub-countdown-unit {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 40px;
}

.pub-countdown-value {
  font-family: "Bricolage Grotesque", sans-serif;
  font-weight: 800;
  font-size: 2.3rem;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.pub-countdown-unit-label {
  font-size: 0.66rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.85;
}

/* Only present when the "Notify me" button renders (creators/index.php) - .pub-countdown itself
   stays the direct .creator-blocks grid child the rest of this file assumes everywhere else, so
   this wrapper only exists for the specific countdowns that need somewhere for the button to sit
   outside the card's own <a>/<div> (same reasoning as .pub-link-card-wrap for the share-trigger
   button on plain link cards - a <button> nested inside an <a> both fires navigation *and* the
   button's own handler on click). */
.pub-countdown-wrap {
  position: relative;
}

.pub-countdown-notify-btn {
  position: absolute;
  top: 12px;
  /* Not 12px (same corner .pub-countdown-platform-icon already sits in when the countdown's URL
     matches a recognized platform - YouTube/Twitch/etc.) - 50px clears that 30px circle plus its
     own 12px inset regardless of whether it's actually rendered for this specific countdown, so
     this doesn't need to special-case the platform-icon-absent case. */
  right: 50px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border: none;
  border-radius: 999px;
  background: rgba(15, 23, 42, 0.55);
  backdrop-filter: blur(6px);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.18s ease, transform 0.18s ease;
}

.pub-countdown-notify-btn:hover {
  background: rgba(15, 23, 42, 0.75);
}

.pub-countdown-notify-btn:active {
  transform: scale(0.96);
}

.pub-countdown-notify-btn[disabled] {
  cursor: default;
  opacity: 0.85;
}

.pub-countdown-notify-icon {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
}

/* .pub-link-hold-icon's default position (top:-6px;right:-6px, deliberately poking past a link
   card's edge - see that rule in the shake/shine/hold section above) would get clipped by
   .pub-countdown's own overflow:hidden (link cards don't have that by default), and the top
   corners here are already busy with the type icon, platform icon, and Notify button. Bottom-
   right is the one corner of a countdown card that's reliably empty. */
.pub-countdown.pub-link-hold .pub-link-hold-icon {
  top: auto;
  bottom: 10px;
  right: 10px;
}

/* Brief confirmation state after a successful subscribe (assets/js/creator.js toggles this,
   removed after a few seconds) - the button itself stays in place afterward rather than
   disappearing, so a visitor can tell notifications are already on if they look again later. */
.pub-countdown-notify-btn.is-notify-confirmed {
  background: rgba(34, 197, 94, 0.85);
}

/* Mobile: the pill's text label ("Notify me"/"Notifications on") doesn't have room next to the
   countdown digits on a small card, so this collapses to an icon-only circle. is-subscribed
   (assets/js/creator.js, kept in sync with the actual PushSubscription state - see the page-load
   check and every click-handler branch there) is the persistent "notifications are currently on"
   state, unlike is-notify-confirmed above which is only a brief post-click flash - with the text
   label gone here, the icon itself has to carry that state, hence the added "ringing" marks below
   rather than relying on the green background alone. */
@media (max-width: 640px), (max-height: 640px) {
  .pub-countdown-notify-btn {
    width: 32px;
    height: 32px;
    padding: 0;
    gap: 0;
    justify-content: center;
  }

  .pub-countdown-notify-btn [data-countdown-notify-label] {
    display: none;
  }

  .pub-countdown-notify-icon {
    width: 15px;
    height: 15px;
    position: relative;
  }

  .pub-countdown-notify-btn.is-subscribed .pub-countdown-notify-icon::before,
  .pub-countdown-notify-btn.is-subscribed .pub-countdown-notify-icon::after {
    content: '';
    position: absolute;
    top: 50%;
    width: 7px;
    height: 7px;
    margin-top: -3.5px;
    border: 1.5px solid currentColor;
    border-radius: 50%;
    opacity: 0.9;
  }

  .pub-countdown-notify-btn.is-subscribed .pub-countdown-notify-icon::before {
    left: -9px;
    border-right-color: transparent;
    border-bottom-color: transparent;
    transform: rotate(45deg);
  }

  .pub-countdown-notify-btn.is-subscribed .pub-countdown-notify-icon::after {
    right: -9px;
    border-left-color: transparent;
    border-bottom-color: transparent;
    transform: rotate(-45deg) scaleX(-1);
  }
}

/* Small heartbeat pop on the seconds digit every tick (class is removed/re-added each second by
   assets/js/creator.js to restart it) - a constant, low-key pulse for however long the countdown
   is running, not just a payoff at the very end. */
@keyframes pub-countdown-tick-pop {
  0% { transform: scale(1); }
  30% { transform: scale(1.18); }
  100% { transform: scale(1); }
}

.pub-countdown-value.is-countdown-tick {
  animation: pub-countdown-tick-pop 0.35s ease-out;
}

/* Final 10 seconds: escalating urgency instead of a flat countdown right up to the reveal - a
   warmer color and a faster pulse on the whole card, building tension into the payoff above. */
@keyframes pub-countdown-urgent-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.03); }
}

.pub-countdown.is-countdown-urgent {
  animation: pub-countdown-urgent-pulse 1s ease-in-out infinite;
}

.pub-countdown.is-countdown-urgent .pub-countdown-value {
  color: #ffd166;
  text-shadow: 0 0 12px rgba(255, 209, 102, 0.7);
}

@media (prefers-reduced-motion: reduce) {
  .pub-countdown-value.is-countdown-tick,
  .pub-countdown.is-countdown-urgent {
    animation: none;
  }
}

/* Reached state: assets/js/creator.js replaces .pub-countdown-timer's own content with the
   type's reached text (see data-countdown-reached-text) rather than toggling separate markup -
   simplest way to swap from "4 ticking numbers" to "one line of text" without a second, normally-
   hidden element sitting in the DOM the whole time for something that only ever matters once. */
.is-countdown-reached .pub-countdown-timer {
  font-family: "Bricolage Grotesque", sans-serif;
  font-weight: 800;
  /* Bigger than .pub-countdown-heading's 1.05rem, clearly so (not just technically) - matches
     the ticking-digits state, where .pub-countdown-value's 1.7rem already reads as obviously
     larger than the title next to it. */
  font-size: 1.5rem;
  animation: pub-countdown-text-pop 0.5s ease;
}

@keyframes pub-countdown-text-pop {
  0% { opacity: 0; transform: scale(0.85); }
  100% { opacity: 1; transform: scale(1); }
}

/* The one-shot "reveal" beat for the actual zero-hour moment (assets/js/creator.js adds/removes
   this class itself, ~900ms) - a quick punch + white flash, the suspenseful payoff for however
   long the numbers were ticking down. Kept separate from the gentler, ongoing glow below so the
   big moment still reads as a distinct event rather than blending into the persistent state. */
@keyframes pub-countdown-reveal-punch {
  0% { transform: scale(1); }
  35% { transform: scale(1.07); }
  60% { transform: scale(0.97); }
  100% { transform: scale(1); }
}

@keyframes pub-countdown-reveal-flash {
  0% { box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18), 0 0 0 0 rgba(255, 255, 255, 0); }
  30% { box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18), 0 0 45px 14px rgba(255, 255, 255, 0.65); }
  100% { box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18), 0 0 0 0 rgba(255, 255, 255, 0); }
}

.pub-countdown.is-countdown-just-reached {
  animation: pub-countdown-reveal-punch 0.6s cubic-bezier(0.34, 1.56, 0.64, 1), pub-countdown-reveal-flash 0.9s ease;
}

/* Persistent (but subtle) pulsing glow for as long as the card stays in the reached state -
   keeps a little energy on it without competing with the one-shot reveal above. */
@keyframes pub-countdown-reached-glow {
  0%, 100% { box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18); }
  50% { box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18), 0 0 24px 4px rgba(255, 255, 255, 0.3); }
}

.pub-countdown.is-countdown-reached:not(.is-countdown-just-reached) {
  animation: pub-countdown-reached-glow 2.6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .is-countdown-reached .pub-countdown-timer,
  .pub-countdown.is-countdown-just-reached,
  .pub-countdown.is-countdown-reached:not(.is-countdown-just-reached) {
    animation: none;
  }
}

/* Only kicks in once a creator explicitly picks a name color (data-name-color-custom is only
   rendered when name_color is non-null - see creators/index.php) - otherwise link titles keep
   the normal contrast-computed --button-text color untouched, so this never affects anyone who
   hasn't opted in. */
body[data-name-color-custom="1"] .pub-link-title {
  color: var(--name-color-complement, inherit);
}

.pub-link-row {
  display: grid;
  gap: 10px;
}

.pub-link-chip {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--button-color, #ffffff);
  border-radius: 16px;
  padding: 10px 12px;
  text-decoration: none;
  color: var(--button-text, #0f172a);
  font-weight: 700;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.pub-link-chip:hover {
  transform: translateY(-3px);
  box-shadow: 0 14px 24px rgba(0, 0, 0, 0.22);
}

.pub-link-chip-icon {
  width: 30px;
  height: 30px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  background: #f1f5f9;
  color: #475569;
  flex: 0 0 auto;
  overflow: hidden;
}

.pub-link-chip-icon img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pub-link-chip-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.86rem;
}

/* Icon Row: circular icon-only chips, label kept for accessibility only. Stays a single
   straight line (scrollable) instead of wrapping to a second row when it doesn't fit - wrapping
   is fine for the list/grid layouts but reads broken for a row of plain circles. */
.pub-link-row--icons {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  gap: 8px;
  overflow-x: auto;
  /* overflow-x: auto forces an implied overflow-y: auto too (can't mix auto/hidden on one axis
     with visible on the other per the CSS Overflow spec), so this box also clips vertically -
     without padding here, .pub-link-chip:hover's translateY(-3px) and bigger box-shadow had no
     room to render and got cut off top and bottom. Padding on the right side too (not just top/
     bottom) for the same reason: a held link's lock badge (.pub-link-row--icons .pub-link-hold-
     icon below) sits at top:-10px;right:-10px past its own chip's edge - confirmed via
     elementsFromPoint() that with only vertical padding, that badge was being clipped away by
     this same overflow entirely invisible, not just visually cramped. 14px covers the badge's
     10px offset plus its own box-shadow spread. */
  padding: 14px 14px 14px 0;
  margin: -14px -14px -14px 0;
  scrollbar-width: none;
  -ms-overflow-style: none;
}

.pub-link-row--icons::-webkit-scrollbar {
  display: none;
}

.pub-link-row--icons .pub-link-chip {
  flex: 0 0 auto;
  width: 46px;
  height: 46px;
  padding: 0;
  justify-content: center;
  border-radius: 50%;
}

.pub-social-footer {
  /* A small deliberate gap on top of what .creator-blocks' own 12px grid gap already gives every
     block type - the icon row's compact height (a row of 46px circles, not a full card) makes
     that shared 12px read as visually tighter next to it than it does between two regular link
     cards, so it gets a bit extra on both sides.
     margin-bottom in particular isn't optional here, just extra polish: .pub-link-row--icons'
     own -14px margin (needed to give a held link's lock badge and hover-lift room to render
     without .pub-link-row--icons' overflow-x:auto clipping them) was leaking through unmodified
     on this side, pulling whatever block comes after up into a 2px overlap - the override below
     is what actually fixes that, not just adds breathing room. */
  margin-top: 8px;
  margin-bottom: 8px;
}

.pub-link-row--icons .pub-link-chip-icon {
  width: 100%;
  height: 100%;
  border-radius: 50%;
}

.pub-link-row--icons .pub-link-chip-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

/* Grid: icon-over-label tiles */
.pub-link-row--grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

/* A single grid-laid-out link would otherwise sit in the first (left) cell like an accident of
   how the 3-column track happens to fill, rather than a deliberate layout - center it instead. */
.pub-link-row--grid .pub-link-chip:only-child {
  grid-column: 2;
}

.pub-link-row--grid .pub-link-chip {
  flex-direction: column;
  text-align: center;
  padding: 14px 8px;
  gap: 8px;
}

.pub-link-row--grid .pub-link-chip-icon {
  width: 40px;
  height: 40px;
  border-radius: 12px;
}

.pub-link-row--grid .pub-link-chip-label {
  width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: 0.78rem;
}

body[data-button="pill"] .pub-link-chip {
  border-radius: 999px;
}

body[data-button="square"] .pub-link-chip {
  /* Was 8px, halfway between this and Rounded's 16px base - read as "still rounded, just a bit
     less" rather than a distinct third style. */
  border-radius: 4px;
}

body[data-button="pill"] .pub-link-row--icons .pub-link-chip,
body[data-button="square"] .pub-link-row--icons .pub-link-chip {
  border-radius: 50%;
}

/* Every link/icon/card on the public page, regardless of layout - browsers natively let you
   click-drag any <a> (or its background image) into a "drag this link out" ghost preview, which
   is never wanted here (this is meant to feel like a static app, not a page of draggable
   hyperlinks) and is actively disruptive on a held link specifically, where a mouse-drag mid-hold
   fights with the press-and-hold progress instead of just completing it. -webkit-user-drag
   handles Chrome/Safari/Edge; Firefox doesn't support that property at all, so
   assets/js/creator.js also preventDefault()s the dragstart event for full cross-browser
   coverage. */
.pub-external-link {
  -webkit-user-drag: none;
}

/* A same-icon picker trigger (render_link_anchor() in creators/index.php) renders as a real
   <button> instead of <a> so it doesn't need an href - but a bare <button> carries its own UA
   border/appearance/font that .pub-link-chip's own rules never touch (they only set padding,
   background, etc.). Combined with this app's global box-sizing: border-box, that default
   border eats into the fixed 46px circle (.pub-link-row--icons .pub-link-chip), making a
   same-icon button's icon look inset/smaller than every sibling <a> chip's. */
button.pub-external-link {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  font: inherit;
  text-align: inherit;
  cursor: pointer;
}

/* Hold-to-unlock links: press-and-hold gesture gate for age-gated/premium destinations. */
/* The native long-press context menu (iOS callout, Android "open in new tab") is deliberately
   left available during the hold, even though it can interrupt/cancel the in-page gesture (the
   OS taking over the touch input mid-press fires pointercancel, same as lifting a finger early) -
   that native menu is the one escape path an in-app browser (Instagram, etc.) can't patch out the
   way it eventually could a JS-based deep-link trick, so it's worth keeping available on purpose. */
.pub-link-hold {
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}

/* Icon Row is the one link layout with its own horizontally-scrollable container
   (.pub-link-row--icons's overflow-x: auto) - touch-action: manipulation still lets the browser
   interpret a finger drifting sideways during a hold as the start of a pan/scroll on that
   container, which fires pointercancel (see creator.js's cancelHold()) and aborts the hold
   partway through, unpredictably. Nothing else this hold gesture competes with exists in
   list/grid/card layouts, only the page's own vertical scroll, which a stationary press-and-hold
   doesn't trigger the same ambiguity against. touch-action: none hands this specific link fully
   over to our own JS, so the row around it can still be swiped normally without a held icon's own
   hold gesture ever being mistaken for the start of that swipe. */
.pub-link-row--icons .pub-link-hold {
  touch-action: none;
}

.pub-link-hold.is-shake {
  animation: pub-link-hold-shake 0.4s ease-in-out;
}

@keyframes pub-link-hold-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(3px); }
}

/* Progress fill: a moving multi-stop brand gradient (not a flat 2-color wash) that scrolls
   leftward as it fills, so the hold reads as something actively happening rather than a static
   bar stretching. background-position transitions on the same --hold-duration as the scaleX fill,
   so both finish together regardless of how long a creator's Countdown block (etc.) sets the hold
   duration to. Purely visual - creator.js never changes here, it only ever toggles is-holding/
   is-unlocked/is-shake same as before. */
.pub-link-hold-progress {
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  /* --accent/--secondary, not a fixed palette - this sits on top of whatever image/color a
     creator's own link card already has, and every page's accent pair is different, so a fixed
     hardcoded gradient inevitably clashes with a lot of them. Translucent (not the flat opaque
     fill an earlier pass here had) so the card's own content stays visible while holding, same
     as the original rgba()-based version did before this whole redesign. */
  background: linear-gradient(100deg, var(--accent, #ff5f6d), var(--secondary, #0ea5e9));
  background-size: 220% 100%;
  background-position: 100% 0;
  opacity: 0.38;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.25s ease-out;
  pointer-events: none;
}

/* Asymmetric transition, same idea as .pub-link-hold-progress's own fast-release/slow-build
   split below: the base class's fast 0.25s applies once is-holding is removed (an early
   release snaps the glow away quickly), while is-holding's own longer var(--hold-duration)
   transition - more specific, so it wins while that class is present - ramps it up slowly
   across the whole hold instead of a glow that pops in immediately. */
.pub-link-hold {
  transition: box-shadow 0.25s ease-out;
}

.pub-link-hold.is-holding {
  transition: box-shadow var(--hold-duration, 900ms) ease-out;
  /* color-mix against the page's own --accent, not a fixed purple - same "match whatever this
     page's colors are" reasoning as .pub-link-hold-progress above. */
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.55), 0 0 26px 5px color-mix(in srgb, var(--accent, #7c3aed) 55%, transparent);
}

.pub-link-hold.is-holding .pub-link-hold-progress {
  transition: transform var(--hold-duration, 900ms) linear, background-position var(--hold-duration, 900ms) linear;
  transform: scaleX(1);
  background-position: 0% 0;
}

.pub-link-hold.is-unlocked {
  animation: pub-link-hold-unlock-pop 0.32s ease-out;
}

@keyframes pub-link-hold-unlock-pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.04); }
  100% { transform: scale(1); }
}

.pub-link-hold.is-unlocked .pub-link-hold-progress {
  /* Green stays fixed here (not accent-based) - it's the universal "unlocked/success" signal,
     independent of whatever the page's own theme colors are. Same translucency as the holding
     state above, for the same reason. */
  background: linear-gradient(100deg, #10b981, #34d399);
  background-position: 0 0;
  opacity: 0.4;
}

/* Radial "burst" flash the instant it unlocks - plays once (the class is only ever added fresh,
   never toggled back on while already present) and fades before is-unlocked itself gets removed
   ~620ms later in creator.js, so it never lingers as a stray white circle. */
.pub-link-hold.is-unlocked::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  background: radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0) 70%);
  animation: pub-link-hold-burst 0.5s ease-out forwards;
  pointer-events: none;
}

@keyframes pub-link-hold-burst {
  0% { opacity: 0.9; transform: scale(0.3); }
  100% { opacity: 0; transform: scale(1.7); }
}

.pub-link-hold-icon {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #111827;
  color: #fbbf24;
  display: grid;
  place-items: center;
  font-size: 0.62rem;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.32);
  z-index: 3;
  pointer-events: none;
  transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
}

.pub-link-hold.is-holding .pub-link-hold-icon {
  animation: pub-link-hold-icon-pulse 0.6s ease-in-out infinite;
}

@keyframes pub-link-hold-icon-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.22); }
}

.pub-link-hold.is-unlocked .pub-link-hold-icon {
  background: #10b981;
  color: #ffffff;
  transform: scale(1.3) rotate(-10deg);
}

@media (prefers-reduced-motion: reduce) {
  .pub-link-hold.is-unlocked,
  .pub-link-hold.is-holding .pub-link-hold-icon {
    animation: none;
  }

  .pub-link-hold.is-unlocked::after {
    display: none;
  }
}

.pub-link-hold > .pub-link-icon,
.pub-link-hold > .pub-link-chip-icon,
.pub-link-hold > .pub-link-title,
.pub-link-hold > .pub-link-chip-label,
.pub-link-hold > .pub-link-featured-label {
  position: relative;
  z-index: 2;
}

/* A ring+badge crammed onto a bare 46px circle (Icon Row, and the Social Icons Row which shares
   this same class) never had the room to read as intentional - no on-page lock indicator here at
   all now. The click still goes to go.php like any other link; go.php itself is what shows the
   press-and-hold gate for these, so nothing is actually unprotected. Featured/list/grid links keep
   the on-page ring+badge (see .pub-link-hold-progress/.pub-link-hold-icon above) - they have the
   size for it to look deliberate. */
.pub-link-row--icons .pub-link-hold-progress,
.pub-link-row--icons .pub-link-hold-icon {
  display: none;
}

/* Attention shake: a brief, gentle wiggle every 5s (opt-in per link, .pub-link-shake), not a
   constant motion - only the first ~7% of each animation cycle actually moves, the rest holds
   still, so it reads as a periodic nudge rather than something distracting the whole time it's
   on screen. Amplitude deliberately stays small (max 2px/1deg) - an earlier version peaked at
   4px/3deg, which read as aggressive rather than a subtle attention cue. */
@keyframes pub-link-shake-pulse {
  0%, 7.1%, 100% {
    transform: translateX(0) rotate(0deg);
  }
  1% {
    transform: translateX(-2px) rotate(-1deg);
  }
  2% {
    transform: translateX(2px) rotate(1deg);
  }
  3% {
    transform: translateX(-2px) rotate(-1deg);
  }
  4% {
    transform: translateX(2px) rotate(1deg);
  }
  5% {
    transform: translateX(-1px) rotate(-0.5deg);
  }
  6% {
    transform: translateX(1px) rotate(0.5deg);
  }
}

.pub-link-shake {
  animation: pub-link-shake-pulse 5s ease-in-out infinite;
}

.pub-link-shake:hover {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .pub-link-shake {
    animation: none;
  }
}

/* Attention shine: same idea as the shake above (opt-in per link, .pub-link-shine), a periodic
   diagonal shine bar sweeping across the whole card instead of a wiggle - same skewed-bar visual
   language as the profile's own verified-badge shine (.creator-badge-shine-rect). Low peak
   opacity and a narrow band, not a wide sheen, so it passes over the label quickly rather than
   sitting on top of it and hurting legibility. overflow: hidden clips the bar to the card's own
   rounded corners - only applies here, not to every link, since links don't otherwise clip their
   own hover lift/shadow. */
.pub-link-shine {
  position: relative;
  overflow: hidden;
}

/* .pub-link-hold-icon (the small lock badge) is deliberately positioned at top:-6px;right:-6px,
   poking past the card's own edge - overflow: hidden above clips that straight off when a link is
   both held and shining. Nudged fully inside the card here so it stays visible; only applies to
   this combination, so a held-but-not-shining link keeps the badge's usual poking-out position. */
.pub-link-hold.pub-link-shine .pub-link-hold-icon {
  top: 4px;
  right: 4px;
}

.pub-link-shine::after {
  content: "";
  position: absolute;
  top: -20%;
  left: -60%;
  width: 35%;
  height: 140%;
  background: linear-gradient(100deg, transparent 0%, rgba(255, 255, 255, 0.32) 50%, transparent 100%);
  transform: skewX(-20deg);
  animation: pub-link-shine-sweep 3.2s ease-in-out infinite;
  pointer-events: none;
  z-index: 3;
}

@keyframes pub-link-shine-sweep {
  0% {
    left: -60%;
  }
  55%, 100% {
    left: 130%;
  }
}

/* The plain white sweep above reads fine on a featured card's image background, but plain
   card/chip links sit on --button-color, which defaults to white - a white sweep at 32% opacity
   over a white card is effectively invisible. Reuses the card's own contrast-computed
   --button-text color instead, the same one already guaranteed to stand out against
   --button-color for the card's label text. */
.pub-link-card.pub-link-shine::after,
.pub-link-chip.pub-link-shine::after {
  background: linear-gradient(100deg, transparent 0%, var(--button-text, #0f172a) 50%, transparent 100%);
  opacity: 0.35;
}

/* Icon Row specifically (also covers the social icons row, same markup) - the sweep bar is
   sized relative to the old card/chip dimensions, badly oversized against a bare 46px circle
   with no card background of its own for it to read as sweeping across. Same reasoning
   body[data-card-shadow="1"] already excludes Icon Row from further up this file for (a hard
   border/offset shadow reads just as cramped/broken there). */
.pub-link-row--icons .pub-link-chip.pub-link-shine::after {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .pub-link-shine::after {
    animation: none;
    display: none;
  }
}

/* Featured links: large image-background hero card with the label overlaid at the bottom. */
.pub-link-featured {
  position: relative;
  z-index: 0;
  display: flex;
  align-items: flex-end;
  min-height: 150px;
  border-radius: 18px;
  overflow: visible;
  background: linear-gradient(135deg, var(--accent), var(--secondary));
  background-size: cover;
  background-position: center;
  text-decoration: none;
  box-shadow: 0 14px 28px rgba(0, 0, 0, 0.24);
  padding: 16px;
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

/* .pub-link-featured's own overflow: visible above wins over .pub-link-shine's overflow: hidden
   on a shining featured card otherwise - same specificity, later in source order - which would
   let the shine bar escape past the card's rounded corners. */
.pub-link-featured.pub-link-shine {
  overflow: hidden;
}

.pub-link-featured-scrim {
  border-radius: inherit;
}

/* Small: collapses down close to a regular link row's own height instead of a hero-sized card -
   still an image-background card underneath with every featured-only control (title font/
   animation/color/outline, hold-to-unlock, shine, label position, custom icon) still available,
   just compact instead of dominating the page. */
.pub-link-featured-small {
  min-height: 64px;
  padding: 12px 16px;
  align-items: center;
  gap: 12px;
}

.pub-link-featured-small .pub-link-featured-label {
  font-size: 1rem;
}

/* At full/tall/half height the icon sits absolutely-positioned in the top-left corner, well clear
   of the label pinned to the bottom - at 64px there's no longer room for both without them
   overlapping. Small size instead lays the icon out inline before the label (position: static
   makes it a real flex item again, same row as .pub-link-featured-label), matching how a normal
   link icon+text row is laid out. */
.pub-link-featured-small .pub-link-featured-icon {
  position: static;
  width: 34px;
  height: 34px;
  border-radius: 10px;
  overflow: hidden;
}

/* Full size (~33% taller than the 150px default) - width is untouched either way, since the
   card is always the link column's full width. */
.pub-link-featured-full {
  min-height: 200px;
}

/* Tall: a genuine poster-style hero, well past "Full" - for a link that's meant to dominate the
   page rather than sit alongside the others. */
.pub-link-featured-tall {
  min-height: 340px;
}

/* No dedicated featured photo, but there is an icon (catalog pick or upload) - a blurred, scaled-
   up copy of it fills the card instead of the flat accent/secondary gradient. A pseudo-element
   rather than filter on the card itself, since filter would blur the label/badge sitting on top
   of it too - ::before paints behind the real children regardless, no z-index needed. Scaled up
   more than the page-background version of this same idea (.bg-blur-layer) because icon source
   files are small, so a heavier blur+zoom hides upscale artifacts better. */
.pub-link-featured-icon-bg::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-image: var(--featured-icon-bg);
  background-size: cover;
  background-position: center;
  filter: blur(26px);
  transform: scale(1.4);
}

/* Moves the label from its default bottom-left spot to bottom-center - still pinned to the
   bottom (.pub-link-featured's own align-items: flex-end, untouched here), just centered
   horizontally instead of left-aligned. Grown min-height gives the label - often a larger/
   stylized font, per the font/outline/animation controls this shares a block editor panel with -
   room to sit without crowding the card's edges, the same way the "Full size" card gets more
   room than "Half". */
.pub-link-featured-label-bottom-center {
  justify-content: center;
  text-align: center;
  min-height: 190px;
}

.pub-link-featured-full.pub-link-featured-label-bottom-center {
  min-height: 230px;
}

.pub-link-featured-tall.pub-link-featured-label-bottom-center {
  min-height: 340px;
}

/* True center: the label sits in the dead middle of the card, not just centered horizontally
   along the bottom edge. Same grown min-height reasoning as bottom-center above. */
.pub-link-featured-label-center {
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 190px;
}

.pub-link-featured-full.pub-link-featured-label-center {
  min-height: 230px;
}

.pub-link-featured-tall.pub-link-featured-label-center {
  min-height: 340px;
}

.pub-link-featured-icon {
  position: absolute;
  top: 12px;
  left: 12px;
  z-index: 2;
  width: 40px;
  height: 40px;
  pointer-events: none;
}

.pub-link-featured-icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  filter: drop-shadow(0 1px 6px rgba(0, 0, 0, 0.55));
}

.pub-link-featured:hover {
  transform: translateY(-3px);
  box-shadow: 0 18px 34px rgba(0, 0, 0, 0.3);
}

.pub-link-featured-scrim {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.68) 100%);
  pointer-events: none;
}

.pub-link-featured-label {
  position: relative;
  z-index: 1;
  display: inline-block;
  /* color is always set inline now (render_featured_link() in creators/index.php) - defaults to
     the same #fff this used to hardcode, so an untouched link renders identically. */
  font-weight: 800;
  font-size: 1.2rem;
  letter-spacing: -0.01em;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}

/* Per-link title font override (Design tab's Font swatches, but scoped to just this one card's
   label) - all four already self-hosted site-wide via fonts.css, so picking one here never
   depends on the page's own body[data-font] choice. No "inherit" entry - that case gets no class
   at all (see $titleFont in render_featured_link()), letting the label keep cascading the page
   font exactly as it did before this control existed. */
.pub-title-font-sora { font-family: "Sora", sans-serif; }
.pub-title-font-bricolage { font-family: "Bricolage Grotesque", sans-serif; }
.pub-title-font-poppins { font-family: "Poppins", sans-serif; }
.pub-title-font-manrope { font-family: "Manrope", sans-serif; }

/* Badge/display fonts, featured-card titles only (not offered as a page-wide font choice). No
   font-weight override here (unlike an earlier version of this rule) - each only ships fonts.css's
   one real weight (400, except Oswald's 700), so requesting .pub-link-featured-label's own
   font-weight: 800 makes the browser synthesize a heavier/faux-bold render on top of that weight
   instead of using it as-is. That's deliberate now, not a bug: on faces this thin at this label's
   font-size, the un-synthesized weight read as too light against an outline. */
.pub-title-font-bebas { font-family: "Bebas Neue", sans-serif; }
.pub-title-font-anton { font-family: "Anton", sans-serif; }
.pub-title-font-archivo { font-family: "Archivo Black", sans-serif; }
.pub-title-font-oswald { font-family: "Oswald", sans-serif; }
.pub-title-font-righteous { font-family: "Righteous", sans-serif; }
.pub-title-font-bangers { font-family: "Bangers", sans-serif; }

/* Per-link title text animations - deliberately animate only the label span, not the whole card
   (that's .pub-link-shake's job) or the background image, so the motion reads as "this word wants
   attention" rather than the entire card wobbling. */
@keyframes pub-title-anim-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.08); }
}

.pub-title-anim-pulse {
  animation: pub-title-anim-pulse 1.4s ease-in-out infinite;
}

@keyframes pub-title-anim-bounce {
  0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
  40% { transform: translateY(-6px); }
  60% { transform: translateY(-3px); }
}

.pub-title-anim-bounce {
  animation: pub-title-anim-bounce 1.8s ease-in-out infinite;
}

@keyframes pub-title-anim-wiggle {
  0%, 100% { transform: rotate(0deg); }
  25% { transform: rotate(-4deg); }
  75% { transform: rotate(4deg); }
}

.pub-title-anim-wiggle {
  animation: pub-title-anim-wiggle 1.2s ease-in-out infinite;
}

@keyframes pub-title-anim-glow {
  0%, 100% { text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); }
  50% { text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4), 0 0 18px var(--accent), 0 0 30px var(--accent); }
}

.pub-title-anim-glow {
  animation: pub-title-anim-glow 1.8s ease-in-out infinite;
}

@keyframes pub-title-anim-fade {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

.pub-title-anim-fade {
  animation: pub-title-anim-fade 1.6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .pub-title-anim-pulse,
  .pub-title-anim-bounce,
  .pub-title-anim-wiggle,
  .pub-title-anim-glow,
  .pub-title-anim-fade {
    animation: none;
  }
}

body[data-button="pill"] .pub-link-featured {
  border-radius: 30px;
}

body[data-button="square"] .pub-link-featured {
  /* Was 8px, halfway between this and Rounded's 16px base - read as "still rounded, just a bit
     less" rather than a distinct third style. */
  border-radius: 4px;
}

.pub-gallery-block {
  border-radius: 16px;
}

.pub-gallery-block .pub-block-title {
  margin-bottom: 8px;
}

.pub-gallery-wrap {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
  /* The clipping boundary for the carousel's sliding track below - stays at the block's own
     width regardless of how wide that track's content actually is. Harmless for grid mode too,
     which never has anything extending past this box in the first place. */
  overflow: hidden;
}

.pub-gallery {
  display: grid;
  gap: 8px;
  width: 100%;
}

/* Grid and carousel share the same fixed cell size (110px, squared off by the item's own
   aspect-ratio: 1/1) so switching between the two layouts doesn't change how big each photo
   looks - grid just wraps those cells onto new rows, carousel lines them up in one scrolling row.
   Flexbox instead of CSS grid here on purpose: grid's auto-fill/auto-fit reserves a fixed set of
   column tracks shared across every row, so a short last row (or the only row, for a small
   gallery) has nothing to center it - it just sits flush left with dead space to its right,
   which is exactly the "off center" look this was doing. Flex-wrap centers each wrapped row
   independently, so 1 image or 3 images that don't fill a row both land in the middle instead. */
.pub-gallery-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.pub-gallery-grid .pub-gallery-item {
  flex: 0 0 auto;
  width: 110px;
}

/* A pure CSS transform loop instead of JS scrollLeft/requestAnimationFrame - the markup already
   contains two identical back-to-back copies of the image set (see render_gallery_item's second
   pass in creators/index.php), so animating from 0 to exactly -50% of the track's own width wraps
   around seamlessly with no DOM measuring, no clone-count math, and (since transform never
   affects layout) no way for this to push, resize, or clip any other block on the page - the
   entire class of bugs the previous scrollLeft-based approach kept running into. Speed comes from
   --gallery-carousel-duration, set inline per-block in creators/index.php based on image count,
   so a bigger gallery drifts for proportionally longer instead of finishing a lap too fast. */
@keyframes pub-gallery-marquee {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}

.pub-gallery-carousel {
  display: flex;
  flex-wrap: nowrap;
  flex-shrink: 0;
  width: max-content;
  animation: pub-gallery-marquee var(--gallery-carousel-duration, 20s) linear infinite;
}

.pub-gallery-carousel:hover {
  animation-play-state: paused;
}

.pub-gallery-carousel .pub-gallery-item {
  flex: 0 0 auto;
  width: 110px;
}

/* Under 4 images, the track (even doubled) doesn't reliably fill a typical card's width - it
   just sits flush left with dead space next to it, no different from grid's own "off center"
   bug. creators/index.php skips the duplicate set and the animation-duration variable entirely
   below that count, and this override turns off the sliding animation and centers the row
   instead - same treatment grid mode gets, since there's nothing worth scrolling through yet. */
.pub-gallery-carousel-static {
  width: 100%;
  justify-content: center;
  animation: none;
}

.pub-gallery-item img {
  -webkit-user-drag: none;
  user-select: none;
}

.pub-gallery-item {
  border: 0;
  padding: 0;
  border-radius: 14px;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  cursor: zoom-in;
  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18);
}

body[data-button="pill"] .pub-gallery-item {
  border-radius: 999px;
}

body[data-button="square"] .pub-gallery-item {
  border-radius: 4px;
}

.pub-gallery-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 0.3s ease;
}

.pub-gallery-item:hover img {
  transform: scale(1.05);
}

.pub-gallery-nav {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 0;
  background: rgba(255, 255, 255, 0.88);
  color: #0f172a;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: 0 0 auto;
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
}

.pub-music-card {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--button-color, #ffffff);
  border-radius: 16px;
  padding: 12px 14px;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.16);
}

.pub-music-cover {
  position: relative;
  width: 52px;
  height: 52px;
  border-radius: 12px;
  border: 0;
  overflow: hidden;
  background: linear-gradient(135deg, var(--accent), var(--secondary));
  color: #fff;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: 0 0 auto;
  font-size: 1.1rem;
  transition: border-radius 0.3s ease;
}

.pub-music-cover img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pub-music-play-icon {
  position: relative;
  z-index: 1;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.42);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 0.68rem;
  /* Hidden until hovered, so the cover art shows clean by default - stays visible while actually
     playing regardless of hover, though, so there's always a visible way to pause without having
     to rediscover the cover on hover first. */
  opacity: 0;
  transition: opacity 0.15s ease;
}

.pub-music-cover:hover .pub-music-play-icon,
.pub-music-cover:focus-visible .pub-music-play-icon,
.pub-music-card.is-playing .pub-music-play-icon {
  opacity: 1;
}

/* The pause state is an inline SVG (fa-pause isn't in the self-hosted FA subset), which doesn't
   size off font-size the way the FA play icon does - needs its own explicit dimensions instead
   of inheriting the intrinsic ~300x150 default an SVG with no width/height falls back to. */
.pub-music-play-icon svg {
  width: 10px;
  height: 10px;
}

.pub-music-card.is-playing .pub-music-cover {
  /* Spins the whole disc - cover art if there is one, the gradient-plate fallback icon if not -
     rather than just an <img> that's only in the DOM when a custom cover was actually uploaded.
     Rounds fully to a circle while playing, like a record; back to the softer square the rest
     of the time, via the transition on the base rule above. */
  border-radius: 50%;
  animation: music-pulse 1.6s ease-in-out infinite, music-spin 3.5s linear infinite;
}

/* Counter-spins against the parent's rotation so the play/pause icon overlay - and the no-cover
   fallback note icon, when there's no real cover art to spin instead - both stay upright rather
   than spinning along with the disc underneath them. Real cover art reads fine rotating like a
   physical vinyl label; a single off-center icon glyph doing the same instead visibly swings
   through a wide arc as the disc turns, which just looks broken rather than intentional. */
.pub-music-card.is-playing .pub-music-play-icon,
.pub-music-card.is-playing .pub-music-cover-fallback-icon {
  animation: music-spin-reverse 3.5s linear infinite;
}

@keyframes music-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.45); }
  50% { box-shadow: 0 0 0 8px rgba(255, 255, 255, 0); }
}

@keyframes music-spin-reverse {
  from { transform: rotate(0deg); }
  to { transform: rotate(-360deg); }
}

.pub-music-meta {
  flex: 1 1 auto;
  min-width: 0;
}

.pub-music-title {
  font-weight: 700;
  color: var(--button-text, #0f172a);
  font-size: 0.92rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pub-music-artist {
  color: #64748b;
  font-size: 0.8rem;
}

.pub-music-seek {
  width: 100%;
  height: 3px;
  margin-top: 6px;
  /* Same complementary-color logic as .pub-link-title when a creator has customized their name
     color; --name-color-complement is only ever defined in that case (see creators/index.php), so
     this correctly falls through to --button-text (guaranteed contrast against .pub-music-card's
     own background) the rest of the time - --accent had no such guarantee, which is what made the
     seek bar hard to see on some color combinations in the first place. */
  accent-color: var(--name-color-complement, var(--button-text, var(--accent, #ff5f6d)));
  cursor: pointer;
}

@keyframes music-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.pub-video-frame {
  position: relative;
  width: 100%;
  padding-top: 56.25%;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18);
}

.pub-video-frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.pub-spotify-frame {
  width: 100%;
  border-radius: 16px;
  overflow: hidden;
}

.pub-spotify-frame iframe {
  display: block;
  width: 100%;
  height: 152px;
  border: 0;
}

.pub-video-native-wrap {
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18);
  cursor: pointer;
}

.pub-video-native {
  width: 100%;
  display: block;
  pointer-events: none;
}

.pub-video-play-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  border: 0;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 1.3rem;
  cursor: pointer;
  transition: opacity 0.2s ease, background-color 0.2s ease;
}

.pub-video-native-wrap:hover .pub-video-play-btn {
  background: rgba(0, 0, 0, 0.75);
}

.pub-video-native-wrap.is-playing .pub-video-play-btn {
  opacity: 0;
  pointer-events: none;
}

.pub-video-fallback-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.8rem;
  font-weight: 700;
  text-decoration: none;
  margin-top: -4px;
}

body[data-button="pill"] .pub-link-card,
body[data-button="pill"] .pub-music-card {
  border-radius: 999px;
}

body[data-button="square"] .pub-link-card,
body[data-button="square"] .pub-music-card {
  /* Was 8px, halfway between this and Rounded's 16px base - read as "still rounded, just a bit
     less" rather than a distinct third style. */
  border-radius: 4px;
}

/* Button fill: Outline - independent of the shape rules above, composable with any of them.
   Scoped to the plain text/list-style link surfaces (card/stack-item) and the music card, not
   Icon Row's chips (.pub-link-chip) or Featured (an image-backed hero card, not a flat-color
   button to begin with) - a solid icon badge or photo card doesn't have a meaningful "outline"
   reading the way a plain color-filled button does. Text/border both use --button-color itself
   rather than --button-text, since there's no longer a filled background to contrast against -
   the button's own accent color has to do double duty as both the outline and the text color. */
body[data-button-fill="outline"] .pub-link-card,
body[data-button-fill="outline"] .pub-link-stack-item,
body[data-button-fill="outline"] .pub-music-card {
  background: transparent;
  /* --outline-button-color (creators/index.php) is button_color itself whenever that already
     contrasts against the page's canvas color, or a guaranteed-readable navy/white fallback when
     it doesn't - not --button-color directly, which has no fill background left to be legible
     against once this mode drops it. */
  border: 2px solid var(--outline-button-color, var(--button-color, #ffffff));
  color: var(--outline-button-color, var(--button-color, #ffffff));
  box-shadow: none;
}

body[data-button-fill="outline"] .pub-link-card:hover,
body[data-button-fill="outline"] .pub-link-stack-item:hover,
body[data-button-fill="outline"] .pub-music-card:hover {
  box-shadow: none;
}

/* .pub-link-title-group's subtitle/title inherit color from the card itself by default (no
   explicit color of their own) - only .pub-link-subtitle sets its own opacity-dimmed version of
   whatever that inherited color is, so both still track --button-color correctly through this. */
body[data-button-fill="outline"] .pub-music-title,
body[data-button-fill="outline"] .pub-music-artist {
  color: var(--outline-button-color, var(--button-color, #ffffff));
}

/* .pub-gallery-block never picked up the button-style setting at all - it kept its own fixed
   16px "Rounded" radius regardless of what Pill/Square was selected everywhere else. */
body[data-button="pill"] .pub-gallery-block {
  border-radius: 32px;
}

body[data-button="square"] .pub-gallery-block {
  border-radius: 4px;
}

.creator-footer {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 22px 0 4px;
}

.creator-footer a {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.82rem;
  text-decoration: none;
  font-weight: 600;
}

.creator-footer a[hidden] {
  display: none;
}

.creator-footer-mark {
  width: 13px;
  height: auto;
  object-fit: contain;
  opacity: 0.85;
}

.creator-footer a strong {
  color: #fff;
}

/* Indiscriminate-crawler tripwire: invisible to real visitors and screen readers alike
   (off-screen + zero-size + aria-hidden + tabindex=-1), but present in the DOM for any
   scraper that blindly follows every <a href> on the page. */
.pub-canary-trap {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
  pointer-events: none !important;
}

/* Bot Pixel: a real 1x1 image (fires via normal image-load, unlike the click-only trap above),
   kept out of layout flow and invisible to screen readers/humans the same way. */
.pub-bot-pixel {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  border: 0 !important;
  pointer-events: none !important;
}

.pub-lightbox[hidden] {
  display: none;
}

.pub-lightbox {
  position: fixed;
  inset: 0;
  z-index: 1400;
  background: rgba(5, 8, 15, 0.86);
  display: grid;
  place-items: center;
  padding: 24px;
}

.pub-lightbox img {
  /* Not max-width/height: 100% - .pub-lightbox is `display: grid; place-items: center` with no
     explicit track size, so the grid cell sizes itself to the image's own natural dimensions
     rather than to the viewport. That makes a 100% max-width/height a percentage resolving
     against the item's own size (a circular reference), which browsers treat as no constraint at
     all - the image just renders near its native pixel size and overflows a smaller viewport
     instead of scaling down to fit. Tying the cap directly to the viewport (minus the container's
     own 24px padding on each side) sidesteps the ambiguity entirely. */
  max-width: calc(100vw - 48px);
  max-height: calc(100vh - 48px);
  border-radius: 12px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
}

.pub-lightbox-close {
  position: absolute;
  top: 10px;
  right: 22px;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 0;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  font-size: 1.4rem;
  cursor: pointer;
  /* Without explicit centering the x sits high in the circle (block display + line-height:
     normal leaves it positioned by the font's own ascent/descent, not the box's true center) -
     same fix .pub-share-nav's arrows already use. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  line-height: 1;
}

.pub-share-backdrop[hidden] {
  display: none;
}

.pub-share-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1500;
  background: rgba(5, 8, 15, 0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.pub-share-modal {
  position: relative;
  width: min(380px, 100%);
  max-height: 90vh;
  overflow-y: auto;
  background: #fff;
  border-radius: 20px;
  padding: 18px;
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.4);
}

.pub-share-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.pub-share-header h2 {
  font-family: "Bricolage Grotesque", sans-serif;
  font-size: 1.1rem;
  color: #14141a;
}

.pub-share-close {
  position: static;
  width: 30px;
  height: 30px;
  font-size: 1.1rem;
}

/* .pub-lightbox-close's base style (white icon, translucent-white circle) is only readable on
   the one true dark full-bleed lightbox (#pubLightbox, the image viewer). Every share/QR/picker/
   about/verified/premium modal reuses the same class but sits on .pub-share-modal's white card,
   so without this it's a white "x" on white - invisible, not a dark/light system-mode issue. */
.pub-share-modal .pub-lightbox-close {
  background: #f1f3f5;
  color: #14141a;
}

/* "More from SotoLink" modal (creators/index.php's footer button) - reuses .pub-share-modal's
   shell (same close button, same header) but is wider and taller than the About/Verified modals
   since it carries a quick-links list plus a full SotoCore ad, not just a short profile summary. */
.more-from-modal {
  width: min(420px, 100%);
}

.more-from-links {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 16px;
}

.more-from-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 12px;
  background: #f8f9fb;
  color: #14141a;
  text-decoration: none;
}

.more-from-row:hover {
  background: #f1f3f5;
}

.more-from-row i {
  width: 20px;
  text-align: center;
  color: var(--brand-blue);
  font-size: 0.95rem;
}

.more-from-row span {
  display: flex;
  flex-direction: column;
  font-weight: 700;
  font-size: 0.88rem;
}

.more-from-row small {
  font-weight: 500;
  color: #6b7280;
  font-size: 0.76rem;
}

.more-from-sotocore {
  border-top: 1px solid #eef0f3;
  padding-top: 16px;
}

.more-from-sotocore-kicker {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.7rem;
  font-weight: 800;
  color: var(--brand-coral);
  margin: 0 0 4px;
}

.more-from-sotocore h3 {
  font-family: "Bricolage Grotesque", sans-serif;
  font-size: 1.05rem;
  margin: 0 0 8px;
  color: #14141a;
}

.more-from-sotocore > p {
  font-size: 0.85rem;
  color: #4b5563;
  margin: 0 0 12px;
  line-height: 1.5;
}

.more-from-sotocore ul {
  margin: 0 0 16px;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.more-from-sotocore li {
  font-size: 0.82rem;
  color: #4b5563;
  line-height: 1.45;
}

.more-from-sotocore-cta {
  display: block;
  width: 100%;
  text-align: center;
}

.pub-share-preview {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 2px;
  padding: 14px 18px;
  border-radius: 14px;
  margin-bottom: 12px;
  background: linear-gradient(150deg, var(--accent, #ff5f6d), var(--secondary, #0ea5e9));
}

body[data-background="solid"] .pub-share-preview {
  background: var(--accent, #ff5f6d);
}

body[data-background="midnight"] .pub-share-preview {
  background: linear-gradient(165deg, #0b0f1a, #1b2036 60%, #101425);
}

.pub-link-share-preview {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 4px;
  padding: 24px 18px;
  border-radius: 14px;
  margin-bottom: 18px;
  overflow: hidden;
  background: linear-gradient(150deg, var(--accent, #ff5f6d), var(--secondary, #0ea5e9));
}

.pub-link-share-preview-icon {
  width: 56px;
  height: 56px;
  border-radius: 12px;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.9);
  color: #475569;
  font-size: 1.4rem;
  margin-bottom: 10px;
  overflow: hidden;
}

.pub-link-share-preview-icon img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.pub-link-share-preview.has-og-image {
  padding: 0;
  min-height: 180px;
  justify-content: flex-end;
  background: #0b0f1a;
}

.pub-link-share-preview.has-og-image .pub-link-share-preview-icon {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border-radius: 0;
  margin: 0;
  background: none;
}

.pub-link-share-preview.has-og-image .pub-link-share-preview-icon img {
  object-fit: cover;
  filter: brightness(0.55);
}

.pub-link-share-preview.has-og-image .pub-link-share-preview-title,
.pub-link-share-preview.has-og-image .pub-link-share-preview-url {
  position: relative;
  z-index: 1;
  padding: 0 16px;
}

.pub-link-share-preview.has-og-image .pub-link-share-preview-title {
  margin-top: 0;
}

.pub-link-share-preview.has-og-image .pub-link-share-preview-url {
  margin-bottom: 18px;
}

.pub-link-share-preview-title {
  color: #fff;
  font-weight: 800;
  font-size: 1rem;
}

.pub-link-share-preview-url {
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.8rem;
}

.pub-share-avatar {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  margin-bottom: 6px;
  background: #fff;
  border: 2px solid rgba(255, 255, 255, 0.6);
  overflow: hidden;
  display: grid;
  place-items: center;
  color: #fff;
  font-weight: 800;
  font-size: 1.1rem;
  overflow: hidden;
}

.pub-share-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pub-share-name {
  font-family: "Bricolage Grotesque", sans-serif;
  font-weight: 800;
  font-size: 1.1rem;
  color: #fff;
}

.pub-share-name-mark {
  width: 0.7em;
  height: 0.7em;
  vertical-align: -0.02em;
  margin-right: 0.22em;
}

/* .creator-verified-badge's own base size (1.9em) assumes the much larger .creator-name heading,
   or gets overridden to a fixed 30px via .is-on-avatar on the main profile - this inline-in-text
   usage (creators/index.php's render_verified_badge_svg() calls inside .pub-share-name) has
   neither, so without this it renders at ~1.9x the name text's own height. 1em (not the original
   0.75em) so the seal's scalloped shape and checkmark are still legible at .pub-share-name's own
   small font-size, instead of shrinking past the point the badge reads as anything but a blur. */
.pub-share-name .creator-verified-badge {
  width: 1em;
  height: 1em;
  vertical-align: -0.22em;
  margin-left: 0.15em;
}

.pub-share-slug {
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.85);
}

.creator-handle-branded {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.creator-handle-mark {
  width: 0.95em;
  height: 0.95em;
  object-fit: contain;
  flex: 0 0 auto;
}

.pub-share-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
}

/* Only two QR formats (PNG/PDF) - a dedicated 2-column layout instead of leaving an empty gap
   in the shared 3-column .pub-share-grid built for the page-share carousel. */
.qr-format-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.pub-qr-preview {
  display: flex;
  justify-content: center;
  padding: 20px;
  border-radius: 14px;
  margin-bottom: 16px;
  background: #fff;
}

.pub-qr-preview canvas {
  width: 100%;
  max-width: 220px;
  height: auto;
  display: block;
}

.pub-share-carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  gap: 12px;
  padding-bottom: 4px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* The 76px-wide items never divide evenly into the carousel's available width, so whichever
     item lands on the edge gets hard-clipped by overflow-x: auto mid-icon, right up against the
     nav arrow's circle - reads as broken rather than as a "peek" hint that there's more to
     scroll. Fading both edges instead makes a partially-visible item look deliberate. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 14px, #000 calc(100% - 14px), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 14px, #000 calc(100% - 14px), transparent 100%);
}

.pub-share-carousel::-webkit-scrollbar {
  display: none;
}

.pub-share-carousel .pub-share-target {
  flex: 0 0 76px;
  scroll-snap-align: start;
}

.pub-share-carousel-wrap {
  position: relative;
  display: flex;
  align-items: center;
  gap: 6px;
}

.pub-share-carousel-wrap .pub-share-carousel {
  flex: 1 1 auto;
  min-width: 0;
}

.pub-share-nav {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 0;
  background: #f1f3f5;
  color: #14141a;
  display: grid;
  place-items: center;
  cursor: pointer;
  flex: 0 0 auto;
  font-size: 0.8rem;
}

.pub-share-nav:disabled {
  opacity: 0.35;
  cursor: default;
}

.pub-share-target {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 4px;
  border-radius: 10px;
  border: 1px solid #e2e8f0;
  background: #fff;
  color: #475569;
  font-size: 0.72rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.pub-share-target:hover {
  border-color: #94a3b8;
  background: #f8f9fa;
}

/* Same-icon account picker (creators/index.php's render_link_anchor()/dedupe_links_by_icon()) -
   a row per option instead of .pub-share-target's grid-of-squares, since a list of accounts
   reads better top-to-bottom than side-by-side. */
.pub-link-picker-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.pub-link-picker-option {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: 12px;
  border: 1px solid #e2e8f0;
  background: #fff;
  color: #14141a;
  text-decoration: none;
  font-weight: 600;
  font-size: 0.9rem;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.pub-link-picker-option:hover {
  border-color: #94a3b8;
  background: #f8f9fa;
}

.pub-link-picker-option-icon {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border-radius: 10px;
  display: grid;
  place-items: center;
  background: #f1f5f9;
  color: #475569;
  overflow: hidden;
}

.pub-link-picker-option-icon img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.pub-link-picker-option-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pub-share-icon {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: transparent;
  overflow: hidden;
}

.pub-share-icon img,
.pub-share-icon svg {
  width: 20px;
  height: 20px;
  object-fit: contain;
}

.pub-share-icon i.fa-brands {
  font-size: 1.1rem;
}

.pub-share-icon-copy {
  color: #14141a;
  font-size: 0.9rem;
}

.pub-share-promo {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid #e2e8f0;
  text-align: center;
}

.pub-share-promo p {
  color: #14141a;
  font-weight: 700;
  font-size: 0.9rem;
}

.pub-share-promo p strong {
  color: var(--brand-coral, #ff5f6d);
}

.pub-share-promo-sub {
  color: #64748b !important;
  font-weight: 500 !important;
  font-size: 0.8rem !important;
  margin-top: 4px;
}

.pub-share-promo-btn {
  display: inline-block;
  margin-top: 8px;
  padding: 8px 22px;
  border-radius: 999px;
  background: #0b0f1a;
  color: #fff;
  font-weight: 700;
  font-size: 0.85rem;
  text-decoration: none;
}

@media (max-width: 640px), (max-height: 640px) {
  .creator-topbar {
    width: min(1120px, 94%);
  }
}

/* Premium look: unlocked avatars drop the standard 4px ring - see .creator-avatar border above.
   The avatar_border profile setting (Design tab) overrides this default in either direction. */
.creator-avatar.is-premium {
  border: none;
}

.creator-avatar.no-avatar-border {
  border: none;
}

.creator-avatar.is-premium.has-avatar-border {
  border-style: solid;
  border-width: var(--avatar-border-width, 4px);
  border-color: var(--avatar-border-color, #fff);
}

.creator-verified-badge-btn {
  background: none;
  border: 0;
  padding: 0;
  margin-left: -0.2em;
  cursor: pointer;
  display: inline-flex;
  vertical-align: middle;
}

/* Overlapping the avatar's bottom-right corner instead of sitting inline after the name -
   .creator-avatar-wrap (an unclipped ancestor of the circular, overflow:hidden avatar) is what
   this actually positions against. .creator-avatar.has-badge-cutout (mask-image set inline from
   verified_badge_cutout_mask_url() in creators/index.php) punches a hole shaped exactly like this
   badge at the same size/position, so the photo behind it doesn't show through - no drop-shadow
   filter here (removed - at this badge's small size, even a few px of blur radius read as a soft
   glowing halo around the whole shape rather than a crisp shadow). Sizes here MUST stay in sync
   with the mask-size/position below and with the desktop override further down - they're
   independent rules (a data URI can't itself respond to a CSS media query) so this can't be
   consolidated into one place. */
.creator-verified-badge-btn.is-on-avatar {
  position: absolute;
  bottom: -1px;
  right: -1px;
  margin-left: 0;
  width: 30px;
  height: 30px;
  padding: 0;
  background: none;
  border-radius: 3px;
}

/* A circle's edge recedes ~27px from the bounding-box corner at a 46px radius (92px avatar) -
   plenty of "empty" corner space for the badge above to nest into at -1px. A square's 18px
   corner radius (.creator-avatar.is-square) recedes barely at all by comparison, so the same
   -1px offset just sits the badge on top of real photo/border content instead of nesting into a
   gap, cutting the border ring off abruptly rather than curving away from it. Pushed further
   outward here specifically for the square shape so more of the badge hangs into the transparent
   margin past the avatar's true edge, the same way it already does for circle. */
.creator-avatar-wrap:has(.creator-avatar.is-square) .creator-verified-badge-btn.is-on-avatar {
  bottom: -7px;
  right: -7px;
}

.creator-avatar.has-badge-cutout {
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

.creator-verified-badge-btn.is-on-avatar .creator-verified-badge {
  width: 100%;
  height: 100%;
}

.creator-verified-badge {
  color: #3b82f6;
  width: 1.9em;
  height: 1.9em;
  vertical-align: middle;
  flex: 0 0 auto;
}

/* The base 1.9em/middle above renders way oversized next to actual heading text in practice -
   reuses the exact same 1em/-0.22em combo already proven out for .pub-share-name's own
   inline-badge-next-to-bold-name case instead. Being em-based, it still scales with .creator-name's
   own (larger) font-size automatically, just at the same well-tuned proportions, not 1.9x them. */
.creator-name .creator-verified-badge {
  width: 1em;
  height: 1em;
  vertical-align: -0.22em;
  margin-left: 0.15em;
}

.creator-free-badge-btn {
  display: inline-flex;
  align-items: center;
  vertical-align: middle;
  margin-left: 6px;
  background: rgba(148, 163, 184, 0.22);
  color: inherit;
  border: 1px solid rgba(148, 163, 184, 0.4);
  border-radius: 999px;
  padding: 2px 9px;
  font-size: 0.52em;
  font-weight: 800;
  letter-spacing: 0.04em;
  cursor: pointer;
  vertical-align: middle;
}

.creator-free-badge-btn:hover {
  background: rgba(148, 163, 184, 0.32);
}

.creator-badge-shine-rect {
  transform-box: fill-box;
  transform-origin: center;
  animation: creator-badge-shine-sweep 2.6s ease-in-out infinite;
}

/* 60px (not the badge's own ~23-unit right edge + a hair) so the shine bar is well clear of the
   clipped badge shape - not just barely past it - by the time it holds and then snaps back to
   -10px for the next loop. The old 34px target left only a sliver of margin, thin enough that the
   reset could show as a visible flicker instead of happening fully off-screen. 85% (not 55%) keeps
   the visible sweep's speed close to the original despite the added travel distance. */
@keyframes creator-badge-shine-sweep {
  0% {
    transform: translateX(-10px);
  }
  85%,
  100% {
    transform: translateX(60px);
  }
}

/* transform-box: view-box (not fill-box) - fill-box computes "center" from this group's own
   rendered content bounds, which includes the shine rect's geometry even though it's clipped
   invisible offscreen at x="-10". That far-left rect was dragging the fill-box bounding box (and
   so the rotation pivot) well left of the badge's actual visual center, so instead of spinning in
   place the badge swung through a visible arc - reading as drifting rather than a clean wobble.
   view-box pins the origin to the SVG's own coordinate system instead, at the rosette's true
   center (viewBox is "0 0 24 24"), regardless of what else the group contains. */
.creator-badge-spin {
  transform-box: view-box;
  transform-origin: 12px 12px;
  animation: creator-badge-wobble 4.5s ease-in-out infinite;
}

@keyframes creator-badge-wobble {
  0%,
  100% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(14deg);
  }
}


.creator-verified-badge.is-sotocore {
  color: #d4a017;
}

.creator-verified-badge.is-concierge {
  color: #7c3aed;
}

.creator-about-disclaimer {
  color: #64748b;
  font-size: 0.76rem;
  text-align: center;
  margin-bottom: 14px;
}

.creator-about-links-heading {
  margin-top: 14px;
  color: #14141a;
  font-weight: 700;
  font-size: 0.82rem;
  text-align: center;
}

.creator-about-links-list {
  margin-top: 8px;
  list-style: none;
  text-align: center;
}

.creator-about-links-list li {
  color: #475569;
  font-size: 0.82rem;
  padding: 3px 0;
}

.creator-verified-modal {
  position: relative;
  text-align: center;
  background-color: #d9dde3;
  background-image:
    radial-gradient(rgba(15, 23, 42, 0.08) 1.2px, transparent 1.2px),
    linear-gradient(135deg, #f5f6f8 0%, #dde1e6 22%, #ffffff 45%, #c7ccd4 65%, #eef0f3 85%, #d4d8de 100%);
  background-size: 16px 16px, 100% 100%;
}

.creator-verified-modal-avatar {
  position: relative;
  width: 84px;
  height: 84px;
  margin: 8px auto 16px;
}

.creator-verified-modal-avatar img,
.creator-verified-modal-avatar span {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  display: grid;
  place-items: center;
  background: #f1f5f9;
  color: #475569;
  font-weight: 800;
  font-size: 1.6rem;
}

.creator-verified-modal-check {
  position: absolute;
  bottom: -4px;
  right: -4px;
  width: 22px;
  height: 22px;
  padding: 4px;
  box-sizing: content-box;
  color: #3b82f6;
  background: #fff;
  border-radius: 50%;
}

.creator-verified-modal-check.is-sotocore {
  color: #d4a017;
}

.creator-verified-modal-check.is-concierge {
  color: #7c3aed;
}

.creator-verified-modal h2 {
  font-size: 1.15rem;
  font-weight: 800;
  color: #14141a;
  margin-bottom: 10px;
}

.creator-verified-modal p {
  color: #475569;
  font-size: 0.85rem;
}

.creator-premium-modal .auth-kicker {
  color: #7c3aed;
}

.creator-premium-modal h2 {
  color: #14141a;
  font-size: 1.3rem;
  font-weight: 800;
  line-height: 1.2;
}

.creator-premium-benefits {
  list-style: none;
  margin: 16px 0;
}

.creator-premium-benefits li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  color: #14141a;
  font-size: 0.85rem;
  font-weight: 600;
}

.creator-premium-benefits li i {
  color: #16a34a;
  width: 16px;
}

.creator-premium-upgrade-btn {
  display: block;
  text-align: center;
  padding: 12px 20px;
  border-radius: 999px;
  background: #0b0f1a;
  color: #fff;
  font-weight: 700;
  font-size: 0.9rem;
  text-decoration: none;
  margin-top: 6px;
}

.creator-verified-modal-promo {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid #e2e8f0;
  color: #64748b !important;
  font-size: 0.78rem !important;
}

/* icon_style = 'mono': hide the brand-color <img> and paint a flat accent-colored silhouette
   instead, using the same image as a CSS mask so no duplicate asset is needed. */
body[data-icon="mono"] .pub-link-chip-icon,
body[data-icon="mono"] .pub-link-icon {
  background: none;
  color: var(--icon-color, #ffffff);
}

/* Icon row specifically: no chip/circle container at all, just the bare glyph floating on
   the page - matches the plain outlined-icon-row look this mode is meant to produce. */
body[data-icon="mono"] .pub-link-row--icons .pub-link-chip {
  background: none;
  box-shadow: none;
}

/* object-fit: cover (not plain contain-scaling) for the same reason as
   .pub-social-footer .pub-link-chip-icon svg below - every glyph's own source viewBox is a
   different aspect ratio, and a plain width+height box contain-scales each one differently,
   leaving them visibly different sizes. This rule's higher specificity (body[data-icon="mono"] +
   two classes + element) wins over that other rule regardless of source order, so it needs the
   same fix independently rather than inheriting it. */
body[data-icon="mono"] .pub-link-chip-icon svg,
body[data-icon="mono"] .pub-link-icon svg {
  width: 60%;
  height: 60%;
  object-fit: cover;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
}

/* icon_style = 'logo': same background removal as mono above, but no color override - the
   glyph's fill is the platform's own fixed brand color baked into the SVG by logo_icon_glyph_svg(),
   not currentColor, so there's nothing here for a creator's icon_color choice to apply to. */
body[data-icon="logo"] .pub-link-chip-icon,
body[data-icon="logo"] .pub-link-icon {
  background: none;
}

body[data-icon="logo"] .pub-link-row--icons .pub-link-chip {
  background: none;
  box-shadow: none;
}

body[data-icon="logo"] .pub-link-chip-icon svg,
body[data-icon="logo"] .pub-link-icon svg {
  width: 60%;
  height: 60%;
  object-fit: cover;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
}

/* Social icons row: always flat glyphs in one shared color, no chip background - independent
   of the site-wide Icon color/style setting, which only affects the regular link cards/rows.
   border: none too, otherwise turning on the 3D shadow card style would leak a border-only ring
   here even though its box-shadow gets correctly suppressed below (this rule's higher
   specificity beats body[data-card-shadow="1"] .pub-link-chip regardless of source order). */
.pub-social-footer.pub-link-row--icons .pub-link-chip {
  background: none;
  box-shadow: none;
  border: none;
}

/* Qualified with body (matching body[data-icon="mono"] .pub-link-chip-icon's own specificity)
   so this reliably wins regardless of source order - without it, the site-wide Flat icon style's
   color rule silently overrode this row's own color whenever icon_style was mono, contradicting
   the "independent of the site-wide Icon color" comment above. */
body .pub-social-footer .pub-link-chip-icon {
  background: none;
  color: var(--social-footer-icon-color, #ffffff);
}

/* object-fit: cover (not the default "contain" behavior of a plain width+height box) so every
   icon fills the same square footprint regardless of its own source viewBox aspect ratio - a
   square viewBox (e.g. the X logo's 640x640) previously rendered visibly larger/wider than a
   taller one (e.g. Instagram/TikTok's 448x512) once both got contain-scaled into the same box.
   A height-only auto-width fix still left them different widths (matching height isn't the same
   as matching apparent size); cover crops instead of shrinking, so every icon ends up the same
   width AND height. Brand icon viewBoxes are all close enough to square that the crop is
   imperceptible - nothing meaningful sits near the edges of any of them. */
.pub-social-footer .pub-link-chip-icon svg {
  width: 72%;
  height: 72%;
  object-fit: cover;
}

/* Opt-in, not opt-out - flat social icons read cleanest with no shadow at all by default. */
body[data-social-icon-shadow="1"] .pub-social-footer .pub-link-chip-icon svg {
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.55));
}

.creator-links-footer {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 14px 20px 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  flex-wrap: wrap;
}

.creator-links-footer a,
.creator-links-footer-btn {
  color: rgba(255, 255, 255, 0.75);
  font-size: 0.72rem;
  text-decoration: none;
  font-weight: 600;
  /* Site-owned chrome (legal links + the About panel trigger), not part of the creator's own
     brand - a fixed drop shadow (same device .creator-qr-label already uses) guarantees this
     stays legible no matter what canvas/card color or image the creator picks underneath it,
     without needing a background chip. Deliberately not themed by the creator's own
     accent/canvas/card colors, and not by the visitor's site-wide light/dark toggle either -
     this row's contrast can't depend on either one. */
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
}

.creator-links-footer-btn {
  background: none;
  border: 0;
  padding: 0;
  font-family: inherit;
  cursor: pointer;
}

.creator-links-footer a:hover,
.creator-links-footer-btn:hover {
  color: #fff;
  text-decoration: underline;
}

.creator-links-footer span {
  color: rgba(255, 255, 255, 0.4);
  font-size: 0.7rem;
}

.creator-about-joined {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 4px;
  padding-top: 14px;
  border-top: 1px solid #e2e8f0;
  color: #64748b;
  font-size: 0.8rem;
  font-weight: 600;
}

.creator-about-bio {
  margin-top: 10px;
  color: #475569;
  font-size: 0.82rem;
  text-align: center;
}

.creator-logo-modal {
  position: relative;
  width: min(380px, 100%);
  max-height: 90vh;
  overflow-y: auto;
  text-align: left;
  border-radius: 24px;
  padding: 34px 28px;
  background: linear-gradient(150deg, var(--brand-gold, #ffcc3f), var(--brand-coral, #ff5f6d) 55%, var(--brand-blue, #0ea5e9));
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.4);
  color: #14141a;
}

.creator-logo-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 0;
  background: rgba(255, 255, 255, 0.5);
  color: #14141a;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  display: grid;
  place-items: center;
}

.creator-logo-modal-kicker {
  font-weight: 800;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 10px;
}

.creator-logo-modal h2 {
  font-family: "Bricolage Grotesque", sans-serif;
  font-size: 1.5rem;
  font-weight: 800;
  line-height: 1.15;
  margin-bottom: 10px;
}

.creator-logo-modal-sub {
  font-size: 0.88rem;
  color: rgba(20, 20, 26, 0.8);
  margin-bottom: 20px;
}

.creator-logo-modal-cta {
  display: block;
  text-align: center;
  padding: 12px 20px;
  border-radius: 999px;
  background: #14141a;
  color: #fff;
  font-weight: 700;
  font-size: 0.92rem;
  text-decoration: none;
  margin-bottom: 12px;
}

.creator-logo-modal-secondary {
  display: block;
  text-align: center;
  color: #14141a;
  font-weight: 600;
  font-size: 0.8rem;
  text-decoration: underline;
}

/* Desktop gets a bigger, wider layout instead of just a phone-width card floating in extra
   whitespace - the canvas card itself, the avatar, the name/bio/slug text, and the QR widget all
   scale up a step. 900px (not just "wider than mobile") so this only kicks in on genuinely
   spacious viewports, not tablet-ish widths that still want the compact layout. Placed at the
   very end of the file on purpose - same-specificity rules earlier in the file (e.g. the base
   .creator-canvas width) were winning over this by source order despite the media query
   matching, since a plain class selector inside @media has no specificity edge over the same
   selector outside it. */
@media (min-width: 900px) {
  .creator-canvas {
    width: min(680px, 90%);
  }

  .creator-avatar {
    width: 128px;
    height: 128px;
  }

  /* Scaled up to match the bigger desktop avatar - the mask itself doesn't need a matching
     override here (mask-size: 100% 100% already covers the whole avatar proportionally at any
     size), just the visible badge button. */
  .creator-verified-badge-btn.is-on-avatar {
    bottom: -1px;
    right: -1px;
    width: 42px;
    height: 42px;
  }

  /* Same reasoning as the mobile .is-square override above, scaled for the bigger 42px badge -
     .creator-avatar.is-square's corner radius doesn't itself scale up at this breakpoint (still a
     flat 18px, same as mobile), so the mismatch is actually worse at desktop size and needs a
     bigger push. */
  .creator-avatar-wrap:has(.creator-avatar.is-square) .creator-verified-badge-btn.is-on-avatar {
    bottom: -10px;
    right: -10px;
  }

  .creator-name {
    font-size: 2rem;
  }

  .creator-bio {
    font-size: 1.08rem;
  }

  .creator-slug {
    font-size: 1rem;
  }

  .creator-qr-code {
    width: 190px;
    height: 190px;
  }

  .creator-qr-label {
    font-size: 1rem;
  }
}
