/* The clubhouse skin: Greenjacket in daylight.

   One sheet, TWO scopes, and the difference between them is the whole point.

   `body.linen` is the SKIN — the ground, the cards, the type colours, the
   buttons, the sheet, the date rail, the time board, the details card, the
   summary bar, the ticket. It is on EVERY page this Worker serves now:
   /my/ carries it from its markup, and so does the booking page, for a guest
   who has never signed in as much as for a member. The club is one product
   and it looks like one.

   `body.member` is the CHROME — the three things that are only true inside
   the member's own app: our marketing header, hero and footer are hidden
   because the shell above already says where the member is; the member
   header, the tab bar and the ticket's way home are built by app.js; and the
   summary bar is lifted clear of the tab bar. A guest's page carries none of
   it. /my/ carries both.

   Splitting them is what let the skin stop being a member's privilege. Every
   selector below begins with one root or the other, and
   test/member-theme.test.js fails the build if a third one ever appears —
   an unscoped rule here would reach the admin pages, which have their own.

   The reference is a studio's client portal — the brand's own linen, white
   cards, one bold button, quiet grey beside it. The faces and the green are
   Greenjacket's own and the accent is the brand slate. Nothing here loads
   from anywhere: every page already serves its faces from /fonts/, and every
   icon is inline SVG in the markup.

   The ground is the brand bible's linen #ecead5, not a paler tint of it,
   which costs about 0.4 of a ratio point against every caption that sits
   straight on it. Two tokens were darkened to pay for that — muted from
   #6b6f62 (4.2 on linen, under the floor) and accent from #4e6c88 (4.5 on
   the nose, with nothing in hand). Every text-on-background pair clears
   4.5:1, measured on the ground it actually sits on:

     ink on linen 13.9    ink on white 16.8    ink-600 on linen 7.0
     muted on linen 5.2   muted on white 6.4   accent on linen 5.4
     accent on white 6.6  danger on white 5.1  ink-600 on white 8.5
     linen on reserve 9.9 (the primary button) ink-600 on a booked cell 7.0

   And the Wallet's packages, which are tiers on white tiles now: the saving
   --ok on white 6.2, the rate --muted on white 6.4, --muted on the segment
   track (the page at .06) 4.7, --ink on the chosen segment 16.8, and white
   on the BEST VALUE badge 6.6.

   Danger is the one token that does NOT clear the floor on the ground —
   #b5502f is 4.2 on linen — and it never lands there: every red word on
   any of these pages is inside a white card (a chip, a reason, a deadline,
   the ticket's error line), where it is 5.1.

   A white card is 1.21:1 against linen and was 1.10 against the tint, so
   cards and the tab bar separate from the page BETTER here, not worse; the
   hairline composites to 1.16 either way.

   The sheet loads after my.css and after styles.css, and `body.linen .x`
   outranks `.x`, so this file only has to say what changes. Four names both
   older sheets already use — --muted, --line, --linen-400 and --danger — are
   redefined here rather than overridden rule by rule: every one of their uses
   lands on the light ground under this skin, so one line each re-tones a
   hundred borders and captions that would otherwise be a hundred rules. */

body.linen {
  /* the ground and the card. --paper is the name every rule below already
     points at; what it holds is the brand's linen. */
  --paper:#ecead5;
  --card:#ffffff;
  --hairline:rgba(28,30,23,.08);
  --card-shadow:0 8px 24px rgba(20,31,14,.06);
  --radius:18px;
  --radius-tile:14px;
  --card-pad:22px;
  --gap:12px;

  /* type */
  --ink:#1c1e17;
  --ink-600:#4a4e42;
  /* Darkened from #6b6f62 when the ground became linen: the old grey was
     4.2:1 on it, and this is the caption colour under every list on every
     page, most of which sit on the page and not on a card. */
  --muted:#5d6156;

  /* the one bold button: the club's green where the reference used terracotta */
  /* The lighter fairway sage, chosen by the owner on 2026-09-13 over the
     deep reserve. Linen on it is 4.9:1. */
  --primary:#4e6342;
  --primary-ink:#ecead5;

  /* links, the active tab, the for-you rule, a tile's underline. The brand
     slate #4e6c88 was exactly 4.52:1 on linen — a pass with nothing in hand,
     and link text is what a member is asked to aim a thumb at, so it is
     taken down a step to hold 5.4 there and 6.6 on a card. */
  --accent:#45607a;

  /* The sage page's #e08563 is 3.2:1 on white, so daylight gets its own red.
     It is 4.2 on linen and 5.1 on a card, and every red word on any of these
     pages is inside a card — see the note at the top of this file. */
  --danger:#b5502f;

  /* Good news that is not a price: what a bigger package saves per hour. The
     club's own green would be the primary, which is the button under it; this
     is a deeper leaf green, 6.2:1 on the white tile it always sits on. In the
     dark it becomes the slate tint, because a green word on a green page is
     not a word. */
  --ok:#3b6d11;

  /* redefined for every rule the older sheets already point at these names */
  --line:rgba(28,30,23,.08);
  --linen-400:#5d6156;
}

/* The tab bar's own arithmetic. Member chrome, not skin: the bar exists only
   inside the member's app, and so does everything that has to stand clear of
   it. A guest's booking page has no bar and reserves nothing for one. */
body.member {
  /* 62px of controls plus the phone's own home indicator, which would
     otherwise sit on top of the Profile tab. Defined here because the
     booking page never loaded my.css, where /my/ first declared it. */
  --tabs:calc(62px + env(safe-area-inset-bottom));
  /* The hairline the bar rules itself off with. A pixel is a pixel until it
     is the pixel between a tab and the Confirm button underneath it. */
  --tab-edge:1px;

  /* How much of the bottom of the window the tab bar has spoken for, as ONE
     number that everything standing clear of it measures against. Flush to
     the bottom edge that is the controls, the home indicator, and the single
     hairline across the top. At the pill breakpoint the bar lifts off the
     edge and is boxed on all four sides, so the reserve grows — see the
     @media below, the only place this is overridden. Anything that has to
     sit above the bar derives its offset from this and never from --tabs, or
     the two drift apart the moment the pill changes shape. */
  --tabs-reserve:calc(var(--tabs) + var(--tab-edge));
}

/* ---- the ground ---- */

body.linen {
  background:var(--paper);
  background-image:none;
  color:var(--ink);
}
body.linen a { color:var(--accent); }
body.linen a:hover { color:var(--primary); }
body.linen :focus-visible { outline:2px solid var(--accent); outline-offset:3px; }

/* ---- the header the member's two pages share ----
   /my/ writes it into the document; the booking page's member mode builds
   the same element, so there is one set of rules and not two. Chrome: a
   guest's page never has one. */

/* Written out rather than adjusted: /my/ has my.css underneath the skin to
   lean on and the booking page has nothing at all, so anything left implicit
   here is a header that falls apart on one of the two pages it dresses. */
body.member .top {
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  width:min(720px,100%); margin:0 auto;
  padding:calc(14px + env(safe-area-inset-top)) 16px 12px;
}
body.member .top .mark { display:block; width:auto; height:20px; }
body.member .top-end { display:flex; align-items:center; gap:14px; }
body.member .hello {
  margin:0; font-family:var(--display); font-size:15px; font-weight:900;
  color:var(--ink);
}
body.member .to-my { font-size:13px; font-weight:700; color:var(--accent); text-decoration:none; white-space:nowrap; }
body.member .to-my:hover { color:var(--primary); }

body.linen .booting { color:var(--muted); }

/* ---- cards ---- */

body.linen .card {
  padding:var(--card-pad);
  border:0; border-radius:var(--radius);
  background:var(--card);
  box-shadow:var(--card-shadow);
}
body.linen .card + .card { margin-top:var(--gap); }
body.linen .card h3 { color:var(--ink); }
body.linen .card p { color:var(--ink-600); }
body.linen .card .eyebrow { color:var(--ink-600); font-size:11px; letter-spacing:.16em; }
body.linen .card.accent { box-shadow:var(--card-shadow); }
/* The one card that is about the member rather than about a booking, marked
   the way the reference marks it: a rule down its edge, no second colour. */
body.linen .card.for-you { border-left:3px solid var(--accent); }
body.linen .card.warn, body.linen .card.rejected { border-left:3px solid var(--danger); }
body.linen .card.pending { border-left:3px solid var(--accent); }
body.linen .card .reason { color:var(--danger); }
body.linen .empty { color:var(--muted); }
body.linen .section-title { color:var(--muted); }
body.linen .next-when { color:var(--ink); }
body.linen .next-head { display:flex; align-items:center; gap:10px; }
body.linen .next-head svg { flex:none; width:20px; height:20px; fill:none; stroke:var(--accent); stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
body.linen .next-head h3 { margin:0; }

/* ---- the one bold button, and the quiet things beside it ---- */

/* In full for the same reason: the ticket's way home is a .primary on a page
   that has never had one. */
body.linen .primary {
  display:block; width:100%; min-height:52px; padding:15px 20px;
  border:0; border-radius:26px;
  background:var(--primary); color:var(--primary-ink);
  font-family:var(--sans); font-size:14px; font-weight:800;
  letter-spacing:.1em; text-transform:uppercase;
  text-align:center; text-decoration:none;
}
body.linen .primary:hover { background:#3d5732; color:var(--primary-ink); }

/* A button that is not ready yet — the ticket's Send before a photo is
   chosen, and the summary bar's Confirm booking before there is a booking.
 *
 * Both used to be white type on the green pill at 28% over a white card,
 * which is about 1.6:1: a label a member cannot read at all, on a control
 * they will tap four times before concluding the app is broken. The green
 * cannot be kept and dimmed, because dimming it on a WHITE card is what
 * produced that. So the pill changes colour instead: a linen-grey fill and
 * the page's own quiet ink, 5.59:1, which reads as "not yet" rather than as
 * "off". The border is an inset shadow, not a border, so the pill does not
 * grow a pixel on the frame it becomes disabled.
 *
 * Said once for both, because they are one button in two places. */
body.linen .primary:disabled,
body.linen .confirm-btn:disabled {
  opacity:1;
  background:#d3d2bf; color:var(--ink-600);
  box-shadow:inset 0 0 0 1px var(--hairline);
  cursor:not-allowed;
}
body.linen .cta { color:var(--accent); }
body.linen .cta:hover { color:var(--primary); }
body.linen .quiet { color:var(--muted); }
body.linen .quiet:hover { color:var(--ink); }
body.linen .ghost {
  border:1px solid var(--hairline); border-radius:26px;
  min-height:52px; color:var(--ink-600); background:transparent;
}
body.linen .ghost:hover { color:var(--ink); border-color:var(--muted); }
body.linen .error { color:var(--danger); }

/* ---- tiles ---- */

body.linen .tiles { gap:var(--gap); }
body.linen .tile {
  padding:16px 10px;
  border:0; border-radius:var(--radius-tile);
  background:var(--card); box-shadow:var(--card-shadow);
}
body.linen .tile b { color:var(--primary); }
/* The accent ties the three tiles together without colouring the figures,
   which stay the club's green wherever a number is the club's own. */
body.linen .tile b::after {
  content:""; display:block; width:18px; height:2px; margin:7px auto 0;
  border-radius:2px; background:var(--accent);
}
body.linen .tile span { color:var(--muted); }
body.linen .allowance { color:var(--muted); }

/* ---- rows and chips ---- */

body.linen .list { gap:10px; }
body.linen .row {
  border:0; border-radius:var(--radius-tile);
  background:var(--card); box-shadow:var(--card-shadow);
  padding:14px 16px;
}
body.linen .row .what b { color:var(--ink); }
body.linen .row .what span { color:var(--muted); }
body.linen .row .when { color:var(--muted); }
body.linen .chip { color:var(--ink-600); border-color:var(--hairline); }
body.linen .chip.paid { color:var(--accent); border-color:rgba(69,96,122,.34); }
body.linen .chip.due { color:var(--danger); border-color:rgba(181,80,47,.34); }
body.linen .chip.dead { color:var(--muted); }
/* Why a booking nobody here remembers making is on this list: the account
   paid for it. Quiet, because it is a fact about the row and not a warning. */
body.linen .row .charged { margin-top:2px; color:var(--muted); }
body.linen .due-by { color:var(--danger); }
body.linen .pay { border-top:1px solid var(--hairline); }
body.linen .pay-row span { color:var(--muted); }
body.linen .pay-row b { color:var(--ink); }
body.linen .note { color:var(--muted); }
/* A booking or purchase code is one thing to read out at a counter, and a
   375px column was breaking it in two — "GJ-20260908-" above "K7RM4P".
   Whatever label stands in front of it wraps instead. */
body.linen .code { white-space:nowrap; }

/* The rest of a long list, folded away. Quiet on purpose: it is a way of
   reading the page, not a thing to do at the club, and the one bold button
   on this screen is already Book my next hour. Written out in full because
   the element is this skin's own — my.css has never heard of it. */
body.linen .see-all {
  display:block; width:100%; margin-top:10px; padding:11px 14px;
  border:1px solid var(--hairline); border-radius:var(--radius-tile);
  background:transparent; color:var(--accent);
  font-family:var(--sans); font-size:13px; font-weight:700; letter-spacing:.04em;
  text-align:center; cursor:pointer;
}
body.linen .see-all:hover { color:var(--primary); border-color:var(--muted); }

/* ---- wallet ---- */

body.linen .balance b { color:var(--primary); }
body.linen .balance span { color:var(--muted); }
body.linen .balance .exp { color:var(--ink-600); }
/* ---- buying hours ----

   The card around the packages keeps its name and loses its paper: what a
   member sees is a grey segmented control and three white tiles standing
   straight on the linen, the way every other list on this page stands on it.
   A white card holding three white tiles is two sheets of the same paper.

   Said with the card's own two classes, so it outranks the dark half's
   `body.linen .card` as well as this one's — that rule gives every card a
   gradient and a hairline, and a box drawn round three tiles and a button is
   a second card nobody asked for. */
body.linen .card.buy { padding:0; border:0; background:none; box-shadow:none; }

body.linen .buy .seg { background:rgba(28,30,23,.06); }
body.linen .buy .seg-btn { color:var(--muted); }
body.linen .buy .seg-btn.on { background:var(--card); color:var(--ink); box-shadow:0 1px 3px rgba(20,31,14,.10); }

body.linen .plan-tier {
  border:1px solid var(--hairline);
  background:var(--card); box-shadow:var(--card-shadow);
}
body.linen .plan-tier:hover { background:var(--card); border-color:var(--muted); }
/* The ring is drawn INSIDE the tile, so a tier does not grow by two pixels
   and push the ones under it down the screen as a thumb moves between them. */
body.linen .plan-tier.picked { box-shadow:inset 0 0 0 2px var(--accent), var(--card-shadow); }
body.linen .tier-mark { border:1.5px solid var(--muted); }
body.linen .picked .tier-mark { border:6px solid var(--accent); }
body.linen .tier-what b { color:var(--ink); }
body.linen .tier-what small { color:var(--muted); }
body.linen .tier-price b { color:var(--ink); }
body.linen .tier-price i { color:var(--ok); }
body.linen .tier-badge { background:var(--accent); color:var(--card); }

/* An investor's month, which is not a wallet: a bar rather than a balance,
   filling as the month is spent, because how much of it is gone is the thing
   a member cannot read off a single number. */
body.linen .allowance-card b { display:block; margin-top:2px; font-family:var(--display); font-size:26px; font-weight:900; line-height:1.15; color:var(--primary); }
body.linen .allowance-bar { margin:14px 0 10px; height:6px; border-radius:999px; background:rgba(28,30,23,.08); overflow:hidden; }
body.linen .allowance-bar i { display:block; height:100%; border-radius:inherit; background:var(--accent); }
body.linen .allowance-card .refresh { color:var(--ink-600); font-size:13.5px; }

body.linen .proof > span { border:1px dashed var(--muted); color:var(--ink); }
body.linen .proof:hover > span { border-color:var(--accent); color:var(--accent); }
/* "No photo chosen yet." sat flush against the picker it belongs to. my.css
   gives .note a top margin and `.card p` — one point more specific — takes
   it away again inside a card, which is the only place this note ever is. */
body.linen .proof + .note { margin-top:9px; }
body.linen .ticket-actions { margin-top:16px; }

/* ---- profile ---- */

body.linen .fact span { color:var(--muted); }
/* my.css breaks these cells `anywhere`, which is right for a long account
   name and wrong for the address above it: it split
   maria.angelica.delossantos.villanue|va@… mid-word, which reads as two
   addresses. `break-word` breaks only a word that cannot fit a line on its
   own, so the browser takes the <wbr> my.js puts at the @ first and reaches
   for a mid-word split only when there is no line left to give. */
body.linen .fact b { color:var(--ink); overflow-wrap:break-word; word-break:normal; }
body.linen .ok { color:var(--accent); }
body.linen .pref span { color:var(--muted); }
/* This said `background`, the shorthand, when all it ever meant was the
   colour — and the shorthand reaches every select on both pages, resetting
   repeat, position and size to their initials along the way. The two time
   pickers in step 02 carry a 12×8 chevron from styles.css that has no
   intrinsic size of its own, so once those three were gone the arrow tiled
   the full height of the control, straight over the hour it was pointing at.
   `background-color` sets the one thing that was wanted. */
body.linen select {
  color:var(--ink); background-color:var(--card);
  border:1px solid var(--hairline); border-radius:12px;
}
body.linen .row-link { color:var(--ink); }
body.linen .row-link + .row-link { border-top:1px solid var(--hairline); }
body.linen .row-link span { color:var(--muted); }
body.linen .version { color:var(--muted); }
body.linen .version a { color:var(--muted); text-decoration:none; border-bottom:1px solid var(--hairline); }
body.linen .version a:hover { color:var(--ink); border-bottom-color:var(--muted); }

/* ---- the tab bar ----
   Written out in full rather than adjusted, because the booking page never
   loaded my.css and has no tab bar of its own to adjust. Chrome, all of it. */

body.member .tabs {
  position:fixed; inset:auto 0 0 0; z-index:60;
  display:grid; grid-template-columns:repeat(4,1fr);
  padding-bottom:env(safe-area-inset-bottom);
  background:var(--card);
  backdrop-filter:none; -webkit-backdrop-filter:none;
  border-top:var(--tab-edge) solid var(--hairline);
  box-shadow:0 -6px 24px rgba(20,31,14,.06);
}
body.member .tab {
  display:grid; gap:3px; justify-items:center; align-content:center;
  min-height:62px; padding:8px 4px;
  color:var(--muted); text-decoration:none;
}
body.member .tab svg { width:22px; height:22px; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
body.member .tab span { font-size:10.5px; font-weight:700; letter-spacing:.06em; }
body.member .tab[aria-current="page"] { color:var(--accent); }

@media (min-width:760px) {
  /* On a laptop the bar stops being a thumb rail and floats: a centred pill
     rather than a metre of chrome across the bottom of the window.
   *
   * A floating pill is taller than a flush one by exactly the gap it floats
   * in, and that is the whole bug this pair of declarations exists to keep
   * out: the pill lifted off the bottom edge, the summary bar's offset did
   * not, and the two ended up sharing the last 80 pixels of the window with
   * the pill on top of the Confirm button. So the gap is named once and used
   * twice — as the pill's own bottom, and as what it adds to the reserve
   * everything else stands clear of. */
  body.member {
    --tab-pill-gap:18px;
    /* Boxed on all four sides now rather than ruled off on one, so the
       hairline is counted twice, and then the gap it floats in. */
    --tabs-reserve:calc(var(--tabs) + 2 * var(--tab-edge) + var(--tab-pill-gap));
  }
  body.member .tabs {
    left:50%; right:auto; bottom:var(--tab-pill-gap); transform:translateX(-50%);
    width:min(520px,92%);
    border:var(--tab-edge) solid var(--hairline); border-radius:26px;
    box-shadow:var(--card-shadow);
  }
}

/* ==================================================================
   THE BOOKING PAGE
   Everything below applies only to public/index.html: /my/ has none of
   these elements. The `body.linen` half dresses it for everybody; the
   `body.member` half is the narrow column and the chrome that only exist
   inside the member's own app.
   ================================================================== */

/* The same three the embed mode hides, for the same reason: the shell above
   already says where the member is, so our marketing header, hero and footer
   would be a second homepage inside their own app. */
body.member .topbar,
body.member .hero,
body.member .foot,
body.member .signed-in,
body.member .details-crest { display:none; }

body.member #booking-main { padding-bottom:calc(var(--tabs-reserve) + 108px); }
body.member .act { max-width:720px; padding:22px 16px 10px; }
body.member .act-game { padding-top:10px; }
body.member .act-details { padding-bottom:12px; }
body.member .act-head { margin-bottom:18px; }
/* The outlined numeral was a full-bleed flourish for a full-bleed page. In a
   720px column it is a smudge behind the heading, so it goes. The guest's
   full-bleed page keeps it — see the daylight section below, where it is
   redrawn in ink. */
body.member .act-num { display:none; }
body.linen .act-title { font-size:clamp(24px,5vw,30px); color:var(--ink); }
body.linen .act-title em { color:var(--accent); }
body.linen .act .act-lock-note { color:var(--muted); }

/* A step nobody has reached yet.
 *
 * The dark page faded one to .35, which is legible because it is fading
 * near-white type down a near-black page — it stays a light grey on a dark
 * ground. Do the same arithmetic in daylight and it runs the other way:
 * every colour here fades TOWARDS the linen it sits on, so "Complete step 01
 * first" arrived at about 2:1 and the whole date-and-details half of the
 * page went with it.
 *
 * .75 is where the weakest thing in a locked section — muted on the ground —
 * comes back over 3:1 (3.18; accent 3.24, ink-600 on a faded card 4.49, ink
 * 6.63). The plan asked for .6, which reads better as "not yet" but leaves
 * that hint at 2.42, and a hint nobody can read is not a hint. What says
 * locked is not the fade anyway: it is the note, the drained colour, and a
 * section that does not answer a tap.
 *
 * styles.css keeps its .35 for a page that is not linen — there is no such
 * page today, and the rule that would break if one came back is this one. */
body.linen .act.locked { opacity:.75; filter:saturate(.5); }

/* ---- 01 game ---- */

body.member .sport-split { min-height:0; }
body.linen .sport-split { gap:var(--gap); }
body.member .sport-panel { min-height:150px; }
body.linen .sport-panel { border:0; border-radius:var(--radius); box-shadow:var(--card-shadow); }
body.linen .sport-panel.selected { box-shadow:0 0 0 2px var(--accent), var(--card-shadow); }
body.member .sport-body { padding:22px 22px 20px; }
body.linen .drawer-label { color:var(--muted); }
body.linen .facility-strip { gap:var(--gap); }
body.linen .facility-card { border:0; border-radius:var(--radius-tile); box-shadow:var(--card-shadow); }
body.linen .facility-card.selected { box-shadow:0 0 0 2px var(--accent), var(--card-shadow); }

/* ---- 02 time ---- */

body.linen .sheet {
  border:0; border-radius:var(--radius);
  background:var(--card); box-shadow:var(--card-shadow);
  padding:var(--card-pad);
}
body.linen .date-chip { border:1px solid var(--hairline); background:var(--card); color:var(--ink); }
body.linen .date-chip:hover { border-color:var(--muted); }
body.linen .date-chip.selected { background:var(--primary); color:var(--primary-ink); border-color:var(--primary); }
/* The day and the month over the date. They were set at .6 of the chip's own
   colour, which on a transparent chip over a near-black page was a light
   grey; on a WHITE chip it is 4.45:1 — a real word, four hundredths under
   the floor. .72 puts it at 6.6 on the white chip and 6.0 on the green one. */
body.linen .date-chip .dow, body.linen .date-chip .mon { opacity:.72; }
body.linen .date-native span { color:var(--muted); }
body.linen .date-native input {
  border:1px solid var(--hairline); background:var(--card); color:var(--ink);
  color-scheme:light;
}
/* "Or pick a date", inside the card it is printed on.

   On a phone, styles.css stacks the date bar and gives this field
   `width:100%` — which is right everywhere except the browser the members
   actually book from. iOS Safari lays a native `input[type=date]` out as
   CONTENT-BOX no matter what the page says: the `*{box-sizing:border-box}`
   at the top of styles.css is overruled by the control's own appearance, so
   `width:100%` sized the TEXT of the field to the column and WebKit then
   added the 14px padding and the 1px border on each side outside it.
   Measured on an iPhone 17 at 402pt: a 318px column holding a 348px field,
   its right edge at x=390 against a card that ends at 382 — eight pixels
   past the paper, with "Sep 17, 2026" centred in the overhang. Chrome
   honours border-box here and measures the column exactly, which is why
   this only ever showed up on a phone.

   `appearance:none` is the line that does the work, and it cannot be traded
   for a plain `box-sizing:border-box` here: with the native appearance left
   on, the declaration is accepted and then computes back to content-box —
   measured, same 348px field — because WebKit re-adjusts the control's box
   after the cascade. It is what returns the box to the page's own sizing; the
   rest is the belt for that brace — a 1fr track that is allowed to be
   narrower than its content, a field that may not out-measure it, and the
   value pinned to the left where every other label in the card starts. The
   picker stays the system one: appearance only dresses the closed field.

   Scoped to the width styles.css itself scopes, so the desktop field — which
   sits beside the date rail with its own padding-block — is not touched. */
@media (max-width: 900px) {
  body.linen .date-native { grid-template-columns:minmax(0,1fr); }
  body.linen .date-native input {
    -webkit-appearance:none; appearance:none;
    box-sizing:border-box; display:block;
    min-width:0; max-width:100%; width:100%;
  }
  body.linen .date-native input::-webkit-date-and-time-value { text-align:left; }
}
body.linen .sheet-context { border-top:1px dashed var(--hairline); color:var(--ink-600); }
body.linen .sheet-context b { color:var(--ink); }
body.linen .timeline-intro { color:var(--ink-600); }
body.linen .timeline-intro b { color:var(--ink); }
body.linen .timeline-hint { color:var(--ink-600); }
body.linen .timeline-track { border:1px solid var(--hairline); background:var(--paper); }
body.linen .time-segment { border-right:1px solid var(--hairline); background:#e9efe2; color:var(--ink); }
/* A tapped cell keeps :hover on iOS, and this rule outranks the selected
   states by one pseudo-class — so the start of a booking was painted with
   the hover wash under the selection's pale text. Selected cells keep their
   own fill, whatever the pointer is doing. */
body.linen .time-segment:hover:not(:disabled):not(.selected) { background:#dde8d3; }
body.linen .time-segment small { color:var(--ink-600); }
body.linen .time-segment.selected { background:#3d5732; color:var(--primary-ink); box-shadow:inset 0 -3px 0 var(--accent); }
body.linen .time-segment.selected small { color:var(--primary-ink); }
body.linen .time-segment.selected-start { background:var(--primary); color:var(--primary-ink); box-shadow:inset 0 -4px 0 var(--accent); }
body.linen .time-segment.selected-start small { color:var(--primary-ink); }
/* A taken half-hour, hatched over paper rather than filled: dark text still
   reads on it, which is the whole reason this skin exists. */
body.linen .time-segment.booked,
body.linen .time-segment.unavailable {
  background:repeating-linear-gradient(-45deg, rgba(28,30,23,.07) 0 8px, rgba(28,30,23,.02) 8px 16px), var(--paper);
}
body.linen .time-segment.booked small,
body.linen .time-segment.unavailable small { color:var(--ink-600); }
body.linen .time-segment.too-short { background:var(--paper); }
body.linen .time-segment.too-short small { color:var(--ink-600); }
body.linen .time-segment.extendable { background:#dde8d3; box-shadow:inset 2px 0 0 var(--accent); }
body.linen .time-segment.extendable small { color:var(--accent); }
body.linen .time-segment.extendable:hover:not(:disabled):not(.selected) { background:#cfe0c2; }
body.linen .time-segment.end-boundary.extendable small { color:var(--ink); }
body.linen .time-selection { border:1px solid var(--hairline); background:var(--paper); }
body.linen .time-selection span { color:var(--muted); }
body.linen .time-selection b { color:var(--ink); }
/* The picker sits inside the paper-coloured .time-selection panel and was
   drawn transparent on purpose; the white box it wore was the shorthand's
   doing as well, so the colour is named here and named as nothing. Repeat,
   position and size are deliberately absent: with the shorthand gone,
   styles.css's own no-repeat, right 2px center and 12px 8px apply again. */
body.linen .time-pick {
  color:var(--ink);
  background-color:transparent;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%234a4e42' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
body.linen .time-pick option { color:var(--ink); background:var(--card); }
body.linen .duration-control button { border:1px solid var(--hairline); background:var(--paper); color:var(--ink); }
body.linen .duration-control button:hover:not(:disabled) { background:#dfdcc2; }
body.linen .slot-empty { color:var(--muted); }
body.linen .slot-loading-label { color:var(--ink-600); }
body.linen .timeline-skeleton { border:1px solid var(--hairline); background:linear-gradient(90deg, rgba(28,30,23,.03), rgba(28,30,23,.09), rgba(28,30,23,.03)); background-size:200% 100%; }
body.linen .slot-error button { border:1px solid var(--hairline); background:var(--paper); color:var(--ink); }
body.linen .slot-error button:hover { border-color:var(--muted); background:#dfdcc2; }
/* Both rails scroll sideways and both drew their thumb in cream, which on
   linen is a scrollbar that is not there. */
body.linen .date-rail { scrollbar-color:rgba(28,30,23,.32) transparent; }
body.linen .date-rail::-webkit-scrollbar-thumb { background:rgba(28,30,23,.28); }
body.linen .timeline { scrollbar-color:rgba(28,30,23,.32) transparent; }

/* ---- 03 details ---- */

body.linen .details-card {
  border:0; border-radius:var(--radius);
  background:var(--card); box-shadow:var(--card-shadow);
  color:var(--ink);
  padding:var(--card-pad);
}
body.linen .details-grid { gap:16px var(--gap); }
body.linen .field > span { color:var(--ink-600); }
body.linen .field > span em { color:var(--accent); }
body.linen .field input,
body.linen .field textarea {
  border:1px solid var(--hairline); background:var(--card); color:var(--ink);
}
body.linen .field input::placeholder,
body.linen .field textarea::placeholder { color:var(--muted); }
body.linen .field input:focus,
body.linen .field textarea:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(69,96,122,.22); }

/* ---- the sticky bar ---- */

body.linen .summary-bar {
  border:1px solid var(--hairline); border-radius:var(--radius);
  background:var(--card); backdrop-filter:none; -webkit-backdrop-filter:none;
  box-shadow:0 14px 34px rgba(20,31,14,.12);
}
body.linen .summary-piece i { color:var(--muted); }
body.linen .summary-piece b { color:var(--ink); }
body.linen .summary-piece.filled b { color:var(--accent); }
body.linen .status { color:var(--ink-600); }
body.linen .status.error { color:var(--danger); }
body.linen .confirm-btn {
  min-height:52px; border-radius:26px;
  background:var(--primary); color:var(--primary-ink);
}
body.linen .confirm-btn:hover:not(:disabled) { background:#3d5732; }
/* Its disabled face is up with .primary's — the same pill in two places. */

/* ---- the sticky bar, lifted clear of the tab bar ----
 *
 * The lift is the tab bar's whole reserved height plus 12px of air, at every
 * width, because --tabs-reserve already knows whether the bar is flush to the
 * bottom edge or a pill floating above it. The number 12 is written once and
 * the pill's own geometry is written once, so neither can move without the
 * other — the two of them sharing the bottom of a laptop window, the pill
 * painted over the summary's columns, is what this arithmetic replaced.
 *
 * The stack, said out loud because a member has to be able to hit all three:
 * the page, then this bar over it at 50, then the tab bar over that at 60, so
 * a tap on Profile is never swallowed by a summary that reaches under it. */
body.member .summary-bar {
  z-index:50;
  bottom:calc(var(--tabs-reserve) + 12px);
  width:min(720px, calc(100vw - 28px));
}
/* And the same lift has to be paid back when the bar is HIDDEN, or it never
 * leaves the window.
 *
 * styles.css parks the bar with `transform:translate(-50%, 130%)` — 130% of
 * the bar's own height, about 80px, so roughly 104px of travel. That was
 * enough to clear the bottom edge while the bar sat at `bottom:18px`. It is
 * not enough here: the rule above rests it at --tabs-reserve + 12px, which
 * is 94px once the tab bar is a floating pill, so the same 104px of travel
 * moves it 94px of its own offset and only 10px past the edge. Measured at a
 * 900px viewport, the "hidden" bar sat at 830–910 with the pill at 818–882 —
 * a dark slab showing under the tabs on every screen where nothing is picked
 * yet.
 *
 * So the hidden state is restated here in absolute terms rather than as a
 * percentage of a height that has nothing to do with how far the bar has to
 * fall: its own height (100%), plus the exact offset it now rests at
 * (--tabs-reserve + 12px, the one above and not a copy of the number), plus
 * 40px so the 34px shadow goes with it. A percentage cannot express that
 * sum, because two of the three terms are not the bar's height.
 *
 * `.summary-bar.show` is untouched — this rule simply does not match an
 * element carrying .show, so the shown state is still translate(-50%, 0) and
 * the transition still runs between the two. */
body.member .summary-bar:not(.show) {
  transform:translate(-50%, calc(100% + var(--tabs-reserve) + 12px + 40px));
}

/* ---- the ticket ---- */

/* The ticket's top is the lighter green too, like the member page's. */
body.linen .ticket-top { background:var(--fairway); }

body.member .success-stage { min-height:auto; padding:16px 16px calc(var(--tabs-reserve) + 32px); }
/* The two notches are cut out of the page behind the ticket, so they have to
   be the page's colour — which is no longer near-black. */
body.linen .ticket-perf::before,
body.linen .ticket-perf::after { background:var(--paper); }

/* ---- the booking code on the ticket ----

   `body.linen .code` above is nowrap, so a code a member reads out at a
   counter is never handed to them in two pieces. The ticket's own code card
   is `.summary-item.code`, which matches that selector — and this card sets
   the code in the club's display face at 22px, where a real one,
   GJ-20260916-K7RM4P, measures 326px. A 375px phone leaves the card 249px.
   So the nowrap did not clip the code: it pushed the card, the summary grid
   and the whole ticket out to ~404px inside a 327px column, and the page's
   own overflow-x:hidden then sliced the right-hand edge off every panel of
   the ticket — head, perforation and body — in both skins, for guests and
   members alike.

   The answer /my already gives its own ticket (my.css, .code-cell): measure
   the size off the column instead of writing one number, and stop growing at
   the 22px the sheet underneath sets. The ramp is a little shallower here
   because the guest's ticket sits in more padding than the member page's —
   24px of stage and 24px of ticket body either side, against /my's 16 and
   20 — so the same column is 16px narrower on the same phone. It stops
   SHRINKING at 16px, which is the size every other value in the grid is set
   in: a booking code smaller than the phone number beside it is the wrong
   card.

   Wrapping is then left ON underneath all of that, and it is the floor
   rather than the plan. A 375px phone reads all but the widest 0.4% of the
   alphabet's codes on one line — six M's and W's in a row is 258px against
   the column's 249 — and below 360px, or where Hilogen never arrived and the
   fallback face is wider, more of them break. They break at the code's own
   hyphen, which still reads as the code; a clipped one does not, and a
   ticket dragged out to 404px clipped every panel on it. `anywhere` is the
   last resort under even that, for a code with no hyphen left to break at. */
body.linen .summary-item.code { white-space:normal; }
body.linen .summary-item.code .summary-value { overflow-wrap:anywhere; }
/* And the grid tracks may shrink: a `1fr` column is min-content wide by
   default, which is how one unbreakable e-mail address widens the ticket the
   same way the code did. */
body.linen .summary-item { min-width:0; }
/* ---- and the headline over it ----

   The same arithmetic, one panel up. "Reservation saved." is what a booking
   with money still owing says, and at the 34px floor the sheet underneath
   gives the headline, "Reservation" alone is 286px — wider than the 275px a
   375px phone leaves inside the ticket's head, and wider still than 320px's
   220. A word cannot be clipped and cannot be hyphenated, so it did what the
   code did: it made the ticket wider than the page.

   34px is where it stops growing, so this only ever makes the headline
   smaller than it is today, and only on a phone — at the 430px edge of the
   query the clamp is within half a pixel of the 34 it hands back to. The
   ramp is not steeper than that because 320px is the width with the least to
   spare: "Reservation" has 220px there, and a headline cannot wrap out of
   trouble the way the code below it can. */
@media (max-width: 430px) {
  body.linen .summary-item.code .summary-value { font-size:clamp(16px, 4.3vw, 22px); }
  body.linen .ticket-headline { font-size:clamp(24px, 7.8vw, 34px); }
}

body.member .ticket-home { margin-bottom:10px; }
/* One bold button on the card, and inside the member's app it is the way
   back to their own page — so booking a second hour becomes the quieter of
   the two. A guest has no way home to offer, so their ticket keeps the one
   solid button it has always had. */
body.member .ticket-again {
  background:transparent; color:var(--ink-600);
  border:1px solid var(--hairline);
}
body.member .ticket-again:hover:not(:disabled) { background:var(--paper); color:var(--ink); }

/* ---- the progress toast ---- */

body.linen .booking-progress {
  border:1px solid var(--hairline);
  background:var(--card); color:var(--ink);
  box-shadow:0 14px 34px rgba(20,31,14,.12);
}
body.linen .booking-progress-track { background:rgba(28,30,23,.08); }
body.linen .booking-progress-bar { background:var(--accent); }

/* ==================================================================
   THE GUEST'S BOOKING PAGE, IN DAYLIGHT

   The marketing header, the hero and the footer are the three parts of
   this page the skin had never had to dress, because member mode simply
   took them away. The site is linen for everyone now, so they are dressed
   here — and every one of them was drawing its type in cream, which on
   linen is type that is not there.
   ================================================================== */

/* ---- the top bar ----
   A light bar, because the dark scrim it faded out of was painted for a
   near-black page and on linen it is a grey smear across the top of one.
   The wordmark in the markup is the reserve one for the same reason the
   member header's is: the ground under it is paper.
   Step dots: muted 5.2 on linen, accent 5.4 for the step you are on. */
body.linen .topbar { background:linear-gradient(var(--paper), rgba(236,234,213,0)); }
body.linen .topbar.scrolled {
  background:rgba(236,234,213,.88);
  border-bottom:1px solid var(--hairline);
}
body.linen .step-dot { color:var(--muted); }
body.linen .step-dot.on { color:var(--accent); }
body.linen .topbar-est { color:var(--muted); }

/* ---- the hero ----
   The one dark thing left on a linen page, and deliberately so: its type is
   the club's cinema face on the club's own night ground, and redrawing it in
   ink would have thrown away the only photographic thing above the fold. So
   it is BOXED rather than bled — a dark card sitting on the linen the way
   every other card here sits on it — and its cream type never meets the
   linen at all.

   The ground runs from the club's green to its darkest, not from fairway:
   over fairway the script line is 3.6:1 and the lede 3.9, both under the
   floor, which is what "verify the photo's lightest region" turns up. From
   --fairway (the lighter green the owner chose) the lightest region gives
   eyebrow 4.8, headline 5.8, script 4.8 and lede 4.6 with the two
   overrides above; on the old reserve stop they were eyebrow 8.7, headline 10.6, script
   6.5 and lede 6.4. No scrim needed on top of that.

   This page has no hero photograph in the repository — the renders live on
   the club's own site and nothing in this sheet may fetch from anywhere — so
   the card carries the brand's night ground, which is what the hero has
   always actually been sitting on. */
/* The type on the sage stop, re-measured: the booking hero's script line
   and lede were set for reserve (6.5 and 6.4) and read 3.6 and 3.8 on
   fairway. One step lighter each puts them at 4.8 and 4.6, over the floor;
   the eyebrow (4.8) and headline (5.8) were already there. */
body.linen .hero-line-2 { color:var(--ember-300); }
body.linen .hero-lede { color:rgba(236,234,213,.88); }
body.linen .ticket-headline em { color:var(--ember-300); }

body.linen .hero-inner {
  padding:clamp(36px,5vw,58px) clamp(26px,4vw,46px);
  border-radius:var(--radius);
  background:linear-gradient(165deg, var(--fairway) 0%, var(--reserve) 100%);
  box-shadow:var(--card-shadow);
  overflow:hidden;
}

/* The outlined numeral behind each act's heading, drawn in cream at 13% for
   a near-black page. In ink at the same weight it is the same flourish. */
body.linen .act-num { -webkit-text-stroke-color:rgba(28,30,23,.13); }

/* The line that says a signed-in member's details are already in the form
   below, and where their hours are. ink-600 7.0 on linen, accent 5.4. */
body.linen .signed-in { color:var(--ink-600); }
body.linen .signed-in a { color:var(--accent); border-bottom-color:rgba(69,96,122,.4); }
body.linen .signed-in a:hover { color:var(--ink); }

/* ==================================================================
   THE GUEST'S INVITATION

   Three placements on the booking page — under the hero, over the details
   grid, on the confirmed ticket — offering one thing: Continue with Google.
   They are in that document's markup, hidden, and guest-sign-in.js shows
   them only to a guest in a browser whose auth Worker has Google open.

   Everything about them is drawn HERE rather than in styles.css because
   styles.css is the sheet underneath and may not change — it is byte-
   compared against main, which is what keeps this skin an overlay. So these
   elements have no rules at all without this block, which is also why the
   geometry is in it: `body.linen` is on every page this Worker serves, so a
   rule scoped to it is simply "the rule", and nothing here is a second
   opinion about anything the sheet below already says.

   What is NOT here: the hiding under .native and .app. Those selectors
   begin with neither of this sheet's two roots — see the test at the top of
   test/member-theme.test.js — and the booking page's own book-shell.css is
   where that document's guest/shell decisions have always lived. The
   member's page is the exception at the foot of this block: it loads no
   book-shell.css, and `body.member.app` does start with a root this sheet
   allows.

   Contrast, measured on the grounds each line actually sits on:
     ink-600 on linen 7.0 · muted on linen 5.2 · muted on white 6.4
     the error red #8f3c22 6.1 on linen and 7.4 on white
   and after dark, on the ticket's own panel: ink-600 8.6, muted 6.4, and
   full linen 5.4 on the fairway page.
   ================================================================== */

body.linen .guest-invite[hidden] { display:none; }
body.linen .guest-invite-copy { margin:0; font-size:13px; line-height:1.5; color:var(--ink-600); }

/* Google's own button, which is Google's and not the club's: their mark,
   their sentence, a white pill with a hairline, and the one control on this
   page that is not set in the club's small caps. The same face the member's
   card wears in my.css — that sheet does not reach this document, so the
   shape is restated rather than shared, and the markup it dresses is the
   same markup. */
body.linen .btn-google {
  display:inline-flex; align-items:center; justify-content:center; gap:10px;
  min-height:48px; padding:12px 22px;
  border:1px solid rgba(28,30,23,.22); border-radius:26px;
  background:#fff; color:#1f1f1f;
  font-family:var(--sans); font-size:14.5px; font-weight:600; letter-spacing:0;
  text-transform:none; cursor:pointer;
}
body.linen .btn-google svg { flex:none; width:18px; height:18px; }
body.linen .btn-google:hover:not([disabled]) { border-color:var(--muted); }
/* Pressed and waiting on the Worker for Google's URL. `progress`, not
   `not-allowed`: the button is doing what it was asked. */
body.linen .btn-google[disabled] { opacity:.7; cursor:progress; }

/* The one line under a button, and only when something failed. */
body.linen .guest-invite-status {
  margin:0; flex-basis:100%;
  font-size:12.5px; line-height:1.45; color:#8f3c22;
}
body.linen .guest-invite-status[hidden] { display:none; }

/* ---- 1 · under the hero ----
   The #signed-in slot's own column, to the pixel, because it is the same
   slot: a member gets that line here and everybody else gets this. */
body.linen .guest-invite-hero {
  max-width:var(--shell); margin:18px auto 0; padding:0 32px;
  display:flex; flex-wrap:wrap; align-items:center; gap:12px 18px;
}
body.linen .guest-invite-hero .guest-invite-copy { flex:1 1 300px; max-width:56ch; }

/* ---- 2 · over the details grid ----
   A line and a small button, divided from the three boxes below by the
   card's own hairline. Above the crest, which is absolutely positioned into
   the corner of that card and would otherwise sit over the button. */
body.linen .guest-invite-details {
  position:relative; z-index:1;
  display:flex; flex-wrap:wrap; align-items:center; gap:12px 16px;
  margin:0 0 22px; padding:0 0 18px;
  border-bottom:1px solid var(--hairline);
}
body.linen .guest-invite-details .guest-invite-copy { flex:1 1 220px; }
body.linen .guest-google-small { min-height:42px; padding:10px 18px; font-size:13.5px; }

/* ---- 3 · on the ticket ----
   A panel under the confirmation and above "Book another slot", on the
   page's own linen so it reads as a note on the ticket rather than a second
   ticket. */
body.linen .guest-invite-ticket {
  display:grid; gap:12px; margin:22px 0 18px; padding:18px;
  border:1px solid var(--hairline); border-radius:var(--radius-tile);
  background:var(--paper);
}
body.linen .guest-invite-ticket .btn-google { width:100%; }

/* The App Store row on that panel.

   Apple's badge is Apple's artwork: their file, served from this Worker,
   drawn at the size it was made at (119.66 × 40 → 120 × 40) and never
   below their 40px floor. No filter, no radius, no tint, no shadow — the
   dark skin swaps in Apple's own white variant rather than inverting this
   one. The 12px of clear space is theirs too: a quarter of the badge's
   height on every side, which is what their guidelines ask for. */
body.linen .guest-app {
  display:flex; flex-wrap:wrap; align-items:center; gap:12px 16px;
  padding-top:14px; border-top:1px solid var(--hairline);
}
body.linen .app-badge { display:inline-flex; flex:none; margin:0 12px 0 0; }
/* 40px tall and the width the file itself says: `width:auto` against a
   fixed height is the one way to be certain Apple's artwork is never
   stretched by a rounding. The attributes on the <img> are the same numbers
   rounded, so the box is reserved before the SVG arrives and nothing
   reflows. */
body.linen .app-badge img { display:block; height:40px; width:auto; }
body.linen .guest-app-copy {
  flex:1 1 220px; margin:0;
  font-size:12.5px; line-height:1.45; color:var(--muted);
}

@media (max-width:900px) {
  /* The hero slot narrows with the rest of the page, and the button takes
     a line of its own rather than squeezing the sentence beside it. */
  body.linen .guest-invite-hero { padding:0 20px; gap:12px; }
  body.linen .guest-invite-hero .btn-google { width:100%; }
}

/* ---- the footer ----
   The emblem in the markup is the reserve one now, for the same reason the
   wordmark is. muted is 5.2 on linen. */
body.linen .foot { border-top:1px solid var(--hairline); color:var(--muted); }
body.linen .foot img { opacity:.75; }
/* The privacy page's own way in, and it stays as quiet as the line it sits
   under: muted 5.2 on linen, ink 13.9 when a thumb is on it. */
body.linen .foot a { color:var(--muted); text-decoration:none; border-bottom:1px solid var(--hairline); }
body.linen .foot a:hover { color:var(--ink); border-bottom-color:var(--muted); }
/* Privacy and the App Store, side by side on one line and wrapping onto two
   on a narrow phone. Both are the footer's own quiet link above — this only
   puts them in a row. */
body.linen .foot-links { display:flex; flex-wrap:wrap; justify-content:center; gap:8px 18px; }

/* The same three words on the member's own page, where the footer is the
   version line at the bottom of Profile. That page loads no book-shell.css,
   so the rule that takes the link away inside the app and the native shell
   has to be here — and `body.member` is a root this sheet allows, which is
   what makes that possible. Nobody standing in the app is offered the app. */
body.member.app .app-store-link,
body.member.native .app-store-link { display:none; }

@media (max-width:900px) {
  /* Only the width narrows here. The lift used to be restated as
     `--tabs + 8px`, which was both a second magic number and, between 760 and
     900 where the pill is already floating and this block wins, eight pixels
     measured off the wrong thing. It is the one rule above at every width
     now. */
  body.member .summary-bar { width:calc(100vw - 20px); }
  body.member #booking-main { padding-bottom:calc(var(--tabs-reserve) + 96px); }
  body.member .sport-panel { min-height:140px; }
}

/* ==================================================================
   INSIDE THE APP

   `app` is a THIRD class, and it is not a third scope: every rule below
   still begins body.linen or body.member, and `.app` only narrows it. The
   inline head script on each document adds it to <html> and <body> when
   Capacitor's bridge is there, and nothing in a browser ever matches any of
   this.

   What it is for: "it doesn't feel like a fluid app yet — kinda feels
   clunky." Four of the things that say "web page in a shell" are answered
   here, in one sheet, because both of the member's documents load it and two
   copies of a tap highlight is two copies to keep right. The scroll rules
   are NOT here — they are about <html>, which a body-scoped sheet cannot
   name — and neither is the rule that hides the booking page's member chrome
   from a guest, which is about a body carrying neither of this sheet's two
   roots. See my.css and book-shell.css.
   ================================================================== */

/* No rubber band past the top of a page that has nothing to pull down to.
   The viewport takes its overscroll from <body> when <html> leaves it auto;
   the two document sheets say it on <html> as well, which is where it is
   really decided. */
body.linen.app { overscroll-behavior-x:none; overscroll-behavior-y:auto; }

/* The member's header and tab bar, sideways.
 *
 * Both documents carry `viewport-fit=cover` now, so the page runs under the
 * notch and the home indicator and the header's `14px + env(top)` finally
 * resolves to a real number — which is the whole of "the Greenjacket logo
 * overlaps with the time". The half that is easy to forget is LANDSCAPE: the
 * top inset goes to zero and a left or right one appears in its place, and
 * the app allows landscape on both device families. The header is the one
 * element in this sheet that spans that edge, and the tab bar is the other. */
body.member.app .top {
  padding-left:calc(16px + env(safe-area-inset-left));
  padding-right:calc(16px + env(safe-area-inset-right));
}
body.member.app .tabs {
  padding-left:env(safe-area-inset-left);
  padding-right:env(safe-area-inset-right);
}

/* The grey flash and the long-press magnifier are the two things a WebView
   does that no app does. Everywhere, including inside cards — a member never
   wants either. */
body.linen.app * { -webkit-tap-highlight-color:transparent; -webkit-touch-callout:none; }

/* A native app's chrome is not text to select, and a long press on it that
   pops a selection handle is the shell showing through. Named element by
   element rather than swept with `*`: the ledger, a booking code, an account
   number and a member's own name are all things somebody copies, and a page
   that cannot be copied from is a worse page than one that flashes grey. */
body.linen.app .top .mark,
body.linen.app .top .members,
body.linen.app .top .hello,
body.linen.app .tab span,
body.linen.app .tab svg,
body.linen.app .eyebrow,
body.linen.app .section-title,
body.linen.app .summary-label,
body.linen.app .act-num,
body.linen.app .step-dot,
body.linen.app .topbar-est { -webkit-user-select:none; user-select:none; }

/* The 300ms double-tap wait, where iOS still has one. Anything a thumb lands
   on; never the <select> for a bay or the date <input>, which keep the
   phone's own pickers and are not touched anywhere in this block. */
body.linen.app a,
body.linen.app button,
body.linen.app label.proof,
body.linen.app [role="button"] { touch-action:manipulation; }

/* A press that visibly presses. 80ms, which is under the threshold at which
   a movement starts being watched rather than felt. */
body.linen.app .primary:active,
body.linen.app .confirm-btn:active,
body.linen.app .tab:active,
body.linen.app .btn:active,
body.linen.app .see-all:active,
body.linen.app .row-link:active,
body.linen.app button.plan-tier:active { transform:scale(.985); transition:transform 80ms; }
/* Except the one that is inert: a dead Send that still presses is a lie. */
body.linen.app .primary:disabled:active,
body.linen.app .confirm-btn:disabled:active { transform:none; }

/* ---- the paint cost of a scroll ----

   Every card on both pages carries `0 8px 24px` and the booking page's
   selected sport panel carries `0 30px 80px`; a blurred shadow is repainted
   on every frame of a flick, and a column of them is the difference between
   a scroll that tracks a thumb and one that lags behind it. Inside the app
   they come down to a shadow that still separates a white card from the
   linen — 1.21:1 of contrast does most of that work already — and the
   backdrop blurs on the two bars go, which the skin had already switched off
   for the tab bar and the summary bar in daylight.

   Written as one token used by name, so there is one number and not nine.
   NOT a redefinition of --card-shadow: that token is measured and pinned by
   test/member-theme.test.js, and a second definition of it under a narrower
   scope is exactly the drift that test exists to catch. */
body.linen.app { --app-shadow:0 4px 12px rgba(20,31,14,.10); }

body.linen.app .card,
body.linen.app .tile,
body.linen.app .row,
body.linen.app .sheet,
body.linen.app .details-card,
body.linen.app .sport-panel,
body.linen.app .facility-card,
body.linen.app .hero-inner,
body.linen.app .my-hero,
body.linen.app .see-all { box-shadow:var(--app-shadow); }
/* The selected states keep their accent ring and lose only the drop. */
body.linen.app .sport-panel.selected,
body.linen.app .facility-card.selected { box-shadow:0 0 0 2px var(--accent), var(--app-shadow); }
body.linen.app .summary-bar { box-shadow:var(--app-shadow); }
body.member.app .tabs { box-shadow:var(--app-shadow); }

/* And the blurs. A backdrop-filter is the page underneath re-rendered and
   blurred on every frame it moves, which on a fixed bar means every frame of
   every scroll. The skin already took the tab bar's and the summary bar's
   away in daylight; these three are what is left. */
body.linen.app .topbar.scrolled,
body.linen.app .booking-progress,
body.linen.app .facility-flag { backdrop-filter:none; -webkit-backdrop-filter:none; }

/* The two photographs the booking page desaturates on its way in and out.
   A filter on something that scrolls is a second full-size paint of it. */
body.linen.app .sport-photo,
body.linen.app .facility-card img { filter:none; }

/* ==================================================================
   INSIDE THE NATIVE SHELL

   `native` is another class and, like `app`, not another scope: every rule
   below still begins body.member and `.native` only narrows it. The booking
   page's inline head script adds it to <html> and <body> when the user agent
   carries `GreenjacketNative` — the SwiftUI app, which puts this document on
   its Book tab under a system navigation bar and over a system tab bar.

   Which means the member's header and the member's tab bar are on the screen
   twice: once in UIKit and once in the markup here. This block is the second
   copy standing down. Not deleted from the document — the same file is the
   member's booking page in every browser and inside the Capacitor app, where
   those two ARE the way around — hidden for the one runtime that has already
   drawn them.

   `.ticket-home` is deliberately left alone. "Back to my page" is the way
   out of a confirmed booking, and in the shell it is not a page load: the
   app watches for that navigation and switches to its own Home tab instead.
   A link the shell answers is not a link to hide.
   ================================================================== */

body.member.native .top,
body.member.native nav.tabs { display:none; }

/* With this page's tab bar gone there is nothing left at the bottom of the
   window but the phone's home indicator — the native bar is outside the web
   view and takes its inset with it, so the view's own bottom is the safe
   area and nothing else. The reserve is the ONE number everything standing
   clear of the bottom measures against, so it is the only thing that has to
   be told, and the summary bar comes to rest just above the indicator
   without a rule of its own. Two classes beats both the base declaration and
   the pill breakpoint's override, so this is the last word at every width. */
body.member.native { --tabs-reserve:env(safe-area-inset-bottom); }

/* ---- and the confirm bar comes off the window altogether ----
 *
 * The owner, on a screenshot of the Book tab: "the confirm booking is not
 * good UI. Maybe just leave it at the bottom so it doesn't float?"
 *
 * Every number above this line is about a bar that floats, and they are all
 * still right for the runtime they were written for. This one is not that
 * runtime. In the shell the window is 375pt wide and the bar appears the
 * moment a bay is tapped — before the time board has even been scrolled to —
 * so the member picks a start time with the control they are heading towards
 * already lying across the bottom of the board and over the Start time field
 * beneath it. On the web that pill is small against a tall page; here it is a
 * slab across the choice it is waiting for.
 *
 * #summary-bar is the last thing inside the form in the markup, straight
 * after 03 Details, so `position:static` is the whole of the change: it lands
 * at the end of the form, after the last field, in the card it already wears.
 *
 * `bottom` is deliberately NOT restated. --tabs-reserve is the one number
 * everything standing clear of the bottom measures against, and this sheet's
 * own tests forbid a second rule setting the bar's offset to anything else —
 * rightly, since two offsets for one bar is how the pill ended up painted
 * over the Confirm button. A static box ignores `bottom` anyway, so there is
 * nothing to say.
 *
 * Both transforms are neutralised. app.js still adds and removes `.show` as
 * the picks come and go; in here that class has nothing left to move, and the
 * bar is simply always at the foot of the form. */
body.member.native .summary-bar {
  position:static;
  transform:none;
  left:auto;
  /* The member's column, not the window: .act is 720px with 16px of padding
     inside it, so the bar's box is that content box. */
  width:min(100% - 32px, 688px);
  margin:18px auto 0;
  transition:none;
  flex-wrap:wrap;
}
body.member.native .summary-bar:not(.show) { transform:none; }

/* The three picks come back. styles.css hides .summary-slots under 900px
 * because a floating pill on a phone has room for the button and nothing
 * else; a card at the end of the form has a whole line for them, and a recap
 * of what is about to be confirmed is the most useful thing that can be in
 * it. It wraps, and the button takes the line underneath. */
body.member.native .summary-bar .summary-slots {
  display:flex; flex-wrap:wrap; gap:10px 20px;
}
body.member.native .summary-bar > button { flex-basis:100%; flex-grow:1; }

/* And the room that was kept for a bar that no longer floats. The page
 * reserved the tab bar's height plus 96–108px of pill; in flow the bar is
 * part of the stack and pays for its own space with its margin, so what is
 * left below it is the home indicator the reserve already carries, plus air.
 * Off the same --tabs-reserve as every other rule here, and no other. */
body.member.native #booking-main { padding-bottom:calc(var(--tabs-reserve) + 24px); }

@media (max-width:900px) {
  body.member.native .summary-bar {
    width:calc(100% - 32px);
    margin:16px auto 0;
    padding:12px 14px;
    gap:12px;
  }
}

/* ==================================================================
   THE CLUBHOUSE AFTER DARK

   The owner, on the site in a browser with the phone set to Dark: "make it
   follow the browser's settings."

   Until now it did not. The dark skin lived at the foot of book-shell.css
   under `body.linen.native`, which is one page (the booking page) in one
   runtime (the SwiftUI shell's WKWebView). Every other way in — Safari on a
   phone in dark mode, Chrome on a laptop at midnight, the member's own page
   at /my/ in any browser at all — got the clubhouse linen and nothing else.
   The Book tab being the one white screen in a dark app was the complaint
   that produced the first block; the whole SITE being the one white thing in
   a dark phone is the same complaint, one scope out.

   ---- the trigger, and why it is only a media query ----

   `@media (prefers-color-scheme: dark)` and nothing else: no query
   parameter, no cookie, no script, no preference of our own to keep in step
   with the phone's. The browser answers the question and the page follows.
   Inside the SwiftUI shell that answer is the app's own appearance — the
   WebView inherits the window's — so a member who sets Appearance to Light
   in the app still gets a linen Book tab, exactly as before, with no message
   crossing the bridge.

   ---- why the rules moved here ----

   book-shell.css is ONE DOCUMENT's sheet: the booking page loads it and /my/
   never has. A skin that has to reach both pages cannot live there. This
   sheet is the one both of them load, and its two roots are exactly the two
   halves the dark skin needs:

     `body.linen` — the SKIN, on every page this Worker serves. The tokens
     and every surface, cell, field and caption the two pages share.

     `body.member` — the CHROME, which only /my/ and member mode have: the
     member header, the tab bar and the pill it becomes on a laptop. The
     native shell draws its own header and tab bar and hides these, so this
     half is the one the dark app never sees and the one nothing before today
     had ever dressed.

   What is left in book-shell.css is the single line a body-scoped sheet
   cannot say: `color-scheme` on the ROOT element. `body.linen` says it too,
   a line below, because that is what reaches the form controls on a page —
   the privacy page loads this sheet and neither document sheet — but the
   root's copy is what makes the viewport's own scrollbars and the canvas
   behind the page come up dark, and a sheet whose every selector must begin
   `body.` cannot reach <html>. The member's page says the same thing in
   my.css for the same reason.

   ---- the palette ----

   The member page's original dark sheet (my/my.css), which is where the
   iOS app's `GJPalette.dark` came from in the first place: a fairway-green
   page under two radial washes, dark translucent cards on it, linen type,
   ember accents, the slate primary button. Most of it arrives by pointing
   the skin's own tokens at those values — one line re-tones a hundred rules,
   the way the skin itself re-tones the sheets underneath it. Per-selector
   rules follow ONLY where a colour was written out by hand, and nowhere the
   token has already covered.

   Three tokens are not simply the light ones inverted:

   `--muted` is linen at .72, which is 5.2 on a card and 3.7 on the bare
   page — so the captions that sit STRAIGHT on the ground get `--page-quiet`,
   linen at .90 (4.8), exactly as GJPalette.dark splits `muted` from
   `pageMuted`. The handful that sit in the TOP WASH — "MEMBERS", "My page",
   the hero's eyebrow, the booting line — get `--ink` outright, because there
   the ground reaches #586d4b and full linen is already only 4.7: every
   quieter voice on this palette is under the floor in that band.

   `--danger` is #e4a385, the dark page's own red lifted until it clears the
   floor on a dark card; the daylight skin's #b5502f is measured for white
   and is 2.2 here.

   `--primary` is the slate and not the club's green: a green pill on a green
   page is not a pill.

   ---- what it measures ----

   Every text/background pair this block changes, on the ground it actually
   sits on. A card's ground is the composite of the .72 gradient's LIGHTEST
   stop over the page (#304727, the worst case); a tile and a booking row are
   the night at .42 over it (#36462c); the page is measured bare (#4e6342)
   and under the top wash at its strongest — #586d4b, sampled out of a
   rendered frame rather than worked out from the gradient, because the
   gradient's centre is 200px above the viewport and only part of its .30
   ever lands on a pixel anybody sees.

   Large display type (the hero's script line, a headline) is held to 3:1;
   everything else to 4.5.

     linen on the page 5.4         linen on page+wash 4.7
     page-quiet on the page 4.8    ink-600 on the page 4.6
     linen on a card 8.4           ink-600 on a card 6.9
     muted on a card 5.2           linen-400 on a card 5.5
     accent on a card 7.4          danger on a card 4.8
     ember-400 on a card 5.5       linen on a field 11.3
     muted on a field 6.6          linen on a select 13.3
     linen on the track 10.4       linen-100 on a free cell 7.7
     linen-300 on a free cell 6.1  linen-100 on a selected cell 4.6
     reserve-900 on the start cell 14.0    ember on the start cell 4.5
     danger on a booked cell 7.2   accent on an extendable cell 5.4
     muted on a too-short cell 7.0 linen on the summary bar 9.6
     muted on the summary bar 5.9  danger on the summary bar 5.5
     linen-050 on the slate pill 5.2       ink-600 on the disabled pill 5.7
     linen on the ticket's head 9.9        linen on the ticket's body 8.4
     muted on a ticket panel 6.4   accent on a ticket panel 9.4
     fairway-300 on a ticket panel 4.6     reserve-900 on the date chip 14.0
     ink on the hero eyebrow 4.7 (on the wash, which is why it is not ember:
     #cddee8 measures 4.4 there at 11.5px)
     ember-400 on "your time." 3.5, at 44px — the large-text floor is 3

   And the member page's own chrome, which nothing had measured before:

     ink on "MEMBERS" and on "My page" 4.7 (both in the wash)
     linen on the tab bar 13.3     muted on the tab bar 7.5
     accent on the active tab 11.7 linen-100 on the hero's head 10.6
     ember-300 on the hero's head 8.7      the hero's lede on it 8.1
     linen on a tile 8.4           linen-400 on a tile 5.5
     accent on a tile 7.4          linen on a booking row 8.4
     muted on a row 5.2            linen-400 on a row 5.5
     accent on the paid chip 7.4
     danger on the awaiting-payment chip 4.8 (and the pay-by line with it)
     accent on the allowance bar 4.9 over its own track
     page-quiet on a section title 4.8
     linen on a sign-in field 11.3 muted on its placeholder 6.6
     Google's #1f1f1f on its own white button 16.5
     black on Apple's white button 21.0    the diag line's red on white 7.2

   And the packages, which are tiers now rather than a list of six lines. A
   tier is the night at .42 over the ground, like every other row; the
   segment track is the darkest green at .34 over it, and the chosen segment
   the night at .72 over THAT:

     ink on a tier 8.4            muted on a tier (the rate) 5.2
     ok on a tier (the saving) 7.4        linen-400 on a tier 5.5
     muted on the segment track 5.1       ink on the chosen segment 11.9
     reserve-900 on the BEST VALUE badge 12.3
     ink-600 on the sentence under the button 4.6 (it is on the page: the
     card around the packages has no paper in either skin)

   A locked step keeps the skin's `opacity:.75`. On a dark ground that fade
   runs the safe way — light type fading towards a dark page stays legible —
   and the weakest thing in one, the lock note, still measures 3.5.
   ================================================================== */

@media (prefers-color-scheme: dark) {

  /* ---- the palette ---- */
  body.linen {
    /* the ground and the card */
    --paper:#4e6342;                      /* the fairway page */
    --card:rgba(20,31,14,.72);            /* a dark translucent surface */
    --hairline:rgba(236,234,213,.16);
    --line:rgba(236,234,213,.16);
    --card-shadow:0 18px 44px rgba(13,20,9,.45);

    /* type */
    --ink:#ecead5;
    --ink-600:rgba(236,234,213,.88);
    --muted:rgba(236,234,213,.72);
    --linen-400:#c3bfa2;

    /* The one bold button is the dark page's slate, not the club's green. */
    --primary:#4e6c88;
    --primary-ink:#faf9f2;
    --accent:#cddee8;
    --danger:#e4a385;

    /* The saving on a package. Daylight's leaf green is 1.4:1 on this page;
       a green word on a green page is not a word, so the success voice here
       is the slate tint — 7.4 on a tier, the same as every other accent. */
    --ok:#cddee8;

    /* The quiet voice for captions that sit on the ground rather than on a
       card — see the note above. */
    --page-quiet:rgba(236,234,213,.9);

    /* The phone's own date picker, the bay <select> and the sign-in fields
       are drawn by the platform and would come up as white sheets sliding
       over a dark page without this. The ROOT's copy — which is what makes
       the viewport's scrollbars and the canvas dark, and which a sheet
       scoped to <body> cannot write — is in book-shell.css and my.css. */
    color-scheme:dark;
  }

  /* ---- the ground ----
     The skin flattens this to one colour and `background-image:none`; the
     washes are the page's own depth and they come back. The same two
     gradients styles.css has always drawn, and the same two GJPalette.dark
     carries as pageWashTop and pageWashRight. */
  body.linen {
    background-color:var(--paper);
    background-image:
      radial-gradient(1400px 700px at 50% -200px, rgba(139,160,124,.30), transparent 70%),
      radial-gradient(1000px 650px at 100% 40%, rgba(20,31,14,.42), transparent 70%);
    color:var(--ink);
  }
  /* Every hover in the daylight skin lands on the club's green, which in
     here is the slate — 2.3:1 on this page. A hover on a dark ground goes
     the other way: towards the linen. */
  body.linen a:hover,
  body.linen .cta:hover,
  body.linen .see-all:hover { color:var(--ink); }
  /* Two lines that sit in the top wash, where even full linen is 4.6 and
     nothing quieter clears the floor at all. */
  body.linen .booting { color:var(--ink); }

  /* ---- the one bold button, and the one that is not ready yet ----
     The skin's hover is the green pressed; this is the slate pressed. The
     disabled pill cannot be the daylight answer either — a linen-grey fill
     is the brightest thing on a dark screen, which is the opposite of "not
     yet" — so it is the slate at .34, which is what the member's own dark
     page has always used, with the quiet ink on it at 5.7. */
  body.linen .primary:hover { background:#3f607d; }
  body.linen .confirm-btn:hover:not(:disabled) { background:#3f607d; }
  body.linen .primary:disabled,
  body.linen .confirm-btn:disabled {
    background:rgba(78,108,136,.34); color:var(--ink-600);
    box-shadow:inset 0 0 0 1px var(--hairline);
  }

  /* ---- the surfaces ----
     A card needs a hairline here to separate from the page; in daylight the
     1.21:1 of white against the linen did that job and the skin took the
     border off. A flat translucent block is not what the app draws either:
     it draws the 180° gradient from reserve to the darkest green at .72,
     which is why these are named rather than left to the token. */
  body.linen .card,
  body.linen .sheet,
  body.linen .details-card {
    background:linear-gradient(180deg, rgba(37,60,29,.72), rgba(20,31,14,.72));
    border:1px solid var(--line);
  }
  body.linen .sport-panel,
  body.linen .facility-card { border:1px solid var(--line); }
  /* A tile and a row are shallower than a card — sunk into the page rather
     than lifted off it, which is what the member's own dark page drew. The
     night at .42 rather than my.css's .34 for the row: the awaiting-payment
     chip's red is 4.4 on the lighter one and 4.8 on this. */
  body.linen .tile,
  body.linen .row {
    background:rgba(20,31,14,.42);
    border:1px solid var(--line);
  }
  /* The figures on both. The daylight skin sets them in the club's green,
     which in the dark is the slate — 2.5:1 on a tile. Every number on this
     page goes back to the linen it was written in. */
  body.linen .tile b,
  body.linen .balance b,
  body.linen .allowance-card b { color:var(--ink); }
  body.linen .allowance-bar { background:rgba(236,234,213,.16); }

  /* ---- buying hours ----
     A tier is a row like every other row on this page, so it is the night at
     .42 over the ground and not the skin's white card. The segmented control
     is cut the other way: its track is darker than the page and the chosen
     segment darker still, which is the only way a pill reads as pressed on a
     ground this light. */
  body.linen .plan-tier {
    background:rgba(20,31,14,.42);
    border:1px solid var(--line);
  }
  body.linen .plan-tier:hover { background:rgba(20,31,14,.58); }
  body.linen .plan-tier.picked { box-shadow:inset 0 0 0 2px var(--accent), var(--card-shadow); }
  body.linen .tier-mark { border:1.5px solid var(--linen-400); }
  body.linen .buy .seg { background:rgba(13,20,9,.34); }
  body.linen .buy .seg-btn.on { background:rgba(20,31,14,.72); box-shadow:none; }
  /* The badge is the accent, which in the dark is a near-white tint — so the
     word on it is the club's darkest green and not the page's linen. */
  body.linen .tier-badge { color:#141f0e; }
  /* The club's crest behind the balance. At .55 it is a watermark on a white
     card and a lamp on a dark one — the linen emblem came out very nearly as
     bright as the number standing on it, and read as a second thing on the
     card rather than as its paper. */
  body.linen .balance::before { opacity:.14; }

  /* ---- the captions that sit on the page rather than on a card ---- */
  body.linen .section-title,
  body.linen .allowance,
  body.linen .version,
  body.linen .version a,
  body.linen .bear-empty,
  body.linen .act .act-lock-note,
  body.linen .drawer-label { color:var(--page-quiet); }
  body.linen #tiles.updating::after { color:var(--page-quiet); }
  body.linen .act-num { -webkit-text-stroke-color:rgba(236,234,213,.13); }

  /* ---- the club's bear ----
     He is dark-green line art on nothing, chosen so he reads on the linen
     and on a white card alike. On a dark card he is a bear nobody can see.
     `brightness(0)` flattens every stroke to black and `invert(1)` turns the
     lot to white, both of which leave the alpha channel alone — so the same
     one file draws him in either skin and there is no second asset to keep
     in step with the first. */
  body.linen .bear-empty img,
  body.linen .card-bear,
  body.linen .signed-out .bear,
  body.linen .slot-empty img { filter:brightness(0) invert(1); opacity:.82; }

  /* ---- the chips on a booking ---- */
  body.linen .chip.paid { border-color:rgba(205,222,232,.42); }
  body.linen .chip.due { border-color:rgba(228,163,133,.5); }

  /* ---- waiting ----
     Both skeletons draw dark light across a light ground. Here the light
     moves the other way. */
  body.linen .shimmer,
  body.linen .timeline-skeleton {
    border-color:rgba(236,234,213,.1);
    background:linear-gradient(90deg, rgba(236,234,213,.04), rgba(236,234,213,.13), rgba(236,234,213,.04));
    background-size:200% 100%;
  }

  /* ---- fields ----
     A field is a well sunk into the card, which is GJPalette.dark's `field`:
     the darkest green at .42. The focus ring is the ember tint, because the
     slate the skin rings with disappears into a dark card. */
  body.linen .field input,
  body.linen .field textarea {
    background:rgba(13,20,9,.42); color:var(--ink); border-color:var(--hairline);
  }
  body.linen .field input:focus,
  body.linen .field textarea:focus {
    border-color:#a9c2d1; box-shadow:0 0 0 3px rgba(169,194,209,.28);
  }
  body.linen select { background-color:rgba(13,20,9,.46); }

  /* ==================================================================
     02 · THE TEE SHEET
     Every cell state below was written out in ink by the daylight skin, and
     every one of them is a colour a member reads a time off.
     ================================================================== */

  /* The date rail. The selected chip is the page's own linen rather than the
     slate pill: the date is the loudest thing in this row and linen on a
     dark sheet is the loudest it can be, at 14.0 for the day and month over
     it — the slate chip left those two at 3.6. */
  body.linen .date-chip { background:var(--card); border-color:var(--hairline); color:var(--ink); }
  body.linen .date-chip:hover { border-color:rgba(236,234,213,.45); }
  body.linen .date-chip.selected {
    background:var(--ink); color:#141f0e; border-color:var(--ink);
  }
  body.linen .date-native input {
    background:rgba(13,20,9,.42); color:var(--ink); border-color:var(--hairline);
    color-scheme:dark;
  }

  body.linen .timeline-track { background:rgba(13,20,9,.38); border-color:var(--hairline); }
  body.linen .time-segment {
    background:rgba(127,176,105,.22); color:#f3f1e5;
    border-right-color:rgba(236,234,213,.1);
  }
  /* A tapped cell keeps :hover on iOS, and this rule outranks the selected
     states by one pseudo-class — so the start of a booking was painted with
     the hover wash under the selection's pale text. Selected cells keep
     their own fill, whatever the pointer is doing. */
  body.linen .time-segment:hover:not(:disabled):not(.selected) { background:rgba(127,176,105,.48); }
  body.linen .time-segment small { color:#dcd9bf; }
  body.linen .time-segment.selected {
    background:rgba(236,234,213,.32); color:#f3f1e5;
    box-shadow:inset 0 -3px 0 var(--primary);
  }
  body.linen .time-segment.selected small { color:#f3f1e5; }
  /* The first cell of the booking is solid linen with the darkest green on
     it — the one inversion on the board, so the start of a selection is
     unmistakable at a glance down a scrolling day. */
  body.linen .time-segment.selected-start {
    background:var(--ink); color:#141f0e;
    box-shadow:inset 0 -4px 0 var(--primary);
  }
  body.linen .time-segment.selected-start small { color:#4e6c88; }
  /* A taken half-hour: the hatch runs dark over the track rather than dark
     over paper, and its caption is the dark page's red at 7.2 on it. */
  body.linen .time-segment.booked,
  body.linen .time-segment.unavailable {
    background:repeating-linear-gradient(-45deg, rgba(13,20,9,.78) 0 8px, rgba(13,20,9,.42) 8px 16px);
  }
  body.linen .time-segment.booked small,
  body.linen .time-segment.unavailable small { color:var(--danger); }
  /* Free, but too narrow to book: sunk into the track, never struck through,
     and its note stays a plain word rather than a warning. */
  body.linen .time-segment.too-short { background:rgba(13,20,9,.34); }
  body.linen .time-segment.too-short small { color:var(--muted); }
  body.linen .time-segment.extendable {
    background:rgba(127,176,105,.3); box-shadow:inset 2px 0 0 #a9c2d1;
  }
  body.linen .time-segment.extendable small { color:var(--accent); }
  body.linen .time-segment.extendable:hover:not(:disabled):not(.selected) { background:rgba(127,176,105,.52); }
  body.linen .time-segment.end-boundary.extendable small { color:#f3f1e5; }

  body.linen .time-selection { background:rgba(13,20,9,.3); border-color:var(--hairline); }
  /* The chevron is a data URI, so its stroke is a colour inside a URL and
     cannot be re-toned any other way: the same 12×8 path, drawn in linen. */
  body.linen .time-pick {
    color:var(--ink);
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%23ecead5' stroke-opacity='.55' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
  /* The option list is drawn by the OS from its own palette and ignores the
     page's, so it keeps an explicit readable pair rather than inheriting a
     translucent card that composites over nothing. */
  body.linen .time-pick option { color:#12200c; background:#faf9f2; }

  body.linen .duration-control button {
    border-color:rgba(236,234,213,.28); background:rgba(236,234,213,.08); color:var(--ink);
  }
  body.linen .duration-control button:hover:not(:disabled) { background:rgba(236,234,213,.18); }
  body.linen .slot-error button {
    border-color:rgba(236,234,213,.24); background:rgba(236,234,213,.08); color:var(--ink);
  }
  body.linen .slot-error button:hover { border-color:rgba(236,234,213,.5); background:rgba(236,234,213,.12); }
  /* Both rails scroll sideways, and the daylight skin drew their thumbs in
     ink — on a dark page that is a scrollbar that is not there. */
  body.linen .date-rail { scrollbar-color:rgba(236,234,213,.35) transparent; }
  body.linen .date-rail::-webkit-scrollbar-thumb { background:rgba(236,234,213,.3); }
  body.linen .timeline { scrollbar-color:rgba(236,234,213,.35) transparent; }

  /* ---- the sticky bar ----
     Nearly opaque rather than .72: it floats over whatever is being
     scrolled, and a translucent bar with a time board sliding under it is
     unreadable in a way a card on a still page never is. */
  body.linen .summary-bar {
    background:linear-gradient(180deg, rgba(37,60,29,.94), rgba(20,31,14,.94));
    border-color:rgba(236,234,213,.22);
    box-shadow:0 18px 44px rgba(13,20,9,.5);
  }

  /* ---- the progress toast ---- */
  body.linen .booking-progress-track { background:rgba(236,234,213,.18); }

  /* ==================================================================
     THE TICKET, BOTH OF THEM
     The booking page's confirmation and the member page's next session are
     the same card drawn twice, and in daylight they share a white body. The
     head goes back to reserve — `--fairway` is the colour of the PAGE here,
     which would leave the ticket's top with no edge at all — and the body
     becomes the same card as everything else, with its panels sunk into it.
     ================================================================== */
  body.linen .ticket-top,
  body.linen .my-ticket-top { background:#253c1d; }
  body.linen .ticket-perf { border-top-color:rgba(236,234,213,.3); }
  body.linen .my-ticket-perf {
    background:none; border-top-color:rgba(236,234,213,.3);
  }
  body.linen .ticket-body,
  body.linen .my-ticket-body {
    background:linear-gradient(180deg, rgba(37,60,29,.72), rgba(20,31,14,.72));
    color:var(--ink);
  }
  body.linen .summary-item {
    background:rgba(13,20,9,.34); border-color:var(--line);
  }
  body.linen .summary-label { color:var(--muted); }
  body.linen .summary-value { color:var(--ink); }
  body.linen .ticket-payment {
    background:rgba(13,20,9,.34); border-color:rgba(205,222,232,.42);
  }
  body.linen .ticket-payment.on-credits { border-color:rgba(139,160,124,.5); }
  body.linen .ticket-payment p { color:var(--muted); }
  body.linen .ticket-payment .pay-due,
  body.linen .ticket-payment .pay-due strong,
  body.linen .ticket-payment .pay-by strong { color:var(--accent); }
  /* Nothing is owed on this line — it is what the hours paid for — so it
     stays the club's green rather than joining the blues. */
  body.linen .ticket-payment .pay-credits { color:#8ba07c; }
  body.linen .pay-row { border-top-color:var(--line); }
  body.linen .ticket-note { color:var(--muted); }
  /* Outlined, so "Book another slot" stays the one solid button on the card. */
  body.linen .calendar-btn {
    background:transparent; color:var(--accent); border-color:rgba(169,194,209,.5);
  }
  body.linen .calendar-btn:hover { background:var(--primary); color:var(--primary-ink); }

  /* ==================================================================
     THE GUEST'S BOOKING PAGE, AFTER DARK
     The marketing header, the hero and the footer: the three the member's
     chrome takes away and a guest always has. Every one of them was drawn
     for the linen and is a light smear on a night page.
     ================================================================== */
  body.linen .topbar { background:linear-gradient(rgba(13,20,9,.85), rgba(13,20,9,0)); }
  /* The wordmark and the footer emblem are the RESERVE files — dark green
     artwork, chosen because the ground under them is paper. On the night
     ground they are very nearly invisible: the mark measures about 1.2:1
     against the bar's own scrim. Both have a linen twin in /assets, and
     `content:` on an <img> swaps the file the element renders without
     touching the markup, which is the only thing a stylesheet can do here —
     one document serves each page to a browser, to Capacitor and to the
     shell, and only some of those are ever dark. */
  body.linen .topbar-mark { content:url("assets/wordmark-h-linen.png"); }
  body.linen .foot img { content:url("assets/emblem-linen.png"); opacity:.55; }
  body.linen .topbar.scrolled { background:rgba(13,20,9,.82); border-bottom-color:var(--hairline); }
  body.linen .step-dot { color:var(--ink-600); }
  body.linen .step-dot.on { color:var(--accent); }
  body.linen .topbar-est { color:var(--ink-600); }
  /* The hero stops being a boxed card for the same reason the ticket's head
     changes colour: on a dark page it was a dark card on a ground of very
     nearly the same green. */
  body.linen .hero-inner {
    background:none; box-shadow:none; padding:0; border-radius:0; overflow:visible;
  }
  /* The eyebrow sits in the strongest part of the top wash (#546948 in a
     rendered frame), where the ember blue is 4.36:1 at 11.5px. Its own short
     ember rule keeps the brand cue in front of it; the words go linen. */
  body.linen .hero-eyebrow { color:var(--ink); }
  /* "your time." stays ember: it is 44px display type, where the floor is
     3:1 and it measures 3.5 on the same ground. */
  body.linen .hero-line-2 { color:#a9c2d1; }
  body.linen .hero-lede { color:var(--page-quiet); }
  body.linen .signed-in { color:var(--ink-600); }
  body.linen .signed-in a { color:var(--accent); border-bottom-color:rgba(205,222,232,.4); }
  body.linen .signed-in a:hover { color:var(--ink); }
  body.linen .foot { color:var(--page-quiet); border-top-color:var(--hairline); }
  body.linen .foot a { color:var(--page-quiet); border-bottom-color:var(--hairline); }
  body.linen .foot a:hover { color:var(--ink); }

  /* ==================================================================
     THE DOORS
     The sign-in card on the member's page, which is the one screen on this
     site a stranger can reach and the only one with somebody else's brand
     on it. Two of the three controls here are not ours to re-tone freely.
     ================================================================== */
  body.linen .signed-out .doors { --door-line:rgba(236,234,213,.24); }
  /* The wordmark on the card is the reserve file, because in daylight the
     card under it is white. Here it is the night card, so it is the linen
     twin — the same swap the marketing header makes above. */
  body.linen .signed-out .mark { content:url("assets/wordmark-h-linen.png"); }
  body.linen .signed-out .doors input {
    background:rgba(13,20,9,.42); color:var(--ink);
  }
  /* .7 of linen-at-.72 is half a linen, which is 3.9 on the field. The
     placeholder is the example number a member copies the shape of. */
  body.linen .signed-out .doors input::placeholder { opacity:.85; }
  body.linen .signed-out .doors .btn-primary:hover:not([disabled]) { background:#3f607d; }
  /* Google's button stays Google's: their own white face, their own #1f1f1f
     label. `--card` is a dark translucent surface in here and would have
     painted near-black type onto it. Google's brand guidelines allow the
     light button on a dark background, and it is the one they draw. */
  body.linen .signed-out .doors .btn-google { background:#fff; border-color:rgba(0,0,0,.18); }
  body.linen .signed-out .doors .btn-google:hover { border-color:rgba(0,0,0,.4); }
  /* And Apple's is Apple's: their Human Interface Guidelines give the black
     button for light backgrounds and the WHITE one for dark, and a black
     pill on this card has no edge at all. The mark is `fill:currentColor`,
     so it turns over with the label. */
  body.linen .signed-out .doors .btn-apple {
    background:#fff; color:#000; border-color:#fff;
  }
  body.linen .signed-out .doors .btn-apple:hover { background:#e6e6e6; }

  /* The error line the app writes onto the page when something throws. It is
     a white box on purpose — it is not part of the design, it is a fault
     report — and the skin's red is measured for a dark card, which is 1.9 on
     white. This is the same red taken down until it reads on the paper it is
     printed on. */
  body.linen .diag { color:#9c3418; }

  /* ==================================================================
     THE MEMBER'S OWN CHROME
     The header, the tab bar and the hero card: the three things only /my/
     and member mode have, which the native shell hides and so nothing has
     ever had to draw in the dark before.
     ================================================================== */

  /* The header's wordmark, for the third time and the last: the ground under
     it here is the bare page. */
  body.member .top .mark { content:url("assets/wordmark-h-linen.png"); }
  /* "MEMBERS" sits in the strongest part of the top wash, where full linen
     is 4.6 and every quieter voice is under the floor. */
  body.member .top .members { color:var(--ink); }
  /* "My page" is in the same band as "MEMBERS", and the accent is 4.2 there.
     It keeps its weight and its place at the end of the header to say it is
     a control; what it gives up is a colour nobody could read. */
  body.member .to-my,
  body.member .to-my:hover { color:var(--ink); }

  /* The hero. In daylight it is the club's night ground on the linen; here
     that gradient STARTS at the colour of the page, so the card had no edge
     at all. It runs from reserve to the darkest green instead — the same two
     stops the ticket's head and the app's own dark hero use — and every line
     on it gains contrast rather than losing it. */
  body.linen .my-hero {
    background:linear-gradient(165deg, #253c1d 0%, #141f0e 100%);
    border:1px solid var(--line);
  }

  /* The tab bar. `--card` is a translucent surface and a tab bar is the one
     fixed thing a whole page scrolls under, so it is nearly opaque — the
     same .92 the member's own dark page has always used. */
  body.member .tabs {
    background:rgba(20,31,14,.92);
    box-shadow:0 -6px 24px rgba(13,20,9,.5);
  }

  /* ==================================================================
     THE GUEST'S INVITATION, AFTER DARK

     Google's button does NOT change. It is white with a hairline and a
     coloured mark on every ground, in every product that draws it, and a
     dark-mode version of somebody else's button is a different button. Only
     the hairline moves, from ink on linen to black on white, because the
     pill's own ground is still white here.

     What does change is everything around it: the copy that sits on the
     fairway page and the panel on the ticket. Apple's badge changes too —
     the black artwork on the ticket's night panel is a black box on a dark
     green ground, and the white one is the file Apple ships for exactly
     that — but the swap is a <picture> in index.html rather than a rule
     here; see the note further down.
     ================================================================== */

  /* Full linen, not the quiet voice: this line sits on the PAGE rather than
     on a card, at 5.4 against the fairway, and the quiet .9 is 4.5 there
     with the hero's own wash still over the top of it. */
  body.linen .guest-invite-hero .guest-invite-copy { color:var(--ink); }
  body.linen .guest-invite-copy { color:var(--ink-600); }

  body.linen .btn-google { background:#fff; color:#1f1f1f; border-color:rgba(0,0,0,.18); }
  body.linen .btn-google:hover:not([disabled]) { border-color:rgba(0,0,0,.4); }

  body.linen .guest-invite-details { border-bottom-color:var(--line); }

  /* Sunk into the ticket's body, like the payment panel and the summary
     items beside it: ink-600 reads 8.6 on it and muted 6.4. */
  body.linen .guest-invite-ticket {
    background:rgba(13,20,9,.34); border-color:var(--line);
  }
  body.linen .guest-app { border-top-color:var(--line); }
  /* No rule for the badge. It swaps itself — a <source> with this very
     media query, in index.html — because the artwork is Apple's and which
     of their two files is served is part of using the badge properly rather
     than part of dressing the page. The white one was read at 2.5× on this
     panel before it was chosen: white ground, black type, their border. */

  /* The daylight red is 2.6 on this page. The skin's own --danger is the
     one measured for it. */
  body.linen .guest-invite-status { color:var(--danger); }

  /* ---- inside the app ----
     The shadow every card comes down to on a flick. Not a redefinition of
     --card-shadow, which is measured and pinned: this is the app's own
     cheaper drop, and in the dark it is a darker one. */
  body.linen.app { --app-shadow:0 4px 12px rgba(13,20,9,.35); }
}
