/* --- General Setup & Fonts --- */
/* Font is loaded with <link rel="stylesheet"> in each page head instead.
   A CSS @import cannot start downloading until this file has been fetched
   and parsed, which serialises two round trips and delays first paint. */

:root {
    --primary-color: #ffffff;
    --accent-color: #ff69b4; /* Hot Pink for accents */
    --text-color: #f0f0f0;
    --glass-bg: rgba(255, 255, 255, 0.01);
    --glass-border: rgba(255, 255, 255, 0.2);
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Poppins', sans-serif;
    color: var(--text-color);
    background-color: #000;
    line-height: 1.6;
    overflow-x: hidden;
}

main {
    padding: 100px 20px 120px 20px;
    max-width: 1200px;
    margin: 0 auto;
}

h1, h2, h3 {
    font-weight: 600;
    margin-bottom: 1rem;
    text-align: center;
    text-shadow: 0 0 10px rgba(0,0,0,0.5);
}

p {
    margin-bottom: 1rem;
    text-align: center;
}

a {
    color: var(--primary-color);
    text-decoration: none;
}

/* --- Glassmorphism Effect --- */
.glass-container, .glass-button {
    background: var(--glass-bg);
    backdrop-filter: blur(15px);
    -webkit-backdrop-filter: blur(15px);
    border: 1px solid var(--glass-border);
    border-radius: 20px;
    box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
}

/* --- Background Animations --- */
#cosmic-background {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
    background: radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%);
    overflow: hidden;
    z-index: -1; /* CRITICAL: This keeps it behind all other content */
}

@keyframes animate-stars {
    from {
        transform: translateY(0px);
    }
    to {
        transform: translateY(-2000px);
    }
}

.stars-small, .stars-medium, .stars-large {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    display: block;
}

/* This trick creates a second set of stars below the viewport that scrolls up, creating an infinite loop */
.stars-small:after, .stars-medium:after, .stars-large:after {
    content: " ";
    position: absolute;
    top: 2000px;
    width: inherit;
    height: inherit;
    background: inherit;
    box-shadow: inherit;
}

/* Layer 1: Small & Fast */
.stars-small {
    width: 1px;
    height: 1px;
    background: transparent;
    animation: animate-stars 50s linear infinite;
    /* Star positions are set here */
    box-shadow: 692px 1388px #FFF, 1264px 1056px #FFF, 82px 1478px #FFF, 1236px 458px #FFF, 1450px 780px #FFF, 478px 1332px #FFF, 150px 186px #FFF, 42px 1974px #FFF, 1370px 162px #FFF, 1334px 1144px #FFF, 1060px 1146px #FFF, 1558px 1088px #FFF, 190px 480px #FFF, 1342px 1056px #FFF, 1024px 1996px #FFF, 1362px 1264px #FFF, 182px 282px #FFF, 1828px 1464px #FFF, 1340px 1822px #FFF, 1724px 1928px #FFF;
}

/* Layer 2: Medium & Slower */
.stars-medium {
    width: 2px;
    height: 2px;
    background: transparent;
    animation: animate-stars 100s linear infinite;
    box-shadow: 104px 1290px #FFF, 1454px 1162px #FFF, 868px 1318px #FFF, 760px 1924px #FFF, 42px 1804px #FFF, 1720px 1438px #FFF, 942px 100px #FFF, 142px 1134px #FFF, 144px 1978px #FFF, 1512px 1474px #FFF, 44px 1566px #FFF, 1136px 144px #FFF, 1136px 1222px #FFF, 1248px 1238px #FFF, 720px 1626px #FFF, 706px 1146px #FFF, 614px 102px #FFF, 1358px 456px #FFF;
}

/* Layer 3: Large & Slowest */
.stars-large {
    width: 3px;
    height: 3px;
    background: transparent;
    animation: animate-stars 150s linear infinite;
    box-shadow: 1844px 1734px #FFF, 112px 1542px #FFF, 246px 1592px #FFF, 1344px 502px #FFF, 712px 1790px #FFF, 1898px 1770px #FFF, 1000px 336px #FFF, 1332px 1138px #FFF, 934px 1888px #FFF, 624px 1252px #FFF, 608px 1104px #FFF, 1284px 926px #FFF, 428px 1488px #FFF, 110px 470px #FFF, 1030px 60px #FFF;
}

/* Day Sky Background */
#day-sky-background {
    position: fixed;
    width: 100%;
    height: 100%;
    background: linear-gradient(to bottom, #87CEEB, #f0f8ff);
    overflow: hidden;
    z-index: -1;
}

.sun {
    position: absolute;
    top: 50px;
    right: 50px;
    width: 100px;
    height: 100px;
    background: #FFD700;
    border-radius: 50%;
    box-shadow: 0 0 50px #FFD700;
}

.cloud {
    position: absolute;
    width: 200px;
    height: 60px;
    background: #fff;
    border-radius: 60px;
    opacity: 0.8;
}
.cloud:before, .cloud:after {
    content: '';
    position: absolute;
    background: #fff;
    border-radius: 50%;
}
.cloud:before { width: 80px; height: 80px; top: -40px; left: 20px; }
.cloud:after { width: 100px; height: 100px; top: -50px; right: 20px; }
.cloud1 { top: 10%; left: -250px; animation: moveclouds 35s linear infinite; }
.cloud2 { top: 25%; left: -250px; animation: moveclouds 20s linear infinite 5s; }
.cloud3 { top: 50%; left: -250px; animation: moveclouds 30s linear infinite 10s; }

@keyframes moveclouds {
    0% { transform: translateX(0); }
    100% { transform: translateX(calc(100vw + 250px)); }
}

/* --- Header & Navigation --- */
header {
    position: fixed;
    top: 10px;
    left: 10px;
    right: 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 20px;
    z-index: 1000;
}

.logo {
    font-size: 1.8rem;
    font-weight: 700;
    text-align: middle;
}

nav a {
    margin-left: 20px;
    font-weight: 400;
    transition: color 0.3s, text-shadow 0.3s;
}

nav a.active, nav a:hover {
    color: var(--accent-color);
}

/* --- Floating Footer --- */
.floating-footer {
    position: fixed;
    bottom: 10px;
    left: 50%;
    transform: translateX(-50%);
    padding: 10px 20px;
    display: flex;
    align-items: center;
    gap: 15px;
    z-index: 1000;
}

.social-icons {
    display: flex;
    gap: 10px;
}

.social-icons a {
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    font-size: 1.2rem;
    font-weight: bold;
    border-radius: 50%;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    transition: background 0.3s, color 0.3s;
}

.social-icons a:hover {
    background: var(--accent-color);
    color: #000;
}

/* --- Home Page: Showcase & Book Grid --- */
#showcase-section, #book-series {
    margin-bottom: 4rem;
}

.swiper-container {
    width: 100%;
    padding-top: 20px;
    padding-bottom: 50px;
}

.swiper-slide.card {
    background-position: center;
    background-size: cover;
    width: 250px;
    height: 400px;
    border-radius: 18px;
    box-shadow: 0 10px 30px rgba(45, 201, 240, 0.5);
}

.swiper-button-next, .swiper-button-prev {
    color: var(--primary-color) !important;
    padding: 20px;
    height: 20px !important;
    width: 20px !important;
}

.swiper-pagination-bullet-active {
    background: var(--accent-color) !important;
}

.book-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 30px;
}

.book-tile {
    text-align: center;
}

.book-tile a{display:block}
.book-tile img{display:block;width:100%;height:auto;object-fit:cover}

.book-tile img {
    width: 100%;
    height: auto;
    border-radius: 10px;
    border: 3px solid var(--glass-border);
    transition: transform 0.3s, box-shadow 0.3s;
}

.book-tile a:hover img {
    transform: scale(1.05);
    box-shadow: 0 0 25px var(--accent-color);
}

.star-rating {
    cursor: pointer;
    font-size: 1.5rem;
}
.star-rating span:hover, .star-rating span.active {
    color: var(--accent-color);
}


/* --- Gallery Page --- */
#submission-section {
    padding: 2rem;
    max-width: 600px;
    margin: 0 auto 3rem auto;
}

#upload-form {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
    align-items: center;
    justify-content: center;
}

.form-group {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-grow: 1;
}

#upload-form input[type="file"] { display: none; }
#upload-form label { cursor: pointer; padding: 10px 20px; }

#upload-form input[type="text"] {
    flex-grow: 1;
    background: transparent;
    border: none;
    outline: none;
    color: var(--primary-color);
    padding: 10px;
    min-width: 200px;
    border-bottom: 1px solid var(--glass-border);
}
#upload-form input[type="text"]::placeholder { color: var(--text-color); opacity: 0.7; }

.disclaimer {
    font-size: 0.8rem;
    margin-top: 1rem;
    opacity: 0.8;
}

#gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 20px;
}

.gallery-item {
    position: relative;
    border: 15px solid transparent;
    border-image: url('https://i.ibb.co/F8DDBw3/frame.png') 30 stretch; /* Simple frame */
    cursor: pointer;
}

.gallery-item img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.gallery-item .overlay {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 15px;
    color: white;
    font-size: 0.9rem;
    opacity: 0;
    transition: opacity 0.3s;
    background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
}

.gallery-item:hover .overlay {
    opacity: 1;
}

.overlay .caption {
    padding: 10px;
    margin-bottom: 5px;
    word-wrap: break-word;
}
.overlay .heart-rating {
    font-size: 1.5rem;
    cursor: pointer;
}
.heart-rating span:hover, .heart-rating span.active {
    color: var(--accent-color);
}

/* Lightbox Modal */
.lightbox {
    position: fixed;
    z-index: 2000;
    padding-top: 50px;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: rgba(0,0,0,0.9);
}

.hidden { display: none; }

.lightbox-content {
    margin: auto;
    display: block;
    max-width: 80%;
    max-height: 85%;
}

.close-btn {
    position: absolute;
    top: 15px;
    right: 35px;
    color: #f1f1f1;
    font-size: 40px;
    font-weight: bold;
    transition: 0.3s;
    cursor: pointer;
}


/* --- About Page --- */
.about-content section {
    padding: 2rem;
    max-width: 800px;
    margin: 2rem auto;
}

/* --- Gallery Page --- */
.gallery-content section {
    padding: 2rem;
    max-width: 800px;
    margin: 2rem auto !important;
}

/* --- Index Page --- */
.index-content section {
    padding: 2rem;
    max-width: 800px;
    margin: 2rem auto;
}

/* --- Shop Page --- */
.shop-content section {
    padding: 2rem;
    max-width: 800px;
    margin: 2rem auto;
}


.profile-pic {
    width: 150px;
    height: 150px;
    border-radius: 50%;
    border: 3px solid var(--glass-border);
    display: block;
    margin: 0 auto 1.5rem auto;
}

#feedback-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    align-items: center;
}
#feedback-form input, #feedback-form textarea {
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: 10px;
    padding: 12px;
    color: var(--primary-color);
    font-family: 'Poppins', sans-serif;
}
#feedback-form textarea { min-height: 100px; }
#feedback-form input::placeholder, #feedback-form textarea::placeholder { color: rgb(5, 69, 129); opacity: 0.7;} /* color: var(--text-color) */


.poll-option {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    border-bottom: 1px solid var(--glass-border);
}
.poll-option:last-child { border-bottom: none; }
.vote-btn { padding: 8px 15px; }


/* --- Shop Page --- */
.shop-section {
    padding: 3rem;
    max-width: 600px;
    margin: 3rem auto;
}

.shop-links {
    display: flex;
    justify-content: center;
    gap: 20px;
    margin-top: 2rem;
}

.shop-link {
    padding: 15px 30px;
    font-size: 1.1rem;
    transition: background 0.3s, transform 0.3s;
}
.shop-link:hover {
    background: var(--accent-color);
    color: #000;
    transform: translateY(-5px);
}

/* --- Responsive Design --- */
@media (max-width: 768px) {
    header {
        flex-direction: column;
        gap: 10px;
    }
    nav a { margin: 0 10px; }
    
    .logo { font-size: 1.5rem; }

    main { padding-top: 120px; }

    #gallery-grid {
        grid-template-columns: repeat(2, 1fr);
        gap: 10px;
    }
    
    .gallery-item {
        border-width: 10px;
    }
}

body.day { --text-color:#054581; --primary-color:#054581; color: var(--text-color) !important; }
body.day :where(h1,h2,h3,h4,h5,h6,p,span,label,strong,em,small,li){ color: var(--text-color) !important; text-shadow:none !important; }
body.day a{ color: var(--primary-color) !important; }



/* inputs and their placeholder text on the about page */
body.day input,
body.day textarea { 
  color: #054581 !important;
}
body.day input::placeholder,
body.day textarea::placeholder {
  color: rgba(5, 69, 129) !important;
}

/* the form itself */
/* keep the inputs full width inside the glass card */
#feedback-form input,
#feedback-form textarea{
  width: 100%;
}

#feedback-form .glass-button.vote-btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: auto !important;
  padding: 8px 15px;        /* same as your .vote-btn */
  border-radius: 999px;     /* true pill */
  line-height: 1;           /* vertical centering of text */
  font-weight: 600;         /* matches the chip look */
}

/* limit cover size on desktop, keep mobile nice */
.book-grid{
  grid-template-columns: repeat(auto-fit, minmax(220px, 320px));
  justify-content: center;
}

.book-tile{
  width: clamp(220px, 30vw, 320px);
  margin: 0 auto;
}

.book-tile img{
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 10px;
  display: block;
}

/* day pages: make hover and active pure hot pink */
body.day header nav a:hover,
body.day header nav a:focus,
body.day header nav a.active,
body.day header nav a[aria-current="page"]{
  color: var(--accent-color) !important;
}


.swiper-slide.card:hover,
.swiper-slide.card:focus-visible,
.swiper-slide.card:active,
.swiper-slide-active.card{
  box-shadow: 0 0 25px var(--accent-color) !important;
}

/* floating footer: same pill height as buttons */
.floating-footer{
  padding: 8px 15px;      /* same as .vote-btn */
  border-radius: 999px;   /* true pill */
}
.floating-footer p{ margin: 0; }
.floating-footer .social-icons a{
  width: 32px;            /* match button height */
  height: 32px;
  font-size: 1rem;
}

/* all buttons use accent pink text */
button,
.glass-button,
.vote-btn,
.shop-link{
  color: var(--accent-color) !important;
  text-shadow: none !important;
  border-radius: 999px;    /* consistent pill look */
  line-height: 1;
  font-weight: 600;
  padding: 8px 15px;       /* unify size */
}

/* keep shop hover readable (pink text, no solid pink fill) */
.shop-link:hover{
  background: var(--glass-bg);
  color: var(--accent-color);
  transform: translateY(-5px);
}

/* 2) Navigation link weight, lock to one weight everywhere */
header nav a{ font-weight: 400; }         /* base */
body.day header nav a{ font-weight: 400; }/* day theme too */

/* footer pill same height as buttons, stay on one line */
.floating-footer{
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 15px;
  border-radius: 999px;
  white-space: nowrap;                 /* do not wrap to rows */
  max-width: calc(100vw - 20px);       /* keep inside viewport */
}

.floating-footer p{ margin: 0; }       /* no extra line height */

/* icon chips trimmed so overall height matches */
.floating-footer .social-icons{ display: inline-flex; gap: 10px; }
.floating-footer .social-icons a{ width: 32px; height: 32px; font-size: 1rem; }

/* size the social icons inside the round buttons */
.social-icons a img{ width:30px; height:30px; display:block; !important }

/* keep the pills on one line and comfy on mobile */
.floating-footer{ white-space:nowrap; }

/* Share your Art button under the showcase */
/* Share your Art button under the showcase */
/* Share your Art button under the showcase */
.share-cta-wrap{
  display:flex;
  justify-content:center;
  margin-top:16px;
}
.cta-share{
  padding:8px 15px;
  border-radius:999px;
  font-weight:600;
  line-height:1;
  background:rgba(45, 201, 240, 0.281);
  border:1px solid rgba(45,201,240,0.76);
  color:#054581 !important;
  text-shadow:none;
  transition:box-shadow .2s, transform .2s, background .2s, border-color .2s;
  box-shadow:0 0 20px rgba(255, 105, 180, 0.795);
}
.cta-share:hover,
.cta-share:focus{
  box-shadow:0 0 30px rgba(45, 201, 240, 0.76);
  transform:translateY(-2px);
  background:rgba(255,105,180,0.24);
  border-color:rgba(255, 105, 180, 0.795);
}
@media (max-width:768px){
  .share-cta-wrap{ margin-top:10px; }
}






/* ============================================================================
   ADDITIONS, 2026-09-05
   Everything above this line is untouched. The backgrounds (cosmic, day sky,
   stars, clouds, nebula) are exactly as they were.

   This block adds: accessibility fixes, the new CSS carousel that replaced
   the Swiper library, legal page styling, and the rebuilt gallery/shop
   layouts. Nothing here changes an existing background rule.
   ========================================================================= */

/* --- Skip link (keyboard users land here first) --- */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  padding: 12px 20px;
  background: #054581;
  color: #fff !important;
  border-radius: 0 0 12px 0;
  font-weight: 600;
}
.skip-link:focus { left: 0; }

/* --- Visible focus on everything interactive.
       Replaces the `outline: none` that used to strip it. --- */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 3px;
  border-radius: 6px;
}
body.day a:focus-visible,
body.day button:focus-visible,
body.day input:focus-visible,
body.day textarea:focus-visible,
body.day [tabindex]:focus-visible {
  outline-color: #c2185b;
}
#upload-form input[type="text"]:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 2px;
}

/* --- Day-mode accent contrast.
       #ff69b4 on the light sky is about 2.8:1, which fails WCAG AA.
       #c2185b is about 5.9:1 and still unmistakably pink. --- */
body.day header nav a:hover,
body.day header nav a:focus,
body.day header nav a.active,
body.day header nav a[aria-current="page"] {
  color: #c2185b !important;
}

/* --- Reduced motion.
       This ONLY applies to visitors who have asked their operating system
       for reduced motion. For everyone else the stars and clouds move
       exactly as they always have. --- */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .stars-small, .stars-medium, .stars-large,
  .cloud, .cloud1, .cloud2, .cloud3,
  .asteroids, .twinkling {
    animation: none !important;
  }
}

/* --- The logo is now a <p>, not an <h1>, so each page's real subject can
       be the h1. These rules keep it looking identical. --- */
p.logo {
  margin-bottom: 0;
  text-align: left;
}
p.logo a {
  color: inherit !important;
  text-decoration: none;
}

/* --- Page titles --- */
.page-title {
  font-size: clamp(1.7rem, 4vw, 2.6rem);
  margin-bottom: 0.3rem;
}
.page-tagline {
  text-align: center;
  opacity: 0.9;
  margin-bottom: 2rem;
  font-size: 1.05rem;
}

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

body.no-scroll { overflow: hidden; }

/* ============================================================
   CAROUSEL, kept for reference. Not in use, the Swiper build is live.
   Native scroll-snap: swipes on touch, scrolls on trackpad,
   arrow keys work, and it costs zero kilobytes of JavaScript.
   ============================================================ */
.fm-carousel {
  position: relative;
  margin: 0 auto 1.5rem;
  max-width: 100%;
  border-radius: 20px;
}
.fm-carousel:focus-visible { outline: 3px solid #ff69b4; outline-offset: 6px; }
body.day .fm-carousel:focus-visible { outline-color: #c2185b; }

.fm-track {
  display: flex;
  gap: 24px;
  list-style: none;
  margin: 0;
  padding: 28px 12px 34px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.fm-track::-webkit-scrollbar { height: 8px; }
.fm-track::-webkit-scrollbar-thumb {
  background: rgba(255, 105, 180, 0.5);
  border-radius: 99px;
}

.fm-card {
  flex: 0 0 auto;
  width: min(260px, 72vw);
  min-height: 330px;
  scroll-snap-align: center;
  border-radius: 20px;
  padding: 26px 22px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: rgba(255, 255, 255, 0.10);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(255, 255, 255, 0.28);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
  transition: transform 0.3s, box-shadow 0.3s, border-color 0.3s;
}
.fm-card:hover { transform: translateY(-6px); }
body.day .fm-card {
  background: rgba(255, 255, 255, 0.42);
  border-color: rgba(5, 69, 129, 0.22);
}
.fm-card.is-highlight {
  border-color: rgba(255, 105, 180, 0.75);
  box-shadow: 0 0 26px rgba(255, 105, 180, 0.45);
}
.invite-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border-radius: 50%;
  margin-bottom: 14px;
  font-weight: 700;
  font-size: 1.1rem;
  background: rgba(255, 105, 180, 0.9);
  color: #fff !important;
}
.fm-card h3 { margin-bottom: 0.5rem; font-size: 1.1rem; }
.fm-card p  { font-size: 0.92rem; margin-bottom: 0.6rem; }
.invite-link { display: inline-block; padding: 9px 18px; margin-top: 6px; font-size: 0.9rem; }

/* ============================================================
   GALLERY
   ============================================================ */
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 20px;
  margin-top: 1.5rem;
}
.gallery-item { margin: 0; }
.gallery-open {
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 16px;
  overflow: hidden;
  background: none;
  cursor: pointer;
}
.gallery-open img { display: block; width: 100%; height: auto; }
.gallery-empty {
  margin-top: 1.5rem;
  padding: 32px 26px;
  border-radius: 20px;
  text-align: center;
  font-size: 1.02rem;
}
.lightbox-caption {
  color: #fff !important;
  text-align: center;
  margin-top: 14px;
  font-size: 0.95rem;
}
#lightbox .close-btn {
  background: none;
  border: none;
  font-size: 2.6rem;
  line-height: 1;
  color: #fff;
  cursor: pointer;
}

/* --- Submission instructions --- */
.submit-steps { list-style: none; counter-reset: step; padding: 0; margin: 1.4rem 0; }
.submit-steps > li {
  counter-increment: step;
  position: relative;
  padding: 0 0 0 54px;
  margin-bottom: 1.5rem;
  text-align: left;
}
.submit-steps > li::before {
  content: counter(step);
  position: absolute;
  left: 0; top: 0;
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-weight: 700;
  background: rgba(255, 105, 180, 0.9);
  color: #fff;
}
.submit-steps h3 { text-align: left; margin-bottom: 0.35rem; font-size: 1.06rem; }
.submit-steps p  { text-align: left; margin-bottom: 0.5rem; }
.submit-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; }
.submit-btn { display: inline-block; padding: 11px 22px; font-size: 0.95rem; }
.submit-rules {
  margin-top: 1.6rem;
  padding: 20px 22px;
  border-radius: 16px;
  border: 1px solid rgba(255, 105, 180, 0.4);
  background: rgba(255, 105, 180, 0.08);
  text-align: left;
}
.submit-rules h3 { text-align: left; margin-bottom: 0.6rem; }
.submit-rules ul { padding-left: 20px; }
.submit-rules li { text-align: left; margin-bottom: 0.5rem; }

/* ============================================================
   ABOUT PAGE
   ============================================================ */
.signature { font-style: italic; margin-top: 1rem; }
.book-meta { font-size: 0.92rem; opacity: 0.88; }
.contact-direct { margin-top: 1.2rem; }
.form-note { font-size: 0.85rem; opacity: 0.85; margin-top: 0.6rem; }
.form-disabled-notice {
  padding: 18px 20px;
  border-radius: 14px;
  border: 1px solid rgba(255, 105, 180, 0.45);
  background: rgba(255, 105, 180, 0.10);
  margin: 1rem 0;
}
#feedback-form .form-group { display: block; margin-bottom: 1rem; text-align: left; }
#feedback-form label { display: block; margin-bottom: 6px; font-weight: 600; }
#feedback-form input,
#feedback-form textarea {
  width: 100%;
  padding: 11px 13px;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  background: rgba(255, 255, 255, 0.14);
  color: inherit;
  font-family: inherit;
  font-size: 1rem;
}

/* --- Poll, now real radio buttons with working labels --- */
#theme-poll fieldset { border: none; padding: 0; margin: 0 0 1rem; }
#theme-poll .poll-option {
  display: flex;
  align-items: center;
  gap: 12px;
  text-align: left;
  padding: 12px 6px;
}
#theme-poll input[type="radio"] { width: 20px; height: 20px; accent-color: #ff69b4; flex: 0 0 auto; }
#theme-poll label { cursor: pointer; font-size: 1rem; }
.poll-result { margin-top: 0.9rem; font-size: 0.95rem; }

/* ============================================================
   SHOP PAGE
   ============================================================ */
.shop-book { display: flex; flex-wrap: wrap; gap: 28px; align-items: flex-start; text-align: left; }
.shop-cover {
  width: 240px; max-width: 45vw; height: auto;
  border-radius: 12px;
  border: 3px solid rgba(255, 255, 255, 0.5);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
.shop-details { flex: 1 1 300px; }
.shop-details h2, .shop-details p { text-align: left; }
.shop-note { font-size: 0.85rem; opacity: 0.82; margin-top: 0.9rem; }

/* ============================================================
   LEGAL PAGES
   ============================================================ */
.legal-content { max-width: 860px; }
.legal-doc { padding: 34px 32px; border-radius: 22px; text-align: left; }
.legal-doc h1 { text-align: left; margin-bottom: 0.3rem; }
.legal-doc h2 {
  text-align: left;
  margin-top: 2.1rem;
  margin-bottom: 0.7rem;
  font-size: 1.28rem;
  padding-bottom: 6px;
  border-bottom: 2px solid rgba(255, 105, 180, 0.4);
}
.legal-doc h3 { text-align: left; margin-top: 1.3rem; margin-bottom: 0.4rem; font-size: 1.05rem; }
.legal-doc p, .legal-doc li { text-align: left; }
.legal-doc ul, .legal-doc ol { padding-left: 22px; margin-bottom: 1rem; }
.legal-doc li { margin-bottom: 0.5rem; }
.legal-meta { font-size: 0.9rem; opacity: 0.8; margin-bottom: 1.6rem; }
.legal-doc a { text-decoration: underline; }

.legal-summary {
  padding: 22px 24px;
  border-radius: 16px;
  border: 1px solid rgba(255, 105, 180, 0.45);
  background: rgba(255, 105, 180, 0.09);
  margin-bottom: 1.6rem;
}
.legal-summary h2 { margin-top: 0; border-bottom: none; font-size: 1.1rem; }

.legal-callout {
  padding: 18px 20px;
  border-radius: 14px;
  border-left: 4px solid #ff69b4;
  background: rgba(255, 255, 255, 0.10);
  margin: 1rem 0;
}
body.day .legal-callout { background: rgba(255, 255, 255, 0.45); }

.table-scroll { overflow-x: auto; margin: 1rem 0; }
.legal-table { width: 100%; border-collapse: collapse; font-size: 0.93rem; min-width: 460px; }
.legal-table th, .legal-table td {
  padding: 11px 13px;
  text-align: left;
  border-bottom: 1px solid rgba(5, 69, 129, 0.22);
  vertical-align: top;
}
.legal-table th { font-weight: 700; }
body:not(.day) .legal-table th,
body:not(.day) .legal-table td { border-bottom-color: rgba(255, 255, 255, 0.22); }

/* --- Legal footer strip --- */
.legal-links {
  position: relative;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 14px 20px 26px;
  font-size: 0.85rem;
  text-align: center;
}
.legal-links a { text-decoration: underline; opacity: 0.9; }
.legal-links a:hover { opacity: 1; }
.legal-copy { opacity: 0.7; width: 100%; }

@media (max-width: 768px) {
  .legal-doc { padding: 24px 18px; }
  .shop-book { gap: 18px; }
  .shop-cover { width: 170px; }
  .fm-card { min-height: 300px; }
}

/* --- Fixes after visual review, 2026-09-05 --- */

/* The floating footer pill is position:fixed at the bottom of the viewport,
   which is exactly how it was designed. The legal links sit in normal flow
   at the end of the document, so they need clearance to avoid being hidden
   underneath the pill when you scroll all the way down. */
.legal-links { padding: 14px 20px 96px; }

/* The showcase section inherited `.index-content section { max-width: 800px }`,
   which cropped the third carousel card mid-air. Widen just this section and
   fade the edges so the scroll is obviously a scroll. */
.index-content #showcase-section { max-width: 1060px; }
.fm-carousel {
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 34px, #000 calc(100% - 34px), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 34px, #000 calc(100% - 34px), transparent 100%);
}
.carousel-hint {
  font-size: 0.82rem;
  opacity: 0.72;
  margin-top: -0.4rem;
  margin-bottom: 1.2rem;
}
@media (max-width: 700px) {
  .index-content #showcase-section { max-width: 100%; }
}

/* --- Contact block on the About page.
       Replaced the Formspree form entirely: a form would have routed every
       message through a third-party company before it reached the inbox. --- */
.contact-block { margin: 1.2rem 0; }
.contact-lead { margin-bottom: 0.8rem; }
.contact-address {
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: 1rem;
  word-break: break-word;
}
.contact-address a { text-decoration: underline; }
.contact-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  justify-content: center;
  margin-bottom: 0.9rem;
}

/* ============================================================
   GALLERY SUBMISSION ADDITIONS
   Added, nothing existing changed. These style only the new
   signature field, the anonymous choice, and the receipt code.
   ============================================================ */
.sign-choice {
  display: flex;
  align-items: center;
  gap: 10px;
  justify-content: center;
}
.sign-choice input[type="checkbox"] {
  width: 18px;
  height: 18px;
  cursor: pointer;
}
.sign-choice label {
  cursor: pointer;
  font-size: 0.92rem;
}
#signature-input:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.receipt-box {
  margin: 1.2rem auto 0;
  padding: 20px 22px;
  border-radius: 18px;
  text-align: center;
  max-width: 460px;
}
.receipt-title {
  font-weight: 700;
  margin-bottom: 8px;
}
.receipt-code {
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: 1.5rem;
  letter-spacing: 2px;
  font-weight: 700;
  margin-bottom: 12px;
  user-select: all;
  word-break: break-all;
}
.receipt-copy {
  padding: 8px 18px;
  font-size: 0.9rem;
  margin-bottom: 10px;
}
/* Inside the share row the copy button is one of three side by side,
   so it does not need a gap under it of its own. */
.receipt-share .receipt-copy { margin-bottom: 0; }

.art-signature {
  display: block;
  margin-top: 6px;
  font-style: italic;
  opacity: 0.9;
  font-size: 0.85rem;
}

/* Part of the casual save deterrent. An image that cannot be
   selected or dragged is harder to pull out of the page by accident.
   This is a speed bump, not a lock. The watermark burned into the
   pixels is the real protection. */
.gallery-item img,
#lightbox img {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
}

/* ============================================================
   GALLERY TILES, FRAME, AND THE HEARTRATE METER
   ============================================================ */

/* ============================================================
   THE FRAME
   ============================================================
   Alisa's gold frame, used whole. Nothing is stretched, nothing
   is sliced, so every piece of the carving stays exactly as she
   made it.

   The window in her frame was measured from the file itself:
     left   23.222%
     top    18.605%
     width  53.444%
     height 62.360%
   Those four numbers are why the art lines up. If the frame
   artwork is ever replaced, re-measure and change them here.

   Her frame's opening is proportioned 0.663. A page of the book
   is 0.744. Rather than squash the frame or crop somebody's
   artwork, the art sits on a mat inside the opening, which is
   what a real framer does.
   ============================================================ */
.gallery-item {
  position: relative;
  aspect-ratio: 900 / 1161;      /* the frame artwork's own shape */
  cursor: pointer;
  border: 0;
  padding: 0;
  background: none;
  box-shadow: none;
  filter: drop-shadow(0 14px 28px rgba(0, 0, 0, 0.45));
}

/* The mat and the artwork, sitting in the frame's opening. */
/* The artwork sits BEHIND the frame and is deliberately wider than
   the opening, so its edges tuck under the gold lip. That is how a
   real picture sits in a real frame, and it is what removes the
   gaps entirely.

   The measured opening is 53.444% wide. The art is set to 59.9%,
   so roughly 3.2% of it hides under the moulding on each side.

   The happy part: at 59.9% wide by 62.36% tall this box lands at
   exactly 0.744, which is the proportion of a page of the book. So
   nothing is cropped and nothing is letterboxed. The art fills it
   perfectly and the overlap is hidden by the frame. */
.gallery-item .fm-window {
  position: absolute;
  left: 19.959%;
  top: 18.589%;
  width: 59.970%;
  height: 62.392%;
  overflow: hidden;
  display: block;
  background: #1a1a1a;
}

.gallery-item .fm-window img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The frame itself, laid over the top. It is decoration, so it
   must never swallow a click meant for the art underneath. */
.gallery-item .fm-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
  -webkit-user-drag: none;
  user-select: none;
}

/* The caption and hearts belong inside the opening, not draped
   over the gold. */
.gallery-item .overlay {
  position: absolute;
  left: 23.222%;
  right: 23.334%;
  top: auto;
  bottom: 19.019%;
  z-index: 3;
  padding: 12px 10px 10px;
  border-radius: 0;
}

/* ---- Heartrate ----
   The tally of every heart a piece has collected, not an average.
   Five hearts from ten people reads as 50. */
.heart-rate-meter {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  margin-top: 6px;
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.3px;
}
.heart-rate-meter .hr-pulse {
  font-size: 1rem;
  line-height: 1;
  color: #ff69b4;
}
.heart-rate-meter .hr-count { color: #fff; }
.heart-rate-meter .hr-label {
  opacity: 0.75;
  font-weight: 400;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.6px;
}
/* The hollow heart sitting between the two words. Slightly larger
   and in the accent colour so it reads as a mark, not a letter. */
.heart-rate-meter .hr-mark {
  color: #ff69b4;
  font-size: 0.92rem;
  line-height: 1;
  margin: 0 1px;
  opacity: 1;
  position: relative;
  top: 0.06em;
}
.heart-rate-meter.is-flatline .hr-mark { opacity: 0.5; }
/* The monitor. A real ECG trace swept across the way a hospital
   monitor draws it, at a speed set by how loved the piece is. */
.heart-rate-meter .hr-monitor {
  --beat: 2.2s;
  flex: 1 1 auto;
  max-width: 92px;
  height: 20px;
  display: block;
  overflow: hidden;
  border-radius: 4px;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 12%, #000 88%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 12%, #000 88%, transparent 100%);
}
.heart-rate-meter .hr-ecg {
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}
.heart-rate-meter .hr-trace {
  fill: none;
  stroke: #ff69b4;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 0 3px rgba(255, 105, 180, 0.75));

  /* The trace is drawn, then erased, over and over. 220 is a little
     longer than the path so there is a gap of quiet between beats,
     which is what makes it read as a pulse rather than a squiggle. */
  stroke-dasharray: 110 110;
  stroke-dashoffset: 220;
  animation: hr-sweep var(--beat) linear infinite;
}
@keyframes hr-sweep {
  from { stroke-dashoffset: 220; }
  to   { stroke-dashoffset: 0; }
}

/* Nothing loved yet. The line goes flat and stops moving, which is
   the honest picture and also quietly funny. */
.heart-rate-meter.is-flatline .hr-trace {
  animation: none;
  stroke-dasharray: none;
  stroke-dashoffset: 0;
  d: path('M0,12 L100,12');
  opacity: 0.4;
}
.heart-rate-meter.is-flatline .hr-pulse { animation: none; opacity: 0.45; }

/* The little heart throbs in time with the trace. */
.heart-rate-meter .hr-pulse {
  --beat: 2.2s;
  animation: hr-throb var(--beat) ease-in-out infinite;
  transform-origin: center;
  display: inline-block;
}
@keyframes hr-throb {
  0%, 62%, 100% { transform: scale(1); }
  68%           { transform: scale(1.45); }
  76%           { transform: scale(1.05); }
  82%           { transform: scale(1.22); }
}

/* One emphasised beat the moment someone gives hearts. */
.heart-rate-meter.just-beat .hr-count {
  animation: hr-bump 0.5s ease-out;
}
@keyframes hr-bump {
  0%   { transform: scale(1);    color: #fff; }
  35%  { transform: scale(1.55); color: #ff69b4; }
  100% { transform: scale(1);    color: #fff; }
}
.heart-rate-meter .hr-count { display: inline-block; }

/* Some people get motion sick, and a looping pulse is exactly the
   kind of thing that does it. Honour the system setting. */
@media (prefers-reduced-motion: reduce) {
  .heart-rate-meter .hr-trace,
  .heart-rate-meter .hr-pulse,
  .heart-rate-meter.just-beat .hr-count {
    animation: none;
  }
  .heart-rate-meter .hr-trace {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
  }
}

/* ---- Social icons ----
   The circles were right, the icons inside them were square PNGs
   with their own backgrounds, so they read as squares. Clip them
   to the circle and let them fill it. */
.social-icons a { overflow: hidden; padding: 0; }
.social-icons img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  display: block;
}

/* The frame <picture> needs its inner <img> to fill it. */
.gallery-item .fm-frame img,
.gallery-item .fm-frame-img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: fill;
  pointer-events: none;
  -webkit-user-drag: none;
}

/* ============================================================
   FRAME PICKER
   The submitter chooses the frame their work hangs in. Gold,
   silver and bronze are deliberately absent: those are the
   podium frames and they have to be earned.
   ============================================================ */
/* The picker is its own full width block, not a column beside the
   label, so the swatches are never squeezed into an odd shape. */
.frame-choice {
  display: block;
  width: 100%;
  text-align: center;
}
.frame-choice-label {
  font-size: 0.92rem;
  margin-bottom: 10px;
  opacity: 0.9;
  text-align: center;
}

/* A grid with a fixed column count rather than a wrapping row.
   Wrapping is what left one frame stranded on its own line. Eight
   across when there is room, four and four when there is not.
   Either way it is always balanced. */
.frame-picker {
  display: grid;
  grid-template-columns: repeat(8, 46px);
  gap: 10px;
  justify-content: center;
}
@media (max-width: 620px) {
  .frame-picker { grid-template-columns: repeat(4, 46px); }
}
@media (max-width: 380px) {
  .frame-picker { grid-template-columns: repeat(4, 40px); gap: 8px; }
  .frame-swatch { width: 40px; height: 50px; }
}
.frame-swatch {
  width: 46px;
  height: 58px;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.10);
  cursor: pointer;
  overflow: hidden;
  position: relative;
  transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
}
.frame-swatch img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  pointer-events: none;
}
.frame-swatch:hover { transform: translateY(-3px); }
.frame-swatch.is-chosen {
  border-color: #ff69b4;
  box-shadow: 0 0 14px rgba(255, 105, 180, 0.65);
  transform: translateY(-3px);
}
.frame-swatch:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 3px;
}
body.day .frame-swatch { background: rgba(5, 69, 129, 0.10); }
body.day .frame-swatch.is-chosen { border-color: #c2185b; box-shadow: 0 0 14px rgba(194, 24, 91, 0.5); }


/* ============================================================
   LIVE FRAME PREVIEW
   Shows the chosen photo inside the chosen frame before the
   submitter commits. Same window measurements as a real tile.
   ============================================================ */
.frame-preview-wrap {
  display: block;
  width: 100%;
  text-align: center;
}
.frame-preview-wrap[hidden] { display: none; }

.frame-preview-label {
  font-size: 0.92rem;
  margin-bottom: 10px;
  opacity: 0.9;
  text-align: center;
}

.fm-preview {
  position: relative;
  width: 100%;
  max-width: 240px;
  margin: 0 auto;
  aspect-ratio: 900 / 1161;
  display: block;
}
.fm-preview .fm-window {
  position: absolute;
  left: 19.959%;
  top: 18.589%;
  width: 59.970%;
  height: 62.392%;
  overflow: hidden;
  display: block;
  background: #1a1a1a;
}
.fm-preview .fm-window img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.fm-preview .fm-frame {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  display: block;
}
.fm-preview .fm-frame picture,
.fm-preview .fm-frame img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: fill;
  pointer-events: none;
  -webkit-user-drag: none;
}

@media (max-width: 380px) {
  .fm-preview { max-width: 200px; }
}


/* ============================================================
   BOOK PICKER AND SIGNATURE ROW
   The signature and the book each take a full row of their own,
   so a long placeholder is never cut off mid word. The anonymous
   choice sits beside the signature, because that is the decision
   it belongs to.
   ============================================================ */
.book-choice,
.signature-row {
  flex-basis: 100%;
  flex-wrap: wrap;
  justify-content: center;
}

.book-choice-label {
  font-size: 0.92rem;
  opacity: 0.9;
  white-space: nowrap;
}

.book-select {
  flex-grow: 1;
  flex-basis: 320px;
  max-width: 510px;
  text-overflow: ellipsis;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  color: var(--primary-color);
  padding: 9px 14px;
  font: inherit;
  font-size: 0.92rem;
  cursor: pointer;
  outline: none;
}
.book-select:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 2px;
}
/* The dropdown list itself is drawn by the browser, so it needs its
   own colours or it opens white on white in dark mode. */
.book-select option {
  background: #12121c;
  color: #f4f4f8;
}
body.day .book-select option {
  background: #ffffff;
  color: #054581;
}
/* On a narrow phone the title needs a slightly smaller face to stay
   whole rather than being cut off by the dropdown arrow. */
@media (max-width: 430px) {
  .book-select { font-size: 0.72rem; padding: 9px 8px; }
  .book-choice-label { white-space: normal; }
}

.signature-row { gap: 14px; }
.signature-row #signature-input {
  flex: 1 1 240px;
  min-width: 0;
}
.signature-row .sign-choice {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* ============================================================
   SENDING THE RECEIPT CODE TO YOURSELF
   ============================================================ */
.receipt-share {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
  margin-bottom: 12px;
}
.receipt-send {
  padding: 8px 16px;
  font-size: 0.9rem;
  text-decoration: none;
  display: inline-block;
}
.receipt-privacy {
  margin-top: 10px;
  font-size: 0.78rem;
  opacity: 0.75;
}


/* ============================================================
   SHOWCASE FRAMES

   Each tile is three layers, bottom to top:

     1. .fm-glow   the frame again, carrying the glow. Sits under
                   everything, so the part of the glow that falls
                   inward lands behind the artwork instead of on
                   top of it. Its own pixels are hidden exactly by
                   layer 3, which is the same picture in the same
                   place.
     2. .fm-art    the submitted artwork.
     3. .fm-frame  the frame itself, always on top.

   This ordering is the whole point. When the glow was on the top
   layer it washed pink across the artwork, which is not a glow
   behind a frame, it is a stain on the picture.

   Nothing about how the carousel behaves is touched: same
   coverflow, same swipe, same arrows, same dots.

   The tile is 310 x 400 rather than 250 x 400 because the frame is
   900 x 1161. At 250 wide the frame would have to stretch about a
   quarter taller than it really is, which on carving shows badly.
   310 x 400 is the same shape as the frame, so it sits true, and
   the tile keeps the exact height it always had.

   The artwork is placed into the frame's opening using the same
   window measurements a real gallery tile uses, so a showcase piece
   and a submitted piece sit in exactly the same place inside their
   frames. .fm-art inherits the slide's own background-image, so the
   inline url Alisa writes on each slide is still the one thing that
   decides what hangs there.
   ============================================================ */
.swiper-slide.card {
  width: 310px;
  position: relative;
  background-repeat: no-repeat;
  background-size: 59.970% 62.392%;
  background-position: 49.86% 49.43%;
}

.swiper-slide.card .fm-glow,
.swiper-slide.card .fm-art,
.swiper-slide.card .fm-frame {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
}
.swiper-slide.card .fm-glow  { z-index: 0; }
.swiper-slide.card .fm-art   { z-index: 1; }
.swiper-slide.card .fm-frame { z-index: 2; }

/* The artwork layer takes whatever background-image the slide
   itself carries, and lands it in the frame's opening.

   It is transparent. An empty frame shows the page through it, the
   way an empty frame should. Nothing is hidden behind it. */
.swiper-slide.card .fm-art {
  left: 19.959%;
  top: 18.589%;
  right: auto;
  bottom: auto;
  width: 59.970%;
  height: 62.392%;
  background-image: inherit;
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
}

.swiper-slide.card .fm-glow picture,
.swiper-slide.card .fm-glow img,
.swiper-slide.card .fm-frame picture,
.swiper-slide.card .fm-frame img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: fill;
  pointer-events: none;
  -webkit-user-drag: none;
}

/* ============================================================
   THE GLOW

   The glow is a picture of a halo, not a shadow cast by the frame.

   That distinction is the whole fix. A drop-shadow is thrown from
   every edge of a shape, so the frame's inner edge threw light
   inward across the artwork as well as outward. An opaque square
   behind the opening hid that, and hiding it is not fixing it: with
   no artwork in the frame the square itself showed.

   assets/frames/frame-halo-190.png is built from the frame's own
   silhouette with the opening filled in, blurred, and then the solid
   part subtracted again. What is left is only what lies OUTSIDE the
   frame's outline. Its middle is transparent, alpha zero, measured.
   There is no inward glow to hide because there is no inward glow.

   It is used as a mask over a flat colour, so the colour is a single
   CSS value. That is what lets each tile glow its own colour.

   The image is padded by 190px on a 900 x 1162 frame, which is
   21.111% of the width and 16.351% of the height. Those are the
   negative insets below. They are not guesses, and if the halo is
   ever rebuilt with different padding these two numbers have to be
   recalculated with it or the glow will sit at the wrong size.

   THE PADDING IS IN THE FILENAME ON PURPOSE. The first version of
   this file was padded by 90px. When it was rebuilt at 190 and saved
   over the same name, browsers and the CDN went on serving the old
   90px image, stretched across the new larger box, and the glow came
   out as a hard wobbly outline floating off the frame. A file whose
   contents change must change its name, because a cache has no way
   to know the bytes behind a URL are different. Rebuild it at a new
   padding, save it as frame-halo-<padding>.png, and change these
   three numbers together.
   ============================================================ */
.swiper-slide.card .fm-glow {
  position: absolute;
  left: -21.111%;
  right: -21.111%;
  top: -16.351%;
  bottom: -16.351%;
  z-index: 0;
  pointer-events: none;

  background-color: var(--glow, #2dc9f0);
  transition: background-color 0.35s ease-out;

  -webkit-mask-image: url("/assets/frames/frame-halo-190.png");
          mask-image: url("/assets/frames/frame-halo-190.png");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* A colour per tile, so the row reads as a set rather than five of
   the same thing. */
.swiper-slide.card:nth-of-type(1) { --glow: #2dc9f0; }  /* blue   */
.swiper-slide.card:nth-of-type(2) { --glow: #a855f7; }  /* purple */
.swiper-slide.card:nth-of-type(3) { --glow: #ff8c1a; }  /* orange */
.swiper-slide.card:nth-of-type(4) { --glow: #22c55e; }  /* green  */
.swiper-slide.card:nth-of-type(5) { --glow: #f5c518; }  /* yellow */

/* Whichever tile is centred is always pink, whatever colour it
   carries when it is off to the side.

   The :nth-of-type(n) is not decoration. Without it this rule counts
   as less specific than the five above and would simply lose. n
   matches every tile, so it changes nothing except the weight. */
.swiper-slide.card:nth-of-type(n):hover,
.swiper-slide.card:nth-of-type(n):focus-visible,
.swiper-slide.card:nth-of-type(n):active,
.swiper-slide-active.card:nth-of-type(n) {
  --glow: var(--accent-color);
}

/* The tile's own rectangular shadows are off. They are what left
   straight-edged gaps against the carving in the first place. Alisa's
   two original rules are still in the stylesheet, untouched, above. */
.swiper-slide.card {
  box-shadow: none;
}
.swiper-slide.card:hover,
.swiper-slide.card:focus-visible,
.swiper-slide.card:active,
.swiper-slide-active.card {
  box-shadow: none !important;
}


/* ============================================================
   BOOK RATING LINE
   One quiet line under the stars saying what the book has scored.
   Uses her existing .book-meta look, just smaller and softer, so
   it reads as a footnote rather than another heading.
   ============================================================ */
.book-rating-note {
  font-size: 0.8rem;
  opacity: 0.8;
  margin-top: 2px;
  min-height: 1.2em;
}
/* Each star is painted with a gradient that is accent colour up to
   --fill and empty after it, clipped to the shape of the character.
   So a 4.2 shows a fifth star a fifth full, and the filled part is
   star shaped rather than a bar behind a star.

   Written here, late in the file, so nothing further up wins. */
.book-tile .star-rating {
  --star-empty: rgba(244, 248, 251, 0.34);
}
body.day .book-tile .star-rating {
  --star-empty: rgba(5, 69, 129, 0.26);
}

.book-tile .star-rating span {
  background-image: linear-gradient(
    90deg,
    var(--accent-color) var(--fill, 0%),
    var(--star-empty) var(--fill, 0%)
  );
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  transition: transform 0.15s ease-out;
}
.book-tile .star-rating span:hover {
  transform: scale(1.15);
}


/* ============================================================
   SHARING THE ARTWORK
   Keeping the code and sharing the picture are two different jobs,
   so the share button gets its own row under a divider rather than
   sitting in the line of code buttons.
   ============================================================ */
.receipt-share-art {
  flex-direction: column;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
  margin-bottom: 0;
  padding-top: 16px;
  border-top: 1px solid var(--glass-border);
}
.receipt-share-btn {
  font-weight: 700;
}
.receipt-share-btn[disabled] {
  opacity: 0.6;
  cursor: progress;
}


/* ============================================================
   SUPPORT JAR
   RESTORED Sept 21 2026. This block was written once and then
   destroyed by an agent that rewrote the stylesheet by slicing it
   at a marker and dropping everything after it. That is why the jar
   on the About page was left unstyled: full size, hard left.

   The jar artwork itself is the link, so one tap on the picture goes
   to Ko-fi. No card, no border, no box, on purpose: it is a quiet
   personal note, not a marketing block.

   These are the numbers the Tailwind version produced: 140px tall,
   scale 1.05 on hover over 200ms ease-out, and the same warm teal
   drop shadow.
   ============================================================ */
.support-jar-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  width: 100%;
  padding: 24px;
  margin: 0 auto 2rem auto;
}

.support-jar-link {
  display: inline-block;
  line-height: 0;
  cursor: pointer;
  transition: transform 0.2s ease-out, filter 0.2s ease-out;
}
.support-jar-link picture { display: block; }
.support-jar-link img {
  height: 140px;
  width: auto;
  max-width: 100%;
  display: block;
}
.support-jar-link:hover,
.support-jar-link:focus-visible {
  transform: scale(1.05);
  filter: drop-shadow(0 8px 22px rgba(0, 180, 180, 0.5));
}
.support-jar-link:focus-visible {
  outline: 3px solid var(--accent-color);
  outline-offset: 6px;
}

.support-jar-note {
  margin: 12px auto 0 auto;
  max-width: 28rem;
  font-size: 0.875rem;
  font-style: italic;
  line-height: 1.6;
  opacity: 0.8;
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .support-jar-link { transition: none; }
  .support-jar-link:hover,
  .support-jar-link:focus-visible { transform: none; }
}

/* ============================================================
   EVERY BUTTON WEARS THE SHARE YOUR ART LOOK

   .cta-share is READ here, never written. Her own block further up
   is the only place that button is defined, and it stays that way.

   The swap is the point, and it is deliberate. At rest the button is
   filled blue and glows pink. Reach for it and the two change places:
   the fill turns pink and the glow turns blue. Every button now does
   that same swap, using her exact numbers:

     rest   background rgba(45, 201, 240, 0.281)
            border     rgba(45, 201, 240, 0.76)
            glow       rgba(255, 105, 180, 0.795) at 20px
     hover  background rgba(255, 105, 180, 0.24)
            border     rgba(255, 105, 180, 0.795)
            glow       rgba(45, 201, 240, 0.76) at 30px
            and a 2px lift

   Left out on purpose:
     .swiper-button-next / -prev  the carousel arrows, which are
                                  bare chevrons, not pills
     .close-btn                   the lightbox X
     .rq-tab                      the review queue tabs on the admin
                                  page, which are hers alone

   Text colour splits by theme. #054581 is what .cta-share uses and
   it only reads on the light pages. The dark pages keep the pink
   text they already had, which is readable there.
   ============================================================ */
button:not(.close-btn):not(.rq-tab):not(.swiper-button-next):not(.swiper-button-prev),
.glass-button:not(.swiper-button-next):not(.swiper-button-prev),
.vote-btn,
.shop-link {
  background: rgba(45, 201, 240, 0.281);
  border: 1px solid rgba(45, 201, 240, 0.76);
  text-shadow: none !important;
  box-shadow: 0 0 20px rgba(255, 105, 180, 0.795);
  transition: box-shadow 0.2s, transform 0.2s, background 0.2s, border-color 0.2s;
}

body.day button:not(.close-btn):not(.rq-tab):not(.swiper-button-next):not(.swiper-button-prev),
body.day .glass-button:not(.swiper-button-next):not(.swiper-button-prev),
body.day .vote-btn,
body.day .shop-link {
  color: #054581 !important;
}

button:not(.close-btn):not(.rq-tab):not(.swiper-button-next):not(.swiper-button-prev):hover,
button:not(.close-btn):not(.rq-tab):not(.swiper-button-next):not(.swiper-button-prev):focus-visible,
button:not(.close-btn):not(.rq-tab):not(.swiper-button-next):not(.swiper-button-prev):active,
.glass-button:not(.swiper-button-next):not(.swiper-button-prev):hover,
.glass-button:not(.swiper-button-next):not(.swiper-button-prev):focus-visible,
.glass-button:not(.swiper-button-next):not(.swiper-button-prev):active,
.vote-btn:hover, .vote-btn:focus-visible, .vote-btn:active,
.shop-link:hover, .shop-link:focus-visible, .shop-link:active {
  box-shadow: 0 0 30px rgba(45, 201, 240, 0.76);
  transform: translateY(-2px);
  background: rgba(255, 105, 180, 0.24);
  border-color: rgba(255, 105, 180, 0.795);
}

/* A disabled button should not look like it is waiting to be
   pressed. */
button[disabled],
.glass-button[disabled] {
  box-shadow: none;
  transform: none;
  opacity: 0.6;
}

@media (prefers-reduced-motion: reduce) {
  button, .glass-button, .vote-btn, .shop-link {
    transition: box-shadow 0.2s, background 0.2s, border-color 0.2s;
  }
  button:hover, .glass-button:hover, .vote-btn:hover,
  .shop-link:hover { transform: none; }
}


/* ============================================================
   THEME POLL RESULTS
   The row already existed. What is new is the bar behind it, the
   percentage beside it, and the line underneath. Her .poll-option
   layout is left alone, only given a stacking context so the bar
   can sit behind the words.
   ============================================================ */
.poll-option {
  position: relative;
  overflow: hidden;
  border-radius: 10px;
}
.poll-option > * {
  position: relative;
  z-index: 1;
}
.poll-bar {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  pointer-events: none;
}
.poll-bar-fill {
  display: block;
  height: 100%;
  width: 0;
  background: rgba(45, 201, 240, 0.22);
  transition: width 0.5s ease-out;
}
.poll-option.is-yours .poll-bar-fill {
  background: rgba(255, 105, 180, 0.26);
}

.poll-count {
  font-size: 0.85rem;
  opacity: 0.8;
  margin-left: auto;
  margin-right: 12px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.poll-option.is-yours .poll-count {
  opacity: 1;
  font-weight: 700;
}

.poll-note {
  margin-top: 12px;
  font-size: 0.85rem;
  opacity: 0.8;
  text-align: center;
}

/* ============================================================
   WHERE TO BUY, SIZED TO ITS WORDS
   The button was 300px wide holding 120px of text, so there was
   about 90px of empty space either side of the label.

   The cause is `.book-tile a { display: block }` further up, which
   is there so the anchor around the cover fills the tile. It caught
   this anchor too. That rule is correct and stays; this one lets the
   button inside the tile shrink to its words instead. The paragraph
   around it is already centred, so an inline-block centres itself.
   ============================================================ */
.book-tile .shop-link {
  display: inline-block;
  width: auto;
}

/* ============================================================
   HEARTRATE IS A READOUT, NOT A CONTROL

   The little beating heart never did anything. It has no click
   handler and never had one. It looked clickable because
   `.gallery-item` sets `cursor: pointer` for the lightbox, and every
   child inherits that, and it went white because clicking a text
   character selects it and a selected glyph is drawn in the
   browser's highlight colours.

   So: nothing here is a control. The arrow stays an arrow, and
   nothing in the readout can be selected. The hearts you give are
   the row of hollow hearts above; this line only reports the total.
   ============================================================ */
.heart-rate-meter {
  cursor: default;
  -webkit-user-select: none;
  user-select: none;
}

/* ============================================================
   SENDING, AND SAYING SO

   Uploading a photo takes a few seconds. If nothing on screen
   moves, people press the button again, which is exactly what
   Alisa did. So the button itself reports: it spins, it changes
   its words, and it will not take a second press.
   ============================================================ */
.submit-status {
  flex-basis: 100%;
  text-align: center;
  margin: 8px 0 0 0;
  min-height: 1.2em;
  font-size: 0.86rem;
}

button.is-sending,
.glass-button.is-sending {
  cursor: progress;
  opacity: 1;
  position: relative;
  padding-left: 38px;
}
/* The spinner sits inside the button's left padding, so the words
   never jump when it appears. */
button.is-sending::before,
.glass-button.is-sending::before {
  content: '';
  position: absolute;
  left: 15px;
  top: 50%;
  width: 14px;
  height: 14px;
  margin-top: -7px;
  border-radius: 50%;
  border: 2px solid rgba(255, 105, 180, 0.35);
  border-top-color: var(--accent-color);
  animation: fm-spin 0.7s linear infinite;
}
@keyframes fm-spin { to { transform: rotate(360deg); } }

/* A disabled button that is working should not look like a dead
   one, so the sending state keeps its glow. */
button.is-sending[disabled],
.glass-button.is-sending[disabled] {
  opacity: 1;
  box-shadow: 0 0 20px rgba(255, 105, 180, 0.795);
}

.receipt-done {
  display: block;
  margin: 16px auto 0 auto;
  padding: 8px 26px;
  font-size: 0.9rem;
}

@media (prefers-reduced-motion: reduce) {
  button.is-sending::before,
  .glass-button.is-sending::before { animation: none; }
}


/* ============================================================
   ONE ROW, ONE QUESTION

   The file picker and the comment were sharing the top row, and
   which line the comment landed on depended on how long the chosen
   file name happened to be. A short name and they sat side by side.
   A long one and the comment dropped below. The layout moved under
   the person typing, and whatever width the file name did not use
   was all the comment box got, so they could only ever see a few
   words of what they had written.

   Every question in this form now takes a full row of its own, in
   the order it is asked: the picture first, then the comment, then
   the book, then the signature. Nothing shifts, and the comment box
   is as wide as the form.

   Her own rule at the top of the file is untouched. #upload-form
   .form-group only has to out-rank .form-group, which it does on an
   id, so the original stays exactly as she wrote it.
   ============================================================ */
#upload-form .form-group {
  flex-basis: 100%;
  /* A flex item will not shrink below its own content unless it is
     told it may. Without this a 53 character file name made its row
     wider than the form itself, which is the same shove in a
     different direction. */
  min-width: 0;
  max-width: 100%;
  justify-content: center;
}

/* The button keeps its own size. Letting it shrink is how "Choose
   Image" ended up broken across two lines when a long file name
   pushed on it. */
#upload-form label.glass-button {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* So a long file name truncates instead of stretching its row. */
#upload-form #file-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ============================================================
   A WAY OUT OF THE CODE BOX

   The box waits until it is dismissed, and that is correct: the
   code is the only proof the artwork belongs to them and it must
   not disappear while they are still writing it down. But waiting
   is only patient if the way out is obvious, otherwise it reads as
   stuck. So there are two, and they do the same thing: a pink X in
   the corner where everyone looks first, and a button at the bottom
   that says what it closes.

   The id carries these rules past the global button look further
   up. That block is read, not edited.
   ============================================================ */
#submit-receipt { position: relative; }

#submit-receipt .receipt-close {
  position: absolute;
  top: 8px;
  right: 10px;
  width: 34px;
  height: 34px;
  padding: 0;
  line-height: 1;
  font-size: 1.6rem;
  font-weight: 700;
  border: none;
  border-radius: 50%;
  background: transparent;
  box-shadow: none;
  color: #ff69b4;
  opacity: 0.85;
  cursor: pointer;
  transform: none;
  transition: opacity 0.2s, background 0.2s, box-shadow 0.2s;
}
#submit-receipt .receipt-close:hover,
#submit-receipt .receipt-close:focus-visible,
#submit-receipt .receipt-close:active {
  opacity: 1;
  color: #ff69b4;
  background: rgba(255, 105, 180, 0.16);
  box-shadow: 0 0 14px rgba(255, 105, 180, 0.6);
  border: none;
  transform: none;
}
#submit-receipt .receipt-close:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 2px;
}

/* The title is centred, so it keeps clear of the X. */
#submit-receipt .receipt-title { padding: 0 36px; }

/* ============================================================
   THE ADVERTISING CHOICE

   This used to be a sentence stating that submitting meant
   agreeing. Now it is a question with a real answer, and the
   answer is no until someone says otherwise. It reads the same as
   the anonymous choice beside the signature, because it is the
   same kind of decision: something the artist chooses about their
   own work.
   ============================================================ */
.ad-choice {
  flex-wrap: wrap;
  gap: 6px;
}
.ad-choice .sign-choice {
  flex: 0 1 auto;
  display: inline-flex;
  align-items: flex-start;
  gap: 8px;
  text-align: left;
}
.ad-choice .ad-choice-note {
  flex-basis: 100%;
  margin: 0;
  font-size: 0.78rem;
  opacity: 0.75;
  text-align: center;
}

/* ============================================================
   WHEN A HEART IS REFUSED, SAY SO

   The reason used to go into a title attribute and nowhere else,
   so a rejected heart looked exactly like a broken site: the
   hearts filled, emptied again, and no word of explanation
   unless you happened to rest the pointer on them. On a phone
   there is no hover at all, so there was no way to find out.

   Empty, it takes no space and moves nothing.
   ============================================================ */
.heart-note {
  margin: 4px 0 0;
  font-size: 0.74rem;
  line-height: 1.3;
  text-align: center;
  opacity: 0;
  max-height: 0;
  overflow: hidden;
  transition: opacity 0.25s ease, max-height 0.25s ease;
}
.heart-note.is-showing {
  opacity: 0.95;
  max-height: 4.5em;
}
.heart-note.is-problem {
  color: #ff8ec4;
}

/* ============================================================
   THE ACCESS NOTICE

   A login screen that says "this page is yours only" reads like a
   diary. This one states what the system is and what happens to
   someone who should not be here.

   Every claim on it is true, which is the point. Attempts really
   are recorded, in the adminlockouts collection, keyed to a hash
   of the address. Three failures really do lock it. A warning
   that bluffs is worth less than no warning at all.
   ============================================================ */
.rq-notice {
  max-width: 44ch;
  margin: 0 auto 10px;
  font-size: 0.88rem;
  line-height: 1.5;
}
.rq-notice-quiet {
  font-size: 0.78rem;
  opacity: 0.75;
  margin-bottom: 18px;
}

/* ============================================================
   SHOW PASSWORD

   A password box you cannot read is how a typo becomes a lockout,
   and this gate locks the address after three wrong tries. The eye
   sits inside the field rather than beside it, so the field keeps
   its full width.

   The id carries these rules past the global button look, which
   would otherwise turn the eye into a blue pill.
   ============================================================ */
.rq-field {
  position: relative;
  display: block;
  width: 100%;
}
.rq-field #key {
  width: 100%;
  /* Room for the eye so a long password never runs under it. */
  padding-right: 48px;
}

#gate .rq-eye {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  /* 44 by 44 is the smallest target a thumb reliably hits. */
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  box-shadow: none;
  color: var(--primary-color);
  opacity: 0.7;
  cursor: pointer;
  transition: opacity 0.2s, background 0.2s;
}
#gate .rq-eye:hover,
#gate .rq-eye:focus-visible,
#gate .rq-eye[aria-pressed="true"] {
  opacity: 1;
  background: rgba(255, 105, 180, 0.16);
  color: #ff69b4;
  border: none;
  box-shadow: none;
  transform: translateY(-50%);
}
#gate .rq-eye:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 2px;
}
#gate .rq-eye svg[hidden] { display: none; }

/* ============================================================
   THE CAROUSEL SLOT PICKER

   Five numbered buttons on every published piece in the review
   queue. The number she picks IS the position on the home page,
   so choosing and ordering are one action rather than two.

   The id carries these past the global button look, which would
   otherwise make every slot a full width blue pill.
   ============================================================ */
.rq-shelf {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 12px;
}
.rq-shelf-label {
  font-size: 0.8rem;
  opacity: 0.85;
  margin-right: 4px;
}
.rq-shelf-note {
  margin-top: 6px;
  font-size: 0.78rem;
  opacity: 0.8;
}

#cards .rq-slot {
  min-width: 34px;
  height: 34px;
  padding: 0 9px;
  font-size: 0.85rem;
  font-weight: 700;
  line-height: 1;
  border-radius: 9px;
  cursor: pointer;
  background: rgba(45, 201, 240, 0.16);
  border: 1px solid rgba(45, 201, 240, 0.5);
  color: inherit;
  box-shadow: none;
  transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
}
#cards .rq-slot:hover:not(:disabled),
#cards .rq-slot:focus-visible {
  background: rgba(255, 105, 180, 0.2);
  border-color: rgba(255, 105, 180, 0.7);
  box-shadow: none;
}
/* The slot this piece actually holds. Colour alone never carries
   meaning here, so it is also aria-pressed and the line underneath
   says it in words. */
#cards .rq-slot.is-on {
  background: rgba(255, 105, 180, 0.32);
  border-color: #ff69b4;
  box-shadow: 0 0 10px rgba(255, 105, 180, 0.55);
}
#cards .rq-slot:disabled { opacity: 0.45; cursor: wait; }
#cards .rq-slot:focus-visible { outline: 3px solid #ff69b4; outline-offset: 2px; }
#cards .rq-slot-off { min-width: 44px; }

/* ============================================================
   THE BACKGROUND NEVER RUNS OUT

   Her words: "there is black space at the bottom like as if the
   background cuts off instread of being endless".

   Two causes, both fixed here without touching a colour, a layout
   or an animation.

   1. Both backdrops are position: fixed but sized with viewport
      units: 100vh on the cosmic one, 100% on the day sky, and the
      day sky has no top or left at all so it sits at whatever its
      static position happens to be. On a desktop those all resolve
      to exactly the viewport and look perfect, which is why this
      never showed up in testing. On a phone they do not. A mobile
      browser's toolbar slides away as you scroll, the visible area
      changes height underneath a value that does not, and the gap
      shows through. inset: 0 on a fixed element means exactly the
      viewport, always, with no unit to be wrong about.

   2. Whatever shows through that gap is the page underneath, and
      the page underneath was pure black. So the gap read as the
      background ending. The colours below are the last stop of each
      of her own gradients, so a gap now shows the same colour the
      backdrop was already painting there and reads as continuous.

      #090a0f is the outer stop of her cosmic radial gradient.
      #f0f8ff is the bottom of her day sky linear gradient.

   Nothing else changes. Same gradients, same stars, same clouds,
   same animations, same z-index.
   ============================================================ */
#cosmic-background,
#day-sky-background {
  inset: 0;
  height: auto;
  width: auto;
}

/* These three lines used to read:
       html { background-color: #090a0f; }
       body { background-color: #090a0f; }
       body.day { background-color: #f0f8ff; }
   and they are what hid Alisa's backgrounds.

   Both background scenes are divs at z-index -1. A negative z-index
   child paints BEHIND its parent's own background. While body had no
   background colour of its own, body's background was handed to the
   page canvas, which paints below everything, and the scenes showed
   through. The moment html was given a colour, body stopped handing
   its background to the canvas and started painting it as an ordinary
   opaque box, directly on top of the cosmic sky and the day sky.

   So the stars, the sun and the clouds were still running, still
   animating, still correct, with a flat rectangle painted over them.

   They are gone. They were belt and braces against a gap at the
   bottom of the page on a phone, and they were never needed: both
   scenes are position fixed with inset 0, so they cover the viewport
   at every scroll position by definition. The inset rule above is the
   fix, on its own, and it stays. */

/* ============================================================
   THE PHONE

   Everything below is inside one media query. Nothing here can
   reach a desktop, a laptop or a tablet in landscape. No colour,
   no gradient, no animation, no font and no markup is touched.
   These are four measured layout faults, each with its cause.

   Measured on a 390 by 844 screen before this block existed:

     home page document width   427px on a 390px screen
     Buy on Amazon button       114 wide and 71 TALL, text on
                                three lines inside the pill
     shop cover                 left edge at 53px, parent box
                                shrink wrapped around it so it
                                could never centre
     floating footer            sitting on top of the book
                                description with nothing
                                reserving space for it
   ============================================================ */
@media (max-width: 600px) {

  /* 1. THE HEADER WAS WIDER THAN THE PHONE.

     It is position: fixed with no width, so it shrink wraps its
     contents: logo 145 plus nav 285 plus 40 of padding is 407,
     on a 390 screen. flex-wrap: nowrap then refused to fold it,
     so Shop was sliced off the right edge and the whole document
     grew to 427 and scrolled sideways.

     Giving a fixed element both a left and a right edge is what
     tells it the screen is narrower than its contents. The 10px
     inset is the one it already had. */
  header.glass-container {
    left: 10px;
    right: 10px;
    width: auto;
    max-width: none;
    flex-wrap: wrap;
    justify-content: center;
    row-gap: 4px;
  }

  /* The sideways scroll is stopped at the element that actually
     scrolls. overflow-x: hidden was already on body, but on a
     phone the scrolling element is html, so that line had never
     done anything. */
  html { overflow-x: hidden; }

  /* 2. THE FLOATING FOOTER SAT ON TOP OF THE PAGE.

     It is fixed to the bottom and nothing reserved room for it,
     so on a short screen it covered the last paragraph. This
     reserves exactly its own height plus breathing room. The
     footer does not move. */
  body { padding-bottom: 96px; }

  /* 3. THE BUY BUTTONS.

     .shop-links is a nowrap row, so two pills with long labels
     were forced to share 284px. "Buy on Amazon" wrapped onto
     three lines inside its own pill, which is why each one came
     out 71px tall and looked broken.

     Stacked, each gets the full width and its label fits on one
     line, which also lands them above the 44px touch target. */
  .shop-links {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
    width: 100%;
  }
  .shop-links .shop-link {
    width: 100%;
    text-align: center;
    white-space: nowrap;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  /* 4. THE COVER WAS STUCK LEFT.

     Measured: the image was 170 wide and its parent box was also
     exactly 170 wide, shrink wrapped around it and sitting at the
     left of the card. Centring the image alone could never work,
     because its parent had no spare width to centre it inside.
     The card centres its children instead.

     Consistency matters more than the one page that showed it, so
     this centres any picture that stacks inside a glass panel, not
     just the shop cover. Every card behaves the same way on a
     phone instead of each one having its own answer.

     THE ACTUAL CAUSE, found by reading the rule rather than
     guessing at it: .shop-book is display: flex. text-align does
     nothing to the position of a flex item, which is why the first
     attempt at this changed nothing. The picture sits at the start
     of the main axis because no justify-content was ever set.

     justify-content: center is the one property that moves it, and
     it only applies once the row has wrapped to a single column,
     which is the only time it is wrong. text-align stays left, so
     her book description reads exactly as it always has. */
  .shop-book {
    justify-content: center;
  }
  .glass-container > picture,
  .glass-container .book-tile {
    text-align: center;
  }
  .shop-book picture,
  .glass-container > picture {
    display: block;
  }
  .shop-cover,
  .shop-book img,
  .glass-container > picture > img,
  .glass-container .book-tile > img {
    display: block;
    margin-left: auto;
    margin-right: auto;
    max-width: 100%;
    height: auto;
  }
}

/* ============================================================
   FULL SCREEN SHOWS THE FRAMED PIECE

   Tapping a piece used to open the bare photograph with the frame
   stripped off, which threw away the thing the artist chose.

   The stage below is the same three layers a tile uses, with the
   same window percentages and the same 900 by 1161 shape, so a
   piece looks identical large and small.

   ONE WIDTH, TWO USERS. --fm-stage-w sizes the frame and the
   caption from the same value, so the words are always exactly as
   wide as the picture above them and can never drift apart or run
   to the edge of the screen. Her words: "needs to be balanced with
   the image width".
   ============================================================ */
#lightbox {
  --fm-stage-w: min(86vw, calc(70vh * 900 / 1161));
  padding-left: 16px;
  padding-right: 16px;
  box-sizing: border-box;
}

/* The old single image is retired. The stage replaces it. Kept in
   the markup because main.js still points at it. */
#lightbox .lightbox-content { display: none; }

#lightbox .fm-stage {
  position: relative;
  width: var(--fm-stage-w);
  aspect-ratio: 900 / 1161;
  margin: 0 auto;
  filter: drop-shadow(0 14px 28px rgba(0, 0, 0, 0.45));
}
#lightbox .fm-stage .fm-window {
  position: absolute;
  left: 19.959%;
  top: 18.589%;
  width: 59.970%;
  height: 62.392%;
  overflow: hidden;
  display: block;
  background: #1a1a1a;
}
#lightbox .fm-stage .fm-window img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
#lightbox .fm-stage .fm-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 2;
  user-select: none;
}
#lightbox .fm-stage .fm-frame-img {
  width: 100%;
  height: 100%;
  display: block;
}

#lightbox .lightbox-caption {
  width: var(--fm-stage-w);
  margin: 16px auto 32px;
  padding: 0;
  line-height: 1.5;
}
#lightbox .lightbox-words { display: block; }
#lightbox .lightbox-by {
  display: block;
  margin-top: 6px;
  font-style: italic;
  opacity: 0.85;
}

/* ============================================================
   THE WINNERS PODIUM
   ============================================================
   Top three by heart rate, on the gallery, above Featured
   Finishes. Alisa approved this layout on 22 September 2026
   and the numbers below are locked. They were measured off her
   own reference image, not judged by eye.

   The one rule that matters: gold is anchored from the BOTTOM,
   silver and bronze from the TOP. Mixing those anchors is what
   put a gap under the gold frame the first time, because one
   frame was hanging off the floor and two off the ceiling and
   they could never respond to a change the same way.

   Verified overlap of each frame with its own pedestal top:
     silver -2.92%   gold -3.38%   bronze -3.84%
   Those three numbers are the regression test. Re-measure after
   any change here and confirm they still read the same.
   ============================================================ */
.fm-podium {
  margin: 0 auto 34px;
  max-width: 1200px;
  padding: 0 16px;
}
.fm-podium[hidden] { display: none; }

.fm-podium-tile {
  position: relative;
  overflow: hidden;              /* the confetti never leaves this box */
  border-radius: 20px;
  padding: 20px 14px 16px;
}

.fm-podium-confetti {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 1;
}

.fm-podium-head {
  position: relative;
  z-index: 5;
  text-align: center;
  margin-bottom: 54px;           /* clears the ornament on the gold frame */
}
.fm-podium-head h2 { margin: 0 0 3px; }
.fm-podium-head p { margin: 0; font-size: 0.78rem; opacity: 0.75; }
.fm-podium-head .fm-podium-heart { color: #ff69b4; }

/* ---- the stand and the three seats ---- */
.fm-podium-stage {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 620px;
  margin: 0 auto;
  aspect-ratio: 1400 / 1147;     /* stand plus the headroom above it */
}

.fm-podium-base {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  display: block;
  z-index: 1;
}
.fm-podium-base img { width: 100%; display: block; }

.fm-seat {
  position: absolute;
  z-index: 3;                    /* in FRONT of the stand, standing on it */
  transform: translateX(-50%);
  display: block;
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.45));
}
.fm-seat .fm-frame {
  position: relative;
  z-index: 2;
  display: block;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}
.fm-seat .fm-frame img { width: 100%; display: block; }

/* Same opening as every gallery tile, so there is only one
   definition anywhere of where a frame's window is. */
.fm-seat .fm-window {
  position: absolute;
  left: 19.959%;
  top: 18.589%;
  width: 59.970%;
  height: 62.392%;
  overflow: hidden;
  display: block;
  background: #1a1a1a;
  z-index: 1;
}
.fm-seat .fm-window img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A place nobody has taken yet. */
.fm-seat[data-empty="true"] .fm-window { background: rgba(255, 255, 255, 0.06); }
.fm-seat-empty {
  position: absolute;
  left: 19.959%;
  top: 18.589%;
  width: 59.970%;
  height: 62.392%;
  z-index: 1;
  display: grid;
  place-items: center;
  font-size: 1.5rem;
  font-weight: 700;
  color: #e8e8ee;
  opacity: 0.75;
}
.fm-seat-empty[hidden] { display: none; }

/* THE LOCKED GEOMETRY. Do not nudge without re-measuring. */
.fm-seat-silver { width: 29.7%; left: 18.5%; top: 19.6%; }
.fm-seat-gold   { width: 32.4%; left: 50.2%; bottom: 46.21%; }
.fm-seat-bronze { width: 29.3%; left: 81.6%; top: 28.9%; }

/* ---- the names and the hearts under the stand ---- */
.fm-podium-meta {
  position: relative;
  z-index: 5;
  display: grid;
  grid-template-columns: 32.6% 34.8% 32.6%;   /* matches the pedestal widths */
  max-width: 620px;
  margin: 6px auto 0;
  text-align: center;
}
.fm-podium-place {
  display: block;
  font-size: 0.82rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  margin-bottom: 3px;
}
.fm-podium-place.p-gold   { color: #f6c65b; }
.fm-podium-place.p-silver { color: #cfd6e0; }
.fm-podium-place.p-bronze { color: #cd8347; }

.fm-podium-total { font-size: 0.8rem; opacity: 0.8; }
.fm-podium-total b { font-size: 1rem; opacity: 1; font-variant-numeric: tabular-nums; }
.fm-podium-total .fm-podium-heart { color: #ff69b4; }
.fm-podium-total.is-empty { font-style: normal; opacity: 0.6; }

.fm-podium-who {
  display: block;
  font-size: 0.74rem;
  font-style: italic;
  opacity: 0.7;
  margin-top: 1px;
}

/* ---- the phone ----
   The stand and the three frames are one picture made of four
   images, so they scale together and nothing here changes their
   relationship. Only the tile's own padding and the type sizes
   come down. */
@media (max-width: 600px) {
  .fm-podium { padding: 0 12px; margin-bottom: 26px; }
  .fm-podium-tile { padding: 16px 10px 12px; border-radius: 16px; }
  .fm-podium-head { margin-bottom: 8.5vw; }
  .fm-podium-head p { font-size: 0.72rem; }
  .fm-podium-place { font-size: 0.72rem; letter-spacing: 0.1em; }
  .fm-podium-total { font-size: 0.74rem; }
  .fm-podium-total b { font-size: 0.92rem; }
  .fm-podium-who { font-size: 0.68rem; }
}

/* Anyone who asked for less motion gets a still podium. The
   confetti is switched off in podium.js as well, this is the
   belt to that pair of braces. */
@media (prefers-reduced-motion: reduce) {
  .fm-podium-confetti { display: none; }
}

/* ---- the podium in day mode ----
   Her day theme flattens every span to #054581 with !important,
   which is right for body text and wrong for a podium: it turned
   GOLD, SILVER and BRONZE into three identical blue words and
   took the pink out of the heart.

   These are the same three metals and the same pink, in the
   darker versions that hold up on a light background. #c2185b is
   the day pink she already uses elsewhere on the site, not a new
   colour. */
body.day .fm-podium-place.p-gold   { color: #8a6508 !important; }
body.day .fm-podium-place.p-silver { color: #5c6673 !important; }
body.day .fm-podium-place.p-bronze { color: #8a4b1f !important; }
body.day .fm-podium-heart          { color: #c2185b !important; }
body.day .fm-seat-empty            { color: #054581 !important; opacity: .55; }
body.day .fm-seat[data-empty="true"] .fm-window { background: rgba(5, 69, 129, 0.08); }

/* ============================================================
   FIVE THINGS ALISA CAUGHT ON HER PHONE, 22 September
   ============================================================
   Every rule below names a specific element. Nothing here
   touches html, body or :root, and nothing is removed.
   ============================================================ */

/* ---- 1. THE BLACK GAP AT THE BOTTOM ----

   The real cause, at last. body carries background-color #000,
   which is Alisa's own rule and has been there all along. body
   has no background of its own painting over anything, so that
   colour is handed to the page canvas, which is correct and is
   why the scenes show through.

   But on a phone the address bar retracts as you scroll, and the
   visible area grows taller than the viewport the layout was
   sized against. A background pinned to 100% of that viewport
   stops short, and the strip it leaves uncovered shows the
   canvas, which is black. That is the gap.

   100lvh is the LARGEST the viewport can ever be on that device,
   toolbar retracted. Pinning to that means there is no state in
   which the scene falls short. 100vh stays first as the fallback
   for anything that does not know lvh.

   This is the fix that should have been made this morning,
   instead of colouring html and body, which painted over both
   scenes for five hours. */
#cosmic-background,
#day-sky-background {
  bottom: auto;
  height: 100vh;
  height: 100lvh;
}

/* ---- 2. THE BOOK DROPDOWN RAN EDGE TO EDGE ----
   The name itself is shortened to V1 in gallery-engine.js. This
   gives the control a gutter so it can never touch either edge. */
@media (max-width: 600px) {
  #book-select {
    max-width: calc(100% - 32px);
    margin-left: auto;
    margin-right: auto;
  }
}

/* ---- 3. THE CAPTION CAME OFF THE TILES ----

   On a phone the caption was rendering as a 54 pixel wide column
   nearly 900 pixels tall, straight across the tiles beside it. It
   was not only ugly, it was swallowing taps: a test tap on the
   first tile was intercepted by the third tile's caption.

   The caption stays in the markup, because the full screen view
   reads the wording and the signature out of it. It is only
   hidden, and the hearts and the heart rate underneath are
   untouched. The description now lives in one place: full
   screen, where Alisa said it belongs. */
.gallery-item .caption {
  display: none;
}

/* ---- 4. THE BOOK TITLE ON THE SHOP PAGE ----
   The cover is centred, so the title under it is centred too. */
.shop-details #vol1-heading {
  text-align: center;
}

/* ---- 5. THE ADVERTISING CHECKBOX SAT ABOVE ITS OWN SENTENCE ----

   The row was aligned to flex-start. That is right when a label is
   one line and wrong the moment it wraps: the box jumps to the top
   left corner of a two line block and reads as though it belongs to
   nothing. At 1280 the label is 44px tall and the box is 18px, so it
   was sitting 26px above the centre of its own sentence.

   Centred against the label block instead, which is how the
   anonymous choice above it already reads. Nothing else about the
   row changes: same box, same 8px gap, same wording. */
.ad-choice .sign-choice {
  align-items: center;
}

/* ---- 6. THE GAP BETWEEN A CHECKBOX AND ITS OWN SENTENCE ----

   Measured, not guessed: the row sets an 8px gap, and then the
   label adds its own 10px 20px padding, which is the right padding
   for the form's other labels and wrong here. So every checkbox sat
   28px away from the words it belongs to.

   The left padding is dropped on these two rows only. Top, bottom
   and right padding stay, so the label keeps its tap target, and
   every other label on the form is untouched. The gap is now the
   8px the row actually asks for.

   The selector carries #upload-form because the padding comes from
   #upload-form label, which is an id rule and outranks a class on
   its own. This is the smallest selector that wins, and it wins by
   specificity rather than by !important, so it stays overridable. */
#upload-form .sign-choice label {
  padding-left: 0;
}

/* ---- 7. THE HEARTS COME OUT FROM UNDER THE FRAME ----

   Two things were wrong and they were the same thing.

   The overlay that holds the hearts is opacity 0 until hover. A
   phone has no hover, so on a phone the hearts were invisible and
   there was nothing to tap. That is why Alisa could not vote.

   And it sat INSIDE the frame's opening, over the artwork, which
   is not where she ever wanted it. Her words: the hearts were
   supposed to be right below the frame.

   So the row moves out to its own line under the frame, and it is
   always there rather than waiting for a pointer that will never
   arrive. The caption is already gone from the tile, so this row
   carries only the hearts and the heart rate, which is all it was
   ever meant to carry.

   The frame, the artwork and the window are untouched. Only the
   row underneath moves. */
.gallery-item {
  margin-bottom: 62px;              /* the row's own line, reserved */
}
.gallery-item .overlay {
  position: absolute;
  top: 100%;
  bottom: auto;
  left: 0;
  right: 0;
  opacity: 1;                        /* no hover required, ever */
  background: none;
  padding: 8px 2px 0;
  border-radius: 0;
  z-index: 4;
  pointer-events: auto;
}

/* Comfortable to hit with a thumb. Five hearts have to share the
   width of a tile, so they cannot each be 44px wide on a phone,
   but they get the full height and all the width there is. */
.gallery-item .heart-rating {
  display: flex;
  justify-content: center;
  gap: 2px;
}
.gallery-item .heart-rating span {
  padding: 4px 3px;
  line-height: 1;
  cursor: pointer;
}

/* ---- 8. THE ROW UNDER THE FRAME, MADE TO LOOK LIKE IT BELONGS ----

   Looking at it honestly at phone size: the hearts were 24px
   outlines spread the full 138px of the tile, which made the row
   louder and wider than the frame above it, and the heart rate
   line was set in wide caps across the whole width with its pulse
   glyph clipped off the right edge. It read as three separate
   things stacked up rather than one caption under a picture.

   The frame is the loud thing. This row is the quiet thing under
   it. So: smaller hearts, held together instead of spread apart,
   and the heart rate line tucked in under them at a size that fits
   without being cut. Pulled in from the tile edges so it sits
   under the visible frame rather than out past it.

   Nothing above the frame changes, and the tap targets stay a
   comfortable height. */
.gallery-item {
  margin-bottom: 54px;
}
.gallery-item .overlay {
  padding: 3px 10px 0;
}
.gallery-item .heart-rating {
  gap: 0;
  font-size: 0;                      /* kills the whitespace between spans */
}
.gallery-item .heart-rating span {
  font-size: 18px;
  padding: 4px 3px;
}
.gallery-item .heart-rate-meter {
  margin-top: 1px;
  gap: 4px;
  font-size: 0.66rem;
  letter-spacing: 0.2px;
  flex-wrap: nowrap;
  overflow: hidden;                  /* the pulse glyph was being cut */
}
.gallery-item .heart-rate-meter .hr-label {
  letter-spacing: 0.4px;
}

/* ---- 9. THE HEARTS WERE RED ON ANDROID ----

   U+2665 and U+2661 have two presentations. On a phone the system
   picks the EMOJI one, which is a colour bitmap, and a colour
   bitmap ignores CSS colour completely. So the hearts Alisa styled
   pink came out the emoji's red on her phone and pink on desktop,
   and no amount of colour on the selector could ever have changed
   it.

   Two belts. Every heart in the markup now carries U+FE0E, the
   text presentation selector, which asks for the outline glyph the
   font draws and CSS can colour. font-variant-emoji says the same
   thing to browsers that support it.

   Her pink is the pink she already had. Only the glyph changes. */
.heart-rating span,
.heart-rate-meter .hr-pulse,
.heart-rate-meter .hr-mark,
.fm-podium-heart,
.hh {
  font-variant-emoji: text;
}

/* ---- 10. THE FRAMES WERE TINY BECAUSE OF DEAD MARGIN ----

   Measured on a 390px screen: 20px of padding on the main column
   plus 32px on the gallery section, each side. 104px of the 390,
   more than a quarter of the screen, spent on empty margin, which
   left each frame 138px wide.

   On a phone the section gives that back. Nothing else changes:
   same two columns, same gap, same tiles, just wider. 138px to
   158px per frame, and still 32px clear of both screen edges once
   the main column's own padding is counted. */
@media (max-width: 600px) {
  #gallery-section {
    padding-left: 12px;
    padding-right: 12px;
  }
}

/* ---- 11. WHICH RATING BELONGS TO WHICH PIECE ----

   The hearts sat 3px under their own frame and only 14px above the
   next one, so the eye had no way to tell which piece a rating
   belonged to. Proximity is the only thing doing that work, and 3
   against 14 is not enough of a difference to read.

   The gap between rows opens up instead. Nothing moves closer: the
   rating still hugs the frame it belongs to, and the next frame
   now starts a clear distance below it. Roughly 3px to its own
   piece against 32px to the next one, which reads at a glance.

   The gap BETWEEN columns is untouched. */
#gallery-grid {
  row-gap: 34px;
}
@media (max-width: 600px) {
  #gallery-grid {
    row-gap: 30px;
    column-gap: 10px;
  }
}

/* ---- 12. THE HEART IS DRAWN NOW, NOT TYPED ----

   U+FE0E was not enough. Android still handed the character to the
   colour emoji font, and a colour emoji is a bitmap that ignores
   CSS colour, so Alisa's hearts stayed the emoji's red on her phone
   however they were styled.

   They are paths now, filled or stroked with currentColor, so
   colour comes from CSS again on every device. Nothing about her
   colours changes, and .active still works because it only ever
   changed the colour.

   1em square so they still scale with whatever font-size the
   surrounding rule sets, and nudged down slightly so they sit on
   the same line as the text beside them. */
.fm-heart {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.14em;
  overflow: visible;
}
.heart-rating span {
  display: inline-flex;
  align-items: center;
  color: #ff69b4;
}

/* The drawn heart is an inline-block, which gives the line a place
   to break that the old character never did. heart(heart)rate is
   one word, so it never breaks. */
.heart-rate-meter,
.heart-rate-meter .hr-label {
  white-space: nowrap;
}

/* ---- 13. SIGNING IS THE REQUIRED ANSWER ----
   The line under the signature row that says so, and the state a
   field gets when it is the thing standing in the way. */
.sign-note {
  flex-basis: 100%;
  margin: 6px 0 0;
  font-size: 0.78rem;
  opacity: 0.7;
  text-align: center;
}
#signature-input[aria-invalid="true"] {
  border-color: #c2185b;
  box-shadow: 0 0 10px rgba(194, 24, 91, 0.45);
}

/* ============================================================
   THE GALLERY PAGE FINDS ITS FEET
   ============================================================
   This page does two jobs: it shows finished art and it takes new
   art in. It is also the page people share, so it is the page that
   earns the traffic, which is why the form folds away here rather
   than moving to a page of its own. Splitting it would cost a click
   at exactly the moment somebody has decided to submit.
   ============================================================ */

/* ---- the jump bar ---- */
.gallery-jump {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin: 14px auto 6px;
  padding: 0 12px;
}
.jump-link {
  display: inline-block;
  padding: 9px 16px;
  min-height: 40px;
  line-height: 22px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background: rgba(255, 255, 255, 0.05);
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
  transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
}
.jump-link:hover,
.jump-link:focus-visible {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.4);
}
/* The one that takes art in gets to look like the invitation it is. */
.jump-share {
  border-color: rgba(255, 105, 180, 0.6);
  background: rgba(255, 105, 180, 0.14);
  box-shadow: 0 0 12px rgba(255, 105, 180, 0.3);
}
.jump-share:hover,
.jump-share:focus-visible {
  background: rgba(255, 105, 180, 0.26);
  border-color: #ff69b4;
  box-shadow: 0 0 18px rgba(255, 105, 180, 0.55);
}
body.day .jump-link { border-color: rgba(5, 69, 129, 0.25); background: rgba(5, 69, 129, 0.06); }
body.day .jump-share { border-color: rgba(194, 24, 91, 0.5); background: rgba(194, 24, 91, 0.08); }

/* ---- the form that folds ---- */
.submission-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 4px 0;
  border-radius: 14px;
}
.submission-summary::-webkit-details-marker { display: none; }
.submission-summary h2 { margin: 0; }
.submission-hint {
  font-size: 0.8rem;
  opacity: 0.72;
}
.submission-fold[open] .submission-hint { opacity: 0; height: 0; overflow: hidden; }
.submission-summary:focus-visible {
  outline: 3px solid #ff69b4;
  outline-offset: 3px;
}
/* A closed panel should look closed, not like a heading that lost
   its section. */
.submission-summary::after {
  content: '';
  display: block;
  width: 26px;
  height: 26px;
  margin-top: 6px;
  background: currentColor;
  opacity: 0.55;
  -webkit-mask: no-repeat center / 20px 20px
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>');
  mask: no-repeat center / 20px 20px
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 9l6 6 6-6" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>');
  transition: transform 0.2s;
}
.submission-fold[open] .submission-summary::after { transform: rotate(180deg); }

/* ---- SHARE THIS PIECE ----
   Sits under the framed artwork in the full screen view, where the
   eye already is after reading the caption. */
.lightbox-share {
  position: relative;
  z-index: 4;
  margin: 14px auto 0;
  display: block;
  font-size: 0.86rem;
  padding: 10px 20px;
  min-height: 44px;
}
.lightbox-share[hidden] { display: none; }

/* ---- 14. THE RIGHT COLUMN RAN INTO THE SCREEN EDGE ----

   The tiles themselves always fitted. What did not was their ink.
   Every tile carries drop-shadow(0 14px 28px), and a 28px blur
   spreads 28px past the box on every side. Measured at 360, 390 and
   412: the box ended 32px clear of the screen, so the shadow ended
   4px clear. Four pixels, on a device with rounded corners and a
   gesture bar, reads as a frame running off the edge.

   I made that worse myself: the side padding went from 32 to 12 to
   give the frames more width. This puts some back and softens the
   shadow on a phone, where a 28px blur is doing nothing anyone can
   see anyway.

   16px padding, 20px blur. Box ends 36px clear, ink ends 16px
   clear, and the frames stay far larger than the 138px they were
   before any of this. */
@media (max-width: 600px) {
  #gallery-section {
    padding-left: 16px;
    padding-right: 16px;
  }
  .gallery-item {
    filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.45));
  }
}

/* ---- 15. WHY THE RIGHT COLUMN SPILLED ON AN IPHONE ----

   Reported from Safari on an iPhone, and not reproducible in
   Chrome, which is the clue.

   A grid item's min-width is auto by default, which means a column
   is not allowed to shrink narrower than its own content. Two
   commits ago I gave the heart rate line white-space: nowrap, to
   stop "heart(heart)rate" breaking across two lines when the heart
   became a drawn shape instead of a character. On a narrow phone,
   with Safari's font metrics rather than Chrome's, that unbreakable
   line measures wider than its column. The column refuses to shrink
   to fit, both columns widen, and the grid grows past the screen.
   The second column is the one that falls off the edge.

   min-width: 0 is the standard fix and it is the right one: it
   tells the columns they may shrink to the track size they were
   given, which is exactly what repeat(2, 1fr) already said. The
   meter keeps nowrap and keeps its own overflow clip, so the line
   still never breaks; it is just no longer allowed to dictate how
   wide the gallery is.

   Belt and braces on the same idea: the grid itself may shrink, and
   the tile is explicitly the width of its track. */
#gallery-grid {
  min-width: 0;
}
.gallery-item {
  min-width: 0;
  max-width: 100%;
  width: 100%;
}
.gallery-item .overlay,
.gallery-item .heart-rate-meter,
.gallery-item .heart-rating {
  min-width: 0;
  max-width: 100%;
}

/* And the line should FIT, not merely be stopped from doing damage.

   Measured at 320px: the heart rate line wanted 111px inside a 99px
   box, so Chrome was quietly clipping the end of it and Safari was
   widening the column to make room. Neither is right. It scales
   down on a narrow phone so the whole line is readable and nothing
   is cut. */
@media (max-width: 400px) {
  .gallery-item .heart-rate-meter {
    font-size: 0.58rem;
    gap: 3px;
  }
  .gallery-item .heart-rating span {
    font-size: 16px;
    padding: 4px 2px;
  }
}

/* ---- 16. THE LEGAL LINKS LANDED ON THE LAST PIECE OF ART ----

   Reported from Safari on an iPhone. The hearts row is positioned
   at top:100% of its tile, which means it paints below the tile but
   takes up no space of its own. Space is reserved by a margin on
   the tile, and a margin on a grid item that also has an
   aspect-ratio is exactly the kind of thing Safari and Chrome
   disagree about. When Safari drops it, the last row's hearts spill
   past the bottom of the grid, and whatever comes next, which is
   the Terms and Contact line, is drawn straight through them.

   Reserving the space on the GRID instead removes the disagreement
   entirely. Padding on a container is not a thing any browser gets
   wrong, it applies after the last row as well as between rows, and
   it does not depend on the items at all. The margin stays as
   belt and braces; this is the braces. */
#gallery-grid {
  padding-bottom: 60px;
}

/* The same guarantee for the page itself, so the last row can never
   reach the fixed pill at the bottom of the screen. */
@media (max-width: 600px) {
  #gallery-section { padding-bottom: 18px; }
}

/* On the narrowest phones the heart rate line still wanted 109px in
   a 99px box, so the end of it was being quietly clipped. The
   letter spacing is what costs the most there, so that goes first. */
@media (max-width: 340px) {
  .gallery-item .heart-rate-meter { font-size: 0.54rem; gap: 2px; }
  .gallery-item .heart-rate-meter .hr-label { letter-spacing: 0; }
}

/* Still six pixels over on a 320px screen. The little ECG trace is
   decoration, and decoration is what goes when there is no room.
   The word, the number and the beating heart all stay. */
@media (max-width: 340px) {
  .gallery-item .hr-monitor { display: none; }
}

/* Five pixels over on a 320px screen, which is an old iPhone SE.
   The column gap is ten, so letting those five show is better than
   clipping the end off the beating heart. It cannot reach the
   neighbouring tile and it cannot widen the column, because the
   column's min-width is zero. */
@media (max-width: 340px) {
  .gallery-item .heart-rate-meter { overflow: visible; }
}

/* ---- 17. THE CLOSE X WAS BEHIND THE FRAME ----

   .close-btn is absolutely positioned with no z-index, and it comes
   first in the lightbox's markup. The framed stage is added after
   it, so the frame painted straight over the top half of the X.
   It still worked. You just could not see it.

   Raised above everything in the lightbox, and given a soft dark
   disc so it stays readable whatever colour frame is behind it.
   44 by 44, which is a proper thumb target, and it respects the
   notch on a phone. */
#lightbox .close-btn {
  z-index: 20;
  top: max(12px, env(safe-area-inset-top, 12px));
  right: 14px;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
  background: rgba(9, 10, 15, 0.55);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  font-size: 2rem;
}
#lightbox .close-btn:hover,
#lightbox .close-btn:focus-visible {
  background: rgba(9, 10, 15, 0.78);
}

/* ---- 18. GET THIS BOOK ----

   Sits beside Share this piece under the framed artwork. Somebody
   looking at a finished page is the warmest audience the site will
   ever have, and until now there was nothing there to buy with.

   The two buttons share a row so neither reads as the main one
   until you look: sharing is what the artist wants, buying is what
   the visitor might want, and both belong to the same moment. */
.lightbox-actions {
  position: relative;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin: 14px auto 0;
}
.lightbox-share,
.lightbox-buy {
  position: relative;
  z-index: 4;
  margin: 14px 6px 0;
  display: inline-grid;
  place-items: center;
  font-size: 0.86rem;
  padding: 10px 20px;
  min-height: 44px;
  text-decoration: none;
}
.lightbox-buy {
  border-color: rgba(255, 105, 180, 0.65);
  background: rgba(255, 105, 180, 0.16);
  box-shadow: 0 0 14px rgba(255, 105, 180, 0.35);
}
.lightbox-buy:hover,
.lightbox-buy:focus-visible {
  background: rgba(255, 105, 180, 0.28);
  border-color: #ff69b4;
}
.lightbox-share[hidden],
.lightbox-buy[hidden] { display: none; }

/* ---- the shop page catches the landing ----
   The header floats over the top of the page, so an anchor without
   a margin puts the book's title underneath it. And when somebody
   arrives from the gallery, the book they came for says so for a
   moment rather than looking like any other section. */
.shop-section {
  scroll-margin-top: 110px;
}
.shop-section:target {
  animation: fm-book-arrival 2.4s ease-out 1;
}
@keyframes fm-book-arrival {
  0%   { box-shadow: 0 0 0 0 rgba(255, 105, 180, 0); }
  18%  { box-shadow: 0 0 26px 4px rgba(255, 105, 180, 0.55); }
  100% { box-shadow: 0 0 0 0 rgba(255, 105, 180, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .shop-section:target { animation: none; }
}
