/* ============================================================================
   FieldSync — native officials app override layer ("app-shell.css")

   EVERY rule in this file is scoped under [data-fsapp="1"], which only
   app-shell.js sets, and only when the page is running inside the Android
   officials app. On the public website and in the Electron desktop app the
   attribute never appears, so this stylesheet is completely inert there.

   That scoping is the whole safety contract: the officials app gets a tuned
   interface without forking run.html / index.html, and without any risk of
   changing how the site looks for everyone else. Keep it that way — never add
   an unscoped selector to this file.

   Scope of changes here is deliberately SIZING AND NOISE ONLY. No layout
   restructuring, no repositioning, nothing that could alter which controls
   exist or what they do. Officials are running a live competition on this
   screen; the safest change is the boring one.
   ========================================================================== */

/* ── Reading distance ──────────────────────────────────────────────────────
   The tablet sits on a table or in one hand at arm's length, outdoors. Lift
   the base size so every relative unit downstream scales with it. */
[data-fsapp="1"] body {
  font-size: 17px;
  -webkit-tap-highlight-color: transparent;
}

/* Keep content out from under the system bars.
   Android 15+ enforces edge-to-edge for targetSdk 35, so the WebView is laid
   out BEHIND the status and navigation bars. Without an explicit top inset the
   page header renders underneath the clock — which instantly reads as a web
   page someone opened rather than an app. */
[data-fsapp="1"] .wrap {
  padding-top: max(16px, calc(env(safe-area-inset-top) + 12px));
  padding-left: max(12px, env(safe-area-inset-left));
  padding-right: max(12px, env(safe-area-inset-right));
  padding-bottom: max(12px, env(safe-area-inset-bottom));
}

/* The topbar logo is sized for a desktop header (95px tall, pulled up 33px by
   a negative margin). At tablet width that lifts it under the status bar and
   collides with the page title. Give it ordinary flow inside the app. */
[data-fsapp="1"] .topbarLogoImg {
  height: 46px;
  margin: 0;
}
[data-fsapp="1"] .topbar h1 {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* The AI assistant bubble floats over the scoring UI. It belongs on the
   website, not on a tool being used to record results at a live meet. */
[data-fsapp="1"] .aiChatFab,
[data-fsapp="1"] .aiChatPanel { display: none !important; }

/* ── Fit the screen: the page must never pan ───────────────────────────────
   The single biggest "this is a website, not an app" tell. .tableWrap in
   styles.css sets only a margin — no overflow — so a table wider than the
   viewport (Entries/Events, or the 10-column Easy Mode sheet at ~1400px)
   overflows its card and drags the whole document sideways. The user then
   swipes the PAGE to reach the rest of the UI, top bar and all.

   Native apps don't do that: the shell stays put and only the wide thing
   scrolls. So lock the document's horizontal axis and give every container
   that can legitimately exceed the viewport its own scroller. Nothing becomes
   unreachable — the scrolling just moves to where it belongs. */
/* `clip`, not `hidden`. Both stop the sideways pan, but `overflow-x:hidden`
   makes the element a scroll container, and that silently breaks
   `position:sticky` for everything inside it — including the Run page's bottom
   navigation, whose whole job is to stay put while a long roster scrolls. */
[data-fsapp="1"],
[data-fsapp="1"] body {
  overflow-x: clip;
  max-width: 100%;
}

[data-fsapp="1"] .wrap {
  max-width: 100%;
  width: 100%;
}

/* Wide tables scroll inside their own card, with momentum. */
[data-fsapp="1"] .tableWrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  max-width: 100%;
}

/* Cards and modals clip rather than push the layout out. */
[data-fsapp="1"] .card,
[data-fsapp="1"] .modalCard {
  max-width: 100%;
}

/* The sub-navigation becomes a swipeable tab strip rather than wrapping into
   an unpredictable number of rows — the tab positions stay stable, which
   matters when someone is reaching for the same tab all afternoon. */
[data-fsapp="1"] .subtabs {
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
[data-fsapp="1"] .subtabs::-webkit-scrollbar { display: none; }
[data-fsapp="1"] .subtab { flex: 0 0 auto; }

/* The top bar carries a lot of controls; let it stack instead of forcing the
   document wider than the screen. */
[data-fsapp="1"] .topbar { flex-wrap: wrap; }
[data-fsapp="1"] .topbar .right { flex-wrap: wrap; justify-content: flex-end; }

/* Long meet names, emails and share codes break instead of stretching a row. */
[data-fsapp="1"] .cardTitle,
[data-fsapp="1"] .tbl td { overflow-wrap: anywhere; }

/* ...but not a person's name. "anywhere" breaks mid-word, so a squeezed column
   produced "Ironwoo / d Track Club". break-word only breaks a word that cannot fit
   on a line of its own, which is what a name column wants. This has to live here
   rather than in styles.css: app-shell.css loads last, so the same specificity
   from styles.css would lose to the rule above. */
[data-fsapp="1"] .tbl td.easyAthleteCell,
[data-fsapp="1"] .tbl td.easyAthleteCell * { overflow-wrap: break-word; }

/* ── Bottom navigation ─────────────────────────────────────────────────────
   On the check-in screen the roster can run to dozens of athletes, and the two
   controls that move an official forward ("Back to Events" / "Event Scoring")
   sit after it. Buried at the end of a long list they may as well not exist —
   so the bar stays pinned to the bottom of the viewport no matter how long the
   list is. Because it is always in front of content, it needs an opaque
   backdrop and clearance from the gesture bar. */
/* FIXED, not sticky.
 *
 * Sticky only pins an element while it would otherwise scroll out of view, so
 * it needs content taller than the viewport to do anything. On a short screen —
 * the events list, or the Total Station setup page — the bar simply sat wherever
 * the content ended, floating in the middle of a tall tablet. The check-in
 * roster was usually long enough to hide that.
 *
 * Fixed pins it to the viewport regardless of how much content there is, which
 * is what "always at the bottom" actually requires. */
[data-fsapp="1"] .bottomNav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 800;
  margin-top: 0;
  padding: 10px 22px max(10px, env(safe-area-inset-bottom));
  background: linear-gradient(to top, var(--bg) 78%, transparent);
}
[data-fsapp="1"] .bottomNavInner {
  max-width: 1200px;          /* line up with .wrap rather than spanning the glass */
  margin: 0 auto;
  box-shadow: 0 -6px 20px rgba(0, 0, 0, .35);
}
/* Taken out of flow, the bar no longer reserves its own space, so the last row
   of a long roster would sit underneath it and be unreachable. Reserve the
   clearance on the page instead. */
[data-fsapp="1"] body[data-page="run"] .wrap {
  padding-bottom: calc(104px + env(safe-area-inset-bottom));
}
/* These two decide where the official goes next — make them thumb-sized. */
[data-fsapp="1"] .bottomNavInner .btn {
  flex: 1 1 0;
  min-height: 60px;
  font-size: 17px;
}

/* ── Strip the desktop chrome ──────────────────────────────────────────────
   What's left in the top bar has to earn its place for an official mid-meet:
   which meet they're in, and who's signed in. Everything else here is either a
   debugging aid, a duplicate, or something the app bar above now handles. */

/* Debug-era status readouts. */
[data-fsapp="1"] #serverPill,
[data-fsapp="1"] #syncPill,
/* The page auto-refreshes on a 2s poll plus SSE; a manual Refresh implies the
   screen might be stale, which is exactly the wrong thing to suggest. */
[data-fsapp="1"] #btnRefresh,
/* Duplicate identity/exit controls — the auth pill already shows who is signed
   in and offers Logout, so a separate Account link is a second door to the
   same room. Navigation between the two areas goes through Home. */
[data-fsapp="1"] .topbar .right a[href="/account"],
[data-fsapp="1"] .topbar .right a[href="/index"],
[data-fsapp="1"] .topbar .right a[href="/admin"],
[data-fsapp="1"] #authAdminLink,
/* Site-wide announcement banner. It advertises the beta programme to website
   visitors — an official already holding the app is the wrong audience, and it
   overlaps the status bar here. */
[data-fsapp="1"] #fsSiteAnnouncement { display: none !important; }

/* The FieldSync logo links to "/" on the login and legal pages. In a browser
   that is a normal way home; in the app it is a one-way trip to the marketing
   site. app-shell.js bounces back if it happens, but not offering the trip is
   better than recovering from it. */
[data-fsapp="1"] .authLogo a,
[data-fsapp="1"] .topbar h1 a {
  pointer-events: none;
  cursor: default;
}

/* Onboarding tour — both the launcher pill and the card that opens itself over
   the UI on arrival. A modal that appears unbidden in front of an official who
   is mid-event is worse than unhelpful, and the tour explains website chrome
   ("Click Meets in the top bar") that this app has already replaced. */
[data-fsapp="1"] .tpPill,
[data-fsapp="1"] .tpBackdrop,
/* The page heading and blurb duplicate the app bar title. */
[data-fsapp="1"] .topbar h1,
[data-fsapp="1"] .topbar .sub {
  display: none !important;
}

/* What remains lines up on one row instead of spilling. */
[data-fsapp="1"] .topbar {
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}
[data-fsapp="1"] .topbar .right {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}
/* Meet switcher is the one control that must always be reachable — give it the
   room and let the identity pill sit at the end. */
[data-fsapp="1"] .meetSwitcher { flex: 1 1 auto; min-width: 0; }
[data-fsapp="1"] .meetSwitcherBtn {
  width: 100%;
  min-height: 52px;
  justify-content: space-between;
  font-size: 16px;
}
[data-fsapp="1"] .authPill { flex: 0 0 auto; }

/* ── Touch targets ─────────────────────────────────────────────────────────
   48px is the documented Android minimum; with gloves and a moving athlete to
   watch, it is not enough. 56px on ordinary controls, more where a mistake
   costs a result. */
[data-fsapp="1"] .btn,
[data-fsapp="1"] button.btn,
[data-fsapp="1"] .segBtn {
  min-height: 56px;
  padding: 12px 20px;
  font-size: 17px;
  font-weight: 600;
  border-radius: 12px;
}

[data-fsapp="1"] select,
[data-fsapp="1"] input[type="text"],
[data-fsapp="1"] input[type="search"],
[data-fsapp="1"] input[type="number"] {
  min-height: 56px;
  font-size: 17px;
  border-radius: 12px;
}

/* Field labels above the selects (Flight / Round / Height / Attempt). */
[data-fsapp="1"] .field label {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .01em;
}

/* ── Result tables ─────────────────────────────────────────────────────────
   The attempts / jumpers / easy-mode sheets are tapped constantly during a
   competition — each row is a target, so give rows real height and stop marks
   rendering small. */
[data-fsapp="1"] .tbl th {
  font-size: 14px;
  letter-spacing: .02em;
}
[data-fsapp="1"] .tbl td {
  padding-top: 14px;
  padding-bottom: 14px;
  font-size: 17px;
}
/* Press feedback ONLY on the sheets whose rows actually open something. The
   check-in roster is not row-tappable — its action is the Here/DNS button — so
   showing a pressed state there would promise something that never happens. */
[data-fsapp="1"] #attemptsBody tr,
[data-fsapp="1"] #verticalBody tr,
[data-fsapp="1"] #easySheetBody tr {
  cursor: pointer;
}
[data-fsapp="1"] #attemptsBody tr:active,
[data-fsapp="1"] #verticalBody tr:active,
[data-fsapp="1"] #easySheetBody tr:active {
  background: rgba(124, 58, 237, .18);
}

/* ── Check-in roster ───────────────────────────────────────────────────────
   The screen an official uses first, often while athletes are still arriving.
   Everything here serves one question per row: is this athlete here or not? */

/* Walk-up music is a presentation feature, not an officiating one. Hidden in
   the app (column and cells together, so the table stays aligned) rather than
   removed, since the website still offers it. */
[data-fsapp="1"] #checkinWalkupTh,
[data-fsapp="1"] .checkinWalkupCell { display: none !important; }

/* The affiliation column is what pushed Check In off the right edge on a phone:
   the table scrolled sideways, so an official could see a name OR its Here/DNS
   button, never both. Below tablet width the affiliation moves under the name —
   the same stacking Easy Mode already uses — and its own column goes away, so
   order, athlete and Check In all fit without scrolling. */
/* ── Officiating settings, inside the gear sheet ─────────────────────────────── */
[data-fsapp="1"] .mpSettings {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--bd);
}
[data-fsapp="1"] .mpSettingsTitle {
  font-weight: 900;
  font-size: 13px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 10px;
}
[data-fsapp="1"] .mpSetting {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 0;
  cursor: pointer;
}
[data-fsapp="1"] .mpSetting + .mpSetting { border-top: 1px solid var(--bd); }
[data-fsapp="1"] .mpSettingText { flex: 1 1 auto; min-width: 0; display: block; }
[data-fsapp="1"] .mpSettingName { display: block; font-weight: 800; font-size: 15px; }
[data-fsapp="1"] .mpSettingSub {
  display: block; color: var(--muted); font-size: 12.5px; line-height: 1.45; margin-top: 3px;
}
/* A real switch, not a 13px tick box: this is touched with a thumb, outdoors.
 *
 * !important throughout because run-theme.css forces background, border and
 * border-radius on every input on this page with !important of its own. Without
 * matching it the track stayed the input colour in both states, so on and off
 * looked identical apart from the knob. */
[data-fsapp="1"] input.mpSwitch {
  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto; width: 56px; height: 32px; margin: 0; padding: 0;
  border-radius: 999px !important;
  border: 1px solid var(--bd) !important;
  background: rgba(148, 163, 184, .25) !important;
  position: relative; transition: background .15s, border-color .15s;
}
[data-fsapp="1"] input.mpSwitch::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 24px; height: 24px; border-radius: 50%;
  background: #fff; transition: transform .15s;
}
[data-fsapp="1"] input.mpSwitch:checked {
  background: var(--accent) !important;
  border-color: var(--accent) !important;
}
[data-fsapp="1"] input.mpSwitch:checked::after { transform: translateX(24px); }

/* Jump-ahead warning. Above the run page's own overlays, since it interrupts one. */
[data-fsapp="1"] .mpJumpBackdrop { z-index: 9600; }
[data-fsapp="1"] .mpJumpCard { width: min(560px, 94vw); }
[data-fsapp="1"] .mpJumpLead { margin: 0; font-size: 15px; line-height: 1.5; }
[data-fsapp="1"] .mpJumpSub {
  margin: 10px 0 0; color: var(--muted); font-size: 13px; line-height: 1.5;
}
[data-fsapp="1"] .mpJumpActions {
  display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; flex-wrap: wrap;
}
[data-fsapp="1"] .mpJumpActions .btn { min-height: 48px; padding: 0 20px; }

/* Both inline fallbacks are off by default and switched on inside the width query
   below. They must be declared BEFORE it: a media query adds no specificity, so a
   later plain rule would override the one inside it. */
[data-fsapp="1"] .checkinAffInline,
[data-fsapp="1"] .scoreStatusInline { display: none; }

@media (max-width: 700px) {
  [data-fsapp="1"] #checkinAffTh,
  [data-fsapp="1"] .checkinAffCell { display: none !important; }
  [data-fsapp="1"] .checkinAffInline {
    display: block;
    margin-top: 2px;
    font-size: 0.82em;
    font-weight: 600;
    color: var(--fsapp-muted, #9aa3b2);
  }
  /* run-theme.css puts min-width:640px on every run-page table, which on a ~411px
     phone viewport guarantees the overflow this fix exists to remove. Relax it for
     this table only — scoring and Easy Mode have many attempt columns and are
     meant to scroll sideways. */
  [data-fsapp="1"] #checkinTable { min-width: 0 !important; }
  [data-fsapp="1"] #checkinTable th:first-child,
  [data-fsapp="1"] #checkinTable td:first-child { width: 44px; }

  /* The scoring grid had the same problem, and worse: Status, Mark and Edit were
     all off the right edge, so entering a mark — the whole job — needed a sideways
     scroll first. Status folds into the Mark cell as a small label and its column
     goes, leaving attempt, athlete and mark visible together. */
  [data-fsapp="1"] #attemptsTable { min-width: 0 !important; }
  [data-fsapp="1"] #attemptsTable th:first-child,
  [data-fsapp="1"] #attemptsTable td:first-child { width: 40px; }
  [data-fsapp="1"] #scoreStatusTh,
  [data-fsapp="1"] .scoreStatusCell { display: none !important; }
  /* A fifth column will not fit a phone, so Up / On Deck / On Hold falls back to
     sitting beside the name — still visible, just not its own column. */
  [data-fsapp="1"] #rotTh,
  [data-fsapp="1"] .rotCell { display: none !important; }
  [data-fsapp="1"] .rotInline { display: inline; }
  [data-fsapp="1"] .scoreStatusInline {
    display: block;
    font-size: 0.72em;
    font-weight: 800;
    letter-spacing: .04em;
    color: var(--fsapp-muted, #9aa3b2);
  }
}

/* The status control carries the whole meaning of the row, so make it read as
   a state at a glance — filled green for present, filled red for scratched —
   and big enough to hit without aiming. app.js gives it a word ("Here"/"DNS")
   in place of the desktop glyph. */
[data-fsapp="1"] .checkBtn {
  min-width: 108px;
  min-height: 52px;
  padding: 8px 18px;
  font-size: 16px;
  font-weight: 800;
  letter-spacing: .02em;
  border-width: 2px;
}
[data-fsapp="1"] .checkBtn.ok {
  color: #065f46;
  background: #34d399;
  border-color: #34d399;
}
[data-fsapp="1"] .checkBtn.dns {
  color: #7f1d1d;
  background: #fca5a5;
  border-color: #fca5a5;
}
[data-fsapp="1"] .checkBtn:active { transform: scale(.96); }

/* Athlete name is what an official scans down the list for. */
[data-fsapp="1"] #checkinBody td:nth-child(2) { font-size: 18px; }

/* Card titles carry the "where am I" signal on a small screen. */
[data-fsapp="1"] .cardTitle { font-size: 22px; }
[data-fsapp="1"] .tableTitle { font-size: 17px; }

/* ── Noise removal ─────────────────────────────────────────────────────────
   Things that belong on the website but are pure distraction (or dead weight)
   inside a purpose-built officiating tool. Navigation is never hidden — an
   official who cannot get back to a screen is worse off than one who sees an
   extra button. */

/* Beta feedback widget: an official mid-meet is not the audience for it. */
[data-fsapp="1"] .mgrFeedbackFab,
[data-fsapp="1"] .mgrFeedbackPanel { display: none !important; }

/* "Install this app" prompts — they are already in the app. Belt and braces:
   app-shell.js also stops pwa-register.js from ever building the banner. */
[data-fsapp="1"] #pwa-install-banner { display: none !important; }

/* ── Total Station status ──────────────────────────────────────────────────
   Connection state must be readable at a glance from a few feet away, since
   it is the one thing an official checks without stopping what they're doing. */
[data-fsapp="1"] #checkinTotalStationStatus {
  font-size: 17px;
  font-weight: 700;
}

/* ── Attempt entry ─────────────────────────────────────────────────────────
   The screen that decides a result, used once per throw with an athlete
   waiting. There are exactly four things an official does here — foul, pass,
   type a mark, or take a laser reading — so those four get the whole dialog
   and everything else gets out of the way.

   Close is gone — tapping FOUL or PASS commits, and dismissing commits
   whatever is typed (closeModalWithAutoSave in app.js), with the header × as
   the dismiss affordance. */
[data-fsapp="1"] #btnCloseAttempt { display: none !important; }

/* ── Save ──────────────────────────────────────────────────────────────────
   A typed mark gives no sign it has been recorded, and relying on "it saves
   when you close" asks an official to trust an invisible rule mid-competition.
   Save therefore sits directly beside the box and appears exactly when what's
   typed is a valid result and isn't stored yet (app.js sets .fsCanSave). Its
   arrival is the confirmation that the entry is good; its disappearance after
   pressing is the confirmation it was saved. */
[data-fsapp="1"] .fsMarkRow {
  display: flex;
  align-items: stretch;
  gap: 10px;
}
[data-fsapp="1"] .fsMarkRow #markInput { flex: 1 1 auto; min-width: 0; }
[data-fsapp="1"] #btnSaveAttempt {
  flex: 0 0 auto;
  min-width: 132px;
  min-height: 68px;
  font-size: 19px;
  font-weight: 800;
  color: #fff;
  background: linear-gradient(135deg, #059669, #34d399);
  border-color: transparent;
}
/* Hidden until the entry is valid and unsaved. */
[data-fsapp="1"] .modalBackdrop:not(.fsCanSave) #btnSaveAttempt {
  display: none !important;
}

/* Keyboard shortcut hint — there is no keyboard here. */
[data-fsapp="1"] #markField .hint:not(#markHint) { display: none !important; }

/* FOUL and PASS: the two most-used calls, side by side and unmissable. */
[data-fsapp="1"] #btnMarkFoul,
[data-fsapp="1"] #btnMarkPass {
  flex: 1 1 0;
  min-height: 76px;
  font-size: 28px;
  font-weight: 900;
  border-radius: 14px;
}
[data-fsapp="1"] #btnMarkPass {
  color: var(--text);
  border-color: var(--bd);
}

/* The mark itself — big enough to read back at a glance before committing. */
[data-fsapp="1"] #markInput {
  min-height: 68px;
  font-size: 26px;
  font-weight: 700;
  text-align: center;
  letter-spacing: .02em;
}
[data-fsapp="1"] #markHint {
  font-size: 15px;
  text-align: center;
  padding-top: 6px;
}
[data-fsapp="1"] #markField label { font-size: 15px; }

/* Laser Measurement — moved above the mark box by app-shell.js, because the
   reading is what fills the box in. Cyan sets it apart from every other
   control here: it is the one button that talks to hardware, not a call the
   official is making themselves. app.js only shows it while the station is
   actually connected. */
[data-fsapp="1"] #btnLaserMeasure {
  display: block;
  width: 100%;
  margin: 0 0 10px;
  min-height: 68px;
  font-size: 19px;
  font-weight: 800;
  color: #0e7490;
  background: #67e8f9;
  border: 2px solid #22d3ee;
}
[data-fsapp="1"] #btnLaserMeasure:active { background: #22d3ee; }
/* .hidden must still win when there's no connection. */
[data-fsapp="1"] #btnLaserMeasure.hidden { display: none !important; }

/* ── Modal scroll lock ─────────────────────────────────────────────────────
   Set by app-shell.js while any dialog is open. Both html and body are locked
   because either can be the scrolling element depending on the page. */
[data-fsapp="1"].fsModalOpen,
[data-fsapp="1"].fsModalOpen body {
  overflow: hidden;
}

/* Dragging the dimmed area does nothing at all — it is not a scroller, and
   without this the gesture falls through to the page behind. */
[data-fsapp="1"] .modalBackdrop:not(.hidden) {
  touch-action: none;
}
/* The dialog's own body still scrolls (it is overflow:auto in styles.css, and
   the keypad makes this dialog tall), but its scroll must not chain out to the
   page when it reaches an end. */
[data-fsapp="1"] .modalBackdrop:not(.hidden) .modalBd {
  touch-action: pan-y;
  overscroll-behavior: contain;
}

/* ── Mark keypad ───────────────────────────────────────────────────────────
   Replaces the system keyboard (see app-shell.js). Fixed position in the
   dialog, so the keys are in the same place on every attempt and an official
   can enter a mark without looking down at their hands. */
[data-fsapp="1"] .fsKeypad {
  margin-top: 12px;
}
[data-fsapp="1"] .fsKeypadGrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}
[data-fsapp="1"] .fsKey {
  min-height: 64px;
  border: 1px solid var(--bd);
  border-radius: 12px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 26px;
  font-weight: 700;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
[data-fsapp="1"] .fsKey:active {
  background: color-mix(in srgb, var(--accent) 22%, var(--card));
  border-color: var(--accent);
}
/* Backspace and Clear are not marks — keep them visually subordinate to the
   digits so the pad still reads as a number pad at a glance. */
[data-fsapp="1"] .fsKeyAux {
  font-size: 20px;
  color: var(--muted);
}

/* Quarter-inch row, English units only. Sits directly above the digits
   because it completes the same number. */
[data-fsapp="1"] .fsKeypadFrac {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-bottom: 8px;
}
[data-fsapp="1"] .fsKeypadFrac[hidden] { display: none; }
[data-fsapp="1"] .fsKeyFrac {
  min-height: 54px;
  font-size: 22px;
  font-weight: 800;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--card));
  border-color: color-mix(in srgb, var(--accent) 40%, var(--bd));
}

/* The box is filled by the pad, never typed into directly — don't let it look
   like a dead/disabled field just because it is read-only. */
[data-fsapp="1"] #markInput:read-only {
  opacity: 1;
  background: var(--card);
  color: var(--text);
}

/* Action row. Once a result is stored, moving to the next athlete is the only
   thing an official wants, so Next Athlete takes the full width — but only
   then (app.js sets .fsSaved). Clear is an occasional correction and doesn't
   need the room; Retire is a once-a-season action that still has to exist, so
   it stays small and quiet beside it. */
[data-fsapp="1"] .rightRow { flex-wrap: wrap; gap: 10px; align-items: center; }
[data-fsapp="1"] #btnNextAttempt {
  order: 1;
  flex: 1 1 100%;
  min-height: 68px;
  font-size: 19px;
  font-weight: 800;
  color: #fff;
  background: linear-gradient(135deg, #7c3aed, #a78bfa);
  border-color: transparent;
}
/* Nothing to move on from until something is stored. Note that in the app the
   open-pit rule does NOT apply: syncAttemptModalNavButtons() clears .hidden for
   FS_NATIVE_APP precisely so open-pit events keep a forward control, since the
   row's Close button is hidden here. Whether a result exists is the only thing
   deciding this. */
[data-fsapp="1"] .modalBackdrop:not(.fsSaved) #btnNextAttempt {
  display: none !important;
}
[data-fsapp="1"] #btnClearAttempt {
  order: 2;
  flex: 0 0 auto;
  min-width: 120px;
  min-height: 46px;
  font-size: 15px;
}
[data-fsapp="1"] #btnRetireAttemptAthlete {
  order: 3;
  flex: 0 0 auto;
  min-height: 38px;
  padding: 0 14px;
  font-size: 13px;
  font-weight: 600;
  opacity: .7;
}

/* ── Modals ────────────────────────────────────────────────────────────────
   Give dialogs room on a tablet and keep their action buttons thumb-sized.
   Only sizing — the backdrop/nesting structure noted in CLAUDE.md is untouched. */
[data-fsapp="1"] .modalCard {
  max-height: 92vh;
  max-height: 92dvh;
}
[data-fsapp="1"] .modalCard .btn { min-height: 56px; }

/* ── Measurement preference (Android officials app only) ─────────────────────
   Every selector stays under [data-fsapp="1"] so none of this reaches the
   website or the desktop app, which keep the original modal flow. */

/* The gear in the run page top bar. It pulses until a choice is made, because
   an official who was never told where Total Station setup lives is exactly the
   person this has to reach. */
[data-fsapp="1"] .mpGear {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  border: 1px solid var(--bd);
  border-radius: 999px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  font-size: 15px;
  font-weight: 800;
  cursor: pointer;
}
[data-fsapp="1"] .mpGearIcon { font-size: 17px; line-height: 1; }
[data-fsapp="1"] .mpGear.mpGearAttention {
  border-color: transparent;
  background: linear-gradient(135deg, #7c3aed, #a78bfa);
  color: #fff;
  animation: mpPulse 2s ease-in-out infinite;
}
@keyframes mpPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(124, 58, 237, .55); }
  50%      { box-shadow: 0 0 0 12px rgba(124, 58, 237, 0); }
}
@media (prefers-reduced-motion: reduce) {
  [data-fsapp="1"] .mpGear.mpGearAttention { animation: none; }
}

/* Chooser: three big targets, readable outdoors, hittable with gloves. */
[data-fsapp="1"] .mpOptions { display: flex; flex-direction: column; gap: 14px; }
[data-fsapp="1"] .mpOption {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  width: 100%;
  padding: 20px;
  border: 2px solid var(--bd);
  border-radius: 16px;
  background: var(--card);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
[data-fsapp="1"] .mpOption:active { transform: scale(.99); }
[data-fsapp="1"] .mpOptionActive { border-color: var(--accent); background: rgba(124, 58, 237, .10); }
[data-fsapp="1"] .mpOptionIcon { font-size: 30px; line-height: 1; }
[data-fsapp="1"] .mpOptionTitle { font-size: 19px; font-weight: 900; }
[data-fsapp="1"] .mpOptionSub {
  margin-top: 6px;
  font-size: 14px;
  font-weight: 600;
  line-height: 1.45;
  color: var(--muted);
}
[data-fsapp="1"] .mpOptionSoon { opacity: .55; cursor: default; }
[data-fsapp="1"] .mpSoon {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 9px;
  border-radius: 999px;
  background: rgba(148, 163, 184, .22);
  font-size: 12px;
  font-weight: 800;
  vertical-align: middle;
}

/* The dedicated setup page inherits the card look of the other run views.
 *
 * measure-pref.js MOVES the Total Station dialog's .modal into this page, so it
 * arrives still carrying dialog layout: a flex column with max-height and
 * overflow:hidden, wrapping a .modalBd of flex:1 1 auto with overflow:auto. A flex
 * item that is itself a scroll container has an automatic minimum size of zero, so
 * once the container's height resolves to anything unexpected the body can collapse
 * to nothing — which is exactly what happened on Android WebView, where the page
 * rendered its header and an empty strip. Desktop Chrome at the same width sized it
 * fine, which is why this only showed up on a phone.
 *
 * So strip the dialog behaviour rather than tune it: as a page section it is a
 * plain block whose height follows its content, and the page scrolls, as every
 * other run view already does. */
[data-fsapp="1"] #runTsConfigHost .modal {
  max-width: none;
  width: 100%;
  border: 0;
  box-shadow: none;
  background: transparent;
  padding: 0;
  display: block;
  height: auto;
  max-height: none;
  overflow: visible;
}
[data-fsapp="1"] #runTsConfigHost .modalBd {
  flex: none;
  height: auto;
  max-height: none;
  overflow: visible;
  padding: 0;
}

/* ── Run page top bar: gear left, profile right, nothing else ────────────────
   Meets, the meet switcher and Home all navigate somewhere. Stacked across the
   top of a tablet they crowd out the two controls that matter and sit exactly
   where a thumb lands, inviting a mis-tap out of the event mid-competition.
   Their jobs move: the meet NAME goes with the events list, and returning to
   the meet list becomes a deliberate button at the bottom of that screen. */
[data-fsapp="1"] body[data-page="run"] .topbar {
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
/* The title block is the logo, "Competition Run" and two lines of description.
   In a browser tab that orients you; in an app you already know what you opened,
   and it is the width that pushes everything else around. */
[data-fsapp="1"] body[data-page="run"] .topbar > div:first-of-type { display: none; }
[data-fsapp="1"] body[data-page="run"] .topbar .right { margin-left: auto; }

/* The gear, top-left. Sized and coloured to be found by someone who was never
   told it exists — that is the entire problem it is solving. */
[data-fsapp="1"] .mpGear {
  order: -1;
  flex: 0 0 auto;
  min-height: 56px;
  padding: 12px 20px;
  font-size: 16px;
  border-width: 2px;
  border-color: rgba(124, 58, 237, .45);
  background: rgba(124, 58, 237, .10);
  color: var(--accent);
}
[data-fsapp="1"] .mpGearIcon { font-size: 20px; }

/* Flash on arrival: a few pulses naming the current mode, then silence. A
   permanent animation is one an official stops seeing by the second event. */
[data-fsapp="1"] .mpGear.mpGearFlash {
  animation: mpFlash .8s ease-in-out 5;
}
@keyframes mpFlash {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(124, 58, 237, .55); }
  50%      { transform: scale(1.04); box-shadow: 0 0 0 14px rgba(124, 58, 237, 0); }
}
@media (prefers-reduced-motion: reduce) {
  [data-fsapp="1"] .mpGear.mpGearFlash { animation: none; }
}

/* Meet name, next to the events it belongs to. */
[data-fsapp="1"] .mpMeetName {
  font-size: 15px;
  font-weight: 800;
  color: var(--accent);
  margin-bottom: 2px;
}

/* On the events list the bottom bar carries a single action, so let it own the
   full width rather than leaving a gap where "Next" would be. */
[data-fsapp="1"] .bottomNav.mpLandingNav .bottomNavInner > .btn { flex: 1 1 auto; }

/* The app top bar wraps by design (see above) so a crowded bar degrades onto a
   second row instead of overflowing. On the run page there are now exactly two
   things in it, so wrapping only ever pushes the account pill onto its own line
   and doubles the height of the bar for no reason. */
[data-fsapp="1"] body[data-page="run"] .topbar { flex-wrap: nowrap; }

/* The old Total Station entry point on Check In. Setup now has its own page, so
   this small button is exactly the thing officials were missing and mis-reading.
   Hidden in CSS with !important rather than by script: tsSyncCheckinUI() sets
   this element's inline display on every re-render, so an inline hide is undone
   the moment the roster refreshes. */
[data-fsapp="1"] #checkinTotalStationField { display: none !important; }

/* Opt-out row under the measurement options. Deliberately quiet: choosing how
   you measure is the action, opting out of being asked is the escape hatch. */
[data-fsapp="1"] .mpFoot {
  display: flex;
  justify-content: center;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--bd);
}
[data-fsapp="1"] .mpNoShow {
  min-height: 48px;
  padding: 10px 20px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  text-decoration: underline;
  cursor: pointer;
}
[data-fsapp="1"] .mpNoShow:active { color: var(--text); }

/* "Walk-up songs won't play — connect Spotify" banner on Check In.
   Walk-up music is a presentation feature and is already stripped from the app,
   so this is advice about something an official here cannot use — noise across
   the top of the screen they spend the most time on. !important because
   showSpotifyWarning() writes display straight onto the element's style
   attribute, which a plain rule cannot override. */
[data-fsapp="1"] #spotifyWarningBanner { display: none !important; }

/* Live interpretation sits in the mark row, between the box and Save, so what
   is about to be stored and the button that stores it are read in one glance. */
[data-fsapp="1"] .fsMarkRow .markReading {
  margin-top: 0;
  /* Sized to its content, not stretched: with flex:1 1 auto and min-width:0 the
     input took the space and squeezed this to a column barely wide enough for
     "40.61", wrapping the unit onto a second line. */
  flex: 0 0 auto;
  min-width: 132px;
  flex-direction: column;
  gap: 2px;
  align-items: flex-start;
  justify-content: center;
}
/* A mark is a single value — never break it across lines. */
[data-fsapp="1"] .fsMarkRow .markReadingAlt { font-size: 19px; white-space: nowrap; }
[data-fsapp="1"] .unitBadge { font-size: 13px; padding: 3px 11px; }

/* Attempt dialog header on the tablet: the athlete is the thing being
   identified, so give it the size; the attempt coordinates stay secondary. */
[data-fsapp="1"] .attemptWho { font-size: 17px; }
[data-fsapp="1"] .attemptWhich { font-size: 14px; }

/* Laser Measurement when the instrument cannot be used.
   Shown rather than hidden: an official who selected Total Station and sees no
   laser option cannot tell whether the feature is missing, the event is wrong,
   or the cable is out. Greyed out with the reason on the button answers that in
   one glance — and answers it before an athlete is waiting in the circle. */
[data-fsapp="1"] #btnLaserMeasure.fsLaserUnavailable {
  color: var(--muted);
  background: transparent;
  border-color: var(--bd);
  border-style: dashed;
  font-weight: 700;
  /* Pressable, not inert: it routes to Total Station setup. */
  cursor: pointer;
}
[data-fsapp="1"] #btnLaserMeasure.fsLaserUnavailable:active { background: rgba(148, 163, 184, .12); }
/* The reason plus the hint is long; let it shrink rather than clip. */
[data-fsapp="1"] #btnLaserMeasure.fsLaserUnavailable { font-size: 16px; }

/* Running order editor on the tablet: drag handles and rows sized for a thumb. */
[data-fsapp="1"] .roRow { grid-template-columns: 56px 60px 1fr 56px; padding: 14px 12px; }
[data-fsapp="1"] .roHandle { font-size: 22px; padding: 12px 0; }
[data-fsapp="1"] .roPos { font-size: 18px; }
[data-fsapp="1"] .roName { font-size: 17px; }
[data-fsapp="1"] .roRemove { min-height: 44px; min-width: 44px; font-size: 22px; }
[data-fsapp="1"] .roResult { padding: 14px 16px; }
[data-fsapp="1"] .roResultName { font-size: 17px; }
[data-fsapp="1"] .roActions .btn { flex: 1 1 0; min-height: 56px; font-size: 17px; }
[data-fsapp="1"] .roList { max-height: 40vh; }
