/**
 * Two themes, one set of names.
 *
 * Every colour in this project resolves through a token below, so the light
 * theme is a second set of values rather than a second set of rules. The dark
 * values here are the original palette, unchanged — dark mode looks exactly as
 * it always did.
 *
 * Tokens are named for their *role* (surface, line, text, tint) rather than
 * their colour, because "oxblood" means a deep red on black and a pale blush on
 * white, and only the role survives the switch.
 */
:root{
  color-scheme:dark;

  /* surfaces, recessed → raised */
  --ink:#0A0A0B;              /* the page */
  --pitch:#111114;            /* recessed block */
  --panel:#17171B;            /* a panel */
  --panel-2:#1C1C21;          /* a cell sitting on a panel */
  --panel-3:#2B2B33;          /* hovered cell */
  --panel-head:#1B1B20;       /* top of a panel-header gradient */
  --topbar-top:#0F0F11;       /* top of the masthead gradient */
  --closed-bg:#101014;        /* a court that simply isn't open */
  --stripe-a:#141419;         /* hatching for taken / past */
  --stripe-b:#1A1A20;
  --scrim:rgba(14,14,16,.94); /* sticky bars over content */
  --qr-bg:#EDEAE5;            /* QR codes need a light field to scan */

  --line:#2A2A31;
  --line-soft:#1F1F25;
  --chalk:rgba(237,234,229,.10);   /* grid rules */

  /**
   * Elevation. On dark, depth comes mostly from the surface being lighter than
   * the page, so the shadow only needs to firm up the edge. On light, where
   * every card is white on near-white, the shadow is doing the actual work.
   */
  --shadow-sm:0 1px 2px rgba(0,0,0,.34);
  --shadow:0 1px 2px rgba(0,0,0,.30), 0 6px 16px rgba(0,0,0,.34);
  --shadow-up:0 -4px 20px rgba(0,0,0,.55);

  /**
   * Type, loudest to quietest.
   *
   * --ash-dim was #5E5E67, which measured 2.79:1 on a panel — it carries the
   * eyebrows, hints and micro-labels, so nine separate places on the page sat
   * under AA. Meeting 4.5:1 against the lightest surface it lands on (--panel,
   * #17171B) needs roughly #808089, which would have left it all but touching
   * --ash and flattened three tiers into two. So --ash rises with it and the
   * hierarchy survives: measured 6.6 / 4.9 against that same panel.
   */
  --bone:#EDEAE5;
  --ash:#9C9CA6;
  --ash-dim:#85858E;
  --faint:#4A4A52;
  --fainter:#33333B;

  /* the brand red, and surfaces tinted with it */
  --signal:#DE2E28;
  --signal-hot:#FF5347;
  /**
   * Text sitting on a red fill. White, matching what the light theme already
   * did — near-black #12040A measured only 4.32:1 on --signal.
   *
   * The brand red sits at a mid luminance, so it is a genuinely hard ground to
   * write on: white reaches 4.64:1 and black 4.53:1, and every value between
   * them falls below AA. That is why there is no softened variant here any
   * more — a translucent white composites to exactly that failing middle
   * (82% measured 3.52:1). On a red fill, text is white or it is illegible.
   */
  --on-signal:#FFFFFF;
  --oxblood:#4A0B12;          /* a red-tinted chip */
  --tint:#201214;             /* a red-tinted panel */
  --tint-line:#6B121B;
  --tint-text:#FFD9D6;
  --tint-text-soft:#FFB9B3;
  /* The crest is red and gold on black, so gold joins the palette — but gold
     and the amber --warn sit ~10 degrees apart on the wheel, and no honest
     hue shift separates them (rotating either way lands on red, which is
     already --signal). So they are separated by *role* instead: gold is only
     ever a hairline, a rule or a 4px dot, and never text or a fill. Warn is
     only ever text or a fill. Nudging warn toward orange below helps a little;
     the role rule is what actually does the work, so keep gold off anything
     that states a booking's condition. */
  --gold:#EFB53F;
  --gold-soft:#7A5514;

  /**
   * Reserved for things that are live: the slot you have selected, the step you
   * are on, the hold that is counting down. Everything else stays quiet, which
   * is the only reason these read as signal rather than decoration.
   *
   * Bloom is inset wherever it sits on a chamfered element. clip-path clips an
   * outer box-shadow away entirely, so an outer glow on a cut corner renders as
   * nothing at all; inset glow falls inside the clip and survives.
   */
  --chamfer:9px;
  --bloom:0 0 18px rgba(255,83,71,.34);
  --bloom-in:inset 0 0 14px rgba(255,83,71,.42);

  /**
   * The red used for small text on a live marker. It needs its own name because
   * the most legible red is a different one in each theme: on black the brighter
   * --signal-hot wins (6.2:1 against --signal's 4.3, which fails AA at 10px),
   * while on white the darker --signal wins. One token, opposite ends.
   */
  --live:#FF5347;

  /* The peak-hour tier label is 8.5px, where WCAG gives no size relief.
     #B4525A measured 3.66:1 on a panel; this keeps the same muted rose and
     clears 4.5. */
  --peak-text:#C9707A;
  --ticket-grad:#1B0F11;
  --ticket-dash:#4A2B2D;
  --blocked-a:#221014;
  --blocked-b:#1A0C0F;
  --blocked-text:#8C5058;
  --banner-bg:#1B1013;
  --banner-text:#F0D9D7;
  --callout-bg:#170F11;
  --callout-text:#D9CFCD;

  /* states */
  --ok:#3FBF7F;
  --ok-line:#1F5E42;
  --ok-bg:#0F2A1F;
  --ok-soft-bg:#0E1A14;
  --ok-text:#6FD9A4;
  --ok-soft-text:#B7DCC7;

  --warn:#E0872C;
  --warn-line:#6E4A12;
  --warn-bg:#26190C;
  --warn-bg-2:#31220F;
  --warn-soft-bg:#1E1308;
  --warn-text:#E8A34A;
  --warn-soft-text:#EBCEAB;
  --warn-strong:#FFD5A2;
  --busy-line:#5C3A16;

  --info-line:#2A4A6B;
  --info-bg:#0F1B27;
  --info-text:#7FB6E8;

  --booked-a:#241216;
  --booked-b:#1B0E11;
  --booked-line:#4A1B22;

  /* The toast deliberately inverts the page so it reads as an overlay. */
  --toast-bg:#EDEAE5;
  --toast-text:#111114;

  /* Space Grotesk carries ₱ (U+20B1) itself — measured under two fallback
     chains, it renders identically in both, which a substituted glyph never
     does. Antonio, which this replaces, did not: it substituted a wider, heavier
     peso from whatever the system offered. Chivo is named next as a same-family
     fallback if Space Grotesk fails to load; it also has the glyph. */
  --f-display:'Space Grotesk','Chivo',ui-sans-serif,system-ui,sans-serif;
  --f-body:'Chivo',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
  --f-mono:'Chivo Mono',ui-monospace,SFMono-Regular,Menlo,monospace;

  --r:2px;
}

/**
 * Light theme: white page, near-black text, as asked. The reds are darkened a
 * step because #E8352B on white is only about 3.4:1 — fine for the big display
 * numerals, too thin for 12px labels. Tinted surfaces invert from deep red on
 * black to a pale blush on white, keeping the same visual hierarchy.
 *
 * Keyed on the attribute rather than a media query: js/theme.js resolves the
 * system preference and stamps `data-theme` on <html> before first paint, so
 * one block covers both "follow the OS" and an explicit choice from the toggle.
 * Written as a media query it would have to exist twice.
 */
:root[data-theme="light"]{
    color-scheme:light;

    /* The page is a tinted canvas and the cards are white, so a panel reads as
       a panel without depending on its 1px border. White-on-white left every
       box flat against the background. */
    --ink:#F1F2F5;
    --pitch:#FFFFFF;
    --panel:#FFFFFF;
    --panel-2:#F4F5F8;
    --panel-3:#E8EAEF;
    --panel-head:#FAFBFC;
    --topbar-top:#FFFFFF;
    --closed-bg:#F4F5F7;
    --stripe-a:#F0F1F4;
    --stripe-b:#E6E7EC;
    --scrim:rgba(255,255,255,.94);
    --qr-bg:#FFFFFF;

    --line:#D9DAE1;
    --line-soft:#E8E9EF;
    --chalk:rgba(17,17,20,.12);

    --shadow-sm:0 1px 2px rgba(20,22,38,.07);
    --shadow:0 1px 2px rgba(20,22,38,.05), 0 6px 16px rgba(20,22,38,.08);
    --shadow-up:0 -4px 20px rgba(20,22,38,.10);

    --bone:#111114;
    --ash:#54545E;
    /* Light needs the opposite move — darker, not lighter. #71717C sat at
       4.31:1 on the tinted canvas; this clears 4.5 without colliding with
       --faint just below it. --ash already passed and is left alone. */
    --ash-dim:#6A6A74;
    /* These land on the hatched "taken / past" fill, which is nearly white —
       the dark-mode values would sit at about 2.4:1 there. */
    --faint:#6C6C76;
    --fainter:#7C7C86;   /* still the quietest tier, but visible on a white cell */

    --signal:#B81F26;
    --signal-hot:#DE2E28;
    --on-signal:#FFFFFF;
    --oxblood:#FBE3E0;
    --tint:#FDEFED;
    --tint-line:#EBB2AC;
    --tint-text:#8A1A13;
    --tint-text-soft:#A5322A;
    /* Gold has to darken hard to stay legible on white — the logo's #EFB53F
       sits at about 1.9:1 there. */
    --gold:#8A6410;
    --gold-soft:#E3CB94;

    /* Glow needs a dark ground to bloom against; on white it just reads as a
       blurry edge. So on light the same tokens become a crisp ring — the live
       thing is still marked, by a hard edge instead of a soft one. */
    --live:#B81F26;
    --bloom:0 0 0 2px rgba(184,31,38,.20);
    --bloom-in:inset 0 0 0 1px rgba(184,31,38,.34);

    --peak-text:#9E3540;
    --ticket-grad:#FFF7F6;
    --ticket-dash:#E6BEB9;
    --blocked-a:#FBE7E4;
    --blocked-b:#F6DCD8;
    --blocked-text:#9E4038;
    --banner-bg:#FDECEA;
    --banner-text:#7A1A14;
    --callout-bg:#FBF1F0;
    --callout-text:#4A2A28;

    --ok:#12764A;
    --ok-line:#9AD3B5;
    --ok-bg:#E4F4EB;
    --ok-soft-bg:#ECF7F1;
    --ok-text:#0F6640;
    --ok-soft-text:#1B5B3C;

    --warn:#96560F;
    --warn-line:#DDB07A;
    --warn-bg:#FBEEDC;
    --warn-bg-2:#F7E3C8;
    --warn-soft-bg:#FDF5E9;
    --warn-text:#7A480C;
    --warn-soft-text:#5F3E10;
    --warn-strong:#4A2F08;
    --busy-line:#DFB483;

    --info-line:#A9C9E9;
    --info-bg:#EDF4FB;
    --info-text:#1A4A78;

    --booked-a:#FDECEA;
    --booked-b:#FBE3E0;
    --booked-line:#EDB9B3;

    --toast-bg:#17171B;
    --toast-text:#FFFFFF;
}

*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
  background:var(--ink);color:var(--bone);
  font-family:var(--f-body);font-size:15px;line-height:1.5;
  -webkit-font-smoothing:antialiased;
  padding-bottom:env(safe-area-inset-bottom);
}
[v-cloak]{display:none}
button{font:inherit;color:inherit}
input,select,textarea{font:inherit;color:inherit}
:focus-visible{outline:2px solid var(--signal-hot);outline-offset:2px}

.eyebrow{
  font-family:var(--f-mono);font-size:10px;letter-spacing:.22em;
  text-transform:uppercase;color:var(--ash-dim);
}

/* ---------- top bar ---------- */
/* Sticky, so it needs to sit above the content that scrolls under it rather
   than sharing an edge with it. */
.topbar{border-bottom:1px solid var(--gold-soft);background:var(--ink);position:sticky;top:0;z-index:30;box-shadow:var(--shadow-sm)}
.topbar-in{max-width:1180px;margin:0 auto;padding:14px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px}
.brand{display:flex;align-items:center;gap:11px;min-width:0}
.brand-mark{width:26px;height:26px;flex:none;border:1.5px solid var(--gold);position:relative;border-radius:1px}
.brand-mark::before{content:"";position:absolute;left:50%;top:0;bottom:0;width:1.5px;background:var(--signal);transform:translateX(-50%)}
.brand-mark::after{content:"";position:absolute;left:4px;right:4px;top:50%;height:1.5px;background:var(--signal);opacity:.45;transform:translateY(-50%)}
/* The crest is supplied on a solid black field with no transparency. Left bare
   it disappears into the dark theme but becomes a hard black rectangle in the
   light theme's white masthead. Seating it on a small rounded plate of that
   same black turns the field into a deliberate badge: invisible against --ink
   on dark, and a crest on light. Uploading a logo with real transparency still
   works — it just sits on the brand's own black, which is where it was drawn. */
.brand-logo{height:34px;max-width:130px;width:auto;object-fit:contain;display:block;flex:none;
  background:#0A0A0B;border-radius:5px;padding:2px}
.brand-name{font-family:var(--f-display);font-weight:400;font-size:17px;letter-spacing:.13em;text-transform:uppercase;line-height:1.1}
.brand-sub{display:block;margin-top:3px}
.guest-tag{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);padding:6px 10px;border-radius:var(--r);white-space:nowrap}

/* Tab labels don't wrap, so on a narrow phone the strip scrolls rather than
   shoving the whole page sideways. */
.topnav{display:flex;gap:6px;flex:0 1 auto;min-width:0;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.topnav::-webkit-scrollbar{display:none}
/* ---------- theme toggle ---------- */
.themebtn{
  flex:none;width:34px;height:32px;display:grid;place-items:center;cursor:pointer;
  background:transparent;border:1px solid var(--line);border-radius:var(--r);color:var(--ash);
  transition:border-color .15s ease,color .15s ease;
}
.themebtn:hover{border-color:var(--signal);color:var(--bone)}
.themebtn svg{width:16px;height:16px;display:block;fill:none;stroke:currentColor;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
/* Following the system is the quiet default, so it reads quieter. */
.themebtn.is-auto{color:var(--ash-dim)}
.themebtn .dotmark{fill:currentColor;stroke:none}

.navlink{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ash);
  background:transparent;border:1px solid var(--line);padding:7px 11px;border-radius:var(--r);cursor:pointer;white-space:nowrap}
.navlink:hover{border-color:var(--ash-dim);color:var(--bone)}
.navlink.is-on{border-color:var(--signal);color:var(--bone);background:var(--oxblood)}

/**
 * The masthead on a phone.
 *
 * Measured at 375px: the brand wanted 196px and the nav needed 203px inside a
 * 335px bar, so the nav lost 80px off its right edge. The theme toggle was
 * entirely off-screen, reachable only by dragging a strip that gives no hint it
 * can be dragged. Navigation has to win that fight, so the nav stops shrinking
 * and the brand gives up the room instead: the sub-line goes, and the name
 * drops to a size that sets on two short lines beside the crest.
 *
 * Losing the sub-line costs little — it reads "area · N courts", and the area
 * is decorative next to the venue's own name while the court count is stated
 * again on the board. Height matters as much as width here: the sub-line wrapped
 * to three lines, which alone made the bar 90px tall on a 812px screen.
 *
 * Scoped to .topbar throughout, because .brand-sub carries real content on the
 * desk's sign-in card ("Staff sign in", and the username on the password
 * screen) where there is no topbar and nothing to make room for.
 */
@media(max-width:700px){
  .topbar-in{padding:10px 14px;gap:10px}
  .topbar .brand{gap:8px}
  .topbar .brand-logo{height:30px}
  .topbar .brand-name{font-size:15px;letter-spacing:.02em}
  .topbar .brand-sub{display:none}
  /* Both the brand and the nav are shrinkable flex items, so by default they
     gave up space in proportion and the nav lost its right edge. Weighting the
     brand's shrink factor far higher makes it absorb effectively all of the
     squeeze, so the nav holds its natural width and stays fully visible.

     This is deliberately not `flex:none` on the nav. The desk's strip is four
     long tabs — 392px, wider than a 375px phone can ever show — and pinning it
     would push the whole page sideways instead of scrolling within the strip.
     Keeping the nav shrinkable means it still yields once the brand has nothing
     left to give, falling back to its own overflow-x rather than to a
     horizontally scrolling page. */
  .topbar .brand{flex-shrink:999}
  .topbar .navlink{padding:7px 9px;letter-spacing:.1em}
}

/**
 * Below ~360px there is roughly 55px left for the wordmark, which is not enough
 * for "SAN JACINTO" on one line — the name broke across three lines and pushed
 * the bar tall again. The crest carries the venue's name inside the artwork, so
 * at this width the text beside it is saying the same thing twice, and dropping
 * it is cheaper than shrinking type to 12px.
 *
 * Conditional on `.brand-logo +`, so this only applies when a logo image is
 * actually present. A venue running the built-in .brand-mark, or one whose logo
 * is a wordless symbol, keeps its name — there the text is the only identity
 * on the page. The <img alt> carries the name for screen readers either way.
 */
@media(max-width:360px){
  .topbar .brand-logo + div{display:none}
  .topbar .brand-logo{height:32px}
}

/* ---------- shell ---------- */
.shell{max-width:1180px;margin:0 auto;padding:26px 20px 40px;display:grid;gap:26px}
@media(min-width:980px){.shell{grid-template-columns:300px 1fr;align-items:start;gap:38px;padding-top:34px}}
@media(min-width:980px){.shell.is-solo{grid-template-columns:minmax(0,760px);justify-content:center}}

/* ---------- rail ---------- */
.rail{display:grid;gap:20px}
@media(min-width:980px){.rail{position:sticky;top:96px}}

.steps{display:flex;gap:6px;overflow:auto;padding-bottom:2px;scrollbar-width:none}
.steps::-webkit-scrollbar{display:none}
@media(min-width:980px){.steps{display:grid;gap:0;overflow:visible}}

.stepitem{display:flex;align-items:center;gap:10px;padding:7px 14px 7px 0;border:0;background:transparent;text-align:left;cursor:default;white-space:nowrap;position:relative}
@media(min-width:980px){.stepitem{padding:9px 0;margin-left:0}}
.stepitem.is-done{cursor:pointer}

/* The node. Round, so an outer bloom is safe here — nothing is clipping it. */
.step-n{font-family:var(--f-mono);font-size:10px;letter-spacing:0;color:var(--ash-dim);
  width:26px;height:26px;flex:none;display:inline-flex;align-items:center;justify-content:center;
  border:1px solid var(--line);border-radius:50%;background:var(--ink);
  position:relative;z-index:1;
  transition:border-color .2s ease,color .2s ease,box-shadow .25s ease}
.stepitem.is-active .step-n{color:var(--live);border-color:var(--signal);box-shadow:var(--bloom)}
.stepitem.is-done .step-n{color:var(--ok);border-color:var(--ok-line)}

/* The line, drawn only where the rail stacks vertically. Across a scrolling
   phone strip the items are unequal widths and a connector would land crooked,
   so there the nodes carry the flow on their own. */
@media(min-width:980px){
  .stepitem::before{content:"";position:absolute;left:13px;top:0;bottom:0;width:1px;background:var(--line);z-index:0}
  .stepitem:first-child::before{top:50%}
  .stepitem:last-child::before{bottom:50%}
  .stepitem.is-done::before{background:var(--signal)}
  .stepitem.is-active::before{background:linear-gradient(180deg,var(--signal) 50%,var(--line) 50%)}
}
.step-l{font-family:var(--f-display);font-weight:400;font-size:14.5px;letter-spacing:.005em;color:var(--ash);line-height:1.15}
.stepitem.is-active .step-l,.stepitem.is-done .step-l{color:var(--bone)}

/* ---------- scoreboard summary ---------- */
.board{border:0;border-top:1px solid var(--gold-soft);background:var(--panel);border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow)}
.board-top{padding:13px 14px;border-bottom:1px solid var(--line-soft);background:transparent;display:flex;justify-content:space-between;align-items:center}
.board-body{padding:14px}
.board-empty{color:var(--ash-dim);font-size:13.5px;line-height:1.6;margin:0}
.kvlist{margin:0}
.kvlist.tight{margin:10px 0 0}
.kv{display:flex;justify-content:space-between;gap:14px;padding:7px 0;border-bottom:1px dashed var(--line-soft)}
.kv:last-child{border-bottom:0}
.kv dt{color:var(--ash);font-size:12.5px}
.kv dd{margin:0;font-family:var(--f-mono);font-size:12.5px;text-align:right}
/* one block per court in the selection summary and on the ticket */
.pickcourt,.ticketcourt{margin-top:12px;padding-top:10px;border-top:1px solid var(--line-soft)}
.pickcourt-h,.ticketcourt-h{display:flex;align-items:baseline;gap:8px}
.pickcourt-code{font-family:var(--f-display);font-size:19px;line-height:1;color:var(--live);flex:none}
.pickcourt-name{font-size:12px;color:var(--ash);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pickcourt-sub{font-family:var(--f-mono);font-size:12px;color:var(--bone);flex:none}
.dropbtn{flex:none;width:20px;height:20px;line-height:1;font-size:15px;padding:0;cursor:pointer;
  background:transparent;border:1px solid var(--line);border-radius:2px;color:var(--ash-dim)}
.dropbtn:hover{border-color:var(--signal);color:var(--signal-hot)}

.slotchips{display:flex;flex-wrap:wrap;gap:5px;margin:10px 0 4px}
.slotchip{font-family:var(--f-mono);font-size:11px;padding:3px 7px;border-radius:2px;background:var(--oxblood);border:1px solid var(--tint-line);color:var(--tint-text)}
.board-total{margin-top:12px;padding-top:12px;border-top:1px solid var(--line);display:flex;align-items:baseline;justify-content:space-between}
.total-num{font-family:var(--f-display);font-weight:700;font-size:32px;letter-spacing:.01em;line-height:1;color:var(--bone)}
.total-num span{font-size:17px;color:var(--ash);margin-right:3px}
.total-num.small{font-size:24px;margin-top:2px}

/* hold countdown */
.holdbar{display:flex;align-items:center;justify-content:space-between;gap:12px;border:0;border-left:2px solid var(--signal);padding:12px 15px;background:var(--panel);
  clip-path:polygon(0 0,calc(100% - var(--chamfer)) 0,100% var(--chamfer),100% 100%,0 100%);
  box-shadow:var(--bloom-in)}
.holdclock{font-family:var(--f-mono);font-size:17px;letter-spacing:.06em}
/* Running out is the one state worth animating: it is the only thing on the
   page that changes without the guest doing anything. */
@keyframes livepulse{0%,100%{opacity:1}50%{opacity:.5}}
.holdclock.is-low{color:var(--signal-hot);animation:livepulse 1.6s ease-in-out infinite}

/* ---------- stage ---------- */
.stage{min-width:0}
.stage-head{margin-bottom:20px}
h1{font-family:var(--f-display);font-weight:300;font-size:clamp(32px,6.4vw,52px);letter-spacing:-.018em;line-height:1.04;margin:8px 0 0}
h1 .accent{color:var(--signal)}
.lede{color:var(--ash);margin:10px 0 0;max-width:56ch;font-size:14.5px}
.banner{border-left:2px solid var(--signal);background:var(--banner-bg);color:var(--banner-text);padding:11px 14px;border-radius:0 var(--r) var(--r) 0;margin:0 0 18px;font-size:13.5px}

.panel{border:0;border-top:1px solid var(--line);background:var(--pitch);border-radius:var(--r);padding:22px 20px;margin-bottom:18px;box-shadow:var(--shadow)}
.panel-h{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:14px}
h2{font-family:var(--f-display);font-weight:400;font-size:18.5px;letter-spacing:-.005em;margin:0}

/* ---------- month calendar ---------- */
.calnav{display:flex;align-items:center;gap:10px}
.calnav-title{font-family:var(--f-display);font-weight:400;font-size:15.5px;letter-spacing:.01em;min-width:8.5em;text-align:center}
.navbtn{width:28px;height:28px;line-height:1;font-size:17px;background:transparent;border:1px solid var(--line);border-radius:var(--r);cursor:pointer;color:var(--ash)}
.navbtn:hover:not(:disabled){border-color:var(--signal);color:var(--bone)}
.navbtn:disabled{opacity:.3;cursor:not-allowed}

.cal{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:4px}
.cal-dow{font-family:var(--f-mono);font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ash-dim);text-align:center;padding-bottom:4px}
.cal-blank{min-height:52px}
.cal-day{
  min-height:52px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;
  padding:6px 2px;cursor:pointer;background:var(--panel);border:1px solid var(--line-soft);
  border-radius:var(--r);transition:border-color .15s ease,background .15s ease;position:relative;
}
.cal-day:hover:not(:disabled){border-color:var(--ash-dim)}
.cal-day:disabled{cursor:not-allowed;background:transparent;border-color:transparent;opacity:.35}
.cal-num{font-family:var(--f-display);font-size:19px;line-height:1}
.cal-note{font-family:var(--f-mono);font-size:8.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ash-dim)}
.cal-note.dim{color:var(--fainter)}
/* Seven columns on a 375px phone leaves each day cell about 35px of usable
   width, while "48 free" set at 8.5px with .06em tracking wants 39px — so the
   note wrapped and pushed every cell in the month taller. Tightening only the
   note keeps the wording intact on one line; nothing here shortens the text. */
/* Between 361 and 430px a day cell has roughly 35px of usable width and the
   note needs 34px at this size, so holding it on one line keeps every cell at
   its natural 52px instead of letting one wrapped label make the whole month
   taller. */
@media(min-width:361px) and (max-width:430px){
  .cal-note{font-size:8px;letter-spacing:.01em;white-space:nowrap}
}
/* Below 361px it genuinely does not fit: seven columns leave about 27px inside
   each cell, and "48 free" would have to drop to ~6px to hold one line. So here
   it is allowed to wrap — taller cells are the honest trade against either an
   unreadable label or a page that scrolls sideways. */
@media(max-width:360px){
  .cal-note{font-size:7.5px;letter-spacing:0}
}
.cal-day.is-weekend .cal-num{color:var(--callout-text)}
.cal-day.is-today{border-color:var(--ash-dim)}
.cal-day.is-today::after{content:"";position:absolute;top:4px;right:5px;width:4px;height:4px;border-radius:50%;background:var(--gold)}

/* availability tone: a glance tells you which days are worth opening */
.cal-day.tone-open .cal-note{color:var(--ok)}
.cal-day.tone-busy{border-color:var(--busy-line)}
.cal-day.tone-busy .cal-note{color:var(--warn)}
.cal-day.tone-full{background:repeating-linear-gradient(135deg,var(--stripe-a) 0 5px,var(--stripe-b) 5px 10px)}
.cal-day.tone-full .cal-num{color:var(--ash-dim)}
.cal-day.tone-full .cal-note{color:var(--ash-dim)}

.cal-day.is-on{background:var(--oxblood);border-color:var(--signal)}
.cal-day.is-on .cal-num{color:var(--tint-text)}
.cal-day.is-on .cal-note{color:var(--tint-text-soft)}
.cal-day.is-on::after{background:var(--signal-hot)}

.callegend{display:flex;flex-wrap:wrap;gap:14px;margin-top:14px;padding-top:12px;border-top:1px solid var(--line-soft);
  font-family:var(--f-mono);font-size:9.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--ash-dim);align-items:center}
.callegend .push{margin-left:auto}
.dotkey{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px;vertical-align:middle}
.dotkey.open{background:var(--ok)}
.dotkey.busy{background:var(--warn)}
.dotkey.full{background:var(--faint)}

/* ---------- SIGNATURE: the slot board — hours down, courts across ---------- */
.sheet{border:0;border-top:1px solid var(--line);border-radius:var(--r);background:var(--pitch);overflow:hidden;margin-bottom:16px;box-shadow:var(--shadow)}
.sheet-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;padding:16px;border-bottom:1px solid var(--line-soft);
  background:transparent;flex-wrap:wrap}
.sheet-head h2{margin-bottom:4px}
.sheet-legend{display:flex;flex-wrap:wrap;gap:16px;padding:10px 16px;border-bottom:1px solid var(--line-soft);
  font-family:var(--f-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash-dim)}
.legend-key{display:inline-flex;align-items:center;gap:7px}
.legend-key.push{margin-left:auto}
.swatch{width:12px;height:12px;border-radius:2px;border:1px solid var(--line);display:inline-block}
.swatch.open{background:var(--panel-2)}
.swatch.on{background:var(--signal);border-color:var(--signal)}
.swatch.taken{background:repeating-linear-gradient(135deg,var(--stripe-a) 0 4px,var(--stripe-b) 4px 8px)}
.swatch.blocked{background:var(--oxblood);border-color:var(--tint-line)}

.sheet-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.sheetgrid{border-collapse:separate;border-spacing:1px;width:100%;background:var(--chalk);min-width:340px}
.sheetgrid thead th{position:sticky;top:0;z-index:2;background:var(--panel);padding:9px 8px;text-align:center;vertical-align:bottom}
.sheetgrid th.c-time{position:sticky;left:0;z-index:3;background:var(--panel);width:88px;min-width:88px;text-align:left;padding:9px 10px}
.sheetgrid tbody th.c-time{z-index:1;background:var(--panel);vertical-align:middle}
.th-code{font-family:var(--f-display);font-size:20px;line-height:1;display:block;color:var(--bone)}
.th-name{font-family:var(--f-body);font-size:11px;color:var(--ash);display:block;margin-top:3px;white-space:nowrap}
.th-meta{font-family:var(--f-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash-dim);display:block;margin-top:4px}
.sheetgrid th.c-court.is-on{box-shadow:inset 0 -2px 0 var(--signal)}
.sheetgrid th.c-court.is-on .th-code{color:var(--signal)}

.r-time{font-family:var(--f-mono);font-size:12.5px;display:block;white-space:nowrap}
.r-tier{font-family:var(--f-mono);font-size:8.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--ash-dim);display:block;margin-top:2px}
.sheetgrid tr.is-peakrow th.c-time{box-shadow:inset 2px 0 0 var(--oxblood)}
.sheetgrid tr.is-peakrow .r-tier{color:var(--peak-text)}

.c-cell{padding:0}
.cell{width:100%;min-width:74px;border:0;padding:11px 6px;cursor:pointer;background:var(--panel-2);
  display:flex;flex-direction:column;align-items:center;gap:3px;transition:background .14s ease,color .14s ease}
.cell:hover:not(:disabled){background:var(--panel-3)}
.cell:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--signal-hot)}
.cell.is-on:focus-visible{box-shadow:inset 0 0 0 2px var(--on-signal),var(--bloom-in)}
.cell-main{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--ash)}
.cell-sub{font-family:var(--f-mono);font-size:12px;color:var(--bone)}
.cell.st-taken,.cell.st-past{cursor:not-allowed;background:repeating-linear-gradient(135deg,var(--stripe-a) 0 5px,var(--stripe-b) 5px 10px)}
.cell.st-taken .cell-main,.cell.st-past .cell-main{color:var(--faint)}
/* This court simply isn't open then — flatter than "taken", it was never offered. */
.cell.st-closed{cursor:not-allowed;background:var(--closed-bg)}
.cell.st-closed .cell-main{color:var(--fainter)}
.cell.st-blocked{cursor:not-allowed;background:linear-gradient(180deg,var(--blocked-a),var(--blocked-b))}
.cell.st-blocked .cell-main{color:var(--blocked-text)}
.cell.is-on{background:var(--signal);
  clip-path:polygon(var(--chamfer) 0,100% 0,100% calc(100% - var(--chamfer)),calc(100% - var(--chamfer)) 100%,0 100%,0 var(--chamfer));
  box-shadow:var(--bloom-in)}
/* The label stays quieter than the price through size and weight rather than
   through transparency, which is what used to cost it its contrast. */
.cell.is-on .cell-main{color:var(--on-signal)}
.cell.is-on .cell-sub{color:var(--on-signal);font-weight:600}

.sheet-foot{margin:0;padding:12px 16px;border-top:1px solid var(--line-soft);color:var(--ash-dim);font-size:12.5px;line-height:1.6}

/* ---------- forms ----------
   Each .field is a grid item, so it stretches to the tallest field in its row.
   Without align-content:start those auto rows stretch too, and a field with no
   hint hands the spare height to its input — leaving neighbouring inputs
   different heights and sitting at different offsets. Pinning the rows to the
   top keeps every label, control, and hint on the same baseline across columns. */
.fields{display:grid;gap:16px 20px;align-items:start}
@media(min-width:620px){.fields{grid-template-columns:1fr 1fr}}
.field{display:grid;gap:6px;min-width:0;align-content:start}
.field.wide{grid-column:1/-1}
.field.narrow{max-width:340px}

label{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ash);line-height:1.4}

/* One height for every single-line control, so a select never sits a few
   pixels off an input beside it. */
.input,select,textarea{
  width:100%;background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
  color:var(--bone);font-size:14.5px;line-height:1.4;padding:11px 12px;
}
.input,select{height:44px}
select{appearance:none;-webkit-appearance:none;padding-right:28px;cursor:pointer;
  background-image:linear-gradient(45deg,transparent 50%,var(--ash) 50%),
                   linear-gradient(135deg,var(--ash) 50%,transparent 50%);
  background-position:calc(100% - 14px) 19px,calc(100% - 9px) 19px;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat}
select:hover{background-color:var(--panel-2)}
.input::placeholder,textarea::placeholder{color:var(--faint)}
.input:focus,select:focus,textarea:focus{border-color:var(--signal);outline:none}
textarea{resize:vertical;min-height:88px}

/* Help and error text share one slot, so swapping between them never nudges
   the control above it. */
.err,.hint{font-size:12px;line-height:1.45;margin:0;min-height:1.45em}
.err{color:var(--signal-hot);font-family:var(--f-mono)}
.hint{color:var(--ash-dim)}
.hint.spaced,.hint.spaced-top{min-height:0}
.input.has-err,select.has-err,textarea.has-err{border-color:var(--signal)}
.hint.spaced{margin:4px 0 16px}
.checkline{display:flex;gap:10px;align-items:flex-start;cursor:pointer;text-transform:none;letter-spacing:0;font-family:var(--f-body);font-size:13.5px;color:var(--bone);margin-top:14px}
.checkline input{width:auto;margin-top:3px}

/* what this device booked */
.saved{display:grid;gap:8px}
.savedrow{display:flex;align-items:center;justify-content:space-between;gap:14px;width:100%;
  text-align:left;cursor:pointer;padding:12px 14px;background:var(--panel);
  border:1px solid var(--line-soft);border-radius:var(--r);box-shadow:var(--shadow-sm);
  transition:border-color .15s ease}
.savedrow:hover{border-color:var(--signal)}
.saved-when{display:grid;gap:3px;min-width:0}
.saved-date{font-size:14px;color:var(--bone)}
.saved-meta{font-size:12px;color:var(--ash-dim)}
.saved-ref{color:var(--signal);white-space:nowrap}

/* payment */
.pays{display:grid;gap:10px}
.pay{display:flex;gap:13px;align-items:flex-start;text-align:left;cursor:pointer;padding:14px;background:var(--panel);border:1px solid var(--line-soft);border-radius:var(--r);box-shadow:var(--shadow-sm)}
.pay:hover{border-color:var(--ash-dim)}
.pay.is-on{border-color:var(--signal);background:linear-gradient(180deg,var(--tint),var(--panel))}
.radio{width:15px;height:15px;flex:none;margin-top:3px;border-radius:50%;border:1.5px solid var(--ash-dim);position:relative}
/**
 * Keyed on any selected row that directly contains a radio, rather than on
 * .pay specifically. Written for the payment-method group, but a second group
 * with identical markup once missed out and its dot never filled — the control
 * worked and looked broken, which is worse than not working. Matching on
 * ".is-on > .radio" means the next such group cannot repeat it.
 */
.is-on > .radio{border-color:var(--signal)}
.is-on > .radio::after{content:"";position:absolute;inset:3px;border-radius:50%;background:var(--signal)}
.pay-name{font-family:var(--f-display);font-weight:400;font-size:16px;letter-spacing:.005em;line-height:1.2;display:block}
.pay-desc{font-size:12.5px;color:var(--ash);margin-top:4px;display:block}

.payout{margin-top:16px;border:1px solid var(--line);border-radius:var(--r);padding:16px;background:var(--panel);box-shadow:var(--shadow-sm)}
.payout-grid{display:grid;gap:16px}
@media(min-width:560px){.payout-grid{grid-template-columns:auto 1fr;align-items:start}}
.qr{width:132px;height:132px;background:var(--qr-bg);padding:8px;border-radius:2px;display:block}
.copyline{display:flex;align-items:center;gap:9px;flex-wrap:wrap;margin-top:3px}
.copyline.spaced-top{margin-top:12px}
.copyval{font-family:var(--f-mono);font-size:16px;letter-spacing:.03em}
.minibtn{font-family:var(--f-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;background:transparent;border:1px solid var(--line);border-radius:2px;padding:4px 8px;cursor:pointer;color:var(--ash)}
.minibtn:hover{border-color:var(--signal);color:var(--bone)}

.callout{border-left:2px solid var(--signal);background:var(--callout-bg);padding:12px 14px;border-radius:0 var(--r) var(--r) 0;font-size:13px;color:var(--callout-text);margin:0;line-height:1.55}
.callout.spaced-top{margin-top:16px}
.callout strong{color:var(--bone)}
/* The forfeit rule is the one thing on the page a guest must not skim past. */
.callout.warn{border-left-color:var(--warn);background:var(--warn-soft-bg);color:var(--warn-soft-text)}
.callout.warn strong{color:var(--warn-strong)}
.callout.ok{border-left-color:var(--ok);background:var(--ok-soft-bg);color:var(--ok-soft-text)}

/* The one thing on this page a guest must not skim past: they are about to
   send money for courts they no longer hold. */
.lostwarn{border:1px solid var(--signal);border-left-width:3px;background:var(--banner-bg);
  border-radius:var(--r);padding:16px 18px;margin-bottom:16px;box-shadow:var(--shadow)}
.lostwarn-head{margin:0 0 6px;font-family:var(--f-display);font-weight:400;font-size:23px;letter-spacing:-.01em;
  color:var(--signal);line-height:1.1}
.lostwarn-body{margin:0 0 14px;font-size:13.5px;line-height:1.6;color:var(--banner-text)}

/* Left from the pay-now chooser, and still carrying the add-on rows: a name on
   the left, a price on the right, a note underneath. */
.plan-body{flex:1;min-width:0}
.plan-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
.plan-amt{font-family:var(--f-display);font-size:20px;line-height:1;white-space:nowrap}
.kv dd.owing{color:var(--warn-strong)}

/* actions */
.actions{display:flex;gap:10px;align-items:center;margin-top:20px}
.btn{font-family:var(--f-display);font-weight:500;font-size:15.5px;letter-spacing:.005em;padding:14px 28px;border:1px solid var(--signal);border-radius:var(--r);cursor:pointer;background:var(--signal);color:var(--on-signal);transition:background .15s ease,border-color .15s ease,box-shadow .2s ease}
.btn:hover:not(:disabled){background:var(--signal-hot);border-color:var(--signal-hot)}
.btn:disabled{background:transparent;border-color:var(--line);color:var(--ash-dim);cursor:not-allowed}
.btn.ghost{background:transparent;border-color:var(--line);color:var(--ash)}
.btn.ghost:hover{border-color:var(--ash-dim);color:var(--bone);background:transparent}

/* confirmation */
.ticket{border:1px solid var(--gold-soft);border-radius:var(--r);background:linear-gradient(180deg,var(--ticket-grad),var(--pitch));overflow:hidden;box-shadow:var(--shadow)}
.ticket-top{padding:22px;border-bottom:1px dashed var(--ticket-dash)}
.ticket-ref{font-family:var(--f-display);font-size:clamp(34px,9vw,54px);letter-spacing:.06em;line-height:1;color:var(--signal);margin:6px 0 0}
.ticket-body{padding:22px}
.stamp{display:inline-flex;align-items:center;gap:8px;font-family:var(--f-mono);font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--tint-text-soft);border:1px solid var(--tint-line);background:var(--oxblood);padding:5px 10px;border-radius:2px}
.dot{width:6px;height:6px;border-radius:50%;background:var(--signal-hot);display:inline-block}
.stamp.is-dead{color:var(--ash);border-color:var(--line);background:var(--panel)}
.stamp.is-dead .dot{background:var(--ash-dim)}

/* mobile sticky bar */
.mobar{position:fixed;left:0;right:0;bottom:0;z-index:40;border-top:1px solid var(--line);background:var(--scrim);backdrop-filter:blur(8px);padding:11px 16px calc(11px + env(safe-area-inset-bottom));display:flex;align-items:center;gap:14px;box-shadow:var(--shadow-up)}
.mobar .mtot{flex:1;min-width:0}
.mobar .btn{padding:12px 20px;font-size:15px}
@media(min-width:980px){.mobar{display:none}}
@media(max-width:979px){
  .shell{padding-bottom:96px}
  .actions.desk-only{display:none}
}

.toast{position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:60;background:var(--toast-bg);color:var(--toast-text);padding:9px 16px;border-radius:2px;box-shadow:var(--shadow);font-family:var(--f-mono);font-size:11.5px;letter-spacing:.1em;text-transform:uppercase}
@media(min-width:980px){.toast{bottom:32px}}

.fade-enter-active,.fade-leave-active{transition:opacity .18s ease}
.fade-enter-from,.fade-leave-to{opacity:0}
@media(prefers-reduced-motion:reduce){*{transition-duration:.01ms !important;animation-duration:.01ms !important}}

/* ---------- extras: add-ons, waiver, receipt ---------- */

/* Sits inside a selected add-on row, so clicks on it must not toggle the row —
   the @click.stop in the markup is what keeps typing a quantity from
   deselecting the thing you are choosing a quantity for. */
.addon-unit{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ash-dim)}
.addon-qty{display:flex;align-items:center;gap:10px;margin-top:10px;padding-top:10px;border-top:1px solid var(--line-soft)}
.addon-qty label{margin:0;white-space:nowrap}
.addon-qty .input.narrow{width:72px;height:36px;padding:6px 8px;text-align:center}
.addon-line{font-family:var(--f-mono);font-size:13px;color:var(--live);margin-left:auto}

/* A disabled question should read as unavailable, not broken. */
.checkline.is-off{opacity:.5}
.checkline.is-off input{cursor:not-allowed}

/**
 * The waiver is stored and rendered as plain text — never HTML — so a staff
 * account cannot inject script into every guest's checkout. `pre-wrap` is what
 * preserves the paragraphs the desk typed without interpreting anything.
 */
.waiver-body{
  white-space:pre-wrap;font-size:13.5px;line-height:1.6;color:var(--ash);
  background:var(--panel-2);border-radius:var(--r);padding:14px 16px;
  max-height:120px;overflow:hidden;position:relative;transition:max-height .2s ease;
}
.waiver-body:not(.is-open)::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:44px;
  background:linear-gradient(180deg,transparent,var(--panel-2));
}
.waiver-body.is-open{max-height:none;overflow:visible}
.waiver-body.is-open::after{display:none}

/* The file input itself is hidden inside the label, so the whole box is the
   target — a 44px-tall tap area rather than the browser's small button. */
.proofdrop{
  display:block;cursor:pointer;text-align:center;
  border:1px dashed var(--line);border-radius:var(--r);
  padding:18px 16px;color:var(--ash);font-family:var(--f-body);
  font-size:13.5px;letter-spacing:0;text-transform:none;line-height:1.5;
  transition:border-color .15s ease,color .15s ease;
}
.proofdrop:hover{border-color:var(--signal);color:var(--bone)}
.proofdrop input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
.proofdrop:focus-within{outline:2px solid var(--signal-hot);outline-offset:2px}
.proofpicked{display:block;color:var(--bone)}
.proofpicked strong{display:block;font-weight:600;margin-bottom:2px;word-break:break-all}

/* The reason the desk gave, set apart from the surrounding copy — it is the one
   thing on the screen the guest actually needs to act on. */
.resend-reason{
  display:block;margin:8px 0;padding-left:10px;
  border-left:2px solid var(--warn-line);color:var(--warn-strong);
}

/* The crest links home now that the landing page is a separate document. It
   still has to read as a masthead rather than as a link, so the underline and
   the inherited link colour both go. */
a.brand{text-decoration:none;color:inherit}
a.brand:hover .brand-name{color:var(--live)}
a.brand:focus-visible{outline:2px solid var(--signal-hot);outline-offset:3px;border-radius:2px}

/* ---------- the ticket's breakdown ---------- */

/* Set off from the booking details above it: this answers a different question
   — not "when am I playing" but "what am I paying for". */
.tkbreak{margin-top:16px;padding-top:14px;border-top:1px solid var(--line-soft)}

/* A second line under a label, for the things a figure needs explaining by:
   how many hours the court time covers, how many towels at what each. */
.kv dt .sub{display:block;font-size:11px;color:var(--ash-dim);margin-top:2px}
.kv dd.muted{color:var(--ash-dim);font-family:var(--f-body);font-size:11.5px;font-style:italic}

.tktotal{display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  margin-top:11px;padding-top:11px;border-top:1px solid var(--line)}
.tktotal span{font-family:var(--f-mono);font-size:10px;letter-spacing:.14em;
  text-transform:uppercase;color:var(--ash-dim)}
.tktotal strong{font-family:var(--f-display);font-size:22px;line-height:1;color:var(--bone)}

/* What is settled and what is still owed, kept apart from the price of things
   so the two are never read as one list. */
.tkpaid{margin-top:12px;padding-top:11px;border-top:1px dashed var(--ticket-dash)}
