/* The app's design tokens, restated for the web.
 *
 * They are duplicated rather than shared because the two sides cannot share
 * anything: one is Dart, the other is CSS in a page that must open in under
 * a second on a phone with two bars of signal. The values below are the
 * source in CLAUDE.md, copied deliberately — when a token changes there, it
 * changes here.
 */
:root {
  --accent: #2ed3a0;
  --on-accent: #062b21;
  --ink: #14161a;
  --ink-soft: #5b6470;
  --bg: #f1f3f6;
  --surface: #ffffff;
  --danger: #c4463f;
  --warning: #c98a21;

  --gap-xs: 4px;
  --gap-sm: 8px;
  --gap-block: 12px;
  --gap-md: 16px;
  --gap-lg: 24px;
  --gap-xl: 32px;

  --radius-control: 18px;
  --radius-card: 20px;

  --tap: 48px;

  --shadow-card: 0 2px 12px rgba(20, 22, 26, 0.06);
  --motion: 200ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #f2f4f7;
    --ink-soft: #9aa3af;
    --bg: #0e0f13;
    --surface: #191b21;
    --shadow-card: 0 2px 12px rgba(0, 0, 0, 0.4);
  }
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: Manrope, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 13px;
  font-weight: 400;
  line-height: 1.46;
  -webkit-font-smoothing: antialiased;
}

.page {
  max-width: 480px;
  margin: 0 auto;
  padding: var(--gap-md);
  padding-bottom: var(--gap-xl);
}

h1 {
  margin: 0;
  font-size: 22px;
  font-weight: 700;
  line-height: 1.2;
}

h2 {
  margin: 0 0 var(--gap-block);
  font-size: 17px;
  font-weight: 700;
}

.caption {
  font-size: 11px;
  font-weight: 500;
  color: var(--ink-soft);
}

.muted {
  color: var(--ink-soft);
}

/* What the master does, directly under her name: the one line that answers
   whether this is the right person at all. */
.craft {
  color: var(--accent);
  font-size: 13px;
  font-weight: 500;
}

.card {
  background: var(--surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--gap-md);
}

.stack > * + * {
  margin-top: var(--gap-block);
}

/* The master, at the top of her own page. */
.master {
  display: flex;
  align-items: center;
  gap: var(--gap-block);
  margin-bottom: var(--gap-lg);
}

/* Square with the corners taken off, at every size and on every surface:
   the same face in the app, in the sheet and on the page. */
.avatar {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-control);
  object-fit: cover;
  background: var(--accent);
  color: var(--on-accent);
  display: grid;
  place-items: center;
  font-size: 17px;
  font-weight: 700;
  flex-shrink: 0;
}

/* A service: name and what it covers on the left, price on the right. */
.service {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-block);
  width: 100%;
  min-height: var(--tap);
  padding: var(--gap-md);
  background: var(--surface);
  border: none;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform var(--motion);
}

.service:active {
  transform: scale(0.99);
}

.service__name {
  display: block;
  font-size: 17px;
  font-weight: 700;
}

.service__price {
  font-size: 17px;
  font-weight: 700;
  white-space: nowrap;
}

.service__meta {
  display: block;
  margin-top: var(--gap-xs);
  color: var(--ink-soft);
}

.includes {
  margin: var(--gap-sm) 0 0;
  padding-left: var(--gap-md);
  color: var(--ink-soft);
}

/* States that replace the whole page: nothing here, or nothing loaded. */
.state {
  text-align: center;
  padding: var(--gap-xl) var(--gap-md);
}

.state__title {
  font-size: 17px;
  font-weight: 700;
  margin-bottom: var(--gap-sm);
}

.skeleton {
  border-radius: var(--radius-card);
  background: linear-gradient(
    90deg,
    var(--surface) 25%,
    var(--bg) 37%,
    var(--surface) 63%
  );
  background-size: 400% 100%;
  animation: shimmer 1.4s ease infinite;
  height: 76px;
}

@keyframes shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: -100% 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
  }
  .service {
    transition: none;
  }
}

/* Going back a step: quiet, above the content, never a bar of its own. */
.back {
  min-height: var(--tap);
  margin-bottom: var(--gap-sm);
  padding: 0;
  background: none;
  border: none;
  color: var(--ink-soft);
  font: inherit;
  cursor: pointer;
}

.day + .day {
  margin-top: var(--gap-md);
}

.day__title {
  margin: 0 0 var(--gap-sm);
  font-size: 13px;
  font-weight: 700;
}

.posts__heading {
  margin: var(--gap-md) 0 var(--gap-sm);
  font-size: 13px;
  font-weight: 700;
}

.posts {
  display: flex;
  gap: var(--gap-sm);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--gap-xs);
}

/* A card in the news strip. Every one of them is a square and a title,
   whether or not there is a photograph behind the square: a row where some
   cards are pictures and others are bare titles reads as broken. */
.post {
  flex: 0 0 auto;
  align-self: flex-start;
  width: 120px;
  padding: 0;
  border: none;
  background: none;
  text-align: left;
  scroll-snap-align: start;
  cursor: pointer;
}

.post__cover--empty {
  display: block;
}

.post__cover {
  display: block;
  width: 120px;
  height: 120px;
  object-fit: cover;
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.post__title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: var(--gap-xs);
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
}

.post__photo {
  width: 100%;
  border-radius: var(--radius-card);
}

.day__discount {
  margin-left: var(--gap-sm);
  padding: 2px var(--gap-xs);
  border-radius: var(--radius-control);
  background: rgba(46, 211, 160, 0.18);
  color: var(--ink);
  font-size: 11px;
  font-weight: 700;
}

.saved {
  margin: 0;
  color: var(--ink);
  font-weight: 700;
}

.times {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
}

.time {
  min-width: 76px;
  min-height: var(--tap);
  padding: 0 var(--gap-block);
  background: var(--surface);
  border: none;
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-card);
  color: inherit;
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  transition: transform var(--motion);
}

.time:active {
  transform: scale(0.97);
}

.field {
  display: block;
}

.field input {
  width: 100%;
  min-height: var(--tap);
  margin-top: var(--gap-xs);
  padding: 0 var(--gap-md);
  background: var(--surface);
  border: none;
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-card);
  color: inherit;
  font: inherit;
  font-size: 15px;
}

.field input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* The one button on the page that does something irreversible-ish. */
.primary {
  width: 100%;
  min-height: var(--tap);
  border: none;
  border-radius: var(--radius-control);
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: transform var(--motion);
}

.primary:active {
  transform: scale(0.99);
}

.primary:disabled {
  opacity: 0.6;
  cursor: default;
}

.error {
  margin: 0 0 var(--gap-block);
  color: var(--danger);
}

.link {
  color: var(--accent);
  word-break: break-all;
}

/* Cancelling: the colour the app uses for a called-off booking. */
.danger {
  width: 100%;
  min-height: var(--tap);
  border: none;
  border-radius: var(--radius-control);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  color: var(--danger);
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}

/* The Telegram door is a link, so it has to be told to look like a button. */
.primary--link {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

/* The second door, quieter than the first but the same size to press. */
.quiet {
  width: 100%;
  min-height: var(--tap);
  border: none;
  border-radius: var(--radius-control);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}

/* Her work: one row that scrolls sideways, the same shape as the news strip
   above it. Photographs are what a client decides on, and only what the
   master published herself is here. */
.works {
  display: flex;
  gap: var(--gap-sm);
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  padding-bottom: var(--gap-xs);
}

.work {
  flex: 0 0 auto;
  width: 120px;
  height: 120px;
  object-fit: cover;
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  scroll-snap-align: start;
}

/* What clients wrote. Last on the page: she reads the work and the price,
   then turns here to check herself. */
.review__stars {
  color: var(--accent);
  letter-spacing: 2px;
}

.review__author {
  font-size: 13px;
}

.review__reply {
  margin-top: var(--gap-xs);
  padding-left: var(--gap-sm);
  border-left: 2px solid var(--accent);
  color: var(--ink-soft);
}

/* Every photograph at once: a grid, because a strip of forty is one row
   with the fortieth thirty swipes away. The squares grow with the screen
   rather than the count. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: var(--gap-sm);
}

.grid__photo {
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: var(--radius-card);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  cursor: zoom-in;
}

/* Step one photograph either way. On a desktop there is no thumb to swipe
   with, and the arrow keys are not something a page can advertise. */
.lightbox__step {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}

.lightbox__step:hover:not(:disabled) {
  background: rgba(0, 0, 0, 0.7);
}

.lightbox__step:disabled {
  opacity: 0.25;
  cursor: default;
}

.lightbox__step--back {
  left: var(--gap-md);
}

.lightbox__step--on {
  right: var(--gap-md);
}

/* The rest of the gallery, on request. Full width would read as another
   door out of the page; this is a shelf label under a shelf. */
.works__more {
  width: auto;
  align-self: start;
  padding: 0 var(--gap-md);
  font-size: 13px;
  font-weight: 500;
}

/* The offer to be reminded. Given a surface of its own and the accent
   button, because it is the one thing on this screen that has to be pressed
   for anything to be sent later. */
.remind {
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}

.remind__title {
  font-size: 15px;
  font-weight: 700;
}

/* A word that acts like a link rather than a button: no surface, no shadow,
   and a tap target that is still tall enough to hit. */
.link {
  align-self: flex-start;
  min-height: var(--tap);
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-soft);
  font: inherit;
  font-size: 13px;
  text-align: left;
  text-decoration: underline;
  cursor: pointer;
}

/* And the one at the very bottom of a master's page sits in the middle,
   away from everything that books. */
.report-link {
  align-self: center;
  margin-top: var(--gap-lg);
}

/* Reporting the photograph she is looking at.
   Behind three dots rather than spelled out: the word laid over somebody's
   work is in the way of it every time it is opened, for the one person in a
   thousand who needs it. Same filled circle as the cross, mirrored. */
.lightbox__more {
  position: absolute;
  top: var(--gap-md);
  left: var(--gap-md);
  z-index: 1;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

.lightbox__menu {
  position: absolute;
  top: calc(var(--gap-md) + 44px);
  left: var(--gap-md);
  z-index: 2;
  display: flex;
  flex-direction: column;
  padding: var(--gap-xs);
  border-radius: var(--radius-control);
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

.lightbox__menu button {
  padding: var(--gap-sm) var(--gap-md);
  border: 0;
  border-radius: var(--radius-control);
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* Her own words about what is wrong: a field with room for a paragraph. */
.field--note {
  width: 100%;
  min-height: 96px;
  padding: var(--gap-block) var(--gap-md);
  border: 0;
  border-radius: var(--radius-control);
  background: var(--surface);
  box-shadow: var(--shadow-card);
  color: var(--ink);
  font: inherit;
  font-size: 15px;
  resize: vertical;
}

/* One reason among the six, and the one she has chosen. */
.report__reason[aria-pressed='true'] {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* One column with the standard gap. The page used to be spaced by the
   browser's own paragraph margins, which no element without a paragraph
   has: the list of reasons sat flush against the line above it, and the box
   for "інше" flush against the list the moment it appeared. */
.report > * + * {
  margin-top: var(--gap-block);
}

.report h2,
.report p {
  margin: 0;
}

.danger {
  color: var(--danger);
}

/* The gallery over the page rather than instead of it: closing has to put
   her back exactly where she was, and the surest way is never to take the
   page away. Contained, not cropped — a screenshot is tall, a manicure is
   square, and both are worth seeing whole. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: flex;
  flex-direction: column;
  justify-content: center;
  background: rgba(14, 15, 19, 0.94);
  cursor: zoom-out;
}

.lightbox__strip {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* The browser keeps the sideways pan; the other axis is left free for the
     drag that closes this. */
  touch-action: pan-x;
}

.lightbox__strip::-webkit-scrollbar {
  display: none;
}

.lightbox__slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--gap-md);
}

.lightbox__photo {
  max-width: min(100%, 900px);
  max-height: 84vh;
  object-fit: contain;
  border-radius: var(--radius-card);
  cursor: default;
}

/* A filled circle, not a bare glyph: over a photograph that can be any
   colour, a thin white cross disappears exactly when the picture is
   bright. */
.lightbox__close {
  position: absolute;
  top: var(--gap-md);
  right: var(--gap-md);
  z-index: 1;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

.lightbox__close:hover {
  background: rgba(0, 0, 0, 0.7);
}

.lightbox__place {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--gap-lg);
  display: flex;
  justify-content: center;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.7);
}

/* On black, the page's own dot — dark ink at a quarter opacity — is a dot
   nobody can see. */
.lightbox__place .dot {
  background: rgba(255, 255, 255, 0.35);
  opacity: 1;
}

.lightbox__place .dot[aria-current='true'] {
  background: #fff;
}

/* Reviews read one at a time. Five of them stacked push everything else off
   the screen and get scrolled past instead of read. */
.deck {
  display: flex;
  gap: var(--gap-sm);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.deck::-webkit-scrollbar {
  display: none;
}

/* One card at a time, with its neighbours showing at the edges.
 *
 * The peek is the affordance: a card that fills the width looks like the
 * whole of it, and nobody swipes what looks complete. The one in the middle
 * is full size and the rest step back, so which one is being read is never a
 * question — and it is the same shape on a phone and on a desktop, rather
 * than two whole cards at one width and one-and-two-halves at another. */
.deck > * {
  flex: 0 0 86%;
  scroll-snap-align: center;
  transform: scale(0.94);
  opacity: 0.6;
  transition: transform var(--motion), opacity var(--motion);
}

.deck > [data-current='true'] {
  transform: none;
  opacity: 1;
}

.deck__nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-md);
  margin-top: var(--gap-sm);
}

.deck__arrow {
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
  color: var(--ink);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

/* A disabled arrow keeps its circle. Dropping the background as well left
   one side of the calendar's header a bare chevron and the other a button,
   which reads as a layout fault rather than as "you cannot go back". */
.deck__arrow[disabled] {
  opacity: 0.4;
  box-shadow: none;
  cursor: default;
}

.dots {
  display: flex;
  justify-content: center;
  gap: var(--gap-xs);
}

.dot {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--ink);
  opacity: 0.25;
}

.dot[aria-current='true'] {
  background: var(--accent);
  opacity: 1;
}

/* On a real screen this stops being a phone page.
 *
 * Who she is moves into a column of its own and stays there while the client
 * works through services, days and times on the right — the same thing a
 * salon's page does, and the reason a booking flow reads as one page rather
 * than four. Every screen renders `header.master` first and its content
 * after, so the two columns cost no markup at all: the header takes the left
 * column, everything else takes the right. */
@media (min-width: 900px) {
  body {
    font-size: 15px;
  }

  .page {
    display: grid;
    grid-template-columns: 320px minmax(0, 1fr);
    /* Only across. A gap on both axes puts 32px between every element in
       the right column as well — a heading and the strip under it end up
       further apart than either of them is tall, and the page reads as a
       list of things standing in a field. The elements keep their own
       margins, the same ones they have on a phone. */
    column-gap: var(--gap-xl);
    row-gap: 0;
    align-items: start;
    max-width: 1040px;
    padding: var(--gap-xl) var(--gap-lg);
  }

  .page > .master {
    position: sticky;
    top: var(--gap-xl);
    flex-direction: column;
    align-items: flex-start;
    gap: var(--gap-md);
    margin-bottom: 0;
  }

  .avatar {
    width: 96px;
    height: 96px;
    /* Three tenths of the side, the same proportion as everywhere else: a
       control's radius on a 96-point square reads as a sharp box. */
    border-radius: 29px;
    font-size: 28px;
  }

  h1 {
    font-size: 28px;
  }

  /* The gallery has room to be a grid rather than a row that scrolls. */
  .works {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: var(--gap-block);
    overflow: visible;
  }

  /* Square, like the thumbnails on a phone: the grid decides the width, so
     the height has to follow it rather than being a number of its own. */
  .work {
    width: 100%;
    height: auto;
    aspect-ratio: 1;
  }

  /* A card the width of a desktop is a line of text nobody finishes, so it
     is capped rather than widened. */
  .deck > * {
    flex: 0 0 min(86%, 520px);
  }

}

/* What the dots become when there are too many to count. */
.deck__count {
  min-width: 56px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
/* A month to choose a day from.
 *
 * The day is asked before the hours because a client arrives knowing roughly
 * when she wants to come, and a fortnight of times in one list is a list to
 * hunt through.
 *
 * Capped at 360px: a calendar stretched across a desktop column is a
 * spreadsheet, with 86px cells and a number lost in the middle of each. The
 * numbers here are deliberately the same as the Mini App's — the same
 * product, and nobody should have to learn it twice. */
.calendar {
  max-width: 360px;
  background: var(--surface);
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-card);
  padding: var(--gap-block);
  margin-bottom: var(--gap-block);
}

.calendar__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap-sm);
  margin-bottom: var(--gap-sm);
}

.calendar__month {
  font-size: 15px;
  font-weight: 700;
}

.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
  justify-items: center;
}

.calendar__weekday {
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-soft);
  padding-bottom: var(--gap-xs);
}

.calendar__day {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 100%;
  max-width: 44px;
  aspect-ratio: 1;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--ink);
  font-family: inherit;
  font-size: 16px;
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
}

/* A day with nothing free is dim but still worth tapping: it answers with
   the days that are free, which is kinder than a dead grey square. */
.calendar__day[data-free='false'] {
  opacity: 0.35;
}

.calendar__day[disabled] {
  opacity: 0.15;
  cursor: default;
}

.calendar__day[aria-current='true'] {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 700;
}

/* A cheaper day is a dot, not a floating "−15%". The percent itself is
   written under the calendar, next to the day she has actually chosen —
   seven of them scattered across a month is confetti, not information. */
.calendar__off {
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: var(--accent);
}

.calendar__day[aria-current='true'] .calendar__off {
  background: var(--on-accent);
}
