/* ==========================================================================
   golf.css — the golfer's stylesheet.

   Loaded ONLY on the golfer surfaces (/, /texts, /watches, /play/*). The
   operator console at /rig keeps teescout.css and base.html and is not touched
   by anything in here. Both files can load on the same page without a fight:
   every class below is namespaced `g-`, and the only bare-element rules are
   inside `.g-app` and wrapped in :where() so they carry no specificity.

   THE MAP, in order:

     §0   tokens — colour, type, space, geometry; the dark override; the one
          webfont; tabular numerals; reduced motion
     §1   shell — .g-app .g-shell .g-page .g-skip, the focus ring
     §2   type and utilities
     §3   header — .g-header .g-brand .g-nav .g-account
     §4   buttons — .g-btn
     §5   forms — .g-field .g-input .g-select .g-check .g-choice .g-checklist
     §6   the sheet page — .g-sheetpage .g-rail .g-sheet (phone / 1024 / 1280)
     §7   the query line — .g-query .g-only
     §8   the mechanism line — .g-mech
     §9   the two taps — .g-days .g-bands .g-carts .g-picker .g-trunc
     §10  the day header — .g-day
     §11  the course run header — .g-run
     §12  the tee row — .g-teelist .g-tee .g-more
     §13  the sticky action — .g-cta
     §14  every state — .g-empty .g-error .g-capped .g-note .g-loading
     §15  cards, watch summaries — /texts, /watch_edit, /watches
     §16  footer — .g-foot
     §17  last-resort guards
     §18  motion — the cancellation signature, and nothing else

   THREE RULES THE WHOLE FILE EXISTS TO ENFORCE:

   1. STRUCTURE IS NAVY. #0B1A2E is the brand ink and every neutral derives
      from it. The outgoing --ink #14231A was a green-black, which made body
      copy itself a fourth green.
   2. GREEN IS A SIGNAL. There are exactly five green declarations in this
      file and each one refers to a tee time a golfer can book:
         .g-tee--released .g-tee__mark      background --live   (the mark)
         .g-tee--released .g-tee__cancelled color      --fairway (the word)
         .g-tee--released                   --g-sweep  --live   (§18, the sweep)
         .g-days__dot--on                   background --live   (a day with one)
         .g-day__cancelled                  color      --fairway (the day count)
      Nothing else. Not a button, not a link, not the focus ring, not the
      wordmark, not a checkbox, not a divider. `uv run python ops/contrast.py`
      prints the ledger; if it lists a fifth, something drifted.
   3. NOTHING A GOLFER READS TO DECIDE IS BELOW 15px, in either scheme, at any
      viewport including 320. Density comes from deleting strings that repeat,
      never from shrinking type.

   Mobile first, always: the base rules ARE the 320px phone and every media
   query is min-width. Nothing here may scroll sideways at 320px.
   ========================================================================== */

/* ==========================================================================
   §0 — TOKENS

   Every ratio in a comment below was computed on 2026-08-06 with the WCAG 2.1
   relative-luminance formula and re-verified against the shipped hexes by
   `uv run python ops/contrast.py`, which parses this block out of this file.
   Re-run it before changing any hex. If a comment and that script disagree,
   the comment is wrong.
   ========================================================================== */

:root {
  /* ---- surfaces -------------------------------------------------------- */
  --paper:     #FFFFFF;  /* the page */
  --panel:     #F4F6FA;  /* 1.08:1 off paper. Navy-tinted, replaces the green
                            #F6F8F4. Cards on /texts and /watch_edit, the query
                            line, the desktop rail. NEVER a tee row — --live
                            measures 2.92:1 on it and fails the 3:1 non-text
                            floor, so a tinted row silently breaks the one mark
                            the product depends on. */

  /* ---- ink ------------------------------------------------------------- */
  --ink:       #0B1A2E;  /* 17.48:1 on paper, 16.16:1 on panel. Brand navy.
                            Times, prices, day headers, primary BUTTON FILL,
                            focus ring. Beats the outgoing #14231A (16.34:1) on
                            the pair that matters most, so this is a measured
                            upgrade, not a repaint. */
  --dim:       #55657B;  /* 5.94:1 on paper, 5.49:1 on panel. Every non-numeric
                            fact on a row: spots, drive time, freshness. Navy-
                            derived; beats the outgoing #5A6B5E (5.68:1). */
  --faint:     #93A0B0;  /* 2.66:1. BANNED FROM TEXT — this is what the old
                            token comment already said and the landing page
                            ignored on five elements. Survives for 1px ticks and
                            empty week-rail cells ONLY. ops/contrast.py fails
                            the build if --faint appears in a `color:`. */

  /* ---- rules ----------------------------------------------------------- */
  --rule:      #D7DDE5;  /* 1.37:1. STRUCTURAL only: day-header underline,
                            column dividers, card borders. A divider is not a
                            UI component and carries no 1.4.11 obligation. It
                            is NOT allowed on the edge of a control — see
                            --control. */
  --control:   #76859B;  /* 3.75:1 on paper, 3.47:1 on panel. THE EDGE OF
                            ANYTHING YOU TYPE INTO, and it is a floor rather
                            than a taste call. WCAG 1.4.11 asks 3:1 of a
                            control's boundary; --rule measured 1.37:1 on paper
                            and 1.26:1 inside the query line, and an input's
                            own fill is --paper on --paper (1:1), so that
                            hairline was the entire affordance. #pick-filter on
                            /watches/{id}/edit renders with no value and no
                            placeholder, i.e. it was a blank box whose only
                            boundary was a 1.37:1 line. Hover (--dim, 5.94) and
                            focus (--ink, 17.48) still step up from here. */
  --rule-hair: #EAEEF4;  /* 1.16:1. The per-row hairline, and only that. At a
                            60px pitch there are ~14 rules on one phone screen;
                            --rule at that frequency reads as a grey field and
                            competes with the times for ink. Same hue at ~40% of
                            the weight, still resolvable at 1px. Acceptance
                            test: if your eye lands on the rules before the
                            times, go lighter. */

  /* ---- the two greens, each proven by ratio ---------------------------- */
  --live:      #23A566;  /* 3.16:1 on paper, 5.53:1 on the dark surface. ONE HEX
                            IN BOTH SCHEMES, so "green means one thing" is true
                            at the value level. Clears the 3:1 graphical floor
                            and FAILS 4.5:1 — which is exactly why it is a mark
                            and never a letter. Spent on: the release mark in
                            the row gutter, and lit half-hour cells in the week
                            rail. Nothing else. */
  --fairway:   #1B6B45;  /* 6.49:1 on paper, 6.00:1 on panel. The word
                            "cancelled" in a tee row and the count in a day
                            header. That is the complete list. Removed from
                            buttons, focus rings, links, accent-color, the logo
                            and the freshness dot. The measured proof it cannot
                            stay a fill: #1B6B45 on the dark surface is 2.69:1,
                            so a primary button in this colour could not survive
                            dark mode. */

  /* ---- the one warm colour --------------------------------------------- */
  --clay:      #B4552E;  /* 4.91:1 on paper, 4.54:1 on panel. Stale-course dot
                            on a run header, error text, destructive button
                            border, the word "gone". Clears 4.5:1 in both
                            schemes and on both surfaces. */

  /* ---- type -------------------------------------------------------------
     Six sizes, down from the 19 literals the previous file carried, eight of
     them crammed between 13 and 17. Body copy and ALL numerals stay on the
     system stack: it is more comfortable than a display face at 15px in bright
     sun, which is the reading condition that matters here. Space Grotesk
     SemiBold is permitted on headings and the wordmark ONLY, never below 17px,
     and never on a numeral a golfer reads.                                  */
  --g-ui:      ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
               "Helvetica Neue", Arial, sans-serif;
  --g-display: "Space Grotesk", var(--g-ui);

  --t-h1:      34px;  /* 600, -0.022em, display. Page h1. Once per page. */
  --t-lg:      20px;  /* 600. TWO THINGS ONLY: the tee time (system, tabular)
                         and the day header (display, uppercase). The day header
                         appears once per ~40 rows, so the tee time is the
                         largest thing in its own neighbourhood. The tee time
                         went UP from 19px. */
  --t-lead:    17px;  /* 600, display. Course run header, card titles. The floor
                         for the display face. */
  --t-body:    16px;  /* 400. Price, body copy, buttons, inputs. 16 on inputs is
                         not a taste call: below it, iOS zooms the page on
                         focus. */
  --t-fact:    15px;  /* 400. THE FLOOR. Spots, drive time, the word "cancelled"
                         (600), the mechanism line, every error and every empty
                         state. Retires 14.5px, which was the most-used size in
                         the outgoing file and set .g-tee__meta — the exact line
                         the whole density decision hangs on. */
  /* --t-axis (13px) IS RETIRED AND MUST NOT COME BACK. It survived one pass on
     .g-bands__hours with the argument that "6-11a" is only a label restating the
     chip's own word, so a golfer who cannot read it loses nothing. That argument
     is wrong twice. It is not a restatement — "Morning" does not tell you the
     band starts at 5:00 because the search window starts at 5:00, and the hours
     are the only place that edge is printed. And the floor in the locked
     decisions is "nothing below 15px", full stop; a floor that accepts a good
     enough reason is not a floor. Every band label now sets --t-fact and the
     chip grid was re-measured around it (§9). */

  --lh-tight:  1.2;   /* times and headings */
  --lh-body:   1.5;   /* everything else */

  /* ---- space ------------------------------------------------------------
     Eight steps, all multiples of 4, zero fractional values. Kills 22 and 30
     (the only non-multiples in the old scale and the origin of the half-pixel
     drift) and the 48 off-scale literals in 15 sizes the old file carried.
     If a value is not here or in the geometry block below, it does not exist. */
  --g-1: 4px;
  --g-2: 8px;
  --g-3: 12px;
  --g-4: 16px;
  --g-5: 24px;
  --g-6: 32px;
  --g-7: 48px;
  --g-8: 64px;

  /* ---- radii ------------------------------------------------------------
     Radius appears only on something you press or type into.                */
  --g-r-press: 8px;   /* buttons, inputs, the query line, the sticky CTA.
                         Replaces 11px: 11 was tuned for a card UI, and with the
                         cards deleted from the sheet a 44px control reads
                         tighter and less consumer at 8. */
  --g-r-card:  14px;  /* scoped to .g-card on /texts and /watch_edit. Never on
                         the sheet — a sheet is ruled, not carded. */
  --g-r-mark:  0;     /* the release mark is a pencil stroke, not a pill */

  /* ---- geometry ---------------------------------------------------------
     Derived measurements, not spacing steps. They live here rather than inline
     because each is referenced from more than one rule or overridden at a
     breakpoint, and a sticky offset that disagrees with the header height by
     one pixel is a bug you can see.                                         */
  --g-tap:     44px;   /* a FLOOR, not a step. Nothing tappable is smaller. */
  --g-head-h:  52px;   /* the sticky header. 94px today, of which 36.6px was a
                          freshness strip about pollers and 42px of padding
                          around a 36px button. Both are gone: freshness is a
                          per-course fact and now prints on the run header where
                          the data lives. Also the `top:` for every sticky day
                          header, which is why it is a token. */
  --g-head-pad: var(--g-1);  /* 52 − 44 tap target = 8, so 4 above and below */
  --g-day-h:   36px;   /* the day header. 3px taller than the two lines of
                          12.5px chips it replaces, and it carries the counts */
  --g-run-h:   34px;   /* the course run header */
  --g-row:     60px;   /* tee-row pitch, phone. Today's rows take five-plus
                          distinct values from 70.3px to 265.9px because the
                          facts wrap on 82.9% of them. A tee sheet with no
                          baseline grid is not a sheet. */
  --g-cta-h:   56px;   /* the sticky action */
  /* --g-week-h, --g-week-label and --g-cell went with the week lattice they
     measured. See §9: the lattice was drawn from a MAX_SLOTS-truncated list, it
     was display:none below 360, and its cells were anchor jumps into a sheet
     LONG_SHEET had already truncated. The day rail that replaced it sizes off
     its own content and needs no geometry token; the one measurement that IS
     load-bearing -- seven cells inside 296px at 320 -- is written at the top of
     §9 where the grid is, not here where nothing can see it. */
  --g-box:     20px;   /* a native checkbox, a radio, the filter's own dot */

  --g-pad:     12px;   /* the page gutter: 12 at 320, 16 from 360, 32 from 1024.
                          It is a token because --g-shell is a CONTENT width and
                          the shell has to add the gutters back — see .g-shell.
                          Measured cost of getting that wrong: with max-width
                          set to 1240 flat, the 1240 of grid inside it had only
                          1176 to live in and the page scrolled sideways at
                          exactly 1280. */
  --g-shell:   560px;  /* CONTENT width. Below 1024: one column, capped so a
                          tablet does not stretch a 5-cell row to 900px. 820 at
                          1024, 1240 at 1280 — see §6. The outgoing 46rem
                          (736px) left 418px of dead margin each side of a
                          1512px screen. */
  --g-rail:    320px;  /* the desktop context rail */
  --g-col:     460px;  /* one day column: 460 at 1024–1279, 420 at 1280+ */
  --g-gutter:  40px;   /* rail-to-sheet and column-to-column. Off the spacing
                          scale on purpose: 320 + 40 + 880 = 1240 and
                          420 + 40 + 420 = 880 is load-bearing arithmetic. */

  --g-mark-gutter: 12px;  /* reserved on EVERY row, used on ~1 in 40, so the
                             marks stack into a column the eye can count
                             without reading */
  --g-mark-w:  3px;
  --g-mark-h:  24px;

  /* ---- the tee-row columns ----------------------------------------------
     Numeric columns are FIXED px and right-aligned so the price and the spot
     count cannot move between row 1 and row 596; the flag channel is the only
     flexible track, so it absorbs every string-length difference.

     These are the 320px values, because 320 is the base. Measured against real
     strings in the system stack on 2026-08-06: "12:20p" at 20px/600 is 62.0px
     in SF Pro and 63.4px in Arial Bold, so a 78px time column clears the worst
     case with 8px of padding. "cancelled" at 15px/600 is 64.1px / 68.4px
     against the 82px the flexible track gets at 320 — it fits, which is the
     whole reason the word can be primary and the mark only an index.
     Fixed ink at 320 = 12+78+56+52+16 = 214 of 296.                         */
  --g-c-time:  78px;
  --g-c-free:  56px;
  --g-c-price: 52px;
  --g-c-go:    16px;
}

/* --------------------------------------------------------------------------
   §0.1 — the wider phone. At >=360px the gutters go 12 → 16 and the columns
   take the width back. 390 content = 358: 12+82+62+58+18 = 232 fixed,
   126 flexible.
   -------------------------------------------------------------------------- */
@media (min-width: 360px) {
  :root {
    --g-pad:     16px;
    --g-c-time:  82px;
    --g-c-free:  62px;
    --g-c-price: 58px;
    --g-c-go:    18px;
  }
}

/* --------------------------------------------------------------------------
   §0.2 — DARK, decided by measurement rather than taste.

   Both candidate surfaces were built with the IDENTICAL text hues and measured
   against each other on 2026-08-06:

     text hue           navy #0B1A2E     green-grey #101C16
     #FFFFFF                 17.48            17.50
     ink   #E6EDF6           14.82            14.84
     dim   #9FB2C9            8.06             8.07
     word  #4FCB8B            8.53             8.54
     live  #23A566            5.53             5.54

   Green-grey is ahead on all four pairs by 0.01–0.02, which is rounding, not
   headroom. The stated tiebreak is "if they tie, take navy", and navy is what
   the brand sheet specifies. NAVY. For the record the shipped landing panel
   #0C2318 measures 16.55 / 14.03 / 7.63 / 5.24 on the same four pairs — worse
   than BOTH candidates on all four — and is replaced by this token.

   The <meta name="color-scheme" content="light"> in golf_base.html must come
   out, and .g-app declares `color-scheme: light dark` so native controls,
   form widgets and scrollbars follow the phone.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
  :root {
    --paper:     #0B1A2E;  /* brand navy is the surface */
    --panel:     #12243C;  /* 1.12:1 off paper. A whisper, as intended. */
    --ink:       #E6EDF6;  /* 14.82:1 on paper, 13.26:1 on panel */
    --dim:       #9FB2C9;  /*  8.06:1 on paper, 7.21:1 on panel — MORE headroom
                              than light mode's 5.94, which is right: 6am in bed
                              is the case this serves. */
    --faint:     #5C7085;  /*  3.42:1. Still banned from text. */
    --rule:      #22374F;  /*  1.44:1 — deliberately stronger than light mode's
                              1.37, because a hairline loses more on a dark
                              surface than it does on white. */
    --control:   #6A8098;  /*  4.29:1 on paper, 3.84:1 on panel. The dark half
                              of the 3:1 control floor. Measured higher than
                              light mode's 3.75 for the same reason --rule is:
                              an edge loses more on a dark surface. */
    --rule-hair: #17293E;  /*  1.18:1 */
    --live:      #23A566;  /*  5.53:1 on paper, 4.95:1 on panel. SAME HEX AS
                              LIGHT. Gets stronger for free. */
    --fairway:   #4FCB8B;  /*  8.53:1 on paper, 7.63:1 on panel. The green WORD,
                              lifted so it clears 4.5:1 on navy; #1B6B45
                              measures 2.69:1 there and would be unreadable.
                              This is the only token whose hex differs between
                              schemes, and it differs because a letter has a
                              4.5:1 obligation that a mark does not. */
    --clay:      #E8875C;  /*  6.70:1 on paper, 5.99:1 on panel */
  }
}

/* --------------------------------------------------------------------------
   §0.3 — THE ONE WEBFONT.

   Self-hosted, subset, one weight, one file, 6,340 bytes. Not fetched from
   Google's CDN: the privacy policy tells a golfer these pages make no
   third-party requests, so a CDN link would make that page a lie. The old
   shell asked Google for two families twice per page load, once in <head> and
   again from an @import at the top of this stylesheet.

   SUBSET: printable ASCII U+0020–U+007E plus nine marks headings reach for
   (nbsp, ·, ½, – — ‘ ’ “ ”) = 104 codepoints, 116 glyphs, kern only. All 41
   course names in the corpus today use only A–Z, 1–5, space, parens and a
   hyphen, so ASCII is already generous; the nine extras cost under 400 bytes
   and stop one curly apostrophe in a course name from falling back mid-word.
   Rebuild: teescout/web/static/fonts/build_subset.py, licence in OFL.txt.

   THE SWAP, measured rather than hoped: this face is a display face and sets
   at most four elements per page, so `swap` moves very little. Space Grotesk
   600 against SF Pro 600 at the same size — "SAT 9 AUG" +2.1%, "MONTAUK DOWNS
   STATE PARK" +2.4%, cap height 0.700 vs 0.705 em — i.e. the uppercase strings
   that make up almost every heading on the sheet land within 2.4%. Mixed case
   is the wide end of the range: "TeeScout" is 78.7px in Space Grotesk and
   72.8px in SF Pro at 17px, a 5.9px difference that shifts nothing, because the
   wordmark sits at the left of a flex header whose nav is pushed right with
   margin-left:auto. Every element that takes this face also takes an explicit
   line-height below, so a swap can never move a block VERTICALLY.
   The residual: a 34px h1 can change its wrap point at 320px. The fix is the
   preload in <head>, not a second font file:
     <link rel="preload" as="font" type="font/woff2" crossorigin
           href="/static/fonts/space-grotesk-600-subset.woff2">
   -------------------------------------------------------------------------- */
@font-face {
  font-family: "Space Grotesk";
  src: url("/static/fonts/space-grotesk-600-subset.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;   /* the wordmark is the brand and it should arrive; the
                           face sets only headings, so a swap moves ~4 elements
                           per page and never the sheet */
}

/* Every numeral on every surface. Non-negotiable — the whole sheet is a column
   of numbers, and proportional figures make it un-scannable. */
.g-num,
.g-tee__time, .g-tee__price, .g-tee__free,
.g-day__count, .g-day__cancelled,
.g-days__n, .g-bands__n, .g-bands__hours, .g-courses__n,
.g-stat__n, .g-card__aside, .g-checklist__count {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Motion: this file has exactly two transitions and both are feedback on a
   thing you just touched. Nothing loops, nothing pulses, nothing eases in on
   scroll. All the decorative motion in the product was on the landing page and
   comes out with this pass. */
@media (prefers-reduced-motion: reduce) {
  .g-app *,
  .g-app *::before,
  .g-app *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
   §1 — SHELL
   ========================================================================== */

/* On <body>. Carries the type, the surface and the sideways-scroll guard.
   `clip` rather than `hidden` because `hidden` would make this a scroll
   container and kill every position:sticky in the file. */
.g-app {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--g-ui);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  font-weight: 400;
  overflow-x: clip;
  color-scheme: light dark;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

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

/* Bare-element rules are wrapped in :where() so they carry NO specificity. A
   component class always wins over them — otherwise `.g-app a` would repaint
   the text on every row that happens to be a link, and the whole sheet is
   links. */
:where(.g-app) :where(img, svg, video) { max-width: 100%; height: auto; }

/* The reading column. 12px gutters at 320 (content 296, which is what the row
   arithmetic in §0 is built on), 16 from 360, 32 from 1024. --g-shell is the
   CONTENT width, so the gutters are added back here — box-sizing is border-box
   everywhere inside .g-app, and without the calc the desktop grid is 64px
   wider than the box it has to fit in. */
.g-shell {
  width: 100%;
  max-width: calc(var(--g-shell) + var(--g-pad) * 2);
  margin: 0 auto;
  padding: 0 var(--g-pad);
}

/* <main class="g-page"> */
.g-page { display: block; padding: var(--g-5) 0 var(--g-8); }

/* The sheet opens on a control strip, not on a heading, so it starts at the
   top of the page: 24px of air between a ruled sticky header and a --panel
   strip carries no information and costs 24px of the 568px an iPhone SE has.
   Prose pages keep the air. */
.g-page--flush { padding-top: 0; }

/* A titled run of content. Sections are separated by air, not by boxes. */
.g-section { margin: 0 0 var(--g-7); }
.g-section:last-child { margin-bottom: 0; }
.g-section__head { margin: 0 0 var(--g-4); }

/* The skip link. Off screen until a keyboard finds it, then a real 44px
   control — it rendered at 27.2px and in green before this pass. */
.g-skip { position: absolute; left: -9999px; top: 0; }

.g-skip:focus {
  position: fixed;
  left: var(--g-3);
  top: var(--g-3);
  z-index: 60;
  display: inline-flex;
  align-items: center;
  min-height: var(--g-tap);
  width: auto;
  padding: 0 var(--g-4);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--g-r-press);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
}

.g-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* One visible ring, everywhere, keyboard only. --ink, not --fairway: a focus
   ring is not a bookable tee time. On the page it measures 17.48:1; on an --ink
   button fill the 2px offset puts the ring on the page background, where it
   measures the same. */
.g-app :focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: var(--g-1);
}

/* ==========================================================================
   §2 — TYPE AND UTILITIES
   ========================================================================== */

/* The display face is allowed here and nowhere else. Every one of these sets
   an explicit line-height, so the webfont swap cannot move a block vertically
   — only its width can change, and only by the ~2.4% measured in §0.3. */
:where(.g-app) :where(h1, h2, h3, h4),
.g-h1, .g-h2, .g-h3 {
  font-family: var(--g-display);
  letter-spacing: -0.022em;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}

.g-h1 { font-size: var(--t-h1);   line-height: var(--lh-tight); }
.g-h2 { font-size: var(--t-lg);   line-height: var(--lh-tight); }
.g-h3 { font-size: var(--t-lead); line-height: var(--lh-tight); }

/* The sentence under a heading that says what the page is for. System stack:
   it is body copy, and body copy is not set in a display face. */
.g-lede {
  margin: var(--g-2) 0 0;
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--dim);
  max-width: 34em;
}

:where(.g-app) :where(p) { margin: 0 0 var(--g-3); }
:where(.g-app) :where(p:last-child) { margin-bottom: 0; }

/* Links are --ink and underlined. They were --fairway, which spent the product's
   one signal colour on navigation. Underline rather than colour is also the
   only version of "this is a link" that survives being colour-blind. */
:where(.g-app) :where(a) {
  color: var(--ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
:where(.g-app) :where(a):hover { text-decoration-thickness: 2px; }

/* Long course names break instead of widening the page. */
:where(.g-app) :where(h1, h2, h3, p, li, td, th, label, summary) {
  overflow-wrap: break-word;
}

.g-dim    { color: var(--dim); }
.g-strong { font-weight: 600; color: var(--ink); }
.g-fact   { font-size: var(--t-fact); line-height: var(--lh-body); }

/* Vertical rhythm helper for a run of cards or rows. */
.g-stack        { display: flex; flex-direction: column; gap: var(--g-3); }
.g-stack--tight { gap: var(--g-2); }
.g-stack--loose { gap: var(--g-5); }

/* Horizontal run that always wraps — never a sideways scroller. */
.g-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--g-2) var(--g-3);
  min-width: 0;
}

.g-hr { height: 1px; background: var(--rule); border: 0; margin: var(--g-5) 0; }

/* ==========================================================================
   §3 — HEADER

   52px on a phone, 56 from 1024. It carries the brand, at most three
   destinations and the account menu. NOTHING ELSE. It was 94px — 11.1% of a
   390 viewport and 16.6% of a 320 one — to hold a 36px button and a status line
   about pollers, and that 42px is most of the 94px saved above the fold.
   ========================================================================== */

/* NAVY CHROME, WHITE SHEET, and this is the rule rather than a repaint.
   Header, rails and the day/band selectors are #0B1A2E; the tee sheet itself
   stays --paper because it is read outdoors in bright sun.

   Done by REDECLARING THE TOKENS inside the header rather than by writing
   literals on six selectors. Everything in here — the wordmark, the two nav
   links, aria-current, the account ring, the signed-out primary button —
   already draws itself from --ink / --dim / --panel / --paper, so one scoped
   block turns the whole component over and nothing inside it needs to know.

   THE HEXES ARE THE DARK SCHEME'S, EXACTLY, and that is what keeps them
   proved: §0.2 measured every one of these against #0B1A2E
   (ink 14.82:1, dim 8.06:1, control 4.29:1, clay 6.70:1, fairway 8.53:1) and
   ops/contrast.py re-checks that column on every run. In dark mode the page
   tokens already hold these values, so this block is a no-op there and the
   header is the same object in both schemes — which is the point: chrome that
   changes colour with the OS is not chrome, it is decoration.

   The one thing that inverts is .g-btn--primary, whose fill is --ink and whose
   label is --paper: on a navy bar the signed-out sign-in button becomes a pale
   button with navy letters, at the same 14.82:1 it had the other way round. */
.g-header {
  --paper:     #0B1A2E;
  --panel:     #12243C;
  --ink:       #E6EDF6;
  --dim:       #9FB2C9;
  --faint:     #5C7085;
  --rule:      #22374F;
  --rule-hair: #17293E;
  --control:   #6A8098;
  --clay:      #E8875C;
  /* No --fairway here. Nothing in the header prints the green WORD -- that is
     the sheet's, on a released row and in a day header -- and ops/contrast.py
     reads any mention of the token outside :root as a spend against the green
     budget, which is exactly the check that should fire if one ever appears. */

  position: sticky;
  top: 0;
  z-index: 30;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}

/* The bar shares the shell's measurements exactly, or the wordmark and the
   first tee time do not start at the same x. */
.g-header__bar {
  width: 100%;
  max-width: calc(var(--g-shell) + var(--g-pad) * 2);
  margin: 0 auto;
  padding: var(--g-head-pad) var(--g-pad);
  min-height: var(--g-head-h);
  display: flex;
  align-items: center;
  gap: var(--g-2);
  min-width: 0;
}

/* ---- the brand lockup ---------------------------------------------------
   <a class="g-brand"><img class="g-brand__mark"><span class="g-brand__word">

   THE MARK IS AN <img>, never a background-image and never inline SVG, so
   swapping the owner's raster for his future vector is a one-file change with
   no template edit and no CSS edit. Its dimensions live on this one class and
   nowhere else. 22px is the brand sheet's stated minimum and also the ceiling
   for the placeholder: it ships as a 4x raster and it WILL go soft against live
   text above 24px.

   The pin is not in the repo yet (design/brand/ is empty). Until the owner's
   export lands the template renders the word alone — never a substitute glyph,
   never a flag emoji, never the old flagstick-disc macro.

   THE WORDMARK IS LIVE TEXT. Because the face is self-hosted for headings
   anyway, "TeeScout" costs nothing extra as real text and beats an SVG in every
   way that matters: crisp at any density, recolourable for dark mode,
   selectable, translatable.

   CLEAR SPACE is the brand sheet's rule — "equal to the height of the T" — and
   the number comes out of the font, not out of the air: Space Grotesk's
   sCapHeight is 700/1000 upem, so a "T" at 17px is 11.9px tall and the gap is
   12px, which is --g-3. (The component spec's parenthetical called 8px the cap
   height of the T at 17px; the font file disagrees, and the font file wins.)
   -------------------------------------------------------------------------- */
.g-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--g-3);
  min-height: var(--g-tap);
  flex: 0 0 auto;
  text-decoration: none;
  min-width: 0;
}

.g-brand__mark {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
  display: block;
}

.g-brand__word {
  font-family: var(--g-display);
  font-weight: 600;
  font-size: var(--t-lead);
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  color: var(--ink);
  white-space: nowrap;
}

/* ---- destinations -------------------------------------------------------
   Two, not three: two of the old three pointed at /texts. The lifetime alert
   odometer that rode in the header as a bare badge is deleted — a bare count
   next to a nav item reads as "unread", and it was neither unread nor recent.
   44px min-height because the old footer links rendered at 16px and the header
   sign-in at 36px; both were floor violations.
   -------------------------------------------------------------------------- */
.g-nav {
  display: flex;
  align-items: center;
  gap: var(--g-1);
  margin-left: auto;
  min-width: 0;
}

.g-nav__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--g-tap);
  padding: 0 var(--g-2);
  border-radius: var(--g-r-press);
  font-size: var(--t-fact);
  font-weight: 500;
  color: var(--dim);
  text-decoration: none;
  white-space: nowrap;
}

@media (min-width: 390px) {
  .g-nav__link { padding: 0 var(--g-3); font-size: var(--t-body); }
}

.g-nav__link:hover { background: var(--panel); color: var(--ink); }

.g-nav__link[aria-current="page"] {
  color: var(--ink);
  font-weight: 600;
  background: var(--panel);
}

/* ---- the account menu ---------------------------------------------------
   A <details>. It opens on tap, on Enter and on Space and closes on Escape,
   and none of that needs script.
   -------------------------------------------------------------------------- */
.g-account { position: relative; flex: 0 0 auto; }

.g-account__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--g-tap);
  height: var(--g-tap);
  cursor: pointer;
  list-style: none;
  -webkit-tap-highlight-color: transparent;
}
.g-account__btn::-webkit-details-marker { display: none; }
.g-account__btn::marker { content: ""; }
.g-account__btn:focus-visible { border-radius: 50%; }

.g-account__ring {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--g-tap) - var(--g-2));   /* 36px inside a 44px tap target */
  height: calc(var(--g-tap) - var(--g-2));
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: var(--panel);
  color: var(--ink);
  font-size: var(--t-fact);
  font-weight: 600;
  line-height: 1;
  transition: border-color 120ms ease, background-color 120ms ease;
}
.g-account__btn:hover .g-account__ring,
.g-account[open] .g-account__ring { border-color: var(--dim); }

.g-account__menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--g-1));
  z-index: 40;
  width: max-content;
  min-width: 15rem;
  max-width: min(20rem, calc(100vw - var(--g-5)));
  padding: var(--g-4);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--g-r-card);
}

.g-account__who {
  margin: 0 0 var(--g-1);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  overflow-wrap: anywhere;
}
.g-account__who b { color: var(--ink); font-weight: 600; }

.g-account__note {
  margin: 0 0 var(--g-3);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

@media (min-width: 1024px) {
  :root { --g-head-h: 56px; --g-pad: 32px; }
}

/* ==========================================================================
   §4 — BUTTONS

   ONE SIZE. "Sign in with Google" shipped at 52 / 44 / 36px and two weights
   across one flow; .g-btn--sm is deleted and the header button is 44px like
   everything else.
   ========================================================================== */

.g-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--g-2);
  min-height: var(--g-tap);
  padding: 0 var(--g-5);
  border-radius: var(--g-r-press);
  border: 1px solid transparent;
  font-family: inherit;
  font-size: var(--t-body);
  font-weight: 500;
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  background: var(--panel);
  color: var(--ink);
  transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
  -webkit-appearance: none;
  appearance: none;
}

.g-btn:hover { text-decoration: none; }

/* One per screen. --ink fill, --paper label: 17.48:1 in light and 14.82:1 in
   dark. It was --fairway, and it moved for two reasons — a button is not a
   bookable tee time, and #1B6B45 measures 2.69:1 on the navy dark surface, so
   the primary action could not have survived dark mode in green. */
.g-btn--primary {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.g-btn--primary:hover { background: var(--dim); border-color: var(--dim); }

/* Everything else. Hairline, no fill — and the hairline is --control, not
   --rule, for the reason spelled out on the token: on an outlined button with
   no fill the border IS the control, and --rule pays 1.37:1 where 1.4.11 asks
   3. Moved with the inputs rather than after them, because a 3.75:1 field
   sitting beside a 1.37:1 button is a worse screen than either alone. */
.g-btn--quiet {
  background: var(--paper);
  border-color: var(--control);
  color: var(--ink);
}
.g-btn--quiet:hover { background: var(--panel); border-color: var(--dim); }

/* Turning something off, deleting a watch. Rare. */
.g-btn--danger {
  background: var(--paper);
  border-color: var(--control);
  color: var(--clay);
}
.g-btn--danger:hover { border-color: var(--clay); }

/* A button that reads as a sentence — "not now", "start over". */
.g-btn--plain {
  min-height: var(--g-tap);
  padding: 0 var(--g-1);
  background: none;
  border-color: transparent;
  color: var(--dim);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.g-btn--plain:hover { color: var(--ink); background: none; }

/* Full width: the default shape on a phone for the one real action. */
.g-btn--wide { width: 100%; }

.g-btn[disabled],
.g-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* A row of buttons: stacked and full width on a phone, inline once there is
   room. Mobile-first, so the stack is the base and 480 opts out. */
.g-btnrow {
  display: flex;
  flex-wrap: wrap;
  gap: var(--g-2);
  align-items: center;
}
.g-btnrow > .g-btn        { flex: 1 1 100%; }
.g-btnrow > .g-btn--plain { flex: 0 0 auto; }

@media (min-width: 480px) {
  .g-btn--wide       { width: auto; }
  .g-btn--wide-always { width: 100%; }
  .g-btnrow > .g-btn { flex: 0 0 auto; }
}

/* ==========================================================================
   §5 — FORMS

   A real <label> always, never a placeholder standing in for one.
   ========================================================================== */

.g-form { margin: 0; }

.g-fieldset {
  border: 0;
  padding: 0;
  margin: 0 0 var(--g-6);
  min-width: 0;   /* a fieldset otherwise refuses to shrink below its content */
}
.g-fieldset:last-child { margin-bottom: 0; }

.g-legend {
  padding: 0;
  margin: 0 0 var(--g-3);
  font-family: var(--g-display);
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  color: var(--ink);
}

.g-field { margin: 0 0 var(--g-4); min-width: 0; }
.g-field:last-child { margin-bottom: 0; }

.g-label {
  display: block;
  margin: 0 0 var(--g-1);
  font-size: var(--t-fact);
  font-weight: 600;
  color: var(--ink);
}

.g-req { color: var(--clay); font-weight: 500; }

/* Said before the box, not after the mistake. */
.g-hint {
  margin: 0 0 var(--g-2);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

.g-input,
.g-select,
.g-textarea {
  display: block;
  width: 100%;
  min-width: 0;
  min-height: var(--g-tap);
  padding: var(--g-2) var(--g-3);
  /* --control, never --rule. The box's fill is identical to what it sits on in
     both schemes (#FFFFFF on #FFFFFF, #0B1A2E on #0B1A2E), so this 1px line is
     the whole affordance and it owes WCAG 1.4.11 a 3:1. --rule paid 1.37:1 on
     paper and 1.26:1 on the query line's --panel. See the token. */
  border: 1px solid var(--control);
  border-radius: var(--g-r-press);
  background: var(--paper);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--t-body);   /* 16px at every width: below it iOS zooms the
                                 page on focus, and there is no desktop reason
                                 to be smaller than the body */
  line-height: var(--lh-body);
  -webkit-appearance: none;
  appearance: none;
}

.g-input::placeholder,
.g-textarea::placeholder { color: var(--dim); }

.g-input:hover,
.g-select:hover,
.g-textarea:hover { border-color: var(--dim); }

.g-input:focus,
.g-select:focus,
.g-textarea:focus { border-color: var(--ink); }

.g-textarea { min-height: calc(var(--g-8) + var(--g-6)); resize: vertical; }

/* Times, prices, phone numbers, distances. */
.g-input--num,
.g-input[type="time"],
.g-input[type="date"],
.g-input[type="tel"],
.g-input[type="number"] { font-variant-numeric: tabular-nums; }

/* The caret is two 5px gradient halves. The 5px is the glyph's own size, not a
   spacing value, which is why the second offset is one token plus one glyph
   width — they have to be exactly adjacent or the arrow comes apart. */
.g-select {
  padding-right: var(--g-6);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--dim) 50%),
    linear-gradient(135deg, var(--dim) 50%, transparent 50%);
  background-position: right calc(var(--g-3) + 5px) center, right var(--g-3) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

/* Two fields side by side once there is room. Stacked on a phone. */
.g-field-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--g-4);
  margin: 0 0 var(--g-4);
}
.g-field-pair:last-child { margin-bottom: 0; }
.g-field-pair > .g-field { margin-bottom: 0; }

@media (min-width: 560px) {
  .g-field-pair { grid-template-columns: 1fr 1fr; }
}

/* A wrong answer, said next to the box it belongs to, at the 15px floor. */
.g-field--bad .g-input,
.g-field--bad .g-select,
.g-field--bad .g-textarea,
.g-app [aria-invalid="true"] { border-color: var(--clay); }

.g-field__err {
  display: block;
  margin: var(--g-1) 0 0;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--clay);
  font-weight: 500;
}

/* Success is not a colour here — green is spent on tee times, so the sentence
   carries the good news and the ink stays navy. */
.g-field__ok {
  display: block;
  margin: var(--g-1) 0 0;
  font-size: var(--t-fact);
  color: var(--ink);
  font-weight: 500;
}

/* ---- checkboxes and radios ---------------------------------------------- */

.g-check {
  display: flex;
  align-items: flex-start;
  gap: var(--g-3);
  min-height: var(--g-tap);
  padding: var(--g-2) var(--g-2);
  margin: 0 calc(var(--g-2) * -1);
  border-radius: var(--g-r-press);
  cursor: pointer;
  min-width: 0;
}
.g-check:hover { background: var(--panel); }

.g-check input[type="checkbox"],
.g-check input[type="radio"] {
  flex: 0 0 auto;
  width: var(--g-box); height: var(--g-box);
  margin: var(--g-1) 0 0;
  accent-color: var(--ink);   /* was --fairway. A ticked box is not a tee time */
  cursor: pointer;
}

.g-check__text { min-width: 0; font-size: var(--t-body); line-height: var(--lh-body); }
.g-check__note { display: block; font-size: var(--t-fact); color: var(--dim); }

/* A scrolling list of things to tick — the 41-course picker. Scrolls DOWN
   only. The count moves ABOVE the box: it was 14.5px --dim underneath a nested
   320px scroller, which is where a count goes to not be read. */
.g-checklist {
  max-height: 20rem;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--g-2) var(--g-3);
  border: 1px solid var(--rule);
  border-radius: var(--g-r-card);
  background: var(--paper);
  -webkit-overflow-scrolling: touch;
}

.g-checklist__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--g-2);
  margin: 0 0 var(--g-2);
}

.g-checklist__count {
  font-size: var(--t-body);
  color: var(--ink);
  margin-left: auto;
}

/* None ticked: the reason is visible, at the floor size, in --clay. */
.g-checklist__count--none { color: var(--clay); font-weight: 500; }

/* ---- day pills ----------------------------------------------------------
   ONE selected-state rule, not two. The old file styled
   `.g-choice:has(input:checked)` and a server-stamped `.g-choice--on` with
   identical blocks, so after a tap the pill you left and the pill you chose
   both painted as selected. The server class is deleted; :has() is the only
   source of truth, and the fill is --ink because a day pill is not a tee time.
   -------------------------------------------------------------------------- */
.g-choices { display: flex; flex-wrap: wrap; gap: var(--g-2); }

.g-choice {
  display: inline-flex;
  align-items: center;
  min-height: var(--g-tap);
  padding: 0 var(--g-4);
  border: 1px solid var(--rule);
  border-radius: var(--g-r-press);
  background: var(--paper);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
}
.g-choice:hover { background: var(--panel); border-color: var(--dim); }

.g-choice input { position: absolute; opacity: 0; pointer-events: none; }

.g-choice:has(input:checked) {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--paper);
}

.g-choice:has(input:focus-visible) { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The buttons at the bottom of a form. */
.g-form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--g-2);
  margin-top: var(--g-5);
  padding-top: var(--g-4);
  border-top: 1px solid var(--rule);
}
.g-form__actions > .g-btn        { flex: 1 1 100%; }
.g-form__actions > .g-btn--plain { flex: 0 0 auto; }

@media (min-width: 480px) {
  .g-form__actions > .g-btn { flex: 0 0 auto; }
}

/* ==========================================================================
   §6 — THE SHEET PAGE

   TWO GENUINELY DIFFERENT LAYOUTS, not one layout at two widths.

   PHONE: one column, everything stacked in the order a golfer needs it —
   what he asked for, which day, which part of the day, then the sheet. Two
   taps and he is looking at ~35 tee times.

   >=1024: TWO PANES. A navy chrome rail on the left carrying the controls and
   an index of the courses that have times on the selected day and band; the
   white sheet on the right. That is not the phone widened and it is not three
   day columns — the day columns were the earlier answer and were deliberately
   replaced, because with the day rail now a filter there is exactly one day on
   the page and nothing to put in a second column.

   The rail is CHROME and the sheet is PAPER, and the split is the whole point:
   navy for the things you press, white for the things you read in bright sun.
   Every hex the rail redeclares is one §0.2 already measured against #0B1A2E.

   The row never stretches. A 5-cell row pulled to 880px strands a $53 half a
   metre from its tee time, which is the 736px problem with more air, so the
   sheet keeps a max-width and the extra pixels go to the rail and the margins.
   ========================================================================== */

.g-sheetpage { display: block; }

/* Below 1024 the rail is not a rail — it is just the first blocks of the page,
   in the order the mobile layout wants them: query, day, band, filters. */
.g-rail { display: block; }

.g-sheet { display: block; }

/* One day: the sticky header, then run headers and rows. */
.g-sheet__day { min-width: 0; }

@media (min-width: 1024px) {
  :root {
    --g-shell: 1000px;  /* rail 320 + gutter 40 + a 640px sheet */
    --g-row:   56px;    /* the desktop pitch: the extra width goes to the
                           columns of the row, not to new content */
  }

  .g-sheetpage {
    display: grid;
    grid-template-columns: var(--g-rail) minmax(0, 1fr);
    column-gap: var(--g-gutter);
    align-items: start;
  }

  /* THE LEFT PANE. Navy chrome, by redeclaring the tokens rather than by
     writing literals on the fifteen components inside it — the query line, the
     day cells, the band chips, the cancellations toggle, the dead-cart line and
     the course index all draw themselves from --ink / --dim / --panel /
     --paper, so one block turns the whole pane over and none of them needs to
     know it happened.

     THE HEXES ARE §0.2's, EXACTLY. Every one was measured against #0B1A2E when
     dark mode was decided: ink 14.82:1, dim 8.06:1, fairway 8.53:1, clay
     6.70:1, control 4.29:1, live 5.53:1. ops/contrast.py re-checks that column
     on every run. In dark mode this block is a no-op, which is the point —
     chrome that changes colour with the OS is decoration, not chrome.

     Sticky, never its own scroller. A second scrollbar is a dashboard move and
     this is not a dashboard. If the pane ever outgrows the viewport the course
     index is what gives way first — it is an index into rows that are all on
     the page anyway, so nothing bookable can hide in it. */
  .g-rail {
    --paper:     #0B1A2E;
    --panel:     #12243C;
    --ink:       #E6EDF6;
    --dim:       #9FB2C9;
    --faint:     #5C7085;
    --rule:      #22374F;
    --rule-hair: #17293E;
    --control:   #6A8098;
    --clay:      #E8875C;
    /* No --fairway: see the note in §3. Nothing in this pane prints a green
       word, and the ledger in ops/contrast.py enforces that. */

    position: sticky;
    top: calc(var(--g-head-h) + 1px + var(--g-5));
    align-self: start;
    max-height: calc(100vh - var(--g-head-h) - var(--g-8));
    overflow: auto;
    padding: var(--g-4);
    border-radius: var(--g-r-card);
    background: var(--paper);
    color: var(--ink);
  }

  /* THE RAIL IS A NAVY CARD, AND IN DARK MODE THE PAGE IS ALSO NAVY. Its local
     --paper is #0B1A2E, which is byte-identical to the dark scheme's page
     --paper, so at 1512 in dark the card had no edge at all: the two panes read
     as one surface and the band chips floated in the middle of nothing.
     Measured, not guessed — the screenshot at design/wave2/play-1512x982-dark.png
     is what showed it.

     Lifted to --panel (#12243C, 1.12:1 off the page) with a --rule hairline,
     which is the same containment the light scheme gets for free from the card
     sitting on white. Nothing inside the rail moves: its children still resolve
     --paper to #0B1A2E, so a chip now reads as a darker inset against the pane
     rather than as the pane itself. */
  @media (prefers-color-scheme: dark) {
    .g-rail {
      background: var(--panel);
      box-shadow: inset 0 0 0 1px var(--rule);
    }
  }

  .g-rail > * + * { margin-top: var(--g-5); }

  /* THE RIGHT PANE stays paper in both schemes, because that is the surface the
     tee times are read on and the sun does not care what the rail is doing. */
  .g-sheet { max-width: 640px; }

}

/* 1280 does NOT introduce a second day column. It used to, and that was the
   right answer while the sheet held seven days; the day rail is a filter now,
   so there is one day on the page and nothing to compare it against. The extra
   width goes to the margins, which is the honest place for it. */

/* ==========================================================================
   §7 — THE QUERY LINE AND THE CANCELLATIONS FILTER
   ========================================================================== */

/* The collapsed search: 48px, one line of live text, and the whole strip is
   the "Change" affordance. It replaces a 368px three-field form. He already
   answered these questions in the URL, so the answer is a sentence, not three
   fields — and the real controls live behind a native <details>, so no JS.

   Measured at 320: "Sag Harbor, NY · 60 min · this week" is 234px in SF Pro
   and 256px in Arial at 16px, plus ~65px for "Change ›" — 299/321 against
   296px of content. That is why .g-query__extra exists and why "this week"
   drops below 360, where the same line measures 227/249 and fits. */
.g-query {
  display: block;
  border-radius: var(--g-r-press);
  background: var(--panel);
}

.g-query__now {
  display: flex;
  align-items: center;
  gap: var(--g-3);
  min-height: var(--g-7);
  padding: 0 var(--g-3);
  font-size: var(--t-body);
  color: var(--ink);
  cursor: pointer;
  list-style: none;
  min-width: 0;
}
.g-query__now::-webkit-details-marker { display: none; }
.g-query__now::marker { content: ""; }

/* WHICH PART GIVES WAY. This was one nowrap-and-ellipsize box holding all three
   facts, which meant the longest fact — the address — ate the width and pushed
   the radius and the window past the right edge, clipped by an ancestor with no
   ellipsis to say they existed. Measured on 2026-08-06 at 390px with
   "225 Powell Ave, Newburgh, NY 12550" the summary read
   "225 Powell Ave, Newburgh, NY 1…   Change ›" and .g-query__extra's right edge
   was at 445.1px against a 390px viewport — the one element in the whole audit
   whose box left the screen. It failed on every street address tried and passed
   on every town; at 375 it failed one address earlier, at
   "Hauppauge, New York 11788".

   So the row is a flex pair: the address shrinks and ellipsizes, the numbers
   are `flex: 0 0 auto` and cannot. He typed the address and recognises it from
   its first characters; the radius and the window are the two facts that say
   why these tee times and not others, and a clipped one is unrecoverable.

   THE ONE RESIDUAL, and it is the trade working rather than failing. Swept over
   six /play variants at 320 and 360 in both schemes, exactly one line still
   ellipsizes: "Sag Harbor, NY · 90 min · this week" at 360, which needs 333px
   against 304 in Chromium's widest fallback stack (it measures 249 in SF Pro
   and fits on a real phone). The address gives up three characters — "Sag
   Harbor, …" — and both numbers survive, which is the order this rule exists to
   fix. The old single box lost "this week" entirely on the same line. Widening
   the .g-query__extra breakpoint past 360 would take the window away from every
   phone that can in fact show it, to buy back seven pixels on one string. */
.g-query__what {
  display: flex;
  align-items: baseline;
  /* The word space between the address and the "·". A flex item starts its own line
     box, so the leading space inside .g-query__fixed is collapsed away and the line
     renders "Montauk, NY· 2 hr". 4px is the nearest step to a 16px word space (~4.4px)
     and it is inert at 1024, where .g-query__what goes back to display: block. */
  gap: 0 var(--g-1);
  min-width: 0;
  overflow: hidden;
}

.g-query__where {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.g-query__fixed {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The part of the sentence that only exists once there is room for it. Still
   dropped WHOLE below 360 rather than cut: "· thi…" is worse than silence. */
.g-query__extra { display: none; }

@media (min-width: 360px) {
  .g-query__extra { display: inline; }
}

.g-query__change {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: var(--t-fact);
  font-weight: 600;
  color: var(--dim);
  white-space: nowrap;
}
.g-query__now:hover .g-query__change { color: var(--ink); }

.g-query__form {
  padding: 0 var(--g-3) var(--g-3);
  display: flex;
  flex-direction: column;
  gap: var(--g-4);
}

/* In the 320px rail the whole sentence plus "Change" needs ~300px and comes out
   as "Sag Harbor, NY · 60 min · thi…". A desktop rail has vertical room and no
   fold to defend, so the line wraps instead of truncating: an address you
   cannot read is not a summary of your search. */
@media (min-width: 1024px) {
  .g-query__now { align-items: flex-start; padding-top: var(--g-3); padding-bottom: var(--g-3); }
  /* Back to one wrapping run of text: `display: block` puts the two spans back
     inline so the line breaks between words instead of between boxes. */
  .g-query__what { display: block; overflow: visible; }
  .g-query__where { white-space: normal; overflow: visible; }
  .g-query__fixed { white-space: normal; }
}

/* The cancellations filter. This is the highest-value control on the page:
   genuine releases measured 0.39%–2.54% of open rows across the next six days
   on 2026-08-06 (21–71 marked against 2,038–3,365 open), so every purely
   visual scheme is solving "find one needle in forty rows" with encoding when
   a WHERE clause solves it outright. It is a ?only=cancelled param over the
   same `released` set the marker uses — no new query, no new classification
   path, no legend to learn.

   IT IS A CHIP NOW, AND THAT IS A REPAIR RATHER THAN A RESTYLE. It was a
   full-width 44px row carrying its label on the left and a 20px unfilled ring
   pushed to the right edge by `margin-left: auto`. That is the shape of an
   unstyled radio button, at every width, sitting directly under two rows of
   controls that fill with --ink to say "on" — so the one control on the sheet
   that could not be read at a glance was the one the whole product is about.
   The ring is deleted (`.g-only__dot` with it) and the chip fills, exactly like
   `.g-days__cell--on` and `.g-bands__chip--on`. One selected-state idiom in
   the rail instead of two.

   Shrink-to-fit rather than full width: a 44px band across the pane read as a
   list row you could open, and there is nothing behind it. Still styled to work
   as an <a>, a <button> or a <label>, and to read as "on" from either
   .g-only--on or [aria-pressed="true"]. The focus ring is §1's --ink outline,
   inherited: a focus ring is not a bookable tee time and never goes green. */
.g-only {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: var(--g-2);
  max-width: 100%;
  min-height: var(--g-tap);
  padding: 0 var(--g-3);
  border: 1px solid var(--rule);
  border-radius: var(--g-r-press);
  background: var(--paper);
  font-family: inherit;
  font-size: var(--t-fact);
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  cursor: pointer;
  transition: background-color 120ms ease, color 120ms ease;
}
.g-only:hover { background: var(--panel); border-color: var(--dim); }

.g-only .g-num { font-variant-numeric: tabular-nums; }

/* On. --ink fill, --paper label — 17.48:1 light, 14.82:1 dark. Not green: the
   filter is a control, and the only green on this page belongs to a tee time
   somebody can book. */
.g-only--on,
.g-only[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.g-only--on:hover,
.g-only[aria-pressed="true"]:hover { background: var(--ink); border-color: var(--ink); }

/* Nothing to filter. It STAYS ON SCREEN and reads (0), the same rule the day
   cells and the band chips keep: "nobody has given one back in this window" is
   an answer, and a control that vanishes teaches nothing. */
.g-only--empty,
.g-only[aria-disabled="true"] {
  color: var(--dim);
  background: none;
  cursor: default;
  pointer-events: none;
}


/* ==========================================================================
   §8 — THE MECHANISM LINE

   Once per sheet, above the first day. THE ONLY PLACE THE PRODUCT EXPLAINS
   ITSELF ON THIS SCREEN, and the difference it describes — a real cancellation
   against a checkout timer that ran out — is the whole product.

   Every value in it renders from analytics.supply_breakdown(conn). No literal
   ships, and the FRACTION prints rather than the percentage, because the same
   corpus gives three different arithmetically-correct answers depending on the
   denominator you pick: 536 of 1,622 (supply_breakdown, verified in the
   database on 2026-08-06), 527 of 2,586 (matcher._phantom_rate) and 18.3% of
   3,808 (the brief). A fraction that moves is honest; a percentage that moves
   is a lie with a decimal point.
   ========================================================================== */

.g-mech {
  margin: 0;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  max-width: 42em;
}

.g-mech .g-num { color: var(--ink); font-weight: 500; }

/* ==========================================================================
   §9 — THE TWO TAPS: THE DAY RAIL AND THE BAND CHIPS

   This section used to be a week rail: 26 binary half-hour cells on desktop and
   3 coarse segments on the phone, drawn per day, hidden entirely below 360.
   All of it is gone, for three separate reasons and any one of them was enough.

   IT WAS WRONG. Every cell and every count came from the merged Python list,
   which is open_slots(limit=MAX_SLOTS) with MAX_SLOTS = 2000. The default week
   window matches 18,111 rows on the live corpus, so that slice covered ONE day:
   histogram [('2026-08-07', 2000)], one day and six zeros against seven real
   ones. A rail whose job is "where is the supply this week" was answering with
   the truncation point of a LIMIT.

   IT WAS UNREACHABLE. `.g-week { display: none }` below 360px. 320 is the base
   width of this design and the whole rail simply did not exist there.

   IT WAS THE WRONG OBJECT. Its cells were anchor jumps into a sheet that
   LONG_SHEET had already truncated, so six `href="#day-…"` pointed at two
   rendered ids. A golfer choosing a day does not want a shape, he wants a
   number and a filter.

   What replaces it is two rows of controls, both of them filters, both carrying
   COUNT(*) figures read from the corpus at request time:

     .g-days    seven day cells   622 -> ~150
     .g-bands   four time bands   ~150 -> ~35

   BOTH WORK AT 320. That is the constraint that shaped them: 7 x 40px + 6 x 2px
   of gap = 292 inside the 296px a 320 viewport leaves, and the bands are a
   2x2 grid there rather than a row of four. Nothing is hidden at any width.

   GREEN APPEARS ONCE IN THIS WHOLE SECTION: .g-days__dot--on, which means that
   day contains a tee time somebody gave back. It is a mark on a count of
   bookable inventory, which is the only thing --live is ever allowed to be.
   Selected state is --ink fill, everywhere, on both controls.
   ========================================================================== */

/* ---- the day rail -------------------------------------------------------
   Seven equal columns. `1fr` and not a fixed width: at 320 that is 41.7px per
   cell, at 390 it is 51.1px, and the cell's content — three short lines
   centred — is width-independent.
   -------------------------------------------------------------------------- */
/* THE 320px BUDGET, WRITTEN DOWN BECAUSE IT IS THE ACCEPTANCE TEST.
   Measured in Chrome at 320x568 against the live corpus on 2026-08-07, top of
   document to top of the first tee row:

     header 53 · query 48 · days 48 · bands 100 · filter 44 · carts 28
     day header 57 · run header 56–101

   A row is 60px, so the first one clears a 568px fold. It did not before: the
   two prose blocks this rebuild deleted put it at 573 with ZERO rows visible.
   The five 4px gaps below are part of that budget — at 320 and only at 320,
   because a 390px screen has the room and looks better with air. */
@media (max-width: 359px) {
  .g-rail .g-stack--tight { gap: var(--g-1); }
}

.g-days {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 2px;
}

.g-days__cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* 48, not 44. The floor is 44 and this is the single most-pressed control on
     the product's most-used page; the extra 4px is bought back by the cell
     carrying its own padding rather than adding to it. */
  min-height: 48px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--g-r-press);
  text-decoration: none;
  color: var(--ink);
  min-width: 0;
}
.g-days__cell:hover { background: var(--panel); }

.g-days__dow {
  font-size: var(--t-fact);
  line-height: 1;
  font-weight: 500;
  color: var(--dim);
  /* MON not Monday, and never below the 15px floor. At 320 "WED" in the system
     stack at 15px/500 measures ~34px against 41.7 of column. */
  letter-spacing: 0.02em;
}

/* The number is the subject of the cell, so it is the one thing set at --t-lg.
   Tabular, because seven of them form a column a finger runs across and 156
   beside 98 must not shuffle. */
.g-days__n {
  font-size: var(--t-lg);
  line-height: 1;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Reserved on every cell, filled on the days that have one, exactly like the
   row-gutter mark: a dot that only exists on some cells would shift the
   baseline of the counts above it by 6px and the rail would look broken on a
   quiet week. */
.g-days__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: transparent;
}

/* GREEN #1 of 3 on this page. This day contains a tee time somebody gave back
   — a count of bookable inventory, which is the only thing green may mark. */
.g-days__dot--on { background: var(--live); }

/* Selected. --ink fill, --paper letters: 17.48:1 light, 14.82:1 dark. Not
   green — a day cell is a control, and the green on this page belongs to tee
   times. */
.g-days__cell--on {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.g-days__cell--on .g-days__dow,
.g-days__cell--on .g-days__n { color: var(--paper); }

/* Nothing on this day. It STAYS ON SCREEN, reads 0, and is STILL A LINK: the
   count says how much is there, not whether the control works, and the same 0
   on the chips below used to mean the opposite. Following it lands on the empty
   state, which names what is empty and offers the best day in the window. */
.g-days__cell--empty .g-days__n { color: var(--dim); font-weight: 400; }

/* ---- the band chips -----------------------------------------------------
   FOUR, and all four always render. 2x2 at 320 where four across would give
   each chip 71px for a word, its hours and a count; one row of four from 360.
   -------------------------------------------------------------------------- */
.g-bands {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* 4px between chips, not 8. Four bordered chips across a 360px phone is the
     tightest cell in the product and the 4px it buys back per chip is the
     difference between "11a-2p 36" sitting on one line and taking a third. The
     chips are separated by their own 1px borders, not by the gap, so halving it
     costs no legibility. */
  gap: var(--g-1);
}

/* FOUR ACROSS FROM 375, NOT FROM 360, and 2x2 again inside the desktop rail.
   Measured in Chromium against the live corpus, chip width and rendered line
   count, after the hours moved to the 15px floor:

     320   2x2       chip 146px   2 lines
     360   4 across  chip  79px   3 lines   <- "11a-2p" 52.5 + gap 4 + "36" 19.3
     360   2x2       chip 166px   2 lines      = 76px against 73px of inner width
     375   4 across  chip  83px   2 lines
     390   4 across  chip  87px   2 lines
     414   4 across  chip  93px   2 lines
     430   4 across  chip  97px   2 lines
     768   4 across  chip 175px   2 lines
     1024  4 across  chip  69px   3 lines   <- the 320px rail, tighter than 360
     1024  2x2       chip 144px   2 lines

   Two lines everywhere and three in two places is not a layout, it is a layout
   with two exceptions nobody chose, and both of them are the same three pixels
   of arithmetic. The two breakpoints below buy them back with width the screen
   actually has: a 360px phone has room for a 2x2 grid, and the desktop rail is
   320px wide with a whole column of vertical space beside it.

   Nothing is hidden and nothing shrinks at either step: the band's word, its
   real hours and its count all print at all seven widths. If a longer band
   label or a five-digit count is ever added, re-measure at 375 first — that is
   now the tightest four-across cell in the product. */
@media (min-width: 375px) {
  .g-bands { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* TWO LINES, NOT THREE, and the second line is measured rather than styled to
   taste. Three stacked lines put the 2x2 grid a 320px phone falls back to at
   130px — the single largest block above the fold on the screen where the whole
   acceptance test is "can you see a tee time without scrolling". Folding the
   hours and the count onto one line takes the chip to the 44px floor and the
   grid to 96px at 320 and 44px from 360.

   Nothing was dropped to do it. The band's word, its real hours and its count
   are all still printed; they are arranged in two rows instead of three. */
.g-bands__chip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  /* 4px between the hours and the count, not 2. 2px bought a line at 360 and cost
     legibility at every width: "5-11a" in --dim beside "32" in --ink read as one
     token, "5-11a32", which is the exact failure a band chip cannot have. Weight
     and colour are not enough separation at 15px; the gap is. */
  gap: 0 var(--g-1);
  min-height: var(--g-tap);
  /* 2px of side padding and a 2px inner gap, and the measurement is what set them.
     The hours moved from 13px to the 15px floor, which added ~8px to the widest
     second line ("11a-2p" + gap + "36"), so the width had to come from somewhere
     that is not the type. It came from the separator (the "·" after the hours,
     deleted, ~7px) and from 2px a side of padding; the 44px tap floor is what
     actually sets the chip's height, so trimming padding costs nothing tappable.

     Chip width and height across the widths, Chromium, live corpus, measured
     after the 15px change and NOT estimated:

        320   2x2,      chip 146px   44px   grid 92px
        360   4 across, chip  79px   61px   "11a-2p 36" takes a third line
        375   4 across, chip  83px   44px
        390   4 across, chip  87px   44px
        414   4 across, chip  93px   44px
        430   4 across, chip  97px   44px
        1024+ 4 across, chip  69px   61px   the desktop rail, third line again

     So exactly two cases render three lines: a 360px phone and the 320px desktop
     rail. Both are legible and both cost 17px on screens that already clear seven
     and ten tee times above the fold respectively. The tightest cell is Midday at
     360 -- "11a-2p" is 52.5px at 15px, the count 19.3px, the gap 4px, against
     73px of inner width -- so if a longer band label or a five-digit count is
     ever added, re-measure at 360 before shipping it. */
  padding: 2px;
  border: 1px solid var(--rule);
  border-radius: var(--g-r-press);
  background: var(--paper);
  text-decoration: none;
  color: var(--ink);
  min-width: 0;
}
.g-bands__chip:hover { background: var(--panel); border-color: var(--dim); }

/* Its own line, so the word never competes with the figures for width. At 320
   the chip is 144px against a 72px "Afternoon"; from 360 it is 83px against the
   same 72px, which is why four across is the layout from 360 and 2x2 below. */
.g-bands__label {
  flex: 0 0 100%;
  text-align: center;
  font-size: var(--t-fact);
  line-height: 1.15;
  font-weight: 600;
  white-space: nowrap;
}

/* 15px, the floor, like everything else a golfer reads. This was 13px and the
   file argued for the exception; see the retired --t-axis note in §2 for why the
   argument does not hold. The width it costs is bought back by dropping the "·"
   separator and tightening the gap rather than by shrinking the type. */
.g-bands__hours {
  font-size: var(--t-fact);
  line-height: 1.25;
  font-weight: 500;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.g-bands__n {
  font-size: var(--t-fact);
  line-height: 1.25;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.g-bands__chip--on {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.g-bands__chip--on .g-bands__hours,
.g-bands__chip--on .g-bands__n { color: var(--paper); }

/* Zero in this band. Visible, readable, and still a link — see the day cells
   above: the count is not what decides whether a control works. */
.g-bands__chip--empty .g-bands__label,
.g-bands__chip--empty .g-bands__n { color: var(--dim); font-weight: 500; }

/* ---- the filter row -----------------------------------------------------
   The cancellations toggle and the dead-cart line, in the order the locked
   layout puts them: the control, then the one sentence under it.
   -------------------------------------------------------------------------- */
.g-filters { display: flex; flex-direction: column; }

/* ---- the dead-cart line -------------------------------------------------
   ONE LINE, replacing a four-line 90px paragraph that appeared on three
   surfaces. It is the best thing this product does and it read as a footnote.
   The expansion is a native <details>, which is keyboard- and
   screen-reader-operable with no script at all.
   -------------------------------------------------------------------------- */
.g-carts { display: block; }

.g-carts__line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 var(--g-1);
  min-height: 28px;
  padding: 0 var(--g-3);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  cursor: pointer;
  list-style: none;
}
.g-carts__line::-webkit-details-marker { display: none; }
.g-carts__line::marker { content: ""; }
.g-carts__line:hover { color: var(--ink); }

.g-carts__n {
  font-size: var(--t-fact);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.g-carts__why { font-weight: 500; }

.g-carts__body {
  padding: var(--g-2) var(--g-3) var(--g-3);
  max-width: 42em;
}

.g-carts__p {
  margin: 0 0 var(--g-2);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}
.g-carts__p:last-child { margin-bottom: 0; }
.g-carts__p .g-num { color: var(--ink); font-weight: 500; }

/* ---- one line saying the sheet is longer than it looks ------------------
   Was a 116px block on the first screen, and the larger of the two prose
   blocks that owned it. It fires far less often now that a sheet is one day
   and one band.
   -------------------------------------------------------------------------- */
.g-trunc {
  margin: 0 0 var(--g-2);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}
.g-trunc .g-num { color: var(--ink); font-weight: 500; }
.g-trunc a { color: var(--ink); }

/* One course only, set from the desktop index. The way out is in the sheet
   rather than only in the index, because a shared link lands a phone in this
   state with no index on screen. */
.g-narrowed {
  display: flex;
  align-items: center;
  gap: var(--g-3);
  margin: 0 0 var(--g-2);
  font-size: var(--t-fact);
  color: var(--dim);
}
.g-narrowed__out { color: var(--ink); font-weight: 500; }

/* ---- the course multi-select --------------------------------------------
   .g-picker is a WRAPPER. partials/course_picker.html is shared with
   /rig/watches and its rows are dual-classed `picker__opt g-check` — the first
   for teescout.css, the second for this file. Neither may be touched, so
   everything this page needs is on the wrapper.
   -------------------------------------------------------------------------- */
.g-picker { display: block; }

.g-picker__now {
  display: flex;
  align-items: center;
  gap: var(--g-3);
  min-height: var(--g-tap);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink);
  cursor: pointer;
  list-style: none;
}
.g-picker__now::-webkit-details-marker { display: none; }
.g-picker__now::marker { content: ""; }

.g-picker__n {
  margin-left: auto;
  font-size: var(--t-fact);
  font-weight: 400;
  color: var(--dim);
}
.g-picker__n .g-num { color: var(--ink); font-weight: 600; }

/* A <fieldset> with the browser's border and padding removed. The legend keeps
   its semantics, which is what makes the four player radios announce as one
   group. */
.g-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
.g-fieldset > .g-label { padding: 0; }

/* ---- the desktop course index ------------------------------------------
   THE LEFT PANE'S SECOND HALF, and it exists only at 1024.

   These rules live HERE and not in §6 beside the rest of the two-pane layout,
   which is the one thing about this component worth writing down. `.g-courses
   { display: none }` and the media query that undoes it have IDENTICAL
   specificity, so the later one in the file wins -- and §9 is after §6. Put the
   override in §6 and the index renders at zero height on every desktop, which
   is exactly what happened and exactly the kind of failure that ships: the
   markup is present, the accessible tree has it, and the pane is simply blank.
   -------------------------------------------------------------------------- */
.g-courses { display: none; }

@media (min-width: 1024px) {
  /* The index of courses on this day and band. Below 1024 it does not exist:
     the run headers in the sheet already name every course, and a second list
     of the same thirty names is duplication a 568px screen cannot afford. */
  .g-courses {
    display: flex;
    flex-direction: column;
    gap: 1px;
  }

  .g-courses__item {
    display: flex;
    align-items: baseline;
    gap: var(--g-2);
    min-height: var(--g-tap);
    padding: var(--g-2) var(--g-2);
    border-radius: var(--g-r-press);
    font-size: var(--t-fact);
    color: var(--ink);
    text-decoration: none;
    min-width: 0;
  }
  .g-courses__item:hover { background: var(--panel); }

  .g-courses__nm {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-weight: 500;
  }

  .g-courses__min { flex: 0 0 auto; color: var(--dim); }

  .g-courses__n {
    flex: 0 0 auto;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }
  /* Selected: a filled row, not a green one. The course index is a control. */
  .g-courses__item--on {
    background: var(--ink);
    color: var(--paper);
  }
  .g-courses__item--on .g-courses__min { color: var(--paper); }

  /* In range, nothing today. Quieter than a row because it is not a control —
     there is nothing behind it to open — but present, because a course that
     disappears reads as one we stopped watching. 15px is the floor and this is
     on it; the weight and colour carry the demotion instead of the size. */
  .g-courses__quiet {
    margin: 10px 2px 0;
    font-size: 15px;
    line-height: 1.45;
    color: var(--dim);
  }

  /* BACK TO 2x2 IN THE RAIL. The pane is 320px of the 1000px shell — narrower
     than a 360px phone once its own 16px padding is off — so four across gives
     each chip 69px and "11a-2p 36" takes a third line. It is the only place on
     a desktop where the sheet's controls are tighter than on a phone, and the
     rail has vertical room the phone does not. Declared here in §9 for the
     reason the course index above it is: this file's later rule wins between
     two selectors of equal specificity, and .g-bands' four-across rule is a few
     dozen lines up. */
  .g-rail .g-bands { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ==========================================================================
   §10 — THE DAY HEADER

   Sticky under the header so the golfer always knows which day he is in, and
   it carries the two counts that no row-level marker can: how much there is,
   and whether the filter found anything today.
   ========================================================================== */

.g-day {
  position: sticky;
  /* +1 for the header's own bottom hairline. Measured in the browser: the bar
     is 52px and .g-header is 53px with its rule, so sticking at exactly
     --g-head-h let one pixel of the row above bleed through under the rule. */
  top: calc(var(--g-head-h) + 1px);
  z-index: 20;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--g-3);
  min-height: var(--g-day-h);
  padding: var(--g-1) 0;
  background: var(--paper);   /* opaque, or rows scroll through it */
  border-bottom: 2px solid var(--ink);
}

/* Wrapping is deliberate and measured: at 320 the name (98px), "· today"
   (51px) and the counts (133px in SF Pro, 143px in Arial) come to 282–293px
   against 296px of content, so on the narrowest phone with the widest fallback
   font the counts drop to a second line rather than shrink or truncate. From
   360 it is one line with 65px to spare. */
.g-day__name {
  font-family: var(--g-display);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  text-transform: uppercase;
  color: var(--ink);
}

.g-day__today {
  font-size: var(--t-fact);
  color: var(--dim);
}

.g-day__counts {
  margin-left: auto;
  display: flex;
  align-items: baseline;
  gap: var(--g-2);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  white-space: nowrap;
}

.g-day__count { color: var(--dim); }

/* GREEN #4 of 4, and the only green WORDS above row level. Every one of these
   refers to a counted, named, bookable tee time further down this column whose
   slot_key is in `released`. The clause is omitted entirely when the count is
   zero — never "0 cancelled". */
.g-day__cancelled { color: var(--fairway); font-weight: 600; }

/* ==========================================================================
   §11 — THE COURSE RUN HEADER

   One per consecutive block of times at one course. THIS IS WHERE THE DENSITY
   COMES FROM. Four facts leave the row and print once instead of once per row:
     course name        (596 of 596 rows today)
     drive time         (identical on all 18 rows of a run)
     the staleness clause (324 of 596 rows — the page currently apologises 8.8x
                        more often than it asserts a cancellation, which cancels
                        out the exact trust the mark exists to build)
     the resident differential, written out in full on every line

   Measured at 320: the name "MONTAUK DOWNS STATE PARK" is 246px at 17px and
   fits on its own line inside 296px; the four-fact line is 292px in SF Pro and
   316px in Arial, so it takes a second line on the narrowest phone. The header
   is therefore min-height, not height — but the ROWS below it keep their fixed
   pitch, which is what the baseline grid actually depends on.
   ========================================================================== */

.g-run {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--g-2);
  min-height: var(--g-run-h);
  padding: var(--g-2) 0 var(--g-1);
  border-bottom: 1px solid var(--rule);
  min-width: 0;
}

.g-run__name {
  font-family: var(--g-display);
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.01em;
  text-transform: uppercase;
  color: var(--ink);
  min-width: 0;
}

.g-run__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--g-2);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  min-width: 0;
}

.g-run__fact { white-space: nowrap; }

/* ...EXCEPT THE FRESHNESS CLAUSE, WHICH IS A SENTENCE. The rest of the facts are
   two or three characters ("91 min", "$62") and breaking one across a line would
   read as two facts, so nowrap is right for them. `has not answered since 12:11
   am yesterday` is 314px at 15px type and the widest phone leaves 296, so nowrap
   on that one pushed the document to 326px and gave /play a sideways scroll at
   320 — the one thing rule 8 forbids outright. Caught by the 320px audit on the
   same change that added the day to the phrase; before it the string was short
   enough to fit and the bug was latent, not absent. Sentences wrap. */
.g-run__read { white-space: normal; }

/* The separator inherits its colour rather than declaring one, which is how it
   stays off --faint and out of the banned-colour audit. */
.g-run__fact + .g-run__fact::before { content: "·"; margin-right: var(--g-2); }

/* Doubt is stated ONCE per course, not 324 times per page. The dot appears
   only when that integration's last read is older than 600s. */
/* Drawn at 8px rather than the spec's 7 because 7 is not on the scale and the
   difference is invisible; a stale dot is not worth an off-scale value. */
.g-run__dot {
  display: inline-block;
  width: var(--g-2); height: var(--g-2);
  border-radius: 50%;
  background: var(--clay);
  margin-right: var(--g-1);
}

/* CLAY BELONGS TO THE FRESHNESS CLAUSE AND TO NOTHING ELSE ON THIS HEADER.

   These two rules used to paint `.g-run__facts`, i.e. the whole line, so a late
   poller turned the drive time and the rate-card facts orange along with the
   apology. Measured on the rebuilt /play at 390 on 2026-08-06: 73 text nodes
   computed to clay, 1,277.5px of vertical text — more than one full 844px phone
   screen — and only 23 of those nodes were about freshness. The other 50 were
   "119 min" (how far you would drive, true at any hour) and "no price or spots
   posted" (a permanent property of the vendor: no Chronogolf course publishes
   capacity at all). Alarm colour on a fact that is not an alarm is how a reader
   learns to stop reading the colour, which is the one thing it is for.

   Scoped to .g-run__read, the apology is one clause per course run, beside its
   own dot, and the facts either side of it stay --dim. */
.g-run--stale .g-run__read { color: var(--dim); }
.g-run--dead .g-run__read { color: var(--clay); }

/* ==========================================================================
   §12 — THE TEE ROW

   ONE MARKUP, ONE SET OF COLUMNS, ONE GRID, at every width. Desktop is not a
   different row; only the column widths change. There is no viewport-dependent
   row CONTENT anywhere — in a no-JS server-rendered app that means rendering
   both and hiding one, and it drifts within a month.

   The whole row is a single <a> to the course's booking deeplink. The chevron
   is decoration and aria-hidden; the row's accessible name carries the
   sentence, including — on a released row — "came back N minutes after
   somebody gave it up", which costs one extra column on a SELECT that already
   reads that row.

   WHAT LEFT THE ROW, ranked by pixels freed. This is the acceptance checklist,
   and not one line of it is a font size:
     1. "Book on <course>" button           -> the row IS the link   ~305px/row
     2. the course name                     -> .g-run header         ~200px/row
     3. "last checked N min ago, may be gone" (on 324 of 596 rows)
                                            -> .g-run + --clay dot   ~230px/row
     4. ", or $34 with a New York licence"  -> .g-run differential   ~150px/row
     5. "44 min drive" (identical 18 of 18) -> .g-run header          ~86px/row
     6. the "soonest" hero row              -> deleted outright
   ========================================================================== */

.g-teelist {
  list-style: none;
  margin: 0;
  padding: 0;
}

.g-tee {
  display: grid;
  grid-template-columns:
    var(--g-mark-gutter)
    var(--g-c-time)
    minmax(0, 1fr)
    var(--g-c-free)
    var(--g-c-price)
    var(--g-c-go);
  align-items: center;
  min-height: var(--g-row);
  border-bottom: 1px solid var(--rule-hair);
  color: var(--ink);
  text-decoration: none;
  transition: border-color 120ms ease, color 120ms ease;
}

/* WHAT IS BIGGEST: the tee time. Five independent mechanisms keep it the hero,
   so losing any one of them does not lose the sheet.
     1. SIZE — 20px against a 16px price and 15px facts. The only other 20px
        thing on the page is the day header, once per ~40 rows.
     2. INK — the time is --ink at 17.48:1 and every other fact is --dim at
        5.94:1, so the times are the darkest vertical stripe on the sheet and
        the eye finds them before it reads.
     3. ISOLATION — the time column holds nothing else, ever. Mark gutter to
        its left, flag channel to its right.
     4. COLON ALIGNMENT — see .g-tee__h below.
     5. DELETION — see the list above. */
.g-tee__time {
  grid-column: 2;
  justify-self: end;
  padding-right: var(--g-2);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.010em;
  color: var(--ink);
  white-space: nowrap;
}

/* THREE SPANS, NOT A U+2007 FIGURE SPACE. The figure space's width is a
   font-stack promise; it misaligns silently on the stacks that get it wrong,
   and this is the one column the entire design is built on. A fixed-width,
   right-aligned hour means 7:12a and 11:48a put their colon on the same x, so
   a finger runs down a true column of minutes — and because the span is
   right-aligned, even a font whose tabular digits are marginally wider than
   `ch` overflows leftwards into the 8px of padding and the colon does not
   move. The spans are also assertable in a template test, which a space is
   not.

   The minute and meridiem spans need no rule of their own — they simply follow
   the hour — but g-tee__m and g-tee__ap exist as names so a template test can
   assert all three are present. */
.g-tee__h,
.g-tee__time .t__h {   /* the row spec's own diagram writes the bare `t__h`
                          spelling; it resolves too, scoped inside .g-tee__time
                          so it can never reach the operator console, but
                          g-tee__h is canonical and the one to write */
  display: inline-block;
  width: 2ch;
  text-align: right;
}

/* The meridiem is fixed-width for the same reason the hour is, and it is not
   optional. The time block is right-aligned in its track, so its total width
   sets where the colon lands — and "a" and "p" are not the same width in a
   proportional UI font. Measured in Chrome at 390px before this rule: the
   colon sat at x=59.92 on a.m. rows and x=61.03 on p.m. rows, a 1.1px stagger
   down a column the entire sheet is built on. 1ch is the digit advance, which
   is wider than either letter in every stack in --g-ui. */
.g-tee__ap,
.g-tee__time .t__ap {
  display: inline-block;
  width: 1ch;
  text-align: left;
}

/* The flag channel: the only flexible track, so it absorbs every string-length
   difference and the numeric columns cannot move between row 1 and row 596.
   It is also the exception channel — "9 holes", "spots not posted",
   "residents only" — and those are --dim, because they are facts, not the one
   claim this product makes.

   IT WRAPS TO TWO LINES. IT DOES NOT ELLIPSIZE ON ONE. The previous rule was
   `white-space: nowrap` with an ellipsis, and the justification written here
   was that "a wrapped fact breaks the 60px pitch". That is arithmetically
   false and the cost of believing it was measured on 2026-08-06 at 320px over
   all 368 flag cells on /play: the track is 82px and 278 of them were clipped —
   270 reading "no price p…" (the string needs 107px) and 8 reading
   "residents …" (96px). The second is the damaging one. It is a booking-
   eligibility constraint, the row still reads "1 free  $34  ›", and the word
   that says who may take the slot is the word the ellipsis eats. At 360 the
   track is 96px and 270 were still clipped; 375 (111px) and up were clean, so
   the defect lived on exactly the two narrowest phones, identically in dark.

   Two lines of 15px at --lh-body is 45px, and the row's min-height is 60px on
   a phone and 56px on the desktop. So a second line costs ZERO pitch: the grid
   row was already taller than the text. Every string this channel ships fits
   inside two lines at 82px — the longest, "gone while you looked", is 152px and
   breaks after "while". `line-clamp: 2` keeps the ellipsis as the backstop for
   a string nobody has written yet, so this can never silently grow a row.

   Not fixed by shrinking type (the floor is 15px and this is a fact a golfer
   reads to decide) and not by taking pixels off the numeric tracks: at 320 the
   widest real strings are "12 free" at 48.9px and "$1,050" at 49.5px against
   56px and 52px of track, so the eight pixels available there do not reach 107
   and would put the price column one long green fee from clipping. */
.g-tee__flag {
  grid-column: 3;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow-wrap: break-word;
}

/* ALWAYS THE WORD. A bare "4" whose only label is a column head is exactly the
   failure this design was sent to prevent. */
.g-tee__free {
  grid-column: 4;
  justify-self: end;
  padding-right: var(--g-2);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  white-space: nowrap;
}

.g-tee__price {
  grid-column: 5;
  justify-self: end;
  padding-right: var(--g-2);
  font-size: var(--t-body);
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
}

.g-tee__go {
  grid-column: 6;
  justify-self: end;
  font-size: var(--t-fact);
  line-height: 1;
  color: var(--dim);
}

/* ---- the release mark ---------------------------------------------------
   3px x 24px of --live in a 12px gutter that is reserved on EVERY row whether
   or not it is used, so the marks stack into a vertical column down the left
   edge and the eye counts them in one pass without reading. A pill is an
   object you read; a rule in a margin is a position you scan. Identical at 320
   and at 1512 — it is the one element in the design that does not need to
   scale.

   THE MEASUREMENT THAT ASSIGNS IT: #23A566 is 3.16:1 on paper. It clears the
   3:1 floor for a non-text graphical object and fails 4.5:1 for text. That is
   precisely why it is a shape and #1B6B45 (6.49:1) is the letters.

   THE WORD IS PRIMARY AND THE MARK IS THE INDEX, not the other way round. A
   3px sliver at 3.16:1 decoded by a key is the first casualty of car-park
   glare and it demands the second look this product cannot afford.
   -------------------------------------------------------------------------- */
.g-tee__mark {
  grid-column: 1;
  justify-self: start;
  align-self: center;
  width: var(--g-mark-w);
  height: var(--g-mark-h);
  border-radius: var(--g-r-mark);
  background: none;
}

/* THE ONLY TWO GREEN DECLARATIONS ON A ROW, and both are gated on ONE class.

   `.g-tee--released` is set from exactly one test in the template —
   `s.slot_key in released`, where `released` comes from _cancellations(), which
   admits supply_class == 'genuine_release' and nothing else. The template never
   reads supply_class, gap_seconds, appear_count or an age, and the gate lives
   in SQL rather than in markup so it survives whoever edits the template next.

   Gating the colour on the row modifier rather than on the child element is a
   safety property, not a style: if somebody prints the word "cancelled" on a
   row that is not in `released`, it comes out --dim. A phantom cart rendered
   as a cancellation is the one failure that destroys the product's only claim,
   and 536 of the 1,622 comebacks in the corpus are phantom carts. */
.g-tee--released .g-tee__mark      { background: var(--live); }
.g-tee--released .g-tee__cancelled { color: var(--fairway); font-weight: 600; }

.g-tee__cancelled { color: var(--dim); }

/* ---- hover and focus ----------------------------------------------------
   NO BACKGROUND FILL, EVER, IN ANY STATE. --live measures 2.92:1 on --panel
   and 2.71:1 on --rule-hair, both under the 3:1 non-text floor, so a tinted
   row background silently breaks the one mark the product depends on. Hover
   darkens the facts from --dim to --ink (5.94 -> 17.48) and thickens the row's
   own hairline from --rule-hair to --rule. That is the whole interaction.
   -------------------------------------------------------------------------- */
.g-tee:hover {
  border-bottom-color: var(--rule);
  color: var(--ink);
}
.g-tee:hover .g-tee__flag,
.g-tee:hover .g-tee__free,
.g-tee:hover .g-tee__go { color: var(--ink); }

/* ---- taken between the read and the tap ---------------------------------
   Written in the old file and never once rendered. Not a link, --dim
   throughout, mark suppressed, and the word "gone" in --clay where the chevron
   was. "gone" measures 33–36px at 15px against an 18px chevron track, so it
   borrows the price track as well — the price on a row you cannot book is the
   least useful of the five facts, and every other column stays exactly where
   its neighbours put it.
   -------------------------------------------------------------------------- */
.g-tee--gone { color: var(--dim); }
.g-tee--gone .g-tee__time,
.g-tee--gone .g-tee__price,
.g-tee--gone .g-tee__free,
.g-tee--gone .g-tee__flag { color: var(--dim); }
.g-tee--gone .g-tee__time { text-decoration: line-through; text-decoration-color: var(--clay); }
.g-tee--gone .g-tee__mark { background: none; }
.g-tee--gone .g-tee__price { display: none; }

.g-tee--gone .g-tee__go {
  grid-column: 5 / -1;
  font-size: var(--t-fact);
  font-weight: 500;
  color: var(--clay);
  white-space: nowrap;
}

/* Some integrations return no deeplink. The row is a <div>, the chevron is
   omitted, and the flag channel says what to do instead. */
.g-tee--nolink { cursor: default; }
.g-tee--nolink .g-tee__go { display: none; }

/* ---- the collapse marker ------------------------------------------------
   _collapse_runs caps at 2 rows per course-hour and stands the rest behind
   "and 4 more…". None of the three design directions gave it a state, and it
   currently renders as a full-width wrapping sentence that breaks the fixed
   pitch. It is a RULED ROW INSIDE THE RHYTHM: same pitch, same hairline, mark
   gutter reserved and empty, label starting at the time column's x, and the
   chevron position carries a plus. Any first-screen row count that ignores
   this marker is wrong.

   It never wraps. Measured at 320: "and 4 more, 8:20a–8:50a" is 160px against
   the 268px the label gets, and the long form ("and 4 more at Pine Hills
   between 8:20 and 8:50, every 10 minutes") is ~430px, so the label ellipsizes
   and the server sends the short string on the narrow screen.
   -------------------------------------------------------------------------- */
.g-more {
  display: grid;
  grid-template-columns:
    var(--g-mark-gutter)
    minmax(0, 1fr)
    var(--g-c-go);
  align-items: center;
  width: 100%;
  min-height: var(--g-row);
  padding: 0;
  border: 0;
  border-bottom: 1px solid var(--rule-hair);
  background: none;
  font-family: inherit;
  text-align: left;
  color: var(--dim);
  cursor: pointer;
  list-style: none;
}
.g-more::-webkit-details-marker { display: none; }
.g-more::marker { content: ""; }

.g-more__label {
  grid-column: 2;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.g-more__go {
  grid-column: 3;
  justify-self: end;
  font-size: var(--t-fact);
  line-height: 1;
  color: var(--dim);
}

.g-more:hover { border-bottom-color: var(--rule); }
.g-more:hover .g-more__label,
.g-more:hover .g-more__go { color: var(--ink); }

/* ==========================================================================
   §13 — THE STICKY ACTION

   Sticky at the bottom, 56px, --ink fill. Placed in the markup AFTER the first
   day section, so it does not exist on the first screen — costing nothing above
   the fold — and permanently in thumb reach from the moment he scrolls. Pure
   CSS, no JS. Today the same action sits at 88.6% of a 5,130px document.

   THE MARKUP CONTRACT, AND WHY IT IS NOT OPTIONAL. `position: sticky; bottom:0`
   is clamped by the element's CONTAINING BLOCK, not by where it sits in the
   flow. Put .g-cta straight inside .g-sheet and the sheet's top edge is already
   above the fold, so the browser lifts the bar to the bottom of the viewport at
   scroll 0 — measured at 390x844: 77px of the first screen and two tee rows
   eaten, which is the opposite of what it was placed there to do.

   So the CTA and everything after it go inside .g-sheet__rest:

       <div class="g-sheet">
         <section class="g-sheet__day"> …first day… </section>
         <div class="g-sheet__rest">
           <div class="g-cta"> … </div>
           <section class="g-sheet__day"> …second day… </section>
         </div>
       </div>

   That wrapper's top edge IS the bar's flow position, so the bar cannot be
   lifted above it: invisible on the first screen, pinned to the bottom from the
   moment the first day scrolls, no script. Measured after the change: 7
   complete rows above the fold at 390x844, against a target of 7 and today's 3.

   The tray is opaque --paper with a hairline above it, because rows sliding
   under a floating pill is how a sheet stops reading as a sheet.
   ========================================================================== */

.g-sheet__rest { display: block; min-width: 0; }

.g-cta {
  position: sticky;
  bottom: 0;
  z-index: 20;
  padding: var(--g-2) 0 var(--g-3);
  background: var(--paper);
  border-top: 1px solid var(--rule-hair);
}

.g-cta__action {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--g-cta-h);
  padding: 0 var(--g-4);
  border: 0;
  border-radius: var(--g-r-press);
  background: var(--ink);
  color: var(--paper);
  font-family: inherit;
  font-size: var(--t-body);
  font-weight: 500;
  text-decoration: none;
  cursor: pointer;
}

/* At the watch limit. The reason is a visible 15px line, not a title=
   attribute: no touch device displays those, and the whole at-limit
   explanation lives in one today. */
.g-cta__why {
  display: block;
  margin: var(--g-2) 0 0;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  text-align: center;
}
.g-cta--at-limit .g-cta__why { color: var(--clay); }

/* ON DESKTOP IT STAYS WHERE IT IS IN THE MARKUP, and that is a deliberate
   trade. The desktop drawing puts this button in the rail, but the rail and the
   sheet are different DOM positions, and the only ways to have it in both are
   to render the button twice — one button, two truth sources, and the copy on
   one of them goes stale — or to move it with script, which this page does not
   have. So it stays a sticky bar under the sheet.

   Two grid corrections are needed for that, and both were found by rendering:
   .g-sheet__rest is display:contents at >=1024, so the bar becomes a grid item
   of the two-column sheet. Left alone it took the top of column 2 and pushed
   Sunday underneath it. `order: 1` places it after every day section in the
   auto-placement pass, and spanning both columns keeps the days side by side.
   The action itself is capped at one column so it reads as a button rather
   than an 880px slab. */
@media (min-width: 1024px) {
  /* The wrapper stops generating a box, so the days inside it go back to being
     grid items of .g-sheet and keep flowing two-up. This override lives here,
     next to the base rule, because when it sat in §6 the later `display: block`
     in this section tied on specificity, won on source order, and trapped every
     day after the first inside one column — found by rendering, not by
     reading. */
  .g-sheet__rest { display: contents; }

  .g-sheet .g-cta,
  .g-sheet__rest .g-cta {
    order: 1;
    grid-column: 1 / -1;
    padding-top: var(--g-4);
  }
  .g-cta__action { width: auto; min-width: 280px; max-width: var(--g-col); }

  /* Unless a template does put it in the rail, in which case the rail is
     already sticky and a second sticky layer on one screen is a dashboard. */
  .g-rail .g-cta {
    position: static;
    padding: 0;
    background: none;
    border-top: 0;
  }
  .g-rail .g-cta__action { min-height: var(--g-tap); width: 100%; max-width: none; }
}

/* ==========================================================================
   §14 — EVERY STATE HAS A DESIGN

   Loading, empty, error, no-booking-link, taken-between-read-and-tap,
   too-many-results, at-limit and the collapse marker. A 123,169px document
   with no cap is not an edge case; it is what a 300-minute search returns.
   ========================================================================== */

/* An empty state says what the filter actually was, and carries the single
   widest lever out of it. "Nothing yet" on its own is not shippable. */
.g-empty,
.g-error {
  border: 1px solid var(--rule);
  border-radius: var(--g-r-card);
  background: var(--panel);
  padding: var(--g-6) var(--g-4);
  text-align: center;
}

.g-empty__h,
.g-error__h {
  margin: 0 0 var(--g-2);
  font-family: var(--g-display);
  font-size: var(--t-lg);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  color: var(--ink);
}

.g-empty__p,
.g-error__p {
  margin: 0 auto var(--g-4);
  max-width: 34em;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

.g-empty__do,
.g-error__do {
  display: flex;
  flex-wrap: wrap;
  gap: var(--g-2);
  justify-content: center;
}
.g-empty__do > .g-btn,
.g-error__do > .g-btn { flex: 1 1 100%; }
.g-empty__do > .g-btn--plain,
.g-error__do > .g-btn--plain { flex: 0 0 auto; }

@media (min-width: 480px) {
  .g-empty__do > .g-btn,
  .g-error__do > .g-btn { flex: 0 0 auto; }
}

/* Inside a card or under a run: same rule, less air, left aligned. */
.g-empty--inline {
  border: 0;
  background: none;
  padding: var(--g-4) 0;
  text-align: left;
}
.g-empty--inline .g-empty__h { font-size: var(--t-lead); }
.g-empty--inline .g-empty__p { margin-left: 0; }
.g-empty--inline .g-empty__do { justify-content: flex-start; }

/* Something is wrong rather than merely absent. A designed page, never a stack
   trace and never raw JSON: `?address=Sag+Harbor%2C+NY` 500s today while the
   identical `?address=Sag+Harbor,+NY` returns 200, and a typo'd max_price
   renders {"detail":[...]} into the browser window. */
.g-error,
.g-empty--bad { border-color: var(--clay); background: var(--paper); }
.g-error__h,
.g-empty--bad .g-empty__h { color: var(--clay); }

/* TOO MANY RESULTS. `capped` is set in app.py and has never been rendered; a
   300-minute search returns 640 rows and a 123,169px document. This is not a
   full-page state — it is the last thing in the sheet, saying how much you are
   looking at and offering the two levers that shorten it. */
.g-capped {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--g-2) var(--g-3);
  padding: var(--g-4) var(--g-3);
  border-bottom: 1px solid var(--rule);
  background: var(--panel);
  border-radius: 0 0 var(--g-r-press) var(--g-r-press);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}
.g-capped .g-num { color: var(--ink); font-weight: 500; }

/* On desktop it belongs under the day columns, not under the rail: it is a
   statement about the sheet. As a direct child of the page grid it would
   otherwise auto-place into column 1, which is where the rail lives. */
@media (min-width: 1024px) {
  .g-sheetpage > .g-capped { grid-column: 2; }
  .g-sheet > .g-capped { grid-column: 1 / -1; order: 2; }
}

/* An inset advisory. The left edge is --ink: it was --rule on --panel, which
   measures 1.18:1 — an edge nobody could see. */
.g-note {
  border: 1px solid var(--rule);
  border-left-width: 3px;
  border-left-color: var(--ink);
  border-radius: var(--g-r-press);
  background: var(--panel);
  padding: var(--g-3) var(--g-4);
  margin: 0 0 var(--g-4);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--ink);
}
.g-note:last-child { margin-bottom: 0; }
.g-note__b { font-weight: 600; }

/* It worked. Deliberately NOT green and deliberately not a new colour: the
   sentence carries the good news, and this product's one saturated colour is
   spent on tee times. */
.g-note--ok { border-left-color: var(--ink); }

/* It did not. Say what to do about it in the same sentence. */
.g-note--bad {
  border-left-color: var(--clay);
  background: var(--paper);
}
.g-note--bad .g-note__b { color: var(--clay); }

/* Neither — a standing fact. */
.g-note--plain { border-left-color: var(--rule); }

/* A one-line note tucked under something, no box. */
.g-inline-note {
  display: block;
  margin: var(--g-2) 0 0;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}
.g-inline-note--bad { color: var(--clay); }
.g-inline-note--ok  { color: var(--ink); }

/* LOADING. htmx swaps the course picker in place; while a request is out the
   old content dims and stops taking taps, so a second tap cannot fire a second
   request. No spinner and no skeleton rows: a skeleton row on a sheet whose
   whole job is "is there a tee time here" is a lie for 120ms. */
.g-app .htmx-indicator { display: none; }
.g-app .htmx-request .htmx-indicator,
.g-app .htmx-request.htmx-indicator { display: inline-flex; }

/* .g-loading is the name; .g-swappable is the same thing under the name
   watch_edit.html already uses. Delete the alias once that template is
   rewritten — two names for one behaviour is how a component quietly forks. */
.g-app .g-loading.htmx-request,
.g-app .htmx-request.g-swappable {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 120ms ease;
}

.g-app [aria-busy="true"] { cursor: progress; }

/* ==========================================================================
   §15 — CARDS AND WATCH SUMMARIES

   Scoped to /texts, /watch_edit and /watches. NEVER on the sheet: a sheet is
   ruled, not carded, and 14px of radius around a tee row is how a list becomes
   a stack of boxes.
   ========================================================================== */

.g-card {
  border: 1px solid var(--rule);
  border-radius: var(--g-r-card);
  background: var(--paper);
  padding: var(--g-4);
  min-width: 0;
}

@media (min-width: 640px) {
  .g-card { padding: var(--g-5); }
}

.g-card--panel,
.g-card--quiet { background: var(--panel); }
.g-card--flush { padding: 0; overflow: hidden; }

.g-card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--g-2) var(--g-3);
  margin: 0 0 var(--g-3);
  min-width: 0;
}
.g-card--flush .g-card__head { padding: var(--g-4) var(--g-4) 0; margin-bottom: var(--g-3); }

.g-card__title {
  margin: 0;
  font-family: var(--g-display);
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  min-width: 0;
}

.g-card__aside {
  margin-left: auto;
  font-size: var(--t-fact);
  color: var(--dim);
  white-space: nowrap;
}

.g-card__note {
  margin: 0 0 var(--g-3);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

.g-card__body { min-width: 0; }
.g-card--flush .g-card__body { padding: 0 var(--g-4); }

.g-card__foot {
  margin: var(--g-4) 0 0;
  padding: var(--g-3) 0 0;
  border-top: 1px solid var(--rule);
  font-size: var(--t-fact);
  color: var(--dim);
}
.g-card--flush .g-card__foot { padding: var(--g-3) var(--g-4) var(--g-4); margin-top: var(--g-3); }

/* One standing question: where, when, how many. Reads as a sentence first and
   a set of fields second. */
.g-watch {
  border: 1px solid var(--rule);
  border-radius: var(--g-r-card);
  background: var(--paper);
  padding: var(--g-4);
  min-width: 0;
}

@media (min-width: 640px) {
  .g-watch { padding: var(--g-5); }
}

.g-watch--off { background: var(--panel); }
.g-watch--off .g-watch__sentence { color: var(--dim); }

.g-watch__top {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--g-2) var(--g-3);
  margin: 0 0 var(--g-2);
  min-width: 0;
}

.g-watch__name {
  margin: 0;
  font-family: var(--g-display);
  font-size: var(--t-lead);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: -0.022em;
  min-width: 0;
}

.g-watch__sentence {
  margin: 0 0 var(--g-3);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.g-watch__courses {
  display: flex;
  flex-wrap: wrap;
  gap: var(--g-1);
  margin: 0 0 var(--g-4);
}

/* A chip is a static label, not a control, so it gets the system's smallest
   radius rather than a pill — a pill reads as something you can press. */
.g-chip {
  display: inline-flex;
  align-items: center;
  min-height: var(--g-5);
  padding: 0 var(--g-2);
  border: 1px solid var(--rule);
  border-radius: var(--g-r-press);
  background: var(--panel);
  font-size: var(--t-fact);
  color: var(--ink);
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.g-chip--more { background: none; color: var(--dim); }

/* On / off. A word, never a colour on its own. */
.g-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--g-1);
  min-height: var(--g-5);
  padding: 0 var(--g-2);
  border-radius: var(--g-r-press);
  border: 1px solid var(--rule);
  background: var(--panel);
  font-size: var(--t-fact);
  font-weight: 600;
  color: var(--dim);
  white-space: nowrap;
  margin-left: auto;
}
.g-pill--on  { border-color: var(--ink); color: var(--ink); background: var(--paper); }
.g-pill--off { color: var(--dim); }

/* Numbers, each with the words that make it mean something. */
.g-watch__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: var(--g-3);
  padding: var(--g-3) 0;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  margin: 0 0 var(--g-4);
}

.g-stat { min-width: 0; }

.g-stat__n {
  display: block;
  font-size: var(--t-lg);
  font-weight: 600;
  letter-spacing: -0.022em;
  line-height: var(--lh-tight);
  color: var(--ink);
}

.g-stat__k {
  display: block;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

/* ---- the compressed watch row, /texts only ------------------------------
   THE CARD BECAME A ROW. Measured in Chromium against Miles's own account on
   2026-08-07, top of the card to the bottom of its border:

                    before      after
     320 x 568      246px       176px
     390 x 844      218px        98px
     1512 (rail)    218px       126px

   Eight watches at 218–246px put the first tee time the product had actually
   sent him at y=2382 on a 390px phone. The page was inverted correctly — the
   watches DO belong above the ledger — and it still opened on nineteen hundred
   pixels of controls, so a golfer scrolled past his own settings to reach the
   thing they produced.

   Nothing was dropped. The name, the state, the days, the hours, the group
   size, the course count, the running total, the last send and both controls
   are all still on the row; they occupy three grid rows instead of six blocks.
   From 360px up the two buttons share the first row with the name — measured at
   390: "Edit" 58px + 8 + "Pause" 68px = 134px, against 334px of inner width, so
   the name keeps 116px and the state pill 84. Below 360 the buttons take a row
   of their own rather than squeezing a name to nothing.

   IT IS A MODIFIER, AND THAT IS THE POINT. `.g-watch` is a generic card in four
   other places — the two plan cards on /pricing, the number card on
   /alerts/phone, the three cards on /watches/full and the summary on
   /watches/{id}/edit — and every one of them wants the roomy block. Compressing
   the base class would have silently squashed all four. */
.g-watch--row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "top" "lines" "foot";
  align-items: center;
  gap: var(--g-2) var(--g-3);
  padding: var(--g-3);
}

/* THE FACTS SPAN BOTH COLUMNS AND THE BUTTONS SIT BESIDE THE NAME. Measured
   first with the buttons spanning both rows instead: two .g-btn at `padding: 0
   var(--g-5)` come to 182px, which left the fact lines 138px of a 390px phone
   and wrapped them onto FIVE lines — a 145px row, worse than the three-row
   stack it replaced. The buttons only need the name's row; the facts want the
   whole width, and one line of facts is worth more than a column of air. */
@media (min-width: 360px) {
  .g-watch--row {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "top   foot"
                         "lines lines";
    row-gap: var(--g-1);
  }
}

/* A 640px card and a 320px navy rail are the same component here, so the
   padding follows the space rather than the breakpoint. */
@media (min-width: 640px) {
  .g-watch--row { padding: var(--g-3) var(--g-4); }
}

.g-watch--row .g-watch__top   { grid-area: top; margin: 0; flex-wrap: nowrap; }
.g-watch--row .g-watch__lines { grid-area: lines; min-width: 0; }

/* Two words, not two paragraphs: --g-5 of side padding is right for the one
   real action on a screen and wrong for a pair of list-row controls repeated
   eight times. At --g-3 the pair measures 134px against 182, and the 48px goes
   to the name — which is the only thing in the row that can be truncated. The
   44px tap floor is untouched; only the horizontal padding moves. */
.g-watch--row .g-watch__foot  { grid-area: foot; flex-wrap: nowrap; }
.g-watch--row .g-watch__foot .g-btn { padding: 0 var(--g-3); }

/* One line, ellipsised. A title that wraps takes the state pill down with it
   and the row grows 26px for a place name the golfer typed and recognises from
   its first characters. --t-lead is the card title size and stays. */
.g-watch--row .g-watch__name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* WHAT IT IS LOOKING FOR. --ink, because it is the thing he might change; the
   line under it is --dim, because it is the evidence he should not. Tabular, so
   "7–11am" and "11am–2pm" do not shuffle down a stack of eight. */
.g-watch__line {
  margin: 0;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* THE SAME THREE FACTS AS .g-watch__stats, ON ONE 22px LINE INSTEAD OF THREE
   34px BLOCKS. /texts opens on the watch cards now, so the card is the first
   thing on the page and every pixel in it is spent before a golfer sees a tee
   time. Measured on the seeded card at 390: the stats grid was 106px of the
   card's 300px, and the largest type in it was a "6" that nobody reads in 20px.

   The separator rides on the END of the preceding fact in the markup rather
   than on a `+ ::before`, because on a card that wraps at 320 an ::before "·"
   lands at the start of the second line, hanging off the left edge with nothing
   in front of it. Same reason play.html's run header does it that way. */
.g-watch__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--g-2);
  margin: 0 0 var(--g-4);
  padding: var(--g-2) 0 0;
  border-top: 1px solid var(--rule);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

.g-watch__fact { white-space: nowrap; }
.g-watch__facts .g-num { color: var(--ink); font-weight: 500; }

/* Inside the row the rule and the 16px gap are gone: the two fact lines are one
   block, and a hairline between "Weekends · 7–11am" and "6 courses · 2 found"
   divides a single thought into two. The gap between cards does the separating,
   and the card's own border does the containing. */
.g-watch--row .g-watch__facts {
  margin: 0;
  padding: 0;
  border-top: 0;
}

.g-watch__foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--g-2);
  align-items: center;
}
.g-watch__foot > .g-btn        { flex: 1 1 100%; }
.g-watch__foot > .g-btn--plain { flex: 0 0 auto; }

@media (min-width: 480px) {
  .g-watch__foot > .g-btn { flex: 0 0 auto; }
}

/* TWO SHORT LABELS, SIDE BY SIDE, AT EVERY WIDTH. They were full-width and
   stacked below 480, which is 52px of a 298px card spent on making "Change it"
   as wide as the screen — and /texts opens on these cards now, so every pixel
   in one is spent before a golfer reaches a tee time. Measured at 390:
   "Change it" is 92px and "Switch it off" 116px against 358px of card, so they
   sit on one line down to 320 with room over. They still WRAP rather than
   shrink, because a control that has been squeezed under its label is worse
   than a control on the next line. */
.g-watch__foot > .g-btn { flex: 0 0 auto; }

/* THE WAY TO ADD ONE, under the list. Two children at most: the primary action
   and — only at the limit, and only when there is something to buy — the second
   way out. Both are real controls; a disabled button explains nothing. */
.g-watchnew {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--g-2);
  margin: var(--g-4) 0 0;
}
.g-watchnew > .g-btn { flex: 1 1 100%; }

@media (min-width: 480px) {
  .g-watchnew > .g-btn { flex: 0 0 auto; }
}

/* The caption on that control, and NOT a --panel note above the cards. On the
   free tier the limit is one, so a standing box at the top of the section is a
   permanent notice about a limit nobody just hit, sitting where the golfer
   looks for the thing he came for. Same words, one third the pixels, attached
   to the button whose shape it explains. */
.g-watchnew__why {
  margin: var(--g-2) 0 0;
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  max-width: 42em;
}
.g-watchnew__why .g-note__b { color: var(--ink); }

/* ==========================================================================
   §15b — THE FOLD

   Two things on /texts are longer than anybody wants on first sight: the tail
   of the ledger, and every tee time we found and stayed quiet about. Both
   collapse behind ONE component, and its summary is `.g-more` — the ruled row
   the tee sheet already uses for "and 4 more", on the same 60px pitch with the
   same hairline and the same mark gutter reserved and empty. A fold that does
   not sit on the sheet's rhythm reads as a different page.

   Native <details>. Keyboard-operable, screen-reader-operable, scriptless, and
   it survives with JS off — which matters here because the held-back rows are
   the product's evidence for its own honesty and must not depend on a bundle.
   ========================================================================== */

.g-fold { display: block; }

.g-fold__body { padding: var(--g-2) 0 0; }

.g-fold__p {
  margin: 0 0 var(--g-3);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
  max-width: 42em;
}
.g-fold__p .g-num { color: var(--ink); font-weight: 500; }

/* The quiet fold is the LAST thing on the page and it opens onto rows that are
   all, by definition, tee times nobody got.

   NO RULE ABOVE IT. It had one, and it put three horizontal lines inside 60px —
   its own, the summary's hairline, and the footer's — under a paragraph that
   had already ended. The section gap does the separating; the count carries the
   weight, because "15" is the whole reason to press it and --dim on a --dim
   label makes it the least visible thing in its own row. */
.g-fold--quiet .g-more__label .g-num { color: var(--ink); font-weight: 600; }

/* ==========================================================================
   §15c — THE FIRST-RUN ASK

   _first_run.html is the whole page while an account is empty, and it is the
   first screen every new golfer sees after signing in. It asks three questions
   — where, which days, what time — and posts them to the route golfers already
   create watches through from the tee sheet.

   IT CARRIES ITS OWN VERTICAL RHYTHM, and that is a workaround with a reason.
   §9 declares `.g-fieldset { margin: 0 }` at the same specificity as §5's
   `.g-fieldset { margin: 0 0 var(--g-6) }` and later in the file, so every
   fieldset in the product currently renders with no gap under it — a legend
   sits hard against the hint of the field above it. That rule belongs scoped to
   the picker it was written for; until it is, this owns-block gives the ask its
   spacing without touching it. Same reason /texts is not allowed a <style>: one
   component, one file, and the fix lands in one place when it lands.
   ========================================================================== */

.g-ask > * + * { margin-top: var(--g-6); }

/* /texts IS ONE READING COLUMN ON A PHONE, CAPPED AT THE SHEET'S OWN WIDTH.
   Measured at 1280 before this cap: .g-shell is 1064, .g-sheet keeps its 640,
   and every section head above it ran the full 1064 — the ledger's count sat
   four hundred pixels from the column it counts, and a card holding one
   sentence was a metre wide. Same 640 as §6's cap, deliberately: nothing on the
   page may be wider than the rows it describes. Below 1024 --g-shell is 560 and
   this cannot bite. */
.g-texts { max-width: 640px; }

/* ---- /texts at 1024: TWO PANES, on the same rule /play splits on -----------
   The cap above WAS the desktop layout, which made this page the phone widened
   with 870px of empty margin beside it at 1512 — the one thing §4b of the
   locked decisions says a desktop must not be.

   THE SPLIT IS THE PRODUCT'S EXISTING RULE, NOT A NEW ONE: the things you press
   are navy chrome, the things you read are paper. On /play that is the controls
   against the sheet. On /texts the watches ARE the controls — they are the only
   thing on the page a golfer can change — and the ledger is the reading.

   `.g-rail` is REUSED, not reimplemented. The markup carries both classes, so
   the navy tokens, the sticky offset, the internal scroll and the dark-mode
   inset all come from §6 and there is exactly one navy pane in this product.
   `.g-texts__side` adds nothing but its grid area; if it ever needs a rule of
   its own, that is the moment to ask whether the two panes have really diverged.

   Below 1024 none of this exists: `.g-rail` is `display: block`, the wrapper is
   not a grid, and the phone gets the single column it had, in the same order. */
@media (min-width: 1024px) {
  /* --g-shell is already 1000px here (§6), which is rail 320 + gutter 40 + a
     640px reading column — the same three numbers, so the two golfer pages line
     up down to the pixel when you tab between them. */
  .g-texts { max-width: none; }

  /* THE SAME THREE NUMBERS AS /play, deliberately: --g-shell is 1000 from 1024
     up, which is rail 320 + gutter 40 + a 640 reading column, so the two golfer
     pages line up down to the pixel when you tab between them. */
  .g-texts__panes {
    display: grid;
    grid-template-columns: var(--g-rail) minmax(0, 1fr);
    column-gap: var(--g-gutter);
    align-items: start;
  }

  /* IN A 320px RAIL THE BUTTONS GO BACK TO THEIR OWN ROW. Measured: the rail's
     card has 264px of inner width, the button pair takes 134 of it, and "Sag
     Harbor, NY" came out as "Sag Harbor, …" with both Montauk watches reading
     "Montauk W…" — a golfer with two similar watches could not tell them apart
     in the pane whose job is telling them apart. Vertical room is the one thing
     this pane has (the ledger beside it is twice as tall), so it spends 26px a
     card to buy the names back. Same markup, one grid-area swap. */
  .g-texts__side .g-watch--row {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas: "top" "lines" "foot";
    row-gap: var(--g-2);
  }

  /* NOT STICKY, AND NOT ITS OWN SCROLLER, which is where this differs from
     /play's rail and the reason is measured: eight watch rows are ~1100px and
     the pane inherits `max-height: calc(100vh - …)`, so on a 950px screen the
     eighth card was cut through the middle by the pane's own bottom edge. On
     /play the thing that gives way inside the rail is the course index, which
     is an index into rows that are all on the page anyway; here it is a control
     the golfer owns. So the pane flows with the document and every watch is
     reachable with the scroll he is already using. */
  .g-texts__side {
    position: static;
    max-height: none;
    overflow: visible;
  }

  /* Its own section rhythm inside the navy pane. .g-section's 48px bottom
     margin is measured for a full-width reading column; inside a card it is
     dead space under the last control. */
  .g-texts__side > .g-section:last-child { margin-bottom: 0; }

  /* The reading column keeps the sheet's own width, exactly as .g-sheet does on
     /play. `1fr` already resolves to 640 at --g-shell: 1000, so this is a floor
     against the shell ever growing rather than a change of layout. */
  .g-texts__main { max-width: 640px; }
}
.g-ask .g-form__actions + .g-hint { margin-top: var(--g-3); }

/* The dead-cart line as a BLOCK in prose rather than as a row in the rail's
   filter stack. On /play it is one of four things in a stack that supplies its
   own gaps; on /texts and on the first run it stands alone between a heading
   and a sheet, and with no margin of its own it reads as part of whichever of
   the two it happens to touch. */
.g-carts--block { margin: var(--g-4) 0; }

/* ==========================================================================
   §16 — FOOTER
   ========================================================================== */

.g-foot {
  border-top: 1px solid var(--rule);
  padding: var(--g-5) 0 var(--g-7);
  font-size: var(--t-fact);
  line-height: var(--lh-body);
  color: var(--dim);
}

.g-foot__line { display: block; }

/* The cadence sentence renders _watched_cadence(), which app.py already
   computes and already injects on every render. The five hardcoded "every 60
   seconds" strings come out: verified against poll_cells on 2026-08-06, 60s is
   true of 15 integrations of 41 — the rest run at 81s, 240s or 1800s. */
.g-foot .g-num { color: var(--ink); }

/* Links at a real tap target — they rendered at 16px tall. Underlined rather
   than coloured: colour alone is not a signal everyone can see, and the colour
   this page has spare is not green. */
.g-foot__link {
  display: inline-flex;
  align-items: center;
  min-height: var(--g-tap);
  color: var(--dim);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--rule);
}
.g-foot__link:hover,
.g-foot__link:focus-visible { color: var(--ink); text-decoration-color: currentColor; }

/* ==========================================================================
   §17 — LAST-RESORT GUARDS
   ========================================================================== */

/* Anything that genuinely cannot shrink gets its own scroller rather than
   pushing the page sideways. Prefer not to need this: nothing in the sheet
   does. */
.g-scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}

/* ==========================================================================
   §18 — MOTION

   THE ONLY MOVING THINGS IN THIS PRODUCT, and they are both the same thing:
   the cancellation signature. A tee time somebody gave back is the whole
   claim; the sweep is the page saying so once, at the moment the row appears.
   Nothing else on any golfer screen animates — not a chip, not a button, not a
   day cell, not a page transition. When everything moves, movement means
   nothing, exactly as with the green.

   ONE keyframe:
     g-sweep  a green wash crossing a cancelled row, once, on arrival

   THE COUNTER NO LONGER TICKS, AND DELETING IT IS THE DECISION RATHER THAN AN
   OVERSIGHT. §4c asks for the fakes-dropped counter to move "when the number
   changes". This page carries no client state to change AGAINST: it is
   server-rendered with no script (tests/test_no_cdn.py — /play and /texts ship
   no script file at all, and the only inline script in the shell is the
   nine-line account-menu closer), so a CSS animation on the number fires once
   per document paint and cannot know whether anything happened. On the live
   corpus at 07:00 that meant animating a "0" into place on every single page
   load. An animation that plays when nothing has changed is not a signature —
   it is the opposite of one, because it spends the product's only moving pixel
   on a non-event and teaches a golfer to ignore it. So the number is set
   without motion until there is something to diff it against.

   WHERE THAT WORK BELONGS, if it is ever wanted: a live-updating sheet, where
   the previous value exists on the client and the animation can be applied by
   the code that replaces the number. `g-tick` is deleted rather than kept
   unused — dead CSS reads as a feature somebody switched off by accident.

   THE SWEEP STAYS, and the distinction is not a loophole. A cancelled row is
   not a number that might not have moved: it is a tee time somebody gave back,
   and its arrival on the sheet IS the event. Every render that draws one is a
   render where the thing being marked is true and present.

   It is `animation`, never `transition`, and one-shot: it cannot fire on
   hover, on focus, or on scroll, so nothing here can pulse at a golfer
   standing in a car park.
   ========================================================================== */

/* THE SWEEP'S COLOUR IS DECLARED ON THE ROW, NOT INSIDE THE KEYFRAME, and that
   is about the audit rather than about CSS. ops/contrast.py walks every block
   outside :root looking for --live and --fairway and fails on any it does not
   have a budget line for; it skips @-blocks, so a green written inside
   @keyframes would spend the signature colour invisibly. Declared here, the
   spend is on `.g-tee--released`, the ledger sees it, and it has a budget line
   beside the mark and the word it belongs with.

   `color-mix` is not universally supported; where it is not, the custom
   property is invalid, the keyframe animates from nothing and the row simply
   appears. That is the correct way for this to fail. */
.g-tee--released { --g-sweep: color-mix(in srgb, var(--live) 14%, transparent); }

@keyframes g-sweep {
  /* A wash, not a slide: the row does not move, so a finger already on it does
     not lose its target. Peaks at 14% of --live and is gone by 900ms. */
  0%   { background-color: var(--g-sweep); }
  100% { background-color: transparent; }
}

.g-tee--released { animation: g-sweep 900ms ease-out 1; }

/* DISABLED, NOT REDUCED. `prefers-reduced-motion: reduce` is a statement that
   motion causes this person a problem, and a shorter version of the problem is
   still the problem. `animation: none` on the one rule above, and the
   universal belt-and-braces sweep for anything added later that forgets. */
@media (prefers-reduced-motion: reduce) {
  .g-tee--released { animation: none; }

  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
