/*
 * Gardening Journal - Site-Wide CSS
 *
 * Design principles:
 * - Mobile-first responsive design
 * - iOS-style photo grid with natural aspect ratios
 * - TurboTax-style centered dialogs for simple screens
 * - Optimal line length (50-75 chars) for notes
 * - Light and dark mode support
 */

/* ==========================================================================
   Fonts

   The rule: Concourse (sans) for what you operate — headings (h1-h3),
   buttons, menu items, form labels; Equity B (serif) for what you
   read — body, prose, notes, captions.  Equity ships in 400,
   400 italic, and 700; declared weights of 500/600 in Equity contexts
   resolve to the nearest real weight (400 and 700) by the CSS
   font-matching algorithm, so nothing is synthesized.
   See docs/style-guide.md.

   Font files use a manual -v1 version suffix instead of a content hash
   (they never change; bump the suffix if they ever do) and are exempted
   from hash renaming in assets/gen-assets.sh.  font-display: swap so text
   renders immediately for cold-cache visitors (shared story links).
   ========================================================================== */

@font-face {
    font-family: "Concourse";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("fonts/concourse-regular-v1.woff2") format("woff2");
}

@font-face {
    font-family: "Concourse";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("fonts/concourse-medium-v1.woff2") format("woff2");
}

@font-face {
    font-family: "Concourse";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("fonts/concourse-semibold-v1.woff2") format("woff2");
}

@font-face {
    font-family: "Concourse";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("fonts/concourse-bold-v1.woff2") format("woff2");
}

@font-face {
    font-family: "Equity";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("fonts/equity-b-regular-v1.woff2") format("woff2");
}

@font-face {
    font-family: "Equity";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("fonts/equity-b-italic-v1.woff2") format("woff2");
}

@font-face {
    font-family: "Equity";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("fonts/equity-b-bold-v1.woff2") format("woff2");
}

/* ==========================================================================
   CSS Variables - Color Palette
   ========================================================================== */

:root {
    /* Primary - Teal */
    --teal-15: #014D40;
    --teal-23: #0C6B58;
    --teal-28: #147D64;
    --teal-34: #199473;
    --teal-41: #27AB83;
    --teal-49: #3EBD93;
    --teal-62: #65D6AD;
    --teal-74: #8EEDC7;
    --teal-87: #C6F7E2;
    --teal-96: #EFFCF6;

    /* Neutrals - Blue Grey */
    --grey-16: #102A43;
    --grey-23: #243B53;
    --grey-30: #334E68;
    --grey-39: #486581;
    --grey-49: #627D98;
    --grey-60: #829AB1;
    --grey-70: #9FB3C8;
    --grey-80: #BCCCDC;
    --grey-89: #D9E2EC;
    --grey-96: #F0F4F8;

    /* Supporting - Red */
    --red-20: #610404;
    --red-25: #780A0A;
    --red-32: #911111;
    --red-38: #A61B1B;
    --red-44: #BA2525;
    --red-55: #D64545;
    --red-66: #E66A6A;
    /* Off the scale's usual desaturation curve on purpose: dark mode's
       danger colour has to clear 3:1 as a glyph on a card without going
       pale. 50% saturation, 3.78 on a dark card, 5.04 on the page. */
    --red-75: #FF8A80;
    --red-78: #F29B9B;
    --red-89: #FACDCD;
    --red-97: #FFEEEE;

    /* Supporting - Blue */
    --blue-21: #003E6B;
    --blue-29: #0A558C;
    --blue-33: #0F609B;
    --blue-39: #186FAF;
    --blue-45: #2680C2;
    --blue-55: #4098D7;
    --blue-65: #62B0E8;
    --blue-75: #84C5F4;
    --blue-85: #B6E0FE;
    --blue-92: #DCEEFB;

    /* Supporting - Yellow */
    --yellow-17: #513C06;
    --yellow-27: #7C5E10;
    --yellow-37: #A27C1A;
    --yellow-48: #C99A2E;
    --yellow-60: #E9B949;
    --yellow-70: #F7D070;
    --yellow-76: #F9DA8B;
    --yellow-81: #F8E3A3;
    --yellow-88: #FCEFC7;
    --yellow-92: #FDF3D9;
    --yellow-96: #FFFAEB;

    /* Supporting - Purple */
    --purple-18: #240754;
    --purple-25: #34126F;
    --purple-31: #421987;
    --purple-38: #51279B;
    --purple-46: #653CAD;
    --purple-51: #724BB7;
    --purple-58: #8662C7;
    --purple-68: #A081D9;
    --purple-84: #CFBCF2;
    --purple-93: #EAE2F8;

    /* Semantic - Light Mode (default) */
    --color-bg: var(--yellow-92);
    /* Surfaces sit ABOVE the page: a card or control is shaped by a fill
       lighter than --color-bg, never by a border. See docs/style-guide.md. */
    --color-surface: var(--yellow-96);
    --color-bg-elevated: var(--yellow-88);
    --color-bg-subtle: var(--yellow-88);
    --color-text: var(--grey-23);
    --color-text-secondary: var(--grey-49);
    --color-text-muted: var(--grey-60);
    --color-border: var(--grey-70);
    --color-border-subtle: var(--grey-96);
    /* Control outlines need 3:1 against the page (WCAG 1.4.11).
       --color-border (grey-70) is 1.95:1 and cannot carry a control. */
    --color-border-control: var(--grey-49);
    /* teal-41 was 2.62:1 against the light page and white on it was
       2.77:1 — the commit chip failed the same threshold the neutrals
       did. teal-34 is the only token clearing 3:1 against BOTH page
       colours; hover and active step down with it so hover does not
       collapse into base. Dark mode already passes at teal-49. */
    --color-primary: var(--teal-34);
    --color-primary-hover: var(--teal-28);
    --color-primary-active: var(--teal-23);
    --color-error: var(--red-44);
    --color-danger: var(--red-44);
    --color-error-bg: var(--red-97);
    --color-warning-bg: var(--yellow-81);
    --color-warning-border: var(--yellow-60);
    --color-link: var(--blue-39);
    /* Tags. Purple was the only hue with no meaning attached: teal is
       primary and means "this commits", blue is link, red is danger,
       yellow is the page. 8.99:1 on the page, 9.52:1 on a card. */
    --color-tag: var(--purple-38);
    /* Hover leaves purple entirely. Staying in the column cannot carry
       it: the deepest purple is only dE 14 from the resting colour, and
       body ink is dE 15 - against a rest that is already dark, a second
       dark colour reads as no change at all. Yellow is the one warm hue
       with no duty attached (teal commits, blue links, red warns), and
       it is where dark mode was already landing by accident, so one
       sentence covers both: a tag hovers warm. dE 57 from rest.

       This is the one place in the sheet where hover LOWERS contrast,
       9.0:1 to 5.5:1. Deliberate: the darker gold that raises it
       instead was tried and could not be seen at all against the
       resting purple. 5.5:1 still clears the 4.5:1 floor with room. */
    --color-tag-hover: var(--yellow-27);
    --tag-weight: 600;
    --color-link-hover: var(--blue-21);
    --color-accent: var(--teal-49);
    --color-text-inverse: var(--yellow-96);
    --color-backdrop: var(--grey-16);
    --color-overlay: color-mix(in srgb, var(--grey-16) 50%, transparent);
    --color-overlay-badge: color-mix(in srgb, var(--grey-16) 50%, transparent);
    --color-shadow: color-mix(in srgb, var(--grey-16) 20%, transparent);
    --color-shadow-heavy: color-mix(in srgb, var(--grey-16) 15%, transparent);
    --color-input-bg: var(--color-surface);
    --color-focus-ring: color-mix(in srgb, var(--teal-28) 20%, transparent);
    --color-navbar-bg: var(--purple-93);
    --color-nav-inactive: var(--grey-39);
    --color-nav-inactive-hover: var(--color-primary);

    /* Layout */
    --header-height: 56px;
    --tab-bar-height: 56px;
    --content-max-width: 900px;
    --measure: 70ch; /* Prose reading width (60-90ch is acceptable) */
    --note-max-width: var(--measure);
    /* Every photo is this long on its longest edge, whichever edge that
       is: a landscape is this wide, a portrait this tall. Naming it
       "max-width" was a lie for portrait photos. */
    --photo-long-side: 480px;
    --photo-gap: 2px;
    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 2rem;
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;

    /* Typography */
    --font-family: 'Equity', Georgia, 'Times New Roman', serif;
    --font-family-prose: 'Equity', Georgia, 'Times New Roman', serif;
    --font-family-headings: 'Concourse', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
    --font-size-xs: 0.75rem;
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.125rem;
    --font-size-xl: 1.25rem;
    --font-size-2xl: 1.5rem;
    --font-size-3xl: 2rem;
    --line-height-tight: 1.25;
    --line-height-normal: 1.5;
    --line-height-relaxed: 1.75;
}

/* Dark Mode */
@media (prefers-color-scheme: dark) {
    :root {
        --color-bg: var(--grey-23);
        --color-surface: var(--grey-30);
        --color-bg-elevated: var(--grey-30);
        --color-bg-subtle: var(--grey-39);
        --color-text: var(--yellow-96);
        --color-text-secondary: var(--grey-70);
        --color-text-muted: var(--grey-60);
        --color-border: var(--grey-39);
        --color-border-subtle: var(--grey-30);
        /* grey-60 was 3.95 on the dark page but only 2.97 on a dark
           card, and a card is where most controls sit. Measure a control
           token against the CARD, not the page. grey-70: 5.34 / 4.01. */
        --color-border-control: var(--grey-70);
        --color-primary: var(--teal-49);
        --color-primary-hover: var(--teal-62);
        --color-primary-active: var(--teal-74);
        --color-error: var(--red-66);
        --color-danger: var(--red-75);
        --color-error-bg: var(--red-20);
        --color-warning-bg: var(--yellow-17);
        --color-warning-border: var(--yellow-48);
        --color-link: var(--blue-65);
        /* 6.64:1 page, 4.99:1 card. purple-68 was tried and rejected at
           2.73:1 on a card - the same figure --color-danger shipped at
           before item 21 had to fix it, and tags do land on a card. */
        --color-tag: var(--purple-84);
        /* Pale gold, not the deep one: on a dark ground hover moves
           away from the page, so here it raises contrast, 6.64:1 to
           11.02:1. Same hue as light mode, opposite end of it. */
        --color-tag-hover: var(--yellow-96);
        /* Light ink on a dark ground optically spreads, so the same
           stroke reads heavier than it does dark-on-light. One weight
           step down compensates. */
        --tag-weight: 500;
        --color-link-hover: var(--blue-75);
        --color-accent: var(--teal-62);
        --color-focus-ring: color-mix(in srgb, var(--teal-49) 20%, transparent);
        --color-navbar-bg: var(--purple-25);
        --color-nav-inactive: var(--grey-60);
        --color-nav-inactive-hover: var(--color-primary);
    }
}

/* ==========================================================================
   Reset & Base
   ========================================================================== */

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

/* The UA's own `[hidden] { display: none }` is a bare element rule, so
   any class that sets display beats it on specificity — and .btn and
   .action-btn both set `display: inline-flex`. Without this, the hidden
   attribute silently does nothing on any button in this design system,
   which is how the Account edit session would have rendered back, check
   and every row's Delete while the screen was at rest. Approved
   2026-08-24. */
[hidden] { display: none !important; }

html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
}

body {
    font-family: var(--font-family);
    font-size: var(--font-size-xl); /* body text IS prose; Equity runs small */
    line-height: var(--line-height-relaxed);
    color: var(--color-text);
    background-color: var(--color-bg);
    min-height: 100vh;
    min-height: 100dvh; /* Dynamic viewport height for mobile */
}

img {
    max-width: 100%;
    height: auto;
    display: block;
}

/* HEADING: one style, three sizes (L 2rem / M 1.5rem / S 1.25rem).
   Concourse 600 (700 reads too heavy), 0.02em letter-spacing.
   Rhythm: 2:1 space above vs. below binds a heading to the content it
   introduces; h1 sits at the top of the page (no space above).
   See docs/style-guide.md. */
h1 {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-3xl);
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: var(--line-height-tight);
    margin: 0 0 1rem;
}

h2 {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-2xl);
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: var(--line-height-tight);
    margin: 2rem 0 0.75rem;
}

h3 {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-xl);
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: var(--line-height-tight);
    color: var(--color-text-secondary);
}

/* Heading S as a class, for date headers on the journal timeline and
   the story subtitle — one class so they stay in sync. Carrier classes
   (.timeline-date, .story-date) keep only layout. */
.date-heading {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-xl);
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: var(--line-height-tight);
    color: var(--color-text-secondary);
    text-transform: none;
}

a {
    color: var(--color-link);
    text-decoration: underline;
}

@media (hover: hover) {
    a:hover {
        color: var(--color-link-hover);
    }
}

.btn-link {
    background: none;
    border: none;
    padding: 0;
    color: var(--color-link);
    cursor: pointer;
    font-size: inherit;
    text-decoration: underline;
}

@media (hover: hover) {
    .btn-link:hover {
        color: var(--color-link-hover);
    }
}

/* Lists */
ol, ul {
    padding-left: 1.5rem;
    margin-bottom: var(--spacing-md);
}

ol li, ul li {
    margin-bottom: var(--spacing-sm);
    line-height: var(--line-height-relaxed);
}

ol li:last-child, ul li:last-child {
    margin-bottom: 0;
}

.emphasize {
    font-weight: 700; /* Equity has no 600; use its real bold */
}

/* ==========================================================================
   Page Layouts
   ========================================================================== */

/*
 * Centered layout - TurboTax style
 * For simple screens: Home, Login, Signup Success, etc.
 * Content centers both vertically and horizontally
 */
.page-centered {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: var(--spacing-lg);
}

.page-centered-content {
    width: 100%;
    max-width: 400px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-md);
}

/* Flex gap governs spacing here; the h1 rhythm margin would double it. */
.page-centered-content h1 {
    margin: 0;
}

/*
 * App layout - with sticky header and tab bar
 * For authenticated screens: Journal, Review, Explore, Settings
 */
.page-app {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
}

.page-app-content {
    flex: 1;
    padding-top: var(--header-height);
    padding-bottom: var(--tab-bar-height);
    overflow-y: auto;
}

/* ==========================================================================
   Sticky Header
   ========================================================================== */

.header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--header-height);
    background-color: var(--color-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 var(--spacing-md);
    z-index: 100;
}

.header-inner {
    width: 100%;
    max-width: var(--content-max-width);
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* space-between leaves room between the title and the buttons only
       while there is room to spare. Once a long title is cut short, its
       "…" would touch the button; gap keeps at least 8px between them. */
    gap: var(--spacing-sm);
}

/* Centre, matching .header-inner and .header-actions. This aligned on
   the text baseline until 2026-08-31, which looked right only because
   nothing ever sat beside the heading to disagree with it. Put a round
   button next to a title and the two line up on the text inside them,
   so the button rides low - while the same button on the other side of
   the bar is centred. Baseline was the one rule in the header out of
   step, so anything added beside a title inherited the fault. */
.header-left {
    display: flex;
    align-items: center;
    gap: var(--spacing-md); /* 16px: at 8px the back arrow crowded the title. Approved 2026-10-10. */
    min-width: 0; /* a flex item will not shrink narrower than its text unless told it may */
}

/* A title too long for the bar stays on one line and ends in "…", as a
   phone app's top bar does. The header is a fixed 56px, so a second line
   spilled out above and below it, and the long title squeezed the back
   arrow into an oval.
   - min-width: 0 lets the title shrink narrower than its text.
   - white-space: nowrap keeps it on one line.
   - overflow: hidden plus text-overflow: ellipsis cut the text at the
     edge and draw "…" in place of the rest.
   Approved 2026-10-10. */
.header-title {
    color: var(--color-text);
    margin: 0; /* h1 rhythm margin does not apply inside the sticky header */
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* On a phone the header title drops from Heading L (2rem, 32px) to
   Heading M (1.5rem, 24px), so more of a long tag name shows before the
   "…". max-width: 479.98px applies this only while the window is
   narrower than 480px - every phone held upright. Body text keeps its
   size. Approved 2026-10-10. */
@media (max-width: 479.98px) {
    .header-title {
        font-size: var(--font-size-2xl);
    }
}

.header-date {
    font-size: var(--font-size-sm);
    font-weight: 400;
    color: var(--color-text-muted);
}

.header-actions {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    /* The buttons never give up room; the title does. Without this the
       group shrank to its narrowest content - for "Rename Tag" the single
       word "Rename" - while the button inside kept its full width and
       hung past the edge. Approved 2026-10-10. */
    flex-shrink: 0;
}

.header-action {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-primary);
    background: none;
    border: none;
    border-radius: var(--radius-md);
    cursor: pointer;
    font-size: var(--font-size-xl);
}

@media (hover: hover) {
    .header-action:hover {
        background-color: var(--color-bg-subtle);
    }
}

/* ==========================================================================
   Bottom Navbar (shared shell for app tab bar and content page nav)
   ========================================================================== */

.navbar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: var(--tab-bar-height);
    background-color: var(--color-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    padding-bottom: env(safe-area-inset-bottom, 0);
    z-index: 100;
}

.navbar-inner {
    width: 100%;
    max-width: var(--content-max-width);
    padding: 0 var(--spacing-md);
    display: flex;
    align-items: center;
    justify-content: space-around;
}

/* -- App tab bar items --------------------------------------------------- */

.navbar-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: var(--spacing-xs);
    background: none;
    border: none;
    cursor: pointer;
    color: var(--color-nav-inactive);
    text-decoration: none;
    font-size: var(--font-size-xs);
    transition: color 0.15s;
}

/* Bottom tab bar items stretch to fill equal widths.
   The header "Add Photos" button also uses .navbar-item
   but should stay at its natural size. */
.navbar-inner .navbar-item {
    flex: 1;
}

@media (hover: hover) {
    .navbar-item:hover {
        text-decoration: none;
        color: var(--color-nav-inactive-hover);
    }
}

.navbar-item.active {
    color: var(--color-primary);
}

.navbar-icon {
    display: inline-block;
    width: 24px;
    height: 24px;
    background-color: currentColor;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
}

.navbar-icon-home {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23000000' viewBox='0 0 256 256'%3E%3Cpath d='M224,120v96a8,8,0,0,1-8,8H160a8,8,0,0,1-8-8V164a4,4,0,0,0-4-4H108a4,4,0,0,0-4,4v52a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V120a16,16,0,0,1,4.69-11.31l80-80a16,16,0,0,1,22.62,0l80,80A16,16,0,0,1,224,120Z'%3E%3C/path%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23000000' viewBox='0 0 256 256'%3E%3Cpath d='M224,120v96a8,8,0,0,1-8,8H160a8,8,0,0,1-8-8V164a4,4,0,0,0-4-4H108a4,4,0,0,0-4,4v52a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V120a16,16,0,0,1,4.69-11.31l80-80a16,16,0,0,1,22.62,0l80,80A16,16,0,0,1,224,120Z'%3E%3C/path%3E%3C/svg%3E");
}

.navbar-icon-journal {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23000000' viewBox='0 0 256 256'%3E%3Cpath d='M232,48H168a32,32,0,0,0-32,32v87.73a8.17,8.17,0,0,1-7.47,8.25,8,8,0,0,1-8.53-8V80A32,32,0,0,0,88,48H24a8,8,0,0,0-8,8V200a8,8,0,0,0,8,8H96a24,24,0,0,1,24,23.94,7.9,7.9,0,0,0,5.12,7.55A8,8,0,0,0,136,232a24,24,0,0,1,24-24h72a8,8,0,0,0,8-8V56A8,8,0,0,0,232,48ZM208,168H168.27a8.17,8.17,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h39.73a8.17,8.17,0,0,1,8.25,7.47A8,8,0,0,1,208,168Zm0-32H168.27a8.17,8.17,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h39.73a8.17,8.17,0,0,1,8.25,7.47A8,8,0,0,1,208,136Zm0-32H168.27A8.17,8.17,0,0,1,160,96.53,8,8,0,0,1,168,88h39.73A8.17,8.17,0,0,1,216,95.47,8,8,0,0,1,208,104Z'%3E%3C/path%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23000000' viewBox='0 0 256 256'%3E%3Cpath d='M232,48H168a32,32,0,0,0-32,32v87.73a8.17,8.17,0,0,1-7.47,8.25,8,8,0,0,1-8.53-8V80A32,32,0,0,0,88,48H24a8,8,0,0,0-8,8V200a8,8,0,0,0,8,8H96a24,24,0,0,1,24,23.94,7.9,7.9,0,0,0,5.12,7.55A8,8,0,0,0,136,232a24,24,0,0,1,24-24h72a8,8,0,0,0,8-8V56A8,8,0,0,0,232,48ZM208,168H168.27a8.17,8.17,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h39.73a8.17,8.17,0,0,1,8.25,7.47A8,8,0,0,1,208,168Zm0-32H168.27a8.17,8.17,0,0,1-8.25-7.47,8,8,0,0,1,8-8.53h39.73a8.17,8.17,0,0,1,8.25,7.47A8,8,0,0,1,208,136Zm0-32H168.27A8.17,8.17,0,0,1,160,96.53,8,8,0,0,1,168,88h39.73A8.17,8.17,0,0,1,216,95.47,8,8,0,0,1,208,104Z'%3E%3C/path%3E%3C/svg%3E");
}

.navbar-icon-account {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23000000' viewBox='0 0 256 256'%3E%3Cpath d='M230.93,220a8,8,0,0,1-6.93,4H32a8,8,0,0,1-6.92-12c15.23-26.33,38.7-45.21,66.09-54.16a72,72,0,1,1,73.66,0c27.39,8.95,50.86,27.83,66.09,54.16A8,8,0,0,1,230.93,220Z'%3E%3C/path%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' fill='%23000000' viewBox='0 0 256 256'%3E%3Cpath d='M230.93,220a8,8,0,0,1-6.93,4H32a8,8,0,0,1-6.92-12c15.23-26.33,38.7-45.21,66.09-54.16a72,72,0,1,1,73.66,0c27.39,8.95,50.86,27.83,66.09,54.16A8,8,0,0,1,230.93,220Z'%3E%3C/path%3E%3C/svg%3E");
}

/* UI Label: Concourse 500 at 1rem, as approved in the specimen. */
.navbar-label {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-base);
    font-weight: 500;
}

/* ==========================================================================
   Photo Grid - iOS Style

   Photos display at natural aspect ratios (4:3 landscape, 3:4 portrait)
   aligned at horizontal and vertical center lines.
   Minimal gaps between photos.
   ========================================================================== */

/* Photos are the stars, and every photo is the same length on its
   longest edge -- a landscape and a portrait must not read as different
   zoom levels.  --photo-ar is the photo's own width/height, emitted
   inline from the persisted dimensions.  Landscape has ar > 1 so min()
   picks 1 and the box is the full long side; portrait has ar < 1 so the
   box is L*ar wide, which makes it exactly L tall.  The short side falls
   out of the photo's own ratio, so nothing is cropped.

   Driven by width, not max-width: max-width only clamps, so a photo
   smaller than the cap would keep its own size and the long side would
   come out different for every photo.

   A photo with no persisted dimensions has no --photo-ar and falls back
   to 4/3, today's assumption, which object-fit: cover then crops to.
   Where the ratio IS known the box matches the photo exactly and cover
   has nothing left to crop, so one declaration serves both. */
.photo-grid-item {
    width: calc(var(--photo-long-side) * min(1, var(--photo-ar, 4/3)));
    max-width: 100%;
    margin-bottom: var(--spacing-sm);
}

/* height:auto keeps the ratio when max-width shrinks the box on a narrow
   phone, so a portrait never distorts to fit. */
.photo-grid-item img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: var(--photo-ar, 4/3);
    object-fit: cover;
    background-color: var(--color-bg-subtle);
    border-radius: var(--radius-md);
}

/* ==========================================================================
   Timeline View

   Photos and notes organized by date sections.
   Date headers separate content.
   ========================================================================== */

.timeline {
    padding: var(--spacing-md);
    max-width: var(--content-max-width);
    margin: 0 auto;
}

.timeline-section {
    background: var(--color-surface);
    border-radius: var(--radius-md);
    padding: var(--spacing-md);
    margin-bottom: var(--spacing-md);
}

/* Layout only; typography comes from .date-heading. */
.timeline-date {
    padding: 0 0 var(--spacing-sm);
    margin-bottom: var(--spacing-sm);
}

/* Full-width stacked photos; each .photo-grid-item carries its own
   bottom margin and width cap. */
.timeline-photos {
    margin-bottom: var(--spacing-sm);
}

/* ==========================================================================
   Notes - Optimal Readability

   Line length between 50-75 characters for comfortable reading.
   ========================================================================== */

.note {
    background-color: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: var(--spacing-md);
    margin: var(--spacing-sm) 0;
}

.note-content {
    max-width: var(--note-max-width);
    font-family: var(--font-family-prose);
    font-size: var(--font-size-xl); /* matches .prose */
    line-height: var(--line-height-relaxed);
    color: var(--color-text);
}

.note-content p {
    margin-bottom: var(--spacing-md);
}

.note-content p:last-child {
    margin-bottom: 0;
}

.note-meta {
    color: var(--color-text-muted); /* de-emphasized by color, not size */
    margin-top: var(--spacing-sm);
}

/* Note editor */
.note-editor {
    width: 100%;
}

.note-textarea {
    width: 100%;
    max-width: var(--note-max-width);
    min-height: 120px;
    padding: var(--spacing-md);
    font-family: var(--font-family-prose);
    font-size: var(--font-size-xl); /* matches .prose */
    line-height: var(--line-height-relaxed);
    color: var(--color-text);
    background-color: var(--color-bg);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    resize: vertical;
}

.note-textarea:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px var(--color-focus-ring);
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-sm);
    padding: var(--spacing-sm) var(--spacing-lg);
    font-family: var(--font-family-headings);
    font-size: var(--font-size-base);
    font-weight: 500;
    line-height: 1;
    text-decoration: none;
    border: none;
    border-radius: var(--radius-lg);
    cursor: pointer;
    transition: background-color 0.15s, opacity 0.15s;
}

@media (hover: hover) {
    .btn:hover {
        text-decoration: none;
    }
}

/* Hover is gated to real pointers, so on a phone the press state is the
   only feedback a control can give. Same scale idiom .floating-add-btn
   already used. */
.btn:active:not(:disabled) {
    transform: scale(0.98);
}

.btn:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-text-inverse);
}

@media (hover: hover) {
    .btn-primary:hover:not(:disabled) {
        background-color: var(--color-primary-hover);
    }
}

.btn-primary:active:not(:disabled) {
    background-color: var(--color-primary-active);
}

.btn-secondary {
    background-color: transparent;
    color: var(--color-primary);
    border: 1px solid var(--color-primary);
}

@media (hover: hover) {
    .btn-secondary:hover:not(:disabled) {
        background-color: var(--teal-96);
    }
}

@media (prefers-color-scheme: dark) and (hover: hover) {
    .btn-secondary:hover:not(:disabled) {
        background-color: color-mix(in srgb, var(--teal-49) 15%, transparent);
    }
}

/* Large button - bigger tap target, same text size */
.btn-lg {
    padding: var(--spacing-md) var(--spacing-xl);
}

.btn-delete {
    background: none;
    border: none;
    color: var(--color-error);
    font-size: var(--font-size-sm);
    cursor: pointer;
    padding: var(--spacing-sm) 0;
    width: 100%;
    text-align: left;
    margin-top: var(--spacing-sm);
}

/* ==========================================================================
   Form Elements
   ========================================================================== */

.form-group {
    margin-bottom: var(--spacing-md);
}

/* Small-caps style label: uppercase Concourse, clearly distinct from
   both prose and the input's entered value. */
.form-label {
    display: block;
    font-family: var(--font-family-headings);
    font-size: var(--font-size-sm);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--color-text-secondary);
    margin-bottom: var(--spacing-xs);
}

.form-input {
    width: 100%;
    padding: var(--spacing-sm) var(--spacing-md);
    font-family: var(--font-family);
    font-size: var(--font-size-xl); /* entered values match body text */
    color: var(--color-text);
    background-color: var(--color-input-bg);
    border: none;
    border-radius: var(--radius-md);
    box-sizing: border-box;
    transition: box-shadow 0.15s;
}

.form-input:focus {
    outline: none;
    box-shadow: 0 0 0 3px var(--color-focus-ring);
}

.form-input::placeholder {
    color: var(--color-text-muted);
}

textarea.form-input {
    resize: none;
    line-height: var(--line-height-relaxed);
}

.form-input-date {
    width: auto;
    padding: var(--spacing-xs) var(--spacing-sm);
    font-weight: 600;
    cursor: pointer;
}

.form-input-locked {
    color: var(--color-text-muted);
    cursor: default;
    opacity: 0.7;
}

.form-input-locked:focus {
    border-color: var(--color-border);
    box-shadow: none;
}

.form-error {
    font-size: var(--font-size-sm);
    color: var(--color-error);
    min-height: calc(var(--font-size-sm) * 1.5);
    padding-top: var(--spacing-xs);
    visibility: hidden;
}

.form-error.is-visible {
    visibility: visible;
}

.form-value {
    font-size: var(--font-size-xl); /* matches body text */
    color: var(--color-text);
    padding: var(--spacing-sm) 0;
}

/* ==========================================================================
   Cards
   ========================================================================== */

.card {
    background-color: var(--color-surface);
    border: none;
    border-radius: var(--radius-md);
    box-shadow: 0 1px 2px var(--color-shadow);
    overflow: hidden;
}

.card-body {
    padding: var(--spacing-md);
}

/* A card title heads its own surface, so it is Heading S at full text
   color rather than the secondary color h3 carries as a subhead. */
.card-title {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-xl);
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: var(--line-height-tight);
    color: var(--color-text);
    margin-bottom: var(--spacing-sm);
}

/* ==========================================================================
   Alerts & Messages
   ========================================================================== */

.alert {
    padding: var(--spacing-md);
    border-radius: var(--radius-md);
    font-size: var(--font-size-sm);
}

.alert-error {
    background-color: var(--color-error-bg);
    color: var(--color-error);
    border: 1px solid var(--color-error);
}

.alert-warning {
    background-color: var(--color-warning-bg);
    color: var(--yellow-27);
    border: 1px solid var(--color-warning-border);
}

@media (prefers-color-scheme: dark) {
    .alert-warning {
        color: var(--yellow-70);
    }
}

/* ==========================================================================
   Empty States
   ========================================================================== */

.empty-state {
    text-align: center;
    padding: var(--spacing-sm) var(--spacing-xl) var(--spacing-xl);
}

.empty-state p {
    max-width: var(--note-max-width);
    margin: var(--spacing-sm) auto 0;
}

.empty-state-arrow {
    margin: 0 auto var(--spacing-xs);
    width: 100px;
    position: relative;
    left: 30%;
    transform: rotate(var(--dynamic-angle, 1deg));
    transition: transform 0.1s linear;
}

/* ==========================================================================
   Logged-In Home Page
   ========================================================================== */

.logged-in-home {
    width: 100%;
    max-width: 65ch;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--spacing-md);
    padding: var(--spacing-md);
}

.logged-in-home .welcome {
    color: var(--color-text);
    margin-bottom: var(--spacing-sm);
}

.trial-message {
    margin: var(--spacing-md) 0;
}

.trial-status-secondary {
    font-size: 0.85rem;
    color: var(--color-text-muted);
    margin-top: var(--spacing-md);
    text-align: center;
}

.logged-in-home .photo-count {
    font-size: var(--font-size-base);
    color: var(--color-text-secondary);
    margin-bottom: var(--spacing-sm);
}

/* ==========================================================================
   Loading & Processing States
   ========================================================================== */

.shimmer {
    background: linear-gradient(
        90deg,
        var(--color-bg-subtle) 0%,
        var(--color-bg-elevated) 50%,
        var(--color-bg-subtle) 100%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* Uploading: the label collapses and the spinner takes its place.
   No colour override - primary is reserved for committing an edit, and
   adding photos is not that. */
.header-add-btn.uploading {
    font-size: 0;
    cursor: default;
    pointer-events: none;
}

.header-add-btn.uploading::after {
    content: '';
    display: block;
    width: 22px;
    height: 22px;
    border: 2.5px solid color-mix(in srgb, var(--color-primary) 40%, transparent);
    border-top-color: var(--color-primary);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
}

.processing-indicator {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-sm);
    color: var(--color-primary);
    font-size: var(--font-size-sm);
}

/* ==========================================================================
   Review Mode Specific
   ========================================================================== */

.review-day-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--spacing-md);
    border-bottom: 1px solid var(--color-border);
}

.review-day-date {
    font-size: var(--font-size-xl);
    font-weight: 600;
    color: var(--color-text);
}

.review-nav-btn {
    width: 44px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: none;
    border: none;
    color: var(--color-primary);
    font-size: var(--font-size-xl);
    cursor: pointer;
    border-radius: var(--radius-md);
}

@media (hover: hover) {
    .review-nav-btn:hover {
        background-color: var(--color-bg-subtle);
    }
}

.review-nav-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.review-photos {
    padding: var(--spacing-md);
}

.review-note {
    padding: var(--spacing-md);
    border-top: 1px solid var(--color-border);
}

/* ==========================================================================
   Fullscreen Photo View (Progressive Disclosure)
   ========================================================================== */

.fullscreen-photo {
    position: fixed;
    inset: 0;
    background-color: var(--color-backdrop);
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
}

.fullscreen-photo img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.fullscreen-photo-counter {
    position: absolute;
    top: var(--spacing-md);
    left: 50%;
    transform: translateX(-50%);
    color: var(--color-text-inverse);
    font-size: var(--font-size-sm);
    background-color: var(--color-overlay-badge);
    padding: var(--spacing-xs) var(--spacing-sm);
    border-radius: var(--radius-sm);
}

.fullscreen-photo-close {
    position: absolute;
    top: var(--spacing-md);
    right: var(--spacing-md);
    color: var(--color-text-inverse);
    font-size: var(--font-size-2xl);
    background: none;
    border: none;
    cursor: pointer;
    padding: var(--spacing-sm);
}

/* Full-screen photo viewer with swipe navigation */
.fullscreen-viewer {
    position: fixed;
    inset: 0;
    background-color: var(--color-backdrop);
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: center;
    touch-action: pinch-zoom;
}

.fullscreen-viewer img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    user-select: none;
    -webkit-user-drag: none;
}

.fullscreen-photo-date {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    color: var(--color-text-inverse);
    font-size: var(--font-size-sm);
    background: var(--color-overlay);
    padding: var(--spacing-xs) var(--spacing-md);
    border-radius: 20px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

/* ==========================================================================
   Celebration / Achievement Messages
   ========================================================================== */

.celebration {
    text-align: center;
    padding: var(--spacing-xl);
    max-width: 400px;
    margin: 0 auto;
}

.celebration-icon {
    font-size: 4rem;
    margin-bottom: var(--spacing-md);
}

/* Heading typography comes from the h1 element style. */
.celebration-title {
    color: var(--color-text);
    margin-bottom: var(--spacing-sm);
}

.celebration-message {
    color: var(--color-text-secondary);
    font-size: var(--font-size-base);
    line-height: var(--line-height-relaxed);
    margin-bottom: var(--spacing-lg);
}

/* ==========================================================================
   Utility Classes
   ========================================================================== */

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

.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }

.text-primary { color: var(--color-primary); }
.text-secondary { color: var(--color-text-secondary); }
.text-muted { color: var(--color-text-muted); }
.text-error { color: var(--color-error); }

.mt-0 { margin-top: 0; }
.mt-sm { margin-top: var(--spacing-sm); }
.mt-md { margin-top: var(--spacing-md); }
.mt-lg { margin-top: var(--spacing-lg); }
.mt-xl { margin-top: var(--spacing-xl); }

.mb-0 { margin-bottom: 0; }
.mb-sm { margin-bottom: var(--spacing-sm); }
.mb-md { margin-bottom: var(--spacing-md); }
.mb-lg { margin-bottom: var(--spacing-lg); }
.mb-xl { margin-bottom: var(--spacing-xl); }

/* Hide file inputs but keep accessible */
input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ==========================================================================
   Timeline Notes
   ========================================================================== */

.timeline-notes {
    margin-top: var(--spacing-sm);
}

.note-entry {
    border-radius: var(--radius-md);
    padding: var(--spacing-sm) var(--spacing-md);
    margin: var(--spacing-xs) 0;
    cursor: pointer;
}

@media (hover: hover) {
    .note-entry:hover {
        background: var(--color-border-subtle);
    }
}

/* A note in the stream is reading text: it keeps the Prose size set by
   .note-content and is never shrunk.  The time is de-emphasized by
   color, like .note-meta. */
.note-entry .note-content {
    color: var(--color-text);
    line-height: var(--line-height-relaxed);
}

.note-entry .note-time {
    color: var(--color-text-muted);
    margin-top: var(--spacing-xs);
}

/* A note is HTML rendered from its text (NoteMarkup): line breaks arrive
   as <br>, titles and lists as elements. These rules hold wherever note
   text is shown - the stream, a caption, the photo page.

   A note's title is the Heading style at its S size - the same size as
   the date above it, told apart by colour: the date is secondary, the
   title is the note's own words. Headings bind down, 2:1. */
.note-title {
    font-family: var(--font-family-headings);
    font-weight: 600;
    letter-spacing: 0.02em;
    line-height: 1.25;
    font-size: var(--font-size-xl);
    color: var(--color-text);
    margin: var(--spacing-md) 0 var(--spacing-sm);
}

.note-title:first-child {
    margin-top: 0;
}

/* The reset takes every list's indent away; a note's list gets it back
   so the markers sit inside the note's own edge. */
.note-list {
    padding-left: 1.5em;
    margin-bottom: var(--spacing-sm);
}

.note-list li + li {
    margin-top: var(--spacing-xs);
}

/* Paragraphs sit a half-step apart, closer than .prose's, because a note
   is a few lines and not an essay. */
.note-content p,
.photo-note-text p,
.entry-caption p {
    margin-bottom: var(--spacing-sm);
}

.note-content > :last-child,
.photo-note-text > :last-child,
.entry-caption > :last-child {
    margin-bottom: 0;
}

/* ==========================================================================
   Floating Add Button
   ========================================================================== */

.floating-add-btn {
    position: fixed;
    bottom: calc(var(--tab-bar-height) + 16px);
    right: 16px;
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--color-primary);
    color: var(--color-text-inverse);
    font-size: 28px;
    font-weight: 300;
    border: none;
    box-shadow: 0 4px 12px var(--color-shadow);
    cursor: pointer;
    z-index: 90;
    transition: background-color 0.15s, transform 0.15s;
}

@media (hover: hover) {
    .floating-add-btn:hover {
        background: var(--color-primary-hover);
    }
}

.floating-add-btn:active {
    transform: scale(0.95);
}

/* ==========================================================================
   Add Menu
   ========================================================================== */

.add-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 4px;
    background: var(--color-bg-elevated);
    border-radius: var(--radius-lg);
    box-shadow: 0 4px 16px var(--color-shadow-heavy);
    z-index: 90;
    overflow: hidden;
}

.add-menu.hidden {
    display: none;
}

.add-menu-item {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    padding: var(--spacing-md) var(--spacing-lg);
    width: 100%;
    border: none;
    background: none;
    font-family: var(--font-family);
    font-size: var(--font-size-base);
    color: var(--color-text);
    cursor: pointer;
    transition: background-color 0.15s;
}

@media (hover: hover) {
    .add-menu-item:hover {
        background: var(--color-bg-subtle);
    }
}

.add-menu-item:not(:last-child) {
    border-bottom: 1px solid var(--color-border-subtle);
}

.add-menu-icon {
    display: inline-block;
    width: 20px;
    height: 20px;
    background-color: currentColor;
    -webkit-mask-size: contain;
    mask-size: contain;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
}

.add-menu-icon-photo {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'/%3E%3Crect x='72' y='40' width='144' height='144' rx='8' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Ccircle cx='120' cy='88' r='16' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Cpath d='M184,184v24a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H72' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Cpath d='M96.69,184l77.65-77.66a8,8,0,0,1,11.32,0L216,136.69' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'/%3E%3Crect x='72' y='40' width='144' height='144' rx='8' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Ccircle cx='120' cy='88' r='16' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Cpath d='M184,184v24a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H72' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Cpath d='M96.69,184l77.65-77.66a8,8,0,0,1,11.32,0L216,136.69' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3C/svg%3E");
}

.add-menu-icon-note {
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'/%3E%3Cpath d='M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Cline x1='136' y1='64' x2='192' y2='120' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'/%3E%3Cpath d='M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3Cline x1='136' y1='64' x2='192' y2='120' fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='12'/%3E%3C/svg%3E");
}

/* ==========================================================================
   Modal
   ========================================================================== */

.modal {
    position: fixed;
    inset: 0;
    background: var(--color-overlay);
    display: flex;
    align-items: flex-end;
    z-index: 200;
}

.modal.hidden {
    display: none;
}

.modal-content {
    background: var(--color-bg-elevated);
    width: 100%;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: var(--spacing-md);
    max-height: 80vh;
    overflow-y: auto;
}

.modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: var(--spacing-md);
}

.modal-close {
    background: none;
    border: none;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    color: var(--color-text-muted);
    padding: var(--spacing-xs);
}

@media (hover: hover) {
    .modal-close:hover {
        color: var(--color-text);
    }
}

.modal-actions {
    margin-top: var(--spacing-md);
    text-align: right;
}

/* The note dialog's action bar: back left, then the date, then trash and
   check on the right - the photo page's order. */
.note-dialog-bar {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    margin-bottom: var(--spacing-md);
}

.note-dialog-bar .form-input-date {
    margin-right: auto;
}

/* The photo page insets its tag box to line up with its caption; in the
   dialog everything already shares the dialog's padding. Tags come after
   the text here - the note is what the dialog is for, and tags are
   filing - so the gap is above, not below. */
.modal-content .entry-tags {
    padding: var(--spacing-md) 0 0;
}

/* A note marked for deletion: the trash marks it and the check deletes
   it. Drained of colour at a quarter opacity, in place, so nothing
   moves - the photo page's and the share list's look. The action bar
   stays at full strength: it holds the undo and the check. A marked
   note is not being edited, so its fields stop answering; typing into a
   ghost would be a trap. Approved 2026-10-08. */
.modal-content.is-staged-delete > :not(.note-dialog-bar),
.modal-content.is-staged-delete .note-dialog-bar .form-input-date {
    opacity: 0.25;
    filter: grayscale(1);
    pointer-events: none;
    transition: opacity 0.15s, filter 0.15s;
}

/* How to make a title or a list, said once, under the box where it
   applies. UI Label, de-emphasised by colour. */
.note-format-hint {
    margin-top: var(--spacing-xs);
    font-family: var(--font-family-headings);
    font-size: var(--font-size-sm);
    color: var(--color-text-muted);
}

.date-label-locked::after {
    content: " (from photo)";
    font-size: var(--font-size-sm);
}

/* Desktop: center modal */
@media (min-width: 768px) {
    .modal {
        align-items: center;
        justify-content: center;
    }

    .modal-content {
        max-width: 500px;
        border-radius: var(--radius-lg);
    }
}

/* ==========================================================================
   Margin Notes

   On wide screens, the figure floats to the right of the paragraph as a
   margin note.  On narrow screens it wraps below the paragraph.
   ========================================================================== */

.margin-note-container {
    max-width: calc(var(--note-max-width) + 220px);
}

.margin-note {
    margin-top: var(--spacing-md);
}

.margin-note img {
    border-radius: var(--radius-md);
    width: 100%;
    height: auto;
    max-width: 300px;
}

.margin-note figcaption {
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    line-height: var(--line-height-normal);
    margin-top: var(--spacing-xs);
    max-width: 300px;
}

@media (min-width: 900px) {
    .margin-note-container {
        position: relative;
    }

    .margin-note {
        position: absolute;
        top: 0;
        left: calc(var(--note-max-width) + var(--spacing-lg));
        width: 200px;
        margin-top: 0;
    }

    .margin-note img {
        max-width: 200px;
    }

    .margin-note figcaption {
        max-width: 200px;
    }
}

/* ==========================================================================
   Page Navigation (content inside .navbar for About, Pricing, Privacy, Terms)
   ========================================================================== */

.page-nav {
    justify-content: space-between;
}

main:has(+ .navbar) {
    padding-bottom: var(--tab-bar-height);
}

.page-nav a {
    display: inline-flex;
    align-items: center;
    gap: var(--spacing-xs);
    color: var(--color-nav-inactive);
    text-decoration: none;
}

@media (hover: hover) {
    .page-nav a:hover {
        color: var(--color-nav-inactive-hover);
    }
}

.page-nav-prev,
.page-nav-next {
    min-width: 0;
    flex: 1;
}

.page-nav-prev {
    text-align: left;
}

.page-nav-next {
    text-align: right;
    justify-content: flex-end;
}

.page-nav-home {
    flex: 0 0 auto;
}

.page-nav-home .navbar-icon {
    width: 20px;
    height: 20px;
}

/* ==========================================================================
   Photo-note card (journal timeline)
   ========================================================================== */

/* Captioned photo: same size as any other photo, caption as prose
   below it. No card box — photos are the stars. */
.photo-note-card {
    cursor: pointer;
    margin-bottom: var(--spacing-md);
    width: calc(var(--photo-long-side) * min(1, var(--photo-ar, 4/3)));
    max-width: 100%;
}

.photo-note-img {
    width: 100%;
    height: auto;
    display: block;
    aspect-ratio: var(--photo-ar, 4/3);
    object-fit: cover;
    border-radius: var(--radius-md);
}

.photo-note-text {
    padding: var(--spacing-xs) 0 0;
    font-family: var(--font-family-prose);
    line-height: var(--line-height-relaxed);
    color: var(--color-text);
}

/* ==========================================================================
   Photo modal — redesigned two-column layout
   ========================================================================== */

.photo-modal-body {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-md);
}

.photo-modal-image-col img.photo-modal-img {
    width: 100%;
    max-height: 50vh;
    object-fit: contain;
    border-radius: var(--radius-md);
}

.photo-modal-fields-col {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
}

.photo-modal-actions {
    display: flex;
    justify-content: space-between;
    gap: var(--spacing-sm);
    margin-top: auto;
}

/* Danger button variant */

.btn-danger {
    background-color: var(--color-danger);
    color: var(--color-text-inverse);
}

@media (hover: hover) {
    .btn-danger:hover:not(:disabled) {
        opacity: 0.85;
    }
}

/* Desktop two-column photo modal */

@media (min-width: 768px) {
    .photo-modal-content {
        max-width: 800px;
    }

    .photo-modal-body {
        flex-direction: row;
        align-items: stretch;
        min-height: 300px;
    }

    .photo-modal-image-col {
        flex: 1;
        display: flex;
        align-items: flex-start;
    }

    .photo-modal-image-col img.photo-modal-img {
        max-height: 60vh;
    }

    .photo-modal-fields-col {
        flex: 1;
    }
}

/* =====================================================================
 * Action controls - the standard set
 *
 * Shape comes from a fill lighter than the page, never from a border.
 * Glyphs are reserved for the two that read the same anywhere: back and
 * check. Everything else uses its word. The primary colour marks
 * committing an edit and nothing else.
 *
 * The action bar - back left, check right - belongs at the top of any
 * screen that is cancellable and committable. A base screen (Journal,
 * Account) has nothing to commit and carries no action bar; its actions
 * live in the bottom nav.
 * ===================================================================== */
.action-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

/* The chip takes its shape from a stroke, not a fill. --color-surface
   on --color-bg is 1.06:1, and on a .card (also --color-surface) exactly
   1:1, so a filled chip was invisible; every palette fill that clears
   3:1 is dark enough to turn Edit into a solid button louder than the
   commit. 48px, not the 44px minimum: a bordered control cannot buy tap
   area with invisible padding, because the stroke is the promise, so hit
   region and drawn box must match. 3rem in the 56px header leaves 4px of
   slack. Approved 2026-08-24. */
.action-btn {
    width: 3rem;
    height: 3rem;
    padding: 0;
    border: 1px solid var(--color-border-control);
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-family-headings);
    font-size: 1.5rem;
    line-height: 1;
    background: var(--color-surface);
    color: var(--color-text);
    cursor: pointer;
    transition: border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
    /* Items in a flex row shrink when the row runs out of room. A long
       title beside the back arrow narrowed its width while the height
       stayed, drawing a tall oval. Approved 2026-10-10. */
    flex-shrink: 0;
}

/* The chevron sits high in its em box; nudge it back to centre. */
.action-btn-back {
    padding-bottom: 0.18em;
    text-decoration: none;
}

/* A word needs width, so it is a pill: same height and stroke, wider
   box. min-width keeps a short label from collapsing into a chip that is
   tall but thin, which the horizontal approach would punish. */
.action-btn-pill {
    width: auto;
    min-width: 3rem;
    border-radius: 999px;
    padding: 0 var(--spacing-md);
    font-size: var(--font-size-base);
    font-weight: 500;
}

.action-btn-primary {
    font-size: 1.15rem;
    background: var(--color-primary);
    color: var(--color-text-inverse);
    border-color: transparent;
}

/* Destructive actions are marked with red ink, never a red fill: the
   same signal survives on top of a photo, where a fill would fight the
   image, and it keeps the rarest action from being the loudest thing on
   the page. It takes the same stroke every other action button has, so
   there is still one pill style. Red marks, grey means marked, primary
   commits — three colours, three jobs. */
.action-btn-danger {
    color: var(--color-danger);
    border-color: var(--color-danger);
}

.action-btn:disabled {
    opacity: 0.5;
    cursor: default;
}

/* Hover strengthens the stroke — by weight first, colour second. Colour
   alone could not carry it: the rest-to-hover step is the same size in
   both schemes (2.69:1 light, 2.79:1 dark), but in dark the stroke moves
   toward near-white and away from the page, while in light a 1px line
   going dark-to-darker on a pale page saturates and reads as the same
   line. Doubling the line is scheme-symmetric.

   box-shadow, not border-width: growing the border would grow the 48px
   box and shove every chip beside it. Approved 2026-08-24. */
@media (hover: hover) {
    .action-btn:hover:not(:disabled) {
        border-color: currentColor;
        box-shadow: 0 0 0 1px currentColor;
    }
    /* Primary is a fill, not a stroke: nothing to thicken, so it steps
       its fill down exactly as .btn-primary does. */
    .action-btn-primary:hover:not(:disabled) {
        background-color: var(--color-primary-hover);
        border-color: transparent;
        box-shadow: none;
    }
    /* Danger keeps its red ink and thickens its own red stroke; without
       this it would hover to currentColor like any neutral chip. */
    .action-btn-danger:hover:not(:disabled) {
        border-color: var(--color-danger);
        box-shadow: 0 0 0 1px var(--color-danger);
    }
}

.action-btn:active:not(:disabled) {
    transform: scale(0.95);
}

/* =====================================================================
 * Entry edit page (full-screen photo edit, replaces the photo modal)
 * ===================================================================== */
.entry-edit-header {
    background-color: var(--color-bg);
}

.entry-edit-header .header-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.entry-edit-actions {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
}

/* Resting keeps the word; editing commits, so it takes the primary
   fill. entry-edit.js swaps the classes with the label. */
.entry-edit-toggle.is-edit {
    width: auto;
    border-radius: 999px;
    padding: 0 var(--spacing-md);
    font-size: var(--font-size-sm);
}

.entry-edit-toggle.is-save {
    font-size: 1.15rem;
    background: var(--color-primary);
    color: var(--color-text-inverse);
}

/* The share glyph ships at 22px; ease it down inside the circle. */
.entry-share-btn svg {
    width: 18px;
    height: 18px;
}

/* Nothing gets shared mid-edit: the link would publish a state the
   author has not committed. */
.entry-edit-page.editing .entry-share-btn {
    display: none;
}

.entry-edit-content {
    display: flex;
    flex-direction: column;
    padding-top: var(--header-height);
}

/* Same long side as a journal or story photo, sized from the persisted
   dimensions the same way (see .photo-grid-item for the derivation).
   The box is the photo's own shape, so there is no letterboxed band
   inside it and the delete X can sit on the photo's real corner. */
.entry-photo-wrap {
    position: relative;
    width: calc(var(--photo-long-side) * min(1, var(--photo-ar, 4/3)));
    max-width: calc(100% - var(--spacing-md) * 2);
    margin: var(--spacing-sm) auto 0;
}

/* height:auto keeps the ratio when max-width shrinks the box on a narrow
   phone. No max-height: the long side is the cap, the same one every
   other photo obeys.

   No 4/3 fallback, as on .story-photo: with no --photo-ar this resolves
   to auto, the image's own ratio. An entry photo is never cropped, and
   guessing a ratio for one would start. */
.entry-photo {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: var(--photo-ar);
    border-radius: var(--radius-md);
}

/* The box edge is the photo edge, so one inset off the corner serves
   both orientations. The mark inside is an SVG, so the chip centres it
   rather than relying on a text glyph to centre itself. */
.entry-delete-x {
    display: none;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: var(--spacing-sm);
    right: var(--spacing-sm);
    z-index: 2;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    /* The stroke is currentColor, not --color-border-control. What
       cannot be trusted over an unknown photo is that grey, which was
       measured against the page and against a card; the glyph's own
       colour is legible wherever the scrim is. So the ring is red around
       the trash can and neutral around the undo once the photo is
       staged, since that tap destroys nothing - it sheds the red along
       with the glyph, for free. Same pairing as the Delete chip on the
       Shares card: red ink, red stroke, no fill.

       box-sizing is border-box, so the 1px eats inward and the 1.75rem
       box does not grow. */
    border: 1px solid currentColor;
    background: color-mix(in srgb, var(--yellow-96) 82%, transparent);
    color: var(--color-danger);
    line-height: 1;
    cursor: pointer;
    transition: background-color 0.15s, box-shadow 0.15s;
}

/* Both halves of the chassis answer the cursor. The ring doubles the way
   .action-btn's does - a box-shadow rather than a fatter border, so the
   box does not grow and nothing beside it shifts - and the scrim itself
   steps more opaque, which is the "a box changes its fill" half this
   control has never had. Inside @media (hover: hover), so a phone sees
   none of it. */
@media (hover: hover) {
    .entry-delete-x:hover {
        box-shadow: 0 0 0 1px currentColor;
        background: color-mix(in srgb, var(--yellow-96) 95%, transparent);
    }
}

.entry-edit-page.editing .entry-delete-x {
    display: inline-flex;
}

/* Red on the glyph, never on the chip. The scrim is what keeps this
   control legible over any photo at all, and a red fill there would
   fight the image. --color-danger on the pale scrim is 5.95 and on the
   dark scrim 5.04; a glyph is a graphical object, so it needs 3:1 rather
   than the 4.5:1 a word would. Approved 2026-08-24. */

/* Once staged, the control sheds the red: red marks what a tap will
   destroy, and this tap now restores. Red marks, grey means marked. */
.entry-photo-wrap.is-staged-delete .entry-delete-x {
    color: var(--color-text);
}

/* Delete is staged, not done: the trash can marks the photo and the
   check in the header is what actually destroys it. Drained of colour and dropped to a
   quarter opacity, the photo reads as already gone while staying the
   thing you can put back. Approved 2026-08-23. */
.entry-photo-wrap.is-staged-delete .entry-photo {
    opacity: 0.25;
    filter: grayscale(1);
    transition: opacity 0.15s, filter 0.15s;
}

/* The pale scrim would vanish over a dark photo; invert it. */
@media (prefers-color-scheme: dark) {
    .entry-delete-x {
        background: color-mix(in srgb, var(--grey-23) 78%, transparent);
    }
}

@media (hover: hover) and (prefers-color-scheme: dark) {
    .entry-delete-x:hover {
        background: color-mix(in srgb, var(--grey-23) 92%, transparent);
    }
}

.entry-edit-fields {
    padding: var(--spacing-md) var(--spacing-lg);
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
}

.entry-date {
    color: var(--color-text-secondary);
    font-size: 0.85rem;
    letter-spacing: 0.05em;
}

.entry-caption {
    color: var(--color-text);
    min-height: 1.5rem;
}

.entry-caption:empty::before {
    content: "No note yet. Tap Edit to add one.";
    color: var(--color-text-muted);
}

.entry-toast {
    position: fixed;
    bottom: 5rem;
    left: 50%;
    transform: translateX(-50%);
    background: var(--color-overlay-badge);
    color: var(--color-text-inverse);
    padding: var(--spacing-sm) var(--spacing-md);
    border-radius: 6px;
    z-index: 300;
    max-width: 90vw;
    overflow-wrap: anywhere;
}

.handle-preview {
    color: var(--color-text-secondary);
    font-weight: 600;
    margin: var(--spacing-sm) 0 var(--spacing-xs);
    min-height: 1.2rem;
    overflow-wrap: anywhere;
}

.handle-modal-hint,
.handle-modal-rules {
    color: var(--color-text-secondary);
    font-size: 0.85rem;
    margin: var(--spacing-xs) 0;
}

@media (min-width: 768px) {
    .entry-edit-content {
        max-width: 768px;
        margin: 0 auto;
        width: 100%;
    }
}

/* Account page shares list */
.share-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: var(--spacing-sm);
}

.share-list-item {
    display: flex;
    align-items: baseline;
    gap: var(--spacing-md);
}

.share-date {
    color: var(--color-text-secondary);
    white-space: nowrap;
}

.share-link {
    color: var(--color-link);
    overflow-wrap: anywhere;
}

.share-status {
    margin-left: auto;
    color: var(--color-text-muted);
}

.share-list-empty {
    color: var(--color-text-muted);
}

/* A staged share row dims and drains exactly as a staged photo does, so
   the feedback is one vocabulary even though the control that stages it
   is a word on a row and a glyph on a photo. The control itself stays at
   full strength — it is the one that puts the row back. */
.share-list-item.is-staged-delete > :not(.action-btn) {
    opacity: 0.25;
    filter: grayscale(1);
    transition: opacity 0.15s, filter 0.15s;
}

/* ==========================================================================
   Prose

   Reading text (paragraphs someone settles in to read) gets the serif,
   a measure cap, and relaxed leading.  Apply to content-page article
   bodies and any long-form text block.  See docs/style-guide.md.
   ========================================================================== */

.prose {
    max-width: var(--measure);
    font-family: var(--font-family-prose);
    font-size: var(--font-size-xl); /* Equity runs small; 1rem reads too small */
    line-height: var(--line-height-relaxed);
}

.prose p {
    margin-bottom: var(--spacing-md);
}

.prose p:last-child {
    margin-bottom: 0;
}

/* ==========================================================================
   Story Page

   Publicly shared single-photo story.  Centered reading column: title,
   photo, date, prose, byline, quiet wordmark footer.  Visitors arrive
   cold from shared links, so keep it simple and fast.
   ========================================================================== */

.page-story {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    min-height: 100dvh;
    padding: var(--spacing-lg) var(--spacing-md);
}

.story-page {
    width: 100%;
    max-width: var(--measure);
    display: flex;
    flex-direction: column;
}

.story-title {
    margin: 0 0 var(--spacing-xs); /* date subtitle sits directly below */
}

/* Same long side as a journal photo, so a portrait story photo is as
   tall as a landscape one is wide. */
.story-photo-wrap {
    width: calc(var(--photo-long-side) * min(1, var(--photo-ar, 4/3)));
    max-width: 100%;
    margin: 0 0 var(--spacing-md);
}

/* No 4/3 fallback here on purpose: with no --photo-ar this resolves to
   auto, i.e. the image's own ratio. A story photo has never been
   cropped, and guessing a ratio for one would start. */
.story-photo {
    width: 100%;
    height: auto;
    aspect-ratio: var(--photo-ar);
    border-radius: var(--radius-md);
}

/* Layout only; typography comes from .date-heading. */
.story-date {
    margin-bottom: var(--spacing-md);
}

/* Story prose is just .prose; this only spaces it above the byline. */
.story-page .prose {
    margin-bottom: var(--spacing-md);
}

.story-byline {
    color: var(--color-text-secondary); /* prose, de-emphasized by color */
}

.story-footer {
    width: 100%;
    max-width: var(--measure);
    margin-top: auto;
    padding-top: var(--spacing-xl);
}

/* A standard link (color + underline) with a separator border. */
.story-wordmark {
    display: inline-block;
    padding-top: var(--spacing-md);
    border-top: 1px solid var(--color-border-subtle);
    color: var(--color-link);
    text-decoration: underline;
}

@media (hover: hover) {
    .story-wordmark:hover {
        color: var(--color-link-hover);
    }
}

/* ==========================================================================
   Reduced motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
    .btn:active:not(:disabled),
    .action-btn:active:not(:disabled),
    .floating-add-btn:active {
        transform: none;
    }
}

/* =====================================================================
   Tags
   =====================================================================
   A tag is the word in the tag colour, with no box. Colour is what
   marks it, the way a link is marked by blue without needing a border.
   An earlier draft drew filled chips and they read as buttons.

   Tags are stored lowercase (TAG-5), so what is on screen is exactly
   what is in the store. There is no text-transform here and there must
   not be one: casing a tag in CSS would make the screen disagree with
   the data, which is the bug this rule exists to prevent.
   ===================================================================== */

.tag-chips,
.tag-list {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Where tags are data, they run together as one line of small text
   separated by a dot, rather than as spaced-out words. A row of tags is
   a caption on the photo, not a menu. */
.tag-chips {
    gap: 0;
}

/* The panel's tags are separate tap targets, so they get real space
   between them and no separator - a dot between two things you can tap
   reads as belonging to one of them. */
.tag-list {
    gap: var(--spacing-sm);
}

/* The child combinator is load-bearing. A tag at rest is a link, so the
   <li> and the <a> inside it BOTH carry .tag-chip - the same trap
   currentTags() in entry-edit.js guards against with :scope >. As a
   descendant rule this also matched the <a>, which is never the last
   child (the remove button follows it, hidden or not, and :last-child
   counts structure rather than what is visible). Every tag then printed
   its own trailing slash: one tag read "salvia /", and in edit mode the
   slash pushed that tag's own remove button into the middle of the row,
   where it looked like it belonged to an empty tag that did not exist. */
.tag-chips > .tag-chip:not(:last-child)::after {
    content: "/";
    /* A slash, not a middot. A middot at this text size is a speck and
       has to be scaled to 1.6em before it reads as a separator, which
       then fights the words either side of it. A slash carries its own
       height at 1em, so it separates without being enlarged. */
    color: var(--color-text-muted);
    margin: 0 0.35em;
}

.tag-chip,
.tag-link {
    display: inline-flex;
    align-items: center;
    gap: 0;
    color: var(--color-tag);
    font-family: var(--font-ui);
    font-weight: var(--tag-weight);
    font-size: var(--font-size-sm);
    line-height: 1.4;
    background: none;
    border: none;
    padding: 0;
    text-decoration: none;
}

/* A tag in the stream was bare text with no padding, so its target was
   one line of small type - about 18px - sitting flat against the bottom
   edge of the photo, and a miss opened the photo. 0.7rem above and below
   makes that a 42px band, and the half above the word is the gap the
   tags were missing: that space now belongs to the tag, so a tap landing
   just under the photo opens the tag.

   On the anchor, not on .tag-chip: the <li> and the <a> both carry the
   class, so a rule written for the class would apply twice and double
   the height. The anchor is also the thing a thumb has to land on, and
   on the entry page - where a tag is plain text, not a link - there is
   no anchor and nothing changes.

   Vertical only. Widening each word would push the slashes apart and
   break the run-together line that makes a row of tags read as a
   caption rather than a menu. */
.tag-chips a.tag-chip {
    padding-top: 0.7rem;
    padding-bottom: 0.7rem;
}

/* A tappable tag keeps the full UI size; only the data rendering shrinks. */
.tag-link {
    font-size: 1rem;
    /* Not the chip's gap: 0. A tag-link carries its count beside the
       word, and the count is a separate piece of information rather than
       part of the name. The chip closes its gap only because the thing
       sitting there is the x, which is pulled tight on purpose. */
    gap: var(--spacing-xs);
}

/* In the search panel a tag IS tappable, so it needs a real hit region
   even though only the word is drawn. */
.tag-list .tag-link {
    min-height: 3rem;
    padding: 0 0.25rem;
}

.tag-count {
    color: var(--color-text-secondary);
    font-weight: 500;
    font-size: var(--font-size-sm);
}

/* Secondary ink, not the tag colour: colouring the remove control to
   match would make it look like part of the name. It is an x rather than
   the trash can because detaching a label destroys nothing - the tag
   survives on every other photo. */
.tag-chip-remove {
    align-items: center;
    justify-content: center;
    width: 1.5rem;
    height: 1.5rem;
    /* The 1.5rem box is the hit target and must stay. Negative margins
       on BOTH sides absorb the dead space between the drawn glyph and
       the box edge, without shrinking the box - so the x reads as part
       of its tag, and the separator after it sits the same distance away
       as it does after a tag with no x. Leading is pulled less than
       trailing: a little air between word and x is wanted, none is
       wanted between the box and the slash. */
    margin-left: -0.3rem;
    margin-right: -0.45rem;
    padding: 0;
    border: none;
    background: none;
    color: var(--color-text-secondary);
    font-family: var(--font-ui);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

/* Tags sit directly under the photo, above the date and the caption. A
   caption can run to several lines, and putting the tags after it meant
   scrolling past the whole note to reach them.

   The inset is stated here rather than inherited. These used to sit
   inside .entry-edit-fields and borrow its padding; out from under it
   they had none of their own and ran to the edge of the screen while the
   date and caption stayed inset. Same --spacing-lg either side, so the
   chips, the date and the caption all start on one line down the page.
   Top only on the vertical - the gap below is .entry-edit-fields' own
   top padding, and repeating it here would push the date down twice. */
.entry-tags {
    display: flex;
    flex-direction: column;
    gap: var(--spacing-xs);
    padding: var(--spacing-md) var(--spacing-lg) 0;
}

/* At rest a tag on this page is a link to its own page, so the subject
   is one tap from the photo instead of a trip back through the journal.
   Inside the session it goes inert: following it would leave the page
   and discard everything staged, and the session promises the opposite.
   pointer-events rather than dropping the href - the word stays exactly
   where it was, and the x beside it is a sibling, so it is untouched. */
.entry-edit-page.editing .tag-chips a.tag-chip {
    pointer-events: none;
}

/* The x and the box to add a tag belong to the edit session, not to the
   page at rest - at rest a tag is just its word. Driven by .editing, the
   same marker the delete x and the share button already use, rather than
   by a hidden attribute: [hidden] carries !important here, so CSS could
   never reveal it again. */
.tag-input-row {
    display: none;
    flex-direction: column;
    gap: var(--spacing-xs);
    position: relative;
}

.entry-edit-page.editing .tag-input-row {
    display: flex;
}

.tag-chip-remove {
    display: none;
}

.entry-edit-page.editing .tag-chip-remove {
    display: inline-flex;
}

/* Suggestions float over content, so unlike a tag they are a surface:
   fill for the surface, stroke for the edge against whatever is beneath.
   The words inside keep the tag colour, so what is being picked is
   recognisably a tag. */
/* A long match list is capped, not scrolled. A box whose height tracked
   the match count would visibly resize on every keystroke, which reads
   as the screen twitching rather than as an answer arriving; a box that
   scrolls asks to be operated. Reserving exactly the cap's worth of
   height fixes both - the box is the same size whether the query matches
   one tag or three hundred, and there is nothing to scroll. The rest are
   reached the way any autocomplete narrows a long list: type another
   letter.

   overflow stays auto rather than hidden as a safety net: if the cap in
   GetTags and this height ever disagree, a match scrolls into view
   rather than vanishing silently. */
.tag-suggestions {
    list-style: none;
    margin: 0;
    padding: var(--spacing-xs) var(--spacing-sm);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    min-height: 5.25rem;
    max-height: 5.25rem;
    overflow-y: auto;
    /* Not a single `gap`. The rows are meant to sit at the text's own
       pitch, so the column gap and the row gap are different numbers. */
    row-gap: 0;
    column-gap: var(--spacing-md);
}

/* Row pitch. Vertical padding here, a row-gap above, and the bare
   `ol li, ul li { margin-bottom: var(--spacing-sm); }` written for prose
   lists all stack on top of the line-height at once - gap and margin
   apply to a flex child together rather than replacing each other - and
   three of them together measured out at about 2.5x a tag's own font
   height. The <li> has to opt out of the prose margin explicitly; zeroing
   the row-gap alone leaves it in place and invisible. */
.tag-suggestion {
    background: none;
    border: none;
    padding: 0 0.15rem;
    color: var(--color-tag);
    font-family: var(--font-ui);
    font-weight: var(--tag-weight);
    font-size: 1rem;
    line-height: 1.25;
    cursor: pointer;
}

.tag-suggestions li {
    margin-bottom: 0;
    line-height: 1.25;
}

.tag-list-empty,
.tag-suggestions-empty {
    color: var(--color-text-muted);
}

/* A tag answers a pointer with colour, like every other hover in this
   sheet: a box changes its fill, bare text changes its ink, the action
   chip thickens its stroke. This was an underline, the only hover here
   drawn as a line, and it was covering for a colour change nobody could
   see - a tag that is a link was picking up the generic a:hover blue,
   which is dE 1.11 from the tag purple, while the autocomplete is a
   <button> that a:hover never matched and so did not change at all.
   Three places, three behaviours, one of them invisible.

   Both selectors are named on purpose. a:hover is more specific than
   .tag-chip alone, so without naming them the blue wins back.

   The element is named too. Both the <li> and the <a> carry .tag-chip,
   and inside an edit session a tag is not a link at all, so
   .tag-chip:hover on its own turns an inert word gold under the cursor.
   a.tag-chip reaches only the tags that are links.

   A match candidate is deliberately absent from this rule. The gold says
   "this tap takes you somewhere"; a .tag-suggestion is a <button> that
   drops a word into the box beside it - it commits nothing and goes
   nowhere - so it stays at the resting purple. The cursor and the press
   already say it is operable. */
@media (hover: hover) {
    a.tag-chip:hover,
    a.tag-link:hover {
        color: var(--color-tag-hover);
        text-decoration: none;
    }

    .tag-chip-remove:hover {
        color: var(--color-text);
    }
}

/* ==========================================================================
   Tag page - one subject across seasons (TAG-17)
   ==========================================================================
   A tag names something followed for years: a planting, a bed being put
   in, a plant type returned to season after season. The question it has
   to answer is "what did I do for this last year, and how was that
   different from the year before", so months run down the side and years
   run across, and the same month of two years sits side by side. */

.tag-years {
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
}

/* One grid, not a row of columns: a month label and that month's cells
   have to share a grid row, or the labels drift out of step with the
   photos as soon as a row changes height. 44px for the labels and 100px
   per year puts a bit over three years on a 390px phone. */
.tag-years-grid {
    display: grid;
    /* The month column carries the page inset itself. It cannot go on
       .tag-years: a sticky element's left is measured from the
       scrollport, padding included, so an inset there would hold until
       you scrolled sideways and then vanish. Widening the column and
       padding the label inside it holds the distance at every scroll
       position, and the label's background still covers the full column
       so photos stay hidden as they pass under it. */
    grid-template-columns:
        calc(44px + var(--spacing-md))
        repeat(var(--tag-years, 1), 100px);
    padding-right: var(--spacing-md);
    gap: var(--spacing-xs);
    width: max-content;
}

/* Full ink, not muted. These labels are the only thing telling you which
   month and year you are looking at, so they are not decoration to be
   pushed back - and --color-text-muted measured 2.64:1 on the light page
   and 3.95:1 on the dark one, both under the 4.5:1 floor for type this
   size. 10.41:1 and 13.17:1 now. */
.tag-years-year,
.tag-years-month {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-xs);
    color: var(--color-text);
    display: flex;
    align-items: center;
}

.tag-years-year {
    justify-content: center;
}

/* The month names stay put while the years scroll under them. */
.tag-years-month {
    position: sticky;
    left: 0;
    z-index: 1;
    padding-left: var(--spacing-md);
    background: var(--color-bg);
}

/* Every photo of a month is drawn, stacked down its cell. Showing one
   with a count would say the others exist while giving no way to open
   them, so a row is as tall as its busiest year. */
.tag-years-cell {
    display: grid;
    gap: var(--spacing-xs);
    align-content: start;
}

/* An empty cell is not a gap in the page - it is the answer to "was
   there anything that month". One tile tall, so a busy year further
   along the row does not stretch it into a large empty box. */
.tag-years-cell.is-empty::before {
    content: "";
    display: block;
    aspect-ratio: 4 / 3;
    border: 1px dashed var(--color-border);
    border-radius: var(--radius-sm);
}

/* A fixed 4:3 box whatever shape the photo is. At this size the photo's
   own proportions are not readable, and ragged boxes would make the same
   month of two years hard to line up by eye. */
.tag-years-photo {
    position: relative;
    display: block;
    aspect-ratio: 4 / 3;
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--color-surface);
}

.tag-years-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* A note lives on the photo's page, not in a 100px cell - about four
   words fit, and a phone has no hover. So the tile says only whether
   there is something to read, and tapping opens the photo where the note
   actually is. The dot never gates the tap: most photos carry no note
   and all of them have to open.

   The dot sits on a photo, not on the page, so it does NOT follow the
   light/dark tokens - its backdrop is whatever the gardener pointed the
   camera at. Light fill on a dark ring is the pair the app already uses
   for anything drawn over a photo (see .fullscreen-photo-counter and
   .entry-toast), and both tokens are deliberately defined once, outside
   the dark-mode block. A dot tied to --color-bg would turn dark grey in
   dark mode and vanish against a shaded photo. */
.tag-years-photo.has-note::after {
    content: "";
    position: absolute;
    top: 4px;
    left: 4px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--color-text-inverse);
    box-shadow: 0 0 0 1.5px var(--color-overlay-badge);
}

/* The grid answers "what did I do this month last year", and the tile
   itself could only say which month and which year - the row it sits in
   and the column above it. The day is the one thing the grid cannot say,
   and it is what tells two photos of the same month apart.

   Just the number. "Jun 14" would repeat the row label and a year would
   repeat the column heading, twice over, in a cell about 100px wide.
   Concourse, because a date is a label rather than something you read.

   Drawn like the dot and not like the page, for the same reason and with
   the same pair of tokens: its backdrop is whatever the camera was
   pointed at. Light ink tied to --color-text-inverse would be readable
   on a bright photo and lost on a dark one without the pill behind it.
   Same 4px inset as the dot and on the same edge, so the two marks line
   up down the left of the tile. */
.tag-years-day {
    position: absolute;
    bottom: 4px;
    left: 4px;
    z-index: 1;
    padding: 1px 4px;
    border-radius: var(--radius-sm);
    font-family: var(--font-family-headings);
    font-weight: 600;
    font-size: 0.6875rem;
    line-height: 1.35;
    color: var(--color-text-inverse);
    background: var(--color-overlay-badge);
}

/* A tagged stand-alone note on a tag's page: the photo's box, filled
   with the note's name instead of a picture. The fill is the surface
   step up from the cell, so it reads as a tile and not as a hole. Two
   lines of the name - all that fit above the day badge in a phone-width
   cell; the second ends in an ellipsis. */
.tag-years-note {
    display: block;
    padding: 6px 6px 22px;
    background: var(--color-bg-elevated);
    text-decoration: none;
}

.tag-years-note-name {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    font-family: var(--font-family-headings);
    font-weight: 600;
    font-size: var(--font-size-sm);
    line-height: 1.3;
    color: var(--color-text);
    overflow-wrap: anywhere;
}

/* A photo with no date belongs to no month and no year, so it has no
   cell. Dropping it would lose a photo carrying the tag. */
/* Sits beside the sideways scroller rather than inside it, so it takes
   the page inset directly. */
.tag-undated {
    margin-top: var(--spacing-lg);
    padding-left: var(--spacing-md);
    padding-right: var(--spacing-md);
}

.tag-undated-heading {
    font-family: var(--font-family-headings);
    font-size: var(--font-size-xs);
    color: var(--color-text-muted);
    margin: 0 0 var(--spacing-xs);
}

.tag-undated-photos {
    display: grid;
    grid-template-columns: repeat(auto-fill, 100px);
    gap: var(--spacing-xs);
}
