/* Campsite booking. One stylesheet, no build step.
   The admin is used by people running a site on paper today, so type is large,
   contrast is high and nothing important is conveyed by colour alone. */

:root {
  --accent: #4A5EDA;
  --ink: #1A1A1A;
  --grey: #7A7A7A;
  --line: #E0E0E0;
  --paper: #FFFFFF;
  --ground: #F6F6F4;
  --ok: #1F7A4D;
  --warn: #9A6B00;
  --stop: #A32020;
  --radius: 10px;
}

* { box-sizing: border-box; }

/* Inter, served from this site's own `public/fonts/` and never from a font
   CDN: nothing here should make a request to somebody else's server on a
   guest's behalf, and the whole face is 48KB. One variable file carries every
   weight between 100 and 900, which is what lets the weights below be 450 and
   550 rather than the four steps a set of static files would give.
   SIL Open Font License; the text sits beside the file.

   Every weight in this sheet came down a step when it went in, because Inter
   puts more ink on the page at the same number than the system face did: what
   was 700 is 600, what was 650 or 600 is 550, and 550 is 450. Bold is for the
   one thing on a screen that has to be read first, and a page where half of
   it is bold has nothing emphasised at all. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-latin-variable.woff2') format('woff2');
}

/* 15px, down from 17 on 20 September 2026 at Owen's direction, twice: too
   heavy, then too big. The hit areas did NOT come down with it. Nothing is
   under 44 pixels and nothing will be: that is a question about an unsteady
   hand, not about eyesight, and the padding absorbs the difference.
   ⚠️ The people using this are in their seventies. The size that suits them
   is not the size that photographs well, and the answer to that is the
   second appearance under "Still to do" in CLAUDE.md, not creep back up. */
body {
  margin: 0;
  font: 15px/1.52 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  color: var(--ink);
  background: var(--ground);
  /* Never let the browser fake a weight by smearing the regular one. A
     synthesised bold is the thing that actually looks heavy-handed, and it is
     what shows for the moment before the face arrives. */
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
}

/* The browser's own default for these is 700. */
strong, b, th { font-weight: 600; }

h1, h2, h3 { line-height: 1.25; margin: 0 0 .5rem; font-weight: 600; }
h1 { font-size: 1.5rem; letter-spacing: -0.015em; }
h2 { font-size: 1.05rem; }
h3 { font-size: .95rem; }
p { margin: 0 0 .75rem; }
a { color: var(--accent); }

.wrap { max-width: 46rem; margin: 0 auto; padding: 1.5rem 1rem 4rem; }
.wrap.narrow { max-width: 34rem; }
.wrap.wide { max-width: 78rem; }
/* A page that is one form to fill in, rather than a screen to survey. */
.wrap.form-page { max-width: 52rem; }

.banner {
  margin: 0; padding: .6rem 1rem; text-align: center;
  background: #FFF4D6; color: #6B4E00; border-bottom: 1px solid #E8D9A8;
}

.hero {
  background: var(--paper); border-bottom: 1px solid var(--line);
  padding: 3rem 1rem 2.5rem; text-align: center;
}
.hero .lede { color: var(--grey); font-size: 1rem; margin: 0; }

.bar {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  background: var(--paper); border-bottom: 1px solid var(--line);
  padding: .85rem 1rem; font-weight: 550;
}
.bar a { color: var(--ink); text-decoration: none; }
.bar nav {
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  column-gap: 1.15rem; row-gap: .35rem; font-weight: 500;
}
.bar nav a { white-space: nowrap; }
.bar nav a { color: var(--accent); }

/* ---- The admin's navigation -------------------------------------------
   Down the side rather than across the top. Eleven destinations strung along
   one row had run out of room and wrapped to two, which put the row's height
   at the mercy of the window width; and the calendar, the screen that wants
   every pixel it can get, was giving up a band of the page to it on every
   scroll. Down the side each destination gets its own line, they can be
   grouped by the job they do, and the one you are on can say so.

   A skip link first, because the navigation is now eleven links deep and
   somebody on a keyboard should not have to walk it on every page. */
.skip {
  position: absolute; left: -9999px;
  background: var(--paper); color: var(--ink); font-weight: 550;
  padding: .7rem 1rem; border: 2px solid var(--accent); border-radius: var(--radius);
}
.skip:focus { left: 1rem; top: 1rem; z-index: 20; }

.frame { display: grid; grid-template-columns: 15rem 1fr; min-height: 100vh; }

.side {
  background: var(--paper); border-right: 1px solid var(--line);
  padding: 1rem .75rem 1rem;
  display: flex; flex-direction: column;
  position: sticky; top: 0; align-self: start; max-height: 100vh; overflow-y: auto;
}
.side .mark { display: inline-flex; align-items: center; padding: 0 .4rem .9rem; }
.side nav { display: flex; flex-direction: column; flex: 1; }

/* A group is a block, not a word floating in a gap. The rule above it and the
   name sitting tight on its own items are what make it read as a heading for
   the four things under it rather than as a stray label. */
.side-set { padding: .6rem 0; border-top: 1px solid var(--line); }
.side-set:first-child { border-top: 0; padding-top: 0; }
.side-group {
  margin: 0 0 .1rem; padding: 0 .6rem;
  color: var(--grey); font-size: .72rem; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; line-height: 1.8;
}
/* Settings is not part of the books: it is the twice-a-year job, so it sits
   at the foot, out of the way of the three that are the day's work. */
.side-foot { margin-top: auto; }

.side ul { list-style: none; margin: 0; padding: 0; }
.side nav a {
  display: flex; align-items: center; gap: .6rem;
  min-height: 2.75rem; padding: .35rem .6rem;
  border-radius: 8px;
  color: var(--ink); text-decoration: none; font-weight: 450;
}
.side nav a:hover { background: var(--ground); }
.side nav a .door-icon { width: 20px; height: 20px; flex: none; color: var(--grey); }
/* The page you are on is marked without leaning on colour: a filled ground
   and the weight of the words. It also had a bar down its left edge, which
   said the same thing a third time and was the loudest of the three. */
.side nav a[aria-current="page"] {
  background: color-mix(in srgb, var(--accent) 10%, white);
  color: var(--accent); font-weight: 600;
}
.side nav a[aria-current="page"] .door-icon { color: var(--accent); }
.side-who {
  margin: .5rem 0 0; padding: .7rem .6rem 0; border-top: 1px solid var(--line);
  font-size: .9rem; line-height: 1.35; font-weight: 550;
}
.side-who .hint { display: block; margin: 0; font-size: .78rem; font-weight: 400; }

/* The frame's own scrollbar belongs to the page, not to the column, so the
   main side of it keeps the measure it had. */
.frame > .wrap { min-width: 0; }

/* Narrow enough that a column down the side would leave nothing beside it:
   the navigation goes back across the top, and scrolls sideways if it must. */
/* Across the top again, and wrapped rather than scrolled. A row that scrolls
   sideways hides the end of itself, and the destination somebody wants is as
   likely to be the eleventh as the first. Wrapping costs a line or two of
   height and shows all of them. */
@media (max-width: 62rem) {
  .frame { display: block; }
  .side {
    position: static; max-height: none; overflow: visible;
    flex-direction: row; align-items: center; flex-wrap: wrap; gap: .5rem .9rem;
    border-right: 0; border-bottom: 1px solid var(--line); padding: .7rem 1rem;
  }
  .side .mark { padding: 0 .4rem 0 0; }
  .side nav { flex: 1; flex-direction: row; flex-wrap: wrap; gap: .1rem .9rem; }
  /* A rule above a group and a heading on top of it are how a column shows
     its structure. Along a row they would be lines across the middle of it,
     so the groups keep their order and drop the decoration. */
  .side-set { border-top: 0; padding: 0; }
  .side-foot { margin-top: 0; }
  .side-group { display: none; }
  .side ul { display: flex; flex-wrap: wrap; gap: .1rem; margin: 0; }
  .side nav a {
    border-bottom: 3px solid transparent;
    white-space: nowrap; font-size: .95rem;
  }
  /* Along a row it is a tab, which is the one place an underline means "this
     one" to everybody and is not a stripe for its own sake. */
  .side nav a[aria-current="page"] { border-bottom-color: var(--accent); }
  .side-who { display: none; }
}
@media (max-width: 40rem) {
  .side .site-logo { height: 2rem; }
  .side nav a { padding: .35rem .5rem; gap: .45rem; }
  .side nav a .door-icon { width: 19px; height: 19px; }
}

.card {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.25rem; margin: 0 0 1.25rem;
}
.card.muted { background: transparent; border-style: dashed; }

.hint { color: var(--grey); font-size: .88rem; margin: 0 0 .5rem; }
.reason { color: var(--stop); margin: 0 0 .5rem; }
.strong { font-weight: 600; }

/* ---- Forms -------------------------------------------------------------
   Two rules do most of the work here. A field is only as wide as what goes
   in it, because a date entered into a box the width of the room is harder
   to read back than one the size of a date. And a form has a measure: past
   about forty-five characters the eye loses the line, and a form that spans
   a wide screen makes somebody's eyes travel further for every field.

   The controls are deliberately large. The people using this are in their
   seventies and coming off paper, so nothing is below the 44 pixel target
   that a finger or an unsteady hand can find. */

.form { max-width: 44rem; }
.form.wide { max-width: 60rem; }

/* Twelve columns, so a field can be a third, a half or the whole line. */
.form-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 1.1rem 1.25rem; }
.field { display: block; margin: 0; grid-column: span 12; min-width: 0; }
.field.short { grid-column: span 3; }     /* a number, a count */
.field.date  { grid-column: span 4; }     /* a date, which has a known width */
.field.half  { grid-column: span 6; }
.field.third { grid-column: span 4; }
.field.two-thirds { grid-column: span 8; }

.label { display: block; font-size: .92rem; font-weight: 550; margin-bottom: .3rem; }
.field .hint { margin: .3rem 0 0; font-size: .85rem; }

input, select, textarea {
  width: 100%; font: inherit; color: inherit; min-height: 2.75rem;
  padding: .55rem .75rem; border: 1px solid var(--line); border-radius: 8px;
  background: var(--paper);
}
textarea { min-height: 4.5rem; }
input:hover, select:hover, textarea:hover { border-color: var(--grey); }
input:focus, select:focus, textarea:focus, button:focus-visible, a.primary:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--accent) 40%, white); outline-offset: 1px;
  border-color: var(--accent);
}

/* A checkbox is a thing you tick, so it gets a whole row you can hit. */
.check {
  display: flex; align-items: center; gap: .6rem; grid-column: span 12;
  min-height: 2.75rem; margin: 0; cursor: pointer;
}
.check input { width: 1.15rem; height: 1.15rem; min-height: 0; accent-color: var(--accent); }

/* What to do next, set apart from what you are filling in. */
.actions {
  grid-column: span 12; display: flex; flex-wrap: wrap; gap: .75rem; align-items: center;
  margin-top: .5rem; padding-top: 1.1rem; border-top: 1px solid var(--line);
}
.actions .quiet { color: var(--grey); text-decoration: none; }
.actions .quiet:hover { color: var(--ink); text-decoration: underline; }

/* `hidden` has to beat anything we set display to, or an element JavaScript
   has hidden stays on the screen: the browser's own rule for [hidden] is
   display:none at the weakest possible strength, and the button rule below
   overrides it. The Move button beside the pitch list is the one that
   matters, and it sat there looking pressable. */
[hidden] { display: none !important; }

button, .primary, .btn {
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-weight: 550; cursor: pointer; min-height: 2.75rem;
  padding: .5rem 1.05rem; border-radius: 8px; border: 1px solid var(--line);
  background: var(--paper); color: var(--ink); text-decoration: none;
}
button.primary, a.primary, button[type="submit"].primary {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
button.danger { border-color: var(--stop); color: var(--stop); }
button:hover, .primary:hover, .btn:hover { filter: brightness(.96); border-color: var(--accent); }
button.small { min-height: 2.25rem; padding: .35rem .8rem; font-size: .9rem; }

/* A group of fields with a heading, for a form long enough to lose your place in. */
.form-section { grid-column: span 12; margin-top: .5rem; }
.form-section:first-of-type { margin-top: 0; }
.form-section > h3 {
  font-size: .85rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase;
  color: var(--grey); margin: 0 0 .9rem; padding-bottom: .5rem;
  border-bottom: 1px solid var(--line);
}
.form-section .form-grid { gap: 1.1rem 1.25rem; }

.search { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 1rem 1.25rem; }
.row-form { display: flex; flex-wrap: wrap; gap: .9rem; align-items: end; }
.row-form .field { margin: 0; min-width: 8rem; flex: 0 1 auto; width: auto; }
.row-form .field input, .row-form .field select { min-width: 8rem; }
.row-form .check { grid-column: auto; }

/* Availability */
.options, .plain, .lately, .movements, .events, .map { list-style: none; margin: 0; padding: 0; }
/* ⚠️ `.plain` is a BULLETED list drawn by hand, not a bare one: the bullet is
   an absolutely positioned ::before sitting in the 1.1rem of left padding the
   li carries for it. A list that takes `.plain` and then sets its own
   `padding` without that indent does not lose the bullet, it hides it behind
   its own first letter, which shows as a blue tick on the side of a capital
   and looks like a rendering fault. If a list wants no bullet it wants
   `list-style: none` above, not `.plain`. */
.plain li { padding: .2rem 0 .2rem 1.1rem; position: relative; }
.plain li::before { content: "•"; position: absolute; left: 0; color: var(--accent); }

.option {
  display: flex; gap: 1rem; align-items: center; justify-content: space-between;
  padding: 1rem 0; border-top: 1px solid var(--line);
}
.option:first-child { border-top: 0; }
.option.unavailable { opacity: .72; }
.option h3 { margin: 0 0 .15rem; }
.option-price { text-align: right; flex: 0 0 auto; }
.price { font-size: 1.15rem; font-weight: 600; margin: 0; }

/* Site map placeholder: the real one is traced artwork per site. */
.map { display: grid; grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr)); gap: .5rem; }
.pitch {
  border: 1px solid var(--line); border-radius: 8px; padding: .5rem;
  display: flex; flex-direction: column; gap: .1rem; font-size: .85rem;
}
.pitch .ref { font-weight: 600; }
.pitch .type { color: var(--grey); }
.pitch.free { background: color-mix(in srgb, var(--ok) 8%, white); border-color: color-mix(in srgb, var(--ok) 35%, white); }
.pitch.free .state { color: var(--ok); font-weight: 550; }
.pitch.taken { background: var(--ground); color: var(--grey); }

.summary { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; margin: 0; }
.summary dt { color: var(--grey); }
.summary dd { margin: 0; }

.badge {
  display: inline-block; font-size: .8rem; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; padding: .2rem .55rem; border-radius: 99px;
  background: color-mix(in srgb, var(--ok) 12%, white); color: var(--ok); margin: 0 0 .5rem;
}
.badge.pending { background: #FFF4D6; color: var(--warn); }
.badge.cancelled, .badge.expired { background: #FBEAEA; color: var(--stop); }
/* A pitch out of use is not a booking in any state, so it does not wear the
   green that means one is confirmed. */
.badge.closed { background: var(--ground); color: var(--grey); }

/* ---- Admin grid ------------------------------------------------------- */
.admin { background: var(--ground); }
.grid-controls { display: flex; flex-wrap: wrap; align-items: baseline; gap: 1rem; justify-content: space-between; }
.grid-controls p { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
/* Stepping a day, a fortnight or a year. These are pressed more than anything
   else on the screen and were the smallest things on it: text-height links,
   for people who are not steady with a mouse. */
.step { padding: .55rem .85rem; font-weight: 500; }
.step:hover { background: color-mix(in srgb, var(--accent) 6%, white); }
/* The one you are on. Not colour alone: the border thickens too, so it still
   reads for anyone who cannot tell the two apart. */
.step.on {
  background: color-mix(in srgb, var(--accent) 12%, white);
  border-color: var(--accent); border-width: 2px; font-weight: 600;
}
.sep { border-left: 1px solid var(--line); height: 1.2em; }

.grid-scroll { overflow-x: auto; background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius); }
/* Fixed layout, so every day is the same width whatever is written in it.
   With automatic layout a long guest name stretches the columns its booking
   spans and the empty runs collapse, which makes a fortnight look like it has
   wide days and narrow ones. The table is sized from the number of days and
   scrolls inside its container; the colgroup pins the pitch column and lets
   the rest divide evenly. */
table.grid { border-collapse: collapse; table-layout: fixed; min-width: 100%; font-size: .85rem; }
table.grid col.pitch-col { width: 8.5rem; }
table.grid th, table.grid td { border: 1px solid var(--line); }
table.grid thead th { padding: .3rem .2rem; text-align: center; font-weight: 550; }
table.grid thead th .dow { display: block; color: var(--grey); font-size: .75rem; }
/* The shape of the week. A campsite's year is a run of weekends, and without
   this a fortnight is fourteen identical columns: finding "the Saturday" meant
   reading every heading. Painted on the column rather than the cell, because a
   booking spanning a fortnight is one cell and could not carry it. The empty
   days hatch over a transparent ground, so the column shows through.
   Both are quiet on purpose: this is orientation, not information, and the
   bookings have to stay the loudest thing on the table. */
table.grid col.weekend { background: #F4F4F0; }
table.grid col.now { background: color-mix(in srgb, var(--accent) 8%, white); }
table.grid thead th.weekend { background: #EDEDE8; }
table.grid thead th.weekend .dow { color: var(--ink); }
table.grid thead th.today { background: color-mix(in srgb, var(--accent) 16%, white); }
table.grid thead th.today .dom { color: var(--accent); }
table.grid thead th.today .dow { color: var(--accent); }
table.grid .corner, table.grid .pitch-head {
  position: sticky; left: 0; z-index: 2; background: var(--paper);
  text-align: left; padding: .35rem .6rem; white-space: nowrap; overflow: hidden;
}
table.grid .pitch-head .ref { font-weight: 600; }
table.grid td { height: 2.6rem; padding: 0; }
table.grid td.empty { background: repeating-linear-gradient(45deg, transparent, transparent 6px, rgba(0,0,0,.022) 6px, rgba(0,0,0,.022) 12px); }
/* Following one pitch across four weeks is what this table is for, so the row
   under the pointer lifts out of it. */
table.grid tbody tr:not(.group):hover td.empty { background-color: rgba(74, 94, 218, .045); }
table.grid tbody tr:not(.group):hover .pitch-head { background: color-mix(in srgb, var(--accent) 7%, white); }
table.grid td.cell { padding: 2px; }

.booking {
  display: flex; align-items: center; gap: .4rem; height: 100%;
  padding: .3rem .5rem; border-radius: 6px; text-decoration: none;
  background: var(--accent); color: #fff; font-weight: 500; cursor: grab;
  overflow: hidden; white-space: nowrap;
}
/* Four kinds of thing on one calendar, and only three can lean on hue.
   The maintenance grey has no chroma, so it sits the same distance from every
   hue: teal, green, purple and terracotta all collided with IT rather than
   with each other. Giving it the hatch the season table already uses for a
   closed day takes it out of the argument, which is honest anyway, because a
   blocked pitch is not a booking.
   That leaves touring, held and seasonal on colour, and the purple passes
   every check against the other two: worst normal-vision separation 30.2, and
   the worst CVD pair does not involve it. It is also nobody else's colour,
   being far from the ok green, the warning amber and the stop red, none of
   which may be borrowed for a category. */
.booking.pending { background: #C08A00; }
.booking.seasonal { background: #7B4EA8; }
.booking.maintenance {
  background: repeating-linear-gradient(45deg, #5B5B5B, #5B5B5B 5px,
                                        #6E6E6E 5px, #6E6E6E 10px);
}
/* It can be picked up and moved, and nothing about a flat coloured bar says
   so. The cursor says it once the pointer is on it; this says it a moment
   earlier, and costs no ink. */
.booking { transition: box-shadow .12s, transform .12s; }
.booking:hover { box-shadow: 0 3px 8px rgba(26, 26, 26, .28); transform: translateY(-1px); }
.booking:active { cursor: grabbing; transform: none; }
.booking.dragging { opacity: .5; }
@media (prefers-reduced-motion: reduce) {
  .booking, .booking:hover { transition: none; transform: none; }
}
.booking .who { overflow: hidden; text-overflow: ellipsis; }
.booking .tag, .pitch-head .tag {
  font-size: .7rem; font-weight: 600; text-transform: uppercase;
  padding: .05rem .35rem; border-radius: 99px; background: rgba(255,255,255,.28);
}
.pitch-head .tag { background: var(--ground); color: var(--grey); margin-left: .4rem; }
.pitch-head .tag.tents { background: color-mix(in srgb, var(--accent) 12%, white); color: var(--accent); }
tr.drop-target td { background: color-mix(in srgb, var(--accent) 12%, white); }

.legend { display: flex; gap: 1.25rem; align-items: center; color: var(--grey); font-size: .9rem; margin: .75rem 0 1.5rem; }
.swatch { display: inline-block; width: .9rem; height: .9rem; border-radius: 3px; vertical-align: -1px; margin-right: .3rem; }
.swatch.confirmed { background: var(--accent); }
.swatch.pending { background: #C08A00; }
.swatch.seasonal { background: #7B4EA8; }
.swatch.maintenance {
  background: repeating-linear-gradient(45deg, #5B5B5B, #5B5B5B 3px,
                                        #6E6E6E 3px, #6E6E6E 6px);
}

.movements li, .events li { display: flex; gap: 1rem; padding: .5rem 0; border-top: 1px solid var(--line); }
.movements li:first-child, .events li:first-child { border-top: 0; }
.movements .ref { font-weight: 600; min-width: 3rem; }
.movements .who { flex: 1; }
.events time { color: var(--grey); min-width: 11rem; }

@media (max-width: 34rem) {
  .search { grid-template-columns: 1fr; }
  .option { flex-direction: column; align-items: flex-start; }
  .option-price { text-align: left; }
  .bar { flex-direction: column; align-items: flex-start; gap: .4rem; }
}

@media print {
  .bar, .side, .skip, .grid-controls p, form, .legend, .hint { display: none; }
  .frame { display: block; }
  body { background: #fff; }
  .card { border: 0; padding: 0; }
}

/* Listings on the seasons and prices screen */
table.listing { width: 100%; border-collapse: collapse; margin: 0 0 1rem; font-size: .9rem; }
table.listing th, table.listing td { text-align: left; padding: .5rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; }
table.listing th { color: var(--grey); font-size: .85rem; font-weight: 550; }
table.listing form { margin: 0; }
table.listing button { padding: .3rem .6rem; font-size: .85rem; }
.card h3 { margin-top: 1.25rem; }

/* Stat tiles and the occupancy chart */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .75rem; margin: 0 0 1.25rem; }
.tile {
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .9rem 1rem; display: flex; flex-direction: column; gap: .1rem;
}
.tile-label { color: var(--grey); font-size: .85rem; font-weight: 550; }
.tile-value { font-size: 1.45rem; font-weight: 600; letter-spacing: -0.02em; }
.tile .hint { margin: 0; font-size: .82rem; }

svg.chart { width: 100%; height: auto; display: block; }
svg.chart .bar { fill: var(--accent); }
/* Months still filling up: the same hue, one step lighter, with the dashed
   divider and the caption carrying the meaning. Never a second categorical
   colour. At 65% this validates against the default accent (#4A5EDA): inside
   the lightness band, above the chroma floor, CVD separation 16.4 and
   normal-vision 17.3. Its contrast against the card is 2.7:1, which is only
   acceptable because the exact figures sit in the table directly below. A site
   with a very different accent should have this re-checked. */
svg.chart .bar.future { fill: color-mix(in srgb, var(--accent) 65%, white); }
svg.chart .grid { stroke: var(--line); stroke-width: 1; }
svg.chart .axis-line { stroke: var(--line); stroke-width: 1; }
svg.chart .divider { stroke: var(--grey); stroke-width: 1; stroke-dasharray: 3 3; }
svg.chart .axis { fill: var(--grey); font-size: 11px; font-family: inherit; }
svg.chart .peak { fill: var(--ink); font-size: 12px; font-weight: 600; font-family: inherit; }
table.listing tfoot td { border-top: 2px solid var(--line); border-bottom: 0; }
table.listing .hint { display: block; font-size: .8rem; }
table.listing tr.overdue td { background: #FDF3F3; }
/* A row with something to press in it sits on one line: with the cells top
   aligned, the words start at the top of the row while a 44px control sits in
   the middle of it, and the row reads as though it is falling over. */
table.listing.rows td, table.listing.rows th { vertical-align: middle; padding: .7rem .6rem; }
table.listing td.right { text-align: right; }
table.listing .col-dates { width: 11rem; }
table.listing .col-move { width: 9.5rem; }
table.listing .col-do { width: 5.5rem; }
/* Inline-flex, on a span inside the cell. `display: flex` on the <td> itself
   stops it being a table cell, and the row's own rule then stops dead at the
   column before it: the borders come apart and the row reads as misaligned. */
.pitch-move { display: inline-flex; align-items: center; gap: .45rem; }
.pitch-move .arrow { color: var(--grey); }
/* Where they are now, in every place the two pitches are shown together. */
.chip.to {
  border-color: color-mix(in srgb, var(--accent) 35%, white);
  background: color-mix(in srgb, var(--accent) 8%, white);
  color: var(--accent);
}
/* A note that explains the list rather than acting on it: it belongs below
   the buttons with room of its own, not tucked under them. */
.after-note { margin-top: 1.25rem; }
table.listing tr.overdue strong { color: var(--stop); }
.terms { border: 1px solid var(--line); border-radius: 8px; padding: .85rem 1rem; margin: 0 0 1rem; background: var(--ground); }
.terms .plain { margin: 0 0 .5rem; font-size: .93rem; }
.terms .check { margin: 0; }

/* Email bodies and issued keys: shown exactly as they are */
pre.email {
  white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9rem; background: var(--ground); border: 1px solid var(--line);
  border-radius: 8px; padding: 1rem; margin: 0 0 1rem;
}
form.inline { display: inline; margin-right: .4rem; }
.reason.setup { background: #FFF4D6; color: #6B4E00; border: 1px solid #E8D9A8; border-radius: 8px; padding: .7rem 1rem; }
.reason.setup a { color: #6B4E00; }

/* Jump straight to a month, rather than stepping a fortnight at a time */
form.jump { display: inline-flex; align-items: center; gap: .4rem; }
form.jump label { color: var(--grey); font-size: .9rem; }
form.jump select { width: auto; padding: .3rem .4rem; font-size: .9rem; }
form.jump button { padding: .3rem .7rem; font-size: .9rem; }

/* The year at a glance. A heat table, not a heat map: the shading gives the
   shape of the season and the number in each cell gives the answer, so nothing
   here has to be read off a shade. One hue, light to dark, monotonic in
   lightness; "nothing booked" is left unfilled so it differs in kind from
   "nearly nothing booked" rather than by a tint too slight to trust. */
table.season { border-collapse: collapse; table-layout: fixed; width: 100%; min-width: 46rem; font-size: .8rem; }
table.season th { color: var(--grey); font-weight: 550; padding: .2rem; text-align: center; }
table.season col.month-col { width: 6.5rem; }
table.season th[scope="row"] {
  position: sticky; left: 0; z-index: 1; background: var(--paper);
  text-align: left; padding-right: .5rem; white-space: nowrap; overflow: hidden;
}
table.season td { border: 1px solid var(--paper); padding: 0; text-align: center; height: 1.6rem; }
table.season td a {
  display: block; line-height: 1.6rem; text-decoration: none; color: inherit;
}
table.season td a:hover { outline: 2px solid var(--ink); outline-offset: -2px; }
table.season .heat-blank { background: transparent; border-color: transparent; }

.heat-0 { background: var(--ground); color: var(--grey); }
.heat-1 { background: color-mix(in srgb, var(--accent) 30%, white); color: var(--ink); }
.heat-2 { background: color-mix(in srgb, var(--accent) 55%, white); color: var(--ink); }
.heat-3 { background: color-mix(in srgb, var(--accent) 80%, white); color: var(--ink); }
.heat-4 { background: color-mix(in srgb, var(--accent) 80%, black); color: #fff; }
.heat-closed {
  background: repeating-linear-gradient(45deg, var(--ground), var(--ground) 3px, #E4E4E0 3px, #E4E4E0 6px);
}
.legend .swatch.heat-0, .legend .swatch.heat-closed { border: 1px solid var(--line); }

/* Grouping the grid by pitch type, so nine rows read as three hardstanding and
   six grass. The category is only named when a site has more than one. */
table.grid tr.group th {
  position: sticky; left: 0; z-index: 3;
  background: var(--ground); text-align: left; font-size: .85rem;
  padding: .35rem .6rem; border-top: 2px solid var(--line);
}
table.grid tr.group .hint { display: inline; margin-left: .4rem; font-weight: 400; }

/* What the grid says after a drag, in place, instead of sending you to another
   page for a change you made by nudging something two rows up. */
.grid-notice {
  margin: 0 0 .75rem; padding: .55rem .9rem; border-radius: 8px;
  background: color-mix(in srgb, var(--ok) 10%, white);
  border: 1px solid color-mix(in srgb, var(--ok) 35%, white); color: var(--ok);
  font-weight: 550;
}
.grid-notice.bad { background: #FBEAEA; border-color: #F0C8C8; color: var(--stop); }
/* A row the dragged booking's nights will not fit on. Greyed out rather than
   marked in red: you have not done anything wrong, that row simply cannot take
   it, and a warning colour for something the interface will not let you do
   reads as a telling off. The browser refuses the drop itself; this only says
   why before you try. */
table.grid tr.drop-blocked td,
table.grid tr.drop-blocked td.empty,
table.grid tr.drop-blocked .pitch-head { background: var(--ground); }
table.grid tr.drop-blocked .booking { opacity: .35; }
table.grid tr.drop-blocked .pitch-head { color: var(--grey); }
table.grid tr.drop-blocked .pitch-head .ref::after {
  content: ' occupied'; color: var(--grey); font-weight: 550; font-size: .8rem;
}

/* The booking screen. Eleven rows of equal weight in one column made you read
   all of it to find any of it, so the pitch, the dates and the length are a
   headline and the rest is grouped by the question it answers. */
.detail-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
.detail-head h2 { margin: 0 0 .25rem; }
.headline { font-size: 1.05rem; color: var(--ink); margin: 0; }
.headline .sep { color: var(--line); margin: 0 .35rem; border: 0; height: auto; }
.detail-columns {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.25rem 2rem; margin-top: 1.25rem;
  border-top: 1px solid var(--line); padding-top: 1rem;
}
.detail-columns h3 { font-size: .9rem; color: var(--grey); margin: 0 0 .4rem; }
.detail-columns h3 + p { margin: 0 0 .75rem; }
.detail-columns .summary { gap: .3rem .75rem; }

/* The front door. One field, so it sits where the eye lands rather than
   pinned to the top of an empty page. */
body.signin { display: grid; place-items: center; min-height: 100vh; }
body.signin .wrap { width: 100%; padding-bottom: 6rem; }
body.signin .card { padding: 1.75rem; }

/* Arrivals and departures: the list somebody prints and carries out to the
   gate. The pitch number leads, because that is what they are looking for. */
.movements li {
  display: grid; grid-template-columns: 3rem 1fr 9.5rem;
  gap: 1rem; align-items: baseline; padding: .7rem 0 .7rem .7rem;
  border-top: 1px solid var(--line);
}
.movements.with-money li { grid-template-columns: 3rem 1fr 9.5rem 7.5rem 17.5rem; }
/* The overview's arrivals: the same row with the button and no money box,
   which is the morning's job rather than the gate's. */
.movements.checking-in li { grid-template-columns: 3rem 1fr 9.5rem 8rem; }
.movements .seen { text-align: right; }
.movements .seen form { margin: 0; }
/* Somebody the site has not seen yet carries no mark of its own. The button
   at the end of the row already says it in words and in colour, "Check in"
   against a quiet green "Checked in", and the heading counts how many are
   left. It has had a wash fading out across the row, which ended in the
   middle of nowhere and read as a smudge, and then a bar down the edge, which
   every row carried until somebody arrived and so said nothing at half past
   eight in the morning. A third decoration for a fact already stated twice is
   not worth the ink. */
.movements li:first-child { border-top: 0; }
.movements .pitch-no {
  font-size: 1.05rem; font-weight: 600; font-variant-numeric: tabular-nums;
}
.movements .who a { color: var(--ink); text-decoration: none; font-weight: 550; }
.movements .who a:hover { text-decoration: underline; }
.movements .who .hint { display: block; margin: 0; }
.movements .owed { text-align: right; }
.movements .owed strong { color: var(--stop); }
.movements .contact { font-variant-numeric: tabular-nums; }
.movements .contact a { color: var(--ink); text-decoration: none; }
.movements .contact a:hover { color: var(--accent); text-decoration: underline; }

/* The arrivals row carries five things at the gate: pitch, guest, telephone,
   a button and a box to take money in. Below about a thousand pixels there is
   not room for all five on a line, and squeezing them leaves the guest's name
   wrapping a word at a time down a column two inches wide. They stack under
   the name instead, which is still one guest per block. */
@media (max-width: 62rem) {
  .movements.checking-in li, .movements.with-money li { grid-template-columns: 3rem 1fr; }
  .movements.checking-in .contact, .movements.with-money .contact,
  .movements.checking-in .seen, .movements.with-money .seen,
  .movements.with-money .owed { grid-column: 2; text-align: left; }
  .movements.with-money .take { justify-content: flex-start; }
}
@media (max-width: 40rem) {
  .movements li { grid-template-columns: 2.5rem 1fr; }
  .movements .contact, .movements .owed, .movements .seen { grid-column: 2; }
  .movements .seen { text-align: left; }
}

/* Things that happened to a booking, in order: payments taken and emails
   written. Both were borrowing the arrivals list, whose first column is three
   rem wide because a pitch number goes in it. A date does not fit in three
   rem, so "Sun, 30 Aug 2026" broke onto four lines, and the payments row had
   five things to put in four columns so the last one fell through to a line
   of its own. A date column the width of a date, and no more columns than
   there are things to say. */
.ledger { list-style: none; margin: 0; padding: 0; }
.ledger li {
  display: grid; gap: 1rem; align-items: baseline;
  padding: .7rem 0; border-top: 1px solid var(--line);
}
.ledger li:first-child { border-top: 0; }
.ledger time {
  color: var(--grey); white-space: nowrap; font-variant-numeric: tabular-nums;
}
.ledger .hint { display: block; margin: 0; font-size: .85rem; font-weight: 400; }

.messages li { grid-template-columns: 9.5rem 1fr auto; }
.messages .subject a { color: var(--ink); text-decoration: none; font-weight: 550; }
.messages .subject a:hover { text-decoration: underline; }
.messages .state { color: var(--grey); font-size: .9rem; text-align: right; }

/* The amount is what somebody is looking for, so it is the one thing set
   solid. Where it came from and who typed it in sit under the method. */
.payments li { grid-template-columns: 9.5rem 6rem 1fr; }
.payments .amount {
  font-weight: 600; font-variant-numeric: tabular-nums; text-align: right;
}
.payments .amount.refund { color: var(--stop); }

.events li { grid-template-columns: 9.5rem 1fr; }
.events .what { font-weight: 550; }

/* The button under a list, which otherwise sits straight against the last
   row and reads as part of it. */
.after { margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.after .hint { margin-bottom: 0; }

@media (max-width: 40rem) {
  .messages li { grid-template-columns: 1fr auto; }
  .payments li { grid-template-columns: 6rem 1fr; }
  .ledger time { grid-column: 1 / -1; }
}

/* The page a guest lands on while the site takes its own bookings. The
   telephone number is the whole point of it, so it is the biggest thing on
   the page and it dials. */
.card.call { text-align: center; padding: 2rem 1.5rem; }
.card.call h2 { margin-bottom: 1rem; }
.call-number {
  display: inline-block; font-size: 1.65rem; font-weight: 600; letter-spacing: -0.01em;
  color: var(--accent); text-decoration: none; padding: .25rem .5rem; border-radius: 8px;
}
.call-number:hover { background: color-mix(in srgb, var(--accent) 8%, white); }
.call-or { color: var(--grey); margin: .25rem 0 1rem; }
.card.call .hint { max-width: 30rem; margin: 0 auto; }

@media (max-width: 34rem) {
  .call-number { font-size: 1.8rem; }
}

/* Extras: a short list where the answer is usually none, so the number sits
   at the end of the line rather than under a label of its own. */
.extras { display: grid; gap: .25rem; }
.extra {
  display: grid; grid-template-columns: 1fr 5rem; gap: 1rem; align-items: center;
  padding: .5rem 0; border-top: 1px solid var(--line); cursor: pointer;
}
.extra:first-child { border-top: 0; }
.extra-name { font-weight: 550; }
.extra-name .hint { display: block; font-weight: 400; margin: 0; }
.extra input { text-align: center; }
.extra.yesno input { width: 1.25rem; height: 1.25rem; min-height: 0; justify-self: center; accent-color: var(--accent); }

/* Finding somebody who has stayed before. Sits above the name field, so it
   reads as "or start typing a name below" rather than as a required step. */
.lookup { position: relative; margin-bottom: 1rem; }
.lookup .field { margin: 0; }
.lookup-results {
  position: absolute; left: 0; right: 0; top: calc(100% - .5rem); z-index: 20;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 6px 24px rgba(26, 26, 26, .12); overflow: hidden;
}
.lookup-hit {
  display: block; width: 100%; text-align: left; cursor: pointer;
  padding: .6rem .8rem; background: none; border: 0;
  border-top: 1px solid var(--line); font: inherit; color: var(--ink);
}
.lookup-hit:first-child { border-top: 0; }
.lookup-hit:hover, .lookup-hit:focus-visible { background: var(--ground); }
.lookup-hit strong { display: block; }
.lookup-hit span { display: block; font-size: .85rem; color: var(--grey); }
.lookup-reach { font-variant-numeric: tabular-nums; }
.lookup-none { margin: 0; padding: .6rem .8rem; font-size: .9rem; color: var(--grey); }
.lookup-chosen { margin: .5rem 0 0; font-size: .9rem; color: var(--ok); }

/* Guests. A person's name is the heading of their booking, and a heading that
   is also a link should not look like one until you go near it. */
.plainlink { color: inherit; text-decoration: none; }
.plainlink:hover { text-decoration: underline; text-decoration-color: var(--accent); }

table.listing td.num, table.listing th.num { text-align: right; font-variant-numeric: tabular-nums; }
table.listing td.wrap { word-break: break-word; }
/* A cancelled stay stays on the list, because it happened, but it is not one
   of the times they came. */
table.listing tr.cancelled td { color: var(--grey); }
table.listing tr.cancelled a { color: var(--grey); }

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding-top: .75rem; border-top: 1px solid var(--line);
}
.pager .step { min-width: 6rem; }
.pager .hint { margin: 0; }

/* Editing an email beside what a guest reads. The point is seeing both at
   once: a page of curly braces does not tell you how the chasing email sounds
   to somebody who is late paying. */
.beside { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start; }
.beside .card { margin: 0 0 1rem; }
.beside textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9rem; }
@media (max-width: 62rem) { .beside { grid-template-columns: 1fr; } }

.email-subject {
  margin: 0 0 .75rem; padding-bottom: .75rem; border-bottom: 1px solid var(--line);
  font-weight: 550;
}
.email-subject .label { display: block; font-weight: 550; }
.email-card .headline { color: var(--grey); }

.placeholders { list-style: none; margin: 0; padding: 0; columns: 2; }
.placeholders li { break-inside: avoid; padding: .2rem 0; }
.placeholders code { font-size: .9rem; }
.placeholders .hint { display: inline; margin-left: .4rem; }
@media (max-width: 48rem) { .placeholders { columns: 1; } }

/* The overview. Four counts big enough to read across a kitchen, then the
   jobs, then the ways through to everything else. */
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; }
.tile {
  display: block; padding: 1rem; text-align: center; text-decoration: none;
  background: var(--ground); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--ink);
}
.tile:hover { border-color: var(--accent); }
.tile-value {
  display: block; font-size: 1.9rem; font-weight: 600; line-height: 1.1;
  font-variant-numeric: tabular-nums; color: var(--accent);
}
.tile-label { display: block; margin-top: .25rem; color: var(--grey); }
@media (max-width: 46rem) { .tiles { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 30rem) { .tiles { grid-template-columns: 1fr; } }


/* ---- The overview -----------------------------------------------------
   The day's work down the middle as a list of people you can act on, and the
   numbers in a rail beside it. It was four big counts across the top with the
   work underneath, which put a row of figures nobody acts on between somebody
   and the thing they opened the page to do. */
.page-head {
  display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start;
  justify-content: space-between; margin: 0 0 1.5rem;
}
.page-head h1 { margin: 0; font-size: 1.45rem; }
.page-head .hint { margin: .15rem 0 0; }
.page-do { display: flex; flex-wrap: wrap; gap: .6rem; margin: 0; }

.dash { display: grid; grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr); gap: 1.5rem; align-items: start; }
.dash .card { margin: 0 0 1rem; }
.dash-side .card:last-child { margin-bottom: 0; }
@media (max-width: 80rem) { .dash { grid-template-columns: 1fr; } }

/* A heading with its own count beside it. "Arrivals, 3 still to check in"
   answers in one line what a card containing the figure 3 answered in a
   quarter of the screen. */
.group { margin: 0 0 1.75rem; }
.section-head {
  display: flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: baseline;
  justify-content: space-between; margin: 0 0 .6rem;
}
.section-head h2 { margin: 0; font-size: 1.02rem; }
.section-head .hint { margin: 0; }
.empty {
  margin: 0; padding: 1.1rem; border: 1px dashed var(--line); border-radius: var(--radius);
}

/* One person, one card. What somebody does here is deal with one arrival at a
   time, and a block with its own edge and its own button is easier to keep
   your place in than a rule between two lines of text. The columns are
   declared and fixed, never auto: a row grid goes on the row, so every row
   works its columns out from its own contents unless it is told otherwise,
   and then a guest owing £280 pushes their telephone number somewhere the
   row below does not have it. */
.people { list-style: none; margin: 0; padding: 0; }
.person {
  display: grid; grid-template-columns: 3.25rem minmax(0, 1fr) 9rem 7.5rem;
  gap: 1rem; align-items: center;
  padding: .85rem 1rem; margin: 0 0 .5rem;
  background: var(--paper); border: 1px solid var(--line); border-radius: var(--radius);
}
.person:last-child { margin-bottom: 0; }
.person-who a { color: var(--ink); text-decoration: none; font-weight: 550; }
.person-who .hint { display: block; margin: 0; font-size: .85rem; }
.person-money { text-align: right; }
.person-money .hint { margin: 0; }
.person-do { display: flex; justify-content: flex-end; position: relative; }
/* The whole card opens the booking. The name is still the only anchor: its
   ::after is stretched over the card, so what the browser has is one ordinary
   link that a middle click opens in a tab. The button is the one thing on the
   card that does something of its own, so it sits above the stretch, and the
   focus ring goes on the stretch rather than on the name, because the thing
   being focused is now the card. */
.person { position: relative; }
.person-who a::after {
  content: ''; position: absolute; inset: 0; border-radius: var(--radius);
}
.person:hover {
  border-color: color-mix(in srgb, var(--accent) 30%, var(--line));
  background: color-mix(in srgb, var(--accent) 4%, var(--paper));
}
.person:hover .person-who a { text-decoration: underline; }
.person-who a:focus-visible { text-decoration: underline; outline: none; }
.person-who a:focus-visible::after {
  outline: 3px solid color-mix(in srgb, var(--accent) 40%, white); outline-offset: 1px;
}
.person-do form { margin: 0; }
.person-do button { width: 100%; min-height: 2.5rem; padding: .4rem .7rem; }

/* A pitch number, and what is owed. Both are short facts that want to be
   found rather than read, so they get an edge of their own. */
.chip {
  display: inline-flex; align-items: center; justify-content: center;
  padding: .2rem .5rem; border-radius: 6px; font-size: .9rem; font-weight: 600;
  border: 1px solid var(--line); background: var(--ground);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.chip.pitch { justify-self: start; min-width: 2.6rem; }
.chip.owing {
  color: var(--stop); border-color: color-mix(in srgb, var(--stop) 35%, white);
  background: color-mix(in srgb, var(--stop) 7%, white);
}

/* The exceptions. It has to be able to say "nothing" and mean it, so it is
   the same card either way; when there is something on it the heading carries
   how many, which is worth knowing, rather than a coloured edge, which is
   not. */
.count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.55rem; height: 1.55rem; padding: 0 .4rem; margin-left: .45rem;
  border-radius: 99px; background: var(--warn); color: #fff;
  font-size: .85rem; font-weight: 600; vertical-align: 2px;
  font-variant-numeric: tabular-nums;
}
/* One thing to do, and underneath it the thing to press. Side by side, the
   sentence wrapped around a column of links and the eye had to come back for
   each one; stacked, it reads as a job with its answer under it. The whole
   block is the link, so the target is the card and not four words of it.
   An older rule gave these a bullet in their left padding, which after this
   became a block left a stray dot on a line of its own. */
.jobs { list-style: none; margin: 0; padding: 0; }
.jobs li { border-top: 1px solid var(--line); }
.jobs li:first-child { border-top: 0; }
.jobs a {
  display: block; padding: .7rem 0; color: var(--ink); text-decoration: none;
  line-height: 1.45;
}
.jobs a:hover { color: var(--accent); }
.job-do {
  display: block; margin-top: .3rem; color: var(--accent); font-weight: 550;
}
.jobs a:hover .job-do { text-decoration: underline; }

/* A figure in the rail: a small label above it so the number is the thing you
   see, and the sentence that qualifies it underneath. */
.stat + .stat { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--line); }
.stat-label {
  margin: 0; color: var(--grey); font-size: .74rem; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
}
.stat-value {
  margin: .1rem 0 .15rem; font-size: 1.6rem; font-weight: 600; line-height: 1.1;
  letter-spacing: -0.02em; font-variant-numeric: tabular-nums;
}
.stat .hint { margin-bottom: 0; }
/* The sentence that qualifies the whole card rather than any one figure in
   it, so it is set apart from the last of them. */
.footnote { margin-top: .9rem; padding-top: .9rem; border-top: 1px solid var(--line); }
/* Already done, and the button undoes it. Quiet, ticked, and not a second
   thing competing with the ones still to do. */
button.done {
  border-color: color-mix(in srgb, var(--ok) 35%, white); color: var(--ok);
  background: color-mix(in srgb, var(--ok) 7%, white);
}
.meter {
  height: .45rem; margin: .8rem 0 0; border-radius: 99px; background: var(--line);
  overflow: hidden;
}
.meter span { display: block; height: 100%; background: var(--accent); }

.lately { list-style: none; margin: 0; padding: 0; }
.lately li { padding: .5rem 0; border-bottom: 1px solid var(--line); }
.lately li:last-child { border-bottom: 0; padding-bottom: 0; }
.lately .hint { display: block; margin: .1rem 0 0; }
.lately a { color: var(--ink); text-decoration: none; font-weight: 550; }
.lately a:hover { text-decoration: underline; }
.lately li.cancelled a { text-decoration: line-through; color: var(--grey); }

/* A site's own mark in the header. Sized by height so any shape of logo sits
   on the same baseline as the navigation beside it, and capped in width so a
   long one cannot push the nav off the row. */
.bar .mark { display: inline-flex; align-items: center; }
.side .site-logo { height: 2.5rem; max-width: 100%; }
.site-logo {
  display: block; height: 2.9rem; width: auto; max-width: 14rem;
}
@media (max-width: 40rem) { .site-logo { height: 2.3rem; } }

/* On the way in there is nothing else on the page, so it can be itself. */
body.signin .site-logo { height: auto; width: 100%; max-width: 15rem; margin: 0 auto 1.25rem; }
body.signin .card { text-align: center; }
body.signin .form, body.signin .actions { text-align: left; }

@media print { .site-logo { height: 1.6rem; } }

/* The sent-items folder. The status is the reassurance, so it reads plainly
   rather than as a coloured pill, and a failure is the only thing that shouts. */
table.listing td.state .hint { display: block; }
table.listing td.state.failed { color: var(--stop); font-weight: 550; }
table.listing td.state.queued { color: var(--warn); }
table.listing td.state.sent { color: var(--ok); }
table.listing td.state.not_sent { color: var(--grey); }
table.listing time { white-space: nowrap; }

/* A second, quieter figure under a count: the seasonal pitches are the same
   number most nights and change twice a year, so they are background rather
   than news, and adding them to the touring count would answer nothing. */
.tile-note { display: block; margin-top: .2rem; font-size: .85rem; color: var(--grey); }

/* Landing on somebody is an exchange, not a refusal. It gets its own colour
   because it is a different act from a move: two guests change places rather
   than one moving into a gap. */
table.grid tr.drop-swap td { background: color-mix(in srgb, var(--accent) 10%, white); }
table.grid tr.drop-swap .booking { outline: 2px solid var(--accent); outline-offset: -2px; }

/* A guest's headline figures. Three, not four: what they have been worth, how
   often they have come, and whether they owe anything. */
.tile.owing .tile-value { color: var(--stop); }

/* Taking the money where you are standing. The amount is filled in, so the
   common case is one press, and it is still editable for the guest who pays
   half of it in cash. */
.take { display: flex; gap: .3rem; align-items: center; margin: 0; }
.take { justify-content: flex-end; }
.take input[name="amount"] { width: 5rem; text-align: right; }
.take select { width: 6rem; }
.take input, .take select { min-height: 2.25rem; padding: .3rem .4rem; font-size: .9rem; }
.take button.small { min-height: 2.25rem; white-space: nowrap; }
@media (max-width: 52rem) { .take { flex-wrap: wrap; justify-content: flex-end; } }

/* A label that is there for a screen reader and not for the eye: every one of
   these inputs sits under a column heading that already says what it is. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (max-width: 40rem) {
  .person { grid-template-columns: 3.25rem minmax(0, 1fr); }
  .person-money, .person-do { grid-column: 2; justify-content: flex-start; text-align: left; }
  .person-do button { width: auto; }
}

/* ---- The booking panel -------------------------------------------------
   A booking slides in from the side of the calendar rather than taking you to
   another screen. Nothing on the page moves to make room for it: it comes in
   over the top, because the fortnight underneath is the thing you were
   looking at and pushing it sideways would answer one question by losing
   another.

   It is inert until it is opened, and the links that fill it are real links,
   so a browser with no JavaScript never reaches any of this. */
.panel { position: fixed; inset: 0; z-index: 40; }
.panel[hidden] { display: none; }

.panel-scrim {
  position: absolute; inset: 0; background: rgba(26, 26, 26, .28);
  opacity: 0; transition: opacity .26s ease;
}
.panel.open .panel-scrim { opacity: 1; }

.sheet {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(30rem, 100%);
  display: flex; flex-direction: column;
  background: var(--paper); border-left: 1px solid var(--line);
  box-shadow: -16px 0 40px rgba(26, 26, 26, .12);
  transform: translateX(100%);
  /* Out quicker than in. A panel that leaves slowly feels stuck; one that
     arrives slightly slower than it leaves feels like it was placed. */
  transition: transform .26s cubic-bezier(.32, .72, 0, 1);
  overflow: hidden;
}
.panel.open .sheet { transform: translateX(0); }
.sheet:focus { outline: none; }
.sheet:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.sheet-head {
  display: flex; gap: 1rem; align-items: flex-start; justify-content: space-between;
  padding: 1.1rem 1.25rem; border-bottom: 1px solid var(--line);
}
/* The name, and beside it a pill saying what state the booking is in: Owen's
   call on 20 September 2026, over the small quiet mark that came before it.
   Confirmed and departed are the quiet ones and wear the grey; only held and
   the two dead states borrow a colour, so a coloured pill still means
   something wants looking at. The word is never the only signal: the foot of
   the panel says the same thing in a sentence. */
.sheet-name {
  display: flex; gap: .5rem; align-items: baseline; flex-wrap: wrap;
  margin: 0 0 .15rem; font-size: 1.15rem; font-weight: 600;
}
.sheet-name .state {
  padding: .1rem .5rem .14rem; border-radius: 99px;
  background: color-mix(in srgb, var(--ink) 7%, white);
  color: color-mix(in srgb, var(--ink) 78%, white);
  font-size: .72rem; font-weight: 550; font-style: normal;
  letter-spacing: .01em; text-transform: none; white-space: nowrap;
}
.sheet-name .state.pending {
  color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, white);
}
.sheet-name .state.cancelled, .sheet-name .state.expired {
  color: var(--stop); background: color-mix(in srgb, var(--stop) 10%, white);
}
.sheet-head .headline { margin: 0; color: var(--grey); font-size: .85rem; }
.sheet-close {
  flex: none; min-height: 2.25rem; width: 2.25rem; padding: 0;
  border-radius: 8px; color: var(--grey);
}
.sheet-close:hover { color: var(--ink); }

/* The bottom of the sheet has to breathe. Everything below the guest is a
   row the same height as the row above it, and with the pinned foot under
   them the last one had nowhere to sit: the padding here is what keeps "Open
   this as a page" off the button. */
.sheet-body { padding: 1.1rem 1.25rem 1.75rem; overflow-y: auto; flex: 1; }
.sheet-body p { margin: 0; }
/* A link at the foot of a band takes its rule from the fold below it rather
   than drawing a second hairline an inch above one. */
.sheet-body .after { margin-top: 1rem; padding-top: 0; border-top: 0; }

/* A labelled band: a small grey heading with its own block under it. The
   panel is a stack of them and they are what make it scannable. */
.band + .band { margin-top: 1.25rem; }
.band + .fold { margin-top: 1.6rem; }
.band + .band { padding-top: 1.25rem; border-top: 1px solid var(--line); }
.band h3 {
  margin: 0 0 .55rem; color: var(--grey); font-size: .72rem; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
}

/* The nights are changed in the band the nights are in, because everything
   about a booking is doable in the panel and the dates were the one thing
   that still sent somebody into the long form below.
   It is a <details>: Edit opens it with no JavaScript at all, and the box
   gives way to the form rather than sitting above it saying something that is
   about to stop being true. */
.stay-band summary {
  display: flex; align-items: center; justify-content: space-between; gap: .75rem;
  min-height: 2.75rem; margin-top: -.6rem; cursor: pointer; list-style: none;
}
.stay-band summary::-webkit-details-marker { display: none; }
.stay-band summary h3 { margin: 0; }
.stay-band summary:hover .edit-mark { text-decoration: underline; }
.edit-mark { color: var(--accent); font-size: .85rem; font-weight: 550; }
.edit-dates summary .showing { display: none; }
.edit-dates[open] summary .shut { display: none; }
.edit-dates[open] summary .showing { display: inline; }
.edit-dates[open] + .stay { display: none; }
.stay-dates { margin: .35rem 0 0; max-width: none; }
.stay-dates .actions { margin-top: .9rem; padding-top: .9rem; }
/* The nights somebody typed will not fit where they are. This is not a fault
   and it is not a refusal, so it does not take the red: it is a decision with
   a consequence, and the amber ground is there to hold the reason and the
   choice together as one thing rather than two sentences floating in a form.
   The consequence itself is said on the button, because a colour cannot say
   "and move them". */
.needs-a-pitch {
  background: color-mix(in srgb, var(--warn) 9%, white);
  border-radius: var(--radius); padding: .8rem .85rem;
}
.needs-a-pitch .mismatch { margin: 0 0 .7rem; }
.needs-a-pitch select { width: 100%; }
.needs-a-pitch .label { color: var(--ink); }

/* One box, two cells and a rule. The dates are two halves of one fact, each
   on a single line with its weekday on it, and the nights and the heads are
   the question the space between them asks. The labels inside the box are
   sentence case: the uppercase belongs to the band heading above it, and
   having both shouted made the box compete with it. */
.stay {
  display: grid; grid-template-columns: 1fr 1fr;
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
}
.stay p { margin: 0; }
.stay-cell { padding: .55rem .8rem .65rem; }
.stay-cell + .stay-cell { border-left: 1px solid var(--line); }
.stay-label { color: var(--grey); font-size: .78rem; }
.stay-date { font-size: .98rem; font-weight: 550; font-variant-numeric: tabular-nums; }
.stay-facts {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 1.25rem;
  margin: 0; padding: .5rem .8rem .55rem; border-top: 1px solid var(--line);
  color: var(--grey); font-size: .88rem;
}
.stay-facts span { display: inline-flex; align-items: center; gap: .4rem; }
.stay-facts .door-icon, .pitch-line .door-icon, .pitch-pick .door-icon,
.guest-lines .door-icon {
  width: 16px; height: 16px; flex: none; color: var(--grey);
}

.pitch-line { display: flex; align-items: center; gap: .5rem; }
.pitch-line strong { font-weight: 600; }

/* The allocation, and the pitches it could be instead. Picking one moves
   them, so the list holds only pitches that are free for these nights and
   that this rate may be sold on. The Move button beside it is for a browser
   running none of this; the panel hides it once the change event is taken
   over. */
.pitch-pick { display: flex; align-items: center; gap: .55rem; margin: 0; }
.pitch-pick select { flex: 1 1 auto; min-width: 0; }
.pitch-pick button { flex: none; }
/* Where they are is not where this rate is sold. A note, never a refusal:
   staff may put anybody anywhere, deliberately. */
.mismatch { margin: .55rem 0 0; color: var(--warn); font-size: .88rem; }

/* A column of figures is read by running an eye down the pence, which only
   works if they are above one another. The label takes the room it needs on
   the left and the figure goes hard against the right edge. */
/* The column gap is padding inside the figure rather than a gap between the
   two halves, so the rule above the balance runs the whole width instead of
   breaking in the middle of the band. */
.summary.figures { grid-template-columns: auto 1fr; gap: .4rem 0; margin: 0; }
.summary.figures dd {
  text-align: right; padding-left: 1rem; font-variant-numeric: tabular-nums;
}
/* What is owed is the answer to the two figures above it, not a third of
   them. */
.summary.figures .answer {
  margin-top: .2rem; padding-top: .5rem; border-top: 1px solid var(--line);
}
.summary.figures dt.answer { color: var(--ink); font-weight: 600; }
.summary.figures dd .hint { display: block; margin: 0; font-size: .8rem; }
.summary.figures dd.owing { color: var(--stop); font-weight: 600; }

.take-money { display: flex; flex-wrap: wrap; gap: .6rem; align-items: end; margin: .9rem 0 0; }
.take-money .field { flex: 1 1 6rem; min-width: 0; }
.take-money .field:nth-of-type(2) { flex: 0 0 8.5rem; }
.take-money button { flex: none; }

.guest-lines { list-style: none; margin: 0; padding: 0; }
.guest-lines li {
  display: flex; gap: .55rem; align-items: baseline; padding: .18rem 0;
  word-break: break-word;
}
.guest-lines a { color: var(--ink); text-decoration: none; }
.guest-lines a:hover { color: var(--accent); text-decoration: underline; }
.guest-lines .hint { margin: 0; }
.plainlink.arrow::after { content: " \2192"; }

.said-note { margin: .9rem 0 0; font-size: .9rem; line-height: 1.5; }
.said-note strong { color: var(--grey); font-weight: 550; }

/* Everything that is not the daily job folds away. <details> opens and closes
   with no JavaScript, so the panel is a working page even if the fetch that
   normally fills it never happens. */
.fold { border-top: 1px solid var(--line); }
.fold summary {
  display: flex; gap: .75rem; align-items: baseline; justify-content: space-between;
  padding: 1.05rem 0; cursor: pointer; font-weight: 550; list-style: none;
}
.fold summary::-webkit-details-marker { display: none; }
.fold summary::after {
  content: ""; flex: none; width: .45rem; height: .45rem; align-self: center;
  border-right: 2px solid var(--grey); border-bottom: 2px solid var(--grey);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform .15s ease;
}
.fold[open] summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.fold summary .hint { margin: 0; }
.fold summary:hover { color: var(--accent); }
.fold-body { padding: .1rem 0 1.35rem; }
.fold-body .form { max-width: none; }
.fold-body .form-section.bare { margin-top: 0; }
.fold-body .form-section.bare + .form-section.bare {
  margin-top: .9rem; padding-top: .9rem; border-top: 1px dashed var(--line);
}
.fold-body .actions { margin-top: .9rem; padding-top: .9rem; }
/* Payments in the panel: a date, an amount, and how it was paid. Messages
   have two things in a row and not three, and taking the payments columns put
   the subject in a 5rem lane, so "Your booking at Pen y Bont Caravan and
   Camping Site" came out one word to a line. Same rule as the lists on their
   own pages: no more columns than there are things to say. */
.fold-body .ledger li { grid-template-columns: 4.5rem 5rem 1fr; }
.fold-body .panel-messages li { grid-template-columns: 4.5rem 1fr; }
/* And the subject reads as the thing it is, not as a link in a paragraph:
   ink, like every other link in the sheet. */
.fold-body .panel-messages .detail a {
  color: var(--ink); text-decoration: none; font-weight: 550;
}
.fold-body .panel-messages .detail a:hover { color: var(--accent); text-decoration: underline; }

.sheet-page {
  margin: 1.5rem 0 0; padding-top: 1.1rem; border-top: 1px solid var(--line);
  font-size: .85rem; line-height: 1.5;
}
.sheet-page .hint { display: inline; margin: 0; }

/* The one thing you came to do, pinned to the bottom while the rest scrolls.
   On an arrival morning it is the same button forty times. */
.sheet-foot {
  display: flex; gap: .9rem; align-items: center;
  padding: 1rem 1.25rem; border-top: 1px solid var(--line); background: var(--ground);
}
.sheet-foot form { margin: 0; }
.sheet-foot button { min-width: 8rem; }
.sheet-foot .hint { margin: 0; }

/* The dialog every question is asked in. It sits in the browser's top layer,
   so it is above the sheet and needs no z-index of its own. The two buttons
   say what they do, which is the whole reason it exists: OK and Cancel cannot
   tell you which way round "Swap them over?" is. */
.ask {
  width: min(28rem, calc(100vw - 2rem)); padding: 0;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--paper); color: var(--ink);
  box-shadow: 0 18px 44px rgba(26, 26, 26, .22);
}
.ask::backdrop { background: rgba(26, 26, 26, .35); }
.ask form { margin: 0; padding: 1.25rem 1.35rem 1.1rem; }
.ask-title { margin: 0 0 .85rem; font-size: 1.1rem; font-weight: 600; }
.ask-body p { margin: 0; line-height: 1.5; }
.ask-line + .ask-line, .ask-line { margin-top: .5rem; color: var(--grey); }
.ask-line:first-child { margin-top: 0; }

/* What is about to happen, drawn rather than described: the name over its
   nights, and the two pitches beside them with an arrow between. Three
   sentences of equal weight have to be read; this is read at a glance and the
   words underneath are only there to check it. */
.ask-moves {
  border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
}
.ask-move {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: .1rem 1rem; align-items: center; padding: .7rem .85rem;
}
.ask-move + .ask-move { border-top: 1px solid var(--line); }
.ask-who { font-weight: 550; }
.ask-when { grid-column: 1; color: var(--grey); font-size: .85rem; }
.ask-pitches {
  grid-column: 2; grid-row: 1 / span 2;
  display: flex; align-items: center; gap: .45rem;
}
.ask-arrow { color: var(--grey); }
/* Two numbers beside a pair of dates read as more dates, so they are told
   what they are. */
.ask-label { color: var(--grey); font-size: .85rem; margin-right: .1rem; }

.ask-flag { margin-top: .85rem; color: var(--warn); font-size: .9rem; }
.ask-note { margin-top: .85rem; color: var(--grey); font-size: .9rem; }

.ask-actions {
  display: flex; gap: .75rem; justify-content: flex-end; flex-wrap: wrap;
  margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--line);
}
/* The one that does the thing is on the right, where the eye finishes. */
.ask-actions button { min-width: 7rem; }

button.working { opacity: .6; }

.said {
  margin: 0 0 1rem; padding: .6rem .8rem; border-radius: 8px;
  background: color-mix(in srgb, var(--ok) 9%, white); color: var(--ok);
  font-size: .9rem;
}


/* The page behind it does not scroll while it is open: two scrolling things
   at once and the wheel belongs to whichever the pointer happens to be over. */
body.panelled { overflow: hidden; }

/* On a narrow screen it is the screen. Sliding a full-width sheet sideways
   still reads as "this came from somewhere and can go back". */
@media (max-width: 34rem) {
  .sheet { width: 100%; border-left: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .panel-scrim, .sheet { transition: none; }
}
