/* ==========================================================================
   de termin.ba design system
   One action colour - the turquoise of the mark, sampled from it rather than
   chosen beside it - Sora for headings, prices and clock times, Source Sans 3
   for everything else. Border before shadow.
   Mobile-first: the customer funnel is designed at 390px and widened up.
   ========================================================================== */

:root {
    /* Brand - the only thing in the product you can press, written in exactly
       one place. experience.css used to redeclare these tokens on the same bare
       :root and it loads second, so for months the stylesheet documented one
       colour and the browser computed another. The duplicate is gone.

       These are the values sampled from the logotype: the mark measures #007C8C
       and the design system rounds it to the token below. A brand that does not
       match its own logotype is not a brand, it is two.

       Measured, so a later edit has something to fail against:
         white on --c-brand        4.72:1
         --c-brand-dark on white   7.67:1
         --c-brand-dark on tint    6.57:1
       and the step from --c-brand to --c-brand-dark is 1.62:1, which is what
       makes a hover visible rather than merely different. */
    --c-brand:        #0A7F8F;
    --c-brand-dark:   #065C68;
    --c-brand-light:  #DEF1F3;
    /* What is legible ON the brand colour, rather than beside it. A token because the
       brand is dark in one scheme and light in the other, and a hardcoded white is
       correct in exactly one of them. */
    --c-brand-ink:    #FFFFFF;

    /* The same turquoise at the strength it can only reach against a dark
       ground. The hero band and the banner that sits on --c-ink. */
    --c-brand-vivid:  #3FD9D0;

    /* Ink - the full-bleed dark bands. A blue near-black rather than pure black,
       so it belongs to the same room as the ground below it. */
    --c-ink:          #102C34;
    --c-ink-text:     #F4FAF8;
    --c-ink-muted:    #BCD0D3;
    --c-ink-line:     rgba(255, 255, 255, 0.16);

    /* Last-minute / discount accent. Burnt orange, because the ochre it replaces
       sank into the warm ground it sat on. A price that dropped has to be
       visible from the far side of a table. */
    --c-accent-ink:   #FFFFFF;
    --c-danger-ink:   #FFFFFF;
    --c-accent:       #C0490F;
    --c-accent-dark:  #97390B;
    --c-accent-light: #FCE8DD;

    /* Feedback. Warning keeps the old ochre so a caution and a bargain never
       arrive in the same colour. */
    --c-success:      #24704F;
    --c-success-ink:  #FFFFFF;
    --c-success-bg:   #DEEFE6;
    --c-danger:       #A8322C;
    --c-danger-dark:  #8A1C1C;
    --c-danger-bg:    #F8E5E3;
    --c-warning:      #8A5D00;
    --c-warning-bg:   #F6EDDA;

    /* Neutrals - grey carrying the brand's own blue, so a page of white cards
       reads as one surface rather than as paper cut from two reams. Declared
       here and nowhere else: experience.css used to redeclare all nine of these
       on the same bare :root, which is why the file you are reading described a
       warm oat product for months while the browser painted this. */
    --c-bg:           #F6F8FA;
    --c-surface:      #FFFFFF;
    --c-surface-alt:  #EEF2F5;
    --c-border:       #DDE5E9;
    /* 3:1 against every surface it is drawn on, because this token is not decoration: it
       is the entire visual boundary of a text field. The field is white on an off-white
       page, so at the old #C4BCAC - 1.89:1 - nothing at all said where the input began.
       Chips and the filter toggle share it for the same reason: a control identified by
       its outline needs the outline to be visible. 3.56:1 on white, 3.17:1 on the alt
       surface, so the promise this token makes survived the move. */
    --c-border-strong:#7B8A93;
    --c-text:         #152B35;
    --c-text-muted:   #536773;

    /* Ratings are gold, and gold only - never the action colour, or a star
       starts looking like something you can press. */
    /* Darker than a star looks in the wild, because this token is never a decoration: its
       one use is the rating on a salon card, 13px of text. The lighter gold sat at 3.63:1
       on white and 3.10:1 on the alt surface. Dark mode keeps its own, already clear. */
    --c-star:         #8E5A10;

    /* Type. Sora carries headings, prices and clock times; Source Sans 3 the
       running text and every form control. Both are vendored below. */
    --font-display: 'Sora', 'Segoe UI', system-ui, sans-serif;
    --font-sans: 'Source Sans 3', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

    /* Scale: 13 14 15 17 19 22 28 36 48 60. Body is 17px, and the smallest text
       anywhere in the product is 13px. Both are a step up from where this
       started, for one reason: the people using it are a hairdresser reading a
       phone across a counter and a customer who is not young. Small type is not
       elegance here, it is a barrier. */
    --fs-2xs:  0.8125rem;
    --fs-xs:   0.875rem;
    --fs-sm:   0.9375rem;
    --fs-base: 1.0625rem;
    --fs-lg:   1.1875rem;
    --fs-xl:   1.375rem;
    --fs-2xl:  1.75rem;
    --fs-3xl:  2.25rem;
    --fs-4xl:  3rem;
    --fs-5xl:  3.75rem;

    /* Space, base 4: 4 8 12 16 24 32 56 72 */
    --sp-1: 0.25rem;
    --sp-2: 0.5rem;
    --sp-3: 0.75rem;
    --sp-4: 1rem;
    --sp-5: 1.5rem;
    --sp-6: 2rem;
    --sp-7: 3.5rem;
    --sp-8: 4.5rem;

    --radius-sm:   8px;   /* small inputs                */
    --radius:      12px;  /* controls, chips             */
    /* Cards and sheets share a radius today, which is the shape the product has always
       had: this file said 16px, experience.css said 22px and won. Two names for one
       number is worth saying out loud rather than leaving as a ladder with two rungs at
       the same height - and closing it by moving cards back to 16px is a decision about
       how every card looks, not about where a token lives. */
    --radius-card: 22px;  /* cards                       */
    --radius-lg:   22px;  /* sheets, modals              */
    --radius-pill: 999px; /* avatars, pills              */

    /* Elevation never replaces a border - it only lifts something that is
       already outlined. 1 hover, 2 drawer/popover, 3 modal/sheet.
       Wide and faint rather than tight and dark, and in the ink of the page rather
       than in black - the two smaller steps rendered this way already; the largest
       one is here brought into the same family, at the size it always had. */
    --shadow-sm: 0 3px 12px rgb(21 43 53 / .025);
    --shadow:    0 16px 48px rgb(21 43 53 / .08);
    --shadow-lg: 0 24px 64px rgb(21 43 53 / .18);

    --container:      80rem;  /* 1280 - marketplace          */
    --container-wide: 100rem; /* 1600 - calendars, tables    */

    /* Touch targets never drop below this; the salon side is used one-handed,
       often with wet hands, sometimes while somebody is sitting in the chair. */
    --tap: 48px;
    /* The floor for a small button. It is not --tap, because dense rows of secondary
       actions genuinely read better at a smaller size - but 40px was under every
       guideline there is, and these are the buttons a salon presses all day. */
    --tap-sm: 44px;
    --tap-cta: 58px;
}

/* The design system is specified in light only. This is the derived dark
   counterpart: the same warmth and the same single action colour, lightened
   until each pair clears 4.5:1 against its own ground. Roles are never
   reassigned - teal stays the only thing you can press. */
@media (prefers-color-scheme: dark) {
    :root {
        /* Dark mode moves the hover lighter rather than darker, so --c-brand-dark
           is the pale end of the same hue. Measured: 8.04:1 for the button label,
           11.26:1 for a link on the page ground. */
        --c-brand:        #4FC4D2;
        --c-brand-dark:   #86DCE6;
        --c-brand-light:  #103038;
        --c-brand-ink:    #052026;
        --c-brand-vivid:  #6FE7DE;
        /* The ink band keeps its own colour in dark mode. It is a step darker
           than the page around it, not a step lighter, or the hero would stop
           being the thing the eye lands on first. */
        --c-ink:          #0B2329;
        --c-ink-text:     #F1F8F7;
        --c-ink-muted:    #AFC4C9;
        --c-ink-line:     rgba(255, 255, 255, 0.14);
        /* Light enough against the dark page that white on top of it is unreadable;
           the label goes dark instead. */
        --c-accent-ink:   #2A1004;
        --c-danger-ink:   #2E0F0D;
        --c-accent:       #F0834A;
        --c-accent-dark:  #F7A277;
        --c-accent-light: #33180D;
        --c-success:      #5FBF8C;
        --c-success-ink:  #06231A;
        --c-success-bg:   #17301F;
        --c-danger:       #E88A84;
        /* Hover moves toward the page's own direction: lighter on a dark ground. */
        --c-danger-dark:  #F3A9A4;
        --c-danger-bg:    #3A1E1C;
        --c-warning:      #D9A44E;
        --c-warning-bg:   #322715;
        --c-bg:           #101A20;
        --c-surface:      #18262E;
        --c-surface-alt:  #20323B;
        --c-border:       #344750;
        --c-border-strong:#8496A0;  /* 3:1 on surface, bg and surface-alt, as in light */
        --c-text:         #EFF5F7;
        --c-text-muted:   #B0C1C9;
        --c-star:         #E0B060;
        /* Three steps, each blurrier than the last. They were not: this file's ladder
           was overruled for the first two and kept for the third, so the largest shadow
           was the least blurred one on the page. */
        --shadow-sm: 0 3px 12px rgb(0 0 0 / .12);
        --shadow:    0 16px 48px rgb(0 0 0 / .25);
        --shadow-lg: 0 24px 64px rgb(0 0 0 / .45);
    }
}

/* Both families are vendored rather than linked from Google's CDN. This is a
   PWA that has to render offline, and a stylesheet the service worker cannot
   reach would drop every page to the fallback face. Each file is a variable
   font, so one per subset covers the whole 400-700 axis. The latin-ext subset
   is not optional here: c, c, d, s and z live in that range, which is most
   Bosnian text. */
@font-face {
    font-family: 'Sora';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/sora-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Sora';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/sora-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/source-sans-3-latin-ext.woff2') format('woff2');
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
    font-family: 'Source Sans 3';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/fonts/source-sans-3-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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

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

body {
    margin: 0;
    font-family: var(--font-sans);
    font-size: var(--fs-base);
    /* 1.6 rather than 1.5. Half the product is read on a phone held at arm's length in a
       salon; the extra leading costs a few pixels and buys back every one of them. */
    line-height: 1.6;
    color: var(--c-text);
    background: var(--c-bg);
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

/* Headings are Sora, and the negative tracking is part of the face's identity
   at display sizes - without it the letterforms drift apart and stop reading
   as one word. */
h1, h2, h3, h4 {
    font-family: var(--font-display);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.03em;
    margin: 0 0 var(--sp-4);
}
h1 { font-size: var(--fs-3xl); }
h2 { font-size: var(--fs-2xl); letter-spacing: -0.02em; }
h3 { font-size: var(--fs-xl); letter-spacing: -0.01em; }
h4 { font-size: var(--fs-lg); letter-spacing: normal; }

/* Every clock time and every price in the product renders through this. Sora
   with tabular figures is what keeps a column of 18:15 / 9:00 / 20 KM aligned
   on its digits instead of ragged. */
.num, time, .price, .slot__time, .slot__price {
    font-family: var(--font-display);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
p  { margin: 0 0 var(--sp-3); }

a { color: var(--c-brand-dark); }
a:hover { color: var(--c-brand); }

/* Focus is always visible. Keyboard users navigate the slot list and calendar. */
:focus-visible {
    outline: 2px solid var(--c-brand);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

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

.skip-link {
    position: absolute; left: var(--sp-3); top: -4rem;
    background: var(--c-surface); color: var(--c-text);
    padding: var(--sp-2) var(--sp-4); border-radius: var(--radius);
    box-shadow: var(--shadow); z-index: 100; transition: top 0.15s ease;
}
.skip-link:focus { top: var(--sp-3); }

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}

/* ==========================================================================
   Layout
   ========================================================================== */

.container { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: var(--sp-4); }
/* Wide enough for a table that has to be read across, not down: the admin directory and
   the week calendar. */
.container--wide { max-width: var(--container-wide); }

main { flex: 1 0 auto; padding-block: var(--sp-6) var(--sp-8); }

.site-header {
    background: var(--c-surface);
    border-bottom: 1px solid var(--c-border);
    position: sticky; top: 0; z-index: 30;
}
.site-header__inner {
    max-width: var(--container); margin-inline: auto;
    padding: var(--sp-3) var(--sp-4);
    display: flex; align-items: center; gap: var(--sp-4);
    flex-wrap: wrap;
}
.brand { display: inline-flex; align-items: center; gap: var(--sp-2); text-decoration: none; color: var(--c-text); font-weight: 700; }
/* The mark carries its own two colours and needs no tile behind it; it used to be a
   letter in a filled square, which is what a placeholder looks like. */
.brand__mark { width: 2.25rem; height: 2.25rem; display: grid; place-items: center; flex-shrink: 0; }
.brand__clock { width: 100%; height: 100%; }
/* The wordmark is the supplied artwork itself now, not an outline of it, so it is sized by
   height and the width comes from the logotype's own proportions rather than from a font
   metric. header.html says at length how the artwork got here.

   Three layers, one per ink, stacked in the same box. Each is a flat fill of a theme token
   cut to shape by a mask carrying that ink's coverage, which is why the colours still follow
   the theme the way a drawn outline did: the footer band redefines the tokens and the logo
   follows, with no second set of artwork to export and keep in step. */
.brand__word { display: block; height: 24px; }
.brand__wordmark { display: block; position: relative; height: 100%; aspect-ratio: 555 / 96; }
.brand__wordmark i {
    position: absolute; inset: 0;
    -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
.brand__wordmark .brand__ink {
    background: var(--c-text);
    -webkit-mask-image: url('/images/wordmark-ink.png');
    mask-image: url('/images/wordmark-ink.png');
}
.brand__wordmark .brand__teal {
    background: var(--c-brand);
    -webkit-mask-image: url('/images/wordmark-brand.png');
    mask-image: url('/images/wordmark-brand.png');
}
.brand__wordmark .brand__tld {
    background: var(--c-text-muted);
    -webkit-mask-image: url('/images/wordmark-tld.png');
    mask-image: url('/images/wordmark-tld.png');
}
/* A browser with no masks would paint three solid bars where the logo goes, which is the
   worst failure in the product to leave to chance. So it gets the same artwork already
   flattened onto each of the three grounds instead - one picture, the other two layers
   gone. Nothing here is fetched by a browser that supports masks, so the fallback costs
   everybody else nothing at all. */
@supports not ((-webkit-mask-image: url('/images/wordmark-ink.png'))
            or (mask-image: url('/images/wordmark-ink.png'))) {
    .brand__wordmark .brand__teal, .brand__wordmark .brand__tld { display: none; }
    .brand__wordmark .brand__ink {
        background: url('/images/wordmark-flat-light.png') center / 100% 100% no-repeat;
    }
    @media (prefers-color-scheme: dark) {
        .brand__wordmark .brand__ink { background-image: url('/images/wordmark-flat-dark.png'); }
    }
    .site-footer .brand__wordmark .brand__ink {
        background-image: url('/images/wordmark-flat-band.png');
    }
}

.site-nav { margin-left: auto; display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.site-nav a {
    text-decoration: none; color: var(--c-text-muted);
    padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm);
    font-size: var(--fs-sm); font-weight: 550;
    min-height: var(--tap); display: inline-flex; align-items: center;
}
.site-nav a:hover { background: var(--c-surface-alt); color: var(--c-text); }
.site-nav__logout { margin: 0; }

/* The foot of every page is a band in the ink, the same dark the hero is set in, so the page
   ends on something rather than trailing off into one more pale strip.

   It is re-themed, not restyled: inside the band the text, muted and brand tokens take their
   on-ink values, and everything that reads them follows without a rule of its own - the logo,
   which is the header's own drawing filled with those tokens, the icons, and the focus ring,
   which becomes the vivid turquoise that is visible on ink where the ordinary one is not. */
.site-footer {
    --c-text: var(--c-ink-text);
    --c-text-muted: var(--c-ink-muted);
    --c-brand: var(--c-brand-vivid);
    margin-top: auto;
    background: var(--c-ink);
    color: var(--c-ink-text);
}
.site-footer__inner {
    max-width: var(--container); margin-inline: auto;
    padding: var(--sp-7) 28px var(--sp-5);
    display: grid; gap: var(--sp-6);
}
.site-footer__top { display: grid; gap: var(--sp-6); }
.site-footer__about { display: grid; justify-items: start; align-content: start; gap: var(--sp-3); max-width: 26rem; }
.site-footer__brand { margin-bottom: var(--sp-1); }
.site-footer__tagline {
    font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 600;
    letter-spacing: -0.02em; line-height: 1.3; margin: 0;
}
.site-footer__lead { color: var(--c-ink-muted); font-size: var(--fs-sm); margin: 0; }
/* However many columns there are - a staff page has no salon column, a deployment with no
   contact details has no contact column - they share the width between them, so the band looks
   built for what it holds and an address has the room not to break in the middle of a word. */
.site-footer__groups {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
    gap: var(--sp-5); align-items: start;
}
/* Four of them do not always fit - above 64rem the groups get three 13rem tracks and their
   gaps and that is the whole width, and a fourth track would squeeze every column to about
   150px, which is where the address starts breaking mid-word again. So on a deployment that
   has configured contact details the store column wraps, and pinning it to the last track is
   what makes it wrap to directly under Kontakt rather than orphan itself under the first
   column. Where four do fit, and where none of this applies because there is no contact
   column to make a fourth, the line changes nothing. */
.site-footer__group--stores { grid-column: -2 / -1; }
.site-footer__heading {
    font-family: var(--font-sans); font-size: var(--fs-2xs); font-weight: 700;
    letter-spacing: 0.085em; text-transform: uppercase; line-height: 1.4;
    color: var(--c-ink-muted); margin: 0 0 var(--sp-1);
}
.site-footer__links { list-style: none; margin: 0; padding: 0; }
/* Every link is a full touch target: a list of them is exactly what a thumb misses. */
.site-footer__links a, .site-footer__legal a {
    display: inline-flex; align-items: center; gap: 10px;
    min-height: var(--tap-sm);
    color: var(--c-ink-text); font-size: var(--fs-sm); text-decoration: none;
}
.site-footer a:hover { color: var(--c-brand-vivid); text-decoration: underline; text-underline-offset: 3px; }
.site-footer__links .icon { width: 18px; height: 18px; color: var(--c-ink-muted); }
.site-footer__links a:hover .icon { color: inherit; }
/* An address is one word as far as the line breaker is concerned; in a column this narrow it
   has to be allowed to break anywhere rather than push the page sideways. */
.site-footer__value { min-width: 0; overflow-wrap: anywhere; }
.site-footer__note { color: var(--c-ink-muted); font-size: var(--fs-xs); line-height: 1.5; margin: var(--sp-2) 0 0; max-width: 17rem; }
/* App Store and Google Play. Neither row is a link or a tap target, so the 44px rhythm the
   columns beside them keep for a thumb would be height bought for nothing; the rows stand at
   their own height and the list carries the spacing instead. */
.site-footer__stores { display: grid; gap: var(--sp-2); }
.site-footer__stores li {
    display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
    color: var(--c-ink-text); font-size: var(--fs-sm);
}
/* The word that makes the row honest, set off from the name so it cannot be read as part of
   it. Muted on the band is 9.2:1, the same token the note and the copyright line already use. */
.site-footer__soon {
    font-size: var(--fs-2xs); line-height: 1.6; white-space: nowrap;
    color: var(--c-ink-muted); border: 1px solid var(--c-ink-line);
    border-radius: 999px; padding: 0 8px;
}
.site-footer address { font-style: normal; }
.site-footer__bottom {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: 0 var(--sp-5); padding-top: var(--sp-3); border-top: 1px solid var(--c-ink-line);
}
.site-footer__meta { color: var(--c-ink-muted); font-size: var(--fs-xs); margin: 0; }
.site-footer__legal { display: flex; flex-wrap: wrap; column-gap: var(--sp-5); }
.site-footer__legal a { color: var(--c-ink-muted); font-size: var(--fs-xs); }
/* Side by side once there is room: the logo and what the product is on the left, the columns
   packed against the right edge, where the legal links below them end too. auto-fit rather than
   auto-fill, so the tracks nothing stands in close up instead of holding the columns off it. */
@media (min-width: 64rem) {
    .site-footer__top { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--sp-7); }
    .site-footer__groups { grid-template-columns: repeat(auto-fit, minmax(10rem, 13rem)); justify-content: end; }
}
@media (max-width: 47.9375rem) {
    .site-footer__inner { padding: var(--sp-6) 20px var(--sp-4); gap: var(--sp-5); }
    .site-footer__top { gap: var(--sp-5); }
    .site-footer__groups { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: var(--sp-4); }
    /* Half a phone is narrower than an email address; the contact column takes the whole row,
       and so does the store column - two names, the word beside each and a sentence under
       them have nowhere to go in half of 390px. */
    .site-footer__group--contact, .site-footer__group--stores { grid-column: 1 / -1; }
}
/* The dark page is nearly the colour of the band, so there the band needs an edge to end on. */
@media (prefers-color-scheme: dark) {
    .site-footer { border-top: 1px solid var(--c-ink-line); }
}

/* ==========================================================================
   Surfaces
   ========================================================================== */

.card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    padding: var(--sp-5);
    box-shadow: var(--shadow-sm);
}
/* A card is a secondary surface, so its heading is a step below a page heading.
   Without this every card on a page shouts at the same volume as the page
   itself, and a reader with nothing to anchor on reads the whole screen as one
   undifferentiated block - which is exactly what makes an interface feel
   complicated to somebody who does not use software all day. */
.card h2 { font-size: var(--fs-xl); }
.card h3 { font-size: var(--fs-lg); }

.card--flush { padding: 0; overflow: hidden; }
.card__header { padding: var(--sp-5); border-bottom: 1px solid var(--c-border); }
.card__body { padding: var(--sp-5); }

.stack > * + * { margin-top: var(--sp-4); }
.stack--tight > * + * { margin-top: var(--sp-2); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.grid { display: grid; gap: var(--sp-4); }
@media (min-width: 40rem)  { .grid--2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 60rem)  { .grid--3 { grid-template-columns: repeat(3, 1fr); }
                             .grid--4 { grid-template-columns: repeat(4, 1fr); } }

/* A panel carries a bottom margin for standing alone in the flow. Inside a grid the gap is
   already doing that job and the two of them add up: two stacked tiles sat 40px apart on a
   phone where every other pair on the screen sits 16px apart. The gap wins - it is the one
   that knows there is a row. */
.grid > .panel,
.grid > .panel--headline { margin-bottom: 0; }

.muted { color: var(--c-text-muted); }
.text-sm { font-size: var(--fs-sm); }
.text-xs { font-size: var(--fs-xs); }
.strike { text-decoration: line-through; color: var(--c-text-muted); font-weight: 400; }

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

/* A button is the one thing on the page that must never be mistaken for
   anything else: solid fill, heavy label, no cleverness. It lifts a pixel on
   hover, which is the whole of the animation budget. */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
    min-height: var(--tap); padding: var(--sp-3) var(--sp-5);
    border-radius: var(--radius); border: 1px solid transparent;
    font: inherit; font-family: var(--font-sans);
    font-weight: 700; letter-spacing: -0.005em;
    text-decoration: none; cursor: pointer;
    background: var(--c-brand); color: var(--c-brand-ink);
    transition: background 0.15s ease, border-color 0.15s ease, transform 0.12s ease;
}
.btn:hover  { background: var(--c-brand-dark); color: var(--c-brand-ink); transform: translateY(-1px); }
.btn:active { transform: none; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; transform: none; }

.btn--secondary { background: var(--c-surface); color: var(--c-text); border-color: var(--c-border-strong); }
.btn--secondary:hover { background: var(--c-surface-alt); color: var(--c-text); }
.btn--ghost { background: transparent; color: var(--c-text); border-color: transparent; }
.btn--ghost:hover { background: var(--c-surface-alt); color: var(--c-text); }
.btn--danger { background: var(--c-danger); color: var(--c-danger-ink); }
.btn--danger:hover { background: var(--c-danger-dark); color: var(--c-danger-ink); }
/* Reserved for the one action that is about a dropped price, so it carries the
   same colour the discount badges do. */
.btn--accent { background: var(--c-accent); color: var(--c-accent-ink); }
.btn--accent:hover { background: var(--c-accent-dark); color: var(--c-accent-ink); }
.btn--sm { min-height: var(--tap-sm); padding: var(--sp-2) var(--sp-3); font-size: var(--fs-sm); }
.btn--lg { min-height: var(--tap-cta); font-size: var(--fs-lg); padding-inline: var(--sp-6); }
.btn--block { width: 100%; }

/* ==========================================================================
   Badges and pills
   ========================================================================== */

.badge {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    padding: 0.2rem var(--sp-3); border-radius: var(--radius-pill);
    font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0;
    background: var(--c-surface-alt); color: var(--c-text-muted);
    border: 1px solid var(--c-border);
}
.badge--brand   { background: var(--c-brand-light); color: var(--c-brand-dark); border-color: transparent; }
/* accent-dark, not accent: the badge sets 14px bold on the pale accent ground, and the
   lighter of the two orange tokens lands at 4.22:1 there - short of 4.5 and not large
   enough text to be allowed the lower bar. This is the discount badge, the one label on
   the page the product exists to make people read. */
.badge--accent  { background: var(--c-accent-light); color: var(--c-accent-dark); border-color: transparent; }
.badge--success { background: var(--c-success-bg); color: var(--c-success); border-color: transparent; }
.badge--danger  { background: var(--c-danger-bg); color: var(--c-danger); border-color: transparent; }
.badge--warning { background: var(--c-warning-bg); color: var(--c-warning); border-color: transparent; }

/* Both badge rows below - the courses a person has, the figures the product counted for them -
   are laid out the same way, because they sit under the same name and a reader should not have
   to work out that two rows of pills are two rows of pills. What differs between them is the
   pill, not the row.

   The achievements used to share this rule and no longer do. They stopped being pills - see the
   medallions further down - and a row of rings is not laid out like a row of pills. The
   certificates keep the shape they always had, which is now also what separates the two claims
   at a glance: a thing somebody signed still looks like a label, and a thing nobody typed looks
   like a measurement. */
.cert-row {
    display: flex; flex-wrap: wrap; gap: var(--sp-2);
    list-style: none; margin: var(--sp-2) 0 0; padding: 0;
}

/* A course badge is not a status badge.

   The eight above all say what is happening to an appointment - Na cekanju, Nije dosla, -20%.
   This one says what a person has done, which is a different kind of claim and gets a different
   treatment: the pill shape stays, so the page still reads as one system, and the colour goes,
   so it cannot compete with the discount badge - the one label on this page the product exists
   to make people read. The seal takes the brand colour and nothing else in the row does.

   There is one state here and no second, paler one. An unconfirmed course never leaves
   /business, so no visitor is ever asked to read a shade and work out what it means. */
.cert {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    padding: 0.15rem var(--sp-3); border-radius: var(--radius-pill);
    font-size: var(--fs-xs); font-weight: 600;
    background: var(--c-surface-alt); color: var(--c-text);
    border: 1px solid var(--c-border);
}
/* 24px is the default and would tower over 14px text; the seal is set to the cap height of the
   name beside it so the pill stays one line tall on a 360px screen. */
.cert .icon { width: 15px; height: 15px; color: var(--c-brand-dark); }
/* The year is a fact about the claim, not a second claim, so it steps back rather than sitting
   at the name's weight. Tabular figures because a wrapped row of these is a column. */
.cert__year { color: var(--c-text-muted); font-weight: 500; font-variant-numeric: tabular-nums; }

/* An achievement is a medallion: a ring cut into one segment per rung of its ladder, lit up to
   the rung this salon reached.

   It used to be a tinted pill the same size and shape as a certificate. The pill was defensible -
   the two are the same kind of object, a short fact about this salon or this person - but it had
   one flaw that no amount of colour fixes: a number printed alone has no scale, so "3 godine
   rada" left a reader to guess whether three is a lot, and 95% and 100% drew identically when
   one of them means "not one all year" and the other does not.

   The ring supplies the scale. Three of five segments says there are salons here with ten
   without a word being written, and the lowest rung visibly sits at the bottom of its ladder.
   That is also what finally separates a measurement from a claim somebody signed: a certificate
   is still a label, and a thing nobody typed now looks like an instrument.

   Every kind takes the same brand tint, as the pills did, and for the unchanged reason: age,
   reliability, volume and regulars are all just measurements, none is better news than another,
   and the semantic greens and ambers in this file mean good and bad elsewhere on the page. The
   icon separates them and is the only thing that needs to. */
.medal-row {
    --medal-size: 48px;
    display: flex; flex-wrap: wrap; gap: var(--sp-5) var(--sp-4);
    list-style: none; margin: var(--sp-4) 0 0; padding: 0;
}
/* Four of these have to sit on one line at 390px, where the content box is about 358. The basis
   is what makes that arithmetic work: four at 76px with three 16px gaps is 340. Allowed to grow
   on a wider screen, but capped, because a medallion stretched across a desktop column stops
   reading as a dial and starts reading as a card. */
.medal {
    flex: 1 1 4.75rem; max-width: 7rem;
    display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
    text-align: center;
}
.medal__dial {
    position: relative;
    display: grid; place-items: center;
    width: var(--medal-size); height: var(--medal-size);
    flex: none;
}
/* Rotated so the ring starts at twelve o'clock. An SVG circle begins at three, which would put
   the first rung of every badge on the page's right-hand edge, where nothing else starts. */
.medal__ring {
    position: absolute; inset: 0; width: 100%; height: 100%;
    transform: rotate(-90deg);
}
/* Flat ends, not round. A rounded cap adds half the stroke width to each end of every arc, which
   at this radius eats the gap between rungs and closes the ring back into the one shape it must
   never be: an unbroken circle, which is a decoration rather than a scale. Flat ends also read as
   a gauge, which is what this is. */
.medal__seg { fill: none; stroke: var(--c-border); }
.medal__seg.is-lit { stroke: var(--c-brand); }
.medal__dial .icon {
    position: relative;
    width: calc(var(--medal-size) * 0.42); height: calc(var(--medal-size) * 0.42);
    color: var(--c-brand-dark);
}
.medal__label {
    font-size: var(--fs-2xs); font-weight: 600; line-height: 1.3;
    color: var(--c-text); font-variant-numeric: tabular-nums;
    text-wrap: balance;
}

/* Beside a name rather than under it. One person carries one badge, and a lone stacked column in
   the middle of a team card would read as a heading for the card rather than a fact inside it. */
.medal-row--inline { --medal-size: 32px; gap: var(--sp-3); }
.medal-row--inline .medal {
    flex: 0 1 auto; max-width: none;
    flex-direction: row; align-items: center; gap: var(--sp-2);
    text-align: left;
}

/* The rule, kept next to the claim.

   Closed by default and plain text inside: a reader who wants the number gets the number, and a
   reader who wants to know whether to believe it does not have to go looking on another page.
   This is the part that makes the whole family defensible rather than decorative, which is why it
   is a disclosure on the salon's own front door and not a line in a help centre. */
.how-counted { margin: var(--sp-3) 0 0; font-size: var(--fs-xs); }
.how-counted > summary {
    cursor: pointer; color: var(--c-brand-dark); font-weight: 600;
    padding: var(--sp-1) 0; width: fit-content;
}
.how-counted > summary:hover { color: var(--c-brand); }
.how-counted ul { margin: var(--sp-2) 0 0; padding-left: var(--sp-4); }
.how-counted li { margin-bottom: var(--sp-1); }
.how-counted p, .how-counted li { color: var(--c-text-muted); line-height: 1.5; }

/* The only instruction on the whole site, kept behind a disclosure beside the buttons it is about.

   Closed by default, because for most people the button simply works. It is here for the one
   whose file went to Preuzimanja instead of the calendar, who is looking at a screen that says
   the appointment is made and hearing nothing at all from the phone. Same shape as the
   disclosure on the salon's page above: a summary that reads as a question, plain text inside. */
.calendar-help { margin: var(--sp-3) 0 0; font-size: var(--fs-xs); }
.calendar-help > summary {
    cursor: pointer; color: var(--c-brand-dark); font-weight: 600;
    padding: var(--sp-1) 0; width: fit-content;
}
.calendar-help > summary:hover { color: var(--c-brand); }
.calendar-help p { color: var(--c-text-muted); line-height: 1.5; margin: var(--sp-2) 0 0; }
.calendar-help ol { margin: var(--sp-1) 0 0; padding-left: var(--sp-4); }
.calendar-help li { color: var(--c-text-muted); line-height: 1.5; margin-bottom: var(--sp-1); }
.calendar-help__device { color: var(--c-text); font-weight: 650; margin-top: var(--sp-3); }
.calendar-help__aside { font-style: italic; }

/* The same ladders on the salon's own screen, laid out as rows rather than as a row of dials.

   The public page is answering "is this salon any good" and wants four badges taken in at a
   glance. This one is answering "where do I stand", which is read one line at a time, and the
   useful half of each line is the sentence beside the ring rather than the ring itself. Same
   medallion, same fragment, turned on its side. */
.ladder-list { display: grid; gap: var(--sp-3); list-style: none; margin: var(--sp-4) 0 0; padding: 0; }
.ladder {
    --medal-size: 48px;
    display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--radius-card); padding: var(--sp-4);
}
/* Takes the slack so the state chip is pushed to the far edge, and can shrink far enough for a
   long Croatian sentence to wrap instead of widening the row past the screen. */
.ladder__body { flex: 1 1 12rem; min-width: 0; }
.ladder__figure { display: block; font-weight: 700; font-variant-numeric: tabular-nums; }
.ladder__next {
    margin: var(--sp-1) 0 0; color: var(--c-text-muted);
    font-size: var(--fs-xs); line-height: 1.4;
}
/* Grey by default, brand when the badge is actually on the page. Not green: green in this file
   means an appointment went well, and a badge appearing is not good news, only news. */
.ladder__state {
    flex: none;
    padding: 0.15rem var(--sp-3); border-radius: var(--radius-pill);
    font-size: var(--fs-2xs); font-weight: 600;
    background: var(--c-surface-alt); color: var(--c-text-muted);
    border: 1px solid var(--c-border);
}
.ladder__state.is-shown {
    background: var(--c-brand-light); color: var(--c-brand-dark); border-color: transparent;
}

/* ==========================================================================
   Forms
   ========================================================================== */

.field { display: block; margin-bottom: var(--sp-4); }
/* A label is a question being asked, so it is set at reading size in the text
   colour. Small grey labels are what makes an ordinary form look like a form
   for people who fill in forms for a living. */
.field__label { display: block; font-weight: 700; font-size: var(--fs-base); margin-bottom: var(--sp-2); }
.field__hint  { display: block; font-size: var(--fs-xs); color: var(--c-text-muted); margin-top: var(--sp-1); }
.field__error { display: block; font-size: var(--fs-sm); color: var(--c-danger); margin-top: var(--sp-1); font-weight: 550; }

/* An allowlist, which means every type left out of it renders as raw browser default -
   21px tall, wrong border, wrong font. That is exactly how file and datetime-local got
   missed. Add the type here when you add the input, or the field will look like it
   belongs to a different website. */
input[type="text"], input[type="email"], input[type="password"], input[type="tel"],
input[type="number"], input[type="date"], input[type="time"], input[type="search"],
input[type="url"], input[type="datetime-local"], input[type="file"],
select, textarea {
    width: 100%; min-height: var(--tap);
    padding: var(--sp-3) var(--sp-3);
    font: inherit; color: var(--c-text);
    background: var(--c-surface);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius);
}
input:disabled, select:disabled, textarea:disabled { background: var(--c-surface-alt); color: var(--c-text-muted); }
textarea { min-height: 6rem; resize: vertical; }

/* Safari on iOS sizes a date or time field by its own native widget and treats that width as a
   floor: `width: 100%` cannot take the control below it, so in a column narrower than the widget
   the field spills straight out of its label - the two time fields overlapping each other, the
   date clipped, the sheet scrolling sideways. Reported from a phone on 29.09.2026.

   Not reproducible in a desktop browser, and that is the trap: Chromium draws both fields narrow
   enough that nothing overflows, which is why `min-width: 0` on the grid item further down looked
   like it had already fixed this. It had not - that line lets the track shrink, and the control
   inside the track is the thing that refuses to. The tell on the phone is the value sitting
   right-aligned in a box whose every neighbour aligns left: that is the widget drawing itself.

   Dropping the native appearance is what makes these obey `width: 100%` like every other field.
   Scoped to iOS on purpose: the same line on Chromium and Android takes away the calendar and
   clock icons that open the picker, and there is nothing to fix there. */
@supports (-webkit-touch-callout: none) {
    input[type="date"], input[type="time"], input[type="datetime-local"] {
        -webkit-appearance: none;
        appearance: none;
        min-width: 0;
        text-align: start;
    }
}
[aria-invalid="true"] { border-color: var(--c-danger); }

/* Under the sign-in card the page still has two people to answer: the salon that has no profile
   yet, and the customer who came through the professionals' door by mistake. Both of them used
   to be one more line of small text below the forgotten-password link - three lines of the same
   size, the same colour and the same shape, and the one that opens an account was the middle one.

   A rule and a gap are what make each of them a separate thing to decide rather than more of the
   sentence above. The block is a flex column so the same class works with one child or two, and
   so the gap does not depend on which element happens to be last. */
.signin-alt {
    display: flex; flex-direction: column; gap: var(--sp-3);
    margin-top: var(--sp-5);
    padding-top: var(--sp-5);
    border-top: 1px solid var(--c-border);
}
.signin-alt > p { margin: 0; }

fieldset { border: 1px solid var(--c-border); border-radius: var(--radius); padding: var(--sp-4); margin: 0 0 var(--sp-4); }
legend { font-weight: 650; padding-inline: var(--sp-2); }

.form-row { display: grid; gap: var(--sp-4); }
@media (min-width: 40rem) { .form-row--2 { grid-template-columns: 1fr 1fr; } }

/* ==========================================================================
   Alerts
   ========================================================================== */

.alert {
    border-radius: var(--radius); padding: var(--sp-3) var(--sp-4);
    border: 1px solid transparent; margin-bottom: var(--sp-4);
}
.alert--info    { background: var(--c-brand-light); color: var(--c-brand-dark); }
.alert--success { background: var(--c-success-bg); color: var(--c-success); }
.alert--danger  { background: var(--c-danger-bg); color: var(--c-danger); }
.alert--warning { background: var(--c-warning-bg); color: var(--c-warning); }
.alert p:last-child { margin-bottom: 0; }

/* ==========================================================================
   Tables
   ========================================================================== */

.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.data th, table.data td { padding: var(--sp-3); text-align: left; border-bottom: 1px solid var(--c-border); }
table.data th { font-weight: 650; color: var(--c-text-muted); white-space: nowrap; }
table.data tbody tr:hover { background: var(--c-surface-alt); }

/* The template picker puts an editable field in every cell of an eighty-row table. Default
   padding turns that into a page nobody scrolls to the bottom of, and a full-width number input
   makes a three-digit minute count look like a paragraph. */
table.data--compact th, table.data--compact td { padding: var(--sp-2); vertical-align: middle; }
.input--narrow { width: 6rem; min-width: 0; display: inline-block; }

/* ==========================================================================
   Empty states
   ========================================================================== */

/* A column with a gap, and not a block that lets its children fall where the line boxes put
   them. Two `.btn` here are two inline-flex boxes, and on this machine they landed exactly
   touching - 647 to 700, then 700 to 753 - while on a phone they overlapped and the outlined
   one's bottom corners came out from under the filled one. Nothing separated them: a button
   carries no margin, and consecutive line boxes are flush by definition. */
.empty {
    display: flex; flex-direction: column; align-items: center; gap: var(--sp-4);
    text-align: center; padding: var(--sp-6) var(--sp-4);
    color: var(--c-text-muted); background: var(--c-surface);
    border: 1px dashed var(--c-border-strong); border-radius: var(--radius);
}

/* The gap is the only spacing in here. A flex container's gap adds to whatever margins the
   children brought with them rather than absorbing them, and headings and paragraphs bring
   plenty. */
.empty > * { margin: 0; }

/* The one line in here that is not muted. This said `h3` alone, and four of the five empty
   states that carry a title write `h2` - so on the search, last minute, favourites and
   analytics screens the title was the same grey as the sentence underneath it, which is why
   none of them read as having a heading at all. */
.empty h2,
.empty h3 { color: var(--c-text); }

/* Full width on a phone. Two centred pills of unequal width read as two things that failed to
   line up; the same two full width read as two offers, one under the other, and each is a
   whole-thumb target. Wider than that they go back to their own width, still stacked. */
.empty .btn { align-self: center; }

@media (max-width: 30rem) {
    .empty .btn { align-self: stretch; }
}

/* ==========================================================================
   Error pages
   ========================================================================== */

.error-page { max-width: 34rem; margin-inline: auto; text-align: center; padding-block: var(--sp-7); }
.error-page__code { font-size: var(--fs-3xl); font-weight: 800; color: var(--c-brand); margin-bottom: var(--sp-2); }

/* ==========================================================================
   HTMX
   ========================================================================== */

.htmx-indicator { opacity: 0; transition: opacity 0.2s ease; }
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator { opacity: 1; }
.htmx-swapping { opacity: 0.5; }

/* ==========================================================================
   Home hero and slot search
   ========================================================================== */

/* The hero is a full-bleed ink band pulled flush against the header, and the
   search card sits on it as the single lit object on the page. The old version
   was a pale tint that faded into the page below it, which meant the first
   screen had no centre. This one has exactly one. */
.hero {
    background: var(--c-ink);
    color: var(--c-ink-text);
    margin-top: calc(-1 * var(--sp-6));
    padding-block: var(--sp-6) var(--sp-7);
}
.hero__title {
    font-size: var(--fs-3xl);
    line-height: 1.02;
    letter-spacing: -0.04em;
    color: var(--c-ink-text);
    max-width: 16ch;
    margin-bottom: var(--sp-3);
}
.hero__lead {
    color: var(--c-ink-muted);
    font-size: var(--fs-lg);
    max-width: 36ch;
    margin-bottom: var(--sp-5);
}
.hero__presets { margin-top: var(--sp-5); }
.hero__presets .muted { color: var(--c-ink-muted); }

/* The shortcuts under the search card. Written the way somebody would say it
   out loud, and sized so a thumb finds one without aiming. A small pill badge
   reads as a label; these have to read as something you press. */
.preset {
    display: inline-flex; align-items: center;
    min-height: 2.75rem; padding-inline: var(--sp-4);
    border-radius: var(--radius-pill);
    border: 1px solid var(--c-border-strong);
    background: var(--c-surface);
    color: var(--c-text);
    font-weight: 700; font-size: var(--fs-sm);
    text-decoration: none;
}
.preset:hover { border-color: var(--c-brand); color: var(--c-brand-dark); }
.preset--deal { background: var(--c-accent); border-color: transparent; color: var(--c-accent-ink); }
.preset--deal:hover { background: var(--c-accent-dark); border-color: transparent; color: var(--c-accent-ink); }

/* On the ink band the same control has to be drawn out of the dark rather than
   laid on top of it, so only the ground and the text colour change. */
.hero__presets .preset {
    border-color: var(--c-ink-line);
    background: rgba(255, 255, 255, 0.07);
    color: var(--c-ink-text);
}
.hero__presets .preset:hover { background: rgba(255, 255, 255, 0.15); color: var(--c-ink-text); }
.hero__presets .preset--deal { background: var(--c-accent); border-color: transparent; color: var(--c-accent-ink); }
.hero__presets .preset--deal:hover { background: var(--c-accent-dark); color: var(--c-accent-ink); }

@media (min-width: 48rem) {
    .hero { padding-block: var(--sp-7) var(--sp-8); }
    .hero__title { font-size: var(--fs-4xl); }
}
@media (min-width: 64rem) {
    .hero__title { font-size: var(--fs-5xl); }
}

/* The search card.
   One white card, one row per question, a divider between them and the action
   sitting inside the card rather than under it. The label is a fixed column so
   the three answers line up as a column of their own - that alignment is what
   makes it read as a single sentence being filled in rather than three separate
   form fields. Values are borderless on purpose: the card is the input. */
.search-box {
    background: var(--c-surface);
    border: 0;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}
.search-box__field {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: 0 var(--sp-4);
    border-bottom: 1px solid var(--c-border);
    min-height: 3.75rem;
}
/* Three plain questions in the same size and colour as the answers beside them.
   They were a row of small grey capitals, which is the single detail that made
   this page feel like software rather than like being asked what you need. */
.search-box__field .field__label {
    flex: 0 0 6.5rem;
    margin: 0;
    font-size: var(--fs-base);
    font-weight: 700;
    text-transform: none;
    letter-spacing: -0.01em;
    color: var(--c-text);
}
.search-box__field > input,
.search-box__field > select {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    background: transparent;
    padding: var(--sp-3) 0;
    font-family: var(--font-sans);
    font-size: var(--fs-lg);
    font-weight: 600;
    color: var(--c-text);
    /* The row already carries the focus ring, so the control must not draw a
       second one inside it. */
    box-shadow: none;
}
.search-box__field:focus-within {
    background: var(--c-brand-light);
}
.search-box__field > input:focus,
.search-box__field > select:focus {
    outline: none;
}
.search-box__submit { padding: var(--sp-3); }
.search-box__submit .btn { min-height: var(--tap-cta); border-radius: var(--radius); font-size: var(--fs-lg); }

@media (min-width: 56rem) {
    /* Four columns, divided vertically rather than horizontally. The card keeps
       its shape; only the axis changes. */
    .search-box {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr auto;
        align-items: stretch;
    }
    .search-box__field {
        flex-direction: column;
        align-items: stretch;
        justify-content: center;
        gap: 0;
        border-bottom: 0;
        border-right: 1px solid var(--c-border);
        padding-block: var(--sp-3);
    }
    .search-box__field .field__label { flex: none; margin-bottom: var(--sp-1); font-size: var(--fs-sm); }
    .search-box__field > input,
    .search-box__field > select { padding: 0; }
    .search-box__submit { display: flex; align-items: center; }
    .search-box__submit .btn { white-space: nowrap; height: 100%; }
}

/* ==========================================================================
   Home rails: what is free right now, and what people book most
   ========================================================================== */

/* A horizontal rail rather than a grid, because its job is to show that free
   appointments exist - not to be exhaustive. Scrolls with the thumb on a phone
   and with a trackpad on a desktop; no controls, nothing to learn. */
.rail {
    display: flex;
    gap: var(--sp-3);
    overflow-x: auto;
    padding-bottom: var(--sp-2);
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
.rail > * { scroll-snap-align: start; }
.rail::-webkit-scrollbar { height: 6px; }
.rail::-webkit-scrollbar-thumb { background: var(--c-border-strong); border-radius: 999px; }

.rail-card {
    flex: 0 0 15rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    padding: var(--sp-4);
    display: grid;
    gap: var(--sp-1);
    color: inherit;
    text-decoration: none;
}
.rail-card:hover { border-color: var(--c-border-strong); }
.rail-card__top { display: flex; justify-content: space-between; gap: var(--sp-2); align-items: baseline; }
.rail-card__name { font-family: var(--font-display); font-weight: 700; font-size: var(--fs-base); letter-spacing: -0.02em; }
.rail-card__rating { font-size: var(--fs-2xs); font-weight: 600; color: var(--c-star); white-space: nowrap; }
.rail-card__meta { font-size: var(--fs-xs); color: var(--c-text-muted); }
.rail-card__times { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-2); }

/* A time is a number you press. Sora, because every clock time in the product
   is Sora, and bordered rather than filled so a rail of them does not read as a
   row of primary buttons. */
.time-chip {
    display: inline-flex;
    align-items: center;
    min-height: 2.375rem;
    padding-inline: var(--sp-3);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius-sm);
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--fs-sm);
    color: inherit;
    text-decoration: none;
    background: var(--c-surface);
}
.time-chip:hover { border-color: var(--c-brand); color: var(--c-brand-dark); }
.time-chip--more { border: 0; color: var(--c-brand); font-family: var(--font-sans); }

/* Pill chips for the popular services. The count is the point: a chip that
   claims nothing behind it is worse than no chip. */
.chip-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    min-height: 2.875rem;
    padding-inline: var(--sp-4);
    border: 1px solid var(--c-border-strong);
    border-radius: 999px;
    background: var(--c-surface);
    font-weight: 700;
    font-size: var(--fs-base);
    color: inherit;
    text-decoration: none;
}
.chip:hover { border-color: var(--c-brand); color: var(--c-brand-dark); }
.chip__count { color: var(--c-text-muted); font-weight: 400; }

/* A group's name ahead of its trades - Ljepota, Zdravlje. Set like the shelf headings on the
   home page, in small capitals and with no outline, so the row reads as trades under headings
   rather than as sixteen of the same thing. Still a chip - the same height, the same target,
   filled when in force - because pressing it searches the whole group. */
.chip--group {
    border-color: transparent;
    background: transparent;
    padding-inline: var(--sp-3);
    font-size: .8125rem;
    letter-spacing: .085em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}
.chip--group:not(:first-child) { margin-inline-start: var(--sp-4); }

/* The one in force. Filled rather than outlined, because a border colour is not a state
   anybody notices - and it links back to the same search without it, so it is also the
   way out of the filter it applies. */
.chip--on {
    background: var(--c-brand);
    border-color: transparent;
    color: var(--c-brand-ink);
}
.chip--on:hover { background: var(--c-brand-dark); border-color: transparent; color: var(--c-brand-ink); }

/* The catalogue above the results reads as navigation, so it gets air above and below it
   rather than sitting flush against the filters and the first card. */
.chip-row--filters { margin-bottom: var(--sp-5); }

/* On a phone the same twelve chips wrap to five rows and push the first result 260px down
   the page - which is the very thing the filter sheet exists to prevent. So they become one
   line that slides sideways: the whole catalogue is still there, and the answer stays where
   the reader is looking. app.js slides the chip in force into view on load; without it the
   list simply starts at the beginning, which is not wrong, only less helpful. */
@media (max-width: 47.9375rem) {
    .chip-row--filters {
        flex-wrap: nowrap;
        overflow-x: auto;
        scroll-snap-type: x proximity;
        /* The row runs edge to edge so it reads as something that continues past the screen,
           rather than as a list that happens to be cut off. */
        margin-inline: calc(-1 * var(--sp-4));
        padding-inline: var(--sp-4);
        scrollbar-width: none;
    }
    .chip-row--filters::-webkit-scrollbar { display: none; }
    .chip-row--filters .chip { flex: 0 0 auto; scroll-snap-align: start; scroll-margin-inline: var(--sp-4); }
}

.section-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--sp-3);
    margin-bottom: var(--sp-3);
}
.section-head h2 { font-size: var(--fs-xl); margin: 0; }
.section-head a { font-size: var(--fs-sm); font-weight: 700; }

/*
  Kept for anything that still asks for it, but the home page no longer does: a tinted card the
  full width of the page put the salon pitch at the same weight as the search itself.
*/
.card--pitch { background: var(--c-brand-light); border-color: transparent; }
.card--pitch h2 { margin-bottom: var(--sp-2); }

/*
  The salon invitation on the customer home page. A rule and a line of text rather than a card:
  it should read like a footnote somebody in the trade will notice, not like an advertisement
  standing between a woman and the appointment she came for.
*/
.pitch {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-4);
    align-items: center;
    justify-content: space-between;
    padding-top: var(--sp-5);
    border-top: 1px solid var(--c-border);
}

.pitch__body { flex: 1 1 20rem; min-width: 0; }
.pitch h2 { font-size: var(--fs-lg); margin-bottom: var(--sp-1); letter-spacing: -0.02em; }
.pitch p { margin: 0; }

/* ==========================================================================
   Salon side: setup, catalogue and schedule screens
   ========================================================================== */

.narrow {
    max-width: 34rem;
    margin-inline: auto;
}

/* Sub-navigation within the business area. Scrolls sideways on a phone rather
   than wrapping, so the row of sections stays one predictable line. */
.subnav {
    display: flex;
    gap: var(--sp-1);
    overflow-x: auto;
    margin-bottom: var(--sp-5);
    padding-bottom: var(--sp-1);
    border-bottom: 1px solid var(--c-border);
    scrollbar-width: thin;
}

.subnav a {
    flex: 0 0 auto;
    padding: var(--sp-2) var(--sp-3);
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    border-radius: var(--radius-sm);
    color: var(--c-text-muted);
    text-decoration: none;
    font-size: var(--fs-sm);
    white-space: nowrap;
}

.subnav a:hover { background: var(--c-surface-alt); color: var(--c-text); }

.subnav a.is-active {
    background: var(--c-brand-light);
    color: var(--c-brand-dark);
    font-weight: 600;
}

/* How many people are waiting for an answer, carried next to Danas on every salon screen.
   Sized in ch so two digits fit without the pill changing shape, and never smaller than a
   readable dot: this is a number to read, not a dot to notice. */
.subnav__badge {
    margin-left: var(--sp-1);
    min-width: 1.6ch;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--c-warning-bg);
    color: var(--c-warning);
    font-size: var(--fs-xs);
    font-weight: 700;
    line-height: 1.5;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

/* Requests waiting for an answer, above the day itself. */
.requests { margin-bottom: var(--sp-4); }
.requests h2 { margin: 0 0 var(--sp-1); }
.requests__list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; }

.requests__item {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-3);
    align-items: center;
    justify-content: space-between;
    padding: var(--sp-3) 0;
    border-top: 1px solid var(--c-border);
}

.requests__item p { margin: 0; }
.requests__who { font-size: var(--fs-base); }
.requests__who span { color: var(--c-text-muted); margin-left: var(--sp-1); }
.requests__deadline { margin-top: var(--sp-1); font-size: var(--fs-sm); font-weight: 600; }
.requests__actions { display: flex; gap: var(--sp-2); }
.requests__actions form { margin: 0; }

.page-head {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--sp-4);
}

.page-head h1, .page-head h2 { margin: 0; }

.inline-form {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: flex-end;
    margin-bottom: var(--sp-5);
}

.link-back {
    display: inline-block;
    margin-top: var(--sp-4);
    font-size: var(--fs-sm);
}

/* Onboarding checklist ---------------------------------------------------- */

.progress-label {
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
    margin-bottom: var(--sp-3);
}

.setup-list {
    list-style: none;
    margin: 0 0 var(--sp-6);
    padding: 0;
    display: grid;
    gap: var(--sp-3);
}

.setup-step {
    display: flex;
    gap: var(--sp-3);
    padding: var(--sp-4);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
}

.setup-step h2 { font-size: var(--fs-lg); margin: 0 0 var(--sp-1); }
.setup-step p { margin: 0 0 var(--sp-3); }

/* The tick is decorative; completion is also conveyed by the "is-done" text
   colour and by the disabled publish button, never by colour alone. */
.setup-step__mark {
    flex: 0 0 auto;
    width: 1.5rem;
    height: 1.5rem;
    margin-top: 0.15rem;
    border-radius: 50%;
    border: 2px solid var(--c-border-strong);
}

.setup-step.is-done .setup-step__mark {
    background: var(--c-success);
    border-color: var(--c-success);
    position: relative;
}

.setup-step.is-done .setup-step__mark::after {
    content: "✓";
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--c-success-ink);
    font-size: var(--fs-sm);
    line-height: 1;
}

.setup-step.is-done h2 { color: var(--c-text-muted); }

/* Lists and tables -------------------------------------------------------- */

.card-list {
    list-style: none;
    margin: 0 0 var(--sp-5);
    padding: 0;
    display: grid;
    gap: var(--sp-3);
}

.card--row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
    align-items: center;
}

.card--row__body { flex: 1 1 12rem; }
.card--row__body p { margin: var(--sp-1) 0 0; }

.avatar {
    width: 3.5rem;
    height: 3.5rem;
    border-radius: 50%;
    object-fit: cover;
    background: var(--c-surface-alt);
}

/* Stands in for a photograph nobody uploaded, at exactly the photograph's size, so a team
   list does not go ragged the moment one person has no picture. */
.avatar--initial {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    color: var(--c-text-muted);
    font-weight: 600;
    font-size: 1.25rem;
    line-height: 1;
}

/* The public team reads as people, not as rows to act on: no buttons, so the card can be
   quieter than the salon-side list it borrows its shape from. */
.team-list .card { align-items: flex-start; }
.team-list .card--row__body strong { display: block; }

/* Three people on a wide screen were three full-width bands holding a face and one line of
   text each. Beyond phone width the team reads as a row of people rather than a stack of
   near-empty rows; below it, one column, because a face and a sentence do not fit beside
   another face and another sentence at 390 px. */
@media (min-width: 40rem) {
    .team-list { grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }
}

.row-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
}

.row-actions form { margin: 0; }

.badge--muted {
    background: var(--c-surface-alt);
    color: var(--c-text-muted);
}

/* Forms ------------------------------------------------------------------- */

.check {
    display: flex;
    gap: var(--sp-3);
    align-items: center;
    min-height: var(--tap);
    font-size: var(--fs-sm);
}

.check input[type="checkbox"], .check input[type="radio"] {
    width: 1.15rem;
    height: 1.15rem;
    flex: 0 0 auto;
    accent-color: var(--c-brand);
}

/* A detail beside the name in a list of checkboxes - the length of a service, say. Its own
   element rather than part of the label text, because the label is what gets announced when
   the box is reached and the number is context, not part of the name. */
.check__meta {
    margin-left: auto;
    color: var(--c-text-muted);
    font-variant-numeric: tabular-nums;
}

fieldset {
    border: 1px solid var(--c-border);
    border-radius: var(--radius-sm);
    padding: var(--sp-3);
    margin: 0;
}

legend {
    padding-inline: var(--sp-2);
    font-size: var(--fs-sm);
    font-weight: 600;
    color: var(--c-text-muted);
}

/* A start/end pair. Time inputs stay side by side even on the narrowest phone,
   because a wrapped range reads as two unrelated fields. */
.time-row {
    display: flex;
    gap: var(--sp-2);
    align-items: center;
    margin-bottom: var(--sp-2);
}

.time-row input[type="time"] {
    flex: 1 1 0;
    min-width: 0;
    min-height: var(--tap);
}

@media (min-width: 40rem) {
    .time-row input[type="time"] { flex: 0 0 8rem; }
}

/* A date beside a time, or a choice beside a count. Side by side where there is room; on a phone
   they stack, because a date field alone takes most of the width and the pair ran out of the
   card. The stack of the form spaces what comes after the pair, as it did for a time row. */
.field-pair { display: grid; gap: var(--sp-4) var(--sp-2); margin-bottom: var(--sp-2); }
.field-pair > .field { margin-bottom: 0; min-width: 0; }

@media (min-width: 30rem) {
    .field-pair { grid-template-columns: minmax(0, 1fr) 9rem; }
}

/* DANAS - the salon's day ------------------------------------------------- */

/* The date stepper. Big targets, because this is tapped with wet hands between
   two clients, and the current date must never leave the screen. */
.day-nav {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}

.day-nav__step {
    flex: 0 0 auto;
    min-width: var(--tap);
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius);
    background: var(--c-surface);
    color: var(--c-text);
    text-decoration: none;
    font-size: var(--fs-lg);
    font-weight: 700;
}

.day-nav__step:hover { background: var(--c-surface-alt); color: var(--c-text); }

.day-nav__label {
    flex: 1 1 auto;
    text-align: center;
    min-width: 0;
}

.day-nav__date { display: block; font-weight: 700; font-size: var(--fs-lg); }
.day-nav__weekday { display: block; font-size: var(--fs-xs); color: var(--c-text-muted); text-transform: uppercase; letter-spacing: 0.04em; }

.day-summary {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2) var(--sp-4);
    margin-bottom: var(--sp-4);
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.day-summary strong { color: var(--c-text); }

.employee-day + .employee-day { margin-top: var(--sp-5); }

.employee-day__head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-3);
    margin-bottom: var(--sp-2);
}

.employee-day__head h2 { font-size: var(--fs-lg); margin: 0; }

/* One column, in the order the day happens. No grid, no overlap maths - a phone
   held at the reception desk reads a list far faster than a calendar. */
.timeline {
    list-style: none;
    margin: 0;
    padding: 0;
    border-radius: var(--radius);
    overflow: hidden;
    border: 1px solid var(--c-border);
    background: var(--c-surface);
}

.timeline__entry {
    display: flex;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--c-border);
}

.timeline__entry:first-child { border-top: 0; }

.timeline__time {
    flex: 0 0 4.25rem;
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    line-height: 1.35;
}

.timeline__time small { display: block; font-weight: 400; font-size: var(--fs-xs); color: var(--c-text-muted); }

.timeline__body { flex: 1 1 auto; min-width: 0; }
.timeline__body p { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--c-text-muted); }
.timeline__title { font-weight: 650; }

/* An empty stretch is not blank space - it is the thing worth selling, so it
   gets the accent and its own call to action. */
.timeline__entry--free {
    background: repeating-linear-gradient(
        135deg,
        var(--c-surface) 0 10px,
        var(--c-surface-alt) 10px 20px);
}

.timeline__entry--free .timeline__time { color: var(--c-text-muted); }

.timeline__fits {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    margin-top: var(--sp-2);
}

.timeline__entry--past { opacity: 0.6; }

/* ZAVRSI / NIJE DOSLA / PROMIJENI, and nothing else. */
.timeline__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-top: var(--sp-3);
}

.timeline__actions form { margin: 0; }

/* Full --tap here, not --tap-sm. These three are pressed dozens of times a day by
   somebody holding scissors, and pressing NIJE DOSLA when you meant ZAVRSI is a
   mistake the customer finds out about. Same reasoning as .salon-card__times. */
.timeline__actions .btn {
    flex: 1 1 7rem;
    min-height: var(--tap);
    font-size: var(--fs-sm);
    padding-inline: var(--sp-2);
}

.timeline__actions form .btn { width: 100%; }

/* The single most-used control in the product. It stays reachable with a thumb
   while the list scrolls behind it. */
.add-booking {
    position: sticky;
    bottom: var(--sp-3);
    z-index: 20;
    margin-top: var(--sp-5);
}

.add-booking .btn { box-shadow: var(--shadow); }

/* Both rules above are sized for a thumb on a phone, and on a phone they are right.
   In the workspace the same panel is over a thousand pixels wide, and there they stop
   being controls: a lone SLJEDECI PUT? grows across the whole row and reads as a banner,
   and the sticky + Termin becomes a wall. So on a wide screen they stop growing and take
   the width of their own label instead, with a minimum that keeps them easy to hit. On a
   phone nothing changes: there they still stretch and share the row. */
@media (min-width: 64rem) {
    .timeline__actions .btn { flex: 0 1 auto; min-width: 8rem; }
    .add-booking { display: flex; justify-content: center; }
    .add-booking .btn--block { width: auto; min-width: 18rem; }
}

/* Week grid ---------------------------------------------------------------- */

.week-grid { display: grid; gap: var(--sp-3); }

.week-day {
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    background: var(--c-surface);
    padding: var(--sp-3);
}

.week-day.is-today { border-color: var(--c-brand); box-shadow: inset 0 0 0 1px var(--c-brand); }

.week-day__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-bottom: var(--sp-2);
    font-size: var(--fs-sm);
}

.week-day__date { font-weight: 700; }

.week-day__list { list-style: none; margin: 0; padding: 0; font-size: var(--fs-sm); }
.week-day__list li { display: flex; gap: var(--sp-2); padding-block: var(--sp-1); }
.week-day__list li + li { border-top: 1px solid var(--c-border); }
.week-day__list time { font-variant-numeric: tabular-nums; font-weight: 650; flex: 0 0 2.75rem; }
.week-day__list span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (min-width: 60rem) {
    .week-grid { grid-template-columns: repeat(7, 1fr); }
    .week-day__list span { white-space: normal; }
}

/* A field sitting inside a row of controls rather than in a stacked form. */
.field--inline { margin-bottom: 0; flex: 1 1 14rem; min-width: 0; }

/* Slot-first results ------------------------------------------------------ */

/* The filter bar above the list. It stays put while the list swaps, so changing
   a filter never scrolls the page out from under the reader. */
.filters {
    display: grid;
    gap: var(--sp-3);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    padding: var(--sp-4);
    margin-bottom: var(--sp-4);
}

.filters .field { margin-bottom: 0; }
.filters__row { display: grid; gap: var(--sp-3); }
.filters__actions { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }

/* Grid items, and a grid item is born with `min-width: auto` - its content's own minimum.
   For `input type=date` and `input type=time` that minimum is whatever the browser's date
   and time widget measures, and on iOS it is wide: wide enough that the track refuses to
   shrink to the phone, the panel grows past the screen, and the sheet scrolls sideways with
   the submit button off the edge. Seen on a phone, not in the pane here - Chromium draws
   those two fields narrow enough to hide the whole thing. The same line stands on
   `.field-pair > .field` further up, put there for the same reason. */
.filters__row > * { min-width: 0; }

/* On a phone the date takes a line of its own and the two times share the next one. A single
   column - what this row gets by default below the breakpoint - stacks all three full width,
   and those three lines are what push the submit button past the bottom of an 85vh sheet. */
.filters__row--when { grid-template-columns: 1fr 1fr; }
.filters__row--when > :first-child { grid-column: 1 / -1; }

@media (min-width: 40rem) {
    .filters__row { grid-template-columns: 2fr 1fr 1fr; }
    .filters__row--when { grid-template-columns: 1fr 1fr 1fr; }
    .filters__row--when > :first-child { grid-column: auto; }
}

/* What to type, offered while it is being typed.

   Drawn over the page rather than in it: pushing the results down half a screen every
   time somebody adds a letter moves the thing they were reading out from under them.
   The wrapper is the positioning context and nothing else, so the field keeps whatever
   layout the page around it already gave it.

   With scripting off the list is never fetched and this draws nothing at all - an empty
   box has no border, because the fragment renders no <ul> when there is no match. */
.typeahead-field { position: relative; }

.typeahead {
    list-style: none;
    margin: var(--sp-1) 0 0;
    padding: var(--sp-1);
    position: absolute;
    left: 0;
    right: 0;
    /* Above the filter bar and the results, below the filter sheet and its scrim. */
    z-index: 30;
    background: var(--c-surface);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    /* Six rows at thumb size is most of a phone once the keyboard is up. Scrolling the
       list is better than a list that runs off the bottom with no way to reach the rest. */
    max-height: min(60vh, 22rem);
    overflow-y: auto;
}

.typeahead a {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-1) var(--sp-2);
    /* A row is pressed with a thumb on a phone, so it is sized like something pressed
       with a thumb rather than like a line of text. */
    min-height: 2.75rem;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    color: var(--c-text);
    text-decoration: none;
}

.typeahead a:hover,
.typeahead a:focus-visible { background: var(--c-brand-light); }

.typeahead__label { font-weight: 600; }

/* The kind, because the same word is often a service in one salon and the name of
   another - and pressing it does something different in each case. */
.typeahead__kind { color: var(--c-text-muted); font-size: var(--fs-2xs); }

.results-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
    margin-bottom: var(--sp-3);
}

.results-count { font-weight: 650; }

/* One row = one real appointment. The time leads, because the time is the product;
   the salon is metadata on it. */
.slot-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); }

.slot {
    display: flex;
    gap: var(--sp-3);
    align-items: center;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    /* Roomier than the rest of the sheet on purpose. This row is the product; on a phone it is
       also a tap target somebody is reaching for one-handed, in a hurry, deciding by eye. */
    padding: var(--sp-4);
    text-decoration: none;
    color: inherit;
    min-height: var(--tap);
}

.slot:hover { border-color: var(--c-brand); background: var(--c-brand-light); color: inherit; }

.slot__when {
    flex: 0 0 4.75rem;
    font-variant-numeric: tabular-nums;
    line-height: 1.15;
}

/* The single largest thing on the row. The whole product is "kad", and a time set in the same
   size as the salon's address makes the reader hunt for the one number they came for. */
.slot__time {
    display: block;
    font-size: var(--fs-2xl);
    font-weight: 700;
    letter-spacing: -0.03em;
}

/* Was set in spaced capitals, which is a typographic tic that reads as a system label rather
   than as a date. Sentence case, in the language the reader already speaks. */
.slot__day  { display: block; font-size: var(--fs-xs); color: var(--c-text-muted); text-transform: capitalize; }

.slot__body { flex: 1 1 auto; min-width: 0; }
.slot__salon { display: block; font-weight: 650; }
.slot__service { display: block; font-size: var(--fs-sm); }
.slot__where { display: block; font-size: var(--fs-xs); color: var(--c-text-muted); }

/* The second number anybody reads, and on a phone often the only other one. It was set at
   running-text size, which made a column of prices something you had to look for. */
.slot__price {
    flex: 0 0 auto;
    text-align: right;
    font-size: var(--fs-xl);
    font-weight: 700;
    letter-spacing: -0.02em;
}

.slot__price .strike { display: block; font-size: var(--fs-xs); font-weight: 400; letter-spacing: 0; }
/* A row whose price has dropped says so in the discount colour, on both the number and the
   border, so it is recognisable without reading a word of it. */
.slot--last-minute { border-color: var(--c-accent); }
.slot--last-minute .slot__price { color: var(--c-accent); }

/* On a narrow phone the four columns leave the salon name about 130px, and it wraps to four
   lines. Below 30rem the row folds instead: mark, time and price on the top line - the three
   things being scanned - and everything that identifies the salon on a full-width line under
   them. Same markup, same reading order for a screen reader. */
@media (max-width: 30rem) {
    .slot { flex-wrap: wrap; row-gap: var(--sp-2); }
    .slot__when { flex: 0 0 auto; }
    .slot__price { margin-left: auto; }
    .slot__body { flex: 1 1 100%; order: 3; }
}

/* ---------------------------------------------------------------------------
   A salon in the result list
   --------------------------------------------------------------------------- */

/* One card per salon, and on it the times that salon has free. The flat row above still exists
   and is what the last-minute page uses, where every row genuinely is a separate freed slot. */
.salon-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-4); }

.salon-card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    padding: var(--sp-4);
    box-shadow: var(--shadow-sm);
}
.salon-card:focus-within { border-color: var(--c-brand); }

/* The header is a single link to the salon. Its own hover is a colour on the name rather than a
   fill on the whole block, because the times sitting underneath are separate links and a card
   that lights up as one thing would be lying about how many targets are in it. */
.salon-card__head {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    text-decoration: none;
    color: inherit;
}
.salon-card__head:hover .salon-card__name { color: var(--c-brand-dark); }

.salon-card__logo {
    flex: 0 0 3.5rem;
    width: 3.5rem;
    height: 3.5rem;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--c-surface-alt);
}
.salon-card__logo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.salon-card__logo--letter {
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    font-weight: 700;
    color: var(--c-brand-dark);
    background: var(--c-brand-light);
}

.salon-card__id { flex: 1 1 auto; min-width: 0; }
.salon-card__name {
    display: block;
    margin: 0;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--fs-lg);
    letter-spacing: -0.02em;
}
.salon-card__meta { display: block; font-size: var(--fs-sm); color: var(--c-text-muted); }

.salon-card__price {
    flex: 0 0 auto;
    text-align: right;
    font-family: var(--font-display);
    font-weight: 700;
    font-size: var(--fs-lg);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}
.salon-card__from {
    display: block;
    font-family: var(--font-sans);
    font-weight: 400;
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    letter-spacing: 0;
}

/* A row rather than a line, because the heart sits at the end of it. The service name is the
   content and the heart is the furniture, so the name takes the space that is going. */
.salon-card__service {
    margin: var(--sp-3) 0 0;
    font-size: var(--fs-sm);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
}
.salon-card__service-name { min-width: 0; }

.salon-card__times {
    list-style: none;
    margin: var(--sp-3) 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
}
/* A time here is the primary target on the whole screen, so it is a full tap target rather than
   the small chip the home rail uses. */
.salon-card__times .time-chip { min-height: var(--tap); padding-inline: var(--sp-4); font-size: var(--fs-base); }
.salon-card__times .time-chip:hover { border-color: var(--c-brand); background: var(--c-brand-light); color: var(--c-brand-dark); }
.time-chip--deal { border-color: var(--c-accent); color: var(--c-accent); }
/* Same pair as .badge--accent, same reason: hovering must not be what drops a discounted
   time below contrast. */
.time-chip--deal:hover { border-color: var(--c-accent); background: var(--c-accent-light); color: var(--c-accent-dark); }

/* On a narrow phone six full-width chips wrap to three rows and the card grows past a third of
   the screen, so only two salons are ever visible at once. Tightening the chip - not shortening
   the row - fits four per line while keeping the 48px tap target intact. */
@media (max-width: 30rem) {
    .salon-card__times .time-chip { padding-inline: var(--sp-3); font-size: var(--fs-sm); }
}

.salon-card__day {
    margin: var(--sp-3) 0 0;
    font-size: var(--fs-xs);
    color: var(--c-text-muted);
    text-transform: capitalize;
}

/* --- The listing pages -------------------------------------------------------
   A price list, not a result list. Everything else public here is a row you press; this is a
   row you read and compare with the one under it, so the three columns line up down the card
   and the numbers are tabular - a column of prices that does not align is a column of prices
   nobody compares. */
.price-list__caption {
    margin: var(--sp-4) 0 var(--sp-2);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--c-border);
    font-family: var(--font-sans);
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}
.price-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    /* The name takes what is going; the other two are as wide as their content and no wider. */
    grid-template-columns: 1fr auto auto;
    column-gap: var(--sp-3);
    align-items: baseline;
    font-size: var(--fs-sm);
}
.price-list li {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    padding: var(--sp-2) 0;
}
.price-list li + li { border-top: 1px solid var(--c-border); }
.price-list__minutes { color: var(--c-text-muted); font-size: var(--fs-xs); }
.price-list__price { font-weight: 600; font-variant-numeric: tabular-nums; }

/* On a phone the duration is the first thing worth giving up: the name needs the width and the
   price is the reason the row is there. It stays in the markup, so it is still read aloud. */
@media (max-width: 26rem) {
    .price-list { grid-template-columns: 1fr auto; }
    .price-list__minutes { display: none; }
}

/* Both rankings on the analytics screen - the services, and the people under them. Four columns
   that the whole list shares, because the question an owner asks of these rows is comparative:
   not "what did this service take" but "which of them took more". Figures that do not start at
   the same x cannot be compared by eye, so the list owns the columns and each row inherits them
   as a subgrid, the same way the public price list two blocks up does.

   No marker on the ol: the rows are already in order and each one says how many, so a printed
   position would be a third way of saying the same thing. The ol still carries the ordering
   itself, which is what a screen reader reads. */
.rank-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    /* The name takes what is going; the three figures are as wide as their content and no wider. */
    grid-template-columns: 1fr auto auto auto;
    column-gap: var(--sp-3);
}
.rank {
    display: grid;
    grid-column: 1 / -1;
    grid-template-columns: subgrid;
    /* Four things set at three sizes, so they sit on one baseline rather than four tops. */
    align-items: baseline;
    padding: var(--sp-2) 0;
}
.rank + .rank { border-top: 1px solid var(--c-border); }

/* A long Croatian service name is the normal case here, not the exception, so the name wraps
   inside its column rather than pushing the figures off the end of the row. */
.rank__name { min-width: 0; overflow-wrap: anywhere; }
.rank__count,
.rank__share { color: var(--c-text-muted); font-size: var(--fs-sm); }
.rank__value { font-weight: 650; }
/* Right-aligned and tabular, which is what lets a column of figures be read as a column. */
.rank__count,
.rank__value,
.rank__share { text-align: right; font-variant-numeric: tabular-nums; }

/* Takings day by day, on the same screen and just under the total they add up to.

   The bars are the markup and there is no script behind them. The viewBox is one unit per day by a
   hundred tall and preserveAspectRatio is none, so one block of markup draws a week of wide bars
   and a year of hairlines with nothing computed twice. That uneven stretch is also why nothing in
   here is stroked or rounded: a shape scaled hard on one axis comes out with an oval corner and a
   line thicker at the top than down the side. */
.trend {
    display: block;
    width: 100%;
    height: 5rem;
    /* The baseline belongs to the box rather than to the drawing, for the reason just above: a line
       inside a viewBox stretched like this is a line of two different thicknesses. It matters more
       than it looks - without it a run of closed days has no floor to be measured against. */
    border-bottom: 1px solid var(--c-border);
    margin: var(--sp-3) 0;
}

/* Every day in the one brand colour, and the best of them at full strength. The chart carries a
   single measurement, so nothing in it changes hue; what changes is how much of the surface shows
   through, which is all the eye needs to tell "a day" from "the day". The semantic greens and
   ambers in this file mean good and bad, and a takings chart has no bad days to mark. */
.trend__bar { fill: var(--c-brand); fill-opacity: 0.45; }
.trend__bar.is-best { fill-opacity: 1; }

/* The links out to the neighbouring listings. Spaced away from the salons so the page reads as
   ending and then offering somewhere else to go, rather than as a list that trails off. */
.directory-links { margin-top: var(--sp-6); }
.directory-links h2 {
    font-size: var(--fs-base);
    margin: 0 0 var(--sp-3);
}

/* The salon mark on a result row. Fixed square either way, so rows line up whether or not
   a salon has uploaded anything - a list that jitters between rows reads as broken. */
.slot__logo {
    flex: 0 0 3.5rem;
    width: 3.5rem;
    height: 3.5rem;
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--c-surface-alt);
}

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

.slot__logo--letter {
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    font-weight: 700;
    color: var(--c-brand-dark);
    background: var(--c-brand-light);
}


/* Morning, afternoon, evening. The heading is quiet on purpose - it is a
   signpost through the times, not a section of the page competing with them. */
.time-part + .time-part { margin-top: var(--sp-4); }
.time-part__label {
    font-family: var(--font-sans);
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--c-text-muted);
    letter-spacing: normal;
    margin: 0 0 var(--sp-2);
}

/* A day of times on a salon's own page: a wall of taps, nothing else. */
.time-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
    gap: var(--sp-2);
}

/* A form is not a box here - the button it wraps is the grid cell. */
.time-grid form { display: contents; }

.time-grid a,
.time-grid button {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: var(--tap);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius-sm);
    background: var(--c-surface);
    color: var(--c-text);
    font-variant-numeric: tabular-nums;
    font-weight: 650;
    text-decoration: none;
}

.time-grid button { font-family: inherit; font-size: inherit; cursor: pointer; }

.time-grid a:hover,
.time-grid button:hover { background: var(--c-brand); border-color: var(--c-brand); color: var(--c-brand-ink); }

.service-picker { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-4); }

.service-picker a {
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius-pill);
    background: var(--c-surface);
    color: var(--c-text);
    text-decoration: none;
    font-size: var(--fs-sm);
    min-height: 2.25rem;
    display: inline-flex;
    align-items: center;
}

.service-picker a.is-active { background: var(--c-brand); border-color: var(--c-brand); color: var(--c-brand-ink); }

/* The receipt. Deliberately plain: nothing was paid, and nothing should look like it was. */
.summary-list { margin: 0; }
.summary-list div { display: flex; justify-content: space-between; gap: var(--sp-3); padding-block: var(--sp-2); }
.summary-list div + div { border-top: 1px solid var(--c-border); }
.summary-list dt { color: var(--c-text-muted); font-size: var(--fs-sm); }
.summary-list dd { margin: 0; font-weight: 650; text-align: right; }

/* Opening hours are the one entry in a summary list with more than one value, so they opt out of
   the two-column row the others share and give each weekday a line of its own. */
.summary-list div.hours { display: block; }
.summary-list div.hours dt { margin-bottom: var(--sp-2); }
.summary-list .hours-row {
    display: flex;
    justify-content: space-between;
    gap: var(--sp-3);
    padding-block: 2px;
    font-weight: 500;
    text-align: left;
}
.summary-list .hours-row span + span { font-weight: 650; font-variant-numeric: tabular-nums; }

.reference {
    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
    font-size: var(--fs-lg);
    letter-spacing: 0.05em;
}

/* Inside the ticket it is a row like any other, not the headline it is when it
   stands alone - the time is the headline there. */
.summary-list .reference {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--c-text-muted);
}

/* ---------------------------------------------------------------------------
   Last minute, smart gaps and standing requests.

   The gaps panel is one number and one button. Everything else on it is
   evidence for that number, so nothing else is allowed to compete with it.
   --------------------------------------------------------------------------- */
.panel,
.panel--headline {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-lg);
    padding: var(--sp-4);
    margin-bottom: var(--sp-5);
}

/* A plain stacked list. Used where the items are sentences rather than records, so it carries
   no bullets and no card of its own. */
.list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--sp-2);
}

.list--checks li {
    position: relative;
    padding-left: 1.6em;
}

.list--checks li::before {
    content: "¹3";
    position: absolute;
    left: 0;
    color: var(--c-success);
    font-weight: 700;
}

.headline {
    font-size: var(--fs-xl);
    font-weight: 700;
    line-height: 1.25;
    margin: 0 0 var(--sp-1);
}

.publish-form {
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
    margin-top: var(--sp-4);
}

.price-choice {
    border: 0;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-3);
}

.price-choice legend {
    padding: 0;
    margin-bottom: var(--sp-2);
    font-size: var(--fs-sm);
    color: var(--c-text-muted);
}

.price-choice label {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    white-space: nowrap;
}

/* Offers and gaps read as the same kind of row, because they are the same
   stretch of time before and after the salon decided to sell it. */
.offer-list {
    list-style: none;
    margin: 0 0 var(--sp-5);
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}

.offer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2) var(--sp-3);
    padding: var(--sp-3);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    /* --radius-md has never existed, so this declaration was dropped as invalid and every
       offer row on the gaps and messages screens rendered square. Same shape as a
       marketplace .slot - an independently bordered row in a gapped list - so it takes
       the same radius. */
    border-radius: var(--radius-card);
}

.offer__when {
    display: flex;
    flex-direction: column;
    min-width: 9.5rem;
}

.offer__who {
    color: var(--c-text-muted);
}

.offer__fits,
.offer__price {
    margin-left: auto;
}

.offer form {
    margin: 0;
}

/* A discount is the only thing on a slot row allowed to shout, so it is the one
   badge that inverts rather than tinting. */
.badge--deal {
    background: var(--c-accent);
    color: var(--c-surface);
    border-color: transparent;
}

.strike {
    text-decoration: line-through;
    color: var(--c-text-muted);
    margin-right: var(--sp-1);
    font-weight: 400;
}

.section-title {
    font-size: var(--fs-lg);
    margin: var(--sp-5) 0 var(--sp-2);
}

/* The confirmation ticket.
   The dashed rule is the only place in the product that borrows a physical
   metaphor, and it earns it: this screen is opened again later, on a phone,
   outside the salon door, to check one thing - the time. So the time is the
   largest object on the page and everything else sits under it. */
.ticket {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-lg);
    padding: var(--sp-5);
    margin-bottom: var(--sp-4);
}
.ticket__stub {
    text-align: center;
    padding-bottom: var(--sp-4);
    border-bottom: 1px dashed var(--c-border);
    margin-bottom: var(--sp-4);
}
.ticket__day {
    margin: 0;
    font-size: var(--fs-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--c-brand-dark);
}
.ticket__time {
    margin: var(--sp-1) 0 0;
    font-family: var(--font-display);
    font-weight: 600;
    font-size: var(--fs-4xl);
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}
.ticket__span { margin: 0; font-size: var(--fs-sm); color: var(--c-text-muted); }
.ticket .summary-list { margin: 0; }

/* ---------------------------------------------------------------------------
   Booking funnel: where am I, and what have I already chosen
   --------------------------------------------------------------------------- */

/* Five dots joined by a line. The line is drawn by the steps themselves rather than
   by a track behind them, so the "done" part of it is coloured for free and there is
   nothing to keep in sync with the step count. */
.steps {
    display: flex;
    list-style: none;
    margin: 0 0 var(--sp-5);
    padding: 0;
    counter-reset: step;
}

.steps__step {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--sp-2);
    position: relative;
    min-width: 0;
}

/* The connector belongs to the step on its right, which is what makes it colour in
   as the customer advances: a step that is done paints the line reaching back to the
   one before it. */
.steps__step + .steps__step::before {
    content: "";
    position: absolute;
    top: 0.6875rem;                       /* half the dot, so it meets the middle */
    right: 50%;
    left: -50%;
    height: 2px;
    background: var(--c-border);
}

.steps__step.is-done::before,
.steps__step.is-current::before {
    background: var(--c-brand);
}

.steps__dot {
    position: relative;                   /* above the connector */
    z-index: 1;
    display: grid;
    place-items: center;
    width: 1.375rem;
    height: 1.375rem;
    border-radius: var(--radius-pill);
    border: 2px solid var(--c-border);
    background: var(--c-surface);
    font-family: var(--font-display);
    font-size: var(--fs-2xs);
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
    line-height: 1;
}

.steps__step.is-current .steps__dot {
    border-color: var(--c-brand);
    background: var(--c-brand);
    color: var(--c-brand-ink);
}

/* A finished step shows a tick instead of its number: the number was only ever there
   to say how far along it sits, and once it is behind you that is no longer the news. */
.steps__step.is-done .steps__dot {
    border-color: var(--c-brand);
    background: var(--c-brand);
    color: var(--c-brand-ink);
    font-size: 0;
}

.steps__step.is-done .steps__dot::after {
    content: "\2713";
    font-size: 0.6875rem;
}

.steps__name {
    font-size: var(--fs-2xs);
    color: var(--c-text-muted);
    text-align: center;
    line-height: 1.2;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.steps__step.is-current .steps__name {
    color: var(--c-text);
    font-weight: 600;
}

/* Under about 380px five names do not fit no matter how they are set, so only the
   one being answered keeps its label. The dots still carry the position.

   Four or more, not every rail. This was written for the booking funnel and applied
   to anything with the class, so the three-step rail on the registration form lost
   two of its three labels on a phone - and that rail exists precisely to tell a salon
   what the two steps after this one are. `:nth-last-child(n+4)` matches an item only
   when three more follow it, so it selects nothing at all on a rail of three; the
   sibling combinator then carries the rule to the rest of a longer one. */
@media (max-width: 24rem) {
    .steps__step:nth-last-child(n+4):not(.is-current) .steps__name,
    .steps__step:nth-last-child(n+4) ~ .steps__step:not(.is-current) .steps__name {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

/* ---------------------------------------------------------------------------
   Mobile tab bar
   --------------------------------------------------------------------------- */

/* Hidden entirely on anything wider than a phone: the header already carries these
   links there, and two navigations competing for the same taps is worse than one. */
.tabbar { display: none; }

@media (max-width: 47.9375rem) {
    .tabbar {
        position: fixed;
        inset: auto 0 0 0;
        z-index: 40;
        display: flex;
        border-top: 1px solid var(--c-border);
        background: var(--c-surface);
        /* The home indicator on a notched phone sits over the bottom of the viewport, so the
           bar has to end above it rather than under it. */
        padding-bottom: env(safe-area-inset-bottom, 0);
    }

    .tabbar a {
        flex: 1;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 3px;
        min-height: var(--tap);
        padding: var(--sp-2) var(--sp-1);
        font-size: var(--fs-2xs);
        font-weight: 600;
        color: var(--c-text-muted);
        text-decoration: none;
    }

    .tabbar a:active { background: var(--c-surface-alt); }

    /* Drawn icons, stroked in the same colour as the label under them, so the
       pair reads as one thing that lights up together. */
    .tabbar__icon {
        width: 24px;
        height: 24px;
        fill: none;
        stroke: currentColor;
        stroke-width: 1.7;
        stroke-linecap: round;
        stroke-linejoin: round;
    }

    /* Room for the bar to sit over, so the last thing on every page stays reachable.
       Applied to the footer rather than to body, because a fixed element that overlaps
       a page's final button is a button nobody can press. */
    .site-footer { padding-bottom: calc(var(--tap) + var(--sp-5) + env(safe-area-inset-bottom, 0)); }
}

/* ---------------------------------------------------------------------------
   A booking in a list
   --------------------------------------------------------------------------- */

/* Date rail on the left, everything else on the right. The rail is fixed width so the
   service names line up down the page and the list can be read as a column rather than
   as a stack of unrelated cards. */
.booking-row {
    display: grid;
    grid-template-columns: 3.75rem 1fr;
    gap: var(--sp-4);
    align-items: start;
}

.booking-row__when {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding-top: 2px;
    border-right: 1px solid var(--c-border);
    margin-right: calc(var(--sp-4) * -0.5);
    padding-right: var(--sp-3);
}

.booking-row__date {
    font-size: var(--fs-2xs);
    color: var(--c-text-muted);
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

.booking-row__time {
    font-family: var(--font-display);
    font-size: var(--fs-xl);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--c-brand);
}

.booking-row__body { min-width: 0; }

/* A past appointment is still worth reading, but it is not the thing on this page that
   needs a decision - so its time stops competing with the upcoming ones for attention. */
.booking-row--past .booking-row__time { color: var(--c-text-muted); }

/* ---------------------------------------------------------------------------
   Filters as a bottom sheet on a phone
   --------------------------------------------------------------------------- */

/* The toggle, the scrim and the two buttons that dismiss belong to a sheet, and a sheet needs
   scripting to open. None of the four is displayed until `.js` says the page can honour them,
   which is also what makes the no-JS fallback correct rather than merely tolerable: without it
   `.filters` is an ordinary form in flow. */
.filters-toggle,
.filters-scrim,
.filters__close,
.filters__done { display: none; }

/* While a sheet is open the page behind it must not scroll - on a phone that is the whole
   difference between a sheet and a box floating over a moving page. */
body.sheet-open { overflow: hidden; }

/* Everything below only applies where scripting is running. With it off, `.filters` keeps
   its ordinary in-flow layout and there is no sheet to open or close. */
@media (max-width: 47.9375rem) {
    .js .filters-toggle {
        display: block;
        width: 100%;
        min-height: var(--tap);
        margin-bottom: var(--sp-4);
        padding: 0 var(--sp-4);
        border: 1px solid var(--c-border-strong);
        border-radius: var(--radius);
        background: var(--c-surface);
        font: inherit;
        font-weight: 600;
        color: var(--c-text);
        text-align: left;
        cursor: pointer;
    }

    /* Displayed only with the sheet: the scrim is what the open state is made of, not a
       permanently present layer that happens to be transparent. */
    .js .filters-wrap.is-open .filters-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 50;
        background: rgba(34, 40, 43, 0.45);
    }

    .js .filters {
        position: fixed;
        inset: auto 0 0 0;
        /* Above the tab bar and above the scrim: the sheet is the thing being answered. */
        z-index: 60;
        max-height: 85vh;
        overflow-y: auto;
        margin: 0;
        padding: var(--sp-5) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom, 0));
        border-radius: var(--radius-lg) var(--radius-lg) 0 0;
        background: var(--c-surface);
        box-shadow: var(--shadow-lg);
        transform: translateY(100%);
        visibility: hidden;
        /* Visibility is not transitioned at all. It used to be eased out with a delay -
           `visibility 0s linear 180ms` - so the panel could be seen sliding away, and the
           delayed flip never came: once opened, the sheet computed to `visible` for the rest
           of the page's life, off screen, with twelve focusable controls still in the tab
           order. Measured in a browser, not deduced. Only the transform is eased now, and
           app.js additionally marks the closed panel `inert` so that being reachable does not
           depend on how the thing is painted. */
        transition: transform 180ms ease-out;
    }

    .js .filters-wrap.is-open .filters {
        transform: translateY(0);
        visibility: visible;
    }

    /* A grab handle, drawn rather than marked up: it says "this pulls down" and there is
       nothing for assistive technology to read in it. */
    .js .filters::before {
        content: "";
        display: block;
        width: 2.5rem;
        height: 4px;
        margin: 0 auto var(--sp-4);
        border-radius: var(--radius-pill);
        background: var(--c-border-strong);
    }

    /* The way out, in the corner a sheet keeps it in. The scrim closes this one too, and so
       does the button at the end, but a panel filled to 85vh leaves barely a strip of scrim
       showing and puts that button below the fold - so on the tallest sheets, which are the
       ones hardest to escape, neither is on screen. Lifted up beside the grab handle and out
       towards the panel's edge so the header costs one row rather than two. */
    .js .filters__close {
        display: flex;
        align-items: center;
        justify-content: center;
        /* An item of the `.filters` grid, so this is what puts it right rather than a float
           or an auto margin that the next change to the panel's padding would quietly undo. */
        justify-self: end;
        width: var(--tap);
        height: var(--tap);
        margin: calc(var(--sp-3) * -1) calc(var(--sp-2) * -1) 0 0;
        padding: 0;
        border: 0;
        border-radius: var(--radius-pill);
        background: var(--c-surface-alt);
        color: var(--c-text-muted);
        cursor: pointer;
    }

    .js .filters__done {
        display: block;
        margin-top: var(--sp-5);
    }

    @media (prefers-reduced-motion: reduce) {
        .js .filters { transition: none; }
    }
}

/* ==========================================================================
   Podijeli - the salon's link and QR code
   ========================================================================== */

.share-grid {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: 1fr;
}

@media (min-width: 48rem) {
    .share-grid { grid-template-columns: minmax(0, 22rem) minmax(0, 1fr); }
}

.share-card { text-align: center; }

/* Square, capped, and on white regardless of theme - a scanner reads contrast, not taste. */
.share-qr {
    display: block;
    width: 100%;
    max-width: 20rem;
    height: auto;
    margin: 0 auto var(--sp-3);
    aspect-ratio: 1;
    background: #fff;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    padding: var(--sp-2);
}

/* The address is meant to be read aloud and typed by hand as well as scanned. */
.share-url {
    font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace;
    font-size: var(--fs-sm);
    word-break: break-all;
    color: var(--c-text);
    user-select: all;
}

/* The referral code itself, which gets read down a phone line more often than it gets copied.
   Large, spaced and selectable in one tap, for the same reason the address above is. */
.share-code {
    font-family: ui-monospace, 'Cascadia Mono', Menlo, monospace;
    font-size: var(--fs-xl);
    letter-spacing: 0.12em;
    font-weight: 600;
    color: var(--c-text);
    user-select: all;
    margin-bottom: var(--sp-1);
}

.share-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    justify-content: center;
    margin-top: var(--sp-3);
}

.share-steps {
    margin: 0;
    padding-left: 1.25rem;
    display: grid;
    gap: var(--sp-2);
}

/* Enhancements that do nothing without scripting are not offered without it. */
.js-only { display: none; }
.js .js-only { display: inline-flex; }

/*
  Printing this page is a supported use: it is how the code gets onto the mirror.
  Everything except the card itself is chrome that wastes ink.
*/
@media print {
    .subnav, .share-actions, .tabbar, .site-header, .site-footer, .alert { display: none; }
    .share-grid { grid-template-columns: 1fr; }
    .share-qr { border: none; max-width: 12cm; }
}

/* An unread message earns a mark on the left edge, not a different colour: the list is read
   top to bottom and a block of tinted cards is harder to read, not easier. */
.card--unread { border-left: 3px solid var(--c-brand); }

.inbox__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--sp-2);
}

/* ==========================================================================
   Legal documents

   These are the only pages on the site that are read as continuous prose
   rather than scanned, so they get the one thing the rest of the product
   deliberately does not: generous line height and space between paragraphs.
   Wider than .narrow because the GDPR rights table needs two real columns.
   ========================================================================== */

.prose {
    max-width: 44rem;
    margin-inline: auto;
    padding-block: var(--sp-5) var(--sp-8);
    line-height: 1.7;
}

.prose h1 { margin-bottom: var(--sp-2); }

.prose h2 {
    margin-top: var(--sp-7);
    margin-bottom: var(--sp-3);
    font-size: var(--fs-xl);
    /* The rule marks where one obligation ends and the next begins - useful when
       somebody is looking for one clause rather than reading the whole thing. */
    padding-top: var(--sp-4);
    border-top: 1px solid var(--c-border);
}

.prose h3 {
    margin-top: var(--sp-5);
    margin-bottom: var(--sp-2);
    font-size: var(--fs-lg);
}

.prose p { margin-bottom: var(--sp-3); }

.prose ul {
    margin-bottom: var(--sp-4);
    padding-left: var(--sp-5);
    list-style: disc;
}

.prose li { margin-bottom: var(--sp-2); }

.prose table {
    width: 100%;
    margin-bottom: var(--sp-4);
    border-collapse: collapse;
    font-size: var(--fs-sm);
}

.prose th,
.prose td {
    padding: var(--sp-2);
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--c-border);
}

.prose th { font-weight: 600; }

.prose code {
    padding: 0 0.25em;
    border-radius: var(--radius-sm);
    background: var(--c-bg);
    font-family: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
    font-size: 0.9em;
}

.prose .alert { margin-bottom: var(--sp-5); }

/* A clause somebody was linked to - the footer's Kolačići goes to one. It stops below the sticky
   header rather than under it, and is marked, because a jump into the middle of a list lands on
   a bullet that looks like every bullet around it. */
.prose [id] { scroll-margin-top: calc(84px + var(--sp-5)); }
.prose li:target {
    background: var(--c-brand-light);
    border-radius: var(--radius-sm);
    box-shadow: 0 0 0 var(--sp-2) var(--c-brand-light);
}


/* ==========================================================================
   Photography

   A beauty appointment is chosen by looking. These are the only rules in the
   sheet that exist purely so a picture is large enough to decide from - the
   warm ground the palette is built on was always meant to sit under them.
   ========================================================================== */

/* Full width of the container, edge to edge on a phone. A cover photograph
   inset by the page gutter looks like an illustration; one that reaches the
   edges looks like the room you are about to walk into. */
.salon-hero {
    margin: 0 calc(-1 * var(--sp-4)) var(--sp-5);
    overflow: hidden;
    background: var(--c-surface-alt);
}

.salon-hero img {
    display: block;
    width: 100%;
    /* Fixed ratio so the page never reflows once the picture arrives. */
    aspect-ratio: 2 / 1;
    object-fit: cover;
}

@media (min-width: 48rem) {
    .salon-hero {
        margin-inline: 0;
        border-radius: var(--radius-card);
    }
    .salon-hero img { aspect-ratio: 3 / 1; }
}

/* Overlaps the cover slightly, the way a profile picture does, so the two read
   as one header rather than as two stacked blocks. */
.salon-logo {
    width: 4.5rem;
    height: 4.5rem;
    border-radius: var(--radius-card);
    object-fit: cover;
    background: var(--c-surface);
    border: 3px solid var(--c-surface);
    box-shadow: 0 2px 8px rgb(34 40 43 / 0.12);
}

.page-head--under-hero {
    justify-content: flex-start;
    gap: var(--sp-4);
    /* Lifted into the cover so the logo sits on the seam. */
    margin-top: calc(-1 * var(--sp-6));
}
.page-head--under-hero .salon-logo { position: relative; }

.salon-gallery {
    list-style: none;
    margin: var(--sp-4) 0 0;
    padding: 0;
    display: grid;
    gap: var(--sp-2);
    grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 40rem) { .salon-gallery { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 64rem) { .salon-gallery { grid-template-columns: repeat(4, 1fr); } }

.salon-gallery img {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    border-radius: var(--radius);
    background: var(--c-surface-alt);
}

/* --- The salon's own side of it ------------------------------------------- */

.photo-preview {
    margin: 0 0 var(--sp-3);
    border-radius: var(--radius-card);
    overflow: hidden;
    background: var(--c-surface-alt);
}

.photo-preview--cover img {
    display: block;
    width: 100%;
    aspect-ratio: 2 / 1;
    object-fit: cover;
}

.photo-preview--logo { width: 6rem; border-radius: var(--radius); }
.photo-preview--logo img { display: block; width: 100%; height: auto; object-fit: cover; }

.photo-grid {
    list-style: none;
    margin: var(--sp-4) 0;
    padding: 0;
    display: grid;
    gap: var(--sp-3);
    grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}

.photo-tile {
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    overflow: hidden;
    background: var(--c-surface);
}

.photo-tile > img {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    background: var(--c-surface-alt);
}

.photo-tile__caption { display: flex; gap: var(--sp-2); padding: var(--sp-2); }
.photo-tile__caption input { flex: 1 1 auto; min-width: 0; }

/* Wrapped rather than squeezed: three small buttons on one line at 360px is
   three targets nobody can hit. */
.photo-tile__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    padding: 0 var(--sp-2) var(--sp-2);
}


/*
  The discount box on the gaps panel appears only when the salon has actually chosen to
  discount. Guarded by @supports so that a browser without :has() keeps today's behaviour -
  the control always visible - rather than losing the ability to set a discount at all.
*/
@supports selector(:has(*)) {
    .publish-form .price-discount { display: none; }
    .publish-form:has(input[value="discount"]:checked) .price-discount { display: block; }
}

/*
  The share page hides every .alert when printed because there it is only chrome.
  On a legal document the banner is the point: a printed copy that quietly drops
  "this is a draft" is worse than no printed copy.
*/
@media print {
    .prose .alert { display: block; }
}

/* --------------------------------------------------------------------------
   The heart: keeping a salon.

   Quiet until it is on. Off, it is the same weight as everything else on the
   card and does not compete with the times, which are what the page is for;
   on, it takes the accent, because at that point it is the only thing on the
   card telling the reader something about themselves.
   -------------------------------------------------------------------------- */
.favourite { display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-1); }
.favourite form { margin: 0; }

.favourite__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    padding: 0;
    border: 0;
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--c-text-muted);
    cursor: pointer;
    transition: color .15s ease, background-color .15s ease;
}
.favourite__btn .icon { width: 22px; height: 22px; }
.favourite__btn:hover { background: var(--c-accent-light); color: var(--c-accent-dark); }
.favourite__btn--on { color: var(--c-accent); }

/* The one refusal this button has. Small, beside the heart, and announced. */
.favourite__note { margin: 0; max-width: 16rem; text-align: right; color: var(--c-danger); }

/* A saved salon's own mark in the rail, where the card is a link and not a heading. */
.rail-card__logo {
    width: 44px;
    height: 44px;
    border-radius: var(--radius);
    object-fit: cover;
    display: block;
}
