/* ============================================================================
   app.css — the look.

   Every colour, size and space comes from a token in brand/theme.css or the
   shop's skin. Nothing here invents a value, which is what keeps a thousand
   lines of CSS looking like one thing rather than forty decisions taken on
   forty different days — and it is also what lets a second shop be a colour
   file rather than a fork of this one.

   That claim used to be aspirational: twenty-odd literals had crept back in,
   most of them in the footer, all of them measured off After Image's own sign.
   A shop with a blue sign inherited beige footer text on blue. The colours are
   now tokens (--band-ink and its ladder, --sign-cream-rgb, --on-accent), and
   bin/contrast.js checks every one of them in both themes, so the next one to
   creep in fails a script rather than shipping quietly.

   THE THREE LITERALS THAT REMAIN ARE DELIBERATE, and none of them is a brand
   colour: rgba(0,0,0,…) in the two text-shadows that sit under text on a
   photograph, rgba(255,255,255,.25) for the specular highlight along the top
   of an avatar disc, and #000 inside a mask-image, which is a mask and not a
   colour at all. Shadow is black and highlight is white in every palette.

   The structure follows the sign: dark bands for the moments that should feel
   like the shopfront — the hero, the footer, the primary buttons — and quiet
   light surfaces for the parts you actually have to read and act on.

   Mobile first throughout. Most of these bookings happen on a phone, held in
   one hand, often standing up.
   ========================================================================== */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  /* clip, not hidden. The full-bleed photograph strip is sized in vw, which on
     a desktop with a classic scrollbar is a dozen pixels wider than the space
     actually available — enough to give the whole page a sideways scroll of
     its own. `hidden` would fix that by making body a scroll container, which
     quietly breaks position: sticky on the tab strip; `clip` just clips. */
  overflow-x: clip;
  background: var(--cream);
  color: var(--ink);
  font: var(--t-body)/1.65 var(--sans);
  text-wrap: pretty;
  /* Without this, iOS renders the dark hero noticeably lighter than the same
     hex on a desktop and the black stops matching the shop's actual sign. */
  -webkit-font-smoothing: antialiased;
}

/* Headings in the sans, not the sign's serif.
   The serif is kept for the one line it belongs to — the strapline off the
   plaque, MEN'S HAIRDRESSING AND GROOMING, in the hero and in the footer. As a
   heading face across a page of service names and prices it read as fussy;
   letting it appear twice, in the two places it is literally quoting the sign,
   is what makes it mean something. */
h1, h2, h3 {
  font-family: var(--sans);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.02em;
  margin: 0 0 .35em;
}
h1 { font-size: var(--t-title); }
h2 { font-size: var(--t-section); }
h3 { font-size: 1.05rem; }
p { margin: 0 0 1em; }
a { color: inherit; }
img { max-width: 100%; }

.wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--s5); }
.wrap.wide { max-width: 1240px; }
.lede { color: var(--ink-soft); font-size: var(--t-lead); margin-bottom: var(--s6); }
.muted { color: var(--ink-soft); }
.small { font-size: var(--t-small); }
.opt { color: var(--ink-faint); font-weight: 400; }

/* A small uppercase label. Used above headings and on section dividers — it is
   most of what makes the page feel considered rather than plain. */
.eyebrow {
  display: block; font: 600 var(--t-tiny)/1 var(--sans);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--ink-faint); margin: 0 0 var(--s3);
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 20;
  background: var(--sign); color: var(--sign-cream);
  padding: var(--s3) var(--s5); border-radius: 0 0 var(--radius-sm) 0;
}
.skip:focus { left: 0; }

/* ============================================================================
   The hero — the shopfront
   ========================================================================== */

/* The shopfront photograph, full-bleed, with the strapline and the address
   standing on it. One hero, not a picture with a black box under it. */
.shopfront {
  position: relative;
  background: var(--sign);
  line-height: 0;          /* kills the inline-image descender gap */
  /* Invisible in daylight, a hairline in dark mode, where the picture's black
     bottom edge and the page are both nearly black and would otherwise bleed
     into each other. Was on .hero, which is no longer the thing that touches
     the page. See --band-edge in brand.css. */
  border-bottom: 1px solid var(--band-edge);
}
.shopfront img {
  display: block; width: 100%;

  /* A HEIGHT IN PIXELS. No vh, no svh, no aspect-ratio, nothing that is
     computed from the viewport — because anything that is gets recomputed as
     the browser's own chrome slides in and out on a phone, and a photograph
     whose box changes height while you scroll re-crops on every frame. That
     is the "weird zoom", and it is why the page felt heavy: a full-width image
     relayout dragged through the entire scroll.

     It was 46vh, then clamp(260px, 46svh, 520px) — svh should not move, but
     "should not" is not good enough for the first thing anybody sees. A fixed
     height cannot move on any browser, present or future. Three of them, at
     the two widths where the crop wants to change, and that is the whole
     mechanism. */
  /* Taller than the 240px it was, by about what the black panel underneath
     used to occupy — the strapline and the address moved onto the picture, so
     the height they cost has moved with them and the page below has not
     shifted down. Still a fixed number, still three of them. */
  height: 360px;
  object-fit: cover;
  /* Biased above centre: the mirror run and the lit ceiling carry the picture,
     the floor does not. */
  object-position: center 40%;
}
/* Taller as the screen widens, because the same strip that is a room on a
   phone is a letterbox across a laptop. Widths, not heights: a window is only
   resized deliberately, and resizing is allowed to change the layout. */
@media (min-width: 700px)  { .shopfront img { height: 460px; } }
@media (min-width: 1100px) { .shopfront img { height: 560px; } }

/* The fade, which now has text standing on it.

   Built the way the .space captions three sections down are built, and for the
   same reason: it ramps, and then it STOPS ramping and holds flat at 94%. A
   gradient still climbing where the glyphs are gives cream on a mid-grey, and
   measures differently at the top of a word than at the bottom of one.

   THE PLATEAU HAS TO BE TALLER THAN THE TEXT, and the binding case is a phone.
   The strapline wraps to two lines there — MEN'S HAIRDRESSING AND GROOMING is
   about 377px of tracked-out Playfair caps against 312px of room — which the
   first version of this did not allow for, so the first line sat on the ramp
   rather than on the flat.

   At 74% of the image with the plateau from 60%, the flat part covers the
   bottom 107px on a phone, 136 at 700px and 166 at 1100px, against a text
   block of 96, 79 and 84. Change the image heights, the padding, the address,
   or the strapline's clamp and that arithmetic moves — redo it rather than
   nudging it.

   Measured over the white epoxy floor, the brightest thing in the frame: the
   strapline is 14.0:1 and the address 9.4:1. */
.shopfront .fade {
  position: absolute; left: 0; right: 0; bottom: -1px; height: 74%;
  /* Through --sign-rgb rather than a hard-coded 19,17,12, so the fade still
     lands on the same black the sign is painted in when dark mode moves it. */
  background: linear-gradient(to bottom,
    rgba(var(--sign-rgb), 0)   0%,
    rgba(var(--sign-rgb), .30) 30%,
    rgba(var(--sign-rgb), .72) 48%,
    rgba(var(--sign-rgb), .94) 60%,
    rgba(var(--sign-rgb), .94) 100%);
  pointer-events: none;
}

/* On the photograph, along its bottom edge. Not a panel below it any more —
   see the note in shop.js. Absolute rather than a flex end, because the
   picture is a fixed height and the text has to land on the plateau of the
   fade rather than wherever two lines of copy happen to reach. */
.hero {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  color: var(--sign-cream);
  text-align: center;
  padding: 0 var(--s5) var(--s5);
  /* Put it back. .shopfront sets line-height: 0 to kill the descender gap
     under an inline image, which was harmless while it contained nothing but
     the picture — and silently collapses every line of text now that the
     strapline and the address live inside it. */
  line-height: 1.5;
}

/* The .hero .mark rule lived here, for a logo that is no longer in the hero —
   it is on the fascia in the photograph and in the header above it. */

/* The line off the shop's own sign, set the way the sign sets it: the display
   serif, in caps, tracked out. It was the system sans, which made the one
   piece of copy that is literally painted on the front of the building look
   like a form label.

   Bigger than it was, now it has a photograph behind it instead of an empty
   panel: on the black box it was a caption in a lot of space, and on the
   picture it is the line on the front of the building. It still clamps, so it
   is a strapline on a phone rather than a paragraph. */
.hero .tagline {
  font: 500 clamp(.95rem, 2.6vw, 1.35rem)/1.4 var(--display);
  text-transform: uppercase; letter-spacing: .18em;
  color: var(--sign-cream); margin: 0 0 var(--s2);
  /* The fade holds at 94% under the text, which is already 9.4:1 over the
     brightest part of the floor. This is for the edges of the letters, which
     are thin at this weight — it costs nothing and it stops the caps looking
     printed on. */
  text-shadow: 0 1px 12px rgba(0,0,0,.5);
}

/* The hero h1 is now .vh — in the document, off the picture. There is no
   styling for it here on purpose: the moment it has a size and a colour,
   somebody will make it visible again by accident. */

/* No bottom margin: the padding on .hero is what holds the text off the edge
   of the picture, and a margin on top of it would push the address down off
   the plateau of the fade and onto the ramp. */
.hero .where {
  color: var(--photo-ink); margin: 0; font-size: var(--t-small);
  text-shadow: 0 1px 10px rgba(0,0,0,.5);
}
.hero .where a { color: inherit; }

/* The open/closed pill and the pair of hero buttons were here. Both are gone:
   the pill told a stranger "Closed" for most of the week, and the buttons
   repeated the two things that are now permanently on screen — the telephone
   in the header, and Book an appointment fixed to the bottom of the viewport.
   The .hero .btn-primary and .hero .btn-ghost inversions went with them —
   there is nothing left in the hero to press. */

/* ============================================================================
   Sticky section nav — Booksy's tab strip
   ========================================================================== */

.tabs {
  /* Under the sticky header, not over it. See --headerh. */
  position: sticky; top: var(--headerh); z-index: 10;
  /* Solid, and no backdrop-filter. It used to be 88% cream over a 12px blur,
     which is a lovely effect and, with a sticky header doing the same thing
     directly above it, two live-blurred layers for a phone to recompute on
     every scroll frame. That is a real cost for something almost nobody
     notices; the page underneath is plain cream anyway. */
  background: var(--cream);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s7);
}
.tabs ul {
  display: flex; gap: var(--s5); list-style: none; margin: 0 auto; padding: 0 var(--s5);
  max-width: var(--maxw); overflow-x: auto; scrollbar-width: none;
  /* The strip scrolls sideways on a narrow phone rather than wrapping to two
     rows, which would push the content down every time. */
}
.tabs ul::-webkit-scrollbar { display: none; }
@media (max-width: 700px) {
  /* Fade the right edge, so a strip that scrolls sideways looks like one. A
     row of links cut off flush at the screen edge looks like the end of the
     list, and the two tabs past it never get tapped. */
  .tabs ul {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  }
}
.tabs a {
  display: block; padding: var(--s4) 0; white-space: nowrap;
  font: 600 var(--t-small)/1 var(--sans); text-decoration: none;
  color: var(--ink-soft); border-bottom: 2px solid transparent;
  transition: color .15s ease, border-color .15s ease;
}
.tabs a:hover { color: var(--ink); border-bottom-color: var(--line); }

/* Where you are. Set by static/shop.js, which also sets aria-current — the
   underline is for the eye and the attribute is for everybody else. Without
   the script this simply never matches and the strip behaves as it did. */
.tabs a.on { color: var(--ink); border-bottom-color: var(--ink); }
/* Thicker than the hover hairline, because this one is answering "where am I"
   rather than "what would happen if I clicked". */
.tabs a.on { border-bottom-width: 3px; padding-bottom: calc(var(--s4) - 1px); }

/* ============================================================================
   Sections
   ========================================================================== */

main { padding-bottom: var(--s7); }
main.wrap, main .wrap { }
/* Clear of both sticky bars when a tab jumps here, plus a little air. Two bars
   now, so this is derived from the same custom property they are. */
.section { margin: 0 0 var(--s9); scroll-margin-top: calc(var(--headerh) + 64px); }

/* Sections arrive as you reach them: a short fade, once each, never again.

   No movement. The first version lifted each section 14px as it faded, which
   on a phone meant a full-width band of photographs being transformed while
   the page was already scrolling — it read as the pictures drifting, and it
   cost a composited layer per section to say very little. A plain opacity fade
   says the same thing for nothing.

   Two conditions on it, and both matter more than the effect does. `:root.js`
   is set by static/shop.js and by nothing else, so a browser that never runs
   the script — or runs it and throws — gets a page with all its content
   visible, which is the only acceptable failure for a page that sells
   haircuts. The reduced-motion query means the same for anybody who has asked
   their device to stop moving things about.

   The photograph strip opts out entirely: static/shop.js skips any section
   holding a full-bleed band. Fading a 100vw row of 1280px images earns
   nothing and is the most expensive thing on the page to animate. */
@media (prefers-reduced-motion: no-preference) {
  :root.js .section.reveal { opacity: 0; transition: opacity .6s ease .05s; }
  :root.js .section.reveal.in { opacity: 1; }
}
.section:last-child { margin-bottom: var(--s7); }
.section-head { margin: 0 0 var(--s5); }
.section-head h2 { margin: 0; }
.section-head p { margin: var(--s2) 0 0; color: var(--ink-soft); font-size: var(--t-small); }

/* ============================================================================
   The barber accordions
   ========================================================================== */

.barbers { display: grid; gap: var(--s3); }

.barber {
  /* Clear of both sticky bars when the team section jumps here, the same way
     .section is. Without it the row you asked for lands under the tab strip. */
  scroll-margin-top: calc(var(--headerh) + 64px);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  transition: box-shadow .2s ease, border-color .2s ease;
}
.barber[open] { box-shadow: var(--shadow-md); border-color: transparent; }

.barber summary {
  display: grid; align-items: center; gap: 2px var(--s4);
  grid-template-columns: auto 1fr auto auto;
  padding: var(--s4) var(--s5);
  cursor: pointer; list-style: none; min-height: 76px;
  transition: background .15s ease;
}
.barber summary::-webkit-details-marker { display: none; }
.barber summary:hover { background: var(--cream-deep); }
.barber summary:focus-visible { outline: 3px solid var(--focus); outline-offset: -3px; }

.barber .who { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.barber .who b { font-size: 1.15rem; font-weight: 700; letter-spacing: -.02em; }
.barber .role {
  font: 600 var(--t-tiny)/1 var(--sans);
  text-transform: uppercase; letter-spacing: var(--track-caps); color: var(--ink-faint);
}
.barber .count { color: var(--ink-soft); font-size: var(--t-small); white-space: nowrap; }
.barber .count b { color: var(--ink); font-weight: 600; }

.barber .chev {
  width: 9px; height: 9px; flex: none;
  border-right: 2px solid var(--ink-faint); border-bottom: 2px solid var(--ink-faint);
  transform: rotate(45deg) translate(-2px,-2px); transition: transform .2s ease;
}
.barber[open] .chev { transform: rotate(225deg) translate(-2px,-2px); }

.barber .blurb {
  margin: 0; padding: 0 var(--s5) var(--s4);
  color: var(--ink-soft); font-size: var(--t-small); max-width: 62ch;
}

/* The avatar. An initial on the barber's own colour stands in until the shop
   supplies photographs — deliberately styled as a choice rather than as a
   missing image, so the page never looks broken. */
.avatar {
  display: grid; place-items: center; flex: none;
  width: 52px; height: 52px; border-radius: 50%;
  /* --sign and not --ink for the fallback: --ink inverts to cream in dark
     mode, so a barber with no colour on file had white initials on a cream
     disc at 1.2:1. --sign is dark in both themes, which is what a white
     initial needs. (--c itself comes from staff.colour in the database and
     does not follow the theme, which is why the ink on it is --on-dark and
     not --on-accent.) */
  background: var(--c, var(--sign)); color: var(--on-dark);
  font-family: var(--sans); font-size: 1.05rem; font-weight: 700; letter-spacing: .01em;
  grid-row: 1 / span 2;
  /* A hairline and a soft drop, on every avatar whatever is inside it.
     This used to be `inset 0 1px 0 rgba(255,255,255,.25)` — a highlight along
     the top edge, which is a lovely touch on a flat disc of colour and does
     absolutely nothing on the four barbers who have photographs: an inset
     shadow paints above the background and BELOW the content, and on an <img>
     the photograph is the content and covers it. So two of the six gained a
     little dimension and four stayed flat, in the same row.
     The hairline is outset, which draws over anything. */
  box-shadow: 0 0 0 1px rgba(var(--sign-rgb), .10), 0 2px 6px rgba(var(--sign-rgb), .14);
}
/* The old highlight, kept for the discs it actually shows up on. */
.avatar:not(.photo) {
  box-shadow: inset 0 1px 0 rgba(255,255,255,.25),
              0 0 0 1px rgba(var(--sign-rgb), .10), 0 2px 6px rgba(var(--sign-rgb), .14);
}

@media (max-width: 600px) {
  .barber summary { grid-template-columns: auto 1fr auto; padding: var(--s4); }
  .barber .count { grid-column: 2; grid-row: 2; white-space: normal; }
  .barber .who { grid-row: 1; }
  .avatar { width: 46px; height: 46px; font-size: 1rem; }
}

/* ============================================================================
   Service rows
   ========================================================================== */

.services { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); }

/* Name and description on the left, price beside its own Book button on the
   right. A grid and not space-between: at 1080px the flex version put the
   price hard against the left edge of the card and the button against the
   right, and a price list where the two ends of a row are a hand's width apart
   is a price list you have to read twice. Three columns: the name takes what
   is left, the price and the button take what they need. */
.service-row {
  display: grid; align-items: center;
  grid-template-columns: 1fr auto auto;
  gap: var(--s5); padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
  transition: background .15s ease;
}
.service-row:last-child { border-bottom: 0; }
.service-row:hover { background: var(--cream-deep); }
.service-row .info { min-width: 0; }
.service-row h3 {
  font-family: var(--sans); font-size: 1rem; font-weight: 600;
  letter-spacing: 0; margin: 0 0 var(--s1);
}
.service-row .note {
  color: var(--ink-soft); font-size: var(--t-small); margin: 0 0 var(--s2); max-width: 52ch;
}
/* Right-aligned against a floor width, so the prices line up down the card and
   the eye can compare a haircut with a beard trim without reading either name.
   The floor is what does that work: each row is its own grid, so without a
   minimum the price column would be as wide as that row's own price and "£12"
   and "£50 · 1 hr" would sit in different places. 8em clears the longest
   pairing on the list, "£37.50 · 1 hr 15". */
.service-row .facts {
  display: flex; align-items: baseline; justify-content: flex-end;
  gap: var(--s3); margin: 0; text-align: right; min-width: 8em;
}
.service-row .mins {
  font-size: var(--t-small); color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
/* The sans, not the display serif. A price is a number you compare against
   another number, and Playfair's old-style-ish figures at 1.25rem read as
   decoration on a price list — "£26" wants to be plain and legible more than
   it wants to be handsome. Tabular so the column lines up. */
.service-row .price {
  font-family: var(--sans); font-size: 1.15rem; font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.service-row .was {
  font-size: var(--t-tiny); color: var(--ink-faint); text-decoration: line-through;
}
/* Outlined until the row is under the pointer, then the shop's black — see the
   note in shop.js for why twelve solid pills was the wrong picture.
   :focus-within as well as :hover, so tabbing through the list fills the same
   button that a pointer would.
   And the pointer check is what makes this safe on a phone: a touch screen has
   no hover to have, so the button would sit outlined forever with nothing to
   tell it apart from the page. There it is simply filled from the start. */
.service-row .book { justify-self: end; }
@media (hover: hover) {
  .service-row:hover .book, .service-row:focus-within .book {
    background: var(--brand); color: var(--on-brand);
    border-color: transparent; box-shadow: var(--shadow);
  }
}
@media (hover: none) {
  .service-row .book { background: var(--brand); color: var(--on-brand); border-color: transparent; }
}

/* Narrow: back to the stack. The price goes under the name where it reads as
   part of the service rather than as a column, and Book takes the full width
   because a thumb is a blunt instrument. */
@media (max-width: 620px) {
  .service-row {
    grid-template-columns: 1fr;
    gap: var(--s3); padding: var(--s4);
  }
  .service-row .facts { justify-content: flex-start; text-align: left; min-width: 0; }
  .service-row .book { width: 100%; justify-self: stretch; }
}

/* ============================================================================
   The team
   ========================================================================== */

/* auto-FIT, not auto-fill, and capped. auto-fill laid out nine 120px columns
   across a 1080px page for six barbers, so the team stopped two thirds of the
   way over and looked like something had failed to load. auto-fit collapses
   the empty tracks; the cap stops six faces spreading so far apart that they
   stop reading as one team. */
.team {
  list-style: none; margin: 0; padding: 0; max-width: 820px;
  display: grid; gap: var(--s5); grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
.team li { text-align: center; }

/* Each face is a link into that barber's own list up in Services — see the
   note in shop.js. Padding and a radius so there is a shape to hover, and the
   whole tile is the target rather than the name: a 84px disc is what the eye
   goes for and what a thumb hits. */
.team .member {
  display: block; padding: var(--s3) var(--s2); border-radius: var(--radius);
  color: inherit; text-decoration: none;
  transition: background .15s ease, transform .15s ease;
}
a.member:hover, a.member:focus-visible { background: var(--cream-deep); }
/* One pixel. The section is six faces in a row and anything more reads as the
   row bouncing; this is just enough to say the tile is live. */
a.member:hover { transform: translateY(-2px); }

/* The ring is in the barber's OWN colour — the same one their initials disc is
   painted in, and the same one their appointments carry in the admin. It is
   drawn outside the disc rather than on it, so it works identically over a
   photograph and over initials. */
.team .avatar {
  width: 84px; height: 84px; font-size: 1.7rem; margin: 0 auto var(--s3); grid-row: auto;
  transition: box-shadow .15s ease;
}
a.member:hover .avatar, a.member:focus-visible .avatar {
  box-shadow: 0 0 0 3px var(--paper), 0 0 0 5px var(--c, var(--ink)),
              0 4px 12px rgba(var(--sign-rgb), .18);
}
@media (prefers-reduced-motion: reduce) { a.member:hover { transform: none; } }

.team b { display: block; font-size: var(--t-small); font-weight: 600; }
.team .role {
  display: block; font: 600 var(--t-tiny)/1.4 var(--sans);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--ink-faint); margin-top: 2px;
}

/* ============================================================================
   Hours and about
   ========================================================================== */

.panel-soft {
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); padding: var(--s5);
}

.hours { border-collapse: collapse; width: 100%; }
.hours th, .hours td {
  text-align: left; padding: var(--s3) 0;
  border-bottom: 1px solid var(--line); font-weight: 400;
}
.hours tr:last-child th, .hours tr:last-child td { border-bottom: 0; }
.hours td { text-align: right; font-variant-numeric: tabular-nums; color: var(--ink-soft); }
.hours .today th, .hours .today td { font-weight: 600; color: var(--ink); }
.hours .today th::before {
  content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--ink); margin-right: var(--s2); vertical-align: middle;
}
.about { max-width: 62ch; color: var(--ink-soft); font-size: var(--t-lead); }

/* ============================================================================
   Booking flow — steps, days, slots
   ========================================================================== */

/* ONE header, one height, on every public page. The booking flow used to get a
   taller centred logo with no telephone while the front page got this bar, so
   the sign changed size and the number disappeared the moment somebody started
   booking — two websites wearing the same colours. Same markup now, from
   siteHeader() in layout.js. */
/* Cream, because the bar is the sign. This matters only when the mark is TYPE
   rather than artwork — a PNG carries its own colour, but the wordmark
   fallback for a shop that has sent no logo yet, and the demo shop's lock-up,
   both take currentColor and were inheriting the ordinary link colour: dark
   ink on the sign black, i.e. an empty header. */
header.site .brand { display: inline-block; color: var(--sign-cream); }
header.site img { height: 30px; width: auto; display: block; }

/* A shop that has not sent artwork yet gets its name set as type rather than
   another shop's mark — see lib/views/logo.js. Sized to sit where a 30px logo
   sits, so the header does not change height on the day the file arrives. */
.wordmark {
  font: 700 1.15rem/1 var(--sans);
  letter-spacing: -.01em; white-space: nowrap;
  color: currentColor; text-decoration: none;
}
.fmark.wordmark { font-size: 1rem; width: auto; }


/* The header stays at the top of the screen on every page, so the sign and the
   telephone are reachable from anywhere on a page this long — the number is
   the point, since plenty of this shop's trade is a question rather than a
   booking, and a number you have to scroll back up for is a call that does not
   happen.

   Its height is derived, not guessed. The row is as tall as the tallest thing
   in it, which is the call button's 44px touch target rather than the 30px
   mark, and the tab strip below sticks at exactly that offset — a wrong number
   here shows up as a hairline of page scrolling between the two bars. Anything
   that changes the header's padding or the tap target must change this too. */
:root { --headerh: calc(var(--tap) + 2 * var(--s3) + 1px); }

header.site {
  position: sticky; top: 0; z-index: 11;   /* over the tab strip, which is 10 */
  background: var(--sign); text-align: center;
  padding: var(--s3) 0; min-height: var(--headerh);
  border-bottom: 1px solid var(--band-edge);
}

/* The header carries the telephone, so it is a three-column grid and not
   a flex row: an empty track, the mark, the number. Flex with space-between
   put the mark hard against the left edge, which reads as a web app's toolbar;
   the sign over the door is centred and this is the same sign. The empty first
   track is what keeps it centred on the PAGE rather than centred in whatever
   space the telephone left over. */
header.site .wrap {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: var(--s3);
}
header.site .hsocial { grid-column: 1; justify-self: start; }
header.site .brand { grid-column: 2; }
header.site .call { grid-column: 3; justify-self: end; }

/* The same circle as the telephone beside it — var(--tap), 44px, the same
   number .call takes its min-height from. They were 36px against the
   telephone's 44 and read as two different kinds of control sitting in one
   bar, which is the thing to avoid when they are the only two controls in it.

   The old note here worried that three things could not share this row on a
   360px phone. They can, comfortably: the mark is 56px wide at its 30px
   height, so one circle, the mark and the telephone come to 168px inside 312px
   of room, and 168 inside 272 at 320px. The width was never the constraint.

   Same border alpha as .call too. Two circles the same size with different
   edge weights still do not read as a pair. */
.hsocial { display: flex; gap: var(--s2); margin: 0; }
/* THE SPAN IS THE DEMO SHOP'S INERT COPY — see socialRow() in layout.js. The
   made-up shop has no Instagram to link to, so its icons are spans rather than
   anchors. They still have to look like the circles beside the telephone: the
   sizing, the edge and the cream all lived on the `a` alone, which rendered
   them as dark-on-dark nothing in a dark bar. Hover and focus stay on the
   anchor, because a thing that lights up under the pointer and then does not
   respond to a click is worse than one that never lit up. */
.hsocial a, .hsocial > span {
  display: inline-grid; place-items: center;
  width: var(--tap); height: var(--tap); border-radius: 50%;
  border: 1px solid rgba(var(--sign-cream-rgb), .30); color: var(--sign-cream);
  transition: background .15s ease, border-color .15s ease;
}
.hsocial a:hover, .hsocial a:focus-visible {
  background: rgba(var(--sign-cream-rgb), .12); border-color: var(--sign-cream);
}
.hsocial svg { width: 17px; height: 17px; }
/* Under 400px the telephone is already down to its icon; one social is enough
   to say the accounts exist, and Instagram is the one that matters here. */
@media (max-width: 400px) {
  .hsocial > :nth-child(2) { display: none; }
}

/* Visually hidden, still announced. The call button's digits are the label a
   sighted user reads; a screen reader gets "Ring the shop" as well, because a
   string of digits read aloud on its own is not an instruction. */
.vh {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.call {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 9px 16px; border-radius: 100px;
  border: 1px solid rgba(var(--sign-cream-rgb), .30);
  color: var(--sign-cream); text-decoration: none;
  font: 600 var(--t-small)/1 var(--sans);
  /* Tabular, so a number with a 1 in it is the same width as one without and
     the button does not twitch between pages. */
  font-variant-numeric: tabular-nums;
  min-height: var(--tap);
  white-space: nowrap;
}
.call:hover { background: rgba(var(--sign-cream-rgb), .12); border-color: var(--sign-cream); }
/* The drawn handset. currentColor, so it follows the button's text on hover
   and in either theme without a second rule. */
.call .ico { display: block; flex: none; width: 17px; height: 17px; }

/* Under about 400px the digits and the logo start fighting for the same line.
   The icon alone still dials, and .vh keeps it announced properly. */
@media (max-width: 400px) {
  .call .num { display: none; }
  /* A circle, not a rounded rectangle. Once the digits are gone this is an
     icon button exactly like the one at the other end of the bar, so it is
     given the same square box and the 100px radius does the rest — the two
     ends of the header then match to the pixel. */
  .call { width: var(--tap); padding: 0; justify-content: center; }
}

.steps { margin: 0 0 var(--s6); }
.steps ol { display: flex; gap: var(--s2); list-style: none; margin: 0; padding: 0; }
.steps li {
  flex: 1; display: flex; align-items: center; gap: var(--s2); min-width: 0;
  font-size: var(--t-tiny); color: var(--ink-faint); padding-top: var(--s3);
  border-top: 3px solid var(--line); white-space: nowrap; overflow: hidden;
}
.steps .n {
  display: grid; place-items: center; flex: none; width: 20px; height: 20px;
  border-radius: 50%; background: var(--line); color: var(--ink-soft);
  font-size: .68rem; font-weight: 700;
}
.steps .done { color: var(--ink-soft); border-top-color: var(--ink-soft); }
.steps .done .n { background: var(--ink-soft); color: var(--paper); }
.steps .now { color: var(--ink); font-weight: 600; border-top-color: var(--ink); }
.steps .now .n { background: var(--ink); color: var(--paper); }
@media (max-width: 520px) { .steps li { font-size: 0; gap: 0; } .steps .n { font-size: .68rem; } }

.back {
  display: inline-flex; align-items: center; gap: var(--s2);
  margin: 0 0 var(--s4); padding: var(--s2) 0;
  color: var(--ink-soft); text-decoration: none; font-size: var(--t-small);
}
.back:hover { color: var(--ink); }

/* ---- service and staff cards (the "don't mind who" route) --------------- */

.group { margin: 0 0 var(--s7); }
.group h2 { margin-bottom: var(--s4); }
.note { color: var(--ink-soft); font-size: var(--t-small); }

.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); }
@media (min-width: 660px) { .cards { grid-template-columns: 1fr 1fr; } }

.card {
  display: grid; gap: 3px; padding: var(--s4) var(--s5);
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  text-decoration: none; color: inherit; min-height: 76px; align-content: center;
  transition: transform .18s ease, box-shadow .18s ease;
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.card .name { font-weight: 600; }
.card .price {
  font-family: var(--sans); font-size: 1.1rem; font-weight: 700;
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
}
.card .mins { color: var(--ink-soft); font-size: var(--t-small); }

.card.service { grid-template-columns: 1fr auto; align-items: center; }
.card.service .name, .card.service .note { grid-column: 1; }
.card.service .meta { grid-column: 2; grid-row: 1 / span 2; text-align: right; }
.card.service .mins { display: block; }

.cards.staff .card { grid-template-columns: auto 1fr auto; align-items: center; gap: 2px var(--s4); }
.cards.staff .name, .cards.staff .note { grid-column: 2; }
.cards.staff .meta { grid-column: 3; grid-row: 1 / span 2; }
.cards.staff .avatar { width: 44px; height: 44px; font-size: 1rem; }
.card.any { border-style: dashed; box-shadow: none; }
.card.any:hover { box-shadow: var(--shadow); }

/* ---- what you are booking ------------------------------------------------ */

/* The face, the service and the price, in one line above the dates. Three
   grid columns rather than flex so the price stays hard right whatever the
   service is called, and the middle column can run out of room without
   pushing it about. */
.booking-of {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: var(--s4); margin: 0 0 var(--s5); padding: var(--s3) var(--s4);
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.booking-of .avatar { width: 46px; height: 46px; font-size: 1rem; }
.booking-of .avatar.any {
  background: var(--cream-deep); color: var(--ink-faint);
  box-shadow: inset 0 0 0 1px var(--line);
}
.of-what { min-width: 0; display: grid; gap: 2px; }
.of-what b { font-weight: 700; letter-spacing: -.01em; }
.of-what .sub { color: var(--ink-soft); font-size: var(--t-small); }
.of-price {
  font-weight: 700; font-size: 1.15rem; font-variant-numeric: tabular-nums;
  letter-spacing: -.01em; white-space: nowrap;
}

/* ---- the times, by part of day ------------------------------------------- */

/* Thirty-nine times in one block is a wall. "Afternoon" is how somebody
   actually decides, so the times are grouped the way they would say it. */
.part { margin: 0 0 var(--s5); }
.part h3 {
  font: 600 var(--t-tiny)/1 var(--sans); text-transform: uppercase;
  letter-spacing: var(--track-caps); color: var(--ink-faint);
  margin: 0 0 var(--s3);
}

/* ---- the calendar, and the day you tapped -------------------------------- */

/* Calendar over times on a phone, side by side once there is room. The times
   column is deliberately the wider of the two: a month is a fixed shape, and
   twenty times at four to a row is what wants the space. */
.pick { display: grid; gap: var(--s5); align-items: start; }
@media (min-width: 900px) {
  .pick { grid-template-columns: 360px 1fr; gap: var(--s6); }
}

.cal-card {
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: var(--s4);
}

.monthnav {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); margin: 0 0 var(--s3);
}
.monthnav h2 {
  margin: 0; font-size: 1.15rem; text-align: center; flex: 1;
  font-variant-numeric: tabular-nums;
}
/* Square, because a month arrow is an icon and not a sentence. */
.monthnav .btn { min-width: var(--tap); padding: var(--s2); }
.monthnav .disabled { opacity: .3; }

.cal {
  display: grid; grid-template-columns: repeat(7, 1fr);
  gap: 2px; text-align: center;
}
.cal .dow {
  font: 600 var(--t-tiny)/2 var(--sans); color: var(--ink-faint);
  text-transform: uppercase; letter-spacing: var(--track-caps);
}
.cal .dow abbr { text-decoration: none; border: 0; }

/* Every cell is the same square whether it is a link, a dead day or a blank,
   so the grid never reflows between months. aspect-ratio rather than a height,
   so the calendar scales with the column it is in. */
/* ROUND. A date is one thing you hit, not a cell in a table — square cells
   read as a grid to be filled in, and the app this shop's customers have used
   for years puts circles here. Every cell is the same circle whether it is a
   link, a dead day or a blank, so the grid never reflows between months, and
   aspect-ratio rather than a height keeps it circular at any column width. */
.cal .d, .cal .pad {
  display: grid; place-items: center;
  aspect-ratio: 1; min-height: 40px;
  border-radius: 50%;
  font-variant-numeric: tabular-nums; font-size: .95rem;
  text-decoration: none;
}
/* A little air, so seven circles in a row read as seven buttons rather than as
   a strip of touching discs. */
.cal { gap: 4px 2px; }

/* A day with something free: a RING, and nothing inside it.

   It used to be a pale green disc with a green dot under the number, and a
   month of those is a wall of colour that says nothing — every day the
   calendar offers is free, by definition, so colouring them all "free" is
   colouring them all the same. An outline reads as a button without filling
   the page in, which is also how the app this shop's customers have used for
   years draws it.

   box-shadow rather than border, so the ring sits inside the circle and the
   grid does not shift by three pixels between a free day and a gone one.
   Deliberately NOT `outline`: that is spoken for by the focus ring a few
   hundred lines down, and one would eat the other. */
.cal a.d {
  color: var(--ink); font-weight: 600;
  box-shadow: inset 0 0 0 1.5px var(--pick);
  transition: background .12s ease, color .12s ease;
}
.cal a.d:hover, .cal a.d:focus-visible { background: var(--pick); color: var(--on-accent); }

/* Nothing free. Greyed and inert — and it really is inert: the markup makes
   these a span rather than a link, so there is nothing to tap and nothing to
   land on with a keyboard. Still drawn, still in its circle, because a day
   that has gone is information and a calendar with holes in it is a puzzle;
   but no ring, no fill, and a cursor that does not promise anything. */
.cal .d.none { color: var(--ink-faint); cursor: default; }

/* Today: a warm fill rather than a second ring, so it can be read at the same
   time as the blue one — a day is very often both, and two concentric rings a
   pixel apart is a smudge rather than two facts. */
.cal .d.today { background: var(--cream-deep); }

/* The day being shown underneath, in the shop's own black, which beats both.
   The ring comes off: a filled black circle is not also an outlined one. */
.cal a.d.on, .cal a.d.on:hover {
  background: var(--sign); color: var(--sign-cream); box-shadow: none;
}

.callout {
  display: flex; align-items: center; gap: var(--s2);
  margin: var(--s4) 0 0; font-size: var(--t-tiny); color: var(--ink-faint);
}
/* The legend has to draw the same thing the calendar draws, or it explains a
   calendar that no longer exists. A ring, at the size it will fit. */
.callout .key {
  width: 14px; height: 14px; border-radius: 50%;
  box-shadow: inset 0 0 0 1.5px var(--pick); flex: none;
}

/* Every day cell links to #times, so on a phone the tap lands on the times
   rather than back at the top of the calendar. The margin is what stops the
   heading being flush against the top edge when it does. */
.times { scroll-margin-top: var(--s5); }
.times h2 { margin: 0 0 var(--s1); }
.times .muted { margin: 0 0 var(--s4); }

.slots { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s2); }
.slots { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); }

/* The times, drawn the same way as the days, for the same reason: twenty green
   tiles is a green panel, and every time on this list is free or it would not
   be on it. Outline, filling on hover — so the one under your thumb is the
   only one wearing colour. */
.slot {
  display: grid; place-items: center; min-height: var(--tap); padding: var(--s2);
  color: var(--pick); border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1.5px var(--pick);
  font-variant-numeric: tabular-nums; font-weight: 600; font-size: .95rem;
  text-decoration: none; transition: background .12s ease, color .12s ease, transform .12s ease;
}
.slot:hover, .slot:focus-visible { background: var(--pick); color: var(--on-accent); transform: scale(1.04); }

/* ---- panels, summaries, confirmation ----------------------------------- */

.panel {
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: var(--s5); margin: 0 0 var(--s5);
}
.panel.narrow, .details, .summary { max-width: 560px; }
.panel.narrow { margin-inline: auto; }

.summary dl, .summary-list { margin: 0; }
.summary dl > div, .summary-list > div {
  display: flex; justify-content: space-between; gap: var(--s4);
  padding: var(--s3) 0; border-bottom: 1px solid var(--line);
}
.summary dl > div:last-child, .summary-list > div:last-child { border-bottom: 0; }
dt { color: var(--ink-soft); font-size: var(--t-small); }
dd { margin: 0; text-align: right; }
.ref { font-family: var(--numeric); font-size: var(--t-small); color: var(--ink-soft); }

.confirmed { text-align: center; max-width: 520px; margin-inline: auto; padding: var(--s6) var(--s5); }
.confirmed .tick {
  display: grid; place-items: center; width: 60px; height: 60px; margin: 0 auto var(--s4);
  border-radius: 50%; background: var(--free-bg); color: var(--free); font-size: 1.8rem;
}
.confirmed .when {
  font-family: var(--sans); font-size: 1.4rem; font-weight: 700; color: var(--ink);
  margin-bottom: var(--s5); letter-spacing: -.01em;
}
.confirmed .summary-list > div { text-align: left; }
.actions { display: flex; gap: var(--s3); justify-content: center; flex-wrap: wrap; margin: var(--s5) 0 var(--s4); }

.notice {
  padding: var(--s3) var(--s4); border-radius: var(--radius-sm); margin: 0 0 var(--s5);
  background: var(--cream-deep); border: 1px solid var(--line); font-size: var(--t-small);
}
.notice.warn { background: var(--warn-bg); border-color: transparent; color: var(--warn); }
.notice.bad  { background: var(--danger-bg); border-color: transparent; color: var(--danger); }

/* ---- the customer we already know --------------------------------------- */

/* Three inputs replaced by one line. It is a panel and not a form field
   because it is not one: nothing here is editable, and drawing it as a filled
   box that happens to be greyed out would invite people to try. */
.known {
  display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap;
  background: var(--cream-deep); border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: var(--s4); margin: 0 0 var(--s5);
}
.known-what { display: grid; gap: 2px; min-width: 0; flex: 1; }
.known-what b { font-size: 1.1rem; letter-spacing: -.01em; }
/* The masks. Numeric face, because that is what a run of bullets and three
   digits is, and it stops '••••• 812' looking like a rendering fault. */
.known-what .sub {
  color: var(--ink-soft); font-size: var(--t-small);
  font-family: var(--numeric); word-break: break-word;
}
.known .eyebrow { margin: 0; }

/* A link, not a button: it changes nothing, it just shows the long form. Given
   a tap target anyway, because it is the escape hatch on a shared phone and
   the one thing on this page somebody may be in a hurry to hit. */
.known-not {
  display: inline-flex; align-items: center; min-height: var(--tap);
  font-size: var(--t-small); font-weight: 600; white-space: nowrap;
}

/* ---- forms -------------------------------------------------------------- */

.field { margin: 0 0 var(--s5); }
.field label { display: block; font-weight: 600; margin: 0 0 var(--s2); font-size: var(--t-small); }

input[type=text], input[type=email], input[type=tel], textarea {
  width: 100%; padding: 13px var(--s4);
  background: var(--field); color: var(--ink);
  border: 1px solid var(--field-line); border-radius: var(--radius-sm);
  /* 16px minimum, always: anything smaller and iOS zooms on focus, which on a
     booking form reads as the page breaking. */
  font: 16px/1.5 var(--sans);
  transition: border-color .15s ease, box-shadow .15s ease;
}
input:hover, textarea:hover { border-color: var(--ink-soft); }
textarea { resize: vertical; }
input:focus-visible, textarea:focus-visible, .slot:focus-visible,
.card:focus-visible, .btn:focus-visible, a:focus-visible, summary:focus-visible {
  outline: 3px solid var(--focus); outline-offset: 2px;
}
.field.bad input, .field.bad textarea { border-color: var(--danger); border-width: 2px; }
.err  { color: var(--danger); font-size: var(--t-small); margin: var(--s2) 0 0; font-weight: 500; }
.help { color: var(--ink-soft); font-size: var(--t-small); margin: var(--s2) 0 0; }

.check { display: flex; gap: var(--s3); align-items: flex-start; font-weight: 400 !important; }
.check input { width: 22px; height: 22px; margin: 1px 0 0; flex: none; accent-color: var(--ink); }

/* Off-screen rather than display:none — some bots skip anything hidden that
   way, and the point is that they fill it in. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---- buttons ------------------------------------------------------------ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: var(--tap); padding: 12px var(--s5);
  border: 1px solid transparent; border-radius: 100px;
  font: 600 var(--t-small)/1 var(--sans); text-decoration: none; cursor: pointer;
  transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary {
  background: var(--brand); color: var(--on-brand); box-shadow: var(--shadow);
}
.btn-primary:hover { box-shadow: var(--shadow-md); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--field-line); }
.btn-ghost:hover { border-color: var(--ink); }
.btn.big { width: 100%; padding: 16px var(--s5); font-size: 1rem; }
.btn.disabled, .btn[aria-disabled=true] { opacity: .35; pointer-events: none; transform: none; }

/* THE FOCUS RING ON THE DARK BANDS.
 *
 * In daylight a skin almost always sets --focus to the same value as --sign,
 * because both are "the shop's darkest colour" — and --sign is what the header,
 * the footer and the hero are painted in, so the ring above was drawing itself
 * in exactly the colour of the bar it sat on. 1.00:1. Every keyboard target in
 * those bands — the logo, the telephone, both social icons, the address link,
 * every link in the footer — had a focus ring that could not be seen.
 *
 * It is fine in dark mode, which is why it survived: there --focus is the
 * lettering colour on near-black at 17:1 and looks immaculate. The bug only
 * exists in the theme most people will actually be looking at.
 *
 * --sign-cream is the lettering off the shop's own sign and does not move
 * between themes, so it is right against --sign in both. bin/contrast.js
 * checks that pairing in both themes for exactly this reason.
 */
header.site :focus-visible,
.hero :focus-visible,
footer.site :focus-visible {
  outline-color: var(--sign-cream);
}

/* ---- footer ------------------------------------------------------------- */

footer.site {
  margin-top: var(--s8); padding: var(--s7) 0 var(--s6);
  background: var(--sign); color: var(--band-ink); font-size: var(--t-small);
  border-top: 1px solid var(--band-edge);   /* dark mode only — see .hero */
}
footer.site a { color: var(--sign-cream); }
footer.site p { margin: 0 0 var(--s2); }

/* TWO HALVES, not three columns.
   It was 1.4fr 1fr 1fr — the brand block, Find us and Get in touch as three
   peers, with the first one simply the widest. Three unequal columns of
   different heights is the shape that looked wrong: nothing lined up with
   anything, and the mark floated in the widest of them.
   Now it is a signature on the left and the details on the right, which is how
   a footer with one brand and two lists ought to divide. auto on the right so
   the columns take exactly what their content needs and sit against the right
   edge, rather than stretching until the gap between "Find us" and "Get in
   touch" is a hand's width. */
/* LEFT AND RIGHT AT EVERY WIDTH, phone included.
   The first version of this split started at 760px, which meant a phone kept
   the old centred stack and the change was invisible on the device most of
   these bookings are made on. It is the same two halves all the way down; only
   the proportions move.

   The left column is capped rather than auto, because `auto` sizes to the
   widest thing in it and the strapline would take the whole row. At 8.5rem the
   mark sits at 112px with a little air, and MEN'S / HAIRDRESSING / AND
   GROOMING breaks into three tidy lines under it rather than one long one. */
.fgrid {
  display: grid; text-align: left; align-items: start;
  grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr);
  gap: var(--s5);
  padding-bottom: var(--s6);
}
@media (min-width: 760px) {
  /* Room now: the brand half takes the slack and the links sit against the
     right edge, taking exactly what they need rather than stretching until
     "Find us" and "Get in touch" are a hand's width apart. */
  .fgrid { grid-template-columns: minmax(180px, 1fr) auto; gap: var(--s7) var(--s8); }
}

/* The right half. Stacked until there is room for two — two columns of four
   lines each in half a phone is two columns of wrapped fragments. */
.fcols { display: grid; gap: var(--s5); }
@media (min-width: 560px) {
  .fcols { grid-template-columns: auto auto; gap: var(--s7); }
}

/* Smaller again. The mark is the sign-off at the end of a scroll, not a
   billboard, and it is now sitting at the top of a half rather than filling a
   third — at 150px it was the biggest thing in the footer and pulled the whole
   band off balance. It sits under the two accounts, and it is the last thing
   in the block, so it carries no bottom margin of its own. */
/* Left-aligned, not centred: the footer is two halves at every width now, so
   there is no width at which the mark should be floating in the middle of
   anything. 100% of a column that is itself capped at 8.5rem. */
.fmark {
  height: auto; width: min(112px, 100%);
  margin: 0; display: block;
}

/* ON A PHONE THE MARK SITS AT THE FOOT OF ITS COLUMN.
   The footer is two halves at every width, and on a narrow screen the right
   half is much the taller of the two: four lines of address, a directions
   link, a heading, a telephone number and a booking link. The left half is two
   circles and the mark. Pinned to the top of its column the mark sat level
   with the second line of the address and left an obvious void beneath it —
   the band read heavy on the right and empty on the left.

   A fixed nudge was tried first and is the wrong tool: any value big enough to
   look deliberate on a 390px screen is too big on a 320px one, and it still
   leaves a void, only a smaller one. Pushing the mark to the BOTTOM of the row
   instead makes the two halves finish together whatever the address turns out
   to be — accounts at the top, mark at the foot, the details framed between
   them — and it needs no number to keep in step with the content.

   margin-top:auto is what does it; the column has to be a flex box stretched
   to the row's height for `auto` to have anything to consume, which is what
   align-self does here (the grid itself is align-items:start, and that is
   still right for the other column). Phone only: from 760px the two halves are
   already about the same height and the mark belongs under the circles. */
@media (max-width: 759px) {
  .fbrand {
    display: flex; flex-direction: column;
    align-self: stretch;
  }
  .fmark { margin-top: auto; }
}

/* A column heading in the footer is a label, not a headline: same treatment as
   .eyebrow up the page, so the footer belongs to the same page. */
.fcol h2 {
  font: 600 var(--t-tiny)/1 var(--sans);
  text-transform: uppercase; letter-spacing: var(--track-caps);
  color: var(--band-ink-soft); margin: 0 0 var(--s3);
}
.fcol p { margin: 0 0 var(--s3); line-height: 1.7; }
.fcol a { text-decoration: none; }
.fcol a:hover { text-decoration: underline; }

/* The number is the most likely thing anyone wants from a footer after the
   address, so it is given the size to say so. */
.fphone a {
  font: 700 1.15rem/1.2 var(--sans); font-variant-numeric: tabular-nums;
  text-decoration: none; border-bottom: 1px solid rgba(var(--sign-cream-rgb), .35);
  padding-bottom: 2px;
}
.fphone a:hover { border-bottom-color: var(--sign-cream); }

/* An action, with the arrow that says so. inline-flex keeps the arrow on the
   same line as its label at every size. */
.flink {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: var(--tap); font-weight: 600;
}
.flink svg { flex: none; }

/* Icon buttons, not words. Circled so they read as a pair of controls and give
   a thumb something the size of a thumb to hit. They open the brand block now,
   with the mark beneath them, so the gap belongs on the bottom. */
.fsocial { display: flex; gap: var(--s3); justify-content: flex-start; margin: 0 0 var(--s4); }
/* Same span as in the header bar, same reason — the demo shop's icons do not
   go anywhere, so they are not anchors. */
.fsocial a, .fsocial > span {
  display: inline-grid; place-items: center;
  width: var(--tap); height: var(--tap); border-radius: 50%;
  border: 1px solid rgba(var(--sign-cream-rgb), .25);
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.fsocial a:hover, .fsocial a:focus-visible {
  background: rgba(var(--sign-cream-rgb), .12); border-color: var(--sign-cream);
  color: var(--on-dark);
}

/* A hairline above the last line, so it reads as a sign-off under the footer
   rather than as another item in it. */
footer.site .small {
  border-top: 1px solid rgba(var(--sign-cream-rgb), .14);
  /* Left, like the two halves above it. It was centred, which under a
     left-aligned footer is the one line that lines up with nothing. */
  padding-top: var(--s4); margin: 0; color: var(--band-ink-soft); text-align: left;
  /* The shop's line and the credit share the row and separate onto two when
     there is not the width for one, rather than the credit being pushed to a
     line of its own at every size. */
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
  justify-content: space-between; align-items: baseline;
}

/* The build credit. A step dimmer than the shop's own line beside it, because
   it is the least important text on the site and must not compete with the
   name above it — but only a step. It was drafted three steps down, at
   3.5:1, on the reasoning that nobody needs to read it; that is the reasoning
   behind most of the unreadable small print on the web, and it does not
   survive being written down. Dim is a matter of hierarchy, not of falling
   under the threshold.

   --band-ink-faint is the bottom of that ladder and is held to 4.5 by
   bin/contrast.js, so a re-skin cannot quietly drop it below the line the way
   a hard-coded hex could. For After Image it is 4.63:1 in light and 4.97:1
   against the darker --sign dark mode swaps in; the link is 6.85 and 7.36.

   The underline is the cream at .22 rather than a tint of the link colour.
   Over --sign the two land within two points of each other on every channel,
   and this one follows the shop's lettering instead of pinning a fourth grey
   into the stylesheet. */
.fby { color: var(--band-ink-faint); }
.fby a {
  color: var(--band-link); text-decoration: none;
  border-bottom: 1px solid rgba(var(--sign-cream-rgb), .22);
}
.fby a:hover, .fby a:focus-visible { color: var(--sign-cream); border-bottom-color: currentColor; }


/* ============================================================================
   Sticky book bar — phones only.
   The one thing a customer came to do, always within thumb reach, without
   having to scroll back to the top.
   ========================================================================== */

/* On every width now, not just phones. It used to be the phone's substitute for
   the pair of buttons in the hero; with those gone it is the only Book control
   on the page, and a desktop visitor cannot be left without one. */
.bookbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 15;
  padding: var(--s3) var(--s4) calc(var(--s3) + env(safe-area-inset-bottom));
  text-align: center;
  pointer-events: none;          /* the strip is not a target; the button is */
}
.bookbar .btn {
  pointer-events: auto;
  box-shadow: var(--shadow-md);
}

/* Out of the way while the services are on screen — set by static/shop.js,
   which is the only thing that ever adds this class. Scrolling to the thing
   you are already reading is a button that does nothing, and on a laptop it
   was doing nothing directly on top of the page.

   Faded and dropped rather than display:none, so it does not disappear from
   under a thumb already on its way down; visibility takes it out of the tab
   order once the movement has finished, and comes straight back on the way
   in because the return transition does not mention it. */
.bookbar { transition: opacity .2s ease, transform .25s ease; }
.bookbar.away {
  opacity: 0; transform: translateY(120%); visibility: hidden;
  transition: opacity .2s ease, transform .25s ease, visibility 0s .25s;
}

@media (min-width: 701px) {
  /* Off to one side above phone widths. Centred, it sat over the middle of the
     column — the one part of the page somebody is actually reading. */
  .bookbar { text-align: right; padding-right: var(--s6); }
}
/* Clear it so it never covers the last line of the page. */
body.has-bookbar footer.site { padding-bottom: calc(var(--s8) + 72px); }

@media (max-width: 700px) {
  /* A full-width bar on a phone: the thumb is at the bottom of the screen, and
     a floating pill wastes the width it could be using. */
  .bookbar {
    background: var(--cream);       /* solid, for the same reason as .tabs */
    border-top: 1px solid var(--line);
    box-shadow: 0 -1px 12px rgba(var(--sign-rgb), .06);
    pointer-events: auto;
  }
  .bookbar .btn { width: 100%; box-shadow: none; }
}

/* ============================================================================
   Photographs
   ========================================================================== */

/* A photo avatar reuses the same box as the initials disc, so a barber with a
   portrait and one without sit at exactly the same size in the same row. */
.avatar.photo {
  object-fit: cover;
  background: var(--cream-deep);   /* while it loads, and behind a transparent PNG */
  padding: 0;
}

/* The "The shop" strip: three photographs of the room.
   Three columns on a wide screen, one on a phone — not a horizontal scroller.
   A swipeable strip hides two of the three photographs behind a gesture that
   plenty of people never make, and there are only three of them. */
.space {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--s3);
  grid-template-columns: 1fr;
}
@media (min-width: 760px) {
  .space { grid-template-columns: repeat(3, 1fr); gap: var(--s4); }

  /* Out of the column and across the whole window. The trick is the negative
     margin: half the viewport minus half the container, on each side, which
     works whatever --maxw is set to and needs no wrapper element.

     The corners come off and the gap tightens, because a band of photographs
     that stops short of the edge with rounded corners is a row of cards, and
     the point of this is to stop being a row of cards for one section.

     body has overflow-x: clip for exactly this rule — see the note there. */
  .space.bleed {
    width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
    gap: 2px;
  }
  .space.bleed li { border-radius: 0; box-shadow: none; }
}
.space li {
  margin: 0;
  border-radius: var(--radius); overflow: hidden;
  background: var(--sign);        /* the letterbox behind a loading image */
  box-shadow: var(--shadow);
  position: relative;
}
.space img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 3 / 2; object-fit: cover;
}
/* The caption sits on the photograph rather than under it, so three cards of
   different caption lengths still line up along the bottom. The scrim is a
   gradient and not a flat tint: a flat one over a dark photograph looks like a
   rendering fault, and over a bright one it isn't enough to read through.

   The stops are not decorative. A gradient that is still ramping where the
   glyphs are gives cream text on a mid-grey, and measured over the brightest
   of the three photographs — the white epoxy floor in space-room — that came
   out at 3.4:1, under the 4.5:1 that 14px text needs. So the ramp finishes
   above the text and the bottom 32% is a flat 94%: every text pixel now sits
   on at least 80% of --sign, which is 8.8:1 against pure white and better
   against anything else. Move the padding and you move the text off the
   plateau, so re-measure if you do. */
.space span {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: var(--s6) var(--s3) var(--s3);
  color: var(--sign-cream);
  font-size: var(--t-small);
  background: linear-gradient(to bottom,
    rgba(var(--sign-rgb), 0)   0%,
    rgba(var(--sign-rgb), .35) 30%,
    rgba(var(--sign-rgb), .80) 50%,
    rgba(var(--sign-rgb), .94) 68%,
    rgba(var(--sign-rgb), .94) 100%);
}

/* The photograph of the front and the hours, as one answer. Stacked on a
   phone; from 900px the picture takes the larger share and the table sits
   beside it, top-aligned, because a table that stretches to a photograph's
   height puts eight days of air between Monday and Tuesday. */
.find { display: grid; gap: var(--s5); align-items: start; }
@media (min-width: 900px) {
  /* Photograph and map down the left, hours down the right: the two picture
     boxes stack into one column so they share an edge, and the table keeps its
     own. Reading order is unchanged on a phone, where it is all one column. */
  .find { grid-template-columns: 1.25fr 1fr; gap: var(--s5) var(--s6); }
  .find .shopfront-card { margin-bottom: 0; max-width: none; }
  .find .mapcard { grid-column: 1; }
  .find .hours-panel { grid-column: 2; grid-row: 1 / span 2; }
}
.hours-panel { max-width: 460px; }

/* The map. Same card as the shopfront photograph beside it, so the two read as
   a pair rather than as a photo and an embed. */
.mapcard {
  margin: 0; border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; box-shadow: var(--shadow); background: var(--cream-deep);
}
.mapcard iframe { display: block; width: 100%; height: 260px; border: 0; }
@media (min-width: 900px) { .mapcard iframe { height: 300px; } }
.mapcard figcaption {
  padding: var(--s3) var(--s4); font-size: var(--t-small);
  border-top: 1px solid var(--line); background: var(--paper);
}
.mapcard figcaption a { text-decoration: none; font-weight: 600; }
.mapcard figcaption a:hover { text-decoration: underline; }

.shopfront-card {
  margin: 0 0 var(--s5); max-width: 620px;
  background: var(--paper); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden;
}
.shopfront-card img { display: block; width: 100%; height: auto; }
.shopfront-card figcaption {
  padding: var(--s3) var(--s4); font-size: var(--t-small); color: var(--ink-soft);
  border-top: 1px solid var(--line);
}

